cozyclay 1.10.0 → 2.0.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 (272) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/live-hub.mjs +35 -93
  40. package/mcp/mesh-file.mjs +48 -0
  41. package/mcp/server.mjs +6 -77
  42. package/mcp/tool-handlers.mjs +219 -376
  43. package/package.json +2 -1
  44. package/src/App.jsx +1249 -9076
  45. package/src/analytics.js +393 -9
  46. package/src/app-context.js +211 -0
  47. package/src/app-stage.jsx +25 -24
  48. package/src/ardy/auto-fix-panel.css +108 -0
  49. package/src/ardy/fix-collisions.js +10 -66
  50. package/src/ardy/ik-drag.js +101 -0
  51. package/src/ardy/ik-key-json.js +43 -0
  52. package/src/ardy/ik.js +97 -67
  53. package/src/ardy/physics-panel.css +2 -0
  54. package/src/ardy/physics-panel.jsx +17 -5
  55. package/src/ardy/platform-fit-panel.css +64 -0
  56. package/src/ardy/platform-fit-panel.jsx +40 -0
  57. package/src/ardy/platform-fit.js +326 -0
  58. package/src/ardy/playback.js +166 -20
  59. package/src/ardy/range-pin.js +299 -0
  60. package/src/ardy/timeline-coordinates.js +26 -0
  61. package/src/ardy/timeline.css +1199 -0
  62. package/src/ardy/timeline.jsx +149 -41
  63. package/src/ardy/waypoints.js +2 -2
  64. package/src/asset-pane.css +462 -0
  65. package/src/asset-pane.jsx +461 -232
  66. package/src/command-bus.js +349 -0
  67. package/src/commands/ai.js +31 -0
  68. package/src/commands/cast.js +140 -0
  69. package/src/commands/elements/character.js +13 -0
  70. package/src/commands/elements/motion.js +14 -0
  71. package/src/commands/elements/object.js +22 -0
  72. package/src/commands/elements/scene.js +16 -0
  73. package/src/commands/elements/shot.js +9 -0
  74. package/src/commands/elements/stage.js +11 -0
  75. package/src/commands/elements.js +155 -0
  76. package/src/commands/export.js +20 -0
  77. package/src/commands/index.js +40 -0
  78. package/src/commands/motion.js +231 -0
  79. package/src/commands/objects.js +174 -0
  80. package/src/commands/project.js +53 -0
  81. package/src/commands/scene.js +81 -0
  82. package/src/commands/shared.js +19 -0
  83. package/src/commands/shot.js +103 -0
  84. package/src/commands/stage.js +17 -0
  85. package/src/commands/view.js +42 -0
  86. package/src/document-store.js +144 -0
  87. package/src/domains/cast.js +1007 -0
  88. package/src/domains/motion.js +3219 -0
  89. package/src/domains/objects.js +932 -0
  90. package/src/domains/scenes.js +817 -0
  91. package/src/domains/shots.js +404 -0
  92. package/src/domains/stage.js +93 -0
  93. package/src/dualview.jsx +113 -57
  94. package/src/facing-marks.js +3 -0
  95. package/src/first-success-guide.jsx +14 -14
  96. package/src/grid-view.js +18 -5
  97. package/src/hierarchy-panel.css +386 -0
  98. package/src/hierarchy-panel.jsx +88 -19
  99. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  100. package/src/i2v-motion-studio.jsx +180 -0
  101. package/src/ik-camera.js +3 -0
  102. package/src/main.jsx +6 -0
  103. package/src/motion/generation.js +105 -0
  104. package/src/motion-readiness-ui.jsx +4 -4
  105. package/src/motion-readiness.js +11 -0
  106. package/src/motion-trail.js +366 -9
  107. package/src/object-gizmo.jsx +7 -1
  108. package/src/panels/CameraPanel.jsx +60 -0
  109. package/src/panels/CharacterTransformPanel.jsx +48 -0
  110. package/src/panels/EnvironmentPanel.jsx +40 -0
  111. package/src/panels/Foldout.jsx +32 -0
  112. package/src/panels/LightPanel.jsx +24 -0
  113. package/src/panels/ObjectTransformPanel.jsx +554 -0
  114. package/src/panels/PosePanel.jsx +97 -0
  115. package/src/panels/ProjectPanel.jsx +33 -0
  116. package/src/panels/PromptBlocksPanel.jsx +442 -0
  117. package/src/panels/PropsPanel.jsx +70 -0
  118. package/src/panels/ReferenceImageField.jsx +91 -0
  119. package/src/panels/RigControlPanel.jsx +78 -0
  120. package/src/panels/RigPanel.jsx +37 -0
  121. package/src/panels/SubjectBox.jsx +47 -0
  122. package/src/panels/SubjectsPanel.jsx +39 -0
  123. package/src/panels/VideoCapturePanel.jsx +180 -0
  124. package/src/panels/details.css +1171 -0
  125. package/src/panels/motion.css +197 -0
  126. package/src/panels/pose.css +39 -0
  127. package/src/planview.jsx +60 -31
  128. package/src/posestudio.jsx +59 -4
  129. package/src/project-browser.css +1073 -0
  130. package/src/project-browser.jsx +284 -123
  131. package/src/range-pin-object-transform.js +19 -0
  132. package/src/range-pin-panel.css +518 -0
  133. package/src/range-pin-panel.jsx +344 -0
  134. package/src/result-modal.jsx +5 -5
  135. package/src/room.jsx +79 -51
  136. package/src/scenes.js +5 -0
  137. package/src/semantic-edit.js +2 -0
  138. package/src/settings-menu.jsx +39 -105
  139. package/src/shell/BottomDock.jsx +450 -0
  140. package/src/shell/DetailsSlot.jsx +513 -0
  141. package/src/shell/LibrarySlot.jsx +86 -0
  142. package/src/shell/MenuBar.jsx +601 -0
  143. package/src/shell/OutlinerSlot.jsx +50 -0
  144. package/src/shell/PreferencesDialog.jsx +538 -0
  145. package/src/shell/PreferencesSlot.jsx +40 -0
  146. package/src/shell/StatusBar.jsx +125 -0
  147. package/src/shell/StudioShell.jsx +92 -0
  148. package/src/shell/TopBar.jsx +129 -0
  149. package/src/shell/ViewportToolbar.jsx +656 -0
  150. package/src/shell/agent-glass.css +296 -0
  151. package/src/shell/dock.css +93 -0
  152. package/src/shell/glass-regions.css +985 -0
  153. package/src/shell/glass.css +687 -0
  154. package/src/shell/log-store.js +52 -0
  155. package/src/shell/mode.css +9 -0
  156. package/src/shell/preferences.css +651 -0
  157. package/src/shell/shell.css +282 -0
  158. package/src/shell/studio-shell-context.js +19 -0
  159. package/src/shell/topbar.css +435 -0
  160. package/src/shell/viewport.css +451 -0
  161. package/src/store/authored-intent.js +22 -0
  162. package/src/store/runtime-adapters.js +92 -0
  163. package/src/store/scene-stage.js +21 -0
  164. package/src/store/use-document-store.js +14 -0
  165. package/src/studio-actions.js +210 -0
  166. package/src/studio-agent-commands.js +38 -268
  167. package/src/studio-agent-context.js +38 -9
  168. package/src/studio-agent-motion.js +122 -378
  169. package/src/studio-agent-protocol.js +66 -30
  170. package/src/studio-app-binding.js +379 -0
  171. package/src/studio-contact-sheet.js +73 -0
  172. package/src/studio-elements.js +58 -51
  173. package/src/styles/themes.css +200 -0
  174. package/src/styles/tokens.css +69 -0
  175. package/src/styles.css +443 -1753
  176. package/src/theme.js +44 -0
  177. package/src/timeline-extent.js +16 -0
  178. package/src/trail-key-conflicts.js +39 -0
  179. package/src/trail-pick.js +59 -0
  180. package/src/ui.jsx +8 -8
  181. package/src/use-case-question.jsx +66 -0
  182. package/src/workflow/AgentPanel.jsx +82 -69
  183. package/src/workflow/agent-client.js +12 -2
  184. package/src/workflow/agent-panel.css +34 -32
  185. package/src/workflow/cozy-scene-node.css +2 -0
  186. package/src/workflow/workflow.css +2 -0
  187. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  188. package/tools/ardy/bridge.mjs +164 -146
  189. package/tools/ardy/visual-qa.mjs +5 -5
  190. package/tools/bench/EXP3.md +61 -0
  191. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  192. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  193. package/tools/bench/cclay_bench_runner.py +42 -0
  194. package/tools/bench/cube-contact.mjs +162 -0
  195. package/tools/bench/exp3.mjs +152 -0
  196. package/tools/bench/extract-bench-lib.mjs +216 -0
  197. package/tools/bench/extract-bench.mjs +302 -0
  198. package/tools/bench/fal-generate.mjs +88 -0
  199. package/tools/bench/fit/README.md +189 -0
  200. package/tools/bench/fit/camera.mjs +59 -0
  201. package/tools/bench/fit/contact.mjs +419 -0
  202. package/tools/bench/fit/footlock.mjs +234 -0
  203. package/tools/bench/fit/motion.mjs +85 -0
  204. package/tools/bench/fit/pin.mjs +27 -0
  205. package/tools/bench/fit/remote.mjs +105 -0
  206. package/tools/bench/fit-bench.mjs +126 -0
  207. package/tools/bench/fit-sanity.mjs +45 -0
  208. package/tools/bench/metrics.mjs +313 -0
  209. package/tools/bench/obs/depth.mjs +258 -0
  210. package/tools/bench/obs/extrinsics.mjs +175 -0
  211. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  212. package/tools/bench/obs/ground.mjs +294 -0
  213. package/tools/bench/obs/heading.mjs +151 -0
  214. package/tools/bench/obs/ladder.mjs +555 -0
  215. package/tools/bench/obs/mannequin-betas.json +124 -0
  216. package/tools/bench/obs/remote.mjs +83 -0
  217. package/tools/bench/obs/rest_joints.py +52 -0
  218. package/tools/bench/obs/ybot-targets.mjs +49 -0
  219. package/tools/bench/obs-bench.mjs +520 -0
  220. package/tools/bench/score.mjs +343 -0
  221. package/tools/bench/summarize.mjs +86 -0
  222. package/tools/dev/pages/privacy.html +13 -8
  223. package/tools/gt-render/browser.mjs +159 -0
  224. package/tools/gt-render/camera-math.mjs +276 -0
  225. package/tools/gt-render/page.mjs +267 -0
  226. package/tools/gt-render/render.mjs +514 -0
  227. package/tools/gt-render/scene-box.mjs +31 -0
  228. package/tools/gt-render/take-transform.mjs +87 -0
  229. package/tools/qa/css-rule-usage.mjs +283 -0
  230. package/tools/qa/studio-control-count.mjs +70 -32
  231. package/tools/run-tests.mjs +58 -3
  232. package/tools/track/DESIGN.md +513 -0
  233. package/tools/track/backfill-provenance.mjs +116 -0
  234. package/tools/track/budget.mjs +8 -0
  235. package/tools/track/check-rig.mjs +209 -0
  236. package/tools/track/diagnostics.schema.json +60 -0
  237. package/tools/track/export-rig.mjs +256 -0
  238. package/tools/track/fallback.mjs +7 -0
  239. package/tools/track/fk-parity-fixture.mjs +161 -0
  240. package/tools/track/gate.mjs +349 -0
  241. package/tools/track/masks.mjs +318 -0
  242. package/tools/track/metrics.mjs +201 -0
  243. package/tools/track/publish-obs.mjs +108 -0
  244. package/tools/track/py/check_env.py +61 -0
  245. package/tools/track/py/eval_lr.py +229 -0
  246. package/tools/track/py/lr_viterbi.py +294 -0
  247. package/tools/track/py/masks.py +405 -0
  248. package/tools/track/py/objective.py +382 -0
  249. package/tools/track/py/rig.py +508 -0
  250. package/tools/track/py/scene.py +326 -0
  251. package/tools/track/py/test_joint_indices.py +123 -0
  252. package/tools/track/py/test_lr_viterbi.py +195 -0
  253. package/tools/track/py/test_masks.py +229 -0
  254. package/tools/track/py/test_rig.py +288 -0
  255. package/tools/track/py/test_scene.py +354 -0
  256. package/tools/track/py/test_track.py +328 -0
  257. package/tools/track/py/track.py +411 -0
  258. package/tools/track/remote.mjs +140 -0
  259. package/tools/track/rig-dump.mjs +227 -0
  260. package/tools/track/run-box-tests.mjs +14 -0
  261. package/tools/track/run-box.mjs +168 -0
  262. package/tools/track/setup-box.sh +59 -0
  263. package/tools/track/study-2d.mjs +728 -0
  264. package/dist/assets/analytics-B1hnH66c.js +0 -1
  265. package/dist/assets/app-Cu1cexZR.css +0 -1
  266. package/dist/assets/app-Dxdh8kds.js +0 -4861
  267. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  268. package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
  269. package/dist/assets/workflow-CJd-PDtV.js +0 -179
  270. package/dist/assets/workflow-DsudxKHF.css +0 -1
  271. package/src/fal-motion-studio.jsx +0 -180
  272. package/src/scene-history.js +0 -129
