maverick-wave 4.27.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.claude/settings.local.json +29 -1
  2. package/.claude/skills/mw-maverick-wave/SKILL.md +42 -25
  3. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +16 -4
  4. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +13 -4
  5. package/.claude/skills/mw-maverick-wave/references/components.md +90 -5
  6. package/.claude/skills/mw-maverick-wave/references/forms.md +14 -2
  7. package/.claude/skills/mw-maverick-wave/references/javascript.md +18 -4
  8. package/.claude/skills/mw-maverick-wave/references/layout.md +53 -19
  9. package/.claude/skills/mw-maverick-wave/references/theming.md +38 -1
  10. package/.impeccable/config.local.json +5 -0
  11. package/.impeccable/hook.cache.json +1 -0
  12. package/CHANGELOG.md +17 -0
  13. package/CLAUDE.md +5 -5
  14. package/README.md +48 -4
  15. package/index.html +19 -38
  16. package/maverick-wave.min.css +14 -12
  17. package/maverick-wave.min.js +1 -1
  18. package/package.json +2 -2
  19. package/scripts/verify.js +27 -2
  20. package/src/js/main.js +266 -75
  21. package/src/partials/accordions-container.html +6 -10
  22. package/src/partials/alerts-container.html +6 -6
  23. package/src/partials/announcement-container.html +5 -5
  24. package/src/partials/avatars-container.html +85 -17
  25. package/src/partials/badges-container.html +6 -6
  26. package/src/partials/blog-posts-container.html +11 -12
  27. package/src/partials/buttons-container.html +3 -3
  28. package/src/partials/cards-container.html +26 -9
  29. package/src/partials/code-container.html +5 -5
  30. package/src/partials/content-slider-container.html +5 -4
  31. package/src/partials/divider-container.html +7 -7
  32. package/src/partials/dropdown-container.html +2 -2
  33. package/src/partials/empty-state-container.html +5 -5
  34. package/src/partials/footer-container.html +21 -18
  35. package/src/partials/form-elements-container.html +14 -14
  36. package/src/partials/gallery-container.html +16 -8
  37. package/src/partials/get-started-container.html +4 -8
  38. package/src/partials/header-container.html +18 -7
  39. package/src/partials/header-utilities-container.html +192 -5
  40. package/src/partials/home-container.html +10 -20
  41. package/src/partials/input-group-container.html +11 -2
  42. package/src/partials/kbd-container.html +1 -1
  43. package/src/partials/login-container.html +5 -1
  44. package/src/partials/media-container.html +2 -2
  45. package/src/partials/modals-container.html +22 -32
  46. package/src/partials/pricing-container.html +9 -9
  47. package/src/partials/skeleton-container.html +6 -6
  48. package/src/partials/stepper-container.html +2 -2
  49. package/src/partials/tables-container.html +12 -12
  50. package/src/partials/tabs-container.html +17 -27
  51. package/src/partials/tags-container.html +23 -17
  52. package/src/partials/techstack-bucket-container.html +13 -0
  53. package/src/partials/tiles-container.html +26 -9
  54. package/src/partials/typography-container.html +7 -6
  55. package/src/partials/utilities-container.html +35 -15
  56. package/src/scss/_layers.scss +10 -0
  57. package/src/scss/abstracts/_index.scss +6 -0
  58. package/src/scss/abstracts/_mixins.scss +61 -153
  59. package/src/scss/abstracts/_variables.scss +123 -319
  60. package/src/scss/base/_base.scss +170 -190
  61. package/src/scss/base/_reset.scss +106 -104
  62. package/src/scss/base/_typography.scss +153 -151
  63. package/src/scss/components/_accordions.scss +97 -97
  64. package/src/scss/components/_alerts.scss +71 -70
  65. package/src/scss/components/_announcement.scss +71 -70
  66. package/src/scss/components/_avatars.scss +130 -142
  67. package/src/scss/components/_badge.scss +131 -132
  68. package/src/scss/components/_blog-post.scss +223 -220
  69. package/src/scss/components/_breadcrumbs.scss +93 -92
  70. package/src/scss/components/_button-bar.scss +102 -110
  71. package/src/scss/components/_buttons.scss +319 -324
  72. package/src/scss/components/_calendar.scss +251 -252
  73. package/src/scss/components/_cards.scss +359 -386
  74. package/src/scss/components/_code.scss +190 -194
  75. package/src/scss/components/_coming-soon.scss +77 -75
  76. package/src/scss/components/_content-slider.scss +90 -89
  77. package/src/scss/components/_divider.scss +55 -53
  78. package/src/scss/components/_dropdown.scss +179 -185
  79. package/src/scss/components/_empty-state.scss +57 -57
  80. package/src/scss/components/_flag.scss +325 -0
  81. package/src/scss/components/_gallery.scss +131 -133
  82. package/src/scss/components/_index.scss +2 -0
  83. package/src/scss/components/_info.scss +151 -167
  84. package/src/scss/components/_kanban.scss +332 -341
  85. package/src/scss/components/_kbd.scss +40 -38
  86. package/src/scss/components/_lang-switch.scss +108 -0
  87. package/src/scss/components/_links.scss +21 -28
  88. package/src/scss/components/_lists.scss +510 -514
  89. package/src/scss/components/_localhost-indicator.scss +26 -24
  90. package/src/scss/components/_media.scss +22 -20
  91. package/src/scss/components/_meta-info.scss +35 -33
  92. package/src/scss/components/_modals.scss +277 -186
  93. package/src/scss/components/_pagination.scss +81 -87
  94. package/src/scss/components/_panels.scss +99 -97
  95. package/src/scss/components/_pricing.scss +183 -241
  96. package/src/scss/components/_progress.scss +112 -89
  97. package/src/scss/components/_prose.scss +35 -33
  98. package/src/scss/components/_ratings.scss +40 -38
  99. package/src/scss/components/_segmented.scss +82 -84
  100. package/src/scss/components/_skeleton.scss +66 -72
  101. package/src/scss/components/_spinners.scss +155 -159
  102. package/src/scss/components/_stepper.scss +116 -116
  103. package/src/scss/components/_tables.scss +155 -148
  104. package/src/scss/components/_tabs.scss +232 -141
  105. package/src/scss/components/_tags.scss +109 -107
  106. package/src/scss/components/_techstack-bucket.scss +123 -122
  107. package/src/scss/components/_testimonial.scss +72 -70
  108. package/src/scss/components/_theme-toggle.scss +53 -51
  109. package/src/scss/components/_tiles.scss +174 -174
  110. package/src/scss/components/_timelines.scss +282 -280
  111. package/src/scss/components/_toasts.scss +66 -72
  112. package/src/scss/form-elements/_checkbox.scss +117 -132
  113. package/src/scss/form-elements/_form.scss +149 -115
  114. package/src/scss/form-elements/_input-group.scss +99 -103
  115. package/src/scss/form-elements/_input.scss +74 -74
  116. package/src/scss/form-elements/_login.scss +48 -48
  117. package/src/scss/form-elements/_prefilled.scss +53 -51
  118. package/src/scss/form-elements/_radio.scss +95 -107
  119. package/src/scss/form-elements/_select.scss +42 -42
  120. package/src/scss/form-elements/_slider.scss +117 -132
  121. package/src/scss/form-elements/_textarea.scss +68 -54
  122. package/src/scss/form-elements/_toggle.scss +127 -140
  123. package/src/scss/layout/_footer.scss +172 -171
  124. package/src/scss/layout/_grid.scss +268 -302
  125. package/src/scss/layout/_header.scss +426 -427
  126. package/src/scss/layout/_home.scss +28 -27
  127. package/src/scss/layout/_main.scss +275 -238
  128. package/src/scss/layout/_page-header.scss +38 -36
  129. package/src/scss/layout/_section.scss +150 -124
  130. package/src/scss/main.scss +1 -1
  131. package/src/scss/utilities/_accessibility.scss +46 -45
  132. package/src/scss/utilities/_aspect.scss +22 -20
  133. package/src/scss/utilities/_corner.scss +23 -5
  134. package/src/scss/utilities/_display.scss +76 -70
  135. package/src/scss/utilities/_elevation.scss +13 -11
  136. package/src/scss/utilities/_flex.scss +83 -85
  137. package/src/scss/utilities/_reveal.scss +40 -63
  138. package/src/scss/utilities/_spacing.scss +63 -64
  139. package/src/scss/utilities/_text.scss +139 -141
  140. package/src/scss/utilities/_touch-targets.scss +130 -147
