@webmcp-auto-ui/ui 2.5.26 → 2.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -373
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -38
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
|
@@ -0,0 +1,179 @@
|
|
|
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, '&')
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/"/g, '"')
|
|
28
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
}
|