create-aura3d 1.3.1 → 1.3.3

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 (126) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +9 -2
  5. package/dist/index.js.map +1 -1
  6. package/package.json +11 -3
  7. package/templates/animation-channel/package.json +1 -1
  8. package/templates/animation-channel/tests/screenshot.spec.ts +16 -0
  9. package/templates/animation-studio/dist/episodes/prompt-to-scene-proof/two-robots-fixing-a-car.document.json +680 -0
  10. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  11. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  12. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  13. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  14. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  15. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  16. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  17. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +2021 -2542
  18. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/broken.png +0 -0
  19. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/chefs.png +0 -0
  20. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/customer.png +0 -0
  21. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-1.png +0 -0
  22. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-2.png +0 -0
  23. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  24. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  25. package/templates/animation-studio/dist/scene/working.document.json +219 -269
  26. package/templates/animation-studio/index.html +12 -0
  27. package/templates/animation-studio/node_modules.publish-aside/.vite/deps/_metadata.json +8 -0
  28. package/templates/animation-studio/node_modules.publish-aside/.vite/deps/package.json +3 -0
  29. package/templates/animation-studio/package-lock.json +1307 -55
  30. package/templates/animation-studio/package.json +11 -5
  31. package/templates/animation-studio/scripts/asset-motion-probe.ts +7 -1
  32. package/templates/animation-studio/scripts/render-live.ts +24 -0
  33. package/templates/animation-studio/src/director/prompt-to-scene.ts +73 -5
  34. package/templates/animation-studio/studio/dist/assets/index-C7f8tHfi.css +1 -0
  35. package/templates/animation-studio/studio/dist/assets/index-CLo3GXyN.js +43 -0
  36. package/templates/animation-studio/studio/dist/index.html +19 -0
  37. package/templates/animation-studio/studio/index.html +18 -0
  38. package/templates/animation-studio/studio/src/App.tsx +498 -0
  39. package/templates/animation-studio/studio/src/components/Console.tsx +372 -0
  40. package/templates/animation-studio/studio/src/components/Icon.tsx +90 -0
  41. package/templates/animation-studio/studio/src/components/Inspector.tsx +265 -0
  42. package/templates/animation-studio/studio/src/components/Outliner.tsx +210 -0
  43. package/templates/animation-studio/studio/src/components/Palette.tsx +204 -0
  44. package/templates/animation-studio/studio/src/components/Stage.tsx +344 -0
  45. package/templates/animation-studio/studio/src/components/Timeline.tsx +230 -0
  46. package/templates/animation-studio/studio/src/components/Topbar.tsx +66 -0
  47. package/templates/animation-studio/studio/src/main.tsx +10 -0
  48. package/templates/animation-studio/studio/src/state/backend.ts +108 -0
  49. package/templates/animation-studio/studio/src/state/fidelity.ts +110 -0
  50. package/templates/animation-studio/studio/src/state/mapDocument.ts +430 -0
  51. package/templates/animation-studio/studio/src/state/sceneTool.ts +66 -0
  52. package/templates/animation-studio/studio/src/state/types.ts +173 -0
  53. package/templates/animation-studio/studio/src/state/util.ts +46 -0
  54. package/templates/animation-studio/studio/src/styles.css +528 -0
  55. package/templates/animation-studio/studio/vite.config.ts +175 -0
  56. package/templates/animation-studio/tests/route-health.spec.ts +33 -0
  57. package/templates/animation-studio/tests/screenshot.spec.ts +30 -0
  58. package/templates/character-controller/package.json +3 -3
  59. package/templates/character-controller/src/main.ts +18 -1
  60. package/templates/cinematic-scene/package.json +1 -1
  61. package/templates/cinematic-scene/tests/screenshot.spec.ts +3 -1
  62. package/templates/episode-builder/package.json +1 -1
  63. package/templates/episode-builder/tests/screenshot.spec.ts +15 -0
  64. package/templates/fighting-game/package.json +1 -1
  65. package/templates/fighting-game/tests/screenshot.spec.ts +15 -0
  66. package/templates/mini-game/package.json +1 -1
  67. package/templates/product-viewer/package.json +1 -1
  68. package/templates/product-viewer/tests/screenshot.spec.ts +3 -1
  69. package/templates/prompt-animation-channel/package.json +1 -1
  70. package/templates/prompt-animation-channel/tests/screenshot.spec.ts +15 -0
  71. package/templates/three-compat-architecture-interior/index.html +16 -0
  72. package/templates/three-compat-architecture-interior/package.json +21 -0
  73. package/templates/three-compat-architecture-interior/playwright.config.ts +14 -0
  74. package/templates/three-compat-architecture-interior/src/main.ts +27 -0
  75. package/templates/three-compat-architecture-interior/tests/route-health.spec.ts +16 -0
  76. package/templates/three-compat-architecture-interior/tests/screenshot.spec.ts +15 -0
  77. package/templates/three-compat-architecture-interior/tsconfig.json +19 -0
  78. package/templates/three-compat-asset-inspector/index.html +16 -0
  79. package/templates/three-compat-asset-inspector/package.json +21 -0
  80. package/templates/three-compat-asset-inspector/playwright.config.ts +14 -0
  81. package/templates/three-compat-asset-inspector/src/main.ts +17 -0
  82. package/templates/three-compat-asset-inspector/tests/route-health.spec.ts +16 -0
  83. package/templates/three-compat-asset-inspector/tests/screenshot.spec.ts +15 -0
  84. package/templates/three-compat-asset-inspector/tsconfig.json +19 -0
  85. package/templates/three-compat-character-viewer/index.html +16 -0
  86. package/templates/three-compat-character-viewer/package.json +21 -0
  87. package/templates/three-compat-character-viewer/playwright.config.ts +14 -0
  88. package/templates/three-compat-character-viewer/src/main.ts +17 -0
  89. package/templates/three-compat-character-viewer/tests/route-health.spec.ts +16 -0
  90. package/templates/three-compat-character-viewer/tests/screenshot.spec.ts +15 -0
  91. package/templates/three-compat-character-viewer/tsconfig.json +19 -0
  92. package/templates/three-compat-custom-threejs-migration/index.html +16 -0
  93. package/templates/three-compat-custom-threejs-migration/package.json +21 -0
  94. package/templates/three-compat-custom-threejs-migration/playwright.config.ts +14 -0
  95. package/templates/three-compat-custom-threejs-migration/src/main.ts +16 -0
  96. package/templates/three-compat-custom-threejs-migration/tests/route-health.spec.ts +16 -0
  97. package/templates/three-compat-custom-threejs-migration/tests/screenshot.spec.ts +15 -0
  98. package/templates/three-compat-custom-threejs-migration/tsconfig.json +19 -0
  99. package/templates/three-compat-large-scene/index.html +16 -0
  100. package/templates/three-compat-large-scene/package.json +21 -0
  101. package/templates/three-compat-large-scene/playwright.config.ts +14 -0
  102. package/templates/three-compat-large-scene/src/main.ts +31 -0
  103. package/templates/three-compat-large-scene/tests/route-health.spec.ts +16 -0
  104. package/templates/three-compat-large-scene/tests/screenshot.spec.ts +15 -0
  105. package/templates/three-compat-large-scene/tsconfig.json +19 -0
  106. package/templates/three-compat-material-authoring/index.html +16 -0
  107. package/templates/three-compat-material-authoring/package.json +21 -0
  108. package/templates/three-compat-material-authoring/playwright.config.ts +14 -0
  109. package/templates/three-compat-material-authoring/src/main.ts +33 -0
  110. package/templates/three-compat-material-authoring/tests/route-health.spec.ts +16 -0
  111. package/templates/three-compat-material-authoring/tests/screenshot.spec.ts +15 -0
  112. package/templates/three-compat-material-authoring/tsconfig.json +19 -0
  113. package/templates/three-compat-postprocess-scene/index.html +16 -0
  114. package/templates/three-compat-postprocess-scene/package.json +21 -0
  115. package/templates/three-compat-postprocess-scene/playwright.config.ts +14 -0
  116. package/templates/three-compat-postprocess-scene/src/main.ts +17 -0
  117. package/templates/three-compat-postprocess-scene/tests/route-health.spec.ts +16 -0
  118. package/templates/three-compat-postprocess-scene/tests/screenshot.spec.ts +15 -0
  119. package/templates/three-compat-postprocess-scene/tsconfig.json +19 -0
  120. package/templates/three-compat-premium-product-viewer/index.html +16 -0
  121. package/templates/three-compat-premium-product-viewer/package.json +21 -0
  122. package/templates/three-compat-premium-product-viewer/playwright.config.ts +14 -0
  123. package/templates/three-compat-premium-product-viewer/src/main.ts +18 -0
  124. package/templates/three-compat-premium-product-viewer/tests/route-health.spec.ts +16 -0
  125. package/templates/three-compat-premium-product-viewer/tests/screenshot.spec.ts +15 -0
  126. package/templates/three-compat-premium-product-viewer/tsconfig.json +19 -0
