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