@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
@@ -6,25 +6,26 @@ export type NodeDisplay = {
6
6
  }
7
7
 
8
8
  const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
9
- item: { icon: '/icons/furniture.png', label: 'Item' },
10
- wall: { icon: '/icons/wall.png', label: 'Wall' },
11
- door: { icon: '/icons/door.png', label: 'Door' },
12
- window: { icon: '/icons/window.png', label: 'Window' },
13
- slab: { icon: '/icons/floor.png', label: 'Slab' },
14
- ceiling: { icon: '/icons/ceiling.png', label: 'Ceiling' },
15
- column: { icon: '/icons/column.png', label: 'Column' },
16
- fence: { icon: '/icons/fence.png', label: 'Fence' },
17
- roof: { icon: '/icons/roof.png', label: 'Roof' },
18
- 'roof-segment': { icon: '/icons/roof.png', label: 'Roof segment' },
19
- stair: { icon: '/icons/stair.png', label: 'Stair' },
20
- 'stair-segment': { icon: '/icons/stair.png', label: 'Stair segment' },
21
- scan: { icon: '/icons/mesh.png', label: '3D Scan' },
22
- guide: { icon: '/icons/floorplan.png', label: 'Guide image' },
9
+ item: { icon: '/icons/item.webp', label: 'Item' },
10
+ wall: { icon: '/icons/wall.webp', label: 'Wall' },
11
+ door: { icon: '/icons/door.webp', label: 'Door' },
12
+ window: { icon: '/icons/window.webp', label: 'Window' },
13
+ slab: { icon: '/icons/floor.webp', label: 'Slab' },
14
+ ceiling: { icon: '/icons/ceiling.webp', label: 'Ceiling' },
15
+ column: { icon: '/icons/column.webp', label: 'Column' },
16
+ elevator: { icon: '/icons/elevator.webp', label: 'Elevator' },
17
+ fence: { icon: '/icons/fence.webp', label: 'Fence' },
18
+ roof: { icon: '/icons/roof.webp', label: 'Roof' },
19
+ 'roof-segment': { icon: '/icons/roof.webp', label: 'Roof segment' },
20
+ stair: { icon: '/icons/stairs.webp', label: 'Stair' },
21
+ 'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
22
+ scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
23
+ guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
23
24
  }
24
25
 
25
26
  export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
26
- if (!node) return { icon: '/icons/select.png', label: 'Selection' }
27
- const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.png', label: node.type }
27
+ if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
28
+ const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
28
29
  // Item nodes carry an asset with its own thumbnail/name
