maverick-wave 5.15.0 → 5.17.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.
@@ -48,18 +48,20 @@ Load the one you need - do not read them all up front.
48
48
  />
49
49
  <link
50
50
  rel="stylesheet"
51
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.0/maverick-wave.min.css"
51
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5/maverick-wave.min.css"
52
52
  />
53
53
  <link
54
54
  rel="stylesheet"
55
55
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
56
56
  />
57
57
  ...
58
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.0/maverick-wave.min.js"></script>
58
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5/maverick-wave.min.js"></script>
59
59
  ```
60
60
 
61
- Pin the version. The JS file is optional and only for server-rendered/static pages -
62
- see below. `viewport-fit=cover` is not optional - see pitfall 30.
61
+ `@5` keeps this snippet current; in a real project pin the exact version so a
62
+ build stays reproducible. The JS file is optional and only for
63
+ server-rendered/static pages - see below. `viewport-fit=cover` is not optional -
64
+ see pitfall 30.
63
65
 
64
66
  ### Angular (or any SPA)
65
67
 
@@ -129,7 +131,7 @@ The classes that mean something _other_ than "on" keep their own names:
129
131
  | Checkbox list item (`li`) | `mw-selected` |
130
132
  | Calendar day, picked | `mw-selected` |
131
133
  | Kanban ticket being edited | `mw-kanban-editing` |
132
- | Modal overlay | `mw-modal-open` |
134
+ | Modal, older overlay div | `mw-modal-open` (a `<dialog>` carries `open` itself) |
133
135
  | Alert, dismissing / dismissed | `mw-alert-closing` → `mw-alert-closed` (`display: none`) |
134
136
  | Field wrapper in error | `mw-field-has-error` |
135
137
  | Single form control in error | `mw-form-element-error` |
@@ -234,8 +236,9 @@ none of them survive into the compiled CSS. Full list in `references/theming.md`
234
236
  - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport, the condition
235
237
  every target-size rule in the framework hangs under.
236
238
  - `touch-floor($size: 2.75rem)` - that condition plus a `min-height`.
237
- - `hit-area($grow: 6px)` - grows the hit area through `::after` without touching
238
- the silhouette.
239
+ - `hit-area($grow: 6px, $box: 1.5rem)` - grows the hit area through `::after`
240
+ without touching the silhouette. An element carrying a `data-tooltip` owns that
241
+ pseudo for its arrow, so those are skipped and grow to `$box` instead.
239
242
  - `hover` / `hover-move`, `focus-ring`, `focus-ring-inset`, `field-focus`,
240
243
  `truncate`, `surface`.
241
244
 
@@ -248,40 +248,44 @@ export class InvoiceListComponent {
248
248
  </div>
249
249
  }
250
250
 
251
- <!-- Delete confirmation -->
252
- <div class="mw-modal-overlay" [class.mw-modal-open]="toDelete() !== null">
253
- <div class="mw-modal mw-modal-sm">
254
- <div class="mw-modal-header">
255
- <h4 class="mw-modal-title">Delete invoice</h4>
256
- <button type="button" class="mw-modal-close" (click)="toDelete.set(null)">
257
- &#120299;
258
- </button>
259
- </div>
260
- <div class="mw-modal-body">
261
- <p class="mw-text-center">
262
- <strong>Delete {{ toDelete()?.number }}?</strong>
263
- </p>
264
- <p class="mw-text-center mw-text-muted">This action cannot be undone.</p>
265
- </div>
266
- <div class="mw-modal-footer">
267
- <button
268
- type="button"
269
- class="mw-btn mw-btn-outline"
270
- (click)="toDelete.set(null)"
271
- >
272
- Cancel
273
- </button>
274
- <button
275
- type="button"
276
- class="mw-btn mw-btn-danger"
277
- (click)="confirmDelete()"
278
- >
279
- <i class="fas fa-trash"></i> Delete
280
- </button>
281
- </div>
251
+ <!-- Delete confirmation - ModalDirective is in angular-services.md -->
252
+ <dialog
253
+ class="mw-modal mw-modal-sm"
254
+ closedby="any"
255
+ appModal
256
+ [isOpen]="toDelete() !== null"
257
+ (dismiss)="toDelete.set(null)"
258
+ aria-labelledby="delete-title"
259
+ >
260
+ <div class="mw-modal-header">
261
+ <h4 class="mw-modal-title" id="delete-title">Delete invoice</h4>
262
+ <button type="button" class="mw-modal-close" (click)="toDelete.set(null)">
263
+ &#120299;
264
+ </button>
282
265
  </div>
283
- <div class="mw-modal-backdrop" (click)="toDelete.set(null)"></div>
284
- </div>
266
+ <div class="mw-modal-body">
267
+ <p class="mw-text-center">
268
+ <strong>Delete {{ toDelete()?.number }}?</strong>
269
+ </p>
270
+ <p class="mw-text-center mw-text-muted">This action cannot be undone.</p>
271
+ </div>
272
+ <div class="mw-modal-footer">
273
+ <button
274
+ type="button"
275
+ class="mw-btn mw-btn-outline"
276
+ (click)="toDelete.set(null)"
277
+ >
278
+ Cancel
279
+ </button>
280
+ <button
281
+ type="button"
282
+ class="mw-btn mw-btn-danger"
283
+ (click)="confirmDelete()"
284
+ >
285
+ <i class="fas fa-trash"></i> Delete
286
+ </button>
287
+ </div>
288
+ </dialog>
285
289
  ```
