Skip to content

feat(ui-refactor): DAW redesign, library overhaul, sections, analysis, and transport - #67

Merged
thcp merged 39 commits into
mainfrom
ui-refactor
May 18, 2026
Merged

thcp merged 39 commits into
mainfrom
ui-refactor

Conversation

@thcp

@thcp thcp commented May 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

DAW UI Redesign

  • Flat dark DAW aesthetic — mixer, topbar, waveform, sidebar
  • Mixer rows aligned with waveform lanes; original track at top when present
  • Non-extracted stems grayed out in mixer and waveform
  • Stem presence cards panel with dynamic range and tempo stability metrics
  • Track info panel moved to footer (art + title + time/stems/fav + Extracted/Source/Quality rows)
  • Topbar height increased 10% with scaled buttons and text
  • Loop region shown as full-height waveform overlay
  • Mixer and waveform rows fill all available vertical space dynamically (no black gap)

Waveform Loading

  • Loading overlay (pulsing glow + animated bars) shown during full pipeline — from job submission through WaveSurfer decode
  • 900ms minimum display time prevents sub-frame flash
  • Stall indicator after 20s if decode hangs
  • Footer waveform placeholder (sum-of-sines pattern) shown when no track is loaded
  • Footer waveform now uses mix_url as source (full song energy) instead of stems[0] which could be a silent vocal stem

Transport Footer

  • Track info card in footer left: album art, title, time/stems/fav button, Extracted/Source/Quality
  • Waveform scrub bar spanning full footer width
  • Time elapsed / total duration display
  • Export Mix button: downloads WAV or MP3, named {title}_exported_mix.{ext}
  • Export Mix grayed out when no track is loaded
  • Speed dropdown (0.5×–2×) in footer chips

