@pascal-app/editor 1.0.6 → 1.0.7

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 (317) 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 +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -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
@@ -119,6 +109,7 @@ export function PanelWrapper({
119
109
  className,
120
110
  width = 320, // default width
121
111
  }: PanelWrapperProps) {
112
+ const i18n = usePascalI18n()
122
113
  const isMobile = useIsMobile()
123
114
  const inStack = useInRightStack()
124
115
  const contextFooter = useContext(InspectorFooterContext)
@@ -131,35 +122,16 @@ export function PanelWrapper({
131
122
  // The wrapper self-resolves the selected node instead of taking a prop:
132
123
  // kind-owned `customPanel`s (wall, slab, …) render their own
133
124
  // <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])
125
+ // through. Extensions only apply to a single-node selection; the action
126
+ // menu opens them (`InspectorExtensionActions`).
127
+ const { selectedId, extensions, primaryExtension } = useInspectorExtensions()
151
128
 
152
129
  // Which extension's content fills the card body (extension mode). The two
153
130
  // expanded modes are EITHER/OR: extension mode replaces the regular
154
131
  // 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
132
  // 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)
133
+ // See `lib/inspector-card-mode.ts` for the transition table.
134
+ const activeExtensionId = useInspectorExpanded((state) => state.activeExtensionId)
163
135
  const activeExtension = primaryExtension ?? resolveActiveExtension(activeExtensionId, extensions)
164
136
 
165
137
  // The panel is collapsed to just its header until the user expands it. The
@@ -174,14 +146,6 @@ export function PanelWrapper({
174
146
  if (primaryExtension) setCollapsed(false)
175
147
  }, [primaryExtension, selectedId, setCollapsed])
176
148
 
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
149
  // Chevron / header press — collapsed → regular, regular → collapsed,
186
150
  // extension mode → regular (exit the extension first, stay expanded).
187
151
  const handleCardToggle = useCallback(() => {
@@ -189,14 +153,12 @@ export function PanelWrapper({
189
153
  setCollapsed(!collapsed)
190
154
  return
191
155
  }
192
- applyMode(toggleCard({ collapsed, activeExtensionId }))
193
- }, [applyMode, collapsed, activeExtensionId, primaryExtension, setCollapsed])
156
+ useInspectorExpanded.getState().applyMode(toggleCard({ collapsed, activeExtensionId }))
157
+ }, [collapsed, activeExtensionId, primaryExtension, setCollapsed])
194
158
 
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])
159
+ // Extension mode is an affordance of the card it was opened on: a card that
160
+ // closes (selection cleared or changed kind) leaves the next one regular.
161
+ useEffect(() => () => useInspectorExpanded.setState({ activeExtensionId: null }), [])
200
162
 
201
163
  // Drag-to-reposition from the header. `offset` is a translation applied on
202
164
  // top of the default `top-20 right-4` anchor; null until first dragged.
@@ -341,6 +303,7 @@ export function PanelWrapper({
341
303
  <div className={cn('flex min-w-0 items-center gap-2', titleContent && 'flex-1 pr-2')}>
342
304
  {onBack && (
343
305
  <button
306
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.back', message: 'Back' })}
344
307
  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
308
  onClick={onBack}
346
309
  type="button"
@@ -388,6 +351,7 @@ export function PanelWrapper({
388
351
  <div className="flex items-center gap-1">
389
352
  {onReset && (
390
353
  <button
354
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.reset', message: 'Reset' })}
391
355
  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
356
  onClick={onReset}
393
357
  type="button"
@@ -395,37 +359,9 @@ export function PanelWrapper({
395
359
  <RotateCcw className="h-4 w-4" />
396
360
  </button>
397
361
  )}
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
362
  <button
427
363
  aria-expanded={!collapsed}
428
- aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
364
+ aria-label={collapsed ? i18n._({ id: 'editor.panels.panelWrapper.expandPanel', message: 'Expand panel' }) : i18n._({ id: 'editor.panels.panelWrapper.collapsePanel', message: 'Collapse panel' })}
429
365
  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
366
  onClick={handleCardToggle}
431
367
  type="button"
@@ -436,6 +372,7 @@ export function PanelWrapper({
436
372
  </button>
437
373
  {onClose && (
438
374
  <button
375
+ aria-label={i18n._({ id: 'editor.panels.panelWrapper.close', message: 'Close' })}
439
376
  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
377
  onClick={onClose}
441
378
  type="button"
@@ -473,10 +410,10 @@ export function PanelWrapper({
473
410
  )}
474
411
  {children}
475
412
  {inspectedIds.length > 0 && (
476
- <PanelSection title="Collections">
413
+ <PanelSection title={i18n._({ id: 'editor.panels.panelWrapper.collections', message: 'Collections' })}>
477
414
  <ActionGroup>
478
415
  <CollectionsPopover nodeIds={inspectedIds}>
479
- <ActionButton label="Manage collections…" />
416
+ <ActionButton label={i18n._({ id: 'editor.panels.panelWrapper.manageCollections', message: 'Manage collections…' })} />
480
417
  </CollectionsPopover>
481
418
  </ActionGroup>
482
419
  </PanelSection>
@@ -507,30 +444,6 @@ export function PanelWrapper({
507
444
 
508
445
  // ─── Plugin inspector extensions ──────────────────────────────────────
509
446
 
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
447
  // `React.lazy` once per loader so the resolved component keeps a stable
535
448
  // identity across renders — same WeakMap pattern as `resolveCustomPanel`
536
449
  // in parametric-inspector.tsx.
@@ -582,14 +495,14 @@ function InspectorExtensionContent({
582
495
  extension: InspectorExtension
583
496
  nodeId: AnyNodeId
584
497
  }) {
498
+ const i18n = usePascalI18n()
585
499
  const node = useScene((s) => s.nodes[nodeId])
586
500
  if (!node) return null
587
501
  const Extension = resolveExtensionComponent(extension)
588
502
  return (
589
503
  <ErrorBoundary fallback={
590
504
  <p className="p-1 text-muted-foreground text-xs">
591
- “{extension.title}” hit an error and was unloaded for this session.
592
- </p>
505
+ {i18n._({ id: 'editor.panels.panelWrapper.titleHitAnErrorAndWas', message: '“{title}” hit an error and was unloaded for this session.', values: { title: extension.title } })}</p>
593
506
  }>
594
507
  <Suspense fallback={null}>
595
508
  <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