maverick-wave 4.13.0 → 4.15.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.
@@ -155,6 +155,14 @@ keys at all (negative gap is invalid CSS and is not generated).
155
155
 
156
156
  **Radius** (`mw-radius-none|xs|sm|md|lg|xl|2xl|full`): 0, 2, 5, 10, 15, 20, 30 px, 50%.
157
157
 
158
+ **The surface signature.** Every panel-like component - card, panel, modal,
159
+ accordion, tile, calendar, pagination, login box - shares one silhouette: sharp
160
+ 5px corners on the top-left/bottom-right diagonal, round 20px ones on the other,
161
+ plus a 2px **corner accent** in the primary tone (`--mw-corner-accent`) sitting
162
+ on the two round corners. It is not a class and not opt-in; the components carry
163
+ it. `mw-corner-plain` drops the accent on a box too small to hold it - see
164
+ `references/layout.md`.
165
+
158
166
  **Elevation** (`mw-elevation-0` … `-5`, and `var(--mw-elevation-N)` inside SCSS).
159
167
  Two shadows per level - a tight contact layer plus a wide ambient one:
160
168
  `1` resting (inputs, tags), `2` raised (cards, panels at rest), `3` floating
@@ -194,7 +202,10 @@ Everything below is documented in `references/components.md` unless marked other
194
202
  **Containers** `mw-card` (+ `simple`, `lg`, `xl`, `stack`, badge, ribbon,
195
203
  feature frame) ·
196
204
  `mw-panel` · `mw-tile` · `mw-accordion` · `mw-tabs` · `mw-modal` ·
197
- `mw-item-list` family
205
+ `mw-item-list` family · `mw-offer` (+ `-head`, `-name`)
206
+
207
+ **Pricing** `mw-price` (+ `-amount`, `-fraction`, `-currency`, `-original`,
208
+ `-period`, `-word`, `-note`, `sm`/`lg`, `stacked`, `center`, `plain`)
198
209
 
199
210
  **Data & status** `mw-table` (+ `subtle`, `sticky-head`, `cards`, `compact`,
200
211
  `hover`, responsive wrappers) · `mw-kanban` (+ `plain`, `compact`) ·
@@ -230,6 +241,7 @@ feature frame) ·
230
241
  `mw-text-currency` · `mw-text-truncate` / `mw-text-clamp-2..5` /
231
242
  `mw-text-break` / `mw-text-nowrap` · `mw-text-balance` / `mw-text-pretty` /
232
243
  `mw-text-eyebrow` / `mw-text-measure` · `mw-elevation-0..5` ·
244
+ `mw-corner-plain` ·
233
245
  `mw-aspect-square|video|wide|portrait|photo` · `mw-d-{sm,md,lg,xl}-*` /
234
246
  `mw-hide-mobile` / `mw-hide-desktop` · `mw-overflow-*` / `mw-snap-x` ·
235
247
  spacing, flex, display, text
@@ -299,6 +299,164 @@ tech-stack grids:
299
299
  </div>
