maverick-wave 3.2.3 → 3.3.1

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 (78) hide show
  1. package/.claude/skills/maverick-wave/SKILL.md +208 -0
  2. package/.claude/skills/maverick-wave/examples/angular-form.md +275 -0
  3. package/.claude/skills/maverick-wave/examples/angular-list-page.md +304 -0
  4. package/.claude/skills/maverick-wave/examples/angular-services.md +348 -0
  5. package/.claude/skills/maverick-wave/examples/static-landing-page.md +362 -0
  6. package/.claude/skills/maverick-wave/references/components.md +812 -0
  7. package/.claude/skills/maverick-wave/references/forms.md +287 -0
  8. package/.claude/skills/maverick-wave/references/javascript.md +70 -0
  9. package/.claude/skills/maverick-wave/references/layout.md +300 -0
  10. package/.claude/skills/maverick-wave/references/theming.md +192 -0
  11. package/.github/workflows/ci.yml +41 -0
  12. package/CHANGELOG.md +13 -0
  13. package/README.md +115 -21
  14. package/index.html +15 -9
  15. package/maverick-wave.min.css +1 -1
  16. package/maverick-wave.min.js +1 -1
  17. package/package.json +4 -2
  18. package/scripts/verify.js +137 -0
  19. package/src/js/main.js +23 -11
  20. package/src/partials/accordions-container.html +22 -27
  21. package/src/partials/alerts-container.html +26 -0
  22. package/src/partials/avatars-container.html +22 -0
  23. package/src/partials/blog-posts-container.html +3 -1
  24. package/src/partials/buttons-container.html +11 -15
  25. package/src/partials/cards-container.html +8 -8
  26. package/src/partials/colors-container.html +33 -20
  27. package/src/partials/documentation-container.html +3 -0
  28. package/src/partials/form-field-container.html +16 -12
  29. package/src/partials/get-started-container.html +93 -35
  30. package/src/partials/header-utilities-container.html +64 -6
  31. package/src/partials/input-group-container.html +5 -3
  32. package/src/partials/page-header-container.html +78 -0
  33. package/src/partials/tables-container.html +130 -0
  34. package/src/partials/tabs-container.html +20 -16
  35. package/src/partials/tags-container.html +55 -1
  36. package/src/scss/abstracts/_index.scss +4 -1
  37. package/src/scss/abstracts/_variables.scss +128 -106
  38. package/src/scss/base/_base.scss +32 -136
  39. package/src/scss/base/_reset.scss +1 -1
  40. package/src/scss/base/_typography.scss +0 -92
  41. package/src/scss/components/_accordions.scss +3 -3
  42. package/src/scss/components/_avatars.scss +28 -1
  43. package/src/scss/components/_blog-post.scss +1 -1
  44. package/src/scss/components/_button-bar.scss +6 -6
  45. package/src/scss/components/_buttons.scss +33 -10
  46. package/src/scss/components/_cards.scss +7 -7
  47. package/src/scss/components/_coming-soon.scss +4 -4
  48. package/src/scss/components/_gallery.scss +2 -2
  49. package/src/scss/components/_index.scss +1 -0
  50. package/src/scss/components/_info.scss +20 -3
  51. package/src/scss/components/_lists.scss +11 -10
  52. package/src/scss/components/_modals.scss +10 -3
  53. package/src/scss/components/_pagination.scss +3 -3
  54. package/src/scss/components/_panels.scss +4 -4
  55. package/src/scss/components/_progress.scss +1 -1
  56. package/src/scss/components/_ratings.scss +1 -1
  57. package/src/scss/components/_spinners.scss +2 -2
  58. package/src/scss/components/_stepper.scss +3 -2
  59. package/src/scss/components/_tables.scss +42 -1
  60. package/src/scss/components/_tabs.scss +5 -5
  61. package/src/scss/components/_tags.scss +43 -27
  62. package/src/scss/components/_tiles.scss +3 -3
  63. package/src/scss/components/_timelines.scss +2 -2
  64. package/src/scss/components/_toasts.scss +92 -0
  65. package/src/scss/form-elements/_form.scss +0 -1
  66. package/src/scss/form-elements/_input.scss +24 -0
  67. package/src/scss/form-elements/_toggle.scss +1 -1
  68. package/src/scss/layout/_footer.scss +4 -4
  69. package/src/scss/layout/_header.scss +78 -4
  70. package/src/scss/layout/_index.scss +1 -0
  71. package/src/scss/layout/_main.scss +1 -1
  72. package/src/scss/layout/_page-header.scss +48 -0
  73. package/src/scss/layout/_section.scss +15 -14
  74. package/src/scss/main.scss +4 -0
  75. package/src/scss/utilities/_flex.scss +4 -0
  76. package/src/scss/utilities/_index.scss +1 -0
  77. package/src/scss/utilities/_text.scss +105 -0
  78. package/.claude/commands/mw.md +0 -595
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: maverick-wave
3
+ description: Complete usage guide for the MaverickWave CSS framework - mw-* classes, --mw-* design tokens, grid, components, forms, theming and integration into Angular, other SPAs or plain HTML. Use whenever markup or styles are written in a project that loads maverick-wave (CSS/SCSS/CDN), when a mw-* class or --mw-* token appears, or when a UI has to be built with it.
4
+ ---
5
+
6
+ # MaverickWave
7
+
8
+ A CSS-only component framework: it ships classes and design tokens, no components,
9
+ no directives, no build-time API. You write plain HTML with `mw-*` classes - in a
10
+ template, a JSX file, a Twig partial, it makes no difference. Everything below is
11
+ therefore framework-neutral; the Angular notes are marked as such because Angular
12
+ is the reference integration.
13
+
14
+ **One rule dominates all others: only use classes that actually exist.** The
15
+ framework is not utility-complete, invented class names fail silently (no error,
16
+ just unstyled markup). Verify with `grep -o '\.mw-[a-z0-9-]*' dist/maverick-wave.min.css | sort -u`
17
+ or, in the framework repo, `npm run verify`.
18
+
19
+ ## Reference files
20
+
21
+ Load the one you need - do not read them all up front.
22
+
23
+ | File | Content |
24
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
25
+ | `references/layout.md` | Page skeleton, grid, container, section, page header, header/navbar, footer, spacing/flex/display/text utilities |
26
+ | `references/components.md` | Every component: markup, variants, sizes, state classes |
27
+ | `references/forms.md` | Inputs, select, textarea, checkbox, radio, toggle, slider, input group, `mw-field` pattern, validation |
28
+ | `references/theming.md` | Token model, `color-mix` derivation, light/dark, SCSS configuration, cherry-picking single components |
29
+ | `references/javascript.md` | What the shipped `main.js` does, why SPAs must not load it, what to implement instead |
30
+
31
+ ## Examples
32
+
33
+ | File | Content |
34
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
35
+ | `examples/angular-list-page.md` | Full list/CRUD page: page header, meta counters, filter bar, table with status tags, empty state, delete modal, toasts |
36
+ | `examples/angular-form.md` | Reactive form: `mw-field`, validation states, input groups, numeric input, toggles, form actions |
37
+ | `examples/angular-services.md` | Theme service, toast service, modal/scroll-lock, tabs and accordion without `main.js` |
38
+ | `examples/static-landing-page.md` | Plain HTML page: header, hero, sections, cards, tiles, accordion, footer, theme toggle |
39
+
40
+ ## Setup
41
+
42
+ ### Plain HTML / CDN
43
+
44
+ ```html
45
+ <link
46
+ rel="stylesheet"
47
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@3.4.0/maverick-wave.min.css"
48
+ />
49
+ <link
50
+ rel="stylesheet"
51
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
52
+ />
53
+ ...
54
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@3.4.0/maverick-wave.min.js"></script>
55
+ ```
56
+
57
+ Pin the version. The JS file is optional and only for server-rendered/static pages -
58
+ see below.
59
+
60
+ ### Angular (or any SPA)
61
+
62
+ ```jsonc
63
+ // angular.json
64
+ "styles": ["src/assets/maverick-wave.min.css", "src/styles.scss"]
65
+ // no "scripts" entry - see the warning below
66
+ ```
67
+
68
+ Or from the npm package via SCSS: `@use 'maverick-wave/src/scss/main';`
69
+ (details and configuration in `references/theming.md`).
70
+
71
+ > **Never load `maverick-wave.min.js` in a SPA.** Everything in it is wired up
72
+ > once on `DOMContentLoaded` and writes straight into the DOM. In an Angular
73
+ > app that fires once during bootstrap: components rendered later are never
74
+ > initialised, and the theme toggle mutates the DOM behind change detection.
75
+ > Reimplement the handful of behaviours in your own components -
76
+ > `references/javascript.md` lists all of them, `examples/angular-services.md`
77
+ > shows the code.
78
+
79
+ ### Icons
80
+
81
+ The framework styles icon slots but ships no icons. Examples use FontAwesome
82
+ (`<i class="fas fa-save"></i>`); any icon element works - inline SVG, an Angular
83
+ `fa-icon`, a `<span>` with a webfont.
84
+
85
+ ### Fonts
86
+
87
+ `--mw-font-family-base` defaults to `'Poppins', sans-serif`, but no font is
88
+ bundled. Load it yourself or override the token.
89
+
90
+ ## Naming rules
91
+
92
+ - Classes: `mw-` prefix, kebab-case: `.mw-button-bar`, `.mw-item-list-compact`
93
+ - Tokens: `--mw-` prefix: `--mw-primary-color`, `--mw-border-accent`
94
+ - Variants are suffixes on the base class: `mw-btn` + `mw-btn-primary`,
95
+ `mw-alert` + `mw-alert-danger`
96
+ - Colour variant names are the same everywhere: `primary`, `secondary`,
97
+ `success`, `warning`, `danger`, `info` (plus `muted` on tags). Not every
98
+ component offers all of them - check `references/components.md`.
99
+ - Size variants: `-sm`, `-lg`, sometimes `-xs` / `-xl`. The unsuffixed class is
100
+ the medium size.
101
+
102
+ ### State classes are not uniform - this is the single most common source of bugs
103
+
104
+ | Component | State class |
105
+ | ----------------------------------------------------------------- | ----------------------------------- |
106
+ | Accordion header + content | `active` (no prefix) |
107
+ | Tabs nav item + panel | `active` (no prefix) |
108
+ | Navbar link | `active` (no prefix) |
109
+ | Timeline step (`mw-timeline-big-step`, `mw-timeline-simple-step`) | `active` (no prefix) |
110
+ | Image slider overlay image + control button | `active` (no prefix) |
111
+ | Button, pressed/selected look | `active` (no prefix) |
112
+ | Theme toggle | `active` (no prefix) |
113
+ | Burger button + navbar drawer | `open` (no prefix) |
114
+ | Gallery dot | `mw-active` |
115
+ | Stepper indicator / label / connector / step | `mw-active`, `mw-done` |
116
+ | Checkbox list item (`li`) | `mw-selected` |
117
+ | Modal overlay | `mw-modal-open` |
118
+ | Alert, after dismissal | `mw-alert-closed` (`display: none`) |
119
+ | Field wrapper in error | `mw-field-has-error` |
120
+ | Single form control in error | `mw-form-element-error` |
121
+
122
+ Angular: `[class.active]="isOpen"` and `[class.mw-active]="i === current"` - the
123
+ binding target must match the table exactly.
124
+
125
+ ## Scales
126
+
127
+ **Spacing** (`mw-m-*`, `mw-p-*`, `mw-gap-*` and every internal value):
128
+
129
+ | key | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
130
+ | --- | --- | --- | ---- | --- | ---- | --- | ---- | ---- | --- | --- | --- | --- | --- | --- | --- |
131
+ | rem | 0 | 0.2 | 0.35 | 0.5 | 0.75 | 1 | 1.35 | 1.75 | 2 | 2.5 | 3.3 | 4.3 | 5.5 | 7 | 9 |
132
+
133
+ Negative margins exist for keys 1-6 (`mw-mt--3` = -0.5rem). Gap has no negative
134
+ keys at all (negative gap is invalid CSS and is not generated).
135
+
136
+ **Font sizes** (`mw-text-3xs` … `mw-text-6xl`): `3xs` 0.6, `2xs` 0.7, `xs` 0.8,
137
+ `sm` 0.9, `base` 1, `md` 1.1, `lg` 1.3, `xl` 1.5, `2xl` 1.8, `3xl` 2.2, `4xl` 2.5,
138
+ `5xl` 3, `6xl` 4.3 rem.
139
+
140
+ **Breakpoints** (max-width, mobile-first markup / desktop-first media queries):
141
+ `xs` 375, `sm` 576, `md` 768, `lg` 992, `xl` 1200, `2xl` 1400 px.
142
+
143
+ **Radius** (`mw-radius-none|xs|sm|md|lg|xl|2xl|full`): 0, 2, 5, 10, 15, 20, 30 px, 50%.
144
+
145
+ ## Component index
146
+
147
+ Everything below is documented in `references/components.md` unless marked otherwise.
148
+
149
+ **Actions** `mw-btn` (+ `primary`, `secondary`, `danger`, `success`, `outline`,
150
+ `link`, `link-muted`, `sm`, `lg`) · `mw-btn-mini` · `mw-button-bar`
151
+
152
+ **Containers** `mw-card` (+ `simple`, `content`, `lg`, `xl`, `stack`, badge,
153
+ ribbon) · `mw-panel` · `mw-tile` · `mw-accordion` · `mw-tabs` · `mw-modal` ·
154
+ `mw-item-list` family
155
+
156
+ **Data & status** `mw-table` (+ `subtle`, `sticky-head`, `cards`, `compact`,
157
+ `hover`, responsive wrappers) · `mw-tag` / `mw-tags` · `mw-info` /
158
+ `mw-info-mini` / `mw-info-counter` · `mw-progress-bar` · `mw-rating` ·
159
+ `mw-meta-header` · `mw-stepper` · `mw-timeline-big` / `mw-timeline-simple`
160
+
161
+ **Feedback** `mw-alert` · `mw-toast-stack` · `mw-empty-state` ·
162
+ `mw-spinner-border` / `mw-spinner-dots` / `mw-spinner-dual-ring` · `mw-skeleton`
163
+
164
+ **Navigation** `mw-header` + `mw-navbar` · `mw-breadcrumbs` · `mw-pagination` ·
165
+ `mw-section-nav` (`references/layout.md`)
166
+
167
+ **Media & content** `mw-avatar` (+ `initials`, `group`) · `mw-gallery` ·
168
+ `mw-image-slider` · `mw-blog-post` · `mw-code-block` / `mw-terminal` ·
169
+ `mw-techstack-bucket` · `mw-coming-soon` · `mw-divider` · `mw-list` family
170
+
171
+ **Forms** (`references/forms.md`) `mw-field` · `mw-input` · `mw-select` ·
172
+ `mw-textarea` · `mw-checkbox` · `mw-radio` · `mw-toggle` · `mw-slider` ·
173
+ `mw-input-group` · `mw-form` / `mw-form-group` / `mw-form-actions` · `mw-login`
174
+
175
+ **Layout** (`references/layout.md`) `mw-main` · `mw-container` · `mw-content` ·
176
+ `mw-section` · `mw-page-header` · `mw-grid-*` · `mw-hero` · `mw-footer`
177
+
178
+ ## Pitfalls
179
+
180
+ 1. **`<section>` gets no padding by itself.** Since 3.4.0 the bare element
181
+ selector is gone - write `<section class="mw-section">`. `mw-section-alternate`
182
+ only paints the pattern, it does not space anything.
183
+ 2. **`mw-form` has no padding** (removed in 3.4.0) - it is a flex column with a
184
+ gap. Safe to put on `mw-modal-body` or `mw-panel-body`.
185
+ 3. **Use `mw-btn-danger` for destructive actions**, never `mw-btn-secondary`. The
186
+ secondary colour is a brand decision and turns yellow, teal or anything else
187
+ the moment the palette changes.
188
+ 4. **One tag or many?** `mw-tag` is a standalone chip and carries its own colour -
189
+ right for a table cell. `mw-tags` is a container that colours all its
190
+ `mw-tags-item` children - right for a tag list. Mixing them (`mw-tag` inside
191
+ `mw-tags`) is wrong.
192
+ 5. **A spinner needs a shape class.** `mw-spinner-primary` alone renders nothing;
193
+ use `mw-spinner-border`, `mw-spinner-dots` or `mw-spinner-dual-ring`.
194
+ 6. **Sticky table headers need a height-limited scroll container** -
195
+ `mw-table-responsive` only scrolls horizontally. Use
196
+ `mw-table-responsive-scroll` (height via `--mw-table-scroll-height`).
197
+ 7. **`--mw-text-muted-color` on a coloured surface is always wrong.** It follows
198
+ the theme; buttons, table headers and badges do not. Use
199
+ `--mw-accent-text-color` there, and `opacity` for disabled states.
200
+ 8. **`mw-card` already lifts on hover** and `mw-btn` already has `inline-flex`
201
+ plus a gap for icons. Neither has a modifier class for it - adding one from
202
+ memory produces markup that does nothing.
203
+ 9. **Overriding one root colour is enough.** Hover tones, translucent
204
+ backgrounds and borders are derived with `color-mix()` at runtime. Setting
205
+ `--mw-primary-color-hover` by hand is usually a sign the base token was not
206
+ set.
207
+ 10. **Browser floor: `color-mix()`** - Chrome 111+, Safari 16.2+, Firefox 113+.
208
+ Older browsers get no colours at all, not merely worse ones.
@@ -0,0 +1,275 @@
1
+ # Example: reactive form
2
+
3
+ Every control type in one form, with the error handling the framework expects:
4
+ `mw-field-has-error` on the wrapper, `mw-field-error` for the message. Nothing
5
+ reacts to `ng-invalid` on its own - the bindings below are the whole mechanism.
6
+
7
+ ## Component
8
+
9
+ ```ts
10
+ import {
11
+ ChangeDetectionStrategy,
12
+ Component,
13
+ inject,
14
+ signal,
15
+ } from '@angular/core';
16
+ import { TitleCasePipe } from '@angular/common';
17
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
18
+
19
+ @Component({
20
+ selector: 'app-project-form',
21
+ imports: [ReactiveFormsModule, TitleCasePipe],
22
+ templateUrl: './project-form.component.html',
23
+ changeDetection: ChangeDetectionStrategy.OnPush,
24
+ })
25
+ export class ProjectFormComponent {
26
+ private readonly fb = inject(FormBuilder);
27
+
28
+ protected readonly saving = signal(false);
29
+
30
+ protected readonly form = this.fb.nonNullable.group({
31
+ name: ['', [Validators.required, Validators.maxLength(80)]],
32
+ email: ['', [Validators.required, Validators.email]],
33
+ budget: [''], // text + inputmode="decimal", parsed on submit
34
+ type: ['', Validators.required],
35
+ priority: ['normal'],
36
+ description: [''],
37
+ teamSize: [3],
38
+ notify: [true],
39
+ terms: [false, Validators.requiredTrue],
40
+ });
41
+
42
+ protected readonly f = this.form.controls;
43
+
44
+ // A control shows its error once the user has touched it
45
+ protected invalid(name: keyof typeof this.f): boolean {
46
+ const control = this.f[name];
47
+ return control.invalid && (control.touched || control.dirty);
48
+ }
49
+
50
+ protected submit(): void {
51
+ if (this.form.invalid) {
52
+ this.form.markAllAsTouched();
53
+ return;
54
+ }
55
+ this.saving.set(true);
56
+ // …persist, then this.saving.set(false)
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Template
62
+
63
+ ```html
64
+ <form class="mw-form" [formGroup]="form" (ngSubmit)="submit()">
65
+ <!-- Group: base data -->
66
+ <div class="mw-form-group">
67
+ <p class="mw-text-muted mw-mb-3">Base data</p>
68
+
69
+ <div class="mw-grid-2">
70
+ <div class="mw-field" [class.mw-field-has-error]="invalid('name')">
71
+ <label class="mw-field-label mw-required" for="name"
72
+ >Project name</label
73
+ >
74
+ <input id="name" type="text" class="mw-input" formControlName="name" />
75
+ @if (invalid('name')) {
76
+ <span class="mw-field-error">
77
+ <i class="fas fa-exclamation-circle"></i>
78
+ @if (f.name.hasError('required')) { A name is required. } @else { At
79
+ most 80 characters. }
80
+ </span>
81
+ } @else {
82
+ <span class="mw-field-hint">Shown in the project list.</span>
83
+ }
84
+ </div>
85
+
86
+ <div class="mw-field" [class.mw-field-has-error]="invalid('email')">
87
+ <label class="mw-field-label mw-required" for="email">Contact</label>
88
+ <div class="mw-input-group">
89
+ <span class="mw-input-group-prefix"
90
+ ><i class="fas fa-envelope"></i
91
+ ></span>
92
+ <input
93
+ id="email"
94
+ type="email"
95
+ class="mw-input"
96
+ formControlName="email"
97
+ />
98
+ </div>
99
+ @if (invalid('email')) {
100
+ <span class="mw-field-error">
101
+ <i class="fas fa-exclamation-circle"></i> Please enter a valid email
102
+ address.
103
+ </span>
104
+ }
105
+ </div>
106
+
107
+ <!-- Money: text + inputmode, never type="number" -->
108
+ <div class="mw-field">
109
+ <label class="mw-field-label" for="budget">Budget</label>
110
+ <div class="mw-input-group">
111
+ <span class="mw-input-group-prefix"
112
+ ><i class="fas fa-euro-sign"></i
113
+ ></span>
114
+ <input
115
+ id="budget"
116
+ type="text"
117
+ inputmode="decimal"
118
+ class="mw-input mw-input-numeric"
119
+ placeholder="0,00"
120
+ formControlName="budget"
121
+ />
122
+ <span class="mw-input-group-suffix">EUR</span>
123
+ </div>
124
+ <span class="mw-field-hint">Comma as decimal separator.</span>
125
+ </div>
126
+
127
+ <div class="mw-field" [class.mw-field-has-error]="invalid('type')">
128
+ <label class="mw-field-label mw-required" for="type">Type</label>
129
+ <select id="type" class="mw-select" formControlName="type">
130
+ <option value="" disabled>Choose…</option>
131
+ <option value="internal">Internal</option>
132
+ <option value="customer">Customer project</option>
133
+ </select>
134
+ @if (invalid('type')) {
135
+ <span class="mw-field-error">
136
+ <i class="fas fa-exclamation-circle"></i> Please choose a type.
137
+ </span>
138
+ }
139
+ </div>
140
+
141
+ <!-- Full width inside the two-column grid -->
142
+ <div class="mw-field" style="grid-column: 1 / -1">
143
+ <label class="mw-field-label" for="description">Description</label>
144
+ <textarea
145
+ id="description"
146
+ class="mw-textarea mw-textarea-resizable-vertical"
147
+ rows="4"
148
+ formControlName="description"
149
+ ></textarea>
150
+ <span class="mw-field-hint">Markdown is allowed.</span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Group: settings -->
156
+ <div class="mw-form-group">
157
+ <p class="mw-text-muted mw-mb-3">Settings</p>
158
+
159
+ <div class="mw-grid-2">
160
+ <div>
161
+ <p class="mw-text-muted mw-text-sm mw-mb-2">Priority</p>
162
+ <div class="mw-radio-group mw-radio-group-inline">
163
+ @for (option of ['low', 'normal', 'high']; track option) {
164
+ <label class="mw-radio">
165
+ <input type="radio" [value]="option" formControlName="priority" />
166
+ <div class="mw-radio-button"></div>
167
+ <div class="mw-radio-label">{{ option | titlecase }}</div>
168
+ </label>
169
+ }
170
+ </div>
171
+ </div>
172
+
173
+ <div>
174
+ <p class="mw-text-muted mw-text-sm mw-mb-2">Notifications</p>
175
+ <div class="mw-toggle-group">
176
+ <label class="mw-toggle mw-toggle-success">
177
+ <input type="checkbox" formControlName="notify" />
178
+ <div class="mw-toggle-track"></div>
179
+ <span class="mw-toggle-label">Notify me about changes</span>
180
+ </label>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Slider: both bindings are needed, the framework computes nothing -->
185
+ <div class="mw-field" style="grid-column: 1 / -1">
186
+ <label class="mw-field-label" for="team">Team size</label>
187
+ <div class="mw-slider-container">
188
+ <div
189
+ class="mw-slider-value mw-slider-numeric"
190
+ [attr.data-value]="f.teamSize.value"
191
+ ></div>
192
+ <input
193
+ id="team"
194
+ type="range"
195
+ class="mw-slider mw-slider-primary"
196
+ min="1"
197
+ max="10"
198
+ formControlName="teamSize"
199
+ [style.--value.%]="f.teamSize.value * 10"
200
+ />
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Single control without a field wrapper -->
207
+ <div
208
+ class="mw-checkbox-group"
209
+ [class.mw-form-element-error]="invalid('terms')"
210
+ >
211
+ <label class="mw-checkbox">
212
+ <input type="checkbox" formControlName="terms" />
213
+ <div class="mw-checkbox-box"></div>
214
+ <div class="mw-checkbox-label">I accept the terms.</div>
215
+ </label>
216
+ </div>
217
+
218
+ <div class="mw-form-actions">
219
+ <p class="mw-form-actions-hint">Fields marked with * are required.</p>
220
+ <button type="button" class="mw-btn mw-btn-outline" (click)="form.reset()">
221
+ Reset
222
+ </button>
223
+ <button type="submit" class="mw-btn mw-btn-primary" [disabled]="saving()">
224
+ @if (saving()) {
225
+ <span class="mw-spinner-border mw-spinner-sm"></span> Saving… } @else {
226
+ <i class="fas fa-save"></i> Save }
227
+ </button>
228
+ </div>
229
+ </form>
230
+ ```
231
+
232
+ ## Notes
233
+
234
+ - **The error state is two bindings**: `mw-field-has-error` on the wrapper
235
+ (border and halo) and the `mw-field-error` element (the message). Hint and
236
+ error should not be visible at the same time.
237
+ - **For a control without a wrapper** - a checkbox group, radio group or slider
238
+ container - use `mw-form-element-error` instead.
239
+ - **`disabled`/`readonly` are attributes**, not classes. With reactive forms use
240
+ `control.disable()`; the muted look comes from the `:disabled` selector.
241
+ - **The slider needs two bindings**: `data-value` for the badge text,
242
+ `--value` for the filled part of the track.
243
+ - **A spinner inside a button** needs its shape class (`mw-spinner-border`) plus
244
+ a size; the button already provides the gap.
245
+ - Multi-column layout comes from `mw-grid-2` inside the group, a full-width
246
+ field from `style="grid-column: 1 / -1"`.
247
+
248
+ ## Number formatting
249
+
250
+ `mw-text-numeric` aligns from the right edge, so it works with both `1,204.50`
251
+ and `1.204,50` - what matters is a constant number of decimals per column. Let
252
+ the pipe do it:
253
+
254
+ ```html
255
+ <td class="mw-text-numeric">
256
+ {{ invoice.total | currency: 'EUR' : 'symbol' : '1.2-2' }}
257
+ </td>
258
+ <td class="mw-text-numeric">{{ item.amount | number: '1.2-2' }}</td>
259
+ ```
260
+
261
+ ```ts
262
+ // app.config.ts
263
+ import { registerLocaleData } from '@angular/common';
264
+ import localeDe from '@angular/common/locales/de';
265
+
266
+ registerLocaleData(localeDe);
267
+
268
+ export const appConfig: ApplicationConfig = {
269
+ providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }],
270
+ };
271
+ ```
272
+
273
+ `'1.2-2'` is the part that keeps the column aligned - it forces exactly two
274
+ decimals. `tabular-nums` only takes effect if the loaded font ships tabular
275
+ figures; the framework bundles no font.