Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1e62c48
docs: document container-first tooling environment
ScottMorris Apr 5, 2026
f292231
docs: define menu system spec and implementation plan
ScottMorris Apr 5, 2026
47de988
docs: align with holistic menu system overhaul strategy
ScottMorris Apr 5, 2026
26d962c
docs: sync sprint context and dispatch agent handoffs for menu overhaul
ScottMorris Apr 5, 2026
5efe8d6
feat: expand menu schema with authored document and migration path
ScottMorris Apr 5, 2026
726ef11
fix(menu): resolve migration resolution bias and schema drift
ScottMorris Apr 5, 2026
52bf508
docs: record menu-system risk review findings and milestone status
ScottMorris Apr 5, 2026
51859c9
feat(menus): prepare frontend state for scene-backed menu document
ScottMorris Apr 5, 2026
54bf874
feat: decouple compiler from legacy menu model using AuthorableMenuRef
ScottMorris Apr 5, 2026
0a5a813
feat(menus): simplify scene state management and make navigation back…
ScottMorris Apr 5, 2026
95b8ea2
docs: add Milestone 4 - Automated Generation & Presets to menu overhaul
ScottMorris Apr 5, 2026
cace399
test(menus): verify menu document initialization and sync-back
ScottMorris Apr 5, 2026
7d5bf87
fix(menu): close compiler air gap and add integration tests
ScottMorris Apr 5, 2026
a3f6531
feat(menus): extract scene editor into multi-pane component architecture
ScottMorris Apr 5, 2026
db28a56
test(menus): add scene editor component tests
ScottMorris Apr 5, 2026
669ae45
feat(menus): implement Bind and Compile mode surfaces for scene editor
ScottMorris Apr 5, 2026
63335e6
docs: finalize Milestone 2 delivery and update team rotation
ScottMorris Apr 5, 2026
7ab6324
feat: expand PlaybackAction and add comprehensive menu validation
ScottMorris Apr 5, 2026
de4b074
fix: resolve build warnings and implement missing Default traits
ScottMorris Apr 5, 2026
1d8b8bc
fix(menus): add exhaustive return paths to actionToString
ScottMorris Apr 5, 2026
a998822
fix(menus): persist button selection and rename Honest Preview to DVD…
ScottMorris Apr 5, 2026
f81d978
feat(menus): add background colour picker and safe area labels
ScottMorris Apr 5, 2026
b85a6f6
feat(menus): add text/image/shape tools and merge Remote into Design
ScottMorris Apr 5, 2026
f3357b1
feat(menus): add Delete key support and undo/redo history
ScottMorris Apr 5, 2026
59e6430
refactor(menus): remove panel collapse buttons from Layers and Inspector
ScottMorris Apr 6, 2026
93ea3c2
docs: finalize Milestone 2 QA and transition to Milestone 4
ScottMorris Apr 6, 2026
eec05e3
feat(menus): render text, image, and shape nodes on the canvas
ScottMorris Apr 6, 2026
8bb69c5
feat(menus): add inspector editors for text, image, and shape nodes
ScottMorris Apr 6, 2026
83badd9
feat: expand SceneNode with width, height and style fields
ScottMorris Apr 6, 2026
837ddda
test(menus): verify full-stack serialization of complex scene nodes
ScottMorris Apr 6, 2026
c2354be
feat(menu): add compiler support for Text and Shape scene nodes
ScottMorris Apr 6, 2026
e3be592
docs: finalize foundation lockdown and move to Milestone 4 automation
ScottMorris Apr 6, 2026
5f1d98c
docs: harmonise menu-system spec and roadmap with holistic implementa…
ScottMorris Apr 6, 2026
6e93d21
fix: resolve unused variable warning in menu renderer
ScottMorris Apr 6, 2026
e3522c6
fix: improve Bind page layout and title appearance
ScottMorris Apr 6, 2026
b8a6f63
docs: finalize core menu overhaul foundation and prepare for PR
ScottMorris Apr 6, 2026
7a60318
fix: resolve CI failures for clippy, Rust fmt, and Prettier
ScottMorris Apr 6, 2026
2a212ce
fix: address Codex review — undo stack reset and background sync
ScottMorris Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .codex
Empty file.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Current DVD authoring capabilities include:

- titleset-aware project editing with drag-and-drop title organisation
- chapter seeding from source media plus chapter-targeted menu and end actions
- menu editing with auto-generated directional navigation
- authored menu routing for VMGM, titleset, and title-return paths, including keyboard-safe entry selection
- menu editing with authored scene documents, layers, and motion timing support
- semantic interaction graphs and remote-navigation preview simulation
- asset inspection with embedded metadata title surfacing, compatibility explanations, and fix-oriented validation
- DVD build planning and execution with diagnostics export and toolchain checks
- bitmap subtitle muxing plus first-pass text subtitle rendering for DVD authoring
Expand Down
38 changes: 19 additions & 19 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ The long-term vision for Spindle is broader than DVD alone: it should become a h

