maverick-wave 5.38.0 → 6.0.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/README.md +63 -47
- package/maverick-wave.min.css +94 -68
- package/maverick-wave.min.js +1 -1
- package/package.json +7 -7
- package/src/js/main.js +382 -57
- package/src/scss/abstracts/_mixins.scss +35 -24
- package/src/scss/abstracts/_variables.scss +15 -0
- package/src/scss/base/_base.scss +107 -64
- package/src/scss/base/_index.scss +1 -0
- package/src/scss/base/_page-transitions.scss +29 -0
- package/src/scss/base/_reset.scss +0 -8
- package/src/scss/components/_accordions.scss +23 -18
- package/src/scss/components/_action-bar.scss +143 -0
- package/src/scss/components/_alerts.scss +13 -1
- package/src/scss/components/_avatars.scss +17 -0
- package/src/scss/components/_badge.scss +12 -0
- package/src/scss/components/_breadcrumbs.scss +10 -2
- package/src/scss/components/_chat.scss +250 -0
- package/src/scss/components/_code.scss +3 -1
- package/src/scss/components/_compare.scss +135 -0
- package/src/scss/components/_deck.scss +8 -14
- package/src/scss/components/_devices.scss +323 -0
- package/src/scss/components/_dropdown.scss +43 -2
- package/src/scss/components/_empty-state.scss +43 -0
- package/src/scss/components/_flag.scss +16 -0
- package/src/scss/components/_index.scss +6 -0
- package/src/scss/components/_lightbox.scss +1 -1
- package/src/scss/components/_marquee.scss +132 -0
- package/src/scss/components/_modals.scss +112 -83
- package/src/scss/components/_occasions.scss +62 -11
- package/src/scss/components/_pagination.scss +91 -0
- package/src/scss/components/_portrait-gallery.scss +1 -1
- package/src/scss/components/_progress.scss +122 -2
- package/src/scss/components/_segmented.scss +65 -2
- package/src/scss/components/_skeleton.scss +8 -0
- package/src/scss/components/_spinners.scss +38 -0
- package/src/scss/components/_stat.scss +232 -0
- package/src/scss/components/_stepper.scss +42 -2
- package/src/scss/components/_stories.scss +36 -22
- package/src/scss/components/_tables.scss +160 -17
- package/src/scss/components/_tabs.scss +42 -4
- package/src/scss/components/_tags.scss +39 -4
- package/src/scss/components/_tiles.scss +1 -1
- package/src/scss/components/_timelines.scss +138 -1
- package/src/scss/components/_toasts.scss +5 -1
- package/src/scss/components/_tooltip.scss +13 -0
- package/src/scss/form-elements/_checkbox.scss +40 -7
- package/src/scss/form-elements/_form.scss +2 -4
- package/src/scss/form-elements/_input.scss +9 -6
- package/src/scss/form-elements/_radio.scss +32 -3
- package/src/scss/form-elements/_select.scss +44 -1
- package/src/scss/form-elements/_slider.scss +18 -0
- package/src/scss/form-elements/_textarea.scss +9 -6
- package/src/scss/form-elements/_toggle.scss +37 -4
- package/src/scss/layout/_app-shell.scss +661 -0
- package/src/scss/layout/_bento.scss +43 -0
- package/src/scss/layout/_footer.scss +114 -2
- package/src/scss/layout/_grid.scss +7 -2
- package/src/scss/layout/_header-reveal.scss +30 -17
- package/src/scss/layout/_header.scss +21 -4
- package/src/scss/layout/_index.scss +2 -0
- package/src/scss/layout/_parallax.scss +13 -11
- package/src/scss/utilities/_accessibility.scss +4 -19
- package/src/scss/utilities/_index.scss +1 -0
- package/src/scss/utilities/_print.scss +7 -3
- package/src/scss/utilities/_reveal.scss +25 -17
- package/src/scss/utilities/_shape.scss +24 -0
- package/src/scss/utilities/_touch-targets.scss +23 -0
- package/src/scss/utilities/_variants.scss +25 -6
package/src/js/main.js
CHANGED
|
@@ -28,6 +28,27 @@
|
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Blocked storage (all cookies blocked, a sandboxed iframe) throws on access
|
|
32
|
+
const storage = {
|
|
33
|
+
get(key) {
|
|
34
|
+
try {
|
|
35
|
+
return localStorage.getItem(key);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
set(key, value) {
|
|
41
|
+
try {
|
|
42
|
+
localStorage.setItem(key, value);
|
|
43
|
+
} catch {}
|
|
44
|
+
},
|
|
45
|
+
remove(key) {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(key);
|
|
48
|
+
} catch {}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
31
52
|
// Bound to the document, the window, or the one element of its kind a page
|
|
32
53
|
// has. Running any of this twice would double the listeners, so it happens
|
|
33
54
|
// once and is not part of init().
|
|
@@ -40,6 +61,7 @@
|
|
|
40
61
|
initThemeToggle();
|
|
41
62
|
initColorSwatches();
|
|
42
63
|
initMobileNav();
|
|
64
|
+
initAppShell();
|
|
43
65
|
initHeaderReveal();
|
|
44
66
|
initParallax();
|
|
45
67
|
initSmoothScrolling();
|
|
@@ -65,6 +87,8 @@
|
|
|
65
87
|
initMosaics(scope);
|
|
66
88
|
initStoryReels(scope);
|
|
67
89
|
initDecks(scope);
|
|
90
|
+
initDevices(scope);
|
|
91
|
+
initAppShells(scope);
|
|
68
92
|
initAccordions(scope);
|
|
69
93
|
initProgressBars(scope);
|
|
70
94
|
initReveals(scope);
|
|
@@ -72,6 +96,7 @@
|
|
|
72
96
|
initAlerts(scope);
|
|
73
97
|
initFormSliders(scope);
|
|
74
98
|
initImageSliders(scope);
|
|
99
|
+
initCompares(scope);
|
|
75
100
|
initCheckboxLists(scope);
|
|
76
101
|
initKanbanBoards(scope);
|
|
77
102
|
initCalendars(scope);
|
|
@@ -818,6 +843,41 @@
|
|
|
818
843
|
});
|
|
819
844
|
}
|
|
820
845
|
|
|
846
|
+
// ===== Devices =====
|
|
847
|
+
// A device written as a button tours its screenshot on a tap: to the end at
|
|
848
|
+
// the pace the CSS sets, a pause to take the last screen in, then back. The
|
|
849
|
+
// laptop's hover is CSS alone - this is the touch-side equivalent.
|
|
850
|
+
function initDevices(root) {
|
|
851
|
+
root.querySelectorAll('button.mw-device').forEach((device) => {
|
|
852
|
+
if (!fresh(device)) return;
|
|
853
|
+
const screen = device.querySelector('.mw-device-screen');
|
|
854
|
+
const shot = screen && screen.querySelector(':scope > img');
|
|
855
|
+
if (!shot) return;
|
|
856
|
+
|
|
857
|
+
let hold = 0;
|
|
858
|
+
const stop = () => {
|
|
859
|
+
clearTimeout(hold);
|
|
860
|
+
setActive(device, false);
|
|
861
|
+
device.setAttribute('aria-pressed', 'false');
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
device.setAttribute('aria-pressed', 'false');
|
|
865
|
+
device.addEventListener('click', () => {
|
|
866
|
+
if (isActive(device)) return stop();
|
|
867
|
+
// A screenshot no taller than the glass has nowhere to go, and a tour
|
|
868
|
+
// that never ends would leave the button pressed
|
|
869
|
+
if (shot.offsetHeight <= screen.clientHeight) return;
|
|
870
|
+
setActive(device, true);
|
|
871
|
+
device.setAttribute('aria-pressed', 'true');
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
shot.addEventListener('transitionend', (event) => {
|
|
875
|
+
if (event.propertyName !== 'translate' || !isActive(device)) return;
|
|
876
|
+
hold = setTimeout(stop, 1200);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
821
881
|
// ===== Decks =====
|
|
822
882
|
// The swipe is the browser's own scroll snap. This adds what snapping cannot:
|
|
823
883
|
// buttons, keys and the wheel, counter, progress, fullscreen and the entrance.
|
|
@@ -897,9 +957,7 @@
|
|
|
897
957
|
|
|
898
958
|
function show(index, smooth = true) {
|
|
899
959
|
const slide = slides[Math.max(0, Math.min(slides.length - 1, index))];
|
|
900
|
-
const reduced =
|
|
901
|
-
window.matchMedia &&
|
|
902
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
960
|
+
const reduced = reducedMotion();
|
|
903
961
|
scroller.scrollTo({
|
|
904
962
|
left: across ? slide.offsetLeft : 0,
|
|
905
963
|
top: across ? 0 : slide.offsetTop,
|
|
@@ -1091,7 +1149,7 @@
|
|
|
1091
1149
|
themeToggle.setAttribute('aria-disabled', 'true');
|
|
1092
1150
|
|
|
1093
1151
|
// Clean up potentially conflicting localStorage
|
|
1094
|
-
|
|
1152
|
+
storage.remove('mw-theme');
|
|
1095
1153
|
|
|
1096
1154
|
// Ensure the theme classes are gone for fixed mode
|
|
1097
1155
|
root.classList.remove('mw-theme-light', 'mw-theme-dark');
|
|
@@ -1101,15 +1159,9 @@
|
|
|
1101
1159
|
|
|
1102
1160
|
// mode switchable - only runs if themeMode === 'switchable'
|
|
1103
1161
|
const systemLight = window.matchMedia('(prefers-color-scheme: light)');
|
|
1162
|
+
const preferred = () => systemLight.matches;
|
|
1104
1163
|
|
|
1105
|
-
|
|
1106
|
-
// the theme tokens sit on :root as the dark set and the light one arrives by
|
|
1107
|
-
// class alone, so a light machine would get a sun icon over a dark page.
|
|
1108
|
-
const followsSystem =
|
|
1109
|
-
window.CSS && CSS.supports('color', 'light-dark(#000, #fff)');
|
|
1110
|
-
const preferred = () => followsSystem && systemLight.matches;
|
|
1111
|
-
|
|
1112
|
-
const stored = localStorage.getItem('mw-theme');
|
|
1164
|
+
const stored = storage.get('mw-theme');
|
|
1113
1165
|
let isLight = stored ? stored === 'light' : preferred();
|
|
1114
1166
|
|
|
1115
1167
|
// Function to apply theme styles and icon
|
|
@@ -1137,7 +1189,7 @@
|
|
|
1137
1189
|
|
|
1138
1190
|
// Until the reader picks a side, the page keeps tracking the OS switch
|
|
1139
1191
|
systemLight.addEventListener('change', () => {
|
|
1140
|
-
if (
|
|
1192
|
+
if (storage.get('mw-theme')) return;
|
|
1141
1193
|
isLight = preferred();
|
|
1142
1194
|
applyTheme(isLight);
|
|
1143
1195
|
updateColorSwatchHexValues();
|
|
@@ -1147,7 +1199,7 @@
|
|
|
1147
1199
|
themeToggle.addEventListener('click', () => {
|
|
1148
1200
|
isLight = !isLight;
|
|
1149
1201
|
applyTheme(isLight);
|
|
1150
|
-
|
|
1202
|
+
storage.set('mw-theme', isLight ? 'light' : 'dark');
|
|
1151
1203
|
|
|
1152
1204
|
// No transition left to wait out, so the swatches can read the new values now
|
|
1153
1205
|
updateColorSwatchHexValues();
|
|
@@ -1280,6 +1332,197 @@
|
|
|
1280
1332
|
});
|
|
1281
1333
|
}
|
|
1282
1334
|
|
|
1335
|
+
// ===== App Shell =====
|
|
1336
|
+
// Only classes change, so an application can drive the same states itself
|
|
1337
|
+
// Where _app-shell.scss docks the sidebar (lg) and the aside (xl)
|
|
1338
|
+
function appBreakpoints() {
|
|
1339
|
+
return {
|
|
1340
|
+
sidebar: window.matchMedia('(width >= 992px)'),
|
|
1341
|
+
aside: window.matchMedia('(width >= 1200px)'),
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// The panel lying over the content, if one does
|
|
1346
|
+
function appOverlay(app, docked) {
|
|
1347
|
+
if (
|
|
1348
|
+
!docked.sidebar.matches &&
|
|
1349
|
+
app.classList.contains('mw-app-sidebar-open')
|
|
1350
|
+
)
|
|
1351
|
+
return app.querySelector('.mw-app-sidebar');
|
|
1352
|
+
if (!docked.aside.matches && app.classList.contains('mw-app-aside-open'))
|
|
1353
|
+
return app.querySelector('.mw-app-aside');
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Whatever an open panel covers goes inert, and every toggle states its panel
|
|
1358
|
+
function syncAppShell(app, docked) {
|
|
1359
|
+
const panel = appOverlay(app, docked);
|
|
1360
|
+
[...app.children].forEach((child) => {
|
|
1361
|
+
child.inert = Boolean(panel) && child !== panel;
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const sidebarOpen = docked.sidebar.matches
|
|
1365
|
+
? !app.classList.contains('mw-app-collapsed')
|
|
1366
|
+
: app.classList.contains('mw-app-sidebar-open');
|
|
1367
|
+
const states = [
|
|
1368
|
+
['[data-mw-app-toggle]', '.mw-app-sidebar', sidebarOpen],
|
|
1369
|
+
[
|
|
1370
|
+
'[data-mw-app-aside-toggle]',
|
|
1371
|
+
'.mw-app-aside',
|
|
1372
|
+
app.classList.contains('mw-app-aside-open'),
|
|
1373
|
+
],
|
|
1374
|
+
];
|
|
1375
|
+
states.forEach(([toggles, selector, open]) => {
|
|
1376
|
+
const target = app.querySelector(selector);
|
|
1377
|
+
app.querySelectorAll(toggles).forEach((toggle) => {
|
|
1378
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
1379
|
+
if (target && target.id && !toggle.hasAttribute('aria-controls')) {
|
|
1380
|
+
toggle.setAttribute('aria-controls', target.id);
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// `data-mw-app-persist="name"` keeps the rail and the docked aside per app
|
|
1387
|
+
function appMemory(app, key, value) {
|
|
1388
|
+
const name = app.dataset.mwAppPersist;
|
|
1389
|
+
if (!name) return null;
|
|
1390
|
+
if (value === undefined) return storage.get(`mw-app:${name}:${key}`);
|
|
1391
|
+
storage.set(`mw-app:${name}:${key}`, value ? '1' : '0');
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function initAppShell() {
|
|
1396
|
+
const docked = appBreakpoints();
|
|
1397
|
+
|
|
1398
|
+
function focusInto(panel) {
|
|
1399
|
+
const target =
|
|
1400
|
+
panel.querySelector('[aria-current="page"], .mw-active') ||
|
|
1401
|
+
panel.querySelector(
|
|
1402
|
+
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
1403
|
+
);
|
|
1404
|
+
if (target) target.focus({ preventScroll: true });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Back to the toggle outside the panel, or focus is left on a hidden one
|
|
1408
|
+
function focusToggle(app, selector, panel) {
|
|
1409
|
+
const toggle = [...app.querySelectorAll(selector)].find(
|
|
1410
|
+
(el) => !panel || !panel.contains(el)
|
|
1411
|
+
);
|
|
1412
|
+
if (toggle) toggle.focus({ preventScroll: true });
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function closeOverlay(app, returnFocus) {
|
|
1416
|
+
const panel = appOverlay(app, docked);
|
|
1417
|
+
if (!panel) return;
|
|
1418
|
+
const aside = panel.classList.contains('mw-app-aside');
|
|
1419
|
+
app.classList.remove(aside ? 'mw-app-aside-open' : 'mw-app-sidebar-open');
|
|
1420
|
+
syncAppShell(app, docked);
|
|
1421
|
+
if (returnFocus) {
|
|
1422
|
+
focusToggle(
|
|
1423
|
+
app,
|
|
1424
|
+
aside ? '[data-mw-app-aside-toggle]' : '[data-mw-app-toggle]',
|
|
1425
|
+
panel
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
document.addEventListener('click', (e) => {
|
|
1431
|
+
const app = e.target.closest('.mw-app');
|
|
1432
|
+
if (!app) return;
|
|
1433
|
+
|
|
1434
|
+
const toggle = e.target.closest('[data-mw-app-toggle]');
|
|
1435
|
+
if (toggle) {
|
|
1436
|
+
if (docked.sidebar.matches) {
|
|
1437
|
+
appMemory(app, 'collapsed', app.classList.toggle('mw-app-collapsed'));
|
|
1438
|
+
syncAppShell(app, docked);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const sidebar = app.querySelector('.mw-app-sidebar');
|
|
1442
|
+
app.classList.remove('mw-app-aside-open');
|
|
1443
|
+
const opened = app.classList.toggle('mw-app-sidebar-open');
|
|
1444
|
+
syncAppShell(app, docked);
|
|
1445
|
+
if (opened) focusInto(sidebar);
|
|
1446
|
+
else if (sidebar && sidebar.contains(toggle)) {
|
|
1447
|
+
focusToggle(app, '[data-mw-app-toggle]', sidebar);
|
|
1448
|
+
}
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const asideToggle = e.target.closest('[data-mw-app-aside-toggle]');
|
|
1453
|
+
if (asideToggle) {
|
|
1454
|
+
const aside = app.querySelector('.mw-app-aside');
|
|
1455
|
+
if (!docked.sidebar.matches)
|
|
1456
|
+
app.classList.remove('mw-app-sidebar-open');
|
|
1457
|
+
const opened = app.classList.toggle('mw-app-aside-open');
|
|
1458
|
+
if (docked.aside.matches) appMemory(app, 'aside', opened);
|
|
1459
|
+
syncAppShell(app, docked);
|
|
1460
|
+
if (opened && aside && !docked.aside.matches) focusInto(aside);
|
|
1461
|
+
if (!opened && aside && aside.contains(asideToggle)) {
|
|
1462
|
+
focusToggle(app, '[data-mw-app-aside-toggle]', aside);
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const panel = appOverlay(app, docked);
|
|
1468
|
+
if (!panel) return;
|
|
1469
|
+
// Scrim taps land on the app; an SPA link in the nav keeps the page
|
|
1470
|
+
if (!panel.contains(e.target)) closeOverlay(app, true);
|
|
1471
|
+
else if (
|
|
1472
|
+
panel.classList.contains('mw-app-sidebar') &&
|
|
1473
|
+
e.target.closest('a[href]')
|
|
1474
|
+
) {
|
|
1475
|
+
closeOverlay(app, false);
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
document.addEventListener('keydown', (e) => {
|
|
1480
|
+
if (e.key !== 'Escape') return;
|
|
1481
|
+
// An open dialog or menu owns this Escape; the next one closes the panel
|
|
1482
|
+
if (document.querySelector('dialog:modal, .mw-dropdown[open]')) return;
|
|
1483
|
+
const app = [...document.querySelectorAll('.mw-app')].find((el) =>
|
|
1484
|
+
appOverlay(el, docked)
|
|
1485
|
+
);
|
|
1486
|
+
if (app) closeOverlay(app, true);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// An undocking aside closes instead of landing on top of the content
|
|
1490
|
+
function onResize() {
|
|
1491
|
+
document.querySelectorAll('.mw-app').forEach((app) => {
|
|
1492
|
+
if (docked.sidebar.matches) app.classList.remove('mw-app-sidebar-open');
|
|
1493
|
+
if (!docked.aside.matches) app.classList.remove('mw-app-aside-open');
|
|
1494
|
+
syncAppShell(app, docked);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
docked.sidebar.addEventListener('change', onResize);
|
|
1499
|
+
docked.aside.addEventListener('change', onResize);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Per shell, so MaverickWave.init() after a render restores that one too
|
|
1503
|
+
function initAppShells(root) {
|
|
1504
|
+
const docked = appBreakpoints();
|
|
1505
|
+
root.querySelectorAll('.mw-app').forEach((app) => {
|
|
1506
|
+
if (!fresh(app)) return;
|
|
1507
|
+
const collapsed = appMemory(app, 'collapsed');
|
|
1508
|
+
const aside = appMemory(app, 'aside');
|
|
1509
|
+
if (collapsed !== null || aside !== null) {
|
|
1510
|
+
// Straight into the remembered state, without animating there
|
|
1511
|
+
app.classList.add('mw-app-restoring');
|
|
1512
|
+
if (collapsed !== null) {
|
|
1513
|
+
app.classList.toggle('mw-app-collapsed', collapsed === '1');
|
|
1514
|
+
}
|
|
1515
|
+
if (aside !== null && docked.aside.matches) {
|
|
1516
|
+
app.classList.toggle('mw-app-aside-open', aside === '1');
|
|
1517
|
+
}
|
|
1518
|
+
requestAnimationFrame(() =>
|
|
1519
|
+
requestAnimationFrame(() => app.classList.remove('mw-app-restoring'))
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
syncAppShell(app, docked);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1283
1526
|
// ===== Progress Bars =====
|
|
1284
1527
|
|
|
1285
1528
|
// The fill is driven from CSS where the browser can do it (_progress.scss), so
|
|
@@ -1332,7 +1575,7 @@
|
|
|
1332
1575
|
// it arrives.
|
|
1333
1576
|
function initReveals(root) {
|
|
1334
1577
|
if (CSS.supports('animation-timeline', 'view()')) return;
|
|
1335
|
-
if (
|
|
1578
|
+
if (reducedMotion()) return;
|
|
1336
1579
|
|
|
1337
1580
|
const targets = Array.from(
|
|
1338
1581
|
root.querySelectorAll('.mw-reveal, .mw-reveal-stagger > *')
|
|
@@ -1379,7 +1622,7 @@
|
|
|
1379
1622
|
const header = document.querySelector('.mw-header-reveal');
|
|
1380
1623
|
if (!header) return;
|
|
1381
1624
|
if (CSS.supports('animation-timeline', 'scroll()')) return;
|
|
1382
|
-
if (
|
|
1625
|
+
if (reducedMotion()) return;
|
|
1383
1626
|
|
|
1384
1627
|
const ribbon = document.querySelector(
|
|
1385
1628
|
'.mw-announcement:not(.mw-announcement-static)'
|
|
@@ -1417,7 +1660,7 @@
|
|
|
1417
1660
|
// CSS where the depth variants can still reach it.
|
|
1418
1661
|
function initParallax() {
|
|
1419
1662
|
if (CSS.supports('animation-timeline', 'view()')) return;
|
|
1420
|
-
if (
|
|
1663
|
+
if (reducedMotion()) return;
|
|
1421
1664
|
|
|
1422
1665
|
const media = document.querySelectorAll('.mw-parallax-media');
|
|
1423
1666
|
if (!media.length) return;
|
|
@@ -1582,7 +1825,7 @@
|
|
|
1582
1825
|
|
|
1583
1826
|
// Not `behavior: 'smooth'`: the browser's own curve takes no duration,
|
|
1584
1827
|
// and the one it picks cannot be slowed down.
|
|
1585
|
-
if (
|
|
1828
|
+
if (reducedMotion()) {
|
|
1586
1829
|
window.scrollTo({ top: target, behavior: 'instant' });
|
|
1587
1830
|
} else {
|
|
1588
1831
|
glideTo(target);
|
|
@@ -1599,7 +1842,14 @@
|
|
|
1599
1842
|
// ===== Scroll Spy =====
|
|
1600
1843
|
function initScrollSpy() {
|
|
1601
1844
|
const sections = document.querySelectorAll('section[id]');
|
|
1602
|
-
|
|
1845
|
+
// Links into this page only, or a scroll clears the current page's mark
|
|
1846
|
+
const page = (path) => path.replace(/index\.html$/, '');
|
|
1847
|
+
const navLinks = [...document.querySelectorAll('.mw-navbar-link')].filter(
|
|
1848
|
+
(link) => {
|
|
1849
|
+
const url = new URL(link.href, location.href);
|
|
1850
|
+
return url.hash && page(url.pathname) === page(location.pathname);
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1603
1853
|
|
|
1604
1854
|
if (sections.length === 0 || navLinks.length === 0) return;
|
|
1605
1855
|
|
|
@@ -1761,6 +2011,14 @@
|
|
|
1761
2011
|
}
|
|
1762
2012
|
|
|
1763
2013
|
// ===== Utility Functions =====
|
|
2014
|
+
// The OS setting or mw-motion-off on <html> - the stylesheet honours both
|
|
2015
|
+
function reducedMotion() {
|
|
2016
|
+
return (
|
|
2017
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches ||
|
|
2018
|
+
document.documentElement.classList.contains('mw-motion-off')
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1764
2022
|
function debounce(func, wait) {
|
|
1765
2023
|
let timeout;
|
|
1766
2024
|
return function () {
|
|
@@ -1840,20 +2098,14 @@
|
|
|
1840
2098
|
|
|
1841
2099
|
// ===== Modals =====
|
|
1842
2100
|
|
|
1843
|
-
//
|
|
1844
|
-
//
|
|
1845
|
-
// Delegated, so a modal added later works too.
|
|
2101
|
+
// A `<dialog>`: focus trap, Escape and the inert background are the
|
|
2102
|
+
// browser's job. Delegated, so a modal added later works too.
|
|
1846
2103
|
function initModals() {
|
|
1847
2104
|
document.addEventListener('click', (e) => {
|
|
1848
2105
|
const closer = e.target.closest('.mw-modal-close');
|
|
1849
2106
|
if (closer) {
|
|
1850
2107
|
const dialog = closer.closest('dialog.mw-modal');
|
|
1851
|
-
if (dialog)
|
|
1852
|
-
dialog.close();
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
const overlay = closer.closest('.mw-modal-overlay');
|
|
1856
|
-
if (overlay) overlay.classList.remove('mw-modal-open');
|
|
2108
|
+
if (dialog) dialog.close();
|
|
1857
2109
|
return;
|
|
1858
2110
|
}
|
|
1859
2111
|
|
|
@@ -1863,42 +2115,27 @@
|
|
|
1863
2115
|
return;
|
|
1864
2116
|
}
|
|
1865
2117
|
|
|
1866
|
-
//
|
|
2118
|
+
// `closedby="any"` where the browser does not understand it yet. A click
|
|
1867
2119
|
// that lands on the dialog element is a click on the backdrop - the
|
|
1868
2120
|
// content sits in .mw-modal-header/body/footer inside it.
|
|
1869
2121
|
if (
|
|
1870
|
-
e.target.matches('dialog.mw-modal[open]') &&
|
|
1871
|
-
!
|
|
2122
|
+
e.target.matches('dialog.mw-modal[open][closedby="any"]') &&
|
|
2123
|
+
!('closedBy' in e.target)
|
|
1872
2124
|
) {
|
|
1873
2125
|
e.target.close();
|
|
1874
2126
|
}
|
|
1875
2127
|
});
|
|
1876
2128
|
}
|
|
1877
2129
|
|
|
1878
|
-
//
|
|
1879
|
-
//
|
|
1880
|
-
|
|
1881
|
-
return 'closedBy' in dialog && dialog.getAttribute('closedby') !== null;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
// Opens either shape. Exposed, because which modal opens when is the
|
|
1885
|
-
// application's decision and not something markup can express on its own.
|
|
2130
|
+
// Exposed, because which modal opens when is the application's decision and
|
|
2131
|
+
// not something markup can express on its own
|
|
2132
|
+
// Not over itself: showModal() throws on a dialog already open as a drawer
|
|
1886
2133
|
function openModal(modal) {
|
|
1887
|
-
if (!modal)
|
|
1888
|
-
if (modal.tagName === 'DIALOG') {
|
|
1889
|
-
modal.showModal();
|
|
1890
|
-
} else {
|
|
1891
|
-
modal.classList.add('mw-modal-open');
|
|
1892
|
-
}
|
|
2134
|
+
if (modal instanceof HTMLDialogElement && !modal.open) modal.showModal();
|
|
1893
2135
|
}
|
|
1894
2136
|
|
|
1895
2137
|
function closeModal(modal) {
|
|
1896
|
-
if (
|
|
1897
|
-
if (modal.tagName === 'DIALOG') {
|
|
1898
|
-
modal.close();
|
|
1899
|
-
} else {
|
|
1900
|
-
modal.classList.remove('mw-modal-open');
|
|
1901
|
-
}
|
|
2138
|
+
if (modal instanceof HTMLDialogElement) modal.close();
|
|
1902
2139
|
}
|
|
1903
2140
|
|
|
1904
2141
|
window.mwOpenModal = (id) => openModal(document.getElementById(id));
|
|
@@ -1948,6 +2185,26 @@
|
|
|
1948
2185
|
});
|
|
1949
2186
|
}
|
|
1950
2187
|
|
|
2188
|
+
// ===== Before/After Compare =====
|
|
2189
|
+
// The range is the control; this only hands its position to the CSS
|
|
2190
|
+
function initCompares(root) {
|
|
2191
|
+
root.querySelectorAll('.mw-compare-range').forEach((range) => {
|
|
2192
|
+
if (!fresh(range)) return;
|
|
2193
|
+
const frame = range.closest('.mw-compare');
|
|
2194
|
+
if (!frame) return;
|
|
2195
|
+
|
|
2196
|
+
const update = () => {
|
|
2197
|
+
const min = Number(range.min) || 0;
|
|
2198
|
+
const max = Number(range.max) || 100;
|
|
2199
|
+
const share = ((range.valueAsNumber - min) / (max - min)) * 100;
|
|
2200
|
+
frame.style.setProperty('--mw-compare-position', share + '%');
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
range.addEventListener('input', update);
|
|
2204
|
+
update();
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1951
2208
|
// ===== Kanban Boards =====
|
|
1952
2209
|
function initKanbanBoards(root) {
|
|
1953
2210
|
root.querySelectorAll('.mw-kanban').forEach((board) => {
|
|
@@ -2517,8 +2774,9 @@
|
|
|
2517
2774
|
const effect = effects.find((name) =>
|
|
2518
2775
|
html.classList.contains('mw-occasion-' + name)
|
|
2519
2776
|
);
|
|
2777
|
+
if (reducedMotion()) return;
|
|
2778
|
+
initOccasionArrivals(effect);
|
|
2520
2779
|
if (!effect) return;
|
|
2521
|
-
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
2522
2780
|
|
|
2523
2781
|
if (html.classList.contains('mw-occasion-scroll')) {
|
|
2524
2782
|
startOccasionDrift(effect);
|
|
@@ -2877,20 +3135,58 @@
|
|
|
2877
3135
|
});
|
|
2878
3136
|
}
|
|
2879
3137
|
|
|
3138
|
+
// No view timeline for the card pieces (Firefox): an observer plays them
|
|
3139
|
+
function initOccasionArrivals(effect) {
|
|
3140
|
+
if (CSS.supports('animation-timeline', 'view()')) return;
|
|
3141
|
+
|
|
3142
|
+
const siteWide =
|
|
3143
|
+
effect &&
|
|
3144
|
+
document.documentElement.classList.contains('mw-occasion-scroll');
|
|
3145
|
+
const hosts = [
|
|
3146
|
+
...document.querySelectorAll(
|
|
3147
|
+
siteWide
|
|
3148
|
+
? '.mw-card, .mw-panel, .mw-testimonial, .mw-occasion-spot'
|
|
3149
|
+
: '.mw-occasion-scroll:not(:root)'
|
|
3150
|
+
),
|
|
3151
|
+
].filter((el) => el.getBoundingClientRect().top >= window.innerHeight);
|
|
3152
|
+
if (!hosts.length) return;
|
|
3153
|
+
|
|
3154
|
+
const obs = new IntersectionObserver(
|
|
3155
|
+
(entries, observer) => {
|
|
3156
|
+
entries.forEach((entry) => {
|
|
3157
|
+
if (!entry.isIntersecting) return;
|
|
3158
|
+
observer.unobserve(entry.target);
|
|
3159
|
+
entry.target.classList.replace(
|
|
3160
|
+
'mw-occasion-waiting',
|
|
3161
|
+
'mw-occasion-arriving'
|
|
3162
|
+
);
|
|
3163
|
+
});
|
|
3164
|
+
},
|
|
3165
|
+
{ threshold: 0.15 }
|
|
3166
|
+
);
|
|
3167
|
+
|
|
3168
|
+
hosts.forEach((el) => {
|
|
3169
|
+
el.classList.add('mw-occasion-waiting');
|
|
3170
|
+
obs.observe(el);
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
|
|
2880
3174
|
// Scrubbed by the scroll on the compositor, so it stands still whenever the
|
|
2881
3175
|
// page does. A lap is a little more than a screen of scrolling, which makes
|
|
2882
3176
|
// the count a matter of page length.
|
|
2883
3177
|
function startOccasionDrift(effect) {
|
|
2884
|
-
// Not CSS.supports: Firefox behind its flag parses the syntax, but plays
|
|
2885
|
-
// the whole page as one lap - pieces that crawl read as stuck
|
|
2886
|
-
if (!window.ScrollTimeline) return;
|
|
2887
|
-
|
|
2888
3178
|
const layer = document.createElement('div');
|
|
2889
3179
|
layer.className = 'mw-occasion-drift';
|
|
2890
3180
|
layer.setAttribute('aria-hidden', 'true');
|
|
3181
|
+
// Not CSS.supports: Firefox behind its flag parses the syntax, but plays
|
|
3182
|
+
// the whole page as one lap - pieces that crawl read as stuck
|
|
3183
|
+
const driven = !window.ScrollTimeline;
|
|
3184
|
+
// Before the append, or the particles start on the clock and park mid-lap
|
|
3185
|
+
if (driven) layer.classList.add('mw-occasion-driven');
|
|
2891
3186
|
if (effect === 'summer') occasionBeams(layer);
|
|
2892
3187
|
else occasionDriftParticles(layer, effect);
|
|
2893
3188
|
document.body.appendChild(layer);
|
|
3189
|
+
if (driven) driveOccasionDrift(layer);
|
|
2894
3190
|
|
|
2895
3191
|
function laps() {
|
|
2896
3192
|
const scrollable = document.documentElement.scrollHeight - innerHeight;
|
|
@@ -2904,6 +3200,32 @@
|
|
|
2904
3200
|
window.addEventListener('resize', debounce(laps, 200), { passive: true });
|
|
2905
3201
|
}
|
|
2906
3202
|
|
|
3203
|
+
// Hands the CSS the progress scroll(root) would; hidden where that is idle
|
|
3204
|
+
function driveOccasionDrift(layer) {
|
|
3205
|
+
const root = document.documentElement;
|
|
3206
|
+
let queued = false;
|
|
3207
|
+
|
|
3208
|
+
const update = () => {
|
|
3209
|
+
queued = false;
|
|
3210
|
+
const scrollable = root.scrollHeight - root.clientHeight;
|
|
3211
|
+
const progress =
|
|
3212
|
+
scrollable > 0 ? Math.min(1, Math.max(0, scrollY / scrollable)) : 0;
|
|
3213
|
+
layer.style.visibility = scrollable > 0 ? '' : 'hidden';
|
|
3214
|
+
layer.style.setProperty('--mw-occasion-progress', progress.toFixed(4));
|
|
3215
|
+
};
|
|
3216
|
+
|
|
3217
|
+
const request = () => {
|
|
3218
|
+
if (queued) return;
|
|
3219
|
+
queued = true;
|
|
3220
|
+
requestAnimationFrame(update);
|
|
3221
|
+
};
|
|
3222
|
+
|
|
3223
|
+
update();
|
|
3224
|
+
window.addEventListener('load', request);
|
|
3225
|
+
window.addEventListener('scroll', request, { passive: true });
|
|
3226
|
+
window.addEventListener('resize', request, { passive: true });
|
|
3227
|
+
}
|
|
3228
|
+
|
|
2907
3229
|
// Sizes at a 1280px window, scaled with it within limits. Smaller than this a
|
|
2908
3230
|
// leaf on the page reads as a speck, and a speck as a rendering fault.
|
|
2909
3231
|
function occasionDriftParticles(layer, effect) {
|
|
@@ -2938,12 +3260,15 @@
|
|
|
2938
3260
|
for (let i = 0; i < count; i++) {
|
|
2939
3261
|
const shape = shapes[i % shapes.length];
|
|
2940
3262
|
const soft = ['maple', 'oak', 'birch', 'petal'].includes(shape);
|
|
3263
|
+
const start = ((i * 37) % 100) * 0.3;
|
|
2941
3264
|
occasionParticle(layer, shape, 'falling', {
|
|
2942
3265
|
'--mw-occasion-x':
|
|
2943
3266
|
((i + between(0.1, 0.9)) * (100 / count)).toFixed(1) + 'vw',
|
|
2944
3267
|
'--mw-occasion-size':
|
|
2945
3268
|
(between(sizes[shape][0], sizes[shape][1]) * scale).toFixed(1) + 'px',
|
|
2946
|
-
'--mw-occasion-start':
|
|
3269
|
+
'--mw-occasion-start': start.toFixed(1) + '%',
|
|
3270
|
+
// The start again as a number - a calc() cannot turn a % into one
|
|
3271
|
+
'--mw-occasion-from': (start / 100).toFixed(3),
|
|
2947
3272
|
'--mw-occasion-lap': String(i % 3),
|
|
2948
3273
|
'--mw-occasion-drift': between(-6, 6).toFixed(1) + 'vw',
|
|
2949
3274
|
'--mw-occasion-sway': between(8, 24).toFixed(0) + 'px',
|