@joanroucoux/cairn-ui 0.6.0 → 0.6.1

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,115 +1,38 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, forwardRef, Directive, model, viewChildren, afterRenderEffect, contentChild, viewChild, afterNextRender, effect, contentChildren } from '@angular/core';
2
+ import { Component, input, computed, InjectionToken, signal, inject, LOCALE_ID, booleanAttribute, output, Directive, ElementRef, DestroyRef, forwardRef, model, viewChildren, afterNextRender, afterRenderEffect, contentChild, viewChild, effect, numberAttribute, contentChildren } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
3
4
  import { scaleLinear } from 'd3-scale';
4
5
  import { curveMonotoneX, line } from 'd3-shape';
5
6
 
6
- const MINUS = '−';
7
- const EM_DASH = '—';
8
- const BULLETS = '••••';
9
- /** Formats a number or an amount for the locale, with a missing value as an em dash and masking behind four bullets. */
10
- function formatAmount(value, options, masked = false) {
11
- if (value === null || value === undefined || Number.isNaN(value)) {
12
- return EM_DASH;
13
- }
14
- const digits = options.fractionDigits ?? 2;
15
- const format = new Intl.NumberFormat(options.locale, {
16
- ...(options.currency ? { style: 'currency', currency: options.currency } : {}),
17
- minimumFractionDigits: digits,
18
- maximumFractionDigits: digits,
19
- });
20
- const magnitude = Math.abs(value);
21
- const parts = format.formatToParts(magnitude);
22
- if (masked) {
23
- let replaced = false;
24
- return parts
25
- .map((part) => {
26
- if (['integer', 'group', 'decimal', 'fraction'].includes(part.type)) {
27
- if (replaced) {
28
- return '';
29
- }
30
- replaced = true;
31
- return BULLETS;
32
- }
33
- return part.value;
34
- })
35
- .join('');
36
- }
37
- const text = parts.map((part) => part.value).join('');
38
- const isZero = Number(format.format(magnitude).replace(/[^\d]/g, '')) === 0;
39
- if (!options.signed || isZero) {
40
- return value < 0 && !isZero ? `${MINUS}${text}` : text;
41
- }
42
- return `${value > 0 ? '+' : MINUS}${text}`;
43
- }
44
-
45
- /** App-wide "amounts are hidden" state. Defaults to never masked; the app overrides it. */
46
- const UI_AMOUNT_MASKED = new InjectionToken('UI_AMOUNT_MASKED', {
47
- providedIn: 'root',
48
- factory: () => signal(false),
49
- });
50
- /** Available figure styles. `AmountNumeric` is derived from this tuple. */
51
- const AMOUNT_NUMERICS = ['tabular', 'proportional'];
52
- /** Available masking behaviors. `AmountMasking` is derived from this tuple. */
53
- const AMOUNT_MASKINGS = ['dots', 'hide'];
54
7
  /**
55
- * A number or an amount of money, formatted for the locale and masked when the app hides amounts.
8
+ * Bar of actions fixed above the tab bar on iPhone: the primary actions of a detail screen, within
9
+ * thumb reach. It sits 52px plus the safe area above the bottom edge, so it clears `ui-tab-bar`, and
10
+ * it is hidden from `64rem`, where the same actions live in the page itself. Its children share the
11
+ * width equally.
56
12
  *
57
13
  * @example
58
- * <ui-amount [value]="total" currency="EUR" numeric="proportional" />
59
- * <ui-amount [value]="dayChange" currency="EUR" signed whenMasked="hide" />
14
+ * <ui-action-bar>
15
+ * <button ui-button variant="outline" size="tall">Sell</button>
16
+ * <button ui-button size="tall">Buy</button>
17
+ * </ui-action-bar>
60
18
  */
61
- class UiAmount {
62
- value = input(/* @ts-ignore */
63
- ...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
64
- currency = input(/* @ts-ignore */
65
- ...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
66
- locale = input(inject(LOCALE_ID), /* @ts-ignore */
67
- ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
68
- signed = input(false, { ...(ngDevMode ? { debugName: "signed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
69
- fractionDigits = input(2, /* @ts-ignore */
70
- ...(ngDevMode ? [{ debugName: "fractionDigits" }] : /* istanbul ignore next */ []));
71
- numeric = input('tabular', /* @ts-ignore */
72
- ...(ngDevMode ? [{ debugName: "numeric" }] : /* istanbul ignore next */ []));
73
- whenMasked = input('dots', /* @ts-ignore */
74
- ...(ngDevMode ? [{ debugName: "whenMasked" }] : /* istanbul ignore next */ []));
75
- #masked = inject(UI_AMOUNT_MASKED);
76
- hidden = computed(() => this.#masked() && this.whenMasked() === 'hide', /* @ts-ignore */
77
- ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
78
- text = computed(() => formatAmount(this.value(), {
79
- locale: this.locale(),
80
- currency: this.currency(),
81
- signed: this.signed(),
82
- fractionDigits: this.fractionDigits(),
83
- }, this.#masked()), /* @ts-ignore */
84
- ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
85
- classes = computed(() => {
86
- const missing = this.value() === null || this.value() === undefined;
87
- return [
88
- 'whitespace-nowrap',
89
- this.numeric() === 'tabular' ? 'tabular-nums' : '',
90
- missing ? 'text-(--subtle-foreground)' : '',
91
- ]
92
- .filter(Boolean)
93
- .join(' ');
94
- }, /* @ts-ignore */
95
- ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
96
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, deps: [], target: i0.ɵɵFactoryTarget.Component });
97
- 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 });
19
+ class UiActionBar {
20
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiActionBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
21
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiActionBar, isStandalone: true, selector: "ui-action-bar", host: { classAttribute: "fixed inset-x-0 bottom-[calc(52px+env(safe-area-inset-bottom))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden" }, ngImport: i0, template: '<ng-content />', isInline: true });
98
22
  }
99
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, decorators: [{
23
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiActionBar, decorators: [{
100
24
  type: Component,
101
25
  args: [{
102
- selector: 'ui-amount',
103
- template: '{{ text() }}',
26
+ selector: 'ui-action-bar',
27
+ template: '<ng-content />',
104
28
  host: {
105
- '[class]': 'classes()',
106
- '[attr.hidden]': 'hidden() || null',
29
+ class: 'fixed inset-x-0 bottom-[calc(52px+env(safe-area-inset-bottom))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden',
107
30
  },
108
31
  }]
109
- }], 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 }] }] } });
32
+ }] });
110
33
 
111
34
  const ALERT_VARIANTS = ['error', 'warning'];
112
- const BASE_CLASSES$7 = 'flex items-start gap-2.5 text-left text-label';
35
+ const BASE_CLASSES$8 = 'flex items-start gap-2.5 text-left text-label';
113
36
  const VARIANT_CLASSES$3 = {
114
37
  error: 'rounded-control bg-(--card) px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--border)]',
115
38
  warning: 'text-(--foreground)',
@@ -125,7 +48,7 @@ class UiAlert {
125
48
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
126
49
  heading = input(/* @ts-ignore */
127
50
  ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
128
- classes = computed(() => `${BASE_CLASSES$7} ${VARIANT_CLASSES$3[this.variant()]}`, /* @ts-ignore */
51
+ classes = computed(() => `${BASE_CLASSES$8} ${VARIANT_CLASSES$3[this.variant()]}`, /* @ts-ignore */
129
52
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
130
53
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, deps: [], target: i0.ɵɵFactoryTarget.Component });
131
54
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiAlert, isStandalone: true, selector: "ui-alert", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.role": "variant() === 'warning' ? 'status' : 'alert'" } }, ngImport: i0, template: `
@@ -224,9 +147,167 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
224
147
  }]
225
148
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }] } });
226
149
 
150
+ const MINUS = '−';
151
+ const EM_DASH = '—';
152
+ const BULLETS = '••••';
153
+ /** Formats a number or an amount for the locale, with a missing value as an em dash and masking behind four bullets. */
154
+ function formatAmount(value, options, masked = false) {
155
+ if (value === null || value === undefined || Number.isNaN(value)) {
156
+ return EM_DASH;
157
+ }
158
+ const digits = options.fractionDigits ?? 2;
159
+ const format = new Intl.NumberFormat(options.locale, {
160
+ ...(options.currency ? { style: 'currency', currency: options.currency } : {}),
161
+ minimumFractionDigits: digits,
162
+ maximumFractionDigits: digits,
163
+ });
164
+ const magnitude = Math.abs(value);
165
+ const parts = format.formatToParts(magnitude);
166
+ if (masked) {
167
+ let replaced = false;
168
+ return parts
169
+ .map((part) => {
170
+ if (['integer', 'group', 'decimal', 'fraction'].includes(part.type)) {
171
+ if (replaced) {
172
+ return '';
173
+ }
174
+ replaced = true;
175
+ return BULLETS;
176
+ }
177
+ return part.value;
178
+ })
179
+ .join('');
180
+ }
181
+ const text = parts.map((part) => part.value).join('');
182
+ const isZero = Number(format.format(magnitude).replace(/[^\d]/g, '')) === 0;
183
+ if (!options.signed || isZero) {
184
+ return value < 0 && !isZero ? `${MINUS}${text}` : text;
185
+ }
186
+ return `${value > 0 ? '+' : MINUS}${text}`;
187
+ }
188
+
189
+ /** App-wide "amounts are hidden" state. Defaults to never masked; the app overrides it. */
190
+ const UI_AMOUNT_MASKED = new InjectionToken('UI_AMOUNT_MASKED', {
191
+ providedIn: 'root',
192
+ factory: () => signal(false),
193
+ });
194
+ /** Available figure styles. `AmountNumeric` is derived from this tuple. */
195
+ const AMOUNT_NUMERICS = ['tabular', 'proportional'];
196
+ /** Available masking behaviors. `AmountMasking` is derived from this tuple. */
197
+ const AMOUNT_MASKINGS = ['dots', 'hide'];
198
+ /**
199
+ * A number or an amount of money, formatted for the locale and masked when the app hides amounts.
200
+ *
201
+ * @example
202
+ * <ui-amount [value]="total" currency="EUR" numeric="proportional" />
203
+ * <ui-amount [value]="dayChange" currency="EUR" signed whenMasked="hide" />
204
+ */
205
+ class UiAmount {
206
+ value = input(/* @ts-ignore */
207
+ ...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
208
+ currency = input(/* @ts-ignore */
209
+ ...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
210
+ locale = input(inject(LOCALE_ID), /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
212
+ signed = input(false, { ...(ngDevMode ? { debugName: "signed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
213
+ fractionDigits = input(2, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "fractionDigits" }] : /* istanbul ignore next */ []));
215
+ numeric = input('tabular', /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "numeric" }] : /* istanbul ignore next */ []));
217
+ whenMasked = input('dots', /* @ts-ignore */
218
+ ...(ngDevMode ? [{ debugName: "whenMasked" }] : /* istanbul ignore next */ []));
219
+ #masked = inject(UI_AMOUNT_MASKED);
220
+ hidden = computed(() => this.#masked() && this.whenMasked() === 'hide', /* @ts-ignore */
221
+ ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
222
+ text = computed(() => formatAmount(this.value(), {
223
+ locale: this.locale(),
224
+ currency: this.currency(),
225
+ signed: this.signed(),
226
+ fractionDigits: this.fractionDigits(),
227
+ }, this.#masked()), /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
229
+ classes = computed(() => {
230
+ const missing = this.value() === null || this.value() === undefined;
231
+ return [
232
+ 'whitespace-nowrap',
233
+ this.numeric() === 'tabular' ? 'tabular-nums' : '',
234
+ missing ? 'text-(--subtle-foreground)' : '',
235
+ ]
236
+ .filter(Boolean)
237
+ .join(' ');
238
+ }, /* @ts-ignore */
239
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, deps: [], target: i0.ɵɵFactoryTarget.Component });
241
+ 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 });
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, decorators: [{
244
+ type: Component,
245
+ args: [{
246
+ selector: 'ui-amount',
247
+ template: '{{ text() }}',
248
+ host: {
249
+ '[class]': 'classes()',
250
+ '[attr.hidden]': 'hidden() || null',
251
+ },
252
+ }]
253
+ }], 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 }] }] } });
254
+
227
255
  /** Available async block states. `AsyncState` is derived from this tuple. */
228
256
  const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'];
