@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) onDragStart(id, e);
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
- `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;`
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
- editable ? 'pt-3 overflow-visible' : 'overflow-hidden',
501
- editable && showGrid && 'ring-border ring-1',
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 editable && showGrid}
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 layout as item (item.id)}
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
- editable && draggable && !item.static && 'cursor-grab',
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={editable && draggable && !item.static}
600
- resizable={editable && !item.static}
601
- resizeEdges={editable ? (['s', 'e', 'se'] as const) : undefined}
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="h-full w-full"
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",