Skip to content

feat(editor): drum companion strip — VSTi-style kit view + sampler pad view, cursor input, GM MIDI monitor - #199

Merged
byrongamatos merged 6 commits into
mainfrom
feat/editor-drum-pad-strip
Jul 11, 2026
Merged

feat(editor): drum companion strip — VSTi-style kit view + sampler pad view, cursor input, GM MIDI monitor#199
byrongamatos merged 6 commits into
mainfrom
feat/editor-drum-pad-strip

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

The drum editor's counterpart to the fretboard companion strip (#198), per the product owner's notes: a docked row of kit pads — one per drum piece, in physical-kit family groups — doing three jobs:

  • Visual cue: selected hits light their pads. The lane grid shows rows; the pads show the kit.
  • Input: click a pad → add a hit of that piece at the snapped cursor, through the same AddDrumHitCmd the lane grid uses (undo-able, sorted, dirty-flagged).
  • MIDI mapping tool: arm Listen and e-kit note-ons flash their pads live, mapped through General MIDI percussion to start (the import default). GM notes with no chart piece (claps, tambourine) flash nothing rather than the wrong pad, and every pad's tooltip documents its GM note numbers — the strip doubles as the mapping reference. Per-kit custom maps are a natural follow-up.

No second device path

src/midi-record.js gains a small tap API (_midiMonitorTap/_midiMonitorUntap + _midiMonitorEnsure): every raw packet routes through the existing _recMidiOnData before the recording gate, riding the same refcounted domain session the Record modal manages. The tap loop is typeof-guarded so the sliced-source midi_domain suite's env stays clean. On the legacy private Web-MIDI backend the monitor sees data whenever the Record modal has a device connected — best-effort by design, documented in the module header.

Wiring notes

  • Mode flips cross the host seam (host.refreshDrumPadStrip) — drum.js flips drumEditMode but the strip imports drum.js's command surface, so a direct import would close a cycle.
  • Pads are DOM buttons (accessibility + CSS flash for free); a sidecar with zero per-frame cost. The Pads toggle persists as an editor pref, never in the pack.

Tests

tests/drum_pad_strip.test.mjs (5, real-import): GM canonical assignments land only on real chart pieces, the pad model covers the kit exactly once in family groups, note-on parsing rejects offs/CC/short packets (channel-agnostic — GM drums arrive on channel 10), selection lighting, and the add-command round-trip through the real EditHistory. Full suite 91/91 · ESLint 0 errors · CHANGELOG updated.

Screenshot (selection lighting + click flash) shared with Christian during review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features
    • Added a docked drum-editor “companion strip” with persistent Kit ⇄ Pads views, selected-hit pad-zone lighting, and click-to-add drum hits at the cursor.
    • Added MIDI Listen to flash pads from incoming General MIDI percussion note-ons, with pad tooltips showing GM note numbers.
  • Bug Fixes
    • Refreshes the drum pad strip when switching the “Edit Drums” mode.
    • Improved MIDI Listen lifecycle to prevent leaked sessions and stale flash behavior.
  • Style
    • Added v3 theme styling for the strip layout, pad states (lit/flash), and kit SVG rendering.
  • Tests
    • Added automated drum pad strip tests, including rendering/lighting, MIDI parsing, undo/redo, and teardown safety.
  • Documentation
    • Updated the unreleased changelog entry for the strip and Listen behavior.

… MIDI monitor

The drum editor's counterpart to the fretboard companion strip (product
owner's ask, 2026-07-10): a docked row of kit pads, one per drum piece in
physical-kit family groups, doing three jobs at once:

- VISUAL CUE: selected hits light their pads — the lane grid shows rows,
  the pads show the KIT.
- INPUT: click a pad -> add a hit of that piece at the snapped cursor,
  through the same AddDrumHitCmd the lane grid uses (undo-able, sorted,
  dirty-flagged — pinned by a real-EditHistory round-trip test).