229
- 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) pl-3 pr-4 text-label bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
257
+ /** Looks of the error block. `AsyncVariant` is derived from this tuple. */
258
+ const ASYNC_VARIANTS = ['elevated', 'plain', 'emphasis', 'inline'];
259
+ /** Alignments of the error block; `auto` is centred from 64rem. `AsyncAlign` is derived from this tuple. */
260
+ const ASYNC_ALIGNS = ['start', 'center', 'auto'];
261
+ const RETRY_BASE_CLASSES = 'relative inline-flex items-center justify-center 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) text-label';
262
+ const RETRY_OUTLINE_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
263
+ const RETRY_PRIMARY_CLASSES = 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)';
264
+ const RETRY_BLOCK_CLASSES = 'min-h-(--row-min) gap-2 pl-3 pr-4';
265
+ const RETRY_INLINE_CLASSES = 'h-11 pointer-fine:h-8 flex-none gap-1.5 pl-2.5 pr-3';
266
+ const ELEVATED_SURFACE_CLASSES = 'rounded-control bg-(--elevated)';
267
+ const CARD_SURFACE_CLASSES = 'rounded-container bg-(--card) shadow-[inset_0_0_0_1px_var(--border)]';
268
+ const BLOCK_SPECS = {
269
+ elevated: {
270
+ surface: ELEVATED_SURFACE_CLASSES,
271
+ gap: 'gap-1',
272
+ padding: { start: 'p-4', center: 'px-4 py-10', auto: 'p-4 lg:py-10' },
273
+ title: 'text-body font-medium',
274
+ message: { start: 'text-label', center: 'text-label', auto: 'text-label' },
275
+ retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },
276
+ retry: RETRY_OUTLINE_CLASSES,
277
+ },
278
+ plain: {
279
+ surface: '',
280
+ gap: 'gap-1',
281
+ padding: { start: 'px-4 py-5', center: 'px-4 py-12', auto: 'px-4 py-5 lg:py-12' },
282
+ title: 'text-body leading-[normal] font-medium',
283
+ message: {
284
+ start: 'text-label leading-[normal]',
285
+ center: 'text-label leading-[normal]',
286
+ auto: 'text-label leading-[normal]',
287
+ },
288
+ retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },
289
+ retry: RETRY_OUTLINE_CLASSES,
290
+ },
291
+ emphasis: {
292
+ surface: '',
293
+ gap: 'gap-3',
294
+ padding: { start: 'px-4 py-6', center: 'px-4 py-14', auto: 'px-4 py-6 lg:py-14' },
295
+ title: 'text-title font-semibold',
296
+ message: {
297
+ start: 'text-label',
298
+ center: 'text-body leading-[normal]',
299
+ auto: 'text-label lg:text-body lg:leading-[normal]',
300
+ },
301
+ retryMargin: { start: 'mt-1', center: 'mt-2', auto: 'mt-1 lg:mt-2' },
302
+ retry: RETRY_PRIMARY_CLASSES,
303
+ },
304
+ };
305
+ const ALIGN_CLASSES = {
306
+ start: 'items-start',
307
+ center: 'items-center text-center',
308
+ auto: 'items-start lg:items-center lg:text-center',
309
+ };
310
+ const INLINE_BOX_CLASSES = 'flex-row items-center justify-between gap-3 pt-1.5 pr-1.5 pb-2 pl-2.5';
230
311
  /**
231
312
  * The three states of a block that depends on a service call, next to the ready content.
232
313
  *
@@ -246,32 +327,72 @@ class UiAsync {
246
327
  ...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
247
328
  retryLabel = input(/* @ts-ignore */
248
329
  ...(ngDevMode ? [undefined, { debugName: "retryLabel" }] : /* istanbul ignore next */ []));
330
+ variant = input('elevated', /* @ts-ignore */
331
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
332
+ align = input('start', /* @ts-ignore */
333
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
334
+ card = input(false, { ...(ngDevMode ? { debugName: "card" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
335
+ fill = input(false, { ...(ngDevMode ? { debugName: "fill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
249
336
  retry = output();
250
- retryButtonClasses = RETRY_BUTTON_CLASSES;
337
+ filling = computed(() => this.fill() && this.state() === 'error', /* @ts-ignore */
338
+ ...(ngDevMode ? [{ debugName: "filling" }] : /* istanbul ignore next */ []));
339
+ iconSize = computed(() => (this.variant() === 'inline' ? 16 : 18), /* @ts-ignore */
340
+ ...(ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []));
341
+ #spec = computed(() => {
342
+ const variant = this.variant();
343
+ return variant === 'inline' ? null : BLOCK_SPECS[variant];
344
+ }, /* @ts-ignore */
345
+ ...(ngDevMode ? [{ debugName: "#spec" }] : /* istanbul ignore next */ []));
346
+ boxClasses = computed(() => {
347
+ const spec = this.#spec();
348
+ const fill = this.fill() ? ' flex-1 justify-center' : '';
349
+ if (spec === null) {
350
+ return `flex ${INLINE_BOX_CLASSES}${this.card() ? ` ${CARD_SURFACE_CLASSES}` : ''}${this.fill() ? ' flex-1' : ''}`;
351
+ }
352
+ const surface = this.card() ? CARD_SURFACE_CLASSES : spec.surface;
353
+ return `flex flex-col ${surface} ${spec.gap} ${ALIGN_CLASSES[this.align()]} ${spec.padding[this.align()]}${fill}`;
354
+ }, /* @ts-ignore */
355
+ ...(ngDevMode ? [{ debugName: "boxClasses" }] : /* istanbul ignore next */ []));
356
+ titleClasses = computed(() => this.#spec()?.title ?? '', /* @ts-ignore */
357
+ ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
358
+ messageClasses = computed(() => this.#spec()?.message[this.align()] ?? '', /* @ts-ignore */
359
+ ...(ngDevMode ? [{ debugName: "messageClasses" }] : /* istanbul ignore next */ []));
360
+ retryButtonClasses = computed(() => {
361
+ const spec = this.#spec();
362
+ if (spec === null) {
363
+ return `${RETRY_BASE_CLASSES} ${RETRY_OUTLINE_CLASSES} ${RETRY_INLINE_CLASSES}`;
364
+ }
365
+ return `${RETRY_BASE_CLASSES} ${spec.retry} ${RETRY_BLOCK_CLASSES} ${spec.retryMargin[this.align()]}`;
366
+ }, /* @ts-ignore */
367
+ ...(ngDevMode ? [{ debugName: "retryButtonClasses" }] : /* istanbul ignore next */ []));
251
368
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, deps: [], target: i0.ɵɵFactoryTarget.Component });
252
- 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: `
369
+ 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 }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { properties: { "attr.aria-busy": "state() === 'loading' || null", "class.flex": "filling()", "class.flex-col": "filling()" } }, ngImport: i0, template: `
253
370
  @switch (state()) {
254
371
  @case ('loading') {
255
372
  <ng-content select="[asyncLoading]" />
256
373
  }
257
374
  @case ('error') {
258
- <div
259
- aria-live="polite"
260
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-4"
261
- role="alert"
262
- >
263
- <span class="text-body font-medium">{{ errorTitle() }}</span>
264
- <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
265
- <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
375
+ <div aria-live="polite" role="alert" [class]="boxClasses()">
376
+ @if (variant() === 'inline') {
377
+ <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
378
+ } @else {
379
+ @if (variant() === 'emphasis') {
380
+ <h2 [class]="titleClasses()">{{ errorTitle() }}</h2>
381
+ } @else {
382
+ <span [class]="titleClasses()">{{ errorTitle() }}</span>
383
+ }
384
+ <span class="text-(--muted-foreground)" [class]="messageClasses()">{{ errorMessage() }}</span>
385
+ }
386
+ <button type="button" [class]="retryButtonClasses()" (click)="retry.emit()">
266
387
  <svg
267
388
  aria-hidden="true"
268
389
  class="block flex-none fill-none stroke-current"
269
- height="18"
270
390
  stroke-linecap="round"
271
391
  stroke-linejoin="round"
272
392
  stroke-width="1.75"
273
393
  viewBox="0 0 24 24"
274
- width="18"
394
+ [attr.height]="iconSize()"
395
+ [attr.width]="iconSize()"
275
396
  >
276
397
  <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
277
398
  <path d="M21 3v5h-5" />
@@ -299,23 +420,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
299
420
  <ng-content select="[asyncLoading]" />
300
421
  }
301
422
  @case ('error') {
302
- <div
303
- aria-live="polite"
304
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-4"
305
- role="alert"
306
- >
307
- <span class="text-body font-medium">{{ errorTitle() }}</span>
308
- <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
309
- <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
423
+ <div aria-live="polite" role="alert" [class]="boxClasses()">
424
+ @if (variant() === 'inline') {
425
+ <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
426
+ } @else {
427
+ @if (variant() === 'emphasis') {
428
+ <h2 [class]="titleClasses()">{{ errorTitle() }}</h2>
429
+ } @else {
430
+ <span [class]="titleClasses()">{{ errorTitle() }}</span>
431
+ }
432
+ <span class="text-(--muted-foreground)" [class]="messageClasses()">{{ errorMessage() }}</span>
433
+ }
434
+ <button type="button" [class]="retryButtonClasses()" (click)="retry.emit()">
310
435
  <svg
311
436
  aria-hidden="true"
312
437
  class="block flex-none fill-none stroke-current"
313
- height="18"
314
438
  stroke-linecap="round"
315
439
  stroke-linejoin="round"
316
440
  stroke-width="1.75"
317
441
  viewBox="0 0 24 24"
318
- width="18"
442
+ [attr.height]="iconSize()"
443
+ [attr.width]="iconSize()"
319
444
  >
320
445
  <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
321
446
  <path d="M21 3v5h-5" />
@@ -334,17 +459,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
334
459
  `,
335
460
  host: {
336
461
  '[attr.aria-busy]': "state() === 'loading' || null",
462
+ '[class.flex]': 'filling()',
463
+ '[class.flex-col]': 'filling()',
337
464
  },
338
465
  }]
339
- }], 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"] }] } });
466
+ }], 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 }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
340
467
 
341
468
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
342
- const AVATAR_SIZES = ['sm', 'md', 'lg'];
343
- const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill bg-(--muted) font-medium tabular-nums';
344
- const SIZE_CLASSES$3 = {
469
+ const AVATAR_SIZES = ['sm', 'md', 'lg', 'auto'];
470
+ const BASE_CLASSES$7 = 'inline-flex items-center justify-center rounded-pill bg-(--muted) font-medium tabular-nums';
471
+ const SIZE_CLASSES$7 = {
345
472
  sm: 'size-8 text-caption',
346
473
  md: 'size-[34px] text-label',
347
474
  lg: 'size-14 text-title',
475
+ auto: 'size-[34px] text-label lg:size-8 lg:text-caption',
348
476
  };
349
477
  /**
350
478
  * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
@@ -359,7 +487,7 @@ class UiAvatar {
359
487
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
360
488
  size = input('md', /* @ts-ignore */
361
489
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
362
- classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$3[this.size()]}`, /* @ts-ignore */
490
+ classes = computed(() => `${BASE_CLASSES$7} ${SIZE_CLASSES$7[this.size()]}`, /* @ts-ignore */
363
491
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
364
492
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
365
493
  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 });
@@ -376,12 +504,98 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
376
504
  },
377
505
  }]
378
506
  }], 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 }] }] } });