@@ -11,5 +11,33 @@
11
11
  "Bash(npm audit:*)",
12
12
  "Bash(npx prettier *)"
13
13
  ]
14
- }
14
+ },
15
+ "hooks": {
16
+ "PostToolUse": [
17
+ {
18
+ "matcher": "Edit|Write",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "[ ! -f '/Users/m1well/.claude/skills/impeccable/scripts/impeccable' ] || '/Users/m1well/.claude/skills/impeccable/scripts/impeccable' hook",
23
+ "timeout": 5,
24
+ "statusMessage": "Checking UI changes"
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "Stop": [
30
+ {
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "[ ! -f '/Users/m1well/.claude/skills/impeccable/scripts/impeccable' ] || '/Users/m1well/.claude/skills/impeccable/scripts/impeccable' hook",
35
+ "timeout": 30,
36
+ "statusMessage": "Design deep pass"
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ "description": "Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop."
15
43
  }
@@ -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@4.25.0/maverick-wave.min.css"
47
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.0.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@4.25.0/maverick-wave.min.js"></script>
54
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.0.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 -
@@ -116,18 +116,18 @@ leave a second tab lit.
116
116
 
117
117
  The classes that mean something _other_ than "on" keep their own names:
118
118
 
119
- | Component | State class |
120
- | -------------------------------------------- | ----------------------------------- |
121
- | Anything switchable, "on" | `mw-active` (`active` deprecated) |
122
- | Burger button + navbar drawer | `open` (no prefix) |
123
- | Stepper indicator / label / connector / step | `mw-active`, `mw-done` |
124
- | Checkbox list item (`li`) | `mw-selected` |
125
- | Calendar day, picked | `mw-selected` |
126
- | Kanban ticket being edited | `mw-kanban-editing` |
127
- | Modal overlay | `mw-modal-open` |
128
- | Alert, after dismissal | `mw-alert-closed` (`display: none`) |
129
- | Field wrapper in error | `mw-field-has-error` |
130
- | Single form control in error | `mw-form-element-error` |
119
+ | Component | State class |
120
+ | -------------------------------------------- | -------------------------------------------------------- |
121
+ | Anything switchable, "on" | `mw-active` (`active` deprecated) |
122
+ | Burger button + navbar drawer | `open` (no prefix) |
123
+ | Stepper indicator / label / connector / step | `mw-active`, `mw-done` |
124
+ | Checkbox list item (`li`) | `mw-selected` |
125
+ | Calendar day, picked | `mw-selected` |
126
+ | Kanban ticket being edited | `mw-kanban-editing` |
127
+ | Modal overlay | `mw-modal-open` |
128
+ | Alert, dismissing / dismissed | `mw-alert-closing` → `mw-alert-closed` (`display: none`) |
129
+ | Field wrapper in error | `mw-field-has-error` |
130
+ | Single form control in error | `mw-form-element-error` |
131
131
 
132
132
  `mw-active` and `mw-selected` are not the same thing and not interchangeable:
133
133
  active is the one of several that is currently showing, selected is a choice the
@@ -150,8 +150,22 @@ keys at all (negative gap is invalid CSS and is not generated).
150
150
  `sm` 0.9, `base` 1, `md` 1.1, `lg` 1.3, `xl` 1.5, `2xl` 1.8, `3xl` 2.2, `4xl` 2.5,
151
151
  `5xl` 3, `6xl` 4.3 rem.
152
152
 
153
- **Breakpoints** (max-width, mobile-first markup / desktop-first media queries):
154
- `xs` 375, `sm` 576, `md` 768, `lg` 992, `xl` 1200, `2xl` 1400 px.
153
+ **Breakpoints**: `xs` 375, `sm` 576, `md` 768, `lg` 992, `xl` 1200, `2xl` 1400 px.
154
+ Column grids and most components are mobile-first (`min-width`); the ranges do
155
+ not overlap, `media-down` stops 0.02px short of its breakpoint. Blocks that only
156
+ adjust something on a phone stay `max-width`.
157
+
158
+ **Overriding.** Everything the framework emits sits in
159
+ `@layer mw.reset, mw.base, mw.forms, mw.components, mw.layout, mw.utilities`.
160
+ Any rule written outside a layer beats all of it, so a single `.mw-card {}` in
161
+ your own stylesheet wins - no `!important`, no doubled selectors. `!important`
162
+ is not a stronger version of this but a weaker one: important declarations
163
+ reverse the layer order and unlayered comes last, so an important rule in
164
+ `mw.base` beats an important one of yours even behind an ID selector. The flip
165
+ side: third-party CSS loaded unlayered also wins, so pull it into a layer of
166
+ its own:
167
+ `@import url('font-awesome.css') layer(vendor);` with `@layer vendor, mw;`
168
+ declared before it.
155
169
 
156
170
  **Radius** (`mw-radius-none|xs|sm|md|lg|xl|2xl|full`): 0, 2, 5, 10, 15, 20, 30 px, 50%.
157
171
 
@@ -221,7 +235,9 @@ feature frame) ·
221
235
 
222
236
  **Navigation** `mw-header` + `mw-navbar` · `mw-breadcrumbs` · `mw-pagination` ·
223
237
  `mw-dropdown` (+ `-menu`, `-item`, `-item-danger`, `-divider`, `-label`,
224
- `-caret`, `-end`, `-up`) · `mw-section-nav` (`references/layout.md`)
238
+ `-caret`, `-end`, `-up`) · `mw-lang-switch` (+ `-code`, `-name`, `-check`,
239
+ `inverted`) · `mw-flag` (+ 23 country codes) ·
240
+ `mw-section-nav` (`references/layout.md`)
225
241
 
226
242
  **Media & content** `mw-avatar` (+ `initials`, `group`) · `mw-gallery` ·
227
243
  `mw-image-slider` · `mw-blog-post` · `mw-testimonial` (+ `-source`, `-detail`,
@@ -368,14 +384,15 @@ feature frame) ·
368
384
  comment. If your component really does need to animate a size, name that
369
385
  property - do not reach for `all`.
370
386
  27. **A control is a `<button>`, never a styled `<div>`.** `mw-tabs-nav-item`,
371
- `mw-theme-toggle`, `mw-gallery-dot` and `mw-accordion-header` are all
372
- written for one, and all four shipped as divs and spans - the first three
373
- until 4.11, the accordion header until 4.12 - that no keyboard could reach.
374
- Each class clears what a `<button>` brings with it, so
375
- `<button type="button" class="mw-tabs-nav-item" data-tab="...">` is the
376
- whole markup. If you build your own clickable thing: the element decides
377
- whether anyone without a mouse can use it, the class only decides how it
378
- looks.
387
+ `mw-theme-toggle`, `mw-gallery-dot`, `mw-accordion-header` and `mw-menu-btn`
388
+ are all written for one, and all five shipped as divs and spans - the first
389
+ three until 4.11, the accordion header until 4.12, the burger after that -
390
+ that no keyboard could reach. Each class clears what a `<button>` brings
391
+ with it, so `<button type="button" class="mw-tabs-nav-item" data-tab="...">`
392
+ is the whole markup. The burger is the worst of the five to get wrong:
393
+ below the collapse breakpoint it is the only route to the navigation.
394
+ If you build your own clickable thing: the element decides whether anyone
395
+ without a mouse can use it, the class only decides how it looks.
379
396
  28. **A link in running text is `mw-link`, not `mw-btn mw-btn-link`.** Since
380
397
  4.13.0 there is a class for exactly that. `mw-btn` is `inline-flex` with
381
398
  `min-height: var(--mw-control-height)`, so a link written that way pulls its
@@ -7,7 +7,9 @@ service or a component per behaviour, none of them longer than a screen.
7
7
  ## Theme service
8
8
 
9
9
  The framework only needs one class on `<body>`. Everything else - persistence,
10
- the toggle UI, the initial value - belongs to the application.
10
+ the toggle UI, the initial value - belongs to the application. The second class,
11
+ `mw-theme-switching` on `<html>`, is not optional on a page of any size: without
12
+ it the flip starts a transition on every element that changes colour.
11
13
 
12
14
  ```ts
13
15
  import { Injectable, effect, signal } from '@angular/core';
@@ -21,7 +23,13 @@ export class ThemeService {
21
23
  constructor() {
22
24
  effect(() => {
23
25
  const light = this.light();
26
+ const root = document.documentElement;
27
+
28
+ root.classList.add('mw-theme-switching');
24
29
  document.body.classList.toggle('mw-theme-light', light);
30
+ void root.offsetHeight; // commit the colours with transitions off
31
+ root.classList.remove('mw-theme-switching');
32
+
25
33
  localStorage.setItem(STORAGE_KEY, light ? 'light' : 'dark');
26
34
  });
27
35
  }
@@ -308,13 +316,17 @@ on every navigation.
308
316
  <span class="mw-profile-btn-name">{{ user().name }}</span>
309
317
  </button>
310
318
 
311
- <div
319
+ <button
320
+ type="button"
312
321
  class="mw-menu-btn"
322
+ aria-label="Menu"
323
+ aria-controls="main-nav"
324
+ [attr.aria-expanded]="menuOpen()"
313
325
  [class.open]="menuOpen()"
314
326
  (click)="menuOpen.set(!menuOpen())"
315
327
  >
316
- <div class="mw-menu-btn-burger"></div>
317
- </div>
328
+ <span class="mw-menu-btn-burger"></span>
329
+ </button>
318
330
  </div>
319
331
  </div>
320
332
  </header>
@@ -20,7 +20,7 @@ accordion FAQ.
20
20
 
21
21
  <link
22
22
  rel="stylesheet"
23
- href="https://cdn.jsdelivr.net/npm/maverick-wave@4.25.0/maverick-wave.min.css"
23
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.0.0/maverick-wave.min.css"
24
24
  />
25
25
  <link
26
26
  rel="stylesheet"
@@ -78,7 +78,15 @@ accordion FAQ.
78
78
  </div>
79
79
  </button>
80
80
 
81
- <div class="mw-menu-btn"><div class="mw-menu-btn-burger"></div></div>
81
+ <button
82
+ type="button"
83
+ class="mw-menu-btn"
84
+ aria-label="Menu"
85
+ aria-expanded="false"
86
+ aria-controls="main-nav"
87
+ >
88
+ <span class="mw-menu-btn-burger"></span>
89
+ </button>
82
90
  </div>
83
91
  </div>
84
92
  </header>
@@ -479,7 +487,7 @@ accordion FAQ.
479
487
  <div class="mw-modal-backdrop" onclick="closeModal('demo')"></div>
480
488
  </div>
481
489
 
482
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@4.25.0/maverick-wave.min.js"></script>
490
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.0.0/maverick-wave.min.js"></script>
483
491
  <script>
484
492
  // The only thing the shipped script does not cover: opening a modal.
485
493
  // Closing works through .mw-modal-close, the backdrop is wired above.
@@ -497,7 +505,8 @@ accordion FAQ.
497
505
  ## Notes
498
506
 
499
507
  - The theme toggle needs no code - the script persists the choice under
500
- `localStorage['mw-theme']` and toggles `mw-theme-light` on `<body>`.
508
+ `localStorage['mw-theme']`, toggles `mw-theme-light` on `<body>` and wraps the
509
+ flip in `mw-theme-switching` so it starts no transitions.
501
510
  - The scroll spy sets `mw-active` on the `mw-navbar-link` whose `href` matches
502
511
  the `section[id]` currently in view - the `<section id="…">` elements are
503
512
  the contract.
@@ -544,8 +544,12 @@ Toggling is JS - see `references/javascript.md`; the shipped script keeps
544
544
  ```html
545
545
  <div class="mw-tabs">
546
546
  <div class="mw-tabs-nav">
547
- <div class="mw-tabs-nav-item mw-active" data-tab="tab1">Details</div>
548
- <div class="mw-tabs-nav-item" data-tab="tab2">History</div>
547
+ <button type="button" class="mw-tabs-nav-item mw-active" data-tab="tab1">
548
+ Details
549
+ </button>
550
+ <button type="button" class="mw-tabs-nav-item" data-tab="tab2">
551
+ History
552
+ </button>
549
553
  </div>
550
554
  <div class="mw-tabs-content">
551
555
  <div class="mw-tabs-panel mw-active" id="tab1">...</div>
@@ -558,6 +562,9 @@ Toggling is JS - see `references/javascript.md`; the shipped script keeps
558
562
  `mw-tabs-pills`.
559
563
  - `data-tab` matches the panel `id` - that pairing is only needed for the
560
564
  shipped JS. In a SPA, bind `mw-active` yourself and drop the attribute.
565
+ - The shipped JS turns the strip into a real tablist: `role`, `aria-selected`,
566
+ `aria-controls`, `aria-labelledby`, a roving `tabindex` and arrow/Home/End
567
+ keys. Without it, that is yours to add - see `references/javascript.md`.
561
568
  - The nav scrolls horizontally instead of wrapping, and says so: a shadow shows
562
569
  on whichever side still has tabs behind it and disappears once that end is
563
570
  reached. Pure CSS, no scroll listener. The fade colour comes from
@@ -602,6 +609,35 @@ Toggling is JS - see `references/javascript.md`; the shipped script keeps
602
609
 
603
610
  Angular: `<div class="mw-modal-overlay" [class.mw-modal-open]="isOpen()">`.
604
611
 
612
+ ### The same modal as a `<dialog>`
613
+
614
+ Same classes, no overlay wrapper and no backdrop element:
615
+
616
+ ```html
617
+ <dialog id="delete-modal" class="mw-modal mw-modal-sm" closedby="any">
618
+ <div class="mw-modal-header">
619
+ <h4 class="mw-modal-title">Delete invoice</h4>
620
+ <button class="mw-modal-close" type="button" aria-label="Close">
621
+ &#120299;
622
+ </button>
623
+ </div>
624
+ <div class="mw-modal-body">…</div>
625
+ <div class="mw-modal-footer">…</div>
626
+ </dialog>
627
+ ```
628
+
629
+ - Escape, the focus trap, `inert` on the page behind it and the scroll lock all
630
+ come from the element. Prefer this shape for anything that asks a question.
631
+ - Opened with `showModal()`, closed with `close()`. `main.js` wires the close
632
+ buttons and exposes `mwOpenModal(id)` / `mwCloseModal(id)`, both of which take
633
+ either shape. A trigger can also carry `data-mw-modal="delete-modal"`.
634
+ - `closedby="any"` dismisses it on a backdrop click; where that attribute is not
635
+ understood the script handles the click instead.
636
+ - Sizes, the bottom sheet below 576px and every `mw-modal-*` part behave exactly
637
+ as above - those rules are class-based.
638
+
639
+ Angular: bind nothing, call `showModal()` on a `viewChild` ref.
640
+
605
641
  ## Alerts & toasts
606
642
 
607
643
  ```html
@@ -618,9 +654,10 @@ Angular: `<div class="mw-modal-overlay" [class.mw-modal-open]="isOpen()">`.
618
654
  ```
619
655
 
620
656
  Variants: `mw-alert-primary`, `-secondary`, `-success`, `-warning`, `-danger`,
621
- `-info`. `mw-alert-title` and the close button are optional. `mw-alert-closed`
622
- hides a dismissed alert (`display: none`) - in a SPA prefer removing it from the
623
- list instead.
657
+ `-info`. `mw-alert-title` and the close button are optional. Dismissal runs in
658
+ two steps: `mw-alert-closing` fades the alert out over `--mw-duration-base`, then
659
+ `mw-alert-closed` hides it (`display: none`) - in a SPA prefer removing it from
660
+ the list instead.
624
661
 
625
662
  **Toasts** are alerts inside a fixed stack:
626
663
 
@@ -1170,6 +1207,54 @@ your own it is `:has(.mw-dropdown[open]) { overflow: visible }`.
1170
1207
  On a coarse pointer the rows grow to 2.75rem and the menu takes at least the
1171
1208
  trigger's full width.
1172
1209
 
1210
+ ## Language switcher
1211
+
1212
+ A `mw-dropdown` with a quieter trigger: no fill and no border until it is
1213
+ hovered, so a header bar does not turn into a row of competing buttons.
1214
+
1215
+ ```html
1216
+ <details class="mw-dropdown mw-dropdown-end mw-lang-switch">
1217
+ <summary aria-label="Language: Deutsch">
1218
+ <span class="mw-flag mw-flag-de"></span>
1219
+ <span class="mw-lang-switch-code">de</span>
1220
+ <i class="fas fa-chevron-down mw-dropdown-caret"></i>
1221
+ </summary>
1222
+ <div class="mw-dropdown-menu">
1223
+ <button
1224
+ type="button"
1225
+ class="mw-dropdown-item mw-active"
1226
+ aria-current="true"
1227
+ data-mw-lang="de"
1228
+ >
1229
+ <span class="mw-flag mw-flag-de"></span>
1230
+ <span class="mw-lang-switch-name" lang="de">Deutsch</span>
1231
+ <i class="fas fa-check mw-lang-switch-check"></i>
1232
+ </button>
1233
+ <button type="button" class="mw-dropdown-item" data-mw-lang="en">
1234
+ <span class="mw-flag mw-flag-gb"></span>
1235
+ <span class="mw-lang-switch-name" lang="en">English</span>
1236
+ <i class="fas fa-check mw-lang-switch-check"></i>
1237
+ </button>
1238
+ </div>
1239
+ </details>
1240
+ ```
1241
+
1242
+ The two-letter code carries the state, not the flag: a flag is a country and
1243
+ never a language - no country stands for English. Write each language name in
1244
+ its own language and put `lang="fr"` on it, so a screen reader pronounces it
1245
+ instead of spelling it out.
1246
+
1247
+ Parts: `mw-lang-switch-code`, `-name`, `-check` (the tick, shown by `mw-active`
1248
+ on the item and kept in the layout while hidden). `mw-lang-switch-inverted` is
1249
+ for a dark bar built without `mw-header`; inside `mw-header` those colours apply
1250
+ on their own.
1251
+
1252
+ Flags: `mw-flag` plus `mw-flag-<iso>` for de, at, ch, gb, us, ie, fr, it, es,
1253
+ pt, nl, be, pl, hu, ro, bg, ua, se, dk, no, fi, tr and jp - CSS gradients, all
1254
+ in the same 4:3 box whatever the real ratio. Never an emoji flag: Windows ships
1255
+ no glyphs for them. Anything outside the set goes as an `<img>` or an inline
1256
+ `<svg>` inside `mw-flag`.
1257
+
1173
1258
  ## Keyboard keys
1174
1259
 
1175
1260
  `<kbd>` is styled directly, so a shortcut in prose needs no class:
@@ -45,14 +45,26 @@ reactive form control.
45
45
  ```
46
46
 
47
47
  - `mw-required` on the label appends a red asterisk (`data-required="true"`
48
- works too).
48
+ works too). A label sitting directly in front of a control with the `required`
49
+ attribute gets the asterisk without either class - useful for template-driven
50
+ and plain HTML forms, where the marker then cannot drift out of step with the
51
+ validation.
49
52
  - `mw-field-hint` is the small muted helper line, `mw-field-error` the small red
50
53
  one. Render only one of them at a time.
51
54
  - `mw-field-has-error` on the **wrapper** turns the border of the contained
52
55
  `mw-input` / `mw-select` / `mw-textarea` red and adds a soft red halo.
53
56
  - `mw-form-element-error` does the same for a single control that has no field
54
57
  wrapper - it also works on `mw-checkbox-group`, `mw-radio-group` and
55
- `mw-slider-container`.
58
+ `mw-slider-container`. On the two groups it adds the inset a field brings with
59
+ it, so the rows do not sit flush against the border.
60
+ - Native validation is styled too: a control that fails `required`, `type` or
61
+ `pattern` gets the same red border and halo through `:user-invalid`, and the
62
+ `mw-field-hint` beside it turns red. `:user-invalid` and not `:invalid`, so an
63
+ untouched empty field is not red on page load. Nothing has to be bound for
64
+ this - which is exactly why the classes still exist for the case below, where
65
+ the validator lives in the component rather than on the element.
66
+ - `mw-textarea` grows with its content (`field-sizing`) between 3 lines and
67
+ 60dvh, where the browser supports it.
56
68
 
57
69
  > **The framework does not style Angular's `ng-invalid` / `ng-touched` classes.**
58
70
  > Bind the framework classes to the control state yourself:
@@ -28,14 +28,14 @@ classes are the entire contract.
28
28
  | Behaviour | What the shipped JS does | What to do instead |
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
- | Tabs | `data-tab` → panel `id`; sets `mw-active` on nav item and panel | Track the selected index/key, bind `mw-active` on both; drop `data-tab` |
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
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`) |
33
- | Mobile nav | Toggles `open` on `mw-menu-btn` and `mw-navbar`, closes on anchor click | One signal, bound to both; reset it on navigation end |
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
- | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` on `<body>` and `mw-active` on the toggle | A theme service - see `examples/angular-services.md` |
35
+ | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` on `<body>` and `mw-active` on the toggle, wrapped in `mw-theme-switching` on `<html>` so the flip starts no transitions | A theme service - see `examples/angular-services.md` |
36
36
  | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
37
37
  | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
38
- | Alerts | Close button adds `mw-alert-closed` (`display: none`) | Remove the alert from the list/signal |
38
+ | Alerts | Close button adds `mw-alert-closing` (fade out), then `mw-alert-closed` (`display: none`) after `--mw-duration-base` | Remove the alert from the list/signal |
39
39
  | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
40
40
  | Gallery | Generates the dots, moves the track, swipe handling, writes `mw-gallery-desc` | Render dots in the template, bind the track transform and `mw-active` on the current dot |
41
41
  | Image slider | Toggles `mw-active` on the overlay image and the control button with the matching `data-index` | Bind `mw-active` from the selected index |
@@ -45,6 +45,20 @@ classes are the entire contract.
45
45
  | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
46
46
  | Color swatches | Showcase-only (prints computed hex values) | Not needed |
47
47
  | 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 |
48
+ | 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 |
49
+
50
+ ## Modals and progress bars
51
+
52
+ `mwOpenModal(id)` / `mwCloseModal(id)` are on `window` and handle both modal
53
+ shapes - the `mw-modal-overlay` div and a `<dialog class="mw-modal">`. Close
54
+ buttons and `data-mw-modal="<id>"` triggers are delegated from the document, so
55
+ markup rendered later still works. A `<dialog>` needs none of it in a SPA: call
56
+ `showModal()` and `close()` on the element.
57
+
58
+ `mw-progress-fill` takes its target width from `data-value="75"` or an inline
59
+ `style="width: 75%"`. Where scroll-driven animations are supported the bar fills
60
+ with the scroll position and the script only hands the value over; elsewhere it
61
+ falls back to setting the width once the bar comes into view.
48
62
 
49
63
  ## A note on the state class
50
64
 
@@ -59,11 +59,11 @@ its children are styled through descendant selectors:
59
59
  <header class="mw-header">
60
60
  <div class="mw-container">
61
61
  <div class="mw-logo">
62
- <button type="button"><img src="logo.svg" alt="Logo" /></button>
62
+ <a href="#start"><img src="logo.svg" alt="Logo" /></a>
63
63
  </div>
64
64
 
65
65
  <div class="mw-header-actions">
66
- <nav class="mw-navbar mw-navbar-medium">
66
+ <nav class="mw-navbar mw-navbar-medium" id="main-nav">
67
67
  <ul class="mw-navbar-list">
68
68
  <li class="mw-navbar-item">
69
69
  <a href="#start" class="mw-navbar-link mw-active">Start</a>
@@ -74,17 +74,29 @@ its children are styled through descendant selectors:
74
74
  </ul>
75
75
  </nav>
76
76
 
77
- <div class="mw-theme-toggle mw-ml-5">
77
+ <button
78
+ type="button"
79
+ class="mw-theme-toggle mw-ml-5"
80
+ aria-label="Toggle light and dark theme"
81
+ >
78
82
  <div class="mw-theme-toggle-slider">
79
83
  <div class="mw-theme-toggle-icon"><i class="fas fa-moon"></i></div>
80
84
  </div>
81
- </div>
85
+ </button>
82
86
 
83
- <button class="mw-login-btn" type="button">
87
+ <button class="mw-login-btn" type="button" aria-label="Log in">
84
88
  <i class="fas fa-lock"></i>
85
89
  </button>
86
90
 
87
- <div class="mw-menu-btn"><div class="mw-menu-btn-burger"></div></div>
91
+ <button
92
+ type="button"
93
+ class="mw-menu-btn"
94
+ aria-label="Menu"
95
+ aria-expanded="false"
96
+ aria-controls="main-nav"
97
+ >
98
+ <span class="mw-menu-btn-burger"></span>
99
+ </button>
88
100
  </div>
89
101
  </div>
90
102
  </header>
@@ -94,7 +106,9 @@ its children are styled through descendant selectors:
94
106
  at `md`, `mw-navbar-medium` (4-5) at `lg`, `mw-navbar-large` (6+) at `xl`.
95
107
  Pick the class by how many links you have.
96
108
  - Below the breakpoint the list is hidden and `mw-menu-btn` appears. Opening the
97
- drawer means adding `open` to **both** `mw-menu-btn` and `mw-navbar`.
109
+ drawer means adding `open` to **both** `mw-menu-btn` and `mw-navbar`, and
110
+ setting `aria-expanded` on the button - below the breakpoint it is the only
111
+ route to the navigation, so it has to be a real `<button>`, not a `<div>`.
98
112
  - The active link carries `mw-active` (bare `active` still works but is
99
113
  deprecated).
100
114
  - `mw-profile-btn` is the signed-in pill, next to or instead of the login button:
@@ -162,14 +176,14 @@ viewport, so keep long tooltips off the outermost elements.
162
176
 
163
177
  ## Sections
164
178
 
165
- | Class | Use |
166
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
167
- | `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`) |
168
- | `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
169
- | `mw-section-title` | Centered `3xl` heading with a decorative primary underline - landing pages |
170
- | `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
171
- | `mw-section-subtitle` | Centered `2xl` heading with a thin secondary underline |
172
- | `mw-section-nav` + `mw-section-btn` | Centered, wrapping row of outline-style jump links |
179
+ | Class | Use |
180
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
181
+ | `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`) |
182
+ | `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
183
+ | `mw-section-title` | Centered `3xl` heading with a decorative primary underline - landing pages |
184
+ | `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
185
+ | `mw-section-subtitle` | Centered `2xl` heading with a thin secondary underline |
186
+ | `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 |
173
187
 
174
188
  ```html
