maverick-wave 5.11.0 → 5.13.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 (32) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +63 -8
  2. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +6 -3
  3. package/.claude/skills/mw-maverick-wave/references/components.md +41 -27
  4. package/.claude/skills/mw-maverick-wave/references/javascript.md +27 -26
  5. package/.claude/skills/mw-maverick-wave/references/layout.md +12 -6
  6. package/.claude/skills/mw-maverick-wave/references/theming.md +59 -4
  7. package/CHANGELOG.md +18 -0
  8. package/README.md +45 -5
  9. package/index.html +12 -11
  10. package/maverick-wave.min.css +4 -4
  11. package/maverick-wave.min.js +1 -1
  12. package/package.json +2 -2
  13. package/src/js/main.js +67 -20
  14. package/src/partials/buttons-container.html +2 -1
  15. package/src/partials/get-started-container.html +52 -3
  16. package/src/partials/palette-container.html +23 -8
  17. package/src/partials/parallax-container.html +8 -5
  18. package/src/partials/tabs-container.html +1 -1
  19. package/src/partials/typography-container.html +3 -0
  20. package/src/scss/abstracts/_functions.scss +50 -0
  21. package/src/scss/abstracts/_mixins.scss +33 -0
  22. package/src/scss/base/_typography.scss +4 -21
  23. package/src/scss/components/_buttons.scss +1 -1
  24. package/src/scss/components/_content-slider.scss +16 -8
  25. package/src/scss/components/_dropdown.scss +1 -1
  26. package/src/scss/components/_gallery.scss +13 -2
  27. package/src/scss/components/_lang-switch.scss +1 -1
  28. package/src/scss/components/_segmented.scss +4 -0
  29. package/src/scss/layout/_header.scss +3 -2
  30. package/src/scss/layout/_parallax.scss +18 -2
  31. package/src/scss/layout/_section.scss +2 -10
  32. package/src/scss/utilities/_touch-targets.scss +22 -16
@@ -42,20 +42,24 @@ Load the one you need - do not read them all up front.
42
42
  ### Plain HTML / CDN
43
43
 
44
44
  ```html
45
+ <meta
46
+ name="viewport"
47
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
48
+ />
45
49
  <link
46
50
  rel="stylesheet"
47
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.css"
51
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.13.0/maverick-wave.min.css"
48
52
  />
49
53
  <link
50
54
  rel="stylesheet"
51
55
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
52
56
  />
53
57
  ...
54
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.js"></script>
58
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.13.0/maverick-wave.min.js"></script>
55
59
  ```
56
60
 
57
61
  Pin the version. The JS file is optional and only for server-rendered/static pages -
58
- see below.
62
+ see below. `viewport-fit=cover` is not optional - see pitfall 30.
59
63
 
60
64
  ### Angular (or any SPA)
61
65
 
@@ -66,7 +70,8 @@ see below.
66
70
  ```
67
71
 
68
72
  Or from the npm package via SCSS: `@use 'maverick-wave/src/scss/main';`
69
- (details and configuration in `references/theming.md`).
73
+ (details and configuration in `references/theming.md`). The viewport meta above
74
+ belongs in `src/index.html` there, with `viewport-fit=cover` just the same.
70
75
 
71
76
  > **Never load `maverick-wave.min.js` in a SPA.** Everything in it is wired up
72
77
  > once on `DOMContentLoaded` and writes straight into the DOM. In an Angular
@@ -148,7 +153,12 @@ keys at all (negative gap is invalid CSS and is not generated).
148
153
 
149
154
  **Font sizes** (`mw-text-3xs` … `mw-text-6xl`): `3xs` 0.6, `2xs` 0.7, `xs` 0.8,
150
155
  `sm` 0.9, `base` 1, `md` 1.1, `lg` 1.3, `xl` 1.5, `2xl` 1.8, `3xl` 2.2, `4xl` 2.5,
151
- `5xl` 3, `6xl` 4.3 rem.
156
+ `5xl` 3, `6xl` 4.3 rem. The utility classes are single steps; `h1`, `h2`,
157
+ `mw-section-title` and `mw-section-subtitle` interpolate instead - `h1` runs
158
+ `3xl` to `5xl` between 375px and 768px, `h2` `2xl` to `4xl`, the two section
159
+ headings one step each up to 576px. The endpoints are the values the old
160
+ breakpoint steps had, so a phone and a desktop look the same and only the widths
161
+ between them moved.
152
162
 
153
163
  **Breakpoints**: `xs` 375, `sm` 576, `md` 768, `lg` 992, `xl` 1200, `2xl` 1400 px.
154
164
  Column grids and most components are mobile-first. The mixins emit range syntax
@@ -213,6 +223,22 @@ the same height by construction. Buttons run one font step above the fields.
213
223
  `--mw-focus-halo-opacity` 28% for the soft ring a form field gets instead of a
214
224
  hard outline. SCSS: `@include focus-ring`, `focus-ring-inset`, `field-focus`.
215
225
 
226
+ **SCSS helpers** - `@use 'maverick-wave/src/scss/abstracts' as *` brings them in;
227
+ none of them survive into the compiled CSS. Full list in `references/theming.md`.
228
+
229
+ - `fluid($min, $max, $from: 'xs', $to: 'md')` - a clamp between two sizes, each
230
+ end a key of `$font-sizes` or a plain rem/px length. The rem term is the
231
+ point: a vw-only clamp ignores the reader's font size and stops responding to
232
+ zoom. A headline passes `$to: 'xl'`.
233
+ - `media-up($bp)` / `media-down($bp)` - the breakpoint map as a query.
234
+ - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport, the condition
235
+ every target-size rule in the framework hangs under.
236
+ - `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
+ - `hover` / `hover-move`, `focus-ring`, `focus-ring-inset`, `field-focus`,
240
+ `truncate`, `surface`.
241
+
216
242
  ## Component index
