maverick-wave 4.10.0 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/skills/maverick-wave/SKILL.md +114 -12
  2. package/.claude/skills/maverick-wave/examples/angular-services.md +8 -4
  3. package/.claude/skills/maverick-wave/examples/static-landing-page.md +18 -8
  4. package/.claude/skills/maverick-wave/references/components.md +168 -9
  5. package/.claude/skills/maverick-wave/references/forms.md +26 -0
  6. package/.claude/skills/maverick-wave/references/javascript.md +34 -27
  7. package/.claude/skills/maverick-wave/references/layout.md +38 -3
  8. package/.claude/skills/maverick-wave/references/theming.md +33 -7
  9. package/CHANGELOG.md +12 -0
  10. package/README.md +79 -4
  11. package/index.html +16 -0
  12. package/maverick-wave.min.css +3 -3
  13. package/maverick-wave.min.js +1 -1
  14. package/package.json +2 -2
  15. package/src/js/main.js +79 -1
  16. package/src/partials/accordions-container.html +41 -16
  17. package/src/partials/alerts-container.html +5 -0
  18. package/src/partials/avatars-container.html +5 -0
  19. package/src/partials/badges-container.html +114 -0
  20. package/src/partials/blog-posts-container.html +5 -0
  21. package/src/partials/breadcrumbs-container.html +1 -1
  22. package/src/partials/button-bar-container.html +5 -0
  23. package/src/partials/buttons-container.html +84 -0
  24. package/src/partials/calendar-container.html +1 -1
  25. package/src/partials/cards-container.html +5 -0
  26. package/src/partials/code-container.html +6 -0
  27. package/src/partials/colors-container.html +1 -1
  28. package/src/partials/coming-soon-container.html +4 -0
  29. package/src/partials/divider-container.html +4 -0
  30. package/src/partials/dropdown-container.html +119 -0
  31. package/src/partials/empty-state-container.html +5 -0
  32. package/src/partials/form-container.html +5 -0
  33. package/src/partials/form-elements-container.html +5 -0
  34. package/src/partials/form-field-container.html +5 -0
  35. package/src/partials/gallery-container.html +5 -0
  36. package/src/partials/get-started-container.html +16 -4
  37. package/src/partials/header-container.html +6 -2
  38. package/src/partials/header-utilities-container.html +8 -2
  39. package/src/partials/html-lists-container.html +5 -0
  40. package/src/partials/info-container.html +4 -0
  41. package/src/partials/input-group-container.html +5 -0
  42. package/src/partials/item-lists-container.html +5 -0
  43. package/src/partials/kanban-container.html +1 -1
  44. package/src/partials/kbd-container.html +63 -0
  45. package/src/partials/meta-info-container.html +5 -0
  46. package/src/partials/pagination-container.html +1 -1
  47. package/src/partials/panels-container.html +4 -0
  48. package/src/partials/progress-container.html +4 -0
  49. package/src/partials/ratings-container.html +1 -1
  50. package/src/partials/skeleton-container.html +5 -0
  51. package/src/partials/spinners-container.html +5 -1
  52. package/src/partials/stepper-container.html +4 -0
  53. package/src/partials/tables-container.html +7 -0
  54. package/src/partials/tabs-container.html +35 -13
  55. package/src/partials/tags-container.html +5 -0
  56. package/src/partials/tiles-container.html +1 -1
  57. package/src/partials/typography-container.html +5 -0
  58. package/src/partials/utilities-container.html +153 -0
  59. package/src/scss/abstracts/_functions.scss +18 -0
  60. package/src/scss/abstracts/_mixins.scss +176 -0
  61. package/src/scss/abstracts/_variables.scss +192 -4
  62. package/src/scss/base/_base.scss +103 -2
  63. package/src/scss/base/_reset.scss +38 -3
  64. package/src/scss/base/_typography.scss +47 -0
  65. package/src/scss/components/_accordions.scss +37 -7
  66. package/src/scss/components/_alerts.scss +3 -3
  67. package/src/scss/components/_avatars.scss +27 -6
  68. package/src/scss/components/_badge.scss +157 -0
  69. package/src/scss/components/_blog-post.scss +10 -7
  70. package/src/scss/components/_breadcrumbs.scss +11 -2
  71. package/src/scss/components/_button-bar.scss +9 -2
  72. package/src/scss/components/_buttons.scss +133 -17
  73. package/src/scss/components/_calendar.scss +13 -2
  74. package/src/scss/components/_cards.scss +12 -5
  75. package/src/scss/components/_code.scss +15 -0
  76. package/src/scss/components/_coming-soon.scss +5 -2
  77. package/src/scss/components/_content-slider.scss +1 -1
  78. package/src/scss/components/_dropdown.scss +219 -0
  79. package/src/scss/components/_gallery.scss +17 -8
  80. package/src/scss/components/_index.scss +3 -0
  81. package/src/scss/components/_info.scss +14 -8
  82. package/src/scss/components/_kanban.scss +40 -6
  83. package/src/scss/components/_kbd.scss +43 -0
  84. package/src/scss/components/_lists.scss +26 -23
  85. package/src/scss/components/_modals.scss +71 -6
  86. package/src/scss/components/_pagination.scss +4 -3
  87. package/src/scss/components/_panels.scss +13 -14
  88. package/src/scss/components/_progress.scss +1 -1
  89. package/src/scss/components/_segmented.scss +8 -4
  90. package/src/scss/components/_stepper.scss +3 -3
  91. package/src/scss/components/_tables.scss +5 -8
  92. package/src/scss/components/_tabs.scss +22 -3
  93. package/src/scss/components/_tags.scss +1 -2
  94. package/src/scss/components/_techstack-bucket.scss +8 -5
  95. package/src/scss/components/_theme-toggle.scss +16 -8
  96. package/src/scss/components/_tiles.scss +14 -4
  97. package/src/scss/components/_timelines.scss +9 -6
  98. package/src/scss/components/_toasts.scss +1 -1
  99. package/src/scss/form-elements/_checkbox.scss +2 -1
  100. package/src/scss/form-elements/_form.scss +2 -2
  101. package/src/scss/form-elements/_input-group.scss +30 -1
  102. package/src/scss/form-elements/_input.scss +10 -10
  103. package/src/scss/form-elements/_login.scss +1 -1
  104. package/src/scss/form-elements/_radio.scss +4 -3
  105. package/src/scss/form-elements/_select.scss +9 -10
  106. package/src/scss/form-elements/_slider.scss +11 -11
  107. package/src/scss/form-elements/_textarea.scss +5 -8
  108. package/src/scss/form-elements/_toggle.scss +6 -5
  109. package/src/scss/layout/_footer.scss +14 -7
  110. package/src/scss/layout/_header.scss +18 -17
  111. package/src/scss/layout/_home.scss +1 -1
  112. package/src/scss/layout/_main.scss +6 -6
  113. package/src/scss/layout/_section.scss +7 -4
  114. package/src/scss/utilities/_accessibility.scss +38 -0
  115. package/src/scss/utilities/_aspect.scss +27 -0
  116. package/src/scss/utilities/_display.scss +52 -0
  117. package/src/scss/utilities/_elevation.scss +16 -0
  118. package/src/scss/utilities/_index.scss +2 -0
  119. package/src/scss/utilities/_text.scss +63 -0
  120. package/src/scss/utilities/_touch-targets.scss +63 -0
