@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,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Dailies shot</title>
7
+ <script type="module" crossorigin src="/shot/assets/index-BBE6Ta9L.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/shot/assets/index-B7notge7.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@dailies-studio/shot-editor",
3
+ "version": "0.1.0",
4
+ "description": "The shot editor chrome: a prebuilt React app served static at /shot by the merged studio server, so user tooling can never break it. The 3D stage lives in src/stage and is served ONLY through the project's Vite (pinned three stack, project assets) via /shot-stage.html — never bundled into dist.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ "./stage": {
9
+ "types": "./src/stage/main.tsx",
10
+ "default": "./src/stage/main.tsx"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
17
+ "sideEffects": false,
18
+ "dependencies": {
19
+ "react": "^19.0.0",
20
+ "react-dom": "^19.0.0",
21
+ "@dailies-studio/contract": "0.1.0",
22
+ "@dailies-studio/engine": "0.1.0",
23
+ "@dailies-studio/shot": "0.1.0",
24
+ "@dailies-studio/runtime": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@react-three/fiber": "^9.6.1",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
30
+ "@types/three": "^0.185.1",
31
+ "@vitejs/plugin-react": "^4.5.0",
32
+ "jsdom": "^26.1.0",
33
+ "three": "^0.185.1",
34
+ "vite": "^6.3.5",
35
+ "@dailies-studio/agent-client": "0.1.0",
36
+ "@dailies-studio/transitions": "0.1.0",
37
+ "@dailies-studio/timeline": "0.1.0",
38
+ "@dailies-studio/ui": "0.1.0"
39
+ },
40
+ "scripts": {
41
+ "dev": "vite",
42
+ "build": "vite build",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }
package/src/api.ts ADDED
@@ -0,0 +1,289 @@
1
+ // The chrome's HTTP surface: /api/shots, plus the render verb.
2
+ //
3
+ // Every write carries the client id header, so the server's change event comes
4
+ // back labelled and this chrome can tell its own echo from someone else's write
5
+ // (see state/clientId.ts). Every write also carries `baseRev`, so a lost race
6
+ // is a 409 the author sees rather than a silent clobber.
7
+ import type { Doc3D, ExportFormat, ExportQualityId, RenderStats } from "@dailies-studio/shot/doc";
8
+ import type { Contract } from "@dailies-studio/contract";
9
+ import { createAgentClient } from "@dailies-studio/agent-client";
10
+ import type { AgentClient, AgentTransport } from "@dailies-studio/agent-client";
11
+ import { CLIENT_ID, CLIENT_ID_HEADER } from "./state/clientId.js";
12
+
13
+ export interface ShotListing {
14
+ name: string;
15
+ mtimeMs: number;
16
+ }
17
+
18
+ export interface LoadedShot {
19
+ name: string;
20
+ doc: Doc3D;
21
+ rev: string;
22
+ }
23
+
24
+ export interface SaveResult {
25
+ saved: true;
26
+ rev: string;
27
+ generated: string | null;
28
+ warning?: string;
29
+ }
30
+
31
+ /** A refused write, carrying enough for the chrome to say WHY. `conflict` is
32
+ * the 409 case specifically, because that one has a recovery ("reload and
33
+ * re-apply") the others do not. */
34
+ export class ApiError extends Error {
35
+ constructor(
36
+ readonly status: number,
37
+ message: string,
38
+ readonly reason?: string,
39
+ ) {
40
+ super(message);
41
+ this.name = "ApiError";
42
+ }
43
+ get conflict(): boolean {
44
+ return this.status === 409;
45
+ }
46
+ }
47
+
48
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
49
+ const res = await fetch(path, {
50
+ ...init,
51
+ headers: {
52
+ ...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
53
+ [CLIENT_ID_HEADER]: CLIENT_ID,
54
+ ...init?.headers,
55
+ },
56
+ });
57
+ const text = await res.text();
58
+ let body: unknown = null;
59
+ if (text.length > 0) {
60
+ try {
61
+ body = JSON.parse(text);
62
+ } catch {
63
+ // A non-JSON body from a route that owes JSON is itself the error.
64
+ throw new ApiError(res.status, text.slice(0, 400));
65
+ }
66
+ }
67
+ if (!res.ok) {
68
+ const b = body as { error?: unknown; reason?: unknown } | null;
69
+ throw new ApiError(
70
+ res.status,
71
+ typeof b?.error === "string" ? b.error : `${init?.method ?? "GET"} ${path} failed (${res.status})`,
72
+ typeof b?.reason === "string" ? b.reason : undefined,
73
+ );
74
+ }
75
+ return body as T;
76
+ }
77
+
78
+ export function listShots(): Promise<{ shots: ShotListing[] }> {
79
+ return request("/api/shots");
80
+ }
81
+
82
+ export function loadShot(name: string): Promise<LoadedShot> {
83
+ return request(`/api/shots/${encodeURIComponent(name)}`);
84
+ }
85
+
86
+ /** `baseRev` is REQUIRED at this seam even though the route allows it to be
87
+ * absent: an editor that omits it is an editor that can silently overwrite
88
+ * another surface's work, and `null` already means "I am claiming a create". */
89
+ export function saveShot(name: string, doc: Doc3D, baseRev: string | null): Promise<SaveResult> {
90
+ return request(`/api/shots/${encodeURIComponent(name)}`, {
91
+ method: "PUT",
92
+ body: JSON.stringify({ doc, baseRev }),
93
+ });
94
+ }
95
+
96
+ export function deleteShot(name: string): Promise<{ deleted: true }> {
97
+ return request(`/api/shots/${encodeURIComponent(name)}`, { method: "DELETE" });
98
+ }
99
+
100
+ export function renameShot(name: string, to: string): Promise<{ renamed: true; generated: string | null }> {
101
+ return request(`/api/shots/${encodeURIComponent(name)}/rename`, {
102
+ method: "POST",
103
+ body: JSON.stringify({ to }),
104
+ });
105
+ }
106
+
107
+ export function duplicateShot(name: string, to: string): Promise<{ duplicated: true }> {
108
+ return request(`/api/shots/${encodeURIComponent(name)}/duplicate`, {
109
+ method: "POST",
110
+ body: JSON.stringify({ to }),
111
+ });
112
+ }
113
+
114
+ // ---- the component registry (instance layers) --------------------------------
115
+
116
+ /**
117
+ * One row of `GET /api/components` — the live registry the ADD menu lists and
118
+ * the instance inspector judges an instance against.
119
+ *
120
+ * Declared here rather than imported from another chrome: only these fields are
121
+ * load-bearing for this lane, and the shot chrome depends on no other chrome.
122
+ *
123
+ * `host` is the instance gate (only "three" components can be instanced — a
124
+ * "dom" one under the three reconciler is a hard crash, and the SERVER's
125
+ * `classifyHost` is what decided the value). `contractHash` is the drift
126
+ * baseline recorded on the layer. `contractAvailability` is the server's own
127
+ * verdict on whether generated code can STATICALLY IMPORT the contract or
128
+ * whether the document must carry a snapshot — consumed verbatim, never
129
+ * re-derived, because re-deriving it chrome-side got builtins wrong in the 2D
130
+ * lane and emitted imports that cannot resolve.
131
+ */
132
+ export interface ComponentRow {
133
+ id: string;
134
+ name: string;
135
+ rel: string;
136
+ exportName: string;
137
+ contractHash: string;
138
+ contractSource: "declared" | "inferred";
139
+ contractAvailability?: "sibling" | "colocated" | "inferred";
140
+ host: "dom" | "three" | "dom-embedded-gl";
141
+ builtin?: boolean;
142
+ /**
143
+ * Present only on catalog EFFECTS, which is what makes it the filter for the
144
+ * material picker: a TSL generator is exactly a builtin that carries this.
145
+ *
146
+ * `thumb` is a pre-rendered PNG path under the server's own thumbs route —
147
+ * the reason the picker can show real renders without a single line of three
148
+ * in the chrome. `softwareRaster` badges a generator measured NOT to finish
149
+ * under `--gl swangle`, so an author learns before they pick rather than
150
+ * after a render has stalled.
151
+ */
152
+ effect?: {
153
+ category: string;
154
+ tags: string[];
155
+ thumb?: string;
156
+ softwareRaster?: "unfinished";
157
+ };
158
+ }
159
+
160
+ export interface ComponentDetail extends ComponentRow {
161
+ contract: Contract;
162
+ }
163
+
164
+ /** A bare ARRAY, not an envelope — the shape that route has always answered. */
165
+ export function listComponents(): Promise<ComponentRow[]> {
166
+ return request("/api/components");
167
+ }
168
+
169
+ export function componentDetail(id: string): Promise<ComponentDetail> {
170
+ return request(`/api/components/${encodeURIComponent(id)}`);
171
+ }
172
+
173
+ // ---- the asset store ---------------------------------------------------------
174
+
175
+ /** What `POST /api/assets` answers with. `path` is the BARE content-addressed
176
+ * filename; identical bytes always land on it, so a re-import is idempotent. */
177
+ export interface UploadedAsset {
178
+ path: string;
179
+ hash: string;
180
+ /** Absent for an extension the store does not recognise. */
181
+ kind?: string;
182
+ }
183
+
184
+ /**
185
+ * Upload one file into the shared asset store (the F2-C route every chrome
186
+ * uses; the shot lane is served by the same server, so it is the same route).
187
+ *
188
+ * The filename rides a URI-ENCODED header because HTTP headers are latin-1 and
189
+ * user filenames are not, and the BODY is the raw bytes — this is the one write
190
+ * in this module that is not JSON, so it overrides `request`'s content-type
191
+ * rather than letting it claim the bytes are JSON.
192
+ */
193
+ export function uploadAsset(file: File): Promise<UploadedAsset> {
194
+ return request("/api/assets", {
195
+ method: "POST",
196
+ body: file,
197
+ headers: {
198
+ "content-type": "application/octet-stream",
199
+ "x-studio-filename": encodeURIComponent(file.name),
200
+ },
201
+ });
202
+ }
203
+
204
+ /**
205
+ * The stored bytes of an asset.
206
+ *
207
+ * Raw, and deliberately not through `request` (which owes JSON). The audio lane
208
+ * reads the STORED WAV rather than the upload: the server transcodes every
209
+ * audio import to canonical 48 kHz PCM, so the original container's duration
210
+ * and the stored one's can differ by a partial frame, and the clip has to be
211
+ * timed against what will actually be mixed.
212
+ */
213
+ export async function fetchAssetBytes(path: string): Promise<ArrayBuffer> {
214
+ const url = `/studio/assets/${encodeURIComponent(path)}`;
215
+ const res = await fetch(url);
216
+ if (!res.ok) throw new ApiError(res.status, `GET ${url} -> ${res.status}`);
217
+ return res.arrayBuffer();
218
+ }
219
+
220
+ // ---- the export button -------------------------------------------------------
221
+
222
+ export interface RenderJob {
223
+ id: string;
224
+ name: string;
225
+ phase: "running" | "done" | "failed" | "cancelled";
226
+ done: number;
227
+ total: number;
228
+ out?: string;
229
+ error?: string;
230
+ elapsedMs: number;
231
+ }
232
+
233
+ export interface RenderRequest {
234
+ format: ExportFormat;
235
+ quality: ExportQualityId;
236
+ /** Absent = the whole timeline. */
237
+ sceneId?: string;
238
+ /** `false` renders a SILENT file even though the document has clips. Absent
239
+ * = the server's default, which is to mux whatever `doc.audio` carries. */
240
+ audio?: boolean;
241
+ }
242
+
243
+ export interface RenderState {
244
+ job: RenderJob | null;
245
+ recent: RenderJob[];
246
+ stats: RenderStats;
247
+ }
248
+
249
+ /**
250
+ * Start a render. Resolves as soon as the job EXISTS, not when it finishes —
251
+ * progress arrives on the SSE bus.
252
+ *
253
+ * A 409 means one is already running, and its `job` rides the error rather than
254
+ * being thrown away, so the dialog can show that instead of an error the author
255
+ * cannot act on.
256
+ */
257
+ export async function startRender(name: string, spec: RenderRequest): Promise<RenderJob> {
258
+ const body = await request<{ job: RenderJob }>(`/api/shots/${encodeURIComponent(name)}/render`, {
259
+ method: "POST",
260
+ body: JSON.stringify(spec),
261
+ });
262
+ return body.job;
263
+ }
264
+
265
+ export function renderState(name: string): Promise<RenderState> {
266
+ return request(`/api/shots/${encodeURIComponent(name)}/render`);
267
+ }
268
+
269
+ export function cancelRender(name: string): Promise<{ cancelled: boolean }> {
270
+ return request(`/api/shots/${encodeURIComponent(name)}/render`, { method: "DELETE" });
271
+ }
272
+
273
+ // ---- the agent lane ----------------------------------------------------------
274
+
275
+ /**
276
+ * The agent transport, shared with every other chrome.
277
+ *
278
+ * The shot chrome is ALWAYS served by the server it calls (there is no
279
+ * standalone shot lane and no hosted pairing), so this is plain same-origin
280
+ * fetch over this module's own error discipline. `createAgentClient` owns the
281
+ * routes, the SSE drain and the event vocabulary, so the four chromes cannot
282
+ * interpret one stream four different ways.
283
+ */
284
+ const agentTransport: AgentTransport = {
285
+ json: (path, init) => request(path, init),
286
+ raw: (path) => fetch(path),
287
+ };
288
+
289
+ export const agent: AgentClient = createAgentClient(agentTransport);