@r2digisolutions/components 0.17.1 → 0.17.3
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/components/organisms/AssistantChat/AssistantChat.svelte +89 -8
- package/dist/components/organisms/AssistantChat/AssistantChat.svelte.d.ts +8 -3
- package/dist/components/organisms/DashboardGrid/DashboardGrid.stories.js +6 -1
- package/dist/components/organisms/DashboardGrid/DashboardGrid.svelte +82 -18
- package/dist/components/organisms/DashboardGrid/DashboardGrid.svelte.d.ts +6 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
2
|
+
import { tick, type Snippet } from 'svelte';
|
|
3
3
|
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
4
4
|
import {
|
|
5
5
|
Sparkles,
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
Volume2,
|
|
11
11
|
VolumeX,
|
|
12
12
|
LoaderCircle,
|
|
13
|
-
RotateCcw
|
|
13
|
+
RotateCcw,
|
|
14
|
+
Copy,
|
|
15
|
+
Check
|
|
14
16
|
} from '@lucide/svelte';
|
|
15
17
|
|
|
16
18
|
export interface AssistantMessage {
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
draft?: string;
|
|
31
33
|
loading?: boolean;
|
|
32
34
|
loadingLabel?: string;
|
|
33
|
-
/** When false,
|
|
35
|
+
/** When false, the spinner bubble is hidden (parent paints status in a message). */
|
|
34
36
|
showLoadingBubble?: boolean;
|
|
35
37
|
error?: string | null;
|
|
36
38
|
voiceEnabled?: boolean;
|
|
@@ -43,12 +45,15 @@
|
|
|
43
45
|
stopMicLabel?: string;
|
|
44
46
|
voiceOnLabel?: string;
|
|
45
47
|
voiceOffLabel?: string;
|
|
48
|
+
copyChatLabel?: string;
|
|
49
|
+
copiedChatLabel?: string;
|
|
46
50
|
class?: string;
|
|
47
51
|
result?: Snippet<[AssistantMessage]>;
|
|
48
52
|
footerExtra?: Snippet;
|
|
49
53
|
onsend?: () => void;
|
|
50
54
|
onclose?: () => void;
|
|
51
55
|
onnewchat?: () => void;
|
|
56
|
+
oncopychat?: () => void | Promise<void>;
|
|
52
57
|
onmicclick?: () => void;
|
|
53
58
|
onvoiceoutputchange?: (on: boolean) => void;
|
|
54
59
|
onsuggestion?: (text: string) => void;
|
|
@@ -77,21 +82,46 @@
|
|
|
77
82
|
stopMicLabel = 'Detener micrófono',
|
|
78
83
|
voiceOnLabel = 'Voz activada',
|
|
79
84
|
voiceOffLabel = 'Voz desactivada',
|
|
85
|
+
copyChatLabel = 'Copiar chat',
|
|
86
|
+
copiedChatLabel = 'Copiado',
|
|
80
87
|
class: className = '',
|
|
81
88
|
result,
|
|
82
89
|
footerExtra,
|
|
83
90
|
onsend,
|
|
84
91
|
onclose,
|
|
85
92
|
onnewchat,
|
|
93
|
+
oncopychat,
|
|
86
94
|
onmicclick,
|
|
87
95
|
onvoiceoutputchange,
|
|
88
96
|
onsuggestion
|
|
89
97
|
}: AssistantChatProps = $props();
|
|
90
98
|
|
|
91
99
|
let scrollEl = $state<HTMLDivElement | null>(null);
|
|
100
|
+
let inputEl: HTMLInputElement | null = null;
|
|
101
|
+
/** Restore the composer after send / SvelteKit form focus reset. */
|
|
102
|
+
let retainComposerFocus = false;
|
|
103
|
+
let copiedFlash = $state(false);
|
|
92
104
|
|
|
93
105
|
const isEmpty = $derived(messages.length === 0 && !loading);
|
|
94
106
|
const canSend = $derived(draft.trim().length > 0 && !loading);
|
|
107
|
+
const pendingAssistantId = $derived.by(() => {
|
|
108
|
+
if (!loading || messages.length === 0) return null;
|
|
109
|
+
const last = messages[messages.length - 1];
|
|
110
|
+
if (last?.role === 'assistant' && !last.content.trim()) return last.id;
|
|
111
|
+
return null;
|
|
112
|
+
});
|
|
113
|
+
const showTrailingLoader = $derived(loading && showLoadingBubble && !pendingAssistantId);
|
|
114
|
+
|
|
115
|
+
function focusComposer() {
|
|
116
|
+
inputEl?.focus({ preventScroll: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Parent can call this after client-side navigation (goto). */
|
|
120
|
+
export function focus() {
|
|
121
|
+
retainComposerFocus = true;
|
|
122
|
+
focusComposer();
|
|
123
|
+
void tick().then(focusComposer);
|
|
124
|
+
}
|
|
95
125
|
|
|
96
126
|
$effect(() => {
|
|
97
127
|
void messages.length;
|
|
@@ -104,14 +134,26 @@
|
|
|
104
134
|
return () => cancelAnimationFrame(raf);
|
|
105
135
|
});
|
|
106
136
|
|
|
137
|
+
$effect(() => {
|
|
138
|
+
if (!open || loading || !retainComposerFocus) return;
|
|
139
|
+
const id = requestAnimationFrame(() => {
|
|
140
|
+
if (open && retainComposerFocus) focusComposer();
|
|
141
|
+
if (!loading) retainComposerFocus = false;
|
|
142
|
+
});
|
|
143
|
+
return () => cancelAnimationFrame(id);
|
|
144
|
+
});
|
|
145
|
+
|
|
107
146
|
function handleClose() {
|
|
147
|
+
retainComposerFocus = false;
|
|
108
148
|
open = false;
|
|
109
149
|
onclose?.();
|
|
110
150
|
}
|
|
111
151
|
|
|
112
152
|
function handleSend() {
|
|
113
153
|
if (!canSend) return;
|
|
154
|
+
retainComposerFocus = true;
|
|
114
155
|
onsend?.();
|
|
156
|
+
void tick().then(focusComposer);
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
function handleKeydown(e: KeyboardEvent) {
|
|
@@ -130,6 +172,15 @@
|
|
|
130
172
|
voiceOutput = !voiceOutput;
|
|
131
173
|
onvoiceoutputchange?.(voiceOutput);
|
|
132
174
|
}
|
|
175
|
+
|
|
176
|
+
async function handleCopyChat() {
|
|
177
|
+
if (!oncopychat || messages.length === 0) return;
|
|
178
|
+
await oncopychat();
|
|
179
|
+
copiedFlash = true;
|
|
180
|
+
window.setTimeout(() => {
|
|
181
|
+
copiedFlash = false;
|
|
182
|
+
}, 1600);
|
|
183
|
+
}
|
|
133
184
|
</script>
|
|
134
185
|
|
|
135
186
|
<div
|
|
@@ -154,6 +205,19 @@
|
|
|
154
205
|
<p class="text-xs text-muted truncate">{statusLabel}</p>
|
|
155
206
|
{/if}
|
|
156
207
|
</div>
|
|
208
|
+
<IconButton
|
|
209
|
+
variant="ghost"
|
|
210
|
+
size="xs"
|
|
211
|
+
label={copiedFlash ? copiedChatLabel : copyChatLabel}
|
|
212
|
+
disabled={messages.length === 0 || !oncopychat}
|
|
213
|
+
onclick={() => void handleCopyChat()}
|
|
214
|
+
>
|
|
215
|
+
{#if copiedFlash}
|
|
216
|
+
<Check size={14} strokeWidth={2} />
|
|
217
|
+
{:else}
|
|
218
|
+
<Copy size={14} strokeWidth={2} />
|
|
219
|
+
{/if}
|
|
220
|
+
</IconButton>
|
|
157
221
|
<IconButton variant="ghost" size="xs" label={newChatLabel} onclick={() => onnewchat?.()}>
|
|
158
222
|
<RotateCcw size={14} strokeWidth={2} />
|
|
159
223
|
</IconButton>
|
|
@@ -193,7 +257,7 @@
|
|
|
193
257
|
{#if message.role === 'user'}
|
|
194
258
|
<div class="flex justify-end">
|
|
195
259
|
<div
|
|
196
|
-
class="rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white max-w-[80%]"
|
|
260
|
+
class="rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white max-w-[80%] whitespace-pre-wrap"
|
|
197
261
|
>
|
|
198
262
|
{message.content}
|
|
199
263
|
</div>
|
|
@@ -208,9 +272,16 @@
|
|
|
208
272
|
<Sparkles size={12} strokeWidth={2} />
|
|
209
273
|
</span>
|
|
210
274
|
<div
|
|
211
|
-
class="rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary max-w-[calc(100%-2rem)]"
|
|
275
|
+
class="rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary max-w-[calc(100%-2rem)] whitespace-pre-wrap"
|
|
212
276
|
>
|
|
213
|
-
{message.
|
|
277
|
+
{#if message.id === pendingAssistantId}
|
|
278
|
+
<span class="gap-2 text-muted flex items-center">
|
|
279
|
+
<LoaderCircle size={14} strokeWidth={2} class="animate-spin shrink-0" />
|
|
280
|
+
<span class="text-xs">{loadingLabel}</span>
|
|
281
|
+
</span>
|
|
282
|
+
{:else}
|
|
283
|
+
{message.content}
|
|
284
|
+
{/if}
|
|
214
285
|
</div>
|
|
215
286
|
</div>
|
|
216
287
|
{#if result}
|
|
@@ -222,7 +293,7 @@
|
|
|
222
293
|
{/if}
|
|
223
294
|
{/each}
|
|
224
295
|
|
|
225
|
-
{#if
|
|
296
|
+
{#if showTrailingLoader}
|
|
226
297
|
<div class="gap-2 flex items-center">
|
|
227
298
|
<span
|
|
228
299
|
class="h-6 w-6 bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400 flex shrink-0 items-center justify-center rounded-full"
|
|
@@ -246,6 +317,17 @@
|
|
|
246
317
|
<footer class="border-border px-3 py-2.5 shrink-0 border-t">
|
|
247
318
|
<div class="gap-1.5 flex items-center">
|
|
248
319
|
<input
|
|
320
|
+
{@attach (node) => {
|
|
321
|
+
inputEl = node;
|
|
322
|
+
$effect(() => {
|
|
323
|
+
if (!open || loading || !retainComposerFocus) return;
|
|
324
|
+
node.focus();
|
|
325
|
+
retainComposerFocus = false;
|
|
326
|
+
});
|
|
327
|
+
return () => {
|
|
328
|
+
if (inputEl === node) inputEl = null;
|
|
329
|
+
};
|
|
330
|
+
}}
|
|
249
331
|
type="text"
|
|
250
332
|
bind:value={draft}
|
|
251
333
|
placeholder={listening ? statusLabel || placeholder : placeholder}
|
|
@@ -253,7 +335,6 @@
|
|
|
253
335
|
'h-9 min-w-0 rounded-xl bg-surface px-3 text-sm text-primary placeholder:text-muted focus:border-brand-400 focus:ring-brand-500/20 flex-1 border transition-shadow outline-none focus:ring-2',
|
|
254
336
|
listening ? 'border-red-400 ring-red-500/20 ring-2' : 'border-border'
|
|
255
337
|
]}
|
|
256
|
-
disabled={loading}
|
|
257
338
|
onkeydown={handleKeydown}
|
|
258
339
|
aria-label={placeholder}
|
|
259
340
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
2
|
export interface AssistantMessage {
|
|
3
3
|
id: string;
|
|
4
4
|
role: 'user' | 'assistant';
|
|
@@ -15,7 +15,7 @@ interface AssistantChatProps {
|
|
|
15
15
|
draft?: string;
|
|
16
16
|
loading?: boolean;
|
|
17
17
|
loadingLabel?: string;
|
|
18
|
-
/** When false,
|
|
18
|
+
/** When false, the spinner bubble is hidden (parent paints status in a message). */
|
|
19
19
|
showLoadingBubble?: boolean;
|
|
20
20
|
error?: string | null;
|
|
21
21
|
voiceEnabled?: boolean;
|
|
@@ -28,16 +28,21 @@ interface AssistantChatProps {
|
|
|
28
28
|
stopMicLabel?: string;
|
|
29
29
|
voiceOnLabel?: string;
|
|
30
30
|
voiceOffLabel?: string;
|
|
31
|
+
copyChatLabel?: string;
|
|
32
|
+
copiedChatLabel?: string;
|
|
31
33
|
class?: string;
|
|
32
34
|
result?: Snippet<[AssistantMessage]>;
|
|
33
35
|
footerExtra?: Snippet;
|
|
34
36
|
onsend?: () => void;
|
|
35
37
|
onclose?: () => void;
|
|
36
38
|
onnewchat?: () => void;
|
|
39
|
+
oncopychat?: () => void | Promise<void>;
|
|
37
40
|
onmicclick?: () => void;
|
|
38
41
|
onvoiceoutputchange?: (on: boolean) => void;
|
|
39
42
|
onsuggestion?: (text: string) => void;
|
|
40
43
|
}
|
|
41
|
-
declare const AssistantChat: import("svelte").Component<AssistantChatProps, {
|
|
44
|
+
declare const AssistantChat: import("svelte").Component<AssistantChatProps, {
|
|
45
|
+
focus: () => void;
|
|
46
|
+
}, "open" | "draft" | "voiceOutput">;
|
|
42
47
|
type AssistantChat = ReturnType<typeof AssistantChat>;
|
|
43
48
|
export default AssistantChat;
|
|
@@ -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;
|