29
30
  if (node.type === 'item') {
30
31
  return {
@@ -5,8 +5,11 @@ import {
5
5
  type AnyNodeId,
6
6
  type BuildingNode,
7
7
  type CeilingNode,
8
+ type ChimneyNode,
8
9
  type ColumnNode,
9
10
  type DoorNode,
11
+ type DormerNode,
12
+ type ElevatorNode,
10
13
  type FenceNode,
11
14
  type ItemNode,
12
15
  type RoofNode,
@@ -23,31 +26,22 @@ import { useCallback, useEffect, useState } from 'react'
23
26
  import { useIsMobile } from '../../../hooks/use-mobile'
24
27
  import { sfxEmitter } from '../../../lib/sfx-bus'
25
28
  import useEditor from '../../../store/use-editor'
26
- import { CeilingPanel } from './ceiling-panel'
27
- import { ColumnPanel } from './column-panel'
28
- import { DoorPanel } from './door-panel'
29
- import { FencePanel } from './fence-panel'
30
- import { ItemPanel } from './item-panel'
31
29
  import { MobilePanelSheet } from './mobile-panel-sheet'
32
30
  import { MobileSelectionBar } from './mobile-selection-bar'
33
31
  import { getNodeDisplay } from './node-display'
34
- import { PaintPanel } from './paint-panel'
32
+ import { resetDesktopInspectorCollapsed } from './panel-wrapper'
33
+ import { ParametricInspector } from './parametric-inspector'
35
34
  import { ReferencePanel } from './reference-panel'
36
- import { RoofPanel } from './roof-panel'
37
- import { RoofSegmentPanel } from './roof-segment-panel'
38
- import { SlabPanel } from './slab-panel'
39
- import { SpawnPanel } from './spawn-panel'
40
- import { StairPanel } from './stair-panel'
41
- import { StairSegmentPanel } from './stair-segment-panel'
42
- import { WallPanel } from './wall-panel'
43
- import { WindowPanel } from './window-panel'
44
35
 
45
36
  type MovableNode =
46
37
  | ItemNode
47
38
  | WindowNode
48
39
  | DoorNode
40
+ | ElevatorNode
49
41
  | CeilingNode
42
+ | ChimneyNode
50
43
  | ColumnNode
44
+ | DormerNode
51
45
  | SlabNode
52
46
  | WallNode
53
47
  | FenceNode
@@ -61,8 +55,11 @@ const MOVABLE_TYPES = new Set<string>([
61
55
  'item',
62
56
  'window',
63
57
  'door',
58
+ 'elevator',
64
59
  'ceiling',
60
+ 'chimney',
65
61
  'column',
62
+ 'dormer',
66
63
  'slab',
67
64
  'wall',
68
65
  'fence',
@@ -77,38 +74,17 @@ function isMovableNode(node: AnyNode | null): node is MovableNode {
77
74
  return !!node && MOVABLE_TYPES.has(node.type)
78
75
  }
79
76
 
80
- function panelForType(type: string | null) {
77
+ function panelForType(type: string | null, footer?: React.ReactNode) {
81
78
  if (!type) return null
82
- switch (type) {
83
- case 'item':
84
- return <ItemPanel />
85
- case 'roof':
86
- return <RoofPanel />
87
- case 'roof-segment':
88
- return <RoofSegmentPanel />
89
- case 'stair':
90
- return <StairPanel />
91
- case 'stair-segment':
92
- return <StairSegmentPanel />
93
- case 'slab':
94
- return <SlabPanel />
95
- case 'spawn':
96
- return <SpawnPanel />
97
- case 'ceiling':
98
- return <CeilingPanel />
99
- case 'column':
100
- return <ColumnPanel />
101
- case 'wall':
102
- return <WallPanel />
103
- case 'fence':
104
- return <FencePanel />
105
- case 'door':
106
- return <DoorPanel />
107
- case 'window':
108
- return <WindowPanel />
109
- default:
110
- return null
111
- }
79
+ // Every kind now renders through `<ParametricInspector>`, which either
80
+ // composes auto-derived editors from `parametrics.groups` or lazy-
81
+ // loads the kind-owned panel via `parametrics.customPanel`. The
82
+ // hardcoded switch is gone — all per-kind panel layout lives in
83
+ // `nodes/src/<kind>/panel.tsx`. The `type` arg is preserved for
84
+ // future cases where we might want a non-registry fallback (e.g.
85
+ // reference scale, paint mode); leave the function shape intact.
86
+ void type
87
+ return <ParametricInspector footer={footer} />
112
88
  }
113
89
 
114
90
  function MobilePanelLayer({
@@ -192,13 +168,12 @@ function MobilePanelLayer({
192
168
  )
193
169
  }
194
170
 
195
- export function PanelManager() {
171
+ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.ReactNode }) {
196
172
  const isMobile = useIsMobile()
197
173
  const selectedIds = useViewer((s) => s.selection.selectedIds)
174
+ const selectedZoneId = useViewer((s) => s.selection.zoneId)
175
+ const setSelection = useViewer((s) => s.setSelection)
198
176
  const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
199
- const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
200
- const mode = useEditor((s) => s.mode)
201
- const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
202
177
  // Only subscribe to the *type* of the single-selected node — string primitive
203
178
  // so we don't re-render on unrelated scene mutations.
204
179
  const selectedNodeType = useScene((s) => {
@@ -212,6 +187,27 @@ export function PanelManager() {
212
187
  return id ? (s.nodes[id as AnyNodeId] ?? null) : null
213
188
  })
214
189
 
190
+ // Node and reference selection are mutually exclusive: selecting a guide
191
+ // clears the node selection (handleGuideSelect), but node selection never
192
+ // cleared a lingering reference — so clicking a wall with a floorplan
193
+ // selected kept showing the reference panel. Clear the stale reference the
194
+ // moment a scene selection appears.
195
+ const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
196
+ useEffect(() => {
197
+ if (selectedIds.length > 0 || selectedZoneId) {
198
+ setSelectedReferenceId(null)
199
+ }
200
+ }, [selectedIds, selectedZoneId, setSelectedReferenceId])
201
+
202
+ // The inspector's expanded state is shared across panel swaps, but a fresh
203
+ // selection after everything was deselected should open collapsed again.
204
+ const hasAnySelection = selectedIds.length > 0 || Boolean(selectedZoneId) || Boolean(selectedReferenceId)
205
+ useEffect(() => {
206
+ if (!hasAnySelection) {
207
+ resetDesktopInspectorCollapsed()
208
+ }
209
+ }, [hasAnySelection])
210
+
215
211
  if (isMobile) {
216
212
  if (selectedReferenceId) {
217
213
  return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
@@ -230,15 +226,16 @@ export function PanelManager() {
230
226
  return <ReferencePanel />
231
227
  }
232
228
 
233
- if (
234
- isPaintPanelOpen &&
235
- mode === 'material-paint' &&
236
- activePaintMaterial?.material?.properties &&
237
- !activePaintMaterial.materialPreset
238
- ) {
239
- return <PaintPanel />
229
+ if (selectedZoneId && selectedIds.length === 0) {
230
+ return (
231
+ <ParametricInspector
232
+ footer={inspectorFooter}
233
+ key={selectedZoneId}
234
+ nodeId={selectedZoneId as AnyNodeId}
235
+ onClose={() => setSelection({ zoneId: null })}
236
+ />
237
+ )
240
238
  }
241
239
 
242
- // Show appropriate panel based on selected node type
243
- return panelForType(selectedNodeType)
240
+ return panelForType(selectedNodeType, inspectorFooter)
244
241
  }
@@ -1,17 +1,76 @@
1
1
  'use client'
2
2
 
3
- import { ChevronLeft, RotateCcw, X } from 'lucide-react'
3
+ import { ChevronDown, ChevronLeft, GripHorizontal, RotateCcw, X } from 'lucide-react'
4
4
  import Image from 'next/image'
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext,
9
+ useLayoutEffect,
10
+ useRef,
11
+ useState,
12
+ } from 'react'
5
13
  import { useIsMobile } from '../../../hooks/use-mobile'
6
14
  import { cn } from '../../../lib/utils'
7
15
 
16
+ const DRAG_MARGIN = 8
17
+ // Pointer travel (px) below which a header press is treated as a click
18
+ // (toggles collapse) rather than a drag.
19
+ const CLICK_SLOP = 4
20
+ let desktopInspectorCollapsed = true
21
+
22
+ /** Forget the shared expanded state. Called when the last selection clears so
23
+ * a fresh selection opens the inspector collapsed — the sharing is only meant
24
+ * to survive swaps between panels (roof ↔ segment), not a close/reopen. */
25
+ export function resetDesktopInspectorCollapsed() {
26
+ desktopInspectorCollapsed = true
27
+ }
28
+
29
+ function clamp(value: number, min: number, max: number): number {
30
+ return Math.min(Math.max(value, min), Math.max(min, max))
31
+ }
32
+
33
+ /**
34
+ * Bounds the panel is allowed to occupy — the viewer column (tagged with
35
+ * `data-viewer-bounds`) so it can't slide under the sidebar or top bar.
36
+ * Falls back to the viewport when the marker isn't found.
37
+ */
38
+ function getDragBounds(el: HTMLElement | null): {
39
+ left: number
40
+ top: number
41
+ right: number
42
+ bottom: number
43
+ } {
44
+ const region = el?.closest('[data-viewer-bounds]')
45
+ const rect = region?.getBoundingClientRect()
46
+ if (!rect) {
47
+ return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }
48
+ }
49
+ return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
50
+ }
51
+
52
+ /**
53
+ * Host-supplied inspector footer (e.g. community's "Save as preset"). The
54
+ * `PanelManager` provides it so every panel — including kind-owned
55
+ * `customPanel`s that render their own `<PanelWrapper>` without threading a
56
+ * `footer` prop — picks it up without per-kind wiring. An explicit `footer`
57
+ * prop still wins over the context.
58
+ */
59
+ export const InspectorFooterContext = createContext<React.ReactNode>(null)
60
+
8
61
  interface PanelWrapperProps {
9
62
  title: string
10
- icon?: string
63
+ /** Either a URL path (legacy panels pass `/icons/floor.webp` etc.,
64
+ * rendered via next/image) OR a React node (registry-driven
65
+ * inspector renders `<Icon icon="lucide:fence" />` from
66
+ * `def.presentation.icon`). */
67
+ icon?: string | React.ReactNode
11
68
  onClose?: () => void
12
69
  onReset?: () => void
13
70
  onBack?: () => void
14
71
  children: React.ReactNode
72
+ /** Pinned below the scrollable body, inside the panel card. */
73
+ footer?: React.ReactNode
15
74
  className?: string
16
75
  width?: number | string
17
76
  }
@@ -23,25 +82,163 @@ export function PanelWrapper({
23
82
  onReset,
24
83
  onBack,
25
84
  children,
85
+ footer,
26
86
  className,
27
87
  width = 320, // default width
28
88
  }: PanelWrapperProps) {
29
89
  const isMobile = useIsMobile()
90
+ const contextFooter = useContext(InspectorFooterContext)
91
+ const resolvedFooter = footer ?? contextFooter
92
+
93
+ const panelRef = useRef<HTMLDivElement>(null)
94
+
95
+ // The whole panel is collapsed to just its header by default; the chevron
96
+ // expands it to reveal the inspector body. Keep the desktop value shared
97
+ // across inspector swaps (roof ↔ segment, etc.) so navigating between
98
+ // related panels preserves whether the user left the inspector open.
99
+ const [collapsed, setCollapsedState] = useState(desktopInspectorCollapsed)
100
+
101
+ const setCollapsed = useCallback(
102
+ (next: boolean | ((previous: boolean) => boolean)) => {
103
+ setCollapsedState((previous) => {
104
+ const resolved = typeof next === 'function' ? next(previous) : next
105
+ desktopInspectorCollapsed = resolved
106
+ return resolved
107
+ })
108
+ },
109
+ [],
110
+ )
111
+
112
+ // Drag-to-reposition from the header. `offset` is a translation applied on
113
+ // top of the default `top-20 right-4` anchor; null until first dragged.
114
+ // Dragging is clamped so no edge of the panel leaves the viewport.
115
+ const [offset, setOffset] = useState<{ x: number; y: number } | null>(null)
116
+ const [isDragging, setIsDragging] = useState(false)
117
+ const dragRef = useRef<{
118
+ startX: number
119
+ startY: number
120
+ baseX: number
121
+ baseY: number
122
+ rectLeft: number
123
+ rectTop: number
124
+ width: number
125
+ height: number
126
+ minLeft: number
127
+ maxLeft: number
128
+ minTop: number
129
+ maxTop: number
130
+ moved: boolean
131
+ } | null>(null)
132
+
133
+ const handleHeaderPointerDown = useCallback(
134
+ (e: React.PointerEvent<HTMLDivElement>) => {
135
+ // Buttons (close / reset / collapse) handle their own clicks.
136
+ if ((e.target as HTMLElement).closest('button')) return
137
+ const rect = panelRef.current?.getBoundingClientRect()
138
+ if (!rect) return
139
+ const bounds = getDragBounds(panelRef.current)
140
+ const base = offset ?? { x: 0, y: 0 }
141
+ dragRef.current = {
142
+ startX: e.clientX,
143
+ startY: e.clientY,
144
+ baseX: base.x,
145
+ baseY: base.y,
146
+ rectLeft: rect.left,
147
+ rectTop: rect.top,
148
+ width: rect.width,
149
+ height: rect.height,
150
+ minLeft: bounds.left + DRAG_MARGIN,
151
+ maxLeft: bounds.right - rect.width - DRAG_MARGIN,
152
+ minTop: bounds.top + DRAG_MARGIN,
153
+ maxTop: bounds.bottom - rect.height - DRAG_MARGIN,
154
+ moved: false,
155
+ }
156
+ setIsDragging(true)
157
+ e.currentTarget.setPointerCapture(e.pointerId)
158
+ },
159
+ [offset],
160
+ )
161
+
162
+ const handleHeaderPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
163
+ const drag = dragRef.current
164
+ if (!drag) return
165
+ const dx = e.clientX - drag.startX
166
+ const dy = e.clientY - drag.startY
167
+ // Hold position until the press clearly becomes a drag, so a click can
168
+ // still toggle collapse.
169
+ if (!drag.moved && Math.hypot(dx, dy) <= CLICK_SLOP) return
170
+ drag.moved = true
171
+ const left = clamp(drag.rectLeft + dx, drag.minLeft, drag.maxLeft)
172
+ const top = clamp(drag.rectTop + dy, drag.minTop, drag.maxTop)
173
+ setOffset({ x: drag.baseX + (left - drag.rectLeft), y: drag.baseY + (top - drag.rectTop) })
174
+ }, [])
175
+
176
+ const handleHeaderPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
177
+ const drag = dragRef.current
178
+ if (!drag) return
179
+ dragRef.current = null
180
+ setIsDragging(false)
181
+ e.currentTarget.releasePointerCapture(e.pointerId)
182
+ // A press that never turned into a drag is a click → toggle collapse.
183
+ if (!drag.moved) setCollapsed((c) => !c)
184
+ }, [])
185
+
186
+ // Expanding can grow the panel past an edge if it was dragged there while
187
+ // collapsed — nudge it back inside the viewer bounds.
188
+ useLayoutEffect(() => {
189
+ if (isMobile || collapsed) return
190
+ const el = panelRef.current
191
+ if (!el) return
192
+ const rect = el.getBoundingClientRect()
193
+ const bounds = getDragBounds(el)
194
+ const left = clamp(rect.left, bounds.left + DRAG_MARGIN, bounds.right - rect.width - DRAG_MARGIN)
195
+ const top = clamp(rect.top, bounds.top + DRAG_MARGIN, bounds.bottom - rect.height - DRAG_MARGIN)
196
+ const dx = left - rect.left
197
+ const dy = top - rect.top
198
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
199
+ setOffset((prev) => ({ x: (prev?.x ?? 0) + dx, y: (prev?.y ?? 0) + dy }))
200
+ }
201
+ }, [collapsed, isMobile])
30
202
 
31
203
  return (
32
204
  <div
33
205
  className={cn(
34
206
  isMobile
35
207
  ? 'flex h-full w-full flex-col overflow-hidden bg-transparent dark:text-foreground'
36
- : 'pointer-events-auto fixed top-20 right-4 z-50 flex max-h-[calc(100dvh-100px)] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground',
208
+ // Cap height at `100dvh - 154px` so a tall panel's bottom edge
209
+ // aligns flush with the top of the floating bottom action bar.
210
+ // Combined with `top-20` (80px), the panel's bottom sits at
211
+ // `100dvh - 74px` — just clearing the bar without leaving a
212
+ // visible gap. The inner `flex-1 overflow-y-auto` content area
213
+ // (below) handles vertical scrolling when content exceeds the
214
+ // cap.
215
+ : 'pointer-events-auto fixed top-20 right-4 z-50 flex max-h-[calc(100dvh-154px)] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground',
37
216
  className,
38
217
  )}
39
- style={isMobile ? undefined : { width }}
218
+ ref={panelRef}
219
+ style={
220
+ isMobile
221
+ ? undefined
222
+ : {
223
+ width,
224
+ transform: offset ? `translate(${offset.x}px, ${offset.y}px)` : undefined,
225
+ }
226
+ }
40
227
  >
41
- {/* Header — desktop only; mobile sheet provides its own header */}
228
+ {/* Header — desktop only; mobile sheet provides its own header. Doubles
229
+ as the drag handle (grip in the middle) for repositioning the panel. */}
42
230
  {!isMobile && (
43
- <div className="flex items-center justify-between border-border/50 border-b px-3 py-3">
44
- <div className="flex items-center gap-2">
231
+ <div
232
+ className={cn(
233
+ 'relative flex select-none items-center justify-between px-3 py-3',
234
+ !collapsed && 'border-border/50 border-b',
235
+ isDragging ? 'cursor-grabbing' : 'cursor-grab',
236
+ )}
237
+ onPointerDown={handleHeaderPointerDown}
238
+ onPointerMove={handleHeaderPointerMove}
239
+ onPointerUp={handleHeaderPointerUp}
240
+ >
241
+ <div className="flex min-w-0 items-center gap-2">
45
242
  {onBack && (
46
243
  <button
47
244
  className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
@@ -51,14 +248,26 @@ export function PanelWrapper({
51
248
  <ChevronLeft className="h-4 w-4" />
52
249
  </button>
53
250
  )}
54
- {icon && (
55
- <Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
56
- )}
251
+ {icon &&
252
+ (typeof icon === 'string' ? (
253
+ <Image
254
+ alt=""
255
+ className="shrink-0 object-contain"
256
+ height={16}
257
+ src={icon}
258
+ width={16}
259
+ />
260
+ ) : (
261
+ <span className="flex shrink-0 items-center justify-center">{icon}</span>
262
+ ))}
57
263
  <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
58
264
  {title}
59
265
  </h2>
60
266
  </div>
61
267
 
268
+ {/* Centered grip — purely a visual drag affordance. */}
269
+ <GripHorizontal className="-translate-x-1/2 pointer-events-none absolute left-1/2 h-4 w-4 text-muted-foreground/40" />
270
+
62
271
  <div className="flex items-center gap-1">
63
272
  {onReset && (
64
273
  <button
@@ -69,6 +278,17 @@ export function PanelWrapper({
69
278
  <RotateCcw className="h-4 w-4" />
70
279
  </button>
71
280
  )}
281
+ <button
282
+ aria-expanded={!collapsed}
283
+ aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
284
+ className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
285
+ onClick={() => setCollapsed((c) => !c)}
286
+ type="button"
287
+ >
288
+ <ChevronDown
289
+ className={cn('h-4 w-4 transition-transform', collapsed ? '' : 'rotate-180')}
290
+ />
291
+ </button>
72
292
  {onClose && (
73
293
  <button
74
294
  className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
@@ -82,8 +302,14 @@ export function PanelWrapper({
82
302
  </div>
83
303
  )}
84
304
 
85
- {/* Content */}
86
- <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
305
+ {/* Content — hidden while the panel is collapsed (desktop). */}
306
+ {!(collapsed && !isMobile) && (
307
+ <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
308
+ )}
309
+
310
+ {resolvedFooter && !(collapsed && !isMobile) && (
311
+ <div className="shrink-0 border-border/50 border-t p-3">{resolvedFooter}</div>
312
+ )}
87
313
  </div>
88
314
  )
89
315
  }