@@ -155,12 +155,39 @@ 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
+ **Elevation** (`mw-elevation-0` … `-5`, and `var(--mw-elevation-N)` inside SCSS).
159
+ Two shadows per level - a tight contact layer plus a wide ambient one:
160
+ `1` resting (inputs, tags), `2` raised (cards, panels at rest), `3` floating
161
+ (a card under the pointer), `4` overlay (dropdown, popover, drawer), `5` modal.
162
+ `0` is explicitly flat. Never write a `box-shadow` by hand - the twelve one-off
163
+ values that used to exist are exactly what this replaced.
164
+
165
+ **Motion** `--mw-duration-instant|fast|base|slow|slower` = 110/180/300/520/900ms
166
+ plus `--mw-duration-zoom` (650ms, for a large surface actually travelling),
167
+ `--mw-ease-out` (things arriving - the default), `--mw-ease-in-out` (A to B and
168
+ back), `--mw-ease-spring` (a pop). Two ready-made transitions:
169
+ `var(--mw-transition)` for hover and focus states, `var(--mw-transition-fast)`
170
+ for anything that should feel instant under the pointer. Both list their
171
+ properties explicitly rather than saying `all`.
172
+
173
+ **Control sizes** `--mw-control-height-sm|base|lg` = 2 / 2.25 / 2.5rem (an even
174
+ 32 / 36 / 40px step) and
175
+ `--mw-control-font-sm|base|lg` = 0.8 / 0.9 / 1rem, shared by `mw-input`,
176
+ `mw-select`, `mw-textarea` and `mw-btn`. A field and the button beside it are
177
+ the same height by construction. Buttons run one font step above the fields.
178
+
179
+ **Focus** `--mw-focus-ring-width` 2px, `--mw-focus-ring-offset` 2px,
180
+ `--mw-focus-ring-color`, plus `--mw-focus-halo-size` 3px /
181
+ `--mw-focus-halo-opacity` 28% for the soft ring a form field gets instead of a
182
+ hard outline. SCSS: `@include focus-ring`, `focus-ring-inset`, `field-focus`.
183
+
158
184
  ## Component index
159
185
 
160
186
  Everything below is documented in `references/components.md` unless marked otherwise.
161
187
 
162
188
  **Actions** `mw-btn` (+ `primary`, `secondary`, `danger`, `success`, `outline`,
163
- `link`, `link-muted`, `plain`, `sm`, `lg`) · `mw-btn-mini` · `mw-button-bar`
189
+ `ghost`, `ghost-danger`, `link`, `link-muted`, `plain`, `icon`, `block`, `sm`,
190
+ `lg`) · `mw-btn-mini` · `mw-button-bar`
164
191
  (+ `left`, `right`, `center`, `between`) · `mw-segmented` · `mw-actions-note`
