@pascal-app/editor 0.7.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 (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  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 +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  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 +5837 -12006
  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 +234 -190
  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 +435 -158
  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 +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  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-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. 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
  }