175
189
  <section class="mw-section mw-section-alternate">
@@ -187,6 +201,10 @@ viewport, so keep long tooltips off the outermost elements.
187
201
  </section>
188
202
  ```
189
203
 
204
+ The strip sticks at `--mw-header-height` and is `--mw-section-nav-height` tall
205
+ (3.7rem). Anything it can cover on a jump needs the sum as `scroll-margin-top`;
206
+ a block wrapping a `mw-section-subtitle` gets that from the framework already.
207
+
190
208
  ## Page header
191
209
 
192
210
  The application counterpart to `mw-section-title`: title and subtitle left,
@@ -264,6 +282,12 @@ Components that paint their own surface (`mw-card`, `mw-panel`, `mw-modal`,
264
282
  `mw-tile`, `mw-calendar`) are exempt and keep the theme's ink - a card in the
265
283
  hero is still a card.
266
284
 
285
+ `mw-btn-outline` is exempt the other way round: inside the hero it takes the
286
+ hero's ink plus a dark scrim and a blur, because its usual border and label are
287
+ tuned for the page background and go quiet over a photograph. A second call to
288
+ action next to `mw-btn-primary` therefore looks different here than it does
289
+ further down the page - that is deliberate, not a stray override.
290
+
267
291
  **Scroll cue** - `mw-scroll-hint` is the "scroll down" cue on the lower edge of
268
292
  the hero. An `<a>` that is a sibling of `mw-hero`, directly in the container:
269
293
 
@@ -286,8 +310,10 @@ is taken on a phone. The bobbing stops under `prefers-reduced-motion`.
286
310
  ## Grid
287
311
 
288
312
  All grid classes are `display: grid` with a preset gap (`mw-gap-*` overrides
289
- it). They collapse to fewer columns on their own - no responsive suffixes to
290
- manage.
313
+ it). They handle their own column count - no responsive suffixes to manage. One
314
+ column is the base and each breakpoint adds to it, so a phone renders them
315
+ without evaluating a single media query; the table below reads the same either
316
+ way.
291
317
 
292
318
  | Class | Columns | Collapses |
293
319
  | ----------------- | ----------------------------------------------- | ---------------------------- |
@@ -414,6 +440,11 @@ chip-sized `mw-card-simple`, a swatch, a marker in a layout demo. A card with
414
440
  content in it never needs this, and it is not the way to switch the signature
415
441
  off across a project - restyle `--mw-corner-accent` for that.
416
442
 
443
+ **Squircle corners** - `mw-squircle` draws the same silhouette with a
444
+ superellipse instead of a circular arc: same four radii, fuller curve, and the
445
+ corner accent follows. Opt-in, because `corner-shape` only lands in Chromium so
446
+ far - everywhere else the box is simply the normal one.
447
+
417
448
  **Aspect ratio** - `mw-aspect-square|video|wide|portrait|photo`. Reserves the
418
449
  box before the image inside it has loaded, so the page does not reflow when the
419
450
  picture arrives. The child fills the box and crops rather than stretching.
@@ -513,11 +544,14 @@ do not.
513
544
  **Flex** - `mw-flex-row`, `mw-flex-column`, `mw-flex-wrap`, `mw-flex-nowrap`,
514
545
  `mw-flex-1`, `mw-flex-grow-1`, `mw-flex-shrink-0`,
515
546
  `mw-justify-start|end|center|between|around|evenly`,
516
- `mw-items-start|end|center|stretch`.
547
+ `mw-items-start|end|center|stretch`,
548
+ `mw-self-start|end|center|stretch`.
517
549
 
518
550
  `mw-flex-1` sets `flex: 1` (basis 0, all items equal). `mw-flex-grow-1` only
519
551
  grows and keeps the content width as the basis - that is the one you want next
520
- to an avatar or an icon.
552
+ to an avatar or an icon. `mw-self-*` is the per-item counterpart to
553
+ `mw-items-*`: it goes on the child, for the one that sits differently from the
554
+ rest of the row.
521
555
 
522
556
  **Text** - alignment `mw-text-left|center|right`; colour `mw-text-primary`,
523
557
  `-secondary`, `-success`, `-warning`, `-danger`, `-info`, `-muted`,