165
192
 
166
193
  **Containers** `mw-card` (+ `simple`, `lg`, `xl`, `stack`, badge, ribbon,
@@ -171,7 +198,8 @@ feature frame) ·
171
198
  **Data & status** `mw-table` (+ `subtle`, `sticky-head`, `cards`, `compact`,
172
199
  `hover`, responsive wrappers) · `mw-kanban` (+ `plain`, `compact`) ·
173
200
  `mw-calendar` (+ `compact`, `plain`) · `mw-tag` /
174
- `mw-tags` · `mw-info` / `mw-info-mini` / `mw-info-counter` · `mw-progress-bar` ·
201
+ `mw-tags` · `mw-badge` (+ `-dot`, `-status`, `-anchor`, `-float`, `-pulse`) ·
202
+ `mw-info` / `mw-info-mini` / `mw-info-counter` · `mw-progress-bar` ·
175
203
  `mw-rating` · `mw-meta-header` · `mw-stepper` · `mw-timeline-big` /
176
204
  `mw-timeline-simple`
177
205
 
@@ -179,11 +207,13 @@ feature frame) ·
179
207
  `mw-spinner-border` / `mw-spinner-dots` / `mw-spinner-dual-ring` · `mw-skeleton`
180
208
 
181
209
  **Navigation** `mw-header` + `mw-navbar` · `mw-breadcrumbs` · `mw-pagination` ·
182
- `mw-section-nav` (`references/layout.md`)
210
+ `mw-dropdown` (+ `-menu`, `-item`, `-item-danger`, `-divider`, `-label`,
211
+ `-caret`, `-end`, `-up`) · `mw-section-nav` (`references/layout.md`)
183
212
 
184
213
  **Media & content** `mw-avatar` (+ `initials`, `group`) · `mw-gallery` ·
185
214
  `mw-image-slider` · `mw-blog-post` · `mw-code-block` / `mw-terminal` ·
186
- `mw-techstack-bucket` · `mw-coming-soon` · `mw-divider` · `mw-list` family
215
+ `mw-techstack-bucket` · `mw-coming-soon` · `mw-divider` · `mw-kbd` ·
216
+ `mw-list` family
187
217
 
188
218
  **Forms** (`references/forms.md`) `mw-field` · `mw-input` · `mw-select` ·
189
219
  `mw-textarea` · `mw-checkbox` · `mw-radio` · `mw-toggle` · `mw-slider` ·
@@ -194,9 +224,14 @@ feature frame) ·
194
224
  `mw-section` · `mw-page-header` · `mw-grid-*` · `mw-row-split` · `mw-hero` ·
195
225
  `mw-footer`
196
226
 
197
- **Utilities** (`references/layout.md`) `mw-sr-only` · `mw-row-split`
198
- (+ `center`) · `mw-text-numeric` / `mw-text-currency` · spacing, flex, display,
199
- text
227
+ **Utilities** (`references/layout.md`) `mw-sr-only` / `mw-sr-only-focusable` /
228
+ `mw-skip-link` · `mw-row-split` (+ `center`) · `mw-text-numeric` /
229
+ `mw-text-currency` · `mw-text-truncate` / `mw-text-clamp-2..5` /
230
+ `mw-text-break` / `mw-text-nowrap` · `mw-text-balance` / `mw-text-pretty` /
231
+ `mw-text-eyebrow` / `mw-text-measure` · `mw-elevation-0..5` ·
232
+ `mw-aspect-square|video|wide|portrait|photo` · `mw-d-{sm,md,lg,xl}-*` /
233
+ `mw-hide-mobile` / `mw-hide-desktop` · `mw-overflow-*` / `mw-snap-x` ·
234
+ spacing, flex, display, text
200
235
 
201
236
  ## Pitfalls
202
237
 
@@ -238,11 +273,13 @@ text
238
273
  Safari 16.4+, Firefox 128+. Both carry the derived tones, so older browsers
239
274
  get no colours at all, not merely worse ones.
240
275
  11. **Touch targets grow on their own.** On `pointer: coarse` or below 768px,
241
- `mw-btn-sm`, `mw-input-sm`, `mw-select-sm` and `mw-textarea-sm` get a 2.5rem
242
- minimum height, a tab 2.75rem, a calendar day 44px. `mw-btn-mini` keeps its
243
- 18px circle - it sits in tag rows and table cells where a bigger one would
244
- shift the layout - and grows its _hit area_ to 28px via a pseudo-element.
245
- Nothing to switch on, and no reason to write the media query again in an app.
276
+ `mw-btn` gets a 2.75rem minimum height, `mw-btn-sm`, `mw-input-sm`,
277
+ `mw-select-sm` and `mw-textarea-sm` 2.5rem, a tab 2.75rem, a calendar day
278
+ 44px, and list rows / menu items / pager pages / accordion headers 2.75rem.
279
+ `mw-btn-mini` keeps its 18px circle - it sits in tag rows and table cells
280
+ where a bigger one would shift the layout - and grows its _hit area_ to 28px
281
+ via a pseudo-element. Nothing to switch on, and no reason to write the media
282
+ query again in an app.
246
283
  12. **`mw-empty-state` has a `-desc`, not a `-text`.** The parts are
