@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,13 +1,14 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { Pentagon, Square } from 'lucide-react'
4
5
  import { cn } from '../../../lib/utils'
5
6
 
6
7
  export type OutlineShape = 'rectangle' | 'polygon'
7
8
 
8
9
  const SHAPES = [
9
- { id: 'rectangle', label: 'Rectangle', Icon: Square },
10
- { id: 'polygon', label: 'Polygon', Icon: Pentagon },
10
+ { id: 'rectangle', label: /* i18n */ { id: 'editor.panels.shapeChoice.rectangle', message: 'Rectangle' }, Icon: Square },
11
+ { id: 'polygon', label: /* i18n */ { id: 'editor.panels.shapeChoice.polygon', message: 'Polygon' }, Icon: Pentagon },
11
12
  ] as const
12
13
 
13
14
  const pill =
@@ -32,13 +33,14 @@ export function ShapeChoice({
32
33
  /** `data-shape-choice` hook for tests and proofs. */
33
34
  data?: string
34
35
  }) {
36
+ const i18n = usePascalI18n()
35
37
  return (
36
38
  <div
37
- className={cn('flex min-h-7 items-center justify-between gap-2', className)}
39
+ className={cn('flex min-h-7 flex-wrap items-center justify-between gap-2', className)}
38
40
  data-shape-choice={data}
39
41
  >
40
42
  <span className="whitespace-nowrap">{label}</span>
41
- <div className="flex items-center gap-1.5">
43
+ <div className="flex flex-wrap items-center gap-1.5">
42
44
  {SHAPES.map(({ id, label: shapeLabel, Icon }) => (
43
45
  <button
44
46
  className={pill}
@@ -49,7 +51,7 @@ export function ShapeChoice({
49
51
  type="button"
50
52
  >
51
53
  <Icon aria-hidden className="h-3 w-3" />
52
- {shapeLabel}
54
+ {i18n._(shapeLabel)}
53
55
  </button>
54
56
  ))}
55
57
  </div>
@@ -4,7 +4,7 @@ import {
4
4
  type AnyNodeId,
5
5
  useLiveNodeOverrides,
6
6
  useScene,
7
- type WallFaceRegion,
7
+ type WallFaceRegion, usePascalI18n,
8
8
  } from '@pascal-app/core'
9
9
  import { useViewer } from '@pascal-app/viewer'
10
10
  import { Trash2 } from 'lucide-react'
@@ -25,6 +25,7 @@ const ROW_ACTION_CLASS =
25
25
 
26
26
  /** The wall panel's "Paint regions" section: one row per region, select or delete. */
27
27
  export function WallPaintRegionList({ wallId }: { wallId: string }) {
28
+ const i18n = usePascalI18n()
28
29
  const nodes = useScene((state) => state.nodes)
29
30
  const materials = useScene((state) => state.materials)
30
31
  const unit = useViewer((state) => state.unit)
@@ -48,7 +49,7 @@ export function WallPaintRegionList({ wallId }: { wallId: string }) {
48
49
  return (
49
50
  // Focus inside this list still lets Delete target the active region.
50
51
  <div className="contents" data-wall-region-list={wallId}>
51
- <PanelSection title="Paint regions">
52
+ <PanelSection title={i18n._({ id: 'editor.panels.wallRegionList.paintRegions', message: 'Paint regions' })}>
52
53
  <div className="flex flex-col gap-1">
53
54
  {rows.map((row) => {
54
55
  const isActive = active?.wallId === wallId && active.regionId === row.id
@@ -75,6 +76,7 @@ export function WallPaintRegionList({ wallId }: { wallId: string }) {
75
76
  backgroundImage: row.finish.imageUrl
76
77
  ? `url(${row.finish.imageUrl})`
77
78
  : undefined,
79
+ backgroundBlendMode: row.finish.tinted ? 'multiply' : undefined,
78
80
  }}
79
81
  />
80
82
  <span className="flex min-w-0 flex-col leading-tight">
@@ -85,13 +87,13 @@ export function WallPaintRegionList({ wallId }: { wallId: string }) {
85
87
  </span>
86
88
  </button>
87
89
  <button
88
- aria-label="Delete region"
90
+ aria-label={i18n._({ id: 'editor.panels.wallRegionList.deleteRegion', message: 'Delete region' })}
89
91
  className={ROW_ACTION_CLASS}
90
92
  onClick={() => {
91
93
  if (isActive) setActive(null)
92
94
  if (removeWallRegion(wallId, row.id)) sfxEmitter.emit('sfx:structure-delete')
93
95
  }}
94
- title="Delete region"
96
+ title={i18n._({ id: 'editor.panels.wallRegionList.deleteRegion', message: 'Delete region' })}
95
97
  type="button"
96
98
  >
97
99
  <Trash2 className="h-3 w-3" />
@@ -99,9 +101,7 @@ export function WallPaintRegionList({ wallId }: { wallId: string }) {
99
101
  </div>
100
102
  )
101
103
  })}
102
- {capNotice && (
103
- <div className="px-1 text-[11px] text-muted-foreground">{capNotice}</div>
104
- )}
104
+ {capNotice && <div className="px-1 text-[11px] text-muted-foreground">{capNotice}</div>}
105
105
  </div>
106
106
  </PanelSection>
107
107
  </div>
@@ -1,6 +1,8 @@
1
1
  import { Icon } from '@iconify/react'
2
+ import { usePascalI18n } from '@pascal-app/core'
2
3
  import type * as React from 'react'
3
4
 
5
+ import { toolHintKey } from '../../../i18n/node-messages'
4
6
  import { cn } from '../../../lib/utils'
5
7
 
6
8
  const MOUSE_SHORTCUTS = {
@@ -70,20 +72,22 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
70
72
  const isCommand = COMMAND_VALUES.has(value)
71
73
  const isShift = value === 'Shift'
72
74
  const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
73
- const commandLabel = IS_MAC ? 'Command' : 'Control'
75
+ const i18n = usePascalI18n()
76
+ const commandLabel = toolHintKey(IS_MAC ? 'Command' : 'Control', i18n)
77
+ const mouseLabel = mouseShortcut ? toolHintKey(mouseShortcut.label, i18n) : undefined
78
+ const shown = displayValue ?? toolHintKey(value, i18n)
74
79
 
75
80
  return (
76
81
  <kbd
77
82
  aria-label={
78
- mouseShortcut?.label ??
79
- (isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
83
+ mouseLabel ?? (isCommand ? commandLabel : isShift ? 'Shift' : shown)
80
84
  }
81
85
  className={cn(
82
86
  'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
83
87
  (mouseShortcut || isShift) && 'justify-center px-1.5',
84
88
  className,
85
89
  )}
86
- title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
90
+ title={mouseLabel ?? (isCommand ? commandLabel : isShift ? 'Shift' : shown)}
87
91
  {...props}
88
92
  >
89
93
  {mouseShortcut ? (
@@ -96,7 +100,7 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
96
100
  icon={mouseShortcut.icon}
97
101
  width={14}
98
102
  />
99
- <span className="sr-only">{mouseShortcut.label}</span>
103
+ <span className="sr-only">{mouseLabel}</span>
100
104
  </>
101
105
  ) : isShift ? (
102
106
  // Icon rather than the ⇧ text glyph — the font renders the glyph's
@@ -117,7 +121,7 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
117
121
  // it up a touch on Mac. "Ctrl" stays at the token's normal size.
118
122
  <span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
119
123
  ) : (
120
- (displayValue ?? value)
124
+ shown
121
125
  )}
122
126
  </kbd>
123
127
  )
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
3
4
  import { Plus } from 'lucide-react'
4
5
  import type { ComponentType, ReactNode } from 'react'
5
6
  import {
@@ -32,19 +33,19 @@ interface IconRailProps {
32
33
  className?: string
33
34
  }
34
35
 
35
- const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
36
+ const sitePanel: { id: PanelId; iconSrc: string; label: MessageDescriptor } = {
36
37
  id: 'site',
37
38
  iconSrc: '/icons/level.webp',
38
- label: 'Site',
39
+ label: /* i18n */ { id: 'editor.sidebar.iconRail.site', message: 'Site' },
39
40
  }
40
41
 
41
- const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
42
+ const settingsPanel: { id: PanelId; iconSrc: string; label: MessageDescriptor } = {
42
43
  id: 'settings',
43
44
  iconSrc: '/icons/settings.webp',
44
- label: 'Settings',
45
+ label: /* i18n */ { id: 'editor.sidebar.iconRail.settings', message: 'Settings' },
45
46
  }
46
47
 
47
- const panels: { id: PanelId; iconSrc: string; label: string }[] = [sitePanel, settingsPanel]
48
+ const panels: { id: PanelId; iconSrc: string; label: MessageDescriptor }[] = [sitePanel, settingsPanel]
48
49
 
49
50
  export function IconRail({
50
51
  activePanel,
@@ -53,6 +54,7 @@ export function IconRail({
53
54
  extraPanels,
54
55
  className,
55
56
  }: IconRailProps) {
57
+ const i18n = usePascalI18n()
56
58
  const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
57
59
  const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
58
60
  const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
@@ -113,7 +115,7 @@ export function IconRail({
113
115
  type="button"
114
116
  >
115
117
  <img
116
- alt={panel.label}
118
+ alt={i18n._(panel.label)}
117
119
  className={cn(
118
120
  'h-6 w-6 object-contain transition-all',
119
121
  !isActive && 'opacity-50 saturate-0',
@@ -122,7 +124,7 @@ export function IconRail({
122
124
  />
123
125
  </button>
124
126
  </TooltipTrigger>
125
- <TooltipContent side="right">{panel.label}</TooltipContent>
127
+ <TooltipContent side="right">{i18n._(panel.label)}</TooltipContent>
126
128
  </Tooltip>
127
129
  )
128
130
  })}
@@ -144,7 +146,7 @@ export function IconRail({
144
146
  type="button"
145
147
  >
146
148
  <img
147
- alt={panel.label}
149
+ alt={i18n._(panel.label)}
148
150
  className={cn(
149
151
  'h-6 w-6 object-contain transition-all',
150
152
  !isActive && 'opacity-50 saturate-0',
@@ -153,7 +155,7 @@ export function IconRail({
153
155
  />
154
156
  </button>
155
157
  </TooltipTrigger>
156
- <TooltipContent side="right">{panel.label}</TooltipContent>
158
+ <TooltipContent side="right">{i18n._(panel.label)}</TooltipContent>
157
159
  </Tooltip>
158
160
  )
159
161
  })}
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import type { AssetInput } from '@pascal-app/core'
3
+ import { type AssetInput, type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
4
4
  import { Root as TooltipRoot } from '@radix-ui/react-tooltip'
5
5
  import NextImage from 'next/image'
6
6
  import { useMemo, useState } from 'react'
@@ -21,10 +21,10 @@ export type FunctionTreeNode = {
21
21
  children: FunctionTreeNode[]
22
22
  }
23
23
 
24
- const SOURCE_CHIPS: Array<{ id: NonNullable<AssetInput['source']>; label: string }> = [
25
- { id: 'library', label: 'Library' },
26
- { id: 'community', label: 'Community' },
27
- { id: 'mine', label: 'Mine' },
24
+ const SOURCE_CHIPS: Array<{ id: NonNullable<AssetInput['source']>; label: MessageDescriptor }> = [
25
+ { id: 'library', label: /* i18n */ { id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.library', message: 'Library' } },
26
+ { id: 'community', label: /* i18n */ { id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.community', message: 'Community' } },
27
+ { id: 'mine', label: /* i18n */ { id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.mine', message: 'Mine' } },
28
28
  ]
29
29
 
30
30
  /** Every slug at or below `node`, so a non-leaf selection matches descendants. */
@@ -64,6 +64,7 @@ export function FunctionTreePanel({
64
64
  leadingTile?: React.ReactNode
65
65
  emptyState?: React.ReactNode
66
66
  }) {
67
+ const i18n = usePascalI18n()
67
68
  const [activeRootSlug, setActiveRootSlug] = useState<string | null>(
68
69
  functionTree[0]?.slug ?? null,
69
70
  )
@@ -178,7 +179,7 @@ export function FunctionTreePanel({
178
179
  setSearch(e.target.value)
179
180
  onSearchChange?.(e.target.value)
180
181
  }}
181
- placeholder="Search..."
182
+ placeholder={i18n._({ id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.search', message: 'Search...' })}
182
183
  type="text"
183
184
  value={search}
184
185
  />
@@ -197,7 +198,7 @@ export function FunctionTreePanel({
197
198
  onClick={() => setActiveSource(isActive ? null : chip.id)}
198
199
  type="button"
199
200
  >
200
- {chip.label}
201
+ {i18n._(chip.label)}
201
202
  </button>
202
203
  )
203
204
  })}
@@ -217,7 +218,7 @@ export function FunctionTreePanel({
217
218
  onClick={() => setActiveChildSlug(null)}
218
219
  type="button"
219
220
  >
220
- All
221
+ {i18n._({ id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.all', message: 'All' })}
221
222
  </button>
222
223
  {activeRoot.children.map((child) => {
223
224
  const isActive = activeChildSlug === child.slug
@@ -250,8 +251,7 @@ export function FunctionTreePanel({
250
251
  ) : isServerSearch && search && searchResults?.length === 0 ? (
251
252
  (emptyState ?? (
252
253
  <div className="flex h-full items-center justify-center text-muted-foreground text-xs">
253
- No results for &ldquo;{search}&rdquo;
254
- </div>
254
+ {i18n._({ id: 'editor.sidebar.panels.itemsPanel.functionTreePanel.noResultsForSearch', message: 'No results for “{search}”', values: { search } })}</div>
255
255
  ))
256
256
  ) : (
257
257
  <ItemCatalog
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import type { AssetInput } from '@pascal-app/core'
4
5
  import NextImage from 'next/image'
5
6
  import { useEffect, useState } from 'react'
@@ -98,6 +99,7 @@ function LegacyItemsPanel({
98
99
  showSourceFilter?: boolean
99
100
  showTagFilters?: boolean
100
101
  }) {
102
+ const i18n = usePascalI18n()
101
103
  const mode = useEditor((s) => s.mode)
102
104
  const catalogCategory = useEditor((s) => s.catalogCategory)
103
105
  const setMode = useEditor((s) => s.setMode)
@@ -167,9 +169,9 @@ function LegacyItemsPanel({
167
169
  // filter even before they own any items. Selecting "Mine" with no
168
170
  // matching items falls through to the empty/no-results state.
169
171
  const sourceChips: Array<{ id: AssetInput['source']; label: string }> = [
170
- { id: 'library', label: 'Library' },
171
- { id: 'community', label: 'Community' },
172
- { id: 'mine', label: 'Mine' },
172
+ { id: 'library', label: i18n._({ id: 'editor.sidebar.panels.itemsPanel.library', message: 'Library' }) },
173
+ { id: 'community', label: i18n._({ id: 'editor.sidebar.panels.itemsPanel.community', message: 'Community' }) },
174
+ { id: 'mine', label: i18n._({ id: 'editor.sidebar.panels.itemsPanel.mine', message: 'Mine' }) },
173
175
  ]
174
176
  const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? [])))
175
177
  const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t))
@@ -215,13 +217,13 @@ function LegacyItemsPanel({
215
217
  type="button"
216
218
  >
217
219
  <NextImage
218
- alt={cat.label}
220
+ alt={i18n._(cat.label)}
219
221
  className={cn('size-7 object-contain', !isActive && 'opacity-60 grayscale')}
220
222
  height={28}
221
223
  src={cat.iconSrc}
222
224
  width={28}
223
225
  />
224
- <span className="font-medium text-[10px] leading-none">{cat.label}</span>
226
+ <span className="font-medium text-[10px] leading-none">{i18n._(cat.label)}</span>
225
227
  </button>
226
228
  )
227
229
  })}
@@ -242,7 +244,7 @@ function LegacyItemsPanel({
242
244
  setSearch(e.target.value)
243
245
  onSearchChange?.(e.target.value)
244
246
  }}
245
- placeholder="Search..."
247
+ placeholder={i18n._({ id: 'editor.sidebar.panels.itemsPanel.search', message: 'Search...' })}
246
248
  type="text"
247
249
  value={search}
248
250
  />
@@ -284,7 +286,7 @@ function LegacyItemsPanel({
284
286
  onClick={() => setActivePlacementTag(null)}
285
287
  type="button"
286
288
  >
287
- All
289
+ {i18n._({ id: 'editor.sidebar.panels.itemsPanel.all', message: 'All' })}
288
290
  </button>
289
291
  {placementTags.map((tag) => {
290
292
  const count = placementCount(tag)
@@ -376,8 +378,7 @@ function LegacyItemsPanel({
376
378
  ) : isServerSearch && search && searchResults?.length === 0 ? (
377
379
  (emptyState ?? (
378
380
  <div className="flex h-full items-center justify-center text-muted-foreground text-xs">
379
- No results for &ldquo;{search}&rdquo;
380
- </div>
381
+ {i18n._({ id: 'editor.sidebar.panels.itemsPanel.noResultsForSearch', message: 'No results for “{search}”', values: { search } })}</div>
381
382
  ))
382
383
  ) : (
383
384
  <ItemCatalog
@@ -73,6 +73,7 @@ import { Switch } from './../../../../../components/ui/primitives/switch'
73
73
  import { cn } from './../../../../../lib/utils'
74
74
  import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
75
75
  import { addStoreyBelow, addStoreyOnTop } from './../../../../../lib/add-storey'
76
+ import { offerExteriorWallCopy } from './../../../../../components/ui/level-wall-copy-prompt'
76
77
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
77
78
  import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
78
79
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
@@ -423,10 +424,12 @@ function FloorSettingsSection() {
423
424
  const addFloor = () => {
424
425
  const newLevel = addStoreyOnTop(building.id)
425
426
  setSelection({ buildingId: building.id, levelId: newLevel.id })
427
+ offerExteriorWallCopy(newLevel.id)
426
428
  }
427
429
  const addBasement = () => {
428
430
  const newLevel = addStoreyBelow(building.id)
429
431
  setSelection({ buildingId: building.id, levelId: newLevel.id })
432
+ offerExteriorWallCopy(newLevel.id)
430
433
  }
431
434
 
432
435
  return (
@@ -1,3 +1,4 @@
1
+ import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
1
2
  import { Keyboard } from 'lucide-react'
2
3
  import { Button } from './../../../../../components/ui/primitives/button'
3
4
  import {
@@ -12,27 +13,31 @@ import {
12
13
  ShortcutToken,
13
14
  shortcutDisplayValue,
14
15
  } from './../../../../../components/ui/primitives/shortcut-token'
16
+ import { TOOL_SHORTCUT_KEYS } from './../../../../../lib/tool-shortcuts'
15
17
 
16
18
  type Shortcut = {
17
- keys: string[]
19
+ keys: readonly string[]
18
20
  action: string
19
21
  note?: string
20
22
  }
21
23
 
22
24
  type ShortcutCategory = {
23
- title: string
25
+ title: MessageDescriptor
24
26
  shortcuts: Shortcut[]
25
27
  }
26
28
 
27
29
  const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
28
30
  {
29
- title: 'Editor Navigation',
31
+ title: /* i18n */ {
32
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.editorNavigation',
33
+ message: 'Editor Navigation',
34
+ },
30
35
  shortcuts: [
31
36
  { keys: ['1'], action: 'Switch to Site' },
32
37
  { keys: ['2'], action: 'Switch to Building' },
33
38
  { keys: ['\\'], action: 'Open or close the split view' },
34
- { keys: ['F'], action: 'Open the items catalog with the item tool' },
35
- { keys: ['Z'], action: 'Draw a zone' },
39
+ { keys: TOOL_SHORTCUT_KEYS.item, action: 'Open the items catalog with the item tool' },
40
+ { keys: TOOL_SHORTCUT_KEYS.zone, action: 'Draw a zone' },
36
41
  {
37
42
  keys: ['Cmd/Ctrl', 'Arrow Up'],
38
43
  action: 'Select next level in the active building',
@@ -45,12 +50,21 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
45
50
  ],
46
51
  },
47
52
  {
48
- title: 'Modes & History',
53
+ title: /* i18n */ {
54
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.modesHistory',
55
+ message: 'Modes & History',
56
+ },
49
57
  shortcuts: [
50
- { keys: ['V'], action: 'Switch to Select mode' },
51
- { keys: ['B'], action: 'Switch to Build mode' },
52
- { keys: ['M'], action: 'Activate the last measurement tool' },
53
- { keys: ['X'], action: 'Switch to Delete mode' },
58
+ { keys: TOOL_SHORTCUT_KEYS.select, action: 'Switch to Select mode' },
59
+ {
60
+ keys: TOOL_SHORTCUT_KEYS.wall,
61
+ action: 'Switch to Build mode, drawing with the last Rooms shape',
62
+ note: 'Rectangle, Polygon or Walls, whichever you picked last; Rectangle the first time.',
63
+ },
64
+ { keys: TOOL_SHORTCUT_KEYS.measurement, action: 'Activate the last measurement tool' },
65
+ { keys: TOOL_SHORTCUT_KEYS.paint, action: 'Switch to Paint mode' },
66
+ { keys: TOOL_SHORTCUT_KEYS.terrain, action: 'Sculpt the ground (terrain)' },
67
+ { keys: TOOL_SHORTCUT_KEYS.delete, action: 'Switch to Delete mode' },
54
68
  {
55
69
  keys: ['Esc'],
56
70
  action: 'Cancel the active tool and return to Select mode',
@@ -63,7 +77,30 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
63
77
  ],
64
78
  },
65
79
  {
66
- title: 'Selection',
80
+ title: /* i18n */ {
81
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.buildTools',
82
+ message: 'Build Tools',
83
+ },
84
+ shortcuts: [
85
+ {
86
+ keys: TOOL_SHORTCUT_KEYS.door,
87
+ action: 'Place a door',
88
+ note: 'D alone pans the view. While placing, the door swings toward the side of the wall the cursor is on; R flips it.',
89
+ },
90
+ { keys: TOOL_SHORTCUT_KEYS.window, action: 'Place a window', note: 'W alone pans the view.' },
91
+ { keys: TOOL_SHORTCUT_KEYS.stair, action: 'Place stairs', note: 'S alone pans the view.' },
92
+ {
93
+ keys: TOOL_SHORTCUT_KEYS.roof,
94
+ action: 'Draw a roof',
95
+ note: "R alone still rotates or flips the selection, and while placing it stays the placement's own R.",
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ title: /* i18n */ {
101
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.selection',
102
+ message: 'Selection',
103
+ },
67
104
  shortcuts: [
68
105
  {
69
106
  keys: ['Cmd/Ctrl', 'C'],
@@ -78,8 +115,7 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
78
115
  {
79
116
  keys: ['Cmd/Ctrl', 'V'],
80
117
  action: 'Paste and place copied objects',
81
- note:
82
- 'Carries a preview under the cursor. Click to place it, or press Escape to cancel.',
118
+ note: 'Carries a preview under the cursor. Click to place it, or press Escape to cancel.',
83
119
  },
84
120
  {
85
121
  keys: ['Cmd/Ctrl', 'Left click'],
@@ -105,14 +141,23 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
105
141
  {
106
142
  keys: ['Cmd/Ctrl', 'G'],
107
143
  action: 'Group the multi-selection (session only)',
108
- note:
109
- 'Editor-only. Plain click a member later to reselect the whole group. Not saved with the project.',
144
+ note: 'Editor-only. Plain click a member later to reselect the whole group. Not saved with the project.',
110
145
  },
111
146
  {
112
147
  keys: ['Cmd/Ctrl', 'Shift', 'G'],
113
148
  action: 'Ungroup the session selection',
114
149
  note: 'Keeps the current selection; only dissolves the session group.',
115
150
  },
151
+ {
152
+ keys: ['Arrow keys'],
153
+ action: 'Nudge the selected wall 1 in (1 cm in metric)',
154
+ note: 'In the floor plan. Stops where it lines up with a parallel wall; joined walls follow.',
155
+ },
156
+ {
157
+ keys: ['Tab'],
158
+ action: 'Select the next object under the pointer',
159
+ note: 'In the floor plan, for walls that overlap or hide under another. Shift+Tab goes back.',
160
+ },
116
161
  {
117
162
  keys: ['Esc'],
118
163
  action: 'Clear the selection',
@@ -121,7 +166,10 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
121
166
  ],
122
167
  },
123
168
  {
124
- title: 'Direct Manipulation',
169
+ title: /* i18n */ {
170
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.directManipulation',
171
+ message: 'Direct Manipulation',
172
+ },
125
173
  shortcuts: [
126
174
  {
127
175
  keys: ['Cmd/Ctrl', 'Left click'],
@@ -141,7 +189,10 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
141
189
  ],
142
190
  },
143
191
  {
144
- title: 'Drawing Tools',
192
+ title: /* i18n */ {
193
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.drawingTools',
194
+ message: 'Drawing Tools',
195
+ },
145
196
  shortcuts: [
146
197
  // Shift and Ctrl each mean one thing held and another tapped, and only
147
198
  // the hold was documented — which read as the taps not existing. Both
@@ -157,9 +208,9 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
157
208
  note: 'Tap and release on its own. Use it when the default half-metre grid is too coarse — placing a window, for instance.',
158
209
  },
159
210
  {
160
- keys: ['Shift'],
161
- action: 'Bypass guided snapping and angle constraints',
162
- note: 'Hold during the active gesture. Passive guide or measurement feedback may stay visible.',
211
+ keys: ['Alt'],
212
+ action: 'Bypass snapping, angle constraints and collisions',
213
+ note: 'Hold during the active gesture; release to snap again. Passive guide or measurement feedback may stay visible.',
163
214
  },
164
215
  {
165
216
  keys: ['Shift'],
@@ -169,7 +220,10 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
169
220
  ],
170
221
  },
171
222
  {
172
- title: 'Item Placement',
223
+ title: /* i18n */ {
224
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.itemPlacement',
225
+ message: 'Item Placement',
226
+ },
173
227
  shortcuts: [
174
228
  {
175
229
  keys: ['R', 'T'],
@@ -177,7 +231,8 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
177
231
  },
178
232
  {
179
233
  keys: ['E'],
180
- action: 'Operate the selected node — doors, windows, and cabinet doors/drawers animate open/closed',
234
+ action:
235
+ 'Operate the selected node — doors, windows, and cabinet doors/drawers animate open/closed',
181
236
  },
182
237
  {
183
238
  keys: ['Shift'],
@@ -187,12 +242,15 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
187
242
  ],
188
243
  },
189
244
  {
190
- title: 'Camera',
245
+ title: /* i18n */ {
246
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.camera',
247
+ message: 'Camera',
248
+ },
191
249
  shortcuts: [
192
250
  {
193
251
  keys: ['W', 'A', 'S', 'D'],
194
252
  action: 'Pan camera',
195
- note: 'Moves in screen space, similar to dragging the camera view.',
253
+ note: 'Moves in screen space, similar to dragging the camera view. With Shift, W, S and D place a window, stairs and a door instead.',
196
254
  },
197
255
  {
198
256
  keys: ['Middle click'],
@@ -208,7 +266,7 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
208
266
  },
209
267
  ]
210
268
 
211
- function ShortcutKeys({ keys }: { keys: string[] }) {
269
+ function ShortcutKeys({ keys }: { keys: readonly string[] }) {
212
270
  return (
213
271
  <div className="flex flex-wrap items-center gap-1">
214
272
  {keys.map((key, index) => (
@@ -222,32 +280,44 @@ function ShortcutKeys({ keys }: { keys: string[] }) {
222
280
  }
223
281
 
224
282
  export function KeyboardShortcutsDialog() {
283
+ const i18n = usePascalI18n()
225
284
  return (
226
285
  <Dialog>
227
286
  <DialogTrigger asChild>
228
287
  <Button className="w-full justify-start gap-2" variant="outline">
229
288
  <Keyboard className="size-4" />
230
- Keyboard shortcuts
289
+ {i18n._({
290
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.keyboardShortcuts',
291
+ message: 'Keyboard shortcuts',
292
+ })}
231
293
  </Button>
232
294
  </DialogTrigger>
233
295
  <DialogContent className="flex max-h-[85vh] flex-col overflow-hidden p-0 sm:max-w-3xl">
234
296
  <DialogHeader className="shrink-0 border-b px-6 py-4">
235
- <DialogTitle>Keyboard shortcuts</DialogTitle>
297
+ <DialogTitle>
298
+ {i18n._({
299
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.keyboardShortcuts',
300
+ message: 'Keyboard shortcuts',
301
+ })}
302
+ </DialogTitle>
236
303
  <DialogDescription>
237
- Shortcuts are context-aware. Guided constraints are enabled by default; hold Shift
238
- during an active gesture to build freely.
304
+ {i18n._({
305
+ id: 'editor.sidebar.panels.settingsPanel.keyboardShortcutsDialog.shortcutsAreContextAwareGuidedConstraints',
306
+ message:
307
+ 'Shortcuts are context-aware. Snapping is a visible mode: tap Shift to cycle it, hold Alt during a gesture to place freely.',
308
+ })}
239
309
  </DialogDescription>
240
310
  </DialogHeader>
241
311
 
242
312
  <div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
243
313
  {SHORTCUT_CATEGORIES.map((category) => (
244
- <section className="space-y-2" key={category.title}>
245
- <h3 className="font-medium text-sm">{category.title}</h3>
314
+ <section className="space-y-2" key={category.title.id}>
315
+ <h3 className="font-medium text-sm">{i18n._(category.title)}</h3>
246
316
  <div className="overflow-hidden rounded-md border border-border/80">
247
317
  {category.shortcuts.map((shortcut, index) => (
248
318
  <div
249
319
  className="grid grid-cols-[minmax(130px,220px)_1fr] gap-3 px-3 py-2"
250
- key={`${category.title}-${shortcut.action}`}
320
+ key={`${category.title.id}-${shortcut.action}`}
251
321
  >
252
322
  <ShortcutKeys keys={shortcut.keys} />
253
323
  <div>