maverick-wave 5.8.0 → 5.11.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 +26 -14
- 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 +14 -14
- package/.claude/skills/mw-maverick-wave/references/layout.md +29 -9
- package/.claude/skills/mw-maverick-wave/references/theming.md +48 -25
- package/CHANGELOG.md +23 -0
- package/README.md +41 -24
- package/index.html +356 -20
- package/maverick-wave.min.css +61 -5
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/scripts/verify.js +7 -1
- package/src/js/main.js +27 -7
- package/src/partials/palette-container.html +105 -65
- package/src/partials/parallax-container.html +20 -1
- package/src/partials/preview-container.html +136 -0
- package/src/partials/utilities-container.html +38 -0
- 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 +97 -8
- 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 +13 -5
- package/src/scss/layout/_header-reveal.scss +5 -5
- package/src/scss/layout/_main.scss +1 -1
- package/src/scss/layout/_parallax.scss +65 -14
- package/src/scss/utilities/_elevation.scss +23 -0
- package/src/scss/utilities/_reveal.scss +5 -5
- package/src/scss/utilities/_touch-targets.scss +1 -1
- package/src/scss/utilities/_variants.scss +74 -7
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ The result is a framework that balances utility with simplicity, offering develo
|
|
|
23
23
|
- A five-step elevation ramp and a motion scale, so every shadow and every transition in the framework comes from one place
|
|
24
24
|
- Mobile as a first-class target: 44px touch targets on a coarse pointer, modals that become bottom sheets, press states on everything, and hover effects that do not latch after a tap
|
|
25
25
|
- Easy Customization via CSS Custom Properties
|
|
26
|
-
- Built-in Light & Dark Mode
|
|
26
|
+
- Built-in Light & Dark Mode - follows the OS by default, switchable per reader
|
|
27
27
|
- SCSS Source Files for advanced customization (Dart Sass, `@use`/`@forward`)
|
|
28
28
|
- Modals as `<div>` or as `<dialog>` - the latter brings the focus trap, Escape and the inert background from the platform
|
|
29
29
|
- Native form validation is styled through `:user-invalid`, alongside the class-driven error states for reactive forms
|
|
@@ -42,12 +42,12 @@ The result is a framework that balances utility with simplicity, offering develo
|
|
|
42
42
|
<title>My MaverickWave Project</title>
|
|
43
43
|
<link
|
|
44
44
|
rel="stylesheet"
|
|
45
|
-
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.
|
|
45
|
+
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.css"
|
|
46
46
|
/>
|
|
47
47
|
</head>
|
|
48
48
|
<body>
|
|
49
49
|
<!-- Your content here -->
|
|
50
|
-
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.
|
|
50
|
+
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.js"></script>
|
|
51
51
|
</body>
|
|
52
52
|
</html>
|
|
53
53
|
```
|
|
@@ -130,7 +130,8 @@ assembled rather than designed.
|
|
|
130
130
|
:root {
|
|
131
131
|
/* Elevation. Each level is two shadows - a tight contact layer that gives
|
|
132
132
|
the box weight, and a wide ambient one that says how high it floats.
|
|
133
|
-
|
|
133
|
+
One ramp for both themes; the theme arrives through the shadow tokens
|
|
134
|
+
the levels are built from. */
|
|
134
135
|
--mw-elevation-1: /* resting: inputs, tags, small controls */;
|
|
135
136
|
--mw-elevation-2: /* raised: cards and panels at rest */;
|
|
136
137
|
--mw-elevation-3: /* floating: a card under the pointer */;
|
|
@@ -347,32 +348,42 @@ layer of its own:
|
|
|
347
348
|
|
|
348
349
|
### Site-wide variants
|
|
349
350
|
|
|
350
|
-
|
|
351
|
+
Fifteen classes on `<html>` retune the whole look without touching markup or
|
|
351
352
|
rebuilding. They stack.
|
|
352
353
|
|
|
353
|
-
| Class
|
|
354
|
-
|
|
|
355
|
-
| `mw-corners-even`
|
|
356
|
-
| `mw-accent-single`
|
|
357
|
-
| `mw-shadows-flat`
|
|
358
|
-
| `mw-surfaces-flush`
|
|
359
|
-
| `mw-hover-static`
|
|
360
|
-
| `mw-scroll-static`
|
|
361
|
-
| `mw-sections-plain`
|
|
362
|
-
| `mw-headings-caps`
|
|
363
|
-
| `mw-
|
|
364
|
-
| `mw-btn-
|
|
365
|
-
| `mw-btn-
|
|
366
|
-
| `mw-btn-
|
|
367
|
-
|
|
368
|
-
|
|
354
|
+
| Class | Effect |
|
|
355
|
+
| -------------------- | ----------------------------------------------------------------------- |
|
|
356
|
+
| `mw-corners-even` | Drops the surface signature - every panel becomes an evenly rounded box |
|
|
357
|
+
| `mw-accent-single` | One brand colour instead of two; `mw-btn-secondary` turns outline |
|
|
358
|
+
| `mw-shadows-flat` | Elevation 1-3 to `none`; dropdown and modal keep theirs |
|
|
359
|
+
| `mw-surfaces-flush` | Cards, panels and footer on the page colour, held by their border |
|
|
360
|
+
| `mw-hover-static` | No hover travels - lifts and image zooms go, colour still responds |
|
|
361
|
+
| `mw-scroll-static` | No scroll entrance - `mw-reveal` blocks sit where they land |
|
|
362
|
+
| `mw-sections-plain` | The hatch behind `mw-section-alternate` collapses into the page colour |
|
|
363
|
+
| `mw-headings-caps` | `h1`-`h3` in capitals |
|
|
364
|
+
| `mw-links-underline` | Links underlined at rest; the stroke thickens on hover |
|
|
365
|
+
| `mw-btn-pill` | Fully rounded buttons; form fields keep their radius |
|
|
366
|
+
| `mw-btn-square` | Buttons cut to a hard corner while the page keeps its radius |
|
|
367
|
+
| `mw-btn-glass` | Filled buttons become a translucent wash with a lit top edge |
|
|
368
|
+
| `mw-btn-tactile` | Filled buttons stand on a darker edge and sink onto it when pressed |
|
|
369
|
+
| `mw-density-compact` | Less padding in cards, panels and controls; type stays put |
|
|
370
|
+
| `mw-density-roomy` | More of the same |
|
|
371
|
+
|
|
372
|
+
Six properties do the rest: `--mw-radius-scale` multiplies every radius (`0`
|
|
369
373
|
squares the framework off), `--mw-root-font-size` moves the whole rem scale,
|
|
370
|
-
|
|
374
|
+
`--mw-motion-scale` the tempo of every transition, plus
|
|
375
|
+
`--mw-font-family-heading`, `--mw-container-width` and
|
|
371
376
|
`--mw-section-padding-block`.
|
|
372
377
|
|
|
378
|
+
The [showcase](https://maverick-wave.m1well.com) has a picker for all of them,
|
|
379
|
+
and the URL carries whatever is set - send that link and the next person opens
|
|
380
|
+
the page exactly as you left it. The panel prints the same setup as markup and
|
|
381
|
+
CSS to copy into a project.
|
|
382
|
+
|
|
373
383
|
A variant of your own that retunes a theme-bound token has to target
|
|
374
384
|
`:root.your-class` **and** `:root.your-class .mw-theme-light` - the light theme
|
|
375
|
-
re-declares those on `<body>` and would shadow a root-only value.
|
|
385
|
+
re-declares those on `<body>` and would shadow a root-only value. The elevation
|
|
386
|
+
tokens are the exception and need only `:root`.
|
|
376
387
|
|
|
377
388
|
### SCSS Source
|
|
378
389
|
|
|
@@ -458,7 +469,7 @@ above):
|
|
|
458
469
|
> change detection. The behaviors it covers (accordion, tabs, modal close, mobile
|
|
459
470
|
> nav, scroll spy, theme toggle, progress bars, sliders, alerts, galleries) are a
|
|
460
471
|
> few lines each in a component - the framework's state classes are the whole
|
|
461
|
-
> contract. Theme switching, for example, is a
|
|
472
|
+
> contract. Theme switching, for example, is a pair of classes on `<body>`:
|
|
462
473
|
|
|
463
474
|
```typescript
|
|
464
475
|
// theme.service.ts - mw-theme-switching suppresses the transitions the flip
|
|
@@ -467,10 +478,16 @@ const root = document.documentElement;
|
|
|
467
478
|
|
|
468
479
|
root.classList.add('mw-theme-switching');
|
|
469
480
|
document.body.classList.toggle('mw-theme-light', isLight);
|
|
481
|
+
// the explicit counterpart - without it a dark choice on a light machine
|
|
482
|
+
// falls back to the OS preference
|
|
483
|
+
document.body.classList.toggle('mw-theme-dark', !isLight);
|
|
470
484
|
void root.offsetHeight;
|
|
471
485
|
root.classList.remove('mw-theme-switching');
|
|
472
486
|
```
|
|
473
487
|
|
|
488
|
+
With neither class the page follows `prefers-color-scheme`, so an app that has
|
|
489
|
+
nothing stored yet can simply leave both off.
|
|
490
|
+
|
|
474
491
|
The `mw-field` wrapper groups label, control, hint and error. Bind the error
|
|
475
492
|
state yourself - the framework does not style Angular's `ng-invalid` /
|
|
476
493
|
`ng-touched` classes:
|
package/index.html
CHANGED
|
@@ -97,6 +97,21 @@
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/* The travel has to sit on the animated section, the height on the block
|
|
101
|
+
inside it */
|
|
102
|
+
.parallax-band-demo {
|
|
103
|
+
--mw-parallax-slow-travel: 60vh;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.parallax-band-demo .mw-parallax {
|
|
107
|
+
min-height: 75vh;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Reuses the hero picture through the token, not a second download */
|
|
111
|
+
.parallax-band-demo .mw-parallax-media {
|
|
112
|
+
background-image: var(--mw-hero-background);
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
/* A calendar fills its container; across the full page that stretches the
|
|
101
116
|
cells to 150px, so the demos get a realistic column instead */
|
|
102
117
|
.calendar-demo {
|
|
@@ -292,6 +307,26 @@
|
|
|
292
307
|
border-top: 1px solid var(--mw-border);
|
|
293
308
|
}
|
|
294
309
|
|
|
310
|
+
/* Splits the list by where a switch shows up, because half of them act on
|
|
311
|
+
parts of the page the preview does not contain */
|
|
312
|
+
.variant-group {
|
|
313
|
+
margin: 20px 0 2px;
|
|
314
|
+
font-size: 0.78rem;
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
color: var(--mw-text-color);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.variant-group:first-child {
|
|
320
|
+
margin-top: 4px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.variant-group-hint {
|
|
324
|
+
margin: 0 0 4px;
|
|
325
|
+
font-size: 0.68rem;
|
|
326
|
+
line-height: 1.35;
|
|
327
|
+
color: var(--mw-text-muted-color);
|
|
328
|
+
}
|
|
329
|
+
|
|
295
330
|
.variant-row {
|
|
296
331
|
display: flex;
|
|
297
332
|
align-items: center;
|
|
@@ -369,14 +404,129 @@
|
|
|
369
404
|
background: var(--mw-primary-accent-text-color);
|
|
370
405
|
}
|
|
371
406
|
|
|
407
|
+
/* The sticky bar would otherwise sit on the section title it jumps to */
|
|
408
|
+
#preview {
|
|
409
|
+
scroll-margin-top: calc(var(--mw-header-height) + 1rem);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.preview-head {
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: flex-start;
|
|
415
|
+
justify-content: space-between;
|
|
416
|
+
gap: 1rem;
|
|
417
|
+
margin-bottom: 1.5rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.preview-title {
|
|
421
|
+
margin: 0 0 0.35rem;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.preview-lede {
|
|
425
|
+
margin: 0;
|
|
426
|
+
max-inline-size: 60ch;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* The panel is fixed over the right of the page, flush with the container
|
|
430
|
+
edge. Below xl there is no room to dodge it and it simply covers part of
|
|
431
|
+
the preview; from there the preview keeps to the strip beside it,
|
|
432
|
+
because a switch that moves something hidden behind the panel
|
|
433
|
+
demonstrates nothing. */
|
|
434
|
+
@media (width >= 1200px) {
|
|
435
|
+
body:has(.palette-panel:not([hidden])) #preview > .mw-container {
|
|
436
|
+
padding-inline-end: 460px;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Both wrap rather than squeezing a field to nothing on a phone */
|
|
441
|
+
.preview-row,
|
|
442
|
+
.preview-actions {
|
|
443
|
+
display: flex;
|
|
444
|
+
flex-wrap: wrap;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 0.75rem;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.preview-row .mw-input {
|
|
450
|
+
flex: 1 1 12rem;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* The one thing here that keeps moving without being touched, so Motion
|
|
454
|
+
tempo has something to show that a hover state cannot */
|
|
455
|
+
.preview-status {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
gap: 14px;
|
|
459
|
+
margin: 0 0 1.25rem;
|
|
460
|
+
font-size: 0.82rem;
|
|
461
|
+
color: var(--mw-text-muted-color);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.preview-status strong {
|
|
465
|
+
display: block;
|
|
466
|
+
color: var(--mw-text-color);
|
|
467
|
+
font-weight: 500;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.preview-status small {
|
|
471
|
+
font-size: 0.75rem;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.preview-row .mw-select {
|
|
475
|
+
flex: 0 1 14rem;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* The spacing sits on the row and not on the label: the button is taller
|
|
479
|
+
than the caption beside it, so a margin under the label leaves the
|
|
480
|
+
button itself resting on the box */
|
|
481
|
+
.variant-setup-head {
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: baseline;
|
|
484
|
+
justify-content: space-between;
|
|
485
|
+
gap: 10px;
|
|
486
|
+
margin: 14px 0 10px;
|
|
487
|
+
}
|
|
488
|
+
|
|
372
489
|
.variant-setup-label {
|
|
373
490
|
display: block;
|
|
374
|
-
margin
|
|
375
|
-
margin-bottom: 5px;
|
|
491
|
+
margin: 0;
|
|
376
492
|
font-size: 0.7rem;
|
|
377
493
|
color: var(--mw-text-muted-color);
|
|
378
494
|
}
|
|
379
495
|
|
|
496
|
+
/* The one control here that leads somewhere - everything else in the
|
|
497
|
+
panel only changes what is on screen. Carries the brand colour so it
|
|
498
|
+
reads as the way out rather than as one more switch. */
|
|
499
|
+
.variant-share {
|
|
500
|
+
display: inline-flex;
|
|
501
|
+
align-items: center;
|
|
502
|
+
gap: 6px;
|
|
503
|
+
flex-shrink: 0;
|
|
504
|
+
padding: 5px 12px;
|
|
505
|
+
border-radius: 999px;
|
|
506
|
+
border: 1px solid var(--mw-primary-color);
|
|
507
|
+
background: var(--mw-primary-color);
|
|
508
|
+
color: var(--mw-primary-accent-text-color);
|
|
509
|
+
font: inherit;
|
|
510
|
+
font-size: 0.72rem;
|
|
511
|
+
font-weight: 500;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
transition: var(--mw-transition-fast);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.variant-share:hover {
|
|
517
|
+
background: var(--mw-primary-color-hover, var(--mw-primary-color));
|
|
518
|
+
transform: translateY(-1px);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.variant-share:active {
|
|
522
|
+
transform: translateY(0);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.variant-share:focus-visible {
|
|
526
|
+
outline: var(--mw-focus-ring-width) solid var(--mw-focus-ring-color);
|
|
527
|
+
outline-offset: var(--mw-focus-ring-offset);
|
|
528
|
+
}
|
|
529
|
+
|
|
380
530
|
.variant-setup {
|
|
381
531
|
display: block;
|
|
382
532
|
width: 100%;
|
|
@@ -401,7 +551,7 @@
|
|
|
401
551
|
<a class="mw-skip-link" href="#main">Skip to content</a>
|
|
402
552
|
|
|
403
553
|
<!-- Header -->
|
|
404
|
-
<header class="mw-header mw-
|
|
554
|
+
<header class="mw-header mw-localhost-indicator-activated">
|
|
405
555
|
<div class="mw-container">
|
|
406
556
|
@@include('./src/partials/header-container.html')
|
|
407
557
|
</div>
|
|
@@ -566,6 +716,22 @@
|
|
|
566
716
|
</div>
|
|
567
717
|
</section>
|
|
568
718
|
|
|
719
|
+
<!-- Parallax band -->
|
|
720
|
+
<section class="mw-parallax-slow parallax-band-demo">
|
|
721
|
+
<div class="mw-parallax mw-parallax-dimmed">
|
|
722
|
+
<div class="mw-parallax-media"></div>
|
|
723
|
+
<div class="mw-parallax-content mw-text-center">
|
|
724
|
+
<div class="mw-container">
|
|
725
|
+
<h2>The page rides over the picture</h2>
|
|
726
|
+
<p class="mw-mb-0">
|
|
727
|
+
This band travels at a quarter of your scrolling. The components
|
|
728
|
+
close in from below.
|
|
729
|
+
</p>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
</section>
|
|
734
|
+
|
|
569
735
|
<!-- Components section -->
|
|
570
736
|
<section id="components" class="mw-section">
|
|
571
737
|
<div class="mw-container">
|
|
@@ -823,6 +989,18 @@
|
|
|
823
989
|
@@include('./src/partials/history-container.html')
|
|
824
990
|
</div>
|
|
825
991
|
</section>
|
|
992
|
+
<!-- Preview section, showcase only - see the partial -->
|
|
993
|
+
<section id="preview" class="mw-section">
|
|
994
|
+
<div class="mw-container">
|
|
995
|
+
<h2 class="mw-section-title">Preview</h2>
|
|
996
|
+
<p class="mw-section-intro">
|
|
997
|
+
One page-shaped example, so the variant switches in the palette
|
|
998
|
+
panel have something to act on that is not a component gallery. Open
|
|
999
|
+
the panel and the page lands here.
|
|
1000
|
+
</p>
|
|
1001
|
+
@@include('./src/partials/preview-container.html')
|
|
1002
|
+
</div>
|
|
1003
|
+
</section>
|
|
826
1004
|
</main>
|
|
827
1005
|
|
|
828
1006
|
<!-- Footer -->
|
|
@@ -1013,9 +1191,21 @@
|
|
|
1013
1191
|
input.addEventListener('change', settle);
|
|
1014
1192
|
});
|
|
1015
1193
|
|
|
1194
|
+
const preview = document.getElementById('preview');
|
|
1195
|
+
|
|
1016
1196
|
const toggle = (open) => {
|
|
1017
1197
|
panel.hidden = !open;
|
|
1018
1198
|
toggleBtn.setAttribute('aria-expanded', String(open));
|
|
1199
|
+
|
|
1200
|
+
// Or the switches act on whatever section the reader is parked at,
|
|
1201
|
+
// which is one component at a time and says little about a look.
|
|
1202
|
+
//
|
|
1203
|
+
// `instant` against the smooth default in the reset: the preview sits
|
|
1204
|
+
// at the end of a page some 80 000px long, and over that distance a
|
|
1205
|
+
// smooth scroll either crawls through every section on the way or, as
|
|
1206
|
+
// here, never starts at all.
|
|
1207
|
+
if (open)
|
|
1208
|
+
preview?.scrollIntoView({ block: 'start', behavior: 'instant' });
|
|
1019
1209
|
};
|
|
1020
1210
|
|
|
1021
1211
|
toggleBtn.addEventListener('click', (event) => {
|
|
@@ -1116,6 +1306,58 @@
|
|
|
1116
1306
|
const root = document.documentElement;
|
|
1117
1307
|
const controls = [...list.querySelectorAll('input, select')];
|
|
1118
1308
|
|
|
1309
|
+
// Built on every render, put into the address bar by nobody: a setup
|
|
1310
|
+
// written to the URL as you go comes back on the next reload, and then
|
|
1311
|
+
// the showcase opens in someone else's look for good. Copy link is the
|
|
1312
|
+
// one way out of here, and what it hands over is this.
|
|
1313
|
+
//
|
|
1314
|
+
// URLSearchParams and not a hand-rolled separator: a value like the
|
|
1315
|
+
// heading font or the page width is full of commas, quotes and brackets,
|
|
1316
|
+
// and escaping those by hand is where this kind of thing breaks.
|
|
1317
|
+
let shareUrl = location.origin + location.pathname;
|
|
1318
|
+
|
|
1319
|
+
const buildShareUrl = (classes, dropped, pairs) => {
|
|
1320
|
+
const params = new URLSearchParams();
|
|
1321
|
+
if (classes.length) params.set('c', classes.join(' '));
|
|
1322
|
+
if (dropped.length) params.set('d', dropped.join(' '));
|
|
1323
|
+
pairs.forEach(([prop, value]) => params.set(prop, value));
|
|
1324
|
+
|
|
1325
|
+
const query = params.toString();
|
|
1326
|
+
return (
|
|
1327
|
+
location.origin + location.pathname + (query ? '#' + query : '')
|
|
1328
|
+
);
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// Back into the shape load() reads, so the link and a pasted setup go
|
|
1332
|
+
// through one parser rather than two that can drift apart. The target in
|
|
1333
|
+
// front of `: no` is only there to make the line read like the printed
|
|
1334
|
+
// one - load() matches on the class name alone.
|
|
1335
|
+
const readHash = () => {
|
|
1336
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
1337
|
+
const lines = [];
|
|
1338
|
+
|
|
1339
|
+
const classes = params.get('c');
|
|
1340
|
+
if (classes) lines.push('html class="' + classes + '"');
|
|
1341
|
+
|
|
1342
|
+
(params.get('d') || '')
|
|
1343
|
+
.split(/\s+/)
|
|
1344
|
+
.filter(Boolean)
|
|
1345
|
+
.forEach((name) => lines.push('markup: no ' + name));
|
|
1346
|
+
|
|
1347
|
+
const pairs = [...params.entries()].filter(([key]) =>
|
|
1348
|
+
key.startsWith('--')
|
|
1349
|
+
);
|
|
1350
|
+
if (pairs.length) {
|
|
1351
|
+
lines.push(
|
|
1352
|
+
':root {',
|
|
1353
|
+
...pairs.map(([key, value]) => ` ${key}: ${value};`),
|
|
1354
|
+
'}'
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return lines.length ? lines.join('\n') : null;
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1119
1361
|
const isOn = (input) =>
|
|
1120
1362
|
input.hasAttribute('data-variant-invert')
|
|
1121
1363
|
? !input.checked
|
|
@@ -1127,8 +1369,10 @@
|
|
|
1127
1369
|
if (document.activeElement === setup) return;
|
|
1128
1370
|
|
|
1129
1371
|
const classes = [];
|
|
1372
|
+
// Unformatted, because both the box and the link are written from
|
|
1373
|
+
// these and formatting only one of the two ways is a bug waiting
|
|
1130
1374
|
const notes = [];
|
|
1131
|
-
const
|
|
1375
|
+
const pairs = [];
|
|
1132
1376
|
|
|
1133
1377
|
// The palette is part of the setup, and it is printed first because a
|
|
1134
1378
|
// project sets its brand colours before it retunes anything else
|
|
@@ -1141,7 +1385,7 @@
|
|
|
1141
1385
|
['--mw-dark-page-background', palette.dark],
|
|
1142
1386
|
['--mw-light-page-background', palette.light],
|
|
1143
1387
|
].forEach(([prop, value]) => {
|
|
1144
|
-
if (value)
|
|
1388
|
+
if (value) pairs.push([prop, value]);
|
|
1145
1389
|
});
|
|
1146
1390
|
|
|
1147
1391
|
controls.forEach((control) => {
|
|
@@ -1152,9 +1396,7 @@
|
|
|
1152
1396
|
if (control.hasAttribute('data-variant-classes')) {
|
|
1153
1397
|
classes.push(control.value);
|
|
1154
1398
|
} else {
|
|
1155
|
-
|
|
1156
|
-
` ${control.dataset.variantProp}: ${control.value};`
|
|
1157
|
-
);
|
|
1399
|
+
pairs.push([control.dataset.variantProp, control.value]);
|
|
1158
1400
|
}
|
|
1159
1401
|
return;
|
|
1160
1402
|
}
|
|
@@ -1164,7 +1406,7 @@
|
|
|
1164
1406
|
const target = control.dataset.variantTarget;
|
|
1165
1407
|
if (target) {
|
|
1166
1408
|
if (!isOn(control)) {
|
|
1167
|
-
notes.push(target
|
|
1409
|
+
notes.push([target, control.dataset.variantClass]);
|
|
1168
1410
|
}
|
|
1169
1411
|
} else if (isOn(control)) {
|
|
1170
1412
|
classes.push(control.dataset.variantClass);
|
|
@@ -1175,12 +1417,24 @@
|
|
|
1175
1417
|
if (classes.length) {
|
|
1176
1418
|
lines.push('html class="' + classes.join(' ') + '"');
|
|
1177
1419
|
}
|
|
1178
|
-
lines.push(...notes);
|
|
1179
|
-
if (
|
|
1420
|
+
lines.push(...notes.map(([target, name]) => `${target}: no ${name}`));
|
|
1421
|
+
if (pairs.length) {
|
|
1422
|
+
lines.push(
|
|
1423
|
+
':root {',
|
|
1424
|
+
...pairs.map(([prop, value]) => ` ${prop}: ${value};`),
|
|
1425
|
+
'}'
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1180
1428
|
|
|
1181
1429
|
setup.value = lines.length
|
|
1182
1430
|
? lines.join('\n')
|
|
1183
1431
|
: 'Default setup - nothing to change.';
|
|
1432
|
+
|
|
1433
|
+
shareUrl = buildShareUrl(
|
|
1434
|
+
classes,
|
|
1435
|
+
notes.map(([, name]) => name),
|
|
1436
|
+
pairs
|
|
1437
|
+
);
|
|
1184
1438
|
};
|
|
1185
1439
|
|
|
1186
1440
|
const optionClasses = (control) =>
|
|
@@ -1252,21 +1506,103 @@
|
|
|
1252
1506
|
// Picking a palette repaints the page, and the box has to say so
|
|
1253
1507
|
window.mwRefreshSetup = render;
|
|
1254
1508
|
|
|
1255
|
-
setup.addEventListener('input', () => load(setup.value));
|
|
1509
|
+
setup.addEventListener('input', () => inPlace(() => load(setup.value)));
|
|
1256
1510
|
// Normalises what was pasted back into the shape render() writes
|
|
1257
1511
|
setup.addEventListener('blur', render);
|
|
1258
1512
|
|
|
1513
|
+
// Type and spacing switches change the height of all 80 000px of page
|
|
1514
|
+
// above the preview as well, so the section slides out from under
|
|
1515
|
+
// whoever is watching it - Text size alone moves it by 16 000px. Hold
|
|
1516
|
+
// its top edge where it was and the change happens in place.
|
|
1517
|
+
//
|
|
1518
|
+
// Only while the preview is on screen: correcting the scroll for someone
|
|
1519
|
+
// parked up in the component sections would move *them* by those 16 000
|
|
1520
|
+
// instead. `instant` because the smooth default would animate the
|
|
1521
|
+
// correction as a scroll of its own.
|
|
1522
|
+
const previewSection = document.getElementById('preview');
|
|
1523
|
+
|
|
1524
|
+
const inPlace = (change) => {
|
|
1525
|
+
const box = previewSection?.getBoundingClientRect();
|
|
1526
|
+
const watching =
|
|
1527
|
+
box && box.bottom > 0 && box.top < window.innerHeight;
|
|
1528
|
+
if (!watching) return change();
|
|
1529
|
+
|
|
1530
|
+
const before = box.top;
|
|
1531
|
+
change();
|
|
1532
|
+
const shift = previewSection.getBoundingClientRect().top - before;
|
|
1533
|
+
if (shift) window.scrollBy({ top: shift, behavior: 'instant' });
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1259
1536
|
controls.forEach((control) =>
|
|
1260
|
-
control.addEventListener('change', () =>
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1537
|
+
control.addEventListener('change', () =>
|
|
1538
|
+
inPlace(() => {
|
|
1539
|
+
apply(control);
|
|
1540
|
+
render();
|
|
1541
|
+
})
|
|
1542
|
+
)
|
|
1264
1543
|
);
|
|
1265
1544
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1545
|
+
const shared = readHash();
|
|
1546
|
+
if (shared) {
|
|
1547
|
+
// Drives the switches, which drive the page - then render() normalises
|
|
1548
|
+
// the box and writes the link back in the order it prints
|
|
1549
|
+
load(shared);
|
|
1550
|
+
render();
|
|
1551
|
+
} else {
|
|
1552
|
+
// Without a link the showcase opens in its own look, every time. That
|
|
1553
|
+
// has to be spelled out: Firefox restores form state across a reload,
|
|
1554
|
+
// so the switches would otherwise come back set the way the last visit
|
|
1555
|
+
// left them, and the page with them.
|
|
1556
|
+
//
|
|
1557
|
+
// The markup is the one source for that starting state - the written
|
|
1558
|
+
// defaults for a plain switch, and for a targeted one whether the
|
|
1559
|
+
// class it stands for is actually on the element it names. A header
|
|
1560
|
+
// shipped without mw-header-reveal keeps it off.
|
|
1561
|
+
controls.forEach((control) => {
|
|
1562
|
+
const target = control.dataset.variantTarget;
|
|
1563
|
+
const el = target && document.querySelector(target);
|
|
1564
|
+
|
|
1565
|
+
if (el) {
|
|
1566
|
+
const present = el.classList.contains(
|
|
1567
|
+
control.dataset.variantClass
|
|
1568
|
+
);
|
|
1569
|
+
control.checked = control.hasAttribute('data-variant-invert')
|
|
1570
|
+
? !present
|
|
1571
|
+
: present;
|
|
1572
|
+
} else if (control.type === 'checkbox') {
|
|
1573
|
+
control.checked = control.defaultChecked;
|
|
1574
|
+
} else {
|
|
1575
|
+
const preset = [...control.options].find(
|
|
1576
|
+
(o) => o.defaultSelected
|
|
1577
|
+
);
|
|
1578
|
+
control.value = preset ? preset.value : '';
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
controls.forEach(apply);
|
|
1583
|
+
render();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
document
|
|
1587
|
+
.getElementById('variant-share')
|
|
1588
|
+
?.addEventListener('click', async (event) => {
|
|
1589
|
+
const button = event.currentTarget;
|
|
1590
|
+
const label = document.getElementById('variant-share-label');
|
|
1591
|
+
try {
|
|
1592
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
1593
|
+
label.textContent = 'Copied';
|
|
1594
|
+
} catch {
|
|
1595
|
+
// Denied permission or an insecure origin. The address bar is no
|
|
1596
|
+
// help here - it never carries the setup - so hand the link over
|
|
1597
|
+
// in the box instead, where it can be selected by hand.
|
|
1598
|
+
setup.value = shareUrl;
|
|
1599
|
+
setup.select();
|
|
1600
|
+
label.textContent = 'Copy from the box';
|
|
1601
|
+
}
|
|
1602
|
+
setTimeout(() => {
|
|
1603
|
+
label.textContent = 'Copy link';
|
|
1604
|
+
}, 1600);
|
|
1605
|
+
});
|
|
1270
1606
|
});
|
|
1271
1607
|
|
|
1272
1608
|
// for the modals - the framework handles both shapes, div and <dialog>
|