maverick-wave 5.30.0 → 5.32.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 (147) hide show
  1. package/README.md +2 -2
  2. package/maverick-wave.min.css +2 -2
  3. package/package.json +8 -2
  4. package/src/scss/abstracts/_variables.scss +2 -0
  5. package/src/scss/components/_modals.scss +23 -9
  6. package/src/scss/components/_testimonial.scss +2 -0
  7. package/src/scss/layout/_home.scss +46 -0
  8. package/src/scss/layout/_main.scss +20 -0
  9. package/src/scss/layout/_parallax.scss +2 -2
  10. package/src/scss/main-lean.scss +8 -3
  11. package/.claude/settings.local.json +0 -43
  12. package/.claude/skills/mw-maverick-wave/SKILL.md +0 -532
  13. package/.claude/skills/mw-maverick-wave/examples/angular-form.md +0 -279
  14. package/.claude/skills/mw-maverick-wave/examples/angular-list-page.md +0 -309
  15. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +0 -538
  16. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +0 -530
  17. package/.claude/skills/mw-maverick-wave/references/components.md +0 -2170
  18. package/.claude/skills/mw-maverick-wave/references/forms.md +0 -358
  19. package/.claude/skills/mw-maverick-wave/references/javascript.md +0 -164
  20. package/.claude/skills/mw-maverick-wave/references/layout.md +0 -764
  21. package/.claude/skills/mw-maverick-wave/references/theming.md +0 -530
  22. package/.github/workflows/ci.yml +0 -41
  23. package/.idea/codeStyles/Project.xml +0 -59
  24. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  25. package/.idea/maverick-wave.iml +0 -12
  26. package/.idea/modules.xml +0 -8
  27. package/.idea/prettier.xml +0 -6
  28. package/.idea/vcs.xml +0 -6
  29. package/.prettierignore +0 -8
  30. package/.prettierrc.json +0 -7
  31. package/CHANGELOG.md +0 -956
  32. package/CLAUDE.md +0 -60
  33. package/gulpfile.js +0 -167
  34. package/index.html +0 -1730
  35. package/release.sh +0 -31
  36. package/scripts/verify.js +0 -252
  37. package/src/assets/favicon/apple-touch-icon.png +0 -0
  38. package/src/assets/favicon/favicon-96x96.png +0 -0
  39. package/src/assets/favicon/favicon.svg +0 -1
  40. package/src/assets/favicon/site.webmanifest +0 -37
  41. package/src/assets/favicon/web-app-manifest-192x192.png +0 -0
  42. package/src/assets/favicon/web-app-manifest-512x512.png +0 -0
  43. package/src/assets/favicon.ico +0 -0
  44. package/src/assets/header-logo.svg +0 -24
  45. package/src/assets/images/gallery-city.svg +0 -49
  46. package/src/assets/images/gallery-desert.svg +0 -25
  47. package/src/assets/images/gallery-forest.svg +0 -46
  48. package/src/assets/images/gallery-lake.svg +0 -26
  49. package/src/assets/images/gallery-mountain.svg +0 -19
  50. package/src/assets/images/gallery-ocean.svg +0 -26
  51. package/src/assets/images/gallery-oldtown.svg +0 -41
  52. package/src/assets/images/gallery-snow.svg +0 -39
  53. package/src/assets/images/photo-balloon.svg +0 -35
  54. package/src/assets/images/photo-forest-path.svg +0 -31
  55. package/src/assets/images/photo-lighthouse.svg +0 -42
  56. package/src/assets/images/photo-palm.svg +0 -27
  57. package/src/assets/images/photo-peak.svg +0 -27
  58. package/src/assets/images/photo-tower.svg +0 -34
  59. package/src/assets/images/photo-waterfall.svg +0 -32
  60. package/src/assets/images/slider-field.svg +0 -124
  61. package/src/assets/images/slider-forest.svg +0 -33
  62. package/src/assets/images/slider-lake.svg +0 -35
  63. package/src/assets/images/slider-mountains.svg +0 -29
  64. package/src/assets/images/slider-trees.svg +0 -41
  65. package/src/assets/images/story-aurora.svg +0 -36
  66. package/src/assets/images/story-beach.svg +0 -34
  67. package/src/assets/images/story-stars.svg +0 -35
  68. package/src/assets/images/tile-analytics.svg +0 -22
  69. package/src/assets/images/tile-cloud.svg +0 -19
  70. package/src/assets/images/tile-ecommerce.svg +0 -14
  71. package/src/assets/images/tile-enterprise.svg +0 -22
  72. package/src/assets/images/tile-marketing.svg +0 -16
  73. package/src/assets/media/demo-chime.wav +0 -0
  74. package/src/assets/media/story-sunrise.jpg +0 -0
  75. package/src/assets/media/story-sunrise.mp4 +0 -0
  76. package/src/partials/accordions-container.html +0 -179
  77. package/src/partials/alerts-container.html +0 -151
  78. package/src/partials/announcement-container.html +0 -67
  79. package/src/partials/avatars-container.html +0 -229
  80. package/src/partials/badges-container.html +0 -114
  81. package/src/partials/blog-posts-container.html +0 -158
  82. package/src/partials/breadcrumbs-container.html +0 -126
  83. package/src/partials/button-bar-container.html +0 -175
  84. package/src/partials/buttons-container.html +0 -272
  85. package/src/partials/calendar-container.html +0 -203
  86. package/src/partials/cards-container.html +0 -727
  87. package/src/partials/code-container.html +0 -194
  88. package/src/partials/colors-container.html +0 -263
  89. package/src/partials/coming-soon-container.html +0 -10
  90. package/src/partials/contact-container.html +0 -85
  91. package/src/partials/content-slider-container.html +0 -36
  92. package/src/partials/divider-container.html +0 -70
  93. package/src/partials/documentation-container.html +0 -204
  94. package/src/partials/dropdown-container.html +0 -119
  95. package/src/partials/empty-state-container.html +0 -79
  96. package/src/partials/feed-container.html +0 -506
  97. package/src/partials/footer-container.html +0 -113
  98. package/src/partials/form-container.html +0 -262
  99. package/src/partials/form-elements-container.html +0 -616
  100. package/src/partials/form-field-container.html +0 -166
  101. package/src/partials/gallery-container.html +0 -62
  102. package/src/partials/get-started-container.html +0 -423
  103. package/src/partials/grid-container.html +0 -191
  104. package/src/partials/header-container.html +0 -69
  105. package/src/partials/header-utilities-container.html +0 -331
  106. package/src/partials/highlights-container.html +0 -71
  107. package/src/partials/history-container.html +0 -67
  108. package/src/partials/home-container.html +0 -31
  109. package/src/partials/html-lists-container.html +0 -63
  110. package/src/partials/info-container.html +0 -104
  111. package/src/partials/input-group-container.html +0 -299
  112. package/src/partials/item-lists-container.html +0 -368
  113. package/src/partials/kanban-container.html +0 -459
  114. package/src/partials/kbd-container.html +0 -63
  115. package/src/partials/leader-row-container.html +0 -139
  116. package/src/partials/login-container.html +0 -65
  117. package/src/partials/lunch-menu-container.html +0 -341
  118. package/src/partials/media-container.html +0 -27
  119. package/src/partials/meta-info-container.html +0 -21
  120. package/src/partials/modals-container.html +0 -199
  121. package/src/partials/mosaic-container.html +0 -173
  122. package/src/partials/page-header-container.html +0 -78
  123. package/src/partials/pagination-container.html +0 -64
  124. package/src/partials/palette-container.html +0 -480
  125. package/src/partials/panels-container.html +0 -126
  126. package/src/partials/parallax-container.html +0 -147
  127. package/src/partials/portrait-gallery-container.html +0 -175
  128. package/src/partials/preview-container.html +0 -136
  129. package/src/partials/pricing-container.html +0 -523
  130. package/src/partials/progress-container.html +0 -230
  131. package/src/partials/prose-container.html +0 -43
  132. package/src/partials/ratings-container.html +0 -104
  133. package/src/partials/section-head-container.html +0 -77
  134. package/src/partials/segmented-container.html +0 -66
  135. package/src/partials/skeleton-container.html +0 -87
  136. package/src/partials/spinners-container.html +0 -96
  137. package/src/partials/stepper-container.html +0 -111
  138. package/src/partials/stories-container.html +0 -182
  139. package/src/partials/tables-container.html +0 -309
  140. package/src/partials/tabs-container.html +0 -290
  141. package/src/partials/tags-container.html +0 -242
  142. package/src/partials/techstack-bucket-container.html +0 -187
  143. package/src/partials/testimonials-container.html +0 -88
  144. package/src/partials/tiles-container.html +0 -247
  145. package/src/partials/timelines-container.html +0 -196
  146. package/src/partials/typography-container.html +0 -74
  147. package/src/partials/utilities-container.html +0 -553
