maverick-wave 5.4.0 → 5.5.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.
@@ -44,14 +44,14 @@ Load the one you need - do not read them all up front.
44
44
  ```html
45
45
  <link
46
46
  rel="stylesheet"
47
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.4.0/maverick-wave.min.css"
47
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.0/maverick-wave.min.css"
48
48
  />
49
49
  <link
50
50
  rel="stylesheet"
51
51
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
52
52
  />
53
53
  ...
54
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.4.0/maverick-wave.min.js"></script>
54
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.0/maverick-wave.min.js"></script>
55
55
  ```
56
56
 
57
57
  Pin the version. The JS file is optional and only for server-rendered/static pages -
@@ -369,3 +369,94 @@ about the class name.
369
369
 
370
370
  `mw-progress-percent` only appends the `%` sign - the number comes from you, and
371
371
  so does the width.
372
+
373
+ ## Scroll reveal
374
+
375
+ `mw-reveal` runs on the browser's scroll timeline and needs nothing from you -
376
+ except in Firefox, which ships none, where the block appears without motion.
377
+ This directive is the Firefox half: it adds to the class, never replaces it.
378
+
379
+ ```ts
380
+ import {
381
+ DestroyRef,
382
+ Directive,
383
+ ElementRef,
384
+ afterNextRender,
385
+ inject,
386
+ signal,
387
+ } from '@angular/core';
388
+
389
+ @Directive({
390
+ selector: '[mwReveal]',
391
+ host: {
392
+ '[class.mw-reveal-hidden]': "state() === 'hidden'",
393
+ '[class.mw-reveal-run]': "state() === 'run'",
394
+ '[style.animation-delay.ms]': 'delay()',
395
+ },
396
+ })
397
+ export class MwRevealDirective {
398
+ private readonly host =
399
+ inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
400
+
401
+ protected readonly state = signal<'off' | 'hidden' | 'run'>('off');
402
+ protected readonly delay = signal(0);
403
+
404
+ constructor() {
405
+ const destroyRef = inject(DestroyRef);
406
+
407
+ afterNextRender(() => {
408
+ if (CSS.supports('animation-timeline', 'view()')) return;
409
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
410
+ // Already on screen: past its entry range in a timeline browser too
411
+ if (this.host.getBoundingClientRect().top < innerHeight) return;
412
+
413
+ this.state.set('hidden');
414
+
415
+ const observer = new IntersectionObserver(
416
+ ([entry]) => {
417
+ if (!entry.isIntersecting) return;
418
+ observer.disconnect();
419
+ this.delay.set(this.columnDelay());
420
+ this.state.set('run');
421
+ },
422
+ { threshold: 0.15 }
423
+ );
424
+
425
+ observer.observe(this.host);
426
+ destroyRef.onDestroy(() => observer.disconnect());
427
+ });
428
+ }
429
+
430
+ // The wave a grid draws in CSS, read back from the columns the browser
431
+ // rendered - one on a phone, so the stagger disappears with them.
432
+ private columnDelay(): number {
433
+ const grid = this.host.parentElement;
434
+ if (!grid?.classList.contains('mw-reveal-stagger')) return 0;
435
+ const columns =
436
+ getComputedStyle(grid).gridTemplateColumns.split(' ').length;
437
+ return ([...grid.children].indexOf(this.host) % columns) * 90;
438
+ }
439
+ }
440
+ ```
441
+
442
+ A single block carries `mw-reveal`, a grid carries `mw-reveal-stagger` and its
443
+ children carry nothing but the directive:
444
+
445
+ ```html
446
+ <article class="mw-card mw-reveal" mwReveal>...</article>
447
+
448
+ <div class="mw-grid-4 mw-reveal-stagger">
449
+ @for (item of items(); track item.id) {
450
+ <article class="mw-card" mwReveal>...</article>
451
+ }
452
+ </div>
453
+ ```
454
+
455
+ Unlike the shipped script this also covers what a route renders later - the
456
+ directive runs per element, not once per page.
457
+
458
+ `mw-header-reveal` and `mw-parallax` need the same treatment and the same
459
+ `CSS.supports` guard. The header is a class toggled past a scroll threshold; the
460
+ parallax writes `--mw-parallax-progress` (0 to 1) on each `mw-parallax-media`
461
+ from a `requestAnimationFrame` loop, reading every layer's rect before writing
462
+ to any of them.
@@ -20,7 +20,7 @@ accordion FAQ.
20
20
 