@@ -0,0 +1,125 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import SourceOffer from "../source-offer.jsx";
3
+ import { useStudioShell } from "./studio-shell-context.js";
4
+ import { ko } from "../locale.js";
5
+ import "./topbar.css";
6
+
7
+ const BUSY = ["preparing", "encoding", "finalizing"];
8
+
9
+ // 2a status bar: status text on the left (export progress, retry and cancel
10
+ // take that slot while an export runs, G7); fps and the source offer on the
11
+ // right. #570: the save state sits under the project name (ProjectHead).
12
+ // In the studio shell there is no standing row: a fixed "Ready", the fps the
13
+ // Sequencer already shows and a source link Help › About already carries were
14
+ // a band of noise under the dock. Only a live status (an export, or a status
15
+ // message) shows, as a capsule floating in the viewport's bottom-left corner.
16
+ // Embeds have no menu bar, so they keep the full row and its source offer.
17
+ export default function StatusBar({ embedded = false }) {
18
+ const { statusText, exportStatus, exportPhaseLabel, resultOpen, recState, tlFps } = useStudioShell();
19
+ // A frame export's status is already shown inside its result dialog.
20
+ const exportShown = exportStatus && !(resultOpen && exportStatus.kind === "frame");
21
+ if (!embedded) {
22
+ if (!exportShown && !statusText) return null;
23
+ return (
24
+ <footer className="brandbar v2-statusbar statusbar-float">
25
+ {exportShown
26
+ ? <ExportStatus status={exportStatus} label={exportPhaseLabel} recording={recState === "recording"} />
27
+ : <span className="statusbar-text">{statusText}</span>}
28
+ </footer>
29
+ );
30
+ }
31
+ return (
32
+ <footer className="brandbar v2-statusbar">
33
+ <div className="statusbar-left">
34
+ {exportShown
35
+ ? <ExportStatus status={exportStatus} label={exportPhaseLabel} recording={recState === "recording"} />
36
+ : <span className="statusbar-text">{statusText || ko("Ready", "준비")}</span>}
37
+ </div>
38
+ <span className="statusbar-fps" title={ko("Timeline frame rate", "타임라인 프레임 레이트")}>{tlFps} fps</span>
39
+ <SourceOffer />
40
+ </footer>
41
+ );
42
+ }
43
+
44
+ function ExportStatus({ status, label, recording }) {
45
+ const { phase, kind, message, completedFrames, frameCount, stage, cancellable, retryable, handedOff, cancel, retry } = status;
46
+ const busy = BUSY.includes(phase);
47
+ const detail = [
48
+ message,
49
+ retryable && ko("Retry keeps the original shot, camera, range and settings; it does not change your edits.", "다시 시도하면 원래 샷·카메라·범위·설정을 사용하며 편집 내용은 바꾸지 않아요."),
50
+ handedOff > 0 && ko(`${handedOff} file(s) already handed off. Retry skips those downloads.`, `파일 ${handedOff}개는 이미 전달했어요. 다시 시도할 때 해당 다운로드는 건너뛰어요.`),
51
+ busy && stage === "mux" && ko("Finalizing MP4; this stage cannot be interrupted.", "MP4 마무리 중이에요. 이 단계는 중단할 수 없어요."),
52
+ ].filter(Boolean).join(" ");
53
+ return (
54
+ <section
55
+ className="export-status statusbar-export"
56
+ data-testid="export-status"
57
+ data-phase={phase}
58
+ data-kind={kind}
59
+ role="status"
60
+ aria-live="polite"
61
+ aria-atomic="true"
62
+ title={detail || undefined}
63
+ >
64
+ <strong className="statusbar-export-phase">{label(phase)}{status.label ? ` · ${status.label}` : ""}</strong>
65
+ {busy && frameCount > 0 && (
66
+ <>
67
+ <progress aria-label={label(phase)} max={frameCount} value={completedFrames} />
68
+ <span className="statusbar-export-count">{completedFrames}/{frameCount}</span>
69
+ </>
70
+ )}
71
+ {!busy && message && <span className="statusbar-export-message">{message}</span>}
72
+ {busy && cancellable && (
73
+ <button type="button" className="statusbar-action" data-testid="export-cancel" onClick={cancel}>
74
+ {ko("Cancel", "취소")}
75
+ </button>
76
+ )}
77
+ {retryable && (
78
+ <button type="button" className="statusbar-action" data-testid="export-retry" disabled={recording} onClick={() => void retry()}>
79
+ {ko("Retry", "재시도")}
80
+ </button>
81
+ )}
82
+ </section>
83
+ );
84
+ }
85
+
86
+ function relativeTime(ms) {
87
+ const minutes = Math.floor(ms / 60000);
88
+ if (minutes < 1) return ko("just now", "방금");
89
+ if (minutes < 60) return ko(`${minutes} min ago`, `${minutes}분 전`);
90
+ const hours = Math.floor(minutes / 60);
91
+ return ko(`${hours} h ago`, `${hours}시간 전`);
92
+ }
93
+
94
+ // "Saved 2 min ago" once a save completes in this session; otherwise the
95
+ // project's own save state (Unsaved changes, Saving…, Save failed, Not saved).
96
+ export function SaveState({ status }) {
97
+ const [savedAt, setSavedAt] = useState(null);
98
+ const [now, setNow] = useState(() => Date.now());
99
+ const previous = useRef(status.state);
100
+ useEffect(() => {
101
+ if (previous.current === "saving" && status.state === "saved") setSavedAt(Date.now());
102
+ if (status.dirty) setSavedAt(null);
103
+ previous.current = status.state;
104
+ }, [status.state, status.dirty]);
105
+ useEffect(() => {
106
+ if (savedAt === null) return undefined;
107
+ setNow(Date.now());
108
+ const timer = setInterval(() => setNow(Date.now()), 30000);
109
+ return () => clearInterval(timer);
110
+ }, [savedAt]);
111
+ const text = savedAt !== null && status.state === "saved" && !status.dirty
112
+ ? ko(`Saved ${relativeTime(now - savedAt)}`, `저장됨 · ${relativeTime(now - savedAt)}`)
113
+ : status.text;
114
+ return (
115
+ <span
116
+ className={"project-save-status status-" + status.state}
117
+ data-testid="project-save-status"
118
+ data-dirty={status.dirty ? "true" : undefined}
119
+ role="status"
120
+ aria-live="polite"
121
+ >
122
+ {text}
123
+ </span>
124
+ );
125
+ }
@@ -0,0 +1,92 @@
1
+ import { useState } from "react";
2
+ import { ko } from "../locale.js";
3
+ import { useStudioShell } from "./studio-shell-context.js";
4
+ import TopBar, { ProjectHead } from "./TopBar.jsx";
5
+ import OutlinerSlot from "./OutlinerSlot.jsx";
6
+ import DetailsSlot from "./DetailsSlot.jsx";
7
+ import BottomDock from "./BottomDock.jsx";
8
+ import StatusBar from "./StatusBar.jsx";
9
+ import PreferencesSlot from "./PreferencesSlot.jsx";
10
+ import "./shell.css";
11
+ import "./glass.css";
12
+ import "./glass-regions.css";
13
+ import "./agent-glass.css";
14
+
15
+ const DOCK_TAB_KEY = "cozyclay.dock.tab.v1";
16
+
17
+ function readDockTab() {
18
+ try {
19
+ return globalThis.localStorage?.getItem(DOCK_TAB_KEY) === "assets" ? "assets" : "animation";
20
+ } catch {
21
+ return "animation";
22
+ }
23
+ }
24
+
25
+ // #570 floating-glass shell: the viewport fills the window and every region
26
+ // floats over it. Left: the Outliner. Right: Details over Agent. Bottom, from
27
+ // the left edge to the right column: the dock, switching between Animation
28
+ // (the Sequencer with the shot preview at its right end) and Assets.
29
+ export default function StudioShell({ viewport, children, ...props }) {
30
+ const { beginWorkspaceResize } = useStudioShell();
31
+ const [dockTab, setDockTab] = useState(readDockTab);
32
+ // Embeds (playview, playground) keep the Sequencer-only dock.
33
+ const embedded = Boolean(props["data-embed-mode"]);
34
+ const shownTab = embedded ? "animation" : dockTab;
35
+ function changeDockTab(next) {
36
+ setDockTab(next);
37
+ try {
38
+ globalThis.localStorage?.setItem(DOCK_TAB_KEY, next);
39
+ } catch (error) {
40
+ console.warn(`[cozyclay] could not store ${DOCK_TAB_KEY}`, error);
41
+ }
42
+ }
43
+ return (
44
+ <div {...props} data-shell="glass" data-dock-tab={shownTab}>
45
+ <TopBar preferences={<PreferencesSlot />} />
46
+ <div className="main">
47
+ <div className="workspace">
48
+ {viewport}
49
+ <div className="studio-left-column">
50
+ <ProjectHead />
51
+ <OutlinerSlot />
52
+ </div>
53
+ <div
54
+ className="workspace-splitter shell-splitter shell-left-splitter"
55
+ role="separator"
56
+ aria-orientation="vertical"
57
+ aria-label={ko("Resize hierarchy panel", "계층 패널 크기 조절")}
58
+ onPointerDown={(event) => beginWorkspaceResize("left", event)}
59
+ />
60
+ <div className="studio-right-column">
61
+ <DetailsSlot />
62
+ </div>
63
+ <div
64
+ className="workspace-splitter workspace-splitter-vertical shell-splitter shell-sidebar-splitter"
65
+ role="separator"
66
+ aria-orientation="vertical"
67
+ aria-label={ko("Resize hierarchy and inspector panel", "계층 및 속성 패널 크기 조절")}
68
+ onPointerDown={(event) => beginWorkspaceResize("sidebar", event)}
69
+ />
70
+ </div>
71
+ <div className="studio-dock-slot">
72
+ <div
73
+ className="workspace-splitter timeline-splitter"
74
+ role="separator"
75
+ aria-label={ko("Resize frame monitor", "프레임 모니터 크기 조절")}
76
+ onPointerDown={(event) => beginWorkspaceResize("timeline", event)}
77
+ />
78
+ <BottomDock tab={shownTab} onTabChange={changeDockTab} embedded={embedded} />
79
+ <div
80
+ className="workspace-splitter shell-splitter shell-camera-splitter"
81
+ role="separator"
82
+ aria-orientation="vertical"
83
+ aria-label={ko("Resize shot preview", "샷 미리보기 크기 조절")}
84
+ onPointerDown={(event) => beginWorkspaceResize("camera", event)}
85
+ />
86
+ </div>
87
+ </div>
88
+ <StatusBar embedded={embedded} />
89
+ {children}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,129 @@
1
+ import { useRef, useState } from "react";
2
+ import { useStudioShell } from "./studio-shell-context.js";
3
+ import ProjectPanel from "../panels/ProjectPanel.jsx";
4
+ import MenuBar, { MenuPopover } from "./MenuBar.jsx";
5
+ import { SaveState } from "./StatusBar.jsx";
6
+ import { ko, isKo } from "../locale.js";
7
+ import "./topbar.css";
8
+
9
+ // 2a top bar: File/Edit/Window/Help, then only the MCP state and the one
10
+ // primary action. Save and Export live in File (G7). #570: the project name
11
+ // and its save state head the left column (ProjectHead below).
12
+ export default function TopBar({ preferences }) {
13
+ const { recState, liveHubStatus, liveWorkspaceHandle } = useStudioShell();
14
+ const connected = liveHubStatus === "connected";
15
+ return (
16
+ <header className="topbar v2-topbar" data-rec-state={recState}>
17
+ <MenuBar preferences={preferences} />
18
+ <div className="topbar-spacer" />
19
+ <span
20
+ className={"topbar-mcp" + (liveWorkspaceHandle ? " live-workspace-handle" : "")}
21
+ data-state={connected ? "connected" : "disconnected"}
22
+ data-live-workspace={liveWorkspaceHandle || undefined}
23
+ title={liveWorkspaceHandle || ko("No MCP client is attached to this studio", "이 스튜디오에 연결된 MCP 클라이언트가 없어요")}
24
+ >
25
+ <i className="topbar-mcp-dot" aria-hidden="true" />
26
+ <span className="topbar-mcp-text">{connected ? ko("MCP connected", "MCP 연결됨") : ko("MCP offline", "MCP 꺼짐")}</span>
27
+ </span>
28
+ <GenerateMotion />
29
+ </header>
30
+ );
31
+ }
32
+
33
+ // #570: the project heads the left column — its name opens the project menu,
34
+ // the line under it is the save state.
35
+ export function ProjectHead() {
36
+ const {
37
+ projectMenuOpen, setProjectMenuOpen, projectDirty, projectName, projectStartupOpen,
38
+ projectManifest, saveStatus,
39
+ } = useStudioShell();
40
+ return (
41
+ <div className="topbar-project studio-project-head">
42
+ <ProjectPanel
43
+ projectMenuOpen={projectMenuOpen}
44
+ setProjectMenuOpen={setProjectMenuOpen}
45
+ projectDirty={projectDirty}
46
+ projectName={projectName}
47
+ projectStartupOpen={projectStartupOpen}
48
+ projectManifest={projectManifest}
49
+ />
50
+ <SaveState status={saveStatus} />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ // "Generate Motion" is today's Scene generate (G6) and the one home for
56
+ // generation (IS-2): with authored prompt blocks it runs them all, as the
57
+ // removed Details "Generate all blocks" did; otherwise it runs the prompt.
58
+ // The caret holds the take bar's Scene actions; a refused action says why in
59
+ // place and in a toast (R3).
60
+ function GenerateMotion() {
61
+ const {
62
+ runArdy, sceneGenerateDisabledReason, sceneAgainDisabledReason, runSceneAgain,
63
+ addSceneBlock, tlFrame, setToast, ardyRunning, promptClips, runStudioAction,
64
+ } = useStudioShell();
65
+ const hasBlocks = promptClips.some((clip) => clip.text.trim());
66
+ const [open, setOpen] = useState(false);
67
+ const caretRef = useRef(null);
68
+ const reason = sceneGenerateDisabledReason();
69
+ const actions = [
70
+ { id: "new", label: ko("Start over", "새로 만들기"), reason, run: () => runArdy({ fresh: true }) },
71
+ { id: "again", label: ko("Take it again", "다시 뽑기"), reason: sceneAgainDisabledReason(), run: runSceneAgain },
72
+ { id: "block", label: isKo ? `프레임 ${tlFrame}에 블록 추가` : `Add block at frame ${tlFrame}`, reason: "", run: addSceneBlock },
73
+ ];
74
+ return (
75
+ <div className="topbar-generate" role="group" aria-label={ko("Generate Motion", "모션 생성")}>
76
+ <button
77
+ type="button"
78
+ className="topbar-generate-main"
79
+ data-testid="topbar-generate"
80
+ aria-disabled={reason ? "true" : undefined}
81
+ data-disabled-reason={reason || undefined}
82
+ title={reason || ko("Generate motion from the prompt and blocks", "프롬프트와 블록으로 모션을 생성합니다")}
83
+ onClick={() => (reason ? setToast(reason) : hasBlocks ? runStudioAction("motion.generateAllBlocks") : runArdy())}
84
+ >
85
+ {ardyRunning ? ko("Generating…", "생성 중…") : (
86
+ <>
87
+ <span className="topbar-generate-full">{ko("Generate Motion", "모션 생성")}</span>
88
+ <span className="topbar-generate-short">{ko("Generate", "생성")}</span>
89
+ </>
90
+ )}
91
+ </button>
92
+ <button
93
+ type="button"
94
+ className="topbar-generate-caret"
95
+ data-testid="topbar-generate-menu"
96
+ ref={caretRef}
97
+ aria-haspopup="menu"
98
+ aria-expanded={open}
99
+ aria-label={ko("More generate actions", "생성 작업 더 보기")}
100
+ onClick={() => setOpen((value) => !value)}
101
+ >
102
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
103
+ </button>
104
+ {open && (
105
+ <MenuPopover anchorRef={caretRef} align="end" onClose={() => setOpen(false)} label={ko("Generate actions", "생성 작업")}>
106
+ {actions.map((action) => (
107
+ <button
108
+ type="button"
109
+ role="menuitem"
110
+ key={action.id}
111
+ className="menubar-item"
112
+ data-generate-action={action.id}
113
+ aria-disabled={action.reason ? "true" : undefined}
114
+ data-disabled-reason={action.reason || undefined}
115
+ onClick={() => {
116
+ setOpen(false);
117
+ if (action.reason) setToast(action.reason);
118
+ else action.run();
119
+ }}
120
+ >
121
+ <span className="menubar-item-label">{action.label}</span>
122
+ {action.reason && <small className="menubar-item-reason">{action.reason}</small>}
123
+ </button>
124
+ ))}
125
+ </MenuPopover>
126
+ )}
127
+ </div>
128
+ );
129
+ }