@dailies-studio/shot-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/assets/index-B7notge7.css +1 -0
  2. package/dist/assets/index-BBE6Ta9L.js +52 -0
  3. package/dist/index.html +13 -0
  4. package/package.json +45 -0
  5. package/src/api.ts +289 -0
  6. package/src/app/App.tsx +394 -0
  7. package/src/audio/index.tsx +74 -0
  8. package/src/audio/preview.ts +364 -0
  9. package/src/bridge.ts +45 -0
  10. package/src/bridgeTypes.ts +582 -0
  11. package/src/channels.ts +40 -0
  12. package/src/main.tsx +15 -0
  13. package/src/panels/ResizeHandle.tsx +113 -0
  14. package/src/panels/agent/AgentPanel.tsx +478 -0
  15. package/src/panels/agent/agent.css +322 -0
  16. package/src/panels/export/ExportDialog.tsx +284 -0
  17. package/src/panels/files/FilePanel.tsx +564 -0
  18. package/src/panels/files/McpSection.tsx +66 -0
  19. package/src/panels/files/icons.tsx +118 -0
  20. package/src/panels/header/DocSwitcher.tsx +85 -0
  21. package/src/panels/header/Header.tsx +132 -0
  22. package/src/panels/header/HistoryControls.tsx +20 -0
  23. package/src/panels/header/StatusBadges.tsx +49 -0
  24. package/src/panels/header/TransportBar.tsx +86 -0
  25. package/src/panels/header/ZoomControls.tsx +38 -0
  26. package/src/panels/help/HelpSection.tsx +190 -0
  27. package/src/panels/inspector/FramingRows.tsx +69 -0
  28. package/src/panels/inspector/InspectorPanel.tsx +178 -0
  29. package/src/panels/inspector/MarkerSection.tsx +63 -0
  30. package/src/panels/inspector/ParamRow.tsx +71 -0
  31. package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
  32. package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
  33. package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
  34. package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
  35. package/src/panels/inspector/animate/ShotSection.tsx +126 -0
  36. package/src/panels/inspector/animate/useBlocks.ts +43 -0
  37. package/src/panels/inspector/design/AlignRow.tsx +63 -0
  38. package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
  39. package/src/panels/inspector/design/ClonerSection.tsx +142 -0
  40. package/src/panels/inspector/design/ComponentSection.tsx +94 -0
  41. package/src/panels/inspector/design/CornerSection.tsx +52 -0
  42. package/src/panels/inspector/design/FillSection.tsx +89 -0
  43. package/src/panels/inspector/design/HtmlSection.tsx +50 -0
  44. package/src/panels/inspector/design/InstanceSection.tsx +302 -0
  45. package/src/panels/inspector/design/MediaSection.tsx +251 -0
  46. package/src/panels/inspector/design/MeshSection.tsx +172 -0
  47. package/src/panels/inspector/design/ShadowSection.tsx +73 -0
  48. package/src/panels/inspector/design/StrokeSection.tsx +85 -0
  49. package/src/panels/inspector/design/TransformSection.tsx +91 -0
  50. package/src/panels/inspector/design/TypographySection.tsx +170 -0
  51. package/src/panels/inspector/scene/CameraSection.tsx +59 -0
  52. package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
  53. package/src/panels/inspector/scene/SceneSection.tsx +71 -0
  54. package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
  55. package/src/panels/inspector/useDocWrite.ts +268 -0
  56. package/src/panels/material/MaterialPicker.tsx +172 -0
  57. package/src/panels/stage/StageMenu.tsx +38 -0
  58. package/src/panels/stage/StagePanel.tsx +131 -0
  59. package/src/panels/timeline/AudioLane.tsx +340 -0
  60. package/src/panels/timeline/BlockRow.tsx +242 -0
  61. package/src/panels/timeline/ChannelRow.tsx +266 -0
  62. package/src/panels/timeline/LifespanRow.tsx +165 -0
  63. package/src/panels/timeline/MarkerLane.tsx +146 -0
  64. package/src/panels/timeline/SceneStrip.tsx +145 -0
  65. package/src/panels/timeline/TimelineDock.tsx +488 -0
  66. package/src/panels/timeline/model.ts +641 -0
  67. package/src/panels/timeline/snap.ts +191 -0
  68. package/src/panels/toolbar/AddMenu.tsx +230 -0
  69. package/src/panels/toolbar/CameraViewBar.tsx +70 -0
  70. package/src/panels/toolbar/ShadingBar.tsx +42 -0
  71. package/src/panels/toolbar/ToolDockBar.tsx +110 -0
  72. package/src/panels/toolbar/ViewModeBar.tsx +49 -0
  73. package/src/shotTokens.css +131 -0
  74. package/src/stage/FrustumGizmo.tsx +184 -0
  75. package/src/stage/GridPlane.tsx +274 -0
  76. package/src/stage/MoveGizmo.tsx +411 -0
  77. package/src/stage/PerfHud.tsx +206 -0
  78. package/src/stage/SplitRenderer.tsx +511 -0
  79. package/src/stage/StageApp.tsx +3423 -0
  80. package/src/stage/StageTools.tsx +581 -0
  81. package/src/stage/bridgeClient.ts +278 -0
  82. package/src/stage/feed.ts +243 -0
  83. package/src/stage/frustumGeometry.ts +172 -0
  84. package/src/stage/gizmoMath.ts +494 -0
  85. package/src/stage/handles.ts +151 -0
  86. package/src/stage/instanceModules.ts +152 -0
  87. package/src/stage/interactions/move.ts +172 -0
  88. package/src/stage/interactions/panZoom.ts +60 -0
  89. package/src/stage/isolation.ts +155 -0
  90. package/src/stage/main.tsx +110 -0
  91. package/src/stage/modalState.ts +427 -0
  92. package/src/stage/overlays/Overlays.tsx +1046 -0
  93. package/src/stage/panes.ts +121 -0
  94. package/src/stage/penState.ts +112 -0
  95. package/src/stage/project.ts +402 -0
  96. package/src/stage/shading.ts +55 -0
  97. package/src/stage/tokenColor.ts +26 -0
  98. package/src/stage/view.ts +182 -0
  99. package/src/stage/workingView.ts +636 -0
  100. package/src/state/agentActions.ts +203 -0
  101. package/src/state/clientId.ts +25 -0
  102. package/src/state/context.tsx +33 -0
  103. package/src/state/insert.ts +507 -0
  104. package/src/state/keymap.ts +560 -0
  105. package/src/state/menu.ts +141 -0
  106. package/src/state/panelSize.ts +26 -0
  107. package/src/state/renderEvents.ts +100 -0
  108. package/src/state/store.ts +4094 -0
  109. package/src/state/triangles.ts +70 -0
  110. package/src/state/types.ts +384 -0
  111. package/src/styles.css +2948 -0
  112. package/src/tools.ts +181 -0