300
300
  ```
301
301
 
302
+ ## Pricing
303
+
304
+ Two pieces that are almost always needed together: `mw-price`, the figure on its
305
+ own, and `mw-offer`, the card it is sold from.
306
+
307
+ ### Price
308
+
309
+ One baseline row - what it cost before, what it costs now, the unit, a discount
310
+ tag:
311
+
312
+ ```html
313
+ <p class="mw-price">
314
+ <span class="mw-price-original">
315
+ <span class="mw-sr-only">Regular price:</span>159 Euro
316
+ </span>
317
+ <span class="mw-sr-only">Now:</span>
318
+ <span class="mw-price-amount"
319
+ >129<span class="mw-price-fraction">,90</span></span
320
+ >
321
+ <span class="mw-price-currency">Euro</span>
322
+ <span class="mw-price-period">/ month</span>
323
+ <span class="mw-tag mw-tag-secondary">-20%</span>
324
+ </p>
325
+ <p class="mw-price-note">Billed yearly, cancel any time.</p>
326
+ ```
327
+
328
+ - Everything in the row is sized in `em` off `--mw-price-size`, so
329
+ `mw-price-sm` (1.5rem) and `mw-price-lg` (3rem) move the whole row at once -
330
+ cents, unit and struck original included. Set the token yourself for any other
331
+ size.
332
+ - `mw-price-original` is muted **and** struck. Muted alone reads as a footnote;
333
+ the line is what says this is no longer the price. The rule is drawn
334
+ explicitly, because the browser default hairline disappears at this size
335
+ against a muted tone.
336
+ - `mw-price-fraction` goes **inside** the amount and sits one step down on the
337
+ baseline, not raised to the cap height - raised cents are a discounter's idiom
338
+ and make a service price read cheaper than it is.
339
+ - `mw-price-currency` and `mw-price-period` stop shrinking at 0.8rem, and
340
+ `mw-price-original` at 0.9rem. Below `mw-price-sm` the `em` chain would put
341
+ them under 12px, and that is the size most offer cards run at.
342
+ - `mw-price-word` for "On request" or "Free" - bold, so it holds the same slot in
343
+ a row of cards, but set well below a figure. Its line box is scaled back up to
344
+ the amount's, so a word and a number in neighbouring cards land on one line.
345
+ - `mw-price-note` is a **sibling** of the row, not a child - inside it, it would
346
+ inherit the price size and join the baseline.
347
+ - A `mw-tag` or `mw-badge` placed directly in the row is centred against the
348
+ digits rather than hung off their baseline.
349
+
350
+ | Class | What it does |
351
+ | ------------------ | --------------------------------------------- |
352
+ | `mw-price-sm/lg` | Retunes `--mw-price-size`, everything follows |
353
+ | `mw-price-stacked` | Old price on its own line above the new one |
354
+ | `mw-price-center` | Centres the row |
355
+ | `mw-price-plain` | Body colour instead of the brand |
356
+
357
+ A struck-through price is a visual convention a screen reader does not pass on -
358
+ `<s>` is announced by almost none of them. Wherever an old and a new price stand
359
+ next to each other, name them with `mw-sr-only`, as above. Without it the reader
360
+ hears two prices and no way to tell which one is charged.
361
+
362
+ ### Offer card
363
+
364
+ `mw-offer` goes on a `mw-card` and does the one thing the card cannot: it pushes
365
+ the price to the bottom of the body, so the prices in a row line up even though
366
+ the feature lists have different lengths.
367
+
368
+ ```html
369
+ <article class="mw-card mw-offer">
370
+ <div class="mw-card-body">
371
+ <h3 class="mw-card-title">Studio</h3>
372
+ <hr class="mw-card-title-divider" />
373
+ <p class="mw-card-text">Description.</p>
374
+ <ul class="mw-list mw-list-check">
375
+ <li>10 projects</li>
376
+ </ul>
377
+ <p class="mw-price mw-price-sm">…</p>
378
+ <p class="mw-price-note">Per month, billed yearly.</p>
379
+ <div class="mw-card-footer">
380
+ <button class="mw-btn mw-btn-primary">Choose</button>
381
+ </div>
382
+ </div>
383
+ </article>
384
+ ```
385
+
386
+ - Bottom-aligned, note included: a card that carries a `mw-price-note` and one
387
+ that does not will **not** have their prices on the same line. Give every card
388
+ in a row a note, or none of them.
389
+ - To lift one plan out of the row, wrap it in `mw-card-feature` - the frame is
390
+ the card's, not the offer's.
391
+ - A price dropped into a `mw-card-footer` instead needs no modifier: it lines up
392
+ with the button beside it on its own, and `mw-price-sm` keeps it from
393
+ outweighing the action. That is the layout for a dated event, where the price
394
+ is a detail rather than the offer.
395
+
396
+ **Head band** - name and price in a solid bar at the top, for when the price is
397
+ the thing being compared and the feature list is only its justification:
398
+
399
+ ```html
400
+ <article class="mw-card mw-offer mw-card-addon-secondary">
401
+ <div class="mw-offer-head">
402
+ <p class="mw-offer-name">Studio</p>
403
+ <p class="mw-price mw-price-sm">…</p>
404
+ </div>
405
+ <div class="mw-card-body">…</div>
406
+ </article>
407
+ ```
408
+
409
+ The band reads `mw-card-addon-*` for its colour - the same set the card badge and
410
+ the ribbon use, primary without one. The price inherits the band's ink, and the
411
+ card drops its top corner arc, which the band already owns.
412
+
413
+ ### Billing cycle
414
+
415
+ A subscription sold on several terms, where the longer one is cheaper. The
416
+ switch is a `mw-segmented` **above** the grid, not one per card - three cards
417
+ with three switches are three states the visitor has to keep in their head.
418
+
419
+ ```html
420
+ <div
421
+ class="mw-segmented mw-segmented-auto"
422
+ role="group"
423
+ aria-label="Billing cycle"
424
+ >
425
+ <button type="button" class="mw-segmented-item mw-active" aria-pressed="true">
426
+ Monthly
427
+ </button>
428
+ <button type="button" class="mw-segmented-item" aria-pressed="false">
429
+ 6 months <span class="mw-tag mw-tag-secondary">-10%</span>
430
+ </button>
431
+ <button type="button" class="mw-segmented-item" aria-pressed="false">
432
+ Yearly <span class="mw-tag mw-tag-secondary">-20%</span>
433
+ </button>
434
+ </div>
435
+ ```
436
+
437
+ - The headline figure stays **per month** on every term. "288 Euro" next to
438
+ "30 Euro" is not a comparison a reader can make in their head; the amount that
439
+ actually leaves the account belongs in the `mw-price-note` below
440
+ ("288 Euro charged once for the year, cancel monthly").
441
+ - The full monthly rate goes in `mw-price-original`, the saving in a `mw-tag`
442
+ next to it. Both carry `hidden` on the monthly term - the framework resets
443
+ `[hidden]` to `display: none !important`, so it beats a component's own
444
+ `display` and an `el.hidden = true` or an Angular `[hidden]` binding works on
445
+ a `mw-tag` too.
446
+ - Wrap the price in `aria-live="polite"` and put `aria-pressed` on the switch
447
+ items. The figure changes without the page moving, which a screen reader
448
+ otherwise never hears.
449
+ - Drop trailing zero cents: "24 Euro", not "24,00 Euro". `mw-price-amount` sets
450
+ tabular figures, so the row does not jump when the term changes.
451
+ - A tag inside the **active** item drops its colour and outlines itself in the
452
+ item's ink - the colour variants mix their background from the page, and an
453
+ orange pill on the blue fill comes out purple. Inactive items keep the colour,
454
+ which is what makes a visitor click them.
455
+
456
+ The switching itself is the application's job - the framework ships the look,
457
+ not the arithmetic. The showcase wires it in a few lines at the bottom of
458
+ `index.html`.
459
+
302
460
  ## Panels
303
461
 
304
462
  A bordered box with a header rule - the "titled section" of an application.
@@ -340,6 +340,16 @@ with it; `mw-text-capitalize` is the plain transform.
340
340
  **Elevation** - `mw-elevation-0` through `-5`, the same ramp every component
341
341
  uses. See the scale table in `SKILL.md`. Never hand-roll a `box-shadow`.
342
342
 
343
+ **Corner accent** - `mw-corner-plain` drops the accent arc that sits on the
344
+ two round corners of every card, panel, modal, tile, accordion, calendar,
345
+ pagination and login box. Everything else stays: the radii, the border, the
346
+ shadow, the hover. It is for a box too small to carry the mark - the arc reaches
347
+ about 34px in from its corner, so under roughly 90px the two of them take up
348
+ most of the outline and the pair reads as a frame instead of as a detail. A
349
+ chip-sized `mw-card-simple`, a swatch, a marker in a layout demo. A card with
350
+ content in it never needs this, and it is not the way to switch the signature
351
+ off across a project - restyle `--mw-corner-accent` for that.
352
+
343
353
  **Aspect ratio** - `mw-aspect-square|video|wide|portrait|photo`. Reserves the
344
354
  box before the image inside it has loaded, so the page does not reflow when the
345
355
  picture arrives. The child fills the box and crops rather than stretching.
@@ -46,49 +46,50 @@ Safari 16.4+, Firefox 128+. The same range is declared as `browserslist` in
46
46
 
47
47
  ## Which token for what
48
48
 
49
- | Token | Role |
50
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51
- | `--mw-primary-color` | Brand colour as a **fill**: primary buttons, table headers, bars, badges - the label on top is `--mw-primary-accent-text-color` |
52
- | `--mw-primary-text-color` | Brand colour as **ink** on a theme surface: text, icons, focus rings, accent borders. Derived by clamping OKLch lightness (`max(l, .68)` on dark, `min(l, .55)` on light), so a colour already in range is used untouched |
53
- | `--mw-primary-color-hover` | Solid hover surface (derived: base + `$hover-shift` black, in both themes) |
54
- | `--mw-primary-background` | 20% tint - focus halo, alert/badge/tag surface, scrollbar tracks |
55
- | `--mw-primary-background-hover` | 45% tint - row and list hover |
56
- | `--mw-primary-info-background` | Alias of `--mw-primary-background`, so a component can interpolate one name across all six colours |
57
- | `--mw-border-accent` | Translucent accent **line**: panel rules, dividers, tab underlines. Theme-aware (70% of `--mw-*-text-color`) |
58
- | `--mw-secondary-*` | Same set for the second brand colour |
59
- | `--mw-success/warning/danger/info-color` | Status colours, each with `-text-color`, `-color-hover`, `-info-background`, `-info-background-hover` |
60
- | `--mw-accent-text-color` | Text on any solid coloured surface (buttons, table/panel headers, badges, stepper dots) |
61
- | `--mw-primary-accent-text-color` | Per colour override of that label. Same for `secondary`, `success`, `warning`, `danger`, `info`. Defaults to `--mw-accent-text-color`, so set one only when a colour needs the opposite label - a neon primary on a dark palette |
62
- | `--mw-gray-color` | Neutral foreground: muted icons, tooltips |
63
- | `--mw-gray-background` | Subtle neutral surface (20% alpha): zebra rows, disabled fields, tracks, skeletons |
64
- | `--mw-surface-muted` | Alias of `--mw-gray-background` under the name you reach for: a slightly set-off area _inside_ a card - hint block, framed paragraph, form summary |
65
- | `--mw-overlay-background` | Heavy scrim (60%) behind modals and blocking spinners |
66
- | `--mw-page-background`, `--mw-card-background`, `--mw-footer-background`, `--mw-border`, `--mw-shadow`, `--mw-text-color`, `--mw-text-muted-color`, `--mw-hero-image-filter` | The **active theme** - aliases pointing at the `--mw-dark-*` or `--mw-light-*` set |
67
- | `--mw-header-*` | Header chrome: `background`, `text-color`, `navbar-list-color`, `navbar-list-active-color`, `burgerbutton-color`, `burgerbutton-open-color`, `border` - dark in both themes. The two burger tokens default to the primary and secondary label ink, because the burger sits on those two surfaces |
68
- | `--mw-form-elements-background`, `--mw-form-elements-color` | Form controls stay light in both themes and therefore have their own pair |
69
- | `--mw-font-family-base`, `-heading`, `-mono` | Font stacks - system stacks by default (`-mono` leads with Fira Code); no font is bundled. Configurable in SCSS, see below |
70
- | `--mw-hero-background`, `--mw-hero-text-color` | Hero image (`url(...)`) and the ink on it. Fixed across themes - the photo does not change with the theme, so its text must not either. Defaults to the light end of the palette |
71
- | `--mw-transition` | Hover and focus states - an explicit paint-only property list at `--mw-duration-base`, never `all` |
72
- | `--mw-card-img-height` | Per-card image height (default `210px`; `mw-card-lg`/`-xl` set it to 340px/480px, 260px/340px below `sm`) |
73
- | `--mw-card-addon-color` | Background of `mw-card-badge` / `mw-card-ribbon`; the `mw-card-addon-*` classes set it, override it for a custom colour |
74
- | `--mw-card-addon-text-color` | Label on that badge/ribbon; the `mw-card-addon-*` classes point it at the matching `--mw-*-accent-text-color` |
75
- | `--mw-progress-ink` | Label inside `mw-progress-inline-label`; the `mw-progress-*` colour classes point it at the matching `--mw-*-accent-text-color` |
76
- | `--mw-table-scroll-height` | Per-table height cap for `mw-table-responsive-scroll` |
77
- | `--mw-kanban-background`, `--mw-kanban-lane-border`, `--mw-kanban-column-min-height` | Per-board surface, lane border and lane floor (120px, 90px on `mw-kanban-compact`) |
78
- | `--mw-container-gutter`, `--mw-container-width` | Page gutter of `mw-container` (fluid `clamp(1rem, 4.2vw + 0.5rem, 4rem)`, never below the safe-area inset) and the width derived from it (`min(1200px, 100% - 2 * gutter)`) |
79
- | `--mw-section-padding-block` | Top/bottom rhythm of `mw-section` (1.75rem) |
80
- | `--mw-calendar-dot` | Colour of a single calendar dot - set it per dot or per cell; the `mw-calendar-dot-*` classes are presets for it |
81
- | `--mw-scroll-hint-cover` | Colour the scroll hint on a tab bar fades into. Preset to the page, re-pointed to the card background inside `mw-card`, `mw-panel`, `mw-modal`, `mw-tile`, `mw-calendar` |
82
- | `--mw-elevation-1` … `-5` | Every shadow in the framework. Two layers per level - contact plus ambient. Never write a `box-shadow` by hand: a hand-rolled one is the wrong colour in one of the two themes |
83
- | `--mw-shadow-near`, `--mw-shadow-far` | The two tones the ramp above is mixed from, per theme |
84
- | `--mw-transition-fast` | The same property list at `--mw-duration-fast` - for a state change that should feel instant under the pointer |
85
- | `--mw-duration-instant\|fast\|base\|slow\|slower` | 110 / 180 / 300 / 520 / 900ms. Anything built on these is covered by `prefers-reduced-motion` for free |
86
- | `--mw-duration-zoom` | 650ms, for a large surface actually travelling - an image scaling inside a card, a slider track |
87
- | `--mw-ease-out`, `--mw-ease-in-out`, `--mw-ease-spring` | Things arriving (the default) / A to B and back / a pop |
88
- | `--mw-control-height-sm\|·\|lg`, `--mw-control-font-sm\|·\|lg`, `--mw-control-line-height` | One size scale for input, select, textarea and button, so a field and the button beside it line up by construction |
89
- | `--mw-focus-ring-width\|offset\|color` | The keyboard focus ring |
90
- | `--mw-focus-halo-size\|opacity` | The soft ring a form field gets instead of a hard outline |
91
- | `--mw-internal-theme-mode` | Read-only: what `$mw-theme-mode` was compiled to |
49
+ | Token | Role |
50
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `--mw-primary-color` | Brand colour as a **fill**: primary buttons, table headers, bars, badges - the label on top is `--mw-primary-accent-text-color` |
52
+ | `--mw-primary-text-color` | Brand colour as **ink** on a theme surface: text, icons, focus rings, accent borders. Derived by clamping OKLch lightness (`max(l, .68)` on dark, `min(l, .55)` on light), so a colour already in range is used untouched |
53
+ | `--mw-primary-color-hover` | Solid hover surface (derived: base + `$hover-shift` black, in both themes) |
54
+ | `--mw-primary-background` | 20% tint - focus halo, alert/badge/tag surface, scrollbar tracks |
55
+ | `--mw-primary-background-hover` | 45% tint - row and list hover |
56
+ | `--mw-primary-info-background` | Alias of `--mw-primary-background`, so a component can interpolate one name across all six colours |
57
+ | `--mw-border-accent` | Translucent accent **line**: panel rules, dividers, tab underlines. Theme-aware (70% of `--mw-*-text-color`) |
58
+ | `--mw-secondary-*` | Same set for the second brand colour |
59
+ | `--mw-success/warning/danger/info-color` | Status colours, each with `-text-color`, `-color-hover`, `-info-background`, `-info-background-hover` |
60
+ | `--mw-accent-text-color` | Text on any solid coloured surface (buttons, table/panel headers, badges, stepper dots) |
61
+ | `--mw-primary-accent-text-color` | Per colour override of that label. Same for `secondary`, `success`, `warning`, `danger`, `info`. Defaults to `--mw-accent-text-color`, so set one only when a colour needs the opposite label - a neon primary on a dark palette |
62
+ | `--mw-gray-color` | Neutral foreground: muted icons, tooltips |
63
+ | `--mw-gray-background` | Subtle neutral surface (20% alpha): zebra rows, disabled fields, tracks, skeletons |
64
+ | `--mw-corner-accent` | The accent arc on the two round corners of every card, panel, modal and tile. Theme-aware: the full `--mw-*-text-color` tone in the dark theme, held to 80% in the light one, where the same blue carries far more contrast on a near-white card. Restyle it to recolour the signature, or `mw-corner-plain` on a single box to drop it |
65
+ | `--mw-surface-muted` | Alias of `--mw-gray-background` under the name you reach for: a slightly set-off area _inside_ a card - hint block, framed paragraph, form summary |
66
+ | `--mw-overlay-background` | Heavy scrim (60%) behind modals and blocking spinners |
67
+ | `--mw-page-background`, `--mw-card-background`, `--mw-footer-background`, `--mw-border`, `--mw-shadow`, `--mw-text-color`, `--mw-text-muted-color`, `--mw-hero-image-filter` | The **active theme** - aliases pointing at the `--mw-dark-*` or `--mw-light-*` set |
68
+ | `--mw-header-*` | Header chrome: `background`, `text-color`, `navbar-list-color`, `navbar-list-active-color`, `burgerbutton-color`, `burgerbutton-open-color`, `border` - dark in both themes. The two burger tokens default to the primary and secondary label ink, because the burger sits on those two surfaces |
69
+ | `--mw-form-elements-background`, `--mw-form-elements-color` | Form controls stay light in both themes and therefore have their own pair |
70
+ | `--mw-font-family-base`, `-heading`, `-mono` | Font stacks - system stacks by default (`-mono` leads with Fira Code); no font is bundled. Configurable in SCSS, see below |
71
+ | `--mw-hero-background`, `--mw-hero-text-color` | Hero image (`url(...)`) and the ink on it. Fixed across themes - the photo does not change with the theme, so its text must not either. Defaults to the light end of the palette |
72
+ | `--mw-transition` | Hover and focus states - an explicit paint-only property list at `--mw-duration-base`, never `all` |
73
+ | `--mw-card-img-height` | Per-card image height (default `210px`; `mw-card-lg`/`-xl` set it to 340px/480px, 260px/340px below `sm`) |
74
+ | `--mw-card-addon-color` | Background of `mw-card-badge` / `mw-card-ribbon`; the `mw-card-addon-*` classes set it, override it for a custom colour |
75
+ | `--mw-card-addon-text-color` | Label on that badge/ribbon; the `mw-card-addon-*` classes point it at the matching `--mw-*-accent-text-color` |
76
+ | `--mw-progress-ink` | Label inside `mw-progress-inline-label`; the `mw-progress-*` colour classes point it at the matching `--mw-*-accent-text-color` |
77
+ | `--mw-table-scroll-height` | Per-table height cap for `mw-table-responsive-scroll` |
78
+ | `--mw-kanban-background`, `--mw-kanban-lane-border`, `--mw-kanban-column-min-height` | Per-board surface, lane border and lane floor (120px, 90px on `mw-kanban-compact`) |
79
+ | `--mw-container-gutter`, `--mw-container-width` | Page gutter of `mw-container` (fluid `clamp(1rem, 4.2vw + 0.5rem, 4rem)`, never below the safe-area inset) and the width derived from it (`min(1200px, 100% - 2 * gutter)`) |
80
+ | `--mw-section-padding-block` | Top/bottom rhythm of `mw-section` (1.75rem) |
81
+ | `--mw-calendar-dot` | Colour of a single calendar dot - set it per dot or per cell; the `mw-calendar-dot-*` classes are presets for it |
82
+ | `--mw-scroll-hint-cover` | Colour the scroll hint on a tab bar fades into. Preset to the page, re-pointed to the card background inside `mw-card`, `mw-panel`, `mw-modal`, `mw-tile`, `mw-calendar` |
83
+ | `--mw-elevation-1` … `-5` | Every shadow in the framework. Two layers per level - contact plus ambient. Never write a `box-shadow` by hand: a hand-rolled one is the wrong colour in one of the two themes |
84
+ | `--mw-shadow-near`, `--mw-shadow-far` | The two tones the ramp above is mixed from, per theme |
85
+ | `--mw-transition-fast` | The same property list at `--mw-duration-fast` - for a state change that should feel instant under the pointer |
86
+ | `--mw-duration-instant\|fast\|base\|slow\|slower` | 110 / 180 / 300 / 520 / 900ms. Anything built on these is covered by `prefers-reduced-motion` for free |
87
+ | `--mw-duration-zoom` | 650ms, for a large surface actually travelling - an image scaling inside a card, a slider track |
88
+ | `--mw-ease-out`, `--mw-ease-in-out`, `--mw-ease-spring` | Things arriving (the default) / A to B and back / a pop |
89
+ | `--mw-control-height-sm\|·\|lg`, `--mw-control-font-sm\|·\|lg`, `--mw-control-line-height` | One size scale for input, select, textarea and button, so a field and the button beside it line up by construction |
90
+ | `--mw-focus-ring-width\|offset\|color` | The keyboard focus ring |
91
+ | `--mw-focus-halo-size\|opacity` | The soft ring a form field gets instead of a hard outline |
92
+ | `--mw-internal-theme-mode` | Read-only: what `$mw-theme-mode` was compiled to |
92
93
 
93
94
  `--mw-container-gutter`, `--mw-container-width` and `--mw-section-padding-block`
94
95
  are the knobs a good default cannot settle, because the right answer differs per
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ 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
+ ## [4.15.0] - 2026-08-29
10
+
11
+ ### Added
12
+
13
+ - `mw-price` - the price figure: struck-through original, discounted amount, cents, unit, period and note, ...
14
+
15
+ ## [4.14.0] - 2026-08-27
16
+
17
+ ### Added
18
+
19
+ - unique maverick-wave corner colors in cards
20
+
9
21
  ## [4.13.0] - 2026-08-27
10
22
 
11
23
  ### Added
package/README.md CHANGED
@@ -17,7 +17,7 @@ The result is a framework that balances utility with simplicity, offering develo
17
17
  ## Features
18
18
 
19
19
  - Responsive Grid System
20
- - 30+ UI Components: Buttons, Cards, Panels, Tabs, Accordions, Modals, Tiles, Alerts, Spinners, Progress Bars, Avatars, Tags, Badges, Dropdown, Ratings, Stepper, Skeleton Loader, Empty State, Divider, and more
20
+ - 30+ UI Components: Buttons, Cards, Panels, Tabs, Accordions, Modals, Tiles, Alerts, Spinners, Progress Bars, Avatars, Tags, Badges, Dropdown, Ratings, Stepper, Skeleton Loader, Empty State, Price, Offer Cards, Divider, and more
21
21
  - Form Elements: Input, Select, Textarea, Checkbox, Radio, Toggle, Input Group, with `mw-field` wrapper pattern for Angular Reactive Forms
22
22
  - Utility Classes for spacing, flex, display, typography, text overflow, elevation and aspect ratio
23
23
  - A five-step elevation ramp and a motion scale, so every shadow and every transition in the framework comes from one place
package/index.html CHANGED
@@ -280,6 +280,7 @@
280
280
  <a href="#code" class="mw-section-btn">Code</a>
281
281
  <a href="#skeleton" class="mw-section-btn">Skeleton</a>
282
282
  <a href="#empty-state" class="mw-section-btn">Empty State</a>
283
+ <a href="#pricing" class="mw-section-btn">Pricing</a>
283
284
  </nav>
284
285
 
285
286
  <!-- Buttons container -->
@@ -348,6 +349,11 @@
348
349
  <div id="empty-state">
349
350
  @@include('./src/partials/empty-state-container.html')
350
351
  </div>
352
+
353
+ <!-- Pricing container -->
354
+ <div id="pricing">
355
+ @@include('./src/partials/pricing-container.html')
356
+ </div>
351
357
  </div>
352
358
  </section>
353
359
 
@@ -497,6 +503,68 @@
497
503
  }
498
504
  });
499
505
 
506
+ // just for the showcase: one segmented switch above the offer grid drives
507
+ // every card in it. The framework ships the look, not the arithmetic - a
508
+ // real application has its own prices and its own state, so this stays
509
+ // here rather than in maverick-wave.js.
510
+ document.addEventListener('DOMContentLoaded', () => {
511
+ const cycleSwitch = document.getElementById('cycle-switch');
512
+ const offers = document.getElementById('cycle-offers');
513
+ if (!cycleSwitch || !offers) return;
514
+
515
+ // Trailing zero cents are noise on a price - "24 Euro", not "24,00
516
+ // Euro". The tabular figures keep the row from jumping anyway.
517
+ const money = (n) => {
518
+ const fixed = n.toFixed(2);
519
+ return fixed.endsWith('.00')
520
+ ? fixed.slice(0, -3)
521
+ : fixed.replace('.', ',');
522
+ };
523
+
524
+ const apply = (button) => {
525
+ const months = Number(button.dataset.cycle);
526
+ const off = Number(button.dataset.cycleOff || 0);
527
+
528
+ cycleSwitch.querySelectorAll('.mw-segmented-item').forEach((item) => {
529
+ const on = item === button;
530
+ item.classList.toggle('mw-active', on);
531
+ item.setAttribute('aria-pressed', String(on));
532
+ });
533
+
534
+ offers.querySelectorAll('.mw-offer').forEach((card) => {
535
+ const rate = Number(card.dataset.rate);
536
+ const perMonth = rate * (1 - off / 100);
537
+ const [whole, cents] = money(perMonth).split(',');
538
+
539
+ card.querySelector('[data-price-amount]').innerHTML = cents
540
+ ? whole + '<span class="mw-price-fraction">,' + cents + '</span>'
541
+ : whole;
542
+
543
+ const original = card.querySelector('.mw-price-original');
544
+ original.hidden = off === 0;
545
+ original.querySelector('[data-price-rate]').textContent =
546
+ money(rate);
547
+
548
+ const tag = card.querySelector('[data-price-off]');
549
+ tag.hidden = off === 0;
550
+ tag.textContent = '-' + off + '%';
551
+
552
+ card.querySelector('[data-price-billing]').textContent =
553
+ off === 0
554
+ ? button.dataset.cycleLabel + '.'
555
+ : money(perMonth * months) +
556
+ ' Euro ' +
557
+ button.dataset.cycleLabel +
558
+ '.';
559
+ });
560
+ };
561
+
562
+ cycleSwitch.querySelectorAll('.mw-segmented-item').forEach((item) => {
563
+ item.addEventListener('click', () => apply(item));
564
+ });
565
+ apply(cycleSwitch.querySelector('.mw-active'));
566
+ });
567
+
500
568
  // for the modals
501
569
  function openModal(modalId) {
502
570
  document.getElementById(modalId).classList.add('mw-modal-open');