create-aura3d 1.3.1 → 1.3.2

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 (49) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.js +1 -1
  3. package/package.json +2 -2
  4. package/templates/animation-channel/package.json +1 -1
  5. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  6. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  7. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  8. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  9. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  10. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  11. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  12. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +2086 -6754
  13. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/broken.png +0 -0
  14. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/chefs.png +0 -0
  15. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/customer.png +0 -0
  16. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  17. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  18. package/templates/animation-studio/dist/scene/working.document.json +219 -348
  19. package/templates/animation-studio/package-lock.json +1307 -55
  20. package/templates/animation-studio/package.json +11 -5
  21. package/templates/animation-studio/studio/dist/assets/index-i8URxOOt.css +1 -0
  22. package/templates/animation-studio/studio/dist/assets/index-pC9hdZ-B.js +11 -0
  23. package/templates/animation-studio/studio/dist/index.html +19 -0
  24. package/templates/animation-studio/studio/index.html +18 -0
  25. package/templates/animation-studio/studio/src/App.tsx +430 -0
  26. package/templates/animation-studio/studio/src/components/Console.tsx +366 -0
  27. package/templates/animation-studio/studio/src/components/Icon.tsx +90 -0
  28. package/templates/animation-studio/studio/src/components/Inspector.tsx +209 -0
  29. package/templates/animation-studio/studio/src/components/Outliner.tsx +210 -0
  30. package/templates/animation-studio/studio/src/components/Palette.tsx +204 -0
  31. package/templates/animation-studio/studio/src/components/Stage.tsx +339 -0
  32. package/templates/animation-studio/studio/src/components/Timeline.tsx +183 -0
  33. package/templates/animation-studio/studio/src/components/Topbar.tsx +66 -0
  34. package/templates/animation-studio/studio/src/main.tsx +10 -0
  35. package/templates/animation-studio/studio/src/state/backend.ts +108 -0
  36. package/templates/animation-studio/studio/src/state/fidelity.ts +110 -0
  37. package/templates/animation-studio/studio/src/state/mapDocument.ts +419 -0
  38. package/templates/animation-studio/studio/src/state/sceneTool.ts +55 -0
  39. package/templates/animation-studio/studio/src/state/types.ts +165 -0
  40. package/templates/animation-studio/studio/src/state/util.ts +46 -0
  41. package/templates/animation-studio/studio/src/styles.css +528 -0
  42. package/templates/animation-studio/studio/vite.config.ts +175 -0
  43. package/templates/character-controller/package.json +3 -3
  44. package/templates/cinematic-scene/package.json +1 -1
  45. package/templates/episode-builder/package.json +1 -1
  46. package/templates/fighting-game/package.json +1 -1
  47. package/templates/mini-game/package.json +1 -1
  48. package/templates/product-viewer/package.json +1 -1
  49. package/templates/prompt-animation-channel/package.json +1 -1