217
243
 
218
244
  Everything below is documented in `references/components.md` unless marked otherwise.
@@ -323,8 +349,13 @@ feature frame) ·
323
349
  44px, and list rows / menu items / pager pages / accordion headers 2.75rem.
324
350
  `mw-btn-mini` keeps its 18px circle - it sits in tag rows and table cells
325
351
  where a bigger one would shift the layout - and grows its _hit area_ to 28px
326
- via a pseudo-element. Nothing to switch on, and no reason to write the media
327
- query again in an app.
352
+ via a pseudo-element, and `mw-kanban-action` does the same - its 32px box
353
+ stays and the hit area around it reaches 44px.
354
+ `mw-techstack-item-sm` takes a real 2.5rem minimum instead, because half of
355
+ those chips carry a `data-tooltip` and that owns the pseudo-element.
356
+ Nothing to switch on, and no reason to write the media query again in an
357
+ app: your own control gets `@include touch-floor` for a height or
358
+ `@include hit-area` where the size is the design.
328
359
  12. **`mw-empty-state` has a `-desc`, not a `-text`.** The parts are
329
360
  `mw-empty-state-icon`, `-title`, `-desc`, plus the size variant
330
361
  `mw-empty-state-sm`. Invented names fail silently, as always.
@@ -360,7 +391,11 @@ feature frame) ·
360
391
  18. **Never write `box-shadow` by hand.** Use `var(--mw-elevation-1..5)` or the
361
392
  `mw-elevation-*` class. A hand-rolled shadow is the wrong colour in one of
362
393
  the two themes - the dark theme's shadow is a light rim over a dark contact
363
- layer, not a black blur.
394
+ layer, not a black blur. The one place the ramp is wrong is a shadow that
395
+ lands on a picture instead of on a theme surface: its light-theme layers are
396
+ cut for paper and vanish over a photograph. That is why the edge after a
397
+ parallax block carries fixed values, and why `mw-parallax-dimmed` hardcodes
398
+ its brightness and text-shadow.
364
399
  19. **Never write a duration or an easing curve by hand** either. Use
365
400
  `var(--mw-transition)` for a hover or focus state,
366
401
  `var(--mw-transition-fast)` for something that should feel instant, and
@@ -427,3 +462,23 @@ feature frame) ·
427
462
  `mw-parallax-media` child stay where the image is: on the container. Same
428
463
  split for `mw-parallax-slow`, and `--mw-parallax-slow-travel` goes with the
429
464
  class on the section - on the inner block it never reaches the animation.
