@joanroucoux/cairn-ui 0.1.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.
@@ -0,0 +1,806 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, Component, output, inject, ElementRef, effect, afterRenderEffect, Directive, model, viewChildren, booleanAttribute } from '@angular/core';
3
+
4
+ /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
5
+ const AVATAR_SIZES = ['sm', 'md'];
6
+ const BASE_CLASSES$4 = 'inline-flex items-center justify-center rounded-full border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
7
+ const SIZE_CLASSES$1 = {
8
+ sm: 'size-8 text-xs',
9
+ md: 'size-11 text-sm',
10
+ };
11
+ /**
12
+ * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
13
+ *
14
+ * @example
15
+ * <ui-avatar initials="JR" label="My account" />
16
+ */
17
+ class UiAvatar {
18
+ initials = input.required(/* @ts-ignore */
19
+ ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
20
+ label = input.required(/* @ts-ignore */
21
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
22
+ size = input('md', /* @ts-ignore */
23
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
24
+ classes = computed(() => `${BASE_CLASSES$4} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
26
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
27
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: UiAvatar, isStandalone: true, selector: "ui-avatar", inputs: { initials: { classPropertyName: "initials", publicName: "initials", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "img" }, properties: { "attr.aria-label": "label()", "class": "classes()" } }, ngImport: i0, template: '<span aria-hidden="true">{{ initials() }}</span>', isInline: true });
28
+ }
29
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiAvatar, decorators: [{
30
+ type: Component,
31
+ args: [{
32
+ selector: 'ui-avatar',
33
+ template: '<span aria-hidden="true">{{ initials() }}</span>',
34
+ host: {
35
+ role: 'img',
36
+ '[attr.aria-label]': 'label()',
37
+ '[class]': 'classes()',
38
+ },
39
+ }]
40
+ }], propDecorators: { initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
41
+
42
+ /** Available badge variants. `BadgeVariant` is derived from this tuple. */
43
+ const BADGE_VARIANTS = ['primary', 'secondary', 'destructive', 'outline'];
44
+ const BASE_CLASSES$3 = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium';
45
+ const VARIANT_CLASSES$2 = {
46
+ primary: 'bg-(--primary) text-(--primary-foreground)',
47
+ secondary: 'bg-(--secondary) text-(--secondary-foreground)',
48
+ destructive: 'bg-(--destructive) text-(--destructive-foreground)',
49
+ outline: 'border border-(--border) text-(--foreground)',
50
+ };
51
+ /**
52
+ * Small status descriptor for counts, states or labels.
53
+ *
54
+ * @example
55
+ * <ui-badge variant="secondary">Draft</ui-badge>
56
+ */
57
+ class UiBadge {
58
+ variant = input('primary', /* @ts-ignore */
59
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
60
+ classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
61
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
62
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
63
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: UiBadge, isStandalone: true, selector: "ui-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
64
+ }
65
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiBadge, decorators: [{
66
+ type: Component,
67
+ args: [{
68
+ selector: 'ui-badge',
69
+ template: '<ng-content />',
70
+ host: {
71
+ '[class]': 'classes()',
72
+ },
73
+ }]
74
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
75
+
76
+ /** Available button variants. `ButtonVariant` is derived from this tuple. */
77
+ const BUTTON_VARIANTS = ['primary', 'secondary', 'outline', 'destructive', 'ghost'];
78
+ /** Available button sizes. `md` is the 44px touch target Cairn asks for everywhere. */
79
+ const BUTTON_SIZES = ['sm', 'md', 'lg'];
80
+ const BASE_CLASSES$2 =
81
+ // Tailwind's preflight sets buttons to cursor: default (matching the native element) since v3.4:
82
+ // cursor-pointer restores the hand cursor consumers expect from a clickable control.
83
+ 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-50';
84
+ const VARIANT_CLASSES$1 = {
85
+ primary: 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground) hover:opacity-90',
86
+ secondary: 'bg-(--secondary) text-(--secondary-foreground) hover:bg-(--secondary)/80',
87
+ outline: 'border border-(--border) bg-transparent hover:bg-(--soft)',
88
+ destructive: 'bg-(--destructive) text-(--destructive-foreground) hover:bg-(--destructive)/90',
89
+ ghost: 'hover:bg-(--accent) hover:text-(--accent-foreground)',
90
+ };
91
+ const SIZE_CLASSES = {
92
+ sm: 'h-9 px-3 text-[13px]',
93
+ md: 'h-11 px-4 text-sm',
94
+ lg: 'h-12 px-6 text-base',
95
+ };
96
+ /**
97
+ * Styled native button. Applied as an attribute so the host keeps every native
98
+ * button behavior (type, disabled, form submission, accessibility) for free.
99
+ *
100
+ * Also applies to an anchor, for the one case where the action really is a navigation:
101
+ * a download the browser has to perform itself. The element stays a link, and so does
102
+ * its role - never use this to make an ordinary link look like a form control.
103
+ *
104
+ * @example
105
+ * <button ui-button variant="destructive" size="sm">Delete</button>
106
+ * <a ui-button variant="outline" href="/api/holdings/export" download>Export</a>
107
+ */
108
+ class UiButton {
109
+ variant = input('primary', /* @ts-ignore */
110
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
111
+ size = input('md', /* @ts-ignore */
112
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
113
+ classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: UiButton, isStandalone: true, selector: "button[ui-button], a[ui-button]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
117
+ }
118
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiButton, decorators: [{
119
+ type: Component,
120
+ args: [{
121
+ selector: 'button[ui-button], a[ui-button]',
122
+ template: '<ng-content />',
123
+ host: {
124
+ '[class]': 'classes()',
125
+ },
126
+ }]
127
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
128
+
129
+ /** Available card variants. `CardVariant` is derived from this tuple. */
130
+ const CARD_VARIANTS = ['default', 'elevated'];
131
+ /** Available card paddings. `CardPadding` is derived from this tuple. */
132
+ const CARD_PADDINGS = ['none', 'sm', 'md'];
133
+ const BASE_CLASSES$1 = 'block rounded-xl border';
134
+ const VARIANT_CLASSES = {
135
+ default: 'bg-(--card) border-(--border) text-(--card-foreground)',
136
+ elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
137
+ };
138
+ const PADDING_CLASSES = {
139
+ none: 'p-0',
140
+ sm: 'p-3',
141
+ md: 'p-5',
142
+ };
143
+ /**
144
+ * Surface that groups related content: a summary block, a table, a side panel.
145
+ *
146
+ * @example
147
+ * <ui-card variant="elevated" padding="sm">Total</ui-card>
148
+ */
149
+ class UiCard {
150
+ variant = input('default', /* @ts-ignore */
151
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
152
+ padding = input('md', /* @ts-ignore */
153
+ ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
154
+ classes = computed(() => `${BASE_CLASSES$1} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
155
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: UiCard, isStandalone: true, selector: "ui-card", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
158
+ }
159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiCard, decorators: [{
160
+ type: Component,
161
+ args: [{
162
+ selector: 'ui-card',
163
+ template: '<ng-content />',
164
+ host: {
165
+ '[class]': 'classes()',
166
+ },
167
+ }]
168
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }] } });
169
+
170
+ /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
171
+ const DELTA_EMPHASES = ['text', 'pill'];
172
+ const BASE_CLASSES = 'inline-flex items-center gap-1 font-semibold tabular-nums';
173
+ const PILL_CLASSES = 'rounded-md px-2.5 py-1';
174
+ const TEXT_CLASSES = {
175
+ positive: 'text-(--positive)',
176
+ negative: 'text-(--negative)',
177
+ neutral: 'text-(--muted-foreground)',
178
+ unknown: 'text-(--subtle-foreground)',
179
+ };
180
+ const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
181
+ /**
182
+ * Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
183
+ *
184
+ * A null value is not a zero: a holding with no cost basis exists, and showing 0 would falsely
185
+ * claim a known gain of zero. Color never carries the sign on its own: the projected text must
186
+ * already contain a + or a minus sign.
187
+ *
188
+ * @example
189
+ * <ui-delta [value]="holding.dayChangeEur" unknownLabel="Not available">
190
+ * {{ holding.dayChangeEur | signedMoney }}
191
+ * </ui-delta>
192
+ */
193
+ class UiDelta {
194
+ value = input.required(/* @ts-ignore */
195
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
196
+ unknownLabel = input.required(/* @ts-ignore */
197
+ ...(ngDevMode ? [{ debugName: "unknownLabel" }] : /* istanbul ignore next */ []));
198
+ emphasis = input('text', /* @ts-ignore */
199
+ ...(ngDevMode ? [{ debugName: "emphasis" }] : /* istanbul ignore next */ []));
200
+ sign = computed(() => {
201
+ const value = this.value();
202
+ if (value === null || value === undefined) {
203
+ return 'unknown';
204
+ }
205
+ if (value > 0) {
206
+ return 'positive';
207
+ }
208
+ if (value < 0) {
209
+ return 'negative';
210
+ }
211
+ return 'neutral';
212
+ }, /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "sign" }] : /* istanbul ignore next */ []));
214
+ classes = computed(() => {
215
+ const sign = this.sign();
216
+ const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
217
+ return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
218
+ }, /* @ts-ignore */
219
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
220
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiDelta, isStandalone: true, selector: "ui-delta", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, unknownLabel: { classPropertyName: "unknownLabel", publicName: "unknownLabel", isSignal: true, isRequired: true, transformFunction: null }, emphasis: { classPropertyName: "emphasis", publicName: "emphasis", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
222
+ @if (sign() === 'unknown') {
223
+ <span aria-hidden="true">&mdash;</span>
224
+ <span class="sr-only">{{ unknownLabel() }}</span>
225
+ } @else {
226
+ <ng-content />
227
+ }
228
+ `, isInline: true });
229
+ }
230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDelta, decorators: [{
231
+ type: Component,
232
+ args: [{
233
+ selector: 'ui-delta',
234
+ template: `
235
+ @if (sign() === 'unknown') {
236
+ <span aria-hidden="true">&mdash;</span>
237
+ <span class="sr-only">{{ unknownLabel() }}</span>
238
+ } @else {
239
+ <ng-content />
240
+ }
241
+ `,
242
+ host: {
243
+ '[class]': 'classes()',
244
+ },
245
+ }]
246
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unknownLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unknownLabel", required: true }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }] } });
247
+
248
+ /** Available dialog widths. `DialogWidth` is derived from this tuple. */
249
+ const DIALOG_WIDTHS = ['md', 'lg'];
250
+ const WIDTH_CLASSES = {
251
+ md: 'max-w-md',
252
+ lg: 'max-w-lg',
253
+ };
254
+ const nextId$1 = (() => {
255
+ let count = 0;
256
+ return () => `ui-dialog-${++count}`;
257
+ })();
258
+ /**
259
+ * Modal dialog on the native <dialog>: focus trap, focus return, Escape and the top layer
260
+ * come from the platform. Two projection slots - the body, and [dialogActions] for the footer.
261
+ *
262
+ * @example
263
+ * <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
264
+ * <p>This cannot be undone.</p>
265
+ * <button dialogActions ui-button variant="outline" (click)="confirming.set(false)">Cancel</button>
266
+ * </ui-dialog>
267
+ */
268
+ class UiDialog {
269
+ heading = input.required(/* @ts-ignore */
270
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
271
+ description = input(/* @ts-ignore */
272
+ ...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
273
+ width = input('lg', /* @ts-ignore */
274
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
275
+ open = input(false, /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
277
+ dismissed = output();
278
+ headingId = nextId$1();
279
+ descriptionId = `${this.headingId}-description`;
280
+ classes = computed(() => `w-full ${WIDTH_CLASSES[this.width()]} rounded-xl border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
281
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
282
+ #host = inject(ElementRef);
283
+ constructor() {
284
+ effect(() => {
285
+ // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
286
+ const dialog = this.#host.nativeElement.querySelector('dialog');
287
+ if (this.open() && !dialog.open) {
288
+ dialog.showModal();
289
+ }
290
+ else if (!this.open() && dialog.open) {
291
+ dialog.close();
292
+ }
293
+ });
294
+ }
295
+ onNativeClose() {
296
+ // The owner already knows about a close it asked for; only a close it did not ask for is news.
297
+ if (this.open()) {
298
+ this.dismissed.emit();
299
+ }
300
+ }
301
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
302
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiDialog, isStandalone: true, selector: "ui-dialog", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
303
+ <dialog
304
+ [attr.aria-describedby]="description() ? descriptionId : null"
305
+ [attr.aria-labelledby]="headingId"
306
+ [class]="classes()"
307
+ (close)="onNativeClose()"
308
+ >
309
+ <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
310
+
311
+ @if (description()) {
312
+ <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
313
+ }
314
+
315
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
316
+ <div class="mt-4 flex flex-col gap-4 empty:hidden">
317
+ <ng-content />
318
+ </div>
319
+
320
+ <div class="mt-6 flex justify-end gap-3">
321
+ <ng-content select="[dialogActions]" />
322
+ </div>
323
+ </dialog>
324
+ `, isInline: true });
325
+ }
326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDialog, decorators: [{
327
+ type: Component,
328
+ args: [{
329
+ selector: 'ui-dialog',
330
+ template: `
331
+ <dialog
332
+ [attr.aria-describedby]="description() ? descriptionId : null"
333
+ [attr.aria-labelledby]="headingId"
334
+ [class]="classes()"
335
+ (close)="onNativeClose()"
336
+ >
337
+ <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
338
+
339
+ @if (description()) {
340
+ <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
341
+ }
342
+
343
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
344
+ <div class="mt-4 flex flex-col gap-4 empty:hidden">
345
+ <ng-content />
346
+ </div>
347
+
348
+ <div class="mt-6 flex justify-end gap-3">
349
+ <ng-content select="[dialogActions]" />
350
+ </div>
351
+ </dialog>
352
+ `,
353
+ }]
354
+ }], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
355
+
356
+ const nextId = (() => {
357
+ let count = 0;
358
+ return () => `ui-field-${++count}`;
359
+ })();
360
+ /**
361
+ * Label, control, hint and error as one unit. The control stays a plain native element:
362
+ * the field finds it in its own projected content and wires the ARIA attributes onto it.
363
+ *
364
+ * @example
365
+ * <ui-field label="Quantity" hint="Leave empty if unknown">
366
+ * <input uiInput type="number" />
367
+ * </ui-field>
368
+ */
369
+ class UiField {
370
+ label = input.required(/* @ts-ignore */
371
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
372
+ hint = input(/* @ts-ignore */
373
+ ...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
374
+ error = input(/* @ts-ignore */
375
+ ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
376
+ #id = nextId();
377
+ hintId = `${this.#id}-hint`;
378
+ errorId = `${this.#id}-error`;
379
+ #host = inject(ElementRef);
380
+ constructor() {
381
+ afterRenderEffect(() => {
382
+ // querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
383
+ const control = this.#host.nativeElement.querySelector('input, select, textarea');
384
+ if (!control) {
385
+ return;
386
+ }
387
+ control.id ||= this.#id;
388
+ this.#host.nativeElement.querySelector('label').htmlFor = control.id;
389
+ const describedBy = [this.hint() ? this.hintId : null, this.error() ? this.errorId : null].filter(Boolean);
390
+ if (describedBy.length > 0) {
391
+ control.setAttribute('aria-describedby', describedBy.join(' '));
392
+ }
393
+ else {
394
+ control.removeAttribute('aria-describedby');
395
+ }
396
+ if (this.error()) {
397
+ control.setAttribute('aria-invalid', 'true');
398
+ }
399
+ else {
400
+ control.removeAttribute('aria-invalid');
401
+ }
402
+ });
403
+ }
404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
405
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
406
+ <div class="flex flex-col gap-1.5">
407
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
408
+ <label class="text-[13px] font-medium text-(--foreground)">{{ label() }}</label>
409
+
410
+ <ng-content />
411
+
412
+ @if (hint()) {
413
+ <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
414
+ }
415
+
416
+ @if (error()) {
417
+ <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ error() }}</p>
418
+ }
419
+ </div>
420
+ `, isInline: true });
421
+ }
422
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiField, decorators: [{
423
+ type: Component,
424
+ args: [{
425
+ selector: 'ui-field',
426
+ template: `
427
+ <div class="flex flex-col gap-1.5">
428
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
429
+ <label class="text-[13px] font-medium text-(--foreground)">{{ label() }}</label>
430
+
431
+ <ng-content />
432
+
433
+ @if (hint()) {
434
+ <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
435
+ }
436
+
437
+ @if (error()) {
438
+ <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ error() }}</p>
439
+ }
440
+ </div>
441
+ `,
442
+ }]
443
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
444
+
445
+ /** Available control sizes. `md` is the 44px touch target. */
446
+ const CONTROL_SIZES = ['sm', 'md'];
447
+ const CONTROL_BASE_CLASSES = 'w-full rounded-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50';
448
+ const CONTROL_SIZE_CLASSES = {
449
+ sm: 'h-9',
450
+ md: 'h-11',
451
+ };
452
+ /**
453
+ * Styled native text input. A directive (not a wrapper component) so the host
454
+ * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
455
+ * and every native attribute keep working without any plumbing.
456
+ *
457
+ * @example
458
+ * <input uiInput type="email" placeholder="you@example.com" />
459
+ */
460
+ class UiInput {
461
+ size = input('md', /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
463
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
464
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
466
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
467
+ }
468
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiInput, decorators: [{
469
+ type: Directive,
470
+ args: [{
471
+ selector: 'input[uiInput]',
472
+ host: {
473
+ '[class]': 'classes()',
474
+ },
475
+ }]
476
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
477
+ /**
478
+ * Styled native textarea. Lives beside the input because it shares every one of its
479
+ * classes; it takes no size, since a textarea grows from a floor instead of sitting
480
+ * at a fixed height.
481
+ *
482
+ * @example
483
+ * <textarea uiTextarea maxlength="280"></textarea>
484
+ */
485
+ class UiTextarea {
486
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
487
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", host: { classAttribute: "w-full rounded-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50 min-h-24 py-2" }, ngImport: i0 });
488
+ }
489
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTextarea, decorators: [{
490
+ type: Directive,
491
+ args: [{
492
+ selector: 'textarea[uiTextarea]',
493
+ host: {
494
+ class: `${CONTROL_BASE_CLASSES} min-h-24 py-2`,
495
+ },
496
+ }]
497
+ }] });
498
+
499
+ /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
500
+ const METER_TONES = [1, 2, 3, 4, 5, 6];
501
+ const TONE_CLASSES = {
502
+ 1: 'bg-(--ramp-1)',
503
+ 2: 'bg-(--ramp-2)',
504
+ 3: 'bg-(--ramp-3)',
505
+ 4: 'bg-(--ramp-4)',
506
+ 5: 'bg-(--ramp-5)',
507
+ 6: 'bg-(--ramp-6)',
508
+ };
509
+ /**
510
+ * Proportion of a whole, drawn as a single bar. One bar per category, never a stacked
511
+ * strip: without hue to separate segments, length has to do the work alone.
512
+ *
513
+ * @example
514
+ * <ui-meter [value]="0.463" label="Funds" valueText="46.3% - 128,656.00 EUR" [tone]="1" />
515
+ */
516
+ class UiMeter {
517
+ value = input.required(/* @ts-ignore */
518
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
519
+ label = input.required(/* @ts-ignore */
520
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
521
+ valueText = input.required(/* @ts-ignore */
522
+ ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
523
+ tone = input(1, /* @ts-ignore */
524
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
525
+ percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
526
+ ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
527
+ fillClasses = computed(() => `h-full rounded-sm ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
528
+ ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
529
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
530
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
531
+ <div
532
+ class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
533
+ role="meter"
534
+ [attr.aria-label]="label()"
535
+ [attr.aria-valuemax]="1"
536
+ [attr.aria-valuemin]="0"
537
+ [attr.aria-valuenow]="value()"
538
+ [attr.aria-valuetext]="valueText()"
539
+ >
540
+ <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
541
+ </div>
542
+ `, isInline: true });
543
+ }
544
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiMeter, decorators: [{
545
+ type: Component,
546
+ args: [{
547
+ selector: 'ui-meter',
548
+ template: `
549
+ <div
550
+ class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
551
+ role="meter"
552
+ [attr.aria-label]="label()"
553
+ [attr.aria-valuemax]="1"
554
+ [attr.aria-valuemin]="0"
555
+ [attr.aria-valuenow]="value()"
556
+ [attr.aria-valuetext]="valueText()"
557
+ >
558
+ <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
559
+ </div>
560
+ `,
561
+ }]
562
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueText", required: true }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
563
+
564
+ const OPTION_CLASSES = 'min-h-11 flex-1 cursor-pointer rounded-md px-3 text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)';
565
+ const SELECTED_CLASSES = 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground)';
566
+ const UNSELECTED_CLASSES = 'bg-(--elevated) text-(--muted-foreground) hover:text-(--foreground)';
567
+ /**
568
+ * Exclusive choice within a small, known set: a time range, a unit, a mode.
569
+ *
570
+ * @example
571
+ * <ui-segmented [options]="ranges" label="Time range" [(value)]="range" />
572
+ */
573
+ class UiSegmented {
574
+ options = input.required(/* @ts-ignore */
575
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
576
+ label = input.required(/* @ts-ignore */
577
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
578
+ value = model.required(/* @ts-ignore */
579
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
580
+ radios = viewChildren('radio', /* @ts-ignore */
581
+ ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
582
+ optionClasses(optionValue) {
583
+ return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
584
+ }
585
+ tabIndexFor(optionValue) {
586
+ const opts = this.options();
587
+ const tabStopValue = opts.some((option) => option.value === this.value()) ? this.value() : opts[0]?.value;
588
+ return optionValue === tabStopValue ? 0 : -1;
589
+ }
590
+ select(optionValue) {
591
+ this.value.set(optionValue);
592
+ }
593
+ onKeydown(event) {
594
+ const delta = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];
595
+ if (delta === undefined) {
596
+ return;
597
+ }
598
+ event.preventDefault();
599
+ const opts = this.options();
600
+ const currentIndex = opts.findIndex((option) => option.value === this.value());
601
+ const nextIndex = (currentIndex + delta + opts.length) % opts.length;
602
+ const next = opts[nextIndex];
603
+ this.value.set(next.value);
604
+ this.radios()[nextIndex]?.nativeElement.focus();
605
+ }
606
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
607
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiSegmented, isStandalone: true, selector: "ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
608
+ <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
609
+ @for (option of options(); track option.value) {
610
+ <button
611
+ #radio
612
+ role="radio"
613
+ type="button"
614
+ [attr.aria-checked]="option.value === value()"
615
+ [attr.tabindex]="tabIndexFor(option.value)"
616
+ [class]="optionClasses(option.value)"
617
+ (click)="select(option.value)"
618
+ (keydown)="onKeydown($event)"
619
+ >
620
+ {{ option.label }}
621
+ </button>
622
+ }
623
+ </div>
624
+ `, isInline: true });
625
+ }
626
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSegmented, decorators: [{
627
+ type: Component,
628
+ args: [{
629
+ selector: 'ui-segmented',
630
+ template: `
631
+ <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
632
+ @for (option of options(); track option.value) {
633
+ <button
634
+ #radio
635
+ role="radio"
636
+ type="button"
637
+ [attr.aria-checked]="option.value === value()"
638
+ [attr.tabindex]="tabIndexFor(option.value)"
639
+ [class]="optionClasses(option.value)"
640
+ (click)="select(option.value)"
641
+ (keydown)="onKeydown($event)"
642
+ >
643
+ {{ option.label }}
644
+ </button>
645
+ }
646
+ </div>
647
+ `,
648
+ }]
649
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
650
+
651
+ const SELECT_CLASSES = 'bg-(--elevated) pr-8';
652
+ /**
653
+ * Styled native select. Deliberately keeps the platform's own chevron and picker:
654
+ * they are what makes the control usable on a phone.
655
+ *
656
+ * @example
657
+ * <select uiSelect><option value="pea">PEA</option></select>
658
+ */
659
+ class UiSelect {
660
+ size = input('md', /* @ts-ignore */
661
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
662
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
663
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
664
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
665
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
666
+ }
667
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSelect, decorators: [{
668
+ type: Directive,
669
+ args: [{
670
+ selector: 'select[uiSelect]',
671
+ host: {
672
+ '[class]': 'classes()',
673
+ },
674
+ }]
675
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
676
+
677
+ const LAST_LINE_WIDTH = '62%';
678
+ /**
679
+ * Decorative placeholder for content that is still loading. Deliberately hidden from
680
+ * assistive technology: the calling screen keeps its own role="status" sentence.
681
+ *
682
+ * @example
683
+ * <ui-skeleton [lines]="3" />
684
+ * <ui-skeleton [height]="196" />
685
+ */
686
+ class UiSkeleton {
687
+ lines = input(1, /* @ts-ignore */
688
+ ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
689
+ height = input(12, /* @ts-ignore */
690
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
691
+ bars = computed(() => {
692
+ const count = Math.max(1, this.lines());
693
+ return Array.from({ length: count }, (_, index) => (index === count - 1 && count > 1 ? LAST_LINE_WIDTH : '100%'));
694
+ }, /* @ts-ignore */
695
+ ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
696
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
697
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: `
698
+ @for (line of bars(); track $index) {
699
+ <span
700
+ class="block rounded bg-(--soft) motion-safe:animate-pulse"
701
+ [style.height.px]="height()"
702
+ [style.width]="line"
703
+ ></span>
704
+ }
705
+ `, isInline: true });
706
+ }
707
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSkeleton, decorators: [{
708
+ type: Component,
709
+ args: [{
710
+ selector: 'ui-skeleton',
711
+ template: `
712
+ @for (line of bars(); track $index) {
713
+ <span
714
+ class="block rounded bg-(--soft) motion-safe:animate-pulse"
715
+ [style.height.px]="height()"
716
+ [style.width]="line"
717
+ ></span>
718
+ }
719
+ `,
720
+ host: {
721
+ 'aria-hidden': 'true',
722
+ class: 'flex flex-col gap-2',
723
+ },
724
+ }]
725
+ }], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
726
+
727
+ /** Breakpoints a secondary column can be held back until. */
728
+ const CELL_BREAKPOINTS = ['md', 'lg'];
729
+ const FROM_CLASSES = {
730
+ md: 'hidden md:table-cell',
731
+ lg: 'hidden lg:table-cell',
732
+ };
733
+ const TH_CLASSES = 'px-3 py-2 align-bottom text-[11px] font-medium uppercase tracking-[0.05em] text-(--subtle-foreground)';
734
+ const TD_CLASSES = 'border-t border-(--hairline) px-3 py-2.5 align-middle text-sm';
735
+ const NUMERIC_CLASSES = 'text-right tabular-nums';
736
+ const TEXTUAL_CLASSES = 'text-left';
737
+ /** Styled native table. The rows and cells stay plain <tr> and <td>. */
738
+ class UiTable {
739
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
740
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
741
+ }
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTable, decorators: [{
743
+ type: Directive,
744
+ args: [{
745
+ selector: 'table[uiTable]',
746
+ host: {
747
+ class: 'w-full border-collapse',
748
+ },
749
+ }]
750
+ }] });
751
+ const cellClasses = (base, numeric, from) => [base, numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES, from ? FROM_CLASSES[from] : ''].filter(Boolean).join(' ');
752
+ /**
753
+ * Column header. `numeric` right-aligns and lines the digits up; `from` holds a
754
+ * secondary column back until the viewport is wide enough for it.
755
+ *
756
+ * @example
757
+ * <th uiTh numeric from="md">Prix de revient</th>
758
+ */
759
+ class UiTh {
760
+ numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
761
+ from = input(null, /* @ts-ignore */
762
+ ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
763
+ classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
764
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
766
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiTh, isStandalone: true, selector: "th[uiTh]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
767
+ }
768
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTh, decorators: [{
769
+ type: Directive,
770
+ args: [{
771
+ selector: 'th[uiTh]',
772
+ host: {
773
+ '[class]': 'classes()',
774
+ },
775
+ }]
776
+ }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
777
+ /** Body cell. Same two inputs as `UiTh`, and they must be set on both to match. */
778
+ class UiTd {
779
+ numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
780
+ from = input(null, /* @ts-ignore */
781
+ ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
782
+ classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
784
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
785
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiTd, isStandalone: true, selector: "td[uiTd]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
786
+ }
787
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTd, decorators: [{
788
+ type: Directive,
789
+ args: [{
790
+ selector: 'td[uiTd]',
791
+ host: {
792
+ '[class]': 'classes()',
793
+ },
794
+ }]
795
+ }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
796
+
797
+ /*
798
+ * Public API surface of the ui library.
799
+ */
800
+
801
+ /**
802
+ * Generated bundle index. Do not edit.
803
+ */
804
+
805
+ export { AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMeter, UiSegmented, UiSelect, UiSkeleton, UiTable, UiTd, UiTextarea, UiTh };
806
+ //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map