507
+ /**
508
+ * Round hit area around a `ui-avatar` that links somewhere: 44px on touch, 40px from 64rem, with a
509
+ * glow on hover and the press scale. The avatar itself keeps its own size.
510
+ *
511
+ * @example
512
+ * <a uiAvatarLink routerLink="/profile"><ui-avatar initials="JR" label="Profile" size="auto" /></a>
513
+ */
514
+ class UiAvatarLink {
515
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatarLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
516
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiAvatarLink, isStandalone: true, selector: "a[uiAvatarLink], button[uiAvatarLink]", host: { classAttribute: "rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[transform,background-color] duration-(--duration-press) ease-out hover:bg-(--glow) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)" }, ngImport: i0 });
517
+ }
518
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatarLink, decorators: [{
519
+ type: Directive,
520
+ args: [{
521
+ selector: 'a[uiAvatarLink], button[uiAvatarLink]',
522
+ host: {
523
+ class: 'rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[transform,background-color] duration-(--duration-press) ease-out hover:bg-(--glow) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)',
524
+ },
525
+ }]
526
+ }] });
527
+
528
+ /** Available back link sizes. `BackLinkSize` is derived from this tuple. */
529
+ const BACK_LINK_SIZES = ['md', 'sm', 'header'];
530
+ const BASE_CLASSES$6 = 'inline-flex items-center gap-0.5 rounded-control text-body font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,color] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-(--ring)';
531
+ const SIZE_CLASSES$6 = {
532
+ md: 'h-11 pr-3 pl-1 text-(--foreground) focus-visible:outline-offset-2',
533
+ sm: 'h-9 pr-2 pl-0.5 text-(--muted-foreground) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-offset-2',
534
+ header: 'h-11 pr-3 pl-1 text-(--foreground) focus-visible:-outline-offset-2',
535
+ };
536
+ const CHEVRON = { md: 22, sm: 22, header: 24 };
537
+ /**
538
+ * Link back to the parent screen: a chevron and the name of the destination.
539
+ *
540
+ * @example
541
+ * <a ui-back-link routerLink="/accounts">Accounts</a>
542
+ */
543
+ class UiBackLink {
544
+ size = input('md', /* @ts-ignore */
545
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
546
+ chevron = computed(() => CHEVRON[this.size()], /* @ts-ignore */
547
+ ...(ngDevMode ? [{ debugName: "chevron" }] : /* istanbul ignore next */ []));
548
+ classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$6[this.size()]}`, /* @ts-ignore */
549
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
550
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBackLink, deps: [], target: i0.ɵɵFactoryTarget.Component });
551
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiBackLink, isStandalone: true, selector: "a[ui-back-link]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
552
+ <svg
553
+ aria-hidden="true"
554
+ fill="none"
555
+ stroke="currentColor"
556
+ stroke-linecap="round"
557
+ stroke-linejoin="round"
558
+ stroke-width="1.75"
559
+ viewBox="0 0 24 24"
560
+ [attr.height]="chevron()"
561
+ [attr.width]="chevron()"
562
+ >
563
+ <path d="m15 18-6-6 6-6" />
564
+ </svg>
565
+ <ng-content />
566
+ `, isInline: true });
567
+ }
568
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBackLink, decorators: [{
569
+ type: Component,
570
+ args: [{
571
+ selector: 'a[ui-back-link]',
572
+ template: `
573
+ <svg
574
+ aria-hidden="true"
575
+ fill="none"
576
+ stroke="currentColor"
577
+ stroke-linecap="round"
578
+ stroke-linejoin="round"
579
+ stroke-width="1.75"
580
+ viewBox="0 0 24 24"
581
+ [attr.height]="chevron()"
582
+ [attr.width]="chevron()"
583
+ >
584
+ <path d="m15 18-6-6 6-6" />
585
+ </svg>
586
+ <ng-content />
587
+ `,
588
+ host: {
589
+ '[class]': 'classes()',
590
+ },
591
+ }]
592
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
379
593
 
380
594
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
381
595
  const BADGE_VARIANTS = ['neutral', 'outline'];
382
596
  const BADGE_SIZES = ['md', 'sm'];
383
597
  const BASE_CLASSES$5 = 'inline-flex items-center rounded-pill text-caption font-medium';
384
- const SIZE_CLASSES$2 = {
598
+ const SIZE_CLASSES$5 = {
385
599
  md: 'h-6 px-2.5',
386
600
  sm: 'h-[22px] px-2',
387
601
  };
@@ -400,7 +614,7 @@ class UiBadge {
400
614
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
401
615
  size = input('md', /* @ts-ignore */
402
616
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
403
- classes = computed(() => `${BASE_CLASSES$5} ${SIZE_CLASSES$2[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
617
+ classes = computed(() => `${BASE_CLASSES$5} ${SIZE_CLASSES$5[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
404
618
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
405
619
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
406
620
  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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
@@ -417,22 +631,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
417
631
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
418
632
 
419
633
  /** Available button variants. `ButtonVariant` is derived from this tuple. */
420
- const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
634
+ const BUTTON_VARIANTS = [
635
+ 'primary',
636
+ 'outline',
637
+ 'ghost',
638
+ 'destructive',
639
+ 'quiet',
640
+ 'quiet-destructive',
641
+ 'outline-destructive',
642
+ 'quiet-glow',
643
+ 'tonal',
644
+ ];
421
645
  /** Available button sizes. */
422
- const BUTTON_SIZES = ['md', 'lg', 'xl', 'xxl', 'icon'];
423
- const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center 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';
646
+ const BUTTON_SIZES = [
647
+ 'compact',
648
+ 'slim',
649
+ 'xs',
650
+ 'sm',
651
+ 'md',
652
+ 'lg',
653
+ 'xl',
654
+ 'xxl',
655
+ 'tall',
656
+ 'block',
657
+ 'icon',
658
+ 'icon-sm',
659
+ ];
660
+ const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center 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-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
661
+ const RING_OUTSIDE = 'focus-visible:outline-offset-2';
662
+ const RING_INSIDE = 'focus-visible:-outline-offset-2';
663
+ const OUTLINE = 'bg-(--card) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow) active:bg-(--soft)';
424
664
  const VARIANT_CLASSES$1 = {
425
- primary: 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)',
426
- outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow) active:bg-(--soft)',
427
- ghost: 'bg-transparent text-(--foreground) hover:bg-(--glow) active:bg-(--soft)',
428
- destructive: 'bg-(--destructive) text-(--destructive-foreground)',
665
+ primary: `${RING_OUTSIDE} bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)`,
666
+ outline: `${RING_OUTSIDE} ${OUTLINE} text-(--foreground)`,
667
+ ghost: `${RING_OUTSIDE} bg-transparent text-(--foreground) hover:bg-(--glow) active:bg-(--soft)`,
668
+ destructive: `${RING_OUTSIDE} bg-(--destructive) text-(--destructive-foreground)`,
669
+ tonal: `${RING_OUTSIDE} bg-(--muted) text-(--foreground) lg:bg-transparent lg:hover:bg-(--glow)`,
670
+ 'quiet-glow': `${RING_INSIDE} bg-transparent text-(--muted-foreground) hover:bg-(--glow) hover:text-(--foreground)`,
671
+ quiet: `${RING_INSIDE} bg-transparent text-(--muted-foreground) hover:bg-(--soft) hover:text-(--foreground) active:bg-(--soft)`,
672
+ 'quiet-destructive': `${RING_INSIDE} bg-transparent text-(--muted-foreground) hover:bg-(--glow) hover:text-(--negative) active:bg-(--soft)`,
673
+ 'outline-destructive': `${RING_OUTSIDE} ${OUTLINE} text-(--negative)`,
429
674
  };
430
- const SIZE_CLASSES$1 = {
675
+ const SIZE_CLASSES$4 = {
676
+ compact: 'h-8 lg:h-7 gap-1.5 px-2.5 lg:px-2 text-label before:absolute before:-inset-x-1 before:-inset-y-1.5 lg:before:hidden',
677
+ slim: 'h-11 lg:h-8 gap-1.5 px-2.5 text-label',
678
+ xs: 'h-8 gap-1.5 px-2.5 text-label',
679
+ sm: 'h-9 gap-2 px-3 text-label',
431
680
  md: 'min-h-(--row-min) gap-2 px-4 text-label has-[>svg:first-child]:pl-3',
432
681
  lg: 'h-11 gap-2 px-[18px] text-label',
433
682
  xl: 'h-[50px] gap-2 px-6 text-body',
434
- xxl: 'h-[52px] gap-2.5 px-6 text-body',
683
+ xxl: 'h-[52px] lg:h-11 gap-2.5 px-6 lg:px-5 text-body lg:has-[>svg:first-child]:pl-4',
684
+ tall: 'h-12 lg:h-10 gap-2 px-4 text-body',
685
+ block: 'h-[50px] lg:h-11 gap-2 px-4 text-body',
435
686
  icon: 'size-(--row-min) gap-2 p-0',
687
+ 'icon-sm': 'size-11 pointer-fine:size-9 gap-2 p-0',
436
688
  };
437
689
  /**
438
690
  * Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
@@ -447,12 +699,15 @@ class UiButton {
447
699
  size = input('md', /* @ts-ignore */
448
700
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
449
701
  loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
450
- classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES$1[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
702
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
703
+ inactive = computed(() => this.loading() || this.busy(), /* @ts-ignore */
704
+ ...(ngDevMode ? [{ debugName: "inactive" }] : /* istanbul ignore next */ []));
705
+ classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES$4[this.size()]}${this.inactive() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
451
706
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
452
707
  constructor() {
453
708
  const host = inject(ElementRef).nativeElement;
454
709
  const guard = (event) => {
455
- if (this.loading()) {
710
+ if (this.inactive()) {
456
711
  event.preventDefault();
457
712
  event.stopImmediatePropagation();
458
713
  }
@@ -461,7 +716,7 @@ class UiButton {
461
716
  inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
462
717
  }
463
718
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
464
- 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: `
719
+ 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 }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-busy": "inactive() || null", "attr.aria-disabled": "inactive() || null" } }, ngImport: i0, template: `
465
720
  @if (loading()) {
466
721
  <span
467
722
  aria-hidden="true"
@@ -486,42 +741,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
486
741
  `,
487
742
  host: {
488
743
  '[class]': 'classes()',
489
- '[attr.aria-busy]': 'loading() || null',
490
- '[attr.aria-disabled]': 'loading() || null',
744
+ '[attr.aria-busy]': 'inactive() || null',
745
+ '[attr.aria-disabled]': 'inactive() || null',
491
746
  },
492
747
  }]
493
- }], 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 }] }] } });
748
+ }], 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 }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }] } });
494
749
 
495
750
  /** Available card variants. `CardVariant` is derived from this tuple. */
496
751
  const CARD_VARIANTS = ['default', 'elevated'];
497
752
  /** Available card paddings. `CardPadding` is derived from this tuple. */
498
- const CARD_PADDINGS = ['none', 'sm', 'md', 'list'];
499
- const BASE_CLASSES$3 = 'block rounded-container';
753
+ const CARD_PADDINGS = ['none', 'sm', 'md', 'list', 'rows'];
754
+ /** Where the card surface (fill, border, radius) and its padding apply. `CardSurface` is derived from this tuple. */
755
+ const CARD_SURFACES = ['always', 'lg', 'max-lg'];
756
+ const BASE_CLASSES$3 = 'block';
500
757
  const VARIANT_CLASSES = {
501
- default: 'bg-(--card) shadow-[inset_0_0_0_1px_var(--border)] text-(--card-foreground)',
502
- elevated: 'bg-(--elevated) shadow-[inset_0_0_0_1px_var(--hairline)] text-(--foreground)',
758
+ default: {
759
+ always: 'rounded-container bg-(--card) shadow-[inset_0_0_0_1px_var(--border)] text-(--card-foreground)',
760
+ lg: 'lg:rounded-container lg:bg-(--card) lg:shadow-[inset_0_0_0_1px_var(--border)] lg:text-(--card-foreground)',
761
+ 'max-lg': 'max-lg:rounded-container max-lg:bg-(--card) max-lg:shadow-[inset_0_0_0_1px_var(--border)] max-lg:text-(--card-foreground)',
762
+ },
763
+ elevated: {
764
+ always: 'rounded-container bg-(--elevated) shadow-[inset_0_0_0_1px_var(--hairline)] text-(--foreground)',
765
+ lg: 'lg:rounded-container lg:bg-(--elevated) lg:shadow-[inset_0_0_0_1px_var(--hairline)] lg:text-(--foreground)',
766
+ 'max-lg': 'max-lg:rounded-container max-lg:bg-(--elevated) max-lg:shadow-[inset_0_0_0_1px_var(--hairline)] max-lg:text-(--foreground)',
767
+ },
503
768
  };
504
769
  const PADDING_CLASSES$1 = {
505
- none: 'p-0',
506
- sm: 'p-3',
507
- md: 'p-(--inset-card)',
508
- list: 'px-2 py-3',
770
+ none: {
771
+ always: 'p-0',
772
+ lg: 'lg:p-0',
773
+ 'max-lg': 'max-lg:p-0',
774
+ },
775
+ sm: {
776
+ always: 'p-3',
777
+ lg: 'lg:p-3',
778
+ 'max-lg': 'max-lg:p-3',
779
+ },
780
+ md: {
781
+ always: 'p-(--inset-card)',
782
+ lg: 'lg:p-(--inset-card)',
783
+ 'max-lg': 'max-lg:p-(--inset-card)',
784
+ },
785
+ list: {
786
+ always: 'px-2 py-3',
787
+ lg: 'lg:px-2 lg:py-3',
788
+ 'max-lg': 'max-lg:px-2 max-lg:py-3',
789
+ },
790
+ rows: {
791
+ always: 'px-(--inset-card) py-1',
792
+ lg: 'lg:px-(--inset-card) lg:py-1',
793
+ 'max-lg': 'max-lg:px-(--inset-card) max-lg:py-1',
794
+ },
509
795
  };
510
796
  /**
511
- * Surface that groups related content: a summary block, a table, a side panel.
797
+ * Surface that groups related content: a summary block, a table, a side panel. `surface` limits the
798
+ * fill, border, radius and padding to one side of `64rem`, for a block that is a card on a desktop and
799
+ * plain on an iPhone, or the reverse.
512
800
  *
513
801
  * @example
514
802
  * <ui-card variant="elevated" padding="sm">Total</ui-card>
803
+ * <ui-card surface="max-lg">Card on an iPhone, plain on a desktop</ui-card>
515
804
  */
516
805
  class UiCard {
517
806
  variant = input('default', /* @ts-ignore */
518
807
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
519
808
  padding = input('md', /* @ts-ignore */
520
809
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
521
- classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES$1[this.padding()]}`, /* @ts-ignore */
810
+ surface = input('always', /* @ts-ignore */
811
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
812
+ classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()][this.surface()]} ${PADDING_CLASSES$1[this.padding()][this.surface()]}`, /* @ts-ignore */
522
813
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
523
814
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
524
- 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 });
815
+ 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 }, surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
525
816
  }
526
817
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, decorators: [{
527
818
  type: Component,
@@ -532,7 +823,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
532
823
  '[class]': 'classes()',
533
824
  },
534
825
  }]
535
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }] } });
826
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }] } });
536
827
 
537
828
  /**
538
829
  * Resolves to the styled control projected into a field.
@@ -544,8 +835,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
544
835
  */
545
836
  const UI_CONTROL = new InjectionToken('UI_CONTROL');
546
837
 
547
- /** Available control sizes. `md` is the 44/40px touch target. */
548
- const CONTROL_SIZES = ['sm', 'md'];
838
+ /** Available control sizes. `md` is the 44/40px touch target; `lg` and `xl` are the 48px sheet and sign-in fields. */
839
+ const CONTROL_SIZES = ['sm', 'md', 'lg', 'xl'];
549
840
  const CONTROL_BASE_CLASSES = 'w-full rounded-control 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-50';
550
841
  const CONTROL_SURFACES = ['background', 'card'];
551
842
  const CONTROL_SURFACE_CLASSES = {
@@ -555,7 +846,10 @@ const CONTROL_SURFACE_CLASSES = {
555
846
  const CONTROL_SIZE_CLASSES = {
556
847
  sm: 'h-9',
557
848
  md: 'min-h-(--row-min)',
849
+ lg: 'min-h-12 lg:min-h-(--row-min)',
850
+ xl: 'h-12 lg:h-11',
558
851
  };
852
+ const SEARCH_CLASSES = '[&::-webkit-search-cancel-button]:appearance-none';
559
853
  /**
560
854
  * Styled native text input. A directive (not a wrapper component) so the host
561
855
  * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
@@ -576,7 +870,7 @@ class UiInput {
576
870
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
577
871
  touched = input(false, /* @ts-ignore */
578
872
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
579
- classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
873
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${SEARCH_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
580
874
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
581
875
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
582
876
  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 }, surface: { classPropertyName: "surface", publicName: "surface", 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 });
@@ -621,10 +915,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
621
915
  }]
622
916
  }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
623
917
 
624
- const CHIP_CLASSES = 'h-10 px-3.5 pointer-fine:h-8 pointer-fine:px-3 rounded-pill text-label font-medium cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)';
918
+ const CHIP_CLASSES$1 = 'h-10 px-3.5 pointer-fine:h-8 pointer-fine:px-3 rounded-pill text-label font-medium cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)';
625
919
  const SELECTED_CLASSES$2 = 'bg-(--primary) text-(--primary-foreground)';
626
920
  const UNSELECTED_CLASSES$1 = 'text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
627
- const nextId$3 = (() => {
921
+ const nextId$4 = (() => {
628
922
  let count = 0;
629
923
  return () => `ui-choice-chips-${++count}`;
630
924
  })();
@@ -659,7 +953,7 @@ class UiChoiceChips {
659
953
  touched = input(false, /* @ts-ignore */
660
954
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
661
955
  touch = output();
662
- labelId = nextId$3();
956
+ labelId = nextId$4();
663
957
  #host = inject(ElementRef);
664
958
  radios = viewChildren('radio', /* @ts-ignore */
665
959
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
@@ -669,7 +963,7 @@ class UiChoiceChips {
669
963
  const state = optionValue === this.value()
670
964
  ? SELECTED_CLASSES$2
671
965
  : `${CONTROL_SURFACE_CLASSES[this.surface()]} ${UNSELECTED_CLASSES$1}`;
672
- return `${CHIP_CLASSES} ${state}`;
966
+ return `${CHIP_CLASSES$1} ${state}`;
673
967
  }
674
968
  tabIndexFor(optionValue) {
675
969
  const opts = this.options();
@@ -784,8 +1078,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
784
1078
 
785
1079
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
786
1080
  const DELTA_EMPHASES = ['text', 'pill'];
787
- const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
788
- const PILL_CLASSES = 'h-6 rounded-pill px-2';
1081
+ /** Type sizes. `DeltaSize` is derived from this tuple. */
1082
+ const DELTA_SIZES = ['label', 'inherit'];
1083
+ /** Font weights. `DeltaWeight` is derived from this tuple. */
1084
+ const DELTA_WEIGHTS = ['medium', 'regular'];
1085
+ const BASE_CLASSES$2 = 'inline-flex items-center gap-1 tabular-nums';
1086
+ const SIZE_CLASSES$3 = {
1087
+ label: 'text-label',
1088
+ inherit: '',
1089
+ };
1090
+ const WEIGHT_CLASSES = {
1091
+ medium: 'font-medium',
1092
+ regular: 'font-normal',
1093
+ };
1094
+ const PILL_CLASSES$1 = 'h-6 rounded-pill px-2';
789
1095
  const TEXT_CLASSES = {
790
1096
  positive: 'text-(--positive)',
791
1097
  negative: 'text-(--negative)',
@@ -812,6 +1118,10 @@ class UiDelta {
812
1118
  ...(ngDevMode ? [{ debugName: "unknownLabel" }] : /* istanbul ignore next */ []));
813
1119
  emphasis = input('text', /* @ts-ignore */
814
1120
  ...(ngDevMode ? [{ debugName: "emphasis" }] : /* istanbul ignore next */ []));
1121
+ size = input('label', /* @ts-ignore */
1122
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1123
+ weight = input('medium', /* @ts-ignore */
1124
+ ...(ngDevMode ? [{ debugName: "weight" }] : /* istanbul ignore next */ []));
815
1125
  sign = computed(() => {
816
1126
  const value = this.value();
817
1127
  if (value === null || value === undefined) {
@@ -828,12 +1138,12 @@ class UiDelta {
828
1138
  ...(ngDevMode ? [{ debugName: "sign" }] : /* istanbul ignore next */ []));
829
1139
  classes = computed(() => {
830
1140
  const sign = this.sign();
831
- const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
832
- return `${BASE_CLASSES$2} ${TEXT_CLASSES[sign]} ${background}`;
1141
+ const background = this.emphasis() === 'pill' ? `${PILL_CLASSES$1} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
1142
+ return `${BASE_CLASSES$2} ${SIZE_CLASSES$3[this.size()]} ${WEIGHT_CLASSES[this.weight()]} ${TEXT_CLASSES[sign]} ${background}`;
833
1143
  }, /* @ts-ignore */
834
1144
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
835
1145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
836
- 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: `
1146
+ 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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, weight: { classPropertyName: "weight", publicName: "weight", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
837
1147
  @if (sign() === 'unknown') {
838
1148
  <span aria-hidden="true">&mdash;</span>
839
1149
  <span class="sr-only">{{ unknownLabel() }}</span>
@@ -858,7 +1168,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
858
1168
  '[class]': 'classes()',
859
1169
  },
860
1170
  }]
861
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unknownLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unknownLabel", required: true }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }] } });
1171
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unknownLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unknownLabel", required: true }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], weight: [{ type: i0.Input, args: [{ isSignal: true, alias: "weight", required: false }] }] } });
862
1172
 
863
1173
  /** Named dialog widths; any other CSS length is accepted as is. `DialogWidth` is derived from this tuple. */
864
1174
  const DIALOG_WIDTHS = ['md', 'lg'];
@@ -910,7 +1220,7 @@ const LAYOUTS = {
910
1220
  footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
911
1221
  },
912
1222
  };
913
- const nextId$2 = (() => {
1223
+ const nextId$3 = (() => {
914
1224
  let count = 0;
915
1225
  return () => `ui-dialog-${++count}`;
916
1226
  })();
@@ -938,8 +1248,18 @@ class UiDialog {
938
1248
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
939
1249
  open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
940
1250
  dismissed = output();
941
- headingId = nextId$2();
1251
+ headingId = nextId$3();
942
1252
  descriptionId = `${this.headingId}-description`;
1253
+ bodyId = `${this.headingId}-body`;
1254
+ scrollable = signal(false, /* @ts-ignore */
1255
+ ...(ngDevMode ? [{ debugName: "scrollable" }] : /* istanbul ignore next */ []));
1256
+ describedBy = computed(() => {
1257
+ if (this.description()) {
1258
+ return this.descriptionId;
1259
+ }
1260
+ return this.layout() === 'confirm' ? this.bodyId : null;
1261
+ }, /* @ts-ignore */
1262
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
943
1263
  widthValue = computed(() => {
944
1264
  const width = this.width();
945
1265
  return Object.hasOwn(WIDTH_VALUES, width) ? WIDTH_VALUES[width] : width;
@@ -961,6 +1281,17 @@ class UiDialog {
961
1281
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
962
1282
  #host = inject(ElementRef);
963
1283
  constructor() {
1284
+ const destroyRef = inject(DestroyRef);
1285
+ afterNextRender(() => {
1286
+ if (typeof ResizeObserver === 'undefined') {
1287
+ return;
1288
+ }
1289
+ const body = this.#host.nativeElement.querySelector('[data-dialog-body]');
1290
+ const observer = new ResizeObserver(() => this.scrollable.set(body.scrollHeight > body.clientHeight));
1291
+ observer.observe(body);
1292
+ observer.observe(body.firstElementChild);
1293
+ destroyRef.onDestroy(() => observer.disconnect());
1294
+ });
964
1295
  afterRenderEffect(() => {
965
1296
  // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
966
1297
  const dialog = this.#host.nativeElement.querySelector('dialog');
@@ -999,7 +1330,7 @@ class UiDialog {
999
1330
  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 }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, truncateDescription: { classPropertyName: "truncateDescription", publicName: "truncateDescription", 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" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)" } }, ngImport: i0, template: `
1000
1331
  <dialog
1001
1332
  #dlg
1002
- [attr.aria-describedby]="description() ? descriptionId : null"
1333
+ [attr.aria-describedby]="describedBy()"
1003
1334
  [attr.aria-labelledby]="headingId"
1004
1335
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1005
1336
  [class]="classes()"
@@ -1042,7 +1373,14 @@ class UiDialog {
1042
1373
  }
1043
1374
  </div>
1044
1375
 
1045
- <div class="min-h-0 flex-1 overflow-y-auto px-4 lg:px-6" data-dialog-body>
1376
+ <div
1377
+ class="min-h-0 flex-1 overflow-y-auto px-4 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) lg:px-6"
1378
+ data-dialog-body
1379
+ [attr.aria-labelledby]="scrollable() ? headingId : null"
1380
+ [attr.role]="scrollable() ? 'region' : null"
1381
+ [attr.tabindex]="scrollable() ? 0 : null"
1382
+ [id]="bodyId"
1383
+ >
1046
1384
  <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1047
1385
  <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
1048
1386
  <ng-content />
@@ -1060,7 +1398,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1060
1398
  args: [{ selector: 'ui-dialog', host: { '(pointerdown)': 'onPointerDown($event)', '(click)': 'onClick($event)' }, template: `
1061
1399
  <dialog
1062
1400
  #dlg
1063
- [attr.aria-describedby]="description() ? descriptionId : null"
1401
+ [attr.aria-describedby]="describedBy()"
1064
1402
  [attr.aria-labelledby]="headingId"
1065
1403
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1066
1404
  [class]="classes()"
@@ -1103,7 +1441,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1103
1441
  }
1104
1442
  </div>
1105
1443
 
1106
- <div class="min-h-0 flex-1 overflow-y-auto px-4 lg:px-6" data-dialog-body>
1444
+ <div
1445
+ class="min-h-0 flex-1 overflow-y-auto px-4 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) lg:px-6"
1446
+ data-dialog-body
1447
+ [attr.aria-labelledby]="scrollable() ? headingId : null"
1448
+ [attr.role]="scrollable() ? 'region' : null"
1449
+ [attr.tabindex]="scrollable() ? 0 : null"
1450
+ [id]="bodyId"
1451
+ >
1107
1452
  <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1108
1453
  <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
1109
1454
  <ng-content />
@@ -1166,7 +1511,7 @@ function rankSlices(slices, othersLabel) {
1166
1511
 
1167
1512
  const identityValue$1 = (value) => `${value}`;
1168
1513
  const identityShare = (share) => `${Math.round(share * 100)}%`;
1169
- const ROW_CLASSES$1 = 'flex w-full items-center gap-3 min-h-14 px-2 py-1 [font-variant-numeric:var(--numeric)] rounded-control text-left transition-colors duration-(--duration-fast) ease-out hover:bg-(--glow) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1514
+ const ROW_CLASSES$1 = 'flex w-full items-center gap-3 min-h-14 px-2 py-1 [font-variant-numeric:var(--numeric)] rounded-control text-left transition-colors duration-(--duration-fast) ease-out hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1170
1515
  const RAMP_DOT_CLASSES = {
1171
1516
  1: 'bg-(--ramp-1)',
1172
1517
  2: 'bg-(--ramp-2)',
@@ -1193,6 +1538,8 @@ class UiDonut {
1193
1538
  ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
1194
1539
  shareFormat = input(identityShare, /* @ts-ignore */
1195
1540
  ...(ngDevMode ? [{ debugName: "shareFormat" }] : /* istanbul ignore next */ []));
1541
+ legendHref = input(/* @ts-ignore */
1542
+ ...(ngDevMode ? [undefined, { debugName: "legendHref" }] : /* istanbul ignore next */ []));
1196
1543
  sliceSelect = output();
1197
1544
  activeId = signal(null, /* @ts-ignore */
1198
1545
  ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
@@ -1241,12 +1588,22 @@ class UiDonut {
1241
1588
  this.activeId.set(null);
1242
1589
  }
1243
1590
  }
1591
+ hrefFor(id) {
1592
+ return this.legendHref()?.(id) ?? null;
1593
+ }
1594
+ follow(event, slice) {
1595
+ if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
1596
+ return;
1597
+ }
1598
+ event.preventDefault();
1599
+ this.select(slice);
1600
+ }
1244
1601
  select(slice) {
1245
1602
  this.activeId.set(slice.id);
1246
1603
  this.sliceSelect.emit(slice.id);
1247
1604
  }
1248
1605
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, deps: [], target: i0.ɵɵFactoryTarget.Component });
1249
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDonut, isStandalone: true, selector: "ui-donut", inputs: { slices: { classPropertyName: "slices", publicName: "slices", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, othersLabel: { classPropertyName: "othersLabel", publicName: "othersLabel", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, shareFormat: { classPropertyName: "shareFormat", publicName: "shareFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sliceSelect: "sliceSelect" }, host: { classAttribute: "flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8" }, ngImport: i0, template: `
1606
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDonut, isStandalone: true, selector: "ui-donut", inputs: { slices: { classPropertyName: "slices", publicName: "slices", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, othersLabel: { classPropertyName: "othersLabel", publicName: "othersLabel", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, shareFormat: { classPropertyName: "shareFormat", publicName: "shareFormat", isSignal: true, isRequired: false, transformFunction: null }, legendHref: { classPropertyName: "legendHref", publicName: "legendHref", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sliceSelect: "sliceSelect" }, host: { classAttribute: "flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8" }, ngImport: i0, template: `
1250
1607
  <div class="relative size-[232px] flex-none sm:size-60">
1251
1608
  <svg class="block h-full w-full -rotate-90" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1252
1609
  <circle
@@ -1294,30 +1651,48 @@ class UiDonut {
1294
1651
 
1295
1652
  <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
1296
1653
  @for (slice of ranked(); track slice.id) {
1297
- <button
1298
- type="button"
1299
- [class]="rowClasses(slice)"
1300
- (blur)="clearHighlight(slice.id)"
1301
- (click)="select(slice)"
1302
- (focus)="highlight(slice.id)"
1303
- (pointerenter)="highlight(slice.id)"
1304
- (pointerleave)="clearHighlight(slice.id)"
1305
- >
1306
- <span [class]="dotClasses(slice)"></span>
1307
- <span class="flex min-w-0 flex-1 flex-col">
1308
- <span class="text-body truncate font-medium">{{ slice.label }}</span>
1309
- <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1310
- subLabel(slice)
1311
- }}</span>
1312
- </span>
1313
- <span class="flex flex-none flex-col items-end whitespace-nowrap">
1314
- <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
1315
- <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1316
- </span>
1317
- </button>
1654
+ @if (hrefFor(slice.id); as href) {
1655
+ <a
1656
+ [attr.href]="href"
1657
+ [class]="rowClasses(slice)"
1658
+ (blur)="clearHighlight(slice.id)"
1659
+ (click)="follow($event, slice)"
1660
+ (focus)="highlight(slice.id)"
1661
+ (pointerenter)="highlight(slice.id)"
1662
+ (pointerleave)="clearHighlight(slice.id)"
1663
+ >
1664
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: slice }" />
1665
+ </a>
1666
+ } @else {
1667
+ <button
1668
+ type="button"
1669
+ [class]="rowClasses(slice)"
1670
+ (blur)="clearHighlight(slice.id)"
1671
+ (click)="select(slice)"
1672
+ (focus)="highlight(slice.id)"
1673
+ (pointerenter)="highlight(slice.id)"
1674
+ (pointerleave)="clearHighlight(slice.id)"
1675
+ >
1676
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: slice }" />
1677
+ </button>
1678
+ }
1318
1679
  }
1319
1680
  </div>
1320
- `, isInline: true });
1681
+
1682
+ <ng-template #rowContent let-slice>
1683
+ <span [class]="dotClasses(slice)"></span>
1684
+ <span class="flex min-w-0 flex-1 flex-col">
1685
+ <span class="text-body truncate font-medium">{{ slice.label }}</span>
1686
+ <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1687
+ subLabel(slice)
1688
+ }}</span>
1689
+ </span>
1690
+ <span class="flex flex-none flex-col items-end whitespace-nowrap">
1691
+ <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
1692
+ <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1693
+ </span>
1694
+ </ng-template>
1695
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
1321
1696
  }
1322
1697
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, decorators: [{
1323
1698
  type: Component,
@@ -1371,37 +1746,201 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1371
1746
 
1372
1747
  <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
1373
1748
  @for (slice of ranked(); track slice.id) {
1374
- <button
1375
- type="button"
1376
- [class]="rowClasses(slice)"
1377
- (blur)="clearHighlight(slice.id)"
1378
- (click)="select(slice)"
1379
- (focus)="highlight(slice.id)"
1380
- (pointerenter)="highlight(slice.id)"
1381
- (pointerleave)="clearHighlight(slice.id)"
1382
- >
1383
- <span [class]="dotClasses(slice)"></span>
1384
- <span class="flex min-w-0 flex-1 flex-col">
1385
- <span class="text-body truncate font-medium">{{ slice.label }}</span>
1386
- <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1387
- subLabel(slice)
1388
- }}</span>
1389
- </span>
1390
- <span class="flex flex-none flex-col items-end whitespace-nowrap">
1391
- <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
1392
- <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1393
- </span>
1394
- </button>
1749
+ @if (hrefFor(slice.id); as href) {
1750
+ <a
1751
+ [attr.href]="href"
1752
+ [class]="rowClasses(slice)"
1753
+ (blur)="clearHighlight(slice.id)"
1754
+ (click)="follow($event, slice)"
1755
+ (focus)="highlight(slice.id)"
1756
+ (pointerenter)="highlight(slice.id)"
1757
+ (pointerleave)="clearHighlight(slice.id)"
1758
+ >
1759
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: slice }" />
1760
+ </a>
1761
+ } @else {
1762
+ <button
1763
+ type="button"
1764
+ [class]="rowClasses(slice)"
1765
+ (blur)="clearHighlight(slice.id)"
1766
+ (click)="select(slice)"
1767
+ (focus)="highlight(slice.id)"
1768
+ (pointerenter)="highlight(slice.id)"
1769
+ (pointerleave)="clearHighlight(slice.id)"
1770
+ >
1771
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: slice }" />
1772
+ </button>
1773
+ }
1395
1774
  }
1396
1775
  </div>
1776
+
1777
+ <ng-template #rowContent let-slice>
1778
+ <span [class]="dotClasses(slice)"></span>
1779
+ <span class="flex min-w-0 flex-1 flex-col">
1780
+ <span class="text-body truncate font-medium">{{ slice.label }}</span>
1781
+ <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1782
+ subLabel(slice)
1783
+ }}</span>
1784
+ </span>
1785
+ <span class="flex flex-none flex-col items-end whitespace-nowrap">
1786
+ <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
1787
+ <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1788
+ </span>
1789
+ </ng-template>
1397
1790
  `,
1791
+ imports: [NgTemplateOutlet],
1398
1792
  host: {
1399
1793
  class: 'flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8',
1400
1794
  },
1401
1795
  }]
1402
- }], propDecorators: { slices: [{ type: i0.Input, args: [{ isSignal: true, alias: "slices", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], othersLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "othersLabel", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], shareFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareFormat", required: false }] }], sliceSelect: [{ type: i0.Output, args: ["sliceSelect"] }] } });
1796
+ }], propDecorators: { slices: [{ type: i0.Input, args: [{ isSignal: true, alias: "slices", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], othersLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "othersLabel", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], shareFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareFormat", required: false }] }], legendHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "legendHref", required: false }] }], sliceSelect: [{ type: i0.Output, args: ["sliceSelect"] }] } });
1403
1797
 
1404
- const nextId$1 = (() => {
1798
+ /**
1799
+ * Link to another site: the label followed by an external-link icon. A 44px target on touch,
1800
+ * an underline on hover with a mouse.
1801
+ *
1802
+ * @example
1803
+ * <a ui-external-link href="https://example.com" target="_blank" rel="noopener">Page on example.com</a>
1804
+ */
1805
+ class UiExternalLink {
1806
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiExternalLink, deps: [], target: i0.ɵɵFactoryTarget.Component });
1807
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiExternalLink, isStandalone: true, selector: "a[ui-external-link]", host: { classAttribute: "inline-flex min-h-11 items-center gap-1.5 rounded-[4px] text-label leading-[17px] font-medium lg:min-h-0 hover:underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) [&>svg]:flex-none" }, ngImport: i0, template: `
1808
+ <ng-content />
1809
+ <svg
1810
+ aria-hidden="true"
1811
+ fill="none"
1812
+ height="14"
1813
+ stroke="currentColor"
1814
+ stroke-linecap="round"
1815
+ stroke-linejoin="round"
1816
+ stroke-width="1.75"
1817
+ viewBox="0 0 24 24"
1818
+ width="14"
1819
+ >
1820
+ <path d="M15 3h6v6" />
1821
+ <path d="M10 14 21 3" />
1822
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
1823
+ </svg>
1824
+ `, isInline: true });
1825
+ }
1826
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiExternalLink, decorators: [{
1827
+ type: Component,
1828
+ args: [{
1829
+ selector: 'a[ui-external-link]',
1830
+ template: `
1831
+ <ng-content />
1832
+ <svg
1833
+ aria-hidden="true"
1834
+ fill="none"
1835
+ height="14"
1836
+ stroke="currentColor"
1837
+ stroke-linecap="round"
1838
+ stroke-linejoin="round"
1839
+ stroke-width="1.75"
1840
+ viewBox="0 0 24 24"
1841
+ width="14"
1842
+ >
1843
+ <path d="M15 3h6v6" />
1844
+ <path d="M10 14 21 3" />
1845
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
1846
+ </svg>
1847
+ `,
1848
+ host: {
1849
+ class: 'inline-flex min-h-11 items-center gap-1.5 rounded-[4px] text-label leading-[17px] font-medium lg:min-h-0 hover:underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) [&>svg]:flex-none',
1850
+ },
1851
+ }]
1852
+ }] });
1853
+
1854
+ /** Available fact row sizes. `FactSize` is derived from this tuple. */
1855
+ const FACT_SIZES = ['md', 'sm', 'auto'];
1856
+ /** Colour of the line under a fact value. `stale` flags an out-of-date figure. */
1857
+ const FACT_SUB_TONES = ['subtle', 'stale'];
1858
+ const SIZE_CLASSES$2 = {
1859
+ md: { row: 'py-3 [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-body' },
1860
+ sm: { row: 'py-2.5', value: 'text-label' },
1861
+ auto: {
1862
+ row: 'py-3 lg:py-2.5 max-lg:[&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]',
1863
+ value: 'text-body lg:text-label',
1864
+ },
1865
+ };
1866
+ const SUB_TONE_CLASSES$1 = {
1867
+ subtle: 'text-(--subtle-foreground)',
1868
+ stale: 'text-(--stale)',
1869
+ };
1870
+ /**
1871
+ * The list that holds `div[ui-fact]` rows, as a native description list.
1872
+ *
1873
+ * @example
1874
+ * <dl uiFacts>
1875
+ * <div ui-fact label="Quantity">500</div>
1876
+ * <div ui-fact label="Price" sub="24/09 · Yahoo">28,64 €</div>
1877
+ * </dl>
1878
+ */
1879
+ class UiFacts {
1880
+ ruled = input(false, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1881
+ classes = computed(() => `m-0 flex flex-col${this.ruled() ? ' shadow-[inset_0_1px_0_var(--hairline)]' : ''}`, /* @ts-ignore */
1882
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1884
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiFacts, isStandalone: true, selector: "dl[uiFacts]", inputs: { ruled: { classPropertyName: "ruled", publicName: "ruled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1885
+ }
1886
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, decorators: [{
1887
+ type: Directive,
1888
+ args: [{
1889
+ selector: 'dl[uiFacts]',
1890
+ host: { '[class]': 'classes()' },
1891
+ }]
1892
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
1893
+ /**
1894
+ * One fact of a `dl[uiFacts]`: a label on the left, its value on the right, an optional sub-line
1895
+ * under the value, and a hairline above every row but the first at the `md` size (the `sm` desktop list is separated by space only). The projected content is the value.
1896
+ *
1897
+ * @example
1898
+ * <div ui-fact label="Price" sub="24/09 · Yahoo" subTone="stale">28,64 €</div>
1899
+ */
1900
+ class UiFact {
1901
+ label = input.required(/* @ts-ignore */
1902
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1903
+ sub = input(/* @ts-ignore */
1904
+ ...(ngDevMode ? [undefined, { debugName: "sub" }] : /* istanbul ignore next */ []));
1905
+ subTone = input('subtle', /* @ts-ignore */
1906
+ ...(ngDevMode ? [{ debugName: "subTone" }] : /* istanbul ignore next */ []));
1907
+ size = input('md', /* @ts-ignore */
1908
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1909
+ classes = computed(() => `flex items-baseline justify-between gap-4 ${SIZE_CLASSES$2[this.size()].row}`, /* @ts-ignore */
1910
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1911
+ valueClasses = computed(() => `${SIZE_CLASSES$2[this.size()].value} tabular-nums`, /* @ts-ignore */
1912
+ ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
1913
+ subClasses = computed(() => `text-caption ${SUB_TONE_CLASSES$1[this.subTone()]}`, /* @ts-ignore */
1914
+ ...(ngDevMode ? [{ debugName: "subClasses" }] : /* istanbul ignore next */ []));
1915
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, deps: [], target: i0.ɵɵFactoryTarget.Component });
1916
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFact, isStandalone: true, selector: "div[ui-fact]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sub: { classPropertyName: "sub", publicName: "sub", isSignal: true, isRequired: false, transformFunction: null }, subTone: { classPropertyName: "subTone", publicName: "subTone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
1917
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
1918
+ <dd class="m-0 flex flex-col items-end text-right">
1919
+ <span [class]="valueClasses()"><ng-content /></span>
1920
+ @if (sub()) {
1921
+ <span [class]="subClasses()">{{ sub() }}</span>
1922
+ }
1923
+ </dd>
1924
+ `, isInline: true });
1925
+ }
1926
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, decorators: [{
1927
+ type: Component,
1928
+ args: [{
1929
+ selector: 'div[ui-fact]',
1930
+ template: `
1931
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
1932
+ <dd class="m-0 flex flex-col items-end text-right">
1933
+ <span [class]="valueClasses()"><ng-content /></span>
1934
+ @if (sub()) {
1935
+ <span [class]="subClasses()">{{ sub() }}</span>
1936
+ }
1937
+ </dd>
1938
+ `,
1939
+ host: { '[class]': 'classes()' },
1940
+ }]
1941
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sub: [{ type: i0.Input, args: [{ isSignal: true, alias: "sub", required: false }] }], subTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "subTone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1942
+
1943
+ const nextId$2 = (() => {
1405
1944
  let count = 0;
1406
1945
  return () => `ui-field-${++count}`;
1407
1946
  })();
@@ -1430,6 +1969,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1430
1969
  },
1431
1970
  }]
1432
1971
  }] });
