@joanroucoux/cairn-ui 0.6.0 → 0.6.2

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(() => this.ruled() ? 'm-0 flex flex-col shadow-[inset_0_1px_0_var(--hairline)]' : 'm-0 flex flex-col', /* @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,15 @@ 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(() => [
2044
+ 'relative flex items-center',
2045
+ this.leading() ? '[&>input]:pl-[38px]' : '',
2046
+ this.trailing() ? '[&>input]:pr-12 lg:[&>input]:pr-11' : '',
2047
+ ]
2048
+ .filter(Boolean)
2049
+ .join(' '), /* @ts-ignore */
1474
2050
  ...(ngDevMode ? [{ debugName: "rowClasses" }] : /* istanbul ignore next */ []));
1475
2051
  labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)', /* @ts-ignore */
1476
2052
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
@@ -1491,6 +2067,7 @@ class UiField {
1491
2067
  }, /* @ts-ignore */
1492
2068
  ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
1493
2069
  constructor() {
2070
+ let markedByField = false;
1494
2071
  afterRenderEffect(() => {
1495
2072
  // querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
1496
2073
  const control = this.#host.nativeElement.querySelector('input, select, textarea');
@@ -1506,24 +2083,31 @@ class UiField {
1506
2083
  else {
1507
2084
  control.removeAttribute('aria-describedby');
1508
2085
  }
1509
- if (this.message()) {
2086
+ if (this.message() || this.invalid()) {
1510
2087
  control.setAttribute('aria-invalid', 'true');
2088
+ markedByField = true;
1511
2089
  }
1512
- else {
2090
+ else if (markedByField) {
1513
2091
  control.removeAttribute('aria-invalid');
2092
+ markedByField = false;
1514
2093
  }
1515
2094
  });
1516
2095
  }
1517
2096
  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: `
2097
+ 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
2098
  <div class="group flex flex-col gap-1.5">
1520
2099
  <!-- 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>
2100
+ <label [class]="labelClasses()"
2101
+ >{{ label() }}
2102
+ @if (optional()) {
2103
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2104
+ }
2105
+ </label>
1522
2106
 
1523
2107
  <div [class]="rowClasses()">
1524
2108
  <ng-content select="[uiFieldLeading]" />
1525
2109
  <ng-content />
1526
-
2110
+ <ng-content select="[uiFieldTrailing]" />
1527
2111
  @if (unit()) {
1528
2112
  <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
1529
2113
  }
@@ -1542,32 +2126,117 @@ class UiField {
1542
2126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, decorators: [{
1543
2127
  type: Component,
1544
2128
  args: [{
1545
- selector: 'ui-field',
2129
+ selector: 'ui-field',
2130
+ template: `
2131
+ <div class="group flex flex-col gap-1.5">
2132
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
2133
+ <label [class]="labelClasses()"
2134
+ >{{ label() }}
2135
+ @if (optional()) {
2136
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2137
+ }
2138
+ </label>
2139
+
2140
+ <div [class]="rowClasses()">
2141
+ <ng-content select="[uiFieldLeading]" />
2142
+ <ng-content />
2143
+ <ng-content select="[uiFieldTrailing]" />
2144
+ @if (unit()) {
2145
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
2146
+ }
2147
+ </div>
2148
+
2149
+ @if (hint()) {
2150
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
2151
+ }
2152
+
2153
+ @if (message()) {
2154
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2155
+ }
2156
+ </div>
2157
+ `,
2158
+ }]
2159
+ }], 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 }] }] } });
2160
+
2161
+ 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';
2162
+ 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)]';
2163
+ const SELECTED_PILL_CLASSES = 'bg-(--primary) text-(--primary-foreground)';
2164
+ const UNSELECTED_PILL_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
2165
+ /**
2166
+ * A row of single-choice filter pills, each with an optional count, that narrows a list in place:
2167
+ * an asset class, a status. On touch it scrolls on one line, bleeding to the screen edges; with a
2168
+ * fine pointer it wraps.
2169
+ *
2170
+ * @example
2171
+ * <ui-filter-chips ariaLabel="Filter by class" [options]="classes()" [(value)]="selectedClass" />
2172
+ */
2173
+ class UiFilterChips {
2174
+ options = input.required(/* @ts-ignore */
2175
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2176
+ value = model.required(/* @ts-ignore */
2177
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2178
+ ariaLabel = input.required(/* @ts-ignore */
2179
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2180
+ chipClasses = CHIP_CLASSES;
2181
+ pillClasses(optionValue) {
2182
+ return `${PILL_CLASSES} ${optionValue === this.value() ? SELECTED_PILL_CLASSES : UNSELECTED_PILL_CLASSES}`;
2183
+ }
2184
+ countClasses(optionValue) {
2185
+ return optionValue === this.value() ? 'text-(--primary-foreground)' : 'text-(--subtle-foreground)';
2186
+ }
2187
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
2188
+ 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: `
2189
+ @for (option of options(); track option.value) {
2190
+ <button
2191
+ class="group/chip"
2192
+ type="button"
2193
+ [attr.aria-pressed]="option.value === value()"
2194
+ [class]="chipClasses"
2195
+ (click)="value.set(option.value)"
2196
+ >
2197
+ <span data-chip [class]="pillClasses(option.value)"
2198
+ >{{ option.label }}
2199
+ @if (option.count !== undefined && option.count !== null) {
2200
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2201
+ option.count
2202
+ }}</span>
2203
+ }
2204
+ </span>
2205
+ </button>
2206
+ }
2207
+ `, isInline: true });
2208
+ }
2209
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, decorators: [{
2210
+ type: Component,
2211
+ args: [{
2212
+ selector: 'ui-filter-chips',
1546
2213
  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>
2214
+ @for (option of options(); track option.value) {
2215
+ <button
2216
+ class="group/chip"
2217
+ type="button"
2218
+ [attr.aria-pressed]="option.value === value()"
2219
+ [class]="chipClasses"
2220
+ (click)="value.set(option.value)"
2221
+ >
2222
+ <span data-chip [class]="pillClasses(option.value)"
2223
+ >{{ option.label }}
2224
+ @if (option.count !== undefined && option.count !== null) {
2225
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2226
+ option.count
2227
+ }}</span>
2228
+ }
2229
+ </span>
2230
+ </button>
2231
+ }
1568
2232
  `,
2233
+ host: {
2234
+ role: 'group',
2235
+ 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',
2236
+ '[attr.aria-label]': 'ariaLabel()',
2237
+ },
1569
2238
  }]
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 }] }] } });
2239
+ }], 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
2240
 
