@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,790 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ area,
7
+ getLevelDisplayName,
8
+ runAsSingleSceneHistoryStep,
9
+ useScene,
10
+ type ZoneNode,
11
+ } from '@pascal-app/core'
12
+ import { setRoomFloorScript } from '@pascal-app/core/agent-operations'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { Check, ChevronDown, Plus } from 'lucide-react'
15
+ import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react'
16
+ import { startCeilingEdit } from '../../../lib/ceiling-edit-session'
17
+ import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
18
+ import { finishScriptCode } from '../../../lib/geometry-script/finish-scripts'
19
+ import { formatAreaLabel, formatLinearMeasurement } from '../../../lib/measurements'
20
+ import { addMezzanineStairs } from '../../../lib/mezzanine-stairs'
21
+ import { roomFloorBase, roomRelativeFloorHeight } from '../../../lib/room-construction-commands'
22
+ import { mezzanineElevationBounds } from '../../../lib/room-handle-drag'
23
+ import type { RoomSelectionRecord } from '../../../lib/room-selection'
24
+ import {
25
+ baseRoomName,
26
+ numberedRoomName,
27
+ type RoomNameSection,
28
+ roomNameSections,
29
+ roomNameUseCount,
30
+ } from '../../../lib/room-name-catalog'
31
+ import { renameRoom, requestRoomDeletion } from '../../../lib/room-structure-commands'
32
+ import { addZoneToNewUnit, assignZoneToUnit, zoneUnits } from '../../../lib/units'
33
+ import { cn } from '../../../lib/utils'
34
+ import useEditor from '../../../store/use-editor'
35
+ import useInteractionScope from '../../../store/use-interaction-scope'
36
+ import {
37
+ DropdownMenu,
38
+ DropdownMenuContent,
39
+ DropdownMenuItem,
40
+ DropdownMenuSeparator,
41
+ DropdownMenuTrigger,
42
+ } from '../primitives/dropdown-menu'
43
+ import { ColorDot } from '../primitives/color-dot'
44
+ import { Popover, PopoverAnchor, PopoverContent } from '../primitives/popover'
45
+ import { ShortcutToken } from '../primitives/shortcut-token'
46
+ import { PanelWrapper } from './panel-wrapper'
47
+ import { RoomOpeningsField } from './room-openings-field'
48
+ import {
49
+ ConstructionPart,
50
+ FloorHeightField,
51
+ PartField,
52
+ pill,
53
+ RoomConstructionRows,
54
+ roomInsideHeight,
55
+ } from './room-construction-rows'
56
+ import { type RoomNameOption, roomNameOptions } from './room-name-options'
57
+
58
+ /** "Room · Ground floor", "Mezzanine · in Kitchen", "Room · not enclosed". */
59
+ export function roomKicker(
60
+ nodes: Record<string, AnyNode>,
61
+ zoneId: string,
62
+ options: { mezzanine?: boolean; open?: boolean } = {},
63
+ ): string {
64
+ const zone = nodes[zoneId]
65
+ if (options.mezzanine && zone?.type === 'zone') {
66
+ const host = zone.hostZoneId ? nodes[zone.hostZoneId] : undefined
67
+ const hostName = host?.type === 'zone' ? host.name?.trim() : ''
68
+ return hostName ? `Mezzanine · in ${hostName}` : 'Mezzanine'
69
+ }
70
+ if (options.open) return 'Room · not enclosed'
71
+ const level = zone?.parentId ? nodes[zone.parentId] : undefined
72
+ return level?.type === 'level' ? `Room · ${getLevelDisplayName(level)}` : 'Room'
73
+ }
74
+
75
+ export function RoomPanel({
76
+ room,
77
+ onClose = () => useEditor.getState().clearRoom(),
78
+ }: {
79
+ room: RoomSelectionRecord
80
+ onClose?: () => void
81
+ }) {
82
+ const nodes = useScene((state) => state.nodes)
83
+ const sections = useMemo(() => roomNameSections(nodes, room.zoneId), [nodes, room.zoneId])
84
+ const name = useScene((state) => state.nodes[room.zoneId as AnyNodeId]?.name ?? '')
85
+ const visible = useInteractionScope(
86
+ (state) => resolveOverlayPolicy(state.scope).conflictingControls === 'shown',
87
+ )
88
+ if (!visible) return null
89
+ const mezzanine = !!room.mezzanine
90
+ return (
91
+ <PanelWrapper
92
+ kicker={roomKicker(nodes, room.zoneId, { mezzanine })}
93
+ onClose={onClose}
94
+ title={name.trim() || (mezzanine ? 'Mezzanine' : 'Room')}
95
+ titleContent={
96
+ <div className="flex min-w-0 flex-1 items-center gap-2">
97
+ <RoomColorDot zoneId={room.zoneId} />
98
+ <RoomName
99
+ key={room.zoneId}
100
+ mezzanine={mezzanine}
101
+ name={name}
102
+ sections={sections}
103
+ zoneId={room.zoneId}
104
+ />
105
+ </div>
106
+ }
107
+ >
108
+ <div className="flex flex-col px-4 pt-3 pb-4 text-sm" data-room-panel={room.key.zoneId}>
109
+ {!mezzanine && <RoomSubline room={room} />}
110
+ <RoomStats room={room} />
111
+ <RoomUnitRow zoneId={room.zoneId} />
112
+ {mezzanine ? (
113
+ <MezzanineRows key={room.zoneId} room={room} />
114
+ ) : (
115
+ <RoomConstructionRows key={room.zoneId} room={room} />
116
+ )}
117
+ <RoomFloorScriptRow key={`floor-script-${room.zoneId}`} zoneId={room.zoneId} />
118
+ </div>
119
+ </PanelWrapper>
120
+ )
121
+ }
122
+
123
+ /**
124
+ * A room the walls do not close (yet): the same title and kicker, and what
125
+ * needs no closed outline — its name, its unit, deleting it.
126
+ */
127
+ export function OpenRoomPanel({ zoneId, onClose }: { zoneId: string; onClose: () => void }) {
128
+ const nodes = useScene((state) => state.nodes)
129
+ const sections = useMemo(() => roomNameSections(nodes, zoneId), [nodes, zoneId])
130
+ const name = useScene((state) => state.nodes[zoneId as AnyNodeId]?.name ?? '')
131
+ const unit = useViewer((s) => s.unit)
132
+ const zone = nodes[zoneId as AnyNodeId]
133
+ if (zone?.type !== 'zone') return null
134
+ return (
135
+ <PanelWrapper
136
+ kicker={roomKicker(nodes, zoneId, { open: true })}
137
+ onClose={onClose}
138
+ title={name.trim() || 'Room'}
139
+ titleContent={
140
+ <div className="flex min-w-0 flex-1 items-center gap-2">
141
+ <RoomColorDot zoneId={zoneId} />
142
+ <RoomName
143
+ key={zoneId}
144
+ mezzanine={false}
145
+ name={name}
146
+ sections={sections}
147
+ zoneId={zoneId}
148
+ />
149
+ </div>
150
+ }
151
+ >
152
+ <div className="flex flex-col px-4 pt-3 pb-4 text-sm" data-open-room-panel={zoneId}>
153
+ <p className="text-muted-foreground text-xs">
154
+ {formatAreaLabel(area([{ outer: zone.polygon, holes: zone.holes }]), unit, 2)} · Close
155
+ its walls to build its floor and ceiling.
156
+ </p>
157
+ <RoomUnitRow zoneId={zoneId} />
158
+ <button
159
+ className="mt-3 w-full rounded-full border border-border/50 py-2 font-medium text-[13px] text-foreground transition-colors hover:bg-accent/50"
160
+ data-delete-open-room
161
+ onClick={() => requestRoomDeletion(zoneId)}
162
+ type="button"
163
+ >
164
+ Delete room
165
+ </button>
166
+ </div>
167
+ </PanelWrapper>
168
+ )
169
+ }
170
+
171
+ /** "Unit: Flat A ›" — which unit the room belongs to; change it, add it to one, or take it out. */
172
+ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
173
+ const nodes = useScene((scene) => scene.nodes)
174
+ const { buildingId, units, current } = useMemo(() => zoneUnits(nodes, zoneId), [nodes, zoneId])
175
+ if (!buildingId) return null
176
+ return (
177
+ <div className="mt-3 flex min-h-8 items-center justify-between gap-3" data-room-unit={zoneId}>
178
+ <span className="text-muted-foreground text-xs">Unit</span>
179
+ <DropdownMenu>
180
+ <DropdownMenuTrigger asChild>
181
+ <button
182
+ aria-label="Unit"
183
+ className="flex min-w-0 items-center gap-1.5 rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
184
+ type="button"
185
+ >
186
+ {current && (
187
+ <span
188
+ aria-hidden
189
+ className="size-2 shrink-0 rounded-full"
190
+ style={{ backgroundColor: current.color }}
191
+ />
192
+ )}
193
+ <span className="truncate">
194
+ {current ? current.name || 'Unit' : units.length ? 'None' : 'Add to a unit'}
195
+ </span>
196
+ <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
197
+ </button>
198
+ </DropdownMenuTrigger>
199
+ <DropdownMenuContent align="end" className="min-w-40">
200
+ {units.map((unit) => (
201
+ <DropdownMenuItem
202
+ key={unit.id}
203
+ onSelect={() => assignZoneToUnit(zoneId as ZoneNode['id'], unit.id)}
204
+ >
205
+ <span
206
+ aria-hidden
207
+ className="size-2 shrink-0 rounded-full"
208
+ style={{ backgroundColor: unit.color }}
209
+ />
210
+ <span className="flex-1 truncate">{unit.name || 'Unit'}</span>
211
+ {unit.id === current?.id && <Check className="size-3.5" />}
212
+ </DropdownMenuItem>
213
+ ))}
214
+ {units.length > 0 && <DropdownMenuSeparator />}
215
+ <DropdownMenuItem onSelect={() => addZoneToNewUnit(zoneId as ZoneNode['id'])}>
216
+ New unit
217
+ </DropdownMenuItem>
218
+ {current && (
219
+ <DropdownMenuItem onSelect={() => assignZoneToUnit(zoneId as ZoneNode['id'], null)}>
220
+ Remove from {current.name || 'unit'}
221
+ </DropdownMenuItem>
222
+ )}
223
+ </DropdownMenuContent>
224
+ </DropdownMenu>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ /**
230
+ * "Scripted finish": a floor a script builds (it owns the floor's colours, so Paint leaves it),
231
+ * with its code to read and a way to take it off, back to the floor's own finish.
232
+ */
233
+ function RoomFloorScriptRow({ zoneId }: { zoneId: string }) {
234
+ const sha = useScene((scene) => {
235
+ const zone = scene.nodes[zoneId as AnyNodeId]
236
+ return zone?.type === 'zone' ? zone.floor?.script?.script : undefined
237
+ })
238
+ const [code, setCode] = useState<{ sha: string; text: string } | null>(null)
239
+ const [open, setOpen] = useState(false)
240
+ if (!sha) return null
241
+ const toggleCode = () => {
242
+ setOpen((shown) => !shown)
243
+ if (code?.sha === sha) return
244
+ finishScriptCode(sha).then(
245
+ (text) => setCode({ sha, text: text ?? 'The script could not be read.' }),
246
+ () => setCode({ sha, text: 'The script could not be read.' }),
247
+ )
248
+ }
249
+ const remove = () => {
250
+ const { changes } = setRoomFloorScript(
251
+ useScene.getState().nodes,
252
+ { zoneId, remove: true },
253
+ { activeLevelId: null },
254
+ )
255
+ runAsSingleSceneHistoryStep(useScene, () => {
256
+ for (const { id, data } of changes?.update ?? [])
257
+ useScene.getState().updateNode(id as AnyNodeId, data)
258
+ })
259
+ }
260
+ return (
261
+ <div className="mt-3 flex flex-col gap-2" data-room-floor-script={zoneId}>
262
+ <div className="flex min-h-8 items-center justify-between gap-3">
263
+ <span className="text-muted-foreground text-xs">Scripted finish</span>
264
+ <div className="flex items-center gap-1.5">
265
+ <button
266
+ className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
267
+ onClick={toggleCode}
268
+ type="button"
269
+ >
270
+ {open ? 'Hide code' : 'View code'}
271
+ </button>
272
+ <button
273
+ className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
274
+ onClick={remove}
275
+ type="button"
276
+ >
277
+ Remove
278
+ </button>
279
+ </div>
280
+ </div>
281
+ {open && (
282
+ <pre className="max-h-60 overflow-auto whitespace-pre rounded-lg border border-border/50 p-2 font-mono text-[11px] text-foreground">
283
+ {code?.sha === sha ? code.text : 'Loading…'}
284
+ </pre>
285
+ )}
286
+ </div>
287
+ )
288
+ }
289
+
290
+ const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
291
+
292
+ /** "2 doors · 3 windows" (the level is in the kicker). */
293
+ function RoomSubline({ room }: { room: RoomSelectionRecord }) {
294
+ const text = useScene((scene) => {
295
+ const walls = new Set(room.boundaryWallIds)
296
+ let doors = 0
297
+ let windows = 0
298
+ for (const node of Object.values(scene.nodes)) {
299
+ if (node.type !== 'door' && node.type !== 'window') continue
300
+ const wallId = node.parentId ?? ('wallId' in node ? (node.wallId as string) : '')
301
+ if (!walls.has(wallId)) continue
302
+ if (node.type === 'door') doors++
303
+ else windows++
304
+ }
305
+ return `${count(doors, 'door')} · ${count(windows, 'window')}`
306
+ })
307
+ return (
308
+ <p className="text-muted-foreground text-xs" data-room-subline>
309
+ {text}
310
+ </p>
311
+ )
312
+ }
313
+
314
+ function ringPerimeter(ring: readonly (readonly [number, number])[]) {
315
+ let total = 0
316
+ for (let i = 0; i < ring.length; i++) {
317
+ const a = ring[i]!
318
+ const b = ring[(i + 1) % ring.length]!
319
+ total += Math.hypot(b[0] - a[0], b[1] - a[1])
320
+ }
321
+ return total
322
+ }
323
+
324
+ /** Area, perimeter and inside height, in the user's units. */
325
+ function RoomStats({ room }: { room: RoomSelectionRecord }) {
326
+ const unit = useViewer((s) => s.unit)
327
+ const metricNotation = useViewer((s) => s.metricNotation)
328
+ const nodes = useScene((scene) => scene.nodes)
329
+ const zone = nodes[room.zoneId as AnyNodeId]
330
+ const perimeter =
331
+ zone?.type === 'zone'
332
+ ? [zone.polygon, ...(zone.holes ?? [])].reduce((sum, ring) => sum + ringPerimeter(ring), 0)
333
+ : 0
334
+ const { height } = roomInsideHeight(room, nodes)
335
+ const stats = [
336
+ { label: 'Area', value: formatAreaLabel(room.area, unit, 2) },
337
+ { label: 'Perimeter', value: formatLinearMeasurement(perimeter, unit, metricNotation) },
338
+ {
339
+ label: 'Height',
340
+ value: height === null ? '—' : formatLinearMeasurement(height, unit, metricNotation),
341
+ },
342
+ ]
343
+ return (
344
+ <div
345
+ className="mt-3 grid grid-cols-3 divide-x divide-border/50 rounded-lg border border-border/50"
346
+ data-room-stats
347
+ >
348
+ {stats.map((stat) => (
349
+ <div className="flex min-w-0 flex-col gap-0.5 px-2.5 py-2" key={stat.label}>
350
+ <span className="text-[11px] text-muted-foreground">{stat.label}</span>
351
+ <span className="truncate font-medium font-mono text-[13px] text-foreground tabular-nums">
352
+ {stat.value}
353
+ </span>
354
+ </div>
355
+ ))}
356
+ </div>
357
+ )
358
+ }
359
+
360
+ /**
361
+ * A mezzanine is built the same way, adapted: its own floor at its height
362
+ * (editable within what core accepts), stairs up to it, and the ceiling over
363
+ * it when there is headroom. No walls of its own, so no walls row.
364
+ */
365
+ function MezzanineRows({ room }: { room: RoomSelectionRecord }) {
366
+ const zoneId = room.zoneId
367
+ const nodes = useScene((scene) => scene.nodes)
368
+ const unit = useViewer((s) => s.unit)
369
+ const metricNotation = useViewer((s) => s.metricNotation)
370
+ const [expanded, setExpanded] = useState<'floor' | 'ceiling' | null>(null)
371
+ const [stairMessage, setStairMessage] = useState<string | null>(null)
372
+ const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
373
+ const elevation = roomRelativeFloorHeight(nodes, zoneId)
374
+ const base = roomFloorBase(nodes, zoneId)
375
+ const absoluteBounds = mezzanineElevationBounds(nodes, zoneId)
376
+ const bounds = absoluteBounds && {
377
+ min: absoluteBounds.min - base,
378
+ max: absoluteBounds.max - base,
379
+ }
380
+ const hasCeiling = Object.values(nodes).some(
381
+ (node) => node.type === 'ceiling' && node.zoneId === zoneId,
382
+ )
383
+ const { ceiling } = roomInsideHeight(room, nodes)
384
+ const toggle = (part: 'floor' | 'ceiling') => () =>
385
+ setExpanded((current) => (current === part ? null : part))
386
+ return (
387
+ <div className="flex flex-col" data-mezzanine-rows={zoneId}>
388
+ <h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">Built with</h3>
389
+ <div className="flex flex-col gap-1">
390
+ <ConstructionPart
391
+ expanded={expanded === 'floor'}
392
+ icon="/icons/floor.webp"
393
+ label="Floor"
394
+ meta={`${length(elevation)} up`}
395
+ on
396
+ onToggleExpanded={toggle('floor')}
397
+ part="floor"
398
+ >
399
+ <FloorHeightField bounds={bounds} zoneId={zoneId} />
400
+ <RoomOpeningsField surface="floor" zoneId={zoneId} />
401
+ </ConstructionPart>
402
+ <ConstructionPart
403
+ control={
404
+ <button
405
+ className={pill}
406
+ data-add-mezzanine-stairs
407
+ onClick={() => {
408
+ const result = addMezzanineStairs(zoneId)
409
+ setStairMessage(result.ok ? null : result.message)
410
+ }}
411
+ type="button"
412
+ >
413
+ Add stairs
414
+ </button>
415
+ }
416
+ expanded={false}
417
+ icon="/icons/stairs.webp"
418
+ label="Stairs"
419
+ meta="Up from the floor below"
420
+ on
421
+ onToggleExpanded={() => {}}
422
+ part="stairs"
423
+ />
424
+ <ConstructionPart
425
+ expanded={expanded === 'ceiling'}
426
+ icon="/icons/ceiling.webp"
427
+ label="Ceiling"
428
+ meta={
429
+ hasCeiling
430
+ ? ceiling === null
431
+ ? 'Over the mezzanine'
432
+ : `${length(ceiling)} headroom`
433
+ : 'Not enough headroom'
434
+ }
435
+ on
436
+ onToggleExpanded={toggle('ceiling')}
437
+ part="ceiling"
438
+ >
439
+ {hasCeiling ? (
440
+ <PartField label="Shape">
441
+ <button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
442
+ Edit shape
443
+ </button>
444
+ </PartField>
445
+ ) : undefined}
446
+ </ConstructionPart>
447
+ </div>
448
+ {stairMessage && (
449
+ <span className="mt-2 text-destructive text-xs" role="status">
450
+ {stairMessage}
451
+ </span>
452
+ )}
453
+ </div>
454
+ )
455
+ }
456
+
457
+ /**
458
+ * The room's colour, as the scene graph shows and edits it: the plan fill and
459
+ * the label tint, never a finish (finishes are painted).
460
+ */
461
+ function RoomColorDot({ zoneId }: { zoneId: string }) {
462
+ const color = useScene((state) => {
463
+ const zone = state.nodes[zoneId as AnyNodeId]
464
+ return zone?.type === 'zone' ? zone.color : undefined
465
+ })
466
+ return (
467
+ <ColorDot
468
+ color={color ?? '#3b82f6'}
469
+ label="Room colour — used in the plan and labels"
470
+ onChange={(next) => useScene.getState().updateNode(zoneId as AnyNodeId, { color: next })}
471
+ side="bottom"
472
+ />
473
+ )
474
+ }
475
+
476
+ /**
477
+ * The name, edited in place: it reads as the panel's heading until focused.
478
+ * Focusing opens every suggestion — the current name is not a filter until
479
+ * the user types — with the caret at the end so the name can be fine-tuned.
480
+ * Picking a name (click or Enter) writes it as one rename — numbered when
481
+ * another room on the level has it ("Bedroom 2") — and lets go of the field;
482
+ * Escape reverts.
483
+ */
484
+ function RoomName({
485
+ zoneId,
486
+ name,
487
+ sections,
488
+ mezzanine,
489
+ }: {
490
+ zoneId: string
491
+ name: string
492
+ sections: readonly RoomNameSection[]
493
+ mezzanine: boolean
494
+ }) {
495
+ const nodes = useScene((state) => state.nodes)
496
+ const [value, setValue] = useState(name)
497
+ const [open, setOpen] = useState(false)
498
+ const [typing, setTyping] = useState(false)
499
+ const [active, setActive] = useState(-1)
500
+ const valueRef = useRef(name)
501
+ // Enter/click commit and then blur, and blur commits too; this keeps it one rename.
502
+ const committedRef = useRef(name)
503
+ const originalRef = useRef(name)
504
+ const inputRef = useRef<HTMLInputElement>(null)
505
+ const listId = useId()
506
+ const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing)
507
+ const expanded = open && options.length > 0
508
+ const activeId = expanded && options[active] ? `${listId}-${active}` : undefined
509
+ const current = baseRoomName(value).toLowerCase()
510
+
511
+ // A rename from elsewhere (undo, a collaborator, the plan) lands in the field
512
+ // unless the user is editing it (the field stays mounted across renames).
513
+ useEffect(() => {
514
+ if (document.activeElement === inputRef.current) return
515
+ valueRef.current = name
516
+ committedRef.current = name
517
+ setValue(name)
518
+ }, [name])
519
+
520
+ // Opening and the arrow keys bring the active row into view; the pointer
521
+ // only highlights (scrolling under it would move the row being clicked).
522
+ const scrollToActive = useRef(false)
523
+ useEffect(() => {
524
+ if (!(activeId && scrollToActive.current)) return
525
+ scrollToActive.current = false
526
+ // The list mounts in the same commit that opens it: wait a frame so the
527
+ // current name, lower in the list, is scrolled to on open.
528
+ const frame = requestAnimationFrame(() =>
529
+ document.getElementById(activeId)?.scrollIntoView({ block: 'nearest' }),
530
+ )
531
+ return () => cancelAnimationFrame(frame)
532
+ }, [activeId])
533
+
534
+ const caretToEnd = () => {
535
+ const input = inputRef.current
536
+ if (!input) return
537
+ const end = input.value.length
538
+ input.setSelectionRange(end, end)
539
+ }
540
+ const showAll = () => {
541
+ setTyping(false)
542
+ setOpen(true)
543
+ const all = roomNameOptions(valueRef.current, sections, false).options
544
+ const here = baseRoomName(valueRef.current).toLowerCase()
545
+ scrollToActive.current = true
546
+ setActive(Math.max(0, all.findIndex((option) => option.value.toLowerCase() === here)))
547
+ }
548
+ const type = (next: string) => {
549
+ valueRef.current = next
550
+ setValue(next)
551
+ }
552
+ const commit = () => {
553
+ const next = valueRef.current
554
+ if (next === committedRef.current) return
555
+ committedRef.current = next
556
+ renameRoom(zoneId, next)
557
+ }
558
+ const pick = (option: RoomNameOption) => {
559
+ const next =
560
+ option.kind === 'typed' ? option.value : numberedRoomName(nodes, zoneId, option.value)
561
+ type(next)
562
+ commit()
563
+ setOpen(false)
564
+ setTyping(false)
565
+ // A pick is the answer: the field lets go. To fine-tune the name, click it
566
+ // again — the caret lands at the end.
567
+ inputRef.current?.blur()
568
+ }
569
+ const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
570
+ const count = options.length
571
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
572
+ event.preventDefault()
573
+ event.stopPropagation()
574
+ if (!expanded) {
575
+ showAll()
576
+ return
577
+ }
578
+ if (!count) return
579
+ const down = event.key === 'ArrowDown'
580
+ scrollToActive.current = true
581
+ setActive((index) => (down ? (index + 1) % count : index <= 0 ? count - 1 : index - 1))
582
+ } else if (event.key === 'Enter') {
583
+ event.preventDefault()
584
+ event.stopPropagation()
585
+ const chosen = expanded ? options[active] : undefined
586
+ if (chosen) pick(chosen)
587
+ else inputRef.current?.blur()
588
+ } else if (event.key === 'Escape') {
589
+ event.preventDefault()
590
+ event.stopPropagation()
591
+ if (expanded) {
592
+ type(originalRef.current)
593
+ setOpen(false)
594
+ setTyping(false)
595
+ requestAnimationFrame(() => inputRef.current?.select())
596
+ } else inputRef.current?.blur()
597
+ } else if (event.key === 'Tab') setOpen(false)
598
+ }
599
+
600
+ const renderOption = (option: RoomNameOption, index: number) => {
601
+ const isCurrent = option.kind === 'suggestion' && option.value.toLowerCase() === current
602
+ const used = option.kind === 'suggestion' ? roomNameUseCount(nodes, zoneId, option.value) : 0
603
+ return (
604
+ <div
605
+ aria-selected={index === active}
606
+ className={cn(
607
+ 'flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] text-foreground',
608
+ index === active && 'bg-accent',
609
+ option.kind === 'typed' && 'mb-0.5 rounded-b-none border-border border-b',
610
+ )}
611
+ data-room-name-option={option.value}
612
+ id={`${listId}-${index}`}
613
+ key={`${option.kind}:${index}:${option.value}`}
614
+ role="option"
615
+ tabIndex={-1}
616
+ onClick={() => pick(option)}
617
+ onMouseDown={(event) => event.preventDefault()}
618
+ onMouseMove={() => setActive(index)}
619
+ >
620
+ {option.kind === 'typed' && <Plus aria-hidden className="size-3.5 shrink-0" />}
621
+ <RoomNameOptionLabel option={option} />
622
+ {isCurrent ? (
623
+ <Check aria-hidden className="size-3.5 shrink-0" />
624
+ ) : used > 0 ? (
625
+ <span
626
+ className="shrink-0 whitespace-nowrap font-mono text-[11px] text-muted-foreground"
627
+ title="Picking it adds a number"
628
+ >
629
+ {used} on this level
630
+ </span>
631
+ ) : null}
632
+ </div>
633
+ )
634
+ }
635
+
636
+ return (
637
+ <Popover open={expanded} onOpenChange={setOpen}>
638
+ <PopoverAnchor asChild>
639
+ {/* A stable hook for host-app onboarding to point at; nothing here reads it. */}
640
+ <div
641
+ className={cn(
642
+ '-mx-1.5 flex min-w-0 flex-1 cursor-text items-center gap-1 rounded-md border border-transparent py-0.5 pr-0.5 pl-1.5 transition-colors hover:bg-accent/50',
643
+ (expanded || open) && 'border-ring/60 bg-accent/40',
644
+ 'focus-within:border-ring/60 focus-within:bg-accent/40',
645
+ )}
646
+ data-guide-target="room-name"
647
+ data-room-name-combobox
648
+ title={name.trim() ? `${name.trim()} · Rename` : 'Rename'}
649
+ >
650
+ <input
651
+ aria-activedescendant={activeId}
652
+ aria-autocomplete="list"
653
+ aria-controls={expanded ? listId : undefined}
654
+ aria-expanded={expanded}
655
+ aria-label={mezzanine ? 'Mezzanine name' : 'Room name'}
656
+ autoComplete="off"
657
+ className="min-w-0 flex-1 text-ellipsis bg-transparent p-0 font-semibold text-foreground text-sm leading-snug tracking-tight outline-none placeholder:text-foreground"
658
+ data-room-name
659
+ maxLength={40}
660
+ placeholder={mezzanine ? 'Mezzanine' : 'Room'}
661
+ ref={inputRef}
662
+ role="combobox"
663
+ spellCheck={false}
664
+ type="text"
665
+ value={value}
666
+ onBlur={() => {
667
+ setOpen(false)
668
+ setTyping(false)
669
+ commit()
670
+ }}
671
+ onChange={(event) => {
672
+ type(event.target.value)
673
+ setTyping(true)
674
+ setOpen(true)
675
+ const next = roomNameOptions(event.target.value, sections, true)
676
+ scrollToActive.current = true
677
+ setActive(next.typed && next.options.length > 1 ? 1 : 0)
678
+ }}
679
+ onFocus={() => {
680
+ originalRef.current = valueRef.current
681
+ showAll()
682
+ }}
683
+ onKeyDown={onKeyDown}
684
+ onMouseUp={(event) => {
685
+ // The click that focused the field lands the caret at the end;
686
+ // a click in an already-focused field reopens the whole list.
687
+ if (event.detail === 1 && !open) showAll()
688
+ if (!typing) caretToEnd()
689
+ }}
690
+ />
691
+ <button
692
+ aria-label={mezzanine ? 'Show mezzanine names' : 'Show room names'}
693
+ className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
694
+ data-room-name-open
695
+ tabIndex={-1}
696
+ type="button"
697
+ onMouseDown={(event) => {
698
+ event.preventDefault()
699
+ const input = inputRef.current
700
+ if (!input) return
701
+ if (expanded) setOpen(false)
702
+ else if (document.activeElement === input) showAll()
703
+ else input.focus()
704
+ }}
705
+ >
706
+ <ChevronDown
707
+ aria-hidden
708
+ className={cn('size-3 transition-transform', expanded && 'rotate-180')}
709
+ />
710
+ </button>
711
+ </div>
712
+ </PopoverAnchor>
713
+ <PopoverContent
714
+ align="start"
715
+ aria-label={mezzanine ? 'Mezzanine names' : 'Room names'}
716
+ className="flex max-h-85 w-(--radix-popover-trigger-width) min-w-60 flex-col overflow-hidden p-0"
717
+ id={listId}
718
+ role="listbox"
719
+ onCloseAutoFocus={(event) => event.preventDefault()}
720
+ // Escape belongs to the field: it reverts the name (the popover's own
721
+ // dismiss would close the list first and turn it into a commit).
722
+ onEscapeKeyDown={(event) => event.preventDefault()}
723
+ onInteractOutside={(event) => {
724
+ if (event.target instanceof Node && inputRef.current?.parentElement?.contains(event.target)) {
725
+ event.preventDefault()
726
+ }
727
+ }}
728
+ onOpenAutoFocus={(event) => event.preventDefault()}
729
+ // React events bubble out of the portal into the panel header, whose
730
+ // pointerdown starts a panel drag and captures the pointer — the
731
+ // release would then land on the header instead of the picked row.
732
+ onPointerDown={(event) => event.stopPropagation()}
733
+ >
734
+ <div className="no-scrollbar min-h-0 flex-1 overflow-y-auto px-1.5 pt-1.5 pb-1">
735
+ {typedRow && renderOption(typedRow, 0)}
736
+ {groups.map((group) => {
737
+ const first = options.indexOf(group.options[0]!)
738
+ return (
739
+ <div aria-labelledby={`${listId}-${group.id}`} key={group.id} role="group">
740
+ <div
741
+ className="px-2.5 pt-2.5 pb-1 text-[11px] text-muted-foreground"
742
+ id={`${listId}-${group.id}`}
743
+ role="presentation"
744
+ >
745
+ {group.title}
746
+ </div>
747
+ {group.options.map((option, offset) => renderOption(option, first + offset))}
748
+ </div>
749
+ )
750
+ })}
751
+ </div>
752
+ <div className="flex shrink-0 flex-wrap gap-x-3 gap-y-1 border-border border-t px-3 py-2 text-[11px] text-muted-foreground">
753
+ {(
754
+ [
755
+ [['↑', '↓'], 'Browse'],
756
+ [['Enter'], 'Pick'],
757
+ [['Esc'], 'Revert'],
758
+ ] as const
759
+ ).map(([keys, action]) => (
760
+ <span className="flex items-center gap-1" key={action}>
761
+ {keys.map((key) => (
762
+ <ShortcutToken className="h-4 min-w-4 justify-center px-1 text-[9.5px]" key={key} value={key} />
763
+ ))}
764
+ {action}
765
+ </span>
766
+ ))}
767
+ </div>
768
+ </PopoverContent>
769
+ </Popover>
770
+ )
771
+ }
772
+
773
+ function RoomNameOptionLabel({ option }: { option: RoomNameOption }) {
774
+ if (option.kind === 'typed') {
775
+ return (
776
+ <span className="min-w-0 flex-1 truncate">
777
+ <span className="text-muted-foreground">Use </span>“{option.value}”
778
+ </span>
779
+ )
780
+ }
781
+ const { value, match } = option
782
+ if (!match) return <span className="min-w-0 flex-1 truncate">{value}</span>
783
+ return (
784
+ <span className="min-w-0 flex-1 truncate">
785
+ {value.slice(0, match.start)}
786
+ <span className="font-semibold underline decoration-muted-foreground underline-offset-3">{value.slice(match.start, match.end)}</span>
787
+ {value.slice(match.end)}
788
+ </span>
789
+ )
790
+ }