maverick-wave 5.8.0 → 5.11.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 +26 -14
- package/.claude/skills/mw-maverick-wave/examples/angular-services.md +10 -1
- package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +4 -3
- package/.claude/skills/mw-maverick-wave/references/components.md +23 -9
- package/.claude/skills/mw-maverick-wave/references/javascript.md +14 -14
- package/.claude/skills/mw-maverick-wave/references/layout.md +29 -9
- package/.claude/skills/mw-maverick-wave/references/theming.md +48 -25
- package/CHANGELOG.md +23 -0
- package/README.md +41 -24
- package/index.html +356 -20
- package/maverick-wave.min.css +61 -5
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/scripts/verify.js +7 -1
- package/src/js/main.js +27 -7
- package/src/partials/palette-container.html +105 -65
- package/src/partials/parallax-container.html +20 -1
- package/src/partials/preview-container.html +136 -0
- package/src/partials/utilities-container.html +38 -0
- package/src/scss/abstracts/_functions.scss +31 -3
- package/src/scss/abstracts/_mixins.scss +62 -12
- package/src/scss/abstracts/_variables.scss +47 -21
- package/src/scss/base/_base.scss +97 -8
- package/src/scss/components/_badge.scss +1 -1
- package/src/scss/components/_buttons.scss +1 -1
- package/src/scss/components/_cards.scss +23 -14
- package/src/scss/components/_dropdown.scss +58 -1
- package/src/scss/components/_kanban.scss +10 -9
- package/src/scss/components/_lang-switch.scss +1 -1
- package/src/scss/components/_localhost-indicator.scss +1 -1
- package/src/scss/components/_modals.scss +13 -1
- package/src/scss/components/_pricing.scss +1 -1
- package/src/scss/components/_progress.scss +9 -5
- package/src/scss/components/_skeleton.scss +1 -1
- package/src/scss/components/_spinners.scss +5 -5
- package/src/scss/components/_stepper.scss +4 -1
- package/src/scss/components/_tabs.scss +28 -8
- package/src/scss/components/_tiles.scss +41 -16
- package/src/scss/components/_timelines.scss +13 -5
- package/src/scss/layout/_header-reveal.scss +5 -5
- package/src/scss/layout/_main.scss +1 -1
- package/src/scss/layout/_parallax.scss +65 -14
- package/src/scss/utilities/_elevation.scss +23 -0
- package/src/scss/utilities/_reveal.scss +5 -5
- package/src/scss/utilities/_touch-targets.scss +1 -1
- package/src/scss/utilities/_variants.scss +74 -7
|
@@ -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@5.
|
|
47
|
+
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.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@5.
|
|
54
|
+
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.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 -
|
|
@@ -151,9 +151,9 @@ keys at all (negative gap is invalid CSS and is not generated).
|
|
|
151
151
|
`5xl` 3, `6xl` 4.3 rem.
|
|
152
152
|
|
|
153
153
|
**Breakpoints**: `xs` 375, `sm` 576, `md` 768, `lg` 992, `xl` 1200, `2xl` 1400 px.
|
|
154
|
-
Column grids and most components are mobile-first
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
Column grids and most components are mobile-first. The mixins emit range syntax
|
|
155
|
+
(`media-up` is `width >= bp`, `media-down` is `width < bp`), so the two never
|
|
156
|
+
overlap at the breakpoint itself and neither has to stop a fraction short of it.
|
|
157
157
|
|
|
158
158
|
**Overriding.** Everything the framework emits sits in
|
|
159
159
|
`@layer mw.reset, mw.base, mw.forms, mw.components, mw.layout, mw.utilities`.
|
|
@@ -187,16 +187,23 @@ Two shadows per level - a tight contact layer plus a wide ambient one:
|
|
|
187
187
|
`0` is explicitly flat. Never write a `box-shadow` by hand - the twelve one-off
|
|
188
188
|
values that used to exist are exactly what this replaced.
|
|
189
189
|
|
|
190
|
+
**Glow** (`mw-glow`, `mw-glow-{primary,secondary,info,success,warning,danger}`).
|
|
191
|
+
The same two-layer idea without the offset, so the surface reads as giving off
|
|
192
|
+
the light rather than casting a shadow. For the one element on a screen that has
|
|
193
|
+
to be seen first, and for nothing else.
|
|
194
|
+
|
|
190
195
|
**Motion** `--mw-duration-instant|fast|base|slow|slower` = 110/180/300/520/900ms
|
|
191
196
|
plus `--mw-duration-zoom` (650ms, for a large surface actually travelling),
|
|
192
197
|
`--mw-ease-out` (things arriving - the default), `--mw-ease-in-out` (A to B and
|
|
193
198
|
back), `--mw-ease-spring` (a pop). Two ready-made transitions:
|
|
194
199
|
`var(--mw-transition)` for hover and focus states, `var(--mw-transition-fast)`
|
|
195
200
|
for anything that should feel instant under the pointer. Both list their
|
|
196
|
-
properties explicitly rather than saying `all`.
|
|
201
|
+
properties explicitly rather than saying `all`. All six durations are multiplied
|
|
202
|
+
by `--mw-motion-scale` (default 1), the tempo counterpart to
|
|
203
|
+
`--mw-radius-scale`; `prefers-reduced-motion` overrides the result.
|
|
197
204
|
|
|
198
205
|
**Control sizes** `--mw-control-height-sm|base|lg` = 1.875 / 2.125 / 2.375rem (an
|
|
199
|
-
even 30 / 34 / 38px step) and
|
|
206
|
+
even 30 / 34 / 38px step, moved by `mw-density-compact` and `-roomy`) and
|
|
200
207
|
`--mw-control-font-sm|base|lg` = 0.8 / 0.9 / 1rem, shared by `mw-input`,
|
|
201
208
|
`mw-select`, `mw-textarea` and `mw-btn`. A field and the button beside it are
|
|
202
209
|
the same height by construction. Buttons run one font step above the fields.
|
|
@@ -258,13 +265,14 @@ feature frame) ·
|
|
|
258
265
|
`mw-section` (+ `mw-section-intro`) · `mw-page-header` · `mw-grid-*` ·
|
|
259
266
|
`mw-columns-2/3` · `mw-row-split` · `mw-hero` (+ `mw-scroll-hint`, `-end`) ·
|
|
260
267
|
`mw-parallax` (+ `-media`, `-content`, `-dimmed`, `-sticky`, `-rise`,
|
|
261
|
-
`-pattern`) · `mw-footer`
|
|
268
|
+
`-slow`, `-pattern`) · `mw-footer`
|
|
262
269
|
|
|
263
270
|
**Utilities** (`references/layout.md`) `mw-sr-only` / `mw-sr-only-focusable` /
|
|
264
271
|
`mw-skip-link` · `mw-row-split` (+ `center`) · `mw-text-numeric` /
|
|
265
272
|
`mw-text-currency` · `mw-text-truncate` / `mw-text-clamp-2..5` /
|
|
266
273
|
`mw-text-break` / `mw-text-nowrap` · `mw-text-balance` / `mw-text-pretty` /
|
|
267
|
-
`mw-text-eyebrow` / `mw-text-measure` · `mw-elevation-0..5` ·
|
|
274
|
+
`mw-text-eyebrow` / `mw-text-measure` · `mw-elevation-0..5` · `mw-glow` /
|
|
275
|
+
`mw-glow-{primary,secondary,info,success,warning,danger}` ·
|
|
268
276
|
`mw-corner-plain` ·
|
|
269
277
|
`mw-aspect-square|video|wide|portrait|photo` · `mw-d-{sm,md,lg,xl}-*` /
|
|
270
278
|
`mw-hide-mobile` / `mw-hide-desktop` · `mw-overflow-*` / `mw-snap-x` ·
|
|
@@ -342,10 +350,12 @@ feature frame) ·
|
|
|
342
350
|
and the open state come from, and it works without script. The framework JS
|
|
343
351
|
only adds Escape and click-outside. Writing your own div-plus-click loses all
|
|
344
352
|
of it.
|
|
345
|
-
17. **An open dropdown
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
353
|
+
17. **An open dropdown can be clipped by anything that hides its overflow.**
|
|
354
|
+
Where the browser supports anchor positioning the menu is `position: fixed`
|
|
355
|
+
and anchored to its trigger, so it escapes the clip on its own. Everywhere
|
|
356
|
+
else it is absolutely positioned and cut off. Either way the framework's own
|
|
357
|
+
containers (panel, card, tile, modal body, responsive table) lift the clip
|
|
358
|
+
while a menu is open. On your own container it is one line:
|
|
349
359
|
`:has(.mw-dropdown[open]) { overflow: visible }`.
|
|
350
360
|
18. **Never write `box-shadow` by hand.** Use `var(--mw-elevation-1..5)` or the
|
|
351
361
|
`mw-elevation-*` class. A hand-rolled shadow is the wrong colour in one of
|
|
@@ -414,4 +424,6 @@ feature frame) ·
|
|
|
414
424
|
reaches that content as a _sibling_. Put the class on the hero itself and
|
|
415
425
|
nothing pins and nothing covers - no error, no effect, and the picture shows
|
|
416
426
|
through every transparent section below. `mw-parallax` and the
|
|
417
|
-
`mw-parallax-media` child stay where the image is: on the container.
|
|
427
|
+
`mw-parallax-media` child stay where the image is: on the container. Same
|
|
428
|
+
split for `mw-parallax-slow`, and `--mw-parallax-slow-travel` goes with the
|
|
429
|
+
class on the section - on the inner block it never reaches the animation.
|
|
@@ -18,7 +18,13 @@ const STORAGE_KEY = 'mw-theme';
|
|
|
18
18
|
|
|
19
19
|
@Injectable({ providedIn: 'root' })
|
|
20
20
|
export class ThemeService {
|
|
21
|
-
|
|
21
|
+
// With nothing stored the stylesheet already follows the OS, so the initial
|
|
22
|
+
// value only has to agree with what is on screen
|
|
23
|
+
readonly light = signal(
|
|
24
|
+
localStorage.getItem(STORAGE_KEY)
|
|
25
|
+
? localStorage.getItem(STORAGE_KEY) === 'light'
|
|
26
|
+
: matchMedia('(prefers-color-scheme: light)').matches
|
|
27
|
+
);
|
|
22
28
|
|
|
23
29
|
constructor() {
|
|
24
30
|
effect(() => {
|
|
@@ -27,6 +33,9 @@ export class ThemeService {
|
|
|
27
33
|
|
|
28
34
|
root.classList.add('mw-theme-switching');
|
|
29
35
|
document.body.classList.toggle('mw-theme-light', light);
|
|
36
|
+
// the explicit counterpart - without it a dark choice on a light machine
|
|
37
|
+
// falls back to the OS preference
|
|
38
|
+
document.body.classList.toggle('mw-theme-dark', !light);
|
|
30
39
|
void root.offsetHeight; // commit the colours with transitions off
|
|
31
40
|
root.classList.remove('mw-theme-switching');
|
|
32
41
|
|
|
@@ -20,7 +20,7 @@ accordion FAQ.
|
|
|
20
20
|
|
|
21
21
|
<link
|
|
22
22
|
rel="stylesheet"
|
|
23
|
-
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.
|
|
23
|
+
href="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.css"
|
|
24
24
|
/>
|
|
25
25
|
<link
|
|
26
26
|
rel="stylesheet"
|
|
@@ -487,7 +487,7 @@ accordion FAQ.
|
|
|
487
487
|
<div class="mw-modal-backdrop" onclick="closeModal('demo')"></div>
|
|
488
488
|
</div>
|
|
489
489
|
|
|
490
|
-
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.
|
|
490
|
+
<script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.11.0/maverick-wave.min.js"></script>
|
|
491
491
|
<script>
|
|
492
492
|
// The only thing the shipped script does not cover: opening a modal.
|
|
493
493
|
// Closing works through .mw-modal-close, the backdrop is wired above.
|
|
@@ -505,7 +505,8 @@ accordion FAQ.
|
|
|
505
505
|
## Notes
|
|
506
506
|
|
|
507
507
|
- The theme toggle needs no code - the script persists the choice under
|
|
508
|
-
`localStorage['mw-theme']`, toggles `mw-theme-light`
|
|
508
|
+
`localStorage['mw-theme']`, toggles `mw-theme-light` / `mw-theme-dark` on
|
|
509
|
+
`<body>` and wraps the
|
|
509
510
|
flip in `mw-theme-switching` so it starts no transitions.
|
|
510
511
|
- The scroll spy sets `mw-active` on the `mw-navbar-link` whose `href` matches
|
|
511
512
|
the `section[id]` currently in view - the `<section id="…">` elements are
|
|
@@ -224,13 +224,21 @@ picker.
|
|
|
224
224
|
means it sits right, not left. Everything is vertically centered, so plain text
|
|
225
225
|
lines up with a button next to it. Actions that no longer fit next to each
|
|
226
226
|
other wrap.
|
|
227
|
-
- The
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
- The card measures **itself**, not the window - a card in a three-column grid
|
|
228
|
+
is just as narrow on a 1200px desktop as it is on a phone, and looks the same
|
|
229
|
+
in both. Every card declares `container: mw-card / inline-size` and takes
|
|
230
|
+
`width: 100%`, so it fills its slot instead of sizing to its content. Give a
|
|
231
|
+
card an explicit width if you need it to hug its content.
|
|
232
|
+
- What steps on the card's own width: the footer stacks to full-width children
|
|
233
|
+
below 360px, and the title, the subtitle and the body padding step up at
|
|
234
|
+
420px. The image height is the one that still follows the window, because it
|
|
235
|
+
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
|
|
239
|
+
clips its overflow, and being a container makes it the containing block for
|
|
240
|
+
`position: fixed`, so a `data-tooltip` **inside** a tile is cut off at its
|
|
241
|
+
edge. Cards do not clip and are unaffected.
|
|
234
242
|
- `mw-card-badge` (top right corner) and `mw-card-ribbon` (diagonal banner) are
|
|
235
243
|
absolutely positioned overlays; colour them with `mw-card-addon-primary`,
|
|
236
244
|
`-secondary`, `-success`, `-warning`, `-danger`, `-info`.
|
|
@@ -813,6 +821,10 @@ tables you know overflow, leave it off the ones that fit.
|
|
|
813
821
|
A board is a grid of equally wide lanes; a ticket is a plain `mw-card` with
|
|
814
822
|
`mw-kanban-card` on top. Everything except the counters and the composer is CSS.
|
|
815
823
|
|
|
824
|
+
The board puts as many lanes in a row as fit at 260px each and wraps the rest
|
|
825
|
+
onto the next. It measures its own width for that, so a board inside a panel or
|
|
826
|
+
a half-width column wraps on a desktop too.
|
|
827
|
+
|
|
816
828
|
```html
|
|
817
829
|
<div class="mw-kanban" style="--mw-kanban-column-min-height: 390px">
|
|
818
830
|
<div class="mw-kanban-column">
|
|
@@ -1207,8 +1219,10 @@ Parts: `mw-dropdown-menu`, `-item`, `-item-danger`, `-divider`, `-label`,
|
|
|
1207
1219
|
picks rather than acts. Icons inside items keep one column, so labels line up
|
|
1208
1220
|
whether or not every item has one.
|
|
1209
1221
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1222
|
+
Where the browser supports anchor positioning the menu is `position: fixed`,
|
|
1223
|
+
anchored to its trigger and flipped into whichever side has room, so it escapes
|
|
1224
|
+
a clipping ancestor by itself. Everywhere else it is absolutely positioned and
|
|
1225
|
+
is clipped by any ancestor that hides its overflow. The framework's own containers lift that clip while a menu is open; on
|
|
1212
1226
|
your own it is `:has(.mw-dropdown[open]) { overflow: visible }`.
|
|
1213
1227
|
|
|
1214
1228
|
On a coarse pointer the rows grow to 2.75rem and the menu takes at least the
|
|
@@ -33,7 +33,7 @@ classes are the entire contract.
|
|
|
33
33
|
| Mobile nav | Toggles `open` on `mw-menu-btn` and `mw-navbar`, writes `aria-expanded` when the button is a `<button>`, closes on anchor click and on Escape (focus returns to the button) | One signal, bound to both; reset it on navigation end |
|
|
34
34
|
| Scroll spy | Sets `mw-active` on `mw-navbar-link` from the scroll position | Router-based: `routerLinkActive="mw-active"` |
|
|
35
35
|
| Anchor scrolling | Intercepts `a[href^="#"]` and runs its own eased scroll - duration scales with distance, capped at 1.4s, cancelled by wheel or touch. Lands on `scroll-padding-top`, moves focus to the target, writes the hash with `replaceState`, and measures a sticky target unpinned | The router; for in-page anchors `scrollIntoView({ behavior: 'smooth' })` or your own animation |
|
|
36
|
-
| 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
|
|
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
37
|
| Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
|
|
38
38
|
| Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
|
|
39
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 |
|
|
@@ -47,23 +47,23 @@ classes are the entire contract.
|
|
|
47
47
|
| Color swatches | Showcase-only (prints computed hex values); exposes `window.mwRefreshColorSwatches` to read them again after a root colour changed | Not needed |
|
|
48
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
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 |
|
|
51
|
-
| Header reveal |
|
|
52
|
-
| Parallax |
|
|
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 |
|
|
53
53
|
|
|
54
54
|
## Scroll-driven animations
|
|
55
55
|
|
|
56
56
|
Four things ride the browser's own scroll timeline: `mw-reveal`,
|
|
57
|
-
`mw-header-reveal`, `mw-parallax` and the `mw-progress-fill` scrub.
|
|
58
|
-
Edge
|
|
59
|
-
|
|
60
|
-
`CSS.supports('animation-timeline', ...)` first and does nothing
|
|
61
|
-
browser has it.
|
|
62
|
-
|
|
63
|
-
Without the script
|
|
64
|
-
place, the picture holds still. The exception is `mw-header-reveal`,
|
|
65
|
-
bar then sits over the hero from the first paint - a layout
|
|
66
|
-
missing effect.
|
|
57
|
+
`mw-header-reveal`, `mw-parallax` and the `mw-progress-fill` scrub. Chrome and
|
|
58
|
+
Edge have had timelines since 115, Firefox since 158 and Safari since 26, and
|
|
59
|
+
there they need no script at all. Older versions get the shipped JS instead.
|
|
60
|
+
Each one checks `CSS.supports('animation-timeline', ...)` first and does nothing
|
|
61
|
+
where the browser has it.
|
|
62
|
+
|
|
63
|
+
Without the script an older browser loses the motion and nothing else: cards
|
|
64
|
+
stand in place, the picture holds still. The exception is `mw-header-reveal`,
|
|
65
|
+
where the bar then sits over the hero from the first paint - a layout
|
|
66
|
+
difference, not a missing effect.
|
|
67
67
|
|
|
68
68
|
## Modals and progress bars
|
|
69
69
|
|
|
@@ -183,8 +183,8 @@ as the header's first child under the same condition.
|
|
|
183
183
|
`data-tooltip="..."` is a global attribute hook, not a class - it works on any
|
|
184
184
|
element and shows a tooltip above it on hover or keyboard focus. It is pure CSS
|
|
185
185
|
(a pseudo element on the trigger), which also means anything that clips its
|
|
186
|
-
overflow cuts it off: a scroll container, or a card carrying a ribbon -
|
|
187
|
-
cards do not clip. Close to the screen edge the bubble can run out of the
|
|
186
|
+
overflow cuts it off: a scroll container, a tile, or a card carrying a ribbon -
|
|
187
|
+
plain cards do not clip. Close to the screen edge the bubble can run out of the
|
|
188
188
|
viewport, so keep long tooltips off the outermost elements.
|
|
189
189
|
|
|
190
190
|
## Sections
|
|
@@ -323,10 +323,10 @@ is taken on a phone. The bobbing stops under `prefers-reduced-motion`.
|
|
|
323
323
|
**Parallax** - `mw-parallax` on the container plus a `mw-parallax-media` child
|
|
324
324
|
moves the picture into its own layer. The container drops its own background and
|
|
325
325
|
the layer reads `--mw-hero-background`, so the image stays configured in one
|
|
326
|
-
place.
|
|
327
|
-
`background-attachment: fixed`, which iOS ignores.
|
|
328
|
-
there the shipped JS moves the layers instead, and without it the
|
|
329
|
-
sits still (`javascript.md`).
|
|
326
|
+
place. Every mode runs on the browser's scroll timeline, and on no
|
|
327
|
+
`background-attachment: fixed`, which iOS ignores. Older Safari and Firefox have
|
|
328
|
+
no timelines; there the shipped JS moves the layers instead, and without it the
|
|
329
|
+
picture simply sits still (`javascript.md`).
|
|
330
330
|
|
|
331
331
|
```html
|
|
332
332
|
<header class="mw-header mw-header-reveal">...</header>
|
|
@@ -356,7 +356,9 @@ through the viewport, so its `view()` timeline stands still with it.
|
|
|
356
356
|
`mw-parallax-rise` puts it at `12vh` and runs the layer on the document's own
|
|
357
357
|
scroll instead - the picture lifts, the text stays put. That works out to the
|
|
358
358
|
same travel per scrolled pixel as a drifting band at `20vh`, which is measured
|
|
359
|
-
over a much longer range.
|
|
359
|
+
over a much longer range. It is a hero move only: the range is the first screen
|
|
360
|
+
of document scroll, so further down a page the picture holds still. Several
|
|
361
|
+
pinned blocks on one page are fine, each takes over from the one before.
|
|
360
362
|
|
|
361
363
|
A second `mw-parallax-media` carrying `mw-parallax-pattern` puts a pattern over
|
|
362
364
|
the picture. It travels a third of the distance the layer below it does, and
|
|
@@ -370,7 +372,20 @@ reads `$mw-hero-image`; anywhere else set `--mw-parallax-pattern-image` on it.
|
|
|
370
372
|
Outside a hero the same classes build a standalone band: `mw-parallax`
|
|
371
373
|
(`min-height: 42vh`), `mw-parallax-media` as an `<img>` or a div with a
|
|
372
374
|
background image, `mw-parallax-content` for what sits on top, and
|
|
373
|
-
`mw-parallax-dimmed` to turn the picture down and the text light.
|
|
375
|
+
`mw-parallax-dimmed` to turn the picture down and the text light. Drop the
|
|
376
|
+
`mw-container` between section and block and the picture runs full width, with
|
|
377
|
+
a `mw-container` inside `mw-parallax-content` around the text instead.
|
|
378
|
+
|
|
379
|
+
`mw-parallax-slow` is the third mode and the one for a band mid-page: the block
|
|
380
|
+
keeps its place in the flow and travels slower than the page, so picture and
|
|
381
|
+
text are still moving while the next section closes in from below. Pinning is
|
|
382
|
+
the same move with the tempo at zero. `--mw-parallax-slow-travel` (`45vh`) reads
|
|
383
|
+
against the block's own height - half the height is half speed, the full height
|
|
384
|
+
stands still, past it the block runs backwards. The dial belongs on the element
|
|
385
|
+
carrying `mw-parallax-slow`, the height on the `mw-parallax` block inside it;
|
|
386
|
+
put the dial on the inner block and it never reaches the animation. Everything
|
|
387
|
+
after it gets the same page background and layer that a pinned block gives its
|
|
388
|
+
siblings.
|
|
374
389
|
|
|
375
390
|
Without scroll timelines, or under `prefers-reduced-motion`, the picture stands
|
|
376
391
|
still and nothing else changes.
|
|
@@ -444,7 +459,7 @@ automatically.
|
|
|
444
459
|
`mw-d-inline-block`, `mw-d-grid`, `mw-d-none`, `mw-d-contents`.
|
|
445
460
|
|
|
446
461
|
**Responsive display** - `mw-d-{sm|md|lg|xl}-{none|block|flex|inline-flex|grid|inline-block}`.
|
|
447
|
-
Mobile-first
|
|
462
|
+
Mobile-first like everything else here: `mw-d-md-flex` means
|
|
448
463
|
"flex from the md breakpoint up", and the unprefixed class beside it is what
|
|
449
464
|
applies below that. The pair `mw-d-none mw-d-md-flex` is hidden on a phone and a
|
|
450
465
|
row from a tablet on. `mw-hide-mobile` (gone below md) and `mw-hide-desktop`
|
|
@@ -469,6 +484,11 @@ with it; `mw-text-capitalize` is the plain transform.
|
|
|
469
484
|
**Elevation** - `mw-elevation-0` through `-5`, the same ramp every component
|
|
470
485
|
uses. See the scale table in `SKILL.md`. Never hand-roll a `box-shadow`.
|
|
471
486
|
|
|
487
|
+
**Glow** - `mw-glow` plus
|
|
488
|
+
`mw-glow-{primary,secondary,info,success,warning,danger}`. Elevation without the
|
|
489
|
+
offset, so the surface gives off the light instead of casting it. Once per
|
|
490
|
+
screen at most.
|
|
491
|
+
|
|
472
492
|
**Scroll entrance** - `mw-reveal` lets a block rise briefly as it scrolls into
|
|
473
493
|
view, driven by the browser's scroll timeline (`animation-timeline: view()`).
|
|
474
494
|
The block stays hidden while it is still below the bottom edge and has arrived
|
|
@@ -135,8 +135,19 @@ Two rules that prevent most colour bugs:
|
|
|
135
135
|
|
|
136
136
|
## Light & dark
|
|
137
137
|
|
|
138
|
-
-
|
|
139
|
-
|
|
138
|
+
- Every theme token is declared once on `:root` as `light-dark(light, dark)`.
|
|
139
|
+
Which half applies is decided by `color-scheme` on the element that **uses**
|
|
140
|
+
the token, and `:root` carries `color-scheme: light dark` - so a page follows
|
|
141
|
+
the OS until someone chooses otherwise.
|
|
142
|
+
- `mw-theme-light` and `mw-theme-dark` on `<body>` are that choice. They set
|
|
143
|
+
little more than `color-scheme`, and every token follows. `mw-theme-dark` is
|
|
144
|
+
not redundant: without it, a reader who picks dark on a machine set to light
|
|
145
|
+
would be pulled straight back to the OS preference.
|
|
146
|
+
- Chrome 119-122 and Safari 16.4-17.4 are in the browserslist but predate
|
|
147
|
+
`light-dark()`. `base/_base.scss` keeps the old route for them behind
|
|
148
|
+
`@supports` - dark on `:root`, light by class, no OS tracking.
|
|
149
|
+
- `--mw-hero-image-filter` is the one theme value that is not a colour and so
|
|
150
|
+
cannot ride along; it follows `prefers-color-scheme` instead.
|
|
140
151
|
- Card, footer and border are derived from the page background by scaling its
|
|
141
152
|
OKLch lightness and chroma by one factor. A card steps **away from the text
|
|
142
153
|
colour** - darker than the page in the dark theme, lighter in the light one.
|
|
@@ -176,7 +187,9 @@ Two rules that prevent most colour bugs:
|
|
|
176
187
|
or move it down with the bar.
|
|
177
188
|
- Persisting the choice, the toggle UI and the initial class are the
|
|
178
189
|
application's job in a SPA (`examples/angular-services.md`). The shipped JS
|
|
179
|
-
does it for static pages using `localStorage` under the key `mw-theme`.
|
|
190
|
+
does it for static pages using `localStorage` under the key `mw-theme`. With
|
|
191
|
+
nothing stored it takes the OS preference and keeps tracking it live, so
|
|
192
|
+
changing the system theme moves the page until the reader picks a side.
|
|
180
193
|
- **Suppress transitions while the class flips.** Colour changes on nearly every
|
|
181
194
|
element at once, and the shared `--mw-transition` turns that into thousands of
|
|
182
195
|
concurrent animations - on a documentation-sized page it is seconds of blocked
|
|
@@ -189,29 +202,35 @@ const root = document.documentElement;
|
|
|
189
202
|
|
|
190
203
|
root.classList.add('mw-theme-switching');
|
|
191
204
|
document.body.classList.toggle('mw-theme-light', isLight);
|
|
205
|
+
document.body.classList.toggle('mw-theme-dark', !isLight);
|
|
192
206
|
void root.offsetHeight; // commit the new colours with transitions off
|
|
193
207
|
root.classList.remove('mw-theme-switching');
|
|
194
208
|
```
|
|
195
209
|
|
|
196
210
|
## Site-wide variants
|
|
197
211
|
|
|
198
|
-
|
|
199
|
-
without a rebuild. They stack, and none of them need a class per element.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
| `mw-
|
|
206
|
-
| `mw-
|
|
207
|
-
| `mw-
|
|
208
|
-
| `mw-
|
|
209
|
-
| `mw-
|
|
210
|
-
| `mw-
|
|
211
|
-
| `mw-
|
|
212
|
-
| `mw-
|
|
213
|
-
| `mw-
|
|
214
|
-
| `mw-btn-
|
|
212
|
+
Fifteen classes on `<html>` and six custom properties retune the whole look
|
|
213
|
+
without a rebuild. They stack, and none of them need a class per element. The
|
|
214
|
+
showcase at https://maverick-wave.m1well.com has a picker for the set, and its
|
|
215
|
+
URL carries the choice - a link is how a look gets agreed on before it is built.
|
|
216
|
+
|
|
217
|
+
| Class | Effect |
|
|
218
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
219
|
+
| `mw-corners-even` | Drops the surface signature - every card, panel and modal becomes an evenly rounded `radius('md')` box |
|
|
220
|
+
| `mw-accent-single` | `--mw-secondary-color` follows the primary; `mw-btn-secondary` turns outline so the two stay apart |
|
|
221
|
+
| `mw-shadows-flat` | Elevation 1-3 to `none`. The dropdown (4) and the modal (5) keep their shadow |
|
|
222
|
+
| `mw-surfaces-flush` | Card, panel and footer background drop to the page colour and the border is redrawn from the ink, because the old one is a neighbouring shade of a surface that is now the page |
|
|
223
|
+
| `mw-hover-static` | No hover travels - lifts, image zooms and slides go. Colour and border still respond |
|
|
224
|
+
| `mw-scroll-static` | No scroll entrance - `mw-reveal` and `mw-reveal-stagger` blocks sit where they land |
|
|
225
|
+
| `mw-sections-plain` | The diagonal hatch behind `mw-section-alternate` collapses into the page colour |
|
|
226
|
+
| `mw-headings-caps` | `h1`-`h3` in capitals with 0.045em tracking |
|
|
227
|
+
| `mw-links-underline` | `mw-link`, `mw-link-muted` and `mw-btn-link` carry their underline at rest. The hover signal moves to the stroke, which thickens to 3px - `mw-link` has no colour change of its own |
|
|
228
|
+
| `mw-btn-pill` | `mw-btn` fully rounded. Form fields keep their own radius |
|
|
229
|
+
| `mw-btn-square` | `mw-btn` to `border-radius: 0`. The other end of the same axis, and unlike `--mw-radius-scale: 0` it leaves the rest of the page rounded |
|
|
230
|
+
| `mw-btn-glass` | Filled buttons become tinted glass: translucent wash, vertical ramp, specular top edge. No `backdrop-filter` - it would trap a fixed-position tooltip inside the button |
|
|
231
|
+
| `mw-btn-tactile` | The four solid variants stand on a 3px edge mixed toward black and travel its full height on `:active`. Outline, ghost and link have no fill to darken and are untouched |
|
|
232
|
+
| `mw-density-compact` | Card and panel padding and the three control heights drop one step down the spacing scale. Type is untouched - `--mw-root-font-size` is the switch that scales everything together |
|
|
233
|
+
| `mw-density-roomy` | The same one step up |
|
|
215
234
|
|
|
216
235
|
| Property | Effect |
|
|
217
236
|
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -220,20 +239,24 @@ without a rebuild. They stack, and none of them need a class per element.
|
|
|
220
239
|
| `--mw-font-family-heading` | Headline typeface; body copy is untouched |
|
|
221
240
|
| `--mw-container-width` | Where the content stops growing |
|
|
222
241
|
| `--mw-section-padding-block` | Air above and below each section |
|
|
242
|
+
| `--mw-motion-scale` | Multiplies all six duration tokens - `0.6` brisk, `1.6` relaxed. `prefers-reduced-motion` still overrides it |
|
|
223
243
|
|
|
224
244
|
**Writing your own.** A variant that retunes a _theme-bound_ token - anything in
|
|
225
|
-
the dark/light maps
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Target both:
|
|
245
|
+
the dark/light maps - cannot be written on `:root` alone: `mw-theme-light`
|
|
246
|
+
re-declares those on `<body>`, which shadows the root value for the entire
|
|
247
|
+
subtree and the switch does nothing in light mode. Target both:
|
|
229
248
|
|
|
230
249
|
```scss
|
|
231
250
|
:root.my-variant,
|
|
232
251
|
:root.my-variant .mw-theme-light {
|
|
233
|
-
--mw-
|
|
252
|
+
--mw-card-background: var(--mw-page-background);
|
|
234
253
|
}
|
|
235
254
|
```
|
|
236
255
|
|
|
256
|
+
`--mw-elevation-1..5` are the exception and need only `:root`: the ramp is one
|
|
257
|
+
set for both themes and reaches the theme through `--mw-shadow-near/-far`, so
|
|
258
|
+
there is no second declaration further down the tree to shadow it.
|
|
259
|
+
|
|
237
260
|
The same trap the other way round: never copy a theme alias _into_ a token on
|
|
238
261
|
`:root` (`--mw-x: var(--mw-page-background)`), because up there it still holds
|
|
239
262
|
the dark value. Write that rule on the element instead.
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ 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.11.0] - 2026-09-13
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- showcase parallax effect
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- small fixes
|
|
18
|
+
|
|
19
|
+
## [5.10.0] - 2026-09-13
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- technical issues
|
|
24
|
+
|
|
25
|
+
## [5.9.0] - 2026-09-13
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- finalized showcase variants
|
|
30
|
+
- changed some little glow up
|
|
31
|
+
|
|
9
32
|
## [5.8.0] - 2026-09-12
|
|
10
33
|
|
|
11
34
|
### Added
|