@runtypelabs/persona 3.2.2 → 3.3.0
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/dist/index.cjs +281 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.global.js +321 -80
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +277 -36
- package/dist/index.js.map +1 -1
- package/dist/widget.css +62 -0
- package/package.json +1 -1
- package/src/components/composer-builder.ts +20 -4
- package/src/components/demo-carousel.ts +699 -0
- package/src/components/header-builder.ts +1 -1
- package/src/index.ts +8 -0
- package/src/styles/widget.css +62 -0
- package/src/types.ts +4 -0
- package/src/ui.ts +31 -14
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import { createIconButton } from "../utils/buttons";
|
|
3
|
+
import { createToggleGroup } from "../utils/buttons";
|
|
4
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface DemoCarouselItem {
|
|
11
|
+
/** URL to load in the iframe (relative or absolute). */
|
|
12
|
+
url: string;
|
|
13
|
+
/** Display title shown in the toolbar. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Optional subtitle/description. */
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DemoCarouselOptions {
|
|
20
|
+
/** Demo pages to cycle through. */
|
|
21
|
+
items: DemoCarouselItem[];
|
|
22
|
+
/** Initial item index. Default: 0. */
|
|
23
|
+
initialIndex?: number;
|
|
24
|
+
/** Initial device viewport. Default: 'desktop'. */
|
|
25
|
+
initialDevice?: "desktop" | "mobile";
|
|
26
|
+
/** Initial color scheme for the iframe wrapper. Default: 'light'. */
|
|
27
|
+
initialColorScheme?: "light" | "dark";
|
|
28
|
+
/** Show zoom +/- controls. Default: true. */
|
|
29
|
+
showZoomControls?: boolean;
|
|
30
|
+
/** Show desktop/mobile toggle. Default: true. */
|
|
31
|
+
showDeviceToggle?: boolean;
|
|
32
|
+
/** Show light/dark scheme toggle. Default: true. */
|
|
33
|
+
showColorSchemeToggle?: boolean;
|
|
34
|
+
/** Called when the active demo changes. */
|
|
35
|
+
onChange?: (index: number, item: DemoCarouselItem) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DemoCarouselHandle {
|
|
39
|
+
/** Root element (already appended to the container). */
|
|
40
|
+
element: HTMLElement;
|
|
41
|
+
/** Navigate to a demo by index. */
|
|
42
|
+
goTo(index: number): void;
|
|
43
|
+
/** Go to the next demo. */
|
|
44
|
+
next(): void;
|
|
45
|
+
/** Go to the previous demo. */
|
|
46
|
+
prev(): void;
|
|
47
|
+
/** Current demo index. */
|
|
48
|
+
getIndex(): number;
|
|
49
|
+
/** Change the device viewport. */
|
|
50
|
+
setDevice(device: "desktop" | "mobile"): void;
|
|
51
|
+
/** Change the wrapper color scheme. */
|
|
52
|
+
setColorScheme(scheme: "light" | "dark"): void;
|
|
53
|
+
/** Override zoom level (null = auto-fit). */
|
|
54
|
+
setZoom(zoom: number | null): void;
|
|
55
|
+
/** Tear down listeners, observer, and DOM. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Constants
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const DEVICE_DIMENSIONS: Record<string, { w: number; h: number }> = {
|
|
64
|
+
desktop: { w: 1280, h: 800 },
|
|
65
|
+
mobile: { w: 390, h: 844 },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ZOOM_STEP = 0.1;
|
|
69
|
+
const ZOOM_MIN = 0.15;
|
|
70
|
+
const ZOOM_MAX = 1.5;
|
|
71
|
+
const STAGE_PADDING = 24;
|
|
72
|
+
const SHADOW_MARGIN = 40;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Injected CSS (self-contained, prefixed persona-dc-)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const CAROUSEL_CSS = /* css */ `
|
|
79
|
+
/* ── Root ── */
|
|
80
|
+
.persona-dc-root {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
width: 100%;
|
|
84
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
85
|
+
font-size: 14px;
|
|
86
|
+
color: #111827;
|
|
87
|
+
line-height: 1.4;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ── Toolbar ── */
|
|
91
|
+
.persona-dc-toolbar {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: 12px;
|
|
96
|
+
padding: 8px 12px;
|
|
97
|
+
background: #fff;
|
|
98
|
+
border: 1px solid #e5e7eb;
|
|
99
|
+
border-bottom: none;
|
|
100
|
+
border-radius: 10px 10px 0 0;
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
}
|
|
103
|
+
.persona-dc-toolbar-lead {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 6px;
|
|
107
|
+
min-width: 0;
|
|
108
|
+
}
|
|
109
|
+
.persona-dc-toolbar-trail {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 8px;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.persona-dc-title-btn {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 4px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
font-size: 13px;
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
max-width: 240px;
|
|
124
|
+
background: none;
|
|
125
|
+
border: 1px solid transparent;
|
|
126
|
+
border-radius: 0.375rem;
|
|
127
|
+
padding: 4px 6px;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
color: inherit;
|
|
130
|
+
font-family: inherit;
|
|
131
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
132
|
+
}
|
|
133
|
+
.persona-dc-title-btn:hover {
|
|
134
|
+
background: #f3f4f6;
|
|
135
|
+
border-color: #e5e7eb;
|
|
136
|
+
}
|
|
137
|
+
.persona-dc-title-btn .persona-dc-title-text {
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
text-overflow: ellipsis;
|
|
140
|
+
}
|
|
141
|
+
.persona-dc-title-btn .persona-dc-title-chevron {
|
|
142
|
+
flex-shrink: 0;
|
|
143
|
+
transition: transform 0.15s ease;
|
|
144
|
+
}
|
|
145
|
+
.persona-dc-title-btn[aria-expanded="true"] .persona-dc-title-chevron {
|
|
146
|
+
transform: rotate(180deg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Title dropdown ── */
|
|
150
|
+
.persona-dc-dropdown {
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 100%;
|
|
153
|
+
left: 0;
|
|
154
|
+
margin-top: 4px;
|
|
155
|
+
min-width: 220px;
|
|
156
|
+
max-width: 320px;
|
|
157
|
+
background: #fff;
|
|
158
|
+
border: 1px solid #e5e7eb;
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06);
|
|
161
|
+
padding: 4px;
|
|
162
|
+
z-index: 100;
|
|
163
|
+
max-height: 300px;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
}
|
|
166
|
+
.persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
align-items: flex-start;
|
|
170
|
+
justify-content: flex-start;
|
|
171
|
+
width: 100%;
|
|
172
|
+
padding: 8px 10px;
|
|
173
|
+
border: none;
|
|
174
|
+
background: none;
|
|
175
|
+
border-radius: 6px;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
text-align: left;
|
|
178
|
+
font-family: inherit;
|
|
179
|
+
font-size: 13px;
|
|
180
|
+
font-weight: 500;
|
|
181
|
+
color: #111827;
|
|
182
|
+
transition: background-color 0.1s ease;
|
|
183
|
+
}
|
|
184
|
+
.persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item:hover {
|
|
185
|
+
background: #f3f4f6;
|
|
186
|
+
}
|
|
187
|
+
.persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item[aria-current="true"] {
|
|
188
|
+
background: #eff6ff;
|
|
189
|
+
color: #2563eb;
|
|
190
|
+
}
|
|
191
|
+
.persona-dc-root .persona-dc-dropdown-desc {
|
|
192
|
+
font-weight: 400;
|
|
193
|
+
font-size: 12px;
|
|
194
|
+
color: #6b7280;
|
|
195
|
+
margin-top: 1px;
|
|
196
|
+
text-align: left;
|
|
197
|
+
}
|
|
198
|
+
.persona-dc-root .persona-dc-dropdown button.persona-dc-dropdown-item[aria-current="true"] .persona-dc-dropdown-desc {
|
|
199
|
+
color: #60a5fa;
|
|
200
|
+
}
|
|
201
|
+
.persona-dc-counter {
|
|
202
|
+
font-size: 12px;
|
|
203
|
+
color: #6b7280;
|
|
204
|
+
white-space: nowrap;
|
|
205
|
+
}
|
|
206
|
+
.persona-dc-zoom-controls {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 2px;
|
|
210
|
+
}
|
|
211
|
+
.persona-dc-zoom-level {
|
|
212
|
+
font-size: 12px;
|
|
213
|
+
color: #6b7280;
|
|
214
|
+
min-width: 36px;
|
|
215
|
+
text-align: center;
|
|
216
|
+
user-select: none;
|
|
217
|
+
}
|
|
218
|
+
.persona-dc-separator {
|
|
219
|
+
width: 1px;
|
|
220
|
+
height: 20px;
|
|
221
|
+
background: #e5e7eb;
|
|
222
|
+
flex-shrink: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ── Stage ── */
|
|
226
|
+
.persona-dc-stage {
|
|
227
|
+
height: 550px;
|
|
228
|
+
min-height: 400px;
|
|
229
|
+
padding: ${STAGE_PADDING}px;
|
|
230
|
+
overflow: auto;
|
|
231
|
+
background: #f0f1f3;
|
|
232
|
+
background-image: radial-gradient(circle, #e0e1e5 1px, transparent 1px);
|
|
233
|
+
background-size: 24px 24px;
|
|
234
|
+
border: 1px solid #e5e7eb;
|
|
235
|
+
border-radius: 0 0 10px 10px;
|
|
236
|
+
display: flex;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ── Iframe wrapper ── */
|
|
240
|
+
.persona-dc-iframe-wrapper {
|
|
241
|
+
position: relative;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
background: #fff;
|
|
244
|
+
border-radius: 10px;
|
|
245
|
+
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
246
|
+
margin: auto;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
transition: border-radius 0.2s ease;
|
|
249
|
+
}
|
|
250
|
+
.persona-dc-iframe-wrapper[data-color-scheme="dark"] {
|
|
251
|
+
background: #0f172a;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Iframe ── */
|
|
255
|
+
.persona-dc-iframe {
|
|
256
|
+
border: none;
|
|
257
|
+
display: block;
|
|
258
|
+
background: #fff;
|
|
259
|
+
transform-origin: top left;
|
|
260
|
+
}
|
|
261
|
+
.persona-dc-iframe-wrapper[data-color-scheme="dark"] .persona-dc-iframe {
|
|
262
|
+
background: #0f172a;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ── Button/toggle base styles (standalone, no widget.css dependency) ── */
|
|
266
|
+
.persona-dc-root .persona-icon-btn {
|
|
267
|
+
display: inline-flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
justify-content: center;
|
|
270
|
+
padding: 0.25rem;
|
|
271
|
+
border-radius: 0.375rem;
|
|
272
|
+
border: 1px solid #e5e7eb;
|
|
273
|
+
background: #ffffff;
|
|
274
|
+
color: #111827;
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
line-height: 1;
|
|
277
|
+
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
278
|
+
}
|
|
279
|
+
.persona-dc-root .persona-icon-btn:hover {
|
|
280
|
+
background: #f3f4f6;
|
|
281
|
+
}
|
|
282
|
+
.persona-dc-root .persona-icon-btn:focus-visible {
|
|
283
|
+
outline: 2px solid #3b82f6;
|
|
284
|
+
outline-offset: 2px;
|
|
285
|
+
}
|
|
286
|
+
.persona-dc-root .persona-icon-btn[aria-pressed="true"] {
|
|
287
|
+
background: #f3f4f6;
|
|
288
|
+
border-color: #d1d5db;
|
|
289
|
+
}
|
|
290
|
+
.persona-dc-root .persona-toggle-group {
|
|
291
|
+
display: inline-flex;
|
|
292
|
+
gap: 0;
|
|
293
|
+
}
|
|
294
|
+
.persona-dc-root .persona-toggle-group > .persona-icon-btn {
|
|
295
|
+
border-radius: 0;
|
|
296
|
+
}
|
|
297
|
+
.persona-dc-root .persona-toggle-group > .persona-icon-btn:first-child {
|
|
298
|
+
border-top-left-radius: 0.375rem;
|
|
299
|
+
border-bottom-left-radius: 0.375rem;
|
|
300
|
+
}
|
|
301
|
+
.persona-dc-root .persona-toggle-group > .persona-icon-btn:last-child {
|
|
302
|
+
border-top-right-radius: 0.375rem;
|
|
303
|
+
border-bottom-right-radius: 0.375rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ── Responsive ── */
|
|
307
|
+
@media (max-width: 640px) {
|
|
308
|
+
.persona-dc-toolbar {
|
|
309
|
+
gap: 8px;
|
|
310
|
+
}
|
|
311
|
+
.persona-dc-zoom-controls {
|
|
312
|
+
display: none;
|
|
313
|
+
}
|
|
314
|
+
.persona-dc-stage {
|
|
315
|
+
height: 400px;
|
|
316
|
+
min-height: 300px;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
function injectStyles(): void {
|
|
322
|
+
if (document.querySelector("style[data-persona-dc-styles]")) return;
|
|
323
|
+
const style = document.createElement("style");
|
|
324
|
+
style.setAttribute("data-persona-dc-styles", "");
|
|
325
|
+
style.textContent = CAROUSEL_CSS;
|
|
326
|
+
document.head.appendChild(style);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Scale helpers (ported from theme editor)
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
function computeFitScale(
|
|
334
|
+
stage: HTMLElement,
|
|
335
|
+
dims: { w: number; h: number },
|
|
336
|
+
): number {
|
|
337
|
+
const availW = stage.clientWidth - STAGE_PADDING * 2 - SHADOW_MARGIN;
|
|
338
|
+
const availH = stage.clientHeight - STAGE_PADDING * 2 - SHADOW_MARGIN;
|
|
339
|
+
if (availW <= 0 || availH <= 0) return 1;
|
|
340
|
+
return Math.min(availW / dims.w, availH / dims.h, 1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function applyScale(
|
|
344
|
+
wrapper: HTMLElement,
|
|
345
|
+
iframe: HTMLIFrameElement,
|
|
346
|
+
dims: { w: number; h: number },
|
|
347
|
+
scale: number,
|
|
348
|
+
device: string,
|
|
349
|
+
): void {
|
|
350
|
+
wrapper.style.width = `${dims.w * scale}px`;
|
|
351
|
+
wrapper.style.height = `${dims.h * scale}px`;
|
|
352
|
+
wrapper.style.borderRadius =
|
|
353
|
+
device === "mobile" ? `${32 * scale}px` : "10px";
|
|
354
|
+
|
|
355
|
+
iframe.style.width = `${dims.w}px`;
|
|
356
|
+
iframe.style.height = `${dims.h}px`;
|
|
357
|
+
iframe.style.transformOrigin = "top left";
|
|
358
|
+
iframe.style.transform = `scale(${scale})`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Factory
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
export function createDemoCarousel(
|
|
366
|
+
container: HTMLElement,
|
|
367
|
+
options: DemoCarouselOptions,
|
|
368
|
+
): DemoCarouselHandle {
|
|
369
|
+
const {
|
|
370
|
+
items,
|
|
371
|
+
initialIndex = 0,
|
|
372
|
+
initialDevice = "desktop",
|
|
373
|
+
initialColorScheme = "light",
|
|
374
|
+
showZoomControls = true,
|
|
375
|
+
showDeviceToggle = true,
|
|
376
|
+
showColorSchemeToggle = true,
|
|
377
|
+
onChange,
|
|
378
|
+
} = options;
|
|
379
|
+
|
|
380
|
+
if (items.length === 0) {
|
|
381
|
+
throw new Error("createDemoCarousel: items array must not be empty");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
injectStyles();
|
|
385
|
+
|
|
386
|
+
// ── State ──
|
|
387
|
+
let currentIndex = Math.max(0, Math.min(initialIndex, items.length - 1));
|
|
388
|
+
let currentDevice = initialDevice;
|
|
389
|
+
let currentScheme = initialColorScheme;
|
|
390
|
+
let zoomOverride: number | null = null;
|
|
391
|
+
let lastAutoScale = 1;
|
|
392
|
+
let destroyed = false;
|
|
393
|
+
|
|
394
|
+
// ── DOM ──
|
|
395
|
+
const root = createElement("div", "persona-dc-root");
|
|
396
|
+
|
|
397
|
+
// Toolbar
|
|
398
|
+
const toolbar = createElement("div", "persona-dc-toolbar");
|
|
399
|
+
const toolbarLead = createElement("div", "persona-dc-toolbar-lead");
|
|
400
|
+
const toolbarTrail = createElement("div", "persona-dc-toolbar-trail");
|
|
401
|
+
|
|
402
|
+
// Prev / title / next / counter
|
|
403
|
+
const prevBtn = createIconButton({
|
|
404
|
+
icon: "chevron-left",
|
|
405
|
+
label: "Previous demo",
|
|
406
|
+
size: 14,
|
|
407
|
+
onClick: () => navigate(-1),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Title button with dropdown
|
|
411
|
+
const titleWrap = createElement("div");
|
|
412
|
+
titleWrap.style.position = "relative";
|
|
413
|
+
|
|
414
|
+
const titleBtn = createElement("button", "persona-dc-title-btn");
|
|
415
|
+
titleBtn.type = "button";
|
|
416
|
+
titleBtn.setAttribute("aria-expanded", "false");
|
|
417
|
+
titleBtn.setAttribute("aria-haspopup", "listbox");
|
|
418
|
+
const titleText = createElement("span", "persona-dc-title-text");
|
|
419
|
+
const titleChevron = createElement("span", "persona-dc-title-chevron");
|
|
420
|
+
const chevronSvg = renderLucideIcon("chevron-down", 12, "currentColor", 2);
|
|
421
|
+
if (chevronSvg) titleChevron.appendChild(chevronSvg);
|
|
422
|
+
titleBtn.append(titleText, titleChevron);
|
|
423
|
+
|
|
424
|
+
// Dropdown list
|
|
425
|
+
const dropdown = createElement("div", "persona-dc-dropdown");
|
|
426
|
+
dropdown.setAttribute("role", "listbox");
|
|
427
|
+
dropdown.style.display = "none";
|
|
428
|
+
let dropdownOpen = false;
|
|
429
|
+
|
|
430
|
+
function buildDropdownItems(): void {
|
|
431
|
+
dropdown.innerHTML = "";
|
|
432
|
+
for (let i = 0; i < items.length; i++) {
|
|
433
|
+
const item = items[i];
|
|
434
|
+
const btn = createElement("button", "persona-dc-dropdown-item");
|
|
435
|
+
btn.type = "button";
|
|
436
|
+
btn.setAttribute("role", "option");
|
|
437
|
+
btn.setAttribute("aria-current", i === currentIndex ? "true" : "false");
|
|
438
|
+
const titleSpan = createElement("span");
|
|
439
|
+
titleSpan.textContent = item.title;
|
|
440
|
+
btn.appendChild(titleSpan);
|
|
441
|
+
if (item.description) {
|
|
442
|
+
const desc = createElement("span", "persona-dc-dropdown-desc");
|
|
443
|
+
desc.textContent = item.description;
|
|
444
|
+
btn.appendChild(desc);
|
|
445
|
+
}
|
|
446
|
+
btn.addEventListener("click", () => {
|
|
447
|
+
closeDropdown();
|
|
448
|
+
goTo(i);
|
|
449
|
+
});
|
|
450
|
+
dropdown.appendChild(btn);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function toggleDropdown(): void {
|
|
455
|
+
dropdownOpen = !dropdownOpen;
|
|
456
|
+
dropdown.style.display = dropdownOpen ? "" : "none";
|
|
457
|
+
titleBtn.setAttribute("aria-expanded", dropdownOpen ? "true" : "false");
|
|
458
|
+
if (dropdownOpen) buildDropdownItems();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function closeDropdown(): void {
|
|
462
|
+
if (!dropdownOpen) return;
|
|
463
|
+
dropdownOpen = false;
|
|
464
|
+
dropdown.style.display = "none";
|
|
465
|
+
titleBtn.setAttribute("aria-expanded", "false");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
titleBtn.addEventListener("click", (e) => {
|
|
469
|
+
e.stopPropagation();
|
|
470
|
+
toggleDropdown();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Close on outside click
|
|
474
|
+
const onDocClick = (): void => closeDropdown();
|
|
475
|
+
document.addEventListener("click", onDocClick);
|
|
476
|
+
|
|
477
|
+
titleWrap.append(titleBtn, dropdown);
|
|
478
|
+
|
|
479
|
+
const nextBtn = createIconButton({
|
|
480
|
+
icon: "chevron-right",
|
|
481
|
+
label: "Next demo",
|
|
482
|
+
size: 14,
|
|
483
|
+
onClick: () => navigate(1),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const counterEl = createElement("span", "persona-dc-counter");
|
|
487
|
+
|
|
488
|
+
toolbarLead.append(prevBtn, titleWrap, nextBtn, counterEl);
|
|
489
|
+
|
|
490
|
+
// Device toggle
|
|
491
|
+
let deviceToggle: ReturnType<typeof createToggleGroup> | null = null;
|
|
492
|
+
if (showDeviceToggle) {
|
|
493
|
+
deviceToggle = createToggleGroup({
|
|
494
|
+
items: [
|
|
495
|
+
{ id: "desktop", icon: "monitor", label: "Desktop" },
|
|
496
|
+
{ id: "mobile", icon: "smartphone", label: "Mobile" },
|
|
497
|
+
],
|
|
498
|
+
selectedId: currentDevice,
|
|
499
|
+
onSelect: (id) => {
|
|
500
|
+
currentDevice = id as "desktop" | "mobile";
|
|
501
|
+
wrapper.dataset.device = currentDevice;
|
|
502
|
+
zoomOverride = null;
|
|
503
|
+
rescale();
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
toolbarTrail.appendChild(deviceToggle.element);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Zoom controls
|
|
510
|
+
let zoomLevelEl: HTMLSpanElement | null = null;
|
|
511
|
+
if (showZoomControls) {
|
|
512
|
+
const zoomWrap = createElement("div", "persona-dc-zoom-controls");
|
|
513
|
+
const zoomOut = createIconButton({
|
|
514
|
+
icon: "minus",
|
|
515
|
+
label: "Zoom out",
|
|
516
|
+
size: 14,
|
|
517
|
+
onClick: () => {
|
|
518
|
+
const current = zoomOverride ?? lastAutoScale;
|
|
519
|
+
zoomOverride = Math.max(ZOOM_MIN, current - ZOOM_STEP);
|
|
520
|
+
rescale();
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
zoomLevelEl = createElement("span", "persona-dc-zoom-level");
|
|
524
|
+
zoomLevelEl.title = "Reset to 100%";
|
|
525
|
+
zoomLevelEl.style.cursor = "pointer";
|
|
526
|
+
zoomLevelEl.addEventListener("click", () => {
|
|
527
|
+
zoomOverride = 1;
|
|
528
|
+
rescale();
|
|
529
|
+
});
|
|
530
|
+
const zoomIn = createIconButton({
|
|
531
|
+
icon: "plus",
|
|
532
|
+
label: "Zoom in",
|
|
533
|
+
size: 14,
|
|
534
|
+
onClick: () => {
|
|
535
|
+
const current = zoomOverride ?? lastAutoScale;
|
|
536
|
+
zoomOverride = Math.min(ZOOM_MAX, current + ZOOM_STEP);
|
|
537
|
+
rescale();
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
const zoomFit = createIconButton({
|
|
541
|
+
icon: "maximize",
|
|
542
|
+
label: "Fit to view",
|
|
543
|
+
size: 14,
|
|
544
|
+
onClick: () => {
|
|
545
|
+
zoomOverride = null;
|
|
546
|
+
rescale();
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
zoomWrap.append(zoomOut, zoomLevelEl, zoomIn, zoomFit);
|
|
550
|
+
toolbarTrail.appendChild(zoomWrap);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Color scheme toggle
|
|
554
|
+
if (showColorSchemeToggle) {
|
|
555
|
+
const sep = createElement("div", "persona-dc-separator");
|
|
556
|
+
toolbarTrail.appendChild(sep);
|
|
557
|
+
const schemeToggle = createToggleGroup({
|
|
558
|
+
items: [
|
|
559
|
+
{ id: "light", icon: "sun", label: "Light" },
|
|
560
|
+
{ id: "dark", icon: "moon", label: "Dark" },
|
|
561
|
+
],
|
|
562
|
+
selectedId: currentScheme,
|
|
563
|
+
onSelect: (id) => {
|
|
564
|
+
currentScheme = id as "light" | "dark";
|
|
565
|
+
wrapper.dataset.colorScheme = currentScheme;
|
|
566
|
+
applySchemeToIframe();
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
toolbarTrail.appendChild(schemeToggle.element);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Open in new tab
|
|
573
|
+
const sep2 = createElement("div", "persona-dc-separator");
|
|
574
|
+
toolbarTrail.appendChild(sep2);
|
|
575
|
+
const openBtn = createIconButton({
|
|
576
|
+
icon: "external-link",
|
|
577
|
+
label: "Open in new tab",
|
|
578
|
+
size: 14,
|
|
579
|
+
onClick: () => {
|
|
580
|
+
window.open(items[currentIndex].url, "_blank");
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
toolbarTrail.appendChild(openBtn);
|
|
584
|
+
|
|
585
|
+
toolbar.append(toolbarLead, toolbarTrail);
|
|
586
|
+
|
|
587
|
+
// Stage + iframe
|
|
588
|
+
const stage = createElement("div", "persona-dc-stage");
|
|
589
|
+
const wrapper = createElement("div", "persona-dc-iframe-wrapper");
|
|
590
|
+
wrapper.dataset.device = currentDevice;
|
|
591
|
+
wrapper.dataset.colorScheme = currentScheme;
|
|
592
|
+
|
|
593
|
+
const iframe = createElement("iframe", "persona-dc-iframe");
|
|
594
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
595
|
+
iframe.setAttribute("loading", "lazy");
|
|
596
|
+
iframe.title = items[currentIndex].title;
|
|
597
|
+
|
|
598
|
+
wrapper.appendChild(iframe);
|
|
599
|
+
stage.appendChild(wrapper);
|
|
600
|
+
root.append(toolbar, stage);
|
|
601
|
+
container.appendChild(root);
|
|
602
|
+
|
|
603
|
+
// ── Logic ──
|
|
604
|
+
|
|
605
|
+
function applySchemeToIframe(): void {
|
|
606
|
+
try {
|
|
607
|
+
const body = iframe.contentDocument?.body;
|
|
608
|
+
if (!body) return;
|
|
609
|
+
if (currentScheme === "dark") {
|
|
610
|
+
body.classList.add("theme-dark");
|
|
611
|
+
} else {
|
|
612
|
+
body.classList.remove("theme-dark");
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
// Cross-origin iframe — silently ignore
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Re-apply scheme after iframe loads new content
|
|
620
|
+
iframe.addEventListener("load", () => applySchemeToIframe());
|
|
621
|
+
|
|
622
|
+
function updateDisplay(): void {
|
|
623
|
+
const item = items[currentIndex];
|
|
624
|
+
titleText.textContent = item.title;
|
|
625
|
+
counterEl.textContent = `${currentIndex + 1} / ${items.length}`;
|
|
626
|
+
iframe.title = item.title;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function navigate(delta: number): void {
|
|
630
|
+
const next = ((currentIndex + delta) % items.length + items.length) % items.length;
|
|
631
|
+
goTo(next);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function goTo(index: number): void {
|
|
635
|
+
if (index < 0 || index >= items.length) return;
|
|
636
|
+
currentIndex = index;
|
|
637
|
+
iframe.src = items[currentIndex].url;
|
|
638
|
+
updateDisplay();
|
|
639
|
+
onChange?.(currentIndex, items[currentIndex]);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function rescale(): void {
|
|
643
|
+
if (destroyed) return;
|
|
644
|
+
const dims = DEVICE_DIMENSIONS[currentDevice] ?? DEVICE_DIMENSIONS.desktop;
|
|
645
|
+
lastAutoScale = computeFitScale(stage, dims);
|
|
646
|
+
const scale = Math.max(
|
|
647
|
+
ZOOM_MIN,
|
|
648
|
+
Math.min(ZOOM_MAX, zoomOverride ?? lastAutoScale),
|
|
649
|
+
);
|
|
650
|
+
applyScale(wrapper, iframe, dims, scale, currentDevice);
|
|
651
|
+
if (zoomLevelEl) {
|
|
652
|
+
zoomLevelEl.textContent = `${Math.round(scale * 100)}%`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ResizeObserver
|
|
657
|
+
const resizeObserver = new ResizeObserver(() => rescale());
|
|
658
|
+
resizeObserver.observe(stage);
|
|
659
|
+
|
|
660
|
+
// Initial render
|
|
661
|
+
updateDisplay();
|
|
662
|
+
iframe.src = items[currentIndex].url;
|
|
663
|
+
// Defer initial scale to next frame so stage has layout dimensions
|
|
664
|
+
requestAnimationFrame(() => rescale());
|
|
665
|
+
|
|
666
|
+
// ── Handle ──
|
|
667
|
+
|
|
668
|
+
function destroy(): void {
|
|
669
|
+
if (destroyed) return;
|
|
670
|
+
destroyed = true;
|
|
671
|
+
resizeObserver.disconnect();
|
|
672
|
+
document.removeEventListener("click", onDocClick);
|
|
673
|
+
root.remove();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
element: root,
|
|
678
|
+
goTo,
|
|
679
|
+
next: () => navigate(1),
|
|
680
|
+
prev: () => navigate(-1),
|
|
681
|
+
getIndex: () => currentIndex,
|
|
682
|
+
setDevice(device: "desktop" | "mobile") {
|
|
683
|
+
currentDevice = device;
|
|
684
|
+
wrapper.dataset.device = device;
|
|
685
|
+
deviceToggle?.setSelected(device);
|
|
686
|
+
zoomOverride = null;
|
|
687
|
+
rescale();
|
|
688
|
+
},
|
|
689
|
+
setColorScheme(scheme: "light" | "dark") {
|
|
690
|
+
currentScheme = scheme;
|
|
691
|
+
wrapper.dataset.colorScheme = scheme;
|
|
692
|
+
},
|
|
693
|
+
setZoom(zoom: number | null) {
|
|
694
|
+
zoomOverride = zoom;
|
|
695
|
+
rescale();
|
|
696
|
+
},
|
|
697
|
+
destroy,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
@@ -92,7 +92,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const headerCopy = createElement("div", "persona-flex persona-flex-col");
|
|
95
|
+
const headerCopy = createElement("div", "persona-flex persona-flex-col persona-flex-1 persona-min-w-0");
|
|
96
96
|
const title = createElement("span", "persona-text-base persona-font-semibold");
|
|
97
97
|
title.style.color = HEADER_THEME_CSS.titleColor;
|
|
98
98
|
title.textContent = config?.launcher?.title ?? "Chat Assistant";
|