@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,179 +0,0 @@
1
- export interface TimelineEvent {
2
- date?: string;
3
- title?: string;
4
- description?: string;
5
- status?: 'done' | 'active' | 'pending';
6
- color?: string;
7
- href?: string;
8
- tags?: string[];
9
- }
10
-
11
- export interface TimelineSpec {
12
- title?: string;
13
- events?: TimelineEvent[];
14
- }
15
-
16
- const STATUS: Record<string, string> = {
17
- done: 'var(--color-teal)',
18
- active: 'var(--color-accent)',
19
- pending: 'var(--color-border2)',
20
- };
21
-
22
- function escapeHtml(s: string): string {
23
- return s
24
- .replace(/&/g, '&amp;')
25
- .replace(/</g, '&lt;')
26
- .replace(/>/g, '&gt;')
27
- .replace(/"/g, '&quot;')
28
- .replace(/'/g, '&#39;');
29
- }
30
-
31
- function escapeAttr(s: string): string {
32
- return escapeHtml(s);
33
- }
34
-
35
- function resolveEvents(spec: Partial<TimelineSpec>, data: unknown): TimelineEvent[] {
36
- if (Array.isArray(spec.events) && spec.events.length) return spec.events;
37
- if (Array.isArray(data)) return data as TimelineEvent[];
38
- return [];
39
- }
40
-
41
- function renderEvent(event: TimelineEvent, index: number, isLast: boolean, interactive: boolean): string {
42
- const dotColor = event.color ?? STATUS[event.status ?? 'pending'] ?? 'var(--color-border2)';
43
- const activeShadow = event.status === 'active' ? `box-shadow:0 0 0 3px ${dotColor}33;` : '';
44
- const pbClass = !isLast ? 'pb-5' : '';
45
- const cursorClass = interactive ? 'cursor-pointer' : '';
46
- const role = interactive ? ' role="button"' : '';
47
- const tabindex = interactive ? ' tabindex="0"' : '';
48
- const title = interactive ? ' title="Double-cliquez pour interagir"' : '';
49
-
50
- const titleHtml = event.href
51
- ? `<a href="${escapeAttr(event.href)}" class="text-accent no-underline hover:underline">${escapeHtml(event.title ?? '')}</a>`
52
- : escapeHtml(event.title ?? '');
53
-
54
- const descriptionHtml = event.description
55
- ? `<div class="text-sm text-text2 mt-0.5">${escapeHtml(event.description)}</div>`
56
- : '';
57
-
58
- const tagsHtml = event.tags?.length
59
- ? `<div class="flex gap-1 flex-wrap mt-1">${event.tags
60
- .map(
61
- (tag) =>
62
- `<span class="text-xs bg-surface2 text-text2 px-1.5 py-0.5 rounded">${escapeHtml(tag)}</span>`
63
- )
64
- .join('')}</div>`
65
- : '';
66
-
67
- const connector = !isLast ? `<div class="w-0.5 flex-1 bg-border mt-1"></div>` : '';
68
-
69
- return `
70
- <div class="flex gap-4 relative ${pbClass} ${cursorClass}" data-event-index="${index}"${role}${tabindex}${title}>
71
- <div class="flex flex-col items-center flex-shrink-0">
72
- <div class="w-3 h-3 rounded-full flex-shrink-0 mt-0.5" style="background:${dotColor};${activeShadow}"></div>
73
- ${connector}
74
- </div>
75
- <div class="flex-1 min-w-0 pb-1">
76
- <div class="text-xs text-text2 mb-0.5">${escapeHtml(event.date ?? '')}</div>
77
- <div class="font-semibold text-text1 text-sm">${titleHtml}</div>
78
- ${descriptionHtml}
79
- ${tagsHtml}
80
- </div>
81
- </div>
82
- `;
83
- }
84
-
85
- export function render(container: HTMLElement, data: any): () => void {
86
- const spec: Partial<TimelineSpec> =
87
- data && typeof data === 'object' && !Array.isArray(data)
88
- ? (data.spec && typeof data.spec === 'object' ? data.spec : data)
89
- : {};
90
- const rawData =
91
- data && typeof data === 'object' && !Array.isArray(data) && 'data' in data
92
- ? (data as any).data
93
- : data;
94
-
95
- const events = resolveEvents(spec, rawData);
96
- const interactive = true;
97
-
98
- const titleHtml = spec.title
99
- ? `<h3 class="text-sm font-semibold text-text1 mb-3">${escapeHtml(spec.title)}</h3>`
100
- : '';
101
-
102
- if (events.length === 0) {
103
- container.innerHTML = `
104
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
105
- ${titleHtml}
106
- <p class="text-text2 text-sm">No events</p>
107
- </div>
108
- `;
109
- return () => {
110
- container.innerHTML = '';
111
- };
112
- }
113
-
114
- const eventsHtml = events
115
- .map((event, i) => renderEvent(event, i, i === events.length - 1, interactive))
116
- .join('');
117
-
118
- container.innerHTML = `
119
- <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
120
- ${titleHtml}
121
- <div class="py-1">${eventsHtml}</div>
122
- </div>
123
- `;
124
-
125
- const root = container.firstElementChild as HTMLElement | null;
126
-
127
- const dispatch = (index: number) => {
128
- const event = events[index];
129
- if (!event) return;
130
- container.dispatchEvent(
131
- new CustomEvent('widget:interact', {
132
- detail: { action: 'eventclick', payload: event },
133
- bubbles: true,
134
- })
135
- );
136
- };
137
-
138
- const findIndex = (target: EventTarget | null): number | null => {
139
- let el = target as HTMLElement | null;
140
- while (el && el !== root) {
141
- if (el.dataset && el.dataset.eventIndex !== undefined) {
142
- const n = Number(el.dataset.eventIndex);
143
- return Number.isFinite(n) ? n : null;
144
- }
145
- el = el.parentElement;
146
- }
147
- return null;
148
- };
149
-
150
- const onDblClick = (e: Event) => {
151
- const i = findIndex(e.target);
152
- if (i !== null) {
153
- dispatch(i);
154
- e.stopPropagation();
155
- }
156
- };
157
-
158
- const onKeyDown = (e: KeyboardEvent) => {
159
- if (e.key !== 'Enter') return;
160
- const i = findIndex(e.target);
161
- if (i !== null) {
162
- dispatch(i);
163
- e.stopPropagation();
164
- }
165
- };
166
-
167
- if (root) {
168
- root.addEventListener('dblclick', onDblClick);
169
- root.addEventListener('keydown', onKeyDown);
170
- }
171
-
172
- return () => {
173
- if (root) {
174
- root.removeEventListener('dblclick', onDblClick);
175
- root.removeEventListener('keydown', onKeyDown);
176
- }
177
- container.innerHTML = '';
178
- };
179
- }
@@ -1,246 +0,0 @@
1
- import { createSafeImage } from '../helpers/safe-image.js';
2
-
3
- export interface TrombinoscopePerson {
4
- name: string;
5
- subtitle?: string;
6
- avatar?: string;
7
- badge?: string;
8
- color?: string;
9
- badgeColor?: string;
10
- }
11
-
12
- export interface TrombinoscopeSpec {
13
- title?: string;
14
- people?: TrombinoscopePerson[];
15
- columns?: number;
16
- showBadge?: boolean;
17
- }
18
-
19
- const COLORS = [
20
- '#7c6dfa',
21
- '#3ecfb2',
22
- '#f0a050',
23
- '#fa6d7c',
24
- '#3b82f6',
25
- '#a855f7',
26
- '#14b8a6',
27
- '#f97316',
28
- ];
29
-
30
- function nameColor(name: string): string {
31
- let h = 0;
32
- for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
33
- return COLORS[Math.abs(h) % COLORS.length]!;
34
- }
35
-
36
- function initials(name: string): string {
37
- return (
38
- name
39
- .split(/\s+/)
40
- .slice(0, 2)
41
- .map((w) => w[0] ?? '')
42
- .join('')
43
- .toUpperCase() || '?'
44
- );
45
- }
46
-
47
- function extractPeople(input: any): TrombinoscopePerson[] {
48
- if (input && typeof input === 'object' && Array.isArray(input.people) && input.people.length) {
49
- return input.people as TrombinoscopePerson[];
50
- }
51
- if (Array.isArray(input)) return input as TrombinoscopePerson[];
52
- if (input && typeof input === 'object' && Array.isArray((input as any).data)) {
53
- return (input as any).data as TrombinoscopePerson[];
54
- }
55
- return [];
56
- }
57
-
58
- /**
59
- * Vanilla renderer for the "trombinoscope" widget.
60
- *
61
- * Contract:
62
- * - render(container, data): () => void
63
- * - `data` is the spec (may include `people`); if `data` is itself an array
64
- * of persons, that is used as the roster.
65
- * - Emits CustomEvent('widget:interact', {
66
- * detail: { action: 'personclick', payload: person }, bubbles: true })
67
- * when a person card is clicked.
68
- * - Cleanup removes all listeners and empties the container.
69
- * - Avatar fallback: createSafeImage handles URL validation + error fallback;
70
- * we additionally render an "initials bubble" when the URL is invalid/missing
71
- * and swap the <img> for an initials bubble on load error (closer to the
72
- * original Svelte behaviour than a generic placeholder).
73
- */
74
- export function render(container: HTMLElement, data: any): () => void {
75
- const spec: Partial<TrombinoscopeSpec> =
76
- data && typeof data === 'object' && !Array.isArray(data) ? data : {};
77
- const people = extractPeople(data);
78
- const cols = spec.columns ?? 4;
79
- const showBadge = spec.showBadge !== false;
80
- const hasClickHandler = true; // vanilla renderer always bubbles clicks
81
-
82
- const cleanups: Array<() => void> = [];
83
-
84
- const root = document.createElement('div');
85
- root.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
86
-
87
- if (spec.title) {
88
- const h = document.createElement('h3');
89
- h.className = 'text-sm font-semibold text-text1 mb-3';
90
- h.textContent = spec.title;
91
- root.appendChild(h);
92
- }
93
-
94
- if (people.length === 0) {
95
- const empty = document.createElement('p');
96
- empty.className = 'text-text2 text-sm';
97
- empty.textContent = 'Aucune personne';
98
- root.appendChild(empty);
99
- container.appendChild(root);
100
- return () => {
101
- container.innerHTML = '';
102
- };
103
- }
104
-
105
- const grid = document.createElement('div');
106
- grid.className = 'grid gap-3 responsive-trombi';
107
- grid.setAttribute('style', `--trombi-cols: repeat(${cols}, minmax(0, 1fr));`);
108
-
109
- for (const person of people) {
110
- const accent = person.color ?? nameColor(person.name);
111
-
112
- const card = document.createElement('div');
113
- card.className =
114
- 'flex flex-col items-center text-center p-3 rounded-lg border border-border hover:border-border2 transition-all' +
115
- (hasClickHandler ? ' cursor-pointer' : '');
116
- if (hasClickHandler) {
117
- card.setAttribute('role', 'button');
118
- card.setAttribute('tabindex', '0');
119
- }
120
-
121
- // Avatar
122
- const avatarClasses = 'w-12 h-12 rounded-full object-cover mb-2 border-2';
123
- const fallbackClasses =
124
- 'w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-base mb-2 flex-shrink-0';
125
-
126
- const buildInitialsFallback = (): HTMLElement => {
127
- const div = document.createElement('div');
128
- div.className = fallbackClasses;
129
- div.setAttribute('style', `background:${accent};`);
130
- div.setAttribute('role', 'img');
131
- div.setAttribute('aria-label', person.name);
132
- div.textContent = initials(person.name);
133
- return div;
134
- };
135
-
136
- const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
137
- const srcLooksValid =
138
- typeof person.avatar === 'string' &&
139
- person.avatar.length > 0 &&
140
- VALID_PREFIXES.some((p) => person.avatar!.startsWith(p));
141
-
142
- if (srcLooksValid) {
143
- const imgEl = createSafeImage({
144
- src: person.avatar!,
145
- alt: person.name,
146
- className: avatarClasses,
147
- style: `border-color:${accent};`,
148
- fallbackText: initials(person.name),
149
- });
150
- card.appendChild(imgEl);
151
-
152
- if (imgEl.tagName === 'IMG') {
153
- const onErr = () => {
154
- const fb = buildInitialsFallback();
155
- imgEl.replaceWith(fb);
156
- };
157
- imgEl.addEventListener('error', onErr, { once: true });
158
- cleanups.push(() => imgEl.removeEventListener('error', onErr));
159
- }
160
- } else {
161
- card.appendChild(buildInitialsFallback());
162
- }
163
-
164
- // Name
165
- const nameEl = document.createElement('div');
166
- nameEl.className = 'text-xs font-semibold text-text1 leading-tight truncate w-full';
167
- nameEl.textContent = person.name;
168
- card.appendChild(nameEl);
169
-
170
- // Subtitle
171
- if (person.subtitle) {
172
- const sub = document.createElement('div');
173
- sub.className = 'text-xs text-text2 mt-0.5 truncate w-full';
174
- sub.textContent = person.subtitle;
175
- card.appendChild(sub);
176
- }
177
-
178
- // Badge
179
- if (showBadge && person.badge) {
180
- const badge = document.createElement('span');
181
- badge.className = 'text-xs font-semibold px-2 py-0.5 rounded-full mt-1.5 text-white';
182
- badge.setAttribute('style', `background:${person.badgeColor ?? accent};`);
183
- badge.textContent = person.badge;
184
- card.appendChild(badge);
185
- }
186
-
187
- // Click / keyboard handlers
188
- const emitClick = () => {
189
- container.dispatchEvent(
190
- new CustomEvent('widget:interact', {
191
- detail: { action: 'personclick', payload: person },
192
- bubbles: true,
193
- }),
194
- );
195
- };
196
- const onClick = (e: Event) => {
197
- emitClick();
198
- e.stopPropagation();
199
- };
200
- const onKey = (e: KeyboardEvent) => {
201
- if (e.key === 'Enter' || e.key === ' ') {
202
- e.preventDefault();
203
- emitClick();
204
- }
205
- };
206
- card.addEventListener('click', onClick);
207
- card.addEventListener('keydown', onKey);
208
- cleanups.push(() => card.removeEventListener('click', onClick));
209
- cleanups.push(() => card.removeEventListener('keydown', onKey));
210
-
211
- grid.appendChild(card);
212
- }
213
-
214
- root.appendChild(grid);
215
-
216
- const count = document.createElement('div');
217
- count.className = 'mt-3 text-xs text-text2';
218
- count.textContent = `${people.length} personne${people.length !== 1 ? 's' : ''}`;
219
- root.appendChild(count);
220
-
221
- // Inject scoped responsive grid styles (preserves original <style> block).
222
- // Only injected once per document to avoid duplication across instances.
223
- const STYLE_ID = 'wmcp-trombi-style';
224
- if (typeof document !== 'undefined' && !document.getElementById(STYLE_ID)) {
225
- const style = document.createElement('style');
226
- style.id = STYLE_ID;
227
- style.textContent =
228
- '.responsive-trombi { grid-template-columns: repeat(2, minmax(0, 1fr)); }' +
229
- '@media (min-width: 768px) { .responsive-trombi { grid-template-columns: var(--trombi-cols); } }';
230
- document.head.appendChild(style);
231
- }
232
-
233
- container.appendChild(root);
234
-
235
- return () => {
236
- for (const fn of cleanups) {
237
- try {
238
- fn();
239
- } catch {
240
- /* swallow */
241
- }
242
- }
243
- cleanups.length = 0;
244
- container.innerHTML = '';
245
- };
246
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Vanilla renderer for the Actions widget.
3
- *
4
- * Mirrors the Svelte version in `ActionsBlock.svelte`:
5
- * renders a flex-wrap row of buttons. Each button may carry its own
6
- * `onclick` handler (preserved from the Svelte shape). When no handler is
7
- * provided, a `widget:interact` CustomEvent is dispatched on the container
8
- * with `{ action: 'action-click', payload: { action: <label>, index } }`.
9
- */
10
-
11
- export interface ActionButton {
12
- label: string;
13
- primary?: boolean;
14
- onclick?: () => void;
15
- }
16
-
17
- export interface ActionsBlockData {
18
- buttons: ActionButton[];
19
- }
20
-
21
- export function render(container: HTMLElement, data: Partial<ActionsBlockData> | undefined | null): () => void {
22
- // Reset the container.
23
- container.innerHTML = '';
24
-
25
- const buttons: ActionButton[] = Array.isArray(data?.buttons) ? (data!.buttons as ActionButton[]) : [];
26
-
27
- const root = document.createElement('div');
28
- root.className = 'p-3 md:p-4 flex gap-2 flex-wrap';
29
- root.setAttribute('role', 'group');
30
-
31
- // Nothing to render: keep the empty wrapper (stays consistent with Svelte output).
32
- if (buttons.length === 0) {
33
- container.appendChild(root);
34
- return () => {
35
- container.innerHTML = '';
36
- };
37
- }
38
-
39
- // Track listeners so we can clean up precisely.
40
- const listeners: Array<{ el: HTMLButtonElement; handler: (ev: Event) => void }> = [];
41
-
42
- buttons.forEach((btn, index) => {
43
- const el = document.createElement('button');
44
- el.type = 'button';
45
-
46
- const base = 'text-xs font-mono px-4 py-2 rounded border transition-all';
47
- const variant = btn.primary
48
- ? 'bg-accent border-accent text-white hover:opacity-85'
49
- : 'border-border2 text-text2 hover:border-accent hover:text-accent';
50
- el.className = `${base} ${variant}`;
51
-
52
- el.textContent = btn.label ?? '';
53
- el.setAttribute('aria-label', btn.label ?? `action-${index}`);
54
-
55
- const handler = (_ev: Event) => {
56
- if (typeof btn.onclick === 'function') {
57
- try {
58
- btn.onclick();
59
- } catch (err) {
60
- console.error('[actions widget] onclick handler threw', err);
61
- }
62
- return;
63
- }
64
- container.dispatchEvent(
65
- new CustomEvent('widget:interact', {
66
- detail: {
67
- action: 'action-click',
68
- payload: { action: btn.label, index },
69
- },
70
- bubbles: true,
71
- })
72
- );
73
- };
74
-
75
- el.addEventListener('click', handler);
76
- listeners.push({ el, handler });
77
- root.appendChild(el);
78
- });
79
-
80
- container.appendChild(root);
81
-
82
- return () => {
83
- for (const { el, handler } of listeners) {
84
- el.removeEventListener('click', handler);
85
- }
86
- listeners.length = 0;
87
- container.innerHTML = '';
88
- };
89
- }
@@ -1,100 +0,0 @@
1
- /**
2
- * Vanilla renderer for the AlertBlock widget.
3
- *
4
- * Contract:
5
- * render(container, data) => cleanup()
6
- *
7
- * Mirrors AlertBlock.svelte — a simple left-bordered alert with optional
8
- * title and message, color-coded by severity level.
9
- */
10
-
11
- export interface AlertBlockData {
12
- title?: string;
13
- message?: string;
14
- level?: 'info' | 'warn' | 'error';
15
- }
16
-
17
- type Level = 'info' | 'warn' | 'error';
18
-
19
- function deriveBorderColor(level: Level | undefined): string {
20
- if (level === 'error') return 'border-accent2';
21
- if (level === 'info') return 'border-blue-500';
22
- return 'border-amber';
23
- }
24
-
25
- function deriveTitleColor(level: Level | undefined): string {
26
- if (level === 'error') return 'text-accent2';
27
- if (level === 'info') return 'text-blue-400';
28
- return 'text-amber';
29
- }
30
-
31
- function normalizeLevel(raw: unknown): Level | undefined {
32
- if (raw === 'info' || raw === 'warn' || raw === 'error') return raw;
33
- return undefined;
34
- }
35
-
36
- export function render(
37
- container: HTMLElement,
38
- data: Partial<AlertBlockData> | null | undefined
39
- ): () => void {
40
- const safe: Partial<AlertBlockData> = data && typeof data === 'object' ? data : {};
41
- const level = normalizeLevel(safe.level);
42
- const title = typeof safe.title === 'string' ? safe.title : '';
43
- const message = typeof safe.message === 'string' ? safe.message : '';
44
-
45
- const borderColor = deriveBorderColor(level);
46
- const titleColor = deriveTitleColor(level);
47
-
48
- // Clear target
49
- container.innerHTML = '';
50
-
51
- // Root
52
- const root = document.createElement('div');
53
- root.className = `p-3 md:p-4 border-l-4 ${borderColor}`;
54
-
55
- // Accessibility: role=alert for warn/error, status for info
56
- if (level === 'error' || level === 'warn') {
57
- root.setAttribute('role', 'alert');
58
- root.setAttribute('aria-live', 'assertive');
59
- } else {
60
- root.setAttribute('role', 'status');
61
- root.setAttribute('aria-live', 'polite');
62
- }
63
-
64
- if (title) {
65
- const titleEl = document.createElement('div');
66
- titleEl.className = `font-semibold text-sm mb-1 ${titleColor}`;
67
- titleEl.textContent = title;
68
- root.appendChild(titleEl);
69
- }
70
-
71
- if (message) {
72
- const msgEl = document.createElement('div');
73
- msgEl.className = 'text-xs font-mono text-text2';
74
- msgEl.textContent = message;
75
- root.appendChild(msgEl);
76
- }
77
-
78
- // Click-to-interact (non-breaking: widget:interact bubbles up)
79
- const onClick = (ev: MouseEvent) => {
80
- container.dispatchEvent(
81
- new CustomEvent('widget:interact', {
82
- detail: {
83
- action: 'alert.click',
84
- payload: { level: level ?? null, title, message },
85
- originalEvent: ev,
86
- },
87
- bubbles: true,
88
- })
89
- );
90
- };
91
- root.addEventListener('click', onClick);
92
-
93
- container.appendChild(root);
94
-
95
- // Cleanup
96
- return () => {
97
- root.removeEventListener('click', onClick);
98
- container.innerHTML = '';
99
- };
100
- }