maverick-wave 4.28.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 (138) hide show
  1. package/.claude/settings.local.json +29 -1
  2. package/.claude/skills/mw-maverick-wave/SKILL.md +39 -24
  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 +42 -5
  6. package/.claude/skills/mw-maverick-wave/references/forms.md +14 -2
  7. package/.claude/skills/mw-maverick-wave/references/javascript.md +17 -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 +11 -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 +223 -83
  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/home-container.html +10 -20
  40. package/src/partials/input-group-container.html +11 -2
  41. package/src/partials/kbd-container.html +1 -1
  42. package/src/partials/login-container.html +5 -1
  43. package/src/partials/media-container.html +2 -2
  44. package/src/partials/modals-container.html +22 -32
  45. package/src/partials/pricing-container.html +9 -9
  46. package/src/partials/skeleton-container.html +6 -6
  47. package/src/partials/stepper-container.html +2 -2
  48. package/src/partials/tables-container.html +12 -12
  49. package/src/partials/tabs-container.html +17 -27
  50. package/src/partials/tags-container.html +23 -17
  51. package/src/partials/techstack-bucket-container.html +13 -0
  52. package/src/partials/tiles-container.html +26 -9
  53. package/src/partials/typography-container.html +7 -6
  54. package/src/partials/utilities-container.html +35 -15
  55. package/src/scss/_layers.scss +10 -0
  56. package/src/scss/abstracts/_index.scss +6 -0
  57. package/src/scss/abstracts/_mixins.scss +61 -153
  58. package/src/scss/abstracts/_variables.scss +123 -319
  59. package/src/scss/base/_base.scss +170 -190
  60. package/src/scss/base/_reset.scss +106 -104
  61. package/src/scss/base/_typography.scss +153 -151
  62. package/src/scss/components/_accordions.scss +97 -97
  63. package/src/scss/components/_alerts.scss +71 -70
  64. package/src/scss/components/_announcement.scss +71 -70
  65. package/src/scss/components/_avatars.scss +130 -142
  66. package/src/scss/components/_badge.scss +131 -132
  67. package/src/scss/components/_blog-post.scss +223 -220
  68. package/src/scss/components/_breadcrumbs.scss +93 -92
  69. package/src/scss/components/_button-bar.scss +102 -110
  70. package/src/scss/components/_buttons.scss +319 -324
  71. package/src/scss/components/_calendar.scss +251 -252
  72. package/src/scss/components/_cards.scss +359 -386
  73. package/src/scss/components/_code.scss +190 -194
  74. package/src/scss/components/_coming-soon.scss +77 -75
  75. package/src/scss/components/_content-slider.scss +90 -89
  76. package/src/scss/components/_divider.scss +55 -53
  77. package/src/scss/components/_dropdown.scss +179 -185
  78. package/src/scss/components/_empty-state.scss +57 -57
  79. package/src/scss/components/_flag.scss +294 -278
  80. package/src/scss/components/_gallery.scss +131 -133
  81. package/src/scss/components/_info.scss +151 -167
  82. package/src/scss/components/_kanban.scss +332 -341
  83. package/src/scss/components/_kbd.scss +40 -38
  84. package/src/scss/components/_lang-switch.scss +89 -97
  85. package/src/scss/components/_links.scss +21 -28
  86. package/src/scss/components/_lists.scss +510 -514
  87. package/src/scss/components/_localhost-indicator.scss +26 -24
  88. package/src/scss/components/_media.scss +22 -20
  89. package/src/scss/components/_meta-info.scss +35 -33
  90. package/src/scss/components/_modals.scss +277 -186
  91. package/src/scss/components/_pagination.scss +81 -87
  92. package/src/scss/components/_panels.scss +99 -97
  93. package/src/scss/components/_pricing.scss +183 -241
  94. package/src/scss/components/_progress.scss +112 -89
  95. package/src/scss/components/_prose.scss +35 -33
  96. package/src/scss/components/_ratings.scss +40 -38
  97. package/src/scss/components/_segmented.scss +82 -84
  98. package/src/scss/components/_skeleton.scss +66 -72
  99. package/src/scss/components/_spinners.scss +155 -159
  100. package/src/scss/components/_stepper.scss +116 -116
  101. package/src/scss/components/_tables.scss +155 -148
  102. package/src/scss/components/_tabs.scss +232 -141
  103. package/src/scss/components/_tags.scss +109 -107
  104. package/src/scss/components/_techstack-bucket.scss +123 -122
  105. package/src/scss/components/_testimonial.scss +72 -70
  106. package/src/scss/components/_theme-toggle.scss +53 -51
  107. package/src/scss/components/_tiles.scss +174 -174
  108. package/src/scss/components/_timelines.scss +282 -280
  109. package/src/scss/components/_toasts.scss +66 -72
  110. package/src/scss/form-elements/_checkbox.scss +117 -132
  111. package/src/scss/form-elements/_form.scss +149 -115
  112. package/src/scss/form-elements/_input-group.scss +99 -103
  113. package/src/scss/form-elements/_input.scss +74 -74
  114. package/src/scss/form-elements/_login.scss +48 -48
  115. package/src/scss/form-elements/_prefilled.scss +53 -51
  116. package/src/scss/form-elements/_radio.scss +95 -107
  117. package/src/scss/form-elements/_select.scss +42 -42
  118. package/src/scss/form-elements/_slider.scss +117 -132
  119. package/src/scss/form-elements/_textarea.scss +68 -54
  120. package/src/scss/form-elements/_toggle.scss +127 -140
  121. package/src/scss/layout/_footer.scss +172 -171
  122. package/src/scss/layout/_grid.scss +268 -302
  123. package/src/scss/layout/_header.scss +426 -427
  124. package/src/scss/layout/_home.scss +28 -27
  125. package/src/scss/layout/_main.scss +275 -238
  126. package/src/scss/layout/_page-header.scss +38 -36
  127. package/src/scss/layout/_section.scss +150 -124
  128. package/src/scss/main.scss +1 -1
  129. package/src/scss/utilities/_accessibility.scss +46 -45
  130. package/src/scss/utilities/_aspect.scss +22 -20
  131. package/src/scss/utilities/_corner.scss +23 -5
  132. package/src/scss/utilities/_display.scss +76 -70
  133. package/src/scss/utilities/_elevation.scss +13 -11
  134. package/src/scss/utilities/_flex.scss +83 -85
  135. package/src/scss/utilities/_reveal.scss +40 -63
  136. package/src/scss/utilities/_spacing.scss +63 -64
  137. package/src/scss/utilities/_text.scss +139 -141
  138. 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.28.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.28.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
 
