maverick-wave 4.14.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.
@@ -202,7 +202,10 @@ Everything below is documented in `references/components.md` unless marked other
202
202
  **Containers** `mw-card` (+ `simple`, `lg`, `xl`, `stack`, badge, ribbon,
203
203
  feature frame) ·
204
204
  `mw-panel` · `mw-tile` · `mw-accordion` · `mw-tabs` · `mw-modal` ·
205
- `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`)
206
209
 
207
210
  **Data & status** `mw-table` (+ `subtle`, `sticky-head`, `cards`, `compact`,
208
211
  `hover`, responsive wrappers) · `mw-kanban` (+ `plain`, `compact`) ·
@@ -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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ 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
+
9
15
  ## [4.14.0] - 2026-08-27
10
16
 
11
17
  ### 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');