cozyclay 3.0.3 → 3.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 (204) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/INSTALL_FOR_AGENTS.md +146 -0
  3. package/README.md +74 -192
  4. package/THIRD_PARTY_NOTICES.md +0 -21
  5. package/bin/agent/agent-routes.mjs +22 -56
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/agent/main-agent.mjs +70 -0
  8. package/bin/agent/main-prompt.mjs +17 -0
  9. package/bin/agent/session-store.mjs +2 -2
  10. package/bin/agent/studio-prompt.mjs +1 -1
  11. package/bin/agent/studio-task-routes.mjs +185 -0
  12. package/bin/agent/studio-tasks.mjs +156 -0
  13. package/bin/agent/studio-tools.mjs +3 -2
  14. package/bin/agent/studio-turn-transaction.mjs +76 -0
  15. package/bin/cozyclay.mjs +5 -7
  16. package/dist/app/index.html +7 -6
  17. package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
  18. package/dist/assets/app-B2BpfCl2.js +5088 -0
  19. package/dist/assets/app-BlwOpest.css +1 -0
  20. package/dist/assets/compile-worker-D0pKioa0.js +5 -0
  21. package/dist/assets/landing-y3EqDgSZ.js +1 -0
  22. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  23. package/dist/assets/motion3-compile-B887n77J.js +1 -0
  24. package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
  25. package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
  26. package/dist/assets/task-board-BKaM7d-9.js +14 -0
  27. package/dist/assets/workflow-B8Dhzexj.js +179 -0
  28. package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
  29. package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
  30. package/dist/cozyclay-package.json +1 -1
  31. package/dist/greybox-to-video/index.html +2 -3
  32. package/dist/index.html +7 -57
  33. package/dist/llms.txt +2 -1
  34. package/dist/previs-software/index.html +2 -2
  35. package/dist/privacy/index.html +3 -7
  36. package/dist/seedance-camera-control/index.html +2 -2
  37. package/dist/sitemap.xml +5 -5
  38. package/dist/sw.js +5 -1
  39. package/dist/thumbs/blank-stage-dark.webp +0 -0
  40. package/dist/thumbs/blank-stage-light.webp +0 -0
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/mesh-file.mjs +3 -1
  43. package/mcp/server.mjs +3 -0
  44. package/package.json +3 -3
  45. package/src/App.jsx +50 -281
  46. package/src/analytics.js +3 -148
  47. package/src/app-stage.jsx +1 -1
  48. package/src/asset-pane.jsx +5 -10
  49. package/src/auto-flat.js +10 -0
  50. package/src/bus-tasks.js +11 -5
  51. package/src/command-bus.js +1 -1
  52. package/src/commands/motion3-compile.js +51 -27
  53. package/src/commands/motion3.js +3 -3
  54. package/src/commands/project.js +1 -1
  55. package/src/commands/shot.js +16 -4
  56. package/src/controls.jsx +0 -11
  57. package/src/cuts.js +11 -0
  58. package/src/domains/cast.js +3 -2
  59. package/src/domains/motion.js +3 -10
  60. package/src/domains/playback.js +32 -0
  61. package/src/domains/scenes.js +3 -22
  62. package/src/domains/shots.js +5 -5
  63. package/src/dualview.jsx +11 -15
  64. package/src/hierarchy-panel.css +18 -4
  65. package/src/hierarchy-panel.jsx +12 -11
  66. package/src/kits/held.js +3 -2
  67. package/src/light-blockers.js +2 -2
  68. package/src/main.jsx +5 -0
  69. package/src/motion/ground.js +5 -14
  70. package/src/motion/playback-frame-store.js +79 -0
  71. package/src/motion/playback-probe.js +16 -0
  72. package/src/motion/playback-subject.js +7 -0
  73. package/src/motion/timeline.css +43 -5
  74. package/src/motion/timeline.jsx +4 -3
  75. package/src/motion3/body.js +37 -5
  76. package/src/motion3/carried.js +11 -3
  77. package/src/motion3/compile-job.js +1 -1
  78. package/src/motion3/compiler/compile.js +7 -1
  79. package/src/motion3/compiler/finger-close.js +137 -0
  80. package/src/motion3/compiler/limbs.js +18 -3
  81. package/src/motion3/compiler/pose.js +6 -0
  82. package/src/motion3/compiler/validate-objects.js +17 -2
  83. package/src/motion3/compiler/validate-parts.js +2 -1
  84. package/src/motion3/compiler/vocab.js +4 -4
  85. package/src/motion3/describe.js +2 -2
  86. package/src/motion3/fix/index.js +5 -3
  87. package/src/motion3/flight.js +38 -0
  88. package/src/motion3/free-motion.js +10 -7
  89. package/src/motion3/load.js +125 -0
  90. package/src/motion3/mass.js +23 -0
  91. package/src/motion3/object-keys.js +11 -6
  92. package/src/motion3/object-pose.js +11 -3
  93. package/src/motion3/pipeline.js +3 -0
  94. package/src/motion3/qa/explain.js +3 -1
  95. package/src/motion3/qa/grasp-checks.js +123 -0
  96. package/src/motion3/qa/hints-grasp.js +71 -0
  97. package/src/motion3/qa/index.js +3 -1
  98. package/src/motion3/qa/object-checks.js +7 -2
  99. package/src/motion3/qa/patch.js +2 -1
  100. package/src/motion3/qa/spec-checks.js +10 -1
  101. package/src/motion3/schema/fields.js +3 -3
  102. package/src/motion3/schema/topics.js +9 -8
  103. package/src/motion3/schema/types.js +4 -1
  104. package/src/motion3/seat.js +84 -0
  105. package/src/motion3/settle.js +11 -6
  106. package/src/motion3/shapes.js +70 -0
  107. package/src/motion3/skills/catch.js +71 -0
  108. package/src/motion3/skills/index.js +10 -3
  109. package/src/motion3/skills/info.js +13 -0
  110. package/src/motion3/skills/throw.js +72 -0
  111. package/src/object-catalog.jsx +0 -4
  112. package/src/object-gizmo.jsx +2 -1
  113. package/src/object-path.js +1 -1
  114. package/src/panels/ProjectPanel.jsx +42 -21
  115. package/src/panels/details.css +15 -4
  116. package/src/popover.js +37 -0
  117. package/src/posestudio.jsx +15 -10
  118. package/src/project-browser-art.jsx +19 -0
  119. package/src/project-browser.css +74 -121
  120. package/src/project-browser.jsx +36 -60
  121. package/src/props.jsx +4 -7
  122. package/src/retired-kinds.js +37 -0
  123. package/src/scene-objects.js +3 -7
  124. package/src/scenes.js +4 -5
  125. package/src/settings-menu.jsx +1 -20
  126. package/src/shell/BottomDock.jsx +9 -9
  127. package/src/shell/MenuBar.jsx +1 -9
  128. package/src/shell/PreferencesDialog.jsx +3 -4
  129. package/src/shell/ViewportToolbar.jsx +3 -3
  130. package/src/shell/add-camera-shot.js +11 -0
  131. package/src/shell/glass-regions.css +3 -2
  132. package/src/shell/glass.css +0 -5
  133. package/src/shell/lens-scrub-math.js +51 -0
  134. package/src/shell/lens-scrub.css +73 -0
  135. package/src/shell/lens-scrub.jsx +98 -0
  136. package/src/shell/mode.css +6 -5
  137. package/src/shell/preferences.css +1 -32
  138. package/src/shell/shell.css +18 -1
  139. package/src/shell/studio-shell-context.js +5 -0
  140. package/src/shot-frame.js +26 -0
  141. package/src/stage-canvas-rigs.jsx +7 -8
  142. package/src/stage-characters.jsx +26 -0
  143. package/src/stage-gizmo.js +43 -0
  144. package/src/stage-labels-ko.js +0 -6
  145. package/src/stage-startup-defaults.js +3 -9
  146. package/src/starter-scenes.js +5 -11
  147. package/src/studio-agent-commands.js +2 -2
  148. package/src/studio-agent-protocol.js +2 -7
  149. package/src/studio-app-binding.js +1 -1
  150. package/src/studio-elements.js +1 -1
  151. package/src/styles/themes.css +13 -0
  152. package/src/styles.css +6 -674
  153. package/src/text/text-object.jsx +1 -1
  154. package/src/ui.jsx +8 -16
  155. package/src/use-embed-bridge.js +3 -6
  156. package/src/use-render-activity.js +3 -1
  157. package/src/view-ghosts.jsx +5 -3
  158. package/src/viewport-visible-rect.js +37 -0
  159. package/src/workflow/AgentPanel.jsx +75 -42
  160. package/src/workflow/AgentProgressCard.jsx +1 -1
  161. package/src/workflow/TaskBoard.jsx +102 -0
  162. package/src/workflow/TaskInline.jsx +114 -0
  163. package/src/workflow/agent-client.js +11 -25
  164. package/src/workflow/agent-panel.css +14 -1
  165. package/src/workflow/attachment-image.js +10 -3
  166. package/src/workflow/attachment-video.js +91 -0
  167. package/src/workflow/task-board.css +98 -0
  168. package/src/workflow/task-board.js +169 -0
  169. package/tools/check-eslint.mjs +2 -1
  170. package/tools/check-knip.mjs +2 -1
  171. package/tools/dev/pages/greybox-to-video.html +2 -3
  172. package/tools/dev/pages/previs-software.html +2 -2
  173. package/tools/dev/pages/privacy.html +3 -7
  174. package/tools/dev/pages/seedance-camera-control.html +2 -2
  175. package/tools/dev-full.mjs +11 -2
  176. package/tools/eslint-baseline.json +6 -6
  177. package/tools/knip-baseline.json +0 -11
  178. package/tools/loc-baseline.json +30 -31
  179. package/tools/metrics-snapshot.mjs +43 -5
  180. package/tools/render-start-thumbs.mjs +175 -0
  181. package/tools/run-tests.mjs +7 -14
  182. package/tools/sitemap.mjs +1 -1
  183. package/dist/assets/analytics-BibyE5ar.js +0 -6
  184. package/dist/assets/app-DhRJ-PBz.js +0 -5088
  185. package/dist/assets/app-f-fkV5uP.css +0 -1
  186. package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
  187. package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
  188. package/dist/assets/landing-DYg87MDY.js +0 -1
  189. package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
  190. package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
  191. package/dist/assets/workflow-BSA22Ofr.js +0 -179
  192. package/dist/scenes/city-block.cclayproject +0 -1
  193. package/src/burn-in.js +0 -91
  194. package/src/camera-tutorial.jsx +0 -520
  195. package/src/facing-marks.js +0 -88
  196. package/src/first-shot-handoff.js +0 -44
  197. package/src/panels/motion.css +0 -197
  198. package/src/set-pieces.jsx +0 -183
  199. package/src/tutorial-analytics.js +0 -86
  200. package/src/tutorial-walk.js +0 -17
  201. package/src/use-camera-tutorial.js +0 -50
  202. package/src/use-semantic-state.js +0 -12
  203. package/tools/metrics/archive-traffic.mjs +0 -138
  204. package/tools/perf/ik-camera-drag.mjs +0 -138
