@pascal-app/editor 1.0.5 → 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 +20 -17
  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,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useScene } from '@pascal-app/core'
3
+ import { formatNumber, parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
5
  import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
6
6
  import { useLinearDisplay } from '../../../lib/use-linear-display'
@@ -20,6 +20,45 @@ interface MetricControlProps {
20
20
  restoreOnCommit?: boolean
21
21
  }
22
22
 
23
+ export function parseMetricInput(
24
+ text: string,
25
+ unit: string,
26
+ options: { isImperial: boolean; locale: string; parseUnit?: string },
27
+ ): number | null {
28
+ const spec = lingoUnitSpec(unit)
29
+ return spec
30
+ ? parseMeasurement(text, spec, {
31
+ bareUnit: options.parseUnit ?? spec.unitId,
32
+ locale: options.locale,
33
+ system: options.isImperial ? 'us' : 'metric',
34
+ })
35
+ : parseLocalizedNumber(text, options.locale)
36
+ }
37
+
38
+ export function formatMetricInputValue(
39
+ value: number,
40
+ precision: number,
41
+ locale: string,
42
+ ): string {
43
+ return formatNumber(roundMetricValueForDisplay(value, precision), {
44
+ locale,
45
+ min: precision,
46
+ max: precision,
47
+ })
48
+ }
49
+
50
+ export function roundMetricValueForDisplay(value: number, precision: number): number {
51
+ const rounded = Number(
52
+ formatNumber(value, {
53
+ locale: 'en',
54
+ min: precision,
55
+ max: precision,
56
+ useGrouping: false,
57
+ }),
58
+ )
59
+ return Number.isFinite(rounded) ? rounded : value
60
+ }
61
+
23
62
  export function MetricControl({
24
63
  label,
25
64
  value,
@@ -33,6 +72,7 @@ export function MetricControl({
33
72
  unit = '',
34
73
  restoreOnCommit = true,
35
74
  }: MetricControlProps) {
75
+ const locale = useLocale()
36
76
  const {
37
77
  isImperial,
38
78
  displayUnit,
@@ -51,16 +91,20 @@ export function MetricControl({
51
91
  )
52
92
  const roundStoredValueForDisplayPrecision = useCallback(
53
93
  (storedValue: number) =>
54
- clamp(toStoredValue(Number.parseFloat(toDisplayValue(storedValue).toFixed(precision)))),
94
+ clamp(toStoredValue(roundMetricValueForDisplay(toDisplayValue(storedValue), precision))),
55
95
  [clamp, precision, toDisplayValue, toStoredValue],
56
96
  )
57
97
 
58
98
  const displayValue = toDisplayValue(value)
99
+ const formatDisplayValue = useCallback(
100
+ (nextValue: number) => formatMetricInputValue(nextValue, precision, locale),
101
+ [locale, precision],
102
+ )
59
103
 
60
104
  const [isEditing, setIsEditing] = useState(false)
61
105
  const [isDragging, setIsDragging] = useState(false)
62
106
  const [isHovered, setIsHovered] = useState(false)
63
- const [inputValue, setInputValue] = useState(displayValue.toFixed(precision))
107
+ const [inputValue, setInputValue] = useState(() => formatDisplayValue(displayValue))
64
108
  const startXRef = useRef(0)
65
109
  const startValueRef = useRef(0)
66
110
  const containerRef = useRef<HTMLDivElement>(null)
@@ -81,9 +125,9 @@ export function MetricControl({
81
125
 
82
126
  useEffect(() => {
83
127
  if (!isEditing) {
84
- setInputValue(displayValue.toFixed(precision))
128
+ setInputValue(formatDisplayValue(displayValue))
85
129
  }
86
- }, [displayValue, precision, isEditing])
130
+ }, [displayValue, formatDisplayValue, isEditing])
87
131
 
88
132
  useEffect(() => {
89
133
  const container = containerRef.current
@@ -221,8 +265,8 @@ export function MetricControl({
221
265
 
222
266
  const handleValueClick = useCallback(() => {
223
267
  setIsEditing(true)
224
- setInputValue(toDisplayValue(value).toFixed(precision))
225
- }, [value, toDisplayValue, precision])
268
+ setInputValue(formatDisplayValue(toDisplayValue(value)))
269
+ }, [formatDisplayValue, value, toDisplayValue])
226
270
 
227
271
  const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
228
272
  setInputValue(e.target.value)
@@ -230,18 +274,10 @@ export function MetricControl({
230
274
 
231
275
  const submitValue = useCallback(() => {
232
276
  const spec = lingoUnitSpec(unit)
233
- let stored = spec
234
- ? parseMeasurement(inputValue, spec, {
235
- bareUnit: parseUnit ?? spec.unitId,
236
- system: isImperial ? 'us' : 'metric',
237
- })
238
- : null
239
- if (stored === null) {
240
- const numValue = Number.parseFloat(inputValue)
241
- stored = Number.isFinite(numValue) ? toStoredValue(numValue) : null
242
- }
277
+ const parsed = parseMetricInput(inputValue, unit, { isImperial, locale, parseUnit })
278
+ const stored = parsed === null ? null : spec ? parsed : toStoredValue(parsed)
243
279
  if (stored === null) {
244
- setInputValue(toDisplayValue(value).toFixed(precision))
280
+ setInputValue(formatDisplayValue(toDisplayValue(value)))
245
281
  } else {
246
282
  applyCommittedValue(clamp(stored))
247
283
  }
@@ -249,13 +285,14 @@ export function MetricControl({
249
285
  }, [
250
286
  inputValue,
251
287
  unit,
288
+ locale,
252
289
  isImperial,
253
290
  parseUnit,
254
291
  applyCommittedValue,
255
292
  clamp,
256
293
  toStoredValue,
257
294
  value,
258
- precision,
295
+ formatDisplayValue,
259
296
  toDisplayValue,
260
297
  ])
261
298
 
@@ -265,6 +302,7 @@ export function MetricControl({
265
302
  ? measurementHint(inputValue, spec, {
266
303
  bareUnit: parseUnit ?? spec.unitId,
267
304
  system: isImperial ? 'us' : 'metric',
305
+ locale,
268
306
  displayUnit: parseUnit ?? spec.unitId,
269
307
  precision,
270
308
  clamp,
@@ -280,25 +318,25 @@ export function MetricControl({
280
318
  if (e.key === 'Enter') {
281
319
  submitValue()
282
320
  } else if (e.key === 'Escape') {
283
- setInputValue(toDisplayValue(value).toFixed(precision))
321
+ setInputValue(formatDisplayValue(toDisplayValue(value)))
284
322
  setIsEditing(false)
285
323
  } else if (e.key === 'ArrowUp') {
286
324
  e.preventDefault()
287
325
  const newV = clamp(value + toStoredValue(step))
288
326
  applyCommittedValue(newV)
289
- setInputValue(toDisplayValue(newV).toFixed(precision))
327
+ setInputValue(formatDisplayValue(toDisplayValue(newV)))
290
328
  } else if (e.key === 'ArrowDown') {
291
329
  e.preventDefault()
292
330
  const newV = clamp(value - toStoredValue(step))
293
331
  applyCommittedValue(newV)
294
- setInputValue(toDisplayValue(newV).toFixed(precision))
332
+ setInputValue(formatDisplayValue(toDisplayValue(newV)))
295
333
  }
296
334
  },
297
335
  [
298
336
  submitValue,
299
337
  value,
300
338
  toDisplayValue,
301
- precision,
339
+ formatDisplayValue,
302
340
  step,
303
341
  clamp,
304
342
  applyCommittedValue,
@@ -363,7 +401,7 @@ export function MetricControl({
363
401
  onClick={handleValueClick}
364
402
  >
365
403
  <span className="font-mono tabular-nums tracking-tight">
366
- {Number(displayValue.toFixed(precision)).toFixed(precision)}
404
+ {formatDisplayValue(displayValue)}
367
405
  </span>
368
406
  {displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>}
369
407
  </div>
@@ -39,7 +39,10 @@ export function SegmentedControl<T extends string>({
39
39
  aria-disabled={option.disabled || undefined}
40
40
  aria-pressed={isSelected}
41
41
  className={cn(
42
- 'relative flex min-h-7 min-w-0 flex-1 items-center justify-center rounded-md px-1.5 py-1 font-medium text-xs leading-4 transition-colors duration-200',
42
+ // Every chip stretches to the row's height (the row is `items-stretch`), so a label that wraps
43
+ // to two lines grows the whole row evenly instead of one tall chip. The tighter line-height
44
+ // keeps a wrapped label reading as one unit and leaves single-line rows at min-h-7.
45
+ 'relative flex min-h-7 min-w-0 flex-1 items-center justify-center rounded-md px-1.5 py-1 font-medium text-xs leading-3.5 transition-colors duration-200',
43
46
  isSelected
44
47
  ? 'bg-white dark:bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
45
48
  : 'text-muted-foreground hover:bg-black/4 dark:hover:bg-white/5 hover:text-foreground',
@@ -54,7 +57,7 @@ export function SegmentedControl<T extends string>({
54
57
  }}
55
58
  type="button"
56
59
  >
57
- <span className="relative z-10 flex min-w-0 items-center justify-center gap-1.5 whitespace-normal text-center [&>svg]:shrink-0">
60
+ <span className="relative z-10 flex min-w-0 items-center justify-center gap-1.5 whitespace-normal text-balance text-center [overflow-wrap:anywhere] [&>svg]:shrink-0">
58
61
  {option.label}
59
62
  </span>
60
63
  </button>
@@ -0,0 +1,23 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { createI18n, getLocale, PascalI18nProvider, setLocale } from '@pascal-app/core'
3
+ import { renderToString } from 'react-dom/server'
4
+ import { parseSliderBareValue, SliderControl } from './slider-control'
5
+
6
+ test('slider numbers use the scoped locale for display and parsing while the global locale is English', () => {
7
+ setLocale('en')
8
+ const i18n = createI18n({ locale: 'fr' })
9
+ try {
10
+ const html = renderToString(
11
+ <PascalI18nProvider i18n={i18n}>
12
+ <SliderControl label="Length" onChange={() => {}} precision={2} value={2.75} />
13
+ </PascalI18nProvider>,
14
+ )
15
+
16
+ expect(getLocale()).toBe('en')
17
+ expect(html).toContain('2,75')
18
+ expect(parseSliderBareValue('2,75', i18n.getLocale())).toBe(2.75)
19
+ expect(parseSliderBareValue('1e2', i18n.getLocale())).toBe(100)
20
+ } finally {
21
+ i18n.destroy()
22
+ }
23
+ })
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useScene } from '@pascal-app/core'
3
+ import { formatNumber, parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
5
  import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
6
6
  import { useLinearDisplay } from '../../../lib/use-linear-display'
@@ -23,6 +23,10 @@ interface SliderControlProps {
23
23
  mixed?: boolean
24
24
  }
25
25
 
26
+ export function parseSliderBareValue(text: string, locale: string): number | null {
27
+ return parseLocalizedNumber(text, locale)
28
+ }
29
+
26
30
  function stepPrecision(s: number): number {
27
31
  if (s <= 0) return 0
28
32
  return Math.max(0, Math.ceil(-Math.log10(s)))
@@ -67,14 +71,24 @@ export function SliderControl({
67
71
  restoreOnCommit = true,
68
72
  mixed = false,
69
73
  }: SliderControlProps) {
74
+ const locale = useLocale()
70
75
  // Values and bounds stay in meters; gestures and input use the displayed unit.
71
76
  const { isImperial, displayUnit, parseUnit, precision, step, toDisplay, toStored } =
72
77
  useLinearDisplay(unit, storedPrecision, storedStep)
78
+ const formatDisplay = useCallback(
79
+ (displayValue: number) =>
80
+ formatNumber(displayValue, {
81
+ locale,
82
+ min: precision,
83
+ max: precision,
84
+ }),
85
+ [locale, precision],
86
+ )
73
87
 
74
88
  const [isEditing, setIsEditing] = useState(false)
75
89
  const [isDragging, setIsDragging] = useState(false)
76
90
  const [isHovered, setIsHovered] = useState(false)
77
- const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
91
+ const [inputValue, setInputValue] = useState(() => formatDisplay(toDisplay(value)))
78
92
  // Live readout while dragging. Multi-edit previews write live overrides
79
93
  // instead of the scene, so `value` would otherwise stay frozen even though
80
94
  // the meshes are moving.
@@ -116,9 +130,9 @@ export function SliderControl({
116
130
 
117
131
  useEffect(() => {
118
132
  if (!isEditing) {
119
- setInputValue(toDisplay(value).toFixed(precision))
133
+ setInputValue(formatDisplay(toDisplay(value)))
120
134
  }
121
- }, [value, precision, isEditing, toDisplay])
135
+ }, [value, isEditing, toDisplay, formatDisplay])
122
136
 
123
137
  // Wheel support on the label
124
138
  useEffect(() => {
@@ -258,8 +272,8 @@ export function SliderControl({
258
272
  const handleValueClick = useCallback(() => {
259
273
  editingRef.current = true
260
274
  setIsEditing(true)
261
- setInputValue(toDisplay(value).toFixed(precision))
262
- }, [value, precision, toDisplay])
275
+ setInputValue(formatDisplay(toDisplay(value)))
276
+ }, [value, toDisplay, formatDisplay])
263
277
 
264
278
  const parseInputValue = useCallback(
265
279
  (text: string) => {
@@ -267,16 +281,17 @@ export function SliderControl({
267
281
  const parsed = spec
268
282
  ? parseMeasurement(text, spec, {
269
283
  bareUnit: parseUnit ?? spec.unitId,
284
+ locale,
270
285
  system: isImperial ? 'us' : 'metric',
271
286
  })
272
287
  : null
273
- const bare = Number(text)
274
- const stored = parsed ?? (text.trim() && Number.isFinite(bare) ? toStored(bare) : null)
288
+ const bare = parseSliderBareValue(text, locale)
289
+ const stored = parsed ?? (bare === null ? null : toStored(bare))
275
290
  return stored === null
276
291
  ? null
277
292
  : clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
278
293
  },
279
- [unit, parseUnit, isImperial, toStored, clamp, toDisplay, precision],
294
+ [unit, parseUnit, locale, isImperial, toStored, clamp, toDisplay, precision],
280
295
  )
281
296
 
282
297
  const submitValue = useCallback(() => {
@@ -285,13 +300,13 @@ export function SliderControl({
285
300
  const nextValue = parseInputValue(inputValue)
286
301
  if (nextValue === null) {
287
302
  onCancel?.()
288
- setInputValue(toDisplay(value).toFixed(precision))
303
+ setInputValue(formatDisplay(toDisplay(value)))
289
304
  } else {
290
305
  onChange(nextValue)
291
306
  onCommit?.(nextValue)
292
307
  }
293
308
  setIsEditing(false)
294
- }, [inputValue, parseInputValue, onCancel, onChange, onCommit, toDisplay, value, precision])
309
+ }, [inputValue, parseInputValue, onCancel, onChange, onCommit, toDisplay, value, formatDisplay])
295
310
 
296
311
  const spec = lingoUnitSpec(unit)
297
312
  const hint =
@@ -299,6 +314,7 @@ export function SliderControl({
299
314
  ? measurementHint(inputValue, spec, {
300
315
  bareUnit: parseUnit ?? spec.unitId,
301
316
  system: isImperial ? 'us' : 'metric',
317
+ locale,
302
318
  displayUnit: parseUnit ?? spec.unitId,
303
319
  precision,
304
320
  clamp,
@@ -317,10 +333,10 @@ export function SliderControl({
317
333
  const adjustedStep = getAdjustedStep(step, e)
318
334
  const newV = applyDisplayDelta(value, direction * adjustedStep, adjustedStep)
319
335
  onChange(newV)
320
- setInputValue(toDisplay(newV).toFixed(precision))
336
+ setInputValue(formatDisplay(toDisplay(newV)))
321
337
  }
322
338
  },
323
- [submitValue, cancelEdit, value, precision, step, applyDisplayDelta, onChange, toDisplay],
339
+ [submitValue, cancelEdit, value, step, applyDisplayDelta, onChange, toDisplay, formatDisplay],
324
340
  )
325
341
 
326
342
  const displayValue = toDisplay(shown)
@@ -414,7 +430,7 @@ export function SliderControl({
414
430
  onClick={handleValueClick}
415
431
  >
416
432
  <span className="font-mono tabular-nums tracking-tight" suppressHydrationWarning>
417
- {Number(displayValue.toFixed(precision)).toFixed(precision)}
433
+ {formatDisplay(displayValue)}
418
434
  </span>
419
435
  {displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>}
420
436
  </div>