@@ -0,0 +1,265 @@
1
+ /* Inspector / Properties — context panel for the current selection. */
2
+ import { Fragment, type ReactNode } from "react";
3
+ import { Icon } from "./Icon";
4
+ import { cap, fmt, shade } from "../state/util";
5
+ import type { EpisodeDocument, Selection } from "../state/types";
6
+
7
+ const STANDARD_CLIP_IDS = [
8
+ "idle", "talk", "gesture", "point", "nod", "wave",
9
+ "walk", "run", "react",
10
+ "sit", "shrug", "cross_arms", "salute", "shake_head"
11
+ ];
12
+
13
+ function Field({
14
+ label,
15
+ icon,
16
+ value,
17
+ mono,
18
+ act,
19
+ chev
20
+ }: {
21
+ label: string;
22
+ icon?: string;
23
+ value: ReactNode;
24
+ mono?: boolean;
25
+ act?: boolean;
26
+ chev?: boolean;
27
+ }) {
28
+ return (
29
+ <div className={"field" + (act ? " act" : "")}>
30
+ <div className="fl">
31
+ {icon && <Icon name={icon} size={11} />}
32
+ {label}
33
+ </div>
34
+ <div className={"fv" + (mono ? " mono" : "")}>
35
+ {value}
36
+ {chev && (
37
+ <span className="chev">
38
+ <Icon name="chevD" size={13} />
39
+ </span>
40
+ )}
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function Sec({ children }: { children: ReactNode }) {
47
+ return (
48
+ <div className="insp-sec">
49
+ {children}
50
+ <span className="ln" />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export interface InspectorProps {
56
+ data: EpisodeDocument;
57
+ sel: Selection | null;
58
+ }
59
+
60
+ export function Inspector({ data, sel }: InspectorProps) {
61
+ if (!sel) {
62
+ return (
63
+ <section className="panel insp">
64
+ <div className="panel-h">
65
+ <Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
66
+ <span className="ttl">Inspector</span>
67
+ </div>
68
+ <div className="insp-empty">
69
+ <Icon name="sliders" size={26} style={{ color: "var(--tx-faint)", opacity: 0.6 }} />
70
+ <div>Nothing selected.</div>
71
+ <div>Select a shot, character, set, or prop — or describe a scene in the Director Console.</div>
72
+ </div>
73
+ </section>
74
+ );
75
+ }
76
+ const typeLabel = ({ shot: "Shot", cast: "Character", set: "Set", prop: "Prop" } as Record<string, string>)[sel.type] || "—";
77
+ let body: ReactNode = null;
78
+
79
+ if (sel.type === "shot") {
80
+ const s = data.shots.find((x) => x.id === sel.id) || data.shots[0];
81
+ const idx = data.shots.indexOf(s) + 1;
82
+ const beats = data.beats.filter((b) => b.shot === s.id);
83
+ body = (
84
+ <Fragment>
85
+ <div className="insp-hero" style={{ backgroundImage: "url(" + s.frame + ")" }}>
86
+ <div className="ov" />
87
+ <div className="tag">{"SHOT " + String(idx).padStart(2, "0")}</div>
88
+ <div className="cap">
89
+ <div className="nm">{s.name}</div>
90
+ <div className="sub">{s.cam}</div>
91
+ </div>
92
+ </div>
93
+ <div className="prop-grid">
94
+ <Field label="Duration" icon="history" value={s.dur + "s"} mono />
95
+ <Field label="Lens" icon="camera" value={s.cam.split("·")[1] || s.cam} mono />
96
+ <Field label="In" value={fmt(s.start)} mono />
97
+ <Field label="Out" value={fmt(s.start + s.dur)} mono />
98
+ </div>
99
+ <div className="prop-grid one">
100
+ <Field label="Framing" icon="frame" value={s.cam.split("·")[0]} />
101
+ </div>
102
+ <Sec>Cast in shot</Sec>
103
+ <div className="castrow">
104
+ {s.who.map((id) => {
105
+ const c = data.cast.find((x) => x.id === id);
106
+ if (!c) return null;
107
+ return (
108
+ <div key={id} className="castchip">
109
+ <span className="d" style={{ background: c.color }}>
110
+ {c.glyph}
111
+ </span>
112
+ {c.name}
113
+ </div>
114
+ );
115
+ })}
116
+ </div>
117
+ <Sec>{beats.length + " beat" + (beats.length === 1 ? "" : "s") + " · director plan"}</Sec>
118
+ {beats.length ? (
119
+ <div className="beat-plan">
120
+ {beats.map((b) => {
121
+ const speaker = data.cast.find((x) => x.id === b.who);
122
+ const listener = data.cast.find((x) => x.id === b.listener);
123
+ return (
124
+ <div key={b.id} className="beat-row" data-testid="director-beat">
125
+ <div className="beat-line">“{cap(b.text)}”</div>
126
+ <div className="beat-intents">
127
+ <span className="beat-chip cam" title="camera framing">{b.camera}</span>
128
+ <span className="beat-chip spk" title="speaker action">
129
+ {(speaker?.name ?? b.who)} · {b.speakingIntent}
130
+ </span>
131
+ {b.listener && (
132
+ <span className="beat-chip lst" title="listener reaction">
133
+ {(listener?.name ?? b.listener)} · {b.listenerIntent}
134
+ </span>
135
+ )}
136
+ </div>
137
+ </div>
138
+ );
139
+ })}
140
+ </div>
141
+ ) : (
142
+ <div className="insp-note">No dialogue beats yet — direct one from the console.</div>
143
+ )}
144
+ </Fragment>
145
+ );
146
+ } else if (sel.type === "cast") {
147
+ const c = data.cast.find((x) => x.id === sel.id) || data.cast[0];
148
+ const appears = data.shots.filter((s) => s.who.includes(c.id)).length;
149
+ body = (
150
+ <Fragment>
151
+ <div className="insp-avatar" style={{ background: "linear-gradient(150deg," + c.color + "," + shade(c.color) + ")" }}>
152
+ {c.glyph}
153
+ </div>
154
+ <div className="insp-name">{c.name}</div>
155
+ <div className="insp-sub">{c.kind}</div>
156
+ <div className="prop-grid">
157
+ <Field label="Lines" icon="mic" value={c.lines || 0} mono />
158
+ <Field label="In shots" icon="film" value={appears} mono />
159
+ <Field
160
+ label="Accent"
161
+ value={
162
+ <span style={{ display: "flex", alignItems: "center", gap: 7 }}>
163
+ <span style={{ width: 13, height: 13, borderRadius: 4, background: c.color }} />
164
+ {c.color}
165
+ </span>
166
+ }
167
+ />
168
+ </div>
169
+ <div className="prop-grid one">
170
+ <Field
171
+ label="Default clip"
172
+ icon="film"
173
+ value={
174
+ <select
175
+ disabled
176
+ value={c.defaultClip ?? "idle"}
177
+ style={{
178
+ width: "100%",
179
+ background: "transparent",
180
+ color: "inherit",
181
+ border: "none",
182
+ fontFamily: "var(--mono)",
183
+ fontSize: 12,
184
+ cursor: "not-allowed"
185
+ }}
186
+ >
187
+ {STANDARD_CLIP_IDS.map((id) => (
188
+ <option key={id} value={id}>{id}</option>
189
+ ))}
190
+ </select>
191
+ }
192
+ />
193
+ </div>
194
+ <div className="prop-grid">
195
+ <Field label="Mouth morph" icon="mic" value={c.mouthMorphIndex ?? -1} mono />
196
+ <Field
197
+ label="Foot IK"
198
+ icon="zap"
199
+ value={
200
+ <input
201
+ type="checkbox"
202
+ disabled
203
+ checked={!!c.footIk}
204
+ style={{ cursor: "not-allowed" }}
205
+ />
206
+ }
207
+ />
208
+ </div>
209
+ <div className="prop-grid one">
210
+ <Field
211
+ label="Grade intent"
212
+ value={
213
+ <span className="beat-chip cam" style={{ fontSize: 10 }}>
214
+ {c.gradeAwareIntent ?? "—"}
215
+ </span>
216
+ }
217
+ />
218
+ </div>
219
+ <div className="insp-note">
220
+ {c.name + " is rigged and assignable to any beat. Type \"cast\" commands in the console to re-pose or re-cast."}
221
+ </div>
222
+ </Fragment>
223
+ );
224
+ } else {
225
+ const list = sel.type === "set" ? data.sets : data.props;
226
+ const o = list.find((x) => x.id === sel.id) || list[0];
227
+ body = (
228
+ <Fragment>
229
+ <div className="insp-avatar" style={{ background: "linear-gradient(150deg,#2b3350,#1a2032)", fontSize: 22 }}>
230
+ <Icon name={o.icon} size={26} style={{ color: "var(--acc)" }} />
231
+ </div>
232
+ <div className="insp-name">{o.name}</div>
233
+ <div className="insp-sub">{typeLabel + " · " + o.meta}</div>
234
+ <div className="prop-grid">
235
+ <Field label="Type" value={o.meta} mono />
236
+ </div>
237
+ <div className="insp-note">Linked into the working document. Re-light or swap from the console with “set …”.</div>
238
+ </Fragment>
239
+ );
240
+ }
241
+
242
+ return (
243
+ <section className="panel insp">
244
+ <div className="panel-h">
245
+ <Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
246
+ <span className="ttl">Inspector</span>
247
+ <span className="sp" />
248
+ <span
249
+ style={{
250
+ fontFamily: "var(--mono)",
251
+ fontSize: 10,
252
+ color: "var(--acc)",
253
+ background: "rgba(123,123,255,.12)",
254
+ border: "1px solid rgba(123,123,255,.25)",
255
+ padding: "2px 7px",
256
+ borderRadius: 6
257
+ }}
258
+ >
259
+ {typeLabel}
260
+ </span>
261
+ </div>
262
+ <div className="insp-scroll">{body}</div>
263
+ </section>
264
+ );
265
+ }
@@ -0,0 +1,210 @@
1
+ /* Scene Outliner — collapsible Shots / Cast / Sets / Props with counts + visibility. */
2
+ import { useState, type ReactNode } from "react";
3
+ import { Icon } from "./Icon";
4
+ import type { CastMember, CastSource, EpisodeDocument, PropEntity, Selection, SelectionType, SetEntity } from "../state/types";
5
+ import { fidelityLabel, type CharacterFidelity } from "../state/fidelity";
6
+
7
+ interface RowProps {
8
+ name: string;
9
+ sel: boolean;
10
+ onSelect: () => void;
11
+ hidden: boolean;
12
+ onToggle: () => void;
13
+ meta?: string;
14
+ accent?: string;
15
+ glyph?: string;
16
+ icon?: string;
17
+ /** Provenance badge (Phase E3): authored-fallback vs catalog-resolved vs user-uploaded. */
18
+ source?: CastSource;
19
+ sourceLabel?: string;
20
+ /** M7 — fidelity tier badge (A/B/C); C renders as "Previz". */
21
+ fidelity?: CharacterFidelity;
22
+ }
23
+
24
+ /** Short, honest badge text + class per cast provenance class (E3). */
25
+ const SOURCE_BADGE: Record<CastSource, { text: string; cls: string }> = {
26
+ "authored-fallback": { text: "fallback", cls: "src-fallback" },
27
+ "catalog-resolved": { text: "catalog", cls: "src-catalog" },
28
+ "user-uploaded": { text: "uploaded", cls: "src-uploaded" }
29
+ };
30
+
31
+ function Row({ name, sel, onSelect, hidden, onToggle, meta, accent, glyph, icon, source, sourceLabel, fidelity }: RowProps) {
32
+ const badge = source ? SOURCE_BADGE[source] : undefined;
33
+ return (
34
+ <div className={"ol-item" + (sel ? " sel" : "")} onClick={onSelect}>
35
+ {glyph ? (
36
+ <div className="av" style={{ background: accent }}>
37
+ {glyph}
38
+ </div>
39
+ ) : (
40
+ <div className="ic">
41
+ <Icon name={icon || "cube"} size={13} />
42
+ </div>
43
+ )}
44
+ <span className="nm">{name}</span>
45
+ {fidelity && (
46
+ <span
47
+ className={"fid-badge fid-" + fidelity.grade.toLowerCase() + (fidelity.previz ? " fid-previz" : "")}
48
+ title={fidelity.reason}
49
+ data-grade={fidelity.grade}
50
+ data-previz={fidelity.previz ? "true" : "false"}
51
+ >
52
+ {fidelity.previz ? "Previz" : fidelityLabel(fidelity.grade)}
53
+ </span>
54
+ )}
55
+ {badge && (
56
+ <span className={"src-badge " + badge.cls} title={sourceLabel ?? badge.text} data-source={source}>
57
+ {badge.text}
58
+ </span>
59
+ )}
60
+ {meta && <span className="meta">{meta}</span>}
61
+ <button
62
+ className={"vis" + (hidden ? " off" : "")}
63
+ title={hidden ? "Show" : "Hide"}
64
+ onClick={(e) => {
65
+ e.stopPropagation();
66
+ onToggle();
67
+ }}
68
+ >
69
+ <Icon name={hidden ? "eyeOff" : "eye"} size={14} />
70
+ </button>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function Group({
76
+ title,
77
+ count,
78
+ children,
79
+ onAdd
80
+ }: {
81
+ title: string;
82
+ count: number;
83
+ children: ReactNode;
84
+ onAdd?: () => void;
85
+ }) {
86
+ const [open, setOpen] = useState(true);
87
+ return (
88
+ <div className="ol-group">
89
+ <div className={"ol-gh" + (open ? "" : " closed")} onClick={() => setOpen(!open)}>
90
+ <span className="tw">
91
+ <Icon name="chevD" size={13} />
92
+ </span>
93
+ {title}
94
+ <span className="cnt">{count}</span>
95
+ {onAdd && (
96
+ <button
97
+ className="add"
98
+ onClick={(e) => {
99
+ e.stopPropagation();
100
+ onAdd();
101
+ }}
102
+ >
103
+ <Icon name="plus" size={13} />
104
+ </button>
105
+ )}
106
+ </div>
107
+ {open && <div>{children}</div>}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ export interface OutlinerProps {
113
+ data: EpisodeDocument;
114
+ sel: Selection | null;
115
+ onSelect: (type: SelectionType, id: string) => void;
116
+ hidden: Set<string>;
117
+ onToggle: (id: string) => void;
118
+ onAddCast: () => void;
119
+ onOpenPalette: () => void;
120
+ }
121
+
122
+ export function Outliner({ data, sel, onSelect, hidden, onToggle, onAddCast, onOpenPalette }: OutlinerProps) {
123
+ return (
124
+ <section className="panel col">
125
+ <div className="ol-search" onClick={onOpenPalette} style={{ cursor: "text" }}>
126
+ <Icon name="search" size={14} />
127
+ <input placeholder="Search scene…" readOnly style={{ cursor: "text" }} />
128
+ <kbd>⌘K</kbd>
129
+ </div>
130
+ <div className="ol-scroll">
131
+ {/* M7 — scene fidelity tier. A grade-C scene is labeled "previz" and never sold as finished. */}
132
+ <div
133
+ className={"scene-fidelity fid-" + data.fidelity.grade.toLowerCase() + (data.fidelity.previz ? " fid-previz" : "")}
134
+ title={data.fidelity.reason}
135
+ data-grade={data.fidelity.grade}
136
+ data-previz={data.fidelity.previz ? "true" : "false"}
137
+ >
138
+ <span className="fid-tier">{data.fidelity.previz ? "Previz" : fidelityLabel(data.fidelity.grade)}</span>
139
+ <span className="fid-note">
140
+ {data.fidelity.previz ? "preview-quality — not a finished render" : "scene fidelity"}
141
+ </span>
142
+ </div>
143
+ <Group title="Shots" count={data.shots.length}>
144
+ {data.shots.map((s) => (
145
+ <div
146
+ key={s.id}
147
+ className={"ol-item" + (sel?.type === "shot" && sel.id === s.id ? " sel" : "")}
148
+ onClick={() => onSelect("shot", s.id)}
149
+ >
150
+ <div className="ic" style={{ background: s.color + "33", color: s.color }}>
151
+ <Icon name="film" size={13} />
152
+ </div>
153
+ <span className="nm">{s.name}</span>
154
+ <span className="meta">{s.dur + "s"}</span>
155
+ </div>
156
+ ))}
157
+ </Group>
158
+
159
+ <Group title="Cast" count={data.cast.length} onAdd={onAddCast}>
160
+ {data.cast.map((c: CastMember) => (
161
+ <Row
162
+ key={c.id}
163
+ name={c.name}
164
+ glyph={c.glyph}
165
+ accent={c.color}
166
+ meta={c.lines ? c.lines + " lines" : "—"}
167
+ source={c.source}
168
+ sourceLabel={c.sourceLabel}
169
+ fidelity={c.fidelity}
170
+ sel={sel?.type === "cast" && sel.id === c.id}
171
+ onSelect={() => onSelect("cast", c.id)}
172
+ hidden={hidden.has(c.id)}
173
+ onToggle={() => onToggle(c.id)}
174
+ />
175
+ ))}
176
+ </Group>
177
+
178
+ <Group title="Sets" count={data.sets.length}>
179
+ {data.sets.map((s: SetEntity) => (
180
+ <Row
181
+ key={s.id}
182
+ name={s.name}
183
+ icon={s.icon}
184
+ meta={s.meta}
185
+ sel={sel?.type === "set" && sel.id === s.id}
186
+ onSelect={() => onSelect("set", s.id)}
187
+ hidden={hidden.has(s.id)}
188
+ onToggle={() => onToggle(s.id)}
189
+ />
190
+ ))}
191
+ </Group>
192
+
193
+ <Group title="Props" count={data.props.length}>
194
+ {data.props.map((p: PropEntity) => (
195
+ <Row
196
+ key={p.id}
197
+ name={p.name}
198
+ icon={p.icon}
199
+ meta={p.meta}
200
+ sel={sel?.type === "prop" && sel.id === p.id}
201
+ onSelect={() => onSelect("prop", p.id)}
202
+ hidden={hidden.has(p.id)}
203
+ onToggle={() => onToggle(p.id)}
204
+ />
205
+ ))}
206
+ </Group>
207
+ </div>
208
+ </section>
209
+ );
210
+ }
@@ -0,0 +1,204 @@
1
+ /* Command palette (⌘K) — jump to shots/cast, run Scene-Tool commands, switch views. */
2
+ import { useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
3
+ import { Icon } from "./Icon";
4
+ import { fmt } from "../state/util";
5
+ import type { ConsoleApi } from "./Console";
6
+ import type { EpisodeDocument, SelectionType, ViewMode } from "../state/types";
7
+
8
+ interface PaletteItem {
9
+ g: string;
10
+ nm: string;
11
+ ds?: string;
12
+ ic?: string;
13
+ av?: string;
14
+ glyph?: string;
15
+ tag?: string;
16
+ mono?: boolean;
17
+ kw?: string;
18
+ run: () => void;
19
+ }
20
+
21
+ export interface PaletteProps {
22
+ open: boolean;
23
+ setOpen: (o: boolean) => void;
24
+ data: EpisodeDocument;
25
+ api: MutableRefObject<ConsoleApi>;
26
+ onJumpShot: (id: string) => void;
27
+ onSelectEntity: (type: SelectionType, id: string) => void;
28
+ onRender: (scope: "shot" | "sequence") => void;
29
+ onView: (m: ViewMode) => void;
30
+ }
31
+
32
+ export function Palette({ open, setOpen, data, api, onJumpShot, onSelectEntity, onRender, onView }: PaletteProps) {
33
+ const [q, setQ] = useState("");
34
+ const [idx, setIdx] = useState(0);
35
+ const inputRef = useRef<HTMLInputElement>(null);
36
+ const listRef = useRef<HTMLDivElement>(null);
37
+
38
+ useEffect(() => {
39
+ if (open) {
40
+ setQ("");
41
+ setIdx(0);
42
+ window.setTimeout(() => inputRef.current?.focus(), 30);
43
+ }
44
+ }, [open]);
45
+
46
+ const items = useMemo<PaletteItem[]>(() => {
47
+ const out: PaletteItem[] = [];
48
+ out.push({ g: "Actions", nm: "Render full sequence", ds: "Low-fi preview of all shots", ic: "play2", kw: "render export preview", run: () => onRender("sequence") });
49
+ out.push({ g: "Actions", nm: "Render current shot", ds: "Preview just the active shot", ic: "film", kw: "render shot preview", run: () => onRender("shot") });
50
+ (["Render", "Wireframe", "Storyboard"] as ViewMode[]).forEach((m) =>
51
+ out.push({
52
+ g: "Actions",
53
+ nm: "View: " + m,
54
+ ds: "Switch viewport mode",
55
+ ic: m === "Render" ? "film" : m === "Wireframe" ? "cube" : "grid",
56
+ kw: "view mode " + m,
57
+ run: () => onView(m)
58
+ })
59
+ );
60
+ data.shots.forEach((s) =>
61
+ out.push({ g: "Go to shot", nm: s.name, ds: s.cam, ic: "film", tag: fmt(s.start), kw: "shot " + s.name + " " + s.cam, run: () => onJumpShot(s.id) })
62
+ );
63
+ data.cast.forEach((c) =>
64
+ out.push({ g: "Cast", av: c.color, glyph: c.glyph, nm: c.name, ds: c.kind, kw: "cast character " + c.name, run: () => onSelectEntity("cast", c.id) })
65
+ );
66
+ (
67
+ [
68
+ ["cast add Pip", "Cast a new character"],
69
+ ["shot retime --id shot-2 --duration 30", "Retime a shot + ripple"],
70
+ ["cam orbit", "Set active shot camera"],
71
+ ["fx add rim light", "Add an effect to the timeline"],
72
+ ["set station --hdr dawn", "Re-light the active set"],
73
+ ["shot add --after shot-2", "Block a new shot"]
74
+ ] as Array<[string, string]>
75
+ ).forEach(([cmd, ds]) =>
76
+ out.push({ g: "Run command", nm: cmd, ds, mono: true, ic: "bolt", kw: "command " + cmd, run: () => api.current.run?.(cmd, true) })
77
+ );
78
+ return out;
79
+ }, [data, api, onJumpShot, onRender, onSelectEntity, onView]);
80
+
81
+ const filtered = useMemo(() => {
82
+ const s = q.trim().toLowerCase();
83
+ if (!s) return items;
84
+ return items.filter((it) => (it.nm + " " + (it.ds || "") + " " + (it.kw || "")).toLowerCase().includes(s));
85
+ }, [q, items]);
86
+
87
+ useEffect(() => {
88
+ setIdx(0);
89
+ }, [q]);
90
+ useEffect(() => {
91
+ const el = listRef.current;
92
+ if (!el) return;
93
+ const on = el.querySelector(".cmdk-item.on");
94
+ if (on) on.scrollIntoView({ block: "nearest" });
95
+ }, [idx]);
96
+
97
+ if (!open) return null;
98
+
99
+ const exec = (it?: PaletteItem) => {
100
+ if (!it) return;
101
+ setOpen(false);
102
+ window.setTimeout(() => it.run(), 10);
103
+ };
104
+ const onKey = (e: React.KeyboardEvent) => {
105
+ if (e.key === "Escape") {
106
+ e.preventDefault();
107
+ setOpen(false);
108
+ } else if (e.key === "ArrowDown") {
109
+ e.preventDefault();
110
+ setIdx((i) => Math.min(filtered.length - 1, i + 1));
111
+ } else if (e.key === "ArrowUp") {
112
+ e.preventDefault();
113
+ setIdx((i) => Math.max(0, i - 1));
114
+ } else if (e.key === "Enter") {
115
+ e.preventDefault();
116
+ exec(filtered[idx]);
117
+ }
118
+ };
119
+
120
+ // group while keeping a flat index for keyboard navigation
121
+ let flat = -1;
122
+ let lastG: string | null = null;
123
+ const rows: Array<{ grp?: string; key: string; it?: PaletteItem; myIdx?: number }> = [];
124
+ filtered.forEach((it) => {
125
+ if (it.g !== lastG) {
126
+ rows.push({ grp: it.g, key: "g" + it.g });
127
+ lastG = it.g;
128
+ }
129
+ flat++;
130
+ rows.push({ it, myIdx: flat, key: it.g + it.nm });
131
+ });
132
+
133
+ return (
134
+ <div className="cmdk-ov" onMouseDown={() => setOpen(false)}>
135
+ <div className="cmdk" onMouseDown={(e) => e.stopPropagation()}>
136
+ <div className="cmdk-in">
137
+ <span className="si">
138
+ <Icon name="search" size={18} />
139
+ </span>
140
+ <input
141
+ ref={inputRef}
142
+ value={q}
143
+ placeholder="Search shots, cast, commands…"
144
+ onChange={(e) => setQ(e.target.value)}
145
+ onKeyDown={onKey}
146
+ />
147
+ <span className="esc">ESC</span>
148
+ </div>
149
+ <div className="cmdk-list" ref={listRef}>
150
+ {filtered.length === 0 ? (
151
+ <div className="cmdk-empty">{"No matches for “" + q + "”"}</div>
152
+ ) : (
153
+ rows.map((r) =>
154
+ r.grp ? (
155
+ <div className="cmdk-grp" key={r.key}>
156
+ {r.grp}
157
+ </div>
158
+ ) : (
159
+ <div
160
+ key={r.key}
161
+ className={"cmdk-item" + (r.myIdx === idx ? " on" : "")}
162
+ onMouseEnter={() => setIdx(r.myIdx!)}
163
+ onClick={() => exec(r.it)}
164
+ >
165
+ {r.it!.av ? (
166
+ <div className="av" style={{ background: r.it!.av }}>
167
+ {r.it!.glyph}
168
+ </div>
169
+ ) : (
170
+ <div className="ic">
171
+ <Icon name={r.it!.ic || "bolt"} size={15} />
172
+ </div>
173
+ )}
174
+ <div className="tx">
175
+ <div className="nm">{r.it!.nm}</div>
176
+ {r.it!.ds && <div className={"ds" + (r.it!.mono ? " mono" : "")}>{r.it!.ds}</div>}
177
+ </div>
178
+ {r.it!.tag ? (
179
+ <span className="tag">{r.it!.tag}</span>
180
+ ) : (
181
+ <span className="ent">
182
+ <Icon name="send" size={14} />
183
+ </span>
184
+ )}
185
+ </div>
186
+ )
187
+ )
188
+ )}
189
+ </div>
190
+ <div className="cmdk-foot">
191
+ <span>
192
+ <span className="kk">↑↓</span>navigate
193
+ </span>
194
+ <span>
195
+ <span className="kk">↵</span>run
196
+ </span>
197
+ <span>
198
+ <span className="kk">⌘K</span>toggle
199
+ </span>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ );
204
+ }