@pascal-app/editor 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5829 -12005
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +178 -149
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +433 -156
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,17 +1,68 @@
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
+
21
+ function clamp(value: number, min: number, max: number): number {
22
+ return Math.min(Math.max(value, min), Math.max(min, max))
23
+ }
24
+
25
+ /**
26
+ * Bounds the panel is allowed to occupy — the viewer column (tagged with
27
+ * `data-viewer-bounds`) so it can't slide under the sidebar or top bar.
28
+ * Falls back to the viewport when the marker isn't found.
29
+ */
30
+ function getDragBounds(el: HTMLElement | null): {
31
+ left: number
32
+ top: number
33
+ right: number
34
+ bottom: number
35
+ } {
36
+ const region = el?.closest('[data-viewer-bounds]')
37
+ const rect = region?.getBoundingClientRect()
38
+ if (!rect) {
39
+ return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }
40
+ }
41
+ return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
42
+ }
43
+
44
+ /**
45
+ * Host-supplied inspector footer (e.g. community's "Save as preset"). The
46
+ * `PanelManager` provides it so every panel — including kind-owned
47
+ * `customPanel`s that render their own `<PanelWrapper>` without threading a
48
+ * `footer` prop — picks it up without per-kind wiring. An explicit `footer`
49
+ * prop still wins over the context.
50
+ */
51
+ export const InspectorFooterContext = createContext<React.ReactNode>(null)
52
+
8
53
  interface PanelWrapperProps {
9
54
  title: string
10
- icon?: string
55
+ /** Either a URL path (legacy panels pass `/icons/floor.png` etc.,
56
+ * rendered via next/image) OR a React node (registry-driven
57
+ * inspector renders `<Icon icon="lucide:fence" />` from
58
+ * `def.presentation.icon`). */
59
+ icon?: string | React.ReactNode
11
60
  onClose?: () => void
12
61
  onReset?: () => void
13
62
  onBack?: () => void
14
63
  children: React.ReactNode
64
+ /** Pinned below the scrollable body, inside the panel card. */
65
+ footer?: React.ReactNode
15
66
  className?: string
16
67
  width?: number | string
17
68
  }
@@ -23,25 +74,150 @@ export function PanelWrapper({
23
74
  onReset,
24
75
  onBack,
25
76
  children,
77
+ footer,
26
78
  className,
27
79
  width = 320, // default width
28
80
  }: PanelWrapperProps) {
29
81
  const isMobile = useIsMobile()
82
+ const contextFooter = useContext(InspectorFooterContext)
83
+ const resolvedFooter = footer ?? contextFooter
84
+
85
+ const panelRef = useRef<HTMLDivElement>(null)
86
+
87
+ // The whole panel is collapsed to just its header by default; the chevron
88
+ // expands it to reveal the inspector body.
89
+ const [collapsed, setCollapsed] = useState(true)
90
+
91
+ // Drag-to-reposition from the header. `offset` is a translation applied on
92
+ // top of the default `top-20 right-4` anchor; null until first dragged.
93
+ // Dragging is clamped so no edge of the panel leaves the viewport.
94
+ const [offset, setOffset] = useState<{ x: number; y: number } | null>(null)
95
+ const [isDragging, setIsDragging] = useState(false)
96
+ const dragRef = useRef<{
97
+ startX: number
98
+ startY: number
99
+ baseX: number
100
+ baseY: number
101
+ rectLeft: number
102
+ rectTop: number
103
+ width: number
104
+ height: number
105
+ minLeft: number
106
+ maxLeft: number
107
+ minTop: number
108
+ maxTop: number
109
+ moved: boolean
110
+ } | null>(null)
111
+
112
+ const handleHeaderPointerDown = useCallback(
113
+ (e: React.PointerEvent<HTMLDivElement>) => {
114
+ // Buttons (close / reset / collapse) handle their own clicks.
115
+ if ((e.target as HTMLElement).closest('button')) return
116
+ const rect = panelRef.current?.getBoundingClientRect()
117
+ if (!rect) return
118
+ const bounds = getDragBounds(panelRef.current)
119
+ const base = offset ?? { x: 0, y: 0 }
120
+ dragRef.current = {
121
+ startX: e.clientX,
122
+ startY: e.clientY,
123
+ baseX: base.x,
124
+ baseY: base.y,
125
+ rectLeft: rect.left,
126
+ rectTop: rect.top,
127
+ width: rect.width,
128
+ height: rect.height,
129
+ minLeft: bounds.left + DRAG_MARGIN,
130
+ maxLeft: bounds.right - rect.width - DRAG_MARGIN,
131
+ minTop: bounds.top + DRAG_MARGIN,
132
+ maxTop: bounds.bottom - rect.height - DRAG_MARGIN,
133
+ moved: false,
134
+ }
135
+ setIsDragging(true)
136
+ e.currentTarget.setPointerCapture(e.pointerId)
137
+ },
138
+ [offset],
139
+ )
140
+
141
+ const handleHeaderPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
142
+ const drag = dragRef.current
143
+ if (!drag) return
144
+ const dx = e.clientX - drag.startX
145
+ const dy = e.clientY - drag.startY
146
+ // Hold position until the press clearly becomes a drag, so a click can
147
+ // still toggle collapse.
148
+ if (!drag.moved && Math.hypot(dx, dy) <= CLICK_SLOP) return
149
+ drag.moved = true
150
+ const left = clamp(drag.rectLeft + dx, drag.minLeft, drag.maxLeft)
151
+ const top = clamp(drag.rectTop + dy, drag.minTop, drag.maxTop)
152
+ setOffset({ x: drag.baseX + (left - drag.rectLeft), y: drag.baseY + (top - drag.rectTop) })
153
+ }, [])
154
+
155
+ const handleHeaderPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
156
+ const drag = dragRef.current
157
+ if (!drag) return
158
+ dragRef.current = null
159
+ setIsDragging(false)
160
+ e.currentTarget.releasePointerCapture(e.pointerId)
161
+ // A press that never turned into a drag is a click → toggle collapse.
162
+ if (!drag.moved) setCollapsed((c) => !c)
163
+ }, [])
164
+
165
+ // Expanding can grow the panel past an edge if it was dragged there while
166
+ // collapsed — nudge it back inside the viewer bounds.
167
+ useLayoutEffect(() => {
168
+ if (isMobile || collapsed) return
169
+ const el = panelRef.current
170
+ if (!el) return
171
+ const rect = el.getBoundingClientRect()
172
+ const bounds = getDragBounds(el)
173
+ const left = clamp(rect.left, bounds.left + DRAG_MARGIN, bounds.right - rect.width - DRAG_MARGIN)
174
+ const top = clamp(rect.top, bounds.top + DRAG_MARGIN, bounds.bottom - rect.height - DRAG_MARGIN)
175
+ const dx = left - rect.left
176
+ const dy = top - rect.top
177
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
178
+ setOffset((prev) => ({ x: (prev?.x ?? 0) + dx, y: (prev?.y ?? 0) + dy }))
179
+ }
180
+ }, [collapsed, isMobile])
30
181
 
