@pascal-app/editor 1.0.0-beta.5 → 1.0.1

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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +5 -0
  6. package/src/components/editor/bake-thumbnail.tsx +4 -1
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +9 -2
  13. package/src/components/editor/export-manager.tsx +175 -33
  14. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  15. package/src/components/editor/first-person-controls.tsx +180 -12
  16. package/src/components/editor/floating-action-menu.tsx +55 -51
  17. package/src/components/editor/floorplan-panel.tsx +99 -15
  18. package/src/components/editor/grid.tsx +12 -7
  19. package/src/components/editor/group-actions.ts +34 -50
  20. package/src/components/editor/group-move-3d.ts +51 -21
  21. package/src/components/editor/group-transform-shared.test.ts +78 -0
  22. package/src/components/editor/group-transform-shared.ts +74 -7
  23. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  24. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  25. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  26. package/src/components/editor/handles/handle-placement.ts +69 -0
  27. package/src/components/editor/handles/linear-resize-drag.ts +78 -0
  28. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  29. package/src/components/editor/handles/resize-snap.ts +10 -1
  30. package/src/components/editor/handles/use-handle-drag.ts +23 -3
  31. package/src/components/editor/index.tsx +151 -24
  32. package/src/components/editor/node-action-menu.tsx +14 -1
  33. package/src/components/editor/node-arrow-handles.tsx +67 -119
  34. package/src/components/editor/selection-manager.tsx +153 -42
  35. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  36. package/src/components/editor/snapshot-capture.test.ts +422 -0
  37. package/src/components/editor/snapshot-capture.ts +145 -0
  38. package/src/components/editor/thumbnail-generator.tsx +295 -239
  39. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  40. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  41. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  42. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  43. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  44. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  45. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  46. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  47. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  48. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  49. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  50. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
  51. package/src/components/procedural-items/inspector.tsx +223 -0
  52. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  53. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  54. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  55. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  56. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  57. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  58. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  59. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  60. package/src/components/systems/selection-affordance-services.ts +19 -0
  61. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  62. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  63. package/src/components/tools/fence/fence-drafting.ts +6 -25
  64. package/src/components/tools/item/draft-creation.test.ts +12 -0
  65. package/src/components/tools/item/draft-creation.ts +10 -0
  66. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  67. package/src/components/tools/item/face-host-commit.ts +56 -0
  68. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  69. package/src/components/tools/item/face-host-preview.ts +82 -0
  70. package/src/components/tools/item/item-surface-placement.test.ts +175 -0
  71. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
  72. package/src/components/tools/item/move-tool.tsx +4 -3
  73. package/src/components/tools/item/placement-math.test.ts +11 -1
  74. package/src/components/tools/item/placement-math.ts +0 -20
  75. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  76. package/src/components/tools/item/placement-strategies.ts +304 -171
  77. package/src/components/tools/item/placement-surface.test.ts +61 -0
  78. package/src/components/tools/item/placement-surface.ts +24 -0
  79. package/src/components/tools/item/placement-types.ts +10 -0
  80. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  81. package/src/components/tools/item/test-face-host.ts +121 -0
  82. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  83. package/src/components/tools/item/use-draft-node.ts +116 -44
  84. package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
  85. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  86. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  87. package/src/components/tools/registry/item-surface-move.test.ts +501 -0
  88. package/src/components/tools/registry/item-surface-move.ts +441 -0
  89. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  90. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  91. package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
  92. package/src/components/tools/registry-tool-context.tsx +30 -0
  93. package/src/components/tools/select/box-select-tool.tsx +3 -1
  94. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  95. package/src/components/tools/select/marquee-footprint.ts +29 -0
  96. package/src/components/tools/shared/placement-box.tsx +57 -7
  97. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  98. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  99. package/src/components/tools/shared/pointer-support-cap.ts +56 -17
  100. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  101. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  102. package/src/components/tools/shared/surface-hit.ts +23 -0
  103. package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
  104. package/src/components/tools/shared/surface-rejection.tsx +29 -0
  105. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  106. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  107. package/src/components/tools/stair/stair-tool.tsx +127 -74
  108. package/src/components/tools/tool-manager.tsx +25 -8
  109. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  110. package/src/components/tools/wall/wall-drafting.ts +23 -114
  111. package/src/components/tools/zone/zone-tool.tsx +13 -4
  112. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  113. package/src/components/ui/action-menu/index.tsx +11 -1
  114. package/src/components/ui/command-palette/editor-commands.tsx +63 -13
  115. package/src/components/ui/command-palette/index.tsx +7 -2
  116. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  117. package/src/components/ui/controls/material-picker.tsx +4 -4
  118. package/src/components/ui/controls/metric-control.tsx +29 -13
  119. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  120. package/src/components/ui/controls/segmented-control.tsx +8 -1
  121. package/src/components/ui/controls/slider-control.tsx +44 -21
  122. package/src/components/ui/controls/toggle-control.tsx +18 -5
  123. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  124. package/src/components/ui/floating-level-selector.tsx +103 -10
  125. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  126. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  127. package/src/components/ui/helpers/item-helper.tsx +1 -1
  128. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  129. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  130. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  131. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  132. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  133. package/src/components/ui/panels/multi-field-value.ts +163 -0
  134. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  135. package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
  136. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  137. package/src/components/ui/panels/node-display.ts +4 -0
  138. package/src/components/ui/panels/panel-manager.tsx +77 -3
  139. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  140. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  141. package/src/components/ui/panels/parametric-inspector.tsx +35 -164
  142. package/src/components/ui/panels/reference-panel.tsx +34 -4
  143. package/src/components/ui/primitives/checkbox.tsx +34 -0
  144. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  145. package/src/components/ui/scene-loader.tsx +35 -0
  146. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
  147. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  148. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
  149. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
  150. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  151. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  152. package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
  153. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  154. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  155. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  156. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  157. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  158. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
  159. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  160. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  161. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  162. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  163. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  164. package/src/components/ui/snap-target-badge.tsx +2 -0
  165. package/src/components/viewer/viewer-scene-header.tsx +5 -1
  166. package/src/components/viewer/viewer-stage.tsx +4 -1
  167. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  168. package/src/components/viewer-overlay.tsx +1 -22
  169. package/src/components/viewer-zone-system.tsx +6 -2
  170. package/src/components/walkthrough-hud.tsx +19 -10
  171. package/src/hooks/use-auto-save.test.ts +73 -1
  172. package/src/hooks/use-auto-save.ts +76 -5
  173. package/src/hooks/use-ceiling-events.test.ts +144 -0
  174. package/src/hooks/use-ceiling-events.ts +5 -1
  175. package/src/hooks/use-drag-action.ts +17 -3
  176. package/src/hooks/use-grid-events.test.ts +240 -0
  177. package/src/hooks/use-grid-events.ts +190 -11
  178. package/src/hooks/use-keyboard.test.ts +244 -0
  179. package/src/hooks/use-keyboard.ts +132 -63
  180. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  181. package/src/hooks/use-save-shortcut.ts +33 -0
  182. package/src/index.tsx +87 -4
  183. package/src/lib/active-placement-surface.test.ts +16 -0
  184. package/src/lib/active-placement-surface.ts +14 -0
  185. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  186. package/src/lib/ceiling-surface-raycast.ts +12 -0
  187. package/src/lib/contextual-help-extension.ts +17 -0
  188. package/src/lib/continuation.test.ts +11 -0
  189. package/src/lib/continuation.ts +8 -1
  190. package/src/lib/direct-manipulation.test.ts +132 -0
  191. package/src/lib/direct-manipulation.ts +38 -1
  192. package/src/lib/editor-api.ts +6 -3
  193. package/src/lib/elevation-guides.test.ts +28 -1
  194. package/src/lib/elevation-guides.ts +22 -7
  195. package/src/lib/export-texture-utils.test.ts +167 -0
  196. package/src/lib/export-texture-utils.ts +121 -0
  197. package/src/lib/export-user-data.test.ts +48 -0
  198. package/src/lib/export-user-data.ts +69 -0
  199. package/src/lib/floating-menu-scale.test.ts +29 -0
  200. package/src/lib/floating-menu-scale.ts +14 -0
  201. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  202. package/src/lib/floorplan/apply-alignment.ts +10 -1
  203. package/src/lib/floorplan/floorplan-export.test.ts +536 -2
  204. package/src/lib/floorplan/floorplan-export.tsx +120 -19
  205. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  206. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  207. package/src/lib/floorplan/floorplan-readonly.ts +5 -0
  208. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  209. package/src/lib/floorplan-grid-event-point.ts +13 -0
  210. package/src/lib/fresh-planar-placement.ts +13 -5
  211. package/src/lib/glb-export-serialize.test.ts +280 -0
  212. package/src/lib/glb-export.test.ts +1375 -21
  213. package/src/lib/glb-export.ts +781 -106
  214. package/src/lib/grid-event-presentation.ts +16 -0
  215. package/src/lib/history.test.ts +528 -147
  216. package/src/lib/history.ts +81 -7
  217. package/src/lib/host-tree-children.test.ts +27 -0
  218. package/src/lib/host-tree-children.ts +79 -0
  219. package/src/lib/interaction/hot-set.test.ts +10 -1
  220. package/src/lib/interaction/hot-set.ts +3 -1
  221. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  222. package/src/lib/interaction/overlay-policy.ts +16 -0
  223. package/src/lib/interaction/registered-drafting.ts +36 -0
  224. package/src/lib/interaction/scope.ts +31 -0
  225. package/src/lib/level-duplication.test.ts +73 -0
  226. package/src/lib/level-duplication.ts +32 -2
  227. package/src/lib/level-print-export.test.ts +484 -0
  228. package/src/lib/level-print-export.ts +526 -0
  229. package/src/lib/linear-display.test.ts +59 -0
  230. package/src/lib/linear-display.ts +39 -0
  231. package/src/lib/local-guide-image.test.ts +24 -0
  232. package/src/lib/local-guide-image.ts +34 -1
  233. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  234. package/src/lib/local-project-presentation-persistence.ts +335 -0
  235. package/src/lib/material-paint.ts +2 -0
  236. package/src/lib/model-export.ts +29 -0
  237. package/src/lib/node-action-movement.ts +21 -0
  238. package/src/lib/paint-preview-owner.ts +71 -0
  239. package/src/lib/planar-cursor-placement.test.ts +178 -1
  240. package/src/lib/planar-cursor-placement.ts +61 -5
  241. package/src/lib/plugin-panels.test.ts +92 -1
  242. package/src/lib/plugin-panels.ts +48 -0
  243. package/src/lib/portable-export.test.ts +455 -0
  244. package/src/lib/portable-export.ts +1042 -0
  245. package/src/lib/print-3mf.test.ts +86 -0
  246. package/src/lib/print-3mf.ts +161 -0
  247. package/src/lib/print-content-scope.test.ts +107 -0
  248. package/src/lib/print-content-scope.ts +90 -0
  249. package/src/lib/print-export.test.ts +294 -0
  250. package/src/lib/print-export.ts +735 -0
  251. package/src/lib/print-feature-thickness.test.ts +147 -0
  252. package/src/lib/print-feature-thickness.ts +180 -0
  253. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  254. package/src/lib/print-golden-house.test.ts +249 -0
  255. package/src/lib/print-roof-solids.test.ts +133 -0
  256. package/src/lib/print-roof-solids.ts +630 -0
  257. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  258. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  259. package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
  260. package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
  261. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  262. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  263. package/src/lib/print-shell-compiler-protocol.ts +38 -0
  264. package/src/lib/print-shell-compiler.ts +273 -0
  265. package/src/lib/print-wall-solids.test.ts +194 -0
  266. package/src/lib/print-wall-solids.ts +328 -0
  267. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  268. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  269. package/src/lib/scene-clipboard.ts +1 -1
  270. package/src/lib/scene.ts +22 -8
  271. package/src/lib/selection-routing.test.ts +105 -1
  272. package/src/lib/selection-routing.ts +19 -0
  273. package/src/lib/send-to-app.test.ts +108 -0
  274. package/src/lib/send-to-app.ts +121 -0
  275. package/src/lib/sfx/movement-tick.ts +37 -0
  276. package/src/lib/sfx-bus.ts +4 -0
  277. package/src/lib/sfx-player.ts +8 -0
  278. package/src/lib/snapping-mode.test.ts +32 -4
  279. package/src/lib/snapping-mode.ts +11 -7
  280. package/src/lib/surface-attachment.ts +77 -0
  281. package/src/lib/units.ts +254 -0
  282. package/src/lib/usdz-export.ts +65 -0
  283. package/src/lib/use-linear-display.ts +8 -32
  284. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  285. package/src/store/live-draft-preview-stores.test.ts +3 -0
  286. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  287. package/src/store/tool-mode.test.ts +87 -0
  288. package/src/store/use-camera-hint-focus.ts +48 -0
  289. package/src/store/use-editor.tsx +299 -188
  290. package/src/store/use-floorplan-draft-preview.ts +9 -0
  291. package/src/store/use-handle-group.ts +4 -0
  292. package/src/store/use-interaction-scope.test.ts +23 -1
  293. package/src/store/use-interaction-scope.ts +63 -2
  294. package/src/store/use-placement-preview.ts +56 -3
  295. package/src/store/use-send-to-app.ts +21 -0
  296. package/src/components/tools/roof/roof-tool.tsx +0 -656
  297. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { AnyNodeId } from '@pascal-app/core'