- MIDI MAPPER: arm Listen and e-kit note-ons flash their pads live,
  mapped through GENERAL MIDI percussion to start (the import default).
  GM notes with no chart piece (claps, tambourine) flash nothing rather
  than the wrong pad; every pad's tooltip documents its GM notes, so the
  strip doubles as the mapping reference. Per-kit custom maps follow.

The monitor NEVER grows a second device path: src/midi-record.js gains a
small tap API (_midiMonitorTap/Untap + _midiMonitorEnsure) that routes
every raw packet through the existing _recMidiOnData BEFORE the recording
gate, riding the same refcounted domain session the Record modal manages.
The tap loop is typeof-guarded so the sliced-source midi_domain suite's
env stays clean (the standing recipe). On the legacy private Web-MIDI
backend the tap sees data whenever the Record modal has a device
connected — best-effort by design.

Mode wiring crosses the host seam (host.refreshDrumPadStrip): drum.js
flips drumEditMode but the strip already imports drum.js's command
surface, so a direct import would close a cycle.

Pads are DOM buttons (a11y + CSS flash for free) — a sidecar with zero
per-frame cost. Shown in drum edit mode; the Pads toggle persists as an
editor pref (never the pack).

tests/drum_pad_strip.test.mjs (5): GM canonical assignments land only on
real chart pieces, the pad model covers the kit exactly once in family
groups, note-on parsing rejects offs/CC/short packets (channel-agnostic —
GM drums arrive on channel 10), selection lighting, and the add-command
round-trip. Suite 91/91, ESLint 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a docked drum-pad strip with persisted Kit/Pads views, GM percussion mapping, clickable hit entry, selection lighting, MIDI listen flashing, editor lifecycle wiring, styling, and automated tests.

Changes

Drum-pad companion strip

Layer / File(s) Summary
Shared MIDI monitor taps
src/midi-record.js
Monitor taps receive raw MIDI packets through the shared device session before recording-state filtering.
Pad mapping and interactions
src/drum-pad-strip.js
GM mappings, persisted views, SVG and grid rendering, selection lighting, snapped undoable hit insertion, MIDI flashing, initialization, and teardown are implemented.
Editor wiring, presentation, and validation
screen.html, assets/v3-theme.css, src/host.js, src/main.js, src/drum.js, tests/drum_pad_strip.test.mjs, CHANGELOG.md
UI controls, styles, refresh hooks, lifecycle integration, mode updates, documentation, and focused tests are added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DrumEditor
  participant DrumPadStrip
  participant AddDrumHitCmd
  participant MidiMonitor
  DrumEditor->>DrumPadStrip: Click pad or arm Listen
  DrumPadStrip->>AddDrumHitCmd: Add snapped drum hit
  DrumPadStrip->>MidiMonitor: Ensure monitor tap
  MidiMonitor-->>DrumPadStrip: Deliver raw NOTE-ON packet
  DrumPadStrip-->>DrumEditor: Flash mapped pad
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new drum companion strip with kit and pad views, cursor input, and a GM MIDI monitor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-drum-pad-strip

Comment @coderabbitai help to get the list of available commands.

…er note)

Rework the flat pad row into a KIT VIEW in the same companion slot as the
fretboard strip: three rows arranged the way the kit faces the player —
cymbal arc on top, hi-hats left of the tom arc in the middle, feet +
snare + a wide kick at the bottom. Same three roles (selection cue,
click-to-add at the cursor, GM MIDI Listen flash); KIT_VIEW_ROWS is pure
layout data, and a new test pins that it covers DRUM_PIECE_ORDER exactly
once. Pads grew to performance-size targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

Reworked per Christian's note: the flat pad row is now a performance-style kit view in the same companion slot as the fretboard strip (#198) — cymbal arc / hats + tom arc / feet + snare + wide kick. Same three roles (selection cue, click-to-add, GM Listen flash); KIT_VIEW_ROWS is pure layout data with a coverage test pinning it to DRUM_PIECE_ORDER. Suite 92 checks green, lint clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/drum_pad_strip.test.mjs (1)

81-97: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the redo result, not only its length.