465
+ That same sibling rule also puts an upward shadow on the one section
466
+ directly after the block, so the overlap reads as a surface and not a cut.
467
+ It sits at `:where()` weight, so a plain `box-shadow` of your own on that
468
+ section replaces it.
469
+ 30. **`viewport-fit=cover` belongs in the viewport meta.** The container
470
+ gutter, the mobile nav panel and the modal padding all budget for the
471
+ cutout with `env(safe-area-inset-*)`, and iOS resolves every one of those
472
+ to `0` without it - on a notched phone the content then sits under the
473
+ rounded corner. Nothing errors, which is why the shipped JS writes a
474
+ console warning on localhost when the meta is missing.
475
+ 31. **Half the layout reads its own width, not the viewport.** `mw-card`,
476
+ `mw-tile`, `mw-stepper`, both timelines and - since 5.13.0 -
477
+ `mw-gallery-container` and `mw-image-slider` are `@container` queries, so
478
+ any of them in a half-width column takes its narrow layout there while the
479
+ window is still a desktop. Two consequences: the query only reaches
480
+ _descendants_, which is why the gallery wrapper around `mw-gallery`,
481
+ `mw-gallery-desc` and `mw-gallery-dots` cannot be dropped; and
482
+ `container-type: inline-size` computes the width as if the element were
483
+ empty, so one dropped into a flex or grid slot needs a width - the
484
+ framework sets `width: 100%` on its own containers already.
@@ -15,12 +15,15 @@ accordion FAQ.
15
15
  <html lang="en">
16
16
  <head>
17
17
  <meta charset="utf-8" />
18
- <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <meta
19
+ name="viewport"
20
+ content="width=device-width, initial-scale=1, viewport-fit=cover"
21
+ />
19
22
  <title>Acme Analytics</title>
20
23
 
21
24
  <link
22
25
  rel="stylesheet"
23
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.css"
26
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.13.0/maverick-wave.min.css"
24
27
  />
25
28
  <link
26
29
  rel="stylesheet"
@@ -487,7 +490,7 @@ accordion FAQ.
487
490
  <div class="mw-modal-backdrop" onclick="closeModal('demo')"></div>
488
491
  </div>
489
492
 
490
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.js"></script>
493
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.13.0/maverick-wave.min.js"></script>
491
494
  <script>
492
495
  // The only thing the shipped script does not cover: opening a modal.
493
496
  // Closing works through .mw-modal-close, the backdrop is wired above.
@@ -188,7 +188,8 @@ picker.
188
188
  - Active position: `mw-active` on the item.
189
189
  - `mw-segmented-secondary` switches the active fill to the secondary colour,
190
190
  `mw-segmented-auto` shrinks the control to its content instead of filling the
191
- row. `disabled` works on an item.
191
+ row - capped at `100%`, so labels wider than a 320px screen truncate instead
192
+ of pushing the row off it. `disabled` works on an item.
192
193
  - Items reach a 2.5rem minimum height on a coarse pointer.
193
194
 
194
195
  ## Cards
@@ -233,9 +234,10 @@ picker.
233
234
  below 360px, and the title, the subtitle and the body padding step up at
234
235
  420px. The image height is the one that still follows the window, because it
235
236
  is declared on the card itself and a container cannot query itself.
236
- - `mw-tile` works the same way, and so do `mw-stepper` and both timelines: the
237
- step labels shrink and the two-sided timeline collapses to one side once the
238
- component is narrow, whatever the window does. One catch on tiles: a tile
237
+ - `mw-tile` works the same way, and so do `mw-stepper`, both timelines, the
238
+ gallery wrapper and the image slider: the step labels shrink and the two-sided
239
+ timeline collapses to one side once the component is narrow, whatever the
240
+ window does. One catch on tiles: a tile
239
241
  clips its overflow, and being a container makes it the containing block for
240
242
  `position: fixed`, so a `data-tooltip` **inside** a tile is cut off at its
241
243
  edge. Cards do not clip and are unaffected.
@@ -890,26 +892,26 @@ a half-width column wraps on a desktop too.
890
892
  </div>