1572
2241
  const axisTicks = (points, maxTicks = 5) => {
1573
2242
  if (points.length <= maxTicks) {
@@ -1824,6 +2493,8 @@ class UiLineChart {
1824
2493
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1825
2494
  valueFormat = input(identityValue, /* @ts-ignore */
1826
2495
  ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
2496
+ tooltipFormat = input(/* @ts-ignore */
2497
+ ...(ngDevMode ? [undefined, { debugName: "tooltipFormat" }] : /* istanbul ignore next */ []));
1827
2498
  deltaFormat = input(identityDelta, /* @ts-ignore */
1828
2499
  ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1829
2500
  timeFormat = input(identityTime, /* @ts-ignore */
@@ -1986,7 +2657,7 @@ class UiLineChart {
1986
2657
  this.activeIndex.set(navigation.index);
1987
2658
  }
1988
2659
  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: `
2660
+ 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
2661
  <div class="relative h-full">
1991
2662
  <svg
1992
2663
  #svgRef
@@ -2097,36 +2768,35 @@ class UiLineChart {
2097
2768
  [class]="tooltipClasses().box"
2098
2769
  [style]="tooltipStyle(point)"
2099
2770
  >
2100
- <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2771
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2772
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2773
+ </p>
2101
2774
  @if (tooltipDelta()) {
2102
2775
  <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2103
2776
  }
2104
2777
  <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
2105
2778
  </div>
2106
2779
  }
2107
-
2108
2780
  @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) {
2781
+ <div class="sr-only">
2782
+ <table>
2783
+ <caption [textContent]="label()"></caption>
2784
+ <thead>
2123
2785
  <tr>
2124
- <td>{{ timeFormat()(point.t) }}</td>
2125
- <td>{{ valueFormat()(point.v) }}</td>
2786
+ <th scope="col">{{ timeColumnLabel() }}</th>
2787
+ <th scope="col">{{ valueColumnLabel() }}</th>
2126
2788
  </tr>
2127
- }
2128
- </tbody>
2129
- </table>
2789
+ </thead>
2790
+ <tbody>
2791
+ @for (point of points(); track point.t) {
2792
+ <tr>
2793
+ <td>{{ timeFormat()(point.t) }}</td>
2794
+ <td>{{ valueFormat()(point.v) }}</td>
2795
+ </tr>
2796
+ }
2797
+ </tbody>
2798
+ </table>
2799
+ </div>
2130
2800
  }
2131
2801
  </div>
2132
2802
  `, isInline: true });
@@ -2246,36 +2916,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2246
2916
  [class]="tooltipClasses().box"
2247
2917
  [style]="tooltipStyle(point)"
2248
2918
  >
2249
- <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2919
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2920
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2921
+ </p>
2250
2922
  @if (tooltipDelta()) {
2251
2923
  <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2252
2924
  }
2253
2925
  <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
2254
2926
  </div>
2255
2927
  }
2256
-
2257
2928
  @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) {
2929
+ <div class="sr-only">
2930
+ <table>
2931
+ <caption [textContent]="label()"></caption>
2932
+ <thead>
2272
2933
  <tr>
2273
- <td>{{ timeFormat()(point.t) }}</td>
2274
- <td>{{ valueFormat()(point.v) }}</td>
2934
+ <th scope="col">{{ timeColumnLabel() }}</th>
2935
+ <th scope="col">{{ valueColumnLabel() }}</th>
2275
2936
  </tr>
2276
- }
2277
- </tbody>
2278
- </table>
2937
+ </thead>
2938
+ <tbody>
2939
+ @for (point of points(); track point.t) {
2940
+ <tr>
2941
+ <td>{{ timeFormat()(point.t) }}</td>
2942
+ <td>{{ valueFormat()(point.v) }}</td>
2943
+ </tr>
2944
+ }
2945
+ </tbody>
2946
+ </table>
2947
+ </div>
2279
2948
  }
2280
2949
  </div>
2281
2950
  `,
@@ -2283,11 +2952,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2283
2952
  class: 'block h-full',
2284
2953
  },
2285
2954
  }]
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 }] }] } });
2955
+ }], 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
2956
 
2288
2957
  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 = (() => {
2958
+ 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';
2959
+ 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';
2960
+ 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';
2961
+ const nextId$1 = (() => {
2291
2962
  let count = 0;
2292
2963
  return () => `ui-menu-${++count}`;
2293
2964
  })();
@@ -2301,7 +2972,7 @@ class UiMenuItem {
2301
2972
  destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2302
2973
  #host = inject(ElementRef).nativeElement;
2303
2974
  #menu = inject(UiMenu);
2304
- classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
2975
+ classes = computed(() => `${this.#menu.sheet() ? SHEET_ITEM_CLASSES : ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
2305
2976
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2306
2977
  focus() {
2307
2978
  this.#host.focus();
@@ -2328,7 +2999,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2328
2999
  }]
2329
3000
  }], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