- Provide a simulated remote-navigation preview.
- Support multiple menus and multiple titlesets.
- Support **Motion Menus** with intro/loop timing.
- Support **Setup Menus** with audio and subtitle selection actions.
- Support **Automated Generation** with auto-pagination for large collections.
- Make build output reproducible from project data.
- Support future expansion into Blu-ray authoring concepts without coupling v1 to BD-specific requirements.

Expand Down Expand Up @@ -176,13 +179,13 @@ Someone who understands FFmpeg and DVD authoring basics and wants a better front

### 8.6 Menus

- Support still-image menu backgrounds in v1.
- Support text and image button overlays.
- Support highlight/select visual states.
- Support explicit navigation mapping (`up`, `down`, `left`, `right`, `activate`).
- Support root menu and per-title scene/chapter menus in later phases.
- Support future autogenerated menu creation for common structures such as title menus, chapter menus, audio menus, and subtitle menus.
- Keep menu-theme concepts in mind early so menu generation and visual styling are not tightly coupled.
- **Scene-Driven Authoring**: Support for a full scene graph with layers, groups, and non-interactive nodes (Text, Image, Shape, Video).
- **Integrated Motion Model**: Menus support an authored timing model (intro, loop, timeout) and background audio from day one.
- **Semantic Interaction**: Explicit focus routing and navigation mapping with remote simulation.
- **Advanced Action Model**: Support for stream selection (audio/subtitle) and action sequencing.
- **Target-Aware Compilation**: Honest compilation into DVD-safe assets with visible downgrade reporting and 'Honest Preview' diagnostics.
- **Menu Sets and Auto-Pagination**: Automated generation of linked menu pages for large collections (e.g., chapter grids) with a hard 36-button hardware limit.
- **Components and Themes**: Reusable design tokens and layout presets to ensure visual consistency across the project.

### 8.7 Navigation and playback structure

Expand Down Expand Up @@ -619,13 +622,12 @@ Examples include:

### 14.1 v1 scope

- Still-image background
- Optional looping audio if feasible later
- Button hotspots
- Button labels and thumbnails
- Highlight/select states
- Safe-area guides
- 4:3 and 16:9 aware layout tools
- Scene-driven authoring with layers and non-interactive nodes.
- Integrated motion model with timing, animation tracks, and background audio.
- Interactive hotspots with semantic playback actions.
- Theme-driven components (HeroTitleButton, ChapterThumbnailTile).
- Target-specific compile variant preview (DVD 4:3/16:9 safe-areas).
- Explicit directional navigation with automatic generation heuristics.

### 14.2 Canvas features

Expand Down Expand Up @@ -1175,8 +1177,8 @@ Because this app orchestrates binaries, the product should clearly communicate:
- DVD-compatible audio target selection constrained by detected toolchain capabilities
- chapter editing
- chapter seeding from source media
- still-image menu editor
- basic navigation mapping
- Scene-driven menu system with authored documents and motion support
- semantic navigation mapping with remote simulation
- chapter-targeted menu and title end actions
- direct titleset editing with compatibility guidance
- reversible subtitle track selection
Expand All @@ -1195,9 +1197,6 @@ Because this app orchestrates binaries, the product should clearly communicate:

### 25.3 Deferred

- motion menus
- autogenerated title, chapter, audio, and subtitle menu creation
- menu themes and theme-aware generation
- advanced VM command logic exposure
- deep program/cell editing
- Blu-ray authoring
Expand Down Expand Up @@ -1771,3 +1770,4 @@ The design system is implemented as CSS custom properties in `design-system.css`
- **Rust**: Unit tests in `models.rs` cover JSON round-trips, serialisation format, domain values, and field initialisation.
- **Frontend**: Vitest with happy-dom and testing-library. Tests cover type helpers, constants, and project creation defaults.
- **No Rust toolchain locally**: Rust tests run via Docker (`ghcr.io/liminal-hq/tauri-dev-desktop:latest`).
.
179 changes: 179 additions & 0 deletions apps/spindle/src/components/menus/BindMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Bind mode — connect authored scene nodes to project metadata.
//
// (c) Copyright 2026 Liminal HQ, Scott Morris
// SPDX-License-Identifier: MIT

import type { MenuButton, PlaybackAction, Title, Menu } from '../../types/project';

export interface BindModeProps {
buttons: MenuButton[];
allTitles: Title[];
allMenus: Menu[];
currentMenuId: string;
defaultFocusId: string | null;
onUpdateButton: (buttonId: string, updates: Partial<MenuButton>) => void;
onSetDefaultFocus: (buttonId: string) => void;
}