After redo, the test verifies hits.length === 2 but not the reinserted piece, time, or velocity. A regression that redoes the wrong hit would pass.

Proposed assertion
     S.history.doRedo();
     assert.strictEqual(S.drumTab.hits.length, 2);
+    assert.deepStrictEqual(S.drumTab.hits[0], hit);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/drum_pad_strip.test.mjs` around lines 81 - 97, Strengthen the redo
verification in the “pad-click add: AddDrumHitCmd round-trip” test by asserting
that the reinserted hit matches the original hit object, including its time,
pitch, and velocity, rather than checking only hits.length. Use deep equality
against hit at the expected sorted position after S.history.doRedo().
src/drum-pad-strip.js (1)

208-224: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard the _midiMonitorEnsure() await.

_midiMonitorEnsure isn't fully wrapped upstream (e.g. _recMidiDomain().listSources() can throw), and armListen() is invoked fire-and-forget from the click handler (Line 233). A rejection here becomes an unhandled promise rejection, and the tap stays registered while aria-pressed/monitorArmed never settle. Wrapping keeps state consistent on failure.

♻️ Proposed guard
     _midiMonitorTap(onMidiData);
-    const ok = await _midiMonitorEnsure();
+    let ok = false;
+    try { ok = await _midiMonitorEnsure(); }
+    catch (e) { console.warn('[Editor] MIDI monitor ensure failed:', e); }
     monitorArmed = true;   // the tap also works whenever the record modal has a session
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/drum-pad-strip.js` around lines 208 - 224, Guard the await of
_midiMonitorEnsure() inside armListen() with try/catch, handling failures by
untapping onMidiData, resetting monitorArmed to false, restoring the button’s
aria-pressed state, and reporting an appropriate status so the fire-and-forget
click handler cannot produce an unhandled rejection or leave partial state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main.js`:
- Line 1813: Update the drum-pad teardown associated with initDrumPadStrip() to
remove the registered onMidiData callback from the shared _midiTaps set when the
editor is destroyed or replaced; ensure the cleanup is safe when listening is
not armed and prevents stale MIDI taps from firing after reinjection.

In `@src/midi-record.js`:
- Around line 251-270: The _midiMonitorEnsure function must not initialize or
persist the record MIDI session from the drum-pad Listen path. Remove its
fallback device selection and _recMidiEnsureOpen call; instead, only re-arm the
listener on an already-established _recMidiHandle, returning whether that handle
is available, while leaving session creation and teardown to the record-session
lifecycle.

---

Nitpick comments:
In `@src/drum-pad-strip.js`:
- Around line 208-224: Guard the await of _midiMonitorEnsure() inside
armListen() with try/catch, handling failures by untapping onMidiData, resetting
monitorArmed to false, restoring the button’s aria-pressed state, and reporting
an appropriate status so the fire-and-forget click handler cannot produce an
unhandled rejection or leave partial state.

In `@tests/drum_pad_strip.test.mjs`:
- Around line 81-97: Strengthen the redo verification in the “pad-click add:
AddDrumHitCmd round-trip” test by asserting that the reinserted hit matches the
original hit object, including its time, pitch, and velocity, rather than
checking only hits.length. Use deep equality against hit at the expected sorted
position after S.history.doRedo().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f076380-350e-4a9d-93b8-c5573499d884

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec753b and f1caac1.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • assets/v3-theme.css
  • screen.html
  • src/drum-pad-strip.js
  • src/drum.js
  • src/host.js
  • src/main.js
  • src/midi-record.js
  • tests/drum_pad_strip.test.mjs

Comment thread src/main.js
Comment thread src/midi-record.js
…ler pad view

Second rework per the product owner: 'kit view' means a drawn drum kit
like a drum VSTi's UI, plus a SECOND, separate sample-pad view.

- KIT: an inline-SVG kit graphic — kick, snare with a rim zone, rack +
  floor toms, the open/closed hat pair with its pedal, the cymbal wash on
  stands, the ride's bell as its own zone, cowbell on the kick mount (the
  VSTi hit-zone idiom). Every piece is its own SVG element (data-piece +
  <title> tooltip), so hit-testing, lit/flash CSS state and tooltips come
  free and nothing repaints per frame. KIT_GRAPHIC is pure shape data; a
  test pins it to DRUM_PIECE_ORDER (once each) and that zones (bell, hat
  pair) reference real instruments.
- PADS: the sampler-style grid (three banks of six), same wiring.
- One switcher (Kit | Pads buttons, editor-pref 'editorDrumPadView';
  corrupt prefs collapse to kit). Both views light from the selection,
  click-to-add at the cursor, and flash from the GM MIDI monitor —
  everything routes through [data-piece], one code path.
- Fix: the strip's click delegate guarded on HTMLElement, which silently
  dropped SVG clicks (SVG elements are SVGElement) — now Element.

Suite 94 checks green (8 in this module), ESLint 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

Second rework per Christian: kit view now means a drawn kit — an inline-SVG drum-kit graphic with VSTi-style hit zones (snare head/rim, ride bow/bell, open/closed hat pair + pedal, cowbell) — plus a second, separate sampler-style pad view (three banks of six). One switcher (Kit ⇄ Pads, persisted editor pref); both views share the [data-piece] wiring for selection lighting, click-to-add, and the GM Listen flash. Also fixes an own-goal: the click delegate's HTMLElement guard silently dropped SVG clicks. Screenshots of both views shared with Christian.

@ChrisBeWithYou ChrisBeWithYou changed the title feat(editor): drum-pad companion strip — kit legend, cursor input, GM MIDI monitor feat(editor): drum companion strip — VSTi-style kit view + sampler pad view, cursor input, GM MIDI monitor Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/drum-pad-strip.js (1)

190-218: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make SVG kit zones keyboard-operable.

The Kit view’s interactive SVG shapes are not focusable, and the delegate only handles click. Keyboard-only users cannot add hits from the default Kit view.

Proposed fix
 function buildKitSvg(meta) {
     const el = [];
@@
     for (const s of KIT_GRAPHIC) {
         const cls = `editor-kit-piece is-${s.kind}`;
         const title = `<title>${gmTitle(meta, s.piece)}</title>`;
+        const attrs = `class="${cls}" data-piece="${s.piece}" tabindex="0" role="button" aria-label="${gmTitle(meta, s.piece)}"`;
         if (s.kind === 'drum' || s.kind === 'rim' || s.kind === 'kick') {
-            el.push(`<circle class="${cls}" data-piece="${s.piece}" cx="${s.cx}" cy="${s.cy}" r="${s.r}">${title}</circle>`);
+            el.push(`<circle ${attrs} cx="${s.cx}" cy="${s.cy}" r="${s.r}">${title}</circle>`);
         } else if (s.kind === 'cym' || s.kind === 'pedal') {
-            el.push(`<ellipse class="${cls}" data-piece="${s.piece}" cx="${s.cx}" cy="${s.cy}" rx="${s.rx}" ry="${s.ry}">${title}</ellipse>`);
+            el.push(`<ellipse ${attrs} cx="${s.cx}" cy="${s.cy}" rx="${s.rx}" ry="${s.ry}">${title}</ellipse>`);
         } else if (s.kind === 'bell') {
-            el.push(`<rect class="${cls}" data-piece="${s.piece}" x="${s.cx - s.w / 2}" y="${s.cy - s.h / 2}" width="${s.w}" height="${s.h}" rx="2">${title}</rect>`);
+            el.push(`<rect ${attrs} x="${s.cx - s.w / 2}" y="${s.cy - s.h / 2}" width="${s.w}" height="${s.h}" rx="2">${title}</rect>`);
         }
@@
     strip.addEventListener('click', (e) => {
@@
     });
+    strip.addEventListener('keydown', (e) => {
+        if (e.key !== 'Enter' && e.key !== ' ') return;
+        const target = e.target instanceof Element ? e.target : null;
+        const pad = target?.closest('.editor-kit-piece[data-piece]');
+        if (!pad) return;
+        e.preventDefault();
+        addHitAtCursor(pad.dataset.piece);
+    });
+#plugin-editor .editor-kit-piece:focus-visible {
+    stroke: `#f8fafc`;
+    stroke-width: 3;
+    outline: none;
+}