891
893
  ```
892
894
 
893
- | Class | Role |
894
- | ------------------------- | ---------------------------------------------------------------------------------------- |
895
- | `mw-kanban` | Board. Grid, one column per lane, same width for all of them |
896
- | `mw-kanban-plain` | Board without its own surface or padding - for a board that already sits on a panel |
897
- | `mw-kanban-compact` | Tighter padding, description clamped to 2 lines instead of 4, lane floor 90px |
898
- | `mw-kanban-column` | Lane: dashed border, flex column |
899
- | `mw-kanban-column-header` | Title + counter + add button in one row |
900
- | `mw-kanban-title` | Lane title, uppercase, truncates |
901
- | `mw-kanban-count` | Ticket counter pill |
902
- | `mw-kanban-add` | Sits **on** `mw-btn` - only trims it to the header line height |
903
- | `mw-kanban-column-body` | Ticket stack; fills the lane so the empty state stays centred |
904
- | `mw-kanban-card` | Ticket. Needs `mw-card` next to it |
905
- | `mw-kanban-card-title` | Ticket title |
906
- | `mw-kanban-card-text` | Description, clamped to 4 lines (2 on a compact board) |
907
- | `mw-kanban-card-footer` | Rule + key on the left, avatar and actions on the right |
908
- | `mw-kanban-card-id` | Ticket key, monospaced so equal-length keys line up across cards |
909
- | `mw-kanban-card-actions` | Right-hand group; resets the avatar margin |
910
- | `mw-kanban-action` | 26px square icon button (32px below `md`), `mw-kanban-action-danger` turns the hover red |
911
- | `mw-kanban-empty` | Placeholder; hides itself as soon as the lane holds a ticket or an open composer |
912
- | `mw-kanban-card-in` | One-shot entry animation for a freshly created ticket |
895
+ | Class | Role |
896
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
897
+ | `mw-kanban` | Board. Grid, one column per lane, same width for all of them |
898
+ | `mw-kanban-plain` | Board without its own surface or padding - for a board that already sits on a panel |
899
+ | `mw-kanban-compact` | Tighter padding, description clamped to 2 lines instead of 4, lane floor 90px |
900
+ | `mw-kanban-column` | Lane: dashed border, flex column |
901
+ | `mw-kanban-column-header` | Title + counter + add button in one row |
902
+ | `mw-kanban-title` | Lane title, uppercase, truncates |
903
+ | `mw-kanban-count` | Ticket counter pill |
904
+ | `mw-kanban-add` | Sits **on** `mw-btn` - only trims it to the header line height |
905
+ | `mw-kanban-column-body` | Ticket stack; fills the lane so the empty state stays centred |
906
+ | `mw-kanban-card` | Ticket. Needs `mw-card` next to it |
907
+ | `mw-kanban-card-title` | Ticket title |
908
+ | `mw-kanban-card-text` | Description, clamped to 4 lines (2 on a compact board) |
909
+ | `mw-kanban-card-footer` | Rule + key on the left, avatar and actions on the right |
910
+ | `mw-kanban-card-id` | Ticket key, monospaced so equal-length keys line up across cards |
911
+ | `mw-kanban-card-actions` | Right-hand group; resets the avatar margin |
912
+ | `mw-kanban-action` | 26px square icon button (32px below `md`, hit area grown to 44px), `mw-kanban-action-danger` turns the hover red |
913
+ | `mw-kanban-empty` | Placeholder; hides itself as soon as the lane holds a ticket or an open composer |
914
+ | `mw-kanban-card-in` | One-shot entry animation for a freshly created ticket |
913
915
 
914
916
  - **Priority is the regular `mw-card-ribbon`** and brings its own colour through
915
917
  `mw-card-addon-danger|warning|info`. It has to be a **direct child** of the
@@ -1645,7 +1647,14 @@ variants apply to it too), `mw-divider-vertical` needs a flex row with a height.
1645
1647
 
1646
1648
  The dots (`mw-gallery-dot`, active one gets `mw-active`) are generated by the
1647
1649
  shipped JS, as is the track transform. In a SPA, render the dots and set
1648
- `transform: translateX(...)` yourself.
1650
+ `transform: translateX(...)` yourself. Swipe is single-finger and decided by the
1651
+ dominant axis, so a page scroll that drifts sideways does not page the gallery.
1652
+
1653
+ `mw-gallery-container` is a query container (`mw-gallery`), and the caption size,
1654
+ the dot size and the 4:3 crop follow _its_ width - 688px and 512px - not the
1655
+ viewport. A gallery in a half-width column therefore takes its narrow layout
1656
+ there. Keep the wrapper: a container query only reaches descendants, and the
1657
+ caption and the dots are siblings of `mw-gallery`.
1649
1658
 
1650
1659
  `mw-image-slider` swaps a base image against overlays through indexed buttons:
1651
1660
 
@@ -1666,7 +1675,10 @@ shipped JS, as is the track transform. In a SPA, render the dots and set
1666
1675
  ```
1667
1676
 
1668
1677
  Control grids: `mw-image-slider-controls-grid-2`, `-grid-3`. Visible overlay and