286
290
 
287
291
  ## Notes
@@ -298,7 +302,8 @@ export class InvoiceListComponent {
298
302
  sits left while the figures sit right.
299
303
  - **`mw-tag`, not `mw-tags`**, for a single status chip in a cell.
300
304
  - **`mw-btn-danger` for the destructive action** in the modal footer.
301
- - The modal is always in the DOM and toggled through `mw-modal-open`; the body
302
- scroll lock happens automatically via `body:has(.mw-modal-open)`.
305
+ - The modal is always in the DOM; `ModalDirective` calls `showModal()` when the
306
+ signal turns true, and Escape, the focus trap and the scroll lock follow from
307
+ the element.
303
308
  - The toast stack lives in the app shell, not on this page - see
304
309
  `angular-services.md`.
@@ -159,45 +159,94 @@ from the signal is cleaner.
159
159
 
160
160
  ## Modal
161
161
 
162
- No service needed - the overlay is toggled by one class and the body scroll lock
163
- is pure CSS (`body:has(.mw-modal-open)`).
162
+ A `<dialog>` with the same `mw-modal` classes. Escape, the focus trap, the inert
163
+ page behind it and the body scroll lock all come from the element, so what is
164
+ left to write is opening it.
165
+
166
+ `[open]="isOpen()"` does **not** work: the attribute opens a non-modal dialog -
167
+ no top layer, no backdrop, no focus trap. It has to be `showModal()`, which
168
+ needs a directive.
169
+
170
+ ```ts
171
+ @Directive({
172
+ selector: 'dialog[appModal]',
173
+ host: {
174
+ '(close)': 'onClose()',
175
+ '(click)': 'onClick($event)',
176
+ },
177
+ })
178
+ export class ModalDirective {
179
+ readonly isOpen = input(false);
180
+ /** the dialog closed itself - Escape, a backdrop click or a form submit */
181
+ readonly dismiss = output<void>();
182
+
183
+ private readonly host =
184
+ inject<ElementRef<HTMLDialogElement>>(ElementRef).nativeElement;
185
+
186
+ constructor() {
187
+ const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
188
+
189
+ effect(() => {
190
+ const open = this.isOpen();
191
+ if (!isBrowser) return;
192
+ if (open && !this.host.open) this.host.showModal();
193
+ else if (!open && this.host.open) this.host.close();
194
+ });
195
+ }
196
+
197
+ // Only when the dialog closed itself - closing it through the signal already
198
+ // went through whatever set the signal, and reporting that back would run the
199
+ // caller's dismiss handler a second time.
200
+ protected onClose(): void {
201
+ if (this.isOpen()) this.dismiss.emit();
202
+ }
203
+
204
+ // A click on the dialog element is a click on its backdrop; the content sits
205
+ // in .mw-modal-header/body/footer inside it. Stands in for `closedby="any"`
206
+ // where that is not supported yet.
207
+ protected onClick(event: MouseEvent): void {
208
+ if (event.target === this.host) this.host.close();
209
+ }
210
+ }
211
+ ```
164
212
 
165
213
  ```ts
166
214
  @Component({
167
215
  selector: 'app-confirm-dialog',
216
+ imports: [ModalDirective],
168
217
  template: `
169
- <div class="mw-modal-overlay" [class.mw-modal-open]="open()">
170
- <div class="mw-modal mw-modal-sm">
171
- <div class="mw-modal-header">
172
- <h4 class="mw-modal-title">{{ title() }}</h4>
173
- <button
174
- type="button"
175
- class="mw-modal-close"
176
- (click)="cancelled.emit()"
177
- >
178
- &#120299;
179
- </button>
180
- </div>
181
- <div class="mw-modal-body"><ng-content /></div>
182
- <div class="mw-modal-footer">
183
- <button
184
- type="button"
185
- class="mw-btn mw-btn-outline"
186
- (click)="cancelled.emit()"
187
- >
188
- Cancel
189
- </button>
190
- <button
191
- type="button"
192
- class="mw-btn mw-btn-danger"
193
- (click)="confirmed.emit()"
194
- >
195
- Delete
196
- </button>
197
- </div>
218
+ <dialog
219
+ class="mw-modal mw-modal-sm"
220
+ closedby="any"
221
+ appModal
222
+ [isOpen]="open()"
223
+ (dismiss)="cancelled.emit()"
224
+ aria-labelledby="confirm-title"
225
+ >
226
+ <div class="mw-modal-header">
227
+ <h4 class="mw-modal-title" id="confirm-title">{{ title() }}</h4>
228
+ <button type="button" class="mw-modal-close" (click)="cancelled.emit()">
229
+ &#120299;
230
+ </button>
198
231
  </div>
199
- <div class="mw-modal-backdrop" (click)="cancelled.emit()"></div>
200
- </div>
232
+ <div class="mw-modal-body"><ng-content /></div>
233
+ <div class="mw-modal-footer">
234
+ <button
235
+ type="button"
236
+ class="mw-btn mw-btn-outline"
237
+ (click)="cancelled.emit()"
238
+ >
239
+ Cancel
240
+ </button>
241
+ <button
242
+ type="button"
243
+ class="mw-btn mw-btn-danger"
244
+ (click)="confirmed.emit()"
245
+ >
246
+ Delete
247
+ </button>
248
+ </div>
249
+ </dialog>
201
250
  `,
202
251
  changeDetection: ChangeDetectionStrategy.OnPush,
203
252
  })
@@ -209,6 +258,19 @@ export class ConfirmDialogComponent {
209
258
  }
210
259
  ```
211
260
 
261
+ `role="dialog"` and `aria-modal` come from `showModal()`; the title is named
262
+ with `aria-labelledby`. Without `autofocus` on something the browser focuses the
263
+ first focusable element, usually the close button - put it on the field or the
264
+ safe action instead.
265
+
266
+ In tests, jsdom implements none of `<dialog>` - not `showModal`, `show` or
267
+ `close`, only the `open` attribute reflects. Specs that render an open modal
268
+ fail with `showModal is not a function` until a setup file adds them.
269
+
270
+ The `mw-modal-overlay` div toggled by `mw-modal-open` is still styled, for
271
+ markup that predates this - it cannot trap focus or make the page inert, so it
272
+ is not what to write now.
273
+
212
274
  ## Accordion
213
275
 
214
276
  `mw-active` on the header **and** the content.
@@ -23,7 +23,7 @@ accordion FAQ.
23
23
 
24
24
  <link
25
25
  rel="stylesheet"
26
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.0/maverick-wave.min.css"
26
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5/maverick-wave.min.css"
27
27
  />
28
28
  <link
29
29
  rel="stylesheet"
@@ -123,7 +123,7 @@ accordion FAQ.
123
123
  <button
124
124
  type="button"
125
125
  class="mw-btn mw-btn-primary mw-btn-lg"
126
- onclick="openModal('demo')"
126
+ data-mw-modal="demo"
127
127
  >
128
128
  Book a demo
129
129
  </button>
@@ -302,7 +302,7 @@ accordion FAQ.
302
302
  <button
303
303
  type="button"
304
304
  class="mw-btn mw-btn-outline"
305
- onclick="openModal('demo')"
305
+ data-mw-modal="demo"
306
306
  >
307
307
  Contact us
308
308
  </button>
@@ -412,11 +412,7 @@ accordion FAQ.
412
412
  <!-- A link in running text is mw-link, not a button class - it
413
413
  keeps the line height of the paragraph around it -->
414
414
  <p>
415
- <button
416
- type="button"
417
- class="mw-link"
418
- onclick="openModal('demo')"
419
- >
415
+ <button type="button" class="mw-link" data-mw-modal="demo">
420
416
  Book a 20 minute demo</button
421
417
  >, no sales pitch.
422
418
  </p>
@@ -454,53 +450,42 @@ accordion FAQ.
454
450
  </div>
455
451
  </footer>
456
452
 
457
- <!-- Modal: markup stays in the DOM, mw-modal-open shows it -->
458
- <div id="demo" class="mw-modal-overlay">
459
- <div class="mw-modal">
460
- <div class="mw-modal-header">
461
- <h4 class="mw-modal-title">Book a demo</h4>
462
- <button type="button" class="mw-modal-close">&#120299;</button>
463
- </div>
464
- <div class="mw-modal-body">
465
- <form class="mw-form">
466
- <div class="mw-field">
467
- <label class="mw-field-label mw-required" for="demo-email"
468
- >Email</label
469
- >
470
- <div class="mw-input-group">
471
- <span class="mw-input-group-prefix"
472
- ><i class="fas fa-envelope"></i
473
- ></span>
474
- <input id="demo-email" type="email" class="mw-input" required />
475
- </div>
453
+ <!-- Markup stays in the DOM; a trigger carrying data-mw-modal="demo" opens
454
+ it, .mw-modal-close closes it, closedby="any" dismisses on the backdrop -->
455
+ <dialog
456
+ id="demo"
457
+ class="mw-modal"
458
+ closedby="any"
459
+ aria-labelledby="demo-title"
460
+ >
461
+ <div class="mw-modal-header">
462
+ <h4 class="mw-modal-title" id="demo-title">Book a demo</h4>
463
+ <button type="button" class="mw-modal-close">&#120299;</button>
464
+ </div>
465
+ <div class="mw-modal-body">
466
+ <form class="mw-form">
467
+ <div class="mw-field">
468
+ <label class="mw-field-label mw-required" for="demo-email"
469
+ >Email</label
470
+ >
471
+ <div class="mw-input-group">
472
+ <span class="mw-input-group-prefix"
473
+ ><i class="fas fa-envelope"></i
474
+ ></span>
475
+ <input id="demo-email" type="email" class="mw-input" required />
476
476
  </div>
477
- </form>
478
- </div>
479
- <div class="mw-modal-footer">
480
- <button
481
- type="button"
482
- class="mw-btn mw-btn-outline"
483
- onclick="closeModal('demo')"
484
- >
485
- Cancel
486
- </button>
487
- <button type="button" class="mw-btn mw-btn-primary">Send</button>
488
- </div>
477
+ </div>
478
+ </form>
489
479
  </div>
490
- <div class="mw-modal-backdrop" onclick="closeModal('demo')"></div>
491
- </div>
492
-
493
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.0/maverick-wave.min.js"></script>
494
- <script>
495
- // The only thing the shipped script does not cover: opening a modal.
496
- // Closing works through .mw-modal-close, the backdrop is wired above.
497
- function openModal(id) {
498
- document.getElementById(id).classList.add('mw-modal-open');
499
- }
500
- function closeModal(id) {
501
- document.getElementById(id).classList.remove('mw-modal-open');
502
- }
503
- </script>
480
+ <div class="mw-modal-footer">
481
+ <button type="button" class="mw-modal-close mw-btn mw-btn-outline">
482
+ Cancel
483
+ </button>
484
+ <button type="button" class="mw-btn mw-btn-primary">Send</button>
485
+ </div>
486
+ </dialog>
487
+
488
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5/maverick-wave.min.js"></script>
504
489
  </body>
505
490
  </html>
506
491
  ```
@@ -590,66 +590,65 @@ Toggling is JS - see `references/javascript.md`; the shipped script keeps
590
590
  ## Modal
591
591
 
592
592
  ```html
593
- <div id="delete-modal" class="mw-modal-overlay">
594
- <div class="mw-modal mw-modal-sm">
595
- <div class="mw-modal-header">
596
- <h4 class="mw-modal-title">Delete invoice</h4>
597
- <button class="mw-modal-close" type="button">&#120299;</button>
598
- </div>
599
- <div class="mw-modal-body">
600
- <p>Invoice 2026-0042 will be removed.</p>
601
- </div>
602
- <div class="mw-modal-footer">
603
- <p class="mw-actions-note">This action cannot be undone.</p>
604
- <button class="mw-btn mw-btn-outline">Cancel</button>
605
- <button class="mw-btn mw-btn-danger">Delete</button>
606
- </div>
593
+ <dialog id="delete-modal" class="mw-modal mw-modal-sm" closedby="any">
594
+ <div class="mw-modal-header">
595
+ <h4 class="mw-modal-title">Delete invoice</h4>
596
+ <button class="mw-modal-close" type="button" aria-label="Close">
597
+ &#120299;
598
+ </button>
607
599
  </div>
608
- <div class="mw-modal-backdrop"></div>
609
- </div>
600
+ <div class="mw-modal-body">
601
+ <p>Invoice 2026-0042 will be removed.</p>
602
+ </div>
603
+ <div class="mw-modal-footer">
604
+ <p class="mw-actions-note">This action cannot be undone.</p>
605
+ <button class="mw-modal-close mw-btn mw-btn-outline">Cancel</button>
606
+ <button class="mw-btn mw-btn-danger">Delete</button>
607
+ </div>
608
+ </dialog>
610
609
  ```
611
610
 
612
- - The overlay is `display: none` until `mw-modal-open` is added to it. That
613
- class is the whole open/close mechanism.
614
- - Body scroll lock is automatic: the stylesheet uses
615
- `body:has(.mw-modal-open)`. Nothing to implement.
611
+ - Escape, the focus trap, `inert` on the page behind it and the body scroll lock
612
+ all come from the element. There is no overlay wrapper and no backdrop div.
613
+ - Opened with `showModal()`, closed with `close()`. `main.js` wires the close
614
+ buttons and exposes `mwOpenModal(id)` / `mwCloseModal(id)`; a trigger can also
615
+ carry `data-mw-modal="delete-modal"` and needs no script at all.
616
+ - `closedby="any"` dismisses it on a backdrop click; where that attribute is not
617
+ understood yet the script handles the click instead.
616
618
  - Sizes: `mw-modal-sm` 370px, default 520px, `mw-modal-lg` 720px,
617
- `mw-modal-xl` 960px. Height is capped at 80-92dvh, the body scrolls.
618
- - `mw-modal-backdrop` is the click-to-close surface; put the close handler on it.
619
+ `mw-modal-xl` 960px. Height is capped at 80-92dvh, the body scrolls. Below
620
+ 576px it becomes a bottom sheet.
621
+ - `mw-modal-close` is the hook, not a look: on the header X it styles the button,
622
+ on a footer action it only closes. Combine it with `mw-btn mw-btn-primary` and
623
+ the button keeps its own paint.
619
624
  - `mw-modal-body` takes a `mw-form` directly - the form brings the field gaps,
620
625
  the body brings the padding.
621
626
  - `mw-actions-note` is the muted line above the buttons - see below.
627
+ - Name it with `aria-labelledby` pointing at the `mw-modal-title`; `role` and
628
+ `aria-modal` come from `showModal()`. Without `autofocus` the browser focuses
629
+ the first focusable element, usually the close button.
622
630
 
623
- Angular: `<div class="mw-modal-overlay" [class.mw-modal-open]="isOpen()">`.
631
+ Angular: `[open]="isOpen()"` does **not** work - the attribute opens a non-modal
632
+ dialog with no top layer, backdrop or focus trap. It takes a directive calling
633
+ `showModal()`, see `examples/angular-services.md`.
624
634
 
625
- ### The same modal as a `<dialog>`
635
+ ### The older overlay div
626
636
 
627
- Same classes, no overlay wrapper and no backdrop element:
637
+ Still styled, for markup that predates the `<dialog>` above. It cannot trap
638
+ focus or make the page inert, so it is not what to write now.
628
639
 
629
640
  ```html
630
- <dialog id="delete-modal" class="mw-modal mw-modal-sm" closedby="any">
631
- <div class="mw-modal-header">
632
- <h4 class="mw-modal-title">Delete invoice</h4>
633
- <button class="mw-modal-close" type="button" aria-label="Close">
634
- &#120299;
635
- </button>
636
- </div>
637
- <div class="mw-modal-body">…</div>
638
- <div class="mw-modal-footer">…</div>
639
- </dialog>
641
+ <div id="delete-modal" class="mw-modal-overlay">
642
+ <div class="mw-modal mw-modal-sm">…</div>
643
+ <div class="mw-modal-backdrop"></div>
644
+ </div>
640
645
  ```
641
646
 
642
- - Escape, the focus trap, `inert` on the page behind it and the scroll lock all
643
- come from the element. Prefer this shape for anything that asks a question.
644
- - Opened with `showModal()`, closed with `close()`. `main.js` wires the close
645
- buttons and exposes `mwOpenModal(id)` / `mwCloseModal(id)`, both of which take
646
- either shape. A trigger can also carry `data-mw-modal="delete-modal"`.
647
- - `closedby="any"` dismisses it on a backdrop click; where that attribute is not
648
- understood the script handles the click instead.
649
- - Sizes, the bottom sheet below 576px and every `mw-modal-*` part behave exactly
650
- as above - those rules are class-based.
651
-
652
- Angular: bind nothing, call `showModal()` on a `viewChild` ref.
647
+ - `display: none` until `mw-modal-open` is added to the overlay - that class is
648
+ the whole open/close mechanism, in Angular
649
+ `[class.mw-modal-open]="isOpen()"`.
650
+ - Body scroll lock comes from `body:has(.mw-modal-open)`.
651
+ - `mw-modal-backdrop` is the click-to-close surface; put the handler on it.
653
652
 
654
653
  ## Alerts & toasts
655
654
 
@@ -29,7 +29,7 @@ classes are the entire contract.
29
29
  | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
30
30
  | Accordion | Toggles `mw-active` on `mw-accordion-header` and the following `mw-accordion-content`, and writes `aria-expanded` when the header is a `<button>` | `[class.mw-active]="isOpen()"` and `[attr.aria-expanded]="isOpen()"` on the header, `mw-active` on the panel |
31
31
  | Tabs | `data-tab` → panel `id`; sets `mw-active` on nav item and panel, and wires the whole tablist: `role`, `aria-selected`, `aria-controls`, `aria-labelledby`, a roving `tabindex` and arrow/Home/End keys | Track the selected index/key, bind `mw-active` on both; drop `data-tab` |
32
- | Modal | Click on `mw-modal-close` removes `mw-modal-open` from the overlay | `[class.mw-modal-open]="isOpen()"`; backdrop click closes. Opening is not in the script at all (the showcase has its own `openModal`) |
32
+ | Modal | `data-mw-modal="<id>"` on a trigger opens that modal; a click on `mw-modal-close` closes the `<dialog>` it sits in, or clears `mw-modal-open` on an overlay div | A `<dialog>` needs a directive calling `showModal()` - `[open]` only opens it non-modally. See `examples/angular-services.md` |
33
33
  | Mobile nav | Toggles `open` on `mw-menu-btn` and `mw-navbar`, writes `aria-expanded` when the button is a `<button>`, closes on anchor click and on Escape (focus returns to the button) | One signal, bound to both; reset it on navigation end |
34
34
  | Scroll spy | Sets `mw-active` on `mw-navbar-link` from the scroll position | Router-based: `routerLinkActive="mw-active"` |
35
35
  | Anchor scrolling | Intercepts `a[href^="#"]` and runs its own eased scroll - duration scales with distance, capped at 1.4s, cancelled by wheel or touch. Lands on `scroll-padding-top`, moves focus to the target, writes the hash with `replaceState`, and measures a sticky target unpinned | The router; for in-page anchors `scrollIntoView({ behavior: 'smooth' })` or your own animation |
@@ -69,10 +69,10 @@ difference, not a missing effect.
69
69
  ## Modals and progress bars
70
70
 
71
71
  `mwOpenModal(id)` / `mwCloseModal(id)` are on `window` and handle both modal
72
- shapes - the `mw-modal-overlay` div and a `<dialog class="mw-modal">`. Close
73
- buttons and `data-mw-modal="<id>"` triggers are delegated from the document, so
74
- markup rendered later still works. A `<dialog>` needs none of it in a SPA: call
75
- `showModal()` and `close()` on the element.
72
+ shapes - a `<dialog class="mw-modal">` and the older `mw-modal-overlay` div.
73
+ Close buttons and `data-mw-modal="<id>"` triggers are delegated from the
74
+ document, so markup rendered later still works and a static page needs no code
75
+ of its own. In a SPA, call `showModal()` and `close()` on the element instead.
76
76
 
77
77
  `mw-progress-fill` takes its target width from `data-value="75"` or an inline
78
78
  `style="width: 75%"`. Where scroll-driven animations are supported the bar fills
@@ -93,8 +93,8 @@ other spelling exists.
93
93
  Pure CSS, nothing to wire up: hover, focus and press states, the card lift,
94
94
  tooltips (`data-tooltip`), `mw-rating` (via `data-rating`), the responsive table
95
95
  card view (`data-label`), all grids and utilities, the body scroll lock while a
96
- modal is open (`body:has(.mw-modal-open)`), the modal turning into a bottom
97
- sheet below 576px, toast entry animations, the sticky table header, the scroll
96
+ modal is open (from the `<dialog>` element, or `body:has(.mw-modal-open)` for an
97
+ overlay div), the modal turning into a bottom sheet below 576px, toast entry animations, the sticky table header, the scroll
98
98
  hint on a tab bar (four gradients, no scroll listener), the kanban empty-lane
99
99
  placeholder (hidden via `:has()` as soon as the lane holds a ticket), touch
100
100
  target sizing on a coarse pointer, `prefers-reduced-motion` handling.
@@ -108,17 +108,16 @@ still a working menu, not a broken one.
108
108
  ## When you do keep the shipped JS
109
109
 
110
110
  For a static page, a landing page or a server-rendered site (Thymeleaf, Twig,
111
- Jekyll, plain HTML) it is exactly right - load it at the end of `<body>`. The
112
- only manual part is opening a modal, which the script does not cover:
111
+ Jekyll, plain HTML) it is exactly right - load it at the end of `<body>` and
112
+ write no JavaScript at all. A modal opens from its trigger:
113
113
 
114
114
  ```html
115
+ <button class="mw-btn mw-btn-primary" data-mw-modal="demo">Book a demo</button>
116
+
117
+ <dialog id="demo" class="mw-modal" closedby="any">
118
+
119
+ <button class="mw-modal-close mw-btn mw-btn-outline">Cancel</button>
120
+ </dialog>
121
+
115
122
  <script src="maverick-wave.min.js"></script>
116
- <script>
117
- function openModal(id) {
118
- document.getElementById(id).classList.add('mw-modal-open');
119
- }
120
- function closeModal(id) {
121
- document.getElementById(id).classList.remove('mw-modal-open');
122
- }
123
- </script>
124
123
  ```
@@ -196,14 +196,14 @@ pointing at the trigger in every combination.
196
196
 
197
197
  ## Sections
198
198
 
199
- | Class | Use |
200
- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
201
- | `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`) |
202
- | `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
203
- | `mw-section-title` | Centered heading with a decorative primary underline, `2xl` growing to `3xl` up to 576px - landing pages |
204
- | `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
205
- | `mw-section-subtitle` | Centered heading with a thin secondary underline, `xl` growing to `2xl` up to 576px |
206
- | `mw-section-nav` + `mw-section-btn` | Sticky single-row strip of outline-style jump links; parks under the header and scrolls sideways when it overflows |
199
+ | Class | Use |
200
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
201
+ | `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`); point it at `--mw-section-padding-fluid` for spacing that grows with the screen |
202
+ | `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
203
+ | `mw-section-title` | Centered heading with a decorative primary underline, `2xl` growing to `3xl` up to 576px - landing pages |
204
+ | `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
205
+ | `mw-section-subtitle` | Centered heading with a thin secondary underline, `xl` growing to `2xl` up to 576px |
206
+ | `mw-section-nav` + `mw-section-btn` | Sticky single-row strip of outline-style jump links; parks under the header and scrolls sideways when it overflows |
207
207
 
208
208
  ```html
209
209
  <section class="mw-section mw-section-alternate">