31
182
  return (
32
183
  <div
33
184
  className={cn(
34
185
  isMobile
35
186
  ? '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',
187
+ // Cap height at `100dvh - 154px` so a tall panel's bottom edge
188
+ // aligns flush with the top of the floating bottom action bar.
189
+ // Combined with `top-20` (80px), the panel's bottom sits at
190
+ // `100dvh - 74px` — just clearing the bar without leaving a
191
+ // visible gap. The inner `flex-1 overflow-y-auto` content area
192
+ // (below) handles vertical scrolling when content exceeds the
193
+ // cap.
194
+ : '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
195
  className,
38
196
  )}
39
- style={isMobile ? undefined : { width }}
197
+ ref={panelRef}
198
+ style={
199
+ isMobile
200
+ ? undefined
201
+ : {
202
+ width,
203
+ transform: offset ? `translate(${offset.x}px, ${offset.y}px)` : undefined,
204
+ }
205
+ }
40
206
  >
41
- {/* Header — desktop only; mobile sheet provides its own header */}
207
+ {/* Header — desktop only; mobile sheet provides its own header. Doubles
208
+ as the drag handle (grip in the middle) for repositioning the panel. */}
42
209
  {!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">
210
+ <div
211
+ className={cn(
212
+ 'relative flex select-none items-center justify-between px-3 py-3',
213
+ !collapsed && 'border-border/50 border-b',
214
+ isDragging ? 'cursor-grabbing' : 'cursor-grab',
215
+ )}
216
+ onPointerDown={handleHeaderPointerDown}
217
+ onPointerMove={handleHeaderPointerMove}
218
+ onPointerUp={handleHeaderPointerUp}
219
+ >
220
+ <div className="flex min-w-0 items-center gap-2">
45
221
  {onBack && (
46
222
  <button
47
223
  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 +227,26 @@ export function PanelWrapper({
51
227
  <ChevronLeft className="h-4 w-4" />
52
228
  </button>
53
229
  )}
54
- {icon && (
55
- <Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
56
- )}
230
+ {icon &&
231
+ (typeof icon === 'string' ? (
232
+ <Image
233
+ alt=""
234
+ className="shrink-0 object-contain"
235
+ height={16}
236
+ src={icon}
237
+ width={16}
238
+ />
239
+ ) : (
240
+ <span className="flex shrink-0 items-center justify-center">{icon}</span>
241
+ ))}
57
242
  <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
58
243
  {title}
59
244
  </h2>
60
245
  </div>
61
246
 
247
+ {/* Centered grip — purely a visual drag affordance. */}
248
+ <GripHorizontal className="-translate-x-1/2 pointer-events-none absolute left-1/2 h-4 w-4 text-muted-foreground/40" />
249
+
62
250
  <div className="flex items-center gap-1">
63
251
  {onReset && (
64
252
  <button
@@ -69,6 +257,17 @@ export function PanelWrapper({
69
257
  <RotateCcw className="h-4 w-4" />
70
258
  </button>
71
259
  )}
260
+ <button
261
+ aria-expanded={!collapsed}
262
+ aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
263
+ 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"
264
+ onClick={() => setCollapsed((c) => !c)}
265
+ type="button"
266
+ >
267
+ <ChevronDown
268
+ className={cn('h-4 w-4 transition-transform', collapsed ? '' : 'rotate-180')}
269
+ />
270
+ </button>
72
271
  {onClose && (
73
272
  <button
74
273
  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 +281,14 @@ export function PanelWrapper({
82
281
  </div>
83
282
  )}
84
283
 
85
- {/* Content */}
86
- <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
284
+ {/* Content — hidden while the panel is collapsed (desktop). */}
285
+ {!(collapsed && !isMobile) && (
286
+ <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
287
+ )}
288
+
289
+ {resolvedFooter && !(collapsed && !isMobile) && (
290
+ <div className="shrink-0 border-border/50 border-t p-3">{resolvedFooter}</div>
291
+ )}
87
292
  </div>
88
293
  )
89
294
  }