Repository navigation
feat(ui-refactor): DAW redesign, library overhaul, sections, analysis, and transport - #67
Merged
Merged
Conversation
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.
…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.
…opdown, and MP3 export
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DAW UI Redesign
Waveform Loading
Transport Footer
{title}_exported_mix.{ext}Export Mix
{title}_exported_mix.wav/.mp3<a download>)Library Sidebar
#tagin search box for partial match autocomplete dropdown (up to 8 suggestions, keyboard navigable)Sections
Analysis & Metadata
#tagsearchClosed issues
Test plan
{title}_exported_mix.wav; Export Mix MP3 → downloads{title}_exported_mix.mp3#rockin library search → autocomplete shows matching tags; click to filter tracks