maverick-wave 5.7.0 → 5.10.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 (45) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +22 -12
  2. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +10 -1
  3. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +4 -3
  4. package/.claude/skills/mw-maverick-wave/references/components.md +23 -9
  5. package/.claude/skills/mw-maverick-wave/references/javascript.md +1 -1
  6. package/.claude/skills/mw-maverick-wave/references/layout.md +8 -3
  7. package/.claude/skills/mw-maverick-wave/references/theming.md +57 -29
  8. package/CHANGELOG.md +19 -0
  9. package/README.md +43 -22
  10. package/index.html +395 -62
  11. package/maverick-wave.min.css +61 -5
  12. package/maverick-wave.min.js +1 -1
  13. package/package.json +2 -2
  14. package/scripts/verify.js +18 -2
  15. package/src/js/main.js +24 -5
  16. package/src/partials/header-container.html +7 -5
  17. package/src/partials/palette-container.html +176 -66
  18. package/src/partials/preview-container.html +136 -0
  19. package/src/partials/utilities-container.html +55 -4
  20. package/src/scss/abstracts/_functions.scss +31 -3
  21. package/src/scss/abstracts/_mixins.scss +62 -12
  22. package/src/scss/abstracts/_variables.scss +47 -21
  23. package/src/scss/base/_base.scss +101 -8
  24. package/src/scss/base/_reset.scss +2 -0
  25. package/src/scss/components/_badge.scss +1 -1
  26. package/src/scss/components/_buttons.scss +1 -1
  27. package/src/scss/components/_cards.scss +23 -14
  28. package/src/scss/components/_dropdown.scss +58 -1
  29. package/src/scss/components/_kanban.scss +10 -9
  30. package/src/scss/components/_lang-switch.scss +1 -1
  31. package/src/scss/components/_localhost-indicator.scss +1 -1
  32. package/src/scss/components/_modals.scss +13 -1
  33. package/src/scss/components/_pricing.scss +1 -1
  34. package/src/scss/components/_progress.scss +9 -5
  35. package/src/scss/components/_skeleton.scss +1 -1
  36. package/src/scss/components/_spinners.scss +5 -5
  37. package/src/scss/components/_stepper.scss +4 -1
  38. package/src/scss/components/_tabs.scss +28 -8
  39. package/src/scss/components/_tiles.scss +41 -16
  40. package/src/scss/components/_timelines.scss +12 -5
  41. package/src/scss/layout/_main.scss +1 -1
  42. package/src/scss/layout/_parallax.scss +10 -5
  43. package/src/scss/utilities/_elevation.scss +23 -0
  44. package/src/scss/utilities/_touch-targets.scss +1 -1
  45. package/src/scss/utilities/_variants.scss +115 -6
package/index.html CHANGED
@@ -124,40 +124,50 @@
124
124
 
125
125
  /* Palette switcher, showcase only. The framework derives every tone from
126
126
  the root colors, so the panel writes nothing but those. */
