@webmcp-auto-ui/ui 2.5.27 → 2.5.28

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 (82) hide show
  1. package/package.json +15 -3
  2. package/src/agent/DataServersPanel.svelte +164 -0
  3. package/src/agent/LLMSelector.svelte +11 -3
  4. package/src/agent/ModelCacheManager.svelte +359 -0
  5. package/src/index.ts +42 -30
  6. package/src/widgets/WidgetRenderer.svelte +114 -104
  7. package/src/widgets/export-widget.ts +28 -1
  8. package/src/widgets/helpers/safe-image.ts +78 -0
  9. package/src/widgets/notebook/.gitkeep +0 -0
  10. package/src/widgets/notebook/chart-renderer.ts +63 -0
  11. package/src/widgets/notebook/compact.ts +823 -0
  12. package/src/widgets/notebook/document.ts +1065 -0
  13. package/src/widgets/notebook/editorial.ts +936 -0
  14. package/src/widgets/notebook/executors/.gitkeep +1 -0
  15. package/src/widgets/notebook/executors/index.ts +4 -0
  16. package/src/widgets/notebook/executors/js-worker.ts +269 -0
  17. package/src/widgets/notebook/executors/sql.ts +206 -0
  18. package/src/widgets/notebook/import-modals.ts +553 -0
  19. package/src/widgets/notebook/left-pane.ts +249 -0
  20. package/src/widgets/notebook/prose.ts +280 -0
  21. package/src/widgets/notebook/recipe-browser.ts +350 -0
  22. package/src/widgets/notebook/recipes/compact.md +124 -0
  23. package/src/widgets/notebook/recipes/document.md +139 -0
  24. package/src/widgets/notebook/recipes/editorial.md +120 -0
  25. package/src/widgets/notebook/recipes/workspace.md +119 -0
  26. package/src/widgets/notebook/resource-extractor.ts +162 -0
  27. package/src/widgets/notebook/share-handlers.ts +222 -0
  28. package/src/widgets/notebook/shared.ts +1592 -0
  29. package/src/widgets/notebook/workspace.ts +852 -0
  30. package/src/widgets/rich/cards.ts +181 -0
  31. package/src/widgets/rich/carousel.ts +319 -0
  32. package/src/widgets/rich/chart-rich.ts +386 -0
  33. package/src/widgets/rich/d3.ts +503 -0
  34. package/src/widgets/rich/data-table.ts +342 -0
  35. package/src/widgets/rich/gallery.ts +350 -0
  36. package/src/widgets/rich/grid-data.ts +173 -0
  37. package/src/widgets/rich/hemicycle.ts +313 -0
  38. package/src/widgets/rich/js-sandbox.ts +106 -0
  39. package/src/widgets/rich/json-viewer.ts +202 -0
  40. package/src/widgets/rich/log.ts +143 -0
  41. package/src/widgets/rich/map.ts +218 -0
  42. package/src/widgets/rich/profile.ts +256 -0
  43. package/src/widgets/rich/sankey.ts +262 -0
  44. package/src/widgets/rich/stat-card.ts +125 -0
  45. package/src/widgets/rich/timeline.ts +179 -0
  46. package/src/widgets/rich/trombinoscope.ts +246 -0
  47. package/src/widgets/simple/actions.ts +89 -0
  48. package/src/widgets/simple/alert.ts +100 -0
  49. package/src/widgets/simple/chart.ts +189 -0
  50. package/src/widgets/simple/code.ts +79 -0
  51. package/src/widgets/simple/kv.ts +68 -0
  52. package/src/widgets/simple/list.ts +89 -0
  53. package/src/widgets/simple/stat.ts +58 -0
  54. package/src/widgets/simple/tags.ts +125 -0
  55. package/src/widgets/simple/text.ts +198 -0
  56. package/src/widgets/SafeImage.svelte +0 -76
  57. package/src/widgets/rich/Cards.svelte +0 -39
  58. package/src/widgets/rich/Carousel.svelte +0 -88
  59. package/src/widgets/rich/Chart.svelte +0 -142
  60. package/src/widgets/rich/D3Widget.svelte +0 -378
  61. package/src/widgets/rich/DataTable.svelte +0 -62
  62. package/src/widgets/rich/Gallery.svelte +0 -94
  63. package/src/widgets/rich/GridData.svelte +0 -44
  64. package/src/widgets/rich/Hemicycle.svelte +0 -78
  65. package/src/widgets/rich/JsSandbox.svelte +0 -51
  66. package/src/widgets/rich/JsonViewer.svelte +0 -42
  67. package/src/widgets/rich/LogViewer.svelte +0 -24
  68. package/src/widgets/rich/MapView.svelte +0 -140
  69. package/src/widgets/rich/ProfileCard.svelte +0 -59
  70. package/src/widgets/rich/Sankey.svelte +0 -56
  71. package/src/widgets/rich/StatCard.svelte +0 -35
  72. package/src/widgets/rich/Timeline.svelte +0 -43
  73. package/src/widgets/rich/Trombinoscope.svelte +0 -48
  74. package/src/widgets/simple/ActionsBlock.svelte +0 -15
  75. package/src/widgets/simple/AlertBlock.svelte +0 -11
  76. package/src/widgets/simple/ChartBlock.svelte +0 -21
  77. package/src/widgets/simple/CodeBlock.svelte +0 -11
  78. package/src/widgets/simple/KVBlock.svelte +0 -16
  79. package/src/widgets/simple/ListBlock.svelte +0 -17
  80. package/src/widgets/simple/StatBlock.svelte +0 -14
  81. package/src/widgets/simple/TagsBlock.svelte +0 -15
  82. package/src/widgets/simple/TextBlock.svelte +0 -122
