@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
@@ -4,12 +4,13 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  area,
7
- getLevelDisplayName,
8
7
  runAsSingleSceneHistoryStep,
9
8
  useScene,
10
- type ZoneNode,
9
+ type ZoneNode, usePascalI18n,
10
+ type PascalI18n,
11
+ useLocale,
11
12
  } from '@pascal-app/core'
12
- import { setRoomFloorScript } from '@pascal-app/core/agent-operations'
13
+ import { type SceneChanges, setRoomFloorScript } from '@pascal-app/core/agent-operations'
13
14
  import { useViewer } from '@pascal-app/viewer'
14
15
  import { Check, ChevronDown, Plus } from 'lucide-react'
15
16
  import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react'
@@ -23,12 +24,11 @@ import { mezzanineElevationBounds } from '../../../lib/room-handle-drag'
23
24
  import type { RoomSelectionRecord } from '../../../lib/room-selection'
24
25
  import {
25
26
  baseRoomName,
26
- numberedRoomName,
27
27
  type RoomNameSection,
28
28
  roomNameSections,
29
29
  roomNameUseCount,
30
30
  } from '../../../lib/room-name-catalog'
31
- import { renameRoom, requestRoomDeletion } from '../../../lib/room-structure-commands'
31
+ import { requestRoomDeletion } from '../../../lib/room-structure-commands'
32
32
  import { addZoneToNewUnit, assignZoneToUnit, zoneUnits } from '../../../lib/units'
33
33
  import { cn } from '../../../lib/utils'
34
34
  import useEditor from '../../../store/use-editor'
@@ -53,23 +53,49 @@ import {
53
53
  RoomConstructionRows,
54
54
  roomInsideHeight,
55
55
  } from './room-construction-rows'
56
- import { type RoomNameOption, roomNameOptions } from './room-name-options'
56
+ import {
57
+ commitRoomNameEdit,
58
+ commitRoomNamePick,
59
+ type RoomNameOption,
60
+ roomNameOptions,
61
+ } from './room-name-options'
62
+ import { localizedLevelName } from '../../../i18n/level-name'
63
+ import { messageWithSlot } from '../../../i18n/message-slot'
64
+ import {
65
+ displayRoomName,
66
+ localizedRoomNameSections,
67
+ } from '../../../lib/room-name-messages'
57
68
 
58
69
  /** "Room · Ground floor", "Mezzanine · in Kitchen", "Room · not enclosed". */
59
70
  export function roomKicker(
60
71
  nodes: Record<string, AnyNode>,
61
72
  zoneId: string,
73
+ i18n: PascalI18n,
74
+ locale: string,
62
75
  options: { mezzanine?: boolean; open?: boolean } = {},
63
76
  ): string {
64
77
  const zone = nodes[zoneId]
65
78
  if (options.mezzanine && zone?.type === 'zone') {
66
79
  const host = zone.hostZoneId ? nodes[zone.hostZoneId] : undefined
67
80
  const hostName = host?.type === 'zone' ? host.name?.trim() : ''
68
- return hostName ? `Mezzanine · in ${hostName}` : 'Mezzanine'
81
+ return hostName
82
+ ? i18n._({
83
+ id: 'editor.panels.roomPanel.kickerMezzanineIn',
84
+ message: 'Mezzanine · in {room}',
85
+ values: { room: displayRoomName(hostName, i18n) },
86
+ })
87
+ : i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' })
69
88
  }
70
- if (options.open) return 'Room · not enclosed'
89
+ if (options.open)
90
+ return i18n._({ id: 'editor.panels.roomPanel.kickerNotEnclosed', message: 'Room · not enclosed' })
71
91
  const level = zone?.parentId ? nodes[zone.parentId] : undefined
72
- return level?.type === 'level' ? `Room · ${getLevelDisplayName(level)}` : 'Room'
92
+ return level?.type === 'level'
93
+ ? i18n._({
94
+ id: 'editor.panels.roomPanel.kickerLevel',
95
+ message: 'Room · {level}',
96
+ values: { level: localizedLevelName(level, i18n, locale) },
97
+ })
98
+ : i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })
73
99
  }
74
100
 