@@ -370,14 +384,15 @@ feature frame) ·
370
384
  comment. If your component really does need to animate a size, name that
371
385
  property - do not reach for `all`.
372
386
  27. **A control is a `<button>`, never a styled `<div>`.** `mw-tabs-nav-item`,
373
- `mw-theme-toggle`, `mw-gallery-dot` and `mw-accordion-header` are all
374
- written for one, and all four shipped as divs and spans - the first three
375
- until 4.11, the accordion header until 4.12 - that no keyboard could reach.
376
- Each class clears what a `<button>` brings with it, so
377
- `<button type="button" class="mw-tabs-nav-item" data-tab="...">` is the
378
- whole markup. If you build your own clickable thing: the element decides
379
- whether anyone without a mouse can use it, the class only decides how it
380
- 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.
381
396
  28. **A link in running text is `mw-link`, not `mw-btn mw-btn-link`.** Since
382
397
  4.13.0 there is a class for exactly that. `mw-btn` is `inline-flex` with
383
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.28.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.28.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
 
@@ -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 |
@@ -47,6 +47,19 @@ classes are the entire contract.
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
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
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.
62
+
50
63
  ## A note on the state class
51
64
 
52
65
  Since 4.0.0 the script writes `mw-active` and clears both `mw-active` and the
@@ -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`,
@@ -78,6 +78,7 @@ Safari 16.4+, Firefox 128+. The same range is declared as `browserslist` in
78
78
  | `--mw-kanban-background`, `--mw-kanban-lane-border`, `--mw-kanban-column-min-height` | Per-board surface, lane border and lane floor (120px, 90px on `mw-kanban-compact`) |
79
79
  | `--mw-container-gutter`, `--mw-container-width` | Page gutter of `mw-container` (fluid `clamp(1rem, 4.2vw + 0.5rem, 4rem)`, never below the safe-area inset) and the width derived from it (`min(1200px, 100% - 2 * gutter)`) |
80
80
  | `--mw-section-padding-block` | Top/bottom rhythm of `mw-section` (3.3rem, stepping down to 2.5rem below `md` and 1.75rem below `sm`) |
81
+ | `--mw-section-nav-height` | Height the sticky `mw-section-nav` reserves (3.7rem). Add it to `--mw-header-height` for the `scroll-margin-top` of anything the strip can cover |
81
82
  | `--mw-calendar-dot` | Colour of a single calendar dot - set it per dot or per cell; the `mw-calendar-dot-*` classes are presets for it |
82
83
  | `--mw-scroll-hint-cover` | Colour the scroll hint on a tab bar fades into. Preset to the page, re-pointed to the card background inside `mw-card`, `mw-panel`, `mw-modal`, `mw-tile`, `mw-calendar` |
83
84
  | `--mw-elevation-1` … `-5` | Every shadow in the framework. Two layers per level - contact plus ambient. Never write a `box-shadow` by hand: a hand-rolled one is the wrong colour in one of the two themes |
@@ -171,9 +172,20 @@ Two rules that prevent most colour bugs:
171
172
  - Persisting the choice, the toggle UI and the initial class are the
172
173
  application's job in a SPA (`examples/angular-services.md`). The shipped JS
173
174
  does it for static pages using `localStorage` under the key `mw-theme`.
175
+ - **Suppress transitions while the class flips.** Colour changes on nearly every
176
+ element at once, and the shared `--mw-transition` turns that into thousands of
177
+ concurrent animations - on a documentation-sized page it is seconds of blocked
178
+ style and layout. `mw-theme-switching` on `<html>` kills them for the flip; the
179
+ forced reflow between the two class changes is what commits the new colours
180
+ before transitions come back.
174
181
 
175
182
  ```ts
