The flagship library-to-lineup Channel Builder — one screen replacing the Channel → Schedule → Items → Playout editor chain, recreated from design-system/design_handoff_channel_builder/ at high fidelity.
What's in it
web/src/builder/ChannelBuilder.tsx (~1,300 lines, own module — App.tsx was already 2,890 lines): 3-column layout (minmax(0,1.2fr) minmax(0,1fr) 304px, independent scroll, 980px collapse). Library browser (Library/Collections segmented, debounced search, per-library filter chips, poster grid/compact density, Load more paging; Collections mode = 5 typed browse calls merged client-side). Native HTML5 drag-and-drop lineup (drag from library or double-click to add, drag-to-reorder, dedupe). Settings rail: name, AUTO channel number (computed client-side from GET /api/channels, validated + uniqueness-checked), channel image dropzone with guide-logo/bug previews (upload deferred to Create; separate bug image visibly deferred), Shuffle → advanced.playbackOrder, Always-playing → advanced.playoutMode (live-styled row), Channel Template picker with save-as-template, Advanced override/view-defaults accordion over the real request surface.
API modules: libraryBrowse, channelTemplates (default 404→null), artwork (multipart /api/artwork/uploads), pickers (client-sorted lists), createChannelFromLineup; schedules.ts now sources filler/watermark fetchers from pickers.ts (barrel dedupe).
Contract semantics surfaced at add time: RerunCollection/Playlist rows error the moment the lineup goes multi-item; MultiCollection + non-shuffle order errors until the Shuffle toggle clears it; server 422s naming lineup[i] map back to row highlights (cleared on lineup mutation). Advanced pickers offer "Inherit from template" instead of a fake "None" — the handler coalesces null with the template value, so clear-to-none is not expressible (filed #135).
group defaults to "ErsatzTV" (required by the handler; editable under Advanced → Behavior). Successful create navigates to /app/channels; artwork uploads are cached across create retries.
Review
Three parallel lenses (fable correctness fork + contract/tests + design-fidelity) → 1 blocker (Dialog focus-steal) + 4 substantial (Inherit semantics, toggle label double-fire, Advanced scroll-into-view, .ctv-lift/.ctv-press motion port) + fixture-fidelity minors, all fixed in 54a0760f; fork verification over the fix diff: SHIP.
Verification
npm run typecheck / npm run test (112 passed, baseline 80 → +32) / npm run build all green. All 3 themes covered by tests.
- Dialog: focus only on open transition (was stealing focus from its own
inputs every parent re-render); Escape listener via latest-ref
- Advanced overrides: honest 'Inherit from template' semantics - the
handler coalesces null with the template value so 'None' could never
clear anything (clear-to-none follow-up: #135); save-as-template now
composes from the same effective-value helper
- toggle rows: label->div (label activation forwarding double-fired in
real browsers); clear stale 422 row highlight on lineup mutations
- port .ctv-lift/.ctv-press motion utilities + apply per prototype;
Advanced expand scroll-into-view; Info glyph on advanced info line
- fixture fidelity: real 'Validation failed' title, verbatim handler
detail message, omit null keys, collectionKind on rerun fixture
- cache successful artwork upload across create retries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
closes #89
The flagship library-to-lineup Channel Builder — one screen replacing the Channel → Schedule → Items → Playout editor chain, recreated from
design-system/design_handoff_channel_builder/at high fidelity.What's in it
web/src/builder/ChannelBuilder.tsx(~1,300 lines, own module — App.tsx was already 2,890 lines): 3-column layout (minmax(0,1.2fr) minmax(0,1fr) 304px, independent scroll, 980px collapse). Library browser (Library/Collections segmented, debounced search, per-library filter chips, poster grid/compact density, Load more paging; Collections mode = 5 typed browse calls merged client-side). Native HTML5 drag-and-drop lineup (drag from library or double-click to add, drag-to-reorder, dedupe). Settings rail: name, AUTO channel number (computed client-side from GET /api/channels, validated + uniqueness-checked), channel image dropzone with guide-logo/bug previews (upload deferred to Create; separate bug image visibly deferred), Shuffle →advanced.playbackOrder, Always-playing →advanced.playoutMode(live-styled row), Channel Template picker with save-as-template, Advanced override/view-defaults accordion over the real request surface.Dialog/ConfirmDialogprimitive (components/overlay.tsx) — portal, Esc/backdrop close, scroll lock, aria wiring, reduced motion.libraryBrowse,channelTemplates(default 404→null),artwork(multipart/api/artwork/uploads),pickers(client-sorted lists),createChannelFromLineup;schedules.tsnow sources filler/watermark fetchers frompickers.ts(barrel dedupe).lineup[i]map back to row highlights (cleared on lineup mutation). Advanced pickers offer "Inherit from template" instead of a fake "None" — the handler coalesces null with the template value, so clear-to-none is not expressible (filed #135).groupdefaults to"ErsatzTV"(required by the handler; editable under Advanced → Behavior). Successful create navigates to /app/channels; artwork uploads are cached across create retries.Review
Three parallel lenses (fable correctness fork + contract/tests + design-fidelity) → 1 blocker (Dialog focus-steal) + 4 substantial (Inherit semantics, toggle label double-fire, Advanced scroll-into-view,
.ctv-lift/.ctv-pressmotion port) + fixture-fidelity minors, all fixed in 54a0760f; fork verification over the fix diff: SHIP.Verification
npm run typecheck/npm run test(112 passed, baseline 80 → +32) /npm run buildall green. All 3 themes covered by tests.🤖 Generated with Claude Code