@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -22,6 +22,17 @@ const MOUSE_SHORTCUTS = {
22
22
  },
23
23
  } as const
24
24
 
25
+ // The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
26
+ // render the symbol for the *current* device so the hint reads native (⌘ on Mac,
27
+ // Ctrl elsewhere) without implying only one of them works.
28
+ const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
29
+
30
+ // Resolved once on the client at module load — the editor HUD is client-only, so
31
+ // there's no server render to mismatch against. `navigator.platform` is enough
32
+ // here and matches the detection used elsewhere (floorplan rotate hint).
33
+ const IS_MAC =
34
+ typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
35
+
25
36
  type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
26
37
  value: string
27
38
  displayValue?: string
@@ -30,16 +41,23 @@ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
30
41
  function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
31
42
  const mouseShortcut =
32
43
  value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
44
+ const isCommand = COMMAND_VALUES.has(value)
45
+ const isShift = value === 'Shift'
46
+ const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
47
+ const commandLabel = IS_MAC ? 'Command' : 'Control'
33
48
 
34
49
  return (
35
50
  <kbd
36
- aria-label={mouseShortcut?.label ?? displayValue ?? value}
51
+ aria-label={
52
+ mouseShortcut?.label ??
53
+ (isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
54
+ }
37
55
  className={cn(
38
56
  'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
39
- mouseShortcut && 'justify-center px-1.5',
57
+ (mouseShortcut || isShift) && 'justify-center px-1.5',
40
58
  className,
41
59
  )}
42
- title={mouseShortcut?.label ?? value}
60
+ title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
43
61
  {...props}
44
62
  >
45
63
  {mouseShortcut ? (
@@ -54,6 +72,24 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
54
72
  />
55
73
  <span className="sr-only">{mouseShortcut.label}</span>
56
74
  </>
75
+ ) : isShift ? (
76
+ // Icon rather than the ⇧ text glyph — the font renders the glyph's
77
+ // crossbar too high to read as the shift key symbol.
78
+ <>
79
+ <Icon
80
+ aria-hidden="true"
81
+ className="shrink-0"
82
+ color="currentColor"
83
+ height={13}
84
+ icon="ph:arrow-fat-up"
85
+ width={13}
86
+ />
87
+ <span className="sr-only">Shift</span>
88
+ </>
89
+ ) : isCommand ? (
90
+ // The ⌘ glyph reads small next to letters at the same font size, so bump
91
+ // it up a touch on Mac. "Ctrl" stays at the token's normal size.
92
+ <span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
57
93
  ) : (
58
94
  (displayValue ?? value)
59
95
  )}
@@ -63,6 +63,7 @@ export const useSidebarStore = create<SidebarStore>()(
63
63
  {
64
64
  name: 'sidebar-preferences',
65
65
  partialize: (state) => ({ width: state.width, isCollapsed: state.isCollapsed }),
66
+ skipHydration: true,
66
67
  },
67
68
  ),
68
69
  )
@@ -17,15 +17,13 @@ interface SceneLoaderProps {
17
17
  }
18
18
 
19
19
  export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps) {
20
- const [loaderClass, setLoaderClass] = useState<string | null>(null)
20
+ const [loaderClass, setLoaderClass] = useState(LOADERS[0]!)
21
21
 
22
22
  useEffect(() => {
23
23
  // Pick a random loader on mount
24
24
  setLoaderClass(LOADERS[Math.floor(Math.random() * LOADERS.length)] ?? LOADERS[0]!)
25
25
  }, [])
26
26
 
27
- if (!loaderClass) return null
28
-
29
27
  return (
30
28
  <div
31
29
  className={cn(
@@ -16,6 +16,7 @@ import useEditor from './../../../store/use-editor'
16
16
  import { type ExtraPanel, IconRail } from './icon-rail'
17
17
  import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
18
18
  import { SitePanel, type SitePanelProps } from './panels/site-panel'
19
+ import { useHostPanels } from './use-plugin-panels'
19
20
 
20
21
  interface AppSidebarProps {
21
22
  appMenuButton?: ReactNode
@@ -31,9 +32,12 @@ export function AppSidebar({
31
32
  sidebarTop,
32
33
  settingsPanelProps,
33
34
  sitePanelProps,
34
- extraPanels,
35
+ extraPanels: hostExtraPanels,
35
36
  commandPaletteEmptyAction,
36
37
  }: AppSidebarProps) {
38
+ // Host-provided panels merged with registered host panels — the icon rail and
39
+ // content area treat both identically.
40
+ const extraPanels = useHostPanels(hostExtraPanels)
37
41
  const activePanel = useEditor((s) => s.activeSidebarPanel)
38
42
  const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
39
43
  const hasActivePanel =
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { Plus } from 'lucide-react'
3
4
  import type { ComponentType, ReactNode } from 'react'
4
5
  import {
5
6
  Tooltip,
@@ -10,7 +11,13 @@ import { cn } from './../../../lib/utils'
10
11
 
11
12
  export type PanelId = string
12
13
 
13
- export type ExtraPanel = { id: string; icon: ReactNode; label: string; component: ComponentType }
14
+ export type ExtraPanel = {
15
+ id: string
16
+ icon: ReactNode
17
+ label: string
18
+ component: ComponentType
19
+ pluginId?: string
20
+ }
14
21
 
15
22
  interface IconRailProps {
16
23
  activePanel: PanelId
@@ -22,13 +29,13 @@ interface IconRailProps {
22
29
 
23
30
  const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
24
31
  id: 'site',
25
- iconSrc: '/icons/level.png',
32
+ iconSrc: '/icons/level.webp',
26
33
  label: 'Site',
27
34
  }
28
35
 
29
36
  const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
30
37
  id: 'settings',
31
- iconSrc: '/icons/settings.png',
38
+ iconSrc: '/icons/settings.webp',
32
39
  label: 'Settings',
33
40
  }
34
41
 
@@ -41,6 +48,38 @@ export function IconRail({
41
48
  extraPanels,
42
49
  className,
43
50
  }: IconRailProps) {
51
+ const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
52
+ const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
53
+ const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
54
+
55
+ const renderExtraPanel = (panel: ExtraPanel) => {
56
+ const isActive = activePanel === panel.id
57
+ return (
58
+ <Tooltip key={panel.id}>
59
+ <TooltipTrigger asChild>
60
+ <button
61
+ className={cn(
62
+ 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
63
+ isActive ? 'bg-accent' : 'hover:bg-accent',
64
+ )}
65
+ onClick={() => onPanelChange(panel.id)}
66
+ type="button"
67
+ >
68
+ <span
69
+ className={cn(
70
+ 'flex h-6 w-6 items-center justify-center transition-all',
71
+ !isActive && 'opacity-50',
72
+ )}
73
+ >
74
+ {panel.id === 'plugins' ? <Plus className="h-5 w-5" /> : panel.icon}
75
+ </span>
76
+ </button>
77
+ </TooltipTrigger>
78
+ <TooltipContent side="right">{panel.label}</TooltipContent>
79
+ </Tooltip>
80
+ )
81
+ }
82
+
44
83
  return (
45
84
  <div
46
85
  className={cn(
@@ -83,34 +122,7 @@ export function IconRail({
83
122
  )
84
123
  })}
85
124
 
86
- {/* Extra panels (injected between site and settings) */}
87
- {extraPanels?.map((panel) => {
88
- const isActive = activePanel === panel.id
89
- return (
90
- <Tooltip key={panel.id}>
91
- <TooltipTrigger asChild>
92
- <button
93
- className={cn(
94
- 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
95
- isActive ? 'bg-accent' : 'hover:bg-accent',
96
- )}
97
- onClick={() => onPanelChange(panel.id)}
98
- type="button"
99
- >
100
- <span
101
- className={cn(
102
- 'flex h-6 w-6 items-center justify-center transition-all',
103
- !isActive && 'opacity-50',
104
- )}
105
- >
106
- {panel.icon}
107
- </span>
108
- </button>
109
- </TooltipTrigger>
110
- <TooltipContent side="right">{panel.label}</TooltipContent>
111
- </Tooltip>
112
- )
113
- })}
125
+ {regularExtraPanels?.map(renderExtraPanel)}
114
126
 
115
127
  {/* Settings panel */}
116
128
  {[settingsPanel].map((panel) => {
@@ -140,6 +152,13 @@ export function IconRail({
140
152
  </Tooltip>
141
153
  )
142
154
  })}
155
+
156
+ {(pluginPanels?.length || pluginsPanel) && (
157
+ <div className="mt-1 flex w-9 flex-col items-center gap-1 border-border/70 border-t pt-2">
158
+ {pluginPanels?.map(renderExtraPanel)}
159
+ {pluginsPanel && renderExtraPanel(pluginsPanel)}
160
+ </div>
161
+ )}
143
162
  </div>
144
163
  )
145
164
  }
@@ -0,0 +1,263 @@
1
+ 'use client'
2
+
3
+ import type { AssetInput } from '@pascal-app/core'
4
+ import { Root as TooltipRoot } from '@radix-ui/react-tooltip'
5
+ import NextImage from 'next/image'
6
+ import { useMemo, useState } from 'react'
7
+ import { triggerSFX } from '../../../../../lib/sfx-bus'
8
+ import { cn } from '../../../../../lib/utils'
9
+ import { ItemCatalog } from '../../../item-catalog/item-catalog'
10
+ import {
11
+ TooltipContent,
12
+ TooltipProvider,
13
+ TooltipTrigger,
14
+ } from '../../../../../components/ui/primitives/tooltip'
15
+
16
+ /** A function-axis taxonomy node, assembled into a tree by the embedder. */
17
+ export type FunctionTreeNode = {
18
+ slug: string
19
+ name: string
20
+ iconUrl?: string | null
21
+ children: FunctionTreeNode[]
22
+ }
23
+
24
+ const SOURCE_CHIPS: Array<{ id: NonNullable<AssetInput['source']>; label: string }> = [
25
+ { id: 'library', label: 'Library' },
26
+ { id: 'community', label: 'Community' },
27
+ { id: 'mine', label: 'Mine' },
28
+ ]
29
+
30
+ /** Every slug at or below `node`, so a non-leaf selection matches descendants. */
31
+ function descendantSlugs(node: FunctionTreeNode): Set<string> {
32
+ const out = new Set<string>()
33
+ const walk = (n: FunctionTreeNode) => {
34
+ out.add(n.slug)
35
+ for (const child of n.children) walk(child)
36
+ }
37
+ walk(node)
38
+ return out
39
+ }
40
+
41
+ function itemFunctionSlugs(item: AssetInput): string[] {
42
+ if (item.functionTags && item.functionTags.length > 0) return item.functionTags
43
+ return item.category ? [item.category] : []
44
+ }
45
+
46
+ /**
47
+ * DB-driven hierarchical Items browse. Roots render as the category tab bar;
48
+ * a selected root with children exposes those children as a secondary chip
49
+ * row. Selecting any node shows items tagged with that node or any descendant.
50
+ * Library / Community / Mine narrows by source on top of the tree selection.
51
+ */
52
+ export function FunctionTreePanel({
53
+ functionTree,
54
+ items,
55
+ onSearchChange,
56
+ searchResults,
57
+ leadingTile,
58
+ emptyState,
59
+ }: {
60
+ functionTree: FunctionTreeNode[]
61
+ items?: AssetInput[]
62
+ onSearchChange?: (query: string) => void
63
+ searchResults?: AssetInput[] | null
64
+ leadingTile?: React.ReactNode
65
+ emptyState?: React.ReactNode
66
+ }) {
67
+ const [activeRootSlug, setActiveRootSlug] = useState<string | null>(
68
+ functionTree[0]?.slug ?? null,
69
+ )
70
+ const [activeChildSlug, setActiveChildSlug] = useState<string | null>(null)
71
+ const [activeSource, setActiveSource] = useState<AssetInput['source'] | null>('library')
72
+ const [search, setSearch] = useState('')
73
+
74
+ const isServerSearch = onSearchChange !== undefined
75
+ const isSearchPending = isServerSearch && search.length > 0 && searchResults === null
76
+
77
+ const activeRoot = functionTree.find((n) => n.slug === activeRootSlug) ?? functionTree[0]
78
+ const activeNode =
79
+ (activeChildSlug && activeRoot?.children.find((c) => c.slug === activeChildSlug)) || activeRoot
80
+
81
+ const matchesSource = (item: AssetInput) => {
82
+ if (!activeSource) return true
83
+ const itemSource = item.source ?? 'library'
84
+ if (activeSource === 'mine') return itemSource === 'mine'
85
+ if (activeSource === 'library') return itemSource === 'library'
86
+ if (activeSource === 'community') {
87
+ if (itemSource === 'community') return true
88
+ if (itemSource === 'mine') return !item.isDraft
89
+ return false
90
+ }
91
+ return true
92
+ }
93
+
94
+ const treeItems = useMemo(() => {
95
+ const base = items ?? []
96
+ if (!activeNode) return base.filter(matchesSource)
97
+ const slugs = descendantSlugs(activeNode)
98
+ return base.filter(
99
+ (item) => matchesSource(item) && itemFunctionSlugs(item).some((s) => slugs.has(s)),
100
+ )
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, [items, activeNode, activeSource])
103
+
104
+ const searchItems = useMemo(() => {
105
+ if (!(isServerSearch && search && searchResults)) return null
106
+ return activeSource ? searchResults.filter(matchesSource) : searchResults
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ }, [isServerSearch, search, searchResults, activeSource])
109
+
110
+ function selectRoot(slug: string) {
111
+ setActiveRootSlug(slug)
112
+ setActiveChildSlug(null)
113
+ setSearch('')
114
+ onSearchChange?.('')
115
+ }
116
+
117
+ return (
118
+ <div className="flex h-full flex-col">
119
+ {/* Root nodes as a category grid — icon when available, otherwise a
120
+ two-letter abbreviation, with the full name in a hover tooltip.
121
+ Mirrors the Build tab's tile grid so the two panels read the same. */}
122
+ <TooltipProvider delayDuration={0} disableHoverableContent>
123
+ <div className="grid shrink-0 grid-cols-5 gap-1.5 border-border/70 border-b p-2">
124
+ {functionTree.map((root) => {
125
+ const isActive = activeRoot?.slug === root.slug
126
+ return (
127
+ <TooltipRoot key={root.slug}>
128
+ <TooltipTrigger asChild>
129
+ <button
130
+ className={cn(
131
+ 'relative flex aspect-square items-center justify-center rounded-xl transition-all duration-200',
132
+ isActive
133
+ ? 'bg-primary/10 ring-1 ring-primary/50'
134
+ : 'bg-muted/40 opacity-70 grayscale hover:bg-muted hover:opacity-100 hover:grayscale-0',
135
+ )}
136
+ onClick={() => {
137
+ triggerSFX('sfx:menu-click')
138
+ selectRoot(root.slug)
139
+ }}
140
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
141
+ type="button"
142
+ >
143
+ {root.iconUrl ? (
144
+ <NextImage
145
+ alt={root.name}
146
+ className="size-7 object-contain"
147
+ height={28}
148
+ src={root.iconUrl}
149
+ width={28}
150
+ />
151
+ ) : (
152
+ <span className="font-semibold text-muted-foreground text-xs uppercase">
153
+ {root.name.slice(0, 2)}
154
+ </span>
155
+ )}
156
+ </button>
157
+ </TooltipTrigger>
158
+ <TooltipContent className="icon-grid-tooltip pointer-events-none" side="top">
159
+ {root.name}
160
+ </TooltipContent>
161
+ </TooltipRoot>
162
+ )
163
+ })}
164
+ </div>
165
+ </TooltipProvider>
166
+
167
+ {/* Search + source filter */}
168
+ <div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
169
+ <div className="flex items-center gap-1.5">
170
+ <input
171
+ className="w-1/2 min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none"
172
+ onChange={(e) => {
173
+ setSearch(e.target.value)
174
+ onSearchChange?.(e.target.value)
175
+ }}
176
+ placeholder="Search..."
177
+ type="text"
178
+ value={search}
179
+ />
180
+ <div className="flex w-1/2 min-w-0 shrink-0 rounded-lg bg-muted p-0.5">
181
+ {SOURCE_CHIPS.map((chip) => {
182
+ const isActive = activeSource === chip.id
183
+ return (
184
+ <button
185
+ className={cn(
186
+ 'min-w-0 flex-1 truncate rounded-md px-1 py-1 text-center font-medium text-[10px] transition-colors',
187
+ isActive
188
+ ? 'bg-background text-foreground shadow-sm'
189
+ : 'text-muted-foreground hover:text-foreground',
190
+ )}
191
+ key={chip.id}
192
+ onClick={() => setActiveSource(isActive ? null : chip.id)}
193
+ type="button"
194
+ >
195
+ {chip.label}
196
+ </button>
197
+ )
198
+ })}
199
+ </div>
200
+ </div>
201
+
202
+ {/* Child nodes of the active root as a secondary chip row */}
203
+ {!search && activeRoot && activeRoot.children.length > 0 && (
204
+ <div className="flex flex-wrap gap-1">
205
+ <button
206
+ className={cn(
207
+ 'cursor-pointer rounded-md px-2 py-0.5 font-medium text-xs transition-colors',
208
+ activeChildSlug === null
209
+ ? 'bg-violet-500 text-white'
210
+ : 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
211
+ )}
212
+ onClick={() => setActiveChildSlug(null)}
213
+ type="button"
214
+ >
215
+ All
216
+ </button>
217
+ {activeRoot.children.map((child) => {
218
+ const isActive = activeChildSlug === child.slug
219
+ return (
220
+ <button
221
+ className={cn(
222
+ 'cursor-pointer rounded-md px-2 py-0.5 font-medium text-xs capitalize transition-colors',
223
+ isActive
224
+ ? 'bg-violet-500 text-white'
225
+ : 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
226
+ )}
227
+ key={child.slug}
228
+ onClick={() => setActiveChildSlug(isActive ? null : child.slug)}
229
+ type="button"
230
+ >
231
+ {child.name}
232
+ </button>
233
+ )
234
+ })}
235
+ </div>
236
+ )}
237
+ </div>
238
+
239
+ {/* Item grid */}
240
+ <div className="min-h-0 flex-1 overflow-y-auto p-3">
241
+ {isSearchPending ? (
242
+ <div className="flex h-full items-center justify-center">
243
+ <div className="size-5 animate-spin rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground" />
244
+ </div>
245
+ ) : isServerSearch && search && searchResults?.length === 0 ? (
246
+ (emptyState ?? (
247
+ <div className="flex h-full items-center justify-center text-muted-foreground text-xs">
248
+ No results for &ldquo;{search}&rdquo;
249
+ </div>
250
+ ))
251
+ ) : (
252
+ <ItemCatalog
253
+ category={'furnish' as never}
254
+ emptyState={emptyState}
255
+ key={activeNode?.slug ?? 'all'}
256
+ leadingTile={leadingTile}
257
+ overrideItems={isServerSearch && search ? (searchItems ?? undefined) : treeItems}
258
+ />
259
+ )}
260
+ </div>
261
+ </div>
262
+ )
263
+ }
@@ -3,12 +3,14 @@
3
3
  import type { AssetInput } from '@pascal-app/core'
4
4
  import NextImage from 'next/image'
5
5
  import { useEffect, useState } from 'react'
6
+ import { triggerSFX } from '../../../../../lib/sfx-bus'
6
7
  import { cn } from '../../../../../lib/utils'
7
8
  import type { CatalogCategory } from '../../../../../store/use-editor'
8
9
  import useEditor from '../../../../../store/use-editor'
9
10
  import { furnishTools } from '../../../action-menu/furnish-tools'
10
11
  import { CATALOG_ITEMS } from '../../../item-catalog/catalog-items'
11
12
  import { ItemCatalog } from '../../../item-catalog/item-catalog'
13
+ import { type FunctionTreeNode, FunctionTreePanel } from './function-tree-panel'
12
14
 
13
15
  const PLACEMENT_TAGS = new Set(['floor', 'wall', 'ceiling', 'countertop'])
14
16
 
@@ -18,6 +20,9 @@ export function ItemsPanel({
18
20
  searchResults,
19
21
  leadingTile,
20
22
  emptyState,
23
+ functionTree,
24
+ showSourceFilter = true,
25
+ showTagFilters = true,
21
26
  }: {
22
27
  items?: AssetInput[]
23
28
  /** Called when the search query changes (community edition uses this for server-side search) */
@@ -34,6 +39,64 @@ export function ItemsPanel({
34
39
  * or no search results). Replaces the default "No results" message.
35
40
  */
36
41
  emptyState?: React.ReactNode
42
+ /**
43
+ * DB-driven function taxonomy. When provided, the panel renders the
44
+ * hierarchical tree browse instead of the legacy hardcoded category tabs.
45
+ */
46
+ functionTree?: FunctionTreeNode[]
47
+ /**
48
+ * Library/Community/Mine source chips. The open-source editor has no
49
+ * uploaded items (only the built-in catalog), so it hides these.
50
+ */
51
+ showSourceFilter?: boolean
52
+ /**
53
+ * Placement/functional tag filter chips under the search row. The
54
+ * open-source editor hides these to keep the panel to plain categories.
55
+ */
56
+ showTagFilters?: boolean
57
+ }) {
58
+ // When the embedder supplies a function taxonomy, the hierarchical browse
59
+ // replaces the legacy `furnishTools` category tabs entirely.
60
+ if (functionTree && functionTree.length > 0) {
61
+ return (
62
+ <FunctionTreePanel
63
+ emptyState={emptyState}
64
+ functionTree={functionTree}
65
+ items={items}
66
+ leadingTile={leadingTile}
67
+ onSearchChange={onSearchChange}
68
+ searchResults={searchResults}
69
+ />
70
+ )
71
+ }
72
+
73
+ return <LegacyItemsPanel
74
+ emptyState={emptyState}
75
+ items={items}
76
+ leadingTile={leadingTile}
77
+ onSearchChange={onSearchChange}
78
+ searchResults={searchResults}
79
+ showSourceFilter={showSourceFilter}
80
+ showTagFilters={showTagFilters}
81
+ />
82
+ }
83
+
84
+ function LegacyItemsPanel({
85
+ items,
86
+ onSearchChange,
87
+ searchResults,
88
+ leadingTile,
89
+ emptyState,
90
+ showSourceFilter = true,
91
+ showTagFilters = true,
92
+ }: {
93
+ items?: AssetInput[]
94
+ onSearchChange?: (query: string) => void
95
+ searchResults?: AssetInput[] | null
96
+ leadingTile?: React.ReactNode
97
+ emptyState?: React.ReactNode
98
+ showSourceFilter?: boolean
99
+ showTagFilters?: boolean
37
100
  }) {
38
101
  const mode = useEditor((s) => s.mode)
39
102
  const catalogCategory = useEditor((s) => s.catalogCategory)
@@ -45,8 +108,11 @@ export function ItemsPanel({
45
108
  const [activeFunctionalTag, setActiveFunctionalTag] = useState<string | null>(null)
46
109
  // Library / Community / Mine. Default to Library so first-time users see
47
110
  // the curated catalog rather than every uploaded item; clicking the chip
48
- // again clears the filter (`null` = show everything).
49
- const [activeSource, setActiveSource] = useState<AssetInput['source'] | null>('library')
111
+ // again clears the filter (`null` = show everything). With the chips hidden
112
+ // there is nothing to filter by, so start unfiltered.
113
+ const [activeSource, setActiveSource] = useState<AssetInput['source'] | null>(
114
+ showSourceFilter ? 'library' : null,
115
+ )
50
116
  const [search, setSearch] = useState('')
51
117
  const isServerSearch = onSearchChange !== undefined
52
118
  // True when server search is active but results haven't come back yet
@@ -108,7 +174,7 @@ export function ItemsPanel({
108
174
  const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? [])))
109
175
  const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t))
110
176
  const functionalTags = allTags.filter((t) => !PLACEMENT_TAGS.has(t))
111
- const hasFilters = allTags.length > 1
177
+ const hasFilters = showTagFilters && allTags.length > 1
112
178
 
113
179
  const placementCount = (tag: string | null) =>
114
180
  categoryItems.filter((item) => {
@@ -129,7 +195,7 @@ export function ItemsPanel({
129
195
  return (
130
196
  <div className="flex h-full flex-col">
131
197
  {/* Category tabs */}
132
- <div className="flex shrink-0 gap-1 overflow-x-auto border-border/70 border-b p-2">
198
+ <div className="flex shrink-0 flex-wrap gap-1 border-border/70 border-b p-2">
133
199
  {furnishTools.map((cat) => {
134
200
  const isActive = activeCategory.catalogCategory === cat.catalogCategory
135
201
  return (
@@ -141,7 +207,11 @@ export function ItemsPanel({
141
207
  : 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground',
142
208
  )}
143
209
  key={cat.catalogCategory}
144
- onClick={() => selectCategory(cat.catalogCategory)}
210
+ onClick={() => {
211
+ triggerSFX('sfx:menu-click')
212
+ selectCategory(cat.catalogCategory)
213
+ }}
214
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
145
215
  type="button"
146
216
  >
147
217
  <NextImage
@@ -161,9 +231,13 @@ export function ItemsPanel({
161
231
  <div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
162
232
  <div className="flex items-center gap-1.5">
163
233
  {/* Search and source filter take 50/50 of the row. `min-w-0` on
164
- both sides lets each half shrink to fit when the panel narrows. */}
234
+ both sides lets each half shrink to fit when the panel narrows.
235
+ With the source chips hidden, search spans the full row. */}
165
236
  <input
166
- className="w-1/2 min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none"
237
+ className={cn(
238
+ 'min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none',
239
+ showSourceFilter ? 'w-1/2' : 'w-full',
240
+ )}
167
241
  onChange={(e) => {
168
242
  setSearch(e.target.value)
169
243
  onSearchChange?.(e.target.value)
@@ -172,7 +246,7 @@ export function ItemsPanel({
172
246
  type="text"
173
247
  value={search}
174
248
  />
175
- {sourceChips.length > 0 && (
249
+ {showSourceFilter && sourceChips.length > 0 && (
176
250
  <div className="flex w-1/2 min-w-0 shrink-0 rounded-lg bg-muted p-0.5">
177
251
  {sourceChips.map((chip) => {
178
252
  const isActive = activeSource === chip.id