export function BindMode({
buttons,
allTitles,
allMenus,
currentMenuId,
defaultFocusId,
onUpdateButton,
onSetDefaultFocus,
}: BindModeProps) {
return (
<div className="bind-mode">
<div className="bind-mode__header">
<h4 className="bind-mode__title">Action Bindings</h4>
<p className="bind-mode__hint text-muted">
Connect each button to a playback action. Set which button receives initial focus.
</p>
</div>

{buttons.length === 0 ? (
<div className="bind-mode__empty text-muted">
No buttons to bind. Switch to Design mode and add buttons first.
</div>
) : (
<div className="bind-mode__table">
<div className="bind-mode__row bind-mode__row--header">
<span className="bind-mode__col bind-mode__col--name">Button</span>
<span className="bind-mode__col bind-mode__col--action">Action</span>
<span className="bind-mode__col bind-mode__col--default">Default</span>
</div>
{buttons.map((btn) => (
<div key={btn.id} className="bind-mode__row">
<span className="bind-mode__col bind-mode__col--name">{btn.label}</span>
<span className="bind-mode__col bind-mode__col--action">
<select
className="bind-mode__select"
value={actionToString(btn.action)}
onChange={(e) =>
onUpdateButton(btn.id, {
action: stringToAction(e.target.value),
})
}
>
<option value="">No action</option>
<optgroup label="Play Title">
{allTitles.map((t) => (
<option key={t.id} value={`playTitle:${t.id}`}>
{t.name}
</option>
))}
</optgroup>
{allTitles.some((t) => t.chapters.length > 0) && (
<optgroup label="Play Chapter">
{allTitles
.filter((t) => t.chapters.length > 0)
.flatMap((t) =>
t.chapters.map((ch) => (
<option key={`${t.id}:${ch.id}`} value={`playChapter:${t.id}:${ch.id}`}>
{t.name} — {ch.name}
</option>
)),
)}
</optgroup>
)}
<optgroup label="Show Menu">
{allMenus
.filter((m) => m.id !== currentMenuId)
.map((m) => (
<option key={m.id} value={`showMenu:${m.id}`}>
{m.name}
</option>
))}
</optgroup>
<option value="stop">Stop</option>
</select>
</span>
<span className="bind-mode__col bind-mode__col--default">
<input
type="radio"
name="default-focus"
checked={defaultFocusId === btn.id}
onChange={() => onSetDefaultFocus(btn.id)}
/>
</span>
</div>
))}
</div>
)}

{/* Navigation summary */}
{buttons.length > 0 && (
<div className="bind-mode__nav-section">
<h4 className="bind-mode__title">Navigation</h4>
<p className="bind-mode__hint text-muted">
Directional navigation for DVD remote control.
</p>
<div className="bind-mode__table">
<div className="bind-mode__row bind-mode__row--header">
<span className="bind-mode__col bind-mode__col--name">Button</span>
<span className="bind-mode__col bind-mode__col--nav">Up</span>
<span className="bind-mode__col bind-mode__col--nav">Down</span>
<span className="bind-mode__col bind-mode__col--nav">Left</span>
<span className="bind-mode__col bind-mode__col--nav">Right</span>
</div>
{buttons.map((btn) => (
<div key={btn.id} className="bind-mode__row">
<span className="bind-mode__col bind-mode__col--name">{btn.label}</span>
{(['navUp', 'navDown', 'navLeft', 'navRight'] as const).map((dir) => (
<span key={dir} className="bind-mode__col bind-mode__col--nav">
<select
className="bind-mode__select bind-mode__select--nav"
value={btn[dir] ?? ''}
onChange={(e) => onUpdateButton(btn.id, { [dir]: e.target.value || null })}
>
<option value="">—</option>
{buttons
.filter((b) => b.id !== btn.id)
.map((b) => (
<option key={b.id} value={b.id}>
{b.label}
</option>
))}
</select>
</span>
))}
</div>
))}
</div>
</div>
)}
</div>
);
}

// ── Helpers ────────────────────────────────────────────────────────────────

function actionToString(action: PlaybackAction | null): string {
if (!action) return '';
switch (action.type) {
case 'playTitle':
return `playTitle:${action.titleId}`;
case 'playChapter':
return `playChapter:${action.titleId}:${action.chapterId}`;
case 'showMenu':
return `showMenu:${action.menuId}`;
case 'stop':
return 'stop';
default:
return '';
}
}

function stringToAction(str: string): PlaybackAction | null {
if (!str) return null;
if (str === 'stop') return { type: 'stop' };
const parts = str.split(':');
const type = parts[0];
if (type === 'playTitle' && parts[1]) return { type: 'playTitle', titleId: parts[1] };
if (type === 'playChapter' && parts[1] && parts[2])
return { type: 'playChapter', titleId: parts[1], chapterId: parts[2] };
if (type === 'showMenu' && parts[1]) return { type: 'showMenu', menuId: parts[1] };
return null;
}
Loading
Loading