maverick-wave 5.12.0 → 5.14.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.
- package/.claude/skills/mw-maverick-wave/SKILL.md +63 -8
- package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +6 -3
- package/.claude/skills/mw-maverick-wave/references/components.md +41 -27
- package/.claude/skills/mw-maverick-wave/references/javascript.md +27 -26
- package/.claude/skills/mw-maverick-wave/references/layout.md +20 -8
- package/.claude/skills/mw-maverick-wave/references/theming.md +59 -4
- package/CHANGELOG.md +19 -0
- package/README.md +45 -5
- package/index.html +5 -4
- package/maverick-wave.min.css +4 -4
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/src/js/main.js +67 -20
- package/src/partials/buttons-container.html +2 -1
- package/src/partials/get-started-container.html +52 -3
- package/src/partials/parallax-container.html +8 -5
- package/src/partials/tabs-container.html +1 -1
- package/src/partials/typography-container.html +3 -0
- package/src/partials/utilities-container.html +29 -0
- package/src/scss/abstracts/_functions.scss +50 -0
- package/src/scss/abstracts/_mixins.scss +33 -0
- package/src/scss/base/_typography.scss +4 -21
- package/src/scss/components/_buttons.scss +1 -1
- package/src/scss/components/_content-slider.scss +16 -8
- package/src/scss/components/_dropdown.scss +1 -1
- package/src/scss/components/_gallery.scss +13 -2
- package/src/scss/components/_lang-switch.scss +1 -1
- package/src/scss/components/_segmented.scss +4 -0
- package/src/scss/layout/_header.scss +3 -2
- package/src/scss/layout/_main.scss +87 -8
- package/src/scss/layout/_parallax.scss +15 -1
- package/src/scss/layout/_section.scss +2 -10
- 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.
|
|
51
|
+
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.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.
|
|
58
|
+
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.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
|
|
327
|
-
|
|
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
|
|
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.
|
|
26
|
+
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.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.
|
|
493
|
+
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.14.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
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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, ~
|
|
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`
|
|
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`
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
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
|
|
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
|
|
@@ -184,8 +185,14 @@ as the header's first child under the same condition.
|
|
|
184
185
|
element and shows a tooltip above it on hover or keyboard focus. It is pure CSS
|
|
185
186
|
(a pseudo element on the trigger), which also means anything that clips its
|
|
186
187
|
overflow cuts it off: a scroll container, a tile, or a card carrying a ribbon -
|
|
187
|
-
plain cards do not clip.
|
|
188
|
-
|
|
188
|
+
plain cards do not clip.
|
|
189
|
+
|
|
190
|
+
Three classes steer where it lands, and they combine: `mw-tooltip-below` puts it
|
|
191
|
+
under the trigger instead of over it - for a control in a fixed header, where
|
|
192
|
+
above is off the screen - and `mw-tooltip-end` / `mw-tooltip-start` line the
|
|
193
|
+
bubble up with that edge of the trigger instead of centring it, which is what
|
|
194
|
+
keeps a tooltip on an outermost element inside the viewport. The arrow keeps
|
|
195
|
+
pointing at the trigger in every combination.
|
|
189
196
|
|
|
190
197
|
## Sections
|
|
191
198
|
|
|
@@ -193,9 +200,9 @@ viewport, so keep long tooltips off the outermost elements.
|
|
|
193
200
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
194
201
|
| `mw-section` | Vertical rhythm for a page band - `--mw-section-padding-block`, 3.3rem (2.5rem below `md`, 1.75rem below `sm`) |
|
|
195
202
|
| `mw-section-alternate` | Diagonal pattern background; combine with `mw-section` |
|
|
196
|
-
| `mw-section-title` | Centered
|
|
203
|
+
| `mw-section-title` | Centered heading with a decorative primary underline, `2xl` growing to `3xl` up to 576px - landing pages |
|
|
197
204
|
| `mw-section-intro` | The lead paragraph under a section title - centred, muted, 46rem measure |
|
|
198
|
-
| `mw-section-subtitle` | Centered
|
|
205
|
+
| `mw-section-subtitle` | Centered heading with a thin secondary underline, `xl` growing to `2xl` up to 576px |
|
|
199
206
|
| `mw-section-nav` + `mw-section-btn` | Sticky single-row strip of outline-style jump links; parks under the header and scrolls sideways when it overflows |
|
|
200
207
|
|
|
201
208
|
```html
|
|
@@ -351,7 +358,12 @@ and bottom, so no edge is ever uncovered.
|
|
|
351
358
|
ride up over it. It goes on the element the page content is a **sibling** of -
|
|
352
359
|
the section, not the hero - because everything after it is given
|
|
353
360
|
`--mw-page-background` and a layer above, or the pinned picture shows through
|
|
354
|
-
the transparent ones.
|
|
361
|
+
the transparent ones. The one section directly after it also gets an upward
|
|
362
|
+
shadow, so the overlap reads as a surface sliding over the picture instead of a
|
|
363
|
+
cut. Fixed values, not the elevation ramp - the shadow falls on the picture
|
|
364
|
+
rather than on a theme surface, and the light theme's near layer is cut for
|
|
365
|
+
paper and disappears there. It sits at `:where()` weight, so a plain rule of
|
|
366
|
+
your own overrides it. Depth drops to `0` there: a pinned block does not travel
|
|
355
367
|
through the viewport, so its `view()` timeline stands still with it.
|
|
356
368
|
`mw-parallax-rise` puts it at `12vh` and runs the layer on the document's own
|
|
357
369
|
scroll instead - the picture lifts, the text stays put. That works out to the
|
|
@@ -370,7 +382,7 @@ reads `$mw-hero-image`; anywhere else set `--mw-parallax-pattern-image` on it.
|
|
|
370
382
|
`--mw-parallax-pattern-depth` is the dial, `4vh` next to a risen hero.
|
|
371
383
|
|
|
372
384
|
Outside a hero the same classes build a standalone band: `mw-parallax`
|
|
373
|
-
(`min-height:
|
|
385
|
+
(`min-height: 60vh`), `mw-parallax-media` as an `<img>` or a div with a
|
|
374
386
|
background image, `mw-parallax-content` for what sits on top, and
|
|
375
387
|
`mw-parallax-dimmed` to turn the picture down and the text light. Drop the
|
|
376
388
|
`mw-container` between section and block and the picture runs full width, with
|
|
@@ -385,7 +397,7 @@ stands still, past it the block runs backwards. The dial belongs on the element
|
|
|
385
397
|
carrying `mw-parallax-slow`, the height on the `mw-parallax` block inside it;
|
|
386
398
|
put the dial on the inner block and it never reaches the animation. Everything
|
|
387
399
|
after it gets the same page background and layer that a pinned block gives its
|
|
388
|
-
siblings.
|
|
400
|
+
siblings, and the section directly after it the same edge shadow.
|
|
389
401
|
|
|
390
402
|
Without scroll timelines, or under `prefers-reduced-motion`, the picture stands
|
|
391
403
|
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 ~
|
|
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 ~
|
|
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 (~
|
|
431
|
-
|
|
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,25 @@ 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.14.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `mw-tooltip-below` - the tooltip under its trigger
|
|
14
|
+
- `mw-tooltip-end` / `mw-tooltip-start` - the tooltip aligned to that edge of its trigger
|
|
15
|
+
|
|
16
|
+
## [5.13.0] - 2026-09-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `fluid()` function, takes scale keys or plain rem/px
|
|
21
|
+
- `touch-context`, `touch-floor` and `hit-area` mixins
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- responsive changes
|
|
26
|
+
- parallax effect with shadow
|
|
27
|
+
|
|
9
28
|
## [5.12.0] - 2026-09-13
|
|
10
29
|
|
|
11
30
|
### Fixed
|