@classic-homes/theme-svelte 0.1.56 → 1.0.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/dist/__tests__/nav-access.test.js +145 -0
- package/dist/__tests__/sidebar-expansion.test.js +59 -0
- package/dist/lib/components/DataPanel.svelte +383 -114
- package/dist/lib/components/DataPanelHeader.svelte +4 -4
- package/dist/lib/components/DataPanelTab.svelte +212 -44
- package/dist/lib/components/DataPanelTab.svelte.d.ts +10 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +1 -0
- package/dist/lib/components/layout/Sidebar.svelte +22 -17
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +10 -1
- package/dist/lib/schemas/auth.d.ts +18 -8
- package/dist/lib/schemas/auth.js +4 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/stores/dataPanel.svelte.d.ts +2 -0
- package/dist/lib/stores/dataPanel.svelte.js +14 -0
- package/dist/lib/stores/sidebar.svelte.d.ts +7 -2
- package/dist/lib/stores/sidebar.svelte.js +9 -3
- package/dist/lib/types/layout.d.ts +52 -4
- package/dist/lib/utils/nav-access.d.ts +61 -0
- package/dist/lib/utils/nav-access.js +74 -0
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
getPinnedResizeHandle,
|
|
30
30
|
constrainSize,
|
|
31
31
|
constrainPosition,
|
|
32
|
+
constrainResize,
|
|
32
33
|
constrainPinnedWidth,
|
|
33
34
|
constrainPinnedHeight,
|
|
34
35
|
detectEdgeSnap,
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
import DataPanelContent from './DataPanelContent.svelte';
|
|
48
49
|
import DataPanelFooter from './DataPanelFooter.svelte';
|
|
49
50
|
import DataPanelTab from './DataPanelTab.svelte';
|
|
51
|
+
import { useFocusTrap } from '../composables/useFocusTrap.svelte.js';
|
|
52
|
+
import { useEscapeKey } from '../composables/useEscapeKey.svelte.js';
|
|
50
53
|
|
|
51
54
|
interface Props {
|
|
52
55
|
/** Panel visibility */
|
|
@@ -150,6 +153,8 @@
|
|
|
150
153
|
let detachedPosition = $state<Position>(DEFAULT_PANEL_STATE.detachedPosition);
|
|
151
154
|
let detachedSize = $state<Size>(DEFAULT_PANEL_STATE.detachedSize);
|
|
152
155
|
let pinnedSize = $state<number>(DEFAULT_PANEL_STATE.pinnedSize);
|
|
156
|
+
// Offset of the collapsed pinned tab along its edge
|
|
157
|
+
let internalTabOffset = $state<number>(DEFAULT_PANEL_STATE.tabOffset);
|
|
153
158
|
|
|
154
159
|
// Card position state (horizontal position for card mode)
|
|
155
160
|
let cardHorizontalPosition = $state<number>(0);
|
|
@@ -211,6 +216,18 @@
|
|
|
211
216
|
const edge = $derived(propEdge ?? internalEdge);
|
|
212
217
|
const isExpanded = $derived(propExpanded ?? internalExpanded);
|
|
213
218
|
|
|
219
|
+
// Whether the panel is currently presented as a modal dialog (detached, or an
|
|
220
|
+
// expanded mobile card). In that state we trap focus for a11y parity with React.
|
|
221
|
+
// A desktop card is a non-modal floating sheet with no scrim, so it must not
|
|
222
|
+
// trap focus — gate the card branch on isMobile.
|
|
223
|
+
const isDialog = $derived(mode === 'detached' || (variant === 'card' && isExpanded && isMobile));
|
|
224
|
+
|
|
225
|
+
// Trap focus inside the panel while it acts as a dialog; return focus on close.
|
|
226
|
+
const { action: focusTrap } = useFocusTrap({ active: () => open && isDialog });
|
|
227
|
+
|
|
228
|
+
// Close on Escape (all modes, matching the previous behavior) unless disabled.
|
|
229
|
+
useEscapeKey(() => handleClose(), { enabled: () => open && !disableClose });
|
|
230
|
+
|
|
214
231
|
// Save timeout for debounced persistence
|
|
215
232
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
216
233
|
|
|
@@ -233,6 +250,7 @@
|
|
|
233
250
|
detachedSize,
|
|
234
251
|
pinnedSize,
|
|
235
252
|
cardSnapIndex: 0, // Legacy field
|
|
253
|
+
tabOffset: internalTabOffset,
|
|
236
254
|
});
|
|
237
255
|
saveTimeout = null;
|
|
238
256
|
}, 300);
|
|
@@ -240,6 +258,13 @@
|
|
|
240
258
|
|
|
241
259
|
onDestroy(() => {
|
|
242
260
|
if (saveTimeout) clearTimeout(saveTimeout);
|
|
261
|
+
// If we unmount mid-drag/resize (e.g. Escape closes the panel), the *End
|
|
262
|
+
// handlers never run — reset the body styles they own so the page isn't
|
|
263
|
+
// left stuck at grabbing cursor / no user-select.
|
|
264
|
+
if (typeof document !== 'undefined') {
|
|
265
|
+
document.body.style.cursor = '';
|
|
266
|
+
document.body.style.userSelect = '';
|
|
267
|
+
}
|
|
243
268
|
});
|
|
244
269
|
|
|
245
270
|
// Initialize from persistence on mount
|
|
@@ -270,6 +295,7 @@
|
|
|
270
295
|
if (persisted.detachedPosition) detachedPosition = persisted.detachedPosition;
|
|
271
296
|
if (persisted.detachedSize) detachedSize = persisted.detachedSize;
|
|
272
297
|
if (persisted.pinnedSize) pinnedSize = persisted.pinnedSize;
|
|
298
|
+
if (persisted.tabOffset !== undefined) internalTabOffset = persisted.tabOffset;
|
|
273
299
|
}
|
|
274
300
|
}
|
|
275
301
|
|
|
@@ -287,17 +313,10 @@
|
|
|
287
313
|
}
|
|
288
314
|
}
|
|
289
315
|
|
|
290
|
-
//
|
|
291
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
292
|
-
if (e.key === 'Escape' && !disableClose) {
|
|
293
|
-
handleClose();
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
316
|
+
// Escape-to-close is handled by useEscapeKey; focus trapping by useFocusTrap.
|
|
297
317
|
|
|
298
318
|
return () => {
|
|
299
319
|
window.removeEventListener('resize', handleResize);
|
|
300
|
-
window.removeEventListener('keydown', handleKeyDown);
|
|
301
320
|
};
|
|
302
321
|
});
|
|
303
322
|
|
|
@@ -347,6 +366,18 @@
|
|
|
347
366
|
handleExpandedChange(!isExpanded);
|
|
348
367
|
}
|
|
349
368
|
|
|
369
|
+
// Header collapse button. A detached full panel collapses by re-pinning to its
|
|
370
|
+
// last edge (edge always holds the previous pin, or the default) and showing the
|
|
371
|
+
// collapsed tab. Pinned/card just toggle expanded.
|
|
372
|
+
function handleCollapseToggle() {
|
|
373
|
+
if (variant === 'full' && mode === 'detached') {
|
|
374
|
+
handleModeChange('pinned');
|
|
375
|
+
handleExpandedChange(false);
|
|
376
|
+
} else {
|
|
377
|
+
handleToggleExpand();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
350
381
|
// Drag handlers for detached mode
|
|
351
382
|
function handleDragStart(e: PointerEvent) {
|
|
352
383
|
if (disableDrag) return;
|
|
@@ -632,13 +663,16 @@
|
|
|
632
663
|
currentPos
|
|
633
664
|
);
|
|
634
665
|
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
666
|
+
// Route through constrainResize so a left/top/corner resize that clamps to
|
|
667
|
+
// min/max compensates the position and keeps the anchored (right/bottom)
|
|
668
|
+
// edge fixed instead of drifting.
|
|
669
|
+
const { position: constrainedPosition, size: constrainedSize } = constrainResize(
|
|
638
670
|
result.position,
|
|
639
|
-
|
|
671
|
+
result.size,
|
|
672
|
+
resolvedConstraints,
|
|
640
673
|
window.innerWidth,
|
|
641
|
-
window.innerHeight
|
|
674
|
+
window.innerHeight,
|
|
675
|
+
resizeHandle
|
|
642
676
|
);
|
|
643
677
|
|
|
644
678
|
detachedSize = constrainedSize;
|
|
@@ -670,6 +704,130 @@
|
|
|
670
704
|
debouncedSave();
|
|
671
705
|
}
|
|
672
706
|
|
|
707
|
+
// Keyboard resize (arrow keys) — parity with the React ResizeHandles slider.
|
|
708
|
+
const KEYBOARD_RESIZE_STEP = 10; // pixels per key press
|
|
709
|
+
const KEYBOARD_RESIZE_STEP_LARGE = 50; // pixels per shift+key press
|
|
710
|
+
|
|
711
|
+
function handleResizeKeyDown(e: KeyboardEvent, handle: ResizeHandle) {
|
|
712
|
+
if (disableResize) return;
|
|
713
|
+
const step = e.shiftKey ? KEYBOARD_RESIZE_STEP_LARGE : KEYBOARD_RESIZE_STEP;
|
|
714
|
+
|
|
715
|
+
if (mode === 'detached') {
|
|
716
|
+
let width = detachedSize.width;
|
|
717
|
+
let height = detachedSize.height;
|
|
718
|
+
let handled = false;
|
|
719
|
+
|
|
720
|
+
switch (e.key) {
|
|
721
|
+
case 'ArrowUp':
|
|
722
|
+
if (handle.includes('top')) ((height += step), (handled = true));
|
|
723
|
+
else if (handle.includes('bottom')) ((height -= step), (handled = true));
|
|
724
|
+
break;
|
|
725
|
+
case 'ArrowDown':
|
|
726
|
+
if (handle.includes('top')) ((height -= step), (handled = true));
|
|
727
|
+
else if (handle.includes('bottom')) ((height += step), (handled = true));
|
|
728
|
+
break;
|
|
729
|
+
case 'ArrowLeft':
|
|
730
|
+
if (handle.includes('left')) ((width += step), (handled = true));
|
|
731
|
+
else if (handle.includes('right')) ((width -= step), (handled = true));
|
|
732
|
+
break;
|
|
733
|
+
case 'ArrowRight':
|
|
734
|
+
if (handle.includes('left')) ((width -= step), (handled = true));
|
|
735
|
+
else if (handle.includes('right')) ((width += step), (handled = true));
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (handled) {
|
|
740
|
+
e.preventDefault();
|
|
741
|
+
// Left/top handles move the panel so the anchored (right/bottom) edge
|
|
742
|
+
// stays put as the size changes; right/bottom handles keep position.
|
|
743
|
+
// constrainResize then clamps size and compensates the position on clamp.
|
|
744
|
+
const candidatePosition = {
|
|
745
|
+
x: handle.includes('left')
|
|
746
|
+
? detachedPosition.x + (detachedSize.width - width)
|
|
747
|
+
: detachedPosition.x,
|
|
748
|
+
y: handle.includes('top')
|
|
749
|
+
? detachedPosition.y + (detachedSize.height - height)
|
|
750
|
+
: detachedPosition.y,
|
|
751
|
+
};
|
|
752
|
+
const { position, size } = constrainResize(
|
|
753
|
+
candidatePosition,
|
|
754
|
+
{ width, height },
|
|
755
|
+
resolvedConstraints,
|
|
756
|
+
window.innerWidth,
|
|
757
|
+
window.innerHeight,
|
|
758
|
+
handle
|
|
759
|
+
);
|
|
760
|
+
detachedSize = size;
|
|
761
|
+
detachedPosition = position;
|
|
762
|
+
debouncedSave();
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
// Pinned mode - single handle grows/shrinks perpendicular to the edge.
|
|
766
|
+
const horizontal = isHorizontalEdge(edge);
|
|
767
|
+
let newSize = pinnedSize;
|
|
768
|
+
let handled = false;
|
|
769
|
+
|
|
770
|
+
if (horizontal) {
|
|
771
|
+
if (
|
|
772
|
+
(e.key === 'ArrowLeft' && edge === 'right') ||
|
|
773
|
+
(e.key === 'ArrowRight' && edge === 'left')
|
|
774
|
+
)
|
|
775
|
+
((newSize += step), (handled = true));
|
|
776
|
+
else if (
|
|
777
|
+
(e.key === 'ArrowRight' && edge === 'right') ||
|
|
778
|
+
(e.key === 'ArrowLeft' && edge === 'left')
|
|
779
|
+
)
|
|
780
|
+
((newSize -= step), (handled = true));
|
|
781
|
+
} else {
|
|
782
|
+
if ((e.key === 'ArrowUp' && edge === 'bottom') || (e.key === 'ArrowDown' && edge === 'top'))
|
|
783
|
+
((newSize += step), (handled = true));
|
|
784
|
+
else if (
|
|
785
|
+
(e.key === 'ArrowDown' && edge === 'bottom') ||
|
|
786
|
+
(e.key === 'ArrowUp' && edge === 'top')
|
|
787
|
+
)
|
|
788
|
+
((newSize -= step), (handled = true));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (handled) {
|
|
792
|
+
e.preventDefault();
|
|
793
|
+
pinnedSize = horizontal
|
|
794
|
+
? constrainPinnedWidth(newSize, resolvedConstraints, window.innerWidth)
|
|
795
|
+
: constrainPinnedHeight(newSize, resolvedConstraints, window.innerHeight);
|
|
796
|
+
debouncedSave();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// aria-value* for the resize sliders.
|
|
802
|
+
function resizeAriaNow(handle: ResizeHandle): number {
|
|
803
|
+
const isWidth = handle.includes('left') || handle.includes('right');
|
|
804
|
+
if (mode === 'detached') return isWidth ? detachedSize.width : detachedSize.height;
|
|
805
|
+
return pinnedSize;
|
|
806
|
+
}
|
|
807
|
+
function resizeAriaMin(handle: ResizeHandle): number {
|
|
808
|
+
const isWidth = handle.includes('left') || handle.includes('right');
|
|
809
|
+
return isWidth ? resolvedConstraints.minWidth : resolvedConstraints.minHeight;
|
|
810
|
+
}
|
|
811
|
+
function resizeAriaMax(handle: ResizeHandle): number {
|
|
812
|
+
const isWidth = handle.includes('left') || handle.includes('right');
|
|
813
|
+
return isWidth ? resolvedConstraints.maxWidth : resolvedConstraints.maxHeight;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// The eight detached resize handles, driving the {#each} in the template.
|
|
817
|
+
const detachedResizeHandles: {
|
|
818
|
+
handle: ResizeHandle;
|
|
819
|
+
orientation: 'horizontal' | 'vertical' | null;
|
|
820
|
+
}[] = [
|
|
821
|
+
{ handle: 'top', orientation: 'horizontal' },
|
|
822
|
+
{ handle: 'bottom', orientation: 'horizontal' },
|
|
823
|
+
{ handle: 'left', orientation: 'vertical' },
|
|
824
|
+
{ handle: 'right', orientation: 'vertical' },
|
|
825
|
+
{ handle: 'top-left', orientation: null },
|
|
826
|
+
{ handle: 'top-right', orientation: null },
|
|
827
|
+
{ handle: 'bottom-left', orientation: null },
|
|
828
|
+
{ handle: 'bottom-right', orientation: null },
|
|
829
|
+
];
|
|
830
|
+
|
|
673
831
|
// Card resize (top edge only when expanded)
|
|
674
832
|
function handleCardResizeStart(e: PointerEvent) {
|
|
675
833
|
if (variant !== 'card' || !isExpanded || disableResize) return;
|
|
@@ -780,6 +938,63 @@
|
|
|
780
938
|
debouncedSave();
|
|
781
939
|
}
|
|
782
940
|
|
|
941
|
+
// The current desktop card width (falls back to the computed default).
|
|
942
|
+
const cardDefaultWidth = $derived(
|
|
943
|
+
Math.min(viewportWidth - resolvedCardConfig.horizontalMargin * 2, resolvedCardConfig.maxWidth)
|
|
944
|
+
);
|
|
945
|
+
const cardCurrentWidth = $derived(cardWidth ?? cardDefaultWidth);
|
|
946
|
+
const cardHeightMin = $derived(CARD_HEADER_HEIGHT * 2);
|
|
947
|
+
const cardHeightMax = $derived(Math.round(viewportHeight * 0.95));
|
|
948
|
+
|
|
949
|
+
// Keyboard resize for the desktop card handles (parity with the full-variant
|
|
950
|
+
// sliders). Top edge steps height; left/right edges step width, with the left
|
|
951
|
+
// edge shifting position so the opposite edge stays anchored.
|
|
952
|
+
function handleCardResizeKeyDown(
|
|
953
|
+
e: KeyboardEvent,
|
|
954
|
+
kind: 'height' | 'width-left' | 'width-right'
|
|
955
|
+
) {
|
|
956
|
+
if (disableResize) return;
|
|
957
|
+
const step = e.shiftKey ? KEYBOARD_RESIZE_STEP_LARGE : KEYBOARD_RESIZE_STEP;
|
|
958
|
+
|
|
959
|
+
if (kind === 'height') {
|
|
960
|
+
let delta = 0;
|
|
961
|
+
if (e.key === 'ArrowUp') delta = step;
|
|
962
|
+
else if (e.key === 'ArrowDown') delta = -step;
|
|
963
|
+
else return;
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
cardExpandedHeight = Math.max(
|
|
966
|
+
cardHeightMin,
|
|
967
|
+
Math.min(cardHeightMax, cardExpandedHeight + delta)
|
|
968
|
+
);
|
|
969
|
+
debouncedSave();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Width handles.
|
|
974
|
+
let widthDelta = 0;
|
|
975
|
+
if (kind === 'width-right') {
|
|
976
|
+
if (e.key === 'ArrowRight') widthDelta = step;
|
|
977
|
+
else if (e.key === 'ArrowLeft') widthDelta = -step;
|
|
978
|
+
} else {
|
|
979
|
+
// Left edge: ArrowLeft grows leftward, ArrowRight shrinks.
|
|
980
|
+
if (e.key === 'ArrowLeft') widthDelta = step;
|
|
981
|
+
else if (e.key === 'ArrowRight') widthDelta = -step;
|
|
982
|
+
}
|
|
983
|
+
if (widthDelta === 0) return;
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
|
|
986
|
+
const maxWidth = Math.min(
|
|
987
|
+
resolvedCardConfig.maxWidth,
|
|
988
|
+
viewportWidth - resolvedCardConfig.horizontalMargin * 2
|
|
989
|
+
);
|
|
990
|
+
const nextWidth = Math.max(CARD_MIN_WIDTH, Math.min(maxWidth, cardCurrentWidth + widthDelta));
|
|
991
|
+
const appliedDelta = nextWidth - cardCurrentWidth;
|
|
992
|
+
cardWidth = nextWidth;
|
|
993
|
+
// Left edge keeps its right edge fixed: growing width moves the panel left.
|
|
994
|
+
if (kind === 'width-left') cardHorizontalPosition -= appliedDelta;
|
|
995
|
+
debouncedSave();
|
|
996
|
+
}
|
|
997
|
+
|
|
783
998
|
// Global event listeners
|
|
784
999
|
function handlePointerMove(e: PointerEvent) {
|
|
785
1000
|
if (isDragging) handleDragMove(e);
|
|
@@ -810,9 +1025,13 @@
|
|
|
810
1025
|
) {
|
|
811
1026
|
window.addEventListener('pointermove', handlePointerMove);
|
|
812
1027
|
window.addEventListener('pointerup', handlePointerUp);
|
|
1028
|
+
// Treat a canceled pointer (OS gesture, scroll) like pointerup so we don't
|
|
1029
|
+
// get stuck mid-interaction with body styles applied.
|
|
1030
|
+
window.addEventListener('pointercancel', handlePointerUp);
|
|
813
1031
|
return () => {
|
|
814
1032
|
window.removeEventListener('pointermove', handlePointerMove);
|
|
815
1033
|
window.removeEventListener('pointerup', handlePointerUp);
|
|
1034
|
+
window.removeEventListener('pointercancel', handlePointerUp);
|
|
816
1035
|
};
|
|
817
1036
|
}
|
|
818
1037
|
});
|
|
@@ -838,7 +1057,7 @@
|
|
|
838
1057
|
});
|
|
839
1058
|
|
|
840
1059
|
// Calculate panel styles
|
|
841
|
-
const panelStyles = $derived(() => {
|
|
1060
|
+
const panelStyles = $derived.by(() => {
|
|
842
1061
|
if (variant === 'card') {
|
|
843
1062
|
// Card mode - bottom sheet style
|
|
844
1063
|
const defaultCardWidth = Math.min(
|
|
@@ -895,7 +1114,7 @@
|
|
|
895
1114
|
);
|
|
896
1115
|
|
|
897
1116
|
// Animation classes
|
|
898
|
-
const animationClass = $derived(() => {
|
|
1117
|
+
const animationClass = $derived.by(() => {
|
|
899
1118
|
if (variant === 'card') {
|
|
900
1119
|
return 'animate-panel-slide-in-bottom';
|
|
901
1120
|
}
|
|
@@ -912,12 +1131,36 @@
|
|
|
912
1131
|
|
|
913
1132
|
// Whether to show content (collapsed card only shows header)
|
|
914
1133
|
const showContent = $derived(variant !== 'card' || isExpanded || isCardVerticalDragging);
|
|
1134
|
+
|
|
1135
|
+
// Rotation for the header collapse chevron (base points up). For the full
|
|
1136
|
+
// variant it points toward the edge the panel collapses to — the pinned edge,
|
|
1137
|
+
// or, when detached, the edge it will re-pin to. The card toggles up (expand) /
|
|
1138
|
+
// down (collapse).
|
|
1139
|
+
const collapseChevronRotation = $derived<string>(
|
|
1140
|
+
variant === 'full'
|
|
1141
|
+
? { top: '', bottom: 'rotate-180', left: '-rotate-90', right: 'rotate-90' }[edge]
|
|
1142
|
+
: isExpanded
|
|
1143
|
+
? 'rotate-180'
|
|
1144
|
+
: ''
|
|
1145
|
+
);
|
|
915
1146
|
</script>
|
|
916
1147
|
|
|
917
1148
|
{#if open}
|
|
918
1149
|
{#if variant === 'full' && !isExpanded && mode === 'pinned'}
|
|
919
1150
|
<!-- Collapsed tab (full variant only) -->
|
|
920
|
-
<DataPanelTab
|
|
1151
|
+
<DataPanelTab
|
|
1152
|
+
{title}
|
|
1153
|
+
{subtitle}
|
|
1154
|
+
{edge}
|
|
1155
|
+
{disableClose}
|
|
1156
|
+
onclick={handleTabClick}
|
|
1157
|
+
onClose={handleClose}
|
|
1158
|
+
offset={internalTabOffset}
|
|
1159
|
+
onOffsetChange={(o) => {
|
|
1160
|
+
internalTabOffset = o;
|
|
1161
|
+
debouncedSave();
|
|
1162
|
+
}}
|
|
1163
|
+
/>
|
|
921
1164
|
{:else}
|
|
922
1165
|
<!-- Snap preview indicator (detached mode only) -->
|
|
923
1166
|
{#if snapPreview}
|
|
@@ -932,21 +1175,21 @@
|
|
|
932
1175
|
></div>
|
|
933
1176
|
{/if}
|
|
934
1177
|
|
|
935
|
-
<!--
|
|
1178
|
+
<!-- Presentational scrim for the expanded mobile card. Keyboard users dismiss
|
|
1179
|
+
via Escape (useEscapeKey); the backdrop is click-to-dismiss only. -->
|
|
936
1180
|
{#if variant === 'card' && isExpanded && isMobile}
|
|
1181
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
937
1182
|
<div
|
|
938
1183
|
class="fixed inset-0 z-40 bg-black/50 transition-opacity"
|
|
939
1184
|
onclick={handleToggleExpand}
|
|
940
|
-
|
|
941
|
-
role="button"
|
|
942
|
-
tabindex="-1"
|
|
943
|
-
aria-label="Close panel"
|
|
1185
|
+
aria-hidden="true"
|
|
944
1186
|
></div>
|
|
945
1187
|
{/if}
|
|
946
1188
|
|
|
947
1189
|
<!-- Panel -->
|
|
948
1190
|
<div
|
|
949
1191
|
bind:this={panelRef}
|
|
1192
|
+
use:focusTrap
|
|
950
1193
|
class={cn(
|
|
951
1194
|
'fixed z-50 flex flex-col bg-background border border-border shadow-lg overflow-hidden',
|
|
952
1195
|
variant === 'full' && mode === 'pinned' && pinnedClasses,
|
|
@@ -960,14 +1203,14 @@
|
|
|
960
1203
|
!isResizing &&
|
|
961
1204
|
!isCardWidthResizing &&
|
|
962
1205
|
'transition-[height,left,width,transform] duration-300 ease-out',
|
|
963
|
-
animationClass
|
|
1206
|
+
animationClass,
|
|
964
1207
|
className
|
|
965
1208
|
)}
|
|
966
|
-
style={Object.entries(panelStyles
|
|
1209
|
+
style={Object.entries(panelStyles)
|
|
967
1210
|
.map(([k, v]) => `${k}:${v}`)
|
|
968
1211
|
.join(';')}
|
|
969
1212
|
role={mode === 'detached' || variant === 'card' ? 'dialog' : 'complementary'}
|
|
970
|
-
aria-modal={
|
|
1213
|
+
aria-modal={isDialog ? 'true' : undefined}
|
|
971
1214
|
aria-labelledby={title ? 'data-panel-title' : undefined}
|
|
972
1215
|
aria-expanded={variant === 'card' ? isExpanded : undefined}
|
|
973
1216
|
tabindex="-1"
|
|
@@ -1052,18 +1295,13 @@
|
|
|
1052
1295
|
title="Drag to move"
|
|
1053
1296
|
onpointerdown={handleCardHeaderPointerDown}
|
|
1054
1297
|
>
|
|
1055
|
-
<svg
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
<
|
|
1062
|
-
stroke-linecap="round"
|
|
1063
|
-
stroke-linejoin="round"
|
|
1064
|
-
stroke-width="2"
|
|
1065
|
-
d="M4 8h16M4 16h16"
|
|
1066
|
-
/>
|
|
1298
|
+
<svg class="w-4 h-4 text-muted-foreground" viewBox="0 0 24 24" fill="currentColor">
|
|
1299
|
+
<circle cx="9" cy="6" r="1.5" />
|
|
1300
|
+
<circle cx="15" cy="6" r="1.5" />
|
|
1301
|
+
<circle cx="9" cy="12" r="1.5" />
|
|
1302
|
+
<circle cx="15" cy="12" r="1.5" />
|
|
1303
|
+
<circle cx="9" cy="18" r="1.5" />
|
|
1304
|
+
<circle cx="15" cy="18" r="1.5" />
|
|
1067
1305
|
</svg>
|
|
1068
1306
|
</button>
|
|
1069
1307
|
{/if}
|
|
@@ -1073,30 +1311,27 @@
|
|
|
1073
1311
|
<button
|
|
1074
1312
|
type="button"
|
|
1075
1313
|
class="p-1.5 rounded hover:bg-muted"
|
|
1076
|
-
onclick={
|
|
1314
|
+
onclick={handleCollapseToggle}
|
|
1077
1315
|
aria-expanded={isExpanded}
|
|
1078
1316
|
aria-label={isExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
1079
1317
|
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
1080
1318
|
>
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
/>
|
|
1098
|
-
</svg>
|
|
1099
|
-
{/if}
|
|
1319
|
+
<!-- Base chevron points up; rotate it so that when pinned it points
|
|
1320
|
+
toward the pinned edge (the collapse direction), and for the card
|
|
1321
|
+
it toggles the usual up (expand) / down (collapse). -->
|
|
1322
|
+
<svg
|
|
1323
|
+
class={cn('w-4 h-4 transition-transform', collapseChevronRotation)}
|
|
1324
|
+
fill="none"
|
|
1325
|
+
viewBox="0 0 24 24"
|
|
1326
|
+
stroke="currentColor"
|
|
1327
|
+
>
|
|
1328
|
+
<path
|
|
1329
|
+
stroke-linecap="round"
|
|
1330
|
+
stroke-linejoin="round"
|
|
1331
|
+
stroke-width="2"
|
|
1332
|
+
d="M5 15l7-7 7 7"
|
|
1333
|
+
/>
|
|
1334
|
+
</svg>
|
|
1100
1335
|
</button>
|
|
1101
1336
|
{/if}
|
|
1102
1337
|
|
|
@@ -1148,52 +1383,30 @@
|
|
|
1148
1383
|
<!-- Resize handles (full variant only) -->
|
|
1149
1384
|
{#if variant === 'full' && !disableResize}
|
|
1150
1385
|
{#if mode === 'detached'}
|
|
1151
|
-
<!-- Edge handles -->
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
></div>
|
|
1176
|
-
<!-- Corner handles (6px radius = 8px panel radius minus 2px offset) -->
|
|
1177
|
-
<div
|
|
1178
|
-
class="resize-handle resize-handle--corner resize-handle--top-left"
|
|
1179
|
-
role="separator"
|
|
1180
|
-
onpointerdown={(e) => handleResizeStart(e, 'top-left')}
|
|
1181
|
-
></div>
|
|
1182
|
-
<div
|
|
1183
|
-
class="resize-handle resize-handle--corner resize-handle--top-right"
|
|
1184
|
-
role="separator"
|
|
1185
|
-
onpointerdown={(e) => handleResizeStart(e, 'top-right')}
|
|
1186
|
-
></div>
|
|
1187
|
-
<div
|
|
1188
|
-
class="resize-handle resize-handle--corner resize-handle--bottom-left"
|
|
1189
|
-
role="separator"
|
|
1190
|
-
onpointerdown={(e) => handleResizeStart(e, 'bottom-left')}
|
|
1191
|
-
></div>
|
|
1192
|
-
<div
|
|
1193
|
-
class="resize-handle resize-handle--corner resize-handle--bottom-right"
|
|
1194
|
-
role="separator"
|
|
1195
|
-
onpointerdown={(e) => handleResizeStart(e, 'bottom-right')}
|
|
1196
|
-
></div>
|
|
1386
|
+
<!-- Edge + corner handles: keyboard-resizable sliders (arrow keys, shift for large steps) -->
|
|
1387
|
+
{#each detachedResizeHandles as { handle, orientation } (handle)}
|
|
1388
|
+
<div
|
|
1389
|
+
class={cn(
|
|
1390
|
+
'resize-handle',
|
|
1391
|
+
orientation === 'horizontal' && 'resize-handle--horizontal',
|
|
1392
|
+
orientation === 'vertical' && 'resize-handle--vertical',
|
|
1393
|
+
orientation === null && 'resize-handle--corner',
|
|
1394
|
+
`resize-handle--${handle}`
|
|
1395
|
+
)}
|
|
1396
|
+
role="slider"
|
|
1397
|
+
tabindex="0"
|
|
1398
|
+
aria-orientation={orientation === 'vertical' ? 'vertical' : 'horizontal'}
|
|
1399
|
+
aria-label={`Resize panel from ${handle.replace('-', ' ')} ${
|
|
1400
|
+
handle.includes('-') ? 'corner' : 'edge'
|
|
1401
|
+
}`}
|
|
1402
|
+
aria-valuenow={resizeAriaNow(handle)}
|
|
1403
|
+
aria-valuemin={resizeAriaMin(handle)}
|
|
1404
|
+
aria-valuemax={resizeAriaMax(handle)}
|
|
1405
|
+
aria-valuetext={`${resizeAriaNow(handle)} pixels`}
|
|
1406
|
+
onpointerdown={(e) => handleResizeStart(e, handle)}
|
|
1407
|
+
onkeydown={(e) => handleResizeKeyDown(e, handle)}
|
|
1408
|
+
></div>
|
|
1409
|
+
{/each}
|
|
1197
1410
|
{:else}
|
|
1198
1411
|
<!-- Single resize handle for pinned mode -->
|
|
1199
1412
|
{@const pinnedHandle = getPinnedResizeHandle(edge)}
|
|
@@ -1205,9 +1418,16 @@
|
|
|
1205
1418
|
edge === 'top' && 'resize-handle--horizontal resize-handle--bottom',
|
|
1206
1419
|
edge === 'bottom' && 'resize-handle--horizontal resize-handle--top'
|
|
1207
1420
|
)}
|
|
1208
|
-
role="
|
|
1209
|
-
|
|
1421
|
+
role="slider"
|
|
1422
|
+
tabindex="0"
|
|
1423
|
+
aria-orientation={isHorizontalEdge(edge) ? 'horizontal' : 'vertical'}
|
|
1424
|
+
aria-label={`Resize panel from ${edge} edge`}
|
|
1425
|
+
aria-valuenow={pinnedSize}
|
|
1426
|
+
aria-valuemin={resizeAriaMin(pinnedHandle)}
|
|
1427
|
+
aria-valuemax={resizeAriaMax(pinnedHandle)}
|
|
1428
|
+
aria-valuetext={`${pinnedSize} pixels`}
|
|
1210
1429
|
onpointerdown={(e) => handleResizeStart(e, pinnedHandle)}
|
|
1430
|
+
onkeydown={(e) => handleResizeKeyDown(e, pinnedHandle)}
|
|
1211
1431
|
></div>
|
|
1212
1432
|
{/if}
|
|
1213
1433
|
{/if}
|
|
@@ -1217,23 +1437,44 @@
|
|
|
1217
1437
|
<!-- Top edge for height resize -->
|
|
1218
1438
|
<div
|
|
1219
1439
|
class="resize-handle resize-handle--horizontal resize-handle--top"
|
|
1220
|
-
role="
|
|
1221
|
-
|
|
1440
|
+
role="slider"
|
|
1441
|
+
tabindex="0"
|
|
1442
|
+
aria-orientation="vertical"
|
|
1443
|
+
aria-label="Resize card height"
|
|
1444
|
+
aria-valuenow={Math.round(cardExpandedHeight)}
|
|
1445
|
+
aria-valuemin={cardHeightMin}
|
|
1446
|
+
aria-valuemax={cardHeightMax}
|
|
1447
|
+
aria-valuetext={`${Math.round(cardExpandedHeight)} pixels`}
|
|
1222
1448
|
onpointerdown={handleCardResizeStart}
|
|
1449
|
+
onkeydown={(e) => handleCardResizeKeyDown(e, 'height')}
|
|
1223
1450
|
></div>
|
|
1224
1451
|
<!-- Left edge for width resize -->
|
|
1225
1452
|
<div
|
|
1226
1453
|
class="resize-handle resize-handle--vertical resize-handle--left resize-handle--card-edge"
|
|
1227
|
-
role="
|
|
1228
|
-
|
|
1454
|
+
role="slider"
|
|
1455
|
+
tabindex="0"
|
|
1456
|
+
aria-orientation="horizontal"
|
|
1457
|
+
aria-label="Resize card width from left edge"
|
|
1458
|
+
aria-valuenow={Math.round(cardCurrentWidth)}
|
|
1459
|
+
aria-valuemin={CARD_MIN_WIDTH}
|
|
1460
|
+
aria-valuemax={resolvedCardConfig.maxWidth}
|
|
1461
|
+
aria-valuetext={`${Math.round(cardCurrentWidth)} pixels`}
|
|
1229
1462
|
onpointerdown={(e) => handleCardWidthResizeStart(e, 'left')}
|
|
1463
|
+
onkeydown={(e) => handleCardResizeKeyDown(e, 'width-left')}
|
|
1230
1464
|
></div>
|
|
1231
1465
|
<!-- Right edge for width resize -->
|
|
1232
1466
|
<div
|
|
1233
1467
|
class="resize-handle resize-handle--vertical resize-handle--right resize-handle--card-edge"
|
|
1234
|
-
role="
|
|
1235
|
-
|
|
1468
|
+
role="slider"
|
|
1469
|
+
tabindex="0"
|
|
1470
|
+
aria-orientation="horizontal"
|
|
1471
|
+
aria-label="Resize card width from right edge"
|
|
1472
|
+
aria-valuenow={Math.round(cardCurrentWidth)}
|
|
1473
|
+
aria-valuemin={CARD_MIN_WIDTH}
|
|
1474
|
+
aria-valuemax={resolvedCardConfig.maxWidth}
|
|
1475
|
+
aria-valuetext={`${Math.round(cardCurrentWidth)} pixels`}
|
|
1236
1476
|
onpointerdown={(e) => handleCardWidthResizeStart(e, 'right')}
|
|
1477
|
+
onkeydown={(e) => handleCardResizeKeyDown(e, 'width-right')}
|
|
1237
1478
|
></div>
|
|
1238
1479
|
{/if}
|
|
1239
1480
|
</div>
|
|
@@ -1249,7 +1490,8 @@
|
|
|
1249
1490
|
background: transparent;
|
|
1250
1491
|
}
|
|
1251
1492
|
|
|
1252
|
-
/* Edge handles -
|
|
1493
|
+
/* Edge handles - invisible hit strip inset from the corners, with a 2px line
|
|
1494
|
+
inset 2px from the panel edge so it aligns with the corner arcs' gap. */
|
|
1253
1495
|
.resize-handle--horizontal {
|
|
1254
1496
|
left: 14px;
|
|
1255
1497
|
right: 14px;
|
|
@@ -1317,10 +1559,16 @@
|
|
|
1317
1559
|
right: 0;
|
|
1318
1560
|
}
|
|
1319
1561
|
|
|
1320
|
-
/* Hover state for edge handles */
|
|
1562
|
+
/* Hover / keyboard-focus state for edge handles */
|
|
1321
1563
|
.resize-handle--horizontal:hover::after,
|
|
1322
|
-
.resize-handle--vertical:hover::after
|
|
1323
|
-
|
|
1564
|
+
.resize-handle--vertical:hover::after,
|
|
1565
|
+
.resize-handle--horizontal:focus-visible::after,
|
|
1566
|
+
.resize-handle--vertical:focus-visible::after {
|
|
1567
|
+
background: hsl(var(--ring));
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
.resize-handle:focus-visible {
|
|
1571
|
+
outline: none;
|
|
1324
1572
|
}
|
|
1325
1573
|
|
|
1326
1574
|
/* Card edge handles - extend to bottom edge (no bottom corners) */
|
|
@@ -1423,4 +1671,25 @@
|
|
|
1423
1671
|
border-bottom-color: hsl(var(--border)) !important;
|
|
1424
1672
|
border-right-color: hsl(var(--border)) !important;
|
|
1425
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
/* Keyboard-focus state for corner handles */
|
|
1676
|
+
.resize-handle--top-left:focus-visible::after {
|
|
1677
|
+
border-top-color: hsl(var(--ring)) !important;
|
|
1678
|
+
border-left-color: hsl(var(--ring)) !important;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.resize-handle--top-right:focus-visible::after {
|
|
1682
|
+
border-top-color: hsl(var(--ring)) !important;
|
|
1683
|
+
border-right-color: hsl(var(--ring)) !important;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.resize-handle--bottom-left:focus-visible::after {
|
|
1687
|
+
border-bottom-color: hsl(var(--ring)) !important;
|
|
1688
|
+
border-left-color: hsl(var(--ring)) !important;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.resize-handle--bottom-right:focus-visible::after {
|
|
1692
|
+
border-bottom-color: hsl(var(--ring)) !important;
|
|
1693
|
+
border-right-color: hsl(var(--ring)) !important;
|
|
1694
|
+
}
|
|
1426
1695
|
</style>
|