4
- import { DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
4
+ import { DEFAULT_LEVEL_HEIGHT, LevelNode, type UnitNode, useScene } from '@pascal-app/core'
5
5
  import { useViewer } from '@pascal-app/viewer'
6
6
  import {
7
7
  AppWindow,
@@ -15,6 +15,7 @@ import {
15
15
  EyeOff,
16
16
  FileJson,
17
17
  Grid3X3,
18
+ Group,
18
19
  Hexagon,
19
20
  Layers,
20
21
  Map,
@@ -39,6 +40,7 @@ import {
39
40
  import { useEffect } from 'react'
40
41
  import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
41
42
  import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
43
+ import { createUnitInBuilding, enterUnitFocus, leaveUnitFocus } from '../../../lib/units'
42
44
  import { useCommandRegistry } from '../../../store/use-command-registry'
43
45
  import type { StructureTool } from '../../../store/use-editor'
44
46
  import useEditor from '../../../store/use-editor'
@@ -49,28 +51,39 @@ export function EditorCommands() {
49
51
  const { navigateTo, setInputValue, setOpen } = useCommandPalette()
50
52
 
51
53
  const setPhase = useEditor((s) => s.setPhase)
52
- const setMode = useEditor((s) => s.setMode)
53
- const setTool = useEditor((s) => s.setTool)
54
+ const armToolMode = useEditor((s) => s.armToolMode)
55
+ const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
54
56
  const setStructureLayer = useEditor((s) => s.setStructureLayer)
55
- const primeMaterialPaintFromSelection = useEditor((s) => s.primeMaterialPaintFromSelection)
56
57
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
57
58
  const setPreviewMode = useEditor((s) => s.setPreviewMode)
58
59
 
59
60
  const exportScene = useViewer((s) => s.exportScene)
61
+ // Focusable units are listed one command each; the key changes only when a
62
+ // unit is added, removed or renamed.
63
+ const unitListKey = useScene((s) =>
64
+ Object.values(s.nodes)
65
+ .filter((n): n is UnitNode => n.type === 'unit')
66
+ .map((n) => `${n.id}:${n.name ?? ''}`)
67
+ .join('|'),
68
+ )
60
69
 
61
70
  // Re-register when exportScene availability changes (it's a conditional action)
62
71
  useEffect(() => {
72
+ void unitListKey
63
73
  const run = (fn: () => void) => {
64
74
  fn()
65
75
  setOpen(false)
66
76
  }
77
+ const focusableUnits = Object.values(useScene.getState().nodes).filter(
78
+ (n): n is UnitNode => n.type === 'unit',
79
+ )
67
80
 
68
81
  const activateTool = (tool: StructureTool) => {
69
82
  run(() => {
70
83
  setPhase('structure')
71
- setMode('build')
72
84
  if (tool === 'zone') setStructureLayer('zones')
73
- setTool(tool)
85
+ else setStructureLayer('elements')
86
+ armToolMode({ mode: 'build', tool })
74
87
  })
75
88
  }
76
89
 
@@ -163,10 +176,9 @@ export function EditorCommands() {
163
176
  shortcut: ['P'],
164
177
  execute: () =>
165
178
  run(() => {
166
- primeMaterialPaintFromSelection()
167
179
  setPhase('structure')
168
180
  setStructureLayer('elements')
169
- setMode('material-paint')
181
+ armMaterialPaint()
170
182
  }),
171
183
  },
172
184
  {
@@ -176,9 +188,8 @@ export function EditorCommands() {
176
188
  icon: <Mountain className="h-4 w-4" />,
177
189
  keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
178
190
  shortcut: ['G'],
179
- // No `setPhase`: `setMode` moves to the site phase itself, and doing it
180
- // here would set the phase twice with a mode reset in between.
181
- execute: () => run(() => setMode('terrain-sculpt')),
191
+ // The ToolMode transition moves to the site phase itself.
192
+ execute: () => run(() => armToolMode({ mode: 'terrain-sculpt' })),
182
193
  },
183
194
 
184
195
  // ── Levels ───────────────────────────────────────────────────────────
@@ -252,6 +263,44 @@ export function EditorCommands() {
252
263
  }),
253
264
  },
254
265
 
266
+ // ── Units ────────────────────────────────────────────────────────────
267
+ {
268
+ id: 'editor.unit.new',
269
+ label: 'New unit',
270
+ group: 'Units',
271
+ icon: <Group className="h-4 w-4" />,
272
+ keywords: ['unit', 'apartment', 'hotel', 'room', 'add', 'create', 'new'],
273
+ when: () => Object.values(useScene.getState().nodes).some((n) => n.type === 'building'),
274
+ execute: () =>
275
+ run(() => {
276
+ const { nodes } = useScene.getState()
277
+ const selectedBuildingId = useViewer.getState().selection.buildingId
278
+ const building =
279
+ (selectedBuildingId ? nodes[selectedBuildingId] : undefined) ??
280
+ Object.values(nodes).find((n) => n.type === 'building')
281
+ if (building?.type !== 'building') return
282
+ createUnitInBuilding(building.id)
283
+ }),
284
+ },
285
+ ...focusableUnits.map((unit) => ({
286
+ id: `editor.unit.focus.${unit.id}`,
287
+ label: `Focus unit: ${unit.name || 'Unit'}`,
288
+ group: 'Units',
289
+ icon: <Group className="h-4 w-4" />,
290
+ keywords: ['unit', 'focus', 'apartment', unit.name || 'Unit'],
291
+ when: () => useViewer.getState().focusedUnitId !== unit.id,
292
+ execute: () => run(() => enterUnitFocus(unit.id)),
293
+ })),
294
+ {
295
+ id: 'editor.unit.exit-focus',
296
+ label: 'Exit unit focus',
297
+ group: 'Units',
298
+ icon: <Group className="h-4 w-4" />,
299
+ keywords: ['unit', 'focus', 'exit', 'leave', 'apartment'],
300
+ when: () => !!useViewer.getState().focusedUnitId,
301
+ execute: () => run(() => leaveUnitFocus()),
302
+ },
303
+
255
304
  // ── Viewer Controls ──────────────────────────────────────────────────
256
305
  {
257
306
  id: 'editor.viewer.wall-mode',
@@ -428,12 +477,13 @@ export function EditorCommands() {
428
477
  setInputValue,
429
478
  setOpen,
430
479
  setPhase,
431
- setMode,
432
- setTool,
480
+ armToolMode,
481
+ armMaterialPaint,
433
482
  setStructureLayer,
434
483
  isPreviewMode,
435
484
  setPreviewMode,
436
485
  exportScene,
486
+ unitListKey,
437
487
  ])
438
488
 
439
489
  return null
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { AnyNodeId, LevelNode } from '@pascal-app/core'
4
4
  import { useScene } from '@pascal-app/core'
5
- import { useViewer } from '@pascal-app/viewer'
5
+ import { markPerfAction, useViewer } from '@pascal-app/viewer'
6
6
  import { Command, useCommandState } from 'cmdk'
7
7
  import { ChevronRight, Search } from 'lucide-react'
8
8
  import type { ReactNode } from 'react'
@@ -408,7 +408,12 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
408
408
  key={level.id}
409
409
  label={getLevelDisplayName(level)}
410
410
  onSelect={() =>
411
- run(() => useViewer.getState().setSelection({ levelId: level.id }))
411
+ run(() => {
412
+ if (level.id !== useViewer.getState().selection.levelId) {
413
+ markPerfAction('level-switch', level.id)
414
+ }
415
+ useViewer.getState().setSelection({ levelId: level.id })
416
+ })
412
417
  }
413
418
  />
414
419
  ))}
@@ -35,7 +35,7 @@ export type MaterialPaintPanelProps = {
35
35
  export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
36
36
  const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
37
37
  const activePaintTarget = useEditor((state) => state.activePaintTarget)
38
- const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
38
+ const armMaterialPaint = useEditor((state) => state.armMaterialPaint)
39
39
  const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
40
40
  const paintEraser = useEditor((state) => state.paintEraser)
41
41
  const setPaintEraser = useEditor((state) => state.setPaintEraser)
@@ -81,7 +81,7 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
81
81
  },
82
82
  },
83
83
  })
