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
@@ -0,0 +1,386 @@
1
+ .v2-outliner {
2
+ min-height: 0;
3
+ max-height: none;
4
+ flex: 1 1 auto;
5
+ display: flex;
6
+ flex-direction: column;
7
+ background: var(--surface-panel, #151517);
8
+ border: 0;
9
+ color: var(--text-1, #e4e4e7);
10
+ font-family: var(--sans, 'IBM Plex Sans');
11
+ font-size: var(--outliner-ui-size, 12px);
12
+ line-height: 1.2;
13
+ overflow: hidden;
14
+ }
15
+
16
+ .v2-outliner .hierarchy-heading {
17
+ height: var(--outliner-header-height, 36px);
18
+ min-height: var(--outliner-header-height, 36px);
19
+ padding: 0 var(--sp-2, 12px);
20
+ border-bottom: var(--outliner-border-width, 1px) solid var(--row-divider, #1d1d20);
21
+ background: var(--surface-panel, #151517);
22
+ color: var(--text-1, #e4e4e7);
23
+ }
24
+
25
+ .v2-outliner .hierarchy-heading strong {
26
+ margin: 0;
27
+ color: var(--text-1, #e4e4e7);
28
+ font-size: var(--outliner-title-size, 12px);
29
+ font-weight: 500;
30
+ letter-spacing: 0;
31
+ }
32
+
33
+ .v2-outliner .hierarchy-kicker,
34
+ .v2-outliner .hierarchy-frame-status {
35
+ display: none;
36
+ }
37
+
38
+ .v2-outliner .v2-outliner-count {
39
+ color: var(--text-3, #636369);
40
+ font-family: var(--mono, 'JetBrains Mono');
41
+ font-size: var(--outliner-secondary-size, 11px);
42
+ font-weight: 400;
43
+ }
44
+
45
+ .v2-outliner .v2-outliner-search-wrap {
46
+ position: relative;
47
+ display: flex;
48
+ align-items: center;
49
+ height: var(--h-control, 28px);
50
+ min-height: var(--h-control, 28px);
51
+ margin: 0 var(--sp-2, 12px) var(--sp-1, 8px);
52
+ border-radius: var(--r-1, 4px);
53
+ background: var(--field, #0f0f11);
54
+ color: var(--text-3, #636369);
55
+ }
56
+
57
+ .v2-outliner .v2-outliner-search-icon {
58
+ position: absolute;
59
+ left: var(--sp-1, 8px);
60
+ color: var(--text-3, #636369);
61
+ font-size: var(--outliner-search-icon-size, 14px);
62
+ line-height: 1;
63
+ pointer-events: none;
64
+ }
65
+
66
+ .v2-outliner .v2-outliner-search {
67
+ width: 100%;
68
+ height: var(--h-control, 28px);
69
+ min-height: var(--h-control, 28px);
70
+ padding: 0 var(--sp-1, 8px) 0 var(--sp-3, 16px);
71
+ border: 0;
72
+ border-radius: var(--r-1, 4px);
73
+ outline: 0;
74
+ background: none;
75
+ color: var(--text-1, #e4e4e7);
76
+ font: inherit;
77
+ font-size: var(--outliner-secondary-size, 11px);
78
+ }
79
+
80
+ .v2-outliner .v2-outliner-search::placeholder {
81
+ color: var(--text-3, #636369);
82
+ opacity: 1;
83
+ }
84
+
85
+ .v2-outliner .v2-outliner-search:focus-visible {
86
+ outline: var(--outliner-border-width, 1px) solid var(--select, #e8a33d);
87
+ outline-offset: calc(var(--outliner-border-width, 1px) * -1);
88
+ }
89
+
90
+ .v2-outliner .hierarchy-tree {
91
+ flex: 1 1 auto;
92
+ min-height: 0;
93
+ padding: 0 var(--sp-1, 8px) var(--sp-1, 8px);
94
+ overflow: auto;
95
+ background: var(--surface-panel, #151517);
96
+ }
97
+
98
+ .v2-outliner .hierarchy-row-wrap {
99
+ position: relative;
100
+ height: var(--h-row, 24px);
101
+ min-height: var(--h-row, 24px);
102
+ margin-left: calc(var(--hierarchy-depth, 0) * var(--outliner-indent, 14px));
103
+ padding-right: 0;
104
+ border-radius: var(--r-1, 4px);
105
+ background: none;
106
+ }
107
+
108
+ .v2-outliner .hierarchy-row-wrap:hover {
109
+ background: var(--surface-raised, #1e1e21);
110
+ }
111
+
112
+ .v2-outliner .hierarchy-row-wrap.selected {
113
+ background: var(--select-tint, rgba(232,163,61,.14));
114
+ box-shadow: none;
115
+ }
116
+
117
+ .v2-outliner .hierarchy-row-wrap[data-own-hidden="true"] {
118
+ opacity: .52;
119
+ }
120
+
121
+ .v2-outliner .hierarchy-row-wrap:before {
122
+ display: none;
123
+ }
124
+
125
+ .v2-outliner .hierarchy-toggle,
126
+ .v2-outliner .hierarchy-fold-space {
127
+ flex: 0 0 var(--outliner-chevron-slot, 12px);
128
+ width: var(--outliner-chevron-slot, 12px);
129
+ height: var(--h-row, 24px);
130
+ padding: 0;
131
+ border: 0;
132
+ background: none;
133
+ }
134
+
135
+ .v2-outliner .hierarchy-toggle {
136
+ display: grid;
137
+ place-items: center;
138
+ color: var(--text-3, #636369);
139
+ font-size: var(--outliner-chevron-size, 8px);
140
+ font-weight: 500;
141
+ line-height: 1;
142
+ cursor: pointer;
143
+ }
144
+
145
+ .v2-outliner .hierarchy-toggle:hover,
146
+ .v2-outliner .hierarchy-toggle:focus-visible {
147
+ color: var(--text-1, #e4e4e7);
148
+ }
149
+
150
+ .v2-outliner .hierarchy-toggle.placeholder,
151
+ .v2-outliner .hierarchy-fold-space {
152
+ pointer-events: none;
153
+ }
154
+
155
+ .v2-outliner .hierarchy-row {
156
+ width: 100%;
157
+ min-width: 0;
158
+ height: var(--h-row, 24px);
159
+ padding: 0 var(--sp-1, 8px) 0 0;
160
+ border: 0;
161
+ background: none;
162
+ color: var(--text-row, #c9c9ce);
163
+ font: inherit;
164
+ font-size: var(--outliner-ui-size, 12px);
165
+ font-weight: 400;
166
+ text-align: left;
167
+ cursor: default;
168
+ }
169
+
170
+ .v2-outliner .hierarchy-row:not(.icon-only) {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: var(--sp-1, 8px);
174
+ }
175
+
176
+ .v2-outliner .hierarchy-row.icon-only {
177
+ flex: 0 0 auto;
178
+ display: inline-flex;
179
+ align-items: center;
180
+ gap: var(--sp-1, 8px);
181
+ width: auto;
182
+ }
183
+
184
+ .v2-outliner .v2-outliner-chip {
185
+ flex: 0 0 var(--outliner-chip-size, 6px);
186
+ width: var(--outliner-chip-size, 6px);
187
+ height: var(--outliner-chip-size, 6px);
188
+ border-radius: var(--outliner-chip-radius, 2px);
189
+ background: var(--chip-folder, #8a7a55);
190
+ }
191
+
192
+ .v2-outliner .v2-outliner-chip.folder { background: var(--chip-folder, #8a7a55); }
193
+ .v2-outliner .v2-outliner-chip.mesh { background: var(--chip-mesh, #7e8b99); }
194
+ .v2-outliner .v2-outliner-chip.cast { background: var(--chip-cast, #5fb584); }
195
+ .v2-outliner .v2-outliner-chip.cam { background: var(--camera, #a78bfa); }
196
+ .v2-outliner .v2-outliner-chip.light { background: var(--chip-light, #d9b84a); }
197
+
198
+ .v2-outliner .hierarchy-label {
199
+ flex: 1 1 auto;
200
+ min-width: 0;
201
+ overflow: hidden;
202
+ color: var(--text-row, #c9c9ce);
203
+ font-size: var(--outliner-ui-size, 12px);
204
+ font-weight: 400;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+
209
+ .v2-outliner .hierarchy-row-wrap.selected .hierarchy-label {
210
+ color: var(--text-1, #e4e4e7);
211
+ font-weight: 500;
212
+ }
213
+
214
+ .v2-outliner .v2-outliner-type {
215
+ flex: 0 0 auto;
216
+ margin-left: auto;
217
+ color: var(--text-off, #4f4f55);
218
+ font-size: var(--outliner-type-size, 10.5px);
219
+ font-weight: 400;
220
+ white-space: nowrap;
221
+ }
222
+
223
+ .v2-outliner .hierarchy-status,
224
+ .v2-outliner .hierarchy-badge {
225
+ display: none;
226
+ }
227
+
228
+ .v2-outliner .hierarchy-eye {
229
+ flex: 0 0 var(--outliner-eye-closed-width, 0px);
230
+ width: var(--outliner-eye-closed-width, 0px);
231
+ height: var(--h-row, 24px);
232
+ padding: 0;
233
+ border: 0;
234
+ background: none;
235
+ color: var(--text-2, #9b9ba1);
236
+ opacity: 0;
237
+ overflow: hidden;
238
+ }
239
+
240
+ .v2-outliner .hierarchy-eye:focus-visible {
241
+ flex-basis: var(--outliner-eye-width, 24px);
242
+ width: var(--outliner-eye-width, 24px);
243
+ opacity: 1;
244
+ outline: var(--outliner-border-width, 1px) solid var(--select, #e8a33d);
245
+ }
246
+
247
+ .v2-outliner .hierarchy-row-wrap[data-drop="over"] {
248
+ border: var(--outliner-border-width, 1px) dashed var(--select, #e8a33d);
249
+ background: var(--select-tint, rgba(232,163,61,.14));
250
+ }
251
+
252
+ .v2-outliner .hierarchy-row-wrap[data-drop="target"] {
253
+ border: var(--outliner-border-width, 1px) solid var(--row-divider, #1d1d20);
254
+ }
255
+
256
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill {
257
+ flex: 1 1 auto;
258
+ min-width: 0;
259
+ max-width: none;
260
+ height: var(--h-row, 24px);
261
+ margin: 0 var(--sp-1, 8px) 0 var(--sp-1, 8px);
262
+ }
263
+
264
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .dropdown,
265
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .trigger {
266
+ width: 100%;
267
+ min-width: 0;
268
+ }
269
+
270
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .trigger {
271
+ height: var(--h-row, 24px);
272
+ gap: var(--outliner-trigger-gap, 6px);
273
+ padding: 0 var(--sp-1, 8px);
274
+ border: 0;
275
+ border-radius: var(--r-1, 4px);
276
+ background: var(--surface-active, #26262a);
277
+ color: var(--text-row, #c9c9ce);
278
+ font: inherit;
279
+ font-size: var(--outliner-ui-size, 12px);
280
+ text-align: left;
281
+ }
282
+
283
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .trigger:hover,
284
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .dropdown.open .trigger {
285
+ background: var(--surface-raised, #1e1e21);
286
+ color: var(--text-1, #e4e4e7);
287
+ }
288
+
289
+ .v2-outliner .hierarchy-row-wrap .hierarchy-scene-pill .dd-caret {
290
+ flex: 0 0 var(--outliner-chevron-slot, 12px);
291
+ width: var(--outliner-chevron-slot, 12px);
292
+ height: var(--outliner-chevron-slot, 12px);
293
+ color: var(--text-3, #636369);
294
+ }
295
+
296
+ .v2-outliner .hierarchy-scene-hint {
297
+ flex: none;
298
+ margin: 0;
299
+ padding: 0 var(--sp-2, 12px) var(--sp-1, 8px);
300
+ color: var(--text-2, #9b9ba1);
301
+ font-size: var(--outliner-secondary-size, 11px);
302
+ }
303
+
304
+ .v2-outliner .v2-outliner-empty {
305
+ min-height: var(--h-row, 24px);
306
+ padding: var(--sp-1, 8px);
307
+ color: var(--text-3, #636369);
308
+ font-size: var(--outliner-secondary-size, 11px);
309
+ }
310
+
311
+ .v2-outliner .hierarchy-rename-input {
312
+ flex: 1 1 auto;
313
+ min-width: 0;
314
+ height: var(--h-control, 28px);
315
+ padding: 0 var(--sp-1, 8px);
316
+ border: 0;
317
+ border-radius: var(--r-1, 4px);
318
+ background: var(--field, #0f0f11);
319
+ color: var(--text-1, #e4e4e7);
320
+ font: inherit;
321
+ font-size: var(--outliner-ui-size, 12px);
322
+ }
323
+
324
+ .v2-outliner .hierarchy-rename-input:focus-visible {
325
+ outline: var(--outliner-border-width, 1px) solid var(--select, #e8a33d);
326
+ }
327
+
328
+ .v2-outliner .hierarchy-context-menu {
329
+ min-width: var(--outliner-menu-width, 220px);
330
+ padding: var(--outliner-menu-padding, 4px);
331
+ border: var(--outliner-border-width, 1px) solid var(--row-divider, #1d1d20);
332
+ border-radius: var(--r-2, 6px);
333
+ background: var(--surface-raised, #1e1e21);
334
+ box-shadow: 0 var(--sp-1, 8px) var(--sp-3, 16px) var(--overlay, rgba(12,12,13,.78));
335
+ }
336
+
337
+ .v2-outliner .hierarchy-context-item {
338
+ min-height: var(--h-control, 28px);
339
+ padding: 0 var(--sp-1, 8px);
340
+ border: 0;
341
+ border-radius: var(--r-1, 4px);
342
+ background: none;
343
+ color: var(--text-row, #c9c9ce);
344
+ font: inherit;
345
+ font-size: var(--outliner-secondary-size, 11px);
346
+ text-align: left;
347
+ }
348
+
349
+ .v2-outliner .hierarchy-context-item:hover,
350
+ .v2-outliner .hierarchy-context-item:focus-visible {
351
+ background: var(--surface-active, #26262a);
352
+ color: var(--text-1, #e4e4e7);
353
+ }
354
+
355
+ .v2-outliner .hierarchy-context-item.danger {
356
+ color: var(--danger, #e5484d);
357
+ }
358
+
359
+ .v2-outliner .add-object-item {
360
+ min-height: var(--h-control, 28px);
361
+ padding: 0 var(--sp-1, 8px);
362
+ border: 0;
363
+ border-radius: var(--r-1, 4px);
364
+ background: none;
365
+ color: var(--text-row, #c9c9ce);
366
+ font: inherit;
367
+ font-size: var(--outliner-secondary-size, 11px);
368
+ }
369
+
370
+ .v2-outliner .add-object-item:hover,
371
+ .v2-outliner .add-object-item:focus-visible {
372
+ background: var(--surface-active, #26262a);
373
+ color: var(--text-1, #e4e4e7);
374
+ }
375
+
376
+ .v2-outliner .add-object-heading {
377
+ padding: var(--sp-1, 8px) var(--sp-1, 8px) calc(var(--sp-1, 8px) / 2);
378
+ color: var(--text-3, #636369);
379
+ font-size: var(--outliner-type-size, 10.5px);
380
+ }
381
+
382
+ @media (max-width: 700px) {
383
+ .v2-outliner .v2-outliner-type {
384
+ display: none;
385
+ }
386
+ }
@@ -2,8 +2,9 @@ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { ko, isKo } from "./locale.js";
3
3
  import { buildHierarchyNodes } from "./hierarchy-model.js";
4
4
  import { sceneObjectIdFromHierarchy } from "./scene-objects.js";
5
- import AddObjectMenu, { CatalogueEntries, displayObjectLabel } from "./object-catalog.jsx";
5
+ import { CatalogueEntries, displayObjectLabel } from "./object-catalog.jsx";
6
6
  import { Dropdown } from "./ui.jsx";
7
+ import "./hierarchy-panel.css";
7
8
 
8
9
  const HIERARCHY_LABELS_KO = {
9
10
  "SCENE 01": "장면 01",
@@ -35,6 +36,8 @@ const HIERARCHY_LABELS_KO = {
35
36
  Environment: "환경",
36
37
  Props: "소품",
37
38
  Light: "조명",
39
+ "Scene structure": "장면 구조",
40
+ "Scene hierarchy": "장면 계층",
38
41
  };
39
42
 
40
43
  const FALLBACK_SCENES = [{ id: "current-scene", name: "SCENE 01" }];
@@ -198,6 +201,57 @@ function displayHierarchyLabel(node) {
198
201
  return isKo ? (HIERARCHY_LABELS_KO[node.label] ?? node.label) : node.label;
199
202
  }
200
203
 
204
+ const OUTLINER_TYPES = {
205
+ folder: "Folder",
206
+ mesh: "Mesh",
207
+ cast: "Cast",
208
+ cam: "Cam",
209
+ light: "Light",
210
+ };
211
+
212
+ function outlinerTypeFor(node) {
213
+ const key = node.kind === "object"
214
+ ? "mesh"
215
+ : node.kind === "character" || node.kind === "bone"
216
+ ? "cast"
217
+ : node.kind === "camera"
218
+ ? "cam"
219
+ : node.kind === "light"
220
+ ? "light"
221
+ : "folder";
222
+ const label = key === "folder"
223
+ ? ko(OUTLINER_TYPES[key], "폴더")
224
+ : key === "mesh"
225
+ ? ko(OUTLINER_TYPES[key], "메시")
226
+ : key === "cast"
227
+ ? ko(OUTLINER_TYPES[key], "캐스트")
228
+ : key === "cam"
229
+ ? ko(OUTLINER_TYPES[key], "카메라")
230
+ : ko(OUTLINER_TYPES[key], "조명");
231
+ return { key, label };
232
+ }
233
+
234
+ function OutlinerChip({ type }) {
235
+ return <span className={`v2-outliner-chip ${type}`} aria-hidden="true" />;
236
+ }
237
+
238
+ function countHierarchyNodes(nodes) {
239
+ return nodes.reduce((count, node) => count + 1 + countHierarchyNodes(node.children ?? []), 0);
240
+ }
241
+
242
+ function filterHierarchyNodes(nodes, query, showB) {
243
+ const normalized = query.trim().toLocaleLowerCase();
244
+ const filterNode = (node) => {
245
+ if (node.optional === "showB" && !showB) return null;
246
+ if (!normalized) return node;
247
+ const label = displayHierarchyLabel(node).toLocaleLowerCase();
248
+ const children = (node.children ?? []).map(filterNode).filter(Boolean);
249
+ if (!label.includes(normalized) && children.length === 0) return null;
250
+ return children.length ? { ...node, children } : { ...node, children: undefined };
251
+ };
252
+ return nodes.map(filterNode).filter(Boolean);
253
+ }
254
+
201
255
  function indexParents(nodes, parent = null, parents = new Map()) {
202
256
  for (const node of nodes) {
203
257
  if (parent) parents.set(node.id, parent);
@@ -210,6 +264,15 @@ function indexParents(nodes, parent = null, parents = new Map()) {
210
264
  * (Rename / Duplicate / Delete / Frame) or the create catalogue, matching
211
265
  * Unity's Hierarchy right-click menu (docs/unity-reference.md §9.7).
212
266
  * Closes on Escape, on any outside mousedown, and after a pick. */
267
+ function findHierarchyNode(nodes, id) {
268
+ for (const node of nodes ?? []) {
269
+ if (node.id === id) return node;
270
+ const nested = findHierarchyNode(node.children, id);
271
+ if (nested) return nested;
272
+ }
273
+ return null;
274
+ }
275
+
213
276
  function RowContextMenu({ menu, onClose, onAction, onAddObject }) {
214
277
  const rootRef = useRef(null);
215
278
  // Deleting a scene document is not undoable, so Delete arms first and only
@@ -275,8 +338,15 @@ function RowContextMenu({ menu, onClose, onAction, onAddObject }) {
275
338
  {ko("+ New scene", "+ 새 장면")}
276
339
  </button>
277
340
  </>
341
+ ) : menu.kind === "character" ? (
342
+ <button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("visibility", menu.id)}>
343
+ {menu.hidden ? ko("Show", "표시") : ko("Hide", "숨기기")}
344
+ </button>
278
345
  ) : menu.kind === "object" ? (
279
346
  <>
347
+ <button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("visibility", menu.id)}>
348
+ {menu.hidden ? ko("Show", "표시") : ko("Hide", "숨기기")}
349
+ </button>
280
350
  <button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("rename", menu.id)}>
281
351
  {ko("Rename", "이름 바꾸기")}
282
352
  </button>
@@ -310,6 +380,7 @@ function TreeRow({
310
380
  onRenameCommit,
311
381
  onRenameCancel,
312
382
  onRowContextMenu,
383
+ onToggleHidden,
313
384
  onRenameStart,
314
385
  // The scene root row hands its name to the pill in `rowExtra`; printing it
315
386
  // twice on one row is the duplication this panel just removed.
@@ -361,6 +432,14 @@ function TreeRow({
361
432
  ...(draggableRow
362
433
  ? {
363
434
  onDragStart: (event) => {
435
+ // The eye and the fold caret sit inside the draggable row.
436
+ // A press on either must not start a move, or the click is
437
+ // swallowed and a small slip reparents the row.
438
+ if (event.target instanceof Element && event.target.closest(".hierarchy-eye, .hierarchy-toggle")) {
439
+ event.preventDefault();
440
+ event.stopPropagation();
441
+ return;
442
+ }
364
443
  event.stopPropagation();
365
444
  event.dataTransfer.setData(HIERARCHY_DRAG_MIME, node.id);
366
445
  event.dataTransfer.effectAllowed = "move";
@@ -410,6 +489,7 @@ function TreeRow({
410
489
  // only exists when the caller asked for reparenting.
411
490
  const dropEvents = rowDrag || drop || null;
412
491
  const label = displayHierarchyLabel(node);
492
+ const outlinerType = outlinerTypeFor(node);
413
493
  const rowWrapRef = useRef(null);
414
494
  const inputRef = useRef(null);
415
495
  // A commit/cancel may race the blur that follows the input unmounting;
@@ -441,6 +521,7 @@ function TreeRow({
441
521
  className={"hierarchy-row-wrap" + (selectedId === node.id ? " selected" : "")}
442
522
  style={{ "--hierarchy-depth": depth }}
443
523
  data-node-id={node.id}
524
+ data-own-hidden={node.hidden ? "true" : undefined}
444
525
  data-drop={drop || rowDropTarget ? (dropOver || rowDropOver ? "over" : "target") : undefined}
445
526
  draggable={draggableRow || undefined}
446
527
  {...(dropEvents ?? {})}
@@ -469,7 +550,7 @@ function TreeRow({
469
550
  // blur commits (docs/unity-reference.md §9.7). A div, not the
470
551
  // row button — an input inside a button is invalid HTML.
471
552
  <div className={"hierarchy-row" + (touched ? " agent-touched" : "")}>
472
- <HierarchyIcon kind={node.kind} />
553
+ <OutlinerChip type={outlinerType.key} />
473
554
  <input
474
555
  ref={inputRef}
475
556
  className="hierarchy-rename-input"
@@ -498,10 +579,31 @@ function TreeRow({
498
579
  onClick={() => onSelect(node.id)}
499
580
  onDoubleClick={onRenameStart ?? undefined}
500
581
  >
501
- <HierarchyIcon kind={node.kind} />
582
+ <OutlinerChip type={outlinerType.key} />
502
583
  {showLabel && <span className="hierarchy-label">{label}</span>}
503
584
  {status && <span className="hierarchy-status">{status}</span>}
504
585
  {badge !== null && badge !== undefined && badge !== 0 && <span className="hierarchy-badge">{badge}</span>}
586
+ <span className="v2-outliner-type">{outlinerType.label}</span>
587
+ </button>
588
+ )}
589
+ {(node.kind === "object" || node.kind === "character") && onToggleHidden && (
590
+ <button
591
+ type="button"
592
+ className="hierarchy-eye"
593
+ data-hidden={node.hidden ? "true" : undefined}
594
+ aria-pressed={node.hidden === true}
595
+ aria-label={node.hidden ? (isKo ? `${label} 표시` : `Show ${label}`) : (isKo ? `${label} 숨기기` : `Hide ${label}`)}
596
+ onClick={(event) => {
597
+ event.preventDefault();
598
+ event.stopPropagation();
599
+ onToggleHidden(node.id);
600
+ }}
601
+ >
602
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
603
+ <path d="M1.5 8s2.5-4 6.5-4 6.5 4 6.5 4-2.5 4-6.5 4S1.5 8 1.5 8z" fill="none" stroke="currentColor" strokeWidth="1.3" />
604
+ <circle cx="8" cy="8" r="1.7" fill="currentColor" />
605
+ {node.hidden && <path d="M3 13 13 3" stroke="currentColor" strokeWidth="1.3" />}
606
+ </svg>
505
607
  </button>
506
608
  )}
507
609
  {rowExtra}
@@ -525,6 +627,7 @@ export default function HierarchyPanel({
525
627
  onDuplicateObject,
526
628
  onDeleteObject,
527
629
  onFrameObject,
630
+ onToggleHidden,
528
631
  propsDrop = null,
529
632
  reparent = null,
530
633
  // Hierarchy ids an agent receipt just changed. The host owns how long they
@@ -539,6 +642,7 @@ export default function HierarchyPanel({
539
642
  onSceneDelete,
540
643
  }) {
541
644
  const [expanded, setExpanded] = useState(() => new Set(["shot", "characterA"]));
645
+ const [search, setSearch] = useState("");
542
646
  const [contextMenu, setContextMenu] = useState(null);
543
647
  // Row currently in in-place rename. The panel owns it: F2/Return and the
544
648
  // row context menu are the only ways in, so app state stays out of it.
@@ -574,6 +678,14 @@ export default function HierarchyPanel({
574
678
  }));
575
679
  }, [activeSceneName, sceneObjects, characters]);
576
680
  const parents = useMemo(() => indexParents(hierarchyNodes), [hierarchyNodes]);
681
+ const filteredHierarchyNodes = useMemo(
682
+ () => filterHierarchyNodes(hierarchyNodes, search, showB),
683
+ [hierarchyNodes, search, showB],
684
+ );
685
+ const outlinerCount = useMemo(
686
+ () => countHierarchyNodes(filteredHierarchyNodes),
687
+ [filteredHierarchyNodes],
688
+ );
577
689
 
578
690
  useEffect(() => {
579
691
  setExpanded((current) => {
@@ -650,8 +762,16 @@ export default function HierarchyPanel({
650
762
  const openRowMenu = (event, id) => {
651
763
  event.preventDefault();
652
764
  event.stopPropagation(); // a row pick must not also open the create menu
653
- if (sceneObjectIdFromHierarchy(id) !== null) {
654
- setContextMenu({ x: event.clientX, y: event.clientY, height: 148, kind: "object", id });
765
+ const node = findHierarchyNode(hierarchyNodes, id);
766
+ if (node?.kind === "object" || node?.kind === "character") {
767
+ setContextMenu({
768
+ x: event.clientX,
769
+ y: event.clientY,
770
+ height: node.kind === "object" ? 180 : 44,
771
+ kind: node.kind,
772
+ id,
773
+ hidden: node.hidden === true,
774
+ });
655
775
  } else if (id === SCENE_ROOT_ID) {
656
776
  // The root row is the scene document: its own verbs, never the
657
777
  // Add-Object catalogue.
@@ -700,6 +820,10 @@ export default function HierarchyPanel({
700
820
  onSceneCreate?.();
701
821
  return;
702
822
  }
823
+ if (action === "visibility") {
824
+ onToggleHidden?.(hierarchyId);
825
+ return;
826
+ }
703
827
  const objectId = sceneObjectIdFromHierarchy(hierarchyId);
704
828
  if (!objectId) return;
705
829
  if (action === "duplicate") onDuplicateObject?.(objectId);
@@ -736,9 +860,9 @@ export default function HierarchyPanel({
736
860
  nodes.flatMap((node) => {
737
861
  if (node.optional === "showB" && !showB) return [];
738
862
  const sceneRoot = node.id === SCENE_ROOT_ID;
739
- // The root carries no fold caret, so nothing can close it: the scene is
740
- // always open under its own name.
741
- const open = sceneRoot || expanded.has(node.id);
863
+ // Search opens only the filtered branches. The stored expansion set is
864
+ // left untouched so clearing the query restores the author's tree.
865
+ const open = sceneRoot || expanded.has(node.id) || Boolean(search.trim());
742
866
  const editing = editingId === node.id;
743
867
  return [
744
868
  <TreeRow
@@ -756,6 +880,7 @@ export default function HierarchyPanel({
756
880
  onRenameCommit={(name) => commitRename(node.id, name)}
757
881
  onRenameCancel={cancelRename}
758
882
  onRowContextMenu={openRowMenu}
883
+ onToggleHidden={onToggleHidden}
759
884
  // The root row names the scene document, and a double-click on a
760
885
  // document name is where every file browser puts rename.
761
886
  onRenameStart={sceneRoot ? () => setEditingId(node.id) : null}
@@ -786,22 +911,26 @@ export default function HierarchyPanel({
786
911
  });
787
912
 
788
913
  return (
789
- <section className="hierarchy-pane" aria-label={ko("Scene hierarchy", "장면 계층")}>
914
+ <section className="hierarchy-pane v2-outliner" aria-label={ko("Outliner", "아웃라이너")}>
790
915
  <div className="hierarchy-heading">
791
- <div>
792
- <span className="hierarchy-kicker">{ko("Hierarchy", "계층")}</span>
793
- <strong>{ko("Scene structure", "장면 구조")}</strong>
794
- </div>
795
- <span className="hierarchy-frame-status">{motionFrames ? (isKo ? `${motionFrames}프레임` : `${motionFrames} frames`) : ko("Blocking", "블로킹")}</span>
916
+ <strong>{ko("Outliner", "아웃라이너")}</strong>
917
+ <span className="v2-outliner-count" data-testid="outliner-count">{outlinerCount}</span>
796
918
  </div>
797
- {onAddObject && (
798
- <div className="hierarchy-toolbar">
799
- <AddObjectMenu onAdd={onAddObject} />
800
- <span className="hierarchy-frame-status">{motionFrames ? (isKo ? `${motionFrames}프레임` : `${motionFrames} frames`) : ko("Blocking", "블로킹")}</span>
801
- </div>
802
- )}
919
+ <label className="v2-outliner-search-wrap">
920
+ <span className="v2-outliner-search-icon" aria-hidden="true">⌕</span>
921
+ <input
922
+ className="v2-outliner-search"
923
+ type="search"
924
+ value={search}
925
+ placeholder={ko("Search", "검색")}
926
+ aria-label={ko("Search Outliner", "아웃라이너 검색")}
927
+ onChange={(event) => setSearch(event.target.value)}
928
+ />
929
+ </label>
803
930
  <div className="hierarchy-tree" role="tree" ref={treeRef} onKeyDown={onTreeKeyDown} onContextMenu={openCreateMenu}>
804
- {renderNodes(hierarchyNodes)}
931
+ {filteredHierarchyNodes.length > 0 ? renderNodes(filteredHierarchyNodes) : (
932
+ <div className="v2-outliner-empty" role="status">{ko("No matches", "일치하는 항목 없음")}</div>
933
+ )}
805
934
  </div>
806
935
  {sceneHint && (
807
936
  <p className="hierarchy-scene-hint" role="status">{sceneHint}</p>