@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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cards (vanilla) — grid of cards with optional image, subtitle, description, tags.
|
|
3
|
+
* Mirrors Cards.svelte. Interactive mode: cards are clickable (dblclick + Enter/Space).
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* render(container, data) -> cleanup()
|
|
7
|
+
* Event:
|
|
8
|
+
* 'widget:interact' { action: 'cardclick', payload: card }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createSafeImage } from '../helpers/safe-image.js';
|
|
12
|
+
|
|
13
|
+
export interface CardItem {
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
href?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CardsSpec {
|
|
23
|
+
title?: string;
|
|
24
|
+
cards?: CardItem[];
|
|
25
|
+
minCardWidth?: string;
|
|
26
|
+
gap?: string;
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
interactive?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CardsPayload {
|
|
32
|
+
spec?: Partial<CardsSpec>;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function escapeHtml(s: string): string {
|
|
37
|
+
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveCards(spec: Partial<CardsSpec>, data: unknown): CardItem[] {
|
|
41
|
+
if (Array.isArray(spec.cards) && spec.cards.length) return spec.cards as CardItem[];
|
|
42
|
+
if (Array.isArray(data)) {
|
|
43
|
+
return (data as Record<string, unknown>[]).map((d) => ({
|
|
44
|
+
title: String(d.title ?? d.name ?? d.label ?? JSON.stringify(d)),
|
|
45
|
+
description: typeof d.description === 'string' ? (d.description as string) : undefined,
|
|
46
|
+
subtitle: typeof d.subtitle === 'string' ? (d.subtitle as string) : undefined,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function render(container: HTMLElement, data: any): () => void {
|
|
53
|
+
const payload: CardsPayload = (data && typeof data === 'object' ? data : {}) as CardsPayload;
|
|
54
|
+
const spec: Partial<CardsSpec> = payload.spec ?? {};
|
|
55
|
+
const inner = payload.data;
|
|
56
|
+
const interactive = spec.interactive === true;
|
|
57
|
+
const cards = resolveCards(spec, inner);
|
|
58
|
+
|
|
59
|
+
// Root
|
|
60
|
+
const root = document.createElement('div');
|
|
61
|
+
root.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
|
|
62
|
+
|
|
63
|
+
if (spec.title) {
|
|
64
|
+
const h3 = document.createElement('h3');
|
|
65
|
+
h3.className = 'text-sm font-semibold text-text1 mb-3';
|
|
66
|
+
h3.textContent = spec.title;
|
|
67
|
+
root.appendChild(h3);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Tracked listeners for cleanup
|
|
71
|
+
const listeners: Array<{ el: HTMLElement; type: string; handler: EventListener }> = [];
|
|
72
|
+
const addL = (el: HTMLElement, type: string, handler: EventListener) => {
|
|
73
|
+
el.addEventListener(type, handler);
|
|
74
|
+
listeners.push({ el, type, handler });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (cards.length === 0) {
|
|
78
|
+
const p = document.createElement('p');
|
|
79
|
+
p.className = 'text-text2 text-sm';
|
|
80
|
+
p.textContent = spec.emptyMessage ?? 'No items';
|
|
81
|
+
root.appendChild(p);
|
|
82
|
+
} else {
|
|
83
|
+
const grid = document.createElement('div');
|
|
84
|
+
grid.className = 'grid';
|
|
85
|
+
const minW = spec.minCardWidth ?? '180px';
|
|
86
|
+
const gap = spec.gap ?? '1rem';
|
|
87
|
+
grid.setAttribute('style', `grid-template-columns: repeat(auto-fill, minmax(${minW}, 1fr)); gap: ${gap};`);
|
|
88
|
+
|
|
89
|
+
for (const card of cards) {
|
|
90
|
+
const cardEl = document.createElement('div');
|
|
91
|
+
cardEl.className =
|
|
92
|
+
'bg-surface2 border border-border rounded-lg overflow-hidden hover:border-border2 transition-all' +
|
|
93
|
+
(interactive ? ' cursor-pointer' : '');
|
|
94
|
+
if (interactive) {
|
|
95
|
+
cardEl.setAttribute('role', 'button');
|
|
96
|
+
cardEl.setAttribute('tabindex', '0');
|
|
97
|
+
cardEl.setAttribute('title', 'Double-cliquez pour interagir');
|
|
98
|
+
|
|
99
|
+
const dispatch = () => {
|
|
100
|
+
container.dispatchEvent(
|
|
101
|
+
new CustomEvent('widget:interact', {
|
|
102
|
+
detail: { action: 'cardclick', payload: card },
|
|
103
|
+
bubbles: true,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
addL(cardEl, 'dblclick', () => dispatch());
|
|
109
|
+
addL(cardEl, 'keydown', (ev: Event) => {
|
|
110
|
+
const ke = ev as KeyboardEvent;
|
|
111
|
+
if (ke.key === 'Enter' || ke.key === ' ') {
|
|
112
|
+
ke.preventDefault();
|
|
113
|
+
dispatch();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (card.image) {
|
|
119
|
+
const img = createSafeImage({
|
|
120
|
+
src: card.image,
|
|
121
|
+
alt: card.title,
|
|
122
|
+
className: 'w-full h-32 object-cover',
|
|
123
|
+
hideOnError: true,
|
|
124
|
+
});
|
|
125
|
+
cardEl.appendChild(img);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const body = document.createElement('div');
|
|
129
|
+
body.className = 'p-3';
|
|
130
|
+
|
|
131
|
+
const titleEl = document.createElement('div');
|
|
132
|
+
titleEl.className = 'font-semibold text-sm text-text1 leading-tight';
|
|
133
|
+
titleEl.textContent = card.title;
|
|
134
|
+
body.appendChild(titleEl);
|
|
135
|
+
|
|
136
|
+
if (card.subtitle) {
|
|
137
|
+
const sub = document.createElement('div');
|
|
138
|
+
sub.className = 'text-xs text-text2 mt-0.5';
|
|
139
|
+
sub.textContent = card.subtitle;
|
|
140
|
+
body.appendChild(sub);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (card.description) {
|
|
144
|
+
const desc = document.createElement('div');
|
|
145
|
+
desc.className = 'text-xs text-text2 mt-1.5 leading-relaxed';
|
|
146
|
+
desc.textContent = card.description;
|
|
147
|
+
body.appendChild(desc);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(card.tags) && card.tags.length) {
|
|
151
|
+
const tagsWrap = document.createElement('div');
|
|
152
|
+
tagsWrap.className = 'flex gap-1 flex-wrap mt-2';
|
|
153
|
+
for (const tag of card.tags) {
|
|
154
|
+
const span = document.createElement('span');
|
|
155
|
+
span.className = 'text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface2 text-text2';
|
|
156
|
+
span.textContent = String(tag);
|
|
157
|
+
tagsWrap.appendChild(span);
|
|
158
|
+
}
|
|
159
|
+
body.appendChild(tagsWrap);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cardEl.appendChild(body);
|
|
163
|
+
grid.appendChild(cardEl);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
root.appendChild(grid);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
container.appendChild(root);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
for (const { el, type, handler } of listeners) {
|
|
173
|
+
el.removeEventListener(type, handler);
|
|
174
|
+
}
|
|
175
|
+
listeners.length = 0;
|
|
176
|
+
container.innerHTML = '';
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Silence unused warning on some toolchains
|
|
181
|
+
void escapeHtml;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carousel (vanilla) — slides with prev/next buttons, dots, auto-advance and swipe.
|
|
3
|
+
* Mirrors Carousel.svelte. Replaces $effect/setInterval with a closure-managed timer.
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* render(container, data) -> cleanup()
|
|
7
|
+
* Payload shape:
|
|
8
|
+
* { spec: Partial<CarouselSpec>, data?: unknown, onslidechange?: (slide, index) => void }
|
|
9
|
+
* Event:
|
|
10
|
+
* 'widget:interact' { action: 'slidechange', payload: { slide, index } }
|
|
11
|
+
*
|
|
12
|
+
* Cleanup: clears the auto-advance interval, removes all listeners and empties the container.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createSafeImage } from '../helpers/safe-image.js';
|
|
16
|
+
|
|
17
|
+
export interface CarouselSlide {
|
|
18
|
+
src?: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CarouselSpec {
|
|
25
|
+
title?: string;
|
|
26
|
+
slides?: CarouselSlide[];
|
|
27
|
+
autoPlay?: boolean;
|
|
28
|
+
interval?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CarouselPayload {
|
|
32
|
+
spec?: Partial<CarouselSpec>;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
onslidechange?: (slide: CarouselSlide, index: number) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveSlides(spec: Partial<CarouselSpec>, data: unknown): CarouselSlide[] {
|
|
38
|
+
if (Array.isArray(spec.slides) && spec.slides.length) return spec.slides as CarouselSlide[];
|
|
39
|
+
if (Array.isArray(data)) return data as CarouselSlide[];
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function render(container: HTMLElement, data: any): () => void {
|
|
44
|
+
const payload: CarouselPayload = (data && typeof data === 'object' ? data : {}) as CarouselPayload;
|
|
45
|
+
const spec: Partial<CarouselSpec> = payload.spec ?? {};
|
|
46
|
+
const inner = payload.data;
|
|
47
|
+
const onslidechange = typeof payload.onslidechange === 'function' ? payload.onslidechange : undefined;
|
|
48
|
+
const slides = resolveSlides(spec, inner);
|
|
49
|
+
const autoPlay = spec.autoPlay !== false;
|
|
50
|
+
const intervalMs = spec.interval ?? 5000;
|
|
51
|
+
|
|
52
|
+
// --- Root ---
|
|
53
|
+
const root = document.createElement('div');
|
|
54
|
+
root.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
|
|
55
|
+
|
|
56
|
+
if (spec.title) {
|
|
57
|
+
const h3 = document.createElement('h3');
|
|
58
|
+
h3.className = 'text-sm font-semibold text-text1 mb-3';
|
|
59
|
+
h3.textContent = spec.title;
|
|
60
|
+
root.appendChild(h3);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Tracked listeners for robust cleanup ---
|
|
64
|
+
const listeners: Array<{ el: EventTarget; type: string; handler: EventListener; opts?: AddEventListenerOptions }> = [];
|
|
65
|
+
const addL = (el: EventTarget, type: string, handler: EventListener, opts?: AddEventListenerOptions) => {
|
|
66
|
+
el.addEventListener(type, handler, opts);
|
|
67
|
+
listeners.push({ el, type, handler, opts });
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// --- Auto-advance timer (closure-managed, replaces $effect) ---
|
|
71
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
72
|
+
|
|
73
|
+
const clearTimer = () => {
|
|
74
|
+
if (timer !== null) {
|
|
75
|
+
clearInterval(timer);
|
|
76
|
+
timer = null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Empty state — no slides, nothing else to wire
|
|
81
|
+
if (slides.length === 0) {
|
|
82
|
+
const p = document.createElement('p');
|
|
83
|
+
p.className = 'text-text2 text-sm';
|
|
84
|
+
p.textContent = 'Aucun contenu';
|
|
85
|
+
root.appendChild(p);
|
|
86
|
+
container.appendChild(root);
|
|
87
|
+
return () => {
|
|
88
|
+
container.innerHTML = '';
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Current index via closure ---
|
|
93
|
+
let currentIdx = 0;
|
|
94
|
+
|
|
95
|
+
// --- Viewport ---
|
|
96
|
+
const viewport = document.createElement('div');
|
|
97
|
+
viewport.className = 'relative overflow-hidden rounded-lg';
|
|
98
|
+
viewport.setAttribute('role', 'region');
|
|
99
|
+
viewport.setAttribute('aria-roledescription', 'carousel');
|
|
100
|
+
viewport.setAttribute('aria-label', spec.title ?? 'Carousel');
|
|
101
|
+
viewport.setAttribute('tabindex', '0');
|
|
102
|
+
|
|
103
|
+
// --- Track (flex row translated on change) ---
|
|
104
|
+
const track = document.createElement('div');
|
|
105
|
+
track.className = 'flex transition-transform duration-300 ease-out';
|
|
106
|
+
track.style.transform = 'translateX(0%)';
|
|
107
|
+
|
|
108
|
+
const slideEls: HTMLElement[] = [];
|
|
109
|
+
slides.forEach((slide, i) => {
|
|
110
|
+
const slideEl = document.createElement('div');
|
|
111
|
+
slideEl.className = 'w-full flex-shrink-0';
|
|
112
|
+
slideEl.setAttribute('role', 'group');
|
|
113
|
+
slideEl.setAttribute('aria-roledescription', 'slide');
|
|
114
|
+
slideEl.setAttribute('aria-label', `${i + 1} / ${slides.length}`);
|
|
115
|
+
slideEl.setAttribute('aria-current', i === 0 ? 'true' : 'false');
|
|
116
|
+
|
|
117
|
+
if (slide.src) {
|
|
118
|
+
const img = createSafeImage({
|
|
119
|
+
src: slide.src,
|
|
120
|
+
alt: slide.title ?? '',
|
|
121
|
+
className: 'w-full h-48 sm:h-64 object-cover',
|
|
122
|
+
loading: 'lazy',
|
|
123
|
+
});
|
|
124
|
+
slideEl.appendChild(img);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (slide.title || slide.subtitle || slide.content) {
|
|
128
|
+
const body = document.createElement('div');
|
|
129
|
+
body.className = 'p-3';
|
|
130
|
+
|
|
131
|
+
if (slide.title) {
|
|
132
|
+
const t = document.createElement('div');
|
|
133
|
+
t.className = 'font-semibold text-sm text-text1';
|
|
134
|
+
t.textContent = slide.title;
|
|
135
|
+
body.appendChild(t);
|
|
136
|
+
}
|
|
137
|
+
if (slide.subtitle) {
|
|
138
|
+
const s = document.createElement('div');
|
|
139
|
+
s.className = 'text-xs text-text2 mt-0.5';
|
|
140
|
+
s.textContent = slide.subtitle;
|
|
141
|
+
body.appendChild(s);
|
|
142
|
+
}
|
|
143
|
+
if (slide.content) {
|
|
144
|
+
const c = document.createElement('div');
|
|
145
|
+
c.className = 'text-xs text-text2 mt-1.5';
|
|
146
|
+
c.textContent = slide.content;
|
|
147
|
+
body.appendChild(c);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
slideEl.appendChild(body);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
track.appendChild(slideEl);
|
|
154
|
+
slideEls.push(slideEl);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
viewport.appendChild(track);
|
|
158
|
+
|
|
159
|
+
// --- Dots container (built up front, updated on change) ---
|
|
160
|
+
const dots: HTMLButtonElement[] = [];
|
|
161
|
+
let dotsWrap: HTMLDivElement | null = null;
|
|
162
|
+
if (slides.length > 1) {
|
|
163
|
+
dotsWrap = document.createElement('div');
|
|
164
|
+
dotsWrap.className = 'flex justify-center gap-1.5 mt-2';
|
|
165
|
+
slides.forEach((_, i) => {
|
|
166
|
+
const dot = document.createElement('button');
|
|
167
|
+
dot.type = 'button';
|
|
168
|
+
dot.className = `w-2 h-2 rounded-full transition-colors ${i === 0 ? 'bg-accent' : 'bg-border2'}`;
|
|
169
|
+
dot.setAttribute('aria-label', `Aller à la diapositive ${i + 1}`);
|
|
170
|
+
dot.setAttribute('aria-current', i === 0 ? 'true' : 'false');
|
|
171
|
+
dots.push(dot);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Render state (DOM updates) ---
|
|
176
|
+
const renderState = () => {
|
|
177
|
+
track.style.transform = `translateX(-${currentIdx * 100}%)`;
|
|
178
|
+
slideEls.forEach((el, i) => {
|
|
179
|
+
el.setAttribute('aria-current', i === currentIdx ? 'true' : 'false');
|
|
180
|
+
});
|
|
181
|
+
dots.forEach((dot, i) => {
|
|
182
|
+
const active = i === currentIdx;
|
|
183
|
+
dot.className = `w-2 h-2 rounded-full transition-colors ${active ? 'bg-accent' : 'bg-border2'}`;
|
|
184
|
+
dot.setAttribute('aria-current', active ? 'true' : 'false');
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// --- Navigation ---
|
|
189
|
+
const resetAuto = () => {
|
|
190
|
+
clearTimer();
|
|
191
|
+
if (autoPlay && slides.length > 1) {
|
|
192
|
+
timer = setInterval(() => {
|
|
193
|
+
// internal tick — not user-initiated
|
|
194
|
+
goTo(currentIdx < slides.length - 1 ? currentIdx + 1 : 0, false);
|
|
195
|
+
}, intervalMs);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const goTo = (i: number, userInitiated: boolean) => {
|
|
200
|
+
const next = Math.max(0, Math.min(i, slides.length - 1));
|
|
201
|
+
if (next === currentIdx && !userInitiated) {
|
|
202
|
+
// still reset timer to avoid drift
|
|
203
|
+
resetAuto();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
currentIdx = next;
|
|
207
|
+
renderState();
|
|
208
|
+
if (userInitiated) {
|
|
209
|
+
const slide = slides[currentIdx];
|
|
210
|
+
onslidechange?.(slide, currentIdx);
|
|
211
|
+
container.dispatchEvent(
|
|
212
|
+
new CustomEvent('widget:interact', {
|
|
213
|
+
detail: { action: 'slidechange', payload: { slide, index: currentIdx } },
|
|
214
|
+
bubbles: true,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
resetAuto();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const prev = (userInitiated: boolean) => {
|
|
222
|
+
goTo(currentIdx > 0 ? currentIdx - 1 : slides.length - 1, userInitiated);
|
|
223
|
+
};
|
|
224
|
+
const next = (userInitiated: boolean) => {
|
|
225
|
+
goTo(currentIdx < slides.length - 1 ? currentIdx + 1 : 0, userInitiated);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// --- Prev/Next buttons ---
|
|
229
|
+
if (slides.length > 1) {
|
|
230
|
+
const prevBtn = document.createElement('button');
|
|
231
|
+
prevBtn.type = 'button';
|
|
232
|
+
prevBtn.className =
|
|
233
|
+
'absolute left-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/40 text-white flex items-center justify-center hover:bg-black/60 text-sm';
|
|
234
|
+
prevBtn.setAttribute('aria-label', 'Diapositive précédente');
|
|
235
|
+
prevBtn.innerHTML = '‹';
|
|
236
|
+
addL(prevBtn, 'click', () => prev(true));
|
|
237
|
+
|
|
238
|
+
const nextBtn = document.createElement('button');
|
|
239
|
+
nextBtn.type = 'button';
|
|
240
|
+
nextBtn.className =
|
|
241
|
+
'absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/40 text-white flex items-center justify-center hover:bg-black/60 text-sm';
|
|
242
|
+
nextBtn.setAttribute('aria-label', 'Diapositive suivante');
|
|
243
|
+
nextBtn.innerHTML = '›';
|
|
244
|
+
addL(nextBtn, 'click', () => next(true));
|
|
245
|
+
|
|
246
|
+
viewport.appendChild(prevBtn);
|
|
247
|
+
viewport.appendChild(nextBtn);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Touch/swipe ---
|
|
251
|
+
let touchStartX = 0;
|
|
252
|
+
addL(
|
|
253
|
+
viewport,
|
|
254
|
+
'touchstart',
|
|
255
|
+
(e) => {
|
|
256
|
+
const te = e as TouchEvent;
|
|
257
|
+
touchStartX = te.touches[0]?.clientX ?? 0;
|
|
258
|
+
},
|
|
259
|
+
{ passive: true },
|
|
260
|
+
);
|
|
261
|
+
addL(
|
|
262
|
+
viewport,
|
|
263
|
+
'touchend',
|
|
264
|
+
(e) => {
|
|
265
|
+
const te = e as TouchEvent;
|
|
266
|
+
const endX = te.changedTouches[0]?.clientX ?? 0;
|
|
267
|
+
const diff = touchStartX - endX;
|
|
268
|
+
if (Math.abs(diff) > 50) {
|
|
269
|
+
if (diff > 0) next(true);
|
|
270
|
+
else prev(true);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{ passive: true },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// --- Keyboard arrows ---
|
|
277
|
+
addL(viewport, 'keydown', (e) => {
|
|
278
|
+
const ke = e as KeyboardEvent;
|
|
279
|
+
if (ke.key === 'ArrowLeft') {
|
|
280
|
+
ke.preventDefault();
|
|
281
|
+
prev(true);
|
|
282
|
+
} else if (ke.key === 'ArrowRight') {
|
|
283
|
+
ke.preventDefault();
|
|
284
|
+
next(true);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// --- Pause auto-advance on hover / focus for accessibility ---
|
|
289
|
+
addL(viewport, 'mouseenter', () => clearTimer());
|
|
290
|
+
addL(viewport, 'mouseleave', () => resetAuto());
|
|
291
|
+
addL(viewport, 'focusin', () => clearTimer());
|
|
292
|
+
addL(viewport, 'focusout', () => resetAuto());
|
|
293
|
+
|
|
294
|
+
// --- Wire dots ---
|
|
295
|
+
if (dotsWrap) {
|
|
296
|
+
dots.forEach((dot, i) => {
|
|
297
|
+
addL(dot, 'click', () => goTo(i, true));
|
|
298
|
+
dotsWrap!.appendChild(dot);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- Mount ---
|
|
303
|
+
root.appendChild(viewport);
|
|
304
|
+
if (dotsWrap) root.appendChild(dotsWrap);
|
|
305
|
+
container.appendChild(root);
|
|
306
|
+
|
|
307
|
+
// --- Start auto-advance ---
|
|
308
|
+
resetAuto();
|
|
309
|
+
|
|
310
|
+
// --- Cleanup ---
|
|
311
|
+
return () => {
|
|
312
|
+
clearTimer();
|
|
313
|
+
for (const { el, type, handler, opts } of listeners) {
|
|
314
|
+
el.removeEventListener(type, handler, opts);
|
|
315
|
+
}
|
|
316
|
+
listeners.length = 0;
|
|
317
|
+
container.innerHTML = '';
|
|
318
|
+
};
|
|
319
|
+
}
|