@pascal-app/editor 1.0.6 → 1.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 (382) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -1,16 +1,11 @@
1
1
  'use client'
2
2
 
3
- import { Icon } from '@iconify/react'
4
3
  import {
5
4
  type AnyNode,
6
5
  type AnyNodeId,
7
- getInspectorExtensions,
8
- type IconRef,
9
6
  type InspectorExtension,
10
- useRegistryVersion,
11
- useScene,
7
+ useScene, usePascalI18n,
12
8
  } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
14
9
  import { ChevronDown, ChevronLeft, GripHorizontal, RotateCcw, X } from 'lucide-react'
15
10
  import {
16
11
  type ComponentType,
@@ -21,18 +16,12 @@ import {
21
16
  useContext,
22
17
  useEffect,
23
18
  useLayoutEffect,
24
- useMemo,
25
19
  useRef,
26
20
  useState,
27
21
  } from 'react'
28
22
  import { useIsMobile } from '../../../hooks/use-mobile'
29
- import { IconRefImage } from '../icon-ref'
30
23
  import { NodeIconImage } from '../node-icon-image'
31
- import {
32
- resolveActiveExtension,
33
- toggleCard,
34
- toggleExtension,
35
- } from '../../../lib/inspector-card-mode'
24
+ import { resolveActiveExtension, toggleCard } from '../../../lib/inspector-card-mode'
36
25
  import { cn } from '../../../lib/utils'
37
26
  import { useInspectorExpanded } from '../../../lib/inspector-expanded'
38
27
  import { ActionButton, ActionGroup } from '../controls/action-button'
@@ -40,6 +29,7 @@ import { PanelSection } from '../controls/panel-section'
40
29
  import { ErrorBoundary } from '../primitives/error-boundary'
41
30
  import { useInRightStack } from '../right-stack'
42
31
  import { CollectionsPopover } from './collections/collections-popover'
32
+ import { renderExtensionIcon, useInspectorExtensions } from './inspector-extensions'
43
33
 
44
34
  const DRAG_MARGIN = 8
45
35
  // Pointer travel (px) below which a header press is treated as a click
@@ -55,7 +45,7 @@ function clamp(value: number, min: number, max: number): number {
55
45
  * `data-viewer-bounds`) so it can't slide under the sidebar or top bar.
56
46
  * Falls back to the viewport when the marker isn't found.
57
47
  */
58
- function getDragBounds(el: HTMLElement | null): {
48
+ function getDragBounds(el: HTMLElement | null, inStack = false): {
59
49
  left: number
60
50
  top: number
61
51
  right: number
@@ -66,7 +56,15 @@ function getDragBounds(el: HTMLElement | null): {
66
56
  if (!rect) {
67
57
  return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }
68
58
  }
69
- return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
59
+ const stack = inStack ? el?.closest('[data-right-stack]') : null
60
+ const stackRect = stack?.getBoundingClientRect()
61
+ const cardRect = stack?.querySelector('[data-right-stack-card]')?.getBoundingClientRect()
62
+ return {
63
+ left: rect.left,
64
+ top: stackRect ? stackRect.top - DRAG_MARGIN : rect.top,
65
+ right: rect.right,
66
+ bottom: cardRect && cardRect.height > 0 ? cardRect.top : rect.bottom,
67
+ }
70
68
  }
71
69
 
72
70
  /**
@@ -119,6 +117,7 @@ export function PanelWrapper({
119
117
  className,
120
118
  width = 320, // default width
121
119
  }: PanelWrapperProps) {
120
+ const i18n = usePascalI18n()
122
121
  const isMobile = useIsMobile()
123
122
  const inStack = useInRightStack()
124
123
  const contextFooter = useContext(InspectorFooterContext)
@@ -131,35 +130,16 @@ export function PanelWrapper({
131
130
  // The wrapper self-resolves the selected node instead of taking a prop:
132
131
  // kind-owned `customPanel`s (wall, slab, …) render their own
133
132
  // <PanelWrapper>, so this is the one spot every inspector card flows
134
- // through. Extensions only apply to a single-node selection.
135
- const registryVersion = useRegistryVersion()
136
- const selectedId = useViewer((s) =>
137
- s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : undefined,
138
- ) as AnyNodeId | undefined
139
- // Subscribe to the selected node's *type* only — a string primitive that
140
- // doesn't change as fields are edited (same trick as ParametricInspector).
141
- const selectedType = useScene((s) => (selectedId ? s.nodes[selectedId]?.type ?? null : null))
142
- const installedPlugins = useScene((s) => s.installedPlugins)
143
- const extensions = useMemo(() => {
144
- // re-derive when plugin extensions register after mount (async plugin load)
145
- void registryVersion
146
- if (!selectedType) return []
147
- return getInspectorExtensions(selectedType).filter(
148
- (extension) => !extension.pluginId || installedPlugins.includes(extension.pluginId),
149
- )
150
- }, [selectedType, installedPlugins, registryVersion])
133
+ // through. Extensions only apply to a single-node selection; the action
134
+ // menu opens them (`InspectorExtensionActions`).
135
+ const { selectedId, extensions, primaryExtension } = useInspectorExtensions()
151
136
 
152
137
  // Which extension's content fills the card body (extension mode). The two
153
138
  // expanded modes are EITHER/OR: extension mode replaces the regular
154
139
  // controls; null shows the regular controls with no extension sections.
155
- // See `lib/inspector-card-mode.ts` for the transition table.
156
- const [activeExtensionId, setActiveExtensionId] = useState<string | null>(null)
157
140
  // Stale ids (kind changed, plugin gated off) fall back to regular mode.
158
- const primaryExtensionId = useScene((state) => {
159
- const node = selectedId ? state.nodes[selectedId] : undefined
160
- return node ? extensions.find((extension) => extension.primaryWhen?.(node))?.id : undefined
161
- })
162
- const primaryExtension = extensions.find((extension) => extension.id === primaryExtensionId)
141
+ // See `lib/inspector-card-mode.ts` for the transition table.
142
+ const activeExtensionId = useInspectorExpanded((state) => state.activeExtensionId)
163
143
  const activeExtension = primaryExtension ?? resolveActiveExtension(activeExtensionId, extensions)
164
144
 
165
145
  // The panel is collapsed to just its header until the user expands it. The
@@ -174,14 +154,6 @@ export function PanelWrapper({
174
154
  if (primaryExtension) setCollapsed(false)
175
155
  }, [primaryExtension, selectedId, setCollapsed])
176
156
 
177
- const applyMode = useCallback(
178
- (next: { collapsed: boolean; activeExtensionId: string | null }) => {
179
- setCollapsed(next.collapsed)
180
- setActiveExtensionId(next.activeExtensionId)
181
- },
182
- [setCollapsed],
183
- )
184
-
185
157
  // Chevron / header press — collapsed → regular, regular → collapsed,
186
158
  // extension mode → regular (exit the extension first, stay expanded).
187
159
  const handleCardToggle = useCallback(() => {
@@ -189,14 +161,12 @@ export function PanelWrapper({
189
161
  setCollapsed(!collapsed)
190
162
  return
191
163
  }
192
- applyMode(toggleCard({ collapsed, activeExtensionId }))
193
- }, [applyMode, collapsed, activeExtensionId, primaryExtension, setCollapsed])
164
+ useInspectorExpanded.getState().applyMode(toggleCard({ collapsed, activeExtensionId }))
165
+ }, [collapsed, activeExtensionId, primaryExtension, setCollapsed])
194
166
 
195
- // Folding the card forgets the active extension — extension mode is a
196
- // one-shot affordance of the header icon, not sticky panel state.
197
- useEffect(() => {
198
- if (collapsed) setActiveExtensionId(null)
199
- }, [collapsed])
167
+ // Extension mode is an affordance of the card it was opened on: a card that
168
+ // closes (selection cleared or changed kind) leaves the next one regular.
169
+ useEffect(() => () => useInspectorExpanded.setState({ activeExtensionId: null }), [])
200
170
 
201
171
  // Drag-to-reposition from the header. `offset` is a translation applied on
202
172
  // top of the default `top-20 right-4` anchor; null until first dragged.
@@ -225,7 +195,7 @@ export function PanelWrapper({
225
195
  if ((e.target as HTMLElement).closest('button, input, label')) return
226
196
  const rect = panelRef.current?.getBoundingClientRect()
227
197
  if (!rect) return
228
- const bounds = getDragBounds(panelRef.current)
198
+ const bounds = getDragBounds(panelRef.current, inStack)
229
199
  const base = offset ?? { x: 0, y: 0 }
230
200
  dragRef.current = {
231
201
  startX: e.clientX,
@@ -245,7 +215,7 @@ export function PanelWrapper({
245
215
  setIsDragging(true)
246
216
  e.currentTarget.setPointerCapture(e.pointerId)
247
217
  },
248
- [offset],
218
+ [offset, inStack],
249
219
  )
250
220
 
251
221
  const handleHeaderPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
@@ -276,22 +246,37 @@ export function PanelWrapper({
276
246
  [handleCardToggle],
277
247
  )
278
248
 
279
- // Expanding can grow the panel past an edge if it was dragged there while
280
- // collapsed — nudge it back inside the viewer bounds.
249
+ // Keep repositioned panels inside the available space as the card or viewer resizes.
281
250
  useLayoutEffect(() => {
282
- if (isMobile || collapsed) return
251
+ if (isMobile) return
283
252
  const el = panelRef.current
284
253
  if (!el) return
285
- const rect = el.getBoundingClientRect()
286
- const bounds = getDragBounds(el)
287
- const left = clamp(rect.left, bounds.left + DRAG_MARGIN, bounds.right - rect.width - DRAG_MARGIN)
288
- const top = clamp(rect.top, bounds.top + DRAG_MARGIN, bounds.bottom - rect.height - DRAG_MARGIN)
289
- const dx = left - rect.left
290
- const dy = top - rect.top
291
- if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
292
- setOffset((prev) => ({ x: (prev?.x ?? 0) + dx, y: (prev?.y ?? 0) + dy }))
254
+ const fit = () => {
255
+ const rect = el.getBoundingClientRect()
256
+ const bounds = getDragBounds(el, inStack)
257
+ const left = clamp(rect.left, bounds.left + DRAG_MARGIN, bounds.right - rect.width - DRAG_MARGIN)
258
+ const top = clamp(rect.top, bounds.top + DRAG_MARGIN, bounds.bottom - rect.height - DRAG_MARGIN)
259
+ const dx = left - rect.left
260
+ const dy = top - rect.top
261
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
262
+ setOffset((prev) => ({ x: (prev?.x ?? 0) + dx, y: (prev?.y ?? 0) + dy }))
263
+ }
293
264
  }
294
- }, [collapsed, isMobile])
265
+ fit()
266
+ const resize = new ResizeObserver(fit)
267
+ resize.observe(el)
268
+ const stack = el.closest('[data-right-stack]')
269
+ if (stack) {
270
+ resize.observe(stack)
271
+ const card = stack.querySelector('[data-right-stack-card]')
272
+ if (card) resize.observe(card)
273
+ }
274
+ window.addEventListener('resize', fit)
275
+ return () => {
276
+ resize.disconnect()
277
+ window.removeEventListener('resize', fit)
278
+ }
279
+ }, [collapsed, isMobile, inStack])
295
280
 
296
281
  return (
297
282
  <div
@@ -302,7 +287,7 @@ export function PanelWrapper({
302
287
  : inStack
303
288
  ? // In the shared right column: it grows down and scrolls on itself,
304
289
  // leaving the shortcuts card below it its room (`right-stack.tsx`).
305
- 'pointer-events-auto flex max-h-full min-h-0 flex-[0_1_auto] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground'
290
+ 'pointer-events-auto flex max-h-[var(--inspector-available-height)] min-h-0 flex-[0_1_auto] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground'
306
291
  // Cap height at `100dvh - 154px` so a tall panel's bottom edge
307
292
  // aligns flush with the top of the floating bottom action bar.
308
293
  // Combined with `top-20` (80px), the panel's bottom sits at
@@ -329,7 +314,7 @@ export function PanelWrapper({
329
314
  {!isMobile && (
330
315
  <div
331
316
  className={cn(
332
- 'relative flex select-none items-center justify-between px-3 py-3',
317
+ 'relative flex shrink-0 select-none items-center justify-between px-3 py-3',
333
318
  !collapsed && 'border-border/50 border-b',
334
319
  isDragging ? 'cursor-grabbing' : 'cursor-grab',
335
320
  )}
@@ -341,6 +326,7 @@ export function PanelWrapper({
341
326
  <div className={cn('flex min-w-0 items-center gap-2', titleContent && 'flex-1 pr-2')}>
342
327
  {onBack && (
343
328
  <button
329
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.back', message: 'Back' })}
344
330
  className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
345
331
  onClick={onBack}
346
332
  type="button"
@@ -388,6 +374,7 @@ export function PanelWrapper({
388
374
  <div className="flex items-center gap-1">
389
375
  {onReset && (
390
376
  <button
377
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.reset', message: 'Reset' })}
391
378
  className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
392
379
  onClick={onReset}
393
380
  type="button"
@@ -395,37 +382,9 @@ export function PanelWrapper({
395
382
  <RotateCcw className="h-4 w-4" />
396
383
  </button>
397
384
  )}
398
- {/* Extension mode buttons — one icon per registered inspector
399
- extension, left of the chevron. Click swaps the card body to
400
- ONLY that extension's content (either/or with the regular
401
- controls); the active icon (highlighted) or the chevron
402
- returns to the regular controls. */}
403
- {extensions.filter((extension) => extension !== primaryExtension).map((extension) => {
404
- const isActive = !collapsed && activeExtensionId === extension.id
405
- return (
406
- <button
407
- aria-label={isActive ? `Close ${extension.title}` : `Open ${extension.title}`}
408
- aria-pressed={isActive}
409
- className={cn(
410
- 'flex h-7 w-7 items-center justify-center rounded-md transition-colors',
411
- isActive
412
- ? 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-400 hover:bg-cyan-500/30'
413
- : 'bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground',
414
- )}
415
- key={extension.id}
416
- onClick={() =>
417
- applyMode(toggleExtension({ collapsed, activeExtensionId }, extension.id))
418
- }
419
- title={extension.title}
420
- type="button"
421
- >
422
- {renderExtensionIcon(extension.icon)}
423
- </button>
424
- )
425
- })}
426
385
  <button
427
386
  aria-expanded={!collapsed}
428
- aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
387
+ aria-label={collapsed ? i18n._({ id: 'editor.panels.panelWrapper.expandPanel', message: 'Expand panel' }) : i18n._({ id: 'editor.panels.panelWrapper.collapsePanel', message: 'Collapse panel' })}
429
388
  className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
430
389
  onClick={handleCardToggle}
431
390
  type="button"
@@ -436,6 +395,7 @@ export function PanelWrapper({
436
395
  </button>
437
396
  {onClose && (
438
397
  <button
398
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.close', message: 'Close' })}
439
399
  className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
440
400
  onClick={onClose}
441
401
  type="button"
@@ -473,10 +433,10 @@ export function PanelWrapper({
473
433
  )}
474
434
  {children}
475
435
  {inspectedIds.length > 0 && (
476
- <PanelSection title="Collections">
436
+ <PanelSection title={i18n._({ id: 'editor.panels.panelWrapper.collections', message: 'Collections' })}>
477
437
  <ActionGroup>
478
438
  <CollectionsPopover nodeIds={inspectedIds}>
479
- <ActionButton label="Manage collections…" />
439
+ <ActionButton label={i18n._({ id: 'editor.panels.panelWrapper.manageCollections', message: 'Manage collections…' })} />
480
440
  </CollectionsPopover>
481
441
  </ActionGroup>
482
442
  </PanelSection>
@@ -507,30 +467,6 @@ export function PanelWrapper({
507
467
 
508
468
  // ─── Plugin inspector extensions ──────────────────────────────────────
509
469
 
510
- /** 16px icon for an extension's header button — mirrors the parametric
511
- * inspector's `renderIcon` (plain <img> so no next/image server deps). */
512
- function renderExtensionIcon(ref: IconRef): React.ReactNode {
513
- if (ref.kind === 'url') {
514
- return <IconRefImage className="h-4 w-4 shrink-0" src={ref.src} />
515
- }
516
- if (ref.kind === 'iconify') {
517
- return <Icon height={16} icon={ref.name} width={16} />
518
- }
519
- if (ref.kind === 'svg') {
520
- return (
521
- <svg height={16} viewBox={ref.viewBox} width={16}>
522
- <path d={ref.path} fill="currentColor" />
523
- </svg>
524
- )
525
- }
526
- const LazyIcon = lazy(ref.module)
527
- return (
528
- <Suspense fallback={null}>
529
- <LazyIcon />
530
- </Suspense>
531
- )
532
- }
533
-
534
470
  // `React.lazy` once per loader so the resolved component keeps a stable
535
471
  // identity across renders — same WeakMap pattern as `resolveCustomPanel`
536
472
  // in parametric-inspector.tsx.
@@ -582,14 +518,14 @@ function InspectorExtensionContent({
582
518
  extension: InspectorExtension
583
519
  nodeId: AnyNodeId
584
520
  }) {
521
+ const i18n = usePascalI18n()
585
522
  const node = useScene((s) => s.nodes[nodeId])
586
523
  if (!node) return null
587
524
  const Extension = resolveExtensionComponent(extension)
588
525
  return (
589
526
  <ErrorBoundary fallback={
590
527
  <p className="p-1 text-muted-foreground text-xs">
591
- “{extension.title}” hit an error and was unloaded for this session.
592
- </p>
528
+ {i18n._({ id: 'editor.panels.panelWrapper.titleHitAnErrorAndWas', message: '“{title}” hit an error and was unloaded for this session.', values: { title: extension.title } })}</p>
593
529
  }>
594
530
  <Suspense fallback={null}>
595
531
  <Extension node={node} />
@@ -1,10 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import type { AnyNode, ParamField } from '@pascal-app/core'
4
5
  import { SegmentedControl } from '../controls/segmented-control'
5
6
  import { SliderControl } from '../controls/slider-control'
6
7
  import { ToggleControl } from '../controls/toggle-control'
7
- import { precisionForStep, prettifyEnumValue, prettifyKey } from './parametric-field-utils'
8
+ import { inspectorFieldDescriptor, inspectorOptionDescriptor } from '../../../i18n/node-messages'
9
+ import { precisionForStep } from './parametric-field-utils'
8
10
 
9
11
  interface ParametricFieldControlProps {
10
12
  field: ParamField<AnyNode>
@@ -21,16 +23,17 @@ export function ParametricFieldControl({
21
23
  onChange,
22
24
  onCommit,
23
25
  }: ParametricFieldControlProps) {
26
+ const i18n = usePascalI18n()
24
27
  const key = String(field.key)
25
28
 
26
29
  switch (field.kind) {
27
30
  case 'number': {
28
- const num = typeof value === 'number' ? value : 0
31
+ const num = typeof value === 'number' ? value : (field.default ?? 0)
29
32
  const step = field.step ?? 0.01
30
33
  const precision = precisionForStep(step)
31
34
  return (
32
35
  <SliderControl
33
- label={prettifyKey(key)}
36
+ label={i18n._(inspectorFieldDescriptor(key))}
34
37
  max={field.max}
35
38
  min={field.min}
36
39
  mixed={mixed}
@@ -50,7 +53,7 @@ export function ParametricFieldControl({
50
53
  return (
51
54
  <ToggleControl
52
55
  checked={checked}
53
- label={prettifyKey(key)}
56
+ label={i18n._(inspectorFieldDescriptor(key))}
54
57
  mixed={mixed}
55
58
  onChange={(next) => {
56
59
  const patch = { [key]: next } as Partial<AnyNode>
@@ -73,14 +76,14 @@ export function ParametricFieldControl({
73
76
  <SegmentedControl
74
77
  mixed={mixed}
75
78
  onChange={apply}
76
- options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
79
+ options={field.options.map((opt) => ({ label: i18n._(inspectorOptionDescriptor(opt)), value: opt }))}
77
80
  value={str}
78
81
  />
79
82
  )
80
83
  }
81
84
  return (
82
85
  <div className="flex items-center justify-between px-3 py-2">
83
- <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
86
+ <span className="text-foreground/80 text-xs">{i18n._(inspectorFieldDescriptor(key))}</span>
84
87
  <select
85
88
  className="rounded-md border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
86
89
  onChange={(e) => apply(e.target.value)}
@@ -88,12 +91,12 @@ export function ParametricFieldControl({
88
91
  >
89
92
  {mixed && (
90
93
  <option disabled value="">
91
- Mixed
94
+ {i18n._({ id: 'editor.panels.parametricFieldControl.mixed', message: 'Mixed' })}
92
95
  </option>
93
96
  )}
94
97
  {field.options.map((opt) => (
95
98
  <option key={opt} value={opt}>
96
- {prettifyEnumValue(opt)}
99
+ {i18n._(inspectorOptionDescriptor(opt))}
97
100
  </option>
98
101
  ))}
99
102
  </select>
@@ -110,7 +113,7 @@ export function ParametricFieldControl({
110
113
  }
111
114
  return (
112
115
  <div className="flex items-center justify-between px-3 py-2">
113
- <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
116
+ <span className="text-foreground/80 text-xs">{i18n._(inspectorFieldDescriptor(key))}</span>
114
117
  <div className="flex items-center gap-2">
115
118
  <input
116
119
  className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
@@ -122,7 +125,7 @@ export function ParametricFieldControl({
122
125
  className="w-20 rounded-md border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
123
126
  onChange={(e) => apply(e.target.value)}
124
127
  type="text"
125
- value={mixed ? 'Mixed' : str}
128
+ value={mixed ? i18n._({ id: 'editor.panels.mixed', message: 'Mixed' }) : str}
126
129
  />
127
130
  </div>
128
131
  </div>
@@ -8,7 +8,7 @@ import {
8
8
  type ParamAction,
9
9
  type ParamField,
10
10
  useScene,
11
- type ZoneNode,
11
+ type ZoneNode, usePascalI18n,
12
12
  } from '@pascal-app/core'
13
13
  import { useViewer } from '@pascal-app/viewer'
14
14
  import { Icon } from '@iconify/react'
@@ -24,6 +24,7 @@ import { ActionButton, ActionGroup } from '../controls/action-button'
24
24
  import { PanelSection } from '../controls/panel-section'
25
25
  import { ParametricFieldControl } from './parametric-field-control'
26
26
  import { InspectorFooterContext, PanelWrapper } from './panel-wrapper'
27
+ import { inspectorActionDescriptor, inspectorGroupDescriptor, nodeLabel } from '../../../i18n/node-messages'
27
28
 
28
29
  /**
29
30
  * Auto-derived right-panel inspector for any registry-backed node.
@@ -46,6 +47,7 @@ export function ParametricInspector({
46
47
  nodeId,
47
48
  onClose,
48
49
  }: { footer?: React.ReactNode; nodeId?: AnyNodeId; onClose?: () => void } = {}) {
50
+ const i18n = usePascalI18n()
49
51
  const selectedIdFromSelection = useViewer((s) => s.selection.selectedIds[0]) as
50
52
  | AnyNodeId
51
53
  | undefined
@@ -127,7 +129,7 @@ export function ParametricInspector({
127
129
  }
128
130
 
129
131
  const presentation = def.presentation
130
- const title = presentation?.label ?? nodeType ?? ''
132
+ const title = presentation?.label && nodeType ? nodeLabel(nodeType, presentation.label, i18n) : (nodeType ?? '')
131
133
  const iconNode = renderIcon(presentation?.icon)
132
134
  const canMove = !!def.capabilities.movable
133
135
  const canDelete = def.capabilities.deletable !== false
@@ -149,7 +151,7 @@ export function ParametricInspector({
149
151
  <PanelSection
150
152
  defaultExpanded={group.defaultExpanded}
151
153
  key={`${nodeType}-${group.label}-${gi}`}
152
- title={group.label}
154
+ title={i18n._(inspectorGroupDescriptor(group.label))}
153
155
  >
154
156
  {group.fields.map((field, fi) => (
155
157
  <FieldRenderer
@@ -167,10 +169,10 @@ export function ParametricInspector({
167
169
  </Suspense>
168
170
  )}
169
171
  {(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
170
- <PanelSection title="Actions">
172
+ <PanelSection title={i18n._({ id: 'editor.panels.parametricInspector.actions', message: 'Actions' })}>
171
173
  <ActionGroup className={isZone ? 'flex-col' : parametrics.actions?.length ? 'grid grid-cols-2 gap-2' : undefined}>
172
174
  {canMove && (
173
- <ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
175
+ <ActionButton icon={<Move className="h-4 w-4" />} label={i18n._({ id: 'editor.panels.parametricInspector.move', message: 'Move' })} onClick={handleMove} />
174
176
  )}
175
177
  {parametrics.actions?.map((action, i) => (
176
178
  <ParamActionButton action={action} key={`paramaction-${i}`} nodeId={selectedId} />
@@ -181,13 +183,13 @@ export function ParametricInspector({
181
183
  <ActionButton
182
184
  className="w-full flex-none"
183
185
  icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
184
- label="Delete"
186
+ label={i18n._({ id: 'editor.panels.parametricInspector.delete', message: 'Delete' })}
185
187
  onClick={() => handleDelete(false)}
186
188
  />
187
189
  <ActionButton
188
190
  className="w-full flex-none"
189
191
  icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
190
- label="Delete with contents"
192
+ label={i18n._({ id: 'editor.panels.parametricInspector.deleteWithContents', message: 'Delete with contents' })}
191
193
  onClick={() => handleDelete(true)}
192
194
  />
193
195
  </>
@@ -195,7 +197,7 @@ export function ParametricInspector({
195
197
  <ActionButton
196
198
  className="border-red-500/40 text-red-700 dark:text-red-200 hover:bg-red-500/15"
197
199
  icon={<Trash2 className="h-4 w-4" />}
198
- label="Delete"
200
+ label={i18n._({ id: 'editor.panels.parametricInspector.delete', message: 'Delete' })}
199
201
  onClick={() => handleDelete()}
200
202
  />
201
203
  ))}
@@ -212,6 +214,7 @@ export function ParametricInspector({
212
214
  // from re-rendering it. The click handler re-reads the live node so the
213
215
  // handler always acts on current state.
214
216
  function ParamActionButton({ action, nodeId }: { action: ParamAction<AnyNode>; nodeId: AnyNodeId }) {
217
+ const i18n = usePascalI18n()
215
218
  const disabled = useScene((s) => {
216
219
  if (!action.enabledIf) return false
217
220
  const n = s.nodes[nodeId]
@@ -225,7 +228,7 @@ function ParamActionButton({ action, nodeId }: { action: ParamAction<AnyNode>; n
225
228
  <img alt="" className="h-4 w-4 shrink-0 object-contain" src={action.iconSrc} />
226
229
  ) : undefined
227
230
  }
228
- label={action.label}
231
+ label={i18n._(inspectorActionDescriptor(action.label))}
229
232
  onClick={() => {
230
233
  const live = useScene.getState().nodes[nodeId]
231
234
  if (live) action.onClick(live as AnyNode)
@@ -311,10 +314,13 @@ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
311
314
  // visibleIf may consult other fields on the node — subscribe to its boolean
312
315
  // result so we re-evaluate when relevant.
313
316
  const visible = useScene((s) => {
314
- const visibleIf = (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf
315
- if (!visibleIf) return true
317
+ const { visibleIf, appliesTo } = field as {
318
+ visibleIf?: (n: AnyNode) => boolean
319
+ appliesTo?: (n: AnyNode) => boolean
320
+ }
321
+ if (!(visibleIf || appliesTo)) return true
316
322
  const n = s.nodes[nodeId]
317
- return n ? visibleIf(n as AnyNode) : false
323
+ return n ? (visibleIf?.(n as AnyNode) ?? true) && (appliesTo?.(n as AnyNode) ?? true) : false
318
324
  })
319
325
  if (!visible) return null
320
326