2330
3001
  /**
2331
- * Secondary actions menu, opened from its trigger through the native Popover API.
3002
+ * Secondary actions menu, opened from its trigger through the native Popover API. With `sheet`, below
3003
+ * 64rem it opens as a bottom action sheet above the tab bar instead of beside its trigger, and
3004
+ * `heading` names what the actions apply to. `width` fixes the outer popover width in pixels (the sheet
3005
+ * always spans the screen below 64rem); without it the menu fits its content.
2332
3006
  *
2333
3007
  * @example
2334
3008
  * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
@@ -2340,10 +3014,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2340
3014
  class UiMenu {
2341
3015
  label = input.required(/* @ts-ignore */
2342
3016
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2343
- menuId = nextId();
3017
+ sheet = input(false, { ...(ngDevMode ? { debugName: "sheet" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3018
+ heading = input(/* @ts-ignore */
3019
+ ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
3020
+ width = input(undefined, { ...(ngDevMode ? { debugName: "width" } : /* istanbul ignore next */ {}), transform: numberAttribute });
3021
+ menuId = nextId$1();
2344
3022
  isOpen = signal(false, /* @ts-ignore */
2345
3023
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2346
- classes = MENU_CLASSES;
3024
+ hasWidth = computed(() => Number.isFinite(this.width()), /* @ts-ignore */
3025
+ ...(ngDevMode ? [{ debugName: "hasWidth" }] : /* istanbul ignore next */ []));
3026
+ widthVar = computed(() => (this.hasWidth() ? `${this.width()}px` : null), /* @ts-ignore */
3027
+ ...(ngDevMode ? [{ debugName: "widthVar" }] : /* istanbul ignore next */ []));
3028
+ classes = computed(() => {
3029
+ const width = !this.hasWidth() ? '' : this.sheet() ? ' lg:w-(--ui-menu-width)' : ' w-(--ui-menu-width)';
3030
+ return `${MENU_CLASSES}${this.sheet() ? ` ${SHEET_CLASSES}` : ''}${width}`;
3031
+ }, /* @ts-ignore */
3032
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2347
3033
  items = contentChildren(UiMenuItem, /* @ts-ignore */
2348
3034
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2349
3035
  #host = inject(ElementRef).nativeElement;
@@ -2354,7 +3040,14 @@ class UiMenu {
2354
3040
  if (typeof this.#host.showPopover === 'function') {
2355
3041
  this.#host.showPopover();
2356
3042
  }
2357
- this.#position(trigger);
3043
+ if (this.#isSheet()) {
3044
+ this.#host.style.position = '';
3045
+ this.#host.style.top = '';
3046
+ this.#host.style.left = '';
3047
+ }
3048
+ else {
3049
+ this.#position(trigger);
3050
+ }
2358
3051
  this.items()[0]?.focus();
2359
3052
  }
2360
3053
  close() {
@@ -2404,6 +3097,9 @@ class UiMenu {
2404
3097
  event.preventDefault();
2405
3098
  items[nextIndex]?.focus();
2406
3099
  }
3100
+ #isSheet() {
3101
+ return this.sheet() && typeof window.matchMedia === 'function' && !window.matchMedia('(min-width: 64rem)').matches;
3102
+ }
2407
3103
  #position(trigger) {
2408
3104
  const margin = 8;
2409
3105
  const triggerRect = trigger.getBoundingClientRect();
@@ -2417,24 +3113,39 @@ class UiMenu {
2417
3113
  this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
2418
3114
  }
2419
3115
  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 });
3116
+ 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: `
3117
+ @if (sheet() && heading()) {
3118
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3119
+ {{ heading() }}
3120
+ </div>
3121
+ }
3122
+ <ng-content />
3123
+ `, isInline: true });
2421
3124
  }
2422
3125
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
2423
3126
  type: Component,
2424
3127
  args: [{
2425
3128
  selector: 'ui-menu',
2426
- template: '<ng-content />',
3129
+ template: `
3130
+ @if (sheet() && heading()) {
3131
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3132
+ {{ heading() }}
3133
+ </div>
3134
+ }
3135
+ <ng-content />
3136
+ `,
2427
3137
  host: {
2428
3138
  role: 'menu',
2429
3139
  popover: 'auto',
2430
3140
  '[id]': 'menuId',
2431
- '[class]': 'classes',
3141
+ '[style.--ui-menu-width]': 'widthVar()',
3142
+ '[class]': 'classes()',
2432
3143
  '[attr.aria-label]': 'label()',
2433
3144
  '(toggle)': 'onToggle($event)',
2434
3145
  '(keydown)': 'onKeydown($event)',
2435
3146
  },
2436
3147
  }]
2437
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
3148
+ }], 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
3149
  /**
2439
3150
  * Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
2440
3151
  * `aria-controls` on it.
@@ -2471,6 +3182,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2471
3182
  }]
2472
3183
  }], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
2473
3184
 
3185
+ /** Track thicknesses. `MeterSize` is derived from this tuple. */
3186
+ const METER_SIZES = ['md', 'sm'];
3187
+ const TRACK_CLASSES = {
3188
+ md: 'h-1.5',
3189
+ sm: 'h-1',
3190
+ };
2474
3191
  /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
2475
3192
  const METER_TONES = [1, 2, 3, 4, 5, 6];
2476
3193
  const TONE_CLASSES = {
@@ -2497,20 +3214,25 @@ class UiMeter {
2497
3214
  ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
2498
3215
  tone = input(1, /* @ts-ignore */
2499
3216
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3217
+ size = input('md', /* @ts-ignore */
3218
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3219
+ trackClasses = computed(() => TRACK_CLASSES[this.size()], /* @ts-ignore */
3220
+ ...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
2500
3221
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
2501
3222
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
2502
3223
  fillClasses = computed(() => `h-full rounded-pill ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
2503
3224
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
2504
3225
  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: `
3226
+ 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
3227
  <div
2507
- class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
3228
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
2508
3229
  role="meter"
2509
3230
  [attr.aria-label]="label()"
2510
3231
  [attr.aria-valuemax]="1"
2511
3232
  [attr.aria-valuemin]="0"
2512
3233
  [attr.aria-valuenow]="value()"
2513
3234
  [attr.aria-valuetext]="valueText()"
3235
+ [class]="trackClasses()"
2514
3236
  >
2515
3237
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
2516
3238
  </div>
@@ -2522,23 +3244,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2522
3244
  selector: 'ui-meter',
2523
3245
  template: `
2524
3246
  <div
2525
- class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
3247
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
2526
3248
  role="meter"
2527
3249
  [attr.aria-label]="label()"
2528
3250
  [attr.aria-valuemax]="1"
2529
3251
  [attr.aria-valuemin]="0"
2530
3252
  [attr.aria-valuenow]="value()"
2531
3253
  [attr.aria-valuetext]="valueText()"
3254
+ [class]="trackClasses()"
2532
3255
  >
2533
3256
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
2534
3257
  </div>
2535
3258
  `,
2536
3259
  }]
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 }] }] } });
3260
+ }], 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
3261
 
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)';
3262
+ 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
3263
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
2541
- const IDLE_CLASSES = 'text-(--muted-foreground)';
3264
+ const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
2542
3265
  /**
2543
3266
  * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
2544
3267
  *
@@ -2567,13 +3290,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2567
3290
  }]
2568
3291
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
2569
3292
 
2570
- const ROW_SIZES = ['md', 'lg', 'xl'];
3293
+ const ROW_SIZES = ['md', 'lg', 'xl', 'card', 'dense'];
2571
3294
  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',
3295
+ const ROW_GAPS = ['default', 'sm'];
3296
+ const SIZE_CLASSES$1 = {
3297
+ md: 'min-h-14',
3298
+ lg: 'min-h-15',
3299
+ xl: 'min-h-18 pointer-fine:min-h-17',
3300
+ card: 'min-h-17',
3301
+ dense: 'min-h-14 lg:min-h-12',
2576
3302
  };
3303
+ const SIZE_GAPS = { md: 'gap-2.5', lg: 'gap-2.5', xl: 'gap-3', card: 'gap-2', dense: 'gap-3' };
2577
3304
  const PADDING_CLASSES = { md: 'px-2.5', sm: 'px-2', none: 'px-0' };
2578
3305
  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
3306
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
@@ -2591,10 +3318,12 @@ class UiRow {
2591
3318
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2592
3319
  padding = input('md', /* @ts-ignore */