247
284
  `mw-empty-state-icon`, `-title`, `-desc`, plus the size variant
248
285
  `mw-empty-state-sm`. Invented names fail silently, as always.
@@ -259,3 +296,68 @@ text
259
296
  that host - it then generates no box and the children take the item role
260
297
  back. Angular: `host: { class: 'mw-d-contents' }`. `mw-header` handles this
261
298
  case on its own, the others do not.
299
+ 15. **A badge is not a tag.** `mw-tag` names something - a topic, a state - and
300
+ sits in a row of its own kind, with a tinted surface. `mw-badge` carries a
301
+ _count_ or a _status_ and usually sits **on** something, filled rather than
302
+ tinted. Number on a bell: badge. "Draft" next to a title: tag.
303
+ 16. **A dropdown is a `<details>`, not a div.** `<details class="mw-dropdown">`
304
+ with a `<summary>` trigger - that is where the keyboard handling, the focus
305
+ and the open state come from, and it works without script. The framework JS
306
+ only adds Escape and click-outside. Writing your own div-plus-click loses all
307
+ of it.
308
+ 17. **An open dropdown is clipped by anything that hides its overflow.** The menu
309
+ is absolutely positioned. The framework's own containers - panel, card, tile,
310
+ modal body, responsive table - lift the clip while a menu is open. On your
311
+ own container it is one line:
312
+ `:has(.mw-dropdown[open]) { overflow: visible }`.
313
+ 18. **Never write `box-shadow` by hand.** Use `var(--mw-elevation-1..5)` or the
314
+ `mw-elevation-*` class. A hand-rolled shadow is the wrong colour in one of
315
+ the two themes - the dark theme's shadow is a light rim over a dark contact
316
+ layer, not a black blur.
317
+ 19. **Never write a duration or an easing curve by hand** either. Use
318
+ `var(--mw-transition)` for a hover or focus state,
319
+ `var(--mw-transition-fast)` for something that should feel instant, and
320
+ `var(--mw-duration-*)` with `var(--mw-ease-*)` for anything else. That is
321
+ also what makes `prefers-reduced-motion` work - it turns the duration tokens
322
+ down, so anything built on them is covered for free.
323
+ 20. **A hover effect that _moves_ something needs the `hover` mixin.** On touch
324
+ `:hover` latches after a tap and stays on, so a lifted card stays lifted,
325
+ visibly out of line with its row. `@include hover { transform: ... }` -
326
+ colour changes are fine unguarded, movement is not. The framework's own
327
+ components already do this.
328
+ 21. **Below 576px a modal is a bottom sheet.** Full width, anchored to the bottom
329
+ edge, rounded on the top two corners, with a grab handle and full-width
330
+ actions in the footer. Nothing to switch on - do not fight it with your own
331
+ media query, and do not put a fixed height on `mw-modal`.
332
+ 22. **Press states exist on every control**, because a finger never hovers.
333
+ `mw-btn` and friends dip 1px and invert their highlight, `mw-btn-mini` and
334
+ `mw-modal-close` scale down. If you build your own control, give it an
335
+ `:active` - on touch it is the only feedback there is.
336
+ 23. **Never set a height on a form control.** `mw-input`, `mw-select`,
337
+ `mw-textarea` and `mw-btn` all take their minimum height from the control
338
+ scale, so a field and the button next to it line up on their own. Pick the
339
+ size step (`-sm` / nothing / `-lg`) and leave the height alone - a hand-set
340
+ one puts that control back out of line with everything around it.
341
+ 24. **Put the size modifier on the input group, not inside it.**
342
+ `mw-input-group-sm` and `-lg` size the prefix, the suffix _and_ the field.
343
+ Adding `mw-input-sm` inside as well is redundant, and mixing the two steps
344
+ is what makes a group look broken.
345
+ 25. **Loops keep their own timing, entrances go on the scale.** A spinner, a
346
+ skeleton shimmer and a pulse ring are ambient - running them at 300ms would
347
+ be frantic, and they are the one place a hand-written duration is right.
348
+ Anything that plays once - a panel appearing, a card sliding in, a drawer -
349
+ uses `var(--mw-duration-*)`.
350
+ 26. **`transition: all` is out.** `var(--mw-transition)` lists paint-only
351
+ properties on purpose. The one exception in the framework is the header
352
+ burger, which morphs by animating `top` and `bottom`, and it says so in a
353
+ comment. If your component really does need to animate a size, name that
354
+ property - do not reach for `all`.
355
+ 27. **A control is a `<button>`, never a styled `<div>`.** `mw-tabs-nav-item`,
356
+ `mw-theme-toggle`, `mw-gallery-dot` and `mw-accordion-header` are all
357
+ written for one, and all four shipped as divs and spans - the first three
358
+ until 4.11, the accordion header until 4.12 - that no keyboard could reach.
359
+ Each class clears what a `<button>` brings with it, so
360
+ `<button type="button" class="mw-tabs-nav-item" data-tab="...">` is the
361
+ whole markup. If you build your own clickable thing: the element decides
362
+ whether anyone without a mouse can use it, the class only decides how it
363
+ looks.
@@ -194,7 +194,7 @@ export class ConfirmDialogComponent {
194
194
 
195
195
  ## Accordion
196
196
 
197
- `active` on the header **and** the content - no prefix.
197
+ `mw-active` on the header **and** the content.
198
198
 
199
199
  ```ts
200
200
  @Component({
@@ -203,16 +203,20 @@ export class ConfirmDialogComponent {
203
203
  <div class="mw-accordion">
204
204
  @for (item of items(); track item.id; let i = $index) {
205
205
  <div class="mw-accordion-item">
206
- <div
206
+ <button
207
+ type="button"
207
208
  class="mw-accordion-header"
208
209
  [class.mw-active]="openIndex() === i"
210
+ [attr.aria-expanded]="openIndex() === i"
211
+ [attr.aria-controls]="item.id"
209
212
  (click)="toggle(i)"
210
213
  >
211
- <h3>{{ item.question }}</h3>
214
+ <span>{{ item.question }}</span>
212
215
  <i class="fas fa-chevron-down mw-accordion-icon"></i>
213
- </div>
216
+ </button>
214
217
  <div
215
218
  class="mw-accordion-content"
219
+ [id]="item.id"
216
220
  [class.mw-active]="openIndex() === i"
217
221
  >
218
222
  <div class="mw-accordion-content-inner">{{ item.answer }}</div>
@@ -234,11 +234,16 @@ wired up by hand.
234
234
 
235
235
  <div class="mw-accordion">
236
236
  <div class="mw-accordion-item">
237
- <div class="mw-accordion-header mw-active">
238
- <h3>Where is the data stored?</h3>
237
+ <button
238
+ type="button"
239
+ class="mw-accordion-header mw-active"
240
+ aria-expanded="true"
241
+ aria-controls="faq-storage"
242
+ >
243
+ <span>Where is the data stored?</span>
239
244
  <i class="fas fa-chevron-down mw-accordion-icon"></i>
240
- </div>
241
- <div class="mw-accordion-content mw-active">
245
+ </button>
246
+ <div class="mw-accordion-content mw-active" id="faq-storage">
242
247
  <div class="mw-accordion-content-inner">
243
248
  <p>In Frankfurt, in our own data centre.</p>
244
249
  </div>
@@ -246,11 +251,16 @@ wired up by hand.
246
251
  </div>
247
252
 
248
253
  <div class="mw-accordion-item">
249
- <div class="mw-accordion-header">
250
- <h3>Can I cancel monthly?</h3>
254
+ <button
255
+ type="button"
256
+ class="mw-accordion-header"
257
+ aria-expanded="false"
258
+ aria-controls="faq-cancel"
259
+ >
260
+ <span>Can I cancel monthly?</span>
251
261
  <i class="fas fa-chevron-down mw-accordion-icon"></i>
252
- </div>
253
- <div class="mw-accordion-content">
262
+ </button>
263
+ <div class="mw-accordion-content" id="faq-cancel">
254
264
  <div class="mw-accordion-content-inner">
255
265
  <p>Yes, at the end of every billing period.</p>
256
266
  </div>
@@ -15,15 +15,55 @@ exhaustive: what is not named does not exist.
15
15
  ```
16
16
 
17
17
  - Variants: `mw-btn-primary`, `mw-btn-secondary`, `mw-btn-danger`,
18
- `mw-btn-success`, `mw-btn-outline`, `mw-btn-link`, `mw-btn-link-muted`
18
+ `mw-btn-success`, `mw-btn-outline`, `mw-btn-ghost`, `mw-btn-ghost-danger`,
19
+ `mw-btn-link`, `mw-btn-link-muted`
20
+ - Shapes: `mw-btn-icon` (square, for an icon-only button), `mw-btn-block`
21
+ (full width)
19
22
  - Sizes: `mw-btn-sm`, `mw-btn-lg`
20
23
  - `mw-btn` is `inline-flex` with a gap - icons need no wrapper and no extra class
21
24
  - `disabled` gets `opacity: .6` and `not-allowed`; there is no disabled class
22
25
  - `mw-active` gives the pressed/selected look on the coloured variants
26
+ - Every variant dips 1px on `:active`, and the solid ones invert their top
27
+ highlight into an inner shadow. On touch that press is the only feedback there
28
+ is, so do not override it away
23
29
  - `mw-btn-link` still has the button padding - `mw-p-0` makes it read as inline
24
30
  text
25
- - On a coarse pointer or below 768px `mw-btn-sm` grows to a 2.5rem minimum
26
- height on its own
31
+ - On a coarse pointer or below 768px `mw-btn` grows to a 2.75rem minimum height
32
+ and `mw-btn-sm` to 2.5rem, on their own
33
+
34
+ ### Three emphasis levels
35
+
36
+ `mw-btn-primary` (or any solid colour) **fills**, `mw-btn-outline` **draws a
37
+ line**, `mw-btn-ghost` does **neither** until you point at it. Ghost is what a
38
+ toolbar, a card's own actions or a row of icon buttons wants - five outlined
39
+ boxes in a row fight the content they sit on.
40
+
41
+ ```html
42
+ <button type="button" class="mw-btn mw-btn-ghost">Rename</button>
43
+ <button type="button" class="mw-btn mw-btn-ghost mw-btn-ghost-danger">
44
+ Remove
45
+ </button>
46
+ ```
47
+
48
+ Ghost is not `mw-btn-plain`: it keeps the button's shape, padding and hit area
49
+ and only leaves them unpainted at rest. `mw-btn-plain` has no look at all.
50
+
51
+ ### Icon-only buttons
52
+
53
+ ```html
54
+ <button
55
+ type="button"
56
+ class="mw-btn mw-btn-ghost mw-btn-icon"
57
+ aria-label="Duplicate"
58
+ >
59
+ <i class="fas fa-copy"></i>
60
+ </button>
61
+ ```
62
+
63
+ `mw-btn-icon` makes the button square and follows the size modifier next to it
64
+ (`mw-btn-sm` / `mw-btn-lg`), growing to 2.75rem on a coarse pointer. It always
65
+ needs an `aria-label` - there is no text to read out. Combine it with any
66
+ variant; ghost is usually the right one.
27
67
 
28
68
  ### `mw-btn-plain` - a button with no button in it
29
69
 
@@ -75,7 +115,7 @@ Row of equally treated buttons, centered by default.
75
115
  - Alignment: `mw-button-bar-left`, `-right`, `-center` (which is also the
76
116
  default), `mw-button-bar-between` pushes first and last apart.
77
117
  - Sizes: `mw-button-bar-sm`, `mw-button-bar-lg`.
78
- - Below 576px it stacks and gives every button 96% width. Right for a row of
118
+ - Below 576px it stacks and gives every button the full width. Right for a row of
79
119
  independent actions - wrong for a switch, see below.
80
120
 
81
121
  ### `mw-actions-note` - one line above a row of actions
@@ -277,21 +317,32 @@ Tile sizes: `mw-tile-sm`, `mw-tile-lg`.
277
317
  ```html
278
318
  <div class="mw-accordion">
279
319
  <div class="mw-accordion-item">
280
- <div class="mw-accordion-header mw-active">
281
- <h3>Question</h3>
320
+ <button
321
+ type="button"
322
+ class="mw-accordion-header mw-active"
323
+ aria-expanded="true"
324
+ aria-controls="faq-1"
325
+ >
326
+ <span>Question</span>
282
327
  <i class="fas fa-chevron-down mw-accordion-icon"></i>
283
- </div>
284
- <div class="mw-accordion-content mw-active">
328
+ </button>
329
+ <div class="mw-accordion-content mw-active" id="faq-1">
285
330
  <div class="mw-accordion-content-inner">Answer</div>
286
331
  </div>
287
332
  </div>
288
333
  </div>
289
334
  ```
290
335
 
336
+ The header is a `<button>` - it is the control that opens the panel, and a
337
+ `<div>` is not focusable. A heading is not allowed inside a button, so the
338
+ question is a `<span>` and the header carries the type itself; a nested `h3`
339
+ still renders the same, for markup written before 4.12.
340
+
291
341
  Open state = `mw-active` on header **and** content (bare `active` still works
292
342
  but is deprecated). The icon rotates via the **header** state - putting the
293
343
  class on the icon instead does nothing. Content taller than 500px scrolls.
294
- Toggling is JS - see `references/javascript.md`.
344
+ Toggling is JS - see `references/javascript.md`; the shipped script keeps
345
+ `aria-expanded` in step when the header is a button.
295
346
 
296
347
  ## Tabs
297
348
 
@@ -809,6 +860,114 @@ Container variants: `mw-tags-primary`, `-secondary`, `-success`, `-info`,
809
860
  `-warning`, `-danger`, `-muted`, size `mw-tags-lg`. `mw-tags-remove` only
810
861
  positions the button - its look comes from `mw-btn-mini` plus a colour variant.
811
862
 
863
+ ## Badges
864
+
865
+ A count or a status, not a label. See the pitfall list in `SKILL.md` for the
866
+ badge-versus-tag rule: a tag names something and sits in a row of its own kind,
867
+ a badge carries a number or a state and usually sits _on_ something.
868
+
869
+ ```html
870
+ <span class="mw-badge mw-badge-danger">99+</span>
871
+ ```
872
+
873
+ Colours: `mw-badge-primary` (the default), `-secondary`, `-success`, `-warning`,
874
+ `-danger`, `-info`, `-muted`, plus `mw-badge-outline`. Sizes: `mw-badge-sm`,
875
+ `mw-badge-lg`.
876
+
877
+ Round on one character, a pill from two on - a `min-width` the padding grows
878
+ past. The digits are `tabular-nums`, so a counter ticking 9 to 10 does not shift
879
+ what sits beside it.
880
+
881
+ **On an icon or a button:**
882
+
883
+ ```html
884
+ <span class="mw-badge-anchor">
885
+ <button class="mw-btn mw-btn-outline">
886
+ <i class="fas fa-inbox"></i> Inbox
887
+ </button>
888
+ <span class="mw-badge mw-badge-danger mw-badge-float">5</span>
889
+ </span>
890
+ ```
891
+
892
+ `mw-badge-anchor` on the thing being badged, `mw-badge-float` on the badge. It
893
+ hangs half off the corner and carries a ring in the surface colour, so it reads
894
+ as a separate object rather than a blob on the icon. The ring follows the
895
+ surface automatically inside a card, panel, modal, tile or the header.
896
+
897
+ **Status:**
898
+
899
+ ```html
900
+ <span class="mw-badge-status mw-badge-status-success">
901
+ <span class="mw-badge-dot"></span> Online
902
+ </span>
903
+ ```
904
+
905
+ `mw-badge-status-{primary|secondary|success|warning|danger|info|muted}` colours
906
+ the dot inside it. A bare `mw-badge-dot-{colour}` works on its own too. Add
907
+ `mw-badge-pulse` for something actually running - a job, an open connection: a
908
+ ring grows out and fades while the dot underneath stays put, so a column of them
909
+ in a table stays readable. Not for decoration.
910
+
911
+ ## Dropdown
912
+
913
+ Built on `<details>` / `<summary>`. That is where the keyboard handling, the
914
+ focus behaviour and the open state a screen reader can see come from, and it
915
+ works with JavaScript switched off. The framework script only adds Escape and
916
+ click-outside.
917
+
918
+ ```html
919
+ <details class="mw-dropdown">
920
+ <summary class="mw-btn mw-btn-primary">
921
+ Actions <i class="fas fa-chevron-down mw-dropdown-caret"></i>
922
+ </summary>
923
+ <div class="mw-dropdown-menu">
924
+ <div class="mw-dropdown-label">Manage</div>
925
+ <button class="mw-dropdown-item"><i class="fas fa-pen"></i> Edit</button>
926
+ <button class="mw-dropdown-item mw-active">Duplicate</button>
927
+ <button class="mw-dropdown-item" disabled>Move</button>
928
+ <hr class="mw-dropdown-divider" />
929
+ <button class="mw-dropdown-item mw-dropdown-item-danger">
930
+ <i class="fas fa-trash"></i> Delete
931
+ </button>
932
+ </div>
933
+ </details>
934
+ ```
935
+
936
+ Parts: `mw-dropdown-menu`, `-item`, `-item-danger`, `-divider`, `-label`,
937
+ `-caret` (rotates with the open state on its own). Alignment:
938
+ `mw-dropdown-end` anchors the menu to the trigger's right edge,
939
+ `mw-dropdown-up` opens it upward.
940
+
941
+ `mw-active` (or `aria-checked="true"`) marks the current choice, for a menu that
942
+ picks rather than acts. Icons inside items keep one column, so labels line up
943
+ whether or not every item has one.
944
+
945
+ The menu is absolutely positioned and is clipped by any ancestor that hides its
946
+ overflow. The framework's own containers lift that clip while a menu is open; on
947
+ your own it is `:has(.mw-dropdown[open]) { overflow: visible }`.
948
+
949
+ On a coarse pointer the rows grow to 2.75rem and the menu takes at least the
950
+ trigger's full width.
951
+
952
+ ## Keyboard keys
953
+
954
+ `<kbd>` is styled directly, so a shortcut in prose needs no class:
955
+
956
+ ```html
957
+ <p>
958
+ Press
959
+ <span class="mw-kbd-group"
960
+ ><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></span
961
+ >
962
+ to open the palette, <kbd>Esc</kbd> to close.
963
+ </p>
964
+ ```
965
+
966
+ `mw-kbd` is the same look for a `<span>` when the markup is not yours to change.
967
+ `mw-kbd-group` wraps a combination so the pluses are spaced and the whole thing
968
+ cannot wrap apart. `mw-kbd-pressed` shows a key mid-press - the raised bottom
969
+ edge drops and the key sits 1px lower.
970
+
812
971
  ## Info badges & counters
813
972
 
814
973
  ```html
@@ -1,5 +1,31 @@
1
1
  # Forms
2
2
 
3
+ ## Control sizes
4
+
5
+ `mw-input`, `mw-select`, `mw-textarea` and `mw-btn` share one scale:
6
+
7
+ | step | height | font |
8
+ | ----- | ------- | ------ |
9
+ | `-sm` | 2rem | 0.8rem |
10
+ | base | 2.25rem | 0.9rem |
11
+ | `-lg` | 2.5rem | 1rem |
12
+
13
+ A field and the button beside it are therefore the same height by construction -
14
+ before 4.11.0 each control worked its own height out and an input, a select and
15
+ a button in one row measured 32.2, 34.4 and 37.2 pixels. Buttons sit one font
16
+ step above the fields: a button carries a label, a field carries what the user
17
+ typed.
18
+
19
+ Never set a height on a control yourself. Pick the step and leave it alone.
20
+
21
+ The minimum only governs while it is the larger of the two numbers. Block
22
+ padding above it wins, and the control silently leaves the scale - which is
23
+ exactly how a button once ended up 1.2px taller than the field beside it. So if
24
+ you do override `padding-block` on a control, keep
25
+ `2 x padding + line-height + border` under the height its step allows.
26
+ On a coarse pointer or below 768px every control grows to 2.75rem on its own.
27
+ Retune the whole scale through `--mw-control-height*` / `--mw-control-font*`.
28
+
3
29
  ## The field pattern
4
30
 
5
31
  `mw-field` groups label, control, hint and error into one unit. It is the
@@ -25,25 +25,26 @@ 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` | `[class.mw-active]="isOpen()"` on both elements |
31
- | Tabs | `data-tab` → panel `id`; sets `mw-active` on nav item and panel | 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`, closes on anchor click | 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
- | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` on `<body>` and `mw-active` on the toggle | A theme service - see `examples/angular-services.md` |
36
- | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
37
- | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
38
- | Alerts | Close button adds `mw-alert-closed` (`display: none`) | Remove the alert from the list/signal |
39
- | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
40
- | Gallery | Generates the dots, moves the track, swipe handling, writes `mw-gallery-desc` | Render dots in the template, bind the track transform and `mw-active` on the current dot |
41
- | 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 |
42
- | Kanban board | Counts the tickets per lane, moves a card between lanes (`data-kanban-move`), 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 |
43
- | 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 |
44
- | 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 |
45
- | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
46
- | Color swatches | Showcase-only (prints computed hex values) | Not needed |
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 | 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`, closes on anchor click | 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
+ | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` on `<body>` and `mw-active` on the toggle | A theme service - see `examples/angular-services.md` |
36
+ | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
37
+ | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
38
+ | Alerts | Close button adds `mw-alert-closed` (`display: none`) | Remove the alert from the list/signal |
39
+ | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
40
+ | Gallery | Generates the dots, moves the track, swipe handling, writes `mw-gallery-desc` | Render dots in the template, bind the track transform and `mw-active` on the current dot |
41
+ | 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 |
42
+ | 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 |
43
+ | 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 |
44
+ | 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 |
45
+ | Header login button | Swaps the FontAwesome lock icon | Bind the icon class |
46
+ | Color swatches | Showcase-only (prints computed hex values) | Not needed |
47
+ | 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 |
47
48
 
48
49
  ## A note on the state class
49
50
 
@@ -56,14 +57,20 @@ other spelling exists.
56
57
 
57
58
  ## What works without any JavaScript
58
59
 
59
- Pure CSS, nothing to wire up: hover and focus states, the card lift, tooltips
60
- (`data-tooltip`), `mw-rating` (via `data-rating`), the responsive table card
61
- view (`data-label`), all grids and utilities, the body scroll lock while a modal
62
- is open (`body:has(.mw-modal-open)`), toast entry animations, the sticky table
63
- header, the scroll hint on a tab bar (four gradients, no scroll listener), the
64
- kanban empty-lane placeholder (hidden via `:has()` as soon as the lane holds a
65
- ticket), touch target sizing on a coarse pointer, `prefers-reduced-motion`
66
- handling.
60
+ Pure CSS, nothing to wire up: hover, focus and press states, the card lift,
61
+ tooltips (`data-tooltip`), `mw-rating` (via `data-rating`), the responsive table
62
+ card view (`data-label`), all grids and utilities, the body scroll lock while a
63
+ modal is open (`body:has(.mw-modal-open)`), the modal turning into a bottom
64
+ sheet below 576px, toast entry animations, the sticky table header, the scroll
65
+ hint on a tab bar (four gradients, no scroll listener), the kanban empty-lane
66
+ placeholder (hidden via `:has()` as soon as the lane holds a ticket), touch
67
+ target sizing on a coarse pointer, `prefers-reduced-motion` handling.
68
+
69
+ `mw-dropdown` is _almost_ in this list: opening, closing, the keyboard and the
70
+ open state are all the `<details>` element, so it works with no script at all.
71
+ The only two things the shipped JS adds are closing on Escape and closing on a
72
+ click somewhere else - worth rebuilding in a SPA, but a menu without them is
73
+ still a working menu, not a broken one.
67
74
 
68
75
  ## When you do keep the shipped JS
69
76