127
- .palette-fab {
128
- position: fixed;
129
- right: 18px;
130
- bottom: 18px;
131
- z-index: 400;
132
- width: 52px;
133
- height: 52px;
134
- border-radius: 50%;
135
- border: 1px solid var(--mw-border-accent);
127
+ .palette-toggle {
128
+ display: none;
129
+ align-items: center;
130
+ justify-content: center;
131
+ width: 50px;
132
+ height: 40px;
133
+ flex-shrink: 0;
134
+ border: none;
135
+ border-radius: calc(10px * var(--mw-radius-scale, 1));
136
136
  background: var(--mw-primary-color);
137
137
  color: var(--mw-primary-accent-text-color);
138
- box-shadow: var(--mw-elevation-4);
139
- font-size: 1.25rem;
138
+ font-size: 1.1rem;
140
139
  cursor: pointer;
141
140
  transition: var(--mw-transition);
142
141
  }
143
142
 
144
- .palette-fab:hover {
143
+ /* Below sm the panel would cover the very thing it is changing */
144
+ @media (min-width: 576px) {
145
+ .palette-toggle {
146
+ display: flex;
147
+ }
148
+ }
149
+
150
+ .palette-toggle:hover {
145
151
  background: var(--mw-primary-color-hover);
146
152
  }
147
153
 
148
- .palette-fab:active {
149
- transform: scale(0.92);
154
+ .palette-toggle:active {
155
+ transform: scale(0.94);
150
156
  }
151
157
 
152
158
  .palette-panel {
153
159
  position: fixed;
154
- right: 18px;
155
- bottom: 82px;
160
+ top: calc(var(--mw-header-height) + 8px);
161
+ /* Flush with the container edge, so it lines up with the button in the
162
+ header rather than sitting further out */
163
+ right: max(18px, calc((100% - var(--mw-container-width)) / 2));
156
164
  z-index: 400;
157
- width: 360px;
165
+ /* Wide enough for three palettes in a row; the min() is for the narrow
166
+ end, where the panel would otherwise reach past the viewport */
167
+ width: min(440px, calc(100% - 24px));
158
168
  /* The variant list makes the panel taller than a laptop in landscape -
159
- the top of it would be behind the browser chrome without this */
160
- max-height: calc(100dvh - 120px);
169
+ it takes what is left under the header and scrolls the rest */
170
+ max-height: calc(100dvh - var(--mw-header-height) - 24px);
161
171
  overflow-y: auto;
162
172
  overscroll-behavior: contain;
163
173
  padding: 18px;
@@ -165,6 +175,26 @@
165
175
  border: 1px solid var(--mw-border);
166
176
  background: var(--mw-card-background);
167
177
  box-shadow: var(--mw-elevation-5);
178
+ transform: translateY(0);
179
+ opacity: 1;
180
+ /* display is in the list so allow-discrete keeps the panel mounted
181
+ through the fade out - `hidden` is the state the script writes */
182
+ transition-property: opacity, transform, display;
183
+ transition-duration: var(--mw-duration-base);
184
+ transition-timing-function: var(--mw-ease-out);
185
+ transition-behavior: allow-discrete;
186
+ }
187
+
188
+ @starting-style {
189
+ .palette-panel {
190
+ opacity: 0;
191
+ transform: translateY(-12px);
192
+ }
193
+ }
194
+
195
+ .palette-panel[hidden] {
196
+ opacity: 0;
197
+ transform: translateY(-12px);
168
198
  }
169
199
 
170
200
  .palette-heading {
@@ -181,7 +211,7 @@
181
211
 
182
212
  .palette-grid {
183
213
  display: grid;
184
- grid-template-columns: 1fr 1fr;
214
+ grid-template-columns: repeat(3, 1fr);
185
215
  gap: 6px;
186
216
  }
187
217
 
@@ -262,6 +292,26 @@
262
292
  border-top: 1px solid var(--mw-border);
263
293
  }
264
294
 
295
+ /* Splits the list by where a switch shows up, because half of them act on
296
+ parts of the page the preview does not contain */
297
+ .variant-group {
298
+ margin: 20px 0 2px;
299
+ font-size: 0.78rem;
300
+ font-weight: 600;
301
+ color: var(--mw-text-color);
302
+ }
303
+
304
+ .variant-group:first-child {
305
+ margin-top: 4px;
306
+ }
307
+
308
+ .variant-group-hint {
309
+ margin: 0 0 4px;
310
+ font-size: 0.68rem;
311
+ line-height: 1.35;
312
+ color: var(--mw-text-muted-color);
313
+ }
314
+
265
315
  .variant-row {
266
316
  display: flex;
267
317
  align-items: center;
@@ -291,7 +341,7 @@
291
341
 
292
342
  .variant-row select {
293
343
  flex-shrink: 0;
294
- max-width: 110px;
344
+ max-width: 135px;
295
345
  padding: 3px 6px;
296
346
  font-size: 0.75rem;
297
347
  color: var(--mw-text-color);
@@ -339,14 +389,129 @@
339
389
  background: var(--mw-primary-accent-text-color);
340
390
  }
341
391
 
392
+ /* The sticky bar would otherwise sit on the section title it jumps to */
393
+ #preview {
394
+ scroll-margin-top: calc(var(--mw-header-height) + 1rem);
395
+ }
396
+
397
+ .preview-head {
398
+ display: flex;
399
+ align-items: flex-start;
400
+ justify-content: space-between;
401
+ gap: 1rem;
402
+ margin-bottom: 1.5rem;
403
+ }
404
+
405
+ .preview-title {
406
+ margin: 0 0 0.35rem;
407
+ }
408
+
409
+ .preview-lede {
410
+ margin: 0;
411
+ max-inline-size: 60ch;
412
+ }
413
+
414
+ /* The panel is fixed over the right of the page, flush with the container
415
+ edge. Below xl there is no room to dodge it and it simply covers part of
416
+ the preview; from there the preview keeps to the strip beside it,
417
+ because a switch that moves something hidden behind the panel
418
+ demonstrates nothing. */
419
+ @media (width >= 1200px) {
420
+ body:has(.palette-panel:not([hidden])) #preview > .mw-container {
421
+ padding-inline-end: 460px;
422
+ }
423
+ }
424
+
425
+ /* Both wrap rather than squeezing a field to nothing on a phone */
426
+ .preview-row,
427
+ .preview-actions {
428
+ display: flex;
429
+ flex-wrap: wrap;
430
+ align-items: center;
431
+ gap: 0.75rem;
432
+ }
433
+
434
+ .preview-row .mw-input {
435
+ flex: 1 1 12rem;
436
+ }
437
+
438
+ /* The one thing here that keeps moving without being touched, so Motion
439
+ tempo has something to show that a hover state cannot */
440
+ .preview-status {
441
+ display: flex;
442
+ align-items: center;
443
+ gap: 14px;
444
+ margin: 0 0 1.25rem;
445
+ font-size: 0.82rem;
446
+ color: var(--mw-text-muted-color);
447
+ }
448
+
449
+ .preview-status strong {
450
+ display: block;
451
+ color: var(--mw-text-color);
452
+ font-weight: 500;
453
+ }
454
+
455
+ .preview-status small {
456
+ font-size: 0.75rem;
457
+ }
458
+
459
+ .preview-row .mw-select {
460
+ flex: 0 1 14rem;
461
+ }
462
+
463
+ /* The spacing sits on the row and not on the label: the button is taller
464
+ than the caption beside it, so a margin under the label leaves the
465
+ button itself resting on the box */
466
+ .variant-setup-head {
467
+ display: flex;
468
+ align-items: baseline;
469
+ justify-content: space-between;
470
+ gap: 10px;
471
+ margin: 14px 0 10px;
472
+ }
473
+
342
474
  .variant-setup-label {
343
475
  display: block;
344
- margin-top: 14px;
345
- margin-bottom: 5px;
476
+ margin: 0;
346
477
  font-size: 0.7rem;
347
478
  color: var(--mw-text-muted-color);
348
479
  }
349
480
 
481
+ /* The one control here that leads somewhere - everything else in the
482
+ panel only changes what is on screen. Carries the brand colour so it
483
+ reads as the way out rather than as one more switch. */
484
+ .variant-share {
485
+ display: inline-flex;
486
+ align-items: center;
487
+ gap: 6px;
488
+ flex-shrink: 0;
489
+ padding: 5px 12px;
490
+ border-radius: 999px;
491
+ border: 1px solid var(--mw-primary-color);
492
+ background: var(--mw-primary-color);
493
+ color: var(--mw-primary-accent-text-color);
494
+ font: inherit;
495
+ font-size: 0.72rem;
496
+ font-weight: 500;
497
+ cursor: pointer;
498
+ transition: var(--mw-transition-fast);
499
+ }
500
+
501
+ .variant-share:hover {
502
+ background: var(--mw-primary-color-hover, var(--mw-primary-color));
503
+ transform: translateY(-1px);
504
+ }
505
+
506
+ .variant-share:active {
507
+ transform: translateY(0);
508
+ }
509
+
510
+ .variant-share:focus-visible {
511
+ outline: var(--mw-focus-ring-width) solid var(--mw-focus-ring-color);
512
+ outline-offset: var(--mw-focus-ring-offset);
513
+ }
514
+
350
515
  .variant-setup {
351
516
  display: block;
352
517
  width: 100%;
@@ -362,22 +527,6 @@
362
527
  white-space: pre-wrap;
363
528
  word-break: break-word;
364
529
  }
365
-
366
- @media (max-width: 480px) {
367
- .palette-fab {
368
- right: 12px;
369
- bottom: 12px;
370
- width: 46px;
371
- height: 46px;
372
- }
373
-
374
- .palette-panel {
375
- right: 12px;
376
- left: 12px;
377
- bottom: 68px;
378
- width: auto;
379
- }
380
- }
381
530
  </style>
382
531
 
383
532
  <script src="maverick-wave.min.js?v={{VERSION}}"></script>
@@ -387,7 +536,7 @@
387
536
  <a class="mw-skip-link" href="#main">Skip to content</a>
388
537
 
389
538
  <!-- Header -->
390
- <header class="mw-header mw-header-reveal mw-localhost-indicator-activated">
539
+ <header class="mw-header mw-localhost-indicator-activated">
391
540
  <div class="mw-container">
392
541
  @@include('./src/partials/header-container.html')
393
542
  </div>
@@ -809,6 +958,18 @@
809
958
  @@include('./src/partials/history-container.html')
810
959
  </div>
811
960
  </section>
961
+ <!-- Preview section, showcase only - see the partial -->
962
+ <section id="preview" class="mw-section">
963
+ <div class="mw-container">
964
+ <h2 class="mw-section-title">Preview</h2>
965
+ <p class="mw-section-intro">
966
+ One page-shaped example, so the variant switches in the palette
967
+ panel have something to act on that is not a component gallery. Open
968
+ the panel and the page lands here.
969
+ </p>
970
+ @@include('./src/partials/preview-container.html')
971
+ </div>
972
+ </section>
812
973
  </main>
813
974
 
814
975
  <!-- Footer -->
@@ -908,12 +1069,12 @@
908
1069
  // root element repaints the page - bar, ink, borders and tints included.
909
1070
  // Nothing is persisted - a palette is for looking at, a reload is the way back.
910
1071
  document.addEventListener('DOMContentLoaded', () => {
911
- const fab = document.getElementById('palette-fab');
1072
+ const toggleBtn = document.getElementById('palette-toggle');
912
1073
  const panel = document.getElementById('palette-panel');
913
1074
  const primaryInput = document.getElementById('palette-primary');
914
1075
  const secondaryInput = document.getElementById('palette-secondary');
915
1076
  const items = [...document.querySelectorAll('.palette-item')];
916
- if (!fab || !panel) return;
1077
+ if (!toggleBtn || !panel) return;
917
1078
 
918
1079
  const root = document.documentElement;
919
1080
 
@@ -999,12 +1160,24 @@
999
1160
  input.addEventListener('change', settle);
1000
1161
  });
1001
1162
 
1163
+ const preview = document.getElementById('preview');
1164
+
1002
1165
  const toggle = (open) => {
1003
1166
  panel.hidden = !open;
1004
- fab.setAttribute('aria-expanded', String(open));
1167
+ toggleBtn.setAttribute('aria-expanded', String(open));
1168
+
1169
+ // Or the switches act on whatever section the reader is parked at,
1170
+ // which is one component at a time and says little about a look.
1171
+ //
1172
+ // `instant` against the smooth default in the reset: the preview sits
1173
+ // at the end of a page some 80 000px long, and over that distance a
1174
+ // smooth scroll either crawls through every section on the way or, as
1175
+ // here, never starts at all.
1176
+ if (open)
1177
+ preview?.scrollIntoView({ block: 'start', behavior: 'instant' });
1005
1178
  };
1006
1179
 
1007
- fab.addEventListener('click', (event) => {
1180
+ toggleBtn.addEventListener('click', (event) => {
1008
1181
  event.stopPropagation();
1009
1182
  toggle(panel.hidden);
1010
1183
  });
@@ -1102,6 +1275,58 @@
1102
1275
  const root = document.documentElement;
1103
1276
  const controls = [...list.querySelectorAll('input, select')];
1104
1277
 
1278
+ // Built on every render, put into the address bar by nobody: a setup
1279
+ // written to the URL as you go comes back on the next reload, and then
1280
+ // the showcase opens in someone else's look for good. Copy link is the
1281
+ // one way out of here, and what it hands over is this.
1282
+ //
1283
+ // URLSearchParams and not a hand-rolled separator: a value like the
1284
+ // heading font or the page width is full of commas, quotes and brackets,
1285
+ // and escaping those by hand is where this kind of thing breaks.
1286
+ let shareUrl = location.origin + location.pathname;
1287
+
1288
+ const buildShareUrl = (classes, dropped, pairs) => {
1289
+ const params = new URLSearchParams();
1290
+ if (classes.length) params.set('c', classes.join(' '));
1291
+ if (dropped.length) params.set('d', dropped.join(' '));
1292
+ pairs.forEach(([prop, value]) => params.set(prop, value));
1293
+
1294
+ const query = params.toString();
1295
+ return (
1296
+ location.origin + location.pathname + (query ? '#' + query : '')
1297
+ );
1298
+ };
1299
+
1300
+ // Back into the shape load() reads, so the link and a pasted setup go
1301
+ // through one parser rather than two that can drift apart. The target in
1302
+ // front of `: no` is only there to make the line read like the printed
1303
+ // one - load() matches on the class name alone.
1304
+ const readHash = () => {
1305
+ const params = new URLSearchParams(location.hash.slice(1));
1306
+ const lines = [];
1307
+
1308
+ const classes = params.get('c');
1309
+ if (classes) lines.push('html class="' + classes + '"');
1310
+
1311
+ (params.get('d') || '')
1312
+ .split(/\s+/)
1313
+ .filter(Boolean)
1314
+ .forEach((name) => lines.push('markup: no ' + name));
1315
+
1316
+ const pairs = [...params.entries()].filter(([key]) =>
1317
+ key.startsWith('--')
1318
+ );
1319
+ if (pairs.length) {
1320
+ lines.push(
1321
+ ':root {',
1322
+ ...pairs.map(([key, value]) => ` ${key}: ${value};`),
1323
+ '}'
1324
+ );
1325
+ }
1326
+
1327
+ return lines.length ? lines.join('\n') : null;
1328
+ };
1329
+
1105
1330
  const isOn = (input) =>
1106
1331
  input.hasAttribute('data-variant-invert')
1107
1332
  ? !input.checked
@@ -1113,8 +1338,10 @@
1113
1338
  if (document.activeElement === setup) return;
1114
1339
 
1115
1340
  const classes = [];
1341
+ // Unformatted, because both the box and the link are written from
1342
+ // these and formatting only one of the two ways is a bug waiting
1116
1343
  const notes = [];
1117
- const props = [];
1344
+ const pairs = [];
1118
1345
 
1119
1346
  // The palette is part of the setup, and it is printed first because a
1120
1347
  // project sets its brand colours before it retunes anything else
@@ -1127,15 +1354,18 @@
1127
1354
  ['--mw-dark-page-background', palette.dark],
1128
1355
  ['--mw-light-page-background', palette.light],
1129
1356
  ].forEach(([prop, value]) => {
1130
- if (value) props.push(` ${prop}: ${value};`);
1357
+ if (value) pairs.push([prop, value]);
1131
1358
  });
1132
1359
 
1133
1360
  controls.forEach((control) => {
1134
1361
  if (control.type !== 'checkbox') {
1135
- if (control.value) {
1136
- props.push(
1137
- ` ${control.dataset.variantProp}: ${control.value};`
1138
- );
1362
+ // A select whose options are class names picks one of a set the
1363
+ // markup cannot hold at the same time - a shape is one or none
1364
+ if (!control.value) return;
1365
+ if (control.hasAttribute('data-variant-classes')) {
1366
+ classes.push(control.value);
1367
+ } else {
1368
+ pairs.push([control.dataset.variantProp, control.value]);
1139
1369
  }
1140
1370
  return;
1141
1371
  }
@@ -1145,7 +1375,7 @@
1145
1375
  const target = control.dataset.variantTarget;
1146
1376
  if (target) {
1147
1377
  if (!isOn(control)) {
1148
- notes.push(target + ': no ' + control.dataset.variantClass);
1378
+ notes.push([target, control.dataset.variantClass]);
1149
1379
  }
1150
1380
  } else if (isOn(control)) {
1151
1381
  classes.push(control.dataset.variantClass);
@@ -1156,19 +1386,37 @@
1156
1386
  if (classes.length) {
1157
1387
  lines.push('html class="' + classes.join(' ') + '"');
1158
1388
  }
1159
- lines.push(...notes);
1160
- if (props.length) lines.push(':root {', ...props, '}');
1389
+ lines.push(...notes.map(([target, name]) => `${target}: no ${name}`));
1390
+ if (pairs.length) {
1391
+ lines.push(
1392
+ ':root {',
1393
+ ...pairs.map(([prop, value]) => ` ${prop}: ${value};`),
1394
+ '}'
1395
+ );
1396
+ }
1161
1397
 
1162
1398
  setup.value = lines.length
1163
1399
  ? lines.join('\n')
1164
1400
  : 'Default setup - nothing to change.';
1401
+
1402
+ shareUrl = buildShareUrl(
1403
+ classes,
1404
+ notes.map(([, name]) => name),
1405
+ pairs
1406
+ );
1165
1407
  };
1166
1408
 
1409
+ const optionClasses = (control) =>
1410
+ [...control.options].map((option) => option.value).filter(Boolean);
1411
+
1167
1412
  const apply = (control) => {
1168
1413
  if (control.type === 'checkbox') {
1169
1414
  const target = control.dataset.variantTarget;
1170
1415
  const el = target ? document.querySelector(target) : root;
1171
1416
  el?.classList.toggle(control.dataset.variantClass, isOn(control));
1417
+ } else if (control.hasAttribute('data-variant-classes')) {
1418
+ root.classList.remove(...optionClasses(control));
1419
+ if (control.value) root.classList.add(control.value);
1172
1420
  } else if (control.value) {
1173
1421
  root.style.setProperty(control.dataset.variantProp, control.value);
1174
1422
  } else {
@@ -1214,6 +1462,9 @@
1214
1462
  control.checked = control.hasAttribute('data-variant-invert')
1215
1463
  ? !on
1216
1464
  : on;
1465
+ } else if (control.hasAttribute('data-variant-classes')) {
1466
+ control.value =
1467
+ optionClasses(control).find((name) => named.has(name)) ?? '';
1217
1468
  } else {
1218
1469
  control.value = props.get(control.dataset.variantProp) ?? '';
1219
1470
  }
@@ -1224,21 +1475,103 @@
1224
1475
  // Picking a palette repaints the page, and the box has to say so
1225
1476
  window.mwRefreshSetup = render;
1226
1477
 
1227
- setup.addEventListener('input', () => load(setup.value));
1478
+ setup.addEventListener('input', () => inPlace(() => load(setup.value)));
1228
1479
  // Normalises what was pasted back into the shape render() writes
1229
1480
  setup.addEventListener('blur', render);
1230
1481
 
1482
+ // Type and spacing switches change the height of all 80 000px of page
1483
+ // above the preview as well, so the section slides out from under
1484
+ // whoever is watching it - Text size alone moves it by 16 000px. Hold
1485
+ // its top edge where it was and the change happens in place.
1486
+ //
1487
+ // Only while the preview is on screen: correcting the scroll for someone
1488
+ // parked up in the component sections would move *them* by those 16 000
1489
+ // instead. `instant` because the smooth default would animate the
1490
+ // correction as a scroll of its own.
1491
+ const previewSection = document.getElementById('preview');
1492
+
1493
+ const inPlace = (change) => {
1494
+ const box = previewSection?.getBoundingClientRect();
1495
+ const watching =
1496
+ box && box.bottom > 0 && box.top < window.innerHeight;
1497
+ if (!watching) return change();
1498
+
1499
+ const before = box.top;
1500
+ change();
1501
+ const shift = previewSection.getBoundingClientRect().top - before;
1502
+ if (shift) window.scrollBy({ top: shift, behavior: 'instant' });
1503
+ };
1504
+
1231
1505
  controls.forEach((control) =>
1232
- control.addEventListener('change', () => {
1233
- apply(control);
1234
- render();
1235
- })
1506
+ control.addEventListener('change', () =>
1507
+ inPlace(() => {
1508
+ apply(control);
1509
+ render();
1510
+ })
1511
+ )
1236
1512
  );
1237
1513
 
1238
- // Applied on load too, so a browser that restored a checked box after a
1239
- // reload does not leave the page and the panel disagreeing
1240
- controls.forEach(apply);
1241
- render();
1514
+ const shared = readHash();
1515
+ if (shared) {
1516
+ // Drives the switches, which drive the page - then render() normalises
1517
+ // the box and writes the link back in the order it prints
1518
+ load(shared);
1519
+ render();
1520
+ } else {
1521
+ // Without a link the showcase opens in its own look, every time. That
1522
+ // has to be spelled out: Firefox restores form state across a reload,
1523
+ // so the switches would otherwise come back set the way the last visit
1524
+ // left them, and the page with them.
1525
+ //
1526
+ // The markup is the one source for that starting state - the written
1527
+ // defaults for a plain switch, and for a targeted one whether the
1528
+ // class it stands for is actually on the element it names. A header
1529
+ // shipped without mw-header-reveal keeps it off.
1530
+ controls.forEach((control) => {
1531
+ const target = control.dataset.variantTarget;
1532
+ const el = target && document.querySelector(target);
1533
+
1534
+ if (el) {
1535
+ const present = el.classList.contains(
1536
+ control.dataset.variantClass
1537
+ );
1538
+ control.checked = control.hasAttribute('data-variant-invert')
1539
+ ? !present
1540
+ : present;
1541
+ } else if (control.type === 'checkbox') {
1542
+ control.checked = control.defaultChecked;
1543
+ } else {
1544
+ const preset = [...control.options].find(
1545
+ (o) => o.defaultSelected
1546
+ );
1547
+ control.value = preset ? preset.value : '';
1548
+ }
1549
+ });
1550
+
1551
+ controls.forEach(apply);
1552
+ render();
1553
+ }
1554
+
1555
+ document
1556
+ .getElementById('variant-share')
1557
+ ?.addEventListener('click', async (event) => {
1558
+ const button = event.currentTarget;
1559
+ const label = document.getElementById('variant-share-label');
1560
+ try {
1561
+ await navigator.clipboard.writeText(shareUrl);
1562
+ label.textContent = 'Copied';
1563
+ } catch {
1564
+ // Denied permission or an insecure origin. The address bar is no
1565
+ // help here - it never carries the setup - so hand the link over
1566
+ // in the box instead, where it can be selected by hand.
1567
+ setup.value = shareUrl;
1568
+ setup.select();
1569
+ label.textContent = 'Copy from the box';
1570
+ }
1571
+ setTimeout(() => {
1572
+ label.textContent = 'Copy link';
1573
+ }, 1600);
1574
+ });
1242
1575
  });
1243
1576
 
1244
1577
  // for the modals - the framework handles both shapes, div and <dialog>