Also applies to: 345-354

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/drum-pad-strip.js` around lines 190 - 218, Make each interactive SVG
shape generated by buildKitSvg keyboard-focusable by adding tabindex="0" and an
appropriate role, while leaving decorative hardware and labels non-interactive.
Update the Kit view’s delegated event handling to respond to Enter and Space
keydown events by triggering the same hit-selection logic as click, and prevent
default scrolling for Space. Ensure the equivalent handler near the alternate
referenced section is updated as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/drum-pad-strip.js`:
- Around line 190-218: Make each interactive SVG shape generated by buildKitSvg
keyboard-focusable by adding tabindex="0" and an appropriate role, while leaving
decorative hardware and labels non-interactive. Update the Kit view’s delegated
event handling to respond to Enter and Space keydown events by triggering the
same hit-selection logic as click, and prevent default scrolling for Space.
Ensure the equivalent handler near the alternate referenced section is updated
as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b9f597c-d845-47ea-afa0-99ebd175f43f

📥 Commits

Reviewing files that changed from the base of the PR and between 5cbc692 and 79c4966.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • assets/v3-theme.css
  • screen.html
  • src/drum-pad-strip.js
  • tests/drum_pad_strip.test.mjs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • screen.html

ChrisBeWithYou and others added 3 commits July 10, 2026 18:32
- _midiMonitorEnsure never auto-picks a device: it opens the SAVED record
  device only, so arming Listen can't silently overwrite the user's
  record-device preference — picking a device stays the Record MIDI
  modal's job (Listen says so when nothing is saved yet).
