cozyclay 1.9.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 (286) hide show
  1. package/CHANGELOG.md +171 -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-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. package/src/scene-history.js +0 -129
@@ -363,6 +363,7 @@ export function createSceneObject(kind, existing = [], placement = {}) {
363
363
  // world-anchored prop — where everything starts, because a fresh object
364
364
  // is dropped in front of the lens, not into someone's hand.
365
365
  attach: null,
366
+ hidden: false,
366
367
  footprint: { ...entry.footprint },
367
368
  height: entry.height,
368
369
  supportY: Number.isFinite(entry.supportY) ? entry.supportY : entry.height,
@@ -413,6 +414,7 @@ export function createCutoutObject({ assetId, aspect = 1, height = CUTOUT_DEFAUL
413
414
  color: CUTOUT_TINT,
414
415
  parent: null,
415
416
  attach: null,
417
+ hidden: false,
416
418
  // Key order matches what `normalizeSceneObject` writes, so a record
417
419
  // survives a storage round trip byte-for-byte.
418
420
  assetId,
@@ -493,6 +495,7 @@ export function createMeshObject({ assetId, height = MESH_DEFAULT_HEIGHT, footpr
493
495
  color: MESH_ENTRY.color,
494
496
  parent: null,
495
497
  attach: null,
498
+ hidden: false,
496
499
  assetId,
497
500
  clay: clay === true,
498
501
  footprint: meshFootprint,
@@ -541,6 +544,36 @@ export function descendantsOf(objects, id) {
541
544
  return out;
542
545
  }
543
546
 
547
+ /** True when this record, a parent above it, or a character carrying any of
548
+ * those records is hidden. A cycle in parent links ends the walk. When the
549
+ * id is in `objects`, that stored record is what the walk reads, so a display
550
+ * copy cannot disagree with the authored flag. Nothing in the lists is changed. */
551
+ export function isEffectivelyHidden(entity, objects = [], characters = []) {
552
+ if (!entity || typeof entity !== "object") return false;
553
+ const byId = new Map();
554
+ for (const object of objects) {
555
+ if (object && typeof object.id === "string") byId.set(object.id, object);
556
+ }
557
+ const characterHidden = (id) => characters.some((item) => item && typeof item === "object" && item.id === id && item.hidden === true);
558
+ const seen = new Set();
559
+ let current = entity;
560
+ while (current && typeof current === "object") {
561
+ const id = typeof current.id === "string" ? current.id : null;
562
+ if (id) {
563
+ if (seen.has(id)) break;
564
+ seen.add(id);
565
+ if (byId.has(id)) current = byId.get(id);
566
+ }
567
+ if (current.hidden === true) return true;
568
+ const characterId = current.attach && typeof current.attach.characterId === "string" ? current.attach.characterId : null;
569
+ if (characterId && characterHidden(characterId)) return true;
570
+ const parentId = typeof current.parent === "string" ? current.parent : null;
571
+ if (!parentId) break;
572
+ current = byId.get(parentId) ?? null;
573
+ }
574
+ return false;
575
+ }
576
+
544
577
  export function updateSceneObject(objects, id, patch) {
545
578
  let changed = false;
546
579
  const target = objects.find((object) => object.id === id);
@@ -595,6 +628,7 @@ export function updateSceneObject(objects, id, patch) {
595
628
  if (typeof patch[key] !== "string" || !patch[key] || patch[key] === object[key]) continue;
596
629
  update[key] = patch[key];
597
630
  }
631
+ if (typeof patch.hidden === "boolean" && patch.hidden !== (object.hidden === true)) update.hidden = patch.hidden;
598
632
  // The travel path is authored geometry, not a bounded transform: it is
599
633
  // normalized by createObjectPath (which repairs or refuses it) and set
600
634
  // wholesale, with null clearing it back to a standing object.
@@ -820,6 +854,7 @@ export function normalizeSceneObject(record) {
820
854
  // not exist. A record written before attachment existed has no field at
821
855
  // all, and null is exactly what it meant: world-anchored.
822
856
  attach: normalizeSceneAttach(record.attach) ?? null,
857
+ hidden: record.hidden === true,
823
858
  // Library kinds take their size from the library — a stored footprint is
824
859
  // stale data, not a fact. Cutouts and meshes are the exceptions: their
825
860
  // size IS per-instance. A cutout rebuilds the footprint from height and
@@ -1042,10 +1077,11 @@ export function objectFootprintBounds(object) {
1042
1077
  * The contact height is exact, never snapped to the 5 cm grid, and never
1043
1078
  * clamped here: the y clamp stays in updateSceneObject, the single owner.
1044
1079
  */
1045
- export function dropToSurfacePatch(object, others) {
1080
+ export function dropToSurfacePatch(object, others, characters = []) {
1046
1081
  const self = objectFootprintBounds(object);
1047
1082
  let highestTop = 0;
1048
1083
  for (const other of others) {
1084
+ if (isEffectivelyHidden(other, others, characters)) continue;
1049
1085
  const bounds = objectFootprintBounds(other);
1050
1086
  if (self.minX >= bounds.maxX - OVERLAP_EPS || bounds.minX >= self.maxX - OVERLAP_EPS) continue;
1051
1087
  if (self.minZ >= bounds.maxZ - OVERLAP_EPS || bounds.minZ >= self.maxZ - OVERLAP_EPS) continue;
package/src/scenes.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { DEFAULT_SENSOR_FORMAT, SENSOR_FORMATS } from "./shot.js";
6
6
  import { normalizeStableItems } from "./stable-items.js";
7
7
  import { normalizeMotionCalibration } from "./ardy/motion-calibration.js";
8
+ import { normalizeTrailEdits } from "./motion-trail.js";
8
9
  import { elementByPath } from "./studio-elements.js";
9
10
  import { wrapAngle } from "./scene-objects.js";
10
11
 
@@ -214,6 +215,10 @@ function normalizeMotionRef(ref) {
214
215
  // boundary for a later scene reload. cloneValue also strips non-finite values
215
216
  // and recursively owns the object, so a caller cannot mutate the saved stage.
216
217
  if (plainObject(ref.calibration)) normalized.calibration = normalizeMotionCalibration(ref.calibration);
218
+ if (ref.trailEdits != null) normalized.trailEdits = normalizeTrailEdits(ref.trailEdits);
219
+ // An agent-installed take keeps the Studio take id its receipts name, so a
220
+ // reload still reports the same take. Optional, like calibration.
221
+ if (typeof ref.studioTakeId === "string" && ref.studioTakeId) normalized.studioTakeId = ref.studioTakeId;
217
222
  return normalized;
218
223
  }
219
224
 
@@ -1,4 +1,5 @@
1
1
  import { createCameraBlock } from "./camera-block.js";
2
+ import { isBusRunActive } from "./command-bus.js";
2
3
 
3
4
  export const FIRST_EDIT_VERSION = 1;
4
5
  export const FIRST_EDIT_KINDS = Object.freeze([
@@ -90,6 +91,7 @@ export function createSemanticState(initial, publish, observe, domain) {
90
91
  const before = current;
91
92
  const after = typeof update === "function" ? update(before) : update;
92
93
  current = after;
94
+ if (authored && !isBusRunActive()) console.warn(`[bus] semantic edit for ${domain} called outside a bus run`);
93
95
  publish(after);
94
96
  if (authored) observe(domain, before, after);
95
97
  return after;
@@ -1,15 +1,14 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import { getAnalyticsOptOut, setAnalyticsOptOut } from "./analytics.js";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
3
2
  import { LOCALE, ko, localeChosen, setLocale } from "./locale.js";
3
+ import PreferencesDialog from "./shell/PreferencesDialog.jsx";
4
4
 
5
5
  // App settings — language and analytics — behind one labelled topbar trigger
6
6
  // (#193, docs/studio-ui-ia.md R4). Neither item edits the document, so they do
7
7
  // not belong on the Save/Export row as bare toggles.
8
8
  //
9
- // The panel is a group of toggle buttons rather than role="menu": aria-pressed
10
- // is the state contract the analytics opt-out already shipped with, and only
11
- // role="button" supports it. Tab order is DOM order, Escape returns focus to
12
- // the trigger.
9
+ // The trigger now opens the v2 Preferences dialog. The old language strings stay
10
+ // here because the locale contract and the camera-tutorial source contract both
11
+ // treat this file as the Settings entry point.
13
12
  const LANGUAGES = [
14
13
  { id: "en", label: "English", action: "Switch to English" },
15
14
  { id: "ko", label: "한국어", action: "한국어로 전환" },
@@ -17,43 +16,35 @@ const LANGUAGES = [
17
16
 
18
17
  export default function SettingsMenu({ motionSetup, motionSetupReveal = 0 }) {
19
18
  const [open, setOpen] = useState(false);
20
- const [setupOpen, setSetupOpen] = useState(false);
21
- const [optedOut, setOptedOut] = useState(getAnalyticsOptOut);
22
19
  const triggerRef = useRef(null);
23
- const setupRef = useRef(null);
20
+ const close = useCallback(() => {
21
+ setOpen(false);
22
+ triggerRef.current?.focus();
23
+ }, []);
24
+ const openCameraTutorial = useCallback(() => {
25
+ window.dispatchEvent(new CustomEvent("cozyclay:camera-tutorial", { detail: { open: true } }));
26
+ setOpen(false);
27
+ }, []);
28
+ const cameraTutorial = (
29
+ <>
30
+ <h4>{ko("Help", "도움말")}</h4>
31
+ <button
32
+ type="button"
33
+ data-testid="settings-camera-tutorial"
34
+ title={ko("Learn the camera in seven steps", "일곱 단계로 카메라 익히기")}
35
+ onClick={openCameraTutorial}
36
+ >
37
+ {ko("Camera tutorial", "카메라 튜토리얼")}
38
+ </button>
39
+ </>
40
+ );
24
41
 
25
- // Generation setup is an app setting, never a document edit. The same
26
- // popover is revealed by the contextual action beside Generate.
42
+ // A contextual readiness action can reveal Preferences directly on the
43
+ // motion connection page without changing the topbar or adding a second
44
+ // settings surface.
27
45
  useEffect(() => {
28
- if (!motionSetupReveal) return;
29
- setOpen(true);
30
- setSetupOpen(true);
46
+ if (motionSetupReveal) setOpen(true);
31
47
  }, [motionSetupReveal]);
32
- useEffect(() => {
33
- if (open && setupOpen) setupRef.current?.focus();
34
- }, [open, setupOpen, motionSetupReveal]);
35
-
36
- // Dismissal mirrors the project menu: listen only while open, ignore
37
- // presses inside the wrap so the trigger keeps toggling, close on Escape
38
- // and hand focus back to the control the operator came from.
39
- useEffect(() => {
40
- if (!open) return undefined;
41
- const onPointerDown = (event) => {
42
- if (event.target instanceof Element && event.target.closest(".settings-menu-wrap")) return;
43
- setOpen(false);
44
- };
45
- const onKeyDown = (event) => {
46
- if (event.key !== "Escape") return;
47
- setOpen(false);
48
- triggerRef.current?.focus();
49
- };
50
- document.addEventListener("pointerdown", onPointerDown);
51
- window.addEventListener("keydown", onKeyDown);
52
- return () => {
53
- document.removeEventListener("pointerdown", onPointerDown);
54
- window.removeEventListener("keydown", onKeyDown);
55
- };
56
- }, [open]);
57
48
 
58
49
  // First-run cue: the studio starts in English even on a Korean browser, so
59
50
  // the only visible hint that Korean exists used to be the old locale button.
@@ -68,78 +59,21 @@ export default function SettingsMenu({ motionSetup, motionSetupReveal = 0 }) {
68
59
  className="topbar-action settings-menu-trigger"
69
60
  data-testid="settings-menu-trigger"
70
61
  aria-expanded={open}
71
- aria-haspopup="true"
62
+ aria-haspopup="dialog"
72
63
  title={ko("Language and analytics", "언어 및 사용 통계")}
73
64
  ref={triggerRef}
74
- onClick={() => setOpen((value) => !value)}
65
+ onClick={() => (open ? close() : setOpen(true))}
75
66
  >
76
67
  {label}
77
68
  <span className="caret">▾</span>
78
69
  </button>
79
- {open && (
80
- <div className="project-menu settings-menu" role="group" aria-label={ko("Settings", "설정")}>
81
- {/* Motion readiness setup (#277): separate from privacy controls. */}
82
- {motionSetup && (
83
- <>
84
- <button
85
- type="button"
86
- data-testid="settings-motion-setup"
87
- aria-expanded={setupOpen}
88
- ref={setupRef}
89
- onClick={() => setSetupOpen((value) => !value)}
90
- >
91
- {ko("Motion generation", "모션 생성")}
92
- </button>
93
- {setupOpen && motionSetup}
94
- </>
95
- )}
96
- <h4>{ko("Language", "언어")}</h4>
97
- {LANGUAGES.map((language) => (
98
- <button
99
- key={language.id}
100
- type="button"
101
- data-testid={`settings-locale-${language.id}`}
102
- aria-pressed={LOCALE === language.id}
103
- title={language.action}
104
- onClick={() => setLocale(language.id)}
105
- >
106
- {language.label}
107
- <span className="mark" aria-hidden="true">{LOCALE === language.id ? "✓" : ""}</span>
108
- </button>
109
- ))}
110
- <h4>{ko("Privacy", "개인정보")}</h4>
111
- <button
112
- type="button"
113
- data-testid="settings-analytics"
114
- aria-pressed={!optedOut}
115
- title={optedOut
116
- ? ko("Turn anonymous analytics on", "익명 사용 통계 켜기")
117
- : ko("Turn anonymous analytics off", "익명 사용 통계 끄기")}
118
- onClick={async () => {
119
- const actual = await setAnalyticsOptOut(!optedOut);
120
- setOptedOut(actual);
121
- }}
122
- >
123
- {ko("Anonymous analytics", "익명 사용 통계")}
124
- <span className="mark">{optedOut ? ko("off", "끔") : ko("on", "켬")}</span>
125
- </button>
126
- {/* Learning the camera is not a document edit and not a topbar
127
- button (R4): the tutorial opens from this closed popover, so the
128
- mode budgets in docs/studio-ui-ia.md §1 are untouched. */}
129
- <h4>{ko("Help", "도움말")}</h4>
130
- <button
131
- type="button"
132
- data-testid="settings-camera-tutorial"
133
- title={ko("Learn the camera in seven steps", "일곱 단계로 카메라 익히기")}
134
- onClick={() => {
135
- window.dispatchEvent(new CustomEvent("cozyclay:camera-tutorial", { detail: { open: true } }));
136
- setOpen(false);
137
- }}
138
- >
139
- {ko("Camera tutorial", "카메라 튜토리얼")}
140
- </button>
141
- </div>
142
- )}
70
+ <PreferencesDialog
71
+ open={open}
72
+ onClose={close}
73
+ motionSetup={motionSetup}
74
+ initialSection={motionSetupReveal ? "ARDY Connection" : "Appearance"}
75
+ cameraTutorial={cameraTutorial}
76
+ />
143
77
  </div>
144
78
  );
145
79
  }