@joanroucoux/cairn-ui 0.1.5 → 0.3.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.
@@ -1,12 +1,205 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Component, InjectionToken, output, inject, ElementRef, effect, booleanAttribute, contentChild, afterRenderEffect, forwardRef, Directive, model, viewChildren } from '@angular/core';
2
+ import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, contentChildren, model, viewChildren } from '@angular/core';
3
+
4
+ const MINUS = '−';
5
+ const EM_DASH = '—';
6
+ const BULLETS = '••••';
7
+ /** Formats a number or an amount for the locale, with a missing value as an em dash and masking behind four bullets. */
8
+ function formatAmount(value, options, masked = false) {
9
+ if (value === null || value === undefined || Number.isNaN(value)) {
10
+ return EM_DASH;
11
+ }
12
+ const digits = options.fractionDigits ?? 2;
13
+ const format = new Intl.NumberFormat(options.locale, {
14
+ ...(options.currency ? { style: 'currency', currency: options.currency } : {}),
15
+ minimumFractionDigits: digits,
16
+ maximumFractionDigits: digits,
17
+ });
18
+ const magnitude = Math.abs(value);
19
+ const parts = format.formatToParts(magnitude);
20
+ if (masked) {
21
+ let replaced = false;
22
+ return parts
23
+ .map((part) => {
24
+ if (['integer', 'group', 'decimal', 'fraction'].includes(part.type)) {
25
+ if (replaced) {
26
+ return '';
27
+ }
28
+ replaced = true;
29
+ return BULLETS;
30
+ }
31
+ return part.value;
32
+ })
33
+ .join('');
34
+ }
35
+ const text = parts.map((part) => part.value).join('');
36
+ const isZero = Number(format.format(magnitude).replace(/[^\d]/g, '')) === 0;
37
+ if (!options.signed || isZero) {
38
+ return value < 0 && !isZero ? `${MINUS}${text}` : text;
39
+ }
40
+ return `${value > 0 ? '+' : MINUS}${text}`;
41
+ }
42
+
43
+ /** App-wide "amounts are hidden" state. Defaults to never masked; the app overrides it. */
44
+ const UI_AMOUNT_MASKED = new InjectionToken('UI_AMOUNT_MASKED', {
45
+ providedIn: 'root',
46
+ factory: () => signal(false),
47
+ });
48
+ /** Available figure styles. `AmountNumeric` is derived from this tuple. */
49
+ const AMOUNT_NUMERICS = ['tabular', 'proportional'];
50
+ /** Available masking behaviors. `AmountMasking` is derived from this tuple. */
51
+ const AMOUNT_MASKINGS = ['dots', 'hide'];
52
+ /**
53
+ * A number or an amount of money, formatted for the locale and masked when the app hides amounts.
54
+ *
55
+ * @example
56
+ * <ui-amount [value]="total" currency="EUR" numeric="proportional" />
57
+ * <ui-amount [value]="dayChange" currency="EUR" signed whenMasked="hide" />
58
+ */
59
+ class UiAmount {
60
+ value = input(/* @ts-ignore */
61
+ ...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
62
+ currency = input(/* @ts-ignore */
63
+ ...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
64
+ locale = input(inject(LOCALE_ID), /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
66
+ signed = input(false, { ...(ngDevMode ? { debugName: "signed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
67
+ fractionDigits = input(2, /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "fractionDigits" }] : /* istanbul ignore next */ []));
69
+ numeric = input('tabular', /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "numeric" }] : /* istanbul ignore next */ []));
71
+ whenMasked = input('dots', /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "whenMasked" }] : /* istanbul ignore next */ []));
73
+ #masked = inject(UI_AMOUNT_MASKED);
74
+ hidden = computed(() => this.#masked() && this.whenMasked() === 'hide', /* @ts-ignore */
75
+ ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
76
+ text = computed(() => formatAmount(this.value(), {
77
+ locale: this.locale(),
78
+ currency: this.currency(),
79
+ signed: this.signed(),
80
+ fractionDigits: this.fractionDigits(),
81
+ }, this.#masked()), /* @ts-ignore */
82
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
83
+ classes = computed(() => {
84
+ const missing = this.value() === null || this.value() === undefined;
85
+ return [
86
+ 'whitespace-nowrap',
87
+ this.numeric() === 'tabular' ? 'tabular-nums' : '',
88
+ missing ? 'text-(--subtle-foreground)' : '',
89
+ ]
90
+ .filter(Boolean)
91
+ .join(' ');
92
+ }, /* @ts-ignore */
93
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
94
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, deps: [], target: i0.ɵɵFactoryTarget.Component });
95
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiAmount, isStandalone: true, selector: "ui-amount", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, signed: { classPropertyName: "signed", publicName: "signed", isSignal: true, isRequired: false, transformFunction: null }, fractionDigits: { classPropertyName: "fractionDigits", publicName: "fractionDigits", isSignal: true, isRequired: false, transformFunction: null }, numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, whenMasked: { classPropertyName: "whenMasked", publicName: "whenMasked", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: '{{ text() }}', isInline: true });
96
+ }
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, decorators: [{
98
+ type: Component,
99
+ args: [{
100
+ selector: 'ui-amount',
101
+ template: '{{ text() }}',
102
+ host: {
103
+ '[class]': 'classes()',
104
+ '[attr.hidden]': 'hidden() || null',
105
+ },
106
+ }]
107
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], signed: [{ type: i0.Input, args: [{ isSignal: true, alias: "signed", required: false }] }], fractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "fractionDigits", required: false }] }], numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], whenMasked: [{ type: i0.Input, args: [{ isSignal: true, alias: "whenMasked", required: false }] }] } });
108
+
109
+ /** Available async block states. `AsyncState` is derived from this tuple. */
110
+ const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'];
111
+ const RETRY_BUTTON_CLASSES = 'mt-2 relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) min-h-(--row-min) px-4 text-label bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
112
+ /**
113
+ * The three states of a block that depends on a service call, next to the ready content.
114
+ *
115
+ * @example
116
+ * <ui-async [state]="totalState()" errorTitle="Total could not be loaded" errorMessage="The server did not answer." retryLabel="Retry" (retry)="reload()">
117
+ * <span asyncLoading><ui-skeleton shape="figure" /></span>
118
+ * <span asyncEmpty>Add a holding to see your total.</span>
119
+ * <span>{{ total() }}</span>
120
+ * </ui-async>
121
+ */
122
+ class UiAsync {
123
+ state = input.required(/* @ts-ignore */
124
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
125
+ errorTitle = input(/* @ts-ignore */
126
+ ...(ngDevMode ? [undefined, { debugName: "errorTitle" }] : /* istanbul ignore next */ []));
127
+ errorMessage = input(/* @ts-ignore */
128
+ ...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
129
+ retryLabel = input(/* @ts-ignore */
130
+ ...(ngDevMode ? [undefined, { debugName: "retryLabel" }] : /* istanbul ignore next */ []));
131
+ retry = output();
132
+ retryButtonClasses = RETRY_BUTTON_CLASSES;
133
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, deps: [], target: i0.ɵɵFactoryTarget.Component });
134
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiAsync, isStandalone: true, selector: "ui-async", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, errorTitle: { classPropertyName: "errorTitle", publicName: "errorTitle", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, retryLabel: { classPropertyName: "retryLabel", publicName: "retryLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { properties: { "attr.aria-busy": "state() === 'loading' || null" } }, ngImport: i0, template: `
135
+ @switch (state()) {
136
+ @case ('loading') {
137
+ <ng-content select="[asyncLoading]" />
138
+ }
139
+ @case ('error') {
140
+ <div
141
+ aria-live="polite"
142
+ class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
143
+ role="alert"
144
+ >
145
+ <span class="text-body font-medium">{{ errorTitle() }}</span>
146
+ <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
147
+ <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
148
+ {{ retryLabel() }}
149
+ </button>
150
+ </div>
151
+ }
152
+ @case ('empty') {
153
+ <ng-content select="[asyncEmpty]" />
154
+ }
155
+ @default {
156
+ <ng-content />
157
+ }
158
+ }
159
+ `, isInline: true });
160
+ }
161
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, decorators: [{
162
+ type: Component,
163
+ args: [{
164
+ selector: 'ui-async',
165
+ template: `
166
+ @switch (state()) {
167
+ @case ('loading') {
168
+ <ng-content select="[asyncLoading]" />
169
+ }
170
+ @case ('error') {
171
+ <div
172
+ aria-live="polite"
173
+ class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
174
+ role="alert"
175
+ >
176
+ <span class="text-body font-medium">{{ errorTitle() }}</span>
177
+ <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
178
+ <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
179
+ {{ retryLabel() }}
180
+ </button>
181
+ </div>
182
+ }
183
+ @case ('empty') {
184
+ <ng-content select="[asyncEmpty]" />
185
+ }
186
+ @default {
187
+ <ng-content />
188
+ }
189
+ }
190
+ `,
191
+ host: {
192
+ '[attr.aria-busy]': "state() === 'loading' || null",
193
+ },
194
+ }]
195
+ }], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], errorTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTitle", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], retryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "retryLabel", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
3
196
 
4
197
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
5
198
  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)';
199
+ const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
7
200
  const SIZE_CLASSES$1 = {
8
- sm: 'size-8 text-xs',
9
- md: 'size-11 text-sm',
201
+ sm: 'size-8 text-caption',
202
+ md: 'size-11 text-label',
10
203
  };
11
204
  /**
12
205
  * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
@@ -21,12 +214,12 @@ class UiAvatar {
21
214
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
22
215
  size = input('md', /* @ts-ignore */
23
216
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
24
- classes = computed(() => `${BASE_CLASSES$4} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
217
+ classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
25
218
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
26
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
27
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", 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 });
219
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
220
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", 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
221
  }
29
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiAvatar, decorators: [{
222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, decorators: [{
30
223
  type: Component,
31
224
  args: [{
32
225
  selector: 'ui-avatar',
@@ -40,29 +233,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
40
233
  }], 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
234
 
42
235
  /** 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';
236
+ const BADGE_VARIANTS = ['neutral', 'outline'];
237
+ const BASE_CLASSES$5 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
45
238
  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)',
239
+ neutral: 'bg-(--muted) text-(--foreground)',
240
+ outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
50
241
  };
51
242
  /**
52
- * Small status descriptor for counts, states or labels.
243
+ * Small status descriptor for counts, states or labels, such as an instrument's asset class.
53
244
  *
54
245
  * @example
55
- * <ui-badge variant="secondary">Draft</ui-badge>
246
+ * <ui-badge>ETF</ui-badge>
56
247
  */
57
248
  class UiBadge {
58
- variant = input('primary', /* @ts-ignore */
249
+ variant = input('neutral', /* @ts-ignore */
59
250
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
60
- classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
251
+ classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
61
252
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
62
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
63
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", 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 });
253
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
254
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", 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
255
  }
65
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiBadge, decorators: [{
256
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, decorators: [{
66
257
  type: Component,
67
258
  args: [{
68
259
  selector: 'ui-badge',
@@ -74,63 +265,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
74
265
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
75
266
 
76
267
  /** 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';
268
+ const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
269
+ /** Available button sizes. `md` is the touch target Cairn asks for everywhere. */
270
+ const BUTTON_SIZES = ['md', 'lg', 'xl', 'icon'];
271
+ const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
84
272
  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)',
273
+ primary: 'bg-(--primary) text-(--primary-foreground) hover:opacity-90',
274
+ outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)',
275
+ ghost: 'bg-transparent text-(--foreground) hover:bg-(--glow)',
276
+ destructive: 'bg-(--destructive) text-(--destructive-foreground) hover:opacity-90',
90
277
  };
91
278
  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',
279
+ md: 'min-h-(--row-min) px-4 text-label',
280
+ lg: 'h-11 px-[18px] text-label',
281
+ xl: 'h-[50px] px-6 text-body',
282
+ icon: 'size-(--row-min) p-0',
95
283
  };
96
284
  /**
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.
285
+ * Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
103
286
  *
104
287
  * @example
105
- * <button ui-button variant="destructive" size="sm">Delete</button>
106
- * <a ui-button variant="outline" href="/api/holdings/export" download>Export</a>
288
+ * <button ui-button variant="destructive">Delete</button>
289
+ * <button ui-button size="icon" variant="ghost" aria-label="More actions">...</button>
107
290
  */
108
291
  class UiButton {
109
292
  variant = input('primary', /* @ts-ignore */
110
293
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
111
294
  size = input('md', /* @ts-ignore */
112
295
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
113
- classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
296
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
297
+ classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
114
298
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
115
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
116
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", 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 });
299
+ constructor() {
300
+ const host = inject(ElementRef).nativeElement;
301
+ const guard = (event) => {
302
+ if (this.loading()) {
303
+ event.preventDefault();
304
+ event.stopImmediatePropagation();
305
+ }
306
+ };
307
+ host.addEventListener('click', guard, { capture: true });
308
+ inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
309
+ }
310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
311
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", 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 }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-busy": "loading() || null", "attr.aria-disabled": "loading() || null" } }, ngImport: i0, template: `
312
+ @if (loading()) {
313
+ <span
314
+ aria-hidden="true"
315
+ class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
316
+ ></span>
317
+ }
318
+ <ng-content />
319
+ `, isInline: true });
117
320
  }
118
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiButton, decorators: [{
321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, decorators: [{
119
322
  type: Component,
120
323
  args: [{
121
324
  selector: 'button[ui-button], a[ui-button]',
122
- template: '<ng-content />',
325
+ template: `
326
+ @if (loading()) {
327
+ <span
328
+ aria-hidden="true"
329
+ class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
330
+ ></span>
331
+ }
332
+ <ng-content />
333
+ `,
123
334
  host: {
124
335
  '[class]': 'classes()',
336
+ '[attr.aria-busy]': 'loading() || null',
337
+ '[attr.aria-disabled]': 'loading() || null',
125
338
  },
126
339
  }]
127
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
340
+ }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
128
341
 
129
342
  /** Available card variants. `CardVariant` is derived from this tuple. */
130
343
  const CARD_VARIANTS = ['default', 'elevated'];
131
344
  /** Available card paddings. `CardPadding` is derived from this tuple. */
132
345
  const CARD_PADDINGS = ['none', 'sm', 'md'];
133
- const BASE_CLASSES$1 = 'block rounded-xl border';
346
+ const BASE_CLASSES$3 = 'block rounded-container border';
134
347
  const VARIANT_CLASSES = {
135
348
  default: 'bg-(--card) border-(--border) text-(--card-foreground)',
136
349
  elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
@@ -151,12 +364,12 @@ class UiCard {
151
364
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
152
365
  padding = input('md', /* @ts-ignore */
153
366
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
154
- classes = computed(() => `${BASE_CLASSES$1} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
367
+ classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
155
368
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
156
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
157
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", 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 });
369
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
370
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", 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
371
  }
159
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiCard, decorators: [{
372
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, decorators: [{
160
373
  type: Component,
161
374
  args: [{
162
375
  selector: 'ui-card',
@@ -179,8 +392,8 @@ const UI_CONTROL = new InjectionToken('UI_CONTROL');
179
392
 
180
393
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
181
394
  const DELTA_EMPHASES = ['text', 'pill'];
182
- const BASE_CLASSES = 'inline-flex items-center gap-1 font-semibold tabular-nums';
183
- const PILL_CLASSES = 'rounded-md px-2.5 py-1';
395
+ const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
396
+ const PILL_CLASSES = 'h-6 rounded-pill px-2';
184
397
  const TEXT_CLASSES = {
185
398
  positive: 'text-(--positive)',
186
399
  negative: 'text-(--negative)',
@@ -224,11 +437,11 @@ class UiDelta {
224
437
  classes = computed(() => {
225
438
  const sign = this.sign();
226
439
  const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
227
- return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
440
+ return `${BASE_CLASSES$2} ${TEXT_CLASSES[sign]} ${background}`;
228
441
  }, /* @ts-ignore */
229
442
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
230
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
231
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", 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: `
443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
444
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", 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: `
232
445
  @if (sign() === 'unknown') {
233
446
  <span aria-hidden="true">&mdash;</span>
234
447
  <span class="sr-only">{{ unknownLabel() }}</span>
@@ -237,7 +450,7 @@ class UiDelta {
237
450
  }
238
451
  `, isInline: true });
239
452
  }
240
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiDelta, decorators: [{
453
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, decorators: [{
241
454
  type: Component,
242
455
  args: [{
243
456
  selector: 'ui-delta',
@@ -259,15 +472,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
259
472
  const DIALOG_WIDTHS = ['md', 'lg'];
260
473
  const WIDTH_CLASSES = {
261
474
  md: 'max-w-md',
262
- lg: 'max-w-lg',
475
+ lg: 'max-w-[560px]',
263
476
  };
264
- const nextId$1 = (() => {
477
+ const nextId$2 = (() => {
265
478
  let count = 0;
266
479
  return () => `ui-dialog-${++count}`;
267
480
  })();
268
481
  /**
269
- * Modal dialog on the native <dialog>: focus trap, focus return, Escape and the top layer
270
- * come from the platform. Two projection slots - the body, and [dialogActions] for the footer.
482
+ * Modal dialog on the native <dialog>: a sheet rising from the bottom under `64rem`, centered
483
+ * above it. Two projection slots - the body, and [dialogActions] for the footer.
271
484
  *
272
485
  * @example
273
486
  * <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
@@ -282,12 +495,11 @@ class UiDialog {
282
495
  ...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
283
496
  width = input('lg', /* @ts-ignore */
284
497
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
285
- open = input(false, /* @ts-ignore */
286
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
498
+ open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
287
499
  dismissed = output();
288
- headingId = nextId$1();
500
+ headingId = nextId$2();
289
501
  descriptionId = `${this.headingId}-description`;
290
- classes = computed(() => `w-full m-auto ${WIDTH_CLASSES[this.width()]} rounded-xl border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
502
+ classes = computed(() => `flex w-full flex-col m-auto mt-auto max-h-[calc(100dvh-2rem)] max-lg:mb-0 max-lg:max-w-none max-lg:rounded-b-none rounded-container ${WIDTH_CLASSES[this.width()]} border border-(--border) bg-(--card) text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
291
503
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
292
504
  #host = inject(ElementRef);
293
505
  constructor() {
@@ -308,62 +520,81 @@ class UiDialog {
308
520
  this.dismissed.emit();
309
521
  }
310
522
  }
311
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", 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: `
523
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
524
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", 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: `
313
525
  <dialog
314
526
  [attr.aria-describedby]="description() ? descriptionId : null"
315
527
  [attr.aria-labelledby]="headingId"
316
528
  [class]="classes()"
317
529
  (close)="onNativeClose()"
318
530
  >
319
- <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
531
+ <div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
532
+ <span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
533
+ </div>
320
534
 
321
- @if (description()) {
322
- <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
323
- }
535
+ <div class="px-6 pt-1 lg:pt-6">
536
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
324
537
 
325
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
326
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
327
- <ng-content />
538
+ @if (description()) {
539
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
540
+ }
328
541
  </div>
329
542
 
330
- <div class="mt-6 flex justify-end gap-3">
543
+ <div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
544
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
545
+ <div class="mt-4 flex flex-col gap-4 empty:hidden">
546
+ <ng-content />
547
+ </div>
548
+ </div>
549
+
550
+ <div
551
+ class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
552
+ data-dialog-footer
553
+ >
331
554
  <ng-content select="[dialogActions]" />
332
555
  </div>
333
556
  </dialog>
334
- `, isInline: true });
557
+ `, isInline: true, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] });
335
558
  }
336
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiDialog, decorators: [{
559
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
337
560
  type: Component,
338
- args: [{
339
- selector: 'ui-dialog',
340
- template: `
561
+ args: [{ selector: 'ui-dialog', template: `
341
562
  <dialog
342
563
  [attr.aria-describedby]="description() ? descriptionId : null"
343
564
  [attr.aria-labelledby]="headingId"
344
565
  [class]="classes()"
345
566
  (close)="onNativeClose()"
346
567
  >
347
- <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
568
+ <div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
569
+ <span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
570
+ </div>
348
571
 
349
- @if (description()) {
350
- <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
351
- }
572
+ <div class="px-6 pt-1 lg:pt-6">
573
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
352
574
 
353
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
354
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
355
- <ng-content />
575
+ @if (description()) {
576
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
577
+ }
578
+ </div>
579
+
580
+ <div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
581
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
582
+ <div class="mt-4 flex flex-col gap-4 empty:hidden">
583
+ <ng-content />
584
+ </div>
356
585
  </div>
357
586
 
358
- <div class="mt-6 flex justify-end gap-3">
587
+ <div
588
+ class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
589
+ data-dialog-footer
590
+ >
359
591
  <ng-content select="[dialogActions]" />
360
592
  </div>
361
593
  </dialog>
362
- `,
363
- }]
594
+ `, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] }]
364
595
  }], 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"] }] } });
365
596
 
366
- const nextId = (() => {
597
+ const nextId$1 = (() => {
367
598
  let count = 0;
368
599
  return () => `ui-field-${++count}`;
369
600
  })();
@@ -397,13 +628,15 @@ class UiField {
397
628
  ...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
398
629
  error = input(/* @ts-ignore */
399
630
  ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
400
- #id = nextId();
631
+ unit = input(/* @ts-ignore */
632
+ ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
633
+ #id = nextId$1();
401
634
  hintId = `${this.#id}-hint`;
402
635
  errorId = `${this.#id}-error`;
403
636
  #host = inject(ElementRef);
404
637
  control = contentChild(UI_CONTROL, /* @ts-ignore */
405
638
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
406
- labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-[13px] font-medium text-(--foreground)', /* @ts-ignore */
639
+ labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label font-medium text-(--muted-foreground)', /* @ts-ignore */
407
640
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
408
641
  /**
409
642
  * The caller's own `error` wins: it is the only way to report something the control does not
@@ -445,25 +678,31 @@ class UiField {
445
678
  }
446
679
  });
447
680
  }
448
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
449
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }], ngImport: i0, template: `
681
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
682
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }], ngImport: i0, template: `
450
683
  <div class="group flex flex-col gap-1.5">
451
684
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
452
685
  <label [class]="labelClasses()">{{ label() }}</label>
453
686
 
454
- <ng-content />
687
+ <div class="relative flex items-center">
688
+ <ng-content />
689
+
690
+ @if (unit()) {
691
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
692
+ }
693
+ </div>
455
694
 
456
695
  @if (hint()) {
457
- <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
696
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
458
697
  }
459
698
 
460
699
  @if (message()) {
461
- <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
700
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
462
701
  }
463
702
  </div>
464
703
  `, isInline: true });
465
704
  }
466
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiField, decorators: [{
705
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, decorators: [{
467
706
  type: Component,
468
707
  args: [{
469
708
  selector: 'ui-field',
@@ -472,26 +711,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
472
711
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
473
712
  <label [class]="labelClasses()">{{ label() }}</label>
474
713
 
475
- <ng-content />
714
+ <div class="relative flex items-center">
715
+ <ng-content />
716
+
717
+ @if (unit()) {
718
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
719
+ }
720
+ </div>
476
721
 
477
722
  @if (hint()) {
478
- <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
723
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
479
724
  }
480
725
 
481
726
  @if (message()) {
482
- <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
727
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
483
728
  }
484
729
  </div>
485
730
  `,
486
731
  }]
487
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }] } });
732
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }] } });
488
733
 
489
- /** Available control sizes. `md` is the 44px touch target. */
734
+ /** Available control sizes. `md` is the 44/40px touch target. */
490
735
  const CONTROL_SIZES = ['sm', 'md'];
491
- 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) enabled:hover:outline-2 enabled:hover:outline-offset-2 enabled:hover:outline-(--ring) enabled:group-hover:outline-2 enabled:group-hover:outline-offset-2 enabled:group-hover:outline-(--ring) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50';
736
+ const CONTROL_BASE_CLASSES = 'w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40';
492
737
  const CONTROL_SIZE_CLASSES = {
493
738
  sm: 'h-9',
494
- md: 'h-11',
739
+ md: 'min-h-(--row-min)',
495
740
  };
496
741
  /**
497
742
  * Styled native text input. A directive (not a wrapper component) so the host
@@ -513,10 +758,10 @@ class UiInput {
513
758
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
514
759
  classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
515
760
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
516
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
517
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
761
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
762
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
518
763
  }
519
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiInput, decorators: [{
764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
520
765
  type: Directive,
521
766
  args: [{
522
767
  selector: 'input[uiInput]',
@@ -542,10 +787,10 @@ class UiTextarea {
542
787
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
543
788
  touched = input(false, /* @ts-ignore */
544
789
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
545
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
546
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) enabled:hover:outline-2 enabled:hover:outline-offset-2 enabled:hover:outline-(--ring) enabled:group-hover:outline-2 enabled:group-hover:outline-offset-2 enabled:group-hover:outline-(--ring) 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" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
790
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
791
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40 min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
547
792
  }
548
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTextarea, decorators: [{
793
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
549
794
  type: Directive,
550
795
  args: [{
551
796
  selector: 'textarea[uiTextarea]',
@@ -556,6 +801,192 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
556
801
  }]
557
802
  }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
558
803
 
804
+ const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
805
+ const ITEM_CLASSES = 'flex w-full items-center gap-2 min-h-(--row-min) px-3 rounded-control text-label hover:bg-(--soft) focus-visible:bg-(--soft) outline-none';
806
+ const nextId = (() => {
807
+ let count = 0;
808
+ return () => `ui-menu-${++count}`;
809
+ })();
810
+ /**
811
+ * One action of a `ui-menu`; `destructive` paints it in `--negative`.
812
+ *
813
+ * @example
814
+ * <button uiMenuItem destructive (click)="remove()">Delete the line</button>
815
+ */
816
+ class UiMenuItem {
817
+ destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
818
+ #host = inject(ElementRef).nativeElement;
819
+ #menu = inject(UiMenu);
820
+ classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
821
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
822
+ focus() {
823
+ this.#host.focus();
824
+ }
825
+ hasFocus() {
826
+ return this.#host === document.activeElement;
827
+ }
828
+ onClick() {
829
+ this.#menu.close();
830
+ }
831
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
832
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuItem, isStandalone: true, selector: "button[uiMenuItem]", inputs: { destructive: { classPropertyName: "destructive", publicName: "destructive", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
833
+ }
834
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, decorators: [{
835
+ type: Directive,
836
+ args: [{
837
+ selector: 'button[uiMenuItem]',
838
+ host: {
839
+ role: 'menuitem',
840
+ tabindex: '-1',
841
+ '[class]': 'classes()',
842
+ '(click)': 'onClick()',
843
+ },
844
+ }]
845
+ }], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
846
+ /**
847
+ * Secondary actions menu, opened from its trigger through the native Popover API.
848
+ *
849
+ * @example
850
+ * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
851
+ * <ui-menu #menu label="Line actions">
852
+ * <button uiMenuItem (click)="edit()">Edit</button>
853
+ * <button uiMenuItem destructive (click)="remove()">Delete the line</button>
854
+ * </ui-menu>
855
+ */
856
+ class UiMenu {
857
+ label = input.required(/* @ts-ignore */
858
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
859
+ menuId = nextId();
860
+ isOpen = signal(false, /* @ts-ignore */
861
+ ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
862
+ classes = MENU_CLASSES;
863
+ items = contentChildren(UiMenuItem, /* @ts-ignore */
864
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
865
+ #host = inject(ElementRef).nativeElement;
866
+ #trigger = null;
867
+ open(trigger) {
868
+ this.#trigger = trigger;
869
+ this.isOpen.set(true);
870
+ if (typeof this.#host.showPopover === 'function') {
871
+ this.#host.showPopover();
872
+ }
873
+ this.#position(trigger);
874
+ this.items()[0]?.focus();
875
+ }
876
+ close() {
877
+ const wasOpen = this.isOpen();
878
+ this.#applyClosed();
879
+ if (wasOpen && typeof this.#host.hidePopover === 'function') {
880
+ this.#host.hidePopover();
881
+ }
882
+ }
883
+ #applyClosed() {
884
+ this.isOpen.set(false);
885
+ this.#trigger?.focus();
886
+ this.#trigger = null;
887
+ }
888
+ onToggle(event) {
889
+ if (event.newState === 'closed') {
890
+ this.#applyClosed();
891
+ }
892
+ }
893
+ onKeydown(event) {
894
+ if (event.key === 'Tab' || event.key === 'Escape') {
895
+ event.preventDefault();
896
+ this.close();
897
+ return;
898
+ }
899
+ const items = this.items();
900
+ const currentIndex = items.findIndex((item) => item.hasFocus());
901
+ const lastIndex = items.length - 1;
902
+ let nextIndex;
903
+ switch (event.key) {
904
+ case 'ArrowDown':
905
+ nextIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;
906
+ break;
907
+ case 'ArrowUp':
908
+ nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;
909
+ break;
910
+ case 'Home':
911
+ nextIndex = 0;
912
+ break;
913
+ case 'End':
914
+ nextIndex = lastIndex;
915
+ break;
916
+ }
917
+ if (nextIndex === undefined) {
918
+ return;
919
+ }
920
+ event.preventDefault();
921
+ items[nextIndex]?.focus();
922
+ }
923
+ #position(trigger) {
924
+ const margin = 8;
925
+ const triggerRect = trigger.getBoundingClientRect();
926
+ const menuRect = this.#host.getBoundingClientRect();
927
+ const fitsBelow = window.innerHeight - triggerRect.bottom >= menuRect.height + margin;
928
+ const top = fitsBelow ? triggerRect.bottom + 4 : Math.max(margin, triggerRect.top - menuRect.height - 4);
929
+ const left = Math.min(Math.max(triggerRect.right - menuRect.width, margin), window.innerWidth - menuRect.width - margin);
930
+ this.#host.style.position = 'fixed';
931
+ this.#host.style.top = `${Math.min(top, window.innerHeight - menuRect.height - margin)}px`;
932
+ this.#host.style.left = `${left}px`;
933
+ this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
934
+ }
935
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
936
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: UiMenu, isStandalone: true, selector: "ui-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" }, listeners: { "toggle": "onToggle($event)", "keydown": "onKeydown($event)" }, properties: { "id": "menuId", "class": "classes", "attr.aria-label": "label()" } }, queries: [{ propertyName: "items", predicate: UiMenuItem, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true });
937
+ }
938
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
939
+ type: Component,
940
+ args: [{
941
+ selector: 'ui-menu',
942
+ template: '<ng-content />',
943
+ host: {
944
+ role: 'menu',
945
+ popover: 'auto',
946
+ '[id]': 'menuId',
947
+ '[class]': 'classes',
948
+ '[attr.aria-label]': 'label()',
949
+ '(toggle)': 'onToggle($event)',
950
+ '(keydown)': 'onKeydown($event)',
951
+ },
952
+ }]
953
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
954
+ /**
955
+ * Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
956
+ * `aria-controls` on it.
957
+ *
958
+ * @example
959
+ * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
960
+ */
961
+ class UiMenuTrigger {
962
+ uiMenuTrigger = input.required(/* @ts-ignore */
963
+ ...(ngDevMode ? [{ debugName: "uiMenuTrigger" }] : /* istanbul ignore next */ []));
964
+ #host = inject(ElementRef).nativeElement;
965
+ toggle() {
966
+ const menu = this.uiMenuTrigger();
967
+ if (menu.isOpen()) {
968
+ menu.close();
969
+ }
970
+ else {
971
+ menu.open(this.#host);
972
+ }
973
+ }
974
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
975
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuTrigger, isStandalone: true, selector: "[uiMenuTrigger]", inputs: { uiMenuTrigger: { classPropertyName: "uiMenuTrigger", publicName: "uiMenuTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "menu" }, listeners: { "click": "toggle()" }, properties: { "attr.aria-expanded": "uiMenuTrigger().isOpen()", "attr.aria-controls": "uiMenuTrigger().menuId" } }, ngImport: i0 });
976
+ }
977
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, decorators: [{
978
+ type: Directive,
979
+ args: [{
980
+ selector: '[uiMenuTrigger]',
981
+ host: {
982
+ 'aria-haspopup': 'menu',
983
+ '[attr.aria-expanded]': 'uiMenuTrigger().isOpen()',
984
+ '[attr.aria-controls]': 'uiMenuTrigger().menuId',
985
+ '(click)': 'toggle()',
986
+ },
987
+ }]
988
+ }], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
989
+
559
990
  /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
560
991
  const METER_TONES = [1, 2, 3, 4, 5, 6];
561
992
  const TONE_CLASSES = {
@@ -584,12 +1015,12 @@ class UiMeter {
584
1015
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
585
1016
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
586
1017
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
587
- fillClasses = computed(() => `h-full rounded-sm ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
1018
+ fillClasses = computed(() => `h-full rounded-control ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
588
1019
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
589
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
590
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", 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: `
1020
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1021
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", 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: `
591
1022
  <div
592
- class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
1023
+ class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
593
1024
  role="meter"
594
1025
  [attr.aria-label]="label()"
595
1026
  [attr.aria-valuemax]="1"
@@ -601,13 +1032,13 @@ class UiMeter {
601
1032
  </div>
602
1033
  `, isInline: true });
603
1034
  }
604
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiMeter, decorators: [{
1035
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, decorators: [{
605
1036
  type: Component,
606
1037
  args: [{
607
1038
  selector: 'ui-meter',
608
1039
  template: `
609
1040
  <div
610
- class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
1041
+ class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
611
1042
  role="meter"
612
1043
  [attr.aria-label]="label()"
613
1044
  [attr.aria-valuemax]="1"
@@ -621,11 +1052,71 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
621
1052
  }]
622
1053
  }], 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 }] }] } });
623
1054
 
624
- 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)';
625
- const SELECTED_CLASSES = 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground)';
626
- const UNSELECTED_CLASSES = 'bg-(--elevated) text-(--muted-foreground) hover:text-(--foreground)';
1055
+ const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label font-medium text-(--muted-foreground) transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1056
+ const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
627
1057
  /**
628
- * Exclusive choice within a small, known set: a time range, a unit, a mode.
1058
+ * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
1059
+ *
1060
+ * @example
1061
+ * <a ui-nav-item [active]="url === '/'" routerLink="/">
1062
+ * <lucide-icon navIcon name="house" />
1063
+ * Portfolio
1064
+ * </a>
1065
+ */
1066
+ class UiNavItem {
1067
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1068
+ classes = computed(() => `${BASE_CLASSES$1}${this.active() ? ` ${ACTIVE_CLASSES}` : ''}`, /* @ts-ignore */
1069
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1070
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1071
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiNavItem, isStandalone: true, selector: "a[ui-nav-item]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: '<ng-content select="[navIcon]" /><ng-content />', isInline: true });
1072
+ }
1073
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, decorators: [{
1074
+ type: Component,
1075
+ args: [{
1076
+ selector: 'a[ui-nav-item]',
1077
+ template: '<ng-content select="[navIcon]" /><ng-content />',
1078
+ host: {
1079
+ '[class]': 'classes()',
1080
+ '[attr.aria-current]': "active() ? 'page' : null",
1081
+ },
1082
+ }]
1083
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1084
+
1085
+ const BASE_CLASSES = 'flex w-full items-center gap-2.5 min-h-14 px-2.5 py-1.5 rounded-control text-left select-none touch-manipulation transition-colors duration-(--duration-press) hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1086
+ const SELECTED_CLASSES$1 = 'bg-(--soft)';
1087
+ /**
1088
+ * Clickable row of a list: a holding, an envelope, an account.
1089
+ *
1090
+ * @example
1091
+ * <a ui-row [selected]="holding.id === openId()" [routerLink]="['/holdings', holding.id]">
1092
+ * {{ holding.name }}
1093
+ * </a>
1094
+ */
1095
+ class UiRow {
1096
+ selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1097
+ classes = computed(() => `${BASE_CLASSES}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
1098
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1099
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
1100
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
1101
+ }
1102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
1103
+ type: Component,
1104
+ args: [{
1105
+ selector: 'a[ui-row], button[ui-row]',
1106
+ template: '<ng-content />',
1107
+ host: {
1108
+ '[class]': 'classes()',
1109
+ '[attr.aria-current]': "selected() ? 'true' : null",
1110
+ },
1111
+ }]
1112
+ }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }] } });
1113
+
1114
+ const OPTION_CLASSES = "relative z-10 min-h-9 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)";
1115
+ const SELECTED_CLASSES = 'text-(--foreground)';
1116
+ const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
1117
+ /**
1118
+ * Exclusive choice within a small, known set: a time range, a unit, a mode. The selected option's
1119
+ * background is a single thumb that slides under it in `transform`, instead of being redrawn.
629
1120
  *
630
1121
  * @example
631
1122
  * <ui-segmented [options]="ranges" label="Time range" [(value)]="range" />
@@ -639,6 +1130,11 @@ class UiSegmented {
639
1130
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
640
1131
  radios = viewChildren('radio', /* @ts-ignore */
641
1132
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
1133
+ thumbIndex = computed(() => {
1134
+ const index = this.options().findIndex((option) => option.value === this.value());
1135
+ return index === -1 ? 0 : index + 1;
1136
+ }, /* @ts-ignore */
1137
+ ...(ngDevMode ? [{ debugName: "thumbIndex" }] : /* istanbul ignore next */ []));
642
1138
  optionClasses(optionValue) {
643
1139
  return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
644
1140
  }
@@ -663,9 +1159,24 @@ class UiSegmented {
663
1159
  this.value.set(next.value);
664
1160
  this.radios()[nextIndex]?.nativeElement.focus();
665
1161
  }
666
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
667
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", 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: `
668
- <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
1162
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
1163
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", 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: `
1164
+ <div
1165
+ class="rounded-control relative grid bg-(--muted) p-0.5"
1166
+ role="radiogroup"
1167
+ [attr.aria-label]="label()"
1168
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
1169
+ >
1170
+ @if (thumbIndex(); as index) {
1171
+ <span
1172
+ aria-hidden="true"
1173
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
1174
+ data-thumb
1175
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
1176
+ [style.width]="'calc(100% / ' + options().length + ')'"
1177
+ ></span>
1178
+ }
1179
+
669
1180
  @for (option of options(); track option.value) {
670
1181
  <button
671
1182
  #radio
@@ -683,12 +1194,27 @@ class UiSegmented {
683
1194
  </div>
684
1195
  `, isInline: true });
685
1196
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSegmented, decorators: [{
1197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, decorators: [{
687
1198
  type: Component,
688
1199
  args: [{
689
1200
  selector: 'ui-segmented',
690
1201
  template: `
691
- <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
1202
+ <div
1203
+ class="rounded-control relative grid bg-(--muted) p-0.5"
1204
+ role="radiogroup"
1205
+ [attr.aria-label]="label()"
1206
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
1207
+ >
1208
+ @if (thumbIndex(); as index) {
1209
+ <span
1210
+ aria-hidden="true"
1211
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
1212
+ data-thumb
1213
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
1214
+ [style.width]="'calc(100% / ' + options().length + ')'"
1215
+ ></span>
1216
+ }
1217
+
692
1218
  @for (option of options(); track option.value) {
693
1219
  <button
694
1220
  #radio
@@ -716,17 +1242,17 @@ const PICKER_CLASSES = [
716
1242
  'pointer-fine:supports-[appearance:base-select]:items-center',
717
1243
  'pointer-fine:supports-[appearance:base-select]:pr-3',
718
1244
  'pointer-fine:supports-[appearance:base-select]:[&_option]:cursor-pointer',
719
- 'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-md',
1245
+ 'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-control',
720
1246
  'pointer-fine:supports-[appearance:base-select]:[&_option]:px-2.5',
721
1247
  'pointer-fine:supports-[appearance:base-select]:[&_option]:py-2',
722
1248
  'pointer-fine:supports-[appearance:base-select]:[&_option:hover]:bg-(--soft)',
723
1249
  'pointer-fine:supports-[appearance:base-select]:[&_option:focus]:bg-(--soft)',
724
1250
  'pointer-fine:supports-[appearance:base-select]:[&_option:checked]:font-medium',
725
- '[&::picker-icon]:text-[11px]',
1251
+ '[&::picker-icon]:text-caption',
726
1252
  '[&::picker-icon]:text-(--muted-foreground)',
727
1253
  '[&::picker(select)]:[appearance:base-select]',
728
1254
  '[&::picker(select)]:my-1.5',
729
- '[&::picker(select)]:rounded-lg',
1255
+ '[&::picker(select)]:rounded-control',
730
1256
  '[&::picker(select)]:border',
731
1257
  '[&::picker(select)]:border-(--border)',
732
1258
  '[&::picker(select)]:bg-(--card)',
@@ -754,10 +1280,10 @@ class UiSelect {
754
1280
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
755
1281
  classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
756
1282
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
757
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
758
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiSelect) }], ngImport: i0 });
1283
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1284
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiSelect) }], ngImport: i0 });
759
1285
  }
760
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSelect, decorators: [{
1286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, decorators: [{
761
1287
  type: Directive,
762
1288
  args: [{
763
1289
  selector: 'select[uiSelect]',
@@ -768,63 +1294,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
768
1294
  }]
769
1295
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
770
1296
 
1297
+ /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
1298
+ const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
771
1299
  const LAST_LINE_WIDTH = '62%';
1300
+ const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
1301
+ const BAR_CLASSES = 'block rounded-control bg-(--muted)';
772
1302
  /**
773
- * Decorative placeholder for content that is still loading. Deliberately hidden from
774
- * assistive technology: the calling screen keeps its own role="status" sentence.
1303
+ * Decorative, pulsing placeholder for content that is still loading, at the shape of the content
1304
+ * it stands in for.
775
1305
  *
776
1306
  * @example
1307
+ * <ui-skeleton shape="figure" />
777
1308
  * <ui-skeleton [lines]="3" />
778
- * <ui-skeleton [height]="196" />
779
1309
  */
780
1310
  class UiSkeleton {
1311
+ shape = input('text', /* @ts-ignore */
1312
+ ...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
781
1313
  lines = input(1, /* @ts-ignore */
782
1314
  ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
783
1315
  height = input(12, /* @ts-ignore */
784
1316
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
1317
+ animation = PULSE_ANIMATION;
785
1318
  bars = computed(() => {
786
1319
  const count = Math.max(1, this.lines());
787
1320
  return Array.from({ length: count }, (_, index) => (index === count - 1 && count > 1 ? LAST_LINE_WIDTH : '100%'));
788
1321
  }, /* @ts-ignore */
789
1322
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
790
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
791
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", 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: `
792
- @for (line of bars(); track $index) {
793
- <span
794
- class="block rounded bg-(--soft) motion-safe:animate-pulse"
795
- [style.height.px]="height()"
796
- [style.width]="line"
797
- ></span>
798
- }
799
- `, isInline: true });
1323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
1324
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, 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: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-11 w-56\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 flex-col justify-center gap-2\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-2/5\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-container h-48 w-full bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill size-40 border-[24px] border-(--muted) bg-transparent\"\n [style.animation]=\"animation\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
800
1325
  }
801
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiSkeleton, decorators: [{
1326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
802
1327
  type: Component,
803
- args: [{
804
- selector: 'ui-skeleton',
805
- template: `
806
- @for (line of bars(); track $index) {
807
- <span
808
- class="block rounded bg-(--soft) motion-safe:animate-pulse"
809
- [style.height.px]="height()"
810
- [style.width]="line"
811
- ></span>
1328
+ args: [{ selector: 'ui-skeleton', template: `
1329
+ @switch (shape()) {
1330
+ @case ('figure') {
1331
+ <span class="${BAR_CLASSES} h-11 w-56" [style.animation]="animation"></span>
1332
+ }
1333
+ @case ('row') {
1334
+ <span class="flex h-14 flex-col justify-center gap-2">
1335
+ <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
1336
+ <span class="${BAR_CLASSES} h-3 w-2/5" [style.animation]="animation"></span>
1337
+ </span>
1338
+ }
1339
+ @case ('chart') {
1340
+ <span class="rounded-container h-48 w-full bg-(--muted)" [style.animation]="animation"></span>
1341
+ }
1342
+ @case ('ring') {
1343
+ <span
1344
+ class="rounded-pill size-40 border-[24px] border-(--muted) bg-transparent"
1345
+ [style.animation]="animation"
1346
+ ></span>
1347
+ }
1348
+ @default {
1349
+ @for (line of bars(); track $index) {
1350
+ <span
1351
+ class="${BAR_CLASSES}"
1352
+ [style.animation]="animation"
1353
+ [style.height.px]="height()"
1354
+ [style.width]="line"
1355
+ ></span>
1356
+ }
1357
+ }
812
1358
  }
813
- `,
814
- host: {
1359
+ `, host: {
815
1360
  'aria-hidden': 'true',
816
1361
  class: 'flex flex-col gap-2',
817
- },
818
- }]
819
- }], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
1362
+ }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
1363
+ }], propDecorators: { shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
820
1364
 
821
1365
  /** Available stat sizes. `StatSize` is derived from this tuple. */
822
1366
  const STAT_SIZES = ['hero', 'tile'];
823
1367
  const VALUE_CLASSES = {
824
- hero: 'text-[48px] leading-[1.1] font-semibold text-(--foreground)',
825
- tile: 'text-2xl font-semibold text-(--foreground)',
1368
+ hero: 'text-display font-semibold text-(--foreground)',
1369
+ tile: 'text-title font-semibold text-(--foreground)',
826
1370
  };
827
- const LABEL_CLASSES = 'text-[13px] font-medium text-(--subtle-foreground)';
1371
+ const LABEL_CLASSES = 'text-label font-medium text-(--subtle-foreground)';
828
1372
  /**
829
1373
  * Label, figure and a named period of change, at two sizes: a `hero` for a screen's single
830
1374
  * headline number, or a smaller `tile` repeated in a grid. The figure itself, its delta and its
@@ -848,8 +1392,8 @@ class UiStat {
848
1392
  labelClasses = LABEL_CLASSES;
849
1393
  valueClasses = computed(() => VALUE_CLASSES[this.size()], /* @ts-ignore */
850
1394
  ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
851
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
852
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: UiStat, isStandalone: true, selector: "ui-stat", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1395
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
1396
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiStat, isStandalone: true, selector: "ui-stat", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
853
1397
  <div class="flex items-start justify-between gap-4">
854
1398
  <div class="flex flex-col gap-1">
855
1399
  <dl>
@@ -859,7 +1403,7 @@ class UiStat {
859
1403
  </dd>
860
1404
  </dl>
861
1405
 
862
- <div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-(--muted-foreground)">
1406
+ <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
863
1407
  <ng-content select="[uiStatDelta]" />
864
1408
  <ng-content select="[uiStatCaption]" />
865
1409
  </div>
@@ -869,7 +1413,7 @@ class UiStat {
869
1413
  </div>
870
1414
  `, isInline: true });
871
1415
  }
872
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiStat, decorators: [{
1416
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, decorators: [{
873
1417
  type: Component,
874
1418
  args: [{
875
1419
  selector: 'ui-stat',
@@ -883,7 +1427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
883
1427
  </dd>
884
1428
  </dl>
885
1429
 
886
- <div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-(--muted-foreground)">
1430
+ <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
887
1431
  <ng-content select="[uiStatDelta]" />
888
1432
  <ng-content select="[uiStatCaption]" />
889
1433
  </div>
@@ -895,22 +1439,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
895
1439
  }]
896
1440
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
897
1441
 
1442
+ const CLASSES = "relative shrink-0 w-[51px] h-[31px] appearance-none cursor-pointer rounded-pill bg-(--muted) checked:bg-(--primary) transition-colors duration-(--duration-fast) ease-out disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2 before:h-(--row-min) before:content-[''] after:absolute after:top-0.5 after:left-0.5 after:size-[27px] after:rounded-pill after:bg-(--card) after:shadow-[0_1px_3px_rgb(0_0_0/0.2)] after:transition-transform after:duration-(--duration-fast) after:ease-out after:content-[''] checked:after:translate-x-[20px]";
1443
+ /**
1444
+ * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
1445
+ *
1446
+ * @example
1447
+ * <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
1448
+ */
1449
+ class UiSwitch {
1450
+ classes = CLASSES;
1451
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1452
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiSwitch, isStandalone: true, selector: "input[type=checkbox][uiSwitch]", host: { attributes: { "role": "switch" }, properties: { "class": "classes" } }, ngImport: i0 });
1453
+ }
1454
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, decorators: [{
1455
+ type: Directive,
1456
+ args: [{
1457
+ selector: 'input[type=checkbox][uiSwitch]',
1458
+ host: {
1459
+ role: 'switch',
1460
+ '[class]': 'classes',
1461
+ },
1462
+ }]
1463
+ }] });
1464
+
898
1465
  /** Breakpoints a secondary column can be held back until. */
899
1466
  const CELL_BREAKPOINTS = ['md', 'lg'];
900
1467
  const FROM_CLASSES = {
901
1468
  md: 'hidden md:table-cell',
902
1469
  lg: 'hidden lg:table-cell',
903
1470
  };
904
- const TH_CLASSES = 'px-3 py-2 align-bottom text-[11px] font-medium uppercase tracking-[0.05em] text-(--subtle-foreground)';
905
- const TD_CLASSES = 'border-t border-(--hairline) px-3 py-2.5 align-middle text-sm';
1471
+ const TH_CLASSES = 'h-9 px-2 align-bottom text-caption font-medium text-(--subtle-foreground) shadow-[inset_0_-1px_0_var(--hairline)]';
1472
+ const TD_CLASSES = 'h-12 px-2 align-middle text-body';
906
1473
  const NUMERIC_CLASSES = 'text-right tabular-nums';
907
1474
  const TEXTUAL_CLASSES = 'text-left';
908
1475
  /** Styled native table. The rows and cells stay plain <tr> and <td>. */
909
1476
  class UiTable {
910
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
911
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
1477
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1478
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
912
1479
  }
913
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTable, decorators: [{
1480
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, decorators: [{
914
1481
  type: Directive,
915
1482
  args: [{
916
1483
  selector: 'table[uiTable]',
@@ -919,6 +1486,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
919
1486
  },
920
1487
  }]
921
1488
  }] });
1489
+ /**
1490
+ * Body or group row. `group` draws the muted banner that opens each account inside a list of
1491
+ * holdings: it carries no hover state of its own, unlike an ordinary row.
1492
+ *
1493
+ * @example
1494
+ * <tr uiTr>...
1495
+ * <tr uiTr group>...
1496
+ */
1497
+ class UiTr {
1498
+ group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1499
+ classes = computed(() => this.group() ? 'min-h-(--row-min) rounded-control bg-(--muted) font-semibold' : 'rounded-control hover:bg-(--glow)', /* @ts-ignore */
1500
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1501
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1502
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTr, isStandalone: true, selector: "tr[uiTr]", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1503
+ }
1504
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, decorators: [{
1505
+ type: Directive,
1506
+ args: [{
1507
+ selector: 'tr[uiTr]',
1508
+ host: {
1509
+ '[class]': 'classes()',
1510
+ },
1511
+ }]
1512
+ }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }] } });
922
1513
  const cellClasses = (base, numeric, from) => [base, numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES, from ? FROM_CLASSES[from] : ''].filter(Boolean).join(' ');
923
1514
  /**
924
1515
  * Column header. `numeric` right-aligns and lines the digits up; `from` holds a
@@ -933,10 +1524,10 @@ class UiTh {
933
1524
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
934
1525
  classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
935
1526
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
936
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
937
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", 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 });
1527
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1528
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", 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 });
938
1529
  }
939
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTh, decorators: [{
1530
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, decorators: [{
940
1531
  type: Directive,
941
1532
  args: [{
942
1533
  selector: 'th[uiTh]',
@@ -952,10 +1543,10 @@ class UiTd {
952
1543
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
953
1544
  classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
954
1545
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
955
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
956
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", 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 });
1546
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1547
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", 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 });
957
1548
  }
958
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: UiTd, decorators: [{
1549
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, decorators: [{
959
1550
  type: Directive,
960
1551
  args: [{
961
1552
  selector: 'td[uiTd]',
@@ -965,6 +1556,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
965
1556
  }]
966
1557
  }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
967
1558
 
1559
+ const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-0.5 touch-manipulation select-none text-(--muted-foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1560
+ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
1561
+ /**
1562
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
1563
+ *
1564
+ * @example
1565
+ * <nav ui-tab-bar>
1566
+ * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
1567
+ * </nav>
1568
+ */
1569
+ class UiTabBar {
1570
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1571
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiTabBar, isStandalone: true, selector: "nav[ui-tab-bar]", host: { classAttribute: "grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]" }, ngImport: i0, template: '<ng-content />', isInline: true });
1572
+ }
1573
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
1574
+ type: Component,
1575
+ args: [{
1576
+ selector: 'nav[ui-tab-bar]',
1577
+ template: '<ng-content />',
1578
+ host: {
1579
+ class: 'grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]',
1580
+ },
1581
+ }]
1582
+ }] });
1583
+ /**
1584
+ * One destination of `ui-tab-bar`.
1585
+ *
1586
+ * @example
1587
+ * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
1588
+ */
1589
+ class UiTab {
1590
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1591
+ classes = computed(() => `${TAB_CLASSES}${this.active() ? ` ${ACTIVE_TAB_CLASSES}` : ''}`, /* @ts-ignore */
1592
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1593
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
1594
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiTab, isStandalone: true, selector: "a[ui-tab]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: `
1595
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
1596
+ <span class="text-caption" data-tab-label><ng-content /></span>
1597
+ `, isInline: true });
1598
+ }
1599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
1600
+ type: Component,
1601
+ args: [{
1602
+ selector: 'a[ui-tab]',
1603
+ template: `
1604
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
1605
+ <span class="text-caption" data-tab-label><ng-content /></span>
1606
+ `,
1607
+ host: {
1608
+ '[class]': 'classes()',
1609
+ '[attr.aria-current]': "active() ? 'page' : null",
1610
+ },
1611
+ }]
1612
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1613
+
968
1614
  /*
969
1615
  * Public API surface of the ui library.
970
1616
  */
@@ -973,5 +1619,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
973
1619
  * Generated bundle index. Do not edit.
974
1620
  */
975
1621
 
976
- export { AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMeter, UiSegmented, UiSelect, UiSkeleton, UiStat, UiTable, UiTd, UiTextarea, UiTh };
1622
+ export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
977
1623
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map