1669
- current button carry `active`.
1678
+ current button carry `active`. `mw-image-slider` is its own query container
1679
+ (`mw-slider`): the three heights (250 / 320 / 370px) and the control columns
1680
+ switch at 420px and 560px of slider width, because the slider caps at 600px and
1681
+ past a tablet the viewport says nothing about the room it has.
1670
1682
 
1671
1683
  ## Blog post
1672
1684
 
@@ -1809,7 +1821,9 @@ is no highlighting engine - wrap spans yourself or plug in Prism/highlight.js.
1809
1821
  </div>
1810
1822
  ```
1811
1823
 
1812
- `mw-techstack-item-sm` is the logo-only chip; the info block is optional.
1824
+ `mw-techstack-item-sm` is the logo-only chip; the info block is optional. On a
1825
+ coarse pointer the chip itself grows from 32px to 2.5rem - it often carries a
1826
+ `data-tooltip`, which owns the pseudo-element a grown hit area would need.
1813
1827
 
1814
1828
  ## Coming soon
1815
1829
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What `maverick-wave.min.js` is
4
4
 
5
- One vanilla IIFE, no dependencies, ~23 kB. It queries the DOM **once** on
5
+ One vanilla IIFE, no dependencies, ~24 kB. It queries the DOM **once** on
6
6
  `DOMContentLoaded` and attaches listeners. There is no re-init API, no
7
7
  `MutationObserver`, no exported module - it is built for a server-rendered or
8
8
  static page.
@@ -25,31 +25,32 @@ classes are the entire contract.
25
25
 
26
26
  ## Behaviour inventory
27
27
 
28
- | Behaviour | What the shipped JS does | What to do instead |
29
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
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, 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`) |
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
- | Scroll spy | Sets `mw-active` on `mw-navbar-link` from the scroll position | Router-based: `routerLinkActive="mw-active"` |
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 |
36
- | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` / `mw-theme-dark` on `<body>` and `mw-active` on the toggle, wrapped in `mw-theme-switching` on `<html>` so the flip starts no transitions. With nothing stored it takes `prefers-color-scheme` and keeps following it | A theme service - see `examples/angular-services.md` |
37
- | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
38
- | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
39
- | 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 |
40
- | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
41
- | 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 |
42
- | 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 |
43
- | Kanban board | Counts the tickets per lane, moves a card between lanes (`data-kanban-move`) and marks the arrival with `mw-kanban-card-moved-forward` / `-back`, clones `mw-kanban-card-template` on save, derives the next key from `data-kanban-prefix`, toggles `mw-active` on the composer | Keep the tickets in a signal/store and render the lanes from it; `mw-active` on the composer, `mw-kanban-editing` on the ticket it replaces. Neither the `<template>` nor the `mw-kanban-composer-*` hook classes are needed |
44
- | Calendar | Renders the month or week grid from `data-calendar="month | week"`, pages with `data-calendar-nav`, draws the status dots from `data-calendar-markers`, toggles `mw-selected`and emits`mw-calendar-select` | Render the cells from a signal and bind `mw-calendar-adjacent`, `mw-calendar-weekend`, `mw-calendar-today` and `mw-selected` yourself; none of the `data-calendar-*` attributes are needed |
45
- | Localhost indicator | On a local hostname, prepends `mw-localhost-indicator-pulse` to the header when it carries `mw-localhost-indicator-activated` | Render the element conditionally |
46
- | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
47
- | Color swatches | Showcase-only (prints computed hex values); exposes `window.mwRefreshColorSwatches` to read them again after a root colour changed | Not needed |
48
- | Dropdown | Delegated to the document: closes the open `mw-dropdown` on Escape, on a click elsewhere and on a click on a `mw-dropdown-item`, and returns focus to the `summary` | The `<details>` does the opening, the keyboard and the state on its own. Rebuild only the two behaviours markup cannot express - or bind `[attr.open]` and keep them in the component |
49
- | Language switcher | Keeps the trigger's flag and code in step with the chosen item, moves `mw-active` and `aria-current`, and fires `mw-language-change` (`detail: { lang, name }`) on the switcher | Bind the trigger from your locale signal and switch the language in your own i18n service; the menu itself is a `<details>` and needs nothing |
50
- | Scroll reveal | Older browsers only: an `IntersectionObserver` adds `mw-reveal-hidden` to what is still below the fold and swaps it for `mw-reveal-run` on entry, with an `animation-delay` per grid column | A directive per element - see `examples/angular-services.md` |
51
- | Header reveal | Older browsers only: toggles `mw-header-away` and `mw-announcement-away` past 270px of scroll, and adds the transition class one frame later so the bar does not slide away on load | The same two classes bound to a scroll signal, behind the same guard |
52
- | Parallax | Older browsers only: writes `--mw-parallax-progress` (0 to 1) on every `mw-parallax-media` from a `requestAnimationFrame` loop | The same, reading every layer's rect before writing to any of them |
28
+ | Behaviour | What the shipped JS does | What to do instead |
29
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
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, 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`) |
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
+ | Scroll spy | Sets `mw-active` on `mw-navbar-link` from the scroll position | Router-based: `routerLinkActive="mw-active"` |
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 |
36
+ | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` / `mw-theme-dark` on `<body>` and `mw-active` on the toggle, wrapped in `mw-theme-switching` on `<html>` so the flip starts no transitions. With nothing stored it takes `prefers-color-scheme` and keeps following it | A theme service - see `examples/angular-services.md` |
37
+ | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
38
+ | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
39
+ | 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 |
40
+ | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
41
+ | Gallery | Generates the dots, moves the track, swipe handling (single finger, dominant axis, passive listeners), writes `mw-gallery-desc` | Render dots in the template, bind the track transform and `mw-active` on the current dot |
42
+ | 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 |
43
+ | Kanban board | Counts the tickets per lane, moves a card between lanes (`data-kanban-move`) and marks the arrival with `mw-kanban-card-moved-forward` / `-back`, clones `mw-kanban-card-template` on save, derives the next key from `data-kanban-prefix`, toggles `mw-active` on the composer | Keep the tickets in a signal/store and render the lanes from it; `mw-active` on the composer, `mw-kanban-editing` on the ticket it replaces. Neither the `<template>` nor the `mw-kanban-composer-*` hook classes are needed |
44
+ | Calendar | Renders the month or week grid from `data-calendar="month | week"`, pages with `data-calendar-nav`, draws the status dots from `data-calendar-markers`, toggles `mw-selected`and emits`mw-calendar-select` | Render the cells from a signal and bind `mw-calendar-adjacent`, `mw-calendar-weekend`, `mw-calendar-today` and `mw-selected` yourself; none of the `data-calendar-*` attributes are needed |
45
+ | Viewport check | On a local hostname, warns on the console when the viewport meta has no `viewport-fit=cover` - without it every `env(safe-area-inset-*)` resolves to 0 | Nothing - check the meta tag once |
46
+ | Localhost indicator | On a local hostname, prepends `mw-localhost-indicator-pulse` to the header when it carries `mw-localhost-indicator-activated` | Render the element conditionally |
47
+ | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
48
+ | Color swatches | Showcase-only (prints computed hex values); exposes `window.mwRefreshColorSwatches` to read them again after a root colour changed | Not needed |
49
+ | 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 |
50
+ | 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 |
51
+ | Scroll reveal | Older browsers only: an `IntersectionObserver` adds `mw-reveal-hidden` to what is still below the fold and swaps it for `mw-reveal-run` on entry, with an `animation-delay` per grid column | A directive per element - see `examples/angular-services.md` |
52
+ | Header reveal | Older browsers only: toggles `mw-header-away` and `mw-announcement-away` past 270px of scroll, and adds the transition class one frame later so the bar does not slide away on load | The same two classes bound to a scroll signal, behind the same guard |
53
+ | Parallax | Older browsers only: writes `--mw-parallax-progress` (0 to 1) on every `mw-parallax-media` from a `requestAnimationFrame` loop | The same, reading every layer's rect before writing to any of them, and measuring against `document.documentElement.clientHeight` - `innerHeight` grows by up to 100px as the URL bar slides away and steps every layer mid-scroll |
53
54
 
54
55
  ## Scroll-driven animations
55
56
 
@@ -28,7 +28,8 @@
28
28
  - `mw-container` is `min(1200px, 100% - 2 * gutter)`, horizontally centered.
29
29
  Nest it inside every full-bleed band (header, section, footer), never around
30
30
  them. The gutter is fluid - `clamp(1rem, 4.2vw + 0.5rem, 4rem)`, never smaller
31
- than the safe-area inset - so it lands on ~24px at 375px, ~32px at 576px,
31
+ than the safe-area inset (which needs `viewport-fit=cover` on the viewport
32
+ meta to be anything but 0) - so it lands on ~24px at 375px, ~32px at 576px,
32
33
  ~40px on a tablet and tops out at 64px, where the 1200px cap takes over. A
33
34
  percentage alone keeps too little on a phone and throws away too much on a
34
35
  desktop; stepping the gutter at a breakpoint instead would make the container
@@ -193,9 +194,9 @@ viewport, so keep long tooltips off the outermost elements.
193
194
  | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
194
195
  | `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`) |
195
196
  | `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