183
+ const root = document.documentElement;
184
+
185
+ root.classList.add('mw-theme-switching');
176
186
  document.body.classList.toggle('mw-theme-light', isLight);
187
+ void root.offsetHeight; // commit the new colours with transitions off
188
+ root.classList.remove('mw-theme-switching');
177
189
  ```
178
190
 
179
191
  ## SCSS configuration
@@ -262,6 +274,29 @@ they are all `var()` references anyway:
262
274
  }
263
275
  ```
264
276
 
277
+ ## Overriding a component
278
+
279
+ Tokens cover colour and rhythm; for anything else write a normal rule. The
280
+ framework emits everything inside
281
+ `@layer mw.reset, mw.base, mw.forms, mw.components, mw.layout, mw.utilities`,
282
+ and an unlayered rule beats every layer regardless of weight:
283
+
284
+ ```css
285
+ /* wins over .mw-card, no !important and no doubled selector */
286
+ .mw-card {
287
+ border-radius: 12px;
288
+ }
289
+ ```
290
+
291
+ The same rule cuts the other way: third-party CSS loaded unlayered also beats
292
+ the framework, which is what makes an icon font quietly win over `mw-tags-icon`.
293
+ Give it a layer of its own:
294
+
295
+ ```css
296
+ @layer vendor, mw;
297
+ @import url('font-awesome.css') layer(vendor);
298
+ ```
299
+
265
300
  ## Importing only what you need
266
301
 
267
302
  The full stylesheet is ~200 kB raw / ~31 kB gzipped. Marketing components
@@ -272,7 +307,9 @@ Angular bundle budgets notice.
272
307
  Every layer forwards one module per file, and no `@extend` crosses a file
273
308
  boundary, so partial imports are safe. **The one thing you must not drop is
274
309
  `base`** - it carries the `:root` tokens; without it every component renders
275
- colourless.
310
+ colourless. The cascade layer order comes along on its own: every module loads
311
+ `abstracts`, which declares it, so a hand-picked subset orders itself the same
312
+ way the full build does.
276
313
 
277
314
  ```scss
278
315
  // styles.scss - configure first, then pick
@@ -0,0 +1,5 @@
1
+ {
2
+ "hook": {
3
+ "consent": "accepted"
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ {"version":1,"sessions":{"5682ec58-6688-479f-994f-990ff918c05d":{"updatedAt":1789084033412,"files":{"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/layout/_header.scss":{"editCount":3,"findings":[],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{}},"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/form-elements/_slider.scss":{"editCount":1,"findings":[],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{}},"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/js/main.js":{"editCount":7,"findings":[]},"/Users/m1well/Dev/workspace/css/maverick-wave/index.html":{"editCount":2,"findings":["low-contrast:0:3.3:1 (need 4.5:1) — text #7d85a5 on #313746","pulsing-dot:0:.mw-spinner-dots>div — 16x16px dot with infinite \"mw-bounce\" animation","dark-glow:0:Colored box-shadow glow (#d1bb21) on dark page"]},"/Users/m1well/Dev/workspace/css/maverick-wave/src/partials/typography-container.html":{"editCount":2,"findings":[],"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/base/_base.scss":{"editCount":2,"findings":[],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{}},"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/layout/_section.scss":{"editCount":7,"findings":[],"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/form-elements/_form.scss":{"editCount":1,"findings":[],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{}},"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/components/_alerts.scss":{"editCount":1,"findings":["side-tab:83"],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{"62d926b4b80efa38e6fbf0fe65f45cf171d9b8a80be0b2ee47ac4cbb6c9498d0":1}},"cleanAcked":true},"/Users/m1well/Dev/workspace/css/maverick-wave/src/scss/layout/_main.scss":{"editCount":1,"findings":[],"stopBaseline":{"version":1,"engine":"0.1.5","counts":{}},"cleanAcked":true}},"footerShown":true}}}