@@ -1,279 +0,0 @@
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-actions-note">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
- - **Prefilled values** take `mw-prefilled` on the control, not on the wrapper.
242
- In an edit form that starts from `patchValue()`, bind it to your own flag:
243
- `[class.mw-prefilled]="loaded() && !form.controls.name.dirty"`. Repeat the
244
- information in the `mw-field-hint`, the marker itself is decoration.
245
- - **The slider needs two bindings**: `data-value` for the badge text,
246
- `--value` for the filled part of the track.
247
- - **A spinner inside a button** needs its shape class (`mw-spinner-border`) plus
248
- a size; the button already provides the gap.
249
- - Multi-column layout comes from `mw-grid-2` inside the group, a full-width
250
- field from `style="grid-column: 1 / -1"`.
251
-
252
- ## Number formatting
253
-
254
- `mw-text-numeric` aligns from the right edge, so it works with both `1,204.50`
255
- and `1.204,50` - what matters is a constant number of decimals per column. Let
256
- the pipe do it:
257
-
258
- ```html
259
- <td class="mw-text-numeric">
260
- {{ invoice.total | currency: 'EUR' : 'symbol' : '1.2-2' }}
261
- </td>
262
- <td class="mw-text-numeric">{{ item.amount | number: '1.2-2' }}</td>
263
- ```
264
-
265
- ```ts
266
- // app.config.ts
267
- import { registerLocaleData } from '@angular/common';
268
- import localeDe from '@angular/common/locales/de';
269
-
270
- registerLocaleData(localeDe);
271
-
272
- export const appConfig: ApplicationConfig = {
273
- providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }],
274
- };
275
- ```
276
-
277
- `'1.2-2'` is the part that keeps the column aligned - it forces exactly two
278
- decimals. `tabular-nums` only takes effect if the loaded font ships tabular
279
- figures; the framework bundles no font.
@@ -1,309 +0,0 @@
1
- # Example: list page with filters, table, modal and toasts
2
-
3
- A complete application page built only from framework classes: page header with
4
- counters and actions, a filter panel, a data table with status chips and a
5
- sticky header, loading and empty states, a delete confirmation modal and a toast
6
- stack.
7
-
8
- The Angular parts are signals, the new control flow and `inject()` - swap them
9
- for any other framework, the markup does not change.
10
-
11
- ## Component
12
-
13
- ```ts
14
- import {
15
- ChangeDetectionStrategy,
16
- Component,
17
- computed,
18
- inject,
19
- signal,
20
- } from '@angular/core';
21
- import { CurrencyPipe, DatePipe, TitleCasePipe } from '@angular/common';
22
- import { FormsModule } from '@angular/forms';
23
- import { RouterLink } from '@angular/router';
24
- import { InvoiceService } from './invoice.service';
25
- import { ToastService } from '../shared/toast.service';
26
-
27
- type Status = 'paid' | 'open' | 'cancelled' | 'draft';
28
-
29
- interface Invoice {
30
- id: string;
31
- number: string;
32
- customer: string;
33
- issuedAt: Date;
34
- total: number;
35
- status: Status;
36
- }
37
-
38
- @Component({
39
- selector: 'app-invoice-list',
40
- imports: [FormsModule, RouterLink, CurrencyPipe, DatePipe, TitleCasePipe],
41
- templateUrl: './invoice-list.component.html',
42
- changeDetection: ChangeDetectionStrategy.OnPush,
43
- })
44
- export class InvoiceListComponent {
45
- private readonly invoices = inject(InvoiceService);
46
- private readonly toasts = inject(ToastService);
47
-
48
- protected readonly loading = this.invoices.loading;
49
- protected readonly search = signal('');
50
- protected readonly status = signal<Status | 'all'>('all');
51
- protected readonly toDelete = signal<Invoice | null>(null);
52
-
53
- protected readonly filtered = computed(() => {
54
- const term = this.search().trim().toLowerCase();
55
- const status = this.status();
56
- return this.invoices
57
- .all()
58
- .filter(
59
- (invoice) =>
60
- (status === 'all' || invoice.status === status) &&
61
- (term === '' ||
62
- invoice.number.toLowerCase().includes(term) ||
63
- invoice.customer.toLowerCase().includes(term))
64
- );
65
- });
66
-
67
- protected readonly drafts = computed(
68
- () => this.invoices.all().filter((i) => i.status === 'draft').length
69
- );
70
-
71
- // Status → tag variant. Keep this mapping in TS, not in the template -
72
- // it is the only place that knows which colour means what.
73
- protected readonly tagClass: Record<Status, string> = {
74
- paid: 'mw-tag-success',
75
- open: 'mw-tag-warning',
76
- cancelled: 'mw-tag-danger',
77
- draft: 'mw-tag-muted',
78
- };
79
-
80
- protected resetFilters(): void {
81
- this.search.set('');
82
- this.status.set('all');
83
- }
84
-
85
- protected confirmDelete(): void {
86
- const invoice = this.toDelete();
87
- if (!invoice) return;
88
-
89
- this.invoices.remove(invoice.id).subscribe({
90
- next: () => {
91
- this.toasts.success(`Invoice ${invoice.number} deleted.`);
92
- this.toDelete.set(null);
93
- },
94
- error: () => this.toasts.error('Deleting failed. Please try again.'),
95
- });
96
- }
97
- }
98
- ```
99
-
100
- ## Template
101
-
102
- ```html
103
- <!-- Page header: title, counters, actions -->
104
- <header class="mw-page-header">
105
- <div>
106
- <h1>Invoices</h1>
107
- <div class="mw-meta-header">
108
- <div class="mw-meta-item">
109
- <i class="fas fa-list"></i><span>Total: {{ filtered().length }}</span>
110
- </div>
111
- <div class="mw-meta-item">
112
- <i class="fas fa-pen"></i><span>Drafts: {{ drafts() }}</span>
113
- </div>
114
- </div>
115
- </div>
116
-
117
- <div class="mw-page-header-actions">
118
- <button
119
- type="button"
120
- class="mw-btn mw-btn-outline mw-btn-sm"
121
- (click)="resetFilters()"
122
- >
123
- <i class="fas fa-rotate-left"></i> Reset
124
- </button>
125
- <button
126
- type="button"
127
- class="mw-btn mw-btn-primary mw-btn-sm"
128
- routerLink="new"
129
- >
130
- <i class="fas fa-plus"></i> New invoice
131
- </button>
132
- </div>
133
- </header>
134
-
135
- <!-- Filter bar -->
136
- <div class="mw-panel mw-mb-6">
137
- <h4 class="mw-panel-header">Filter</h4>
138
- <div class="mw-panel-body">
139
- <div class="mw-form">
140
- <div class="mw-grid-2">
141
- <div class="mw-field">
142
- <label class="mw-field-label" for="search">Search</label>
143
- <div class="mw-input-group">
144
- <span class="mw-input-group-prefix"
145
- ><i class="fas fa-search"></i
146
- ></span>
147
- <input
148
- id="search"
149
- type="text"
150
- class="mw-input"
151
- placeholder="Number or customer…"
152
- [ngModel]="search()"
153
- (ngModelChange)="search.set($event)"
154
- />
155
- </div>
156
- </div>
157
-
158
- <div class="mw-field">
159
- <label class="mw-field-label" for="status">Status</label>
160
- <select
161
- id="status"
162
- class="mw-select"
163
- [ngModel]="status()"
164
- (ngModelChange)="status.set($event)"
165
- >
166
- <option value="all">All</option>
167
- <option value="open">Open</option>
168
- <option value="paid">Paid</option>
169
- <option value="cancelled">Cancelled</option>
170
- <option value="draft">Draft</option>
171
- </select>
172
- </div>
173
- </div>
174
- </div>
175
- </div>
176
- </div>
177
-
178
- @if (loading()) {
179
- <!-- Loading -->
180
- <div class="mw-skeleton-card">
181
- <span class="mw-skeleton mw-skeleton-title"></span>
182
- <span class="mw-skeleton mw-skeleton-text"></span>
183
- <span class="mw-skeleton mw-skeleton-text"></span>
184
- <span class="mw-skeleton mw-skeleton-text"></span>
185
- </div>
186
- } @else if (filtered().length === 0) {
187
- <!-- Empty -->
188
- <div class="mw-empty-state mw-empty-state-primary">
189
- <div class="mw-empty-state-icon"><i class="fas fa-file-invoice"></i></div>
190
- <p class="mw-empty-state-title">No invoices found</p>
191
- <p class="mw-empty-state-desc">Adjust the filters or create a new invoice.</p>
192
- <button type="button" class="mw-btn mw-btn-outline" (click)="resetFilters()">
193
- Reset filters
194
- </button>
195
- </div>
196
- } @else {
197
- <!-- Table -->
198
- <div class="mw-table-responsive-scroll" style="--mw-table-scroll-height: 520px">
199
- <table class="mw-table mw-table-subtle mw-table-sticky-head mw-table-cards">
200
- <thead>
201
- <tr>
202
- <th>Number</th>
203
- <th>Customer</th>
204
- <th>Date</th>
205
- <th>Status</th>
206
- <th class="mw-text-numeric">Total</th>
207
- <th></th>
208
- </tr>
209
- </thead>
210
- <tbody>
211
- @for (invoice of filtered(); track invoice.id) {
212
- <tr>
213
- <td data-label="Number">{{ invoice.number }}</td>
214
- <td data-label="Customer">{{ invoice.customer }}</td>
215
- <td data-label="Date">{{ invoice.issuedAt | date: 'dd.MM.yyyy' }}</td>
216
- <td data-label="Status">
217
- <span class="mw-tag" [class]="tagClass[invoice.status]">
218
- {{ invoice.status | titlecase }}
219
- </span>
220
- </td>
221
- <td data-label="Total" class="mw-text-numeric">
222
- {{ invoice.total | currency: 'EUR' : 'symbol' : '1.2-2' }}
223
- </td>
224
- <td data-label="">
225
- <div class="mw-d-flex mw-gap-2 mw-justify-end">
226
- <button
227
- type="button"
228
- class="mw-btn-mini mw-btn-mini-primary"
229
- data-tooltip="Edit"
230
- [routerLink]="[invoice.id]"
231
- >
232
- <i class="fas fa-pen"></i>
233
- </button>
234
- <button
235
- type="button"
236
- class="mw-btn-mini mw-btn-mini-danger"
237
- data-tooltip="Delete"
238
- (click)="toDelete.set(invoice)"
239
- >
240
- <i class="fas fa-trash"></i>
241
- </button>
242
- </div>
243
- </td>
244
- </tr>
245
- }
246
- </tbody>
247
- </table>
248
- </div>
249
- }
250
-
251
- <!-- Delete confirmation - ModalDirective is in angular-services.md -->
252
- <dialog
253
- class="mw-modal mw-modal-sm"
254
- closedby="any"
255
- appModal
256
- [isOpen]="toDelete() !== null"
257
- (dismiss)="toDelete.set(null)"
258
- aria-labelledby="delete-title"
259
- >
260
- <div class="mw-modal-header">
261
- <h4 class="mw-modal-title" id="delete-title">Delete invoice</h4>
262
- <button type="button" class="mw-modal-close" (click)="toDelete.set(null)">
263
- &#120299;
264
- </button>
265
- </div>
266
- <div class="mw-modal-body">
267
- <p class="mw-text-center">
268
- <strong>Delete {{ toDelete()?.number }}?</strong>
269
- </p>
270
- <p class="mw-text-center mw-text-muted">This action cannot be undone.</p>
271
- </div>
272
- <div class="mw-modal-footer">
273
- <button
274
- type="button"
275
- class="mw-btn mw-btn-outline"
276
- (click)="toDelete.set(null)"
277
- >
278
- Cancel
279
- </button>
280
- <button
281
- type="button"
282
- class="mw-btn mw-btn-danger"
283
- (click)="confirmDelete()"
284
- >
285
- <i class="fas fa-trash"></i> Delete
286
- </button>
287
- </div>
288
- </dialog>
289
- ```
290
-
291
- ## Notes
292
-
293
- - **`mw-table-subtle` for data tables.** The default primary header bar is a
294
- landing-page look; over 200 rows it shouts louder than the data.
295
- - **Sticky needs a height cap.** `mw-table-sticky-head` only works inside
296
- `mw-table-responsive-scroll`; the height comes from
297
- `--mw-table-scroll-height` and can be set per table inline.
298
- - **`mw-table-cards` needs `data-label` on every cell**, including the action
299
- column (empty string is fine) - below `md` each row becomes a card and the
300
- label is rendered as the cell heading.
301
- - **`mw-text-numeric` on the header cell as well**, otherwise the column heading
302
- sits left while the figures sit right.
303
- - **`mw-tag`, not `mw-tags`**, for a single status chip in a cell.
304
- - **`mw-btn-danger` for the destructive action** in the modal footer.
305
- - The modal is always in the DOM; `ModalDirective` calls `showModal()` when the
306
- signal turns true, and Escape, the focus trap and the scroll lock follow from
307
- the element.
308
- - The toast stack lives in the app shell, not on this page - see
309
- `angular-services.md`.