@webmcp-auto-ui/ui 2.5.32 → 2.5.33

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.
Files changed (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
@@ -0,0 +1,126 @@
1
+ <svelte:options customElement={{ tag: 'auto-carousel', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ import SafeImage from '../SafeImage.svelte';
5
+
6
+ export interface CarouselSlide { src?: string; content?: string; title?: string; subtitle?: string; }
7
+ export interface CarouselData {
8
+ title?: string;
9
+ slides?: CarouselSlide[];
10
+ autoPlay?: boolean;
11
+ interval?: number;
12
+ }
13
+
14
+ interface Props { data?: CarouselData | null; }
15
+ let { data = {} }: Props = $props();
16
+
17
+ const slides = $derived<CarouselSlide[]>(
18
+ Array.isArray(data?.slides) && data!.slides!.length ? data!.slides! : []
19
+ );
20
+
21
+ let current = $state(0);
22
+ let paused = $state(false);
23
+ let timer: ReturnType<typeof setInterval> | null = null;
24
+
25
+ function goTo(i: number) {
26
+ current = Math.max(0, Math.min(i, slides.length - 1));
27
+ resetAuto();
28
+ }
29
+ function prev() { goTo(current > 0 ? current - 1 : slides.length - 1); }
30
+ function next() { goTo(current < slides.length - 1 ? current + 1 : 0); }
31
+
32
+ function resetAuto() {
33
+ if (timer) clearInterval(timer);
34
+ if (data?.autoPlay !== false && slides.length > 1 && !paused) {
35
+ timer = setInterval(next, data?.interval ?? 5000);
36
+ }
37
+ }
38
+
39
+ $effect(() => {
40
+ if (data?.autoPlay !== false && slides.length > 1) {
41
+ resetAuto();
42
+ return () => { if (timer) clearInterval(timer); };
43
+ }
44
+ });
45
+
46
+ function onPauseStart() { paused = true; if (timer) { clearInterval(timer); timer = null; } }
47
+ function onPauseEnd() { paused = false; resetAuto(); }
48
+
49
+ // Touch/swipe support
50
+ let touchStartX = 0;
51
+ function onTouchStart(e: TouchEvent) { touchStartX = e.touches[0].clientX; }
52
+ function onTouchEnd(e: TouchEvent) {
53
+ const diff = touchStartX - e.changedTouches[0].clientX;
54
+ if (Math.abs(diff) > 50) { diff > 0 ? next() : prev(); }
55
+ }
56
+ </script>
57
+
58
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
59
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
60
+ {#if slides.length === 0}
61
+ <p class="text-text2 text-sm">Aucun contenu</p>
62
+ {:else}
63
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
64
+ <div
65
+ class="relative overflow-hidden rounded-lg"
66
+ aria-roledescription="carousel"
67
+ aria-label={data?.title ?? 'Carousel'}
68
+ ontouchstart={onTouchStart}
69
+ ontouchend={onTouchEnd}
70
+ onmouseenter={onPauseStart}
71
+ onmouseleave={onPauseEnd}
72
+ onfocusin={onPauseStart}
73
+ onfocusout={onPauseEnd}
74
+ >
75
+ <div class="flex transition-transform duration-300 ease-out" style="transform: translateX(-{current * 100}%);">
76
+ {#each slides as slide, i}
77
+ <div
78
+ class="w-full flex-shrink-0"
79
+ role="group"
80
+ aria-roledescription="slide"
81
+ aria-label="{i + 1} sur {slides.length}{slide.title ? ': ' + slide.title : ''}"
82
+ aria-hidden={i !== current ? true : undefined}
83
+ >
84
+ {#if slide.src}
85
+ <SafeImage src={slide.src} alt={slide.title ?? ''} class="w-full h-48 sm:h-64 object-cover" loading="lazy" />
86
+ {/if}
87
+ {#if slide.title || slide.subtitle || slide.content}
88
+ <div class="p-3">
89
+ {#if slide.title}<div class="font-semibold text-sm text-text1">{slide.title}</div>{/if}
90
+ {#if slide.subtitle}<div class="text-xs text-text2 mt-0.5">{slide.subtitle}</div>{/if}
91
+ {#if slide.content}<div class="text-xs text-text2 mt-1.5">{slide.content}</div>{/if}
92
+ </div>
93
+ {/if}
94
+ </div>
95
+ {/each}
96
+ </div>
97
+
98
+ {#if slides.length > 1}
99
+ <button
100
+ class="absolute left-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/40 text-white flex items-center justify-center hover:bg-black/60 text-sm"
101
+ aria-label="Slide précédente"
102
+ onclick={prev}
103
+ >&lsaquo;</button>
104
+ <button
105
+ class="absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/40 text-white flex items-center justify-center hover:bg-black/60 text-sm"
106
+ aria-label="Slide suivante"
107
+ onclick={next}
108
+ >&rsaquo;</button>
109
+ {/if}
110
+ </div>
111
+
112
+ {#if slides.length > 1}
113
+ <div class="flex justify-center gap-1.5 mt-2" role="tablist" aria-label="Slides">
114
+ {#each slides as _, i}
115
+ <button
116
+ class="w-2 h-2 rounded-full transition-colors {i === current ? 'bg-accent' : 'bg-border2'}"
117
+ role="tab"
118
+ aria-selected={i === current}
119
+ aria-label="Slide {i + 1}"
120
+ onclick={() => goTo(i)}
121
+ ></button>
122
+ {/each}
123
+ </div>
124
+ {/if}
125
+ {/if}
126
+ </div>
@@ -0,0 +1,221 @@
1
+ <svelte:options customElement={{ tag: 'auto-chart-rich', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface ChartDataset { label?: string; values: number[]; color?: string; }
5
+ export interface ChartData {
6
+ title?: string;
7
+ type?: 'bar' | 'line' | 'area' | 'pie' | 'donut';
8
+ labels?: string[];
9
+ datasets?: ChartDataset[];
10
+ legend?: boolean;
11
+ xAxis?: { label?: string };
12
+ yAxis?: { label?: string };
13
+ }
14
+
15
+ interface Props { data?: ChartData | null; }
16
+ let { data = {} }: Props = $props();
17
+
18
+ const PAL = [
19
+ '#6366f1','#f59e0b','#10b981','#ef4444','#8b5cf6',
20
+ '#06b6d4','#f97316','#ec4899','#14b8a6','#a855f7',
21
+ '#eab308','#3b82f6','#22c55e','#e11d48','#0ea5e9',
22
+ ];
23
+
24
+ const fmt = new Intl.NumberFormat('fr-FR');
25
+
26
+ /**
27
+ * Defensive coercion — port of normalizeBars from be5ab97.
28
+ * Accepts raw values arrays or object shapes, never throws.
29
+ */
30
+ function coerceValues(raw: unknown): number[] {
31
+ if (!Array.isArray(raw)) return [];
32
+ const toNum = (v: unknown): number => {
33
+ if (typeof v === 'number' && Number.isFinite(v)) return v;
34
+ if (typeof v === 'string') { const n = Number(v); return Number.isFinite(n) ? n : 0; }
35
+ return 0;
36
+ };
37
+ return raw.map(toNum);
38
+ }
39
+
40
+ function isPrimitiveLabel(v: unknown): v is string | number | boolean {
41
+ return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
42
+ }
43
+
44
+ function normalizeDatasets(raw: unknown): ChartDataset[] {
45
+ if (!Array.isArray(raw)) return [];
46
+ return raw.flatMap(item => {
47
+ if (!item || typeof item !== 'object') return [];
48
+ const o = item as Record<string, unknown>;
49
+ // Validate values field
50
+ const values = coerceValues(o.values);
51
+ if (!values.length) return [];
52
+ return [{ label: isPrimitiveLabel(o.label) ? String(o.label) : undefined, values, color: typeof o.color === 'string' ? o.color : undefined }];
53
+ });
54
+ }
55
+
56
+ const datasets = $derived<ChartDataset[]>(normalizeDatasets(data?.datasets));
57
+ const labels = $derived<string[]>(Array.isArray(data?.labels) ? (data!.labels! as unknown[]).filter(isPrimitiveLabel).map(String) : []);
58
+ const type = $derived(data?.type ?? 'bar');
59
+ const isPie = $derived(type === 'pie' || type === 'donut');
60
+ const allVals = $derived(datasets.flatMap(d => d.values));
61
+ const maxVal = $derived(Math.max(...allVals.filter(v => typeof v === 'number'), 1));
62
+ const xLabels = $derived(labels.length > 0 ? labels : (datasets[0]?.values ?? []).map((_, i) => String(i + 1)));
63
+ const showLegend = $derived(data?.legend !== false && datasets.length > 1);
64
+
65
+ function col(ds: ChartDataset, i: number) { return ds.color ?? PAL[i % PAL.length]; }
66
+
67
+ const isCategoricalBar = $derived(type === 'bar' && datasets.length === 1);
68
+ const isCategoricalLine = $derived((type === 'line' || type === 'area') && datasets.length === 1);
69
+ function barCol(ds: ChartDataset, di: number, xi: number) { return isCategoricalBar ? PAL[xi % PAL.length] : col(ds, di); }
70
+
71
+ // Pie
72
+ const pieTotal = $derived.by<number>(() => {
73
+ const ds = datasets[0];
74
+ if (!ds) return 1;
75
+ return ds.values.reduce((a, b) => a + b, 0) || 1;
76
+ });
77
+
78
+ interface Slice { label: string; value: number; color: string; startAngle: number; endAngle: number; pct: number; }
79
+ const slices = $derived.by<Slice[]>(() => {
80
+ if (!isPie || !datasets.length) return [];
81
+ const ds = datasets[0];
82
+ const tot = pieTotal;
83
+ let angle = -Math.PI / 2;
84
+ return (ds.values).map((v, i) => {
85
+ const pct = v / tot;
86
+ const start = angle;
87
+ angle += pct * 2 * Math.PI;
88
+ return { label: xLabels[i] ?? String(i), value: v, color: col(ds, i), startAngle: start, endAngle: angle, pct };
89
+ });
90
+ });
91
+
92
+ function arc(cx: number, cy: number, r: number, start: number, end: number, ir = 0): string {
93
+ const x1 = cx + r * Math.cos(start), y1 = cy + r * Math.sin(start);
94
+ const x2 = cx + r * Math.cos(end), y2 = cy + r * Math.sin(end);
95
+ const large = end - start > Math.PI ? 1 : 0;
96
+ if (ir > 0) {
97
+ const ix1 = cx + ir * Math.cos(end), iy1 = cy + ir * Math.sin(end);
98
+ const ix2 = cx + ir * Math.cos(start), iy2 = cy + ir * Math.sin(start);
99
+ return `M${x1},${y1}A${r},${r},0,${large},1,${x2},${y2}L${ix1},${iy1}A${ir},${ir},0,${large},0,${ix2},${iy2}Z`;
100
+ }
101
+ return `M${cx},${cy}L${x1},${y1}A${r},${r},0,${large},1,${x2},${y2}Z`;
102
+ }
103
+
104
+ let tooltip = $state<string | null>(null);
105
+ </script>
106
+
107
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
108
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
109
+ {#if !datasets.length}
110
+ <p class="text-text2 text-sm">No data</p>
111
+ {:else if isPie}
112
+ <div class="relative flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
113
+ <svg viewBox="-1 -1 2 2" class="w-40 h-40 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" role="img" aria-label={data?.title ?? 'Pie chart'}>
114
+ {#each slices as s}
115
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
116
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
117
+ <path
118
+ d={arc(0, 0, 0.9, s.startAngle, s.endAngle, type === 'donut' ? 0.5 : 0)}
119
+ fill={s.color}
120
+ opacity="0.85"
121
+ onmouseenter={() => tooltip = `${s.label}: ${fmt.format(s.value)} (${Math.round(s.pct * 100)}%)`}
122
+ onmouseleave={() => tooltip = null}
123
+ class="cursor-pointer hover:opacity-100 transition-opacity"
124
+ >
125
+ <title>{s.label}: {fmt.format(s.value)} ({Math.round(s.pct * 100)}%)</title>
126
+ </path>
127
+ {/each}
128
+ </svg>
129
+ <div class="flex flex-col gap-1.5 text-xs">
130
+ {#each slices as s}
131
+ <div class="flex items-center gap-1.5">
132
+ <div class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background:{s.color};"></div>
133
+ <span class="text-text2">{s.label}</span>
134
+ <span class="text-text2 ml-1">{Math.round(s.pct * 100)}%</span>
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ </div>
139
+ {#if tooltip}<div class="mt-2 text-xs text-text2 font-mono">{tooltip}</div>{/if}
140
+ {:else}
141
+ <div class="relative">
142
+ {#if type === 'bar'}
143
+ <div class="flex items-end gap-1 h-24 sm:h-32 mb-1" role="img" aria-label={data?.title ?? 'Bar chart'}>
144
+ {#each xLabels as lbl, xi}
145
+ <div class="flex-1 flex gap-0.5 items-end h-full">
146
+ {#each datasets as ds, di}
147
+ {@const v = ds.values[xi] ?? 0}
148
+ {@const pct = Math.round(v / maxVal * 100)}
149
+ <div
150
+ class="flex-1 rounded-t transition-all hover:opacity-80 cursor-default"
151
+ style="height:{pct}%;background:{barCol(ds, di, xi)};"
152
+ title="{ds.label ?? ''} {lbl}: {fmt.format(v)}"
153
+ ></div>
154
+ {/each}
155
+ </div>
156
+ {/each}
157
+ </div>
158
+ <div class="flex gap-1 mb-2">
159
+ {#each xLabels as lbl}
160
+ <div class="flex-1 text-center text-[9px] font-mono text-text2 truncate">{lbl}</div>
161
+ {/each}
162
+ </div>
163
+ {:else}
164
+ {@const W = 400}{@const H = 120}{@const pad = 10}
165
+ <svg viewBox="0 0 {W} {H}" class="w-full" xmlns="http://www.w3.org/2000/svg" role="img" aria-label={data?.title ?? 'Line chart'}>
166
+ {#each datasets as ds, di}
167
+ {@const pts = ds.values.map((v, i) => `${pad + i * ((W - pad * 2) / (ds.values.length - 1 || 1))},${H - pad - (v / maxVal) * (H - pad * 2)}`)}
168
+ {#if type === 'area'}
169
+ <polygon
170
+ points="{pad},{H - pad} {pts.join(' ')} {pad + ((ds.values.length - 1) * ((W - pad * 2) / (ds.values.length - 1 || 1)))},{H - pad}"
171
+ fill={col(ds, di)}
172
+ opacity="0.15"
173
+ />
174
+ {/if}
175
+ <polyline points={pts.join(' ')} fill="none" stroke={col(ds, di)} stroke-width="2" stroke-linejoin="round" />
176
+ {#each ds.values as v, i}
177
+ {@const cx = pad + i * ((W - pad * 2) / (ds.values.length - 1 || 1))}
178
+ {@const cy = H - pad - (v / maxVal) * (H - pad * 2)}
179
+ <circle cx={cx} cy={cy} r="4" fill={isCategoricalLine ? PAL[i % PAL.length] : col(ds, di)} stroke="var(--color-surface, white)" stroke-width="1.5">
180
+ <title>{ds.label ?? xLabels[i] ?? String(i)}: {fmt.format(v)}</title>
181
+ </circle>
182
+ {/each}
183
+ {/each}
184
+ </svg>
185
+ <div class="flex gap-1">
186
+ {#each xLabels as lbl}
187
+ <div class="flex-1 text-center text-[9px] font-mono text-text2 truncate">{lbl}</div>
188
+ {/each}
189
+ </div>
190
+ {/if}
191
+ </div>
192
+ {#if isCategoricalLine && xLabels.length > 1}
193
+ <div class="flex gap-3 flex-wrap mt-2">
194
+ {#each xLabels as lbl, xi}
195
+ <div class="flex items-center gap-1 text-xs">
196
+ <div class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background:{PAL[xi % PAL.length]};"></div>
197
+ <span class="text-text2">{lbl}</span>
198
+ </div>
199
+ {/each}
200
+ </div>
201
+ {:else if isCategoricalBar && xLabels.length > 1}
202
+ <div class="flex gap-3 flex-wrap mt-2">
203
+ {#each xLabels as lbl, xi}
204
+ <div class="flex items-center gap-1 text-xs">
205
+ <div class="w-2.5 h-2.5 rounded-sm flex-shrink-0" style="background:{PAL[xi % PAL.length]};"></div>
206
+ <span class="text-text2">{lbl}</span>
207
+ </div>
208
+ {/each}
209
+ </div>
210
+ {:else if showLegend}
211
+ <div class="flex gap-3 flex-wrap mt-2">
212
+ {#each datasets as ds, i}
213
+ <div class="flex items-center gap-1 text-xs">
214
+ <div class="w-2.5 h-2.5 rounded-sm flex-shrink-0" style="background:{col(ds, i)};"></div>
215
+ <span class="text-text2">{ds.label ?? `Series ${i + 1}`}</span>
216
+ </div>
217
+ {/each}
218
+ </div>
219
+ {/if}
220
+ {/if}
221
+ </div>
@@ -0,0 +1,52 @@
1
+ <svelte:options customElement={{ tag: 'auto-chat-input', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ import ChatInline from '../../base/chat-inline.svelte';
5
+
6
+ export interface ChatInputData {
7
+ placeholder?: string;
8
+ }
9
+
10
+ interface Props {
11
+ data?: ChatInputData | null;
12
+ placeholder?: string;
13
+ disabled?: boolean;
14
+ value?: string;
15
+ }
16
+
17
+ let { data = null, placeholder = '', disabled = $bindable(false), value = $bindable('') }: Props = $props();
18
+
19
+ const resolvedPlaceholder = $derived(data?.placeholder ?? placeholder ?? 'Type a message...');
20
+
21
+ let rootEl = $state<HTMLElement | null>(null);
22
+
23
+ function handleSubmit(e: CustomEvent<string>) {
24
+ const text = e.detail;
25
+ if (!text) return;
26
+ rootEl?.dispatchEvent(
27
+ new CustomEvent('widget:interact', {
28
+ detail: { action: 'submit', payload: { text } },
29
+ bubbles: true,
30
+ })
31
+ );
32
+ }
33
+
34
+ function handleStop() {
35
+ rootEl?.dispatchEvent(
36
+ new CustomEvent('widget:interact', {
37
+ detail: { action: 'stop', payload: null },
38
+ bubbles: true,
39
+ })
40
+ );
41
+ }
42
+ </script>
43
+
44
+ <div bind:this={rootEl} class="w-full">
45
+ <ChatInline
46
+ placeholder={resolvedPlaceholder}
47
+ {disabled}
48
+ bind:value
49
+ onsubmit={handleSubmit}
50
+ onstop={handleStop}
51
+ />
52
+ </div>
@@ -0,0 +1,132 @@
1
+ <svelte:options customElement={{ tag: 'auto-data-table', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface DataTableColumn { key: string; label: string; align?: 'left'|'center'|'right'; type?: 'text'|'number'|'boolean'|'link'; }
5
+ export interface DataTableData {
6
+ title?: string;
7
+ columns?: DataTableColumn[];
8
+ rows?: Record<string,unknown>[];
9
+ compact?: boolean;
10
+ striped?: boolean;
11
+ emptyMessage?: string;
12
+ }
13
+
14
+ interface Props {
15
+ data?: DataTableData | null;
16
+ onrowclick?: (row: Record<string,unknown>) => void;
17
+ }
18
+
19
+ let { data = {}, onrowclick }: Props = $props();
20
+
21
+ const MAX = 200;
22
+ const fmt = new Intl.NumberFormat('fr-FR');
23
+ let sortCol = $state<string|null>(null);
24
+ let sortAsc = $state(true);
25
+
26
+ const rows = $derived<Record<string,unknown>[]>(
27
+ Array.isArray(data?.rows) && (data!.rows as unknown[]).length
28
+ ? data!.rows as Record<string,unknown>[]
29
+ : []
30
+ );
31
+
32
+ const columns = $derived<DataTableColumn[]>(
33
+ Array.isArray(data?.columns) && (data!.columns as unknown[]).length
34
+ ? data!.columns as DataTableColumn[]
35
+ : rows.length > 0
36
+ ? Object.keys(rows[0] as object).map(k => ({ key: k, label: k }))
37
+ : []
38
+ );
39
+
40
+ const sorted = $derived.by<Record<string,unknown>[]>(() => {
41
+ if (!sortCol) return rows;
42
+ return [...rows].sort((a, b) => {
43
+ const av = a[sortCol!], bv = b[sortCol!];
44
+ const an = Number(av), bn = Number(bv);
45
+ const c = !isNaN(an) && !isNaN(bn)
46
+ ? an - bn
47
+ : String(av ?? '').localeCompare(String(bv ?? ''), 'fr');
48
+ return sortAsc ? c : -c;
49
+ });
50
+ });
51
+
52
+ const displayed = $derived(sorted.slice(0, MAX));
53
+ const overflow = $derived(rows.length > MAX ? rows.length - MAX : 0);
54
+
55
+ function dv(v: unknown): string {
56
+ if (v == null) return '';
57
+ if (typeof v === 'object') return JSON.stringify(v);
58
+ return String(v);
59
+ }
60
+
61
+ const compact = $derived(data?.compact === true);
62
+
63
+ function handleSort(colKey: string) {
64
+ if (sortCol === colKey) {
65
+ sortAsc = !sortAsc;
66
+ } else {
67
+ sortCol = colKey;
68
+ sortAsc = true;
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
74
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
75
+ {#if columns.length === 0 && rows.length === 0}
76
+ <p class="text-text2 text-sm">{data?.emptyMessage ?? 'No data'}</p>
77
+ {:else}
78
+ <div class="overflow-auto max-h-[480px] rounded border border-border">
79
+ <table class="w-full border-collapse" role="grid">
80
+ <thead>
81
+ <tr>
82
+ {#each columns as col}
83
+ <th
84
+ class="sticky top-0 bg-surface2 border-b-2 border-border2 px-3 py-2 text-left text-xs font-mono font-medium text-text2 uppercase tracking-wider whitespace-nowrap cursor-pointer select-none hover:text-text1 transition-colors {col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : ''}"
85
+ aria-sort={sortCol === col.key ? (sortAsc ? 'ascending' : 'descending') : 'none'}
86
+ onclick={() => handleSort(col.key)}
87
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleSort(col.key); } }}
88
+ tabindex="0"
89
+ role="columnheader"
90
+ scope="col"
91
+ >
92
+ {col.label}{#if sortCol === col.key}<span class="ml-1 text-accent" aria-hidden="true">{sortAsc ? '↑' : '↓'}</span>{/if}
93
+ </th>
94
+ {/each}
95
+ </tr>
96
+ </thead>
97
+ <tbody>
98
+ {#each displayed as row, i}
99
+ <tr
100
+ class="hover:bg-surface2 transition-colors {data?.striped !== false && i % 2 === 1 ? 'bg-white/[0.02]' : ''} {onrowclick ? 'cursor-pointer' : ''}"
101
+ title={onrowclick ? 'Double-cliquez pour interagir' : undefined}
102
+ ondblclick={() => onrowclick?.(row)}
103
+ >
104
+ {#each columns as col}
105
+ {@const val = row[col.key]}
106
+ <td class="border-b border-border text-text1 overflow-hidden text-ellipsis whitespace-nowrap max-w-[280px] {compact ? 'px-2 py-1 text-xs' : 'px-3 py-2 text-sm'} {col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : ''}">
107
+ {#if val == null}<span class="text-text2">—</span>
108
+ {:else if col.type === 'boolean'}<span>{val ? '✓' : '✗'}</span>
109
+ {:else if col.type === 'link' && typeof val === 'string'}<a href={val} class="text-accent hover:underline">{val}</a>
110
+ {:else if typeof val === 'object'}<code class="text-xs bg-surface2 px-1 py-0.5 rounded">{JSON.stringify(val)}</code>
111
+ {:else if typeof val === 'number'}<span title={String(val)}>{fmt.format(val)}</span>
112
+ {:else}
113
+ {@const s = String(val)}
114
+ <span title={s}>{s.length > 80 ? s.slice(0, 77) + '…' : s}</span>
115
+ {/if}
116
+ </td>
117
+ {/each}
118
+ </tr>
119
+ {/each}
120
+ {#if overflow > 0}
121
+ <tr>
122
+ <td colspan={columns.length} class="text-center text-text2 text-xs py-2 px-3">
123
+ … {overflow} more rows
124
+ </td>
125
+ </tr>
126
+ {/if}
127
+ </tbody>
128
+ </table>
129
+ </div>
130
+ <div class="mt-2 text-text2 text-xs">{rows.length} ligne{rows.length !== 1 ? 's' : ''}</div>
131
+ {/if}
132
+ </div>
@@ -0,0 +1,115 @@
1
+ <svelte:options customElement={{ tag: 'auto-gallery', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ import SafeImage from '../SafeImage.svelte';
5
+ import Dialog from '../../base/dialog-root.svelte';
6
+ import DialogTrigger from '../../base/dialog-trigger.svelte';
7
+ import DialogContent from '../../base/dialog-content.svelte';
8
+
9
+ export interface GalleryImage { src: string; alt?: string; caption?: string; href?: string; }
10
+ export interface GalleryData {
11
+ title?: string;
12
+ images?: GalleryImage[];
13
+ columns?: number;
14
+ emptyMessage?: string;
15
+ }
16
+
17
+ interface Props { data?: GalleryData | null; }
18
+ let { data = {} }: Props = $props();
19
+
20
+ const images = $derived<GalleryImage[]>(
21
+ Array.isArray(data?.images) && data!.images!.length ? data!.images! :
22
+ []
23
+ );
24
+
25
+ let lightboxOpen = $state(false);
26
+ let lightboxIdx = $state(0);
27
+ const lightboxImg = $derived(images[lightboxIdx] ?? null);
28
+
29
+ function open(i: number) { lightboxIdx = i; lightboxOpen = true; }
30
+ function close() { lightboxOpen = false; }
31
+ function prev() { if (lightboxIdx > 0) lightboxIdx--; }
32
+ function next() { if (lightboxIdx < images.length - 1) lightboxIdx++; }
33
+
34
+ function onKey(e: KeyboardEvent) {
35
+ if (!lightboxOpen) return;
36
+ if (e.key === 'Escape') close();
37
+ else if (e.key === 'ArrowLeft') prev();
38
+ else if (e.key === 'ArrowRight') next();
39
+ }
40
+ </script>
41
+
42
+ <svelte:window onkeydown={onKey} />
43
+
44
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
45
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
46
+ {#if images.length === 0}
47
+ <p class="text-text2 text-sm">{data?.emptyMessage ?? 'Aucune image'}</p>
48
+ {:else if images.length === 1}
49
+ {@const img = images[0]}
50
+ <Dialog bind:open={lightboxOpen}>
51
+ <DialogTrigger class="relative overflow-hidden rounded-lg border border-border hover:border-border2 transition-all cursor-pointer bg-transparent p-0 group w-full text-left">
52
+ <SafeImage src={img.src} alt={img.alt ?? ''} class="w-full max-h-[400px] object-contain rounded-lg" loading="eager" />
53
+ {#if img.caption || img.alt}
54
+ <div class="mt-2 text-center text-xs text-text2">{img.caption ?? img.alt}</div>
55
+ {/if}
56
+ </DialogTrigger>
57
+ <DialogContent class="max-w-[90vw] max-h-[90vh] p-2 overflow-hidden">
58
+ {#if lightboxImg}
59
+ <SafeImage src={lightboxImg.src} alt={lightboxImg.alt ?? ''} class="max-w-full max-h-[85vh] object-contain rounded" />
60
+ {#if lightboxImg.caption}
61
+ <div class="text-center text-text2 text-sm mt-2">{lightboxImg.caption}</div>
62
+ {/if}
63
+ {/if}
64
+ </DialogContent>
65
+ </Dialog>
66
+ {:else}
67
+ <Dialog bind:open={lightboxOpen}>
68
+ <div class="grid gap-2 responsive-gallery" style="--gallery-cols: repeat({data?.columns ?? 3}, minmax(0, 1fr));">
69
+ {#each images as img, i}
70
+ <DialogTrigger
71
+ class="relative overflow-hidden rounded-lg border border-border hover:border-border2 transition-all cursor-pointer bg-transparent p-0 group text-left"
72
+ onclick={() => (lightboxIdx = i)}
73
+ >
74
+ <SafeImage src={img.src} alt={img.alt ?? ''} class="w-full h-32 sm:h-40 object-cover transition-transform group-hover:scale-105" loading="lazy" />
75
+ {#if img.caption}
76
+ <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5">
77
+ <span class="text-white text-xs">{img.caption}</span>
78
+ </div>
79
+ {/if}
80
+ </DialogTrigger>
81
+ {/each}
82
+ </div>
83
+ <div class="mt-2 text-xs text-text2">{images.length} image{images.length !== 1 ? 's' : ''}</div>
84
+
85
+ <DialogContent class="max-w-[90vw] max-h-[90vh] p-2 overflow-hidden">
86
+ {#if lightboxImg}
87
+ <div class="relative">
88
+ <SafeImage src={lightboxImg.src} alt={lightboxImg.alt ?? ''} class="max-w-full max-h-[80vh] object-contain rounded" />
89
+ {#if lightboxImg.caption}
90
+ <div class="text-center text-text2 text-sm mt-2">{lightboxImg.caption}</div>
91
+ {/if}
92
+ {#if lightboxIdx > 0}
93
+ <button
94
+ class="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70"
95
+ aria-label="Image précédente"
96
+ onclick={prev}>&lsaquo;</button>
97
+ {/if}
98
+ {#if lightboxIdx < images.length - 1}
99
+ <button
100
+ class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70"
101
+ aria-label="Image suivante"
102
+ onclick={next}>&rsaquo;</button>
103
+ {/if}
104
+ </div>
105
+ <div class="text-center text-xs text-text2 mt-1">{lightboxIdx + 1} / {images.length}</div>
106
+ {/if}
107
+ </DialogContent>
108
+ </Dialog>
109
+ {/if}
110
+ </div>
111
+
112
+ <style>
113
+ .responsive-gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
114
+ @media (min-width: 768px) { .responsive-gallery { grid-template-columns: var(--gallery-cols); } }
115
+ </style>