196
- | `mw-section-title` | Centered `3xl` heading with a decorative primary underline - landing pages |
197
+ | `mw-section-title` | Centered heading with a decorative primary underline, `2xl` growing to `3xl` up to 576px - landing pages |
197
198
  | `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
198
- | `mw-section-subtitle` | Centered `2xl` heading with a thin secondary underline |
199
+ | `mw-section-subtitle` | Centered heading with a thin secondary underline, `xl` growing to `2xl` up to 576px |
199
200
  | `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 |
200
201
 
201
202
  ```html
@@ -351,7 +352,12 @@ and bottom, so no edge is ever uncovered.
351
352
  ride up over it. It goes on the element the page content is a **sibling** of -
352
353
  the section, not the hero - because everything after it is given
353
354
  `--mw-page-background` and a layer above, or the pinned picture shows through
354
- the transparent ones. Depth drops to `0` there: a pinned block does not travel
355
+ the transparent ones. The one section directly after it also gets an upward
356
+ shadow, so the overlap reads as a surface sliding over the picture instead of a
357
+ cut. Fixed values, not the elevation ramp - the shadow falls on the picture
358
+ rather than on a theme surface, and the light theme's near layer is cut for
359
+ paper and disappears there. It sits at `:where()` weight, so a plain rule of
360
+ your own overrides it. Depth drops to `0` there: a pinned block does not travel
355
361
  through the viewport, so its `view()` timeline stands still with it.
356
362
  `mw-parallax-rise` puts it at `12vh` and runs the layer on the document's own
357
363
  scroll instead - the picture lifts, the text stays put. That works out to the
@@ -370,7 +376,7 @@ reads `$mw-hero-image`; anywhere else set `--mw-parallax-pattern-image` on it.
370
376
  `--mw-parallax-pattern-depth` is the dial, `4vh` next to a risen hero.
371
377
 
372
378
  Outside a hero the same classes build a standalone band: `mw-parallax`
373
- (`min-height: 42vh`), `mw-parallax-media` as an `<img>` or a div with a
379
+ (`min-height: 60vh`), `mw-parallax-media` as an `<img>` or a div with a
374
380
  background image, `mw-parallax-content` for what sits on top, and
375
381
  `mw-parallax-dimmed` to turn the picture down and the text light. Drop the
376
382
  `mw-container` between section and block and the picture runs full width, with
@@ -385,7 +391,7 @@ stands still, past it the block runs backwards. The dial belongs on the element
385
391
  carrying `mw-parallax-slow`, the height on the `mw-parallax` block inside it;
386
392
  put the dial on the inner block and it never reaches the animation. Everything
387
393
  after it gets the same page background and layer that a pinned block gives its
388
- siblings.
394
+ siblings, and the section directly after it the same edge shadow.
389
395
 
390
396
  Without scroll timelines, or under `prefers-reduced-motion`, the picture stands
391
397
  still and nothing else changes.
@@ -353,6 +353,61 @@ they are all `var()` references anyway:
353
353
  }
354
354
  ```