@@ -0,0 +1,37 @@
1
+ // Object kinds an old project can carry that this build no longer ships.
2
+
3
+ /**
4
+ * Kinds an earlier build shipped and this one does not (hand-built set
5
+ * pieces), with the standing size they had. They exist only so an old project
6
+ * still opens: each record becomes a plain cube with the same transform and
7
+ * the same size, so its blocking, collisions, route and children survive.
8
+ */
9
+ const RETIRED_KINDS = Object.freeze({
10
+ chair: { width: 0.6, depth: 0.6, height: 1.15 },
11
+ car: { width: 1.8, depth: 4.5, height: 1.4 },
12
+ "small-plane": { width: 3.4, depth: 3.6, height: 1.4 },
13
+ });
14
+
15
+ /** A stored record of a retired kind as the cube that stands in for it; any
16
+ * other record is returned untouched. Scale absorbs the retired footprint, the
17
+ * same single-`scale` fallback normalizeSceneObject reads applies first. */
18
+ export function retireObjectKind(record) {
19
+ const size = record && typeof record === "object" ? RETIRED_KINDS[record.renderer] : undefined;
20
+ if (!size) return record;
21
+ const single = record.scaleX === undefined && record.scaleY === undefined && record.scaleZ === undefined ? Number(record.scale) : NaN;
22
+ const axis = (value) => {
23
+ const n = value === undefined ? (Number.isFinite(single) ? single : 1) : Number(value);
24
+ return Number.isFinite(n) ? n : 1;
25
+ };
26
+ const { scale: _scale, ...rest } = record;
27
+ return {
28
+ ...rest,
29
+ renderer: "cube",
30
+ scaleX: axis(record.scaleX) * size.width,
31
+ scaleY: axis(record.scaleY) * size.height,
32
+ scaleZ: axis(record.scaleZ) * size.depth,
33
+ footprint: { width: 1, depth: 1 },
34
+ height: 1,
35
+ supportY: 1,
36
+ };
37
+ }
@@ -24,6 +24,7 @@ import { MESH_DEFAULT_HEIGHT, MESH_HEIGHT_MIN } from "./scene-mesh.js";
24
24
  import { normalizeSculptRecipe, sculptStandingBox } from "./sculpt-recipe.js";