2593
3320
  ...(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 */
3321
+ gap = input('default', /* @ts-ignore */
3322
+ ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
3323
+ 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
3324
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2596
3325
  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 });
3326
+ 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
3327
  }
2599
3328
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
2600
3329
  type: Component,
@@ -2606,9 +3335,70 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2606
3335
  '[attr.aria-current]': "selected() ? 'true' : null",
2607
3336
  },
2608
3337
  }]
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 }] }] } });
3338
+ }], 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 }] }] } });
3339
+ /**
3340
+ * Lays out a `ui-row` with a trailing action beside it, such as a "more" icon button: the row takes
3341
+ * the room the action leaves and the action keeps its own size.
3342
+ *
3343
+ * @example
3344
+ * <div uiRowItem>
3345
+ * <a ui-row size="xl" padding="sm" routerLink="/accounts/1">...</a>
3346
+ * <button ui-button variant="quiet" size="icon-sm" aria-label="Account actions">...</button>
3347
+ * </div>
3348
+ */
3349
+ class UiRowItem {
3350
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3351
+ 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 });
3352
+ }
3353
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, decorators: [{
3354
+ type: Directive,
3355
+ args: [{
3356
+ selector: '[uiRowItem]',
3357
+ host: {
3358
+ class: 'flex items-center gap-1 [&>[ui-row]]:w-auto [&>[ui-row]]:min-w-0 [&>[ui-row]]:flex-1 [&>:not([ui-row])]:flex-none',
3359
+ },
3360
+ }]
3361
+ }] });
3362
+ /**
3363
+ * Non-interactive row of a list, separated from the next by a hairline: a passkey with its delete
3364
+ * button, a device, a setting. It is a 72px row (68px from 64rem) with a 12px gap; put it on an
3365
+ * `li` or a `div`. Use `ui-row` instead when the whole row is a link or an action.
3366
+ *
3367
+ * @example
3368
+ * <ul>
3369
+ * <li uiListRow>
3370
+ * <span class="flex-1">MacBook Air</span>
3371
+ * <button ui-button variant="quiet-destructive" size="icon-sm" aria-label="Delete MacBook Air">...</button>
3372
+ * </li>
3373
+ * </ul>
3374
+ */
3375
+ class UiListRow {
3376
+ ruled = input(true, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3377
+ classes = computed(() => this.ruled()
3378
+ ? 'flex min-h-18 items-center gap-3 py-1.5 lg:min-h-17 shadow-[inset_0_-1px_0_var(--hairline)]'
3379
+ : 'flex min-h-18 items-center gap-3 py-1.5 lg:min-h-17', /* @ts-ignore */
3380
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3381
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3382
+ 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 });
3383
+ }
3384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, decorators: [{
3385
+ type: Directive,
3386
+ args: [{
3387
+ selector: '[uiListRow]',
3388
+ host: { '[class]': 'classes()' },
3389
+ }]
3390
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
2610
3391
 
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)";
3392
+ const nextId = (() => {
3393
+ let count = 0;
3394
+ return () => `ui-segmented-${++count}`;
3395
+ })();
3396
+ 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)";
3397
+ const SEGMENTED_SIZES = ['md', 'sm'];
3398
+ const SIZE_CLASSES = {
3399
+ md: 'h-10 pointer-fine:h-8',
3400
+ sm: 'h-10 pointer-fine:h-[30px]',
3401
+ };
2612
3402
  const SELECTED_CLASSES = 'text-(--foreground)';