355
355
 
356
+ ## Mixins and functions
357
+
358
+ `@use 'maverick-wave/src/scss/abstracts' as *` brings the whole set in - the
359
+ tokens, the functions and the mixins. They exist on the SCSS path only; the
360
+ compiled CSS carries none of them, so a CDN project cannot reach them.
361
+
362
+ Functions read the maps: `spacing('4')`, `font-size('lg')`,
363
+ `font-weight('bold')`, `radius('md')`, `breakpoint('md')`, `color('primary-color')`,
364
+ `z-index('menu')`, `motion('base')`.
365
+
366
+ ```scss
367
+ @use 'maverick-wave/src/scss/abstracts' as *;
368
+
369
+ .hero-title {
370
+ font-size: fluid(1.95rem, 4.2rem, $to: 'xl');
371
+ margin-bottom: spacing('6');
372
+ }
373
+
374
+ .my-icon-button {
375
+ @include touch-floor;
376
+ }
377
+
378
+ .my-dot {
379
+ @include hit-area(7px);
380
+ }
381
+ ```
382
+
383
+ - `fluid($min, $max, $from: 'xs', $to: 'md')` - a clamp between two sizes. Each
384
+ end is a key of `$font-sizes` or a plain rem/px length, so a hero that does
385
+ not sit on the scale still gets one. The rem term is what the vw-only form
386
+ throws away: a size in vw alone ignores the reader's font size and stops
387
+ responding to zoom. The default range ends at `md` because that is where the
388
+ stepped heading sizes it replaces reached their largest value - a headline
389
+ that should keep growing passes `$to: 'xl'`.
390
+ - `media-up($bp)` / `media-down($bp)` - the `$breakpoints` map as range syntax,
391
+ `width >= bp` and `width < bp`, so the two never overlap at the breakpoint.
392
+ - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport. Every
393
+ target-size rule in the framework hangs under it; put your own control under
394
+ the same condition instead of guessing it.
395
+ - `touch-floor($size: 2.75rem)` - that condition plus a `min-height`, and it
396
+ takes a `@content` block for whatever else the control needs there. Not for a
397
+ link inside a sentence: WCAG 2.5.8 exempts those, and a `min-height` there
398
+ pushes the lines of the paragraph apart.
399
+ - `hit-area($grow: 6px)` - grows the hit area through `::after` without touching
400
+ the silhouette, for a control whose size is the design. Neighbours need a gap
401
+ of at least twice `$grow`, or two hit areas overlap and a tap lands on the one
402
+ next door. It takes `::after`, so it is out for anything carrying a
403
+ `data-tooltip`.
404
+ - `hover` / `hover-move` - a hover effect that only runs where hovering is real.
405
+ See the pitfall on latching `:hover`.
406
+ - `focus-ring`, `focus-ring-inset`, `field-focus`, `focus-halo` - the ring, in
407
+ its four shapes.
408
+ - `truncate`, `scrollbar`, `surface`, `corner-accent` - the shared silhouette
409
+ and the one-line helpers the components use.
410
+
356
411
  ## Overriding a component
