maverick-wave 5.29.0 → 5.31.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 (144) 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/components/_avatars.scss +35 -3
  5. package/src/scss/components/_dropdown.scss +24 -1
  6. package/src/scss/components/_lang-switch.scss +4 -8
  7. package/src/scss/components/_modals.scss +33 -5
  8. package/.claude/settings.local.json +0 -43
  9. package/.claude/skills/mw-maverick-wave/SKILL.md +0 -532
  10. package/.claude/skills/mw-maverick-wave/examples/angular-form.md +0 -279
  11. package/.claude/skills/mw-maverick-wave/examples/angular-list-page.md +0 -309
  12. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +0 -538
  13. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +0 -530
  14. package/.claude/skills/mw-maverick-wave/references/components.md +0 -2169
  15. package/.claude/skills/mw-maverick-wave/references/forms.md +0 -358
  16. package/.claude/skills/mw-maverick-wave/references/javascript.md +0 -164
  17. package/.claude/skills/mw-maverick-wave/references/layout.md +0 -764
  18. package/.claude/skills/mw-maverick-wave/references/theming.md +0 -528
  19. package/.github/workflows/ci.yml +0 -41
  20. package/.idea/codeStyles/Project.xml +0 -59
  21. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  22. package/.idea/maverick-wave.iml +0 -12
  23. package/.idea/modules.xml +0 -8
  24. package/.idea/prettier.xml +0 -6
  25. package/.idea/vcs.xml +0 -6
  26. package/.prettierignore +0 -8
  27. package/.prettierrc.json +0 -7
  28. package/CHANGELOG.md +0 -950
  29. package/CLAUDE.md +0 -60
  30. package/gulpfile.js +0 -167
  31. package/index.html +0 -1730
  32. package/release.sh +0 -31
  33. package/scripts/verify.js +0 -252
  34. package/src/assets/favicon/apple-touch-icon.png +0 -0
  35. package/src/assets/favicon/favicon-96x96.png +0 -0
  36. package/src/assets/favicon/favicon.svg +0 -1
  37. package/src/assets/favicon/site.webmanifest +0 -37
  38. package/src/assets/favicon/web-app-manifest-192x192.png +0 -0
  39. package/src/assets/favicon/web-app-manifest-512x512.png +0 -0
  40. package/src/assets/favicon.ico +0 -0
  41. package/src/assets/header-logo.svg +0 -24
  42. package/src/assets/images/gallery-city.svg +0 -49
  43. package/src/assets/images/gallery-desert.svg +0 -25
  44. package/src/assets/images/gallery-forest.svg +0 -46
  45. package/src/assets/images/gallery-lake.svg +0 -26
  46. package/src/assets/images/gallery-mountain.svg +0 -19
  47. package/src/assets/images/gallery-ocean.svg +0 -26
  48. package/src/assets/images/gallery-oldtown.svg +0 -41
  49. package/src/assets/images/gallery-snow.svg +0 -39
  50. package/src/assets/images/photo-balloon.svg +0 -35
  51. package/src/assets/images/photo-forest-path.svg +0 -31
  52. package/src/assets/images/photo-lighthouse.svg +0 -42
  53. package/src/assets/images/photo-palm.svg +0 -27
  54. package/src/assets/images/photo-peak.svg +0 -27
  55. package/src/assets/images/photo-tower.svg +0 -34
  56. package/src/assets/images/photo-waterfall.svg +0 -32
  57. package/src/assets/images/slider-field.svg +0 -124
  58. package/src/assets/images/slider-forest.svg +0 -33
  59. package/src/assets/images/slider-lake.svg +0 -35
  60. package/src/assets/images/slider-mountains.svg +0 -29
  61. package/src/assets/images/slider-trees.svg +0 -41
  62. package/src/assets/images/story-aurora.svg +0 -36
  63. package/src/assets/images/story-beach.svg +0 -34
  64. package/src/assets/images/story-stars.svg +0 -35
  65. package/src/assets/images/tile-analytics.svg +0 -22
  66. package/src/assets/images/tile-cloud.svg +0 -19
  67. package/src/assets/images/tile-ecommerce.svg +0 -14
  68. package/src/assets/images/tile-enterprise.svg +0 -22
  69. package/src/assets/images/tile-marketing.svg +0 -16
  70. package/src/assets/media/demo-chime.wav +0 -0
  71. package/src/assets/media/story-sunrise.jpg +0 -0
  72. package/src/assets/media/story-sunrise.mp4 +0 -0
  73. package/src/partials/accordions-container.html +0 -179
  74. package/src/partials/alerts-container.html +0 -151
  75. package/src/partials/announcement-container.html +0 -67
  76. package/src/partials/avatars-container.html +0 -229
  77. package/src/partials/badges-container.html +0 -114
  78. package/src/partials/blog-posts-container.html +0 -158
  79. package/src/partials/breadcrumbs-container.html +0 -126
  80. package/src/partials/button-bar-container.html +0 -175
  81. package/src/partials/buttons-container.html +0 -272
  82. package/src/partials/calendar-container.html +0 -203
  83. package/src/partials/cards-container.html +0 -727
  84. package/src/partials/code-container.html +0 -194
  85. package/src/partials/colors-container.html +0 -263
  86. package/src/partials/coming-soon-container.html +0 -10
  87. package/src/partials/contact-container.html +0 -85
  88. package/src/partials/content-slider-container.html +0 -36
  89. package/src/partials/divider-container.html +0 -70
  90. package/src/partials/documentation-container.html +0 -204
  91. package/src/partials/dropdown-container.html +0 -119
  92. package/src/partials/empty-state-container.html +0 -79
  93. package/src/partials/feed-container.html +0 -506
  94. package/src/partials/footer-container.html +0 -113
  95. package/src/partials/form-container.html +0 -262
  96. package/src/partials/form-elements-container.html +0 -616
  97. package/src/partials/form-field-container.html +0 -166
  98. package/src/partials/gallery-container.html +0 -62
  99. package/src/partials/get-started-container.html +0 -423
  100. package/src/partials/grid-container.html +0 -191
  101. package/src/partials/header-container.html +0 -69
  102. package/src/partials/header-utilities-container.html +0 -331
  103. package/src/partials/highlights-container.html +0 -71
  104. package/src/partials/history-container.html +0 -67
  105. package/src/partials/home-container.html +0 -31
  106. package/src/partials/html-lists-container.html +0 -63
  107. package/src/partials/info-container.html +0 -104
  108. package/src/partials/input-group-container.html +0 -299
  109. package/src/partials/item-lists-container.html +0 -368
  110. package/src/partials/kanban-container.html +0 -459
  111. package/src/partials/kbd-container.html +0 -63
  112. package/src/partials/leader-row-container.html +0 -139
  113. package/src/partials/login-container.html +0 -65
  114. package/src/partials/lunch-menu-container.html +0 -341
  115. package/src/partials/media-container.html +0 -27
  116. package/src/partials/meta-info-container.html +0 -21
  117. package/src/partials/modals-container.html +0 -199
  118. package/src/partials/mosaic-container.html +0 -173
  119. package/src/partials/page-header-container.html +0 -78
  120. package/src/partials/pagination-container.html +0 -64
  121. package/src/partials/palette-container.html +0 -480
  122. package/src/partials/panels-container.html +0 -126
  123. package/src/partials/parallax-container.html +0 -147
  124. package/src/partials/portrait-gallery-container.html +0 -175
  125. package/src/partials/preview-container.html +0 -136
  126. package/src/partials/pricing-container.html +0 -523
  127. package/src/partials/progress-container.html +0 -230
  128. package/src/partials/prose-container.html +0 -43
  129. package/src/partials/ratings-container.html +0 -104
  130. package/src/partials/section-head-container.html +0 -77
  131. package/src/partials/segmented-container.html +0 -66
  132. package/src/partials/skeleton-container.html +0 -87
  133. package/src/partials/spinners-container.html +0 -96
  134. package/src/partials/stepper-container.html +0 -111
  135. package/src/partials/stories-container.html +0 -182
  136. package/src/partials/tables-container.html +0 -309
  137. package/src/partials/tabs-container.html +0 -290
  138. package/src/partials/tags-container.html +0 -242
  139. package/src/partials/techstack-bucket-container.html +0 -187
  140. package/src/partials/testimonials-container.html +0 -88
  141. package/src/partials/tiles-container.html +0 -247
  142. package/src/partials/timelines-container.html +0 -196
  143. package/src/partials/typography-container.html +0 -74
  144. 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`.