1972
+ /**
1973
+ * Marks an element, typically an icon button, as the trailing action of a `ui-field`: it sits on the
1974
+ * control's right edge, square to the control's height, and the control's text is padded to clear it.
1975
+ *
1976
+ * @example
1977
+ * <ui-field label="Password">
1978
+ * <input uiInput type="password" size="xl" />
1979
+ * <button uiFieldTrailing type="button" aria-label="Show the password" [attr.aria-pressed]="shown()">...</button>
1980
+ * </ui-field>
1981
+ *
1982
+ * A toggle such as this one should carry `aria-pressed`. The slot shares the right edge with `unit`:
1983
+ * set one or the other, not both.
1984
+ */
1985
+ class UiFieldTrailing {
1986
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1987
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldTrailing, isStandalone: true, selector: "[uiFieldTrailing]", host: { classAttribute: "absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)" }, ngImport: i0 });
1988
+ }
1989
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, decorators: [{
1990
+ type: Directive,
1991
+ args: [{
1992
+ selector: '[uiFieldTrailing]',
1993
+ host: {
1994
+ class: 'absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)',
1995
+ },
1996
+ }]
1997
+ }] });
1433
1998
  /**
1434
1999
  * Label, control, hint and error as one unit. The control stays a plain native element:
1435
2000
  * the field finds it in its own projected content and wires the ARIA attributes onto it.
@@ -1462,7 +2027,10 @@ class UiField {
1462
2027
  ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
1463
2028
  unit = input(/* @ts-ignore */