21
21
  <link
22
22
  rel="stylesheet"
23
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.4.0/maverick-wave.min.css"
23
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.0/maverick-wave.min.css"
24
24
  />
25
25
  <link
26
26
  rel="stylesheet"
@@ -487,7 +487,7 @@ accordion FAQ.
487
487
  <div class="mw-modal-backdrop" onclick="closeModal('demo')"></div>
488
488
  </div>
489
489
 
490
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.4.0/maverick-wave.min.js"></script>
490
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.0/maverick-wave.min.js"></script>
491
491
  <script>
492
492
  // The only thing the shipped script does not cover: opening a modal.
493
493
  // Closing works through .mw-modal-close, the backdrop is wired above.
@@ -523,8 +523,9 @@ accordion FAQ.
523
523
  - `mw-reveal-stagger` goes on the grid (or `mw-columns-*`) and reveals every
524
524
  child as it scrolls in, the second and third of each three a beat later.
525
525
  `mw-reveal` on a single block reveals that block as one. Both are off under
526
- `prefers-reduced-motion`, and a browser without scroll timelines renders the
527
- blocks in place - never invisible.
526
+ `prefers-reduced-motion`. A page like this one loads the script, so Firefox -
527
+ which has no scroll timelines - runs the same entrance off an observer;
528
+ without the script the blocks stand in place, never invisible.
528
529
  - `mw-offer` on a card pushes the price row to the bottom of the body, so the
529
530
  prices in a row line up. The `mw-card-feature` frame around the middle plan
530
531
  reaches 29px above its card - the `mw-mt-10` on the grid is that room.
@@ -536,6 +536,9 @@ still renders the same, for markup written before 4.12.
536
536
  Open state = `mw-active` on header **and** content (bare `active` still works
537
537
  but is deprecated). The icon rotates via the **header** state - putting the
538
538
  class on the icon instead does nothing. Content taller than 500px scrolls.
539
+ The `mw-accordion-content-inner` wrapper is required and not decoration: the
540
+ panel opens by animating a grid row and that wrapper is the row, so content
541
+ placed straight into `mw-accordion-content` never collapses when closed.
539
542
  Toggling is JS - see `references/javascript.md`; the shipped script keeps
540
543
  `aria-expanded` in step when the header is a button.
541
544
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What `maverick-wave.min.js` is
4
4
 
5
- One vanilla IIFE, no dependencies, ~8 kB. It queries the DOM **once** on
5
+ One vanilla IIFE, no dependencies, ~23 kB. It queries the DOM **once** on
6
6
  `DOMContentLoaded` and attaches listeners. There is no re-init API, no
7
7
  `MutationObserver`, no exported module - it is built for a server-rendered or
8
8
  static page.
@@ -44,9 +44,26 @@ classes are the entire contract.
44
44
  | Calendar | Renders the month or week grid from `data-calendar="month | week"`, pages with `data-calendar-nav`, draws the status dots from `data-calendar-markers`, toggles `mw-selected`and emits`mw-calendar-select` | Render the cells from a signal and bind `mw-calendar-adjacent`, `mw-calendar-weekend`, `mw-calendar-today` and `mw-selected` yourself; none of the `data-calendar-*` attributes are needed |
45
45
  | Localhost indicator | On a local hostname, prepends `mw-localhost-indicator-pulse` to the header when it carries `mw-localhost-indicator-activated` | Render the element conditionally |
46
46
  | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