2613
3403
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
2614
3404
  /**
@@ -2623,8 +3413,12 @@ class UiSegmented {
2623
3413
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2624
3414
  label = input.required(/* @ts-ignore */
2625
3415
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3416
+ showLabel = input(false, { ...(ngDevMode ? { debugName: "showLabel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3417
+ labelId = nextId();
2626
3418
  value = model.required(/* @ts-ignore */
2627
3419
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3420
+ size = input('md', /* @ts-ignore */
3421
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2628
3422
  radios = viewChildren('radio', /* @ts-ignore */
2629
3423
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
2630
3424
  thumbIndex = computed(() => {
@@ -2633,7 +3427,7 @@ class UiSegmented {
2633
3427
  }, /* @ts-ignore */
2634
3428
  ...(ngDevMode ? [{ debugName: "thumbIndex" }] : /* istanbul ignore next */ []));
2635
3429
  optionClasses(optionValue) {
2636
- return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
3430
+ return `${OPTION_CLASSES} ${SIZE_CLASSES[this.size()]} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
2637
3431
  }
2638
3432
  tabIndexFor(optionValue) {
2639
3433
  const opts = this.options();
@@ -2657,37 +3451,45 @@ class UiSegmented {
2657
3451
  this.radios()[nextIndex]?.nativeElement.focus();
2658
3452
  }
2659
3453
  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>
3454
+ 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: `
3455
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3456
+ @if (showLabel()) {
3457
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3458
+ label()
3459
+ }}</span>
2675
3460
  }
3461
+ <div
3462
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3463
+ role="radiogroup"
3464
+ [attr.aria-label]="showLabel() ? null : label()"
3465
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3466
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3467
+ >
3468
+ @if (thumbIndex(); as index) {
3469
+ <span
3470
+ aria-hidden="true"
3471
+ 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"
3472
+ data-thumb
3473
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3474
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3475
+ ></span>
3476
+ }
2676
3477
 
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
- }
3478
+ @for (option of options(); track option.value) {
3479
+ <button
3480
+ #radio
3481
+ role="radio"
3482
+ type="button"
3483
+ [attr.aria-checked]="option.value === value()"
3484
+ [attr.tabindex]="tabIndexFor(option.value)"
3485
+ [class]="optionClasses(option.value)"
3486
+ (click)="select(option.value)"
3487
+ (keydown)="onKeydown($event)"
3488
+ >
3489
+ {{ option.label }}
3490
+ </button>
3491
+ }
3492
+ </div>
2691
3493
  </div>
2692
3494
  `, isInline: true });
2693
3495
  }
@@ -2696,40 +3498,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2696
3498
  args: [{
2697
3499
  selector: 'ui-segmented',
2698
3500
  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>
3501
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3502
+ @if (showLabel()) {
3503
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3504
+ label()
3505
+ }}</span>
2713
3506
  }
3507
+ <div
3508
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3509
+ role="radiogroup"
3510
+ [attr.aria-label]="showLabel() ? null : label()"
3511
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3512
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3513
+ >
3514
+ @if (thumbIndex(); as index) {
3515
+ <span
3516
+ aria-hidden="true"
3517
+ 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"
3518
+ data-thumb
3519
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3520
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3521
+ ></span>
3522
+ }
2714
3523
 
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
- }
3524
+ @for (option of options(); track option.value) {
3525
+ <button
3526
+ #radio
3527
+ role="radio"
3528
+ type="button"
3529
+ [attr.aria-checked]="option.value === value()"
3530
+ [attr.tabindex]="tabIndexFor(option.value)"
3531
+ [class]="optionClasses(option.value)"
3532
+ (click)="select(option.value)"
3533
+ (keydown)="onKeydown($event)"
3534
+ >
3535
+ {{ option.label }}
3536
+ </button>
3537
+ }
3538
+ </div>
2729
3539
  </div>
2730
3540
  `,
2731
3541
  }]
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 }] }] } });
3542
+ }], 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
3543
 