1464
2029
  ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
1465
- #id = nextId$1();
2030
+ optional = input(/* @ts-ignore */
2031
+ ...(ngDevMode ? [undefined, { debugName: "optional" }] : /* istanbul ignore next */ []));
2032
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2033
+ #id = nextId$2();
1466
2034
  hintId = `${this.#id}-hint`;
1467
2035
  errorId = `${this.#id}-error`;
1468
2036
  #host = inject(ElementRef);
@@ -1470,7 +2038,9 @@ class UiField {
1470
2038
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
1471
2039
  leading = contentChild(UiFieldLeading, /* @ts-ignore */
1472
2040
  ...(ngDevMode ? [{ debugName: "leading" }] : /* istanbul ignore next */ []));
1473
- rowClasses = computed(() => this.leading() ? 'relative flex items-center [&>input]:pl-[38px]' : 'relative flex items-center', /* @ts-ignore */
2041
+ trailing = contentChild(UiFieldTrailing, /* @ts-ignore */
2042
+ ...(ngDevMode ? [{ debugName: "trailing" }] : /* istanbul ignore next */ []));
2043
+ rowClasses = computed(() => `relative flex items-center${this.leading() ? ' [&>input]:pl-[38px]' : ''}${this.trailing() ? ' [&>input]:pr-12 lg:[&>input]:pr-11' : ''}`, /* @ts-ignore */
1474
2044
  ...(ngDevMode ? [{ debugName: "rowClasses" }] : /* istanbul ignore next */ []));
1475
2045
  labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)', /* @ts-ignore */
1476
2046
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
@@ -1491,6 +2061,7 @@ class UiField {
1491
2061
  }, /* @ts-ignore */
1492
2062
  ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
1493
2063
  constructor() {
2064
+ let markedByField = false;
1494
2065
  afterRenderEffect(() => {
1495
2066
  // querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
1496
2067
  const control = this.#host.nativeElement.querySelector('input, select, textarea');
@@ -1506,24 +2077,31 @@ class UiField {
1506
2077
  else {
1507
2078
  control.removeAttribute('aria-describedby');
1508
2079
  }
1509
- if (this.message()) {
2080
+ if (this.message() || this.invalid()) {
1510
2081
  control.setAttribute('aria-invalid', 'true');
2082
+ markedByField = true;
1511
2083
  }
1512
- else {
2084
+ else if (markedByField) {
1513
2085
  control.removeAttribute('aria-invalid');
2086
+ markedByField = false;
1514
2087
  }
1515
2088
  });
1516
2089
  }
1517
2090
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
1518
- 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 }, { propertyName: "leading", first: true, predicate: UiFieldLeading, descendants: true, isSignal: true }], ngImport: i0, template: `
2091
+ 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 }, optional: { classPropertyName: "optional", publicName: "optional", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }, { propertyName: "leading", first: true, predicate: UiFieldLeading, descendants: true, isSignal: true }, { propertyName: "trailing", first: true, predicate: UiFieldTrailing, descendants: true, isSignal: true }], ngImport: i0, template: `
1519
2092
  <div class="group flex flex-col gap-1.5">
1520
2093
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
1521
- <label [class]="labelClasses()">{{ label() }}</label>
2094
+ <label [class]="labelClasses()"
2095
+ >{{ label() }}
2096
+ @if (optional()) {
2097
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2098
+ }
2099
+ </label>
1522
2100
 
1523
2101
  <div [class]="rowClasses()">
1524
2102
  <ng-content select="[uiFieldLeading]" />
1525
2103
  <ng-content />
1526
-
2104
+ <ng-content select="[uiFieldTrailing]" />
1527
2105
  @if (unit()) {
1528
2106
  <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
1529
2107
  }
@@ -1542,32 +2120,117 @@ class UiField {
1542
2120
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, decorators: [{
1543
2121
  type: Component,
1544
2122
  args: [{
1545
- selector: 'ui-field',
2123
+ selector: 'ui-field',
2124
+ template: `
2125
+ <div class="group flex flex-col gap-1.5">
2126
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
2127
+ <label [class]="labelClasses()"
2128
+ >{{ label() }}
2129
+ @if (optional()) {
2130
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2131
+ }
2132
+ </label>
2133
+
2134
+ <div [class]="rowClasses()">
2135
+ <ng-content select="[uiFieldLeading]" />
2136
+ <ng-content />
2137
+ <ng-content select="[uiFieldTrailing]" />
2138
+ @if (unit()) {
2139
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
2140
+ }
2141
+ </div>
2142
+
2143
+ @if (hint()) {
2144
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
2145
+ }
2146
+
2147
+ @if (message()) {
2148
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2149
+ }
2150
+ </div>
2151
+ `,
2152
+ }]
2153
+ }], 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 }] }], optional: [{ type: i0.Input, args: [{ isSignal: true, alias: "optional", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }], leading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldLeading), { isSignal: true }] }], trailing: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldTrailing), { isSignal: true }] }] } });
2154
+
2155
+ const CHIP_CLASSES = 'flex-none flex h-11 items-center rounded-pill border-0 bg-transparent p-0 font-[inherit] cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) pointer-fine:h-8 pointer-fine:focus-visible:outline-offset-2';
2156
+ const PILL_CLASSES = 'flex h-[34px] items-center gap-1.5 rounded-pill px-3.5 text-label font-medium whitespace-nowrap pointer-fine:h-8 pointer-fine:px-3 pointer-fine:group-hover/chip:shadow-[inset_0_0_0_1px_var(--muted-foreground)]';
2157
+ const SELECTED_PILL_CLASSES = 'bg-(--primary) text-(--primary-foreground)';
2158
+ const UNSELECTED_PILL_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
2159
+ /**
2160
+ * A row of single-choice filter pills, each with an optional count, that narrows a list in place:
2161
+ * an asset class, a status. On touch it scrolls on one line, bleeding to the screen edges; with a
2162
+ * fine pointer it wraps.
2163
+ *
2164
+ * @example
2165
+ * <ui-filter-chips ariaLabel="Filter by class" [options]="classes()" [(value)]="selectedClass" />
2166
+ */
2167
+ class UiFilterChips {
2168
+ options = input.required(/* @ts-ignore */
2169
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2170
+ value = model.required(/* @ts-ignore */
2171
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2172
+ ariaLabel = input.required(/* @ts-ignore */
2173
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2174
+ chipClasses = CHIP_CLASSES;
2175
+ pillClasses(optionValue) {
2176
+ return `${PILL_CLASSES} ${optionValue === this.value() ? SELECTED_PILL_CLASSES : UNSELECTED_PILL_CLASSES}`;
2177
+ }
2178
+ countClasses(optionValue) {
2179
+ return optionValue === this.value() ? 'text-(--primary-foreground)' : 'text-(--subtle-foreground)';
2180
+ }
2181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
2182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFilterChips, isStandalone: true, selector: "ui-filter-chips", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "ariaLabel()" }, classAttribute: "flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0" }, ngImport: i0, template: `
2183
+ @for (option of options(); track option.value) {
2184
+ <button
2185
+ class="group/chip"
2186
+ type="button"
2187
+ [attr.aria-pressed]="option.value === value()"
2188
+ [class]="chipClasses"
2189
+ (click)="value.set(option.value)"
2190
+ >
2191
+ <span data-chip [class]="pillClasses(option.value)"
2192
+ >{{ option.label }}
2193
+ @if (option.count !== undefined && option.count !== null) {
2194
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2195
+ option.count
2196
+ }}</span>
2197
+ }
2198
+ </span>
2199
+ </button>
2200
+ }
2201
+ `, isInline: true });
2202
+ }
2203
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, decorators: [{
2204
+ type: Component,
2205
+ args: [{
2206
+ selector: 'ui-filter-chips',
1546
2207
  template: `
1547
- <div class="group flex flex-col gap-1.5">
1548
- <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
1549
- <label [class]="labelClasses()">{{ label() }}</label>
1550
-
1551
- <div [class]="rowClasses()">
1552
- <ng-content select="[uiFieldLeading]" />
1553
- <ng-content />
1554
-
1555
- @if (unit()) {
1556
- <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
1557
- }
1558
- </div>
1559
-
1560
- @if (hint()) {
1561
- <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
1562
- }
1563
-
1564
- @if (message()) {
1565
- <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
1566
- }
1567
- </div>
2208
+ @for (option of options(); track option.value) {
2209
+ <button
2210
+ class="group/chip"
2211
+ type="button"
2212
+ [attr.aria-pressed]="option.value === value()"
2213
+ [class]="chipClasses"
2214
+ (click)="value.set(option.value)"
2215
+ >
2216
+ <span data-chip [class]="pillClasses(option.value)"
2217
+ >{{ option.label }}
2218
+ @if (option.count !== undefined && option.count !== null) {
2219
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2220
+ option.count
2221
+ }}</span>
2222
+ }
2223
+ </span>
2224
+ </button>
2225
+ }
1568
2226
  `,
2227
+ host: {
2228
+ role: 'group',
2229
+ class: 'flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0',
2230
+ '[attr.aria-label]': 'ariaLabel()',
2231
+ },
1569
2232
  }]
1570
- }], 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 }] }], leading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldLeading), { isSignal: true }] }] } });
2233
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }] } });
1571
2234
 