47
- | Color swatches | Showcase-only (prints computed hex values) | Not needed |
47
+ | Color swatches | Showcase-only (prints computed hex values); exposes `window.mwRefreshColorSwatches` to read them again after a root colour changed | Not needed |
48
48
  | Dropdown | Delegated to the document: closes the open `mw-dropdown` on Escape, on a click elsewhere and on a click on a `mw-dropdown-item`, and returns focus to the `summary` | The `<details>` does the opening, the keyboard and the state on its own. Rebuild only the two behaviours markup cannot express - or bind `[attr.open]` and keep them in the component |
49
49
  | Language switcher | Keeps the trigger's flag and code in step with the chosen item, moves `mw-active` and `aria-current`, and fires `mw-language-change` (`detail: { lang, name }`) on the switcher | Bind the trigger from your locale signal and switch the language in your own i18n service; the menu itself is a `<details>` and needs nothing |
50
+ | Scroll reveal | Firefox only: an `IntersectionObserver` adds `mw-reveal-hidden` to what is still below the fold and swaps it for `mw-reveal-run` on entry, with an `animation-delay` per grid column | A directive per element - see `examples/angular-services.md` |
51
+ | Header reveal | Firefox only: toggles `mw-header-away` and `mw-announcement-away` past 270px of scroll, and adds the transition class one frame later so the bar does not slide away on load | The same two classes bound to a scroll signal, behind the same guard |
52
+ | Parallax | Firefox only: writes `--mw-parallax-progress` (0 to 1) on every `mw-parallax-media` from a `requestAnimationFrame` loop | The same, reading every layer's rect before writing to any of them |
53
+
54
+ ## Scroll-driven animations
55
+
56
+ Four things ride the browser's own scroll timeline: `mw-reveal`,
57
+ `mw-header-reveal`, `mw-parallax` and the `mw-progress-fill` scrub. In Chrome,
58
+ Edge and Safari they need no script at all. Firefox ships no scroll timelines,
59
+ so there - and only there - the shipped JS stands in for them. Each one checks
60
+ `CSS.supports('animation-timeline', ...)` first and does nothing where the
61
+ browser has it.
62
+
63
+ Without the script Firefox loses the motion and nothing else: cards stand in
64
+ place, the picture holds still. The exception is `mw-header-reveal`, where the
65
+ bar then sits over the hero from the first paint - a layout difference, not a
66
+ missing effect.
50
67
 
51
68
  ## Modals and progress bars
52
69
 
@@ -142,8 +142,9 @@ A fixed `mw-announcement` rides along without a class of its own - both cover
142
142
  the height of the pair, so they arrive as one block rather than the ribbon
143
143
  catching up. Focus inside either brings both back regardless of the scroll
144
144
  position, so tabbing never lands on a link that is off screen (WCAG 2.4.11).
145
- Without scroll timelines, or under `prefers-reduced-motion`, the bar is simply
146
- there.
145
+ Under `prefers-reduced-motion` the bar is simply there, and so it is without
146
+ scroll timelines unless `maverick-wave.min.js` is on the page - in Firefox that
147
+ is what rides it in (`javascript.md`).
147
148
 
148
149
  **Localhost indicator.** Put `mw-localhost-indicator-activated` on the header
149
150
  and the shipped JS prepends a pulsing bar when the host is localhost/127.0.0.1/
@@ -322,8 +323,10 @@ is taken on a phone. The bobbing stops under `prefers-reduced-motion`.
322
323
  **Parallax** - `mw-parallax` on the container plus a `mw-parallax-media` child
323
324
  moves the picture into its own layer. The container drops its own background and
324
325
  the layer reads `--mw-hero-background`, so the image stays configured in one
325
- place. Both modes run on the browser's scroll timeline - no listener, and no
326
- `background-attachment: fixed`, which iOS ignores.
326
+ place. Both modes run on the browser's scroll timeline, and on no
327
+ `background-attachment: fixed`, which iOS ignores. Firefox has neither timeline;
328
+ there the shipped JS moves the layers instead, and without it the picture simply
329
+ sits still (`javascript.md`).
327
330
 