@@ -1,51 +0,0 @@
1
- <script lang="ts">
2
- export interface JsSandboxSpec {
3
- title?: string;
4
- code: string;
5
- html?: string;
6
- css?: string;
7
- height?: string;
8
- }
9
-
10
- interface Props { spec: JsSandboxSpec; }
11
- let { spec }: Props = $props();
12
-
13
- const srcdoc = $derived(`<!DOCTYPE html>
14
- <html>
15
- <head>
16
- <meta charset="utf-8">
17
- <style>
18
- *,*::before,*::after{box-sizing:border-box}
19
- body{margin:0;padding:8px;font-family:system-ui,sans-serif;font-size:13px;background:var(--bg,#1a1a2e);color:var(--fg,#e2e2e8)}
20
- ${spec.css ?? ''}
21
- </style>
22
- </head>
23
- <body>
24
- <div id="root">${spec.html ?? ''}</div>
25
- <script>
26
- (function(){
27
- try{
28
- ${spec.code}
29
- }catch(e){
30
- document.getElementById('root').innerHTML='<pre style="color:red;white-space:pre-wrap">'+e+'</pre>';
31
- }
32
- })();
33
- <\/script>
34
- </body>
35
- </html>`);
36
- </script>
37
-
38
- <div class="bg-surface border border-border rounded-lg overflow-hidden font-sans">
39
- {#if spec.title}
40
- <div class="px-3 py-2 border-b border-border text-sm font-semibold text-text1 flex items-center gap-2">
41
- <span class="text-xs opacity-50">JS</span>
42
- {spec.title}
43
- </div>
44
- {/if}
45
- <iframe
46
- srcdoc={srcdoc}
47
- sandbox="allow-scripts"
48
- title={spec.title ?? 'JS Sandbox'}
49
- style="width:100%;height:{spec.height ?? '300px'};border:none;display:block;"
50
- ></iframe>
51
- </div>
@@ -1,42 +0,0 @@
1
- <script lang="ts">
2
- export interface JsonViewerSpec { title?: string; data?: unknown; maxDepth?: number; expanded?: boolean; theme?: 'dark'|'light'; }
3
- interface Props { spec: Partial<JsonViewerSpec>; data?: unknown; }
4
- let { spec, data }: Props = $props();
5
- const value = $derived(spec.data!==undefined ? spec.data : data);
6
- const maxDepth = $derived(spec.maxDepth??5);
7
- const expanded = $derived(spec.expanded!==false);
8
- </script>
9
-
10
- {#snippet node(val: unknown, depth: number)}
11
- {#if val===null}<span style="color:var(--color-text2)">null</span>
12
- {:else if typeof val==='boolean'}<span style="color:#a855f7">{String(val)}</span>
13
- {:else if typeof val==='number'}<span style="color:var(--color-amber)">{val}</span>
14
- {:else if typeof val==='string'}<span style="color:var(--color-teal)">"{val}"</span>
15
- {:else if Array.isArray(val)}
16
- {#if depth>=maxDepth}<span class="text-text2">[Array({val.length})]</span>
17
- {:else}
18
- <details open={expanded&&depth<2}>
19
- <summary class="cursor-pointer text-text2 hover:text-text1 select-none">Array({val.length})</summary>
20
- <div class="ml-4 border-l border-border pl-3 mt-0.5">
21
- {#each val as item, i}<div class="py-0.5"><span class="text-text2 text-xs mr-1">{i}:</span>{@render node(item, depth+1)}</div>{/each}
22
- </div>
23
- </details>
24
- {/if}
25
- {:else if typeof val==='object'}
26
- {#if depth>=maxDepth}<span class="text-text2">{`{Object(${Object.keys(val as object).length})}`}</span>
27
- {:else}
28
- <details open={expanded&&depth<2}>
29
- <summary class="cursor-pointer text-text2 hover:text-text1 select-none">{`{${Object.keys(val as object).length}}`}</summary>
30
- <div class="ml-4 border-l border-border pl-3 mt-0.5">
31
- {#each Object.entries(val as Record<string,unknown>) as [k, v]}<div class="py-0.5"><span class="text-accent mr-1">"{k}":</span>{@render node(v, depth+1)}</div>{/each}
32
- </div>
33
- </details>
34
- {/if}
35
- {:else}<span class="text-text2">{String(val)}</span>
36
- {/if}
37
- {/snippet}
38
-
39
- <div class="bg-bg border border-border rounded-lg p-3 md:p-4 font-mono text-xs leading-5 text-text1">
40
- {#if spec.title}<div class="font-sans text-sm font-semibold text-text1 mb-3">{spec.title}</div>{/if}
41
- {#if value===undefined}<span class="text-text2">undefined</span>{:else}{@render node(value, 0)}{/if}
42
- </div>
@@ -1,24 +0,0 @@
1
- <script lang="ts">
2
- export interface LogEntry { timestamp?: string; level?: 'debug'|'info'|'warn'|'error'; message: string; source?: string; }
3
- export interface LogViewerSpec { title?: string; entries?: LogEntry[]; maxHeight?: string; }
4
- interface Props { spec: Partial<LogViewerSpec>; data?: unknown; }
5
- let { spec, data }: Props = $props();
6
- const LEVEL: Record<string,string> = { debug:'text-text2', info:'text-teal', warn:'text-amber', error:'text-accent2' };
7
- const entries=$derived<LogEntry[]>(Array.isArray(spec.entries)&&spec.entries.length?spec.entries:Array.isArray(data)?data as LogEntry[]:[]);
8
- </script>
9
- <div class="bg-bg border border-border rounded-lg font-mono">
10
- {#if spec.title}<div class="px-4 py-2 border-b border-border text-xs text-text2">{spec.title}</div>{/if}
11
- <div class="overflow-y-auto text-xs leading-5 p-3 flex flex-col gap-0.5" style="max-height:{spec.maxHeight??'320px'};">
12
- {#if !entries.length}<span class="text-text2">No log entries</span>
13
- {:else}
14
- {#each entries as e}
15
- <div class="flex gap-2 items-start hover:bg-surface2 px-1 rounded">
16
- {#if e.timestamp}<span class="text-text2 flex-shrink-0">{e.timestamp}</span>{/if}
17
- <span class="flex-shrink-0 uppercase text-[10px] font-semibold w-10 {LEVEL[e.level??'info']??LEVEL.info}">{e.level??'info'}</span>
18
- {#if e.source}<span class="text-text2 flex-shrink-0">[{e.source}]</span>{/if}
19
- <span class="text-text1 break-all">{e.message}</span>
20
- </div>
21
- {/each}
22
- {/if}
23
- </div>
24
- </div>
@@ -1,140 +0,0 @@
1
- <script lang="ts">
2
- import { onMount, onDestroy } from 'svelte';
3
-
4
- export interface LatLng { lat: number; lng: number; }
5
- export interface MapMarker { lat: number; lng: number; label?: string; color?: string; }
6
- export interface MapSpec {
7
- title?: string;
8
- center?: LatLng;
9
- zoom?: number;
10
- height?: string;
11
- markers?: MapMarker[];
12
- }
13
- interface Props {
14
- spec: Partial<MapSpec>;
15
- data?: unknown;
16
- onmarkerclick?: (m: MapMarker) => void;
17
- }
18
-
19
- let { spec, onmarkerclick }: Props = $props();
20
-
21
- let container: HTMLDivElement | undefined = $state();
22
- let leafletLoaded = $state(false);
23
- let map: import('leaflet').Map | undefined;
24
- let activeMarkers: import('leaflet').CircleMarker[] = [];
25
- let L: typeof import('leaflet') | undefined;
26
-
27
- function markerColor(color?: string): string {
28
- return color ?? 'var(--color-accent)';
29
- }
30
-
31
- function syncMarkers(markers: MapMarker[]): void {
32
- if (!L || !map) return;
33
- activeMarkers.forEach(mk => mk.remove());
34
- activeMarkers = [];
35
- for (const marker of markers) {
36
- const cm = L.circleMarker([marker.lat, marker.lng], {
37
- radius: 7,
38
- color: markerColor(marker.color),
39
- fillColor: markerColor(marker.color),
40
- fillOpacity: 0.85,
41
- weight: 1.5,
42
- }).addTo(map);
43
- if (marker.label) {
44
- cm.bindTooltip(marker.label, {
45
- direction: 'top',
46
- offset: L.point(0, -8),
47
- className: 'mapview-tooltip',
48
- });
49
- }
50
- cm.on('click', () => onmarkerclick?.(marker));
51
- activeMarkers.push(cm);
52
- }
53
- }
54
-
55
- onMount(async () => {
56
- const leaflet = await import('leaflet');
57
- await import('leaflet/dist/leaflet.css');
58
- L = (leaflet.default ?? leaflet) as typeof import('leaflet');
59
- if (!container || !L) return;
60
-
61
- const center: [number, number] = spec.center
62
- ? [spec.center.lat, spec.center.lng]
63
- : [46.6, 2.3];
64
-
65
- map = L.map(container, {
66
- center,
67
- zoom: spec.zoom ?? 6,
68
- zoomControl: true,
69
- attributionControl: true,
70
- });
71
-
72
- L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
73
- attribution: '&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
74
- subdomains: 'abcd',
75
- maxZoom: 19,
76
- }).addTo(map);
77
-
78
- syncMarkers(spec.markers ?? []);
79
- leafletLoaded = true;
80
- });
81
-
82
- onDestroy(() => {
83
- map?.remove();
84
- map = undefined;
85
- });
86
-
87
- $effect(() => {
88
- if (!leafletLoaded) return;
89
- syncMarkers(spec.markers ?? []);
90
- });
91
-
92
- $effect(() => {
93
- if (!map || !spec.center) return;
94
- map.setView([spec.center.lat, spec.center.lng], spec.zoom ?? 6);
95
- });
96
- </script>
97
-
98
- <style>
99
- :global(.mapview-tooltip) {
100
- background: var(--color-surface2);
101
- border: 1px solid var(--color-border);
102
- color: var(--color-text1);
103
- font-size: 11px;
104
- font-family: ui-monospace, monospace;
105
- border-radius: 4px;
106
- padding: 2px 6px;
107
- box-shadow: 0 2px 8px rgba(0,0,0,0.4);
108
- }
109
- :global(.mapview-tooltip::before) {
110
- border-top-color: var(--color-border);
111
- }
112
- :global(.leaflet-container) {
113
- background: var(--color-bg);
114
- }
115
- </style>
116
-
117
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
118
- {#if spec.title}
119
- <h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>
120
- {/if}
121
- <div
122
- bind:this={container}
123
- class="rounded overflow-hidden border border-border"
124
- style="height:{spec.height ?? '400px'};"
125
- >
126
- {#if !leafletLoaded}
127
- <div class="w-full h-full bg-bg flex flex-col items-center justify-center text-text2 text-sm gap-2">
128
- <svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
129
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 13l4.553 2.276A1 1 0 0021 21.382V10.618a1 1 0 00-1.447-.894L15 12m0 8V12M9 7l6-2.5"/>
130
- </svg>
131
- <span class="font-mono text-xs">Chargement de la carte…</span>
132
- </div>
133
- {/if}
134
- </div>
135
- {#if spec.markers?.length}
136
- <div class="mt-2 text-xs text-text2 font-mono">
137
- {spec.markers.length} marqueur{spec.markers.length > 1 ? 's' : ''}
138
- </div>
139
- {/if}
140
- </div>
@@ -1,59 +0,0 @@
1
- <script lang="ts">
2
- import SafeImage from '../SafeImage.svelte';
3
- export interface ProfileField { label: string; value: string; href?: string; }
4
- export interface ProfileStat { label: string; value: string; }
5
- export interface ProfileAction { label: string; href?: string; variant?: 'primary'|'secondary'|'danger'; onclick?: () => void; }
6
- export interface ProfileSpec { name?: string; subtitle?: string; avatar?: { src: string; alt?: string }; badge?: { text: string; variant?: 'default'|'success'|'warning'|'error' }; fields?: ProfileField[]; stats?: ProfileStat[]; actions?: ProfileAction[]; }
7
- interface Props { spec: Partial<ProfileSpec>; }
8
- let { spec }: Props = $props();
9
- /** Track if avatar failed to load — fall back to initials */
10
- let avatarFailed = $state(false);
11
- $effect(() => { if (spec.avatar?.src) avatarFailed = false; });
12
- const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
13
- const avatarValid = $derived(
14
- !!spec.avatar?.src && VALID_PREFIXES.some(p => spec.avatar!.src.startsWith(p))
15
- );
16
- const BADGE: Record<string,string> = { default:'bg-surface2 text-text2', success:'bg-teal/20 text-teal', warning:'bg-amber/20 text-amber', error:'bg-accent2/20 text-accent2' };
17
- const ACTION: Record<string,string> = { primary:'bg-accent text-white', secondary:'bg-surface2 text-text2', danger:'bg-accent2 text-white' };
18
- const initials = $derived((spec.name??'?').split(/\s+/).slice(0,2).map((w:string)=>w[0]??'').join('').toUpperCase()||'?');
19
- </script>
20
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans max-w-full md:max-w-[480px]">
21
- <div class="flex flex-col sm:flex-row items-center sm:items-start mb-4 gap-3 sm:gap-4">
22
- {#if avatarValid && !avatarFailed}
23
- <img src={spec.avatar?.src} alt={spec.avatar?.alt??''} class="w-16 h-16 rounded-full object-cover border-2 border-border2 flex-shrink-0" onerror={() => { avatarFailed = true; }} />
24
- {:else}
25
- <div class="w-16 h-16 rounded-full bg-accent text-white flex items-center justify-center text-xl font-bold flex-shrink-0">{initials}</div>
26
- {/if}
27
- <div>
28
- <h3 class="text-lg font-bold text-text1 m-0">{spec.name??''}</h3>
29
- {#if spec.subtitle}<div class="text-sm text-text2 mt-0.5">{spec.subtitle}</div>{/if}
30
- {#if spec.badge}<div class="mt-1"><span class="text-xs font-semibold px-2 py-0.5 rounded-full {BADGE[spec.badge.variant??'default']??BADGE.default}">{spec.badge.text}</span></div>{/if}
31
- </div>
32
- </div>
33
- {#if spec.fields?.length}
34
- <dl class="border-t border-border pt-3 m-0">
35
- {#each spec.fields as f}
36
- <div class="flex gap-2 mb-1.5">
37
- <dt class="text-xs text-text2 min-w-[80px] sm:min-w-[100px] font-mono">{f.label}</dt>
38
- <dd class="text-sm text-text1 m-0">{#if f.href}<a href={f.href} class="text-accent hover:underline">{f.value}</a>{:else}{f.value}{/if}</dd>
39
- </div>
40
- {/each}
41
- </dl>
42
- {/if}
43
- {#if spec.stats?.length}
44
- <div class="flex flex-wrap border border-border rounded overflow-hidden mt-3">
45
- {#each spec.stats as s}<div class="text-center px-4 py-2 border-r border-border last:border-r-0 flex-1 min-w-[80px]"><div class="text-xl font-bold text-accent">{s.value}</div><div class="text-xs text-text2">{s.label}</div></div>{/each}
46
- </div>
47
- {/if}
48
- {#if spec.actions?.length}
49
- <div class="flex gap-2 mt-3 flex-wrap">
50
- {#each spec.actions as a}
51
- {#if a.href}
52
- <a href={a.href} class="inline-flex items-center px-3.5 py-1.5 rounded text-xs font-semibold no-underline {ACTION[a.variant??'secondary']??ACTION.secondary}">{a.label}</a>
53
- {:else}
54
- <button class="inline-flex items-center px-3.5 py-1.5 rounded text-xs font-semibold border-0 cursor-pointer {ACTION[a.variant??'secondary']??ACTION.secondary}" onclick={a.onclick}>{a.label}</button>
55
- {/if}
56
- {/each}
57
- </div>
58
- {/if}
59
- </div>
@@ -1,56 +0,0 @@
1
- <script lang="ts">
2
- export interface SankeyNode { id: string; label: string; color?: string; summary?: string; }
3
- export interface SankeyLink { source: string; target: string; value: number; label?: string; }
4
- export interface SankeySpec { title?: string; nodes?: SankeyNode[]; links?: SankeyLink[]; }
5
- interface Props { spec: Partial<SankeySpec>; onnodeclick?: (n: SankeyNode) => void; onlinkclick?: (l: SankeyLink) => void; }
6
- let { spec, onnodeclick, onlinkclick }: Props = $props();
7
- let host: HTMLElement | undefined = $state();
8
- const nodes=$derived<SankeyNode[]>(Array.isArray(spec.nodes)?spec.nodes:[]);
9
- const links=$derived<SankeyLink[]>(Array.isArray(spec.links)?spec.links:[]);
10
- const nodeMap=$derived(new Map(nodes.map(n=>[n.id,n])));
11
- const maxVal=$derived(Math.max(...links.map(l=>l.value),1));
12
- const sorted=$derived([...links].sort((a,b)=>b.value-a.value));
13
- function dispatchNodeDblclick(node: SankeyNode | undefined) {
14
- if (!node || !host) return;
15
- host.dispatchEvent(new CustomEvent('widget:node-dblclick', {
16
- detail: { nodeId: node.id, nodeData: node },
17
- bubbles: true,
18
- }));
19
- }
20
- </script>
21
- <div bind:this={host} class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
22
- {#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
23
- {#if !nodes.length||!links.length}<p class="text-text2 text-sm">No flow data</p>
24
- {:else}
25
- <div class="text-xs text-text2 mb-2 font-mono">{nodes.length} nodes · {links.length} flows</div>
26
- <div class="flex flex-col gap-1.5">
27
- {#each sorted as link}
28
- {@const src=nodeMap.get(link.source)}
29
- {@const tgt=nodeMap.get(link.target)}
30
- {@const sc=src?.color??'var(--color-accent)'}{@const tc=tgt?.color??'var(--color-teal)'}
31
- {@const pct=Math.round(link.value/maxVal*100)}
32
- {@const barH=Math.max(4,Math.round(link.value/maxVal*20))}
33
- <!-- svelte-ignore a11y_click_events_have_key_events -->
34
- <!-- svelte-ignore a11y_no_static_element_interactions -->
35
- <div class="flex items-center gap-2 text-xs cursor-pointer hover:opacity-80 transition-opacity" role="button" tabindex="0" onclick={()=>onlinkclick?.(link)} onkeydown={(e)=>{if(e.key==='Enter')onlinkclick?.(link)}}>
36
- <span
37
- class="text-text2 min-w-[80px] truncate font-mono"
38
- style="color:{sc};"
39
- title={src?.summary ?? src?.label ?? link.source}
40
- ondblclick={(e)=>{e.stopPropagation(); dispatchNodeDblclick(src); onnodeclick?.(src!);}}
41
- >{src?.label??link.source}</span>
42
- <div class="flex-1 bg-surface2 rounded-full overflow-hidden" style="height:{barH}px;">
43
- <div class="rounded-full h-full" style="width:{pct}%;background:linear-gradient(to right,{sc},{tc});"></div>
44
- </div>
45
- <span
46
- class="text-text2 min-w-[80px] truncate text-right font-mono"
47
- style="color:{tc};"
48
- title={tgt?.summary ?? tgt?.label ?? link.target}
49
- ondblclick={(e)=>{e.stopPropagation(); dispatchNodeDblclick(tgt); onnodeclick?.(tgt!);}}
50
- >{tgt?.label??link.target}</span>
51
- <span class="text-text2 min-w-[40px] text-right font-mono">{link.value}</span>
52
- </div>
53
- {/each}
54
- </div>
55
- {/if}
56
- </div>
@@ -1,35 +0,0 @@
1
- <script lang="ts">
2
- export interface StatCardTrend { direction: 'up'|'down'|'flat'; value?: string; positive?: boolean; }
3
- export interface StatCardSpec { label?: string; value?: unknown; unit?: string; delta?: string; trend?: 'up'|'down'|'flat'|StatCardTrend; previousValue?: unknown; variant?: 'default'|'success'|'warning'|'error'|'info'; }
4
- interface Props { spec: Partial<StatCardSpec>; }
5
- let { spec }: Props = $props();
6
- const COLORS: Record<string, string> = { default:'var(--color-accent)', success:'var(--color-teal)', warning:'var(--color-amber)', error:'var(--color-accent2)', info:'#3b82f6' };
7
- const accent = $derived(COLORS[spec.variant ?? 'default'] ?? 'var(--color-accent)');
8
- const trendInfo = $derived(() => {
9
- const t = spec.trend;
10
- if (!t) return null;
11
- if (typeof t === 'string') {
12
- const arr = t==='up'?'↑':t==='down'?'↓':'→';
13
- const col = t==='up'?'var(--color-teal)':t==='flat'?'var(--color-text2)':'var(--color-accent2)';
14
- return { arrow: arr, val: spec.delta??'', color: col };
15
- }
16
- const positive = t.positive ?? t.direction==='up';
17
- return { arrow: t.direction==='up'?'↑':t.direction==='down'?'↓':'→', val: t.value??'', color: positive?'var(--color-teal)':t.direction==='flat'?'var(--color-text2)':'var(--color-accent2)' };
18
- });
19
- </script>
20
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans min-w-[140px] md:min-w-[160px]" style="border-top: 3px solid {accent};">
21
- <div class="text-center py-2">
22
- <div class="text-xs font-mono text-text2 uppercase tracking-widest mb-2">{spec.label ?? ''}</div>
23
- <div class="text-3xl md:text-4xl font-bold leading-none" style="color:{accent};">
24
- {spec.value ?? '—'}{#if spec.unit}<span class="text-base text-text2 ml-1">{spec.unit}</span>{/if}
25
- </div>
26
- {#if trendInfo()}
27
- {@const t = trendInfo()!}
28
- <div class="flex items-center gap-1 justify-center mt-1">
29
- <span class="text-base font-bold" style="color:{t.color};">{t.arrow}</span>
30
- {#if t.val}<span class="text-sm font-semibold" style="color:{t.color};">{t.val}</span>{/if}
31
- </div>
32
- {/if}
33
- {#if spec.previousValue !== undefined}<div class="text-xs text-text2 mt-1">prev: {spec.previousValue}</div>{/if}
34
- </div>
35
- </div>
@@ -1,43 +0,0 @@
1
- <script lang="ts">
2
- export interface TimelineEvent { date?: string; title?: string; description?: string; status?: 'done'|'active'|'pending'; color?: string; href?: string; tags?: string[]; }
3
- export interface TimelineSpec { title?: string; events?: TimelineEvent[]; }
4
- interface Props { spec: Partial<TimelineSpec>; data?: unknown; oneventclick?: (e: TimelineEvent) => void; }
5
- let { spec, data, oneventclick }: Props = $props();
6
- const STATUS: Record<string,string> = { done:'var(--color-teal)', active:'var(--color-accent)', pending:'var(--color-border2)' };
7
- const events = $derived<TimelineEvent[]>(Array.isArray(spec.events)&&spec.events.length ? spec.events : Array.isArray(data) ? data as TimelineEvent[] : []);
8
- </script>
9
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
10
- {#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
11
- {#if events.length===0}<p class="text-text2 text-sm">No events</p>
12
- {:else}
13
- <div class="py-1">
14
- {#each events as event, i}
15
- {@const isLast=i===events.length-1}
16
- {@const dotColor=event.color??STATUS[event.status??'pending']??'var(--color-border2)'}
17
- <!-- svelte-ignore a11y_click_events_have_key_events -->
18
- <!-- svelte-ignore a11y_no_static_element_interactions -->
19
- <!-- svelte-ignore a11y_interactive_supports_focus -->
20
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
21
- <div class="flex gap-4 relative {!isLast?'pb-5':''} {oneventclick?'cursor-pointer':''}" role={oneventclick?"button":undefined} tabindex={oneventclick?0:undefined} title={oneventclick?"Double-cliquez pour interagir":undefined} ondblclick={()=>oneventclick?.(event)} onkeydown={(e)=>{if(e.key==='Enter')oneventclick?.(event)}}>
22
- <div class="flex flex-col items-center flex-shrink-0">
23
- <div class="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style="background:{dotColor};{event.status==='active'?`box-shadow:0 0 0 3px ${dotColor}33;`:''}"></div>
24
- {#if !isLast}<div class="w-0.5 flex-1 bg-border mt-1"></div>{/if}
25
- </div>
26
- <div class="flex-1 min-w-0 pb-1">
27
- <div class="text-xs text-text2 mb-0.5">{event.date??''}</div>
28
- <div class="font-semibold text-text1 text-sm">
29
- {#if event.href}<a href={event.href} class="text-accent no-underline hover:underline">{event.title??''}</a>
30
- {:else}{event.title??''}{/if}
31
- </div>
32
- {#if event.description}<div class="text-sm text-text2 mt-0.5">{event.description}</div>{/if}
33
- {#if event.tags?.length}
34
- <div class="flex gap-1 flex-wrap mt-1">
35
- {#each event.tags as tag}<span class="text-xs bg-surface2 text-text2 px-1.5 py-0.5 rounded">{tag}</span>{/each}
36
- </div>
37
- {/if}
38
- </div>
39
- </div>
40
- {/each}
41
- </div>
42
- {/if}
43
- </div>
@@ -1,48 +0,0 @@
1
- <script lang="ts">
2
- export interface TrombinoscopePerson { name: string; subtitle?: string; avatar?: string; badge?: string; color?: string; badgeColor?: string; }
3
- export interface TrombinoscopeSpec { title?: string; people?: TrombinoscopePerson[]; columns?: number; showBadge?: boolean; }
4
- interface Props { spec: Partial<TrombinoscopeSpec>; data?: unknown; onpersonclick?: (p: TrombinoscopePerson) => void; }
5
- let { spec, data, onpersonclick }: Props = $props();
6
- const COLORS = ['#7c6dfa','#3ecfb2','#f0a050','#fa6d7c','#3b82f6','#a855f7','#14b8a6','#f97316'];
7
- const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
8
- function isValidImageUrl(url: string | undefined): boolean { return !!url && VALID_PREFIXES.some(p => url.startsWith(p)); }
9
- function nameColor(name: string): string { let h=0; for(let i=0;i<name.length;i++) h=name.charCodeAt(i)+((h<<5)-h); return COLORS[Math.abs(h)%COLORS.length]; }
10
- function initials(name: string): string { return name.split(/\s+/).slice(0,2).map((w:string)=>w[0]??'').join('').toUpperCase()||'?'; }
11
- /** Track per-person avatar load failures to fall back to initials */
12
- let failedAvatars = $state(new Set<string>());
13
- const people = $derived.by<TrombinoscopePerson[]>(()=>{ if(Array.isArray(spec.people)&&spec.people.length) return spec.people; if(Array.isArray(data)) return data as TrombinoscopePerson[]; return []; });
14
- const cols = $derived(spec.columns??4);
15
- </script>
16
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
17
- {#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
18
- {#if people.length===0}<p class="text-text2 text-sm">Aucune personne</p>
19
- {:else}
20
- <div class="grid gap-3 responsive-trombi" style="--trombi-cols: repeat({cols}, minmax(0, 1fr));">
21
- {#each people as person}
22
- {@const accent=person.color??nameColor(person.name)}
23
- <!-- svelte-ignore a11y_click_events_have_key_events -->
24
- <!-- svelte-ignore a11y_no_static_element_interactions -->
25
- <!-- svelte-ignore a11y_interactive_supports_focus -->
26
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
27
- <div class="flex flex-col items-center text-center p-3 rounded-lg border border-border hover:border-border2 transition-all {onpersonclick?'cursor-pointer':''}" role={onpersonclick?"button":undefined} tabindex={onpersonclick?0:undefined} onclick={()=>onpersonclick?.(person)}>
28
- {#if isValidImageUrl(person.avatar) && !failedAvatars.has(person.avatar!)}
29
- <img src={person.avatar} alt={person.name} class="w-12 h-12 rounded-full object-cover mb-2 border-2" style="border-color:{accent};" onerror={() => { failedAvatars = new Set([...failedAvatars, person.avatar!]); }} />
30
- {:else}
31
- <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>
32
- {/if}
33
- <div class="text-xs font-semibold text-text1 leading-tight truncate w-full">{person.name}</div>
34
- {#if person.subtitle}<div class="text-xs text-text2 mt-0.5 truncate w-full">{person.subtitle}</div>{/if}
35
- {#if spec.showBadge!==false&&person.badge}
36
- <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>
37
- {/if}
38
- </div>
39
- {/each}
40
- </div>
41
- <div class="mt-3 text-xs text-text2">{people.length} personne{people.length!==1?'s':''}</div>
42
- {/if}
43
- </div>
44
-
45
- <style>
46
- .responsive-trombi { grid-template-columns: repeat(2, minmax(0, 1fr)); }
47
- @media (min-width: 768px) { .responsive-trombi { grid-template-columns: var(--trombi-cols); } }
48
- </style>
@@ -1,15 +0,0 @@
1
- <script lang="ts">
2
- export interface ActionButton { label: string; primary?: boolean; onclick?: () => void; }
3
- export interface ActionsBlockData { buttons: ActionButton[]; }
4
- interface Props { data: Partial<ActionsBlockData>; }
5
- let { data }: Props = $props();
6
- </script>
7
- <div class="p-3 md:p-4 flex gap-2 flex-wrap">
8
- {#each (data.buttons ?? []) as btn}
9
- <button
10
- class="text-xs font-mono px-4 py-2 rounded border transition-all
11
- {btn.primary ? 'bg-accent border-accent text-white hover:opacity-85' : 'border-border2 text-text2 hover:border-accent hover:text-accent'}"
12
- onclick={btn.onclick}
13
- >{btn.label}</button>
14
- {/each}
15
- </div>
@@ -1,11 +0,0 @@
1
- <script lang="ts">
2
- export interface AlertBlockData { title?: string; message?: string; level?: 'info' | 'warn' | 'error'; }
3
- interface Props { data: Partial<AlertBlockData>; }
4
- let { data }: Props = $props();
5
- const borderColor = $derived(data.level === 'error' ? 'border-accent2' : data.level === 'info' ? 'border-blue-500' : 'border-amber');
6
- const titleColor = $derived(data.level === 'error' ? 'text-accent2' : data.level === 'info' ? 'text-blue-400' : 'text-amber');
7
- </script>
8
- <div class="p-3 md:p-4 border-l-4 {borderColor}">
9
- {#if data.title}<div class="font-semibold text-sm mb-1 {titleColor}">{data.title}</div>{/if}
10
- {#if data.message}<div class="text-xs font-mono text-text2">{data.message}</div>{/if}
11
- </div>
@@ -1,21 +0,0 @@
1
- <script lang="ts">
2
- export interface ChartBlockData { title?: string; bars: [string, number][]; }
3
- interface Props { data: Partial<ChartBlockData>; }
4
- let { data }: Props = $props();
5
- const bars = $derived(data.bars ?? []);
6
- const max = $derived(Math.max(...bars.map(b => b[1]), 1));
7
- </script>
8
- <div class="p-3 md:p-4">
9
- {#if data.title}<div class="text-[10px] font-mono text-text2 mb-4 uppercase tracking-widest">{data.title}</div>{/if}
10
- <div class="flex items-end gap-1.5 h-32">
11
- {#each bars as [, val]}
12
- <div class="flex-1 rounded-t bg-accent opacity-80 hover:opacity-100 transition-all"
13
- style="height: max(2px, {Math.round(val / max * 100)}%)"></div>
14
- {/each}
15
- </div>
16
- <div class="flex gap-1.5 mt-1">
17
- {#each bars as [label]}
18
- <span class="flex-1 text-center text-[9px] font-mono text-text2 truncate">{label}</span>
19
- {/each}
20
- </div>
21
- </div>
@@ -1,11 +0,0 @@
1
- <script lang="ts">
2
- export interface CodeBlockData { lang?: string; content?: string; }
3
- interface Props { data: Partial<CodeBlockData>; }
4
- let { data }: Props = $props();
5
- </script>
6
- <div class="rounded overflow-hidden">
7
- <div class="bg-black/40 px-3 py-1.5 md:px-4 border-b border-border">
8
- <span class="text-[10px] font-mono text-text2">{data.lang ?? 'text'}</span>
9
- </div>
10
- <pre class="font-mono text-xs text-teal bg-black/30 p-3 md:p-4 overflow-x-auto leading-relaxed">{data.content ?? ''}</pre>
11
- </div>
@@ -1,16 +0,0 @@
1
- <script lang="ts">
2
- export interface KVBlockData { title?: string; rows: [string, string][]; }
3
- interface Props { data: Partial<KVBlockData>; }
4
- let { data }: Props = $props();
5
- </script>
6
- <div class="p-3 md:p-4">
7
- {#if data.title}<div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{data.title}</div>{/if}
8
- <div class="flex flex-col gap-1.5">
9
- {#each (data.rows ?? []) as [k, v]}
10
- <div class="flex justify-between items-center text-sm border-b border-border pb-1.5 last:border-none last:pb-0">
11
- <span class="font-mono text-xs text-text2">{k}</span>
12
- <span class="text-text1 font-medium">{v}</span>
13
- </div>
14
- {/each}
15
- </div>
16
- </div>
@@ -1,17 +0,0 @@
1
- <script lang="ts">
2
- export interface ListBlockData { title?: string; items: string[]; }
3
- interface Props { data: Partial<ListBlockData>; onitemclick?: (item: string, index: number) => void; }
4
- let { data, onitemclick }: Props = $props();
5
- </script>
6
- <div class="p-3 md:p-4">
7
- {#if data.title}<div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{data.title}</div>{/if}
8
- <ul class="flex flex-col gap-1.5">
9
- {#each (data.items ?? []) as item, i}
10
- <!-- svelte-ignore a11y_click_events_have_key_events -->
11
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
12
- <li class="text-sm text-text1 bg-surface2 rounded px-3 py-2 border-l-2 border-accent {onitemclick ? 'cursor-pointer hover:bg-surface2/80' : ''}"
13
- title={onitemclick?"Double-cliquez pour interagir":undefined}
14
- ondblclick={() => onitemclick?.(item, i)}>{item}</li>
15
- {/each}
16
- </ul>
17
- </div>
@@ -1,14 +0,0 @@
1
- <script lang="ts">
2
- export interface StatBlockData { label: string; value: string; trend?: string; trendDir?: 'up' | 'down' | 'neutral'; }
3
- interface Props { data: Partial<StatBlockData>; }
4
- let { data }: Props = $props();
5
- const trendColor = $derived(data.trendDir === 'up' ? 'text-teal' : data.trendDir === 'down' ? 'text-accent2' : 'text-text2');
6
- const trendArrow = $derived(data.trendDir === 'up' ? '↑' : data.trendDir === 'down' ? '↓' : '→');
7
- </script>
8
- <div class="p-4 md:p-5">
9
- <div class="text-[11px] font-mono text-text2 mb-1 uppercase tracking-widest">{data.label ?? 'Metric'}</div>
10
- <div class="text-3xl md:text-4xl font-bold text-text1 leading-none">{data.value ?? '—'}</div>
11
- {#if data.trend}
12
- <div class="text-xs font-mono mt-2 {trendColor}">{trendArrow} {data.trend}</div>
13
- {/if}
14
- </div>
@@ -1,15 +0,0 @@
1
- <script lang="ts">
2
- export interface TagItem { text: string; active?: boolean; }
3
- export interface TagsBlockData { label?: string; tags: TagItem[]; }
4
- interface Props { data: Partial<TagsBlockData>; }
5
- let { data }: Props = $props();
6
- </script>
7
- <div class="p-3 md:p-4 flex gap-2 flex-wrap items-center">
8
- {#if data.label}<span class="text-[10px] font-mono text-text2">{data.label}</span>{/if}
9
- {#each (data.tags ?? []) as tag}
10
- <span class="text-[11px] font-mono px-3 py-1 rounded-full border transition-colors
11
- {tag.active ? 'border-teal text-teal bg-teal/10' : 'border-border2 text-text2'}">
12
- {tag.text}
13
- </span>
14
- {/each}
15
- </div>