1572
2235
  const axisTicks = (points, maxTicks = 5) => {
1573
2236
  if (points.length <= maxTicks) {
@@ -1824,6 +2487,8 @@ class UiLineChart {
1824
2487
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1825
2488
  valueFormat = input(identityValue, /* @ts-ignore */
1826
2489
  ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
2490
+ tooltipFormat = input(/* @ts-ignore */
2491
+ ...(ngDevMode ? [undefined, { debugName: "tooltipFormat" }] : /* istanbul ignore next */ []));
1827
2492
  deltaFormat = input(identityDelta, /* @ts-ignore */
1828
2493
  ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1829
2494
  timeFormat = input(identityTime, /* @ts-ignore */
@@ -1986,7 +2651,7 @@ class UiLineChart {
1986
2651
  this.activeIndex.set(navigation.index);
1987
2652
  }
1988
2653
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1989
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
2654
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, tooltipFormat: { classPropertyName: "tooltipFormat", publicName: "tooltipFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
1990
2655
  <div class="relative h-full">
1991
2656
  <svg
1992
2657
  #svgRef
@@ -2097,36 +2762,35 @@ class UiLineChart {
2097
2762
  [class]="tooltipClasses().box"
2098
2763
  [style]="tooltipStyle(point)"
2099
2764
  >
2100
- <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2765
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2766
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2767
+ </p>
2101
2768
  @if (tooltipDelta()) {
2102
2769
  <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2103
2770
  }
2104
2771
  <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
2105
2772
  </div>
2106
2773
  }
2107
-
2108
2774
  @if (points().length > 0) {
2109
- <table class="sr-only">
2110
- <caption>
2111
- {{
2112
- label()
2113
- }}
2114
- </caption>
2115
- <thead>
2116
- <tr>
2117
- <th scope="col">{{ timeColumnLabel() }}</th>
2118
- <th scope="col">{{ valueColumnLabel() }}</th>
2119
- </tr>
2120
- </thead>
2121
- <tbody>
2122
- @for (point of points(); track point.t) {
2775
+ <div class="sr-only">
2776
+ <table>
2777
+ <caption [textContent]="label()"></caption>
2778
+ <thead>
2123
2779
  <tr>
2124
- <td>{{ timeFormat()(point.t) }}</td>
2125
- <td>{{ valueFormat()(point.v) }}</td>
2780
+ <th scope="col">{{ timeColumnLabel() }}</th>
2781
+ <th scope="col">{{ valueColumnLabel() }}</th>
2126
2782
  </tr>
2127
- }
2128
- </tbody>
2129
- </table>
2783
+ </thead>
2784
+ <tbody>
2785
+ @for (point of points(); track point.t) {
2786
+ <tr>
2787
+ <td>{{ timeFormat()(point.t) }}</td>
2788
+ <td>{{ valueFormat()(point.v) }}</td>
2789
+ </tr>
2790
+ }
2791
+ </tbody>
2792
+ </table>
2793
+ </div>
2130
2794
  }
2131
2795
  </div>
2132
2796
  `, isInline: true });
@@ -2246,36 +2910,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2246
2910
  [class]="tooltipClasses().box"
2247
2911
  [style]="tooltipStyle(point)"
2248
2912
  >
2249
- <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2913
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2914
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2915
+ </p>
2250
2916
  @if (tooltipDelta()) {
2251
2917
  <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2252
2918
  }
2253
2919
  <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
2254
2920
  </div>
2255
2921
  }
2256
-
2257
2922
  @if (points().length > 0) {
2258
- <table class="sr-only">
2259
- <caption>
2260
- {{
2261
- label()
2262
- }}
2263
- </caption>
2264
- <thead>
2265
- <tr>
2266
- <th scope="col">{{ timeColumnLabel() }}</th>
2267
- <th scope="col">{{ valueColumnLabel() }}</th>
2268
- </tr>
2269
- </thead>
2270
- <tbody>
2271
- @for (point of points(); track point.t) {
2923
+ <div class="sr-only">
2924
+ <table>
2925
+ <caption [textContent]="label()"></caption>
2926
+ <thead>
2272
2927
  <tr>
2273
- <td>{{ timeFormat()(point.t) }}</td>
2274
- <td>{{ valueFormat()(point.v) }}</td>
2928
+ <th scope="col">{{ timeColumnLabel() }}</th>
2929
+ <th scope="col">{{ valueColumnLabel() }}</th>
2275
2930
  </tr>
2276
- }
2277
- </tbody>
2278
- </table>
2931
+ </thead>
2932
+ <tbody>
2933
+ @for (point of points(); track point.t) {
2934
+ <tr>
2935
+ <td>{{ timeFormat()(point.t) }}</td>
2936
+ <td>{{ valueFormat()(point.v) }}</td>
2937
+ </tr>
2938
+ }
2939
+ </tbody>
2940
+ </table>
2941
+ </div>
2279
2942
  }
2280
2943
  </div>
2281
2944
  `,
@@ -2283,11 +2946,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2283
2946
  class: 'block h-full',
2284
2947
  },
2285
2948
  }]
2286
- }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
2949
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], tooltipFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
2287
2950
 
2288
2951
  const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16),inset_0_0_0_1px_var(--border)] 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';
