@webmcp-auto-ui/ui 2.5.31 → 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
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chart.ts — vanilla renderer for the simple bar chart widget.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors ChartBlock.svelte 1:1: a minimal SVG-less inline bar chart built
|
|
5
|
-
* from flexbox-sized <div> bars. We keep the flex/div approach (not SVG)
|
|
6
|
-
* because it is strictly what the Svelte widget does — Tailwind classes,
|
|
7
|
-
* hover transitions, truncation on labels — and we want visual parity.
|
|
8
|
-
*
|
|
9
|
-
* PNG export is still wired via the generic export-widget.ts fallback
|
|
10
|
-
* (html-to-image captures the DOM). We do NOT expose __exportPng here
|
|
11
|
-
* because the flex/div layout rasterises identically through html-to-image.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
15
|
-
|
|
16
|
-
export interface ChartBlockData {
|
|
17
|
-
title?: string;
|
|
18
|
-
bars: [string, number][];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type ChartInput = Partial<ChartBlockData> | null | undefined;
|
|
22
|
-
|
|
23
|
-
// ── derived computations (mirrors $derived in Svelte) ───────────────────────
|
|
24
|
-
|
|
25
|
-
function computeBars(data: ChartInput): [string, number][] {
|
|
26
|
-
const raw = data?.bars;
|
|
27
|
-
if (!Array.isArray(raw)) return [];
|
|
28
|
-
return raw.filter(
|
|
29
|
-
(b) => Array.isArray(b) && b.length >= 2
|
|
30
|
-
) as [string, number][];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function computeMax(bars: [string, number][]): number {
|
|
34
|
-
let m = 1;
|
|
35
|
-
for (const [, v] of bars) {
|
|
36
|
-
const n = typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
37
|
-
if (n > m) m = n;
|
|
38
|
-
}
|
|
39
|
-
return m;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
function el<K extends keyof HTMLElementTagNameMap>(
|
|
45
|
-
tag: K,
|
|
46
|
-
className?: string
|
|
47
|
-
): HTMLElementTagNameMap[K] {
|
|
48
|
-
const node = document.createElement(tag);
|
|
49
|
-
if (className) node.className = className;
|
|
50
|
-
return node;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function dispatchInteract(
|
|
54
|
-
container: HTMLElement,
|
|
55
|
-
action: string,
|
|
56
|
-
payload: unknown
|
|
57
|
-
): void {
|
|
58
|
-
container.dispatchEvent(
|
|
59
|
-
new CustomEvent('widget:interact', {
|
|
60
|
-
detail: { action, payload },
|
|
61
|
-
bubbles: true,
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── renderer ────────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Render a bar chart into `container`. Returns a cleanup function that
|
|
70
|
-
* removes listeners and empties the container.
|
|
71
|
-
*
|
|
72
|
-
* Contract:
|
|
73
|
-
* - data.title : optional string header (mono, uppercase tracking).
|
|
74
|
-
* - data.bars : array of [label, value] tuples. Missing/invalid → empty.
|
|
75
|
-
* - Partial data yields an empty placeholder (no bars, no labels).
|
|
76
|
-
*
|
|
77
|
-
* Events:
|
|
78
|
-
* - `widget:interact` CustomEvent with detail `{ action: 'bar:click',
|
|
79
|
-
* payload: { index, label, value } }` when a bar is clicked. Bars are
|
|
80
|
-
* keyboard-activatable (Enter/Space) and exposed as role=button.
|
|
81
|
-
*/
|
|
82
|
-
export function render(container: HTMLElement, data: ChartInput): () => void {
|
|
83
|
-
// Reset in case of re-render into a dirty container.
|
|
84
|
-
container.innerHTML = '';
|
|
85
|
-
|
|
86
|
-
const bars = computeBars(data);
|
|
87
|
-
const max = computeMax(bars);
|
|
88
|
-
const title = typeof data?.title === 'string' ? data.title : '';
|
|
89
|
-
|
|
90
|
-
const root = el('div', 'p-3 md:p-4');
|
|
91
|
-
|
|
92
|
-
// Title
|
|
93
|
-
if (title) {
|
|
94
|
-
const titleEl = el(
|
|
95
|
-
'div',
|
|
96
|
-
'text-[10px] font-mono text-text2 mb-4 uppercase tracking-widest'
|
|
97
|
-
);
|
|
98
|
-
titleEl.textContent = title;
|
|
99
|
-
root.appendChild(titleEl);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Bars row
|
|
103
|
-
const barsRow = el('div', 'flex items-end gap-1.5 h-32');
|
|
104
|
-
const labelsRow = el('div', 'flex gap-1.5 mt-1');
|
|
105
|
-
|
|
106
|
-
// Track bar-level listeners so we can unbind on cleanup.
|
|
107
|
-
const unbinders: Array<() => void> = [];
|
|
108
|
-
|
|
109
|
-
if (bars.length === 0) {
|
|
110
|
-
// Placeholder: keep rows present so layout stays stable.
|
|
111
|
-
const placeholder = el('div', 'flex-1 rounded-t bg-accent opacity-20');
|
|
112
|
-
placeholder.style.height = '2px';
|
|
113
|
-
barsRow.appendChild(placeholder);
|
|
114
|
-
|
|
115
|
-
const placeholderLbl = el(
|
|
116
|
-
'span',
|
|
117
|
-
'flex-1 text-center text-[9px] font-mono text-text2 truncate'
|
|
118
|
-
);
|
|
119
|
-
placeholderLbl.textContent = '';
|
|
120
|
-
labelsRow.appendChild(placeholderLbl);
|
|
121
|
-
} else {
|
|
122
|
-
bars.forEach(([label, val], index) => {
|
|
123
|
-
const numVal = typeof val === 'number' && Number.isFinite(val) ? val : 0;
|
|
124
|
-
const pct = Math.round((numVal / max) * 100);
|
|
125
|
-
|
|
126
|
-
const bar = el(
|
|
127
|
-
'div',
|
|
128
|
-
'flex-1 rounded-t bg-accent opacity-80 hover:opacity-100 transition-all cursor-pointer'
|
|
129
|
-
);
|
|
130
|
-
bar.style.height = `max(2px, ${pct}%)`;
|
|
131
|
-
bar.setAttribute('role', 'button');
|
|
132
|
-
bar.setAttribute('tabindex', '0');
|
|
133
|
-
bar.setAttribute(
|
|
134
|
-
'aria-label',
|
|
135
|
-
`${String(label ?? '')}: ${String(numVal)}`
|
|
136
|
-
);
|
|
137
|
-
bar.title = `${String(label ?? '')}: ${String(numVal)}`;
|
|
138
|
-
|
|
139
|
-
const onClick = () => {
|
|
140
|
-
dispatchInteract(container, 'bar:click', {
|
|
141
|
-
index,
|
|
142
|
-
label: String(label ?? ''),
|
|
143
|
-
value: numVal,
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
const onKey = (ev: KeyboardEvent) => {
|
|
147
|
-
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
148
|
-
ev.preventDefault();
|
|
149
|
-
onClick();
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
bar.addEventListener('click', onClick);
|
|
154
|
-
bar.addEventListener('keydown', onKey);
|
|
155
|
-
unbinders.push(() => {
|
|
156
|
-
bar.removeEventListener('click', onClick);
|
|
157
|
-
bar.removeEventListener('keydown', onKey);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
barsRow.appendChild(bar);
|
|
161
|
-
|
|
162
|
-
const lbl = el(
|
|
163
|
-
'span',
|
|
164
|
-
'flex-1 text-center text-[9px] font-mono text-text2 truncate'
|
|
165
|
-
);
|
|
166
|
-
lbl.textContent = String(label ?? '');
|
|
167
|
-
labelsRow.appendChild(lbl);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
root.appendChild(barsRow);
|
|
172
|
-
root.appendChild(labelsRow);
|
|
173
|
-
container.appendChild(root);
|
|
174
|
-
|
|
175
|
-
// a11y: describe the widget as a figure.
|
|
176
|
-
container.setAttribute('role', 'figure');
|
|
177
|
-
if (title) container.setAttribute('aria-label', title);
|
|
178
|
-
|
|
179
|
-
// SVG_NS is imported but unused in this flex-based impl; keep it referenced
|
|
180
|
-
// so future variants (e.g. axis ticks) can drop in without re-import.
|
|
181
|
-
void SVG_NS;
|
|
182
|
-
|
|
183
|
-
return () => {
|
|
184
|
-
for (const off of unbinders) off();
|
|
185
|
-
container.innerHTML = '';
|
|
186
|
-
container.removeAttribute('role');
|
|
187
|
-
container.removeAttribute('aria-label');
|
|
188
|
-
};
|
|
189
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vanilla renderer for the "code" widget.
|
|
3
|
-
*
|
|
4
|
-
* Contract:
|
|
5
|
-
* render(container, data) => cleanup()
|
|
6
|
-
*
|
|
7
|
-
* Data shape: { lang?: string; content?: string }
|
|
8
|
-
*
|
|
9
|
-
* Security: uses textContent (never innerHTML) for the code source to avoid XSS.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface CodeBlockData {
|
|
13
|
-
lang?: string;
|
|
14
|
-
content?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function render(container: HTMLElement, data: any): () => void {
|
|
18
|
-
// Defensive normalization
|
|
19
|
-
const safe: CodeBlockData =
|
|
20
|
-
data && typeof data === 'object' ? (data as CodeBlockData) : {};
|
|
21
|
-
const lang = typeof safe.lang === 'string' && safe.lang.length > 0 ? safe.lang : 'text';
|
|
22
|
-
const content = typeof safe.content === 'string' ? safe.content : '';
|
|
23
|
-
|
|
24
|
-
// Clear container before mounting
|
|
25
|
-
container.innerHTML = '';
|
|
26
|
-
|
|
27
|
-
// Root wrapper
|
|
28
|
-
const wrapper = document.createElement('div');
|
|
29
|
-
wrapper.className = 'rounded overflow-hidden';
|
|
30
|
-
|
|
31
|
-
// Header bar showing the language
|
|
32
|
-
const header = document.createElement('div');
|
|
33
|
-
header.className = 'bg-black/40 px-3 py-1.5 md:px-4 border-b border-border';
|
|
34
|
-
|
|
35
|
-
const langLabel = document.createElement('span');
|
|
36
|
-
langLabel.className = 'text-[10px] font-mono text-text2';
|
|
37
|
-
langLabel.textContent = lang;
|
|
38
|
-
// A11y: expose the language for assistive tech
|
|
39
|
-
langLabel.setAttribute('aria-label', `Language: ${lang}`);
|
|
40
|
-
header.appendChild(langLabel);
|
|
41
|
-
|
|
42
|
-
// Code block (<pre><code> for semantics + a11y)
|
|
43
|
-
const pre = document.createElement('pre');
|
|
44
|
-
pre.className =
|
|
45
|
-
'font-mono text-xs text-teal bg-black/30 p-3 md:p-4 overflow-x-auto leading-relaxed';
|
|
46
|
-
// A11y: code regions are useful as landmarks for screen readers
|
|
47
|
-
pre.setAttribute('tabindex', '0');
|
|
48
|
-
pre.setAttribute('role', 'region');
|
|
49
|
-
pre.setAttribute('aria-label', `Code block (${lang})`);
|
|
50
|
-
|
|
51
|
-
const code = document.createElement('code');
|
|
52
|
-
// Hint for assistive tech + syntax highlighters (e.g. highlight.js, prism)
|
|
53
|
-
code.className = `language-${lang}`;
|
|
54
|
-
// IMPORTANT: textContent, never innerHTML — prevents XSS.
|
|
55
|
-
code.textContent = content;
|
|
56
|
-
pre.appendChild(code);
|
|
57
|
-
|
|
58
|
-
wrapper.appendChild(header);
|
|
59
|
-
wrapper.appendChild(pre);
|
|
60
|
-
container.appendChild(wrapper);
|
|
61
|
-
|
|
62
|
-
// Interaction: emit a "copy" intent when the user double-clicks the code.
|
|
63
|
-
// Consumers can listen via container.addEventListener('widget:interact', ...).
|
|
64
|
-
const onDblClick = () => {
|
|
65
|
-
container.dispatchEvent(
|
|
66
|
-
new CustomEvent('widget:interact', {
|
|
67
|
-
detail: { action: 'copy', payload: { lang, content } },
|
|
68
|
-
bubbles: true,
|
|
69
|
-
})
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
pre.addEventListener('dblclick', onDblClick);
|
|
73
|
-
|
|
74
|
-
// Cleanup: remove listeners and wipe DOM
|
|
75
|
-
return () => {
|
|
76
|
-
pre.removeEventListener('dblclick', onDblClick);
|
|
77
|
-
container.innerHTML = '';
|
|
78
|
-
};
|
|
79
|
-
}
|
package/src/widgets/simple/kv.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KV vanilla renderer.
|
|
3
|
-
* Ported from KVBlock.svelte — preserves Tailwind classes, a11y, and data shape.
|
|
4
|
-
*
|
|
5
|
-
* Data contract:
|
|
6
|
-
* { title?: string; rows?: [string, string][] }
|
|
7
|
-
*
|
|
8
|
-
* Interactions: this widget is purely presentational — no events emitted.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface KVData {
|
|
12
|
-
title?: string;
|
|
13
|
-
rows?: [string, string][];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function render(container: HTMLElement, data: any): () => void {
|
|
17
|
-
const d: KVData = (data && typeof data === 'object' ? data : {}) as KVData;
|
|
18
|
-
|
|
19
|
-
const wrap = document.createElement('div');
|
|
20
|
-
wrap.className = 'p-3 md:p-4';
|
|
21
|
-
|
|
22
|
-
if (d.title) {
|
|
23
|
-
const titleEl = document.createElement('div');
|
|
24
|
-
titleEl.className = 'text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest';
|
|
25
|
-
titleEl.textContent = String(d.title);
|
|
26
|
-
wrap.appendChild(titleEl);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const list = document.createElement('div');
|
|
30
|
-
list.className = 'flex flex-col gap-1.5';
|
|
31
|
-
|
|
32
|
-
const rows: [string, string][] = Array.isArray(d.rows) ? d.rows : [];
|
|
33
|
-
|
|
34
|
-
if (rows.length === 0) {
|
|
35
|
-
const empty = document.createElement('div');
|
|
36
|
-
empty.className = 'text-xs text-text2 italic';
|
|
37
|
-
empty.textContent = '—';
|
|
38
|
-
list.appendChild(empty);
|
|
39
|
-
} else {
|
|
40
|
-
for (const entry of rows) {
|
|
41
|
-
const row = document.createElement('div');
|
|
42
|
-
row.className =
|
|
43
|
-
'flex justify-between items-center text-sm border-b border-border pb-1.5 last:border-none last:pb-0';
|
|
44
|
-
|
|
45
|
-
const k = Array.isArray(entry) ? entry[0] : '';
|
|
46
|
-
const v = Array.isArray(entry) ? entry[1] : '';
|
|
47
|
-
|
|
48
|
-
const keyEl = document.createElement('span');
|
|
49
|
-
keyEl.className = 'font-mono text-xs text-text2';
|
|
50
|
-
keyEl.textContent = k == null ? '' : String(k);
|
|
51
|
-
|
|
52
|
-
const valEl = document.createElement('span');
|
|
53
|
-
valEl.className = 'text-text1 font-medium';
|
|
54
|
-
valEl.textContent = v == null ? '' : String(v);
|
|
55
|
-
|
|
56
|
-
row.appendChild(keyEl);
|
|
57
|
-
row.appendChild(valEl);
|
|
58
|
-
list.appendChild(row);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
wrap.appendChild(list);
|
|
63
|
-
container.appendChild(wrap);
|
|
64
|
-
|
|
65
|
-
return () => {
|
|
66
|
-
container.innerHTML = '';
|
|
67
|
-
};
|
|
68
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vanilla renderer for the "list" widget.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors ListBlock.svelte:
|
|
5
|
-
* - optional title
|
|
6
|
-
* - items rendered as <li> with Tailwind classes
|
|
7
|
-
* - double-click (and Enter/Space for a11y) dispatches a bubbling
|
|
8
|
-
* `widget:interact` CustomEvent with { action: 'itemclick', payload: { item, index } }
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface ListBlockData {
|
|
12
|
-
title?: string;
|
|
13
|
-
items: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type Cleanup = () => void;
|
|
17
|
-
|
|
18
|
-
export function render(container: HTMLElement, data: Partial<ListBlockData>): Cleanup {
|
|
19
|
-
// Reset container
|
|
20
|
-
container.innerHTML = '';
|
|
21
|
-
|
|
22
|
-
const items = Array.isArray(data?.items) ? data!.items : [];
|
|
23
|
-
const title = typeof data?.title === 'string' ? data!.title : undefined;
|
|
24
|
-
|
|
25
|
-
const wrapper = document.createElement('div');
|
|
26
|
-
wrapper.className = 'p-3 md:p-4';
|
|
27
|
-
|
|
28
|
-
if (title) {
|
|
29
|
-
const titleEl = document.createElement('div');
|
|
30
|
-
titleEl.className =
|
|
31
|
-
'text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest';
|
|
32
|
-
titleEl.textContent = title;
|
|
33
|
-
wrapper.appendChild(titleEl);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const ul = document.createElement('ul');
|
|
37
|
-
ul.className = 'flex flex-col gap-1.5';
|
|
38
|
-
|
|
39
|
-
// Track listeners for cleanup
|
|
40
|
-
const disposers: Array<() => void> = [];
|
|
41
|
-
|
|
42
|
-
items.forEach((item, i) => {
|
|
43
|
-
const li = document.createElement('li');
|
|
44
|
-
li.className =
|
|
45
|
-
'text-sm text-text1 bg-surface2 rounded px-3 py-2 border-l-2 border-accent cursor-pointer hover:bg-surface2/80';
|
|
46
|
-
li.title = 'Double-cliquez pour interagir';
|
|
47
|
-
li.textContent = String(item);
|
|
48
|
-
|
|
49
|
-
// a11y: make keyboard-interactive
|
|
50
|
-
li.setAttribute('role', 'button');
|
|
51
|
-
li.setAttribute('tabindex', '0');
|
|
52
|
-
|
|
53
|
-
const fire = () => {
|
|
54
|
-
container.dispatchEvent(
|
|
55
|
-
new CustomEvent('widget:interact', {
|
|
56
|
-
detail: { action: 'itemclick', payload: { item, index: i } },
|
|
57
|
-
bubbles: true,
|
|
58
|
-
})
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const onDblClick = () => fire();
|
|
63
|
-
const onKeyDown = (ev: KeyboardEvent) => {
|
|
64
|
-
if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {
|
|
65
|
-
ev.preventDefault();
|
|
66
|
-
fire();
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
li.addEventListener('dblclick', onDblClick);
|
|
71
|
-
li.addEventListener('keydown', onKeyDown);
|
|
72
|
-
|
|
73
|
-
disposers.push(() => {
|
|
74
|
-
li.removeEventListener('dblclick', onDblClick);
|
|
75
|
-
li.removeEventListener('keydown', onKeyDown);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
ul.appendChild(li);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
wrapper.appendChild(ul);
|
|
82
|
-
container.appendChild(wrapper);
|
|
83
|
-
|
|
84
|
-
return () => {
|
|
85
|
-
for (const d of disposers) d();
|
|
86
|
-
disposers.length = 0;
|
|
87
|
-
container.innerHTML = '';
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export interface StatBlockData {
|
|
2
|
-
label: string;
|
|
3
|
-
value: string;
|
|
4
|
-
trend?: string;
|
|
5
|
-
trendDir?: 'up' | 'down' | 'neutral';
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Vanilla renderer for the "stat" / StatBlock widget.
|
|
10
|
-
*
|
|
11
|
-
* Mounts a metric block (label + value + optional trend) into `container`.
|
|
12
|
-
* Returns a cleanup function that clears the container.
|
|
13
|
-
*
|
|
14
|
-
* Contract:
|
|
15
|
-
* - Pure imperative DOM (no Svelte runes, no framework).
|
|
16
|
-
* - Emits no events by default (the Svelte original had none either) but keeps
|
|
17
|
-
* the standard `widget:interact` channel available via helper if ever needed.
|
|
18
|
-
* - Tailwind classes and CSS vars are preserved verbatim.
|
|
19
|
-
*/
|
|
20
|
-
export function render(container: HTMLElement, data: any): () => void {
|
|
21
|
-
const d: Partial<StatBlockData> = (data && typeof data === 'object') ? data : {};
|
|
22
|
-
|
|
23
|
-
const trendDir = d.trendDir;
|
|
24
|
-
const trendColor =
|
|
25
|
-
trendDir === 'up' ? 'text-teal'
|
|
26
|
-
: trendDir === 'down' ? 'text-accent2'
|
|
27
|
-
: 'text-text2';
|
|
28
|
-
const trendArrow =
|
|
29
|
-
trendDir === 'up' ? '↑'
|
|
30
|
-
: trendDir === 'down' ? '↓'
|
|
31
|
-
: '→';
|
|
32
|
-
|
|
33
|
-
const root = document.createElement('div');
|
|
34
|
-
root.className = 'p-4 md:p-5';
|
|
35
|
-
|
|
36
|
-
const labelEl = document.createElement('div');
|
|
37
|
-
labelEl.className = 'text-[11px] font-mono text-text2 mb-1 uppercase tracking-widest';
|
|
38
|
-
labelEl.textContent = d.label ?? 'Metric';
|
|
39
|
-
root.appendChild(labelEl);
|
|
40
|
-
|
|
41
|
-
const valueEl = document.createElement('div');
|
|
42
|
-
valueEl.className = 'text-3xl md:text-4xl font-bold text-text1 leading-none';
|
|
43
|
-
valueEl.textContent = d.value ?? '—';
|
|
44
|
-
root.appendChild(valueEl);
|
|
45
|
-
|
|
46
|
-
if (d.trend) {
|
|
47
|
-
const trendEl = document.createElement('div');
|
|
48
|
-
trendEl.className = `text-xs font-mono mt-2 ${trendColor}`;
|
|
49
|
-
trendEl.textContent = `${trendArrow} ${d.trend}`;
|
|
50
|
-
root.appendChild(trendEl);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
container.appendChild(root);
|
|
54
|
-
|
|
55
|
-
return () => {
|
|
56
|
-
container.innerHTML = '';
|
|
57
|
-
};
|
|
58
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vanilla renderer for the Tags widget.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the Svelte TagsBlock.svelte component but uses imperative DOM
|
|
5
|
-
* manipulation so it can be mounted without a framework.
|
|
6
|
-
*
|
|
7
|
-
* Contract:
|
|
8
|
-
* render(container, data) -> cleanup()
|
|
9
|
-
*
|
|
10
|
-
* Data shape:
|
|
11
|
-
* { label?: string; tags: Array<{ text: string; active?: boolean }> }
|
|
12
|
-
*
|
|
13
|
-
* Events:
|
|
14
|
-
* Dispatches a bubbling `widget:interact` CustomEvent on the container
|
|
15
|
-
* when a tag is activated (click / Enter / Space) with:
|
|
16
|
-
* detail: { action: 'tag:toggle', payload: { index, text, active } }
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export interface TagItem {
|
|
20
|
-
text: string;
|
|
21
|
-
active?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface TagsBlockData {
|
|
25
|
-
label?: string;
|
|
26
|
-
tags: TagItem[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type Cleanup = () => void;
|
|
30
|
-
|
|
31
|
-
const WRAPPER_CLASS = 'p-3 md:p-4 flex gap-2 flex-wrap items-center';
|
|
32
|
-
const LABEL_CLASS = 'text-[10px] font-mono text-text2';
|
|
33
|
-
const TAG_BASE_CLASS =
|
|
34
|
-
'text-[11px] font-mono px-3 py-1 rounded-full border transition-colors';
|
|
35
|
-
const TAG_ACTIVE_CLASS = 'border-teal text-teal bg-teal/10';
|
|
36
|
-
const TAG_INACTIVE_CLASS = 'border-border2 text-text2';
|
|
37
|
-
|
|
38
|
-
export function render(container: HTMLElement, data: any): Cleanup {
|
|
39
|
-
// Reset container
|
|
40
|
-
container.innerHTML = '';
|
|
41
|
-
|
|
42
|
-
const safeData: Partial<TagsBlockData> =
|
|
43
|
-
data && typeof data === 'object' ? data : {};
|
|
44
|
-
const tags: TagItem[] = Array.isArray(safeData.tags) ? safeData.tags : [];
|
|
45
|
-
|
|
46
|
-
const wrapper = document.createElement('div');
|
|
47
|
-
wrapper.className = WRAPPER_CLASS;
|
|
48
|
-
wrapper.setAttribute('role', 'group');
|
|
49
|
-
if (safeData.label) {
|
|
50
|
-
wrapper.setAttribute('aria-label', String(safeData.label));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (safeData.label) {
|
|
54
|
-
const labelEl = document.createElement('span');
|
|
55
|
-
labelEl.className = LABEL_CLASS;
|
|
56
|
-
labelEl.textContent = String(safeData.label);
|
|
57
|
-
wrapper.appendChild(labelEl);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Robustness: empty tags -> render nothing beyond (possibly) the label.
|
|
61
|
-
// If neither label nor tags, leave the wrapper empty silently.
|
|
62
|
-
const listeners: Array<{
|
|
63
|
-
el: HTMLElement;
|
|
64
|
-
type: string;
|
|
65
|
-
fn: EventListener;
|
|
66
|
-
}> = [];
|
|
67
|
-
|
|
68
|
-
tags.forEach((tag, index) => {
|
|
69
|
-
if (!tag || typeof tag !== 'object') return;
|
|
70
|
-
const text = tag.text == null ? '' : String(tag.text);
|
|
71
|
-
const active = Boolean(tag.active);
|
|
72
|
-
|
|
73
|
-
const chip = document.createElement('span');
|
|
74
|
-
chip.className = `${TAG_BASE_CLASS} ${active ? TAG_ACTIVE_CLASS : TAG_INACTIVE_CLASS}`;
|
|
75
|
-
chip.textContent = text;
|
|
76
|
-
|
|
77
|
-
// a11y + interactivity
|
|
78
|
-
chip.setAttribute('role', 'button');
|
|
79
|
-
chip.setAttribute('tabindex', '0');
|
|
80
|
-
chip.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
81
|
-
chip.dataset.index = String(index);
|
|
82
|
-
|
|
83
|
-
const emit = () => {
|
|
84
|
-
const ev = new CustomEvent('widget:interact', {
|
|
85
|
-
detail: {
|
|
86
|
-
action: 'tag:toggle',
|
|
87
|
-
payload: { index, text, active },
|
|
88
|
-
},
|
|
89
|
-
bubbles: true,
|
|
90
|
-
});
|
|
91
|
-
container.dispatchEvent(ev);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const onClick: EventListener = (e) => {
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
emit();
|
|
97
|
-
};
|
|
98
|
-
const onKey: EventListener = (e) => {
|
|
99
|
-
const ke = e as KeyboardEvent;
|
|
100
|
-
if (ke.key === 'Enter' || ke.key === ' ') {
|
|
101
|
-
ke.preventDefault();
|
|
102
|
-
emit();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
chip.addEventListener('click', onClick);
|
|
107
|
-
chip.addEventListener('keydown', onKey);
|
|
108
|
-
listeners.push({ el: chip, type: 'click', fn: onClick });
|
|
109
|
-
listeners.push({ el: chip, type: 'keydown', fn: onKey });
|
|
110
|
-
|
|
111
|
-
wrapper.appendChild(chip);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
container.appendChild(wrapper);
|
|
115
|
-
|
|
116
|
-
return () => {
|
|
117
|
-
for (const { el, type, fn } of listeners) {
|
|
118
|
-
el.removeEventListener(type, fn);
|
|
119
|
-
}
|
|
120
|
-
listeners.length = 0;
|
|
121
|
-
container.innerHTML = '';
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export default { render };
|