2734
3544
  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
3545
  // Spelled out prefix included: Tailwind scans for literal strings, so a prefix applied in code
@@ -2790,7 +3600,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2790
3600
  }], 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
3601
 
2792
3602
  /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
2793
- const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
3603
+ const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring', 'dot'];
2794
3604
  const LAST_LINE_WIDTH = '62%';
2795
3605
  const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
2796
3606
  const BAR_CLASSES = 'block rounded-control bg-(--muted)';
@@ -2818,7 +3628,7 @@ class UiSkeleton {
2818
3628
  }, /* @ts-ignore */
2819
3629
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
2820
3630
  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"] });
3631
+ 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
3632
  }
2823
3633
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
2824
3634
  type: Component,
@@ -2839,6 +3649,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2839
3649
  @case ('chart') {
2840
3650
  <span class="rounded-control min-h-50 w-full flex-1 bg-(--muted)" [style.animation]="animation"></span>
2841
3651
  }
3652
+ @case ('dot') {
3653
+ <span class="rounded-pill size-3 bg-(--muted)" [style.animation]="animation"></span>
3654
+ }
2842
3655
  @case ('ring') {
2843
3656
  <span
2844
3657
  class="rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]"
@@ -2975,6 +3788,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2975
3788
  }]
2976
3789
  }] });