75
101
  export function RoomPanel({
@@ -79,8 +105,15 @@ export function RoomPanel({
79
105
  room: RoomSelectionRecord
80
106
  onClose?: () => void
81
107
  }) {
108
+ const i18n = usePascalI18n()
109
+ const locale = useLocale()
82
110
  const nodes = useScene((state) => state.nodes)
83
- const sections = useMemo(() => roomNameSections(nodes, room.zoneId), [nodes, room.zoneId])
111
+ const catalogVersion = i18n.getVersion()
112
+ const sections = useMemo(
113
+ () => localizedRoomNameSections(roomNameSections(nodes, room.zoneId), i18n),
114
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-translate on a catalog change
115
+ [i18n, catalogVersion, nodes, room.zoneId],
116
+ )
84
117
  const name = useScene((state) => state.nodes[room.zoneId as AnyNodeId]?.name ?? '')
85
118
  const visible = useInteractionScope(
86
119
  (state) => resolveOverlayPolicy(state.scope).conflictingControls === 'shown',
@@ -89,9 +122,9 @@ export function RoomPanel({
89
122
  const mezzanine = !!room.mezzanine
90
123
  return (
91
124
  <PanelWrapper
92
- kicker={roomKicker(nodes, room.zoneId, { mezzanine })}
125
+ kicker={roomKicker(nodes, room.zoneId, i18n, locale, { mezzanine })}
93
126
  onClose={onClose}
94
- title={name.trim() || (mezzanine ? 'Mezzanine' : 'Room')}
127
+ title={(name.trim() && displayRoomName(name, i18n)) || (mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' }) : i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' }))}
95
128
  titleContent={
96
129
  <div className="flex min-w-0 flex-1 items-center gap-2">
97
130
  <RoomColorDot zoneId={room.zoneId} />
@@ -125,17 +158,27 @@ export function RoomPanel({
125
158
  * needs no closed outline — its name, its unit, deleting it.
126
159
  */
127
160
  export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: () => void }) {
161
+ const i18n = usePascalI18n()
162
+ const locale = useLocale()
128
163
  const nodes = useScene((state) => state.nodes)
129
- const sections = useMemo(() => roomNameSections(nodes, zoneId), [nodes, zoneId])
164
+ const catalogVersion = i18n.getVersion()
165
+ const sections = useMemo(
166
+ () => localizedRoomNameSections(roomNameSections(nodes, zoneId), i18n),
167
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-translate on a catalog change
168
+ [i18n, catalogVersion, nodes, zoneId],
169
+ )
130
170
  const name = useScene((state) => state.nodes[zoneId as AnyNodeId]?.name ?? '')
131
171
  const unit = useViewer((s) => s.unit)
132
172
  const zone = nodes[zoneId as AnyNodeId]
133
173
  if (zone?.type !== 'zone') return null
134
174
  return (
135
175
  <PanelWrapper
136
- kicker={roomKicker(nodes, zoneId, { open: true })}
176
+ kicker={roomKicker(nodes, zoneId, i18n, locale, { open: true })}
137
177
  onClose={onClose}
138
- title={name.trim() || 'Room'}
178
+ title={
179
+ (name.trim() && displayRoomName(name, i18n)) ||
180
+ i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })
181
+ }
139
182
  titleContent={
140
183
  <div className="flex min-w-0 flex-1 items-center gap-2">
141
184
  <RoomColorDot zoneId={zoneId} />
@@ -151,8 +194,11 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
151
194
  >
152
195
  <div className="flex flex-col px-4 pt-3 pb-4 text-sm" data-open-room-panel={zoneId}>
153
196
  <p className="text-muted-foreground text-xs">
154
- {formatAreaLabel(area([{ outer: zone.polygon, holes: zone.holes }]), unit, 2)} · Close
155
- its walls to build its floor and ceiling.
197
+ {i18n._({
198
+ id: 'editor.panels.roomPanel.openRoomArea',
199
+ message: '{area} · Close its walls to build its floor and ceiling.',
200
+ values: { area: formatAreaLabel(area([{ outer: zone.polygon, holes: zone.holes }]), unit, 2) },
201
+ })}
156
202
  </p>
157
203
  <RoomUnitRow zoneId={zoneId} />
158
204
  <button
@@ -161,7 +207,7 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
161
207
  onClick={() => requestRoomDeletion(zoneId)}
162
208
  type="button"
163
209
  >
164
- Delete room
210
+ {i18n._({ id: 'editor.panels.roomPanel.deleteRoom', message: 'Delete room' })}
165
211
  </button>
166
212
  </div>
167
213
  </PanelWrapper>
@@ -170,16 +216,17 @@ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: ()
170
216
 
171
217
  /** "Unit: Flat A ›" — which unit the room belongs to; change it, add it to one, or take it out. */
172
218
  export function RoomUnitRow({ zoneId }: { zoneId: string }) {
219
+ const i18n = usePascalI18n()
173
220
  const nodes = useScene((scene) => scene.nodes)
174
221
  const { buildingId, units, current } = useMemo(() => zoneUnits(nodes, zoneId), [nodes, zoneId])
175
222
  if (!buildingId) return null
176
223
  return (
177
224
  <div className="mt-3 flex min-h-8 items-center justify-between gap-3" data-room-unit={zoneId}>
178
- <span className="text-muted-foreground text-xs">Unit</span>
225
+ <span className="text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}</span>
179
226
  <DropdownMenu>
180
227
  <DropdownMenuTrigger asChild>
181
228
  <button
182
- aria-label="Unit"
229
+ aria-label={i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}
183
230
  className="flex min-w-0 items-center gap-1.5 rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
184
231
  type="button"
185
232
  >
@@ -191,7 +238,7 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
191
238
  />
192
239
  )}
193
240
  <span className="truncate">
194
- {current ? current.name || 'Unit' : units.length ? 'None' : 'Add to a unit'}
241
+ {current ? current.name || i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' }) : units.length ? i18n._({ id: 'editor.panels.roomPanel.none', message: 'None' }) : i18n._({ id: 'editor.panels.roomPanel.addToAUnit', message: 'Add to a unit' })}
195
242
  </span>
196
243
  <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
197
244
  </button>
@@ -207,17 +254,23 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
207
254
  className="size-2 shrink-0 rounded-full"
208
255
  style={{ backgroundColor: unit.color }}
209
256
  />
210
- <span className="flex-1 truncate">{unit.name || 'Unit'}</span>
257
+ <span className="flex-1 truncate">{unit.name || i18n._({ id: 'editor.panels.roomPanel.unit', message: 'Unit' })}</span>
211
258
  {unit.id === current?.id && <Check className="size-3.5" />}
212
259
  </DropdownMenuItem>
213
260
  ))}
214
261
  {units.length > 0 && <DropdownMenuSeparator />}
215
262
  <DropdownMenuItem onSelect={() => addZoneToNewUnit(zoneId as ZoneNode['id'])}>
216
- New unit
263
+ {i18n._({ id: 'editor.panels.roomPanel.newUnit', message: 'New unit' })}
217
264
  </DropdownMenuItem>
218
265
  {current && (
219
266
  <DropdownMenuItem onSelect={() => assignZoneToUnit(zoneId as ZoneNode['id'], null)}>
220
- Remove from {current.name || 'unit'}
267
+ {current.name
268
+ ? i18n._({
269
+ id: 'editor.panels.roomPanel.removeFromUnitNamed',
270
+ message: 'Remove from {unit}',
271
+ values: { unit: current.name },
272
+ })
273
+ : i18n._({ id: 'editor.panels.roomPanel.removeFromUnit', message: 'Remove from unit' })}
221
274
  </DropdownMenuItem>
222
275
  )}
223
276
  </DropdownMenuContent>
@@ -226,72 +279,94 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
226
279
  )
227
280
  }
228
281
 
229
- /**
230
- * "Scripted finish": a floor a script builds (it owns the floor's colours, so Paint leaves it),
231
- * with its code to read and a way to take it off, back to the floor's own finish.
232
- */
282
+ /** The room's floor a script builds, with its code to read and a way to take it off. */
233
283
  function RoomFloorScriptRow({ zoneId }: { zoneId: string }) {
234
284
  const sha = useScene((scene) => {
235
285
  const zone = scene.nodes[zoneId as AnyNodeId]
236
286
  return zone?.type === 'zone' ? zone.floor?.script?.script : undefined
237
287
  })
288
+ if (!sha) return null
289
+ return (
290
+ <ScriptedFinishRow
291
+ data-room-floor-script={zoneId}
292
+ remove={() =>
293
+ setRoomFloorScript(
294
+ useScene.getState().nodes,
295
+ { zoneId, remove: true },
296
+ { activeLevelId: null },
297
+ ).changes
298
+ }
299
+ sha={sha}
300
+ />
301
+ )
302
+ }
303
+
304
+ /**
305
+ * "Scripted finish": a floor a script builds (a room's floor, a slab's surface; it owns the floor's
306
+ * colours, so Paint leaves it), with its code to read and a way to take it off, back to the floor's
307
+ * own finish: `remove` answers the removal's changes, applied as one undo step.
308
+ */
309
+ export function ScriptedFinishRow({
310
+ sha,
311
+ remove,
312
+ ...attributes
313
+ }: { sha: string; remove: () => SceneChanges | undefined } & Record<`data-${string}`, string>) {
314
+ const i18n = usePascalI18n()
238
315
  const [code, setCode] = useState<{ sha: string; text: string } | null>(null)
239
316
  const [open, setOpen] = useState(false)
240
- if (!sha) return null
317
+ const unreadable = i18n._({
318
+ id: 'editor.panels.scriptUnreadable',
319
+ message: 'The script could not be read.',
320
+ })
241
321
  const toggleCode = () => {
242
322
  setOpen((shown) => !shown)
243
323
  if (code?.sha === sha) return
244
324
  finishScriptCode(sha).then(
245
- (text) => setCode({ sha, text: text ?? 'The script could not be read.' }),
246
- () => setCode({ sha, text: 'The script could not be read.' }),
325
+ (text) => setCode({ sha, text: text ?? unreadable }),
326
+ () => setCode({ sha, text: unreadable }),
247
327
  )
248
328
  }
249
- const remove = () => {
250
- const { changes } = setRoomFloorScript(
251
- useScene.getState().nodes,
252
- { zoneId, remove: true },
253
- { activeLevelId: null },
254
- )
329
+ const removeScript = () => {
330
+ const changes = remove()
255
331
  runAsSingleSceneHistoryStep(useScene, () => {
256
332
  for (const { id, data } of changes?.update ?? [])
257
333
  useScene.getState().updateNode(id as AnyNodeId, data)
258
334
  })
259
335
  }
260
336
  return (
261
- <div className="mt-3 flex flex-col gap-2" data-room-floor-script={zoneId}>
337
+ <div className="mt-3 flex flex-col gap-2" {...attributes}>
262
338
  <div className="flex min-h-8 items-center justify-between gap-3">
263
- <span className="text-muted-foreground text-xs">Scripted finish</span>
339
+ <span className="text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.scriptedFinish', message: 'Scripted finish' })}</span>
264
340
  <div className="flex items-center gap-1.5">
265
341
  <button
266
342
  className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
267
343
  onClick={toggleCode}
268
344
  type="button"
269
345
  >
270
- {open ? 'Hide code' : 'View code'}
346
+ {open ? i18n._({ id: 'editor.panels.roomPanel.hideCode', message: 'Hide code' }) : i18n._({ id: 'editor.panels.roomPanel.viewCode', message: 'View code' })}
271
347
  </button>
272
348
  <button
273
349
  className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
274
- onClick={remove}
350
+ onClick={removeScript}
275
351
  type="button"
276
352
  >
277
- Remove
353
+ {i18n._({ id: 'editor.panels.roomPanel.remove', message: 'Remove' })}
278
354
  </button>
279
355
  </div>
280
356
  </div>
281
357
  {open && (
282
358
  <pre className="max-h-60 overflow-auto whitespace-pre rounded-lg border border-border/50 p-2 font-mono text-[11px] text-foreground">
283
- {code?.sha === sha ? code.text : 'Loading…'}
359
+ {code?.sha === sha ? code.text : i18n._({ id: 'editor.panels.roomPanel.loading', message: 'Loading…' })}
284
360
  </pre>
285
361
  )}
286
362
  </div>
287
363
  )
288
364
  }
289
365
 
290
- const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
291
-
292
366
  /** "2 doors · 3 windows" (the level is in the kicker). */
293
367
  function RoomSubline({ room }: { room: RoomSelectionRecord }) {
294
- const text = useScene((scene) => {
368
+ const i18n = usePascalI18n()
369
+ const counts = useScene((scene) => {
295
370
  const walls = new Set(room.boundaryWallIds)
296
371
  let doors = 0
297
372
  let windows = 0
@@ -302,7 +377,14 @@ function RoomSubline({ room }: { room: RoomSelectionRecord }) {
302
377
  if (node.type === 'door') doors++
303
378
  else windows++
304
379
  }
305
- return `${count(doors, 'door')} · ${count(windows, 'window')}`
380
+ return `${doors}:${windows}`
381
+ })
382
+ const [doors, windows] = counts.split(':').map(Number)
383
+ const text = i18n._({
384
+ id: 'editor.panels.roomPanel.openingCounts',
385
+ message:
386
+ '{doors, plural, one {{doorCount} door} other {{doorCount} doors}} · {windows, plural, one {{windowCount} window} other {{windowCount} windows}}',
387
+ values: { doors, windows, doorCount: String(doors), windowCount: String(windows) },
306
388
  })
307
389
  return (
308
390
  <p className="text-muted-foreground text-xs" data-room-subline>
@@ -323,6 +405,7 @@ function ringPerimeter(ring: readonly (readonly [number, number])[]) {
323
405
 
324
406
  /** Area, perimeter and inside height, in the user's units. */
325
407
  function RoomStats({ room }: { room: RoomSelectionRecord }) {
408
+ const i18n = usePascalI18n()
326
409
  const unit = useViewer((s) => s.unit)
327
410
  const metricNotation = useViewer((s) => s.metricNotation)
328
411
  const nodes = useScene((scene) => scene.nodes)
@@ -333,10 +416,10 @@ function RoomStats({ room }: { room: RoomSelectionRecord }) {
333
416
  : 0
334
417
  const { height } = roomInsideHeight(room, nodes)
335
418
  const stats = [
336
- { label: 'Area', value: formatAreaLabel(room.area, unit, 2) },
337
- { label: 'Perimeter', value: formatLinearMeasurement(perimeter, unit, metricNotation) },
419
+ { label: i18n._({ id: 'editor.panels.roomPanel.area', message: 'Area' }), value: formatAreaLabel(room.area, unit, 2) },
420
+ { label: i18n._({ id: 'editor.panels.roomPanel.perimeter', message: 'Perimeter' }), value: formatLinearMeasurement(perimeter, unit, metricNotation) },
338
421
  {
339
- label: 'Height',
422
+ label: i18n._({ id: 'editor.panels.roomPanel.height', message: 'Height' }),
340
423
  value: height === null ? '—' : formatLinearMeasurement(height, unit, metricNotation),
341
424
  },
342
425
  ]
@@ -363,6 +446,7 @@ function RoomStats({ room }: { room: RoomSelectionRecord }) {
363
446
  * it when there is headroom. No walls of its own, so no walls row.
364
447
  */
365
448
  function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
449
+ const i18n = usePascalI18n()
366
450
  const zoneId = room.zoneId
367
451
  const nodes = useScene((scene) => scene.nodes)
368
452
  const unit = useViewer((s) => s.unit)
@@ -385,12 +469,12 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
385
469
  setExpanded((current) => (current === part ? null : part))
386
470
  return (
387
471
  <div className="flex flex-col" data-mezzanine-rows={zoneId}>
388
- <h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">Built with</h3>
472
+ <h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomPanel.builtWith', message: 'Built with' })}</h3>
389
473
  <div className="flex flex-col gap-1">
390
474
  <ConstructionPart
391
475
  expanded={expanded === 'floor'}
392
476
  icon="/icons/floor.webp"
393
- label="Floor"
477
+ label={i18n._({ id: 'editor.panels.roomPanel.floor', message: 'Floor' })}
394
478
  meta={`${length(elevation)} up`}
395
479
  on
396
480
  onToggleExpanded={toggle('floor')}
@@ -410,12 +494,12 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
410
494
  }}
411
495
  type="button"
412
496
  >
413
- Add stairs
497
+ {i18n._({ id: 'editor.panels.roomPanel.addStairs', message: 'Add stairs' })}
414
498
  </button>
415
499
  }
416
500
  expanded={false}
417
501
  icon="/icons/stairs.webp"
418
- label="Stairs"
502
+ label={i18n._({ id: 'editor.panels.roomPanel.stairs', message: 'Stairs' })}
419
503
  meta="Up from the floor below"
420
504
  on
421
505
  onToggleExpanded={() => {}}
@@ -424,22 +508,26 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
424
508
  <ConstructionPart
425
509
  expanded={expanded === 'ceiling'}
426
510
  icon="/icons/ceiling.webp"
427
- label="Ceiling"
511
+ label={i18n._({ id: 'editor.panels.roomPanel.ceiling', message: 'Ceiling' })}
428
512
  meta={
429
513
  hasCeiling
430
514
  ? ceiling === null
431
- ? 'Over the mezzanine'
432
- : `${length(ceiling)} headroom`
433
- : 'Not enough headroom'
515
+ ? i18n._({ id: 'editor.panels.roomPanel.overTheMezzanine', message: 'Over the mezzanine' })
516
+ : i18n._({
517
+ id: 'editor.panels.roomPanel.headroom',
518
+ message: '{height} headroom',
519
+ values: { height: length(ceiling) },
520
+ })
521
+ : i18n._({ id: 'editor.panels.roomPanel.notEnoughHeadroom', message: 'Not enough headroom' })
434
522
  }
435
523
  on
436
524
  onToggleExpanded={toggle('ceiling')}
437
525
  part="ceiling"
438
526
  >
439
527
  {hasCeiling ? (
440
- <PartField label="Shape">
528
+ <PartField label={i18n._({ id: 'editor.panels.roomPanel.shape', message: 'Shape' })}>
441
529
  <button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
442
- Edit shape
530
+ {i18n._({ id: 'editor.panels.roomPanel.editShape', message: 'Edit shape' })}
443
531
  </button>
444
532
  </PartField>
445
533
  ) : undefined}
@@ -459,6 +547,7 @@ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
459
547
  * the label tint, never a finish (finishes are painted).
460
548
  */
461
549
  function RoomColorDot({ zoneId }: { zoneId: string }) {
550
+ const i18n = usePascalI18n()
462
551
  const color = useScene((state) => {
463
552
  const zone = state.nodes[zoneId as AnyNodeId]
464
553
  return zone?.type === 'zone' ? zone.color : undefined
@@ -466,7 +555,7 @@ function RoomColorDot({ zoneId }: { zoneId: string }) {
466
555
  return (
467
556
  <ColorDot
468
557
  color={color ?? '#3b82f6'}
469
- label="Room colour — used in the plan and labels"
558
+ label={i18n._({ id: 'editor.panels.roomPanel.roomColourUsedInThePlan', message: 'Room colour — used in the plan and labels' })}
470
559
  onChange={(next) => useScene.getState().updateNode(zoneId as AnyNodeId, { color: next })}
471
560
  side="bottom"
472
561
  />
@@ -492,30 +581,33 @@ function RoomName({
492
581
  sections: readonly RoomNameSection[]
493
582
  mezzanine: boolean
494
583
  }) {
584
+ const i18n = usePascalI18n()
495
585
  const nodes = useScene((state) => state.nodes)
496
- const [value, setValue] = useState(name)
586
+ // The field shows the name in the UI language; the scene keeps what was stored.
587
+ const display = displayRoomName(name, i18n)
588
+ const [value, setValue] = useState(display)
497
589
  const [open, setOpen] = useState(false)
498
590
  const [typing, setTyping] = useState(false)
499
591
  const [active, setActive] = useState(-1)
500
- const valueRef = useRef(name)
592
+ const valueRef = useRef(display)
501
593
  // Enter/click commit and then blur, and blur commits too; this keeps it one rename.
502
- const committedRef = useRef(name)
503
- const originalRef = useRef(name)
594
+ const committedRef = useRef(display)
595
+ const originalRef = useRef(display)
504
596
  const inputRef = useRef<HTMLInputElement>(null)
505
597
  const listId = useId()
506
- const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing)
598
+ const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing, name)
507
599
  const expanded = open && options.length > 0
508
600
  const activeId = expanded && options[active] ? `${listId}-${active}` : undefined
509
- const current = baseRoomName(value).toLowerCase()
601
+ const current = baseRoomName(name).toLowerCase()
510
602
 
511
603
  // A rename from elsewhere (undo, a collaborator, the plan) lands in the field
512
604
  // unless the user is editing it (the field stays mounted across renames).
513
605
  useEffect(() => {
514
606
  if (document.activeElement === inputRef.current) return
515
- valueRef.current = name
516
- committedRef.current = name
517
- setValue(name)
518
- }, [name])
607
+ valueRef.current = display
608
+ committedRef.current = display
609
+ setValue(display)
610
+ }, [display])
519
611
 
520
612
  // Opening and the arrow keys bring the active row into view; the pointer
521
613
  // only highlights (scrolling under it would move the row being clicked).
@@ -541,7 +633,7 @@ function RoomName({
541
633
  setTyping(false)
542
634
  setOpen(true)
543
635
  const all = roomNameOptions(valueRef.current, sections, false).options
544
- const here = baseRoomName(valueRef.current).toLowerCase()
636
+ const here = baseRoomName(name).toLowerCase()
545
637
  scrollToActive.current = true
546
638
  setActive(Math.max(0, all.findIndex((option) => option.value.toLowerCase() === here)))
547
639
  }
@@ -551,15 +643,14 @@ function RoomName({
551
643
  }
552
644
  const commit = () => {
553
645
  const next = valueRef.current
554
- if (next === committedRef.current) return
646
+ const committed = committedRef.current
555
647
  committedRef.current = next
556
- renameRoom(zoneId, next)
648
+ commitRoomNameEdit(zoneId, name, committed, next, i18n)
557
649
  }
558
650
  const pick = (option: RoomNameOption) => {
559
- const next =
560
- option.kind === 'typed' ? option.value : numberedRoomName(nodes, zoneId, option.value)
561
- type(next)
562
- commit()
651
+ const shown = commitRoomNamePick(zoneId, name, option, i18n)
652
+ type(shown)
653
+ committedRef.current = shown
563
654
  setOpen(false)
564
655
  setTyping(false)
565
656
  // A pick is the answer: the field lets go. To fine-tune the name, click it
@@ -583,7 +674,9 @@ function RoomName({
583
674
  event.preventDefault()
584
675
  event.stopPropagation()
585
676
  const chosen = expanded ? options[active] : undefined
586
- if (chosen) pick(chosen)
677
+ // Enter on the typed row commits the field, as blur does; only a click
678
+ // on it uses the text exactly as typed.
679
+ if (chosen?.kind === 'suggestion') pick(chosen)
587
680
  else inputRef.current?.blur()
588
681
  } else if (event.key === 'Escape') {
589
682
  event.preventDefault()
@@ -624,10 +717,9 @@ function RoomName({
624
717
  ) : used > 0 ? (
625
718
  <span
626
719
  className="shrink-0 whitespace-nowrap font-mono text-[11px] text-muted-foreground"
627
- title="Picking it adds a number"
720
+ title={i18n._({ id: 'editor.panels.roomPanel.pickingItAddsANumber', message: 'Picking it adds a number' })}
628
721
  >
629
- {used} on this level
630
- </span>
722
+ {i18n._({ id: 'editor.panels.roomPanel.usedOnThisLevel', message: '{used} on this level', values: { used } })}</span>
631
723
  ) : null}
632
724
  </div>
633
725
  )
@@ -645,19 +737,27 @@ function RoomName({
645
737
  )}
646
738
  data-guide-target="room-name"
647
739
  data-room-name-combobox
648
- title={name.trim() ? `${name.trim()} · Rename` : 'Rename'}
740
+ title={
741
+ name.trim()
742
+ ? i18n._({
743
+ id: 'editor.panels.roomPanel.renameNamed',
744
+ message: '{name} · Rename',
745
+ values: { name: displayRoomName(name, i18n) },
746
+ })
747
+ : i18n._({ id: 'editor.panels.roomPanel.rename', message: 'Rename' })
748
+ }
649
749
  >
650
750
  <input
651
751
  aria-activedescendant={activeId}
652
752
  aria-autocomplete="list"
653
753
  aria-controls={expanded ? listId : undefined}
654
754
  aria-expanded={expanded}
655
- aria-label={mezzanine ? 'Mezzanine name' : 'Room name'}
755
+ aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanineName', message: 'Mezzanine name' }) : i18n._({ id: 'editor.panels.roomPanel.roomName', message: 'Room name' })}
656
756
  autoComplete="off"
657
757
  className="min-w-0 flex-1 text-ellipsis bg-transparent p-0 font-semibold text-foreground text-sm leading-snug tracking-tight outline-none placeholder:text-foreground"
658
758
  data-room-name
659
759
  maxLength={40}
660
- placeholder={mezzanine ? 'Mezzanine' : 'Room'}
760
+ placeholder={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanine', message: 'Mezzanine' }) : i18n._({ id: 'editor.panels.roomPanel.room', message: 'Room' })}
661
761
  ref={inputRef}
662
762
  role="combobox"
663
763
  spellCheck={false}
@@ -672,9 +772,8 @@ function RoomName({
672
772
  type(event.target.value)
673
773
  setTyping(true)
674
774
  setOpen(true)
675
- const next = roomNameOptions(event.target.value, sections, true)
676
775
  scrollToActive.current = true
677
- setActive(next.typed && next.options.length > 1 ? 1 : 0)
776
+ setActive(roomNameOptions(event.target.value, sections, true, name).active)
678
777
  }}
679
778
  onFocus={() => {
680
779
  originalRef.current = valueRef.current
@@ -689,7 +788,7 @@ function RoomName({
689
788
  }}
690
789
  />
691
790
  <button
692
- aria-label={mezzanine ? 'Show mezzanine names' : 'Show room names'}
791
+ aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.showMezzanineNames', message: 'Show mezzanine names' }) : i18n._({ id: 'editor.panels.roomPanel.showRoomNames', message: 'Show room names' })}
693
792
  className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
694
793
  data-room-name-open
695
794
  tabIndex={-1}
@@ -712,7 +811,7 @@ function RoomName({
712
811
  </PopoverAnchor>
713
812
  <PopoverContent
714
813
  align="start"
715
- aria-label={mezzanine ? 'Mezzanine names' : 'Room names'}
814
+ aria-label={mezzanine ? i18n._({ id: 'editor.panels.roomPanel.mezzanineNames', message: 'Mezzanine names' }) : i18n._({ id: 'editor.panels.roomPanel.roomNames', message: 'Room names' })}
716
815
  className="flex max-h-85 w-(--radix-popover-trigger-width) min-w-60 flex-col overflow-hidden p-0"
717
816
  id={listId}
718
817
  role="listbox"
@@ -752,12 +851,12 @@ function RoomName({
752
851
  <div className="flex shrink-0 flex-wrap gap-x-3 gap-y-1 border-border border-t px-3 py-2 text-[11px] text-muted-foreground">
753
852
  {(
754
853
  [
755
- [['↑', '↓'], 'Browse'],
756
- [['Enter'], 'Pick'],
757
- [['Esc'], 'Revert'],
854
+ [['↑', '↓'], i18n._({ id: 'editor.panels.roomPanel.browse', message: 'Browse' })],
855
+ [['Enter'], i18n._({ id: 'editor.panels.roomPanel.pick', message: 'Pick' })],
856
+ [['Esc'], i18n._({ id: 'editor.panels.roomPanel.revert', message: 'Revert' })],
758
857
  ] as const
759
858
  ).map(([keys, action]) => (
760
- <span className="flex items-center gap-1" key={action}>
859
+ <span className="flex items-center gap-1" key={keys[0]}>
761
860
  {keys.map((key) => (
762
861
  <ShortcutToken className="h-4 min-w-4 justify-center px-1 text-[9.5px]" key={key} value={key} />
763
862
  ))}
@@ -771,20 +870,28 @@ function RoomName({
771
870
  }
772
871
 
773
872
  function RoomNameOptionLabel({ option }: { option: RoomNameOption }) {
873
+ const i18n = usePascalI18n()
774
874
  if (option.kind === 'typed') {
775
875
  return (
776
876
  <span className="min-w-0 flex-1 truncate">
777
- <span className="text-muted-foreground">Use </span>“{option.value}”
877
+ {messageWithSlot(
878
+ i18n,
879
+ /* i18n */ { id: 'editor.panels.roomPanel.useTyped', message: '{use} “{name}”', values: { name: option.value } },
880
+ 'use',
881
+ <span className="text-muted-foreground">
882
+ {i18n._({ id: 'editor.panels.roomPanel.use', message: 'Use' })}
883
+ </span>,
884
+ )}
778
885
  </span>
779
886
  )
780
887
  }
781
- const { value, match } = option
782
- if (!match) return <span className="min-w-0 flex-1 truncate">{value}</span>
888
+ const { label, match } = option
889
+ if (!match) return <span className="min-w-0 flex-1 truncate">{label}</span>
783
890
  return (
784
891
  <span className="min-w-0 flex-1 truncate">
785
- {value.slice(0, match.start)}
786
- <span className="font-semibold underline decoration-muted-foreground underline-offset-3">{value.slice(match.start, match.end)}</span>
787
- {value.slice(match.end)}
892
+ {label.slice(0, match.start)}
893
+ <span className="font-semibold underline decoration-muted-foreground underline-offset-3">{label.slice(match.start, match.end)}</span>
894
+ {label.slice(match.end)}
788
895
  </span>
789
896
  )
790
897
  }