@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.
- package/package.json +15 -2
- package/src/agent/DiagnosticModal.svelte +126 -50
- package/src/agent/EphemeralBubble.svelte +13 -3
- package/src/agent/MCPserversList.svelte +147 -0
- package/src/agent/McpConnector.svelte +10 -1
- package/src/agent/RecipeBrowser.svelte +384 -0
- package/src/agent/RemoteMCPserversDemo.svelte +5 -121
- package/src/agent/ToolBrowser.svelte +133 -0
- package/src/agent/WebMCPserversList.svelte +2 -0
- package/src/agent/useAgentLoop.svelte.ts +396 -0
- package/src/base/chat-inline.svelte +64 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/components/HeaderControls.svelte +78 -0
- package/src/index.ts +13 -35
- package/src/stores/canvas.svelte.ts +0 -6
- package/src/widgets/SafeImage.svelte +67 -0
- package/src/widgets/WidgetRenderer.svelte +153 -78
- package/src/widgets/notebook/executors/index.ts +0 -1
- package/src/widgets/notebook/executors/sql.ts +32 -182
- package/src/widgets/notebook/import-modal-api.ts +237 -0
- package/src/widgets/notebook/import-modal.svelte +738 -0
- package/src/widgets/notebook/left-pane.ts +1 -1
- package/src/widgets/notebook/notebook.svelte +75 -0
- package/src/widgets/notebook/notebook.ts +38 -73
- package/src/widgets/notebook/prose.ts +6 -3
- package/src/widgets/notebook/shared.ts +68 -49
- package/src/widgets/rich/cards.svelte +74 -0
- package/src/widgets/rich/carousel.svelte +126 -0
- package/src/widgets/rich/chart-rich.svelte +221 -0
- package/src/widgets/rich/chat-input.svelte +52 -0
- package/src/widgets/rich/data-table.svelte +132 -0
- package/src/widgets/rich/gallery.svelte +115 -0
- package/src/widgets/rich/grid-data.svelte +85 -0
- package/src/widgets/rich/hemicycle.svelte +95 -0
- package/src/widgets/rich/js-sandbox.svelte +67 -0
- package/src/widgets/rich/json-viewer.svelte +82 -0
- package/src/widgets/rich/log.svelte +62 -0
- package/src/widgets/rich/profile.svelte +91 -0
- package/src/widgets/rich/sankey.svelte +73 -0
- package/src/widgets/rich/stat-card.svelte +60 -0
- package/src/widgets/rich/timeline.svelte +95 -0
- package/src/widgets/rich/trombinoscope.svelte +87 -0
- package/src/widgets/simple/actions.svelte +36 -0
- package/src/widgets/simple/alert.svelte +52 -0
- package/src/widgets/simple/chart.svelte +38 -0
- package/src/widgets/simple/code.svelte +30 -0
- package/src/widgets/simple/kv.svelte +31 -0
- package/src/widgets/simple/list.svelte +35 -0
- package/src/widgets/simple/stat.svelte +36 -0
- package/src/widgets/simple/tags.svelte +34 -0
- package/src/widgets/simple/text.svelte +130 -0
- package/src/widgets/helpers/safe-image.ts +0 -78
- package/src/widgets/notebook/import-modals.ts +0 -560
- package/src/widgets/notebook/recipe-browser.ts +0 -350
- package/src/widgets/rich/cards.ts +0 -181
- package/src/widgets/rich/carousel.ts +0 -319
- package/src/widgets/rich/chart-rich.ts +0 -386
- package/src/widgets/rich/d3.ts +0 -503
- package/src/widgets/rich/data-table.ts +0 -342
- package/src/widgets/rich/gallery.ts +0 -350
- package/src/widgets/rich/grid-data.ts +0 -173
- package/src/widgets/rich/hemicycle.ts +0 -313
- package/src/widgets/rich/js-sandbox.ts +0 -122
- package/src/widgets/rich/json-viewer.ts +0 -202
- package/src/widgets/rich/log.ts +0 -143
- package/src/widgets/rich/map.ts +0 -218
- package/src/widgets/rich/profile.ts +0 -256
- package/src/widgets/rich/sankey.ts +0 -257
- package/src/widgets/rich/stat-card.ts +0 -125
- package/src/widgets/rich/timeline.ts +0 -179
- package/src/widgets/rich/trombinoscope.ts +0 -246
- package/src/widgets/simple/actions.ts +0 -89
- package/src/widgets/simple/alert.ts +0 -100
- package/src/widgets/simple/chart.ts +0 -189
- package/src/widgets/simple/code.ts +0 -79
- package/src/widgets/simple/kv.ts +0 -68
- package/src/widgets/simple/list.ts +0 -89
- package/src/widgets/simple/stat.ts +0 -58
- package/src/widgets/simple/tags.ts +0 -125
- 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
|
+
>‹</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
|
+
>›</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}>‹</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}>›</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>
|