- New _midiMonitorRelease(): Listen-off drops the monitor's shared
  session ref (refcounted at the domain; a no-op while a take is
  recording, so it can never yank the device from the record path).
- initDrumPadStrip self-heals across re-boots: untap + disarm first, so
  a previous injection's armed Listen can't keep a stale tap firing
  behind a fresh DOM that shows Listen off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Signed-off-by: ChrisBeWithYou <chris@rifflarr.local>
The drum companion strip's MIDI monitor tap + domain device session were
never released on screen teardown. Leaving the editor (or a re-injection)
with Listen armed leaked the domain session and left onMidiData in the tap
set. initDrumPadStrip's self-heal untapped the handler but never released
the device, so the session ref leaked until an unrelated Record open/close.

Add teardownDrumPadStrip(): untaps onMidiData, releases our device-session
ref ONLY when we armed it (never yanks a session the Record modal opened),
and clears pending flash timers. Wired into __editorScreenTeardown alongside
the other injection-scoped cleanup. Idempotent and safe when never armed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos merged commit 5b31cbe into main Jul 11, 2026
byrongamatos pushed a commit that referenced this pull request Jul 12, 2026
The squash-merge resolutions that landed the sweep bar (#201) and the
drum-pad strip (#199) each dropped their block's closing </div> at the
canvas-wrap overlay anchor — the seam every chrome PR collides on. Since
then #editor-canvas has been nested inside the HIDDEN drum-pad strip
(itself inside the hidden sweep bar), so the canvas laid out at 0x0:
chart and waveform invisible after every load and import, while the
status bar reported the load and all 108 JS tests stayed green (nothing
parses the markup). First tester-visible in the 20260711 nightly.

Restores the two closers, byte-identical markup otherwise, and adds
tests/screen_markup.test.mjs: a dependency-free tag-stack walk that
fails if screen.html ever unbalances again, if #editor-canvas stops
being a direct child of #editor-canvas-wrap, or if any canvas-wrap
overlay becomes an ancestor instead of a sibling. Fails 3/3 on main.

Verified end-to-end on the :8000 testbed via headless Chromium: before
the fix the canvas had clientHeight 0 and the timeline was black; after,
ruler + minimap + waveform + 1328 notes render.


Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants