@webmcp-auto-ui/ui 2.5.32 → 2.5.34
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 +16 -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 +68 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/base/dialog-trigger.svelte +3 -2
- 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 +51 -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,95 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-timeline', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface TimelineEvent {
|
|
5
|
+
date?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status?: 'done' | 'active' | 'pending';
|
|
9
|
+
color?: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TimelineData {
|
|
15
|
+
title?: string;
|
|
16
|
+
events?: TimelineEvent[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
data?: TimelineData | null;
|
|
21
|
+
oneventclick?: (e: TimelineEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { data = {}, oneventclick }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const STATUS: Record<string, string> = {
|
|
27
|
+
done: 'var(--color-teal)',
|
|
28
|
+
active: 'var(--color-accent)',
|
|
29
|
+
pending: 'var(--color-border2)',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const events = $derived<TimelineEvent[]>(
|
|
33
|
+
Array.isArray(data?.events) && (data!.events as unknown[]).length
|
|
34
|
+
? data!.events as TimelineEvent[]
|
|
35
|
+
: []
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function handleEventActivate(event: TimelineEvent, e: KeyboardEvent) {
|
|
39
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
oneventclick?.(event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
47
|
+
{#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
|
|
48
|
+
{#if events.length === 0}
|
|
49
|
+
<p class="text-text2 text-sm">No events</p>
|
|
50
|
+
{:else}
|
|
51
|
+
<div class="py-1">
|
|
52
|
+
{#each events as event, i}
|
|
53
|
+
{@const isLast = i === events.length - 1}
|
|
54
|
+
{@const dotColor = event.color ?? STATUS[event.status ?? 'pending'] ?? 'var(--color-border2)'}
|
|
55
|
+
<div
|
|
56
|
+
class="flex gap-4 relative {!isLast ? 'pb-5' : ''} {oneventclick ? 'cursor-pointer' : ''}"
|
|
57
|
+
role={oneventclick ? 'button' : undefined}
|
|
58
|
+
tabindex={oneventclick ? 0 : undefined}
|
|
59
|
+
aria-label={oneventclick ? `${event.title ?? 'Event'} — ${event.date ?? ''}` : undefined}
|
|
60
|
+
title={oneventclick ? 'Double-cliquez pour interagir' : undefined}
|
|
61
|
+
ondblclick={() => oneventclick?.(event)}
|
|
62
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(event); } }}
|
|
63
|
+
>
|
|
64
|
+
<div class="flex flex-col items-center flex-shrink-0">
|
|
65
|
+
<div
|
|
66
|
+
class="w-3 h-3 rounded-full flex-shrink-0 mt-0.5"
|
|
67
|
+
style="background:{dotColor};{event.status === 'active' ? `box-shadow:0 0 0 3px ${dotColor}33;` : ''}"
|
|
68
|
+
></div>
|
|
69
|
+
{#if !isLast}<div class="w-0.5 flex-1 bg-border mt-1"></div>{/if}
|
|
70
|
+
</div>
|
|
71
|
+
<div class="flex-1 min-w-0 pb-1">
|
|
72
|
+
<div class="text-xs text-text2 mb-0.5">{event.date ?? ''}</div>
|
|
73
|
+
<div class="font-semibold text-text1 text-sm">
|
|
74
|
+
{#if event.href}
|
|
75
|
+
<a href={event.href} class="text-accent no-underline hover:underline">{event.title ?? ''}</a>
|
|
76
|
+
{:else}
|
|
77
|
+
{event.title ?? ''}
|
|
78
|
+
{/if}
|
|
79
|
+
</div>
|
|
80
|
+
{#if event.description}
|
|
81
|
+
<div class="text-sm text-text2 mt-0.5">{event.description}</div>
|
|
82
|
+
{/if}
|
|
83
|
+
{#if event.tags?.length}
|
|
84
|
+
<div class="flex gap-1 flex-wrap mt-1">
|
|
85
|
+
{#each event.tags as tag}
|
|
86
|
+
<span class="text-xs bg-surface2 text-text2 px-1.5 py-0.5 rounded">{tag}</span>
|
|
87
|
+
{/each}
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
{/each}
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-trombinoscope', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface TrombinoscopePerson { name: string; subtitle?: string; avatar?: string; badge?: string; color?: string; badgeColor?: string; }
|
|
5
|
+
export interface TrombinoscopeData {
|
|
6
|
+
title?: string;
|
|
7
|
+
people?: TrombinoscopePerson[];
|
|
8
|
+
columns?: number;
|
|
9
|
+
showBadge?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data?: TrombinoscopeData | null;
|
|
14
|
+
onpersonclick?: (p: TrombinoscopePerson) => void;
|
|
15
|
+
}
|
|
16
|
+
let { data = {}, onpersonclick }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const COLORS = ['#7c6dfa', '#3ecfb2', '#f0a050', '#fa6d7c', '#3b82f6', '#a855f7', '#14b8a6', '#f97316'];
|
|
19
|
+
const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
|
|
20
|
+
|
|
21
|
+
function isValidImageUrl(url: string | undefined): boolean {
|
|
22
|
+
return !!url && VALID_PREFIXES.some(p => url.startsWith(p));
|
|
23
|
+
}
|
|
24
|
+
function nameColor(name: string): string {
|
|
25
|
+
let h = 0;
|
|
26
|
+
for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
|
|
27
|
+
return COLORS[Math.abs(h) % COLORS.length];
|
|
28
|
+
}
|
|
29
|
+
function initials(name: string): string {
|
|
30
|
+
return name.split(/\s+/).slice(0, 2).map((w: string) => w[0] ?? '').join('').toUpperCase() || '?';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Track per-person avatar load failures to fall back to initials */
|
|
34
|
+
let failedAvatars = $state(new Set<string>());
|
|
35
|
+
|
|
36
|
+
const people = $derived.by<TrombinoscopePerson[]>(() => {
|
|
37
|
+
if (Array.isArray(data?.people) && data!.people!.length) return data!.people!;
|
|
38
|
+
return [];
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const cols = $derived(data?.columns ?? 4);
|
|
42
|
+
</script>
|
|
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 people.length === 0}<p class="text-text2 text-sm">Aucune personne</p>
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="grid gap-3 responsive-trombi" style="--trombi-cols: repeat({cols}, minmax(0, 1fr));">
|
|
49
|
+
{#each people as person}
|
|
50
|
+
{@const accent = person.color ?? nameColor(person.name)}
|
|
51
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
52
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
53
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
54
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
55
|
+
<div
|
|
56
|
+
class="flex flex-col items-center text-center p-3 rounded-lg border border-border hover:border-border2 transition-all {onpersonclick ? 'cursor-pointer' : ''}"
|
|
57
|
+
role={onpersonclick ? 'button' : undefined}
|
|
58
|
+
tabindex={onpersonclick ? 0 : undefined}
|
|
59
|
+
onclick={() => onpersonclick?.(person)}
|
|
60
|
+
>
|
|
61
|
+
{#if isValidImageUrl(person.avatar) && !failedAvatars.has(person.avatar!)}
|
|
62
|
+
<img
|
|
63
|
+
src={person.avatar}
|
|
64
|
+
alt={person.name}
|
|
65
|
+
class="w-12 h-12 rounded-full object-cover mb-2 border-2"
|
|
66
|
+
style="border-color:{accent};"
|
|
67
|
+
onerror={() => { failedAvatars = new Set([...failedAvatars, person.avatar!]); }}
|
|
68
|
+
/>
|
|
69
|
+
{:else}
|
|
70
|
+
<div class="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-base mb-2 flex-shrink-0" style="background:{accent};">{initials(person.name)}</div>
|
|
71
|
+
{/if}
|
|
72
|
+
<div class="text-xs font-semibold text-text1 leading-tight truncate w-full">{person.name}</div>
|
|
73
|
+
{#if person.subtitle}<div class="text-xs text-text2 mt-0.5 truncate w-full">{person.subtitle}</div>{/if}
|
|
74
|
+
{#if data?.showBadge !== false && person.badge}
|
|
75
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full mt-1.5 text-white" style="background:{person.badgeColor ?? accent};">{person.badge}</span>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
78
|
+
{/each}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="mt-3 text-xs text-text2">{people.length} personne{people.length !== 1 ? 's' : ''}</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.responsive-trombi { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
86
|
+
@media (min-width: 768px) { .responsive-trombi { grid-template-columns: var(--trombi-cols); } }
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-actions', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface ActionButton {
|
|
5
|
+
label: string;
|
|
6
|
+
primary?: boolean;
|
|
7
|
+
/** aria-label override for icon-only buttons */
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
onclick?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ActionsData {
|
|
13
|
+
buttons?: ActionButton[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
data?: ActionsData | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { data = {} }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const buttons = $derived(data?.buttons ?? []);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class="p-3 md:p-4 flex gap-2 flex-wrap">
|
|
26
|
+
{#each buttons as btn}
|
|
27
|
+
<button
|
|
28
|
+
class="text-xs font-mono px-4 py-2 rounded border transition-all
|
|
29
|
+
{btn.primary
|
|
30
|
+
? 'bg-accent border-accent text-white hover:opacity-85'
|
|
31
|
+
: 'border-border2 text-text2 hover:border-accent hover:text-accent'}"
|
|
32
|
+
aria-label={btn.ariaLabel ?? btn.label}
|
|
33
|
+
onclick={btn.onclick}
|
|
34
|
+
>{btn.label}</button>
|
|
35
|
+
{/each}
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-alert', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface AlertData {
|
|
5
|
+
title?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
level?: 'info' | 'warn' | 'error';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
data?: AlertData | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { data = {} }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const title = $derived(data?.title);
|
|
17
|
+
const message = $derived(data?.message);
|
|
18
|
+
const level = $derived(data?.level ?? 'warn');
|
|
19
|
+
|
|
20
|
+
const borderColor = $derived(
|
|
21
|
+
level === 'error'
|
|
22
|
+
? 'border-accent2'
|
|
23
|
+
: level === 'info'
|
|
24
|
+
? 'border-blue-500'
|
|
25
|
+
: 'border-amber',
|
|
26
|
+
);
|
|
27
|
+
const titleColor = $derived(
|
|
28
|
+
level === 'error'
|
|
29
|
+
? 'text-accent2'
|
|
30
|
+
: level === 'info'
|
|
31
|
+
? 'text-blue-400'
|
|
32
|
+
: 'text-amber',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// a11y: error/warn → role="alert" + aria-live="assertive"
|
|
36
|
+
// info → role="status" + aria-live="polite"
|
|
37
|
+
const role = $derived(level === 'error' || level === 'warn' ? 'alert' : 'status');
|
|
38
|
+
const ariaLive = $derived(level === 'error' || level === 'warn' ? 'assertive' : 'polite');
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
class="p-3 md:p-4 border-l-4 {borderColor}"
|
|
43
|
+
role={role}
|
|
44
|
+
aria-live={ariaLive}
|
|
45
|
+
>
|
|
46
|
+
{#if title}
|
|
47
|
+
<div class="font-semibold text-sm mb-1 {titleColor}">{title}</div>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if message}
|
|
50
|
+
<div class="text-xs font-mono text-text2">{message}</div>
|
|
51
|
+
{/if}
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-chart', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface ChartData {
|
|
5
|
+
title?: string;
|
|
6
|
+
bars?: [string, number][];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
data?: ChartData | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { data = {} }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const title = $derived(data?.title);
|
|
16
|
+
const bars = $derived(data?.bars ?? []);
|
|
17
|
+
const max = $derived(Math.max(...bars.map((b) => b[1]), 1));
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="p-3 md:p-4">
|
|
21
|
+
{#if title}
|
|
22
|
+
<div class="text-[10px] font-mono text-text2 mb-4 uppercase tracking-widest">{title}</div>
|
|
23
|
+
{/if}
|
|
24
|
+
<div class="flex items-end gap-1.5 h-32" role="img" aria-label={title ?? 'Bar chart'}>
|
|
25
|
+
{#each bars as [label, val]}
|
|
26
|
+
<div
|
|
27
|
+
class="flex-1 rounded-t bg-accent opacity-80 hover:opacity-100 transition-all"
|
|
28
|
+
style="height: max(2px, {Math.round((val / max) * 100)}%)"
|
|
29
|
+
title="{label}: {val}"
|
|
30
|
+
></div>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex gap-1.5 mt-1" aria-hidden="true">
|
|
34
|
+
{#each bars as [label]}
|
|
35
|
+
<span class="flex-1 text-center text-[9px] font-mono text-text2 truncate">{label}</span>
|
|
36
|
+
{/each}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-code', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface CodeData {
|
|
5
|
+
lang?: string;
|
|
6
|
+
content?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
data?: CodeData | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { data = {} }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const lang = $derived(data?.lang ?? 'text');
|
|
16
|
+
const content = $derived(data?.content ?? '');
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="rounded overflow-hidden">
|
|
20
|
+
<div class="bg-black/40 px-3 py-1.5 md:px-4 border-b border-border">
|
|
21
|
+
<span class="text-[10px] font-mono text-text2">{lang}</span>
|
|
22
|
+
</div>
|
|
23
|
+
<!-- a11y: tabindex + focus-visible for keyboard access to scrollable code block -->
|
|
24
|
+
<pre
|
|
25
|
+
class="font-mono text-xs text-teal bg-black/30 p-3 md:p-4 overflow-x-auto leading-relaxed focus:outline focus:outline-2 focus:outline-accent"
|
|
26
|
+
tabindex="0"
|
|
27
|
+
role="region"
|
|
28
|
+
aria-label="Code block: {lang}"
|
|
29
|
+
>{content}</pre>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-kv', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface KvData {
|
|
5
|
+
title?: string;
|
|
6
|
+
rows?: [string, string][];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
data?: KvData | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { data = {} }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const title = $derived(data?.title);
|
|
16
|
+
const rows = $derived(data?.rows ?? []);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="p-3 md:p-4">
|
|
20
|
+
{#if title}
|
|
21
|
+
<div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{title}</div>
|
|
22
|
+
{/if}
|
|
23
|
+
<div class="flex flex-col gap-1.5">
|
|
24
|
+
{#each rows as [k, v]}
|
|
25
|
+
<div class="flex justify-between items-center text-sm border-b border-border pb-1.5 last:border-none last:pb-0">
|
|
26
|
+
<span class="font-mono text-xs text-text2">{k}</span>
|
|
27
|
+
<span class="text-text1 font-medium">{v}</span>
|
|
28
|
+
</div>
|
|
29
|
+
{/each}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-list', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface ListData {
|
|
5
|
+
title?: string;
|
|
6
|
+
items?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
data?: ListData | null;
|
|
11
|
+
onitemclick?: (item: string, index: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { data = {}, onitemclick }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const title = $derived(data?.title);
|
|
17
|
+
const items = $derived(data?.items ?? []);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="p-3 md:p-4">
|
|
21
|
+
{#if title}
|
|
22
|
+
<div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{title}</div>
|
|
23
|
+
{/if}
|
|
24
|
+
<ul class="flex flex-col gap-1.5">
|
|
25
|
+
{#each items as item, i}
|
|
26
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
27
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
28
|
+
<li
|
|
29
|
+
class="text-sm text-text1 bg-surface2 rounded px-3 py-2 border-l-2 border-accent {onitemclick ? 'cursor-pointer hover:bg-surface2/80' : ''}"
|
|
30
|
+
title={onitemclick ? 'Double-cliquez pour interagir' : undefined}
|
|
31
|
+
ondblclick={() => onitemclick?.(item, i)}
|
|
32
|
+
>{item}</li>
|
|
33
|
+
{/each}
|
|
34
|
+
</ul>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-stat', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface StatData {
|
|
5
|
+
label?: string;
|
|
6
|
+
value?: string | number;
|
|
7
|
+
trend?: string;
|
|
8
|
+
trendDir?: 'up' | 'down' | 'neutral';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
data?: StatData | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { data = {} }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const label = $derived(data?.label ?? 'Metric');
|
|
18
|
+
const value = $derived(data?.value ?? '—');
|
|
19
|
+
const trend = $derived(data?.trend);
|
|
20
|
+
const trendDir = $derived(data?.trendDir);
|
|
21
|
+
|
|
22
|
+
const trendColor = $derived(
|
|
23
|
+
trendDir === 'up' ? 'text-teal' : trendDir === 'down' ? 'text-accent2' : 'text-text2',
|
|
24
|
+
);
|
|
25
|
+
const trendArrow = $derived(
|
|
26
|
+
trendDir === 'up' ? '↑' : trendDir === 'down' ? '↓' : '→',
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="p-4 md:p-5">
|
|
31
|
+
<div class="text-[11px] font-mono text-text2 mb-1 uppercase tracking-widest">{label}</div>
|
|
32
|
+
<div class="text-3xl md:text-4xl font-bold text-text1 leading-none">{value}</div>
|
|
33
|
+
{#if trend}
|
|
34
|
+
<div class="text-xs font-mono mt-2 {trendColor}">{trendArrow} {trend}</div>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-tags', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface TagItem {
|
|
5
|
+
text: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TagsData {
|
|
10
|
+
label?: string;
|
|
11
|
+
tags?: TagItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
data?: TagsData | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { data = {} }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const label = $derived(data?.label);
|
|
21
|
+
const tags = $derived(data?.tags ?? []);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="p-3 md:p-4 flex gap-2 flex-wrap items-center">
|
|
25
|
+
{#if label}
|
|
26
|
+
<span class="text-[10px] font-mono text-text2">{label}</span>
|
|
27
|
+
{/if}
|
|
28
|
+
{#each tags as tag}
|
|
29
|
+
<span
|
|
30
|
+
class="text-[11px] font-mono px-3 py-1 rounded-full border transition-colors
|
|
31
|
+
{tag.active ? 'border-teal text-teal bg-teal/10' : 'border-border2 text-text2'}"
|
|
32
|
+
>{tag.text}</span>
|
|
33
|
+
{/each}
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-text', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface TextData {
|
|
5
|
+
content?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
data?: TextData | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { data = {} }: Props = $props();
|
|
13
|
+
|
|
14
|
+
/** Minimal markdown → HTML renderer (no deps, naturally XSS-safe: only produces known tags) */
|
|
15
|
+
function renderMarkdown(src: string): string {
|
|
16
|
+
if (!src) return '';
|
|
17
|
+
|
|
18
|
+
// Escape HTML entities first (XSS protection)
|
|
19
|
+
const esc = (s: string) =>
|
|
20
|
+
s
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"');
|
|
25
|
+
|
|
26
|
+
const lines = src.split('\n');
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
let inCode = false;
|
|
29
|
+
let codeLines: string[] = [];
|
|
30
|
+
let inUl = false;
|
|
31
|
+
let inOl = false;
|
|
32
|
+
|
|
33
|
+
const closeList = () => {
|
|
34
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
35
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Inline formatting: bold, italic, code, links */
|
|
39
|
+
const inline = (s: string): string =>
|
|
40
|
+
esc(s)
|
|
41
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
42
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
43
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
44
|
+
.replace(
|
|
45
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
46
|
+
'<a href="$2" target="_blank" rel="noopener">$1</a>',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.trimStart().startsWith('```')) {
|
|
51
|
+
if (!inCode) {
|
|
52
|
+
closeList();
|
|
53
|
+
inCode = true;
|
|
54
|
+
codeLines = [];
|
|
55
|
+
} else {
|
|
56
|
+
out.push(`<pre><code>${esc(codeLines.join('\n'))}</code></pre>`);
|
|
57
|
+
inCode = false;
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (inCode) { codeLines.push(line); continue; }
|
|
62
|
+
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
|
|
65
|
+
if (!trimmed) { closeList(); out.push(''); continue; }
|
|
66
|
+
|
|
67
|
+
const hMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
68
|
+
if (hMatch) {
|
|
69
|
+
closeList();
|
|
70
|
+
const level = hMatch[1].length;
|
|
71
|
+
out.push(`<h${level}>${inline(hMatch[2])}</h${level}>`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (/^[-*+]\s+/.test(trimmed)) {
|
|
76
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
77
|
+
if (!inUl) { out.push('<ul>'); inUl = true; }
|
|
78
|
+
out.push(`<li>${inline(trimmed.replace(/^[-*+]\s+/, ''))}</li>`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const olMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
|
|
83
|
+
if (olMatch) {
|
|
84
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
85
|
+
if (!inOl) { out.push('<ol>'); inOl = true; }
|
|
86
|
+
out.push(`<li>${inline(olMatch[2])}</li>`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (/^[-*_]{3,}$/.test(trimmed)) {
|
|
91
|
+
closeList();
|
|
92
|
+
out.push('<hr>');
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
closeList();
|
|
97
|
+
out.push(`<p>${inline(trimmed)}</p>`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (inCode) out.push(`<pre><code>${esc(codeLines.join('\n'))}</code></pre>`);
|
|
101
|
+
closeList();
|
|
102
|
+
|
|
103
|
+
return out.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rendered = $derived(renderMarkdown(data?.content ?? ''));
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div class="tb-md p-4 md:p-5 text-sm leading-relaxed">{@html rendered}</div>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
.tb-md { color: var(--color-text2); }
|
|
113
|
+
.tb-md :global(h1) { font-size: 1.5em; font-weight: 700; color: var(--color-text1); margin: 0.8em 0 0.4em; }
|
|
114
|
+
.tb-md :global(h2) { font-size: 1.25em; font-weight: 600; color: var(--color-text1); margin: 0.7em 0 0.35em; }
|
|
115
|
+
.tb-md :global(h3) { font-size: 1.1em; font-weight: 600; color: var(--color-text1); margin: 0.6em 0 0.3em; }
|
|
116
|
+
.tb-md :global(h4), .tb-md :global(h5), .tb-md :global(h6) { font-size: 1em; font-weight: 600; color: var(--color-text1); margin: 0.5em 0 0.25em; }
|
|
117
|
+
.tb-md :global(p) { margin: 0.4em 0; }
|
|
118
|
+
.tb-md :global(strong) { font-weight: 600; color: var(--color-text1); }
|
|
119
|
+
.tb-md :global(em) { font-style: italic; }
|
|
120
|
+
.tb-md :global(a) { color: var(--color-accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
121
|
+
.tb-md :global(a:hover) { opacity: 0.8; }
|
|
122
|
+
.tb-md :global(ul), .tb-md :global(ol) { margin: 0.4em 0; padding-left: 1.5em; }
|
|
123
|
+
.tb-md :global(ul) { list-style: disc; }
|
|
124
|
+
.tb-md :global(ol) { list-style: decimal; }
|
|
125
|
+
.tb-md :global(li) { margin: 0.15em 0; }
|
|
126
|
+
.tb-md :global(code) { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 0.9em; background: var(--color-surface2); padding: 0.15em 0.35em; border-radius: 4px; }
|
|
127
|
+
.tb-md :global(pre) { background: var(--color-surface2); border-radius: 6px; padding: 0.75em 1em; margin: 0.5em 0; overflow-x: auto; }
|
|
128
|
+
.tb-md :global(pre code) { background: none; padding: 0; font-size: 0.85em; }
|
|
129
|
+
.tb-md :global(hr) { border: none; border-top: 1px solid var(--color-surface2); margin: 0.8em 0; }
|
|
130
|
+
</style>
|