@@ -0,0 +1,366 @@
1
+ /* Director Console (hero) — transcript + command engine + autocomplete. */
2
+ import { useEffect, useRef, useState, type MutableRefObject } from "react";
3
+ import { Icon } from "./Icon";
4
+ import { boldHtml, highlightArgs } from "../state/util";
5
+ import { VERBS, parse } from "../state/sceneTool";
6
+ import { parseCliResult, runSceneCommand } from "../state/backend";
7
+ import type { ComposerMode, CmdTurn, Turn } from "../state/types";
8
+
9
+ /** Imperative handle so the palette can run a command in the console. */
10
+ export interface ConsoleApi {
11
+ /** Run a command; `forceCommand` runs it as a raw scene-tool command regardless of mode. */
12
+ run?: (text: string, forceCommand?: boolean) => void;
13
+ }
14
+
15
+ const CHIPS = ["cast add Pip", "shot retime --id shot-2 --duration 30", "cam orbit", "fx add rim light"];
16
+
17
+ /** The portion of a command after its (possibly two-word) verb — the card's args line. */
18
+ function argTail(raw: string, verb: string): string {
19
+ const t = raw.trim();
20
+ return verb && t.toLowerCase().startsWith(verb.toLowerCase()) ? t.slice(verb.length).trim() : "";
21
+ }
22
+
23
+ function CmdCard({ turn }: { turn: CmdTurn }) {
24
+ return (
25
+ <div className="cmd">
26
+ <div className="cmd-top">
27
+ <span className="pr">aura</span>
28
+ <span style={{ color: "var(--tx-faint)" }}>›</span>
29
+ <span className="verb">{turn.verb}</span>
30
+ <span className="args" dangerouslySetInnerHTML={{ __html: highlightArgs(turn.args || "") }} />
31
+ {turn.state === "run" ? (
32
+ <span className="cmd-st st-run">
33
+ <span className="spin" />
34
+ validating
35
+ </span>
36
+ ) : turn.state === "bad" ? (
37
+ <span className="cmd-st st-bad">
38
+ <Icon name="x" size={11} />
39
+ rejected
40
+ </span>
41
+ ) : (
42
+ <span className="cmd-st st-ok">
43
+ <Icon name="check" size={11} />
44
+ committed
45
+ </span>
46
+ )}
47
+ </div>
48
+ {turn.state !== "run" && turn.diffs && (
49
+ <div className="cmd-body">
50
+ {turn.diffs.map((d, i) => (
51
+ <div className="diff" key={i}>
52
+ <span className={"op " + d.k}>{d.op}</span>
53
+ <span className="txt" dangerouslySetInnerHTML={{ __html: d.t }} />
54
+ </div>
55
+ ))}
56
+ </div>
57
+ )}
58
+ {turn.state === "ok" && (
59
+ <div className="cmd-foot">
60
+ <Icon name="history" size={13} />
61
+ {"ran in " + turn.dur}
62
+ <span className="commit">
63
+ <Icon name="check" size={12} />
64
+ {"doc @ " + turn.hash}
65
+ </span>
66
+ </div>
67
+ )}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function TurnView({ turn }: { turn: Turn }) {
73
+ if (turn.type === "you")
74
+ return (
75
+ <div className="turn you">
76
+ <div className="bub">{turn.text}</div>
77
+ </div>
78
+ );
79
+ if (turn.type === "dir")
80
+ return (
81
+ <div className="turn dir">
82
+ <div className="lbl">
83
+ <span className="mk">
84
+ <Icon name="sparkles" size={10} style={{ color: "#fff" }} />
85
+ </span>
86
+ Aura
87
+ </div>
88
+ <div className="think" dangerouslySetInnerHTML={{ __html: boldHtml(turn.think) }} />
89
+ </div>
90
+ );
91
+ if (turn.type === "cmd")
92
+ return (
93
+ <div className="turn dir">
94
+ <CmdCard turn={turn} />
95
+ </div>
96
+ );
97
+ if (turn.type === "render")
98
+ return (
99
+ <div className="turn dir">
100
+ <div className="rcard">
101
+ <div className="thumb" style={{ backgroundImage: "url(" + turn.frame + ")" }}>
102
+ <div className="badge">{turn.label}</div>
103
+ </div>
104
+ <div className="rmeta">
105
+ <Icon name="film" size={13} />
106
+ <b>{turn.shot}</b>
107
+ {"· " + turn.meta}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ return null;
113
+ }
114
+
115
+ export interface ConsoleProps {
116
+ transcript: Turn[];
117
+ setTranscript: React.Dispatch<React.SetStateAction<Turn[]>>;
118
+ selShot: string | null;
119
+ onRender: (scope: "shot" | "sequence") => void;
120
+ /** Called after a committed Scene-Tool mutation so the app re-syncs with the real document. */
121
+ onSceneCommit: () => void;
122
+ api: MutableRefObject<ConsoleApi>;
123
+ }
124
+
125
+ export function Console({ transcript, setTranscript, selShot, onRender, onSceneCommit, api }: ConsoleProps) {
126
+ const [mode, setMode] = useState<ComposerMode>("Prompt");
127
+ const [val, setVal] = useState("");
128
+ const [focus, setFocus] = useState(false);
129
+ const [acIdx, setAcIdx] = useState(0);
130
+ const scrollRef = useRef<HTMLDivElement>(null);
131
+ const inputRef = useRef<HTMLTextAreaElement>(null);
132
+
133
+ useEffect(() => {
134
+ const el = scrollRef.current;
135
+ if (el) el.scrollTop = el.scrollHeight;
136
+ }, [transcript]);
137
+
138
+ const suggestions = (() => {
139
+ const q = val.trim().toLowerCase();
140
+ if (!q) return VERBS.slice(0, 6);
141
+ return VERBS.filter((v) => v.verb.startsWith(q.split(" ")[0]) || v.verb.includes(q));
142
+ })();
143
+
144
+ const push = (t: Turn) => setTranscript((p) => [...p, t]);
145
+ const update = (id: string, patch: Partial<CmdTurn>) =>
146
+ setTranscript((p) => p.map((t) => (t.id === id && t.type === "cmd" ? { ...t, ...patch } : t)));
147
+
148
+ // Command mode runs the raw scene-tool command against the REAL CLI (POST /api/scene);
149
+ // Prompt mode shows the user's intent — their own coding agent drives the actual commands.
150
+ // `render`/`render --shot` short-circuits to the real render pipeline.
151
+ const run = (text: string, forceCommand = false) => {
152
+ const raw = text.trim();
153
+ if (!raw) return;
154
+ push({ type: "you", id: "u" + Date.now(), text: raw });
155
+ setVal("");
156
+ if (inputRef.current) inputRef.current.style.height = "auto";
157
+
158
+ const p = parse(raw);
159
+
160
+ // Render verb → the real render pipeline (also reachable from the Render button).
161
+ if (p.verb === "render") {
162
+ const cmdId = "c" + Date.now();
163
+ const scope = p.flags.shot ? "shot" : "sequence";
164
+ push({ type: "cmd", id: cmdId, verb: "render", args: p.flags.shot ? "--shot " + p.flags.shot : "--scope sequence", state: "run" });
165
+ onRender(scope);
166
+ update(cmdId, { state: "ok", diffs: [{ op: "~", k: "mod", t: "render queued · " + scope }], dur: "—", hash: "" });
167
+ return;
168
+ }
169
+
170
+ // Prompt mode: do NOT mutate. The directing agent (the user's coding agent) reads this
171
+ // intent and runs the concrete scene-tool commands itself. We only echo the intent.
172
+ // (The palette forces command execution, so it bypasses this.)
173
+ if (mode === "Prompt" && !forceCommand) {
174
+ window.setTimeout(
175
+ () =>
176
+ push({
177
+ type: "dir",
178
+ id: "d" + Date.now(),
179
+ think:
180
+ "Noted. Your coding agent is the director — it will translate this into validated **Scene-Tool** commands. " +
181
+ "Switch to **Command** mode to run a raw command here against the working document."
182
+ }),
183
+ 300
184
+ );
185
+ return;
186
+ }
187
+
188
+ // Command mode: run the REAL CLI and render the committed / rejected card.
189
+ const cmdId = "c" + Date.now();
190
+ push({ type: "cmd", id: cmdId, verb: p.verb || raw.split(/\s+/)[0], args: argTail(raw, p.verb), state: "run" });
191
+ void runSceneCommand(raw).then((res) => {
192
+ const { diffs } = parseCliResult(raw, res);
193
+ if (!res.ok || res.rejected) {
194
+ update(cmdId, { state: "bad", diffs });
195
+ return;
196
+ }
197
+ update(cmdId, { state: "ok", diffs, dur: res.ms ? (res.ms / 1000).toFixed(1) + "s" : "—", hash: res.hash || "—" });
198
+ // Re-sync the panels with the now-mutated working document.
199
+ onSceneCommit();
200
+ });
201
+ };
202
+
203
+ useEffect(() => {
204
+ api.current.run = run;
205
+ });
206
+
207
+ const onKey = (e: React.KeyboardEvent) => {
208
+ if (e.key === "Enter" && !e.shiftKey) {
209
+ e.preventDefault();
210
+ run(val);
211
+ setAcIdx(0);
212
+ return;
213
+ }
214
+ if (focus && suggestions.length) {
215
+ if (e.key === "ArrowDown") {
216
+ e.preventDefault();
217
+ setAcIdx((i) => Math.min(suggestions.length - 1, i + 1));
218
+ }
219
+ if (e.key === "ArrowUp") {
220
+ e.preventDefault();
221
+ setAcIdx((i) => Math.max(0, i - 1));
222
+ }
223
+ if (e.key === "Tab") {
224
+ e.preventDefault();
225
+ const s = suggestions[acIdx];
226
+ setVal(s.verb + " ");
227
+ inputRef.current?.focus();
228
+ }
229
+ }
230
+ };
231
+
232
+ const showAc = focus && mode === "Command" && suggestions.length > 0;
233
+
234
+ return (
235
+ <section className="panel col console">
236
+ <div className="cns-h">
237
+ <div className="cns-orb">
238
+ <Icon name="sparkles" size={15} style={{ color: "#fff" }} />
239
+ </div>
240
+ <div className="meta">
241
+ <div className="nm">Edit this scene</div>
242
+ <div className="sub">
243
+ <span className="d" />
244
+ ask the AI in plain English, or type an exact command
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <div className="cns-scroll" ref={scrollRef}>
250
+ {transcript.map((t) => (
251
+ <TurnView key={t.id} turn={t} />
252
+ ))}
253
+ </div>
254
+
255
+ <div className="composer">
256
+ <div className="chips">
257
+ {CHIPS.map((c) => (
258
+ <button
259
+ key={c}
260
+ className="chip"
261
+ onClick={() => {
262
+ setVal(c);
263
+ setMode("Command");
264
+ inputRef.current?.focus();
265
+ }}
266
+ >
267
+ <span className="pr">›</span>
268
+ {c.length > 22 ? c.slice(0, 22) + "…" : c}
269
+ </button>
270
+ ))}
271
+ </div>
272
+ <div className={"cbox" + (focus ? " focus" : "")}>
273
+ {showAc && (
274
+ <div className="ac">
275
+ <div className="ac-h">Scene-Tool commands</div>
276
+ {suggestions.map((s, i) => (
277
+ <div
278
+ key={s.verb}
279
+ className={"ac-item" + (i === acIdx ? " on" : "")}
280
+ onMouseEnter={() => setAcIdx(i)}
281
+ onMouseDown={(e) => {
282
+ e.preventDefault();
283
+ setVal(s.verb + " ");
284
+ inputRef.current?.focus();
285
+ }}
286
+ >
287
+ <span className="verb">
288
+ {s.verb} <span className="fl">{s.tail}</span>
289
+ </span>
290
+ <span className="desc">{s.desc}</span>
291
+ {i === acIdx && <kbd>tab</kbd>}
292
+ </div>
293
+ ))}
294
+ </div>
295
+ )}
296
+ <div className="cmode">
297
+ <div className="seg">
298
+ {(["Prompt", "Command"] as ComposerMode[]).map((m) => (
299
+ <button key={m} className={mode === m ? "on" : ""} onClick={() => setMode(m)}>
300
+ <Icon name={m === "Prompt" ? "wand" : "bolt"} size={12} />
301
+ {m === "Prompt" ? "Ask AI" : "Command"}
302
+ </button>
303
+ ))}
304
+ </div>
305
+ <span className="hint">{mode === "Prompt" ? "plain English · your AI agent runs it" : "exact command · runs now"}</span>
306
+ </div>
307
+ {mode === "Command" && (
308
+ <div className="cmd-list">
309
+ <div className="cmd-list-h">Top commands — click one to use it</div>
310
+ {VERBS.map((v) => (
311
+ <button
312
+ key={v.verb}
313
+ className="cmd-list-item"
314
+ onClick={() => {
315
+ setVal(v.verb + " ");
316
+ inputRef.current?.focus();
317
+ }}
318
+ >
319
+ <span className="verb">
320
+ {v.verb} <span className="fl">{v.tail}</span>
321
+ </span>
322
+ <span className="desc">{v.desc}</span>
323
+ </button>
324
+ ))}
325
+ </div>
326
+ )}
327
+ <textarea
328
+ ref={inputRef}
329
+ className={"cinput" + (mode === "Command" ? " mono" : "")}
330
+ rows={3}
331
+ value={val}
332
+ placeholder={
333
+ mode === "Prompt"
334
+ ? "Tell your AI agent what to change — e.g. “make the second line angrier and cut to a close-up” (it writes this down; your agent does it)"
335
+ : "Type an exact command — e.g. set space · cast add robot --name Pip · shot retime …"
336
+ }
337
+ onChange={(e) => {
338
+ setVal(e.target.value);
339
+ e.target.style.height = "auto";
340
+ e.target.style.height = Math.min(120, e.target.scrollHeight) + "px";
341
+ }}
342
+ onFocus={() => setFocus(true)}
343
+ onBlur={() => window.setTimeout(() => setFocus(false), 120)}
344
+ onKeyDown={onKey}
345
+ />
346
+ <div className="cbar">
347
+ <div className="sp" />
348
+ <button
349
+ className="btn btn-warm"
350
+ style={{ height: 30 }}
351
+ onClick={() => selShot && run("render --shot " + selShot)}
352
+ disabled={!selShot}
353
+ title="Render current shot"
354
+ >
355
+ <Icon name="play2" size={14} />
356
+ Render shot
357
+ </button>
358
+ <button className="send" disabled={!val.trim()} onClick={() => run(val)}>
359
+ <Icon name="send" size={16} />
360
+ </button>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </section>
365
+ );
366
+ }
@@ -0,0 +1,90 @@
1
+ /* Icon set — stroke icons, Lucide-ish. Ported from design `icons.jsx`. */
2
+ import type { CSSProperties } from "react";
3
+
4
+ const P: Record<string, string> = {
5
+ play: "M6 4l14 8-14 8z",
6
+ pause: "M7 4h4v16H7zM13 4h4v16h-4z",
7
+ prev: "M7 5v14M19 5l-9 7 9 7z",
8
+ next: "M17 5v14M5 5l9 7-9 7z",
9
+ search: "M11 19a8 8 0 100-16 8 8 0 000 16zM21 21l-4.3-4.3",
10
+ chevD: "M6 9l6 6 6-6",
11
+ chevR: "M9 6l6 6-6 6",
12
+ chevL: "M15 6l-6 6 6 6",
13
+ plus: "M12 5v14M5 12h14",
14
+ eye: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z@M12 12m-3 0a3 3 0 106 0 3 3 0 10-6 0",
15
+ eyeOff: "M3 3l18 18@M10.6 10.6a3 3 0 004.2 4.2@M9.4 5.2A10 10 0 0112 5c6.5 0 10 7 10 7a18 18 0 01-3 3.8M6.1 6.1A18 18 0 002 12s3.5 7 10 7a10 10 0 003.9-.8",
16
+ user: "M12 12a4 4 0 100-8 4 4 0 000 8zM4 21a8 8 0 0116 0",
17
+ film: "M3 4h18v16H3zM7 4v16M17 4v16M3 9h4M17 9h4M3 15h4M17 15h4",
18
+ globe: "M12 21a9 9 0 100-18 9 9 0 000 18zM3 12h18M12 3a14 14 0 010 18 14 14 0 010-18",
19
+ frame: "M4 7V4h3M20 7V4h-3M4 17v3h3M20 17v3h-3",
20
+ planet: "M12 16a5 5 0 100-10 5 5 0 000 10z@M4.5 14c-1.6.9-2.4 1.9-2 2.7.7 1.4 5 .8 9.6-1.4s7.9-5.2 7.2-6.6c-.4-.8-1.7-1-3.4-.7",
21
+ cube: "M12 2l8 4.5v9L12 20l-8-4.5v-9zM12 2v9M12 11l8-4.5M12 11l-8-4.5",
22
+ spark: "M12 3l1.8 5.4L19 10l-5.2 1.6L12 17l-1.8-5.4L5 10l5.2-1.6z",
23
+ layers: "M12 3l9 5-9 5-9-5zM3 13l9 5 9-5",
24
+ camera: "M3 8h3l1.5-2h9L18 8h3v11H3zM12 16a3 3 0 100-6 3 3 0 000 6z",
25
+ zap: "M13 2L4 14h7l-1 8 9-12h-7z",
26
+ sliders: "M4 7h10M18 7h2M4 17h2M10 17h10@M14 5v4M8 15v4",
27
+ render: "M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8",
28
+ send: "M4 12l16-8-6 16-3-6-7-2z",
29
+ bolt: "M11 2L4 13h6l-1 9 8-12h-6z",
30
+ mic: "M12 3a3 3 0 013 3v6a3 3 0 01-6 0V6a3 3 0 013-3zM5 11a7 7 0 0014 0M12 18v3",
31
+ image: "M3 4h18v16H3zM3 16l5-5 4 4 3-3 6 6M9 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3",
32
+ grid: "M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z",
33
+ expand: "M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5",
34
+ settings:
35
+ "M12 15a3 3 0 100-6 3 3 0 000 6z@M19 12a7 7 0 00-.1-1.3l2-1.5-2-3.4-2.3 1a7 7 0 00-2.3-1.3L16 2H8l-.3 2.2a7 7 0 00-2.3 1.3l-2.3-1-2 3.4 2 1.5a7 7 0 000 2.6l-2 1.5 2 3.4 2.3-1a7 7 0 002.3 1.3L8 22h8l.3-2.2a7 7 0 002.3-1.3l2.3 1 2-3.4-2-1.5A7 7 0 0019 12z",
36
+ check: "M5 12l5 5 9-11",
37
+ x: "M6 6l12 12M18 6L6 18",
38
+ play2: "M8 5v14l11-7z",
39
+ download: "M12 3v12M7 11l5 5 5-5M5 21h14",
40
+ share: "M4 12v8h16v-8M12 3v12M8 7l4-4 4 4",
41
+ history: "M12 8v4l3 2M3 12a9 9 0 109-9 9 9 0 00-8.5 6M3 4v3h3",
42
+ folder: "M3 6h6l2 2h10v11H3z",
43
+ sound: "M11 5L6 9H3v6h3l5 4zM16 9a4 4 0 010 6M19 6a8 8 0 010 12",
44
+ wand: "M5 19l9-9M14 6l1.5-1.5M18 10l1.5-.5M15 13l1 1M19 14l.5 1.5@M14 6l4 4",
45
+ move: "M12 3v18M3 12h18M9 6l3-3 3 3M9 18l3 3 3-3M6 9l-3 3 3 3M18 9l3 3-3 3",
46
+ sparkles: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5zM5 15l.8 2.2L8 18l-2.2.8L5 21l-.8-2.2L2 18l2.2-.8z"
47
+ };
48
+
49
+ export interface IconProps {
50
+ name: string;
51
+ size?: number;
52
+ sw?: number;
53
+ style?: CSSProperties;
54
+ className?: string;
55
+ }
56
+
57
+ export function Icon({ name, size = 16, sw = 1.8, style, className }: IconProps) {
58
+ const d = P[name];
59
+ if (!d) return null;
60
+ const parts = d.split("@");
61
+ return (
62
+ <svg
63
+ width={size}
64
+ height={size}
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth={sw}
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ style={style}
72
+ className={className}
73
+ >
74
+ {parts.map((p, i) => (
75
+ <path key={i} d={p} />
76
+ ))}
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ export function Logo({ size = 30 }: { size?: number }) {
82
+ return (
83
+ <div className="logo" style={{ width: size, height: size }}>
84
+ <svg width={size * 0.62} height={size * 0.62} viewBox="0 0 24 24" fill="none">
85
+ <path d="M12 2l9 16H3z" fill="none" stroke="#fff" strokeWidth={2} strokeLinejoin="round" opacity={0.95} />
86
+ <circle cx={12} cy={13} r={3} fill="#fff" />
87
+ </svg>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,209 @@
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
+ function Field({
8
+ label,
9
+ icon,
10
+ value,
11
+ mono,
12
+ act,
13
+ chev
14
+ }: {
15
+ label: string;
16
+ icon?: string;
17
+ value: ReactNode;
18
+ mono?: boolean;
19
+ act?: boolean;
20
+ chev?: boolean;
21
+ }) {
22
+ return (
23
+ <div className={"field" + (act ? " act" : "")}>
24
+ <div className="fl">
25
+ {icon && <Icon name={icon} size={11} />}
26
+ {label}
27
+ </div>
28
+ <div className={"fv" + (mono ? " mono" : "")}>
29
+ {value}
30
+ {chev && (
31
+ <span className="chev">
32
+ <Icon name="chevD" size={13} />
33
+ </span>
34
+ )}
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ function Sec({ children }: { children: ReactNode }) {
41
+ return (
42
+ <div className="insp-sec">
43
+ {children}
44
+ <span className="ln" />
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export interface InspectorProps {
50
+ data: EpisodeDocument;
51
+ sel: Selection | null;
52
+ }
53
+
54
+ export function Inspector({ data, sel }: InspectorProps) {
55
+ if (!sel) {
56
+ return (
57
+ <section className="panel insp">
58
+ <div className="panel-h">
59
+ <Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
60
+ <span className="ttl">Inspector</span>
61
+ </div>
62
+ <div className="insp-empty">
63
+ <Icon name="sliders" size={26} style={{ color: "var(--tx-faint)", opacity: 0.6 }} />
64
+ <div>Nothing selected.</div>
65
+ <div>Select a shot, character, set, or prop — or describe a scene in the Director Console.</div>
66
+ </div>
67
+ </section>
68
+ );
69
+ }
70
+ const typeLabel = ({ shot: "Shot", cast: "Character", set: "Set", prop: "Prop" } as Record<string, string>)[sel.type] || "—";
71
+ let body: ReactNode = null;
72
+
73
+ if (sel.type === "shot") {
74
+ const s = data.shots.find((x) => x.id === sel.id) || data.shots[0];
75
+ const idx = data.shots.indexOf(s) + 1;
76
+ const beats = data.beats.filter((b) => b.shot === s.id);
77
+ body = (
78
+ <Fragment>
79
+ <div className="insp-hero" style={{ backgroundImage: "url(" + s.frame + ")" }}>
80
+ <div className="ov" />
81
+ <div className="tag">{"SHOT " + String(idx).padStart(2, "0")}</div>
82
+ <div className="cap">
83
+ <div className="nm">{s.name}</div>
84
+ <div className="sub">{s.cam}</div>
85
+ </div>
86
+ </div>
87
+ <div className="prop-grid">
88
+ <Field label="Duration" icon="history" value={s.dur + "s"} mono />
89
+ <Field label="Lens" icon="camera" value={s.cam.split("·")[1] || s.cam} mono />
90
+ <Field label="In" value={fmt(s.start)} mono />
91
+ <Field label="Out" value={fmt(s.start + s.dur)} mono />
92
+ </div>
93
+ <div className="prop-grid one">
94
+ <Field label="Framing" icon="frame" value={s.cam.split("·")[0]} />
95
+ </div>
96
+ <Sec>Cast in shot</Sec>
97
+ <div className="castrow">
98
+ {s.who.map((id) => {
99
+ const c = data.cast.find((x) => x.id === id);
100
+ if (!c) return null;
101
+ return (
102
+ <div key={id} className="castchip">
103
+ <span className="d" style={{ background: c.color }}>
104
+ {c.glyph}
105
+ </span>
106
+ {c.name}
107
+ </div>
108
+ );
109
+ })}
110
+ </div>
111
+ <Sec>{beats.length + " beat" + (beats.length === 1 ? "" : "s") + " · director plan"}</Sec>
112
+ {beats.length ? (
113
+ <div className="beat-plan">
114
+ {beats.map((b) => {
115
+ const speaker = data.cast.find((x) => x.id === b.who);
116
+ const listener = data.cast.find((x) => x.id === b.listener);
117
+ return (
118
+ <div key={b.id} className="beat-row" data-testid="director-beat">
119
+ <div className="beat-line">“{cap(b.text)}”</div>
120
+ <div className="beat-intents">
121
+ <span className="beat-chip cam" title="camera framing">{b.camera}</span>
122
+ <span className="beat-chip spk" title="speaker action">
123
+ {(speaker?.name ?? b.who)} · {b.speakingIntent}
124
+ </span>
125
+ {b.listener && (
126
+ <span className="beat-chip lst" title="listener reaction">
127
+ {(listener?.name ?? b.listener)} · {b.listenerIntent}
128
+ </span>
129
+ )}
130
+ </div>
131
+ </div>
132
+ );
133
+ })}
134
+ </div>
135
+ ) : (
136
+ <div className="insp-note">No dialogue beats yet — direct one from the console.</div>
137
+ )}
138
+ </Fragment>
139
+ );
140
+ } else if (sel.type === "cast") {
141
+ const c = data.cast.find((x) => x.id === sel.id) || data.cast[0];
142
+ const appears = data.shots.filter((s) => s.who.includes(c.id)).length;
143
+ body = (
144
+ <Fragment>
145
+ <div className="insp-avatar" style={{ background: "linear-gradient(150deg," + c.color + "," + shade(c.color) + ")" }}>
146
+ {c.glyph}
147
+ </div>
148
+ <div className="insp-name">{c.name}</div>
149
+ <div className="insp-sub">{c.kind}</div>
150
+ <div className="prop-grid">
151
+ <Field label="Lines" icon="mic" value={c.lines || 0} mono />
152
+ <Field label="In shots" icon="film" value={appears} mono />
153
+ <Field
154
+ label="Accent"
155
+ value={
156
+ <span style={{ display: "flex", alignItems: "center", gap: 7 }}>
157
+ <span style={{ width: 13, height: 13, borderRadius: 4, background: c.color }} />
158
+ {c.color}
159
+ </span>
160
+ }
161
+ />
162
+ </div>
163
+ <div className="insp-note">
164
+ {c.name + " is rigged and assignable to any beat. Type “cast” commands in the console to re-pose or re-cast."}
165
+ </div>
166
+ </Fragment>
167
+ );
168
+ } else {
169
+ const list = sel.type === "set" ? data.sets : data.props;
170
+ const o = list.find((x) => x.id === sel.id) || list[0];
171
+ body = (
172
+ <Fragment>
173
+ <div className="insp-avatar" style={{ background: "linear-gradient(150deg,#2b3350,#1a2032)", fontSize: 22 }}>
174
+ <Icon name={o.icon} size={26} style={{ color: "var(--acc)" }} />
175
+ </div>
176
+ <div className="insp-name">{o.name}</div>
177
+ <div className="insp-sub">{typeLabel + " · " + o.meta}</div>
178
+ <div className="prop-grid">
179
+ <Field label="Type" value={o.meta} mono />
180
+ </div>
181
+ <div className="insp-note">Linked into the working document. Re-light or swap from the console with “set …”.</div>
182
+ </Fragment>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <section className="panel insp">
188
+ <div className="panel-h">
189
+ <Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
190
+ <span className="ttl">Inspector</span>
191
+ <span className="sp" />
192
+ <span
193
+ style={{
194
+ fontFamily: "var(--mono)",
195
+ fontSize: 10,
196
+ color: "var(--acc)",
197
+ background: "rgba(123,123,255,.12)",
198
+ border: "1px solid rgba(123,123,255,.25)",
199
+ padding: "2px 7px",
200
+ borderRadius: 6
201
+ }}
202
+ >
203
+ {typeLabel}
204
+ </span>
205
+ </div>
206
+ <div className="insp-scroll">{body}</div>
207
+ </section>
208
+ );
209
+ }