2289
- const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
2290
- const nextId = (() => {
2952
+ const SHEET_CLASSES = 'max-lg:fixed max-lg:inset-x-4 max-lg:top-auto max-lg:w-auto max-lg:bottom-[calc(76px+env(safe-area-inset-bottom))] max-lg:min-w-0 max-lg:p-1.5 max-lg:bg-(--card) max-lg:shadow-[0_12px_32px_rgb(0_0_0/0.2),inset_0_0_0_1px_var(--border)] max-lg:backdrop:bg-[rgb(0_0_0/0.36)] max-lg:transition-[opacity,translate,overlay,display] max-lg:scale-100 max-lg:translate-y-4 max-lg:open:translate-y-0 max-lg:starting:open:scale-100 max-lg:starting:open:translate-y-4';
2953
+ const SHEET_ITEM_CLASSES = 'flex w-full items-center gap-3 lg:pointer-fine:gap-2.5 min-h-12 lg:min-h-11 lg:pointer-fine:min-h-9 px-3 lg:pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-6px)] lg:rounded-[calc(var(--radius-container)-4px)] text-body lg:pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-5 [:where(&>svg)]:flex-none lg:[:where(&>svg)]:size-[18px] lg:pointer-fine:[:where(&>svg)]:size-4';
2954
+ const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-[18px] [:where(&>svg)]:flex-none pointer-fine:[:where(&>svg)]:size-4';
2955
+ const nextId$1 = (() => {
2291
2956
  let count = 0;
2292
2957
  return () => `ui-menu-${++count}`;
2293
2958
  })();
@@ -2301,7 +2966,7 @@ class UiMenuItem {
2301
2966
  destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2302
2967
  #host = inject(ElementRef).nativeElement;
2303
2968
  #menu = inject(UiMenu);
2304
- classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
2969
+ classes = computed(() => `${this.#menu.sheet() ? SHEET_ITEM_CLASSES : ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
2305
2970
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2306
2971
  focus() {
2307
2972
  this.#host.focus();
@@ -2328,7 +2993,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2328
2993
  }]
2329
2994
  }], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
2330
2995
  /**
2331
- * Secondary actions menu, opened from its trigger through the native Popover API.
2996
+ * Secondary actions menu, opened from its trigger through the native Popover API. With `sheet`, below
2997
+ * 64rem it opens as a bottom action sheet above the tab bar instead of beside its trigger, and
2998
+ * `heading` names what the actions apply to. `width` fixes the outer popover width in pixels (the sheet
2999
+ * always spans the screen below 64rem); without it the menu fits its content.
2332
3000
  *
2333
3001
  * @example
2334
3002
  * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
@@ -2340,10 +3008,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2340
3008
  class UiMenu {
2341
3009
  label = input.required(/* @ts-ignore */
2342
3010
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2343
- menuId = nextId();
3011
+ sheet = input(false, { ...(ngDevMode ? { debugName: "sheet" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3012
+ heading = input(/* @ts-ignore */
3013
+ ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
3014
+ width = input(undefined, { ...(ngDevMode ? { debugName: "width" } : /* istanbul ignore next */ {}), transform: numberAttribute });
3015
+ menuId = nextId$1();
2344
3016
  isOpen = signal(false, /* @ts-ignore */
2345
3017
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2346
- classes = MENU_CLASSES;
3018
+ hasWidth = computed(() => Number.isFinite(this.width()), /* @ts-ignore */
3019
+ ...(ngDevMode ? [{ debugName: "hasWidth" }] : /* istanbul ignore next */ []));
3020
+ widthVar = computed(() => (this.hasWidth() ? `${this.width()}px` : null), /* @ts-ignore */
3021
+ ...(ngDevMode ? [{ debugName: "widthVar" }] : /* istanbul ignore next */ []));
3022
+ classes = computed(() => {
3023
+ const width = !this.hasWidth() ? '' : this.sheet() ? ' lg:w-(--ui-menu-width)' : ' w-(--ui-menu-width)';
3024
+ return `${MENU_CLASSES}${this.sheet() ? ` ${SHEET_CLASSES}` : ''}${width}`;
3025
+ }, /* @ts-ignore */
3026
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2347
3027
  items = contentChildren(UiMenuItem, /* @ts-ignore */
2348
3028
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2349
3029
  #host = inject(ElementRef).nativeElement;
@@ -2354,7 +3034,14 @@ class UiMenu {
2354
3034
  if (typeof this.#host.showPopover === 'function') {
2355
3035
  this.#host.showPopover();
2356
3036
  }
2357
- this.#position(trigger);
3037
+ if (this.#isSheet()) {
3038
+ this.#host.style.position = '';
3039
+ this.#host.style.top = '';
3040
+ this.#host.style.left = '';
3041
+ }
3042
+ else {
3043
+ this.#position(trigger);
3044
+ }
2358
3045
  this.items()[0]?.focus();
2359
3046
  }
2360
3047
  close() {
@@ -2404,6 +3091,9 @@ class UiMenu {
2404
3091
  event.preventDefault();
2405
3092
  items[nextIndex]?.focus();
2406
3093
  }
3094
+ #isSheet() {
3095
+ return this.sheet() && typeof window.matchMedia === 'function' && !window.matchMedia('(min-width: 64rem)').matches;
3096
+ }
2407
3097
  #position(trigger) {
2408
3098
  const margin = 8;
2409
3099
  const triggerRect = trigger.getBoundingClientRect();
@@ -2417,24 +3107,39 @@ class UiMenu {
2417
3107
  this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
2418
3108
  }
2419
3109
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2420
- 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 });
3110
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiMenu, isStandalone: true, selector: "ui-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sheet: { classPropertyName: "sheet", publicName: "sheet", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" }, listeners: { "toggle": "onToggle($event)", "keydown": "onKeydown($event)" }, properties: { "id": "menuId", "style.--ui-menu-width": "widthVar()", "class": "classes()", "attr.aria-label": "label()" } }, queries: [{ propertyName: "items", predicate: UiMenuItem, isSignal: true }], ngImport: i0, template: `
3111
+ @if (sheet() && heading()) {
3112
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3113
+ {{ heading() }}
3114
+ </div>
3115
+ }
3116
+ <ng-content />
3117
+ `, isInline: true });
2421
3118
  }
2422
3119
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
2423
3120
  type: Component,
2424
3121
  args: [{
2425
3122
  selector: 'ui-menu',
2426
- template: '<ng-content />',
3123
+ template: `
3124
+ @if (sheet() && heading()) {
3125
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3126
+ {{ heading() }}
3127
+ </div>
3128
+ }
3129
+ <ng-content />
3130
+ `,
2427
3131
  host: {
2428
3132
  role: 'menu',
2429
3133
  popover: 'auto',
2430
3134
  '[id]': 'menuId',
2431
- '[class]': 'classes',
3135
+ '[style.--ui-menu-width]': 'widthVar()',
3136
+ '[class]': 'classes()',
2432
3137
  '[attr.aria-label]': 'label()',
2433
3138
  '(toggle)': 'onToggle($event)',
2434
3139
  '(keydown)': 'onKeydown($event)',
2435
3140
  },
2436
3141
  }]
2437
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
3142
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sheet: [{ type: i0.Input, args: [{ isSignal: true, alias: "sheet", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
2438
3143
  /**
2439
3144
  * Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
2440
3145
  * `aria-controls` on it.
@@ -2471,6 +3176,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2471
3176
  }]
2472
3177
  }], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
2473
3178
 
3179
+ /** Track thicknesses. `MeterSize` is derived from this tuple. */
3180
+ const METER_SIZES = ['md', 'sm'];
3181
+ const TRACK_CLASSES = {
3182
+ md: 'h-1.5',
3183
+ sm: 'h-1',
3184
+ };
2474
3185
  /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
2475
3186
  const METER_TONES = [1, 2, 3, 4, 5, 6];
2476
3187
  const TONE_CLASSES = {
@@ -2497,20 +3208,25 @@ class UiMeter {
2497
3208
  ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
2498
3209
  tone = input(1, /* @ts-ignore */
2499
3210
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3211
+ size = input('md', /* @ts-ignore */
3212
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3213
+ trackClasses = computed(() => TRACK_CLASSES[this.size()], /* @ts-ignore */
3214
+ ...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
2500
3215
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
2501
3216
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
2502
3217
  fillClasses = computed(() => `h-full rounded-pill ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
2503
3218
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
2504
3219
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
2505
- 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: `
3220
+ 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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2506
3221
  <div
2507
- class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
3222
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
2508
3223
  role="meter"
2509
3224
  [attr.aria-label]="label()"
2510
3225
  [attr.aria-valuemax]="1"
2511
3226
  [attr.aria-valuemin]="0"
2512
3227
  [attr.aria-valuenow]="value()"
2513
3228
  [attr.aria-valuetext]="valueText()"
3229
+ [class]="trackClasses()"
2514
3230
  >
2515
3231
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
2516
3232
  </div>
@@ -2522,23 +3238,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2522
3238
  selector: 'ui-meter',
2523
3239
  template: `
2524
3240
  <div
2525
- class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
3241
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
2526
3242
  role="meter"
2527
3243
  [attr.aria-label]="label()"
2528
3244
  [attr.aria-valuemax]="1"
2529
3245
  [attr.aria-valuemin]="0"
2530
3246
  [attr.aria-valuenow]="value()"
2531
3247
  [attr.aria-valuetext]="valueText()"
3248
+ [class]="trackClasses()"
2532
3249
  >
2533
3250
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
2534
3251
  </div>
2535
3252
  `,
2536
3253
  }]
2537
- }], 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 }] }] } });
3254
+ }], 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 }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2538
3255
 
2539
- const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label font-medium transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3256
+ const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label leading-normal font-medium transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
2540
3257
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
2541
- const IDLE_CLASSES = 'text-(--muted-foreground)';
3258
+ const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
2542
3259
  /**
2543
3260
  * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
2544
3261
  *
@@ -2567,13 +3284,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2567
3284
  }]
2568
3285
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
2569
3286
 
2570
- const ROW_SIZES = ['md', 'lg', 'xl'];
3287
+ const ROW_SIZES = ['md', 'lg', 'xl', 'card', 'dense'];
2571
3288
  const ROW_PADDINGS = ['md', 'sm', 'none'];
2572
- const SIZE_CLASSES = {
2573
- md: 'min-h-14 gap-2.5',
2574
- lg: 'min-h-15 gap-2.5',
2575
- xl: 'min-h-18 pointer-fine:min-h-17 gap-3',
3289
+ const ROW_GAPS = ['default', 'sm'];
3290
+ const SIZE_CLASSES$1 = {
3291
+ md: 'min-h-14',
3292
+ lg: 'min-h-15',
3293
+ xl: 'min-h-18 pointer-fine:min-h-17',
3294
+ card: 'min-h-17',
3295
+ dense: 'min-h-14 lg:min-h-12',
2576
3296
  };
3297
+ const SIZE_GAPS = { md: 'gap-2.5', lg: 'gap-2.5', xl: 'gap-3', card: 'gap-2', dense: 'gap-3' };
2577
3298
  const PADDING_CLASSES = { md: 'px-2.5', sm: 'px-2', none: 'px-0' };
2578
3299
  const BASE_CLASSES = 'flex w-full items-center 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)';
2579
3300
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
@@ -2591,10 +3312,12 @@ class UiRow {
2591
3312
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2592
3313
  padding = input('md', /* @ts-ignore */
2593
3314
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
2594
- classes = computed(() => `${BASE_CLASSES} ${SIZE_CLASSES[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
3315
+ gap = input('default', /* @ts-ignore */
3316
+ ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
3317
+ classes = computed(() => `${BASE_CLASSES} ${SIZE_CLASSES$1[this.size()]} ${this.gap() === 'sm' ? 'gap-2' : SIZE_GAPS[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
2595
3318
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2596
3319
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
2597
- 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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
3320
+ 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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
2598
3321
  }
2599
3322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
2600
3323
  type: Component,
@@ -2606,9 +3329,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2606
3329
  '[attr.aria-current]': "selected() ? 'true' : null",
2607
3330
  },
2608
3331
  }]
2609
- }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }] } });
3332
+ }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }] } });
3333
+ /**
3334
+ * Lays out a `ui-row` with a trailing action beside it, such as a "more" icon button: the row takes
3335
+ * the room the action leaves and the action keeps its own size.
3336
+ *
3337
+ * @example
3338
+ * <div uiRowItem>
3339
+ * <a ui-row size="xl" padding="sm" routerLink="/accounts/1">...</a>
3340
+ * <button ui-button variant="quiet" size="icon-sm" aria-label="Account actions">...</button>
3341
+ * </div>
3342
+ */
3343
+ class UiRowItem {
3344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3345
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiRowItem, isStandalone: true, selector: "[uiRowItem]", host: { classAttribute: "flex items-center gap-1 [&>[ui-row]]:w-auto [&>[ui-row]]:min-w-0 [&>[ui-row]]:flex-1 [&>:not([ui-row])]:flex-none" }, ngImport: i0 });
3346
+ }
3347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, decorators: [{
3348
+ type: Directive,
3349
+ args: [{
3350
+ selector: '[uiRowItem]',
3351
+ host: {
3352
+ class: 'flex items-center gap-1 [&>[ui-row]]:w-auto [&>[ui-row]]:min-w-0 [&>[ui-row]]:flex-1 [&>:not([ui-row])]:flex-none',
3353
+ },
3354
+ }]
3355
+ }] });
3356
+ /**
3357
+ * Non-interactive row of a list, separated from the next by a hairline: a passkey with its delete
3358
+ * button, a device, a setting. It is a 72px row (68px from 64rem) with a 12px gap; put it on an
3359
+ * `li` or a `div`. Use `ui-row` instead when the whole row is a link or an action.
3360
+ *
3361
+ * @example
3362
+ * <ul>
3363
+ * <li uiListRow>
3364
+ * <span class="flex-1">MacBook Air</span>
3365
+ * <button ui-button variant="quiet-destructive" size="icon-sm" aria-label="Delete MacBook Air">...</button>
3366
+ * </li>
3367
+ * </ul>
3368
+ */
3369
+ class UiListRow {
3370
+ ruled = input(true, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3371
+ classes = computed(() => `flex min-h-18 items-center gap-3 py-1.5 lg:min-h-17${this.ruled() ? ' shadow-[inset_0_-1px_0_var(--hairline)]' : ''}`, /* @ts-ignore */
3372
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3373
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3374
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiListRow, isStandalone: true, selector: "[uiListRow]", inputs: { ruled: { classPropertyName: "ruled", publicName: "ruled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
3375
+ }
3376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, decorators: [{
3377
+ type: Directive,
3378
+ args: [{
3379
+ selector: '[uiListRow]',
3380
+ host: { '[class]': 'classes()' },
3381
+ }]
3382
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
2610
3383
 
2611
- const OPTION_CLASSES = "relative z-10 h-10 pointer-fine:h-8 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) active:scale-(--press-scale)";
3384
+ const nextId = (() => {
3385
+ let count = 0;
3386
+ return () => `ui-segmented-${++count}`;
3387
+ })();
3388
+ const OPTION_CLASSES = "relative z-10 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) active:scale-(--press-scale)";
3389
+ const SEGMENTED_SIZES = ['md', 'sm'];
3390
+ const SIZE_CLASSES = {
3391
+ md: 'h-10 pointer-fine:h-8',
3392
+ sm: 'h-10 pointer-fine:h-[30px]',
3393
+ };
2612
3394
  const SELECTED_CLASSES = 'text-(--foreground)';
2613
3395
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
2614
3396
  /**
@@ -2623,8 +3405,12 @@ class UiSegmented {
2623
3405
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2624
3406
  label = input.required(/* @ts-ignore */
2625
3407
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3408
+ showLabel = input(false, { ...(ngDevMode ? { debugName: "showLabel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3409
+ labelId = nextId();
2626
3410
  value = model.required(/* @ts-ignore */
2627
3411
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3412
+ size = input('md', /* @ts-ignore */
3413
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2628
3414
  radios = viewChildren('radio', /* @ts-ignore */
2629
3415
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
2630
3416
  thumbIndex = computed(() => {
@@ -2633,7 +3419,7 @@ class UiSegmented {
2633
3419
  }, /* @ts-ignore */
2634
3420
  ...(ngDevMode ? [{ debugName: "thumbIndex" }] : /* istanbul ignore next */ []));
2635
3421
  optionClasses(optionValue) {
2636
- return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
3422
+ return `${OPTION_CLASSES} ${SIZE_CLASSES[this.size()]} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
2637
3423
  }
2638
3424
  tabIndexFor(optionValue) {
2639
3425
  const opts = this.options();
@@ -2657,37 +3443,45 @@ class UiSegmented {
2657
3443
  this.radios()[nextIndex]?.nativeElement.focus();
2658
3444
  }
2659
3445
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
2660
- 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: `
2661
- <div
2662
- class="rounded-control relative grid bg-(--muted) p-0.5"
2663
- role="radiogroup"
2664
- [attr.aria-label]="label()"
2665
- [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
2666
- >
2667
- @if (thumbIndex(); as index) {
2668
- <span
2669
- aria-hidden="true"
2670
- 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),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
2671
- data-thumb
2672
- [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2673
- [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2674
- ></span>
3446
+ 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 }, showLabel: { classPropertyName: "showLabel", publicName: "showLabel", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
3447
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3448
+ @if (showLabel()) {
3449
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3450
+ label()
3451
+ }}</span>
2675
3452
  }
3453
+ <div
3454
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3455
+ role="radiogroup"
3456
+ [attr.aria-label]="showLabel() ? null : label()"
3457
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3458
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3459
+ >
3460
+ @if (thumbIndex(); as index) {
3461
+ <span
3462
+ aria-hidden="true"
3463
+ 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),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
3464
+ data-thumb
3465
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3466
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3467
+ ></span>
3468
+ }
2676
3469
 
2677
- @for (option of options(); track option.value) {
2678
- <button
2679
- #radio
2680
- role="radio"
2681
- type="button"
2682
- [attr.aria-checked]="option.value === value()"
2683
- [attr.tabindex]="tabIndexFor(option.value)"
2684
- [class]="optionClasses(option.value)"
2685
- (click)="select(option.value)"
2686
- (keydown)="onKeydown($event)"
2687
- >
2688
- {{ option.label }}
2689
- </button>
2690
- }
3470
+ @for (option of options(); track option.value) {
3471
+ <button
3472
+ #radio
3473
+ role="radio"
3474
+ type="button"
3475
+ [attr.aria-checked]="option.value === value()"
3476
+ [attr.tabindex]="tabIndexFor(option.value)"
3477
+ [class]="optionClasses(option.value)"
3478
+ (click)="select(option.value)"
3479
+ (keydown)="onKeydown($event)"
3480
+ >
3481
+ {{ option.label }}
3482
+ </button>
3483
+ }
3484
+ </div>
2691
3485
  </div>
2692
3486
  `, isInline: true });
2693
3487
  }
@@ -2696,40 +3490,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2696
3490
  args: [{
2697
3491
  selector: 'ui-segmented',
2698
3492
  template: `
2699
- <div
2700
- class="rounded-control relative grid bg-(--muted) p-0.5"
2701
- role="radiogroup"
2702
- [attr.aria-label]="label()"
2703
- [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
2704
- >
2705
- @if (thumbIndex(); as index) {
2706
- <span
2707
- aria-hidden="true"
2708
- 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),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
2709
- data-thumb
2710
- [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2711
- [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2712
- ></span>
3493
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3494
+ @if (showLabel()) {
3495
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3496
+ label()
3497
+ }}</span>
2713
3498
  }
3499
+ <div
3500
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3501
+ role="radiogroup"
3502
+ [attr.aria-label]="showLabel() ? null : label()"
3503
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3504
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3505
+ >
3506
+ @if (thumbIndex(); as index) {
3507
+ <span
3508
+ aria-hidden="true"
3509
+ 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),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
3510
+ data-thumb
3511
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3512
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3513
+ ></span>
3514
+ }
2714
3515
 
2715
- @for (option of options(); track option.value) {
2716
- <button
2717
- #radio
2718
- role="radio"
2719
- type="button"
2720
- [attr.aria-checked]="option.value === value()"
2721
- [attr.tabindex]="tabIndexFor(option.value)"
2722
- [class]="optionClasses(option.value)"
2723
- (click)="select(option.value)"
2724
- (keydown)="onKeydown($event)"
2725
- >
2726
- {{ option.label }}
2727
- </button>
2728
- }
3516
+ @for (option of options(); track option.value) {
3517
+ <button
3518
+ #radio
3519
+ role="radio"
3520
+ type="button"
3521
+ [attr.aria-checked]="option.value === value()"
3522
+ [attr.tabindex]="tabIndexFor(option.value)"
3523
+ [class]="optionClasses(option.value)"
3524
+ (click)="select(option.value)"
3525
+ (keydown)="onKeydown($event)"
3526
+ >
3527
+ {{ option.label }}
3528
+ </button>
3529
+ }
3530
+ </div>
2729
3531
  </div>
2730
3532
  `,
2731
3533
  }]
2732
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
3534
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], showLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabel", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
2733
3535
 
2734
3536
  const SELECT_CLASSES = 'cursor-pointer appearance-none pr-10 bg-(image:--chevron-down) bg-no-repeat bg-[position:right_0.75rem_center] bg-[size:1.125rem]';
2735
3537
  // Spelled out prefix included: Tailwind scans for literal strings, so a prefix applied in code
@@ -2790,7 +3592,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2790
3592
  }], 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 }] }] } });