84
- setActivePaintMaterial({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
84
+ armMaterialPaint({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
85
85
  setAutoEditMaterialId(id)
86
86
  }
87
87
 
@@ -112,11 +112,13 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
112
112
  </div>
113
113
 
114
114
  {/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
115
- <div className="min-h-0 flex-1">
115
+ {/* A stable hook for host-app onboarding to point at. Static, and read
116
+ only from outside: nothing here depends on it. */}
117
+ <div className="min-h-0 flex-1" data-guide-target="paint-material">
116
118
  <MaterialPicker
117
119
  onCreateMaterialRequest={onCreateMaterialRequest}
118
120
  onSelectMaterialPreset={(materialPreset) => {
119
- setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
121
+ armMaterialPaint({ materialPreset, sourceTarget: activePaintTarget })
120
122
  }}
121
123
  selectedMaterialPreset={activePaintMaterial?.materialPreset}
122
124
  />
@@ -17,7 +17,7 @@ import { Plus } from 'lucide-react'
17
17
  import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
18
18
  import { triggerSFX } from '../../../lib/sfx-bus'
19
19
 
20
- export type MaterialSourceFilter = 'all' | MaterialSource
20
+ export type MaterialSourceFilter = MaterialSource
21
21
 
22
22
  export type MaterialPickerProps = {
23
23
  selectedMaterialPreset?: string
@@ -28,8 +28,9 @@ export type MaterialPickerProps = {
28
28
  onCreateMaterialRequest?: () => void
29
29
  }
30
30
 
31
+ // No 'All': the browse surfaces (Items / Rooms / Build) dropped it and default
32
+ // to the Pascal library — the combined list buried the curated set.
31
33
  const SOURCE_FILTERS: { id: MaterialSourceFilter; label: string }[] = [
32
- { id: 'all', label: 'All' },
33
34
  { id: 'pascal', label: 'Pascal' },
34
35
  { id: 'mine', label: 'Mine' },
35
36
  { id: 'workspace', label: 'Workspace' },
@@ -41,7 +42,6 @@ function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
41
42
  }
42
43
 
43
44
  function filterBySource(items: MaterialCatalogItem[], filter: MaterialSourceFilter) {
44
- if (filter === 'all') return items
45
45
  return items.filter((item) => (item.source ?? 'pascal') === filter)
46
46
  }
47
47
 
@@ -60,7 +60,7 @@ export function MaterialPicker({
60
60
  const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
61
61
  MATERIAL_CATEGORIES[0],
62
62
  )
63
- const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('all')
63
+ const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('pascal')
64
64
  // Version counter so host registrations/unregistrations re-render the picker.
65
65
  const libraryVersion = useSyncExternalStore(
66
66
  subscribeLibraryMaterials,
@@ -2,11 +2,7 @@
2
2
 
3
3
  import { useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
- import {
6
- lingoUnitSpec,
7
- measurementHint,
8
- parseMeasurement,
9
- } from '../../../lib/measurement-parser'
5
+ import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
10
6
  import { useLinearDisplay } from '../../../lib/use-linear-display'
11
7
  import { cn } from '../../../lib/utils'
12
8
 
@@ -31,8 +27,8 @@ export function MetricControl({
31
27
  onCommit,
32
28
  min = Number.NEGATIVE_INFINITY,
33
29
  max = Number.POSITIVE_INFINITY,
34
- precision = 2,
35
- step = 1,
30
+ precision: storedPrecision = 2,
31
+ step: storedStep = 1,
36
32
  className,
37
33
  unit = '',
38
34
  restoreOnCommit = true,
@@ -40,9 +36,12 @@ export function MetricControl({
40
36
  const {
41
37
  isImperial,
42
38
  displayUnit,
39
+ parseUnit,
40
+ precision,
41
+ step,
43
42
  toDisplay: toDisplayValue,
44
43
  toStored: toStoredValue,
45
- } = useLinearDisplay(unit, precision)
44
+ } = useLinearDisplay(unit, storedPrecision, storedStep)
46
45
 
47
46
  const clamp = useCallback(
48
47
  (val: number) => {
@@ -110,7 +109,14 @@ export function MetricControl({
110
109
 
111
110
  container.addEventListener('wheel', handleWheel, { passive: false })
112
111
  return () => container.removeEventListener('wheel', handleWheel)
113
- }, [isEditing, step, clamp, applyCommittedValue, toStoredValue, roundStoredValueForDisplayPrecision])
112
+ }, [
113
+ isEditing,
114
+ step,
115
+ clamp,
116
+ applyCommittedValue,
117
+ toStoredValue,
118
+ roundStoredValueForDisplayPrecision,
119
+ ])
114
120
 
115
121
  useEffect(() => {
116
122
  if (!isHovered || isEditing) return
@@ -226,7 +232,7 @@ export function MetricControl({
226
232
  const spec = lingoUnitSpec(unit)
227
233
  let stored = spec
228
234
  ? parseMeasurement(inputValue, spec, {
229
- bareUnit: isImperial ? 'ft' : spec.unitId,
235
+ bareUnit: parseUnit ?? spec.unitId,
230
236
  system: isImperial ? 'us' : 'metric',
231
237
  })
232
238
  : null
@@ -244,6 +250,7 @@ export function MetricControl({
244
250
  inputValue,
245
251
  unit,
246
252
  isImperial,
253
+ parseUnit,
247
254
  applyCommittedValue,
248
255
  clamp,
249
256
  toStoredValue,
@@ -256,9 +263,9 @@ export function MetricControl({
256
263
  const hint =
257
264
  isEditing && spec
258
265
  ? measurementHint(inputValue, spec, {
259
- bareUnit: isImperial ? 'ft' : spec.unitId,
266
+ bareUnit: parseUnit ?? spec.unitId,
260
267
  system: isImperial ? 'us' : 'metric',
261
- displayUnit: isImperial ? 'ft' : spec.unitId,
268
+ displayUnit: parseUnit ?? spec.unitId,
262
269
  precision,
263
270
  clamp,
264
271
  })
@@ -287,7 +294,16 @@ export function MetricControl({
287
294
  setInputValue(toDisplayValue(newV).toFixed(precision))
288
295
  }
289
296
  },
290
- [submitValue, value, toDisplayValue, precision, step, clamp, applyCommittedValue, toStoredValue],
297
+ [
298
+ submitValue,
299
+ value,
300
+ toDisplayValue,
301
+ precision,
302
+ step,
303
+ clamp,
304
+ applyCommittedValue,
305
+ toStoredValue,
306
+ ],
291
307
  )
292
308
 
293
309
  return (
@@ -33,7 +33,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
33
33
  const removeSceneMaterial = useScene((state) => state.removeSceneMaterial)
34
34
  const activePaintTarget = useEditor((state) => state.activePaintTarget)
35
35
  const activePaintRef = useEditor((state) => state.activePaintMaterial?.materialPreset)
36
- const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
36
+ const armMaterialPaint = useEditor((state) => state.armMaterialPaint)
37
37
 
38
38
  const materialEntries = useMemo(
39
39
  () => Object.entries(materials) as [SceneMaterialId, SceneMaterial][],
@@ -76,7 +76,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
76
76
  key={id}
77
77
  removeSceneMaterial={removeSceneMaterial}
78
78
  sceneMaterial={sceneMaterial}
79
- setActivePaintMaterial={setActivePaintMaterial}
79
+ armMaterialPaint={armMaterialPaint}
80
80
  updateSceneMaterial={updateSceneMaterial}
81
81
  usageCount={usageCounts.get(id) ?? 0}
82
82
  />
@@ -95,7 +95,7 @@ function SceneMaterialRow({
95
95
  addSceneMaterial,
96
96
  updateSceneMaterial,
97
97
  removeSceneMaterial,
98
- setActivePaintMaterial,
98
+ armMaterialPaint,
99
99
  }: {
100
100
  id: SceneMaterialId
101
101
  sceneMaterial: SceneMaterial
@@ -106,7 +106,7 @@ function SceneMaterialRow({
106
106
  addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
107
107
  updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
108
108
  removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
109
- setActivePaintMaterial: ReturnType<typeof useEditor.getState>['setActivePaintMaterial']
109
+ armMaterialPaint: ReturnType<typeof useEditor.getState>['armMaterialPaint']
110
110
  }) {
111
111
  // A freshly-created material (via "+ Custom") mounts with its editor open.
112
112
  const [isEditingMaterial, setIsEditingMaterial] = useState(autoEdit)
@@ -141,6 +141,7 @@ function SceneMaterialRow({
141
141
  className={`rounded-md border border-border/60 bg-background/40 p-2 ${
142
142
  isActive ? 'ring-1 ring-primary ring-inset' : ''
143
143
  }`}
144
+ data-testid={`scene-material-row-${id}`}
144
145
  >
145
146
  <div className="flex items-center gap-2">
146
147
  <span
@@ -174,7 +175,7 @@ function SceneMaterialRow({
174
175
  <Button
175
176
  aria-label="Paint with"
176
177
  onClick={() =>
177
- setActivePaintMaterial({
178
+ armMaterialPaint({
178
179
  materialPreset: toSceneMaterialRef(id),
179
180
  sourceTarget: activePaintTarget,
180
181
  })
@@ -7,6 +7,8 @@ interface SegmentedControlProps<T extends string> {
7
7
  onChange: (value: T) => void
8
8
  options: { label: React.ReactNode; value: T }[]
9
9
  className?: string
10
+ disabled?: boolean
11
+ mixed?: boolean
10
12
  }
11
13
 
12
14
  export function SegmentedControl<T extends string>({
@@ -14,16 +16,19 @@ export function SegmentedControl<T extends string>({
14
16
  onChange,
15
17
  options,
16
18
  className,
19
+ disabled = false,
20
+ mixed = false,
17
21
  }: SegmentedControlProps<T>) {
18
22
  return (
19
23
  <div
20
24
  className={cn(
21
25
  'flex h-9 w-full items-center rounded-lg border border-border/50 bg-[#2C2C2E] p-[3px]',
26
+ disabled && 'opacity-60',
22
27
  className,
23
28
  )}
24
29
  >
25
30
  {options.map((option) => {
26
- const isSelected = value === option.value
31
+ const isSelected = !mixed && value === option.value
27
32
  return (
28
33
  <button
29
34
  className={cn(
@@ -31,7 +36,9 @@ export function SegmentedControl<T extends string>({
31
36
  isSelected
32
37
  ? 'bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
33
38
  : 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
39
+ disabled && 'cursor-not-allowed hover:bg-transparent hover:text-muted-foreground',
34
40
  )}
41
+ disabled={disabled}
35
42
  key={option.value}
36
43
  onClick={() => onChange(option.value)}
37
44
  type="button"
@@ -2,11 +2,7 @@
2
2
 
3
3
  import { useScene } from '@pascal-app/core'
4
4
  import { useCallback, useEffect, useRef, useState } from 'react'
5
- import {
6
- lingoUnitSpec,
7
- measurementHint,
8
- parseMeasurement,
9
- } from '../../../lib/measurement-parser'
5
+ import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
10
6
  import { useLinearDisplay } from '../../../lib/use-linear-display'
11
7
  import { cn } from '../../../lib/utils'
12
8
 
@@ -22,6 +18,7 @@ interface SliderControlProps {
22
18
  className?: string
23
19
  unit?: string
24
20
  restoreOnCommit?: boolean
21
+ mixed?: boolean
25
22
  }
26
23
 
27
24
  function stepPrecision(s: number): number {
@@ -59,23 +56,25 @@ export function SliderControl({
59
56
  onCommit,
60
57
  min = Number.NEGATIVE_INFINITY,
61
58
  max = Number.POSITIVE_INFINITY,
62
- precision = 0,
63
- step = 1,
59
+ precision: storedPrecision = 0,
60
+ step: storedStep = 1,
64
61
  className,
65
62
  unit = '',
66
63
  restoreOnCommit = true,
64
+ mixed = false,
67
65
  }: SliderControlProps) {
68
- // Display/storage conversion so the value honors the metric/imperial toggle.
69
- // `value`, `onChange`, `onCommit`, `min`/`max`/`clamp` are always in the
70
- // stored unit (meters for `unit === 'm'`); the step, drag deltas, text field
71
- // and rendered number are in the DISPLAY unit (feet when imperial). For
72
- // metric and non-length units these conversions are the identity.
73
- const { isImperial, displayUnit, toDisplay, toStored } = useLinearDisplay(unit, precision)
66
+ // Values and bounds stay in meters; gestures and input use the displayed unit.
67
+ const { isImperial, displayUnit, parseUnit, precision, step, toDisplay, toStored } =
68
+ useLinearDisplay(unit, storedPrecision, storedStep)
74
69
 
75
70
  const [isEditing, setIsEditing] = useState(false)
76
71
  const [isDragging, setIsDragging] = useState(false)
77
72
  const [isHovered, setIsHovered] = useState(false)
78
73
  const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
74
+ // Live readout while dragging. Multi-edit previews write live overrides
75
+ // instead of the scene, so `value` would otherwise stay frozen even though
76
+ // the meshes are moving.
77
+ const [dragDisplay, setDragDisplay] = useState<number | null>(null)
79
78
 
80
79
  const dragRef = useRef<{
81
80
  // Original value at drag start — preserved across modifier re-anchors so
@@ -89,8 +88,9 @@ export function SliderControl({
89
88
  stepMultiplier: number
90
89
  } | null>(null)
91
90
  const labelRef = useRef<HTMLDivElement>(null)
92
- const valueRef = useRef(value)
93
- valueRef.current = value
91
+ const shown = dragDisplay ?? value
92
+ const valueRef = useRef(shown)
93
+ valueRef.current = shown
94
94
 
95
95
  const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
96
96
  // Apply a signed display-unit delta to a stored value, rounding in the
@@ -99,7 +99,9 @@ export function SliderControl({
99
99
  (storedValue: number, displayDelta: number, displayStep: number) =>
100
100
  clamp(
101
101
  toStored(
102
- Number.parseFloat((toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep))),
102
+ Number.parseFloat(
103
+ (toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep)),
104
+ ),
103
105
  ),
104
106
  ),
105
107
  [clamp, toDisplay, toStored],
@@ -185,6 +187,7 @@ export function SliderControl({
185
187
  const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
186
188
  if (newValue !== valueRef.current) {
187
189
  valueRef.current = newValue
190
+ setDragDisplay(newValue)
188
191
  onChange(newValue)
189
192
  }
190
193
  },
@@ -209,6 +212,7 @@ export function SliderControl({
209
212
  useScene.temporal.getState().resume()
210
213
  onCommit?.(finalVal)
211
214
  }
215
+ setDragDisplay(null)
212
216
  },
213
217
  [onChange, onCommit, restoreOnCommit],
214
218
  )
@@ -222,7 +226,7 @@ export function SliderControl({
222
226
  const spec = lingoUnitSpec(unit)
223
227
  let stored = spec
224
228
  ? parseMeasurement(inputValue, spec, {
225
- bareUnit: isImperial ? 'ft' : spec.unitId,
229
+ bareUnit: parseUnit ?? spec.unitId,
226
230
  system: isImperial ? 'us' : 'metric',
227
231
  })
228
232
  : null
@@ -239,15 +243,27 @@ export function SliderControl({
239
243
  onCommit?.(nextValue)
240
244
  }
241
245
  setIsEditing(false)
242
- }, [inputValue, unit, isImperial, onChange, onCommit, clamp, precision, value, toDisplay, toStored])
246
+ }, [
247
+ inputValue,
248
+ unit,
249
+ isImperial,
250
+ parseUnit,
251
+ onChange,
252
+ onCommit,
253
+ clamp,
254
+ precision,
255
+ value,
256
+ toDisplay,
257
+ toStored,
258
+ ])
243
259
 
244
260
  const spec = lingoUnitSpec(unit)
245
261
  const hint =
246
262
  isEditing && spec
247
263
  ? measurementHint(inputValue, spec, {
248
- bareUnit: isImperial ? 'ft' : spec.unitId,
264
+ bareUnit: parseUnit ?? spec.unitId,
249
265
  system: isImperial ? 'us' : 'metric',
250
- displayUnit: isImperial ? 'ft' : spec.unitId,
266
+ displayUnit: parseUnit ?? spec.unitId,
251
267
  precision,
252
268
  clamp,
253
269
  })
@@ -272,7 +288,7 @@ export function SliderControl({
272
288
  [submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
273
289
  )
274
290
 
275
- const displayValue = toDisplay(value)
291
+ const displayValue = toDisplay(shown)
276
292
 
277
293
  return (
278
294
  <div
@@ -340,6 +356,13 @@ export function SliderControl({
340
356
  </span>
341
357
  )}
342
358
  </>
359
+ ) : mixed && !isDragging ? (
360
+ <div
361
+ className="flex cursor-text items-center text-muted-foreground transition-colors hover:text-foreground"
362
+ onClick={handleValueClick}
363
+ >
364
+ <span className="font-mono tracking-tight">Mixed</span>
365
+ </div>
343
366
  ) : (
344
367
  <div
345
368
  className="flex cursor-text items-center text-foreground/60 transition-colors hover:text-foreground"
@@ -8,9 +8,16 @@ interface ToggleControlProps {
8
8
  checked: boolean
9
9
  onChange: (checked: boolean) => void
10
10
  className?: string
11
+ mixed?: boolean
11
12
  }
12
13
 
13
- export function ToggleControl({ label, checked, onChange, className }: ToggleControlProps) {
14
+ export function ToggleControl({
15
+ label,
16
+ checked,
17
+ onChange,
18
+ className,
19
+ mixed = false,
20
+ }: ToggleControlProps) {
14
21
  return (
15
22
  <div
16
23
  className={cn(
@@ -26,12 +33,18 @@ export function ToggleControl({ label, checked, onChange, className }: ToggleCon
26
33
  <div
27
34
  className={cn(
28
35
  'flex h-5 w-5 items-center justify-center rounded-[4px] border transition-all duration-200',
29
- checked
30
- ? 'border-primary bg-primary text-primary-foreground'
31
- : 'border-border bg-black/20 text-transparent group-hover:border-muted-foreground',
36
+ mixed
37
+ ? 'border-muted-foreground/70 bg-black/20 text-muted-foreground'
38
+ : checked
39
+ ? 'border-primary bg-primary text-primary-foreground'
40
+ : 'border-border bg-black/20 text-transparent group-hover:border-muted-foreground',
32
41
  )}
33
42
  >
34
- <Check className="h-3.5 w-3.5" strokeWidth={3} />
43
+ {mixed ? (
44
+ <div className="h-0.5 w-2.5 rounded-full bg-current" />
45
+ ) : (
46
+ <Check className="h-3.5 w-3.5" strokeWidth={3} />
47
+ )}
35
48
  </div>
36
49
  </div>
37
50
  )