@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
@@ -1,4 +1,6 @@
1
+ import { downloadProjectFile } from '../../../../../lib/download-project-file'
1
2
  import {
3
+ getProjectFileSink,
2
4
  clearSceneHistory,
3
5
  DEFAULT_LEVEL_HEIGHT,
4
6
  emitter,
@@ -26,9 +28,12 @@ import {
26
28
  Layers,
27
29
  ListTree,
28
30
  Map as MapIcon,
31
+ Monitor,
32
+ Moon,
29
33
  Save,
30
34
  ChevronDown,
31
35
  Send,
36
+ Sun,
32
37
  Trash2,
33
38
  Upload,
34
39
  } from 'lucide-react'
@@ -55,6 +60,7 @@ import {
55
60
  sendGlbToLocalApp,
56
61
  waitForLocalImport,
57
62
  } from '../../../../../lib/send-to-app'
63
+ import { SegmentedControl } from './../../../../../components/ui/controls/segmented-control'
58
64
  import { Button } from './../../../../../components/ui/primitives/button'
59
65
  import {
60
66
  Dialog,
@@ -66,12 +72,15 @@ import { Input } from './../../../../../components/ui/primitives/input'
66
72
  import { Switch } from './../../../../../components/ui/primitives/switch'
67
73
  import { cn } from './../../../../../lib/utils'
68
74
  import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
75
+ import { addStoreyBelow, addStoreyOnTop } from './../../../../../lib/add-storey'
69
76
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
70
77
  import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
71
78
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
72
79
  import useFloorplanMode from './../../../../../store/use-floorplan-mode'
73
80
  import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
74
81
  import { AudioSettingsDialog } from './audio-settings-dialog'
82
+ import { AnimationSpeedSetting } from './animation-speed-setting'
83
+ import { ConstructionAnimationSetting } from './construction-animation-setting'
75
84
  import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
76
85
  import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
77
86
  import { PrintExportButton } from './print-export-button'
@@ -275,7 +284,8 @@ function SettingsSubheading({ children }: { children: ReactNode }) {
275
284
  return <h4 className="font-medium text-muted-foreground text-xs">{children}</h4>
276
285
  }
277
286
 
278
- function SettingsSwitchRow({
287
+ /** One on/off preference: a label, a line on what it does, a switch. Hosts use it for their rows. */
288
+ export function SettingsSwitchRow({
279
289
  checked,
280
290
  description,
281
291
  label,
@@ -300,6 +310,65 @@ function SettingsSwitchRow({
300
310
  )
301
311
  }
302
312
 
313
+ export type InterfaceTheme = 'system' | 'light' | 'dark'
314
+
315
+ const INTERFACE_THEME_OPTIONS: { label: ReactNode; value: InterfaceTheme }[] = [
316
+ {
317
+ label: (
318
+ <>
319
+ <Monitor className="size-3.5" />
320
+ System
321
+ </>
322
+ ),
323
+ value: 'system',
324
+ },
325
+ {
326
+ label: (
327
+ <>
328
+ <Sun className="size-3.5" />
329
+ Light
330
+ </>
331
+ ),
332
+ value: 'light',
333
+ },
334
+ {
335
+ label: (
336
+ <>
337
+ <Moon className="size-3.5" />
338
+ Dark
339
+ </>
340
+ ),
341
+ value: 'dark',
342
+ },
343
+ ]
344
+
345
+ /**
346
+ * The interface theme as a System / Light / Dark row. The editor applies no theme itself: its
347
+ * chrome follows the host's `.dark` class, so a host that owns the setting passes this row.
348
+ */
349
+ export function InterfaceThemeRow({
350
+ onChange,
351
+ value,
352
+ }: {
353
+ onChange: (value: InterfaceTheme) => void
354
+ value: InterfaceTheme
355
+ }) {
356
+ const labelId = useId()
357
+ return (
358
+ <div className="flex flex-col gap-2 py-1 @sm:flex-row @sm:items-center @sm:justify-between @sm:gap-4">
359
+ <div className="min-w-0 space-y-0.5">
360
+ <p className="font-medium text-sm" id={labelId}>
361
+ Interface theme
362
+ </p>
363
+ <p className="text-muted-foreground text-xs">System follows your device’s appearance.</p>
364
+ </div>
365
+ <div aria-labelledby={labelId} className="w-full shrink-0 @sm:w-60" role="group">
366
+ <SegmentedControl onChange={onChange} options={INTERFACE_THEME_OPTIONS} value={value} />
367
+ </div>
368
+ </div>
369
+ )
370
+ }
371
+
303
372
  export interface SettingsPanelProps {
304
373
  /** Merged onto the scrolling root so hosts can restyle padding (e.g. inside a dialog). */
305
374
  className?: string
@@ -311,6 +380,11 @@ export interface SettingsPanelProps {
311
380
  field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
312
381
  value: boolean,
313
382
  ) => Promise<void>
383
+ /**
384
+ * Rows a host adds under Preferences, for its own features and settings (SettingsSwitchRow,
385
+ * InterfaceThemeRow).
386
+ */
387
+ preferences?: ReactNode
314
388
  }
315
389
 
316
390
  function FloorSettingsSection() {
@@ -346,20 +420,14 @@ function FloorSettingsSection() {
346
420
 
347
421
  if (!building) return null
348
422
 
349
- const addLevel = (level: number) => {
350
- const newLevel = LevelNode.parse({
351
- level,
352
- height: DEFAULT_LEVEL_HEIGHT,
353
- children: [],
354
- parentId: building.id,
355
- })
356
- createNode(newLevel, building.id)
423
+ const addFloor = () => {
424
+ const newLevel = addStoreyOnTop(building.id)
425
+ setSelection({ buildingId: building.id, levelId: newLevel.id })
426
+ }
427
+ const addBasement = () => {
428
+ const newLevel = addStoreyBelow(building.id)
357
429
  setSelection({ buildingId: building.id, levelId: newLevel.id })
358
430
  }
359
-
360
- const addFloor = () =>
361
- addLevel(levels.length === 0 ? 0 : Math.max(0, ...levels.map((level) => level.level)) + 1)
362
- const addBasement = () => addLevel(Math.min(0, ...levels.map((level) => level.level)) - 1)
363
431
 
364
432
  return (
365
433
  <div className="space-y-3">
@@ -435,6 +503,7 @@ export function SettingsPanel({
435
503
  projectName,
436
504
  projectVisibility,
437
505
  onVisibilityChange,
506
+ preferences,
438
507
  }: SettingsPanelProps = {}) {
439
508
  const fileInputRef = useRef<HTMLInputElement>(null)
440
509
  const copyResetTimeoutRef = useRef<number | null>(null)
@@ -556,21 +625,20 @@ export function SettingsPanel({
556
625
  }
557
626
  const json = JSON.stringify(sceneData, null, 2)
558
627
  const blob = new Blob([json], { type: 'application/json' })
559
- const url = URL.createObjectURL(blob)
560
- const link = document.createElement('a')
561
- link.href = url
562
628
  const date = new Date().toISOString().split('T')[0]
563
- link.download = `layout_${date}.json`
564
- link.click()
565
- URL.revokeObjectURL(url)
629
+ setModelExportError(null)
630
+ void downloadProjectFile(blob, `layout_${date}.json`).catch((error: unknown) => {
631
+ setModelExportError(error instanceof Error ? error.message : 'Could not save the file')
632
+ })
566
633
  }
567
634
 
568
635
  const handleFileLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
569
636
  const file = e.target.files?.[0]
570
637
  if (!file) return
571
638
 
639
+ const fileSink = getProjectFileSink()
572
640
  const reader = new FileReader()
573
- reader.onload = (event) => {
641
+ reader.onload = async (event) => {
574
642
  const text = event.target?.result as string
575
643
  let parsed: unknown
576
644
  try {
@@ -597,10 +665,20 @@ export function SettingsPanel({
597
665
  })
598
666
  return
599
667
  }
668
+ const result = validateBuildJson(parsed)
669
+ if (result.ok) {
670
+ try {
671
+ await fileSink?.({ blob: file, name: file.name })
672
+ } catch (error) {
673
+ setModelExportError(error instanceof Error ? error.message : 'Could not save the source file')
674
+ return
675
+ }
676
+ if (fileSink !== getProjectFileSink()) return
677
+ }
600
678
  setPendingImport({
601
679
  fileName: file.name,
602
680
  fileSizeBytes: file.size,
603
- result: validateBuildJson(parsed),
681
+ result,
604
682
  })
605
683
  }
606
684
  reader.readAsText(file)
@@ -1184,7 +1262,13 @@ export function SettingsPanel({
1184
1262
  />
1185
1263
  </SettingsSection>
1186
1264
 
1187
- <SettingsSection description="Sound levels and keyboard shortcuts." title="Preferences">
1265
+ <SettingsSection
1266
+ description="Sound levels, the construction animation and keyboard shortcuts."
1267
+ title="Preferences"
1268
+ >
1269
+ <ConstructionAnimationSetting />
1270
+ <AnimationSpeedSetting />
1271
+ {preferences}
1188
1272
  <div className="grid gap-2 @sm:grid-cols-2">
1189
1273
  <AudioSettingsDialog />
1190
1274
  <KeyboardShortcutsDialog />
@@ -1204,7 +1288,7 @@ export function SettingsPanel({
1204
1288
  </Button>
1205
1289
  </div>
1206
1290
  </DialogTrigger>
1207
- <DialogContent className="h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
1291
+ <DialogContent className="dark h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
1208
1292
  <DialogTitle className="sr-only">Scene Graph</DialogTitle>
1209
1293
  <div
1210
1294
  className="flex h-full min-h-0 w-full min-w-0 *:h-full *:w-full *:overflow-y-auto"
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { InterfaceThemeRow, SettingsPanel } from '.'
4
+
5
+ const pressedLabels = (html: string) =>
6
+ [...html.matchAll(/aria-pressed="true"[^>]*>(?:<[^>]+>)*([^<]+)/g)].map((match) => match[1])
7
+
8
+ describe('Interface theme setting', () => {
9
+ test("the row shows the host's value pressed", () => {
10
+ const html = renderToStaticMarkup(<InterfaceThemeRow onChange={() => {}} value="light" />)
11
+ expect(html).toContain('Interface theme')
12
+ expect(pressedLabels(html)).toEqual(['Light'])
13
+ })
14
+
15
+ test('Settings shows the row a host passes, and none without it', () => {
16
+ const row = <InterfaceThemeRow onChange={() => {}} value="system" />
17
+ expect(renderToStaticMarkup(<SettingsPanel preferences={row} />)).toContain('Interface theme')
18
+ expect(renderToStaticMarkup(<SettingsPanel />)).not.toContain('Interface theme')
19
+ })
20
+ })
@@ -1,3 +1,5 @@
1
+ import { getProjectFileSink } from '@pascal-app/core'
2
+ import { downloadProjectFile } from '../../../../../lib/download-project-file'
1
3
  import { AlertTriangle, Printer } from 'lucide-react'
2
4
  import { useState } from 'react'
3
5
  import { Button } from '../../../../../components/ui/primitives/button'
@@ -23,15 +25,6 @@ type PreparedPrintExport = {
23
25
 
24
26
  type PrintModelExportFormat = Extract<ModelExportFormat, 'print-3mf' | 'print-stl'>
25
27
 
26
- function downloadArtifact(artifact: ModelExportArtifact) {
27
- const url = URL.createObjectURL(artifact.blob)
28
- const link = document.createElement('a')
29
- link.href = url
30
- link.download = artifact.filename
31
- link.click()
32
- URL.revokeObjectURL(url)
33
- }
34
-
35
28
  function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport) {
36
29
  const bundleDiagnostic = report.diagnostics.find((item) => item.severity === 'error')
37
30
  if (bundleDiagnostic || !isPrintLevelBundleReport(report)) return bundleDiagnostic?.message
@@ -81,11 +74,12 @@ export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) {
81
74
  const handleExport = async (format: PrintModelExportFormat) => {
82
75
  if (!modelExport) return
83
76
 
77
+ const fileSink = getProjectFileSink()
84
78
  setExportingFormat(format)
85
79
  setError(null)
86
80
  try {
87
81
  const prepared = await preparePrintExport(modelExport, onlyVisible, format)
88
- downloadArtifact(prepared.artifact)
82
+ await downloadProjectFile(prepared.artifact.blob, prepared.artifact.filename, fileSink)
89
83
  } catch (reason) {
90
84
  setError(reason instanceof Error ? reason.message : '3D print export failed.')
91
85
  } finally {
@@ -1,8 +1,9 @@
1
- import { type BuildingNode, DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
1
+ import { type BuildingNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { Building2, Plus } from 'lucide-react'
4
4
  import { memo, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
+ import { addStoreyOnTop } from './../../../../../lib/add-storey'
6
7
  import {
7
8
  Tooltip,
8
9
  TooltipContent,
@@ -23,7 +24,6 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
23
24
  isLast,
24
25
  }: BuildingTreeNodeProps) {
25
26
  const [expanded, setExpanded] = useState(true)
26
- const createNode = useScene((state) => state.createNode)
27
27
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
28
28
  const name = useScene((s) => s.nodes[nodeId]?.name)
29
29
  // Levels and elevators first, units after, so the units read as one group.
@@ -46,15 +46,7 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
46
46
 
47
47
  const handleAddLevel = (e: React.MouseEvent) => {
48
48
  e.stopPropagation()
49
- const nodes = useScene.getState().nodes
50
- const levelCount = children.filter((childId) => nodes[childId]?.type === 'level').length
51
- const newLevel = LevelNode.parse({
52
- level: levelCount,
53
- height: DEFAULT_LEVEL_HEIGHT,
54
- children: [],
55
- parentId: nodeId,
56
- })
57
- createNode(newLevel, nodeId)
49
+ addStoreyOnTop(nodeId)
58
50
  }
59
51
 
60
52
  return (
@@ -2,10 +2,9 @@ import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
4
  type BuildingNode,
5
- DEFAULT_LEVEL_HEIGHT,
6
5
  emitter,
7
6
  type GuideNode,
8
- LevelNode,
7
+ type LevelNode,
9
8
  type ScanNode,
10
9
  type SiteNode,
11
10
  useScene,
@@ -55,6 +54,7 @@ import {
55
54
  type LevelDuplicatePreset,
56
55
  } from '@pascal-app/core/building'
57
56
  import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
57
+ import { addStoreyOnTop } from './../../../../../lib/add-storey'
58
58
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
59
59
  import {
60
60
  formatAreaLabel,
@@ -200,7 +200,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
200
200
  className={cn(
201
201
  'flex h-6 w-6 cursor-pointer items-center justify-center rounded transition-colors',
202
202
  isEditing
203
- ? 'bg-orange-500/20 text-orange-400'
203
+ ? 'bg-orange-500/20 text-orange-700 dark:text-orange-400'
204
204
  : 'text-muted-foreground hover:bg-accent',
205
205
  )}
206
206
  onClick={handleToggleEdit}
@@ -256,7 +256,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
256
256
  className={cn(
257
257
  'flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded',
258
258
  points.length > 3
259
- ? 'text-muted-foreground hover:bg-red-500/20 hover:text-red-400'
259
+ ? 'text-muted-foreground hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400'
260
260
  : 'cursor-not-allowed text-muted-foreground/30',
261
261
  )}
262
262
  disabled={points.length <= 3}
@@ -1014,7 +1014,6 @@ const LevelsSection = memo(function LevelsSection({
1014
1014
  onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
1015
1015
  onDeleteAsset?: (projectId: string, url: string) => void
1016
1016
  } = {}) {
1017
- const createNode = useScene((state) => state.createNode)
1018
1017
  const updateNode = useScene((state) => state.updateNode)
1019
1018
  const selectedBuildingId = useViewer((state) => state.selection.buildingId)
1020
1019
  const selectedLevelId = useViewer((state) => state.selection.levelId)
@@ -1037,13 +1036,7 @@ const LevelsSection = memo(function LevelsSection({
1037
1036
  if (!building) return null
1038
1037
 
1039
1038
  const handleAddLevel = () => {
1040
- const newLevel = LevelNode.parse({
1041
- level: levels.length,
1042
- height: DEFAULT_LEVEL_HEIGHT,
1043
- children: [],
1044
- parentId: building.id,
1045
- })
1046
- createNode(newLevel, building.id)
1039
+ const newLevel = addStoreyOnTop(building.id)
1047
1040
  setSelection({ buildingId: building.id, levelId: newLevel.id })
1048
1041
  }
1049
1042
 
@@ -283,14 +283,15 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
283
283
  />
284
284
  )}
285
285
 
286
- <button
287
- className="z-10 flex h-4 w-4 shrink-0 items-center justify-center bg-inherit"
288
- onClick={(e) => {
289
- e.stopPropagation()
290
- onToggle()
291
- }}
292
- >
293
- {hasChildren ? (
286
+ {/* A row with nothing to fold keeps the space, and a click there selects the row. */}
287
+ {hasChildren ? (
288
+ <button
289
+ className="z-10 flex h-4 w-4 shrink-0 items-center justify-center bg-inherit"
290
+ onClick={(e) => {
291
+ e.stopPropagation()
292
+ onToggle()
293
+ }}
294
+ >
294
295
  <motion.div
295
296
  animate={{ rotate: expanded ? 90 : 0 }}
296
297
  initial={false}
@@ -298,8 +299,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
298
299
  >
299
300
  <ChevronRight className="h-3 w-3" />
300
301
  </motion.div>
301
- ) : null}
302
- </button>
302
+ </button>
303
+ ) : (
304
+ <span className="h-4 w-4 shrink-0" />
305
+ )}
303
306
  <div className="flex min-w-0 flex-1 items-center gap-2">
304
307
  <span
305
308
  className={cn(
@@ -195,14 +195,14 @@ export function ZonePanel() {
195
195
  />
196
196
  <ActionButton
197
197
  className="w-full flex-none"
198
- icon={<Trash2 className="h-4 w-4 text-red-400" />}
198
+ icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
199
199
  label="Delete"
200
200
  onClick={() => deleteSelectedZone(false)}
201
201
  type="button"
202
202
  />
203
203
  <ActionButton
204
204
  className="w-full flex-none"
205
- icon={<Trash2 className="h-4 w-4 text-red-400" />}
205
+ icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
206
206
  label="Delete with contents"
207
207
  onClick={() => deleteSelectedZone(true)}
208
208
  type="button"
@@ -47,8 +47,8 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
47
47
  className={cn(
48
48
  'relative h-7 rounded-md px-3 font-medium text-sm transition-colors',
49
49
  isActive
50
- ? 'bg-accent text-foreground'
51
- : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
50
+ ? 'bg-sidebar-accent text-foreground'
51
+ : 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground',
52
52
  )}
53
53
  key={tab.id}
54
54
  onClick={() => {
@@ -101,8 +101,8 @@ export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailPr
101
101
  className={cn(
102
102
  'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
103
103
  showActive
104
- ? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
105
- : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
104
+ ? 'bg-sidebar-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
105
+ : 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
106
106
  )}
107
107
  onClick={() => {
108
108
  triggerSFX('sfx:menu-click')
@@ -28,6 +28,7 @@ import {
28
28
  } from 'lucide-react'
29
29
  import { cn } from '../../lib/utils'
30
30
  import { ActionButton } from '../ui/action-menu/action-button'
31
+ import { FollowPascalToggle } from '../ui/action-menu/follow-pascal-toggle'
31
32
  import {
32
33
  DropdownMenu,
33
34
  DropdownMenuContent,
@@ -103,13 +104,14 @@ function VisibilityMenu({
103
104
  className="hover:bg-white/5 hover:text-foreground"
104
105
  label="Visibility"
105
106
  size="icon"
107
+ tooltipClassName="dark"
106
108
  tooltipSide="top"
107
109
  variant="ghost"
108
110
  >
109
111
  <Eye className="h-5 w-5" />
110
112
  </ActionButton>
111
113
  </DropdownMenuTrigger>
112
- <DropdownMenuContent align="center" className="min-w-44" side="top">
114
+ <DropdownMenuContent align="center" className="dark min-w-44" side="top">
113
115
  {canShowScans && (
114
116
  <DropdownMenuItem
115
117
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
@@ -160,13 +162,14 @@ function DisplayMenu() {
160
162
  className="hover:bg-white/5 hover:text-foreground"
161
163
  label="Display settings"
162
164
  size="icon"
165
+ tooltipClassName="dark"
163
166
  tooltipSide="top"
164
167
  variant="ghost"
165
168
  >
166
169
  <SlidersHorizontal className="h-5 w-5" />
167
170
  </ActionButton>
168
171
  </DropdownMenuTrigger>
169
- <DropdownMenuContent align="center" className="min-w-56" side="top">
172
+ <DropdownMenuContent align="center" className="dark min-w-56" side="top">
170
173
  <DropdownMenuItem
171
174
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
172
175
  >
@@ -207,7 +210,7 @@ function DisplayMenu() {
207
210
  <span>Render</span>
208
211
  <span className="ml-auto text-muted-foreground text-xs">{activeShading.name}</span>
209
212
  </DropdownMenuSubTrigger>
210
- <DropdownMenuSubContent className="min-w-56">
213
+ <DropdownMenuSubContent className="dark min-w-56">
211
214
  {SHADING_OPTIONS.map((option) => {
212
215
  const OptionIcon = option.icon
213
216
  return (
@@ -232,12 +235,12 @@ function DisplayMenu() {
232
235
  <DropdownMenuSub>
233
236
  <DropdownMenuSubTrigger>
234
237
  <SwatchBook className="h-4 w-4" />
235
- <span>Theme</span>
238
+ <span>Scene theme</span>
236
239
  <span className="ml-auto truncate text-muted-foreground text-xs">
237
240
  {activeTheme.name}
238
241
  </span>
239
242
  </DropdownMenuSubTrigger>
240
- <DropdownMenuSubContent className="min-w-48">
243
+ <DropdownMenuSubContent className="dark min-w-48">
241
244
  {SCENE_THEMES.map((t) => {
242
245
  const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
243
246
  (role) => t.clayTints?.[role] ?? CLAY_PALETTE[role],
@@ -270,7 +273,7 @@ function DisplayMenu() {
270
273
  <span>Edges</span>
271
274
  <span className="ml-auto text-muted-foreground text-xs">{activeEdges.name}</span>
272
275
  </DropdownMenuSubTrigger>
273
- <DropdownMenuSubContent className="min-w-56">
276
+ <DropdownMenuSubContent className="dark min-w-56">
274
277
  {EDGE_OPTIONS.map((option) => (
275
278
  <DropdownMenuItem
276
279
  key={option.id}
@@ -360,6 +363,7 @@ export const ViewerControlsBar = ({
360
363
  useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
361
364
  }}
362
365
  size="icon"
366
+ tooltipClassName="dark"
363
367
  tooltipSide="top"
364
368
  variant="ghost"
365
369
  >
@@ -383,6 +387,7 @@ export const ViewerControlsBar = ({
383
387
  useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
384
388
  }}
385
389
  size="icon"
390
+ tooltipClassName="dark"
386
391
  tooltipSide="top"
387
392
  variant="ghost"
388
393
  >
@@ -406,6 +411,7 @@ export const ViewerControlsBar = ({
406
411
  label={`Walkthrough: ${walkthroughActive ? 'On' : 'Off'}`}
407
412
  onClick={onWalkthroughToggle}
408
413
  size="icon"
414
+ tooltipClassName="dark"
409
415
  tooltipSide="top"
410
416
  variant="ghost"
411
417
  >
@@ -414,12 +420,16 @@ export const ViewerControlsBar = ({
414
420
 
415
421
  <div className="mx-1 h-5 w-px bg-border/40" />
416
422
 
423
+ {/* Follow Pascal */}
424
+ <FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
425
+
417
426
  {/* Camera actions */}
418
427
  <ActionButton
419
428
  className="group hidden hover:bg-white/5 sm:inline-flex"
420
429
  label="Orbit left"
421
430
  onClick={() => emitter.emit('camera-controls:orbit-ccw')}
422
431
  size="icon"
432
+ tooltipClassName="dark"
423
433
  tooltipSide="top"
424
434
  variant="ghost"
425
435
  >
@@ -435,6 +445,7 @@ export const ViewerControlsBar = ({
435
445
  label="Orbit right"
436
446
  onClick={() => emitter.emit('camera-controls:orbit-cw')}
437
447
  size="icon"
448
+ tooltipClassName="dark"
438
449
  tooltipSide="top"
439
450
  variant="ghost"
440
451
  >
@@ -450,6 +461,7 @@ export const ViewerControlsBar = ({
450
461
  label="Top view"
451
462
  onClick={() => emitter.emit('camera-controls:top-view')}
452
463
  size="icon"
464
+ tooltipClassName="dark"
453
465
  tooltipSide="top"
454
466
  variant="ghost"
455
467
  >
@@ -23,6 +23,7 @@ import { RoomAssemblyMesh } from '../editor/room-highlight'
23
23
  import {
24
24
  RoomLabelAnchor3D,
25
25
  RoomPillChip,
26
+ RoomPillDetail,
26
27
  RoomPillsLayer3D,
27
28
  roomLabelEntries,
28
29
  roomLabelsVisible,
@@ -108,7 +109,22 @@ export function ViewerRooms({ source }: { source: ViewerSource }) {
108
109
  {pillsPresent && levelId ? (
109
110
  <RoomPillsLayer3D
110
111
  entries={entries}
112
+ // A level of its own: nothing open or placed on one floor carries to the next.
113
+ key={levelId}
111
114
  levelObject={() => source.objectFor(levelId)}
115
+ renderDetail={(entry) => {
116
+ const zone = nodes[entry.zoneId]
117
+ if (zone?.type !== 'zone') return null
118
+ return (
119
+ <RoomPillDetail
120
+ areaM2={entry.area}
121
+ color={zone.color}
122
+ name={zoneLabel(zone)}
123
+ view="3d"
124
+ zoneId={zone.id}
125
+ />
126
+ )
127
+ }}
112
128
  renderLabel={(entry, anchors, elements) => {
113
129
  const zone = nodes[entry.zoneId]
114
130
  if (zone?.type !== 'zone') return null