2791
3593
 
2792
3594
  /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
2793
- const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
3595
+ const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring', 'dot'];
2794
3596
  const LAST_LINE_WIDTH = '62%';
2795
3597
  const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
2796
3598
  const BAR_CLASSES = 'block rounded-control bg-(--muted)';
@@ -2818,7 +3620,7 @@ class UiSkeleton {
2818
3620
  }, /* @ts-ignore */
2819
3621
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
2820
3622
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
2821
- 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 }, size: { classPropertyName: "size", publicName: "size", 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-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\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-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\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"] });
3623
+ 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 }, size: { classPropertyName: "size", publicName: "size", 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-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\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-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('dot') {\n <span class=\"rounded-pill size-3 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\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"] });
2822
3624
  }
2823
3625
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
2824
3626
  type: Component,
@@ -2839,6 +3641,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2839
3641
  @case ('chart') {
2840
3642
  <span class="rounded-control min-h-50 w-full flex-1 bg-(--muted)" [style.animation]="animation"></span>
2841
3643
  }
3644
+ @case ('dot') {
3645
+ <span class="rounded-pill size-3 bg-(--muted)" [style.animation]="animation"></span>
3646
+ }
2842
3647
  @case ('ring') {
2843
3648
  <span
2844
3649
  class="rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]"
@@ -2975,6 +3780,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2975
3780
  }]
2976
3781
  }] });
2977
3782
 
3783
+ const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-[3px] touch-manipulation select-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3784
+ const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3785
+ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3786
+ /**
3787
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
3788
+ *
3789
+ * @example
3790
+ * <nav ui-tab-bar>
3791
+ * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3792
+ * </nav>
3793
+ */
3794
+ class UiTabBar {
3795
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3796
+ 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 });
3797
+ }
3798
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
3799
+ type: Component,
3800
+ args: [{
3801
+ selector: 'nav[ui-tab-bar]',
3802
+ template: '<ng-content />',
3803
+ host: {
3804
+ 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)]',
3805
+ },
3806
+ }]
3807
+ }] });
3808
+ /**
3809
+ * One destination of `ui-tab-bar`.
3810
+ *
3811
+ * @example
3812
+ * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3813
+ */
3814
+ class UiTab {
3815
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3816
+ classes = computed(() => `${TAB_CLASSES} ${this.active() ? ACTIVE_TAB_CLASSES : INACTIVE_TAB_CLASSES}`, /* @ts-ignore */
3817
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3818
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
3819
+ 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: `
3820
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3821
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3822
+ `, isInline: true });
3823
+ }
3824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
3825
+ type: Component,
3826
+ args: [{
3827
+ selector: 'a[ui-tab]',
3828
+ template: `
3829
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3830
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3831
+ `,
3832
+ host: {
3833
+ '[class]': 'classes()',
3834
+ '[attr.aria-current]': "active() ? 'page' : null",
3835
+ },
3836
+ }]
3837
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3838
+
2978
3839
  /** Breakpoints a secondary column can be held back until. */
2979
3840
  const CELL_BREAKPOINTS = ['md', 'lg'];
2980
3841
  /** Body row heights. */
@@ -3168,7 +4029,9 @@ class UiGroupCell {
3168
4029
  [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3169
4030
  >
3170
4031
  <div class="flex min-w-0 items-baseline gap-3">
3171
- <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
4032
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4033
+ {{ name() }}
4034
+ </h2>
3172
4035
  @if (meta()) {
3173
4036
  <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3174
4037
  }
@@ -3190,7 +4053,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3190
4053
  [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3191
4054
  >
3192
4055
  <div class="flex min-w-0 items-baseline gap-3">
3193
- <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
4056
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4057
+ {{ name() }}
4058
+ </h2>
3194
4059
  @if (meta()) {
3195
4060
  <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3196
4061
  }
@@ -3277,62 +4142,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3277
4142
  }]
3278
4143
  }], propDecorators: { narrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "narrow", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
3279
4144
 
3280
- const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-[3px] touch-manipulation select-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3281
- const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3282
- const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3283
- /**
3284
- * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
3285
- *
3286
- * @example
3287
- * <nav ui-tab-bar>
3288
- * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3289
- * </nav>
3290
- */
3291
- class UiTabBar {
3292
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3293
- 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 });
3294
- }
3295
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
3296
- type: Component,
3297
- args: [{
3298
- selector: 'nav[ui-tab-bar]',
3299
- template: '<ng-content />',
3300
- host: {
3301
- 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)]',
3302
- },
3303
- }]
3304
- }] });
3305
- /**
3306
- * One destination of `ui-tab-bar`.
3307
- *
3308
- * @example
3309
- * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3310
- */
3311
- class UiTab {
3312
- active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3313
- classes = computed(() => `${TAB_CLASSES} ${this.active() ? ACTIVE_TAB_CLASSES : INACTIVE_TAB_CLASSES}`, /* @ts-ignore */
3314
- ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3315
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
3316
- 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: `
3317
- <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3318
- <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3319
- `, isInline: true });
3320
- }
3321
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
3322
- type: Component,
3323
- args: [{
3324
- selector: 'a[ui-tab]',
3325
- template: `
3326
- <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3327
- <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3328
- `,
3329
- host: {
3330
- '[class]': 'classes()',
3331
- '[attr.aria-current]': "active() ? 'page' : null",
3332
- },
3333
- }]
3334
- }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3335
-
3336
4145
  /*
3337
4146
  * Public API surface of the ui library.
3338
4147
  */
@@ -3341,5 +4150,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3341
4150
  * Generated bundle index. Do not edit.
3342
4151
  */
3343
4152
 
3344
- export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DIALOG_LAYOUTS, DIALOG_WIDTHS, GROUP_SIZES, METER_TONES, ROW_PADDINGS, ROW_SIZES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiAlert, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiField, UiFieldLeading, UiGroup, UiGroupCell, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowLink, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
4153
+ export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_ALIGNS, ASYNC_STATES, ASYNC_VARIANTS, AVATAR_SIZES, BACK_LINK_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_SURFACES, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DELTA_SIZES, DELTA_WEIGHTS, DIALOG_LAYOUTS, DIALOG_WIDTHS, FACT_SIZES, FACT_SUB_TONES, GROUP_SIZES, METER_SIZES, METER_TONES, ROW_GAPS, ROW_PADDINGS, ROW_SIZES, SEGMENTED_SIZES, SKELETON_SHAPES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiActionBar, UiAlert, UiAmount, UiAsync, UiAvatar, UiAvatarLink, UiBackLink, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiExternalLink, UiFact, UiFacts, UiField, UiFieldLeading, UiFieldTrailing, UiFilterChips, UiGroup, UiGroupCell, UiInput, UiLineChart, UiListRow, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowItem, UiRowLink, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
3345
4154
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map