357
412
 
358
413
  Tokens cover colour and rhythm; for anything else write a normal rule. The
@@ -378,7 +433,7 @@ Give it a layer of its own:
378
433
 
379
434
  ## Importing only what you need
380
435
 
381
- The full stylesheet is ~200 kB raw / ~31 kB gzipped. Marketing components
436
+ The full stylesheet is ~281 kB raw / ~40 kB gzipped. Marketing components
382
437
  (`blog-post`, `gallery`, `content-slider`, `techstack-bucket`, `tiles`,
383
438
  `coming-soon`, `ratings`, `home`, `hero`) are dead weight in an application, and
384
439
  Angular bundle budgets notice.
@@ -419,7 +474,7 @@ way the full build does.
419
474
  @use 'maverick-wave/src/scss/utilities';
420
475
  ```
421
476
 
422
- That set compiles to ~100 kB raw / ~17 kB gzipped - half the full build.
477
+ That set compiles to ~137 kB raw / ~21 kB gzipped - about half the full build.
423
478
 
424
479
  Details worth knowing:
425
480
 
@@ -427,8 +482,8 @@ Details worth knowing:
427
482
  before any other module loads it, so the `with (...)` line goes at the top of
428
483
  the file. Configuring `main` instead pulls in everything again.
429
484
  - `base` forwards `reset`, `base` and `typography`. If you already have your own
430
- reset, `@use '.../base/base'` gives you the `:root` block alone (~11 kB with a
431
- component or two).
485
+ reset, `@use '.../base/base'` gives you the `:root` block alone (~15 kB raw /
486
+ ~3 kB gzipped).
432
487
  - Module names are the file names without the leading underscore:
433
488
  `components/_buttons.scss` becomes `components/buttons`.
434
489
  - Layer index files (`components`, `form-elements`, `layout`, `utilities`,
package/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ Patch releases are only for test purposes - here I only document major and minor
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [5.13.0] - 2026-09-15
10
+
11
+ ### Added
12
+
13
+ - `fluid()` function, takes scale keys or plain rem/px
14
+ - `touch-context`, `touch-floor` and `hit-area` mixins
15
+
16
+ ### Changed
17
+
18
+ - responsive changes
19
+ - parallax effect with shadow
20
+
21
+ ## [5.12.0] - 2026-09-13
22
+
23
+ ### Fixed
24
+
25
+ - theme panel dropdowns
26
+
9
27
  ## [5.11.0] - 2026-09-13
10
28
 
11
29
  ### Changed