@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,85 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-grid-data', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface GridDataColumn { key: string; label: string; width?: string; }
|
|
5
|
+
export interface GridDataHighlight { row: number; col: number; color?: string; }
|
|
6
|
+
export interface GridDataData {
|
|
7
|
+
title?: string;
|
|
8
|
+
columns?: GridDataColumn[];
|
|
9
|
+
rows?: unknown[][];
|
|
10
|
+
highlights?: GridDataHighlight[];
|
|
11
|
+
cellHeight?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
data?: GridDataData | null;
|
|
16
|
+
oncellclick?: (row: number, col: number, value: unknown) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { data = {}, oncellclick }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const columns = $derived(Array.isArray(data?.columns) ? data!.columns as GridDataColumn[] : []);
|
|
22
|
+
const rows = $derived(Array.isArray(data?.rows) ? data!.rows as unknown[][] : []);
|
|
23
|
+
const cellH = $derived(data?.cellHeight ?? 32);
|
|
24
|
+
|
|
25
|
+
const hlMap = $derived.by(() => {
|
|
26
|
+
const m = new Map<string, string>();
|
|
27
|
+
if (Array.isArray(data?.highlights)) {
|
|
28
|
+
for (const h of data!.highlights as GridDataHighlight[]) {
|
|
29
|
+
m.set(`${h.row},${h.col}`, h.color ?? 'color-mix(in srgb, var(--color-accent) 20%, transparent)');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return m;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function bg(r: number, c: number): string {
|
|
36
|
+
return hlMap.get(`${r},${c}`) ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dv(v: unknown): string {
|
|
40
|
+
if (v == null) return '';
|
|
41
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
42
|
+
return String(v);
|
|
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 !columns.length && !rows.length}
|
|
49
|
+
<p class="text-text2 text-sm">No data</p>
|
|
50
|
+
{:else}
|
|
51
|
+
<div class="overflow-auto rounded border border-border">
|
|
52
|
+
<table class="w-full border-collapse text-xs font-mono" role="grid">
|
|
53
|
+
{#if columns.length}
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
{#each columns as col}
|
|
57
|
+
<th
|
|
58
|
+
class="sticky top-0 bg-surface2 px-3 py-2 text-left text-text2 border-b border-r border-border whitespace-nowrap font-medium"
|
|
59
|
+
style={col.width ? `width:${col.width}` : ''}
|
|
60
|
+
scope="col"
|
|
61
|
+
>
|
|
62
|
+
{col.label}
|
|
63
|
+
</th>
|
|
64
|
+
{/each}
|
|
65
|
+
</tr>
|
|
66
|
+
</thead>
|
|
67
|
+
{/if}
|
|
68
|
+
<tbody>
|
|
69
|
+
{#each rows as row, ri}
|
|
70
|
+
<tr class="hover:bg-surface2">
|
|
71
|
+
{#each (Array.isArray(row) ? row : []) as cell, ci}
|
|
72
|
+
<td
|
|
73
|
+
class="px-3 text-text2 border-b border-r border-border {oncellclick ? 'cursor-pointer' : ''} hover:bg-surface2"
|
|
74
|
+
style="height:{cellH}px;{bg(ri, ci) ? `background:${bg(ri, ci)};` : ''}"
|
|
75
|
+
title={oncellclick ? 'Double-cliquez pour interagir' : undefined}
|
|
76
|
+
ondblclick={() => oncellclick?.(ri, ci, cell)}
|
|
77
|
+
>{dv(cell)}</td>
|
|
78
|
+
{/each}
|
|
79
|
+
</tr>
|
|
80
|
+
{/each}
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-hemicycle', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface HemicycleGroup { id: string; label: string; seats: number; color: string; }
|
|
5
|
+
export interface HemicycleData {
|
|
6
|
+
title?: string;
|
|
7
|
+
groups?: HemicycleGroup[];
|
|
8
|
+
totalSeats?: number;
|
|
9
|
+
rows?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data?: HemicycleData | null;
|
|
14
|
+
ongroupclick?: (g: HemicycleGroup) => void;
|
|
15
|
+
}
|
|
16
|
+
let { data = {}, ongroupclick }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const groups = $derived<HemicycleGroup[]>(Array.isArray(data?.groups) ? data!.groups! : []);
|
|
19
|
+
const total = $derived(data?.totalSeats ?? groups.reduce((s, g) => s + g.seats, 0));
|
|
20
|
+
|
|
21
|
+
const W = 420, H = 230, cx = W / 2, cy = H - 10, rMin = 60, step = 28;
|
|
22
|
+
const rows = $derived(data?.rows ?? Math.min(Math.max(3, Math.ceil(Math.sqrt(total / 6))), 7));
|
|
23
|
+
|
|
24
|
+
interface Seat { x: number; y: number; color: string; gid: string }
|
|
25
|
+
|
|
26
|
+
const seats = $derived.by<Seat[]>(() => {
|
|
27
|
+
if (!groups.length || !total) return [];
|
|
28
|
+
const radii = Array.from({ length: rows }, (_, i) => rMin + i * step);
|
|
29
|
+
const circs = radii.map(r => Math.PI * r);
|
|
30
|
+
const totalC = circs.reduce((a, b) => a + b, 0);
|
|
31
|
+
const spr = radii.map(r => Math.round(Math.PI * r / totalC * total));
|
|
32
|
+
spr[spr.length - 1] += total - spr.reduce((a, b) => a + b, 0);
|
|
33
|
+
const sorted = [...groups].sort((a, b) => a.seats - b.seats);
|
|
34
|
+
const colors: { color: string; gid: string }[] = [];
|
|
35
|
+
for (const g of sorted) for (let i = 0; i < g.seats; i++) colors.push({ color: g.color, gid: g.id });
|
|
36
|
+
while (colors.length < total) colors.push({ color: '#333355', gid: '' });
|
|
37
|
+
const result: Seat[] = [];
|
|
38
|
+
let idx = 0;
|
|
39
|
+
for (let row = 0; row < rows; row++) {
|
|
40
|
+
const r = radii[row], n = spr[row];
|
|
41
|
+
for (let j = 0; j < n; j++) {
|
|
42
|
+
if (idx >= colors.length) break;
|
|
43
|
+
const angle = Math.PI - (j / (n - 1 || 1)) * Math.PI;
|
|
44
|
+
result.push({ x: cx + r * Math.cos(angle), y: cy - r * Math.sin(angle), ...colors[idx++] });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const rMax = $derived(rMin + rows * step);
|
|
51
|
+
const legend = $derived([...groups].sort((a, b) => b.seats - a.seats));
|
|
52
|
+
let tooltip = $state<{ label: string; seats: number } | null>(null);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
56
|
+
{#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
|
|
57
|
+
{#if !groups.length || !total}<p class="text-text2 text-sm">No data</p>
|
|
58
|
+
{:else}
|
|
59
|
+
<div class="relative">
|
|
60
|
+
<svg viewBox="0 0 {W} {H}" class="block w-full max-h-[220px]" xmlns="http://www.w3.org/2000/svg">
|
|
61
|
+
<path d="M {cx - rMax - 15} {cy} A {rMax + 15} {rMax + 15} 0 0 1 {cx + rMax + 15} {cy}" fill="none" stroke="var(--color-surface2)" stroke-width="2"/>
|
|
62
|
+
{#each seats as s}
|
|
63
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
64
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
65
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
66
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
67
|
+
<circle cx={s.x.toFixed(1)} cy={s.y.toFixed(1)} r="4" fill={s.color} opacity="0.9"
|
|
68
|
+
class={s.gid ? 'cursor-pointer' : ''}
|
|
69
|
+
onmouseenter={() => { const g = groups.find(g => g.id === s.gid); if (g) tooltip = { label: g.label, seats: g.seats }; }}
|
|
70
|
+
onmouseleave={() => tooltip = null}
|
|
71
|
+
ondblclick={() => { const g = groups.find(g => g.id === s.gid); if (g) ongroupclick?.(g); }}>
|
|
72
|
+
{#if s.gid}<title>Double-click to interact</title>{/if}
|
|
73
|
+
</circle>
|
|
74
|
+
{/each}
|
|
75
|
+
<text x={cx} y={cy + 18} text-anchor="middle" font-size="11" fill="var(--color-text2)" font-family="system-ui">{total} seats</text>
|
|
76
|
+
</svg>
|
|
77
|
+
{#if tooltip}
|
|
78
|
+
<div class="absolute top-0 right-0 bg-surface2 border border-border2 rounded px-2 py-1 text-xs text-text1 pointer-events-none">
|
|
79
|
+
<span class="font-semibold">{tooltip.label}</span> — {tooltip.seats}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-3">
|
|
84
|
+
{#each legend as g}
|
|
85
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
86
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
87
|
+
<div class="flex items-center gap-1.5 text-xs cursor-pointer hover:opacity-80" role="button" tabindex="0" title="Double-click to interact" ondblclick={() => ongroupclick?.(g)} onkeydown={(e) => { if (e.key === 'Enter') ongroupclick?.(g); }}>
|
|
88
|
+
<div class="w-3 h-3 rounded-full flex-shrink-0" style="background:{g.color};"></div>
|
|
89
|
+
<span class="text-text2">{g.label}</span>
|
|
90
|
+
<span class="text-text2">{g.seats}</span>
|
|
91
|
+
</div>
|
|
92
|
+
{/each}
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-js-sandbox', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface JsSandboxData {
|
|
5
|
+
title?: string;
|
|
6
|
+
code?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
css?: string;
|
|
9
|
+
height?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props { data?: JsSandboxData | null; }
|
|
13
|
+
let { data = {} }: Props = $props();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tolerate two shapes (fix f620091):
|
|
17
|
+
* - bare spec: data = { code, html, css, title, height }
|
|
18
|
+
* - wrapped: data = { spec: { code, ... } }
|
|
19
|
+
* The cast handles the legacy wrapped shape defensively.
|
|
20
|
+
*/
|
|
21
|
+
const resolved = $derived<JsSandboxData>(() => {
|
|
22
|
+
const d = data as Record<string, unknown> | null | undefined;
|
|
23
|
+
if (d && typeof d === 'object' && 'spec' in d && d.spec && typeof d.spec === 'object') {
|
|
24
|
+
return d.spec as JsSandboxData;
|
|
25
|
+
}
|
|
26
|
+
return (d as JsSandboxData) ?? {};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const srcdoc = $derived(`<!DOCTYPE html>
|
|
30
|
+
<html>
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8">
|
|
33
|
+
<style>
|
|
34
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
35
|
+
body{margin:0;padding:8px;font-family:system-ui,sans-serif;font-size:13px;}
|
|
36
|
+
${resolved().css ?? ''}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div id="root">${resolved().html ?? ''}</div>
|
|
41
|
+
<script>
|
|
42
|
+
(function(){
|
|
43
|
+
try{
|
|
44
|
+
${resolved().code ?? '// no code provided'}
|
|
45
|
+
}catch(e){
|
|
46
|
+
document.getElementById('root').innerHTML='<pre style="color:red;white-space:pre-wrap">'+e+'<\/pre>';
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
<\/script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>`);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="bg-surface border border-border rounded-lg overflow-hidden font-sans">
|
|
55
|
+
{#if resolved().title}
|
|
56
|
+
<div class="px-3 py-2 border-b border-border text-sm font-semibold text-text1 flex items-center gap-2">
|
|
57
|
+
<span class="text-xs opacity-50">JS</span>
|
|
58
|
+
{resolved().title}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
<iframe
|
|
62
|
+
{srcdoc}
|
|
63
|
+
sandbox="allow-scripts"
|
|
64
|
+
title={resolved().title ?? 'JS Sandbox'}
|
|
65
|
+
style="width:100%;height:{resolved().height ?? '300px'};border:none;display:block;"
|
|
66
|
+
></iframe>
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-json-viewer', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface JsonViewerData {
|
|
5
|
+
title?: string;
|
|
6
|
+
data?: unknown;
|
|
7
|
+
maxDepth?: number;
|
|
8
|
+
expanded?: boolean;
|
|
9
|
+
theme?: 'dark' | 'light';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data?: JsonViewerData | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { data = {} }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const value = $derived(data?.data !== undefined ? data.data : undefined);
|
|
19
|
+
const maxDepth = $derived(data?.maxDepth ?? 5);
|
|
20
|
+
const expanded = $derived(data?.expanded !== false);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#snippet node(val: unknown, depth: number, seen: WeakSet<object>)}
|
|
24
|
+
{#if val === null}
|
|
25
|
+
<span style="color:var(--color-text2)">null</span>
|
|
26
|
+
{:else if typeof val === 'boolean'}
|
|
27
|
+
<span style="color:#a855f7">{String(val)}</span>
|
|
28
|
+
{:else if typeof val === 'number'}
|
|
29
|
+
<span style="color:var(--color-amber)">{val}</span>
|
|
30
|
+
{:else if typeof val === 'string'}
|
|
31
|
+
<span style="color:var(--color-teal)">"{val}"</span>
|
|
32
|
+
{:else if Array.isArray(val)}
|
|
33
|
+
{#if depth >= maxDepth}
|
|
34
|
+
<span class="text-text2">[Array({val.length})]</span>
|
|
35
|
+
{:else if seen.has(val)}
|
|
36
|
+
<span class="text-text2 italic">[Circular]</span>
|
|
37
|
+
{:else}
|
|
38
|
+
{@const nextSeen = (seen.add(val), seen)}
|
|
39
|
+
<details open={expanded && depth < 2}>
|
|
40
|
+
<summary class="cursor-pointer text-text2 hover:text-text1 select-none">Array({val.length})</summary>
|
|
41
|
+
<div class="ml-4 border-l border-border pl-3 mt-0.5">
|
|
42
|
+
{#each val as item, i}
|
|
43
|
+
<div class="py-0.5">
|
|
44
|
+
<span class="text-text2 text-xs mr-1">{i}:</span>{@render node(item, depth + 1, nextSeen)}
|
|
45
|
+
</div>
|
|
46
|
+
{/each}
|
|
47
|
+
</div>
|
|
48
|
+
</details>
|
|
49
|
+
{/if}
|
|
50
|
+
{:else if typeof val === 'object'}
|
|
51
|
+
{#if depth >= maxDepth}
|
|
52
|
+
<span class="text-text2">{`{Object(${Object.keys(val as object).length})}`}</span>
|
|
53
|
+
{:else if seen.has(val as object)}
|
|
54
|
+
<span class="text-text2 italic">[Circular]</span>
|
|
55
|
+
{:else}
|
|
56
|
+
{@const nextSeen = ((seen as WeakSet<object>).add(val as object), seen)}
|
|
57
|
+
<details open={expanded && depth < 2}>
|
|
58
|
+
<summary class="cursor-pointer text-text2 hover:text-text1 select-none">{`{${Object.keys(val as object).length}}`}</summary>
|
|
59
|
+
<div class="ml-4 border-l border-border pl-3 mt-0.5">
|
|
60
|
+
{#each Object.entries(val as Record<string, unknown>) as [k, v]}
|
|
61
|
+
<div class="py-0.5">
|
|
62
|
+
<span class="text-accent mr-1">"{k}":</span>{@render node(v, depth + 1, nextSeen)}
|
|
63
|
+
</div>
|
|
64
|
+
{/each}
|
|
65
|
+
</div>
|
|
66
|
+
</details>
|
|
67
|
+
{/if}
|
|
68
|
+
{:else}
|
|
69
|
+
<span class="text-text2">{String(val)}</span>
|
|
70
|
+
{/if}
|
|
71
|
+
{/snippet}
|
|
72
|
+
|
|
73
|
+
<div class="bg-bg border border-border rounded-lg p-3 md:p-4 font-mono text-xs leading-5 text-text1">
|
|
74
|
+
{#if data?.title}
|
|
75
|
+
<div class="font-sans text-sm font-semibold text-text1 mb-3">{data.title}</div>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if value === undefined}
|
|
78
|
+
<span class="text-text2">undefined</span>
|
|
79
|
+
{:else}
|
|
80
|
+
{@render node(value, 0, new WeakSet())}
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-log', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
timestamp?: string;
|
|
6
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
+
message: string;
|
|
8
|
+
source?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LogData {
|
|
12
|
+
title?: string;
|
|
13
|
+
entries?: LogEntry[];
|
|
14
|
+
maxHeight?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props { data?: LogData | null; }
|
|
18
|
+
let { data = {} }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const LEVEL: Record<string, string> = {
|
|
21
|
+
debug: 'text-text2',
|
|
22
|
+
info: 'text-teal',
|
|
23
|
+
warn: 'text-amber',
|
|
24
|
+
error: 'text-accent2',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const entries = $derived<LogEntry[]>(
|
|
28
|
+
Array.isArray(data?.entries) && data!.entries!.length ? data!.entries! : []
|
|
29
|
+
);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div class="bg-bg border border-border rounded-lg font-mono">
|
|
33
|
+
{#if data?.title}
|
|
34
|
+
<div class="px-4 py-2 border-b border-border text-xs text-text2">{data.title}</div>
|
|
35
|
+
{/if}
|
|
36
|
+
<div
|
|
37
|
+
class="overflow-y-auto text-xs leading-5 p-3 flex flex-col gap-0.5"
|
|
38
|
+
style="max-height:{data?.maxHeight ?? '320px'};"
|
|
39
|
+
role="log"
|
|
40
|
+
aria-live="polite"
|
|
41
|
+
aria-label={data?.title ?? 'Journal'}
|
|
42
|
+
>
|
|
43
|
+
{#if !entries.length}
|
|
44
|
+
<span class="text-text2">No log entries</span>
|
|
45
|
+
{:else}
|
|
46
|
+
{#each entries as e}
|
|
47
|
+
<div class="flex gap-2 items-start hover:bg-surface2 px-1 rounded">
|
|
48
|
+
{#if e.timestamp}
|
|
49
|
+
<span class="text-text2 flex-shrink-0">{e.timestamp}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
<span class="flex-shrink-0 uppercase text-[10px] font-semibold w-10 {LEVEL[e.level ?? 'info'] ?? LEVEL.info}">
|
|
52
|
+
{e.level ?? 'info'}
|
|
53
|
+
</span>
|
|
54
|
+
{#if e.source}
|
|
55
|
+
<span class="text-text2 flex-shrink-0">[{e.source}]</span>
|
|
56
|
+
{/if}
|
|
57
|
+
<span class="text-text1 break-all">{e.message}</span>
|
|
58
|
+
</div>
|
|
59
|
+
{/each}
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-profile', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface ProfileField { label: string; value: string; href?: string; }
|
|
5
|
+
export interface ProfileStat { label: string; value: string; }
|
|
6
|
+
export interface ProfileAction { label: string; href?: string; variant?: 'primary' | 'secondary' | 'danger'; onclick?: () => void; }
|
|
7
|
+
export interface ProfileData {
|
|
8
|
+
name?: string;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
avatar?: { src: string; alt?: string };
|
|
11
|
+
badge?: { text: string; variant?: 'default' | 'success' | 'warning' | 'error' };
|
|
12
|
+
fields?: ProfileField[];
|
|
13
|
+
stats?: ProfileStat[];
|
|
14
|
+
actions?: ProfileAction[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props { data?: ProfileData | null; }
|
|
18
|
+
let { data = {} }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
|
|
21
|
+
|
|
22
|
+
/** Track if avatar failed to load — fall back to initials */
|
|
23
|
+
let avatarFailed = $state(false);
|
|
24
|
+
$effect(() => { if (data?.avatar?.src) avatarFailed = false; });
|
|
25
|
+
|
|
26
|
+
const avatarValid = $derived(
|
|
27
|
+
!!data?.avatar?.src && VALID_PREFIXES.some(p => data!.avatar!.src.startsWith(p))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const BADGE: Record<string, string> = {
|
|
31
|
+
default: 'bg-surface2 text-text2',
|
|
32
|
+
success: 'bg-teal/20 text-teal',
|
|
33
|
+
warning: 'bg-amber/20 text-amber',
|
|
34
|
+
error: 'bg-accent2/20 text-accent2',
|
|
35
|
+
};
|
|
36
|
+
const ACTION: Record<string, string> = {
|
|
37
|
+
primary: 'bg-accent text-white',
|
|
38
|
+
secondary: 'bg-surface2 text-text2',
|
|
39
|
+
danger: 'bg-accent2 text-white',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const initials = $derived(
|
|
43
|
+
(data?.name ?? '?').split(/\s+/).slice(0, 2).map((w: string) => w[0] ?? '').join('').toUpperCase() || '?'
|
|
44
|
+
);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans max-w-full md:max-w-[480px]">
|
|
48
|
+
<div class="flex flex-col sm:flex-row items-center sm:items-start mb-4 gap-3 sm:gap-4">
|
|
49
|
+
{#if avatarValid && !avatarFailed}
|
|
50
|
+
<img src={data?.avatar?.src} alt={data?.avatar?.alt ?? ''} class="w-16 h-16 rounded-full object-cover border-2 border-border2 flex-shrink-0" onerror={() => { avatarFailed = true; }} />
|
|
51
|
+
{:else}
|
|
52
|
+
<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>
|
|
53
|
+
{/if}
|
|
54
|
+
<div>
|
|
55
|
+
<h3 class="text-lg font-bold text-text1 m-0">{data?.name ?? ''}</h3>
|
|
56
|
+
{#if data?.subtitle}<div class="text-sm text-text2 mt-0.5">{data.subtitle}</div>{/if}
|
|
57
|
+
{#if data?.badge}<div class="mt-1"><span class="text-xs font-semibold px-2 py-0.5 rounded-full {BADGE[data.badge.variant ?? 'default'] ?? BADGE.default}">{data.badge.text}</span></div>{/if}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
{#if data?.fields?.length}
|
|
61
|
+
<dl class="border-t border-border pt-3 m-0">
|
|
62
|
+
{#each data.fields as f}
|
|
63
|
+
<div class="flex gap-2 mb-1.5">
|
|
64
|
+
<dt class="text-xs text-text2 min-w-[80px] sm:min-w-[100px] font-mono">{f.label}</dt>
|
|
65
|
+
<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>
|
|
66
|
+
</div>
|
|
67
|
+
{/each}
|
|
68
|
+
</dl>
|
|
69
|
+
{/if}
|
|
70
|
+
{#if data?.stats?.length}
|
|
71
|
+
<div class="flex flex-wrap border border-border rounded overflow-hidden mt-3">
|
|
72
|
+
{#each data.stats as s}
|
|
73
|
+
<div class="text-center px-4 py-2 border-r border-border last:border-r-0 flex-1 min-w-[80px]">
|
|
74
|
+
<div class="text-xl font-bold text-accent">{s.value}</div>
|
|
75
|
+
<div class="text-xs text-text2">{s.label}</div>
|
|
76
|
+
</div>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
{#if data?.actions?.length}
|
|
81
|
+
<div class="flex gap-2 mt-3 flex-wrap">
|
|
82
|
+
{#each data.actions as a}
|
|
83
|
+
{#if a.href}
|
|
84
|
+
<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>
|
|
85
|
+
{:else}
|
|
86
|
+
<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>
|
|
87
|
+
{/if}
|
|
88
|
+
{/each}
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-sankey', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface SankeyNode { id: string; label: string; color?: string; summary?: string; }
|
|
5
|
+
export interface SankeyLink { source: string; target: string; value: number; label?: string; }
|
|
6
|
+
export interface SankeyData {
|
|
7
|
+
title?: string;
|
|
8
|
+
nodes?: SankeyNode[];
|
|
9
|
+
links?: SankeyLink[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
data?: SankeyData | null;
|
|
14
|
+
onnodeclick?: (n: SankeyNode) => void;
|
|
15
|
+
onlinkclick?: (l: SankeyLink) => void;
|
|
16
|
+
}
|
|
17
|
+
let { data = {}, onnodeclick, onlinkclick }: Props = $props();
|
|
18
|
+
|
|
19
|
+
let host: HTMLElement | undefined = $state();
|
|
20
|
+
|
|
21
|
+
const nodes = $derived<SankeyNode[]>(Array.isArray(data?.nodes) ? data!.nodes! : []);
|
|
22
|
+
const links = $derived<SankeyLink[]>(Array.isArray(data?.links) ? data!.links! : []);
|
|
23
|
+
const nodeMap = $derived(new Map(nodes.map(n => [n.id, n])));
|
|
24
|
+
const maxVal = $derived(Math.max(...links.map(l => l.value), 1));
|
|
25
|
+
const sorted = $derived([...links].sort((a, b) => b.value - a.value));
|
|
26
|
+
|
|
27
|
+
function dispatchNodeDblclick(node: SankeyNode | undefined) {
|
|
28
|
+
if (!node || !host) return;
|
|
29
|
+
// Fix(audit): emit widget:interact { action: 'node-dblclick' } — not the orphan widget:node-dblclick event
|
|
30
|
+
host.dispatchEvent(new CustomEvent('widget:interact', {
|
|
31
|
+
detail: { action: 'node-dblclick', payload: node },
|
|
32
|
+
bubbles: true,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div bind:this={host} class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
38
|
+
{#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
|
|
39
|
+
{#if !nodes.length || !links.length}<p class="text-text2 text-sm">No flow data</p>
|
|
40
|
+
{:else}
|
|
41
|
+
<div class="text-xs text-text2 mb-2 font-mono">{nodes.length} nodes · {links.length} flows</div>
|
|
42
|
+
<div class="flex flex-col gap-1.5">
|
|
43
|
+
{#each sorted as link}
|
|
44
|
+
{@const src = nodeMap.get(link.source)}
|
|
45
|
+
{@const tgt = nodeMap.get(link.target)}
|
|
46
|
+
{@const sc = src?.color ?? 'var(--color-accent)'}
|
|
47
|
+
{@const tc = tgt?.color ?? 'var(--color-teal)'}
|
|
48
|
+
{@const pct = Math.round(link.value / maxVal * 100)}
|
|
49
|
+
{@const barH = Math.max(4, Math.round(link.value / maxVal * 20))}
|
|
50
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
51
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
52
|
+
<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); }}>
|
|
53
|
+
<span
|
|
54
|
+
class="text-text2 min-w-[80px] truncate font-mono"
|
|
55
|
+
style="color:{sc};"
|
|
56
|
+
title={src?.summary ?? src?.label ?? link.source}
|
|
57
|
+
ondblclick={(e) => { e.stopPropagation(); dispatchNodeDblclick(src); onnodeclick?.(src!); }}
|
|
58
|
+
>{src?.label ?? link.source}</span>
|
|
59
|
+
<div class="flex-1 bg-surface2 rounded-full overflow-hidden" style="height:{barH}px;">
|
|
60
|
+
<div class="rounded-full h-full" style="width:{pct}%;background:linear-gradient(to right,{sc},{tc});"></div>
|
|
61
|
+
</div>
|
|
62
|
+
<span
|
|
63
|
+
class="text-text2 min-w-[80px] truncate text-right font-mono"
|
|
64
|
+
style="color:{tc};"
|
|
65
|
+
title={tgt?.summary ?? tgt?.label ?? link.target}
|
|
66
|
+
ondblclick={(e) => { e.stopPropagation(); dispatchNodeDblclick(tgt); onnodeclick?.(tgt!); }}
|
|
67
|
+
>{tgt?.label ?? link.target}</span>
|
|
68
|
+
<span class="text-text2 min-w-[40px] text-right font-mono">{link.value}</span>
|
|
69
|
+
</div>
|
|
70
|
+
{/each}
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-stat-card', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
export interface StatCardTrend { direction: 'up' | 'down' | 'flat'; value?: string; positive?: boolean; }
|
|
5
|
+
export interface StatCardData {
|
|
6
|
+
label?: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
unit?: string;
|
|
9
|
+
delta?: string;
|
|
10
|
+
trend?: 'up' | 'down' | 'flat' | StatCardTrend;
|
|
11
|
+
previousValue?: unknown;
|
|
12
|
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props { data?: StatCardData | null; }
|
|
16
|
+
let { data = {} }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const COLORS: Record<string, string> = {
|
|
19
|
+
default: 'var(--color-accent)',
|
|
20
|
+
success: 'var(--color-teal)',
|
|
21
|
+
warning: 'var(--color-amber)',
|
|
22
|
+
error: 'var(--color-accent2)',
|
|
23
|
+
info: '#3b82f6',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const accent = $derived(COLORS[data?.variant ?? 'default'] ?? 'var(--color-accent)');
|
|
27
|
+
|
|
28
|
+
const trendInfo = $derived(() => {
|
|
29
|
+
const t = data?.trend;
|
|
30
|
+
if (!t) return null;
|
|
31
|
+
if (typeof t === 'string') {
|
|
32
|
+
const arr = t === 'up' ? '↑' : t === 'down' ? '↓' : '→';
|
|
33
|
+
const col = t === 'up' ? 'var(--color-teal)' : t === 'flat' ? 'var(--color-text2)' : 'var(--color-accent2)';
|
|
34
|
+
return { arrow: arr, val: data?.delta ?? '', color: col };
|
|
35
|
+
}
|
|
36
|
+
const positive = t.positive ?? t.direction === 'up';
|
|
37
|
+
return {
|
|
38
|
+
arrow: t.direction === 'up' ? '↑' : t.direction === 'down' ? '↓' : '→',
|
|
39
|
+
val: t.value ?? '',
|
|
40
|
+
color: positive ? 'var(--color-teal)' : t.direction === 'flat' ? 'var(--color-text2)' : 'var(--color-accent2)',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<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};">
|
|
46
|
+
<div class="text-center py-2">
|
|
47
|
+
<div class="text-xs font-mono text-text2 uppercase tracking-widest mb-2">{data?.label ?? ''}</div>
|
|
48
|
+
<div class="text-3xl md:text-4xl font-bold leading-none" style="color:{accent};">
|
|
49
|
+
{data?.value ?? '—'}{#if data?.unit}<span class="text-base text-text2 ml-1">{data.unit}</span>{/if}
|
|
50
|
+
</div>
|
|
51
|
+
{#if trendInfo()}
|
|
52
|
+
{@const t = trendInfo()!}
|
|
53
|
+
<div class="flex items-center gap-1 justify-center mt-1">
|
|
54
|
+
<span class="text-base font-bold" style="color:{t.color};">{t.arrow}</span>
|
|
55
|
+
{#if t.val}<span class="text-sm font-semibold" style="color:{t.color};">{t.val}</span>{/if}
|
|
56
|
+
</div>
|
|
57
|
+
{/if}
|
|
58
|
+
{#if data?.previousValue !== undefined}<div class="text-xs text-text2 mt-1">prev: {data.previousValue}</div>{/if}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|