@echodial/deck 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1327 -0
  3. package/bin/deck.mjs +219 -0
  4. package/build.mjs +338 -0
  5. package/dist/api-buckets.json +10486 -0
  6. package/dist/api.json +57896 -0
  7. package/dist/brand/deck-apple-touch-icon.png +0 -0
  8. package/dist/brand/deck-logo-dark.svg +1 -0
  9. package/dist/brand/deck-logo-light.svg +1 -0
  10. package/dist/brand/deck-logo-stacked.svg +1 -0
  11. package/dist/brand/deck-logo.svg +1 -0
  12. package/dist/brand/deck-mark.svg +7 -0
  13. package/dist/brand/deck-og.png +0 -0
  14. package/dist/collisions.json +223 -0
  15. package/dist/deck-adapters.js +482 -0
  16. package/dist/deck-adapters.min.js +23 -0
  17. package/dist/deck-extras.js +823 -0
  18. package/dist/deck-extras.min.js +5 -0
  19. package/dist/deck-icons.svg +184 -0
  20. package/dist/deck.bundle.js +2638 -0
  21. package/dist/deck.bundle.min.js +86 -0
  22. package/dist/deck.css +7904 -0
  23. package/dist/deck.esm.js +2644 -0
  24. package/dist/deck.js +1331 -0
  25. package/dist/deck.min.css +32 -0
  26. package/dist/deck.min.js +60 -0
  27. package/dist/layers/anchor.css +192 -0
  28. package/dist/layers/buttons.css +210 -0
  29. package/dist/layers/charts.css +337 -0
  30. package/dist/layers/combobox.css +248 -0
  31. package/dist/layers/components.css +868 -0
  32. package/dist/layers/container.css +199 -0
  33. package/dist/layers/datagrid.css +335 -0
  34. package/dist/layers/datepicker.css +305 -0
  35. package/dist/layers/forms.css +401 -0
  36. package/dist/layers/gradients.css +302 -0
  37. package/dist/layers/inputs.css +522 -0
  38. package/dist/layers/layers.css +87 -0
  39. package/dist/layers/layout.css +235 -0
  40. package/dist/layers/logical.css +226 -0
  41. package/dist/layers/media.css +400 -0
  42. package/dist/layers/mobile.css +245 -0
  43. package/dist/layers/motion.css +379 -0
  44. package/dist/layers/nav.css +771 -0
  45. package/dist/layers/perf.css +234 -0
  46. package/dist/layers/print.css +198 -0
  47. package/dist/layers/reset.css +103 -0
  48. package/dist/layers/space3d.css +280 -0
  49. package/dist/layers/toasts.css +201 -0
  50. package/dist/layers/tokens.css +229 -0
  51. package/dist/layers/type.css +114 -0
  52. package/dist/layers/utilities.css +241 -0
  53. package/dist/sizes.json +52 -0
  54. package/dist/usage.json +6340 -0
  55. package/package.json +94 -0
  56. package/src/00-layers.css +87 -0
  57. package/src/01-tokens.css +229 -0
  58. package/src/02-reset.css +103 -0
  59. package/src/03-type.css +114 -0
  60. package/src/04-layout.css +235 -0
  61. package/src/05-buttons.css +210 -0
  62. package/src/06-forms.css +401 -0
  63. package/src/07-components.css +868 -0
  64. package/src/08-mobile.css +245 -0
  65. package/src/09-utilities.css +241 -0
  66. package/src/10-datepicker.css +305 -0
  67. package/src/11-combobox.css +248 -0
  68. package/src/12-datagrid.css +335 -0
  69. package/src/13-toasts.css +201 -0
  70. package/src/14-charts.css +337 -0
  71. package/src/16-motion.css +379 -0
  72. package/src/18-container.css +199 -0
  73. package/src/19-logical.css +226 -0
  74. package/src/20-gradients.css +302 -0
  75. package/src/21-space3d.css +280 -0
  76. package/src/22-nav.css +771 -0
  77. package/src/23-inputs.css +522 -0
  78. package/src/24-media.css +400 -0
  79. package/src/25-anchor.css +192 -0
  80. package/src/26-perf.css +234 -0
  81. package/src/99-print.css +198 -0
  82. package/src/brand/deck-apple-touch-icon.png +0 -0
  83. package/src/brand/deck-logo-dark.svg +1 -0
  84. package/src/brand/deck-logo-light.svg +1 -0
  85. package/src/brand/deck-logo-stacked.svg +1 -0
  86. package/src/brand/deck-logo.svg +1 -0
  87. package/src/brand/deck-mark.svg +7 -0
  88. package/src/brand/deck-og.png +0 -0
  89. package/src/brand/sources.json +7 -0
  90. package/src/deck-icons.svg +184 -0
  91. package/src/js/deck-adapters.js +482 -0
  92. package/src/js/deck-extras.js +823 -0
  93. package/src/js/deck.js +1331 -0
