@classic-homes/theme-svelte 0.1.50 → 0.1.51
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/dist/lib/components/DataPanel.svelte +1399 -0
- package/dist/lib/components/DataPanel.svelte.d.ts +63 -0
- package/dist/lib/components/DataPanelContent.svelte +17 -0
- package/dist/lib/components/DataPanelContent.svelte.d.ts +10 -0
- package/dist/lib/components/DataPanelFooter.svelte +55 -0
- package/dist/lib/components/DataPanelFooter.svelte.d.ts +15 -0
- package/dist/lib/components/DataPanelHeader.svelte +220 -0
- package/dist/lib/components/DataPanelHeader.svelte.d.ts +37 -0
- package/dist/lib/components/DataPanelTab.svelte +72 -0
- package/dist/lib/components/DataPanelTab.svelte.d.ts +14 -0
- package/dist/lib/components/DateTimePicker.svelte +171 -52
- package/dist/lib/components/DateTimePicker.svelte.d.ts +2 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +1 -1
- package/dist/lib/composables/index.d.ts +17 -0
- package/dist/lib/composables/index.js +17 -1
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/schemas/common.d.ts +2 -2
- package/dist/lib/stores/dataPanel.svelte.d.ts +37 -0
- package/dist/lib/stores/dataPanel.svelte.js +182 -0
- package/dist/lib/utils/date.d.ts +29 -0
- package/dist/lib/utils/date.js +118 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1399 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
import type {
|
|
6
|
+
PanelEdge,
|
|
7
|
+
PanelMode,
|
|
8
|
+
PanelVariant,
|
|
9
|
+
CardConfig,
|
|
10
|
+
Position,
|
|
11
|
+
Size,
|
|
12
|
+
PanelConstraints,
|
|
13
|
+
DataPanelAction,
|
|
14
|
+
ResizeHandle,
|
|
15
|
+
} from '@classic-homes/data-panel-core';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_PANEL_STATE,
|
|
18
|
+
DEFAULT_CONSTRAINTS,
|
|
19
|
+
DEFAULT_CARD_CONFIG,
|
|
20
|
+
CARD_HEADER_HEIGHT,
|
|
21
|
+
CARD_EXPANDED_RATIO,
|
|
22
|
+
CARD_MIN_WIDTH,
|
|
23
|
+
loadPanelState,
|
|
24
|
+
savePanelState,
|
|
25
|
+
getPointerPosition,
|
|
26
|
+
calculateDragPosition,
|
|
27
|
+
calculateResize,
|
|
28
|
+
getResizeCursor,
|
|
29
|
+
getPinnedResizeHandle,
|
|
30
|
+
constrainSize,
|
|
31
|
+
constrainPosition,
|
|
32
|
+
constrainPinnedWidth,
|
|
33
|
+
constrainPinnedHeight,
|
|
34
|
+
detectEdgeSnap,
|
|
35
|
+
getPinnedPositionStyles,
|
|
36
|
+
getDetachedPositionStyles,
|
|
37
|
+
getInitialDetachedPosition,
|
|
38
|
+
isHorizontalEdge,
|
|
39
|
+
// Pinned drag-to-detach utilities
|
|
40
|
+
calculateDetachDistance,
|
|
41
|
+
calculatePullOffset,
|
|
42
|
+
calculateDetachedPositionFromPinned,
|
|
43
|
+
getPinnedPullTransform,
|
|
44
|
+
} from '@classic-homes/data-panel-core';
|
|
45
|
+
|
|
46
|
+
import DataPanelHeader from './DataPanelHeader.svelte';
|
|
47
|
+
import DataPanelContent from './DataPanelContent.svelte';
|
|
48
|
+
import DataPanelFooter from './DataPanelFooter.svelte';
|
|
49
|
+
import DataPanelTab from './DataPanelTab.svelte';
|
|
50
|
+
|
|
51
|
+
interface Props {
|
|
52
|
+
/** Panel visibility */
|
|
53
|
+
open?: boolean;
|
|
54
|
+
/** Callback when open state changes */
|
|
55
|
+
onOpenChange?: (open: boolean) => void;
|
|
56
|
+
/** Panel mode - pinned to edge or detached floating */
|
|
57
|
+
mode?: PanelMode;
|
|
58
|
+
/** Callback when mode changes */
|
|
59
|
+
onModeChange?: (mode: PanelMode) => void;
|
|
60
|
+
/** Panel variant - 'full' for traditional sidebar, 'card' for mobile bottom sheet */
|
|
61
|
+
variant?: PanelVariant;
|
|
62
|
+
/** Callback when variant changes */
|
|
63
|
+
onVariantChange?: (variant: PanelVariant) => void;
|
|
64
|
+
/** Edge to pin panel to (when mode is 'pinned') */
|
|
65
|
+
edge?: PanelEdge;
|
|
66
|
+
/** Callback when edge changes */
|
|
67
|
+
onEdgeChange?: (edge: PanelEdge) => void;
|
|
68
|
+
/** Whether panel is expanded or collapsed */
|
|
69
|
+
expanded?: boolean;
|
|
70
|
+
/** Callback when expanded state changes */
|
|
71
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
72
|
+
/** Panel title */
|
|
73
|
+
title?: string;
|
|
74
|
+
/** Panel subtitle */
|
|
75
|
+
subtitle?: string;
|
|
76
|
+
/** Content inside the panel */
|
|
77
|
+
children: Snippet;
|
|
78
|
+
/** Footer action buttons */
|
|
79
|
+
actions?: DataPanelAction[];
|
|
80
|
+
/** Size constraints */
|
|
81
|
+
constraints?: PanelConstraints;
|
|
82
|
+
/** Card mode configuration */
|
|
83
|
+
cardConfig?: CardConfig;
|
|
84
|
+
/** Disable close button */
|
|
85
|
+
disableClose?: boolean;
|
|
86
|
+
/** Disable resize functionality */
|
|
87
|
+
disableResize?: boolean;
|
|
88
|
+
/** Disable drag functionality (detached mode) */
|
|
89
|
+
disableDrag?: boolean;
|
|
90
|
+
/** Disable mode switching */
|
|
91
|
+
disableModeSwitch?: boolean;
|
|
92
|
+
/** localStorage key for state persistence */
|
|
93
|
+
persistKey?: string;
|
|
94
|
+
/** Distance in pixels for edge snap detection */
|
|
95
|
+
snapThreshold?: number;
|
|
96
|
+
/** Distance in pixels to drag before detaching from pinned mode (0 to disable) */
|
|
97
|
+
detachThreshold?: number;
|
|
98
|
+
/** Custom header content (replaces title/subtitle) */
|
|
99
|
+
header?: Snippet;
|
|
100
|
+
/** Custom header actions */
|
|
101
|
+
headerActions?: Snippet;
|
|
102
|
+
/** Custom footer content */
|
|
103
|
+
footer?: Snippet;
|
|
104
|
+
/** Custom footer actions */
|
|
105
|
+
footerActions?: Snippet;
|
|
106
|
+
/** Additional class */
|
|
107
|
+
class?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let {
|
|
111
|
+
open = $bindable(true),
|
|
112
|
+
onOpenChange,
|
|
113
|
+
mode: propMode,
|
|
114
|
+
onModeChange,
|
|
115
|
+
variant: propVariant,
|
|
116
|
+
onVariantChange,
|
|
117
|
+
edge: propEdge,
|
|
118
|
+
onEdgeChange,
|
|
119
|
+
expanded: propExpanded,
|
|
120
|
+
onExpandedChange,
|
|
121
|
+
title,
|
|
122
|
+
subtitle,
|
|
123
|
+
children,
|
|
124
|
+
actions = [],
|
|
125
|
+
constraints = {},
|
|
126
|
+
cardConfig = {},
|
|
127
|
+
disableClose = false,
|
|
128
|
+
disableResize = false,
|
|
129
|
+
disableDrag = false,
|
|
130
|
+
disableModeSwitch = false,
|
|
131
|
+
persistKey,
|
|
132
|
+
snapThreshold = 20,
|
|
133
|
+
detachThreshold = 40,
|
|
134
|
+
header,
|
|
135
|
+
headerActions,
|
|
136
|
+
footer,
|
|
137
|
+
footerActions,
|
|
138
|
+
class: className,
|
|
139
|
+
}: Props = $props();
|
|
140
|
+
|
|
141
|
+
// Resolve constraints and card config (use $derived to react to prop changes)
|
|
142
|
+
const resolvedConstraints = $derived({ ...DEFAULT_CONSTRAINTS, ...constraints });
|
|
143
|
+
const resolvedCardConfig = $derived({ ...DEFAULT_CARD_CONFIG, ...cardConfig });
|
|
144
|
+
|
|
145
|
+
// Internal state - initialized from props or persistence
|
|
146
|
+
let internalMode = $state<PanelMode>(DEFAULT_PANEL_STATE.mode);
|
|
147
|
+
let internalVariant = $state<PanelVariant>(DEFAULT_PANEL_STATE.variant);
|
|
148
|
+
let internalEdge = $state<PanelEdge>(DEFAULT_PANEL_STATE.edge);
|
|
149
|
+
let internalExpanded = $state<boolean>(DEFAULT_PANEL_STATE.isExpanded);
|
|
150
|
+
let detachedPosition = $state<Position>(DEFAULT_PANEL_STATE.detachedPosition);
|
|
151
|
+
let detachedSize = $state<Size>(DEFAULT_PANEL_STATE.detachedSize);
|
|
152
|
+
let pinnedSize = $state<number>(DEFAULT_PANEL_STATE.pinnedSize);
|
|
153
|
+
|
|
154
|
+
// Card position state (horizontal position for card mode)
|
|
155
|
+
let cardHorizontalPosition = $state<number>(0);
|
|
156
|
+
let cardExpandedHeight = $state<number>(0);
|
|
157
|
+
// Card width state (for desktop width resizing)
|
|
158
|
+
let cardWidth = $state<number | null>(null); // null means use default calculated width
|
|
159
|
+
|
|
160
|
+
// Drag/resize state
|
|
161
|
+
let isDragging = $state(false);
|
|
162
|
+
let isResizing = $state(false);
|
|
163
|
+
let isCardVerticalDragging = $state(false);
|
|
164
|
+
let isCardHorizontalDragging = $state(false);
|
|
165
|
+
let dragStart = $state<Position>({ x: 0, y: 0 });
|
|
166
|
+
let dragPanelStart = $state<Position>({ x: 0, y: 0 });
|
|
167
|
+
let resizeHandle = $state<ResizeHandle | null>(null);
|
|
168
|
+
let resizeStart = $state<Position>({ x: 0, y: 0 });
|
|
169
|
+
let resizePanelStart = $state<Position>({ x: 0, y: 0 });
|
|
170
|
+
let resizeSizeStart = $state<Size>({ width: 0, height: 0 });
|
|
171
|
+
|
|
172
|
+
// Card drag state
|
|
173
|
+
let cardDragStartY = $state(0);
|
|
174
|
+
let cardDragStartX = $state(0);
|
|
175
|
+
let cardDragStartTime = $state(0);
|
|
176
|
+
let cardDragCurrentHeight = $state(CARD_HEADER_HEIGHT);
|
|
177
|
+
let cardDragStartExpanded = $state(false);
|
|
178
|
+
let cardDragStartHorizontalPosition = $state(0);
|
|
179
|
+
// Flag to indicate we're animating after a drag (keeps using cardDragCurrentHeight for height)
|
|
180
|
+
let isCardHeightTransitioning = $state(false);
|
|
181
|
+
|
|
182
|
+
// Card width resize state
|
|
183
|
+
let isCardWidthResizing = $state(false);
|
|
184
|
+
let cardWidthResizeHandle = $state<'left' | 'right' | null>(null);
|
|
185
|
+
let cardWidthResizeStartX = $state(0);
|
|
186
|
+
let cardWidthResizeStartWidth = $state(0);
|
|
187
|
+
let cardWidthResizeStartLeft = $state(0);
|
|
188
|
+
|
|
189
|
+
// Snap preview state
|
|
190
|
+
let snapPreview = $state<PanelEdge | null>(null);
|
|
191
|
+
|
|
192
|
+
// Pinned drag-to-detach state
|
|
193
|
+
let isPinnedDragging = $state(false);
|
|
194
|
+
let pullOffset = $state(0);
|
|
195
|
+
let pinnedDragStartPosition = $state<Position>({ x: 0, y: 0 });
|
|
196
|
+
let pinnedDragHasDetached = $state(false);
|
|
197
|
+
|
|
198
|
+
// Panel element ref
|
|
199
|
+
let panelRef = $state<HTMLDivElement | null>(null);
|
|
200
|
+
|
|
201
|
+
// Viewport dimensions
|
|
202
|
+
let viewportWidth = $state(typeof window !== 'undefined' ? window.innerWidth : 1024);
|
|
203
|
+
let viewportHeight = $state(typeof window !== 'undefined' ? window.innerHeight : 768);
|
|
204
|
+
|
|
205
|
+
// Detect mobile
|
|
206
|
+
let isMobile = $derived(viewportWidth < 640);
|
|
207
|
+
|
|
208
|
+
// Derived state - use props if provided, otherwise internal state
|
|
209
|
+
const mode = $derived(propMode ?? internalMode);
|
|
210
|
+
const variant = $derived(propVariant ?? internalVariant);
|
|
211
|
+
const edge = $derived(propEdge ?? internalEdge);
|
|
212
|
+
const isExpanded = $derived(propExpanded ?? internalExpanded);
|
|
213
|
+
|
|
214
|
+
// Save timeout for debounced persistence
|
|
215
|
+
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
216
|
+
|
|
217
|
+
function debouncedSave() {
|
|
218
|
+
if (!persistKey) return;
|
|
219
|
+
|
|
220
|
+
if (saveTimeout) {
|
|
221
|
+
clearTimeout(saveTimeout);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
saveTimeout = setTimeout(() => {
|
|
225
|
+
savePanelState(persistKey, {
|
|
226
|
+
mode: internalMode,
|
|
227
|
+
variant: internalVariant,
|
|
228
|
+
edge: internalEdge,
|
|
229
|
+
isExpanded: internalExpanded,
|
|
230
|
+
detachedPosition,
|
|
231
|
+
detachedSize,
|
|
232
|
+
pinnedSize,
|
|
233
|
+
cardSnapIndex: 0, // Legacy field
|
|
234
|
+
});
|
|
235
|
+
saveTimeout = null;
|
|
236
|
+
}, 300);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Initialize from persistence on mount
|
|
240
|
+
onMount(() => {
|
|
241
|
+
// Update viewport dimensions
|
|
242
|
+
viewportWidth = window.innerWidth;
|
|
243
|
+
viewportHeight = window.innerHeight;
|
|
244
|
+
|
|
245
|
+
// Initialize card expanded height
|
|
246
|
+
cardExpandedHeight = viewportHeight * CARD_EXPANDED_RATIO;
|
|
247
|
+
|
|
248
|
+
// Listen for resize
|
|
249
|
+
const handleResize = () => {
|
|
250
|
+
viewportWidth = window.innerWidth;
|
|
251
|
+
viewportHeight = window.innerHeight;
|
|
252
|
+
cardExpandedHeight = viewportHeight * CARD_EXPANDED_RATIO;
|
|
253
|
+
};
|
|
254
|
+
window.addEventListener('resize', handleResize);
|
|
255
|
+
|
|
256
|
+
if (persistKey) {
|
|
257
|
+
const persisted = loadPanelState(persistKey);
|
|
258
|
+
if (persisted) {
|
|
259
|
+
if (propMode === undefined && persisted.mode) internalMode = persisted.mode;
|
|
260
|
+
if (propVariant === undefined && persisted.variant) internalVariant = persisted.variant;
|
|
261
|
+
if (propEdge === undefined && persisted.edge) internalEdge = persisted.edge;
|
|
262
|
+
if (propExpanded === undefined && persisted.isExpanded !== undefined)
|
|
263
|
+
internalExpanded = persisted.isExpanded;
|
|
264
|
+
if (persisted.detachedPosition) detachedPosition = persisted.detachedPosition;
|
|
265
|
+
if (persisted.detachedSize) detachedSize = persisted.detachedSize;
|
|
266
|
+
if (persisted.pinnedSize) pinnedSize = persisted.pinnedSize;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Center detached panel on first show if at default position
|
|
271
|
+
if (mode === 'detached') {
|
|
272
|
+
if (
|
|
273
|
+
detachedPosition.x === DEFAULT_PANEL_STATE.detachedPosition.x &&
|
|
274
|
+
detachedPosition.y === DEFAULT_PANEL_STATE.detachedPosition.y
|
|
275
|
+
) {
|
|
276
|
+
detachedPosition = getInitialDetachedPosition(
|
|
277
|
+
detachedSize,
|
|
278
|
+
window.innerWidth,
|
|
279
|
+
window.innerHeight
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle escape key to close
|
|
285
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
286
|
+
if (e.key === 'Escape' && !disableClose) {
|
|
287
|
+
handleClose();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
291
|
+
|
|
292
|
+
return () => {
|
|
293
|
+
window.removeEventListener('resize', handleResize);
|
|
294
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// State update handlers
|
|
299
|
+
function handleModeChange(newMode: PanelMode) {
|
|
300
|
+
if (propMode === undefined) {
|
|
301
|
+
internalMode = newMode;
|
|
302
|
+
debouncedSave();
|
|
303
|
+
}
|
|
304
|
+
onModeChange?.(newMode);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleVariantChange(newVariant: PanelVariant) {
|
|
308
|
+
if (propVariant === undefined) {
|
|
309
|
+
internalVariant = newVariant;
|
|
310
|
+
debouncedSave();
|
|
311
|
+
}
|
|
312
|
+
onVariantChange?.(newVariant);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleEdgeChange(newEdge: PanelEdge) {
|
|
316
|
+
if (propEdge === undefined) {
|
|
317
|
+
internalEdge = newEdge;
|
|
318
|
+
debouncedSave();
|
|
319
|
+
}
|
|
320
|
+
onEdgeChange?.(newEdge);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function handleExpandedChange(newExpanded: boolean) {
|
|
324
|
+
if (propExpanded === undefined) {
|
|
325
|
+
internalExpanded = newExpanded;
|
|
326
|
+
debouncedSave();
|
|
327
|
+
}
|
|
328
|
+
onExpandedChange?.(newExpanded);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function handleClose() {
|
|
332
|
+
open = false;
|
|
333
|
+
onOpenChange?.(false);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function handleTabClick() {
|
|
337
|
+
handleExpandedChange(true);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handleToggleExpand() {
|
|
341
|
+
handleExpandedChange(!isExpanded);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Drag handlers for detached mode
|
|
345
|
+
function handleDragStart(e: PointerEvent) {
|
|
346
|
+
if (disableDrag) return;
|
|
347
|
+
|
|
348
|
+
// Only drag from header
|
|
349
|
+
const target = e.target as HTMLElement;
|
|
350
|
+
if (!target.closest('[data-panel-header]')) return;
|
|
351
|
+
|
|
352
|
+
const pos = getPointerPosition(e);
|
|
353
|
+
|
|
354
|
+
if (mode === 'detached') {
|
|
355
|
+
// Standard detached drag
|
|
356
|
+
isDragging = true;
|
|
357
|
+
dragStart = pos;
|
|
358
|
+
dragPanelStart = { ...detachedPosition };
|
|
359
|
+
document.body.style.cursor = 'grabbing';
|
|
360
|
+
document.body.style.userSelect = 'none';
|
|
361
|
+
} else if (mode === 'pinned' && variant === 'full' && detachThreshold > 0) {
|
|
362
|
+
// Start pinned drag-to-detach
|
|
363
|
+
isPinnedDragging = true;
|
|
364
|
+
pinnedDragStartPosition = pos;
|
|
365
|
+
pinnedDragHasDetached = false;
|
|
366
|
+
pullOffset = 0;
|
|
367
|
+
document.body.style.cursor = 'grab';
|
|
368
|
+
document.body.style.userSelect = 'none';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function handleDragMove(e: PointerEvent) {
|
|
373
|
+
if (!isDragging) return;
|
|
374
|
+
|
|
375
|
+
const currentPos = getPointerPosition(e);
|
|
376
|
+
const newPosition = calculateDragPosition(
|
|
377
|
+
{
|
|
378
|
+
isDragging: true,
|
|
379
|
+
startPosition: dragStart,
|
|
380
|
+
startPanelPosition: dragPanelStart,
|
|
381
|
+
currentPosition: currentPos,
|
|
382
|
+
},
|
|
383
|
+
currentPos
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Check for edge snap
|
|
387
|
+
const snap = detectEdgeSnap(
|
|
388
|
+
newPosition,
|
|
389
|
+
detachedSize,
|
|
390
|
+
window.innerWidth,
|
|
391
|
+
window.innerHeight,
|
|
392
|
+
snapThreshold
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
snapPreview = snap.shouldSnap ? snap.edge : null;
|
|
396
|
+
|
|
397
|
+
// Constrain position to viewport
|
|
398
|
+
detachedPosition = constrainPosition(
|
|
399
|
+
newPosition,
|
|
400
|
+
detachedSize,
|
|
401
|
+
window.innerWidth,
|
|
402
|
+
window.innerHeight
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function handleDragEnd() {
|
|
407
|
+
if (!isDragging) return;
|
|
408
|
+
|
|
409
|
+
isDragging = false;
|
|
410
|
+
document.body.style.cursor = '';
|
|
411
|
+
document.body.style.userSelect = '';
|
|
412
|
+
|
|
413
|
+
// Apply snap if active
|
|
414
|
+
if (snapPreview) {
|
|
415
|
+
handleModeChange('pinned');
|
|
416
|
+
handleEdgeChange(snapPreview);
|
|
417
|
+
snapPreview = null;
|
|
418
|
+
} else {
|
|
419
|
+
debouncedSave();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Pinned drag-to-detach handlers
|
|
424
|
+
function handlePinnedDragMove(e: PointerEvent) {
|
|
425
|
+
if (!isPinnedDragging) return;
|
|
426
|
+
|
|
427
|
+
const currentPos = getPointerPosition(e);
|
|
428
|
+
const distance = calculateDetachDistance(pinnedDragStartPosition, currentPos, edge);
|
|
429
|
+
|
|
430
|
+
if (distance >= detachThreshold && !pinnedDragHasDetached) {
|
|
431
|
+
// Threshold exceeded - detach the panel
|
|
432
|
+
pinnedDragHasDetached = true;
|
|
433
|
+
isPinnedDragging = false;
|
|
434
|
+
pullOffset = 0;
|
|
435
|
+
|
|
436
|
+
// Calculate where to position the detached panel
|
|
437
|
+
const newPosition = calculateDetachedPositionFromPinned(
|
|
438
|
+
currentPos,
|
|
439
|
+
pinnedSize,
|
|
440
|
+
detachedSize,
|
|
441
|
+
edge,
|
|
442
|
+
window.innerWidth,
|
|
443
|
+
window.innerHeight
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Switch to detached mode and start regular drag
|
|
447
|
+
detachedPosition = newPosition;
|
|
448
|
+
handleModeChange('detached');
|
|
449
|
+
|
|
450
|
+
// Immediately start detached dragging from current position
|
|
451
|
+
isDragging = true;
|
|
452
|
+
dragStart = currentPos;
|
|
453
|
+
dragPanelStart = newPosition;
|
|
454
|
+
document.body.style.cursor = 'grabbing';
|
|
455
|
+
} else if (!pinnedDragHasDetached) {
|
|
456
|
+
// Still within threshold - show pull effect
|
|
457
|
+
pullOffset = calculatePullOffset(distance, detachThreshold);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function handlePinnedDragEnd() {
|
|
462
|
+
if (!isPinnedDragging) return;
|
|
463
|
+
|
|
464
|
+
isPinnedDragging = false;
|
|
465
|
+
pullOffset = 0;
|
|
466
|
+
document.body.style.cursor = '';
|
|
467
|
+
document.body.style.userSelect = '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Card mode handlers
|
|
471
|
+
function handleCardHeaderPointerDown(e: PointerEvent) {
|
|
472
|
+
if (variant !== 'card') return;
|
|
473
|
+
|
|
474
|
+
const target = e.target as HTMLElement;
|
|
475
|
+
|
|
476
|
+
// Check for drag handle FIRST (before button check, since drag handle is a button)
|
|
477
|
+
const isDragHandle = target.closest('[data-card-drag-handle]');
|
|
478
|
+
|
|
479
|
+
// Don't start drag if clicking on other buttons (not the drag handle)
|
|
480
|
+
if (!isDragHandle && target.closest('button')) return;
|
|
481
|
+
|
|
482
|
+
const pos = getPointerPosition(e);
|
|
483
|
+
cardDragStartY = pos.y;
|
|
484
|
+
cardDragStartX = pos.x;
|
|
485
|
+
cardDragStartTime = Date.now();
|
|
486
|
+
cardDragStartExpanded = isExpanded;
|
|
487
|
+
cardDragCurrentHeight = isExpanded ? cardExpandedHeight : CARD_HEADER_HEIGHT;
|
|
488
|
+
cardDragStartHorizontalPosition = cardHorizontalPosition;
|
|
489
|
+
|
|
490
|
+
// On mobile: vertical drag to expand/collapse
|
|
491
|
+
// On desktop: horizontal drag to reposition (via drag handle), click header to expand/collapse
|
|
492
|
+
if (isMobile) {
|
|
493
|
+
isCardVerticalDragging = true;
|
|
494
|
+
} else {
|
|
495
|
+
if (isDragHandle) {
|
|
496
|
+
isCardHorizontalDragging = true;
|
|
497
|
+
} else {
|
|
498
|
+
// Clicking elsewhere on header toggles expand
|
|
499
|
+
isCardVerticalDragging = true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
document.body.style.userSelect = 'none';
|
|
504
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function handleCardPointerMove(e: PointerEvent) {
|
|
508
|
+
const currentPos = getPointerPosition(e);
|
|
509
|
+
|
|
510
|
+
if (isCardVerticalDragging) {
|
|
511
|
+
const deltaY = cardDragStartY - currentPos.y; // Positive when dragging up
|
|
512
|
+
|
|
513
|
+
// Both mobile and desktop: change height directly during drag
|
|
514
|
+
const startHeight = cardDragStartExpanded ? cardExpandedHeight : CARD_HEADER_HEIGHT;
|
|
515
|
+
cardDragCurrentHeight = Math.max(
|
|
516
|
+
CARD_HEADER_HEIGHT,
|
|
517
|
+
Math.min(viewportHeight * 0.95, startHeight + deltaY)
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (isCardHorizontalDragging && !isMobile) {
|
|
522
|
+
const deltaX = currentPos.x - cardDragStartX;
|
|
523
|
+
// Calculate card width and default offset (centered position)
|
|
524
|
+
const cardWidth = Math.min(
|
|
525
|
+
viewportWidth - resolvedCardConfig.horizontalMargin * 2,
|
|
526
|
+
resolvedCardConfig.maxWidth
|
|
527
|
+
);
|
|
528
|
+
const defaultLeftOffset = Math.max(
|
|
529
|
+
resolvedCardConfig.horizontalMargin,
|
|
530
|
+
(viewportWidth - cardWidth) / 2
|
|
531
|
+
);
|
|
532
|
+
// Calculate bounds: can move from left margin to right margin
|
|
533
|
+
const minOffset = -(defaultLeftOffset - resolvedCardConfig.horizontalMargin);
|
|
534
|
+
const maxOffset =
|
|
535
|
+
viewportWidth - cardWidth - defaultLeftOffset - resolvedCardConfig.horizontalMargin;
|
|
536
|
+
// Apply delta to starting position and clamp to bounds
|
|
537
|
+
const newPosition = cardDragStartHorizontalPosition + deltaX;
|
|
538
|
+
cardHorizontalPosition = Math.max(minOffset, Math.min(maxOffset, newPosition));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function handleCardPointerUp(e: PointerEvent) {
|
|
543
|
+
const endTime = Date.now();
|
|
544
|
+
const currentPos = getPointerPosition(e);
|
|
545
|
+
const deltaTime = endTime - cardDragStartTime;
|
|
546
|
+
const deltaY = cardDragStartY - currentPos.y;
|
|
547
|
+
const deltaX = currentPos.x - cardDragStartX;
|
|
548
|
+
|
|
549
|
+
document.body.style.userSelect = '';
|
|
550
|
+
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
|
551
|
+
|
|
552
|
+
if (isCardVerticalDragging) {
|
|
553
|
+
// Tap detection - toggle and exit early
|
|
554
|
+
if (deltaTime < 200 && Math.abs(deltaY) < 10) {
|
|
555
|
+
isCardVerticalDragging = false;
|
|
556
|
+
handleToggleExpand();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Determine final state based on velocity or position
|
|
561
|
+
const velocity = deltaY / deltaTime;
|
|
562
|
+
let newExpanded: boolean;
|
|
563
|
+
|
|
564
|
+
if (Math.abs(velocity) > 0.5) {
|
|
565
|
+
// Velocity-based: fast swipe snaps in direction of swipe
|
|
566
|
+
newExpanded = velocity > 0;
|
|
567
|
+
} else {
|
|
568
|
+
// Position-based: snap to whichever state the card is closer to
|
|
569
|
+
const midpoint = (CARD_HEADER_HEIGHT + cardExpandedHeight) / 2;
|
|
570
|
+
newExpanded = cardDragCurrentHeight > midpoint;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const targetHeight = newExpanded ? cardExpandedHeight : CARD_HEADER_HEIGHT;
|
|
574
|
+
|
|
575
|
+
// Step 1: Stop dragging (enables CSS transitions) but keep current height
|
|
576
|
+
isCardVerticalDragging = false;
|
|
577
|
+
isCardHeightTransitioning = true;
|
|
578
|
+
handleExpandedChange(newExpanded);
|
|
579
|
+
|
|
580
|
+
// Step 2: Next frame, update height to trigger the animation
|
|
581
|
+
// This ensures transitions are enabled before the height changes
|
|
582
|
+
requestAnimationFrame(() => {
|
|
583
|
+
cardDragCurrentHeight = targetHeight;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isCardHorizontalDragging) {
|
|
588
|
+
isCardHorizontalDragging = false;
|
|
589
|
+
debouncedSave();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Resize handlers
|
|
594
|
+
function handleResizeStart(e: PointerEvent, handle: ResizeHandle) {
|
|
595
|
+
if (disableResize) return;
|
|
596
|
+
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
e.stopPropagation();
|
|
599
|
+
|
|
600
|
+
isResizing = true;
|
|
601
|
+
resizeHandle = handle;
|
|
602
|
+
const pos = getPointerPosition(e);
|
|
603
|
+
resizeStart = pos;
|
|
604
|
+
resizePanelStart = { ...detachedPosition };
|
|
605
|
+
resizeSizeStart =
|
|
606
|
+
mode === 'detached' ? { ...detachedSize } : { width: pinnedSize, height: pinnedSize };
|
|
607
|
+
|
|
608
|
+
document.body.style.cursor = getResizeCursor(handle);
|
|
609
|
+
document.body.style.userSelect = 'none';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function handleResizeMove(e: PointerEvent) {
|
|
613
|
+
if (!isResizing || !resizeHandle) return;
|
|
614
|
+
|
|
615
|
+
const currentPos = getPointerPosition(e);
|
|
616
|
+
|
|
617
|
+
if (mode === 'detached') {
|
|
618
|
+
const result = calculateResize(
|
|
619
|
+
{
|
|
620
|
+
isResizing: true,
|
|
621
|
+
handle: resizeHandle,
|
|
622
|
+
startPosition: resizeStart,
|
|
623
|
+
startSize: resizeSizeStart,
|
|
624
|
+
startPanelPosition: resizePanelStart,
|
|
625
|
+
},
|
|
626
|
+
currentPos
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
// Constrain size and position
|
|
630
|
+
const constrainedSize = constrainSize(result.size, resolvedConstraints);
|
|
631
|
+
const constrainedPosition = constrainPosition(
|
|
632
|
+
result.position,
|
|
633
|
+
constrainedSize,
|
|
634
|
+
window.innerWidth,
|
|
635
|
+
window.innerHeight
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
detachedSize = constrainedSize;
|
|
639
|
+
detachedPosition = constrainedPosition;
|
|
640
|
+
} else {
|
|
641
|
+
// Pinned mode - resize perpendicular to edge
|
|
642
|
+
const delta = isHorizontalEdge(edge)
|
|
643
|
+
? currentPos.x - resizeStart.x
|
|
644
|
+
: currentPos.y - resizeStart.y;
|
|
645
|
+
|
|
646
|
+
const direction = edge === 'left' || edge === 'top' ? 1 : -1;
|
|
647
|
+
const newSize = resizeSizeStart.width + delta * direction;
|
|
648
|
+
|
|
649
|
+
if (isHorizontalEdge(edge)) {
|
|
650
|
+
pinnedSize = constrainPinnedWidth(newSize, resolvedConstraints, window.innerWidth);
|
|
651
|
+
} else {
|
|
652
|
+
pinnedSize = constrainPinnedHeight(newSize, resolvedConstraints, window.innerHeight);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function handleResizeEnd() {
|
|
658
|
+
if (!isResizing) return;
|
|
659
|
+
|
|
660
|
+
isResizing = false;
|
|
661
|
+
resizeHandle = null;
|
|
662
|
+
document.body.style.cursor = '';
|
|
663
|
+
document.body.style.userSelect = '';
|
|
664
|
+
debouncedSave();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Card resize (top edge only when expanded)
|
|
668
|
+
function handleCardResizeStart(e: PointerEvent) {
|
|
669
|
+
if (variant !== 'card' || !isExpanded || disableResize) return;
|
|
670
|
+
|
|
671
|
+
e.preventDefault();
|
|
672
|
+
e.stopPropagation();
|
|
673
|
+
|
|
674
|
+
isResizing = true;
|
|
675
|
+
const pos = getPointerPosition(e);
|
|
676
|
+
resizeStart = pos;
|
|
677
|
+
resizeSizeStart = { width: 0, height: cardExpandedHeight };
|
|
678
|
+
|
|
679
|
+
document.body.style.cursor = 'ns-resize';
|
|
680
|
+
document.body.style.userSelect = 'none';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function handleCardResizeMove(e: PointerEvent) {
|
|
684
|
+
if (!isResizing || variant !== 'card') return;
|
|
685
|
+
|
|
686
|
+
const currentPos = getPointerPosition(e);
|
|
687
|
+
const deltaY = resizeStart.y - currentPos.y;
|
|
688
|
+
const newHeight = Math.max(
|
|
689
|
+
CARD_HEADER_HEIGHT * 2,
|
|
690
|
+
Math.min(viewportHeight * 0.95, resizeSizeStart.height + deltaY)
|
|
691
|
+
);
|
|
692
|
+
cardExpandedHeight = newHeight;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Card width resize handlers (desktop only)
|
|
696
|
+
function handleCardWidthResizeStart(e: PointerEvent, handle: 'left' | 'right') {
|
|
697
|
+
if (variant !== 'card' || !isExpanded || disableResize || isMobile) return;
|
|
698
|
+
|
|
699
|
+
e.preventDefault();
|
|
700
|
+
e.stopPropagation();
|
|
701
|
+
|
|
702
|
+
isCardWidthResizing = true;
|
|
703
|
+
cardWidthResizeHandle = handle;
|
|
704
|
+
const pos = getPointerPosition(e);
|
|
705
|
+
cardWidthResizeStartX = pos.x;
|
|
706
|
+
|
|
707
|
+
// Calculate current width
|
|
708
|
+
const defaultCardWidth = Math.min(
|
|
709
|
+
viewportWidth - resolvedCardConfig.horizontalMargin * 2,
|
|
710
|
+
resolvedCardConfig.maxWidth
|
|
711
|
+
);
|
|
712
|
+
const currentWidth = cardWidth ?? defaultCardWidth;
|
|
713
|
+
cardWidthResizeStartWidth = currentWidth;
|
|
714
|
+
|
|
715
|
+
// Calculate current left position
|
|
716
|
+
const defaultLeftOffset = Math.max(
|
|
717
|
+
resolvedCardConfig.horizontalMargin,
|
|
718
|
+
(viewportWidth - defaultCardWidth) / 2
|
|
719
|
+
);
|
|
720
|
+
cardWidthResizeStartLeft = defaultLeftOffset + cardHorizontalPosition;
|
|
721
|
+
|
|
722
|
+
document.body.style.cursor = 'ew-resize';
|
|
723
|
+
document.body.style.userSelect = 'none';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function handleCardWidthResizeMove(e: PointerEvent) {
|
|
727
|
+
if (!isCardWidthResizing || !cardWidthResizeHandle) return;
|
|
728
|
+
|
|
729
|
+
const currentPos = getPointerPosition(e);
|
|
730
|
+
const deltaX = currentPos.x - cardWidthResizeStartX;
|
|
731
|
+
|
|
732
|
+
if (cardWidthResizeHandle === 'right') {
|
|
733
|
+
// Right handle: only adjust width
|
|
734
|
+
const newWidth = Math.max(
|
|
735
|
+
CARD_MIN_WIDTH,
|
|
736
|
+
Math.min(
|
|
737
|
+
resolvedCardConfig.maxWidth,
|
|
738
|
+
viewportWidth - cardWidthResizeStartLeft - resolvedCardConfig.horizontalMargin,
|
|
739
|
+
cardWidthResizeStartWidth + deltaX
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
cardWidth = newWidth;
|
|
743
|
+
} else {
|
|
744
|
+
// Left handle: adjust both left position and width
|
|
745
|
+
const newLeft = cardWidthResizeStartLeft + deltaX;
|
|
746
|
+
const newWidth = cardWidthResizeStartWidth - deltaX;
|
|
747
|
+
|
|
748
|
+
// Constrain: left can't go below margin, width can't go below min or above max
|
|
749
|
+
if (newLeft >= resolvedCardConfig.horizontalMargin && newWidth >= CARD_MIN_WIDTH) {
|
|
750
|
+
const constrainedWidth = Math.min(newWidth, resolvedCardConfig.maxWidth);
|
|
751
|
+
cardWidth = constrainedWidth;
|
|
752
|
+
|
|
753
|
+
// Calculate new horizontal position offset from default centered position
|
|
754
|
+
const defaultCardWidth = Math.min(
|
|
755
|
+
viewportWidth - resolvedCardConfig.horizontalMargin * 2,
|
|
756
|
+
resolvedCardConfig.maxWidth
|
|
757
|
+
);
|
|
758
|
+
const defaultLeftOffset = Math.max(
|
|
759
|
+
resolvedCardConfig.horizontalMargin,
|
|
760
|
+
(viewportWidth - defaultCardWidth) / 2
|
|
761
|
+
);
|
|
762
|
+
cardHorizontalPosition = newLeft - defaultLeftOffset;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function handleCardWidthResizeEnd() {
|
|
768
|
+
if (!isCardWidthResizing) return;
|
|
769
|
+
|
|
770
|
+
isCardWidthResizing = false;
|
|
771
|
+
cardWidthResizeHandle = null;
|
|
772
|
+
document.body.style.cursor = '';
|
|
773
|
+
document.body.style.userSelect = '';
|
|
774
|
+
debouncedSave();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Global event listeners
|
|
778
|
+
function handlePointerMove(e: PointerEvent) {
|
|
779
|
+
if (isDragging) handleDragMove(e);
|
|
780
|
+
if (isPinnedDragging) handlePinnedDragMove(e);
|
|
781
|
+
if (isResizing && variant !== 'card') handleResizeMove(e);
|
|
782
|
+
if (isResizing && variant === 'card') handleCardResizeMove(e);
|
|
783
|
+
if (isCardVerticalDragging || isCardHorizontalDragging) handleCardPointerMove(e);
|
|
784
|
+
if (isCardWidthResizing) handleCardWidthResizeMove(e);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function handlePointerUp(e: PointerEvent) {
|
|
788
|
+
if (isDragging) handleDragEnd();
|
|
789
|
+
if (isPinnedDragging) handlePinnedDragEnd();
|
|
790
|
+
if (isResizing) handleResizeEnd();
|
|
791
|
+
if (isCardVerticalDragging || isCardHorizontalDragging) handleCardPointerUp(e);
|
|
792
|
+
if (isCardWidthResizing) handleCardWidthResizeEnd();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Add/remove global listeners
|
|
796
|
+
$effect(() => {
|
|
797
|
+
if (
|
|
798
|
+
isDragging ||
|
|
799
|
+
isPinnedDragging ||
|
|
800
|
+
isResizing ||
|
|
801
|
+
isCardVerticalDragging ||
|
|
802
|
+
isCardHorizontalDragging ||
|
|
803
|
+
isCardWidthResizing
|
|
804
|
+
) {
|
|
805
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
806
|
+
window.addEventListener('pointerup', handlePointerUp);
|
|
807
|
+
return () => {
|
|
808
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
809
|
+
window.removeEventListener('pointerup', handlePointerUp);
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Reset card position and width when switching to mobile
|
|
815
|
+
$effect(() => {
|
|
816
|
+
if (isMobile && variant === 'card') {
|
|
817
|
+
// Reset to centered, full-width on mobile
|
|
818
|
+
cardHorizontalPosition = 0;
|
|
819
|
+
cardWidth = null;
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Clear height transition flag after CSS transition completes
|
|
824
|
+
// This returns control to the normal isExpanded-based height calculation
|
|
825
|
+
$effect(() => {
|
|
826
|
+
if (isCardHeightTransitioning) {
|
|
827
|
+
const timeout = setTimeout(() => {
|
|
828
|
+
isCardHeightTransitioning = false;
|
|
829
|
+
}, 350); // Slightly longer than CSS transition (300ms) to ensure completion
|
|
830
|
+
return () => clearTimeout(timeout);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Calculate panel styles
|
|
835
|
+
const panelStyles = $derived(() => {
|
|
836
|
+
if (variant === 'card') {
|
|
837
|
+
// Card mode - bottom sheet style
|
|
838
|
+
const defaultCardWidth = Math.min(
|
|
839
|
+
viewportWidth - resolvedCardConfig.horizontalMargin * 2,
|
|
840
|
+
resolvedCardConfig.maxWidth
|
|
841
|
+
);
|
|
842
|
+
// Use custom cardWidth if set, otherwise use default
|
|
843
|
+
const actualCardWidth = cardWidth ?? defaultCardWidth;
|
|
844
|
+
const defaultLeftOffset = Math.max(
|
|
845
|
+
resolvedCardConfig.horizontalMargin,
|
|
846
|
+
(viewportWidth - defaultCardWidth) / 2
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
// Height based on expanded state or drag
|
|
850
|
+
let height: number;
|
|
851
|
+
if (isCardVerticalDragging || isCardHeightTransitioning) {
|
|
852
|
+
// During drag or animated transition, use cardDragCurrentHeight
|
|
853
|
+
height = cardDragCurrentHeight;
|
|
854
|
+
} else {
|
|
855
|
+
height = isExpanded ? cardExpandedHeight : CARD_HEADER_HEIGHT;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
bottom: '0',
|
|
860
|
+
left: `${defaultLeftOffset + cardHorizontalPosition}px`,
|
|
861
|
+
width: `${actualCardWidth}px`,
|
|
862
|
+
height: `${height}px`,
|
|
863
|
+
maxHeight: `${viewportHeight * 0.95}px`,
|
|
864
|
+
borderRadius: `${resolvedCardConfig.borderRadius}px ${resolvedCardConfig.borderRadius}px 0 0`,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
if (mode === 'pinned') {
|
|
868
|
+
const baseStyles = getPinnedPositionStyles(edge, pinnedSize);
|
|
869
|
+
// Add pull transform when dragging from pinned
|
|
870
|
+
if (pullOffset > 0) {
|
|
871
|
+
const transform = getPinnedPullTransform(pullOffset, edge);
|
|
872
|
+
return { ...baseStyles, transform };
|
|
873
|
+
}
|
|
874
|
+
return baseStyles;
|
|
875
|
+
}
|
|
876
|
+
return getDetachedPositionStyles(detachedPosition, detachedSize);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Panel position classes for full pinned mode
|
|
880
|
+
const pinnedClasses = $derived(
|
|
881
|
+
mode === 'pinned' && variant === 'full'
|
|
882
|
+
? {
|
|
883
|
+
left: 'left-0 top-0 bottom-0 border-r',
|
|
884
|
+
right: 'right-0 top-0 bottom-0 border-l',
|
|
885
|
+
top: 'top-0 left-0 right-0 border-b',
|
|
886
|
+
bottom: 'bottom-0 left-0 right-0 border-t',
|
|
887
|
+
}[edge]
|
|
888
|
+
: ''
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Animation classes
|
|
892
|
+
const animationClass = $derived(() => {
|
|
893
|
+
if (variant === 'card') {
|
|
894
|
+
return 'animate-panel-slide-in-bottom';
|
|
895
|
+
}
|
|
896
|
+
if (mode === 'pinned') {
|
|
897
|
+
return {
|
|
898
|
+
left: 'animate-slide-right',
|
|
899
|
+
right: 'animate-slide-left',
|
|
900
|
+
top: 'animate-slide-down',
|
|
901
|
+
bottom: 'animate-slide-up',
|
|
902
|
+
}[edge];
|
|
903
|
+
}
|
|
904
|
+
return 'animate-scale-in';
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// Whether to show content (collapsed card only shows header)
|
|
908
|
+
const showContent = $derived(variant !== 'card' || isExpanded || isCardVerticalDragging);
|
|
909
|
+
</script>
|
|
910
|
+
|
|
911
|
+
{#if open}
|
|
912
|
+
{#if variant === 'full' && !isExpanded && mode === 'pinned'}
|
|
913
|
+
<!-- Collapsed tab (full variant only) -->
|
|
914
|
+
<DataPanelTab {title} {edge} onclick={handleTabClick} />
|
|
915
|
+
{:else}
|
|
916
|
+
<!-- Snap preview indicator (detached mode only) -->
|
|
917
|
+
{#if snapPreview}
|
|
918
|
+
<div
|
|
919
|
+
class={cn(
|
|
920
|
+
'fixed z-40 bg-primary/20 border-2 border-primary border-dashed transition-all',
|
|
921
|
+
snapPreview === 'left' && 'left-0 top-0 bottom-0 w-80',
|
|
922
|
+
snapPreview === 'right' && 'right-0 top-0 bottom-0 w-80',
|
|
923
|
+
snapPreview === 'top' && 'top-0 left-0 right-0 h-64',
|
|
924
|
+
snapPreview === 'bottom' && 'bottom-0 left-0 right-0 h-64'
|
|
925
|
+
)}
|
|
926
|
+
></div>
|
|
927
|
+
{/if}
|
|
928
|
+
|
|
929
|
+
<!-- Overlay for expanded card on mobile -->
|
|
930
|
+
{#if variant === 'card' && isExpanded && isMobile}
|
|
931
|
+
<div
|
|
932
|
+
class="fixed inset-0 z-40 bg-black/50 transition-opacity"
|
|
933
|
+
onclick={handleToggleExpand}
|
|
934
|
+
onkeydown={(e) => e.key === 'Enter' && handleToggleExpand()}
|
|
935
|
+
role="button"
|
|
936
|
+
tabindex="-1"
|
|
937
|
+
aria-label="Close panel"
|
|
938
|
+
></div>
|
|
939
|
+
{/if}
|
|
940
|
+
|
|
941
|
+
<!-- Panel -->
|
|
942
|
+
<div
|
|
943
|
+
bind:this={panelRef}
|
|
944
|
+
class={cn(
|
|
945
|
+
'fixed z-50 flex flex-col bg-background border border-border shadow-lg overflow-hidden',
|
|
946
|
+
variant === 'full' && mode === 'pinned' && pinnedClasses,
|
|
947
|
+
variant === 'full' && mode === 'detached' && 'rounded-lg',
|
|
948
|
+
variant === 'card' && 'rounded-t-lg border-b-0',
|
|
949
|
+
isPinnedDragging && pullOffset > 0 && 'shadow-2xl',
|
|
950
|
+
!isDragging &&
|
|
951
|
+
!isPinnedDragging &&
|
|
952
|
+
!isCardHorizontalDragging &&
|
|
953
|
+
!isCardVerticalDragging &&
|
|
954
|
+
!isResizing &&
|
|
955
|
+
!isCardWidthResizing &&
|
|
956
|
+
'transition-[height,left,width,transform] duration-300 ease-out',
|
|
957
|
+
animationClass(),
|
|
958
|
+
className
|
|
959
|
+
)}
|
|
960
|
+
style={Object.entries(panelStyles())
|
|
961
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
962
|
+
.join(';')}
|
|
963
|
+
role={mode === 'detached' || variant === 'card' ? 'dialog' : 'complementary'}
|
|
964
|
+
aria-modal={mode === 'detached' || (variant === 'card' && isExpanded) ? 'true' : undefined}
|
|
965
|
+
aria-labelledby={title ? 'data-panel-title' : undefined}
|
|
966
|
+
aria-expanded={variant === 'card' ? isExpanded : undefined}
|
|
967
|
+
tabindex="-1"
|
|
968
|
+
onpointerdown={variant === 'full' &&
|
|
969
|
+
(mode === 'detached' || (mode === 'pinned' && detachThreshold > 0))
|
|
970
|
+
? handleDragStart
|
|
971
|
+
: undefined}
|
|
972
|
+
>
|
|
973
|
+
<!-- Header -->
|
|
974
|
+
<div
|
|
975
|
+
data-panel-header
|
|
976
|
+
class={cn(
|
|
977
|
+
'relative flex items-center justify-between gap-2 border-b border-border bg-muted/30 px-4',
|
|
978
|
+
variant === 'full' && 'py-3',
|
|
979
|
+
variant === 'card' && 'pt-5 pb-3',
|
|
980
|
+
variant === 'full' &&
|
|
981
|
+
!disableDrag &&
|
|
982
|
+
(mode === 'detached' || (mode === 'pinned' && detachThreshold > 0)) &&
|
|
983
|
+
'cursor-grab',
|
|
984
|
+
variant === 'card' && 'cursor-pointer'
|
|
985
|
+
)}
|
|
986
|
+
onpointerdown={variant === 'card' ? handleCardHeaderPointerDown : undefined}
|
|
987
|
+
>
|
|
988
|
+
<!-- Card drag handle / puller (overlays header) -->
|
|
989
|
+
{#if variant === 'card' && resolvedCardConfig.showDragHandle}
|
|
990
|
+
<div
|
|
991
|
+
class="absolute top-1.5 left-1/2 -translate-x-1/2 w-10 h-1.5 rounded-full bg-muted-foreground/30 cursor-pointer touch-none"
|
|
992
|
+
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && handleToggleExpand()}
|
|
993
|
+
onpointerdown={handleCardHeaderPointerDown}
|
|
994
|
+
role="button"
|
|
995
|
+
tabindex="0"
|
|
996
|
+
aria-label={isExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
997
|
+
aria-expanded={isExpanded}
|
|
998
|
+
></div>
|
|
999
|
+
{/if}
|
|
1000
|
+
{#if variant === 'card'}
|
|
1001
|
+
<div
|
|
1002
|
+
class="flex-1 min-w-0"
|
|
1003
|
+
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && handleToggleExpand()}
|
|
1004
|
+
role="button"
|
|
1005
|
+
tabindex="0"
|
|
1006
|
+
>
|
|
1007
|
+
{#if header}
|
|
1008
|
+
{@render header()}
|
|
1009
|
+
{:else}
|
|
1010
|
+
{#if subtitle}
|
|
1011
|
+
<p class="text-sm text-muted-foreground truncate">{subtitle}</p>
|
|
1012
|
+
{/if}
|
|
1013
|
+
{#if title}
|
|
1014
|
+
<h3 id="data-panel-title" class="text-lg font-semibold truncate">{title}</h3>
|
|
1015
|
+
{/if}
|
|
1016
|
+
{/if}
|
|
1017
|
+
</div>
|
|
1018
|
+
{:else}
|
|
1019
|
+
<div class="flex-1 min-w-0">
|
|
1020
|
+
{#if header}
|
|
1021
|
+
{@render header()}
|
|
1022
|
+
{:else}
|
|
1023
|
+
{#if subtitle}
|
|
1024
|
+
<p class="text-sm text-muted-foreground truncate">{subtitle}</p>
|
|
1025
|
+
{/if}
|
|
1026
|
+
{#if title}
|
|
1027
|
+
<h3 id="data-panel-title" class="text-lg font-semibold truncate">{title}</h3>
|
|
1028
|
+
{/if}
|
|
1029
|
+
{/if}
|
|
1030
|
+
</div>
|
|
1031
|
+
{/if}
|
|
1032
|
+
|
|
1033
|
+
<div class="flex items-center gap-1">
|
|
1034
|
+
{#if headerActions}
|
|
1035
|
+
{@render headerActions()}
|
|
1036
|
+
{/if}
|
|
1037
|
+
|
|
1038
|
+
<!-- Horizontal drag handle (desktop card only) -->
|
|
1039
|
+
{#if variant === 'card' && !isMobile}
|
|
1040
|
+
<button
|
|
1041
|
+
data-card-drag-handle
|
|
1042
|
+
type="button"
|
|
1043
|
+
class="p-1.5 rounded hover:bg-muted cursor-move touch-none"
|
|
1044
|
+
aria-label="Drag to reposition panel"
|
|
1045
|
+
title="Drag to move"
|
|
1046
|
+
onpointerdown={handleCardHeaderPointerDown}
|
|
1047
|
+
>
|
|
1048
|
+
<svg
|
|
1049
|
+
class="w-4 h-4 text-muted-foreground"
|
|
1050
|
+
fill="none"
|
|
1051
|
+
viewBox="0 0 24 24"
|
|
1052
|
+
stroke="currentColor"
|
|
1053
|
+
>
|
|
1054
|
+
<path
|
|
1055
|
+
stroke-linecap="round"
|
|
1056
|
+
stroke-linejoin="round"
|
|
1057
|
+
stroke-width="2"
|
|
1058
|
+
d="M4 8h16M4 16h16"
|
|
1059
|
+
/>
|
|
1060
|
+
</svg>
|
|
1061
|
+
</button>
|
|
1062
|
+
{/if}
|
|
1063
|
+
|
|
1064
|
+
<!-- Expand/collapse toggle -->
|
|
1065
|
+
{#if variant === 'card' || (variant === 'full' && mode === 'pinned')}
|
|
1066
|
+
<button
|
|
1067
|
+
type="button"
|
|
1068
|
+
class="p-1.5 rounded hover:bg-muted"
|
|
1069
|
+
onclick={handleToggleExpand}
|
|
1070
|
+
aria-expanded={isExpanded}
|
|
1071
|
+
aria-label={isExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
1072
|
+
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
1073
|
+
>
|
|
1074
|
+
{#if isExpanded}
|
|
1075
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1076
|
+
<path
|
|
1077
|
+
stroke-linecap="round"
|
|
1078
|
+
stroke-linejoin="round"
|
|
1079
|
+
stroke-width="2"
|
|
1080
|
+
d="M19 9l-7 7-7-7"
|
|
1081
|
+
/>
|
|
1082
|
+
</svg>
|
|
1083
|
+
{:else}
|
|
1084
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1085
|
+
<path
|
|
1086
|
+
stroke-linecap="round"
|
|
1087
|
+
stroke-linejoin="round"
|
|
1088
|
+
stroke-width="2"
|
|
1089
|
+
d="M5 15l7-7 7 7"
|
|
1090
|
+
/>
|
|
1091
|
+
</svg>
|
|
1092
|
+
{/if}
|
|
1093
|
+
</button>
|
|
1094
|
+
{/if}
|
|
1095
|
+
|
|
1096
|
+
<!-- Mode menu (full variant only) -->
|
|
1097
|
+
{#if variant === 'full' && !disableModeSwitch}
|
|
1098
|
+
<DataPanelHeader
|
|
1099
|
+
{mode}
|
|
1100
|
+
{edge}
|
|
1101
|
+
isExpanded={true}
|
|
1102
|
+
disableModeSwitch={false}
|
|
1103
|
+
onModeChange={handleModeChange}
|
|
1104
|
+
onEdgeChange={handleEdgeChange}
|
|
1105
|
+
showModeMenuOnly
|
|
1106
|
+
/>
|
|
1107
|
+
{/if}
|
|
1108
|
+
|
|
1109
|
+
<!-- Close button -->
|
|
1110
|
+
{#if !disableClose}
|
|
1111
|
+
<button
|
|
1112
|
+
type="button"
|
|
1113
|
+
class="p-1.5 rounded hover:bg-muted"
|
|
1114
|
+
onclick={handleClose}
|
|
1115
|
+
aria-label="Close panel"
|
|
1116
|
+
title="Close"
|
|
1117
|
+
>
|
|
1118
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1119
|
+
<path
|
|
1120
|
+
stroke-linecap="round"
|
|
1121
|
+
stroke-linejoin="round"
|
|
1122
|
+
stroke-width="2"
|
|
1123
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1124
|
+
/>
|
|
1125
|
+
</svg>
|
|
1126
|
+
</button>
|
|
1127
|
+
{/if}
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<!-- Content (hidden when card is collapsed) -->
|
|
1132
|
+
{#if showContent}
|
|
1133
|
+
<DataPanelContent>
|
|
1134
|
+
{@render children()}
|
|
1135
|
+
</DataPanelContent>
|
|
1136
|
+
|
|
1137
|
+
<!-- Footer -->
|
|
1138
|
+
<DataPanelFooter {actions} {footer} {footerActions} />
|
|
1139
|
+
{/if}
|
|
1140
|
+
|
|
1141
|
+
<!-- Resize handles (full variant only) -->
|
|
1142
|
+
{#if variant === 'full' && !disableResize}
|
|
1143
|
+
{#if mode === 'detached'}
|
|
1144
|
+
<!-- Edge handles -->
|
|
1145
|
+
<div
|
|
1146
|
+
class="resize-handle resize-handle--horizontal resize-handle--top"
|
|
1147
|
+
onpointerdown={(e) => handleResizeStart(e, 'top')}
|
|
1148
|
+
></div>
|
|
1149
|
+
<div
|
|
1150
|
+
class="resize-handle resize-handle--horizontal resize-handle--bottom"
|
|
1151
|
+
onpointerdown={(e) => handleResizeStart(e, 'bottom')}
|
|
1152
|
+
></div>
|
|
1153
|
+
<div
|
|
1154
|
+
class="resize-handle resize-handle--vertical resize-handle--left"
|
|
1155
|
+
onpointerdown={(e) => handleResizeStart(e, 'left')}
|
|
1156
|
+
></div>
|
|
1157
|
+
<div
|
|
1158
|
+
class="resize-handle resize-handle--vertical resize-handle--right"
|
|
1159
|
+
onpointerdown={(e) => handleResizeStart(e, 'right')}
|
|
1160
|
+
></div>
|
|
1161
|
+
<!-- Corner handles (6px radius = 8px panel radius minus 2px offset) -->
|
|
1162
|
+
<div
|
|
1163
|
+
class="resize-handle resize-handle--corner resize-handle--top-left"
|
|
1164
|
+
onpointerdown={(e) => handleResizeStart(e, 'top-left')}
|
|
1165
|
+
></div>
|
|
1166
|
+
<div
|
|
1167
|
+
class="resize-handle resize-handle--corner resize-handle--top-right"
|
|
1168
|
+
onpointerdown={(e) => handleResizeStart(e, 'top-right')}
|
|
1169
|
+
></div>
|
|
1170
|
+
<div
|
|
1171
|
+
class="resize-handle resize-handle--corner resize-handle--bottom-left"
|
|
1172
|
+
onpointerdown={(e) => handleResizeStart(e, 'bottom-left')}
|
|
1173
|
+
></div>
|
|
1174
|
+
<div
|
|
1175
|
+
class="resize-handle resize-handle--corner resize-handle--bottom-right"
|
|
1176
|
+
onpointerdown={(e) => handleResizeStart(e, 'bottom-right')}
|
|
1177
|
+
></div>
|
|
1178
|
+
{:else}
|
|
1179
|
+
<!-- Single resize handle for pinned mode -->
|
|
1180
|
+
{@const pinnedHandle = getPinnedResizeHandle(edge)}
|
|
1181
|
+
<div
|
|
1182
|
+
class={cn(
|
|
1183
|
+
'resize-handle',
|
|
1184
|
+
edge === 'left' && 'resize-handle--vertical resize-handle--right',
|
|
1185
|
+
edge === 'right' && 'resize-handle--vertical resize-handle--left',
|
|
1186
|
+
edge === 'top' && 'resize-handle--horizontal resize-handle--bottom',
|
|
1187
|
+
edge === 'bottom' && 'resize-handle--horizontal resize-handle--top'
|
|
1188
|
+
)}
|
|
1189
|
+
onpointerdown={(e) => handleResizeStart(e, pinnedHandle)}
|
|
1190
|
+
></div>
|
|
1191
|
+
{/if}
|
|
1192
|
+
{/if}
|
|
1193
|
+
|
|
1194
|
+
<!-- Card resize handles (top edge for height, left/right for width - desktop only when expanded) -->
|
|
1195
|
+
{#if variant === 'card' && isExpanded && !disableResize && !isMobile}
|
|
1196
|
+
<!-- Top edge for height resize -->
|
|
1197
|
+
<div
|
|
1198
|
+
class="resize-handle resize-handle--horizontal resize-handle--top"
|
|
1199
|
+
onpointerdown={handleCardResizeStart}
|
|
1200
|
+
></div>
|
|
1201
|
+
<!-- Left edge for width resize -->
|
|
1202
|
+
<div
|
|
1203
|
+
class="resize-handle resize-handle--vertical resize-handle--left resize-handle--card-edge"
|
|
1204
|
+
onpointerdown={(e) => handleCardWidthResizeStart(e, 'left')}
|
|
1205
|
+
></div>
|
|
1206
|
+
<!-- Right edge for width resize -->
|
|
1207
|
+
<div
|
|
1208
|
+
class="resize-handle resize-handle--vertical resize-handle--right resize-handle--card-edge"
|
|
1209
|
+
onpointerdown={(e) => handleCardWidthResizeStart(e, 'right')}
|
|
1210
|
+
></div>
|
|
1211
|
+
{/if}
|
|
1212
|
+
</div>
|
|
1213
|
+
{/if}
|
|
1214
|
+
{/if}
|
|
1215
|
+
|
|
1216
|
+
<style>
|
|
1217
|
+
/* Base resize handle - invisible hit area */
|
|
1218
|
+
.resize-handle {
|
|
1219
|
+
position: absolute;
|
|
1220
|
+
z-index: 10;
|
|
1221
|
+
touch-action: none;
|
|
1222
|
+
background: transparent;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/* Edge handles - horizontal (top/bottom) */
|
|
1226
|
+
.resize-handle--horizontal {
|
|
1227
|
+
left: 14px;
|
|
1228
|
+
right: 14px;
|
|
1229
|
+
height: 8px;
|
|
1230
|
+
cursor: ns-resize;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.resize-handle--horizontal::after {
|
|
1234
|
+
content: '';
|
|
1235
|
+
position: absolute;
|
|
1236
|
+
left: 0;
|
|
1237
|
+
right: 0;
|
|
1238
|
+
height: 2px;
|
|
1239
|
+
background: transparent;
|
|
1240
|
+
transition: background-color 0.15s ease;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.resize-handle--top {
|
|
1244
|
+
top: 2px;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.resize-handle--top::after {
|
|
1248
|
+
top: 0;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.resize-handle--bottom {
|
|
1252
|
+
bottom: 2px;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.resize-handle--bottom::after {
|
|
1256
|
+
bottom: 0;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/* Edge handles - vertical (left/right) */
|
|
1260
|
+
.resize-handle--vertical {
|
|
1261
|
+
top: 14px;
|
|
1262
|
+
bottom: 14px;
|
|
1263
|
+
width: 8px;
|
|
1264
|
+
cursor: ew-resize;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.resize-handle--vertical::after {
|
|
1268
|
+
content: '';
|
|
1269
|
+
position: absolute;
|
|
1270
|
+
top: 0;
|
|
1271
|
+
bottom: 0;
|
|
1272
|
+
width: 2px;
|
|
1273
|
+
background: transparent;
|
|
1274
|
+
transition: background-color 0.15s ease;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.resize-handle--left {
|
|
1278
|
+
left: 2px;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.resize-handle--left::after {
|
|
1282
|
+
left: 0;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.resize-handle--right {
|
|
1286
|
+
right: 2px;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.resize-handle--right::after {
|
|
1290
|
+
right: 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/* Hover state for edge handles */
|
|
1294
|
+
.resize-handle--horizontal:hover::after,
|
|
1295
|
+
.resize-handle--vertical:hover::after {
|
|
1296
|
+
background: hsl(var(--border));
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/* Card edge handles - extend to bottom edge (no bottom corners) */
|
|
1300
|
+
.resize-handle--card-edge {
|
|
1301
|
+
top: 14px;
|
|
1302
|
+
bottom: 2px;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/* Corner handles - quarter-circle arcs matching panel border-radius */
|
|
1306
|
+
.resize-handle--corner {
|
|
1307
|
+
width: 20px;
|
|
1308
|
+
height: 20px;
|
|
1309
|
+
background: transparent;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
.resize-handle--corner::after {
|
|
1313
|
+
content: '';
|
|
1314
|
+
position: absolute;
|
|
1315
|
+
/* Element size slightly larger than radius for good hover target */
|
|
1316
|
+
width: 10px;
|
|
1317
|
+
height: 10px;
|
|
1318
|
+
/* !important needed to override Tailwind preflight reset (border-width: 0) */
|
|
1319
|
+
border: 2px solid transparent !important;
|
|
1320
|
+
transition:
|
|
1321
|
+
border-top-color 0.15s ease,
|
|
1322
|
+
border-right-color 0.15s ease,
|
|
1323
|
+
border-bottom-color 0.15s ease,
|
|
1324
|
+
border-left-color 0.15s ease;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/* Corner positions */
|
|
1328
|
+
.resize-handle--top-left {
|
|
1329
|
+
top: 2px;
|
|
1330
|
+
left: 2px;
|
|
1331
|
+
cursor: nwse-resize;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.resize-handle--top-left::after {
|
|
1335
|
+
top: 0;
|
|
1336
|
+
left: 0;
|
|
1337
|
+
/* Match panel's 8px border-radius for smooth visual continuity */
|
|
1338
|
+
border-top-left-radius: 8px;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
.resize-handle--top-left:hover::after {
|
|
1342
|
+
border-top-color: hsl(var(--border)) !important;
|
|
1343
|
+
border-left-color: hsl(var(--border)) !important;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.resize-handle--top-right {
|
|
1347
|
+
top: 2px;
|
|
1348
|
+
right: 2px;
|
|
1349
|
+
cursor: nesw-resize;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.resize-handle--top-right::after {
|
|
1353
|
+
top: 0;
|
|
1354
|
+
right: 0;
|
|
1355
|
+
/* Match panel's 8px border-radius for smooth visual continuity */
|
|
1356
|
+
border-top-right-radius: 8px;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.resize-handle--top-right:hover::after {
|
|
1360
|
+
border-top-color: hsl(var(--border)) !important;
|
|
1361
|
+
border-right-color: hsl(var(--border)) !important;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.resize-handle--bottom-left {
|
|
1365
|
+
bottom: 2px;
|
|
1366
|
+
left: 2px;
|
|
1367
|
+
cursor: nesw-resize;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.resize-handle--bottom-left::after {
|
|
1371
|
+
bottom: 0;
|
|
1372
|
+
left: 0;
|
|
1373
|
+
/* Match panel's 8px border-radius for smooth visual continuity */
|
|
1374
|
+
border-bottom-left-radius: 8px;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.resize-handle--bottom-left:hover::after {
|
|
1378
|
+
border-bottom-color: hsl(var(--border)) !important;
|
|
1379
|
+
border-left-color: hsl(var(--border)) !important;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.resize-handle--bottom-right {
|
|
1383
|
+
bottom: 2px;
|
|
1384
|
+
right: 2px;
|
|
1385
|
+
cursor: nwse-resize;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.resize-handle--bottom-right::after {
|
|
1389
|
+
bottom: 0;
|
|
1390
|
+
right: 0;
|
|
1391
|
+
/* Match panel's 8px border-radius for smooth visual continuity */
|
|
1392
|
+
border-bottom-right-radius: 8px;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.resize-handle--bottom-right:hover::after {
|
|
1396
|
+
border-bottom-color: hsl(var(--border)) !important;
|
|
1397
|
+
border-right-color: hsl(var(--border)) !important;
|
|
1398
|
+
}
|
|
1399
|
+
</style>
|