@pascal-app/editor 1.0.4 → 1.0.6

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 (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +40 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -26,6 +26,7 @@ import { VIEW_2D } from '../../../store/view-layout'
26
26
  import useFloorplanMode from '../../../store/use-floorplan-mode'
27
27
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
28
28
  import { ActionButton } from './action-button'
29
+ import { useActionMenuPopupSide } from './placement'
29
30
 
30
31
  const measurementOptions = [
31
32
  { kind: 'distance', label: 'Distance', icon: Ruler },
@@ -62,6 +63,7 @@ const constructionDimensionOptions = [
62
63
  }[]
63
64
 
64
65
  export function MeasurementControl() {
66
+ const popupSide = useActionMenuPopupSide()
65
67
  const [isOpen, setIsOpen] = useState(false)
66
68
  const mode = useEditor((state) => state.mode)
67
69
  const tool = useEditor((state) => state.tool)
@@ -146,8 +148,8 @@ export function MeasurementControl() {
146
148
  className={cn(
147
149
  'rounded-r-none p-0 text-muted-foreground',
148
150
  isControlActive
149
- ? 'bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
150
- : 'hover:bg-cyan-500/15 hover:text-cyan-400',
151
+ ? 'bg-cyan-500/20 text-cyan-700 hover:bg-cyan-500/20 dark:text-cyan-400 dark:hover:text-accent-foreground'
152
+ : 'hover:bg-cyan-500/15 hover:text-cyan-700 dark:hover:text-cyan-400',
151
153
  )}
152
154
  label={`Measure: ${selectedLabel}`}
153
155
  onClick={handlePrimaryClick}
@@ -166,8 +168,8 @@ export function MeasurementControl() {
166
168
  className={cn(
167
169
  'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
168
170
  isOpen
169
- ? 'bg-cyan-500/15 text-cyan-400'
170
- : 'hover:bg-cyan-500/10 hover:text-cyan-400',
171
+ ? 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400'
172
+ : 'hover:bg-cyan-500/10 hover:text-cyan-700 dark:hover:text-cyan-400',
171
173
  )}
172
174
  type="button"
173
175
  >
@@ -182,7 +184,7 @@ export function MeasurementControl() {
182
184
  <PopoverContent
183
185
  align="center"
184
186
  className="max-h-[70vh] w-64 overflow-y-auto rounded-lg border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
185
- side="top"
187
+ side={popupSide}
186
188
  sideOffset={14}
187
189
  >
188
190
  <div aria-label="Measurement type" className="space-y-1" role="menu">
@@ -198,8 +200,8 @@ export function MeasurementControl() {
198
200
  className={cn(
199
201
  'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
200
202
  isSelected
201
- ? 'bg-white/10 text-foreground'
202
- : 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
203
+ ? 'bg-black/6 dark:bg-white/10 text-foreground'
204
+ : 'text-muted-foreground hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground',
203
205
  )}
204
206
  key={option.kind}
205
207
  onClick={() => {
@@ -234,8 +236,8 @@ export function MeasurementControl() {
234
236
  className={cn(
235
237
  'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
236
238
  isSelected
237
- ? 'bg-white/10 text-foreground'
238
- : 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
239
+ ? 'bg-black/6 dark:bg-white/10 text-foreground'
240
+ : 'text-muted-foreground hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground',
239
241
  )}
240
242
  key={`${option.mode}-${option.chainMode}`}
241
243
  onClick={() => {
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ /** Viewport edge the action menu docks to. */
4
+ export type ActionMenuPlacement = 'bottom' | 'top'
5
+
6
+ /** Where chrome under a top-docked menu starts: below the host toolbar row and the menu itself. */
7
+ export const TOP_MENU_CLEARANCE = '7.25rem'
8
+
9
+ const ActionMenuPlacementContext = createContext<ActionMenuPlacement>('bottom')
10
+
11
+ export const ActionMenuPlacementProvider = ActionMenuPlacementContext.Provider
12
+
13
+ export function useActionMenuPlacement(): ActionMenuPlacement {
14
+ return useContext(ActionMenuPlacementContext)
15
+ }
16
+
17
+ /** Side tooltips and popovers open toward, away from the docked edge. */
18
+ export function useActionMenuPopupSide(): 'top' | 'bottom' {
19
+ return useActionMenuPlacement() === 'top' ? 'bottom' : 'top'
20
+ }
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import { useHasPointAskHandler } from '@pascal-app/core'
4
+ import { canPoint, enterPointMode, leavePointMode } from '../../../lib/point-ask/session'
5
+ import { cn } from '../../../lib/utils'
6
+ import useEditor from '../../../store/use-editor'
7
+ import { usePointAsk } from '../../../store/use-point-ask'
8
+ import { isViewVisible, VIEW_3D } from '../../../store/view-layout'
9
+ import { ActionButton } from './action-button'
10
+
11
+ /**
12
+ * Point and ask (key C): the toolbar's way into the mode where hovering names what Pascal would
13
+ * be told about and a click opens a bubble to ask. Offered only where a host answers asks. In
14
+ * the 2D plan and the walkthrough it is disabled and says so (v1 works in the 3D view).
15
+ */
16
+ export function PointAskButton() {
17
+ const available = useHasPointAskHandler()
18
+ const active = usePointAsk((s) => s.active)
19
+ const sceneVisible = useEditor((s) => isViewVisible(s, VIEW_3D))
20
+ const firstPerson = useEditor((s) => s.isFirstPersonMode)
21
+ if (!(available || active)) return null
22
+ const unavailable = !active && (!sceneVisible || firstPerson)
23
+ return (
24
+ <ActionButton
25
+ className={cn(
26
+ 'group text-muted-foreground',
27
+ active
28
+ ? 'bg-[#a99bff]/20 text-[#5d49df] dark:text-[#c9c1ff] dark:hover:text-accent-foreground'
29
+ : 'hover:bg-[#a99bff]/15 hover:text-[#5d49df] dark:hover:text-[#c9c1ff]',
30
+ )}
31
+ data-guide-target="mode-point-ask"
32
+ disabled={unavailable}
33
+ isActive={active}
34
+ label="Point and ask"
35
+ onClick={() => {
36
+ if (active) leavePointMode()
37
+ else if (canPoint()) enterPointMode(true)
38
+ }}
39
+ shortcut="C"
40
+ size="icon"
41
+ tooltipContent={
42
+ unavailable ? <p>Point and ask works in the 3D view</p> : <p>Point and ask (C)</p>
43
+ }
44
+ variant="ghost"
45
+ >
46
+ <svg aria-hidden className="h-5 w-5" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" viewBox="0 0 24 24">
47
+ <path d="M4.5 3.5l8.2 3.7-3.4 1.3-1.3 3.4z" fill="currentColor" fillOpacity="0.25" />
48
+ <path d="M12.5 12.5h6.5a2 2 0 012 2v3a2 2 0 01-2 2h-2.2l-2.3 2v-2h-2a2 2 0 01-2-2v-3a2 2 0 012-2z" />
49
+ </svg>
50
+ </ActionButton>
51
+ )
52
+ }
@@ -20,6 +20,7 @@ import { useUploadStore } from '../../../store/use-upload'
20
20
  import { SliderControl } from '../controls/slider-control'
21
21
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
22
22
  import { ActionButton } from './action-button'
23
+ import { useActionMenuPopupSide } from './placement'
23
24
 
24
25
  const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
25
26
  const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
@@ -149,7 +150,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
149
150
  <>
150
151
  <button
151
152
  aria-label="Upload scan or guide image"
152
- className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
153
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
153
154
  disabled={isAddingGuide}
154
155
  onClick={() => fileInputRef.current?.click()}
155
156
  type="button"
@@ -170,6 +171,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
170
171
  // ── Guides toggle + dropdown ────────────────────────────────────────────────
171
172
 
172
173
  function GuidesControl() {
174
+ const popupSide = useActionMenuPopupSide()
173
175
  const showGuides = useViewer((state) => state.showGuides)
174
176
  const setShowGuides = useViewer((state) => state.setShowGuides)
175
177
  const setSelection = useViewer((state) => state.setSelection)
@@ -207,8 +209,8 @@ function GuidesControl() {
207
209
  className={cn(
208
210
  'rounded-r-none p-0',
209
211
  showGuides
210
- ? 'bg-white/15'
211
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
212
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
213
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
212
214
  )}
213
215
  label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`}
214
216
  onClick={() => setShowGuides(!showGuides)}
@@ -221,7 +223,7 @@ function GuidesControl() {
221
223
  className="h-[28px] w-[28px] object-contain"
222
224
  src="/icons/floorplan.webp"
223
225
  />
224
- <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
226
+ <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
225
227
  {guides.length}
226
228
  </span>
227
229
  </div>
@@ -236,11 +238,11 @@ function GuidesControl() {
236
238
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
237
239
  showGuides
238
240
  ? isOpen
239
- ? 'bg-white/10'
240
- : 'bg-white/5 hover:bg-white/8'
241
+ ? 'bg-black/6 dark:bg-white/10'
242
+ : 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
241
243
  : isOpen
242
- ? 'bg-white/8'
243
- : 'opacity-60 hover:bg-white/5 hover:opacity-100',
244
+ ? 'bg-black/5 dark:bg-white/8'
245
+ : 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
244
246
  )}
245
247
  type="button"
246
248
  >
@@ -252,7 +254,7 @@ function GuidesControl() {
252
254
  <PopoverContent
253
255
  align="center"
254
256
  className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
255
- side="top"
257
+ side={popupSide}
256
258
  sideOffset={14}
257
259
  >
258
260
  <div className="space-y-3">
@@ -284,7 +286,7 @@ function GuidesControl() {
284
286
  className={cn(
285
287
  'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
286
288
  selectedReferenceId === guide.id
287
- ? 'border-foreground/35 bg-white/10'
289
+ ? 'border-foreground/35 bg-black/6 dark:bg-white/10'
288
290
  : 'border-border/45',
289
291
  )}
290
292
  key={guide.id}
@@ -349,6 +351,7 @@ function GuidesControl() {
349
351
  // ── Scans toggle + dropdown ─────────────────────────────────────────────────
350
352
 
351
353
  function ScansControl() {
354
+ const popupSide = useActionMenuPopupSide()
352
355
  const showScans = useViewer((state) => state.showScans)
353
356
  const setShowScans = useViewer((state) => state.setShowScans)
354
357
  const setSelection = useViewer((state) => state.setSelection)
@@ -386,8 +389,8 @@ function ScansControl() {
386
389
  className={cn(
387
390
  'rounded-r-none p-0',
388
391
  showScans
389
- ? 'bg-white/15'
390
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
392
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
393
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
391
394
  )}
392
395
  label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`}
393
396
  onClick={() => setShowScans(!showScans)}
@@ -396,7 +399,7 @@ function ScansControl() {
396
399
  >
397
400
  <div className="relative">
398
401
  <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
399
- <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
402
+ <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
400
403
  {scans.length}
401
404
  </span>
402
405
  </div>
@@ -411,11 +414,11 @@ function ScansControl() {
411
414
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
412
415
  showScans
413
416
  ? isOpen
414
- ? 'bg-white/10'
415
- : 'bg-white/5 hover:bg-white/8'
417
+ ? 'bg-black/6 dark:bg-white/10'
418
+ : 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
416
419
  : isOpen
417
- ? 'bg-white/8'
418
- : 'opacity-60 hover:bg-white/5 hover:opacity-100',
420
+ ? 'bg-black/5 dark:bg-white/8'
421
+ : 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
419
422
  )}
420
423
  type="button"
421
424
  >
@@ -427,7 +430,7 @@ function ScansControl() {
427
430
  <PopoverContent
428
431
  align="center"
429
432
  className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
430
- side="top"
433
+ side={popupSide}
431
434
  sideOffset={14}
432
435
  >
433
436
  <div className="space-y-3">
@@ -459,7 +462,7 @@ function ScansControl() {
459
462
  className={cn(
460
463
  'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
461
464
  selectedReferenceId === scan.id
462
- ? 'border-foreground/35 bg-white/10'
465
+ ? 'border-foreground/35 bg-black/6 dark:bg-white/10'
463
466
  : 'border-border/45',
464
467
  )}
465
468
  key={scan.id}
@@ -578,7 +581,7 @@ function ReferenceListSection({
578
581
  </div>
579
582
  <button
580
583
  aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
581
- className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
584
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
582
585
  onClick={() => setShow(!show)}
583
586
  type="button"
584
587
  >
@@ -594,7 +597,7 @@ function ReferenceListSection({
594
597
  className={cn(
595
598
  'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
596
599
  selectedReferenceId === node.id
597
- ? 'border-foreground/35 bg-white/10'
600
+ ? 'border-foreground/35 bg-black/6 dark:bg-white/10'
598
601
  : 'border-border/45',
599
602
  )}
600
603
  key={node.id}
@@ -657,6 +660,7 @@ function ReferenceListSection({
657
660
  }
658
661
 
659
662
  function ReferencesControl() {
663
+ const popupSide = useActionMenuPopupSide()
660
664
  const showScans = useViewer((state) => state.showScans)
661
665
  const setShowScans = useViewer((state) => state.setShowScans)
662
666
  const showGuides = useViewer((state) => state.showGuides)
@@ -682,8 +686,8 @@ function ReferencesControl() {
682
686
  className={cn(
683
687
  'rounded-r-none p-0',
684
688
  anyVisible
685
- ? 'bg-white/15'
686
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
689
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
690
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
687
691
  )}
688
692
  label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
689
693
  onClick={toggleAll}
@@ -696,7 +700,7 @@ function ReferencesControl() {
696
700
  className="h-[28px] w-[28px] object-contain"
697
701
  src="/icons/floorplan.webp"
698
702
  />
699
- <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
703
+ <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
700
704
  {total}
701
705
  </span>
702
706
  </div>
@@ -710,11 +714,11 @@ function ReferencesControl() {
710
714
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
711
715
  anyVisible
712
716
  ? isOpen
713
- ? 'bg-white/10'
714
- : 'bg-white/5 hover:bg-white/8'
717
+ ? 'bg-black/6 dark:bg-white/10'
718
+ : 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
715
719
  : isOpen
716
- ? 'bg-white/8'
717
- : 'opacity-60 hover:bg-white/5 hover:opacity-100',
720
+ ? 'bg-black/5 dark:bg-white/8'
721
+ : 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
718
722
  )}
719
723
  type="button"
720
724
  >
@@ -726,7 +730,7 @@ function ReferencesControl() {
726
730
  <PopoverContent
727
731
  align="center"
728
732
  className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
729
- side="top"
733
+ side={popupSide}
730
734
  sideOffset={14}
731
735
  >
732
736
  <div className="space-y-3">
@@ -765,6 +769,7 @@ function ReferencesControl() {
765
769
  // ── Reference floor control ────────────────────────────────────────────────────────────────────
766
770
 
767
771
  function ReferenceFloorControl() {
772
+ const popupSide = useActionMenuPopupSide()
768
773
  const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
769
774
  const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
770
775
  const referenceFloorOffset = useEditor((state) => state.referenceFloorOffset)
@@ -784,8 +789,8 @@ function ReferenceFloorControl() {
784
789
  className={cn(
785
790
  'rounded-r-none p-0',
786
791
  showReferenceFloor && selectedLevel
787
- ? 'bg-white/15'
788
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
792
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
793
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
789
794
  )}
790
795
  disabled={!hasLowerLevels}
791
796
  label={
@@ -801,7 +806,7 @@ function ReferenceFloorControl() {
801
806
  >
802
807
  <div className="relative">
803
808
  <Layers2 className="h-4 w-4" />
804
- <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
809
+ <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
805
810
  {lowerLevels.length}
806
811
  </span>
807
812
  </div>
@@ -815,11 +820,11 @@ function ReferenceFloorControl() {
815
820
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
816
821
  showReferenceFloor && selectedLevel
817
822
  ? isOpen
818
- ? 'bg-white/10'
819
- : 'bg-white/5 hover:bg-white/8'
823
+ ? 'bg-black/6 dark:bg-white/10'
824
+ : 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
820
825
  : isOpen
821
- ? 'bg-white/8'
822
- : 'opacity-60 hover:bg-white/5 hover:opacity-100',
826
+ ? 'bg-black/5 dark:bg-white/8'
827
+ : 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
823
828
  )}
824
829
  disabled={!hasLowerLevels}
825
830
  type="button"
@@ -832,7 +837,7 @@ function ReferenceFloorControl() {
832
837
  <PopoverContent
833
838
  align="center"
834
839
  className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
835
- side="top"
840
+ side={popupSide}
836
841
  sideOffset={14}
837
842
  >
838
843
  <div className="space-y-3">
@@ -848,7 +853,7 @@ function ReferenceFloorControl() {
848
853
  </div>
849
854
  <button
850
855
  aria-label={showReferenceFloor ? 'Hide reference floor' : 'Show reference floor'}
851
- className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
856
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
852
857
  disabled={!hasLowerLevels}
853
858
  onClick={toggleReferenceFloor}
854
859
  type="button"
@@ -870,8 +875,8 @@ function ReferenceFloorControl() {
870
875
  return (
871
876
  <button
872
877
  className={cn(
873
- 'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-white/8',
874
- isSelected && showReferenceFloor && 'bg-white/10 text-foreground',
878
+ 'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-black/5 dark:hover:bg-white/8',
879
+ isSelected && showReferenceFloor && 'bg-black/6 dark:bg-white/10 text-foreground',
875
880
  )}
876
881
  key={level.id}
877
882
  onClick={() => {
@@ -928,8 +933,8 @@ function RiserControl() {
928
933
  <ActionButton
929
934
  className={cn(
930
935
  isRiserOpen
931
- ? 'bg-white/15'
932
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
936
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
937
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
933
938
  )}
934
939
  label="Riser diagram"
935
940
  onClick={toggleRiserOpen}
@@ -128,11 +128,12 @@ type BuildToolTileProps = Omit<ComponentPropsWithoutRef<'button'>, 'children' |
128
128
  iconSrc: string
129
129
  label: string
130
130
  size?: 'lg' | 'sm'
131
+ tone?: 'primary' | 'neutral'
131
132
  }
132
133
 
133
134
  /**
134
135
  * One tool tile: the isometric icon over its name. Inactive icons stay grey and
135
- * take their colour on hover; the armed tool keeps it, on the primary tint.
136
+ * take their colour on hover. Neutral tiles keep that behavior when armed.
136
137
  */
137
138
  export function BuildToolTile({
138
139
  active,
@@ -141,6 +142,7 @@ export function BuildToolTile({
141
142
  iconSrc,
142
143
  label,
143
144
  size = 'sm',
145
+ tone = 'primary',
144
146
  ...buttonProps
145
147
  }: BuildToolTileProps) {
146
148
  return (
@@ -151,7 +153,11 @@ export function BuildToolTile({
151
153
  // Small tiles grow to two label lines ("Spawn point"); a grid row's
152
154
  // tiles stretch together, so a row keeps one height.
153
155
  size === 'lg' ? 'h-[104px] gap-1.5 px-1.5 pt-2 pb-2.5' : 'min-h-[74px] gap-1 px-1 pt-1.5 pb-2',
154
- active ? 'bg-primary/10 ring-1 ring-primary/50' : 'bg-muted/40 hover:bg-muted',
156
+ active
157
+ ? tone === 'neutral'
158
+ ? 'bg-black/8 ring-1 ring-foreground/30 dark:bg-muted'
159
+ : 'bg-primary/10 ring-1 ring-primary/50'
160
+ : 'bg-black/4 hover:bg-black/8 dark:bg-muted/40 dark:hover:bg-muted',
155
161
  className,
156
162
  )}
157
163
  type="button"
@@ -162,7 +168,8 @@ export function BuildToolTile({
162
168
  className={cn(
163
169
  'min-h-0 object-contain transition duration-200 group-hover:scale-110',
164
170
  size === 'lg' ? 'size-14' : 'size-9',
165
- !active && 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0',
171
+ (!active || tone === 'neutral') &&
172
+ 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0',
166
173
  )}
167
174
  draggable={false}
168
175
  src={iconSrc}
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import type { AnyNodeId } from '@pascal-app/core'
4
- import { DEFAULT_LEVEL_HEIGHT, LevelNode, type UnitNode, useScene } from '@pascal-app/core'
3
+ import { type AnyNodeId, type LevelNode, type UnitNode, useScene } from '@pascal-app/core'
5
4
  import { useViewer } from '@pascal-app/viewer'
6
5
  import {
7
6
  AppWindow,
@@ -37,6 +36,7 @@ import {
37
36
  Video,
38
37
  } from 'lucide-react'
39
38
  import { useEffect } from 'react'
39
+ import { addStoreyOnTop } from '../../../lib/add-storey'
40
40
  import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
41
41
  import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
42
42
  import { getSavedSceneDocument } from '../../../lib/scene'
@@ -203,16 +203,7 @@ export function EditorCommands() {
203
203
  const { nodes } = useScene.getState()
204
204
  const building = Object.values(nodes).find((n) => n.type === 'building')
205
205
  if (!building) return
206
- const levelCount = building.children.filter(
207
- (childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
208
- ).length
209
- const newLevel = LevelNode.parse({
210
- level: levelCount,
211
- height: DEFAULT_LEVEL_HEIGHT,
212
- children: [],
213
- parentId: building.id,
214
- })
215
- useScene.getState().createNode(newLevel, building.id)
206
+ const newLevel = addStoreyOnTop(building.id)
216
207
  useViewer.getState().setSelection({ levelId: newLevel.id })
217
208
  }),
218
209
  },
@@ -3,15 +3,15 @@
3
3
  import type { AnyNodeId, LevelNode } from '@pascal-app/core'
4
4
  import { useScene } from '@pascal-app/core'
5
5
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
6
- import { Command, useCommandState } from 'cmdk'
6
+ import { Command, defaultFilter, useCommandState } from 'cmdk'
7
7
  import { ChevronRight, Search } from 'lucide-react'
8
- import type { ReactNode } from 'react'
9
- import { useEffect, useState } from 'react'
8
+ import type { ReactNode, RefObject } from 'react'
9
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react'
10
10
  import { create } from 'zustand'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
13
13
  import { getLevelDisplayName } from '@pascal-app/core'
14
- import { useCommandRegistry } from '../../../store/use-command-registry'
14
+ import { type CommandAction, useCommandRegistry } from '../../../store/use-command-registry'
15
15
  import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -54,6 +54,28 @@ function resolve(value: string | (() => string)): string {
54
54
  return typeof value === 'function' ? value() : value
55
55
  }
56
56
 
57
+ type ActionGroup = [group: string, actions: CommandAction[]]
58
+
59
+ /**
60
+ * The registered actions by group, in the order they were registered. A search puts the group
61
+ * holding the best match first: cmdk ranks the items inside a group but leaves the groups where
62
+ * they are, so a strong match could sit below a screen of weak ones.
63
+ */
64
+ export function rankedGroups(actions: CommandAction[], search: string): ActionGroup[] {
65
+ const groups = new Map<string, CommandAction[]>()
66
+ for (const action of actions) {
67
+ groups.set(action.group, [...(groups.get(action.group) ?? []), action])
68
+ }
69
+ const ordered: ActionGroup[] = [...groups]
70
+ if (!search) return ordered
71
+ const bestMatch = ([, list]: ActionGroup) =>
72
+ Math.max(...list.map((action) => defaultFilter(resolve(action.label), search, action.keywords)))
73
+ return ordered
74
+ .map((entry) => ({ entry, score: bestMatch(entry) }))
75
+ .sort((a, b) => b.score - a.score)
76
+ .map(({ entry }) => entry)
77
+ }
78
+
57
79
  function Shortcut({ keys }: { keys: string[] }) {
58
80
  return (
59
81
  <span className="ml-auto flex shrink-0 items-center gap-0.5">
@@ -162,6 +184,20 @@ export interface CommandPaletteEmptyAction {
162
184
  onSelect: (query: string) => void
163
185
  }
164
186
 
187
+ /**
188
+ * A search selects the first item; this scrolls the list back to the top to show it. cmdk scrolls
189
+ * before the list re-renders the selection, so once a search has moved the groups it scrolls to
190
+ * wherever the item selected before went. Rendered after the list, whose ref is set by then.
191
+ */
192
+ function TopOnFirstItem({ list }: { list: RefObject<HTMLDivElement | null> }) {
193
+ const value = useCommandState((s) => s.value)
194
+ useLayoutEffect(() => {
195
+ const first = list.current?.querySelector('[cmdk-item]:not([aria-disabled="true"])')
196
+ if (value && first?.getAttribute('aria-selected') === 'true') list.current?.scrollTo({ top: 0 })
197
+ }, [list, value])
198
+ return null
199
+ }
200
+
165
201
  function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
166
202
  const count = useCommandState((s) => s.filtered.count)
167
203
  const search = useCommandState((s) => s.search)
@@ -193,6 +229,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
193
229
 
194
230
  const [meta, setMeta] = useState('⌘')
195
231
  const [isFullscreen, setIsFullscreen] = useState(false)
232
+ const listRef = useRef<HTMLDivElement>(null)
196
233
 
197
234
  const page = pages[pages.length - 1]
198
235
 
@@ -255,15 +292,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
255
292
  })
256
293
  }
257
294
 
258
- // ---------------------------------------------------------------------------
259
- // Group registered actions by group (preserving insertion order)
260
- // ---------------------------------------------------------------------------
261
- const grouped = actions.reduce<Map<string, typeof actions>>((acc, action) => {
262
- const list = acc.get(action.group) ?? []
263
- list.push(action)
264
- acc.set(action.group, list)
265
- return acc
266
- }, new Map())
295
+ const grouped = rankedGroups(actions, inputValue)
267
296
 
268
297
  const onClose = () => setOpen(false)
269
298
  const onBack = () => {
@@ -326,7 +355,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
326
355
  />
327
356
  </div>
328
357
 
329
- <Command.List className="max-h-100 overflow-y-auto p-1.5">
358
+ <Command.List className="max-h-100 overflow-y-auto p-1.5" ref={listRef}>
330
359
  {(!emptyAction || page) && (
331
360
  <Command.Empty className="py-8 text-center text-muted-foreground text-sm">
332
361
  No commands found.
@@ -344,7 +373,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
344
373
 
345
374
  {/* ── Root view: render from registry ───────────────────────── */}
346
375
  {!page &&
347
- Array.from(grouped.entries()).map(([group, groupActions]) => (
376
+ grouped.map(([group, groupActions]) => (
348
377
  <Command.Group heading={group} key={group}>
349
378
  {groupActions.map((action) => (
350
379
  <Item
@@ -439,6 +468,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
439
468
  </Command.Group>
440
469
  )}
441
470
  </Command.List>
471
+ <TopOnFirstItem list={listRef} />
442
472
 
443
473
  {/* Footer hint */}
444
474
  <div className="flex items-center justify-between border-border/50 border-t px-3 py-2">