@@ -0,0 +1,190 @@
1
+ // The left rail's Manual: how the editor works, the tool table and the keymap,
2
+ // rendered where a first-time author actually looks.
3
+ //
4
+ // ── ONE LIST, PINNED BY A TEST ──────────────────────────────────────────
5
+ //
6
+ // The tool rows render straight from TOOL_SPECS, so that half cannot drift.
7
+ // The shortcut rows cannot be derived — `resolveKey` is a function, not a
8
+ // table — so each row carries the EXACT EVENT it claims to describe plus the
9
+ // action kind the keymap must answer for it, and test/helpSection.test.tsx
10
+ // walks every row through `resolveKey`. A binding change without a manual
11
+ // edit is a red test, not a stale manual.
12
+ //
13
+ // No store, no context: this section is static prose over static tables,
14
+ // which is what lets the test mount it bare. The agent connect steps moved to
15
+ // the File tab's MCP section (McpSection.tsx), where an author actually looks.
16
+ import { SHOT_TOOLS, specOf } from "../../tools.js";
17
+ import type { KeyAction, KeyEventLike } from "../../state/keymap.js";
18
+
19
+ export interface ShortcutRow {
20
+ /** What the manual shows, house glyph style (⌘ ⇧ ⌥ — ⌘ is Ctrl on Windows). */
21
+ combo: string;
22
+ does: string;
23
+ group: "View" | "Working view" | "Edit" | "Animate & camera";
24
+ /** The event this combo produces, so the test can ask the keymap itself. */
25
+ probe: KeyEventLike;
26
+ /** Arrows answer differently with a selection; everything else omits it. */
27
+ hasSelection?: boolean;
28
+ /** The pen's two rows answer differently with a path open, and ONLY with one
29
+ * open — Escape means deselect the rest of the time, which is why the
30
+ * context carries "holds points" and not "the pen tool is armed". */
31
+ penActive?: boolean;
32
+ /** Escape's third meaning: leave one level of isolation. It only answers
33
+ * that with NOTHING selected, so the row carries the bit and the test
34
+ * probes with `hasSelection` false. */
35
+ isolation?: boolean;
36
+ /** The modal starters (G/R/S) answer only with the cursor over the working
37
+ * pane — the same letters arm tools everywhere else, and both readings
38
+ * have rows. */
39
+ workingPane?: boolean;
40
+ /** The axis and terminator rows answer only while a transform is LIVE. */
41
+ transformModal?: boolean;
42
+ /** The action `resolveKey(probe)` must return. */
43
+ action: KeyAction["kind"];
44
+ }
45
+
46
+ const probe = (
47
+ key: string,
48
+ m: { shift?: boolean; ctrl?: boolean; alt?: boolean; code?: string } = {},
49
+ ): KeyEventLike => ({
50
+ key,
51
+ shiftKey: m.shift === true,
52
+ altKey: m.alt === true,
53
+ ctrlKey: m.ctrl === true,
54
+ metaKey: false,
55
+ ...(m.code === undefined ? {} : { code: m.code }),
56
+ });
57
+
58
+ export const SHORTCUTS: readonly ShortcutRow[] = [
59
+ { group: "View", combo: "Space", does: "Play / pause", probe: probe(" "), action: "playToggle" },
60
+ { group: "View", combo: "← →", does: "Step one frame (nothing selected)", probe: probe("ArrowRight"), action: "step" },
61
+ { group: "View", combo: "1 / 2 / 3", does: "Working / split / camera view", probe: probe("1"), action: "stageLayout" },
62
+ { group: "View", combo: "[", does: "Hide / show the left panel", probe: probe("["), action: "togglePanel" },
63
+ { group: "View", combo: "]", does: "Hide / show the right panel", probe: probe("]"), action: "togglePanel" },
64
+ { group: "View", combo: ".", does: "Hide every panel", probe: probe("."), action: "togglePanel" },
65
+ { group: "View", combo: "\\", does: "Fit the timeline", probe: probe("\\"), action: "fitTimeline" },
66
+ { group: "View", combo: "⌘+ / ⌘−", does: "Zoom in / out", probe: probe("=", { ctrl: true }), action: "zoom" },
67
+ { group: "View", combo: "⌘0", does: "Zoom to 100%", probe: probe("0", { ctrl: true }), action: "zoomReset" },
68
+ { group: "View", combo: "Home", does: "Fit the active pane", probe: probe("Home"), action: "fit" },
69
+ { group: "View", combo: "⇧C", does: "Fit the active pane (Blender's frame all)", probe: probe("c", { shift: true }), action: "fit" },
70
+ { group: "View", combo: "Z", does: "Cycle working-pane shading (solid / wire / material / rendered)", probe: probe("z"), action: "shadingCycle" },
71
+ { group: "View", combo: "⇧Z", does: "Wireframe on / off", probe: probe("z", { shift: true }), action: "wireframeToggle" },
72
+
73
+ { group: "Working view", combo: "G", does: "Grab the selection — move it with the mouse, click to place", probe: probe("g"), workingPane: true, hasSelection: true, action: "modalStart" },
74
+ { group: "Working view", combo: "R", does: "Rotate the selection (roll)", probe: probe("r"), workingPane: true, hasSelection: true, action: "modalStart" },
75
+ { group: "Working view", combo: "S", does: "Scale the selection", probe: probe("s"), workingPane: true, hasSelection: true, action: "modalStart" },
76
+ { group: "Working view", combo: "G / S", does: "Nothing selected: move / zoom the camera", probe: probe("g"), workingPane: true, action: "modalStart" },
77
+ { group: "Working view", combo: "X / Y / Z", does: "Constrain the transform to an axis (again clears; G+Z = depth)", probe: probe("x"), transformModal: true, action: "modalAxis" },
78
+ { group: "Working view", combo: "0-9", does: "Type an exact value: G X 200 Enter", probe: probe("2"), transformModal: true, action: "modalNumber" },
79
+ // `hasSelection` because R is the one that needs it: the CAMERA has no modal
80
+ // roll, so switching to rotate on a camera transform would arm one that could
81
+ // never emit.
82
+ { group: "Working view", combo: "G / R / S", does: "Switch transform without leaving the gesture", probe: probe("r"), transformModal: true, hasSelection: true, action: "modalStart" },
83
+ { group: "Working view", combo: "Enter", does: "Confirm the transform (or click)", probe: probe("Enter"), transformModal: true, action: "modalConfirm" },
84
+ { group: "Working view", combo: "Esc", does: "Cancel the transform (or right-click)", probe: probe("Escape"), transformModal: true, action: "modalCancel" },
85
+ { group: "Working view", combo: "Num1 / 3 / 7", does: "Front / right / top view (⌘ = the opposite)", probe: probe("1", { code: "Numpad1" }), action: "workingViewPreset" },
86
+ { group: "Working view", combo: "Num0", does: "Camera view, and back again", probe: probe("0", { code: "Numpad0" }), action: "stageLayout" },
87
+ { group: "Working view", combo: "Num.", does: "Frame the selection", probe: probe(".", { code: "NumpadDecimal" }), action: "fitSelection" },
88
+ { group: "Working view", combo: "Num2 / 4 / 6 / 8", does: "Orbit 15°; Num9 looks from the opposite side", probe: probe("4", { code: "Numpad4" }), action: "orbitStep" },
89
+ { group: "Working view", combo: "Num5", does: "Orthographic / perspective", probe: probe("5", { code: "Numpad5" }), action: "toggleWorkingOrtho" },
90
+ { group: "Working view", combo: "Num/", does: "Go inside the selection, and back out", probe: probe("/", { code: "NumpadDivide" }), action: "toggleIsolation" },
91
+ { group: "Working view", combo: "Num+ / Num−", does: "Zoom the active pane", probe: probe("+", { code: "NumpadAdd" }), action: "zoom" },
92
+
93
+ { group: "Edit", combo: "Arrows", does: "Nudge selection 1 px (⇧ = 10)", probe: probe("ArrowRight"), hasSelection: true, action: "nudge" },
94
+ { group: "Edit", combo: "⌘Z", does: "Undo", probe: probe("z", { ctrl: true }), action: "undo" },
95
+ { group: "Edit", combo: "⌘⇧Z", does: "Redo", probe: probe("z", { ctrl: true, shift: true }), action: "redo" },
96
+ { group: "Edit", combo: "⌘S", does: "Save", probe: probe("s", { ctrl: true }), action: "save" },
97
+ { group: "Edit", combo: "⌘D", does: "Duplicate", probe: probe("d", { ctrl: true }), action: "duplicate" },
98
+ { group: "Edit", combo: "⌘A / A", does: "Select all, in the scope you are in", probe: probe("a", { ctrl: true }), action: "selectAll" },
99
+ { group: "Edit", combo: "⌥A", does: "Deselect all", probe: probe("a", { alt: true }), action: "deselect" },
100
+ { group: "Edit", combo: "B", does: "Box select with the next drag", probe: probe("b"), action: "boxSelect" },
101
+ { group: "Edit", combo: "⇧D", does: "Duplicate", probe: probe("d", { shift: true }), action: "duplicate" },
102
+ { group: "Edit", combo: "Esc", does: "Deselect", probe: probe("Escape"), action: "deselect" },
103
+ { group: "Edit", combo: "Del", does: "Delete selection", probe: probe("Delete"), action: "deleteSelection" },
104
+ { group: "Edit", combo: "⌘G", does: "Group", probe: probe("g", { ctrl: true }), action: "group" },
105
+ { group: "Edit", combo: "⌘⇧G", does: "Ungroup", probe: probe("g", { ctrl: true, shift: true }), action: "ungroup" },
106
+ { group: "Edit", combo: "⌥L", does: "Lock / unlock", probe: probe("l", { alt: true }), action: "toggleLock" },
107
+ { group: "Edit", combo: "H", does: "Hide the selection", probe: probe("h"), action: "setHidden" },
108
+ { group: "Edit", combo: "⇧H", does: "Hide everything else", probe: probe("h", { shift: true }), action: "setHidden" },
109
+ { group: "Edit", combo: "⌥H", does: "Show everything again", probe: probe("h", { alt: true }), action: "setHidden" },
110
+ { group: "Edit", combo: "⌥G / ⌥R", does: "Clear the position / the rotation", probe: probe("g", { alt: true }), action: "clearTransform" },
111
+ { group: "Edit", combo: "Enter", does: "Edit inside the selection", probe: probe("Enter"), hasSelection: true, action: "enterIsolation" },
112
+ { group: "Edit", combo: "Esc", does: "Come back out one level", probe: probe("Escape"), isolation: true, action: "exitIsolation" },
113
+ { group: "Edit", combo: "Enter", does: "Finish the path you are drawing", probe: probe("Enter"), penActive: true, action: "penCommit" },
114
+ { group: "Edit", combo: "Esc", does: "Throw away the path you are drawing", probe: probe("Escape"), penActive: true, action: "penAbandon" },
115
+
116
+ { group: "Animate & camera", combo: "⇧A", does: "Animate mode — edits become blocks", probe: probe("a", { shift: true }), action: "animateToggle" },
117
+ { group: "Animate & camera", combo: "M", does: "Drop a marker at the playhead", probe: probe("m"), action: "addMarker" },
118
+ { group: "Animate & camera", combo: "⌥C", does: "Point the camera at the working view", probe: probe("c", { alt: true }), action: "alignCameraToView" },
119
+ { group: "Animate & camera", combo: "⌘B", does: "Split layer at the playhead", probe: probe("b", { ctrl: true }), action: "splitAtPlayhead" },
120
+ { group: "Animate & camera", combo: "⌘E", does: "Export video", probe: probe("e", { ctrl: true }), action: "openExport" },
121
+ ];
122
+
123
+ const GROUPS = ["View", "Working view", "Edit", "Animate & camera"] as const;
124
+
125
+ /** The sentences a first session needs, in the order a session meets them.
126
+ * Mouse gestures live HERE and not in the shortcut table below, because every
127
+ * row of that table is walked through the real keymap by its test — a row for
128
+ * a gesture no key produces would have to carry a fake event to pass. */
129
+ const BASICS: readonly string[] = [
130
+ "Camera view: scroll to pan, ctrl+scroll to zoom at the pointer, space-drag to pan.",
131
+ "Working view (1 / 2): drag empty space or middle-drag to orbit, scroll to dolly, space-drag or ⇧middle-drag to pan. Click a layer to select it, then G / R / S transform it Blender-style; the panels tuck away while you work.",
132
+ "Working view aids: a ground grid under the frame that coarsens as you pull back, three coloured world axes (X red, Y green, Z blue) through the canvas centre, and an axis ball top-right — click an end to look down that axis.",
133
+ "Right-click a layer, on the stage or in the outline, for duplicate, hide, lock, isolate, delete — and on a model, a material.",
134
+ "Models: the outline shows each one's triangle count, and tints it once the scene gets heavy enough to feel it. Give a model a surface with right-click → Apply material; a toon model also grows a sun handle you can drag to aim its key.",
135
+ "Double-click a layer to go INSIDE it: the working pane frames that subtree flat and head-on and everything else dims. Orbit, pan or dolly and the camera is yours; Home puts it back head-on. A blue bar over the pane says you are inside; Esc backs out one level, or click a crumb in the left panel.",
136
+ "Drag arms (red/green/blue) sit on the selection, or on the CAMERA when nothing is selected — the same subject G and S pick. Z moves a layer in depth; on the camera it dollies.",
137
+ "Header zoom, ⌘+ / ⌘− and ⇧C all act on the pane you last clicked. ⇧Z is the wireframe toggle.",
138
+ "Draw: arm a tool in the top bar, then drag on the stage. Text takes a click.",
139
+ "Animate: toggle ⇧A, then edit — the change becomes a block ending at the playhead.",
140
+ "Camera: use the timeline's + shot button for a move, or in the working view (1) drag the yellow frustum's edge to pan, its apex to zoom, or its arms for one axis at a time.",
141
+ "Audio: drop a file on the timeline's audio lane. Markers ride their clip.",
142
+ "Everything exports exactly as previewed — same engine, same frames.",
143
+ ];
144
+
145
+ export function HelpSection() {
146
+ return (
147
+ <details className="help-section" data-testid="help-section">
148
+ <summary className="file-section-label help-summary">Manual</summary>
149
+
150
+ <ul className="help-basics">
151
+ {BASICS.map((line) => (
152
+ <li key={line}>{line}</li>
153
+ ))}
154
+ </ul>
155
+
156
+ <div className="file-section-label">Tools</div>
157
+ <ul className="help-rows">
158
+ {SHOT_TOOLS.map((tool) => {
159
+ const spec = specOf(tool);
160
+ return (
161
+ <li key={tool} className="help-row">
162
+ <span>{spec.label}</span>
163
+ {spec.key !== null && (
164
+ <kbd>{`${spec.shift === true ? "⇧" : ""}${spec.key.toUpperCase()}`}</kbd>
165
+ )}
166
+ </li>
167
+ );
168
+ })}
169
+ </ul>
170
+
171
+ {GROUPS.map((group) => (
172
+ <div key={group}>
173
+ <div className="file-section-label">{group}</div>
174
+ <ul className="help-rows">
175
+ {SHORTCUTS.filter((row) => row.group === group).map((row) => (
176
+ // Keyed on the combo AND what it does: Esc and Enter each mean
177
+ // three things now, depending on what is open, and every one of
178
+ // them is a row.
179
+ <li key={`${row.combo}:${row.does}`} className="help-row">
180
+ <span>{row.does}</span>
181
+ <kbd>{row.combo}</kbd>
182
+ </li>
183
+ ))}
184
+ </ul>
185
+ </div>
186
+ ))}
187
+ <p className="help-note">⌘ is Ctrl on Windows and Linux.</p>
188
+ </details>
189
+ );
190
+ }
@@ -0,0 +1,69 @@
1
+ // A `Framing` as two rows of fields. ONE definition, three subjects: the
2
+ // scene's own camera, and a move's `from` and `to`.
3
+ //
4
+ // It is shared rather than copied because the list IS the schema. A sixth
5
+ // framing field would otherwise have to be remembered in two files, and the one
6
+ // that got forgotten would silently keep an authored value the author could
7
+ // never see or reach.
8
+ //
9
+ // The write is a PATCH and not a whole framing, so a caller can route it
10
+ // straight at whichever seam owns its subject: `setSceneCamera` goes through
11
+ // the refusal fork, `setShotFraming` does not, and this component knows about
12
+ // neither.
13
+ import { InspectorRow, NumberField } from "@dailies-studio/ui";
14
+ import { MAX_FRAMING_ZOOM, MIN_FRAMING_ZOOM } from "@dailies-studio/shot/doc";
15
+ import type { Framing } from "@dailies-studio/shot/doc";
16
+
17
+ /** The five fields, in the order an author thinks about them: where it looks,
18
+ * how much it sees, how it is held, what is sharp. */
19
+ const FIELDS: ReadonlyArray<{
20
+ key: keyof Framing;
21
+ label: string;
22
+ unit?: string;
23
+ step: number;
24
+ min?: number;
25
+ max?: number;
26
+ }> = [
27
+ { key: "x", label: "X", step: 1 },
28
+ { key: "y", label: "Y", step: 1 },
29
+ // Clamped at the widget as well as in the store: the store's clamp is the
30
+ // boundary that must hold, and this one is what stops the author typing a
31
+ // value that silently becomes a different one.
32
+ { key: "zoom", label: "Zoom", step: 0.01, min: MIN_FRAMING_ZOOM, max: MAX_FRAMING_ZOOM },
33
+ { key: "rollDeg", label: "Roll", unit: "deg", step: 0.5 },
34
+ { key: "focusZ", label: "Focus", step: 1 },
35
+ ];
36
+
37
+ export function FramingRows(props: {
38
+ framing: Framing;
39
+ /** Prefixes every field's accessible name ("Camera x", "From x"). Three
40
+ * framings can be on screen at once, so the label alone is not a name. */
41
+ name: string;
42
+ onCommit(patch: Partial<Framing>): void;
43
+ }) {
44
+ return (
45
+ <>
46
+ {[FIELDS.slice(0, 2), FIELDS.slice(2)].map((group, i) => (
47
+ <InspectorRow key={i}>
48
+ {group.map((f) => {
49
+ const limits: { min?: number; max?: number } = {};
50
+ if (f.min !== undefined) limits.min = f.min;
51
+ if (f.max !== undefined) limits.max = f.max;
52
+ return (
53
+ <NumberField
54
+ key={f.key}
55
+ value={props.framing[f.key]}
56
+ label={f.label}
57
+ ariaLabel={`${props.name} ${f.label.toLowerCase()}`}
58
+ {...(f.unit === undefined ? {} : { unit: f.unit })}
59
+ {...limits}
60
+ step={f.step}
61
+ onCommit={(next) => props.onCommit({ [f.key]: next })}
62
+ />
63
+ );
64
+ })}
65
+ </InspectorRow>
66
+ ))}
67
+ </>
68
+ );
69
+ }
@@ -0,0 +1,178 @@
1
+ // The right rail: Design when a layer is selected, Scene when nothing is.
2
+ //
3
+ // ── TWO MODE BITS, AND THE TAB ONLY MOVES ONE ───────────────────────────
4
+ //
5
+ // Clicking these tabs sets `inspectorMode`, which is a VIEW choice. It does NOT
6
+ // set `animateMode`, which is a BEHAVIOUR choice and belongs to ⇧A alone.
7
+ // Collapsing them would mean that looking at the blocks silently armed block
8
+ // authoring on the next canvas drag — a data-corrupting surprise dressed as a
9
+ // convenience.
10
+ //
11
+ // The reverse dependency DOES exist and is deliberate: turning animate ON moves
12
+ // the tab to Animate, because the author is about to author blocks and wants to
13
+ // see them. One way only.
14
+ import { SegmentedControl } from "@dailies-studio/ui";
15
+ import { useShotEditor, useStore } from "../../state/context.js";
16
+ import { useSelectedLayers } from "./useDocWrite.js";
17
+ import { TransformSection } from "./design/TransformSection.js";
18
+ import { AppearanceSection } from "./design/AppearanceSection.js";
19
+ import { FillSection } from "./design/FillSection.js";
20
+ import { StrokeSection } from "./design/StrokeSection.js";
21
+ import { ShadowSection } from "./design/ShadowSection.js";
22
+ import { CornerSection } from "./design/CornerSection.js";
23
+ import { HtmlSection } from "./design/HtmlSection.js";
24
+ import { TextStaggerSection, TypographySection } from "./design/TypographySection.js";
25
+ import { MeshSection } from "./design/MeshSection.js";
26
+ import { ClonerSection } from "./design/ClonerSection.js";
27
+ import { MediaSection } from "./design/MediaSection.js";
28
+ import { InstanceSection } from "./design/InstanceSection.js";
29
+ import { ComponentSection } from "./design/ComponentSection.js";
30
+ import { SceneSection } from "./scene/SceneSection.js";
31
+ import { CameraSection } from "./scene/CameraSection.js";
32
+ import { EffectsSection } from "./scene/EffectsSection.js";
33
+ import { TransitionSection } from "./scene/TransitionSection.js";
34
+ import { InOutPresets } from "./animate/InOutPresets.js";
35
+ import { DuringBlocks } from "./animate/DuringBlocks.js";
36
+ import { BlockTiming } from "./animate/BlockTiming.js";
37
+ import { ResolvedChannels } from "./animate/ResolvedChannels.js";
38
+ import { ShotSection } from "./animate/ShotSection.js";
39
+ import { MarkerSection } from "./MarkerSection.js";
40
+ import { CAMERA_ROW_ID } from "../timeline/model.js";
41
+
42
+ export function InspectorPanel() {
43
+ const store = useStore();
44
+ const doc = useShotEditor((s) => s.doc);
45
+ const mode = useShotEditor((s) => s.inspectorMode);
46
+ // The camera's row holds SHOTS, not blocks, and a shot belongs to the scene
47
+ // rather than to any layer. So it is the one Animate subject that does not
48
+ // come from the selection, and it wins: a bar the author just clicked is what
49
+ // they are looking at.
50
+ //
51
+ // It RESOLVES THE SHOT rather than testing the row id, because this branch
52
+ // replaces the whole tab: a focus that ShotSection cannot resolve would render
53
+ // the Animate tab as literally nothing, no placeholder and no layer sections.
54
+ // Two ways to get there, both reachable — focusing a bar in one scene and
55
+ // scrubbing into another (the panel reads the PLAYHEAD's scene), and undoing
56
+ // the ⇧C that created the shot the focus points at.
57
+ const cameraShot = useShotEditor((s) => {
58
+ const focused = s.selectedBlock;
59
+ if (focused === null || focused.layerId !== CAMERA_ROW_ID || focused.sceneId !== s.sceneId) return false;
60
+ return s.doc?.scenesById[focused.sceneId]?.shots.some((shot) => shot.id === focused.blockId) ?? false;
61
+ });
62
+ const layers = useSelectedLayers();
63
+ // THE FIRST SUBJECT, above the mode fork and above the selection. It matches
64
+ // `deleteSelection`'s order — marker, then block, then layers — so the panel
65
+ // and the Delete key can never be looking at two different things. A marker
66
+ // is neither a design property nor an animation, so it replaces the body
67
+ // rather than living in one of the tabs.
68
+ //
69
+ // It RESOLVES THE MARKER rather than testing the id for null, for the reason
70
+ // the camera branch below does: this replaces the whole panel, so a selection
71
+ // pointing at a marker that is gone would render the rail as literally
72
+ // nothing. Undoing the `M` that created it is the one-keystroke way there.
73
+ const markerSelected = useShotEditor(
74
+ (s) => s.selectedMarker !== null && (s.doc?.markers ?? []).some((m) => m.id === s.selectedMarker),
75
+ );
76
+ if (doc === null) return null;
77
+ if (markerSelected) {
78
+ return (
79
+ <aside className="inspector" data-testid="inspector">
80
+ <MarkerSection />
81
+ </aside>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <aside className="inspector" data-testid="inspector">
87
+ <SegmentedControl
88
+ ariaLabel="Inspector mode"
89
+ value={mode}
90
+ options={[
91
+ { value: "design", label: "Design" },
92
+ { value: "animate", label: "Animate" },
93
+ ]}
94
+ onChange={(next) => store.setInspectorMode(next as "design" | "animate")}
95
+ />
96
+
97
+ {mode === "animate" ? (
98
+ cameraShot ? (
99
+ <ShotSection />
100
+ ) : layers.length === 0 ? (
101
+ // Nothing selected: the tab has no subject. Saying what to do beats an
102
+ // empty panel that reads as broken.
103
+ <p className="inspector-empty" data-testid="animate-placeholder">
104
+ Select a layer to add an entrance, an exit, or a During block.
105
+ </p>
106
+ ) : (
107
+ <>
108
+ {/* First, because it is the only thing here that describes the
109
+ layer rather than proposing an edit to it: what the stack adds
110
+ up to at this frame is the context every choice below is made
111
+ against. */}
112
+ <ResolvedChannels />
113
+ <InOutPresets slot="in" title="In" />
114
+ <DuringBlocks />
115
+ <InOutPresets slot="out" title="Out" />
116
+ {/* Last, because it is about ONE block and everything above chooses
117
+ which. A timing editor above the list it belongs to would read as
118
+ the panel's subject rather than the focused row's. */}
119
+ <BlockTiming />
120
+ </>
121
+ )
122
+ ) : layers.length === 0 ? (
123
+ <>
124
+ <SceneSection />
125
+ {/* Between the scene and its post stack, which is the order the
126
+ picture is made in: the scene, the camera looking at it, then what
127
+ happens to the frame after. */}
128
+ <CameraSection />
129
+ <EffectsSection />
130
+ {/* …and last, what happens when the frame stops being this shot's. */}
131
+ <TransitionSection />
132
+ </>
133
+ ) : (
134
+ <>
135
+ <TransformSection />
136
+ <AppearanceSection />
137
+ {/* Above the paint sections: a mesh's look comes from its material,
138
+ and `paint` is inert on it (types.ts), so the model IS the
139
+ subject the fills below have nothing to say about. */}
140
+ <MeshSection />
141
+ {/* Beside the model section for the same reason it sits above the
142
+ paint ones: an instance paints itself, so its contract IS the
143
+ subject and the fills below have nothing to say about it. */}
144
+ <InstanceSection />
145
+ {/* …and a component placement, for the same reason again: what it
146
+ draws comes from its definition, so the fills below are inert. */}
147
+ <ComponentSection />
148
+ {/* Beside the model section and above the paint ones for the same
149
+ reason: a cloner paints nothing of its own, so what it DOES is the
150
+ subject the fills below have nothing to say about. */}
151
+ <ClonerSection />
152
+ {/* Above the paint sections for the same reason the three geometry
153
+ sections are: a media layer PAINTS ITSELF from its asset, and
154
+ `paint.fill` never reaches it (the runtime asks the rasterizer
155
+ only for a "mask" and a "plate", and the fill branch lives in
156
+ neither) — so the source IS the subject the fill below has
157
+ nothing to say about. Corner, stroke and shadow still apply,
158
+ through exactly those two rasters. */}
159
+ <MediaSection />
160
+ <FillSection />
161
+ <StrokeSection />
162
+ <CornerSection />
163
+ <ShadowSection />
164
+ <TypographySection />
165
+ {/* Beside Typography, because it is the other kind whose content is
166
+ authored here rather than dragged on the stage. Below it because a
167
+ snippet's two fields are taller than everything above them. */}
168
+ <HtmlSection />
169
+ {/* Directly under Typography, because it says how that type moves —
170
+ and it is the one Design-tab section that only matters once a
171
+ `stagger` block exists, so it sits last and collapsed. */}
172
+ <TextStaggerSection />
173
+ </>
174
+ )}
175
+ </aside>
176
+ );
177
+ }
178
+
@@ -0,0 +1,63 @@
1
+ // The selected marker: where it is, what it says, what colour it is drawn in.
2
+ //
3
+ // It sits OUTSIDE the Design/Animate fork rather than inside one of the tabs. A
4
+ // marker is neither a design property nor an animation: putting it in a tab
5
+ // would mean the panel could be showing the wrong tab for its own subject, and
6
+ // the author would click a diamond and see nothing.
7
+ //
8
+ // The FRAME field is a master frame, like every other number about a marker, and
9
+ // it is the only place in this chrome where one is typed rather than dragged.
10
+ import { ColorPicker, InspectorRow, InspectorSection, NumberField, TextField } from "@dailies-studio/ui";
11
+ import { useShotEditor, useStore } from "../../state/context.js";
12
+ import { timecode } from "../timeline/model.js";
13
+ import { NO_SCRUB } from "./useDocWrite.js";
14
+
15
+ /** What a marker with no `color` is drawn in. The same value shotTokens.css
16
+ * gives `--marker-pin`, restated here because the swatch needs a concrete
17
+ * colour to show and a CSS variable is not one. */
18
+ const DEFAULT_MARKER_COLOR = "#ffffff";
19
+
20
+ export function MarkerSection() {
21
+ const store = useStore();
22
+ const doc = useShotEditor((s) => s.doc);
23
+ const selected = useShotEditor((s) => s.selectedMarker);
24
+ if (doc === null || selected === null) return null;
25
+ const marker = (doc.markers ?? []).find((m) => m.id === selected);
26
+ if (marker === undefined) return null;
27
+
28
+ return (
29
+ <InspectorSection title="Marker" persistKey="shot:marker" collapsible={false}>
30
+ <InspectorRow label="Label">
31
+ <TextField
32
+ value={marker.label ?? ""}
33
+ ariaLabel="Marker label"
34
+ onCommit={(label) => store.setMarker(marker.id, { label })}
35
+ />
36
+ </InspectorRow>
37
+ <InspectorRow>
38
+ <NumberField
39
+ value={marker.frame}
40
+ label="Frame"
41
+ ariaLabel="Marker frame"
42
+ min={0}
43
+ step={1}
44
+ onCommit={(frame) => store.setMarker(marker.id, { frame: Math.max(0, Math.round(frame)) })}
45
+ />
46
+ <span className="insp-dim">{timecode(marker.frame, doc.fps)}</span>
47
+ </InspectorRow>
48
+ <InspectorRow label="Colour">
49
+ <ColorPicker
50
+ value={marker.color ?? DEFAULT_MARKER_COLOR}
51
+ ariaLabel="Marker colour"
52
+ scrub={NO_SCRUB}
53
+ commit={(color) => store.setMarker(marker.id, { color })}
54
+ />
55
+ </InspectorRow>
56
+ <InspectorRow>
57
+ <button type="button" className="btn" onClick={() => store.removeMarker(marker.id)}>
58
+ Delete marker
59
+ </button>
60
+ </InspectorRow>
61
+ </InspectorSection>
62
+ );
63
+ }
@@ -0,0 +1,71 @@
1
+ // One preset param, spec-driven. Shared by the motion-block panel and the
2
+ // transition panel — both drive a `TransitionParamSpec`, and two copies of a
3
+ // widget that turns a spec into a control is two places for "vec2 means two
4
+ // fields" to be decided differently.
5
+ //
6
+ // Written here rather than through `PropControl`, because that widget
7
+ // dispatches a `ControlSpec` from @dailies-studio/contract and these are
8
+ // `TransitionParamSpec`s — a deliberately smaller vocabulary of exactly three
9
+ // kinds. Adapting the one into the other would be more code than the three
10
+ // arms, and would put a lossy translation between the author and the value.
11
+ import { InspectorRow, NumberField, Toggle } from "@dailies-studio/ui";
12
+ import type { TransitionParamSpec } from "@dailies-studio/transitions";
13
+ import type { Value, Vec2 } from "@dailies-studio/engine";
14
+
15
+ export function ParamRow(props: {
16
+ spec: TransitionParamSpec;
17
+ value: Value;
18
+ onCommit(next: Value): void;
19
+ }) {
20
+ const { spec } = props;
21
+ const label = spec.label ?? spec.name;
22
+ const limits: { min?: number; max?: number } = {};
23
+ if (spec.min !== undefined) limits.min = spec.min;
24
+ if (spec.max !== undefined) limits.max = spec.max;
25
+
26
+ if (spec.kind === "boolean") {
27
+ return (
28
+ <InspectorRow label={label}>
29
+ <Toggle
30
+ checked={props.value === true}
31
+ ariaLabel={label}
32
+ onChange={(next) => props.onCommit(next)}
33
+ />
34
+ </InspectorRow>
35
+ );
36
+ }
37
+ if (spec.kind === "vec2") {
38
+ const v = asVec2(props.value);
39
+ return (
40
+ <InspectorRow label={label}>
41
+ {([0, 1] as const).map((i) => (
42
+ <NumberField
43
+ key={i}
44
+ value={v[i]}
45
+ ariaLabel={`${label} ${i === 0 ? "X" : "Y"}`}
46
+ {...limits}
47
+ step={spec.step ?? 0.01}
48
+ onCommit={(next) => props.onCommit(i === 0 ? [next, v[1]] : [v[0], next])}
49
+ />
50
+ ))}
51
+ </InspectorRow>
52
+ );
53
+ }
54
+ return (
55
+ <InspectorRow label={label}>
56
+ <NumberField
57
+ value={typeof props.value === "number" ? props.value : 0}
58
+ ariaLabel={label}
59
+ {...limits}
60
+ step={spec.step ?? 0.01}
61
+ onCommit={(next) => props.onCommit(next)}
62
+ />
63
+ </InspectorRow>
64
+ );
65
+ }
66
+
67
+ function asVec2(v: Value): Vec2 {
68
+ return Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "number"
69
+ ? [v[0], v[1]]
70
+ : [0, 0];
71
+ }