@@ -0,0 +1,823 @@
1
+ /*! ==========================================================================
2
+ * deck-extras.js v0.1 — behaviour for the extended component set
3
+ * Loads after deck.js and registers itself with Deck.init.
4
+ * No dependencies, including the QR encoder.
5
+ * ======================================================================== */
6
+
7
+ (function (Deck) {
8
+ 'use strict';
9
+ if (!Deck) { console.warn('deck-extras.js: load deck.js first'); return; }
10
+
11
+ const $ = (s, r = document) => r.querySelector(s);
12
+ const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
13
+ const once = node => { if (node.dataset.deckX) return false; node.dataset.deckX = '1'; return true; };
14
+ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
15
+
16
+ /* ======================================================================
17
+ Carousel
18
+ <div class="carousel" data-deck-carousel data-autoplay="6000">
19
+ ====================================================================== */
20
+
21
+ function carousels(root) {
22
+ $$('[data-deck-carousel]', root).forEach(car => {
23
+ if (!once(car)) return;
24
+ const track = $('.carousel-track', car);
25
+ if (!track) return;
26
+ const slides = $$('.carousel-slide', track);
27
+ /* ::scroll-marker and ::scroll-button give us the dots, the arrows, the
28
+ active and disabled states and the keyboard order for free. Where they
29
+ exist the CSS hides the DIY ones, so drop the references here and the
30
+ building and wiring below skips itself. */
31
+ const diy = !CSS.supports('selector(::scroll-marker)');
32
+ const dots = diy ? $('.carousel-dots', car) : null;
33
+ const prev = diy ? $('.carousel-prev', car) : null;
34
+ const next = diy ? $('.carousel-next', car) : null;
35
+ const rtl = () => getComputedStyle(track).direction === 'rtl';
36
+
37
+ if (dots && !dots.children.length) {
38
+ dots.innerHTML = slides.map((s, i) =>
39
+ `<button type="button" class="carousel-dot" data-go="${i}" aria-label="Slide ${i + 1}"></button>`).join('');
40
+ }
41
+
42
+ const current = () => {
43
+ const mid = track.scrollLeft + track.clientWidth / 2;
44
+ let best = 0, bestD = Infinity;
45
+ slides.forEach((s, i) => {
46
+ const d = Math.abs(s.offsetLeft + s.offsetWidth / 2 - mid);
47
+ if (d < bestD) { bestD = d; best = i; }
48
+ });
49
+ return best;
50
+ };
51
+
52
+ const go = i => {
53
+ const s = slides[Math.max(0, Math.min(slides.length - 1, i))];
54
+ if (!s) return;
55
+ track.scrollTo({
56
+ left: s.offsetLeft - (track.clientWidth - s.offsetWidth) / 2,
57
+ behavior: reduced() ? 'auto' : 'smooth'
58
+ });
59
+ };
60
+
61
+ const paint = () => {
62
+ const i = current();
63
+ if (dots) $$('.carousel-dot', dots).forEach((d, n) =>
64
+ d.setAttribute('aria-current', String(n === i)));
65
+ if (prev) prev.disabled = i === 0;
66
+ if (next) next.disabled = i === slides.length - 1;
67
+ car.dispatchEvent(new CustomEvent('deck:slide', { bubbles: true, detail: { index: i } }));
68
+ };
69
+
70
+ track.addEventListener('scroll', () => requestAnimationFrame(paint), { passive: true });
71
+ prev?.addEventListener('click', () => go(current() - 1));
72
+ next?.addEventListener('click', () => go(current() + 1));
73
+ dots?.addEventListener('click', e => {
74
+ const b = e.target.closest('[data-go]');
75
+ if (b) go(Number(b.dataset.go));
76
+ });
77
+
78
+ car.addEventListener('keydown', e => {
79
+ const back = rtl() ? 'ArrowRight' : 'ArrowLeft';
80
+ const fwd = rtl() ? 'ArrowLeft' : 'ArrowRight';
81
+ if (e.key === back) { e.preventDefault(); go(current() - 1); }
82
+ if (e.key === fwd) { e.preventDefault(); go(current() + 1); }
83
+ });
84
+
85
+ const ms = Number(car.dataset.autoplay || 0);
86
+ if (ms > 0 && !reduced()) {
87
+ let timer = setInterval(tick, ms);
88
+ function tick() {
89
+ const i = current();
90
+ go(i >= slides.length - 1 ? 0 : i + 1);
91
+ }
92
+ const stop = () => clearInterval(timer);
93
+ const start = () => { stop(); timer = setInterval(tick, ms); };
94
+ car.addEventListener('pointerenter', stop);
95
+ car.addEventListener('pointerleave', start);
96
+ car.addEventListener('focusin', stop);
97
+ document.addEventListener('visibilitychange', () => document.hidden ? stop() : start());
98
+ }
99
+
100
+ paint();
101
+ car.carousel = { go, current, paint };
102
+ });
103
+ }
104
+
105
+ /* ======================================================================
106
+ Drawer, mega menu, speed dial, banner
107
+ ====================================================================== */
108
+
109
+ function drawers(root) {
110
+ $$('[data-deck-drawer]', root).forEach(trigger => {
111
+ if (!once(trigger)) return;
112
+ const target = $(trigger.dataset.deckDrawer);
113
+ if (!target) return;
114
+ trigger.addEventListener('click', () => target.showModal());
115
+ // Click on the backdrop closes it; the dialog element itself does not
116
+ target.addEventListener('click', e => {
117
+ if (e.target === target) target.close();
118
+ });
119
+ $$('[data-drawer-close]', target).forEach(b =>
120
+ b.addEventListener('click', () => target.close()));
121
+ });
122
+ }
123
+
124
+ function megaMenus(root) {
125
+ $$('[data-deck-mega]', root).forEach(trigger => {
126
+ if (!once(trigger)) return;
127
+ const panel = $(trigger.dataset.deckMega);
128
+ if (!panel) return;
129
+ const place = () => {
130
+ /* 25-anchor.css anchors .mega where the browser supports it, and these
131
+ inline insets would then be resolved against the anchored region and
132
+ double-offset the panel. */
133
+ if (CSS.supports('anchor-name: --a')) return;
134
+ const r = trigger.getBoundingClientRect();
135
+ panel.style.insetBlockStart = (r.bottom + 8) + 'px';
136
+ const w = panel.offsetWidth;
137
+ const start = Math.min(Math.max(8, r.left + r.width / 2 - w / 2), innerWidth - w - 8);
138
+ panel.style.insetInlineStart = start + 'px';
139
+ };
140
+ panel.addEventListener('toggle', e => { if (e.newState === 'open') place(); });
141
+ addEventListener('resize', () => { if (panel.matches(':popover-open')) place(); });
142
+ // Hover intent on pointer devices, click everywhere
143
+ if (matchMedia('(hover: hover)').matches) {
144
+ let t;
145
+ const open = () => { clearTimeout(t); t = setTimeout(() => panel.showPopover(), 90); };
146
+ const close = () => { clearTimeout(t); t = setTimeout(() => panel.hidePopover(), 180); };
147
+ [trigger, panel].forEach(n => {
148
+ n.addEventListener('pointerenter', open);
149
+ n.addEventListener('pointerleave', close);
150
+ });
151
+ }
152
+ });
153
+ }
154
+
155
+ function speedDials(root) {
156
+ $$('.speed-dial', root).forEach(dial => {
157
+ if (!once(dial)) return;
158
+ const fab = $('.fab', dial);
159
+ const set = on => {
160
+ dial.classList.toggle('is-open', on);
161
+ fab?.setAttribute('aria-expanded', String(on));
162
+ };
163
+ fab?.addEventListener('click', () => set(!dial.classList.contains('is-open')));
164
+ document.addEventListener('click', e => {
165
+ if (!dial.contains(e.target)) set(false);
166
+ });
167
+ dial.addEventListener('keydown', e => { if (e.key === 'Escape') { set(false); fab?.focus(); } });
168
+ });
169
+ }
170
+
171
+ function banners(root) {
172
+ $$('.banner', root).forEach(banner => {
173
+ if (!once(banner)) return;
174
+ const key = banner.dataset.dismissKey;
175
+ if (key) {
176
+ try { if (localStorage.getItem('deck-banner-' + key)) { banner.hidden = true; return; } } catch (e) {}
177
+ }
178
+ $('.banner-close', banner)?.addEventListener('click', () => {
179
+ banner.hidden = true;
180
+ if (key) { try { localStorage.setItem('deck-banner-' + key, '1'); } catch (e) {} }
181
+ });
182
+ });
183
+ }
184
+
185
+ /* ======================================================================
186
+ Number, phone, range pair
187
+ ====================================================================== */
188
+
189
+ function numbers(root) {
190
+ $$('.number', root).forEach(box => {
191
+ if (!once(box)) return;
192
+ const input = $('input', box);
193
+ const step = Number(input.step) || 1;
194
+ const min = input.min === '' ? -Infinity : Number(input.min);
195
+ const max = input.max === '' ? Infinity : Number(input.max);
196
+ const decimals = (String(step).split('.')[1] || '').length;
197
+
198
+ const nudge = dir => {
199
+ const now = Number(input.value) || 0;
200
+ const next = Math.min(max, Math.max(min, now + dir * step));
201
+ input.value = next.toFixed(decimals);
202
+ input.dispatchEvent(new Event('input', { bubbles: true }));
203
+ input.dispatchEvent(new Event('change', { bubbles: true }));
204
+ paint();
205
+ };
206
+ const paint = () => {
207
+ const now = Number(input.value);
208
+ $$('button', box).forEach(b => {
209
+ const d = Number(b.dataset.step || (b === box.firstElementChild ? -1 : 1));
210
+ b.disabled = d < 0 ? now <= min : now >= max;
211
+ });
212
+ };
213
+
214
+ $$('button', box).forEach(b => {
215
+ const dir = Number(b.dataset.step || (b === box.firstElementChild ? -1 : 1));
216
+ let hold, repeat;
217
+ const start = e => {
218
+ e.preventDefault();
219
+ nudge(dir);
220
+ hold = setTimeout(() => { repeat = setInterval(() => nudge(dir), 70); }, 420);
221
+ };
222
+ const stop = () => { clearTimeout(hold); clearInterval(repeat); };
223
+ b.addEventListener('pointerdown', start);
224
+ ['pointerup', 'pointerleave', 'pointercancel'].forEach(ev => b.addEventListener(ev, stop));
225
+ });
226
+ input.addEventListener('input', paint);
227
+ paint();
228
+ });
229
+ }
230
+
231
+ function phones(root) {
232
+ $$('.phone', root).forEach(box => {
233
+ if (!once(box)) return;
234
+ const input = $('input', box);
235
+ const select = $('select', box);
236
+ const codeEl = $('.phone-code', box);
237
+ const flagEl = $('.phone-flag', box);
238
+
239
+ const sync = () => {
240
+ const opt = select?.selectedOptions[0];
241
+ if (!opt) return;
242
+ if (codeEl) codeEl.textContent = opt.dataset.code || '';
243
+ if (flagEl) flagEl.textContent = opt.dataset.flag || '';
244
+ input.placeholder = opt.dataset.mask
245
+ ? opt.dataset.mask.replace(/#/g, '0')
246
+ : input.placeholder;
247
+ };
248
+ select?.addEventListener('change', () => { sync(); format(); });
249
+
250
+ /* FINDINGS 62: this destroys a digit when someone edits the middle of a
251
+ number. The loop stops at the end of the MASK, not the end of the
252
+ digits, so inserting a character shifts everything along and the last
253
+ digit falls off with no warning -- measured: caret at index 8 of
254
+ "(202) 555-0147", type 9, get "(202) 559-5014". The row still reports
255
+ itself .is-valid because it still holds ten digits.
256
+
257
+ Assigning input.value also collapses the caret to the end.
258
+
259
+ Fix: count digits before the caret, rebuild, restore the caret to the
260
+ same digit offset; and refuse the keystroke when the mask is full
261
+ rather than dropping data silently. */
262
+ const format = () => {
263
+ const mask = select?.selectedOptions[0]?.dataset.mask;
264
+ const digits = input.value.replace(/\D/g, '');
265
+ if (!mask) { input.value = digits; return; }
266
+ let out = '', d = 0;
267
+ for (const ch of mask) {
268
+ if (d >= digits.length) break;
269
+ out += ch === '#' ? digits[d++] : ch;
270
+ }
271
+ input.value = out;
272
+ const full = (mask.match(/#/g) || []).length;
273
+ box.classList.toggle('is-valid', digits.length === full);
274
+ };
275
+ input.addEventListener('input', format);
276
+ input.addEventListener('blur', () => {
277
+ box.dispatchEvent(new CustomEvent('deck:change', {
278
+ bubbles: true,
279
+ detail: {
280
+ code: select?.selectedOptions[0]?.dataset.code || '',
281
+ number: input.value.replace(/\D/g, ''),
282
+ e164: (select?.selectedOptions[0]?.dataset.code || '') + input.value.replace(/\D/g, '')
283
+ }
284
+ }));
285
+ });
286
+ sync();
287
+ });
288
+ }
289
+
290
+ function rangePairs(root) {
291
+ $$('.range-pair', root).forEach(pair => {
292
+ if (!once(pair)) return;
293
+ const [lo, hi] = $$('input[type="range"]', pair);
294
+ if (!lo || !hi) return;
295
+ const min = Number(lo.min || 0), max = Number(lo.max || 100);
296
+ const pct = v => ((v - min) / (max - min)) * 100;
297
+ const gap = Number(pair.dataset.gap || 0);
298
+
299
+ const paint = () => {
300
+ if (Number(lo.value) > Number(hi.value) - gap) {
301
+ // whichever handle moved last gets pushed back
302
+ if (document.activeElement === lo) lo.value = String(Number(hi.value) - gap);
303
+ else hi.value = String(Number(lo.value) + gap);
304
+ }
305
+ pair.style.setProperty('--lo', pct(Number(lo.value)).toFixed(2));
306
+ pair.style.setProperty('--hi', pct(Number(hi.value)).toFixed(2));
307
+ const readout = pair.parentElement?.querySelector('.range-readout');
308
+ if (readout) {
309
+ const [a, b] = readout.children;
310
+ if (a) a.textContent = lo.dataset.prefix ? lo.dataset.prefix + lo.value : lo.value;
311
+ if (b) b.textContent = hi.dataset.prefix ? hi.dataset.prefix + hi.value : hi.value;
312
+ }
313
+ pair.dispatchEvent(new CustomEvent('deck:change', {
314
+ bubbles: true, detail: { min: Number(lo.value), max: Number(hi.value) }
315
+ }));
316
+ };
317
+ [lo, hi].forEach(i => i.addEventListener('input', paint));
318
+ paint();
319
+ });
320
+ }
321
+
322
+ /* ======================================================================
323
+ Copy to clipboard
324
+ ====================================================================== */
325
+
326
+ async function writeClipboard(text) {
327
+ try {
328
+ await navigator.clipboard.writeText(text);
329
+ return true;
330
+ } catch (e) {
331
+ // http origins and older engines
332
+ const ta = document.createElement('textarea');
333
+ ta.value = text;
334
+ ta.setAttribute('readonly', '');
335
+ ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
336
+ document.body.append(ta);
337
+ ta.select();
338
+ let ok = false;
339
+ try { ok = document.execCommand('copy'); } catch (err) {}
340
+ ta.remove();
341
+ return ok;
342
+ }
343
+ }
344
+
345
+ function copiers(root) {
346
+ $$('[data-deck-copy]', root).forEach(btn => {
347
+ if (!once(btn)) return;
348
+ btn.addEventListener('click', async () => {
349
+ const sel = btn.dataset.deckCopy;
350
+ const text = sel && sel !== 'true'
351
+ ? ($(sel)?.value ?? $(sel)?.textContent ?? sel)
352
+ : (btn.closest('.copy')?.querySelector('code, .copy-value')?.textContent
353
+ ?? btn.previousElementSibling?.textContent ?? '');
354
+ const ok = await writeClipboard(String(text).trim());
355
+ btn.classList.toggle('is-copied', ok);
356
+ /* FINDINGS 63: this is set AFTER the subtree has already changed, so
357
+ the first press announces nothing -- a live region has to exist
358
+ before its content changes. Every press after that announces,
359
+ because the attribute is by then already there. Declare it in the
360
+ markup, or write into a separate role="status" element. */
361
+ btn.setAttribute('aria-live', 'polite');
362
+ if (ok && !btn.classList.contains('copy-inline') && !$('.copy-done', btn)) {
363
+ Deck.toast({ kind: 'good', title: 'Copied', duration: 2000 });
364
+ }
365
+ setTimeout(() => btn.classList.remove('is-copied'), 1800);
366
+ });
367
+ });
368
+ }
369
+
370
+ /* ======================================================================
371
+ WYSIWYG editor
372
+ ====================================================================== */
373
+
374
+ function editors(root) {
375
+ $$('.editor', root).forEach(ed => {
376
+ if (!once(ed)) return;
377
+ const content = $('.editor-content', ed);
378
+ const target = ed.dataset.target ? $(ed.dataset.target) : null;
379
+ const counter = $('.editor-count', ed);
380
+ const limit = Number(ed.dataset.limit || 0);
381
+ if (!content) return;
382
+ content.contentEditable = 'true';
383
+ content.setAttribute('role', 'textbox');
384
+ content.setAttribute('aria-multiline', 'true');
385
+
386
+ const run = (cmd, value = null) => {
387
+ content.focus();
388
+ document.execCommand(cmd, false, value);
389
+ paint();
390
+ sync();
391
+ };
392
+
393
+ const paint = () => {
394
+ $$('[data-cmd]', ed).forEach(b => {
395
+ let on = false;
396
+ try { on = document.queryCommandState(b.dataset.cmd); } catch (e) {}
397
+ b.setAttribute('aria-pressed', String(on));
398
+ });
399
+ if (counter) {
400
+ const n = content.textContent.trim().length;
401
+ counter.textContent = limit ? `${n} / ${limit}` : `${n} characters`;
402
+ counter.classList.toggle('is-over', limit > 0 && n > limit);
403
+ }
404
+ };
405
+
406
+ const sync = () => {
407
+ if (target) target.value = content.innerHTML;
408
+ ed.dispatchEvent(new CustomEvent('deck:change', {
409
+ bubbles: true, detail: { html: content.innerHTML, text: content.textContent }
410
+ }));
411
+ };
412
+
413
+ ed.addEventListener('click', e => {
414
+ const b = e.target.closest('[data-cmd]');
415
+ if (!b) return;
416
+ e.preventDefault();
417
+ const cmd = b.dataset.cmd;
418
+ if (cmd === 'createLink') {
419
+ const url = prompt('Link address');
420
+ if (url) run('createLink', url);
421
+ return;
422
+ }
423
+ run(cmd, b.dataset.value || null);
424
+ });
425
+
426
+ $$('.editor-select', ed).forEach(sel => {
427
+ sel.addEventListener('change', () => run('formatBlock', sel.value));
428
+ });
429
+
430
+ // Paste as plain text, so a paste from Word does not import its styling
431
+ content.addEventListener('paste', e => {
432
+ e.preventDefault();
433
+ const text = (e.clipboardData || window.clipboardData).getData('text/plain');
434
+ document.execCommand('insertText', false, text);
435
+ });
436
+
437
+ content.addEventListener('input', () => { paint(); sync(); });
438
+ content.addEventListener('keyup', paint);
439
+ content.addEventListener('mouseup', paint);
440
+ document.addEventListener('selectionchange', () => {
441
+ if (content.contains(document.getSelection()?.anchorNode)) paint();
442
+ });
443
+
444
+ if (target && target.value) content.innerHTML = target.value;
445
+ paint();
446
+ });
447
+ }
448
+
449
+ /* ======================================================================
450
+ Lazy images
451
+ ====================================================================== */
452
+
453
+ function lazies(root) {
454
+ const imgs = $$('.lazy img, img[data-src]', root).filter(i => !i.dataset.deckX);
455
+ if (!imgs.length) return;
456
+ const load = img => {
457
+ img.dataset.deckX = '1';
458
+ if (img.dataset.src) img.src = img.dataset.src;
459
+ if (img.dataset.srcset) img.srcset = img.dataset.srcset;
460
+ const done = () => img.classList.add('is-loaded');
461
+ if (img.complete && img.naturalWidth) done();
462
+ else img.addEventListener('load', done, { once: true });
463
+ img.addEventListener('error', () => img.closest('.lazy')?.classList.add('is-error'), { once: true });
464
+ };
465
+ if (!('IntersectionObserver' in window)) return imgs.forEach(load);
466
+ const io = new IntersectionObserver(entries => {
467
+ entries.forEach(e => {
468
+ if (!e.isIntersecting) return;
469
+ load(e.target);
470
+ io.unobserve(e.target);
471
+ });
472
+ }, { rootMargin: '200px' });
473
+ imgs.forEach(i => io.observe(i));
474
+ }
475
+
476
+ /* ======================================================================
477
+ QR code
478
+ A complete encoder: byte mode, versions 1 to 10, error correction
479
+ L / M / Q / H, all eight masks scored by the standard penalty rules.
480
+ No dependency, no network call, no canvas — it emits SVG.
481
+ ====================================================================== */
482
+
483
+ const QR = (() => {
484
+ // Galois field GF(256) with the QR primitive polynomial 0x11D
485
+ const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
486
+ for (let i = 0, x = 1; i < 255; i++) {
487
+ EXP[i] = x; LOG[x] = i;
488
+ x <<= 1; if (x & 0x100) x ^= 0x11d;
489
+ }
490
+ for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
491
+ const mul = (a, b) => (a && b) ? EXP[LOG[a] + LOG[b]] : 0;
492
+
493
+ // [ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data] by version, then L M Q H
494
+ const RS = {
495
+ 1: [[7,1,19,0,0],[10,1,16,0,0],[13,1,13,0,0],[17,1,9,0,0]],
496
+ 2: [[10,1,34,0,0],[16,1,28,0,0],[22,1,22,0,0],[28,1,16,0,0]],
497
+ 3: [[15,1,55,0,0],[26,1,44,0,0],[18,2,17,0,0],[22,2,13,0,0]],
498
+ 4: [[20,1,80,0,0],[18,2,32,0,0],[26,2,24,0,0],[16,4,9,0,0]],
499
+ 5: [[26,1,108,0,0],[24,2,43,0,0],[18,2,15,2,16],[22,2,11,2,12]],
500
+ 6: [[18,2,68,0,0],[16,4,27,0,0],[24,4,19,0,0],[28,4,15,0,0]],
501
+ 7: [[20,2,78,0,0],[18,4,31,0,0],[18,2,14,4,15],[26,4,13,1,14]],
502
+ 8: [[24,2,97,0,0],[22,2,38,2,39],[22,4,18,2,19],[26,4,14,2,15]],
503
+ 9: [[30,2,116,0,0],[22,3,36,2,37],[20,4,16,4,17],[24,4,12,4,13]],
504
+ 10: [[18,2,68,2,69],[26,4,43,1,44],[24,6,19,2,20],[28,6,15,2,16]]
505
+ };
506
+ const ALIGN = {
507
+ 1: [], 2: [6,18], 3: [6,22], 4: [6,26], 5: [6,30],
508
+ 6: [6,34], 7: [6,22,38], 8: [6,24,42], 9: [6,26,46], 10: [6,28,50]
509
+ };
510
+ const ECL = { L: 0, M: 1, Q: 2, H: 3 };
511
+ const ECL_BITS = { L: 1, M: 0, Q: 3, H: 2 };
512
+
513
+ function generator(n) {
514
+ let poly = [1];
515
+ for (let i = 0; i < n; i++) {
516
+ const next = new Array(poly.length + 1).fill(0);
517
+ for (let j = 0; j < poly.length; j++) {
518
+ next[j] ^= mul(poly[j], EXP[i]);
519
+ next[j + 1] ^= poly[j];
520
+ }
521
+ poly = next;
522
+ }
523
+ return poly;
524
+ }
525
+
526
+ function ecBytes(data, n) {
527
+ const gen = generator(n);
528
+ const rem = new Array(n).fill(0);
529
+ for (const byte of data) {
530
+ const factor = byte ^ rem[0];
531
+ rem.shift();
532
+ rem.push(0);
533
+ for (let i = 0; i < n; i++) rem[i] ^= mul(gen[i + 1], factor);
534
+ }
535
+ return rem;
536
+ }
537
+
538
+ function formatBits(ecl, mask) {
539
+ const data = (ECL_BITS[ecl] << 3) | mask;
540
+ let rem = data;
541
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) * 0x537);
542
+ return ((data << 10) | rem) ^ 0x5412;
543
+ }
544
+
545
+ function versionBits(v) {
546
+ let rem = v;
547
+ for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >> 11) * 0x1f25);
548
+ return (v << 12) | rem;
549
+ }
550
+
551
+ function pickVersion(len, ecl) {
552
+ for (let v = 1; v <= 10; v++) {
553
+ const [ec, b1, d1, b2, d2] = RS[v][ECL[ecl]];
554
+ const total = b1 * d1 + b2 * d2;
555
+ const countBits = v < 10 ? 8 : 16;
556
+ if (len <= Math.floor((total * 8 - 4 - countBits) / 8)) return v;
557
+ }
558
+ throw new Error('QR: content too long for versions 1 to 10. Shorten it or link to it.');
559
+ }
560
+
561
+ function encode(text, ecl = 'M') {
562
+ const bytes = Array.from(new TextEncoder().encode(text));
563
+ const version = pickVersion(bytes.length, ecl);
564
+ const [ecPer, b1, d1, b2, d2] = RS[version][ECL[ecl]];
565
+ const totalData = b1 * d1 + b2 * d2;
566
+ const countBits = version < 10 ? 8 : 16;
567
+
568
+ // Bit stream: mode 0100, character count, payload, terminator, padding
569
+ const bits = [];
570
+ const push = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); };
571
+ push(4, 4);
572
+ push(bytes.length, countBits);
573
+ bytes.forEach(b => push(b, 8));
574
+ for (let i = 0; i < 4 && bits.length < totalData * 8; i++) bits.push(0);
575
+ while (bits.length % 8) bits.push(0);
576
+ const data = [];
577
+ for (let i = 0; i < bits.length; i += 8) {
578
+ data.push(parseInt(bits.slice(i, i + 8).join(''), 2));
579
+ }
580
+ const PAD = [0xec, 0x11];
581
+ for (let i = 0; data.length < totalData; i++) data.push(PAD[i % 2]);
582
+
583
+ // Split into blocks, compute error correction, then interleave
584
+ const blocks = [], ecs = [];
585
+ let at = 0;
586
+ for (let i = 0; i < b1 + b2; i++) {
587
+ const size = i < b1 ? d1 : d2;
588
+ const block = data.slice(at, at + size);
589
+ at += size;
590
+ blocks.push(block);
591
+ ecs.push(ecBytes(block, ecPer));
592
+ }
593
+ const out = [];
594
+ const maxData = Math.max(d1, d2);
595
+ for (let i = 0; i < maxData; i++)
596
+ blocks.forEach(b => { if (i < b.length) out.push(b[i]); });
597
+ for (let i = 0; i < ecPer; i++)
598
+ ecs.forEach(b => out.push(b[i]));
599
+
600
+ return { version, ecl, codewords: out };
601
+ }
602
+
603
+ function build(text, ecl = 'M') {
604
+ const { version, codewords } = encode(text, ecl);
605
+ const size = version * 4 + 17;
606
+ const grid = Array.from({ length: size }, () => new Array(size).fill(null));
607
+ const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
608
+
609
+ const put = (x, y, v, res = true) => {
610
+ if (x < 0 || y < 0 || x >= size || y >= size) return;
611
+ grid[y][x] = v;
612
+ if (res) reserved[y][x] = true;
613
+ };
614
+
615
+ // Finder patterns and separators
616
+ [[0, 0], [size - 7, 0], [0, size - 7]].forEach(([ox, oy]) => {
617
+ for (let y = -1; y <= 7; y++) for (let x = -1; x <= 7; x++) {
618
+ const on = (x >= 0 && x <= 6 && (y === 0 || y === 6)) ||
619
+ (y >= 0 && y <= 6 && (x === 0 || x === 6)) ||
620
+ (x >= 2 && x <= 4 && y >= 2 && y <= 4);
621
+ put(ox + x, oy + y, on ? 1 : 0);
622
+ }
623
+ });
624
+
625
+ // Timing patterns
626
+ for (let i = 8; i < size - 8; i++) {
627
+ put(i, 6, i % 2 === 0 ? 1 : 0);
628
+ put(6, i, i % 2 === 0 ? 1 : 0);
629
+ }
630
+
631
+ // Alignment patterns, skipping the ones that collide with finders
632
+ const centers = ALIGN[version];
633
+ centers.forEach(cy => centers.forEach(cx => {
634
+ if ((cx === 6 && cy === 6) ||
635
+ (cx === 6 && cy === size - 7) ||
636
+ (cx === size - 7 && cy === 6)) return;
637
+ for (let y = -2; y <= 2; y++) for (let x = -2; x <= 2; x++) {
638
+ const on = Math.max(Math.abs(x), Math.abs(y)) !== 1;
639
+ put(cx + x, cy + y, on ? 1 : 0);
640
+ }
641
+ }));
642
+
643
+ // Dark module, then reserve the two format strips so data skips them
644
+ put(8, size - 8, 1);
645
+ for (let i = 0; i < 9; i++) { reserved[8][i] = true; reserved[i][8] = true; }
646
+ for (let i = 0; i < 8; i++) {
647
+ reserved[8][size - 1 - i] = true;
648
+ reserved[size - 1 - i][8] = true;
649
+ }
650
+
651
+ // Version information blocks, versions 7 and up
652
+ if (version >= 7) {
653
+ const vb = versionBits(version);
654
+ for (let i = 0; i < 18; i++) {
655
+ const b = (vb >> i) & 1;
656
+ const a = Math.floor(i / 3), c = i % 3;
657
+ grid[a][size - 11 + c] = b; reserved[a][size - 11 + c] = true;
658
+ grid[size - 11 + c][a] = b; reserved[size - 11 + c][a] = true;
659
+ }
660
+ }
661
+
662
+ // Place data in the zig-zag, skipping the timing column
663
+ let bit = 0;
664
+ const stream = [];
665
+ codewords.forEach(cw => { for (let i = 7; i >= 0; i--) stream.push((cw >> i) & 1); });
666
+
667
+ let up = true;
668
+ for (let right = size - 1; right > 0; right -= 2) {
669
+ if (right === 6) right = 5;
670
+ for (let step = 0; step < size; step++) {
671
+ const y = up ? size - 1 - step : step;
672
+ for (let c = 0; c < 2; c++) {
673
+ const x = right - c;
674
+ if (reserved[y][x]) continue;
675
+ grid[y][x] = bit < stream.length ? stream[bit++] : 0;
676
+ }
677
+ }
678
+ up = !up;
679
+ }
680
+
681
+ // Try every mask, keep the one with the lowest penalty
682
+ const maskFn = [
683
+ (x, y) => (x + y) % 2 === 0,
684
+ (x, y) => y % 2 === 0,
685
+ (x, y) => x % 3 === 0,
686
+ (x, y) => (x + y) % 3 === 0,
687
+ (x, y) => (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0,
688
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
689
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
690
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0
691
+ ];
692
+
693
+ let best = null, bestScore = Infinity;
694
+ for (let m = 0; m < 8; m++) {
695
+ const test = grid.map(r => r.slice());
696
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
697
+ if (!reserved[y][x] && maskFn[m](x, y)) test[y][x] ^= 1;
698
+ }
699
+ const fb = formatBits(ecl, m);
700
+ const fbit = i => (fb >> i) & 1;
701
+ // grid is indexed [row][column]
702
+ // First copy: down column 8, then left along row 8
703
+ for (let i = 0; i <= 5; i++) test[i][8] = fbit(i);
704
+ test[7][8] = fbit(6);
705
+ test[8][8] = fbit(7);
706
+ test[8][7] = fbit(8);
707
+ for (let i = 9; i <= 14; i++) test[8][14 - i] = fbit(i);
708
+ // Second copy: along row 8 beside the top-right finder,
709
+ // then down column 8 beside the bottom-left finder
710
+ for (let i = 0; i <= 7; i++) test[8][size - 1 - i] = fbit(i);
711
+ for (let i = 8; i <= 14; i++) test[size - 15 + i][8] = fbit(i);
712
+ test[size - 8][8] = 1;
713
+ const score = penalty(test, size);
714
+ if (score < bestScore) { bestScore = score; best = test; }
715
+ }
716
+ return { modules: best, size, version };
717
+ }
718
+
719
+ function penalty(m, size) {
720
+ let score = 0;
721
+ // Rule 1: runs of five or more of the same colour
722
+ const run = line => {
723
+ let total = 0, count = 1;
724
+ for (let i = 1; i < size; i++) {
725
+ if (line[i] === line[i - 1]) count++;
726
+ else { if (count >= 5) total += count - 2; count = 1; }
727
+ }
728
+ if (count >= 5) total += count - 2;
729
+ return total;
730
+ };
731
+ for (let y = 0; y < size; y++) score += run(m[y]);
732
+ for (let x = 0; x < size; x++) score += run(m.map(r => r[x]));
733
+ // Rule 2: two by two blocks
734
+ for (let y = 0; y < size - 1; y++) for (let x = 0; x < size - 1; x++) {
735
+ const v = m[y][x];
736
+ if (v === m[y][x + 1] && v === m[y + 1][x] && v === m[y + 1][x + 1]) score += 3;
737
+ }
738
+ // Rule 3: the finder-like 1011101 pattern with four light modules beside it
739
+ const pat1 = [1,0,1,1,1,0,1,0,0,0,0];
740
+ const pat2 = [0,0,0,0,1,0,1,1,1,0,1];
741
+ const scan = line => {
742
+ let s = 0;
743
+ for (let i = 0; i <= size - 11; i++) {
744
+ const slice = line.slice(i, i + 11).join('');
745
+ if (slice === pat1.join('') || slice === pat2.join('')) s += 40;
746
+ }
747
+ return s;
748
+ };
749
+ for (let y = 0; y < size; y++) score += scan(m[y]);
750
+ for (let x = 0; x < size; x++) score += scan(m.map(r => r[x]));
751
+ // Rule 4: deviation from an even split of dark and light
752
+ let dark = 0;
753
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) dark += m[y][x];
754
+ const ratio = Math.abs((dark * 100) / (size * size) - 50);
755
+ score += Math.floor(ratio / 5) * 10;
756
+ return score;
757
+ }
758
+
759
+ function svg(text, ecl = 'M') {
760
+ const { modules, size } = build(text, ecl);
761
+ // One path-free approach: merge horizontal runs into rects so the SVG
762
+ // stays small even at version 10
763
+ let rects = '';
764
+ for (let y = 0; y < size; y++) {
765
+ let x = 0;
766
+ while (x < size) {
767
+ if (!modules[y][x]) { x++; continue; }
768
+ let w = 1;
769
+ while (x + w < size && modules[y][x + w]) w++;
770
+ rects += `<rect x="${x}" y="${y}" width="${w}" height="1"/>`;
771
+ x += w;
772
+ }
773
+ }
774
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" role="img"><g>${rects}</g></svg>`;
775
+ }
776
+
777
+ return { svg, build, encode };
778
+ })();
779
+
780
+ function qrCodes(root) {
781
+ $$('[data-deck-qr]', root).forEach(node => {
782
+ if (!once(node)) return;
783
+ const value = node.dataset.deckQr || node.textContent.trim();
784
+ const level = (node.dataset.ecl || 'M').toUpperCase();
785
+ try {
786
+ node.innerHTML = QR.svg(value, level) + (node.dataset.caption
787
+ ? `<span class="qr-caption">${node.dataset.caption}</span>` : '');
788
+ } catch (err) {
789
+ node.innerHTML = `<span class="text-sm text-muted">${err.message}</span>`;
790
+ }
791
+ });
792
+ }
793
+
794
+ /* ======================================================================
795
+ Register
796
+ ====================================================================== */
797
+
798
+ const baseInit = Deck.init.bind(Deck);
799
+ Deck.init = function (root = document) {
800
+ baseInit(root);
801
+ carousels(root);
802
+ drawers(root);
803
+ megaMenus(root);
804
+ speedDials(root);
805
+ banners(root);
806
+ numbers(root);
807
+ phones(root);
808
+ rangePairs(root);
809
+ copiers(root);
810
+ editors(root);
811
+ lazies(root);
812
+ qrCodes(root);
813
+ return Deck;
814
+ };
815
+
816
+ Deck.qr = QR;
817
+ Deck.copy = writeClipboard;
818
+
819
+ if (document.readyState !== 'loading') Deck.init();
820
+ else document.addEventListener('DOMContentLoaded', () => Deck.init());
821
+
822
+ })(typeof window !== 'undefined' && window.Deck ? window.Deck
823
+ : typeof Deck !== 'undefined' ? Deck : null);