328
331
  ```html
329
332
  <header class="mw-header mw-header-reveal">...</header>
@@ -467,19 +470,22 @@ with it; `mw-text-capitalize` is the plain transform.
467
470
  uses. See the scale table in `SKILL.md`. Never hand-roll a `box-shadow`.
468
471
 
469
472
  **Scroll entrance** - `mw-reveal` lets a block rise briefly as it scrolls into
470
- view, driven by the browser's scroll timeline (`animation-timeline: view()`) -
471
- no JavaScript, no observer. The block stays hidden until it is 8dvh above the
472
- bottom edge and has arrived by 34dvh, the same zone for every block whatever
473
- its height, so the motion happens where the eye is and is over before the
474
- block is read. Doubly guarded: `prefers-reduced-motion` turns it
475
- off, and a browser without scroll timelines renders the block in place instead
476
- of leaving it invisible. Put it on section content, not on the section itself -
477
- a screen-high band finishes its entrance before its content is halfway up.
473
+ view, driven by the browser's scroll timeline (`animation-timeline: view()`).
474
+ The block stays hidden while it is still below the bottom edge and has arrived
475
+ three fifths of the way in - measured along its own entry, so it is never still
476
+ transparent once it stands in its place, whatever its height. A block taller
477
+ than the screen is capped at one viewport by the entry phase itself. Doubly guarded:
478
+ `prefers-reduced-motion` turns it off, and a browser without scroll timelines
479
+ renders the block in place instead of leaving it invisible. Firefox is that
480
+ browser - there the shipped JS runs the entrance off an `IntersectionObserver`,
481
+ so a page without the script keeps the block and loses only the motion
482
+ (`javascript.md`). Put it on section content, not on the section itself - a
483
+ screen-high band finishes its entrance before its content is halfway up.
478
484
 
479
485
  A row of cards crosses the viewport edge together, so `mw-reveal` on each of
480
486
  them rises as one slab. `mw-reveal-stagger` goes on the **grid** instead: every
481
- child reveals, and each one in a row 4dvh of scroll after the one before it -
482
- a wave across the row. One class, nothing per card. On `mw-grid-2` to `mw-grid-5` and
487
+ child reveals, and each one in a row a tenth of that entry after the one
488
+ before it - a wave across the row. One class, nothing per card. On `mw-grid-2` to `mw-grid-5` and
483
489
  their `-lg` variants the wave follows the actual columns at every breakpoint:
484
490
  four steps in a four-column row, two once it has collapsed to two. Any other
485
491
  container - `mw-columns-*`, a layout of your own - gets a fixed cycle of three.
@@ -9,6 +9,10 @@ borders, the theme's surface stack, the ink variant the dark theme needs. That i
9
9
  the difference to pre-3.4.0, where derived values were baked in at compile time
10
10
  and a palette switch meant setting 41 variables.
11
11
 
12
+ Writing those tokens on `<html>` at runtime is how a palette is swapped without
13
+ a reload. It repaints the same breadth the theme flip does and needs the same
14
+ guard against a page-wide interpolation - see **Light & dark** below.
15
+
12
16
  Browser floor for that: `color-mix()` **and** `oklch(from ...)` - Chrome 119+,
13
17
  Safari 16.4+, Firefox 128+. The same range is declared as `browserslist` in
14
18
  `package.json`, so Autoprefixer and cssnano target exactly it.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ Patch releases are only for test purposes - here I only document major and minor
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [5.5.0] - 2026-09-12
10
+
11
+ ### Added
12
+
13
+ - showcase themes
14
+
15
+ ### Fixed
16
+
17
+ - some firefox fixes
18
+ - scrolling
19
+
9
20
  ## [5.4.0] - 2026-09-11
10
21
 
11
22
  ### Added
package/CLAUDE.md CHANGED
@@ -38,7 +38,7 @@ There are no test or lint scripts.
38
38
 
39
39
  **JavaScript** (`src/js/main.js`) - Single file. All components auto-initialize on `DOMContentLoaded`. Includes: gallery and image sliders, theme toggle (persisted to localStorage, transitions suppressed during the flip), accordion, mobile nav, progress bar (IntersectionObserver), scroll spy, tabs (which also get their ARIA and arrow-key handling here), alerts/toasts, modals, range inputs, dropdowns, language switcher, checkbox lists, kanban board, calendar.
40
40
 
41
- **HTML showcase** (`index.html` + `src/partials/`) - The top-level `index.html` uses `@@include()` syntax to pull in 60 partials from `src/partials/`. These compile into `dist/index.html`.
41
+ **HTML showcase** (`index.html` + `src/partials/`) - The top-level `index.html` uses `@@include()` syntax to pull in 63 partials from `src/partials/`. These compile into `dist/index.html`.
42
42
 
43
43
  ## Naming Conventions
44
44
 
package/README.md CHANGED
@@ -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.4.0/maverick-wave.min.css"
45
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.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.4.0/maverick-wave.min.js"></script>
50
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.5.0/maverick-wave.min.js"></script>
51
51
  </body>
52
52
  </html>
53
53
  ```