2977
3790
 
3791
+ 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)';
3792
+ const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3793
+ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3794
+ /**
3795
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
3796
+ *
3797
+ * @example
3798
+ * <nav ui-tab-bar>
3799
+ * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3800
+ * </nav>
3801
+ */
3802
+ class UiTabBar {
3803
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3804
+ 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 });
3805
+ }
3806
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
3807
+ type: Component,
3808
+ args: [{
3809
+ selector: 'nav[ui-tab-bar]',
3810
+ template: '<ng-content />',
3811
+ host: {
3812
+ 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)]',
3813
+ },
3814
+ }]
3815
+ }] });
3816
+ /**
3817
+ * One destination of `ui-tab-bar`.
3818
+ *
3819
+ * @example
3820
+ * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3821
+ */
3822
+ class UiTab {
3823
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3824
+ classes = computed(() => `${TAB_CLASSES} ${this.active() ? ACTIVE_TAB_CLASSES : INACTIVE_TAB_CLASSES}`, /* @ts-ignore */
3825
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3826
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
3827
+ 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: `
3828
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3829
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3830
+ `, isInline: true });
3831
+ }
3832
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
3833
+ type: Component,
3834
+ args: [{
3835
+ selector: 'a[ui-tab]',
3836
+ template: `
3837
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3838
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3839
+ `,
3840
+ host: {
3841
+ '[class]': 'classes()',
3842
+ '[attr.aria-current]': "active() ? 'page' : null",
3843
+ },
3844
+ }]
3845
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3846
+
2978
3847
  /** Breakpoints a secondary column can be held back until. */
2979
3848
  const CELL_BREAKPOINTS = ['md', 'lg'];
2980
3849
  /** Body row heights. */
@@ -3168,7 +4037,9 @@ class UiGroupCell {
3168
4037
  [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3169
4038
  >
3170
4039
  <div class="flex min-w-0 items-baseline gap-3">
3171
- <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
4040
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4041
+ {{ name() }}
4042
+ </h2>
3172
4043
  @if (meta()) {
3173
4044
  <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3174
4045
  }
@@ -3190,7 +4061,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3190
4061
  [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3191
4062
  >
3192
4063
  <div class="flex min-w-0 items-baseline gap-3">
3193
- <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
4064
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4065
+ {{ name() }}
4066
+ </h2>
3194
4067
  @if (meta()) {
3195
4068
  <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3196
4069
  }
@@ -3277,62 +4150,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3277
4150
  }]
3278
4151
  }], propDecorators: { narrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "narrow", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
3279
4152
 
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
4153
  /*
3337
4154
  * Public API surface of the ui library.
3338
4155
  */
@@ -3341,5 +4158,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3341
4158
  * Generated bundle index. Do not edit.
3342
4159
  */
3343
4160
 
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 };
4161
+ 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
4162
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map