@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.
Files changed (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. 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
- }
@@ -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 };