@r2digisolutions/components 0.17.1 → 0.17.2
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.
|
@@ -4,7 +4,12 @@ const meta = {
|
|
|
4
4
|
component: DashboardGridStory,
|
|
5
5
|
tags: ['autodocs'],
|
|
6
6
|
parameters: {
|
|
7
|
-
layout: 'padded'
|
|
7
|
+
layout: 'padded',
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component: 'Responsive dashboard grid. Use `stackBelow` (default `960`, or `false` to disable) to collapse into a single-column reading-order stack when the **grid container** is narrow (ResizeObserver; sidebar-aware). Drag/resize are disabled while stacked.'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
8
13
|
},
|
|
9
14
|
argTypes: {
|
|
10
15
|
example: {
|
|
@@ -89,6 +89,12 @@
|
|
|
89
89
|
onreset?: () => void;
|
|
90
90
|
/** Label shown in the drop placeholder while dragging */
|
|
91
91
|
dropLabel?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Container width (px) below which the grid stacks into a single column.
|
|
94
|
+
* Measured on the grid itself (sidebar-aware). Pass `false` to disable.
|
|
95
|
+
* Default `960`.
|
|
96
|
+
*/
|
|
97
|
+
stackBelow?: number | false;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
let {
|
|
@@ -118,13 +124,15 @@
|
|
|
118
124
|
onremove,
|
|
119
125
|
onadd,
|
|
120
126
|
onreset,
|
|
121
|
-
dropLabel = 'Drop here'
|
|
127
|
+
dropLabel = 'Drop here',
|
|
128
|
+
stackBelow = 960
|
|
122
129
|
}: DashboardGridProps = $props();
|
|
123
130
|
|
|
124
131
|
const DRAG_THRESHOLD = 6;
|
|
125
132
|
|
|
126
133
|
let containerEl: HTMLDivElement | null = null;
|
|
127
134
|
let ghostEl: HTMLDivElement | null = null;
|
|
135
|
+
let stacked = $state(false);
|
|
128
136
|
let draggingId = $state<string | null>(null);
|
|
129
137
|
let resizingId = $state<string | null>(null);
|
|
130
138
|
let resizeEdge = $state<WidgetResizeEdge>('se');
|
|
@@ -158,6 +166,11 @@
|
|
|
158
166
|
|
|
159
167
|
const contentRows = $derived(layoutBounds(layout).rows);
|
|
160
168
|
const rows = $derived(Math.max(contentRows + (editable ? padRows : 0), minRows));
|
|
169
|
+
const displayLayout = $derived.by((): GridItem[] => {
|
|
170
|
+
if (!stacked) return layout;
|
|
171
|
+
return [...layout].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
172
|
+
});
|
|
173
|
+
const interactionsEnabled = $derived(!stacked);
|
|
161
174
|
const draggingItem = $derived(layout.find((it) => it.id === draggingId) ?? null);
|
|
162
175
|
const snapPreview = $derived.by((): GridItem | null => {
|
|
163
176
|
if (!draggingItem || !dragMoved) return null;
|
|
@@ -276,9 +289,36 @@
|
|
|
276
289
|
};
|
|
277
290
|
}
|
|
278
291
|
|
|
292
|
+
function syncStacked(width: number) {
|
|
293
|
+
if (stackBelow === false) {
|
|
294
|
+
if (stacked) {
|
|
295
|
+
resetInteraction();
|
|
296
|
+
stacked = false;
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const next = width < stackBelow;
|
|
301
|
+
if (next === stacked) return;
|
|
302
|
+
if (next) resetInteraction();
|
|
303
|
+
stacked = next;
|
|
304
|
+
}
|
|
305
|
+
|
|
279
306
|
function attachContainer(node: HTMLElement) {
|
|
280
307
|
containerEl = node as HTMLDivElement;
|
|
308
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
309
|
+
syncStacked(node.clientWidth);
|
|
310
|
+
return () => {
|
|
311
|
+
if (containerEl === node) containerEl = null;
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const ro = new ResizeObserver((entries) => {
|
|
315
|
+
const w = entries[0]?.contentRect.width ?? node.clientWidth;
|
|
316
|
+
syncStacked(w);
|
|
317
|
+
});
|
|
318
|
+
ro.observe(node);
|
|
319
|
+
syncStacked(node.clientWidth);
|
|
281
320
|
return () => {
|
|
321
|
+
ro.disconnect();
|
|
282
322
|
if (containerEl === node) containerEl = null;
|
|
283
323
|
};
|
|
284
324
|
}
|
|
@@ -300,11 +340,12 @@
|
|
|
300
340
|
const target = e.target as HTMLElement | null;
|
|
301
341
|
if (target?.closest('button, a, [data-resize-handle], [data-widget-toolbar]')) return;
|
|
302
342
|
onselect?.(id);
|
|
303
|
-
if (draggable)
|
|
343
|
+
if (!interactionsEnabled || !draggable) return;
|
|
344
|
+
onDragStart(id, e);
|
|
304
345
|
}
|
|
305
346
|
|
|
306
347
|
function onDragStart(id: string, e: PointerEvent) {
|
|
307
|
-
if (!editable || !draggable) return;
|
|
348
|
+
if (!editable || !draggable || !interactionsEnabled) return;
|
|
308
349
|
e.preventDefault();
|
|
309
350
|
e.stopPropagation();
|
|
310
351
|
const item = layout.find((it) => it.id === id);
|
|
@@ -341,7 +382,7 @@
|
|
|
341
382
|
}
|
|
342
383
|
|
|
343
384
|
function onResizeStart(id: string, e: PointerEvent, edge: WidgetResizeEdge) {
|
|
344
|
-
if (!editable) return;
|
|
385
|
+
if (!editable || !interactionsEnabled) return;
|
|
345
386
|
e.preventDefault();
|
|
346
387
|
e.stopPropagation();
|
|
347
388
|
const item = layout.find((it) => it.id === id);
|
|
@@ -440,6 +481,11 @@
|
|
|
440
481
|
}
|
|
441
482
|
|
|
442
483
|
function styleFor(item: GridItem): string {
|
|
484
|
+
if (stacked) {
|
|
485
|
+
// Prefer content height; keep a floor from the saved desktop span.
|
|
486
|
+
const minH = Math.max(item.h * rowHeight, 160);
|
|
487
|
+
return `width:100%;min-height:${minH}px;height:auto;`;
|
|
488
|
+
}
|
|
443
489
|
const x = item.x + 1;
|
|
444
490
|
const y = item.y + 1;
|
|
445
491
|
return `grid-column: ${x} / span ${item.w}; grid-row: ${y} / span ${item.h};`;
|
|
@@ -455,10 +501,26 @@
|
|
|
455
501
|
}
|
|
456
502
|
|
|
457
503
|
const gridStyle = $derived(
|
|
458
|
-
|
|
504
|
+
stacked
|
|
505
|
+
? `display:flex;flex-direction:column;gap:${gap}px;`
|
|
506
|
+
: `display:grid;grid-template-columns:repeat(${cols},minmax(0,1fr));grid-auto-rows:${rowHeight}px;gap:${gap}px;min-height:${rows * rowHeight + Math.max(0, rows - 1) * gap}px;`
|
|
459
507
|
);
|
|
460
508
|
|
|
461
509
|
const guideRows = $derived(Math.max(rows, minRows));
|
|
510
|
+
const showGuides = $derived(editable && showGrid && !stacked);
|
|
511
|
+
const containerOverflowClass = $derived.by(() => {
|
|
512
|
+
if (stacked) {
|
|
513
|
+
return editable && showGrid ? 'pt-3 overflow-visible' : 'overflow-visible';
|
|
514
|
+
}
|
|
515
|
+
return editable ? 'pt-3 overflow-visible' : 'overflow-hidden';
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
$effect(() => {
|
|
519
|
+
// Re-evaluate when the breakpoint prop changes (ResizeObserver handles width).
|
|
520
|
+
void stackBelow;
|
|
521
|
+
if (containerEl) syncStacked(containerEl.clientWidth);
|
|
522
|
+
else if (stackBelow === false) syncStacked(0);
|
|
523
|
+
});
|
|
462
524
|
|
|
463
525
|
onDestroy(() => {
|
|
464
526
|
resetInteraction();
|
|
@@ -497,13 +559,13 @@
|
|
|
497
559
|
{@attach attachContainer}
|
|
498
560
|
class={[
|
|
499
561
|
'rounded-xl relative w-full',
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
editable && 'select-none'
|
|
562
|
+
containerOverflowClass,
|
|
563
|
+
showGuides && 'ring-border ring-1',
|
|
564
|
+
editable && !stacked && 'select-none'
|
|
503
565
|
]}
|
|
504
566
|
style={gridStyle}
|
|
505
567
|
>
|
|
506
|
-
{#if
|
|
568
|
+
{#if showGuides}
|
|
507
569
|
{#each Array.from({ length: cols * guideRows }, (_, i) => i) as cell (cell)}
|
|
508
570
|
{@const cx = cell % cols}
|
|
509
571
|
{@const cy = Math.floor(cell / cols)}
|
|
@@ -516,7 +578,7 @@
|
|
|
516
578
|
{/each}
|
|
517
579
|
{/if}
|
|
518
580
|
|
|
519
|
-
{#if draggable && originPreview}
|
|
581
|
+
{#if interactionsEnabled && draggable && originPreview}
|
|
520
582
|
<div
|
|
521
583
|
class="pointer-events-none z-[2] rounded-xl border-2 border-dashed border-muted/60 bg-muted/10"
|
|
522
584
|
style={styleFor(originPreview)}
|
|
@@ -524,7 +586,7 @@
|
|
|
524
586
|
></div>
|
|
525
587
|
{/if}
|
|
526
588
|
|
|
527
|
-
{#if draggable && snapPreview}
|
|
589
|
+
{#if interactionsEnabled && draggable && snapPreview}
|
|
528
590
|
<div
|
|
529
591
|
class="pointer-events-none z-[3] rounded-xl border-2 border-dashed border-brand-500 bg-brand-500/20 shadow-[inset_0_0_0_1px_rgba(var(--color-brand-500),0.25)]"
|
|
530
592
|
style={styleFor(snapPreview)}
|
|
@@ -538,16 +600,18 @@
|
|
|
538
600
|
</div>
|
|
539
601
|
{/if}
|
|
540
602
|
|
|
541
|
-
{#each
|
|
603
|
+
{#each displayLayout as item (item.id)}
|
|
542
604
|
{@const meta = widgetMeta(item)}
|
|
543
605
|
{@const clamped = clampItem(item, cols)}
|
|
544
606
|
{@const selected = selectedId === item.id}
|
|
607
|
+
{@const canDrag = editable && interactionsEnabled && draggable && !item.static}
|
|
608
|
+
{@const canResize = editable && interactionsEnabled && !item.static}
|
|
545
609
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
546
610
|
<div
|
|
547
611
|
data-grid-id={item.id}
|
|
548
612
|
class={[
|
|
549
613
|
'min-h-0 min-w-0 relative z-[1] flex flex-col',
|
|
550
|
-
|
|
614
|
+
canDrag && 'cursor-grab',
|
|
551
615
|
(draggingId === item.id || resizingId === item.id) && 'z-20',
|
|
552
616
|
draggingId === item.id && dragMoved && 'pointer-events-none opacity-30',
|
|
553
617
|
selected && !dragMoved && 'ring-brand-500 ring-offset-surface ring-2 ring-offset-2'
|
|
@@ -596,13 +660,13 @@
|
|
|
596
660
|
flush={meta.flush ?? false}
|
|
597
661
|
collapsible={meta.collapsible ?? false}
|
|
598
662
|
{editable}
|
|
599
|
-
draggable={
|
|
600
|
-
resizable={
|
|
601
|
-
resizeEdges={
|
|
663
|
+
draggable={canDrag}
|
|
664
|
+
resizable={canResize}
|
|
665
|
+
resizeEdges={canResize ? (['s', 'e', 'se'] as const) : undefined}
|
|
602
666
|
loading={meta.loading}
|
|
603
667
|
empty={meta.empty}
|
|
604
668
|
onreload={meta.onReload}
|
|
605
|
-
class=
|
|
669
|
+
class={stacked ? 'min-h-full w-full' : 'h-full w-full'}
|
|
606
670
|
ondragstart={(e) => onDragStart(item.id, e)}
|
|
607
671
|
onresizestart={(e, edge) => onResizeStart(item.id, e, edge)}
|
|
608
672
|
>
|
|
@@ -633,7 +697,7 @@
|
|
|
633
697
|
{/if}
|
|
634
698
|
</div>
|
|
635
699
|
|
|
636
|
-
{#if draggable && ghostBox}
|
|
700
|
+
{#if interactionsEnabled && draggable && ghostBox}
|
|
637
701
|
<div
|
|
638
702
|
{@attach attachGhost}
|
|
639
703
|
class="pointer-events-none fixed z-[500]"
|
|
@@ -61,6 +61,12 @@ interface DashboardGridProps {
|
|
|
61
61
|
onreset?: () => void;
|
|
62
62
|
/** Label shown in the drop placeholder while dragging */
|
|
63
63
|
dropLabel?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Container width (px) below which the grid stacks into a single column.
|
|
66
|
+
* Measured on the grid itself (sidebar-aware). Pass `false` to disable.
|
|
67
|
+
* Default `960`.
|
|
68
|
+
*/
|
|
69
|
+
stackBelow?: number | false;
|
|
64
70
|
}
|
|
65
71
|
declare const DashboardGrid: import("svelte").Component<DashboardGridProps, {
|
|
66
72
|
GridItem: typeof GridItem;
|