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.
- package/.claude/skills/mw-maverick-wave/SKILL.md +22 -12
- package/.claude/skills/mw-maverick-wave/examples/angular-services.md +10 -1
- package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +4 -3
- package/.claude/skills/mw-maverick-wave/references/components.md +23 -9
- package/.claude/skills/mw-maverick-wave/references/javascript.md +1 -1
- package/.claude/skills/mw-maverick-wave/references/layout.md +8 -3
- package/.claude/skills/mw-maverick-wave/references/theming.md +57 -29
- package/CHANGELOG.md +19 -0
- package/README.md +43 -22
- package/index.html +395 -62
- package/maverick-wave.min.css +61 -5
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/scripts/verify.js +18 -2
- package/src/js/main.js +24 -5
- package/src/partials/header-container.html +7 -5
- package/src/partials/palette-container.html +176 -66
- package/src/partials/preview-container.html +136 -0
- package/src/partials/utilities-container.html +55 -4
- package/src/scss/abstracts/_functions.scss +31 -3
- package/src/scss/abstracts/_mixins.scss +62 -12
- package/src/scss/abstracts/_variables.scss +47 -21
- package/src/scss/base/_base.scss +101 -8
- package/src/scss/base/_reset.scss +2 -0
- package/src/scss/components/_badge.scss +1 -1
- package/src/scss/components/_buttons.scss +1 -1
- package/src/scss/components/_cards.scss +23 -14
- package/src/scss/components/_dropdown.scss +58 -1
- package/src/scss/components/_kanban.scss +10 -9
- package/src/scss/components/_lang-switch.scss +1 -1
- package/src/scss/components/_localhost-indicator.scss +1 -1
- package/src/scss/components/_modals.scss +13 -1
- package/src/scss/components/_pricing.scss +1 -1
- package/src/scss/components/_progress.scss +9 -5
- package/src/scss/components/_skeleton.scss +1 -1
- package/src/scss/components/_spinners.scss +5 -5
- package/src/scss/components/_stepper.scss +4 -1
- package/src/scss/components/_tabs.scss +28 -8
- package/src/scss/components/_tiles.scss +41 -16
- package/src/scss/components/_timelines.scss +12 -5
- package/src/scss/layout/_main.scss +1 -1
- package/src/scss/layout/_parallax.scss +10 -5
- package/src/scss/utilities/_elevation.scss +23 -0
- package/src/scss/utilities/_touch-targets.scss +1 -1
- 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-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
border
|
|
135
|
-
border:
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
149
|
-
transform: scale(0.
|
|
154
|
+
.palette-toggle:active {
|
|
155
|
+
transform: scale(0.94);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
.palette-panel {
|
|
153
159
|
position: fixed;
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
max-height: calc(100dvh -
|
|
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:
|
|
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:
|
|
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
|
|
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-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
1357
|
+
if (value) pairs.push([prop, value]);
|
|
1131
1358
|
});
|
|
1132
1359
|
|
|
1133
1360
|
controls.forEach((control) => {
|
|
1134
1361
|
if (control.type !== 'checkbox') {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1506
|
+
control.addEventListener('change', () =>
|
|
1507
|
+
inPlace(() => {
|
|
1508
|
+
apply(control);
|
|
1509
|
+
render();
|
|
1510
|
+
})
|
|
1511
|
+
)
|
|
1236
1512
|
);
|
|
1237
1513
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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>
|