maverick-wave 5.8.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 (43) 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 +48 -25
  8. package/CHANGELOG.md +13 -0
  9. package/README.md +41 -24
  10. package/index.html +325 -20
  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 +7 -1
  15. package/src/js/main.js +24 -5
  16. package/src/partials/palette-container.html +105 -65
  17. package/src/partials/preview-container.html +136 -0
  18. package/src/partials/utilities-container.html +38 -0
  19. package/src/scss/abstracts/_functions.scss +31 -3
  20. package/src/scss/abstracts/_mixins.scss +62 -12
  21. package/src/scss/abstracts/_variables.scss +47 -21
  22. package/src/scss/base/_base.scss +97 -8
  23. package/src/scss/components/_badge.scss +1 -1
  24. package/src/scss/components/_buttons.scss +1 -1
  25. package/src/scss/components/_cards.scss +23 -14
  26. package/src/scss/components/_dropdown.scss +58 -1
  27. package/src/scss/components/_kanban.scss +10 -9
  28. package/src/scss/components/_lang-switch.scss +1 -1
  29. package/src/scss/components/_localhost-indicator.scss +1 -1
  30. package/src/scss/components/_modals.scss +13 -1
  31. package/src/scss/components/_pricing.scss +1 -1
  32. package/src/scss/components/_progress.scss +9 -5
  33. package/src/scss/components/_skeleton.scss +1 -1
  34. package/src/scss/components/_spinners.scss +5 -5
  35. package/src/scss/components/_stepper.scss +4 -1
  36. package/src/scss/components/_tabs.scss +28 -8
  37. package/src/scss/components/_tiles.scss +41 -16
  38. package/src/scss/components/_timelines.scss +12 -5
  39. package/src/scss/layout/_main.scss +1 -1
  40. package/src/scss/layout/_parallax.scss +10 -5
  41. package/src/scss/utilities/_elevation.scss +23 -0
  42. package/src/scss/utilities/_touch-targets.scss +1 -1
  43. package/src/scss/utilities/_variants.scss +74 -7
package/index.html CHANGED
@@ -292,6 +292,26 @@
292
292
  border-top: 1px solid var(--mw-border);
293
293
  }
294
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
+
295
315
  .variant-row {
296
316
  display: flex;
297
317
  align-items: center;
@@ -369,14 +389,129 @@
369
389
  background: var(--mw-primary-accent-text-color);
370
390
  }
371
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
+
372
474
  .variant-setup-label {
373
475
  display: block;
374
- margin-top: 14px;
375
- margin-bottom: 5px;
476
+ margin: 0;
376
477
  font-size: 0.7rem;
377
478
  color: var(--mw-text-muted-color);
378
479
  }
379
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
+
380
515
  .variant-setup {
381
516
  display: block;
382
517
  width: 100%;
@@ -401,7 +536,7 @@
401
536
  <a class="mw-skip-link" href="#main">Skip to content</a>
402
537
 
403
538
  <!-- Header -->
404
- <header class="mw-header mw-header-reveal mw-localhost-indicator-activated">
539
+ <header class="mw-header mw-localhost-indicator-activated">
405
540
  <div class="mw-container">
406
541
  @@include('./src/partials/header-container.html')
407
542
  </div>
@@ -823,6 +958,18 @@
823
958
  @@include('./src/partials/history-container.html')
824
959
  </div>
825
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>
826
973
  </main>
827
974
 
828
975
  <!-- Footer -->
@@ -1013,9 +1160,21 @@
1013
1160
  input.addEventListener('change', settle);
1014
1161
  });
1015
1162
 
1163
+ const preview = document.getElementById('preview');
1164
+
1016
1165
  const toggle = (open) => {
1017
1166
  panel.hidden = !open;
1018
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' });
1019
1178
  };
1020
1179
 
1021
1180
  toggleBtn.addEventListener('click', (event) => {
@@ -1116,6 +1275,58 @@
1116
1275
  const root = document.documentElement;
1117
1276
  const controls = [...list.querySelectorAll('input, select')];
1118
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
+
1119
1330
  const isOn = (input) =>
1120
1331
  input.hasAttribute('data-variant-invert')
1121
1332
  ? !input.checked
@@ -1127,8 +1338,10 @@
1127
1338
  if (document.activeElement === setup) return;
1128
1339
 
1129
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
1130
1343
  const notes = [];
1131
- const props = [];
1344
+ const pairs = [];
1132
1345
 
1133
1346
  // The palette is part of the setup, and it is printed first because a
1134
1347
  // project sets its brand colours before it retunes anything else
@@ -1141,7 +1354,7 @@
1141
1354
  ['--mw-dark-page-background', palette.dark],
1142
1355
  ['--mw-light-page-background', palette.light],
1143
1356
  ].forEach(([prop, value]) => {
1144
- if (value) props.push(` ${prop}: ${value};`);
1357
+ if (value) pairs.push([prop, value]);
1145
1358
  });
1146
1359
 
1147
1360
  controls.forEach((control) => {
@@ -1152,9 +1365,7 @@
1152
1365
  if (control.hasAttribute('data-variant-classes')) {
1153
1366
  classes.push(control.value);
1154
1367
  } else {
1155
- props.push(
1156
- ` ${control.dataset.variantProp}: ${control.value};`
1157
- );
1368
+ pairs.push([control.dataset.variantProp, control.value]);
1158
1369
  }
1159
1370
  return;
1160
1371
  }
@@ -1164,7 +1375,7 @@
1164
1375
  const target = control.dataset.variantTarget;
1165
1376
  if (target) {
1166
1377
  if (!isOn(control)) {
1167
- notes.push(target + ': no ' + control.dataset.variantClass);
1378
+ notes.push([target, control.dataset.variantClass]);
1168
1379
  }
1169
1380
  } else if (isOn(control)) {
1170
1381
  classes.push(control.dataset.variantClass);
@@ -1175,12 +1386,24 @@
1175
1386
  if (classes.length) {
1176
1387
  lines.push('html class="' + classes.join(' ') + '"');
1177
1388
  }
1178
- lines.push(...notes);
1179
- 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
+ }
1180
1397
 
1181
1398
  setup.value = lines.length
1182
1399
  ? lines.join('\n')
1183
1400
  : 'Default setup - nothing to change.';
1401
+
1402
+ shareUrl = buildShareUrl(
1403
+ classes,
1404
+ notes.map(([, name]) => name),
1405
+ pairs
1406
+ );
1184
1407
  };
1185
1408
 
1186
1409
  const optionClasses = (control) =>
@@ -1252,21 +1475,103 @@
1252
1475
  // Picking a palette repaints the page, and the box has to say so
1253
1476
  window.mwRefreshSetup = render;
1254
1477
 
1255
- setup.addEventListener('input', () => load(setup.value));
1478
+ setup.addEventListener('input', () => inPlace(() => load(setup.value)));
1256
1479
  // Normalises what was pasted back into the shape render() writes
1257
1480
  setup.addEventListener('blur', render);
1258
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
+
1259
1505
  controls.forEach((control) =>
1260
- control.addEventListener('change', () => {
1261
- apply(control);
1262
- render();
1263
- })
1506
+ control.addEventListener('change', () =>
1507
+ inPlace(() => {
1508
+ apply(control);
1509
+ render();
1510
+ })
1511
+ )
1264
1512
  );
1265
1513
 
1266
- // Applied on load too, so a browser that restored a checked box after a
1267
- // reload does not leave the page and the panel disagreeing
1268
- controls.forEach(apply);
1269
- 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
+ });
1270
1575
  });
1271
1576
 
1272
1577
  // for the modals - the framework handles both shapes, div and <dialog>