@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
@@ -55,11 +55,14 @@ import {
55
55
  type LevelDuplicatePreset,
56
56
  } from '@pascal-app/core/building'
57
57
  import {
58
- getDefaultLevelName,
59
- getLevelDisplayName,
58
+ formatNumber,
60
59
  roofLevelOrderBreach,
61
60
  servesAsRoofLevel,
61
+ useLocale,
62
+ usePascalI18n,
62
63
  } from '@pascal-app/core'
64
+ import { authoredLevelName, localizedLevelName } from '../../i18n/level-name'
65
+ import { messageWithSlot } from '../../i18n/message-slot'
63
66
  import { addStoreyBelow, addStoreyOnTop, setGroundFloor } from '../../lib/add-storey'
64
67
  import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
65
68
  import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
@@ -69,6 +72,7 @@ import { useIsMobile } from '../../hooks/use-mobile'
69
72
  import { ActionButton } from './controls/action-button'
70
73
  import { SliderControl } from './controls/slider-control'
71
74
  import { LevelDuplicateDialog } from './level-duplicate-dialog'
75
+ import { LevelWallCopyPrompt, offerExteriorWallCopy } from './level-wall-copy-prompt'
72
76
  import {
73
77
  Dialog,
74
78
  DialogContent,
@@ -90,28 +94,31 @@ function LevelInlineRename({
90
94
  isEditing: boolean
91
95
  onStopEditing: () => void
92
96
  }) {
97
+ const i18n = usePascalI18n()
98
+ const locale = useLocale()
93
99
  const updateNode = useScene((s) => s.updateNode)
94
- const defaultName = getDefaultLevelName(level.level)
95
- const [value, setValue] = useState(level.name || '')
100
+ const defaultName = localizedLevelName({ level: level.level }, i18n, locale)
101
+ const authoredName = authoredLevelName(level)
102
+ const [value, setValue] = useState(authoredName ?? '')
96
103
  const inputRef = useRef<HTMLInputElement>(null)
97
104
 
98
105
  useEffect(() => {
99
106
  if (isEditing) {
100
- setValue(level.name || '')
107
+ setValue(authoredName ?? '')
101
108
  setTimeout(() => {
102
109
  inputRef.current?.focus()
103
110
  inputRef.current?.select()
104
111
  }, 0)
105
112
  }
106
- }, [isEditing, level.name])
113
+ }, [isEditing, authoredName])
107
114
 
108
115
  const handleSave = useCallback(() => {
109
116
  const trimmed = value.trim()
110
- if (trimmed !== level.name) {
117
+ if ((trimmed || undefined) !== authoredName) {
111
118
  updateNode(level.id, { name: trimmed || undefined })
112
119
  }
113
120
  onStopEditing()
114
- }, [value, level.id, level.name, updateNode, onStopEditing])
121
+ }, [value, level.id, authoredName, updateNode, onStopEditing])
115
122
 
116
123
  if (!isEditing) return null
117
124
 
