maverick-wave 3.3.0 → 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.
- package/.claude/skills/maverick-wave/SKILL.md +208 -0
- package/.claude/skills/maverick-wave/examples/angular-form.md +275 -0
- package/.claude/skills/maverick-wave/examples/angular-list-page.md +304 -0
- package/.claude/skills/maverick-wave/examples/angular-services.md +348 -0
- package/.claude/skills/maverick-wave/examples/static-landing-page.md +362 -0
- package/.claude/skills/maverick-wave/references/components.md +812 -0
- package/.claude/skills/maverick-wave/references/forms.md +287 -0
- package/.claude/skills/maverick-wave/references/javascript.md +70 -0
- package/.claude/skills/maverick-wave/references/layout.md +300 -0
- package/.claude/skills/maverick-wave/references/theming.md +192 -0
- package/.github/workflows/ci.yml +41 -0
- package/CHANGELOG.md +13 -0
- package/README.md +115 -21
- package/index.html +15 -9
- package/maverick-wave.min.css +1 -1
- package/maverick-wave.min.js +1 -1
- package/package.json +3 -1
- package/scripts/verify.js +137 -0
- package/src/js/main.js +23 -11
- package/src/partials/accordions-container.html +22 -27
- package/src/partials/alerts-container.html +26 -0
- package/src/partials/avatars-container.html +22 -0
- package/src/partials/buttons-container.html +11 -15
- package/src/partials/cards-container.html +8 -8
- package/src/partials/colors-container.html +33 -20
- package/src/partials/documentation-container.html +3 -0
- package/src/partials/form-field-container.html +16 -12
- package/src/partials/get-started-container.html +93 -35
- package/src/partials/header-utilities-container.html +64 -6
- package/src/partials/input-group-container.html +5 -3
- package/src/partials/page-header-container.html +78 -0
- package/src/partials/tables-container.html +130 -0
- package/src/partials/tabs-container.html +20 -16
- package/src/partials/tags-container.html +55 -1
- package/src/scss/abstracts/_index.scss +4 -1
- package/src/scss/abstracts/_variables.scss +128 -106
- package/src/scss/base/_base.scss +32 -136
- package/src/scss/base/_reset.scss +1 -1
- package/src/scss/base/_typography.scss +0 -92
- package/src/scss/components/_accordions.scss +3 -3
- package/src/scss/components/_avatars.scss +28 -1
- package/src/scss/components/_blog-post.scss +1 -1
- package/src/scss/components/_button-bar.scss +6 -6
- package/src/scss/components/_buttons.scss +33 -10
- package/src/scss/components/_cards.scss +7 -7
- package/src/scss/components/_coming-soon.scss +4 -4
- package/src/scss/components/_gallery.scss +2 -2
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/components/_info.scss +20 -3
- package/src/scss/components/_lists.scss +11 -10
- package/src/scss/components/_modals.scss +10 -3
- package/src/scss/components/_pagination.scss +3 -3
- package/src/scss/components/_panels.scss +4 -4
- package/src/scss/components/_progress.scss +1 -1
- package/src/scss/components/_ratings.scss +1 -1
- package/src/scss/components/_spinners.scss +2 -2
- package/src/scss/components/_stepper.scss +3 -2
- package/src/scss/components/_tables.scss +42 -1
- package/src/scss/components/_tabs.scss +5 -5
- package/src/scss/components/_tags.scss +43 -27
- package/src/scss/components/_tiles.scss +3 -3
- package/src/scss/components/_timelines.scss +2 -2
- package/src/scss/components/_toasts.scss +92 -0
- package/src/scss/form-elements/_form.scss +0 -1
- package/src/scss/form-elements/_input.scss +24 -0
- package/src/scss/form-elements/_toggle.scss +1 -1
- package/src/scss/layout/_footer.scss +4 -4
- package/src/scss/layout/_header.scss +78 -4
- package/src/scss/layout/_index.scss +1 -0
- package/src/scss/layout/_main.scss +1 -1
- package/src/scss/layout/_page-header.scss +48 -0
- package/src/scss/layout/_section.scss +15 -14
- package/src/scss/main.scss +4 -0
- package/src/scss/utilities/_flex.scss +4 -0
- package/src/scss/utilities/_index.scss +1 -0
- package/src/scss/utilities/_text.scss +105 -0
- 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.
|