@@ -515,9 +515,8 @@ maverick-wave/
515
515
  │ ├── utilities/ # Spacing, flex, display helpers
516
516
  │ └── main.scss # SCSS entry point
517
517
  ├── .claude/
518
- │ ├── commands/mw.md # Claude Code slash command (Angular quick reference)
519
518
  │ └── skills/
520
- │ └── maverick-wave/ # Claude Code skill: full usage guide + examples
519
+ │ └── mw-maverick-wave/ # Claude Code skill: full usage guide + examples
521
520
  ├── scripts/verify.js # Class and token consistency check (npm run verify)
522
521
  ├── .prettierrc.json # Prettier configuration
523
522
  ├── gulpfile.js # Gulp tasks configuration
package/index.html CHANGED
@@ -118,6 +118,144 @@
118
118
  height: 50px;
119
119
  }
120
120
  }
121
+
122
+ /* Palette switcher, showcase only. The framework derives every tone from
123
+ the root colors, so the panel writes nothing but those. */
124
+ .palette-fab {
125
+ position: fixed;
126
+ right: 18px;
127
+ bottom: 18px;
128
+ z-index: 400;
129
+ width: 52px;
130
+ height: 52px;
131
+ border-radius: 50%;
132
+ border: 1px solid var(--mw-border-accent);
133
+ background: var(--mw-primary-color);
134
+ color: var(--mw-primary-accent-text-color);
135
+ box-shadow: var(--mw-elevation-4);
136
+ font-size: 1.25rem;
137
+ cursor: pointer;
138
+ transition: var(--mw-transition);
139
+ }
140
+
141
+ .palette-fab:hover {
142
+ background: var(--mw-primary-color-hover);
143
+ }
144
+
145
+ .palette-fab:active {
146
+ transform: scale(0.92);
147
+ }
148
+
149
+ .palette-panel {
150
+ position: fixed;
151
+ right: 18px;
152
+ bottom: 82px;
153
+ z-index: 400;
154
+ width: 300px;
155
+ padding: 18px;
156
+ border-radius: 12px;
157
+ border: 1px solid var(--mw-border);
158
+ background: var(--mw-card-background);
159
+ box-shadow: var(--mw-elevation-5);
160
+ }
161
+
162
+ .palette-heading {
163
+ font-size: 1.1rem;
164
+ margin-bottom: 4px;
165
+ }
166
+
167
+ .palette-hint {
168
+ font-size: 0.8rem;
169
+ line-height: 1.35;
170
+ color: var(--mw-text-muted-color);
171
+ margin-bottom: 14px;
172
+ }
173
+
174
+ .palette-grid {
175
+ display: grid;
176
+ grid-template-columns: 1fr 1fr;
177
+ gap: 6px;
178
+ }
179
+
180
+ .palette-item {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ padding: 6px 8px;
185
+ border-radius: 8px;
186
+ border: 1px solid var(--mw-border);
187
+ background: transparent;
188
+ color: var(--mw-text-color);
189
+ font-size: 0.8rem;
190
+ text-align: left;
191
+ cursor: pointer;
192
+ transition: var(--mw-transition);
193
+ }
194
+
195
+ .palette-item::before {
196
+ content: '';
197
+ width: 16px;
198
+ height: 16px;
199
+ flex-shrink: 0;
200
+ border-radius: 50%;
201
+ background: linear-gradient(
202
+ 135deg,
203
+ var(--swatch-primary) 50%,
204
+ var(--swatch-secondary) 50%
205
+ );
206
+ }
207
+
208
+ .palette-item:hover,
209
+ .palette-item.mw-active {
210
+ border-color: var(--mw-primary-text-color);
211
+ }
212
+
213
+ .palette-item.mw-active {
214
+ background: var(--mw-primary-background);
215
+ }
216
+
217
+ .palette-custom {
218
+ display: flex;
219
+ gap: 10px;
220
+ margin-top: 14px;
221
+ padding-top: 12px;
222
+ border-top: 1px solid var(--mw-border);
223
+ }
224
+
225
+ .palette-field {
226
+ flex: 1;
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 4px;
230
+ font-size: 0.75rem;
231
+ color: var(--mw-text-muted-color);
232
+ }
233
+
234
+ .palette-field input {
235
+ width: 100%;
236
+ height: 30px;
237
+ padding: 2px;
238
+ border: 1px solid var(--mw-border);
239
+ border-radius: 6px;
240
+ background: var(--mw-card-background);
241
+ cursor: pointer;
242
+ }
243
+
244
+ @media (max-width: 576px) {
245
+ .palette-fab {
246
+ right: 12px;
247
+ bottom: 12px;
248
+ width: 46px;
249
+ height: 46px;
250
+ }
251
+
252
+ .palette-panel {
253
+ right: 12px;
254
+ left: 12px;
255
+ bottom: 68px;
256
+ width: auto;
257
+ }
258
+ }
121
259
  </style>
