@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.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|