25
25
  import { GREY_BOX } from "./object-colors.js";
26
26
  import { isEffectivelyHidden } from "./object-hidden.js";
27
+ import { retireObjectKind } from "./retired-kinds.js";
27
28
  import { objectSize } from "./object-size.js";
28
29
  import { EFFECT_ENTRY, effectFields } from "./effects/effect-record.js";
29
30
  import { TEXT_ENTRY, TEXT_KIND, textFields, textPatch } from "./text/text-record.js";
@@ -65,8 +66,7 @@ const snapTo = (value, step) => Number((step > 0 ? Math.round(value / step) * st
65
66
  * object stands (used for the selection box and the gizmo's height).
66
67
  *
67
68
  * Primitives are grey-box grey on purpose: a blockout stands in for something
68
- * else, and a tinted maquette reads as a finished prop. The hand-built set
69
- * pieces keep their clay colours because they ARE the thing they depict.
69
+ * else, and a tinted maquette reads as a finished prop.
70
70
  */
71
71
  export const OBJECT_LIBRARY = [
72
72
  { kind: "cube", label: "Cube", group: "Primitives", footprint: { width: 1, depth: 1 }, height: 1, color: GREY_BOX },
@@ -75,11 +75,6 @@ export const OBJECT_LIBRARY = [
75
75
  { kind: "cylinder", label: "Cylinder", group: "Primitives", footprint: { width: 1, depth: 1 }, height: 1, color: GREY_BOX },
76
76
  { kind: "cone", label: "Cone", group: "Primitives", footprint: { width: 1, depth: 1 }, height: 1, color: GREY_BOX },
77
77
  { kind: "plane", label: "Plane", group: "Primitives", footprint: { width: 2, depth: 2 }, height: 0, color: GREY_BOX },
78
- // supportY is the walkable surface, which can differ from the object's
79
- // bounding height (a chair's back rises above its seat).
80
- { kind: "chair", label: "Chair", group: "Set pieces", footprint: { width: 0.6, depth: 0.6 }, height: 1.15, supportY: 0.495, color: "#b9855d" },
81
- { kind: "car", label: "Car", group: "Set pieces", footprint: { width: 1.8, depth: 4.5 }, height: 1.4, color: "#d98770" },
82
- { kind: "small-plane", label: "Plane (aircraft)", group: "Set pieces", footprint: { width: 3.4, depth: 3.6 }, height: 1.4, color: "#7896a4" },
83
78
  ];
84
79
 
85
80
  /**
@@ -994,6 +989,7 @@ function objectOpacity(value) {
994
989
 
995
990
  export function normalizeSceneObject(record) {
996
991
  if (!record || typeof record !== "object" || Array.isArray(record)) return null;
992
+ record = retireObjectKind(record);
997
993
  const entry = objectLibraryEntry(record.renderer);
998
994
  if (!entry) return null;
999
995
  if (typeof record.id !== "string" || !record.id) return null;
package/src/scenes.js CHANGED
@@ -6,7 +6,7 @@ import { DEFAULT_SENSOR_FORMAT, SENSOR_FORMATS } from "./shot.js";
6
6
  import { normalizeStableItems } from "./stable-items.js";
7
7
 
8
8
  import { elementByPath } from "./studio-elements.js";
9
- import { wrapAngle } from "./scene-objects.js";
9
+ import { wrapAngle } from "./scene-objects.js"; import { retireObjectKind } from "./retired-kinds.js";
10
10
  import { normalizeTakeSource } from "./domains/motion/take-source.js";
11
11
  import { createKeyLight, normalizeLighting } from "./stage-lighting.js";
12
12
  // The timeline clock lives in timeline-fps.js (three-free, so the agent
@@ -115,8 +115,6 @@ export function normalizeReferenceImage(value) {
115
115
 
116
116
  /** Stature band for a cast member: the gizmo's scale handles are a deliberate
117
117
  * artistic ask, and a previs giant or child is legitimate. */
118
- export const CHARACTER_SCALE_MIN = CHARACTER_SCALE_LIMITS.min;
119
- export const CHARACTER_SCALE_MAX = CHARACTER_SCALE_LIMITS.max;
120
118
  const clampScale = (value) => (Number.isFinite(value) && value > 0
121
119
  ? Math.max(CHARACTER_SCALE_LIMITS.min, Math.min(CHARACTER_SCALE_LIMITS.max, value))
122
120
  : 1);
@@ -373,7 +371,8 @@ function repairScene(record, existing, fallbackNumber, fallbackStage, legacyCloc
373
371
  return {
374
372
  id: record.id,
375
373
  name: uniqueName(typeof record.name === "string" ? record.name : `SCENE ${String(fallbackNumber).padStart(2, "0")}`, existing),
376
- objects: record.objects.filter(plainObject).map((object) => cloneValue(object)),
374
+ // A kind this build no longer ships opens as the cube that stands in for it.
375
+ objects: record.objects.filter(plainObject).map((object) => retireObjectKind(cloneValue(object))),
377
376
  // The shot document is a sealed envelope with its OWN version, and it
378
377
  // migrates its own frames when its reader opens it.
379
378
  shotDocument: cloneValue(record.shotDocument ?? null),
@@ -412,7 +411,7 @@ export function readSceneDocument(raw, legacyRaw = null) {
412
411
  return { status: "corrupt", document: createSceneDocument(), dropped: 0, quarantineRaw: legacyRaw };
413
412
  }
414
413
  const document = createSceneDocument();
415
- document.scenes[0].objects = legacy.objects.filter(plainObject).map((object) => cloneValue(object));
414
+ document.scenes[0].objects = legacy.objects.filter(plainObject).map((object) => retireObjectKind(cloneValue(object)));
416
415
  return { status: "migrated", document, dropped: legacy.objects.length - document.scenes[0].objects.length };
417
416
  }
418
417
  return { status: "absent", document: createSceneDocument(), dropped: 0 };
@@ -7,8 +7,7 @@ import PreferencesDialog from "./shell/PreferencesDialog.jsx";
7
7
  // not belong on the Save/Export row as bare toggles.
8
8
  //
9
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.
10
+ // here because the locale contract treats this file as the Settings entry point.
12
11
  const LANGUAGES = [
13
12
  { id: "en", label: "English", action: "Switch to English" },
14
13
  { id: "ko", label: "한국어", action: "한국어로 전환" },
@@ -21,23 +20,6 @@ export default function SettingsMenu() {
21
20
  setOpen(false);
22
21
  triggerRef.current?.focus();
23
22
  }, []);
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
- );
41
23
 
42
24
  // First-run cue: the studio starts in English even on a Korean browser, so
43
25
  // the only visible hint that Korean exists used to be the old locale button.
@@ -64,7 +46,6 @@ export default function SettingsMenu() {
64
46
  open={open}
65
47
  onClose={close}
66
48
  initialSection="Appearance"
67
- cameraTutorial={cameraTutorial}
68
49
  />
69
50
  </div>
70
51
  );
@@ -1,12 +1,13 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { useStudioShell } from "./studio-shell-context.js";
2
+ import { useStudioPlaybackFrame, useStudioShell } from "./studio-shell-context.js";
3
3
  import { logStore } from "./log-store.js";
4
4
  import "./dock.css";
5
5
  import LibrarySlot from "./LibrarySlot.jsx";
6
6
  import BoardSlot from "./BoardSlot.jsx";
7
+ import LensScrub from "./lens-scrub.jsx";
7
8
  import { ko } from "../locale.js";
8
9
  import Timeline, { timelineTake } from "../motion/timeline.jsx";
9
- import { DEFAULT_PLAYBACK_SPEED, SHOT_ASPECT_PRESETS, sceneObjectNameDisplayKo } from "../app-stage.jsx";
10
+ import { DEFAULT_PLAYBACK_SPEED, sceneObjectNameDisplayKo } from "../app-stage.jsx";
10
11
 
11
12
  import { trackFeature } from "../analytics.js";
12
13
  import { defaultRailRange } from "../camera-rail-schedule.js";
@@ -67,11 +68,11 @@ function dockTabsFor(previsMode) {
67
68
  * switching keeps the Sequencer's scroll and zoom and the Library's folder
68
69
  * and search. Any stored tab the mode does not offer opens its first tab. */
69
70
  export default function BottomDock({ tab: requestedTab = "animation", onTabChange, embedded = false }) {
70
- const { previsMode } = useStudioShell();
71
+ const { previsMode } = useStudioShell(), liveFrame = useStudioPlaybackFrame();
71
72
  const tabs = embedded ? dockTabsFor(null) : dockTabsFor(previsMode);
72
73
  const tab = tabs.some((entry) => entry.key === requestedTab) ? requestedTab : tabs[0].key;
73
74
  const {
74
- tlFrame, motion, waypointMode, craneSelectedIndex,
75
+ motion, waypointMode, craneSelectedIndex,
75
76
  isCameraSelection, addActiveCranePoint, deleteSelectedCranePoint, setCraneSelectedIndex, tlFrameCount,
76
77
  tlFps, characters, activeCharIndex, ghostLayers, pathSpeed,
77
78
  tlPlaying, workflowMode, waypoints, pendingWaypointFrame,
@@ -205,7 +206,7 @@ export default function BottomDock({ tab: requestedTab = "animation", onTabChang
205
206
  {!embedded && previsMode === "storyboard" && <BoardSlot active={tab === "board"} />}
206
207
  <div className="bottom-timeline" hidden={tab !== "animation"}>
207
208
  <Timeline
208
- frame={tlFrame}
209
+ frame={liveFrame}
209
210
  craneSelectedIndex={craneSelectedIndex}
210
211
  cameraSelected={isCameraSelection}
211
212
  onCranePointAdd={addActiveCranePoint}
@@ -390,11 +391,10 @@ const CARD_RATIOS = [
390
391
 
391
392
  /** The shot camera's card at the dock's right end. The camera itself is drawn
392
393
  * into the stage canvas under `.vp-shot-preview`, which sits over the card's
393
- * frame slot (see glass.css); the card holds its title, ratio and look-through. */
394
+ * frame slot (see glass.css); the card holds its title, lens, ratio and look-through. */
394
395
  function ShotCard() {
395
- const { activeShot, shot, shotAspectKey, runStudioAction, enterShotLook, exitShotLook, lookThroughShot, tlFps, embedMode } = useStudioShell();
396
+ const { activeShot, shot, fovDeg, shotsDomain, shotAspectKey, runStudioAction, enterShotLook, exitShotLook, lookThroughShot, tlFps, embedMode } = useStudioShell();
396
397
  if (embedMode) return null;
397
- const ratio = SHOT_ASPECT_PRESETS[shotAspectKey]?.label?.replace(/:1$/, "") ?? shotAspectKey;
398
398
  const frames = activeShot && Number.isFinite(activeShot.startFrame) && Number.isFinite(activeShot.endFrame)
399
399
  ? `${activeShot.startFrame}–${activeShot.endFrame} · ${((activeShot.endFrame - activeShot.startFrame + 1) / (tlFps || 24)).toFixed(1)}s`
400
400
  : null;
@@ -403,7 +403,7 @@ function ShotCard() {
403
403
  <header className="dock-shot-head">
404
404
  <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 5.75A1.5 1.5 0 0 1 3.5 4.25h6A1.5 1.5 0 0 1 11 5.75v4.5a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 2 10.25z M11 7l3-1.5v5L11 9" /></svg>
405
405
  <span className="dock-shot-title">{ko("Shot Camera", "샷 카메라")}</span>
406
- <span className="dock-shot-meta">{shot.focalMm} mm · {ratio}</span>
406
+ <LensScrub fovDeg={fovDeg} sensorHeightMm={shot?.usedSensorHeightMm} shotsDomain={shotsDomain} disabled={!shot} />
407
407
  </header>
408
408
  {/* While the main view looks through this camera the card has nothing
409
409
  of its own to show (the preview pass is off): it says where the
@@ -196,7 +196,7 @@ export default function MenuBar({ preferences }) {
196
196
  ? panelCount ? "" : noPanels
197
197
  : shots.length ? "" : ko("Add a shot first — a pack describes one cut", "샷을 먼저 추가하세요 — 팩은 컷 하나를 설명합니다");
198
198
 
199
- // The camera tutorial's handoff opens Export directly; its flyout lives in File.
199
+ // Export's flyout lives in File, so an open Export menu opens File.
200
200
  useEffect(() => {
201
201
  if (exportMenuOpen) setOpen("file");
202
202
  }, [exportMenuOpen]);
@@ -534,14 +534,6 @@ export default function MenuBar({ preferences }) {
534
534
  ),
535
535
  help: (
536
536
  <>
537
- <button
538
- type="button"
539
- role="menuitem"
540
- className="menubar-item"
541
- onClick={item(() => window.dispatchEvent(new CustomEvent("cozyclay:camera-tutorial", { detail: { open: true, source: "help" } })))}
542
- >
543
- <span className="menubar-item-label">{ko("Tutorial", "튜토리얼")}</span>
544
- </button>
545
537
  <button type="button" role="menuitem" className="menubar-item" onClick={item(() => setDialog("shortcuts"))}>
546
538
  <span className="menubar-item-label">{ko("Keyboard shortcuts", "단축키")}</span>
547
539
  </button>
@@ -114,7 +114,7 @@ function ThemeControls() {
114
114
  }
115
115
 
116
116
  function preferenceSection(section, props) {
117
- const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView, cameraTutorial } = props;
117
+ const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView } = props;
118
118
  const languageControls = (
119
119
  <div className="v2-preferences__language-controls">
120
120
  {LANGUAGES.map((language) => (
@@ -158,7 +158,6 @@ function preferenceSection(section, props) {
158
158
  />
159
159
  </PreferenceRow>
160
160
  </PreferenceSection>
161
- {cameraTutorial && <div className="v2-preferences__help">{cameraTutorial}</div>}
162
161
  </>
163
162
  );
164
163
  case "Viewport":
@@ -181,7 +180,7 @@ function preferenceSection(section, props) {
181
180
  }
182
181
  }
183
182
 
184
- export default function PreferencesDialog({ open, onClose, initialSection = "Appearance", cameraTutorial }) {
183
+ export default function PreferencesDialog({ open, onClose, initialSection = "Appearance" }) {
185
184
  const dialogRef = useRef(null);
186
185
  const searchRef = useRef(null);
187
186
  const closeRef = useRef(onClose);
@@ -233,7 +232,7 @@ export default function PreferencesDialog({ open, onClose, initialSection = "App
233
232
  }, [open, initialSection]);
234
233
 
235
234
  if (!open || typeof document === "undefined") return null;
236
- const currentProps = { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView, cameraTutorial };
235
+ const currentProps = { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView };
237
236
 
238
237
  return createPortal(
239
238
  <div className="v2-preferences" data-testid="preferences-dialog-backdrop" onPointerDown={(event) => { if (event.target === event.currentTarget) closeRef.current?.(); }}>
@@ -11,7 +11,7 @@ import { elementByPath } from "../studio-elements.js";
11
11
  import { saveAutoColor } from "../auto-color.js";
12
12
  import { trackFeature } from "../analytics.js";
13
13
  import { GUIDE_LABELS, nextGuideMode } from "../shot-guides.js";
14
- import GhostsMenuGroup from "./GhostsMenuGroup.jsx";
14
+ import GhostsMenuGroup from "./GhostsMenuGroup.jsx"; import { addCameraShot } from "./add-camera-shot.js";
15
15
  import "./viewport.css";
16
16
 
17
17
  // G1: the single mode+tool pill. Keys 1-5 and W/E/R are bound in App's
@@ -122,7 +122,7 @@ export default function ViewportToolbar() {
122
122
  autoColor, setAutoColor, isCharacterSelection, partColoursChoice, embedMode,
123
123
  agentCollapsed, addSceneObject, spawnCharacter, characters, flySpeed,
124
124
  ikEditTool, setIkEditTool, motion, setRangePinPartPick,
125
- previsMode, sculptView, requestSculptView,
125
+ previsMode, sculptView, requestSculptView, appContext, setToast,
126
126
  } = useStudioShell();
127
127
  const menu = useOverlayMenu();
128
128
 
@@ -292,7 +292,7 @@ export default function ViewportToolbar() {
292
292
  role="menuitem"
293
293
  className="add-object-item"
294
294
  data-add="camera"
295
- onClick={() => { runStudioAction("shot.create"); menu.close(); }}
295
+ onClick={() => { addCameraShot({ appContext, runStudioAction, setToast }); menu.close(); }}
296
296
  >
297
297
  <span className="add-object-swatch vp-add-swatch-camera" aria-hidden="true" />
298
298
  <span>{ko("Camera", "카메라")}</span>
@@ -0,0 +1,11 @@
1
+ // Add > Camera names the cut it made (ViewportToolbar). A refusal is already toasted by runStudioAction.
2
+ import { ko } from "../locale.js";
3
+
4
+ export function addCameraShot({ appContext, runStudioAction, setToast }) {
5
+ const shots = appContext.storeDomain("shot"), before = shots.read(), receipt = runStudioAction("shot.create");
6
+ const made = shots.read().find((row) => row.id === receipt?.affectedIds.find((id) => !before.some((old) => old.id === id)));
7
+ if (!made) return;
8
+ setToast(made.kind === "still"
9
+ ? ko(`${made.name} added — a storyboard panel at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임의 스토리보드 패널`)
10
+ : ko(`${made.name} added — a camera cut at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임에서 카메라 컷`));
11
+ }
@@ -800,11 +800,12 @@
800
800
 
801
801
  /* ----------------------------------------------- Dock: Animation | Assets */
802
802
  /* The switch sits at the head's left end, where the Sequencer's transport
803
- and the Library's Content | Log row start after it. */
803
+ and the Library's Content | Log row start after it. It paints above the
804
+ timeline (z-index 6, timeline.css), which would otherwise take its clicks. */
804
805
  .app[data-shell="glass"]:not([data-embed-mode]) .v2-dock { --dock-tabs-w: 176px; }
805
806
  .app[data-shell="glass"]:not([data-embed-mode]) .dock-tabs {
806
807
  position: absolute;
807
- z-index: 3;
808
+ z-index: 7;
808
809
  top: 9px;
809
810
  left: 16px;
810
811
  box-sizing: border-box;
@@ -326,11 +326,6 @@
326
326
  font-size: 11.5px;
327
327
  font-weight: 400;
328
328
  }
329
- .app[data-shell="glass"]:not([data-embed-mode]) .studio-project-head .project-menu {
330
- top: calc(100% + 6px);
331
- left: -6px;
332
- z-index: 40;
333
- }
334
329
 
335
330
  /* --------------------------------------------- viewport overlay rails */
336
331
  /* Overlays fill the open stage between the columns: the mode switch rides
@@ -0,0 +1,51 @@
1
+ // The Shot Camera card's focal-length scrub (BottomDock ShotCard). The lens is
2
+ // stored as a vertical FOV (shotsDomain.changeLens takes degrees); the card
3
+ // speaks millimetres on the current filmback, the shot.js lens relation.
4
+ import { PRIME_SET } from "../shot.js";
5
+
6
+ // Editor lens range: shotsDomain.changeLens refuses FOVs outside 14-90°.
7
+ const FOV_MIN = 14, FOV_MAX = 90;
8
+ // px of horizontal drag per millimetre, and the Shift / Alt multipliers.
9
+ const PX_PER_MM = 4, FINE = 0.1, COARSE = 4;
10
+
11
+ const round1 = (value) => Math.round(value * 10) / 10;
12
+
13
+ export function fovDegToMm(fovDeg, sensorHeightMm) {
14
+ return sensorHeightMm / (2 * Math.tan((fovDeg * Math.PI) / 360));
15
+ }
16
+
17
+ export function mmToFovDeg(mm, sensorHeightMm) {
18
+ return (2 * Math.atan(sensorHeightMm / (2 * mm)) * 180) / Math.PI;
19
+ }
20
+
21
+ /** The millimetre range the editor FOV range allows on this filmback. */
22
+ export function lensRange(sensorHeightMm) {
23
+ return { min: round1(fovDegToMm(FOV_MAX, sensorHeightMm)) + 0.1, max: round1(fovDegToMm(FOV_MIN, sensorHeightMm)) - 0.1 };
24
+ }
25
+
26
+ export function clampMm(mm, range) {
27
+ return round1(Math.min(range.max, Math.max(range.min, mm)));
28
+ }
29
+
30
+ /** The lens a horizontal drag of dx px from startMm lands on. */
31
+ export function dragMm(startMm, dx, { fine = false, coarse = false } = {}, range) {
32
+ const scale = fine ? FINE : coarse ? COARSE : 1;
33
+ return clampMm(startMm + (dx / PX_PER_MM) * scale, range);
34
+ }
35
+
36
+ /** A click steps to the next real prime in range, wrapping to the widest. */
37
+ export function nextPrime(mm, range) {
38
+ const primes = PRIME_SET.filter((prime) => prime >= range.min && prime <= range.max);
39
+ return primes.find((prime) => prime > mm + 0.05) ?? primes[0];
40
+ }
41
+
42
+ /** The left arrow steps back to the previous real prime, wrapping to the longest. */
43
+ export function previousPrime(mm, range) {
44
+ const primes = PRIME_SET.filter((prime) => prime >= range.min && prime <= range.max);
45
+ return [...primes].reverse().find((prime) => prime < mm - 0.05) ?? primes.at(-1);
46
+ }
47
+
48
+ export function formatMm(mm) {
49
+ const value = round1(mm);
50
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} mm`;
51
+ }
@@ -0,0 +1,73 @@
1
+ /* The Shot Camera card's focal-length scrub (lens-scrub.jsx): a small field, the mm value over a track with a
2
+ thumb, so it reads as a control at rest; the step arrows appear just outside it on hover, taking no width. */
3
+ .dock-lens-scrub {
4
+ position: relative;
5
+ flex: none;
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: 0;
9
+ padding: 1px;
10
+ border-radius: 6px;
11
+ background: var(--chip-fill, rgba(127, 127, 127, .1));
12
+ color: var(--text-1, currentColor);
13
+ font: 500 11px var(--sans);
14
+ font-variant-numeric: tabular-nums;
15
+ cursor: ew-resize;
16
+ user-select: none;
17
+ touch-action: none;
18
+ }
19
+ .dock-lens-body { display: flex; flex-direction: column; align-items: stretch; gap: 2px; padding: 2px 4px; }
20
+ .dock-lens-value { line-height: 1; white-space: nowrap; text-align: center; }
21
+ .dock-lens-track {
22
+ position: relative;
23
+ width: 38px;
24
+ height: 2px;
25
+ border-radius: 1px;
26
+ background: var(--hairline, rgba(127, 127, 127, .3));
27
+ }
28
+ .dock-lens-fill {
29
+ position: absolute;
30
+ inset: 0 auto 0 0;
31
+ width: var(--lens-at, 0%);
32
+ border-radius: inherit;
33
+ background: var(--text-3, currentColor);
34
+ }
35
+ .dock-lens-thumb {
36
+ position: absolute;
37
+ top: 50%;
38
+ left: var(--lens-at, 0%);
39
+ width: 5px;
40
+ height: 5px;
41
+ border-radius: 50%;
42
+ background: var(--text-2, currentColor);
43
+ box-shadow: 0 0 0 1px var(--surface-raised, #fff);
44
+ transform: translate(-50%, -50%);
45
+ }
46
+ .dock-lens-step {
47
+ position: absolute;
48
+ top: 50%;
49
+ display: grid;
50
+ place-items: center;
51
+ width: 12px;
52
+ height: 18px;
53
+ border-radius: 3px;
54
+ color: var(--text-3, currentColor);
55
+ cursor: pointer;
56
+ opacity: 0;
57
+ transform: translateY(-50%);
58
+ transition: opacity .12s;
59
+ }
60
+ .dock-lens-step[data-step="back"] { right: 100%; }
61
+ .dock-lens-step[data-step="next"] { left: 100%; }
62
+ .dock-lens-scrub:is(:hover, :focus-visible, .dragging) .dock-lens-step { opacity: 1; }
63
+ .dock-lens-step:hover { color: var(--select); background: var(--hairline, rgba(127, 127, 127, .2)); }
64
+ .dock-lens-scrub:hover,
65
+ .dock-lens-scrub.dragging { color: var(--select); }
66
+ .dock-lens-scrub:hover .dock-lens-fill,
67
+ .dock-lens-scrub.dragging .dock-lens-fill,
68
+ .dock-lens-scrub:hover .dock-lens-thumb,
69
+ .dock-lens-scrub.dragging .dock-lens-thumb { background: var(--select); }
70
+ .dock-lens-scrub.dragging .dock-lens-thumb { transform: translate(-50%, -50%) scale(1.3); }
71
+ .dock-lens-scrub:focus-visible { outline: 2px solid var(--select); outline-offset: 1px; }
72
+ .dock-lens-scrub[aria-disabled="true"] { cursor: default; opacity: .45; background: none; box-shadow: none; color: var(--label, var(--text-2)); }
73
+ .dock-lens-scrub[aria-disabled="true"] :is(.dock-lens-fill, .dock-lens-thumb, .dock-lens-step) { visibility: hidden; }
@@ -0,0 +1,98 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
3
+ import { ko } from "../locale.js";
4
+ import { PRESETS } from "../app-stage.jsx";
5
+ import { clampMm, dragMm, fovDegToMm, formatMm, lensRange, mmToFovDeg, nextPrime, previousPrime } from "./lens-scrub-math.js";
6
+ import "./lens-scrub.css";
7
+
8
+ // A drag starts after this many px; less is a click (the next prime).
9
+ const DRAG_SLOP = 3;
10
+ // A click waits this long so a double-click (reset) does not also step.
11
+ const CLICK_DELAY_MS = 220;
12
+
13
+ /** The Shot Camera card's focal length: drag left/right to change the lens.
14
+ * A drag is one undo step (shotsDomain's gesture commits on pointer-up, Esc
15
+ * cancels); a click steps to the next prime (the left arrow: the previous
16
+ * one); a double-click resets. It is drawn as a field with arrows and a thumb
17
+ * so it reads as something to drag, not as a label. */
18
+ export default function LensScrub({ fovDeg, sensorHeightMm, shotsDomain, disabled = false }) {
19
+ const off = disabled || !shotsDomain || !Number.isFinite(fovDeg) || !(sensorHeightMm > 0);
20
+ const range = off ? { min: 0, max: 0 } : lensRange(sensorHeightMm);
21
+ const mm = off ? 0 : clampMm(fovDegToMm(fovDeg, sensorHeightMm), range);
22
+ const drag = useRef(null);
23
+ const clickTimer = useRef(0);
24
+ const [dragging, setDragging] = useState(false);
25
+ useEffect(() => () => clearTimeout(clickTimer.current), []);
26
+
27
+ const setMm = (value) => shotsDomain.changeLens(mmToFovDeg(clampMm(value, range), sensorHeightMm));
28
+ const onPointerDown = (event) => {
29
+ if (off || event.button !== 0) return;
30
+ event.currentTarget.setPointerCapture?.(event.pointerId);
31
+ drag.current = { x: event.clientX, mm, moved: false, back: event.target.closest?.("[data-step]")?.dataset.step === "back" };
32
+ };
33
+ const onPointerMove = (event) => {
34
+ const state = drag.current;
35
+ if (!state) return;
36
+ const dx = event.clientX - state.x;
37
+ if (!state.moved) {
38
+ if (Math.abs(dx) < DRAG_SLOP) return;
39
+ state.moved = true;
40
+ setDragging(true);
41
+ shotsDomain.beginGesture();
42
+ }
43
+ setMm(dragMm(state.mm, dx, { fine: event.shiftKey, coarse: event.altKey }, range));
44
+ };
45
+ const onPointerUp = () => {
46
+ const state = drag.current;
47
+ drag.current = null;
48
+ setDragging(false);
49
+ if (!state || state.moved) return;
50
+ clearTimeout(clickTimer.current);
51
+ clickTimer.current = setTimeout(() => setMm((state.back ? previousPrime : nextPrime)(state.mm, range)), CLICK_DELAY_MS);
52
+ };
53
+ const onDoubleClick = () => {
54
+ if (off) return;
55
+ clearTimeout(clickTimer.current);
56
+ shotsDomain.changeLens(PRESETS.medium.fov);
57
+ };
58
+ const onKeyDown = (event) => {
59
+ if (off || (event.key !== "ArrowLeft" && event.key !== "ArrowRight")) return;
60
+ event.preventDefault();
61
+ event.stopPropagation();
62
+ const step = (event.shiftKey ? 10 : 1) * (event.key === "ArrowRight" ? 1 : -1);
63
+ setMm(Math.round(mm + step));
64
+ };
65
+
66
+ return (
67
+ <span
68
+ className={"dock-lens-scrub" + (dragging ? " dragging" : "")}
69
+ role="slider"
70
+ tabIndex={off ? -1 : 0}
71
+ aria-label={ko("Focal length", "초점 거리")}
72
+ aria-disabled={off || undefined}
73
+ aria-valuemin={range.min}
74
+ aria-valuemax={range.max}
75
+ aria-valuenow={mm}
76
+ aria-valuetext={off ? ko("No shot camera", "샷 카메라 없음") : formatMm(mm)}
77
+ aria-orientation="horizontal"
78
+ title={ko("Focal length — drag to change, click an arrow for the next lens", "초점 거리 — 드래그해 변경, 화살표를 눌러 다음 렌즈")}
79
+ data-testid="dock-lens-scrub"
80
+ onPointerDown={onPointerDown}
81
+ onPointerMove={onPointerMove}
82
+ onPointerUp={onPointerUp}
83
+ onPointerCancel={() => { drag.current = null; setDragging(false); }}
84
+ onDoubleClick={onDoubleClick}
85
+ onKeyDown={onKeyDown}
86
+ >
87
+ <span className="dock-lens-step" data-step="back" aria-hidden="true"><FiChevronLeft size={9} /></span>
88
+ <span className="dock-lens-body">
89
+ <span className="dock-lens-value">{off ? "— mm" : formatMm(mm)}</span>
90
+ <span className="dock-lens-track" aria-hidden="true" style={{ "--lens-at": `${off ? 0 : ((mm - range.min) / (range.max - range.min)) * 100}%` }}>
91
+ <span className="dock-lens-fill" />
92
+ <span className="dock-lens-thumb" />
93
+ </span>
94
+ </span>
95
+ <span className="dock-lens-step" data-step="next" aria-hidden="true"><FiChevronRight size={9} /></span>
96
+ </span>
97
+ );
98
+ }
@@ -1,12 +1,13 @@
1
1
  /* Pose mode (#521) is IK editing on the motion workspace: it hides what
2
- Motion hides (scene transform and camera framing chrome, the inset and the
3
- shot preview) and shows the mode hint. The timeline's pose-correction
4
- tools are hidden only in Scene and Camera, so they stay visible here. */
2
+ Motion hides (scene transform and camera framing chrome, the inset) and
3
+ shows the mode hint. The shot preview stays: it is drawn into the dock's
4
+ Shot Camera frame, and hiding it left that frame a hole onto the editor
5
+ view. The timeline's pose-correction tools are hidden only in Scene and
6
+ Camera, so they stay visible here. */
5
7
  .app[data-workflow-mode="pose"] .workflow-scene-context,
6
8
  .app[data-workflow-mode="pose"] .workflow-camera-context { display: none; }
7
9
  .app[data-workflow-mode="pose"] .workflow-toolbar-hint { display: inline-flex; }
8
- .app[data-workflow-mode="pose"] .vp-inset,
9
- .app[data-workflow-mode="pose"] .vp-shot-preview { display: none !important; }
10
+ .app[data-workflow-mode="pose"] .vp-inset { display: none !important; }
10
11
 
11
12
  /* A storyboard project (#650) is stills: the take, motion and pose-correction
12
13
  tools, the root-path (waypoint) switch and a prop's Top-View travel path
@@ -203,8 +203,7 @@
203
203
  .v2-preferences .v2-preferences__select:focus-visible,
204
204
  .v2-preferences .v2-preferences__toggle:focus-visible,
205
205
  .v2-preferences .v2-preferences__primary-button:focus-visible,
206
- .v2-preferences .v2-preferences__reset:focus-visible,
207
- .v2-preferences .v2-preferences__help button:focus-visible {
206
+ .v2-preferences .v2-preferences__reset:focus-visible {
208
207
  outline: none;
209
208
  box-shadow: var(--prefs-focus-ring, 0 0 0 1px var(--select, #e8a33d));
210
209
  }
@@ -506,36 +505,6 @@
506
505
  font: 500 var(--prefs-secondary-size, 11px) var(--mono, 'JetBrains Mono');
507
506
  }
508
507
 
509
- .v2-preferences .v2-preferences__help {
510
- padding-top: var(--sp-3, 16px);
511
- }
512
-
513
- .v2-preferences .v2-preferences__help h4 {
514
- margin: 0 0 var(--sp-1, 8px);
515
- color: var(--text-3, #636369);
516
- font-size: var(--prefs-secondary-size, 11px);
517
- font-weight: 500;
518
- letter-spacing: 0;
519
- text-transform: none;
520
- }
521
-
522
- .v2-preferences .v2-preferences__help button {
523
- min-height: var(--h-control, 28px);
524
- padding: 0 var(--sp-2, 12px);
525
- border: 0;
526
- border-radius: var(--r-1, 4px);
527
- background: var(--field-fill, var(--surface-raised, #1e1e21));
528
- color: var(--text-2, #9b9ba1);
529
- font: inherit;
530
- font-size: var(--prefs-secondary-size, 11px);
531
- cursor: pointer;
532
- }
533
-
534
- .v2-preferences .v2-preferences__help button:hover {
535
- background: var(--surface-raised, #1e1e21);
536
- color: var(--text-1, #e4e4e7);
537
- }
538
-
539
508
  .v2-preferences .v2-preferences__motion-setup {
540
509
  max-width: var(--prefs-control-width, 240px);
541
510
  display: grid;