122
260
 
123
261
  <script src="maverick-wave.min.js"></script>
@@ -538,6 +676,17 @@
538
676
  </div>
539
677
  </div>
540
678
  </section>
679
+ <!-- History section -->
680
+ <section id="history" class="mw-section mw-section-alternate">
681
+ <div class="mw-container">
682
+ <h2 class="mw-section-title">History</h2>
683
+ <p class="mw-section-intro">
684
+ The major releases so far. Everything in between - the minors and
685
+ the patch releases - sits in the changelog.
686
+ </p>
687
+ @@include('./src/partials/history-container.html')
688
+ </div>
689
+ </section>
541
690
  </main>
542
691
 
543
692
  <!-- Footer -->
@@ -546,6 +695,9 @@
546
695
  @@include('./src/partials/footer-container.html')
547
696
  </div>
548
697
  </footer>
698
+
699
+ <!-- Palette switcher -->
700
+ @@include('./src/partials/palette-container.html')
549
701
  <script>
550
702
  // just for the showcase (login button triggers login error)
551
703
  document.addEventListener('DOMContentLoaded', () => {
@@ -629,6 +781,111 @@
629
781
  apply(cycleSwitch.querySelector('.mw-active'));
630
782
  });
631
783
 
784
+ // just for the showcase: the palette switcher. Every tone in the framework
785
+ // is derived from the root colors with color-mix(), so writing those on the
786
+ // root element repaints the page - bar, ink, borders and tints included.
787
+ // Nothing is persisted - a palette is for looking at, a reload is the way back.
788
+ document.addEventListener('DOMContentLoaded', () => {
789
+ const fab = document.getElementById('palette-fab');
790
+ const panel = document.getElementById('palette-panel');
791
+ const primaryInput = document.getElementById('palette-primary');
792
+ const secondaryInput = document.getElementById('palette-secondary');
793
+ const items = [...document.querySelectorAll('.palette-item')];
794
+ if (!fab || !panel) return;
795
+
796
+ const root = document.documentElement;
797
+
798
+ const DARK_INK = '#131925';
799
+ const LIGHT_INK = '#f2f6fc';
800
+
801
+ const luminance = (hex) => {
802
+ const [r, g, b] = [1, 3, 5].map((i) => {
803
+ const channel = parseInt(hex.slice(i, i + 2), 16) / 255;
804
+ return channel <= 0.04045
805
+ ? channel / 12.92
806
+ : ((channel + 0.055) / 1.055) ** 2.4;
807
+ });
808
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
809
+ };
810
+
811
+ const contrast = (a, b) => {
812
+ const [high, low] = [luminance(a), luminance(b)].sort(
813
+ (x, y) => y - x
814
+ );
815
+ return (high + 0.05) / (low + 0.05);
816
+ };
817
+
818
+ // Measured, not thresholded - a lightness cutoff gets a saturated mid
819
+ // tone wrong
820
+ const ink = (fill) =>
821
+ contrast(fill, DARK_INK) > contrast(fill, LIGHT_INK)
822
+ ? DARK_INK
823
+ : LIGHT_INK;
824
+
825
+ // Cuts the transition the new colours started: the whole page
826
+ // interpolating at once is the expensive part, and the hex labels in the
827
+ // Colors section read computed values, so they need the landed ones
828
+ const settle = () => {
829
+ root.classList.add('mw-theme-switching');
830
+ void root.offsetHeight;
831
+ root.classList.remove('mw-theme-switching');
832
+ window.mwRefreshColorSwatches?.();
833
+ };
834
+
835
+ const paint = (name, color) => {
836
+ root.style.setProperty(`--mw-${name}-color`, color);
837
+ root.style.setProperty(`--mw-${name}-accent-text-color`, ink(color));
838
+ };
839
+
840
+ items.forEach((item) => {
841
+ const { primary, secondary, dark, light } = item.dataset;
842
+ item.style.setProperty('--swatch-primary', primary);
843
+ item.style.setProperty('--swatch-secondary', secondary);
844
+
845
+ item.addEventListener('click', () => {
846
+ items.forEach((other) =>
847
+ other.classList.toggle('mw-active', other === item)
848
+ );
849
+ paint('primary', primary);
850
+ paint('secondary', secondary);
851
+ root.style.setProperty('--mw-dark-page-background', dark);
852
+ root.style.setProperty('--mw-light-page-background', light);
853
+ primaryInput.value = primary;
854
+ secondaryInput.value = secondary;
855
+ settle();
856
+ });
857
+ });
858
+
859
+ // A hand-picked color belongs to no preset - the page backgrounds stay put
860
+ [
861
+ ['primary', primaryInput],
862
+ ['secondary', secondaryInput],
863
+ ].forEach(([name, input]) => {
864
+ input.addEventListener('input', () => {
865
+ items.forEach((item) => item.classList.remove('mw-active'));
866
+ paint(name, input.value);
867
+ });
868
+ // Dragging the picker fires `input` per pixel - the rest waits for the end
869
+ input.addEventListener('change', settle);
870
+ });
871
+
872
+ const toggle = (open) => {
873
+ panel.hidden = !open;
874
+ fab.setAttribute('aria-expanded', String(open));
875
+ };
876
+
877
+ fab.addEventListener('click', (event) => {
878
+ event.stopPropagation();
879
+ toggle(panel.hidden);
880
+ });
881
+ document.addEventListener('click', (event) => {
882
+ if (!panel.hidden && !panel.contains(event.target)) toggle(false);
883
+ });
884
+ document.addEventListener('keydown', (event) => {
885
+ if (event.key === 'Escape') toggle(false);
886
+ });
887
+ });
888
+
632
889
  // for the modals - the framework handles both shapes, div and <dialog>
633
890
  const openModal = window.mwOpenModal;
634
891
  const closeModal = window.mwCloseModal;