@@ -173,14 +180,19 @@ function LevelRow({
173
180
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
174
181
  const [isEditing, setIsEditing] = useState(false)
175
182
  const updateNode = useScene((s) => s.updateNode)
183
+ const i18n = usePascalI18n()
184
+ const locale = useLocale()
176
185
  const { isImperial, toDisplay, displayUnit, precision: displayPrecision } = useLinearDisplay('m', 2)
177
186
 
178
187
  const storeyHeight = getStoredLevelHeight(level)
179
188
  // Decimal units keep the compact readout; integer millimeters must retain trailing zeroes.
180
- const formattedStoreyHeight = toDisplay(storeyHeight).toFixed(displayPrecision)
181
- const storeyHeightLabel = `${
182
- displayPrecision > 0 ? formattedStoreyHeight.replace(/0$/, '') : formattedStoreyHeight
183
- } ${displayUnit}`
189
+ const formattedStoreyHeight = formatNumber(toDisplay(storeyHeight), {
190
+ locale,
191
+ min: Math.max(0, displayPrecision - 1),
192
+ max: displayPrecision,
193
+ })
194
+ const storeyHeightLabel = `${formattedStoreyHeight} ${displayUnit}`
195
+ const levelName = localizedLevelName(level, i18n, locale)
184
196
  // Same rule as the site panel and command palette: the ordinal-0 ground
185
197
  // floor is the vertical model's zero anchor and must never be deletable.
186
198
  const canDeleteLevel = level.level !== 0
@@ -219,7 +231,7 @@ function LevelRow({
219
231
  >
220
232
  <button
221
233
  {...dragHandleProps}
222
- aria-label={`Reorder ${getLevelDisplayName(level)}`}
234
+ aria-label={i18n._({ id: 'editor.floatingLevelSelector.reorderLevelname', message: 'Reorder {levelName}', values: { levelName } })}
223
235
  className={cn(
224
236
  'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-black/4 dark:hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
225
237
  isDragging && 'cursor-grabbing opacity-100',
@@ -229,7 +241,7 @@ function LevelRow({
229
241
  dragHandleProps?.onClick?.(e)
230
242
  }}
231
243
  ref={dragHandleRef}
232
- title="Drag to reorder"
244
+ title={i18n._({ id: 'editor.floatingLevelSelector.dragToReorder', message: 'Drag to reorder' })}
233
245
  type="button"
234
246
  >
235
247
  <GripVertical className="h-3.5 w-3.5" />
@@ -242,15 +254,15 @@ function LevelRow({
242
254
  e.stopPropagation()
243
255
  setIsEditing(true)
244
256
  }}
245
- title={getLevelDisplayName(level)}
257
+ title={levelName}
246
258
  type="button"
247
259
  >
248
- <span className="truncate">{getLevelDisplayName(level)}</span>
260
+ <span className="truncate">{levelName}</span>
249
261
  {unitDotColor && (
250
262
  <span
251
263
  className="ml-1.5 h-1.5 w-1.5 shrink-0 rounded-full"
252
264
  style={{ backgroundColor: unitDotColor }}
253
- title="Focused unit has zones here"
265
+ title={i18n._({ id: 'editor.floatingLevelSelector.focusedUnitHasZonesHere', message: 'Focused unit has zones here' })}
254
266
  />
255
267
  )}
256
268
  </button>
@@ -261,7 +273,7 @@ function LevelRow({
261
273
  <button
262
274
  className="mr-0.5 shrink-0 whitespace-nowrap rounded px-1 py-0.5 font-mono text-[10px] text-muted-foreground tabular-nums transition-colors hover:bg-black/4 hover:text-foreground dark:text-muted-foreground/50 dark:hover:bg-white/5 dark:hover:text-foreground"
263
275
  onClick={(e) => e.stopPropagation()}
264
- title="Level height"
276
+ title={i18n._({ id: 'editor.floatingLevelSelector.levelHeight', message: 'Level height' })}
265
277
  type="button"
266
278
  >
267
279
  {storeyHeightLabel}
@@ -275,7 +287,7 @@ function LevelRow({
275
287
  sideOffset={8}
276
288
  >
277
289
  <SliderControl
278
- label="Level height"
290
+ label={i18n._({ id: 'editor.floatingLevelSelector.levelHeight', message: 'Level height' })}
279
291
  max={20}
280
292
  min={1}
281
293
  onChange={(v) => updateNode(level.id, { height: v })}
@@ -301,14 +313,19 @@ function LevelRow({
301
313
  <Popover>
302
314
  <PopoverTrigger asChild>
303
315
  <button
304
- className="flex h-5 w-4 shrink-0 items-center justify-center text-muted-foreground/40 opacity-0 transition-all hover:text-foreground group-hover/level:opacity-100"
316
+ aria-label={i18n._({
317
+ id: 'editor.layers.levelOptions',
318
+ message: '{levelName} options',
319
+ values: { levelName },
320
+ })}
321
+ className="flex h-5 w-4 shrink-0 items-center justify-center text-muted-foreground/40 opacity-0 transition-all hover:text-foreground group-hover/level:opacity-100 focus-visible:opacity-100"
305
322
  onClick={(e) => e.stopPropagation()}
306
323
  type="button"
307
324
  >
308
325
  <MoreVertical className="h-3 w-3" />
309
326
  </button>
310
327
  </PopoverTrigger>
311
- <PopoverContent align="start" className="w-44 p-1" side="right" sideOffset={8}>
328
+ <PopoverContent align="start" className="w-max min-w-44 max-w-72 p-1" side="right" sideOffset={8}>
312
329
  <button
313
330
  className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
314
331
  onClick={(e) => {
@@ -318,7 +335,7 @@ function LevelRow({
318
335
  type="button"
319
336
  >
320
337
  <Copy className="h-3 w-3" />
321
- Duplicate level
338
+ {i18n._({ id: 'editor.floatingLevelSelector.duplicateLevel', message: 'Duplicate level' })}
322
339
  </button>
323
340
  <button
324
341
  className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
@@ -329,7 +346,7 @@ function LevelRow({
329
346
  type="button"
330
347
  >
331
348
  <Copy className="h-3 w-3" />
332
- Duplicate with options...
349
+ {i18n._({ id: 'editor.floatingLevelSelector.duplicateWithOptions', message: 'Duplicate with options...' })}
333
350
  </button>
334
351
  <button
335
352
  className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors enabled:hover:bg-black/6 dark:enabled:hover:bg-white/10 enabled:hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
@@ -340,13 +357,13 @@ function LevelRow({
340
357
  }}
341
358
  title={
342
359
  isGround
343
- ? 'This floor stands at grade'
344
- : 'Seat the building so this floor stands at grade; the floors under it are below grade'
360
+ ? i18n._({ id: 'editor.floatingLevelSelector.thisFloorStandsAtGrade', message: 'This floor stands at grade' })
361
+ : i18n._({ id: 'editor.floatingLevelSelector.seatTheBuildingSoThisFloor', message: 'Seat the building so this floor stands at grade; the floors under it are below grade' })
345
362
  }
346
363
  type="button"
347
364
  >
348
365
  <ArrowDownToLine className="h-3 w-3" />
349
- Set as ground floor
366
+ {i18n._({ id: 'editor.floatingLevelSelector.setAsGroundFloor', message: 'Set as ground floor' })}
350
367
  </button>
351
368
  {onPaste && (
352
369
  <button
@@ -358,7 +375,7 @@ function LevelRow({
358
375
  type="button"
359
376
  >
360
377
  <ClipboardPaste className="h-3 w-3" />
361
- Paste copied selection
378
+ {i18n._({ id: 'editor.floatingLevelSelector.pasteCopiedSelection', message: 'Paste copied selection' })}
362
379
  </button>
363
380
  )}
364
381
  <button
@@ -368,11 +385,11 @@ function LevelRow({
368
385
  e.stopPropagation()
369
386
  onRequestDelete()
370
387
  }}
371
- title={canDeleteLevel ? 'Delete level' : 'The ground level cannot be deleted'}
388
+ title={canDeleteLevel ? i18n._({ id: 'editor.floatingLevelSelector.deleteLevel', message: 'Delete level' }) : i18n._({ id: 'editor.floatingLevelSelector.theGroundLevelCannotBeDeleted', message: 'The ground level cannot be deleted' })}
372
389
  type="button"
373
390
  >
374
391
  <Trash2 className="h-3 w-3" />
375
- Delete level
392
+ {i18n._({ id: 'editor.floatingLevelSelector.deleteLevel', message: 'Delete level' })}
376
393
  </button>
377
394
  </PopoverContent>
378
395
  </Popover>
@@ -455,7 +472,6 @@ function SortableLevelRow({
455
472
  type GroundLineState = {
456
473
  levelId: string
457
474
  seated: boolean
458
- atFoundationTop: boolean
459
475
  offset: number
460
476
  }
461
477
 
@@ -466,9 +482,15 @@ type GroundLineState = {
466
482
  * a click seats the building.
467
483
  */
468
484
  function GroundLine({ ground, onSeat }: { ground: GroundLineState; onSeat: () => void }) {
485
+ const i18n = usePascalI18n()
469
486
  const { toDisplay, displayUnit, precision } = useLinearDisplay('m', 2)
470
487
  const over = toDisplay(ground.offset)
471
- const label = `Ground · ${over > 0 ? '+' : ''}${over.toFixed(precision)}`
488
+ const label = i18n._({
489
+ id: 'editor.floatingLevelSelector.groundOffset',
490
+ message: 'Ground · {offset}',
491
+ values: { offset: `${over > 0 ? '+' : ''}${over.toFixed(precision)}` },
492
+ })
493
+ const distance = `${Math.abs(over).toFixed(precision)} ${displayUnit}`
472
494
  return (
473
495
  <div
474
496
  className="relative flex items-center gap-1.5 px-2 py-0.5"
@@ -477,7 +499,7 @@ function GroundLine({ ground, onSeat }: { ground: GroundLineState; onSeat: () =>
477
499
  {ground.seated ? (
478
500
  <span
479
501
  className="shrink-0 font-mono text-[10px] text-muted-foreground/70 tabular-nums"
480
- title="Ground line: the levels under it are below grade"
502
+ title={i18n._({ id: 'editor.floatingLevelSelector.groundLineTheLevelsUnderIt', message: 'Ground line: the levels under it are below grade' })}
481
503
  >
482
504
  {label}
483
505
  </span>
@@ -485,7 +507,21 @@ function GroundLine({ ground, onSeat }: { ground: GroundLineState; onSeat: () =>
485
507
  <button
486
508
  className="shrink-0 rounded-full px-1.5 font-mono text-[10px] text-amber-400 tabular-nums transition-colors hover:bg-amber-400/10"
487
509
  onClick={onSeat}
488
- title={`The ground floor stands ${Math.abs(toDisplay(ground.offset)).toFixed(precision)} ${displayUnit} ${ground.offset > 0 ? 'over' : 'under'} grade. Seat the ground floor 8 in over grade`}
510
+ title={
511
+ ground.offset > 0
512
+ ? i18n._({
513
+ id: 'editor.floatingLevelSelector.groundFloorOverGrade',
514
+ message:
515
+ 'The ground floor stands {distance} over grade. Seat the ground floor 8 in over grade',
516
+ values: { distance },
517
+ })
518
+ : i18n._({
519
+ id: 'editor.floatingLevelSelector.groundFloorUnderGrade',
520
+ message:
521
+ 'The ground floor stands {distance} under grade. Seat the ground floor 8 in over grade',
522
+ values: { distance },
523
+ })
524
+ }
489
525
  type="button"
490
526
  >
491
527
  {label}
@@ -504,6 +540,7 @@ function GroundLine({ ground, onSeat }: { ground: GroundLineState; onSeat: () =>
504
540
  // ── Unit focus chip: name + × to end focus, only while a unit is focused ────
505
541
 
506
542
  function UnitFocusChip() {
543
+ const i18n = usePascalI18n()
507
544
  const focusedUnitId = useViewer((s) => s.focusedUnitId)
508
545
  const focusedUnit = useScene((s) => {
509
546
  const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
@@ -521,12 +558,12 @@ function UnitFocusChip() {
521
558
  className="h-2 w-2 shrink-0 rounded-full"
522
559
  style={{ backgroundColor: focusedUnit.color }}
523
560
  />
524
- <span className="truncate">{focusedUnit.name || 'Unit'}</span>
561
+ <span className="truncate">{focusedUnit.name || i18n._({ id: 'editor.floatingLevelSelector.unit', message: 'Unit' })}</span>
525
562
  <button
526
- aria-label="Exit unit focus"
563
+ aria-label={i18n._({ id: 'editor.floatingLevelSelector.exitUnitFocus', message: 'Exit unit focus' })}
527
564
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
528
565
  onClick={() => leaveUnitFocus()}
529
- title="Exit unit focus"
566
+ title={i18n._({ id: 'editor.floatingLevelSelector.exitUnitFocus', message: 'Exit unit focus' })}
530
567
  type="button"
531
568
  >
532
569
  <X className="h-3 w-3" />
@@ -538,6 +575,8 @@ function UnitFocusChip() {
538
575
  // ── Main component ──────────────────────────────────────────────────────────
539
576
 
540
577
  export function FloatingLevelSelector() {
578
+ const i18n = usePascalI18n()
579
+ const locale = useLocale()
541
580
  // Desktop's view bar is a row above the stage; mobile's floats over it.
542
581
  const isMobile = useIsMobile()
543
582
  const selectedBuildingId = useViewer((s) => s.selection.buildingId)
@@ -596,7 +635,6 @@ export function FloatingLevelSelector() {
596
635
  ? {
597
636
  levelId: seat.levelId,
598
637
  seated: seat.seated,
599
- atFoundationTop: seat.atFoundationTop,
600
638
  offset: seat.offset,
601
639
  }
602
640
  : null
@@ -616,12 +654,14 @@ export function FloatingLevelSelector() {
616
654
  if (!resolvedBuildingId) return
617
655
  const newLevel = addStoreyOnTop(resolvedBuildingId)
618
656
  setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
657
+ offerExteriorWallCopy(newLevel.id)
619
658
  }, [resolvedBuildingId, setSelection])
620
659
 
621
660
  const handleAddBelow = useCallback(() => {
622
661
  if (!resolvedBuildingId) return
623
662
  const newLevel = addStoreyBelow(resolvedBuildingId)
624
663
  setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
664
+ offerExteriorWallCopy(newLevel.id)
625
665
  }, [resolvedBuildingId, setSelection])
626
666
 
627
667
  const handleInsertBetween = useCallback(
@@ -645,6 +685,7 @@ export function FloatingLevelSelector() {
645
685
  .map((l) => ({ id: l.id as AnyNodeId, data: { level: l.level + 1 } as Partial<AnyNode> })),
646
686
  })
647
687
  setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
688
+ offerExteriorWallCopy(newLevel.id, lower.id)
648
689
  },
649
690
  [resolvedBuildingId, levels, applyNodeChanges, setSelection],
650
691
  )
@@ -735,7 +776,10 @@ export function FloatingLevelSelector() {
735
776
 
736
777
  return (
737
778
  <>
738
- <div className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}>
779
+ <div
780
+ className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}
781
+ data-surface="level-selector"
782
+ >
739
783
  <div className="relative">
740
784
  {/* Floating + at top edge: a floor on top, under the roof level when one tops the stack */}
741
785
  {!draggingLevelId && (
@@ -745,7 +789,7 @@ export function FloatingLevelSelector() {
745
789
  // read only from outside: nothing here depends on it.
746
790
  data-guide-target="level-add"
747
791
  onClick={handleAddAbove}
748
- title={topIsRoofLevel ? 'Add a floor under the roof' : 'Add level above'}
792
+ title={topIsRoofLevel ? i18n._({ id: 'editor.floatingLevelSelector.addAFloorUnderTheRoof', message: 'Add a floor under the roof' }) : i18n._({ id: 'editor.floatingLevelSelector.addLevelAbove', message: 'Add level above' })}
749
793
  type="button"
750
794
  >
751
795
  <Plus className="h-2.5 w-2.5" />
@@ -757,7 +801,7 @@ export function FloatingLevelSelector() {
757
801
  <button
758
802
  className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
759
803
  onClick={handleAddBelow}
760
- title="Add level below"
804
+ title={i18n._({ id: 'editor.floatingLevelSelector.addLevelBelow', message: 'Add level below' })}
761
805
  type="button"
762
806
  >
763
807
  <Plus className="h-2.5 w-2.5" />
@@ -799,7 +843,7 @@ export function FloatingLevelSelector() {
799
843
  }
800
844
  onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
801
845
  onPaste={() => handlePasteToLevel(level)}
802
- isGround={ground?.levelId === level.id && ground.atFoundationTop}
846
+ isGround={ground?.levelId === level.id && ground.seated}
803
847
  onRequestDelete={() => setDeletingLevel(level)}
804
848
  onSetGround={() => handleSetGround(level.id)}
805
849
  onSelect={() => {
@@ -816,7 +860,7 @@ export function FloatingLevelSelector() {
816
860
  <button
817
861
  className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
818
862
  onClick={() => handleInsertBetween(sortedIndex - 1)}
819
- title="Insert level here"
863
+ title={i18n._({ id: 'editor.floatingLevelSelector.insertLevelHere', message: 'Insert level here' })}
820
864
  type="button"
821
865
  >
822
866
  <Plus className="h-2.5 w-2.5" />
@@ -834,17 +878,25 @@ export function FloatingLevelSelector() {
834
878
  </DndContext>
835
879
  </div>
836
880
  <UnitFocusChip />
881
+ <LevelWallCopyPrompt />
837
882
  </div>
838
883
 
839
884
  {/* Delete confirmation dialog */}
840
885
  <Dialog onOpenChange={(open) => !open && setDeletingLevel(null)} open={!!deletingLevel}>
841
886
  <DialogContent showCloseButton={false}>
842
887
  <DialogHeader>
843
- <DialogTitle>Delete level</DialogTitle>
888
+ <DialogTitle>{i18n._({ id: 'editor.floatingLevelSelector.deleteLevel', message: 'Delete level' })}</DialogTitle>
844
889
  <DialogDescription>
845
- Are you sure you want to delete{' '}
846
- <strong>{deletingLevel ? getLevelDisplayName(deletingLevel) : ''}</strong>? All
847
- walls, floors, and objects on this level will be permanently removed.
890
+ {messageWithSlot(
891
+ i18n,
892
+ /* i18n */ {
893
+ id: 'editor.floatingLevelSelector.deleteLevelConfirm',
894
+ message:
895
+ 'Are you sure you want to delete {levelName}? All walls, floors, and objects on this level will be permanently removed.',
896
+ },
897
+ 'levelName',
898
+ <strong>{deletingLevel ? localizedLevelName(deletingLevel, i18n, locale) : ''}</strong>,
899
+ )}
848
900
  </DialogDescription>
849
901
  </DialogHeader>
850
902
  <DialogFooter>
@@ -853,14 +905,14 @@ export function FloatingLevelSelector() {
853
905
  onClick={() => setDeletingLevel(null)}
854
906
  type="button"
855
907
  >
856
- Cancel
908
+ {i18n._({ id: 'editor.floatingLevelSelector.cancel', message: 'Cancel' })}
857
909
  </button>
858
910
  <button
859
911
  className="rounded-full bg-red-600 px-4 py-2 text-sm text-white transition-colors hover:bg-red-700"
860
912
  onClick={handleConfirmDelete}
861
913
  type="button"
862
914
  >
863
- Delete
915
+ {i18n._({ id: 'editor.floatingLevelSelector.delete', message: 'Delete' })}
864
916
  </button>
865
917
  </DialogFooter>
866
918
  </DialogContent>
@@ -1,3 +1,4 @@
1
+ import { usePascalI18n } from '@pascal-app/core'
1
2
  import type { HudTitle } from '../../../lib/hud-title'
2
3
  import { ContextualHelperPanel } from './contextual-helper-panel'
3
4
 
@@ -9,13 +10,14 @@ interface BuildingHelperProps {
9
10
  // Rotate is one hint with both keys (R / T) — never two separate
10
11
  // counterclockwise / clockwise rows — to match every other placement helper.
11
12
  export function BuildingHelper({ showRotate, title = null }: BuildingHelperProps) {
13
+ const i18n = usePascalI18n()
12
14
  return (
13
15
  <ContextualHelperPanel
14
16
  title={title}
15
17
  hints={[
16
- { keys: ['Left click'], label: 'Place building' },
17
- ...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
18
- { keys: ['Esc'], label: 'Cancel' },
18
+ { keys: ['Left click'], label: i18n._({ id: 'editor.helpers.buildingHelper.placeBuilding', message: 'Place building' }) },
19
+ ...(showRotate ? [{ keys: ['R', 'T'], label: i18n._({ id: 'editor.helpers.buildingHelper.rotate', message: 'Rotate' }) }] : []),
20
+ { keys: ['Esc'], label: i18n._({ id: 'editor.helpers.buildingHelper.cancel', message: 'Cancel' }) },
19
21
  ]}
20
22
  />
21
23
  )