Export Mix

  • Pipeline always produces mix.wav including when all 6 stems are selected (previously no file was produced)
  • Export uses mix_url (selected-stems mix) with fallback to original stem
  • Filenames: alphanumeric + underscore only, {title}_exported_mix.wav/.mp3
  • In Tauri/WKWebView: uses system browser via open_url command (WKWebView doesn't support <a download>)

Library Sidebar

  • Redesigned with Recent, Stem Collections, Tags, and Favorites sections
  • Subfolders: drag a folder onto the middle zone of another to reparent; circular nesting blocked
  • Folder drag-to-reorder via grip handle (3-zone drop target)
  • Favorites heart, Extracted/Source/Quality on track card; heart moved next to title in footer
  • Sidebar never collapses from library button or search input
  • Trash purge persists denylist to localStorage; purged tracks never reappear on reload
  • Tag search: type #tag in search box for partial match autocomplete dropdown (up to 8 suggestions, keyboard navigable)

Sections

  • Interactive sections bar with drag, resize, rename
  • Sections persist across backend restarts (saved to metadata.json)
  • Saving indicator — spinner while PATCH in-flight, Saved ✓ on success
  • Add button moved into Sections label row

Analysis & Metadata

  • YouTube tags extracted from yt-dlp and shown as chips in library sidebar; filter by #tag search
  • Dynamic range (DR) and tempo stability metrics added
  • Key confidence meter

Closed issues

Test plan

  • Import a YouTube URL → stems load, original track appears at top, mixer rows align with waveform lanes
  • Import with a subset of stems selected → only selected stems visible, others grayed
  • Press Extract → loading overlay appears immediately and stays until waveform is decoded
  • No track loaded → footer shows placeholder waveform, Export Mix button is grayed out
  • Export Mix WAV → downloads {title}_exported_mix.wav; Export Mix MP3 → downloads {title}_exported_mix.mp3
  • Import with all 6 stems → Export Mix still works (mix.wav always produced now)
  • Footer waveform accurately reflects song energy (not a silent stem)
  • Type #rock in library search → autocomplete shows matching tags; click to filter tracks
  • Time display in footer updates during playback
  • Sections bar: add, rename, drag, resize, persist across reload
  • YouTube tags appear as chips in sidebar Tags section; click to filter
  • Drag a folder onto another → subfolder created; circular nesting blocked
  • Empty trash, reload → purged tracks do not reappear
  • Audio plays correctly in Safari / WKWebView

thcp and others added 30 commits May 16, 2026 18:58
Replace the glassmorphism panel layout with a flat, dark DAW-style UI
matching the Stemdeck-static.html reference design.

- Rewrite index.html: new topbar with composer pill (URL zone + stem
  chips + Process button), sidebar rail, track header with energy bars
  and Key/BPM/LUFS analysis, section ribbon, waveform ruler, horizontal
  mixer lanes, and transport footer
- Add daw.css: complete new design system using .daw prefix; flat solid
  surfaces, stem color-mix() chips, horizontal mixer lane rows styled
  for the JS-built .lane-header.mx-row elements
- Update variables.css: new palette tokens (--bg, --bg-2, --panel,
  --border, stem colours, --accent) with legacy compat aliases for
  waves.css

All 65 JS-required IDs and hooks (.app, .url-wrap, .stem-choice,
.stem-list, .mixer-column, .energy-row, etc.) are preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove duplicate SVG logo; keep text-only wordmark
- Add "All" toggle before stem chips in composer pill
- Expand composer pill max-width 920→1080px
- Replace topbar "new release available" text with notification panel
  under bell button (dropdown card, outside-click close, version relay
  via MutationObserver from brandVersion element)
- Track header grid: info card 320→380px, energy panel fixed 200px
- Hide .daw-version from topbar (content surfaced in notif panel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove daw.css overrides for .wave-scroll/.wave-canvas/.waves-grid/.loop-region
  that shadowed waves.css's dark background, golden scrollbar, and loop marker
  visual styles; only keep layout wrapper (.daw-wave-panel flex column)
- Add .daw .wave-scroll flex:1 override so wave fills full panel height
  (zoom toolbar now lives in .daw-wave-header outside .wave-editor)
- Add wireAllButton() in main.js: toggles all stems on/off via selectedStems,
  syncs aria-pressed state when individual chips are clicked
- Remove data-stem="all" from All button to avoid main.js stem handler picking it up

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
waves.css applies padding:12px 14px 14px and border-radius:12px to any
.wave-editor element. Our .daw-wave-panel.wave-editor was inheriting
those, creating a visible gap/frame around the waveform canvas instead
of a flush edge-to-edge dark panel.

Strip padding, min-height, and border-radius via !important overrides
so the wave-scroll fills the panel completely with its dark background
and golden scrollbar as intended by waves.css.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…x offsets

waves.css hides #multitrack-container (opacity:0) and shows the custom
stem-waveform-layer (SVG min/max peaks) in the active state. Two issues
prevented this from working in our new layout:

1. Height chain broken: waves-column used height:calc(100%-34px) but
   wave-canvas had no explicit height, so the percentage never resolved.
   Fix: .daw .wave-canvas { height: 100% }.

2. 48px inner stem strip: the original layout had a 48px icon strip
   inside the wave panel, so waves.css offsets multitrack-container and
   stem-waveform-layer by left:48px. Our mixer is a separate 300px panel,
   so those offsets must be zeroed out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
waves.css hides #multitrack-container (opacity:0, position:absolute) in
favour of the custom SVG stem-waveform-layer. User wants the WaveSurfer
bar style instead (vertical bars with gaps on dark background).

- Force #multitrack-container opacity:1 and position:relative so it is
  visible and contributes to waves-column height
- Hide .stem-waveform-layer entirely
- Set waves-column height:auto so it sizes from WaveSurfer's injected height
- Remove now-redundant inset:0 override (only needed for absolute positioning)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e zoom toolbar

- Transport footer: add display:flex !important to override waves.css grid
- Wave/mixer gap: zero border on .daw .wave-scroll and the 48px inset/margin-left
  offsets (waves-grid, lanes-ruler, multitrack-container) that assumed an icon strip
- Double playhead: set WaveSurfer global cursorWidth:0 — CSS .playhead-marker is the
  only active playhead now
- Loop region position: loopOverlayParent() returns rulerTime first so the loop
  overlay lives in the same coordinate space as the time→percent calculation
- Zoom toolbar: removed +/−/slider controls from wave header; zoom is mouse-scroll
  only; Fit button in footer retained; dropped dead zoomInBtn/zoomOutBtn/zoomTrack
  refs that were causing a ReferenceError aborting main.js init (breaking the split
  stems button and all subsequent wiring)
- Mixer row height: 64px → 66px (64 wave + 2px separator) to match WaveSurfer lane
  pitch and eliminate cumulative row drift

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parent #loop-region to .waves-column instead of rulerTime so the
yellow overlay spans all stem lanes like a real DAW. The % position
calculation remains correct at any zoom level since waves-column width
represents the full timeline extent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop zoom controls entirely — Fit button, Ctrl+wheel zoom, zoom state,
and all zoom helper functions. Keep the ResizeObserver (recalculates
--wave-playhead-h on resize) and the plain vertical wheel pan handler.
applyWaveZoom() now only sets --wave-playhead-h and syncs WaveSurfer
pxPerSec to fit-to-viewport (zoom=1 always).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mixer.js: revert to audioEl.volume for WKWebView (MediaElementSource →
  GainNode does not reliably pass audio in Safari); remove GainNode path
- player.js: remove attachAnalysers() import and call — VU meters use
  pre-computed envelope data, not live Web Audio analysis; calling
  createMediaElementSource on canplay was disconnecting Safari's native
  audio output path
- index.html: add rail-library class to Library button so catalog.js
  selectors (.rail-library) actually find it
- main.js: Library button click in trash view switches back to library
  instead of collapsing the sidebar
- catalog.js: normalize YouTube URLs to yt:<videoId> in normalizeSource
  to deduplicate youtu.be/xxx vs youtube.com/watch?v=xxx and variants
  with &t= / ?si= query params; re-importing a trashed track removes it
  from trash and places new import in library
- daw.css: add Empty trash button (clear-bin-bar/btn), hide lib-header
  in trash view, show clear-bin-bar only when sidebar has trash-view class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ash purge

- Subfolders: folders now support parentId; drag a folder onto the
  middle zone of another to nest it, or use the subfolder button on
  hover; delete cascades to children
- Folder drag-to-reorder: grip handle on each folder header; three-zone
  drop target (top/bottom = reorder, middle = reparent as subfolder);
  circular-nesting guard prevents invalid trees
- Sidebar never collapses from within: library button and search input
  no longer trigger collapse; catalogToggle is expand-only
- Unsorted count shows only truly unsorted tracks (not total library)
- Trash purge fix: markJobsDeleted() persists a denylist in localStorage
  before clearing trash; syncWithServer() skips trashed and hard-deleted
  IDs so purged tracks never reappear on reload; DELETE /api/jobs/{id}
  fired for each purged track to remove server files and registry entry
- New folder button redesigned as a labeled chip (icon + text)
- Sidebar width increased to 390px

Closes #38, #39, #40, #41, #42, #43, #44

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the energy bar panel with per-stem presence cards. Backend computes
mean RMS per extracted stem after collect(), normalizes 0-100, stores in
metadata.json and surfaces in the API. Frontend renders one card per stem
in STEM_NAMES order: extracted stems show a colored fill bar + percentage,
non-extracted stems render grayed with "—".
Row 1: track card + KEY / BPM / LUFS / DURATION / SCALE metadata cards.
Row 2: 6 stem presence cards (VOCAL PRESENCE, DRUM INTENSITY, BASS DEPTH,
GUITAR PRESENCE, PIANO PRESENCE, OTHER) with large colored percentage values.
Matches the reference design — replaces the old 3-column grid with a full-width
card grid closer to the screenshot.
Dynamic range = peak_db - integrated LUFS (dB), classified as Compressed /
Moderate / High / Wide. Tempo stability = 0-100% from beat interval coefficient
of variation; shown green when >= 80%. Both stored in metadata.json, surfaced
in the API, and displayed as metadata cards in the track info panel row.
Heart button toggles favorite per track (stored in catalog localStorage).
Extracted date derived from created_at (now surfaced in API). Source and
Quality derived from source_url (YouTube vs local file + format).
…sitions

Multitrack.create() now receives all TRACK_NAMES (7 entries) with url:null
for non-extracted stems. Previously only extracted stems were passed, so
WaveSurfer placed Drums at row 1 (Vocals position), Piano at row 2 (Drums
position), etc. Fixed indices mean each stem always occupies the same row
regardless of which subset was extracted.
…and Favorites

- Rail: add Favorites button (heart), keep Library + Trash, remove Projects/Storage
- Library view: three sections — Recent (last 3 tracks by date), Stem Collections
  (folders), Tags (chips from track.tags; click chip to filter)
- Favorites view: filtered list of heart-marked tracks, accessible via rail button
- Tags: #tag search prefix filters tracks by tag; clicking active chip clears filter
- Unsorted folder now renders as a standard collapsible folder in Stem Collections
- daw-lib-header simplified to New folder button only (label removed)
- favorites-view hides lib-header and clear-bin-bar via CSS class on sidebar
…nd persist

- Backend: Job.sections field, PATCH /api/jobs/{id}/sections endpoint with
  pydantic validation (id, name, start/end range, color hex check), loads from
  metadata.json on registry recovery
- sections.js: new module — section blocks positioned by %-time over the waveform,
  drag-to-move with no-overlap clamping, left/right resize handles, double-click
  rename inline input, delete button on hover, + button to add section (auto-opens
  rename), debounced PATCH save, 8-color cycling palette
- UI: sections bar is the existing daw-section-ribbon row (label changed to
  Sections); each block has colored border + colored text, 36px height
- Wired into player.destroyPlayer (destroySections), catalog.loadTrackIntoStudio,
  and job.applyState (initSections on done)
…o wave header

- Add section button moved from floating inside timeline to the Sections label
  column (HTML static button, wired by initSections); shows as 'Add' with + icon
- Wave header left column now shows 'Mixer' title + 'Drag fader · M/S' sublabel
  (previously empty box)
Two bugs caused sections to vanish on track reload:

1. destroySections() was cancelling the debounced save timer, dropping all
   unsaved changes when the user switched to another track before 600ms elapsed.
   Fix: flush the save immediately before clearing state (JSON.stringify runs
   synchronously before the first await, so the body is captured correctly).

2. loadTrackIntoStudio skipped the API fetch when a track had cached audio/analysis
   data in localStorage, so server-written sections were never loaded. Fix: always
   fetch fresh state from /api/jobs/{id} on every track load.
…indicator

- Call registry_persist() after writing sections to metadata.json so
  registry.json stays in sync; sections were lost on backend restart
  because jobs already in registry.json skip the metadata.json recovery path
- Check res.ok in _save() before showing "Saved ✓" to surface HTTP errors
- Silence spurious audio errors for null-URL placeholder waveform tracks
- Update help dialog: correct GitHub org URL and add stemdeck.app link
Extract tags and categories from yt-dlp info after download, lowercase
and deduplicate, cap at 8 entries. Stored on Job.tags, written to
metadata.json, served via to_state(). The frontend tag chips and #tag
search filter were already wired to track.tags.
STEM_NAMES always occupy WaveSurfer rows 0-5 so mixer lanes stay aligned.
"original" is appended at row 6 only when it has a URL — omitting it when
absent eliminates the phantom 70px gap that shifted all mixer rows down.

Also reorders mixer DOM and overview waveform CSS order to match, and fixes
renderAllMiniWaves to use trackIndex for stem→wavesurfer mapping.
When original.wav exists the user expects it at the top (A/B comparison).
Prepend it to orderedNames only when present; STEM_NAMES follow. Omitting
it when absent still prevents the phantom 70px gap from the previous bug.
The mixer fill loop was capped at STEM_NAMES.length (6), so when original
occupied one slot only 5 stems were shown, leaving a phantom WaveSurfer
row at the bottom with no corresponding mixer row. Cap now scales to 7
when original is present, matching orderedNames in wireUpAudio.
thcp added 3 commits May 18, 2026 11:18
…00ms

If canplay fires before a browser repaint (cache hit or instant null-URL
resolution), the pulsing glow animation flashed and disappeared in under
one frame. Now the overlay stays visible for a minimum of 900ms so the
user always sees the loading state when opening a track.
Previously the pulsing glow only appeared during WaveSurfer decode.
Now it shows immediately when Extract Stems is clicked and stays on
through the full pipeline (download → analyze → separate → mix) until
canplay fires after WaveSurfer finishes loading the stems. Hidden on
error and cancel.
@thcp thcp changed the title feat(library): subfolders, folder drag-reorder, sidebar fixes, and trash purge feat(ui-refactor): DAW redesign, library overhaul, sections, analysis, and transport May 18, 2026
thcp added 6 commits May 18, 2026 14:33
…eform

- Relocate full track card (art, title, time/stems, fav, detail rows) from
  top transport header into the footer left section; all element IDs preserved
  so no JS logic changes needed
- Title extracted as a full-width row above the art+detail body so it
  left-aligns with the thumbnail edge
- Footer waveform renders a dim organic placeholder (sum-of-sines) when no
  track is loaded; replaced by real waveform on track load
- Fix waveform bar background/border so it blends into the footer seamlessly
- Reduce bar count 300→150, pixel-snap positions, tighten gap and amplitude
  for smoother bar rendering
- Footer height set to 200px to accommodate full track card
- Move favorite heart button next to song title (remove margin-left: auto)
- Topbar height +10% (70→77px); scale up composer pill, URL input, stem
  chips, and Split Stems button to fill the extra space
- Footer waveform: 300 bars edge-to-edge (last bar reaches canvas.width),
  matching bar computation in placeholder wave
- Footer track info panel title left-aligns with thumbnail via full-width
  title row above the art+details body row
Compute lane height from available panel height at load time so stems
fill the full area between header and footer with no black gap. Deferred
with rAF in renderEmptyShell so clientHeight is measured after layout.
Three bugs prevented exports from working:
- _triggerDownload used <a download> which WKWebView silently ignores;
  now uses open_url Tauri command so the system browser handles the save
- downloadCurrentMix looked for "original" stem which is absent when all
  6 stems are selected; _exportMixUrl now prefers mix_url (the actual
  selected-stems mix) with "original" as fallback
- mix_url from the job API was never forwarded through stateMetadataToTrack
  or wireUpAudio; now mapped and passed from both job.js and catalog.js
@thcp
thcp marked this pull request as ready for review May 18, 2026 19:22
@thcp
thcp merged commit 04c0b4d into main May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant