@joanroucoux/cairn-ui 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,151 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, viewChild, afterNextRender, contentChildren, model, viewChildren } from '@angular/core';
3
- import { pie, arc, curveMonotoneX, line } from 'd3-shape';
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';
4
4
  import { scaleLinear } from 'd3-scale';
5
+ import { curveMonotoneX, line } from 'd3-shape';
6
+
7
+ /**
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.
12
+ *
13
+ * @example
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>
18
+ */
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 });
22
+ }
23
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiActionBar, decorators: [{
24
+ type: Component,
25
+ args: [{
26
+ selector: 'ui-action-bar',
27
+ template: '<ng-content />',
28
+ host: {
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',
30
+ },
31
+ }]
32
+ }] });
33
+
34
+ const ALERT_VARIANTS = ['error', 'warning'];
35
+ const BASE_CLASSES$8 = 'flex items-start gap-2.5 text-left text-label';
36
+ const VARIANT_CLASSES$3 = {
37
+ error: 'rounded-control bg-(--card) px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--border)]',
38
+ warning: 'text-(--foreground)',
39
+ };
40
+ /**
41
+ * Inline message explaining a failure or a consequence: an icon, an optional heading and a text.
42
+ *
43
+ * @example
44
+ * <ui-alert heading="Sign-in failed">Try again.</ui-alert>
45
+ */
46
+ class UiAlert {
47
+ variant = input('error', /* @ts-ignore */
48
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
49
+ heading = input(/* @ts-ignore */
50
+ ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
51
+ classes = computed(() => `${BASE_CLASSES$8} ${VARIANT_CLASSES$3[this.variant()]}`, /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
53
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, deps: [], target: i0.ɵɵFactoryTarget.Component });
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: `
55
+ @if (variant() === 'warning') {
56
+ <svg
57
+ aria-hidden="true"
58
+ class="mt-px block flex-none stroke-(--negative)"
59
+ fill="none"
60
+ height="18"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ stroke-width="1.75"
64
+ viewBox="0 0 24 24"
65
+ width="18"
66
+ >
67
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
68
+ <path d="M12 9v4" />
69
+ <path d="M12 17h.01" />
70
+ </svg>
71
+ } @else {
72
+ <svg
73
+ aria-hidden="true"
74
+ class="block flex-none stroke-(--negative)"
75
+ fill="none"
76
+ height="18"
77
+ stroke-linecap="round"
78
+ stroke-linejoin="round"
79
+ stroke-width="1.75"
80
+ viewBox="0 0 24 24"
81
+ width="18"
82
+ [class.mt-px]="heading()"
83
+ >
84
+ <circle cx="12" cy="12" r="10" />
85
+ <line x1="12" x2="12" y1="8" y2="12" />
86
+ <line x1="12" x2="12.01" y1="16" y2="16" />
87
+ </svg>
88
+ }
89
+ <div class="flex flex-col gap-0.5">
90
+ @if (heading()) {
91
+ <span class="font-medium">{{ heading() }}</span>
92
+ }
93
+ <span class="text-pretty" [class.text-(--muted-foreground)]="heading()"><ng-content /></span>
94
+ </div>
95
+ `, isInline: true });
96
+ }
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, decorators: [{
98
+ type: Component,
99
+ args: [{
100
+ selector: 'ui-alert',
101
+ template: `
102
+ @if (variant() === 'warning') {
103
+ <svg
104
+ aria-hidden="true"
105
+ class="mt-px block flex-none stroke-(--negative)"
106
+ fill="none"
107
+ height="18"
108
+ stroke-linecap="round"
109
+ stroke-linejoin="round"
110
+ stroke-width="1.75"
111
+ viewBox="0 0 24 24"
112
+ width="18"
113
+ >
114
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
115
+ <path d="M12 9v4" />
116
+ <path d="M12 17h.01" />
117
+ </svg>
118
+ } @else {
119
+ <svg
120
+ aria-hidden="true"
121
+ class="block flex-none stroke-(--negative)"
122
+ fill="none"
123
+ height="18"
124
+ stroke-linecap="round"
125
+ stroke-linejoin="round"
126
+ stroke-width="1.75"
127
+ viewBox="0 0 24 24"
128
+ width="18"
129
+ [class.mt-px]="heading()"
130
+ >
131
+ <circle cx="12" cy="12" r="10" />
132
+ <line x1="12" x2="12" y1="8" y2="12" />
133
+ <line x1="12" x2="12.01" y1="16" y2="16" />
134
+ </svg>
135
+ }
136
+ <div class="flex flex-col gap-0.5">
137
+ @if (heading()) {
138
+ <span class="font-medium">{{ heading() }}</span>
139
+ }
140
+ <span class="text-pretty" [class.text-(--muted-foreground)]="heading()"><ng-content /></span>
141
+ </div>
142
+ `,
143
+ host: {
144
+ '[class]': 'classes()',
145
+ '[attr.role]': "variant() === 'warning' ? 'status' : 'alert'",
146
+ },
147
+ }]
148
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }] } });
5
149
 
6
150
  const MINUS = '−';
7
151
  const EM_DASH = '—';
@@ -110,7 +254,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
110
254
 
111
255
  /** Available async block states. `AsyncState` is derived from this tuple. */
112
256
  const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'];
113
- const RETRY_BUTTON_CLASSES = 'mt-2 relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) min-h-(--row-min) px-4 text-label bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
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';
114
311
  /**
115
312
  * The three states of a block that depends on a service call, next to the ready content.
116
313
  *
@@ -130,23 +327,76 @@ class UiAsync {
130
327
  ...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
131
328
  retryLabel = input(/* @ts-ignore */
132
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 });
133
336
  retry = output();
134
- 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 */ []));
135
368
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, deps: [], target: i0.ɵɵFactoryTarget.Component });
136
- 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: `
137
370
  @switch (state()) {
138
371
  @case ('loading') {
139
372
  <ng-content select="[asyncLoading]" />
140
373
  }
141
374
  @case ('error') {
142
- <div
143
- aria-live="polite"
144
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
145
- role="alert"
146
- >
147
- <span class="text-body font-medium">{{ errorTitle() }}</span>
148
- <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
149
- <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()">
387
+ <svg
388
+ aria-hidden="true"
389
+ class="block flex-none fill-none stroke-current"
390
+ stroke-linecap="round"
391
+ stroke-linejoin="round"
392
+ stroke-width="1.75"
393
+ viewBox="0 0 24 24"
394
+ [attr.height]="iconSize()"
395
+ [attr.width]="iconSize()"
396
+ >
397
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
398
+ <path d="M21 3v5h-5" />
399
+ </svg>
150
400
  {{ retryLabel() }}
151
401
  </button>
152
402
  </div>
@@ -170,14 +420,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
170
420
  <ng-content select="[asyncLoading]" />
171
421
  }
172
422
  @case ('error') {
173
- <div
174
- aria-live="polite"
175
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
176
- role="alert"
177
- >
178
- <span class="text-body font-medium">{{ errorTitle() }}</span>
179
- <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
180
- <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()">
435
+ <svg
436
+ aria-hidden="true"
437
+ class="block flex-none fill-none stroke-current"
438
+ stroke-linecap="round"
439
+ stroke-linejoin="round"
440
+ stroke-width="1.75"
441
+ viewBox="0 0 24 24"
442
+ [attr.height]="iconSize()"
443
+ [attr.width]="iconSize()"
444
+ >
445
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
446
+ <path d="M21 3v5h-5" />
447
+ </svg>
181
448
  {{ retryLabel() }}
182
449
  </button>
183
450
  </div>
@@ -192,16 +459,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
192
459
  `,
193
460
  host: {
194
461
  '[attr.aria-busy]': "state() === 'loading' || null",
462
+ '[class.flex]': 'filling()',
463
+ '[class.flex-col]': 'filling()',
195
464
  },
196
465
  }]
197
- }], 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"] }] } });
198
467
 
199
468
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
200
- const AVATAR_SIZES = ['sm', 'md'];
201
- const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
202
- const SIZE_CLASSES$1 = {
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 = {
203
472
  sm: 'size-8 text-caption',
204
- md: 'size-11 text-label',
473
+ md: 'size-[34px] text-label',
474
+ lg: 'size-14 text-title',
475
+ auto: 'size-[34px] text-label lg:size-8 lg:text-caption',
205
476
  };
206
477
  /**
207
478
  * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
@@ -216,7 +487,7 @@ class UiAvatar {
216
487
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
217
488
  size = input('md', /* @ts-ignore */
218
489
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
219
- classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
490
+ classes = computed(() => `${BASE_CLASSES$7} ${SIZE_CLASSES$7[this.size()]}`, /* @ts-ignore */
220
491
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
221
492
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
222
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 });
@@ -233,10 +504,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
233
504
  },
234
505
  }]
235
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 }] }] } });
236
593
 
237
594
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
238
595
  const BADGE_VARIANTS = ['neutral', 'outline'];
239
- const BASE_CLASSES$5 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
596
+ const BADGE_SIZES = ['md', 'sm'];
597
+ const BASE_CLASSES$5 = 'inline-flex items-center rounded-pill text-caption font-medium';
598
+ const SIZE_CLASSES$5 = {
599
+ md: 'h-6 px-2.5',
600
+ sm: 'h-[22px] px-2',
601
+ };
240
602
  const VARIANT_CLASSES$2 = {
241
603
  neutral: 'bg-(--muted) text-(--foreground)',
242
604
  outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
@@ -250,10 +612,12 @@ const VARIANT_CLASSES$2 = {
250
612
  class UiBadge {
251
613
  variant = input('neutral', /* @ts-ignore */
252
614
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
253
- classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
615
+ size = input('md', /* @ts-ignore */
616
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
617
+ classes = computed(() => `${BASE_CLASSES$5} ${SIZE_CLASSES$5[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
254
618
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
255
619
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
256
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiBadge, isStandalone: true, selector: "ui-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
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 });
257
621
  }
258
622
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, decorators: [{
259
623
  type: Component,
@@ -264,24 +628,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
264
628
  '[class]': 'classes()',
265
629
  },
266
630
  }]
267
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
631
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
268
632
 
269
633
  /** Available button variants. `ButtonVariant` is derived from this tuple. */
270
- const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
271
- /** Available button sizes. `md` is the touch target Cairn asks for everywhere. */
272
- const BUTTON_SIZES = ['md', 'lg', 'xl', 'icon'];
273
- const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
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
+ ];
645
+ /** Available button sizes. */
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)';
274
664
  const VARIANT_CLASSES$1 = {
275
- primary: 'bg-(--primary) text-(--primary-foreground) hover:opacity-90',
276
- outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)',
277
- ghost: 'bg-transparent text-(--foreground) hover:bg-(--glow)',
278
- destructive: 'bg-(--destructive) text-(--destructive-foreground) hover:opacity-90',
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)`,
279
674
  };
280
- const SIZE_CLASSES = {
281
- md: 'min-h-(--row-min) px-4 text-label',
282
- lg: 'h-11 px-[18px] text-label',
283
- xl: 'h-[50px] px-6 text-body',
284
- icon: 'size-(--row-min) p-0',
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',
680
+ md: 'min-h-(--row-min) gap-2 px-4 text-label has-[>svg:first-child]:pl-3',
681
+ lg: 'h-11 gap-2 px-[18px] text-label',
682
+ xl: 'h-[50px] gap-2 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',
686
+ icon: 'size-(--row-min) gap-2 p-0',
687
+ 'icon-sm': 'size-11 pointer-fine:size-9 gap-2 p-0',
285
688
  };
286
689
  /**
287
690
  * Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
@@ -296,12 +699,15 @@ class UiButton {
296
699
  size = input('md', /* @ts-ignore */
297
700
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
298
701
  loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
299
- classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[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 */
300
706
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
301
707
  constructor() {
302
708
  const host = inject(ElementRef).nativeElement;
303
709
  const guard = (event) => {
304
- if (this.loading()) {
710
+ if (this.inactive()) {
305
711
  event.preventDefault();
306
712
  event.stopImmediatePropagation();
307
713
  }
@@ -310,7 +716,7 @@ class UiButton {
310
716
  inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
311
717
  }
312
718
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
313
- 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: `
314
720
  @if (loading()) {
315
721
  <span
316
722
  aria-hidden="true"
@@ -335,41 +741,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
335
741
  `,
336
742
  host: {
337
743
  '[class]': 'classes()',
338
- '[attr.aria-busy]': 'loading() || null',
339
- '[attr.aria-disabled]': 'loading() || null',
744
+ '[attr.aria-busy]': 'inactive() || null',
745
+ '[attr.aria-disabled]': 'inactive() || null',
340
746
  },
341
747
  }]
342
- }], 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 }] }] } });
343
749
 
344
750
  /** Available card variants. `CardVariant` is derived from this tuple. */
345
751
  const CARD_VARIANTS = ['default', 'elevated'];
346
752
  /** Available card paddings. `CardPadding` is derived from this tuple. */
347
- const CARD_PADDINGS = ['none', 'sm', 'md'];
348
- const BASE_CLASSES$3 = 'block rounded-container border';
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';
349
757
  const VARIANT_CLASSES = {
350
- default: 'bg-(--card) border-(--border) text-(--card-foreground)',
351
- elevated: 'bg-(--elevated) border-(--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
+ },
352
768
  };
353
- const PADDING_CLASSES = {
354
- none: 'p-0',
355
- sm: 'p-3',
356
- md: 'p-5',
769
+ const PADDING_CLASSES$1 = {
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
+ },
357
795
  };
358
796
  /**
359
- * 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.
360
800
  *
361
801
  * @example
362
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>
363
804
  */
364
805
  class UiCard {
365
806
  variant = input('default', /* @ts-ignore */
366
807
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
367
808
  padding = input('md', /* @ts-ignore */
368
809
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
369
- classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[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 */
370
813
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
371
814
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
372
- 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 });
373
816
  }
374
817
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, decorators: [{
375
818
  type: Component,
@@ -380,7 +823,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
380
823
  '[class]': 'classes()',
381
824
  },
382
825
  }]
383
- }], 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 }] }] } });
384
827
 
385
828
  /**
386
829
  * Resolves to the styled control projected into a field.
@@ -392,17 +835,270 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
392
835
  */
393
836
  const UI_CONTROL = new InjectionToken('UI_CONTROL');
394
837
 
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'];
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';
841
+ const CONTROL_SURFACES = ['background', 'card'];
842
+ const CONTROL_SURFACE_CLASSES = {
843
+ background: 'bg-(--background)',
844
+ card: 'bg-(--card)',
845
+ };
846
+ const CONTROL_SIZE_CLASSES = {
847
+ sm: 'h-9',
848
+ md: 'min-h-(--row-min)',
849
+ lg: 'min-h-12 lg:min-h-(--row-min)',
850
+ xl: 'h-12 lg:h-11',
851
+ };
852
+ const SEARCH_CLASSES = '[&::-webkit-search-cancel-button]:appearance-none';
853
+ /**
854
+ * Styled native text input. A directive (not a wrapper component) so the host
855
+ * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
856
+ * and every native attribute keep working without any plumbing.
857
+ *
858
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
859
+ * it can show the message without any binding here.
860
+ *
861
+ * @example
862
+ * <input uiInput type="email" placeholder="you@example.com" />
863
+ */
864
+ class UiInput {
865
+ size = input('md', /* @ts-ignore */
866
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
867
+ surface = input('background', /* @ts-ignore */
868
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
869
+ errors = input([], /* @ts-ignore */
870
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
871
+ touched = input(false, /* @ts-ignore */
872
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
873
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${SEARCH_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
874
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
875
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
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 });
877
+ }
878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
879
+ type: Directive,
880
+ args: [{
881
+ selector: 'input[uiInput]',
882
+ host: {
883
+ '[class]': 'classes()',
884
+ },
885
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }],
886
+ }]
887
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
888
+ /**
889
+ * Styled native textarea. Lives beside the input because it shares every one of its
890
+ * classes; it takes no size, since a textarea grows from a floor instead of sitting
891
+ * at a fixed height.
892
+ *
893
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
894
+ * it can show the message without any binding here.
895
+ *
896
+ * @example
897
+ * <textarea uiTextarea maxlength="280"></textarea>
898
+ */
899
+ class UiTextarea {
900
+ errors = input([], /* @ts-ignore */
901
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
902
+ touched = input(false, /* @ts-ignore */
903
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
905
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-control 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 bg-(--background) min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
906
+ }
907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
908
+ type: Directive,
909
+ args: [{
910
+ selector: 'textarea[uiTextarea]',
911
+ host: {
912
+ class: `${CONTROL_BASE_CLASSES} bg-(--background) min-h-24 py-2`,
913
+ },
914
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],
915
+ }]
916
+ }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
917
+
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)';
919
+ const SELECTED_CLASSES$2 = 'bg-(--primary) text-(--primary-foreground)';
920
+ const UNSELECTED_CLASSES$1 = 'text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
921
+ const nextId$4 = (() => {
922
+ let count = 0;
923
+ return () => `ui-choice-chips-${++count}`;
924
+ })();
925
+ /**
926
+ * Exclusive choice among a handful of named options, shown as wrapping pills: an account envelope,
927
+ * a category, a frequency. Binds to Signal Forms with `[formField]`.
928
+ *
929
+ * @example
930
+ * <ui-choice-chips label="Envelope" [options]="envelopes" [(value)]="envelope" />
931
+ *
932
+ * @example
933
+ * <ui-choice-chips label="Envelope" [options]="envelopes" [formField]="form.envelope" />
934
+ */
935
+ class UiChoiceChips {
936
+ options = input.required(/* @ts-ignore */
937
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
938
+ value = model.required(/* @ts-ignore */
939
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
940
+ label = input(/* @ts-ignore */
941
+ ...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
942
+ ariaLabel = input(/* @ts-ignore */
943
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
944
+ ariaLabelledby = input(/* @ts-ignore */
945
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []));
946
+ surface = input('background', /* @ts-ignore */
947
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
948
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
949
+ errors = input([], /* @ts-ignore */
950
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
951
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
952
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
953
+ touched = input(false, /* @ts-ignore */
954
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
955
+ touch = output();
956
+ labelId = nextId$4();
957
+ #host = inject(ElementRef);
958
+ radios = viewChildren('radio', /* @ts-ignore */
959
+ ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
960
+ labelledBy = computed(() => this.ariaLabelledby() ?? (this.label() ? this.labelId : null), /* @ts-ignore */
961
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
962
+ chipClasses(optionValue) {
963
+ const state = optionValue === this.value()
964
+ ? SELECTED_CLASSES$2
965
+ : `${CONTROL_SURFACE_CLASSES[this.surface()]} ${UNSELECTED_CLASSES$1}`;
966
+ return `${CHIP_CLASSES$1} ${state}`;
967
+ }
968
+ tabIndexFor(optionValue) {
969
+ const opts = this.options();
970
+ const tabStopValue = opts.some((option) => option.value === this.value()) ? this.value() : opts[0]?.value;
971
+ return optionValue === tabStopValue ? 0 : -1;
972
+ }
973
+ select(optionValue) {
974
+ this.value.set(optionValue);
975
+ }
976
+ onFocusOut(event) {
977
+ if (!this.#host.nativeElement.contains(event.relatedTarget)) {
978
+ this.touch.emit();
979
+ }
980
+ }
981
+ onKeydown(event, index) {
982
+ const opts = this.options();
983
+ const target = {
984
+ ArrowRight: index + 1,
985
+ ArrowDown: index + 1,
986
+ ArrowLeft: index - 1,
987
+ ArrowUp: index - 1,
988
+ Home: 0,
989
+ End: opts.length - 1,
990
+ }[event.key];
991
+ if (target === undefined) {
992
+ return;
993
+ }
994
+ event.preventDefault();
995
+ const nextIndex = ((target % opts.length) + opts.length) % opts.length;
996
+ this.value.set(opts[nextIndex].value);
997
+ this.radios()[nextIndex].nativeElement.focus();
998
+ }
999
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiChoiceChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
1000
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiChoiceChips, isStandalone: true, selector: "ui-choice-chips", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiChoiceChips) }], viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
1001
+ <div
1002
+ class="flex flex-col gap-1.5"
1003
+ role="radiogroup"
1004
+ [attr.aria-invalid]="invalid() || null"
1005
+ [attr.aria-label]="ariaLabel() ?? null"
1006
+ [attr.aria-labelledby]="labelledBy()"
1007
+ [attr.aria-required]="required() || null"
1008
+ (focusout)="onFocusOut($event)"
1009
+ >
1010
+ @if (label()) {
1011
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
1012
+ label()
1013
+ }}</span>
1014
+ }
1015
+
1016
+ <div class="flex flex-wrap gap-1.5">
1017
+ @for (option of options(); track option.value; let index = $index) {
1018
+ <button
1019
+ #radio
1020
+ role="radio"
1021
+ type="button"
1022
+ [attr.aria-checked]="option.value === value()"
1023
+ [attr.tabindex]="tabIndexFor(option.value)"
1024
+ [class]="chipClasses(option.value)"
1025
+ [disabled]="disabled()"
1026
+ (click)="select(option.value)"
1027
+ (keydown)="onKeydown($event, index)"
1028
+ >
1029
+ {{ option.label }}
1030
+ </button>
1031
+ }
1032
+ </div>
1033
+ </div>
1034
+ `, isInline: true });
1035
+ }
1036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiChoiceChips, decorators: [{
1037
+ type: Component,
1038
+ args: [{
1039
+ selector: 'ui-choice-chips',
1040
+ template: `
1041
+ <div
1042
+ class="flex flex-col gap-1.5"
1043
+ role="radiogroup"
1044
+ [attr.aria-invalid]="invalid() || null"
1045
+ [attr.aria-label]="ariaLabel() ?? null"
1046
+ [attr.aria-labelledby]="labelledBy()"
1047
+ [attr.aria-required]="required() || null"
1048
+ (focusout)="onFocusOut($event)"
1049
+ >
1050
+ @if (label()) {
1051
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
1052
+ label()
1053
+ }}</span>
1054
+ }
1055
+
1056
+ <div class="flex flex-wrap gap-1.5">
1057
+ @for (option of options(); track option.value; let index = $index) {
1058
+ <button
1059
+ #radio
1060
+ role="radio"
1061
+ type="button"
1062
+ [attr.aria-checked]="option.value === value()"
1063
+ [attr.tabindex]="tabIndexFor(option.value)"
1064
+ [class]="chipClasses(option.value)"
1065
+ [disabled]="disabled()"
1066
+ (click)="select(option.value)"
1067
+ (keydown)="onKeydown($event, index)"
1068
+ >
1069
+ {{ option.label }}
1070
+ </button>
1071
+ }
1072
+ </div>
1073
+ </div>
1074
+ `,
1075
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiChoiceChips) }],
1076
+ }]
1077
+ }], 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"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
1078
+
395
1079
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
396
1080
  const DELTA_EMPHASES = ['text', 'pill'];
397
- const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
398
- 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';
399
1095
  const TEXT_CLASSES = {
400
1096
  positive: 'text-(--positive)',
401
1097
  negative: 'text-(--negative)',
402
1098
  neutral: 'text-(--muted-foreground)',
403
1099
  unknown: 'text-(--subtle-foreground)',
404
1100
  };
405
- const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
1101
+ const PILL_BACKGROUND_CLASS = 'bg-(--soft)';
406
1102
  /**
407
1103
  * Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
408
1104
  *
@@ -422,6 +1118,10 @@ class UiDelta {
422
1118
  ...(ngDevMode ? [{ debugName: "unknownLabel" }] : /* istanbul ignore next */ []));
423
1119
  emphasis = input('text', /* @ts-ignore */
424
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 */ []));
425
1125
  sign = computed(() => {
426
1126
  const value = this.value();
427
1127
  if (value === null || value === undefined) {
@@ -438,12 +1138,12 @@ class UiDelta {
438
1138
  ...(ngDevMode ? [{ debugName: "sign" }] : /* istanbul ignore next */ []));
439
1139
  classes = computed(() => {
440
1140
  const sign = this.sign();
441
- const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
442
- 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}`;
443
1143
  }, /* @ts-ignore */
444
1144
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
445
1145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
446
- 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: `
447
1147
  @if (sign() === 'unknown') {
448
1148
  <span aria-hidden="true">&mdash;</span>
449
1149
  <span class="sr-only">{{ unknownLabel() }}</span>
@@ -468,26 +1168,70 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
468
1168
  '[class]': 'classes()',
469
1169
  },
470
1170
  }]
471
- }], 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 }] }] } });
472
1172
 
473
- /** Available dialog widths. `DialogWidth` is derived from this tuple. */
1173
+ /** Named dialog widths; any other CSS length is accepted as is. `DialogWidth` is derived from this tuple. */
474
1174
  const DIALOG_WIDTHS = ['md', 'lg'];
475
- const WIDTH_CLASSES = {
476
- md: 'max-w-md',
477
- lg: 'max-w-[560px]',
1175
+ const WIDTH_VALUES = {
1176
+ md: '28rem',
1177
+ lg: '560px',
478
1178
  };
479
- const nextId$2 = (() => {
1179
+ /** Spacing presets. `DialogLayout` is derived from this tuple. */
1180
+ const DIALOG_LAYOUTS = ['trade', 'form', 'list', 'confirm'];
1181
+ const LAYOUTS = {
1182
+ trade: {
1183
+ dialog: '',
1184
+ header: 'items-start lg:pt-6',
1185
+ title: 'pt-1 lg:pt-0',
1186
+ cross: 'lg:-mt-1.5',
1187
+ icon: 'lg:size-5',
1188
+ handle: 'pt-[7.5px]',
1189
+ body: 'pt-3 pb-4 lg:pt-5 lg:pb-5',
1190
+ footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
1191
+ },
1192
+ form: {
1193
+ dialog: '',
1194
+ header: 'items-center lg:pt-6',
1195
+ title: '',
1196
+ cross: '',
1197
+ icon: 'lg:size-[22px]',
1198
+ handle: 'pt-[7.5px]',
1199
+ body: 'pt-2 pb-5 lg:pt-4 lg:pb-5',
1200
+ footer: 'max-lg:pb-[calc(12px+env(safe-area-inset-bottom))] lg:pb-5',
1201
+ },
1202
+ list: {
1203
+ dialog: 'max-lg:h-[calc(100dvh-58px)] lg:mt-24 lg:mb-auto lg:max-h-[min(760px,calc(100dvh-96px-32px))]',
1204
+ header: 'items-center lg:pt-5',
1205
+ title: '',
1206
+ cross: '',
1207
+ icon: 'lg:size-[22px]',
1208
+ handle: 'pt-[7.5px]',
1209
+ body: 'pt-2 pb-4 lg:pt-4 lg:pb-5',
1210
+ footer: 'pt-3 shadow-[0_-1px_0_var(--hairline)] max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pt-4 lg:pb-4',
1211
+ },
1212
+ confirm: {
1213
+ dialog: '',
1214
+ header: 'items-center pt-3 lg:pt-6',
1215
+ title: '',
1216
+ cross: '',
1217
+ icon: 'lg:size-[22px]',
1218
+ handle: 'pt-[11.5px]',
1219
+ body: 'pt-3 pb-5 lg:pt-3 lg:pb-6',
1220
+ footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
1221
+ },
1222
+ };
1223
+ const nextId$3 = (() => {
480
1224
  let count = 0;
481
1225
  return () => `ui-dialog-${++count}`;
482
1226
  })();
483
1227
  /**
484
- * Modal dialog on the native <dialog>: a sheet rising from the bottom under `64rem`, centered
485
- * above it. Two projection slots - the body, and [dialogActions] for the footer.
1228
+ * Modal dialog on the native <dialog>: a bottom sheet under `64rem`, centered above it.
486
1229
  *
487
1230
  * @example
488
- * <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
489
- * <p>This cannot be undone.</p>
490
- * <button dialogActions ui-button variant="outline" (click)="confirming.set(false)">Cancel</button>
1231
+ * <ui-dialog heading="Buy" description="Ferrari" closeLabel="Close" width="528px" [open]="buying()" (dismissed)="buying.set(false)">
1232
+ * <p>Body</p>
1233
+ * <button dialogActions ui-button variant="outline" class="max-lg:hidden" (click)="buying.set(false)">Cancel</button>
1234
+ * <button dialogActions ui-button>Buy 40 shares</button>
491
1235
  * </ui-dialog>
492
1236
  */
493
1237
  class UiDialog {
@@ -495,17 +1239,60 @@ class UiDialog {
495
1239
  ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
496
1240
  description = input(/* @ts-ignore */
497
1241
  ...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
1242
+ closeLabel = input(/* @ts-ignore */
1243
+ ...(ngDevMode ? [undefined, { debugName: "closeLabel" }] : /* istanbul ignore next */ []));
1244
+ layout = input('trade', /* @ts-ignore */
1245
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
1246
+ truncateDescription = input(false, { ...(ngDevMode ? { debugName: "truncateDescription" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
498
1247
  width = input('lg', /* @ts-ignore */
499
1248
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
500
1249
  open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
501
1250
  dismissed = output();
502
- headingId = nextId$2();
1251
+ headingId = nextId$3();
503
1252
  descriptionId = `${this.headingId}-description`;
504
- classes = computed(() => `flex w-full flex-col m-auto mt-auto max-h-[calc(100dvh-2rem)] max-lg:mb-0 max-lg:max-w-none max-lg:rounded-b-none rounded-container ${WIDTH_CLASSES[this.width()]} border border-(--border) bg-(--card) text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
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 */ []));
1263
+ widthValue = computed(() => {
1264
+ const width = this.width();
1265
+ return Object.hasOwn(WIDTH_VALUES, width) ? WIDTH_VALUES[width] : width;
1266
+ }, /* @ts-ignore */
1267
+ ...(ngDevMode ? [{ debugName: "widthValue" }] : /* istanbul ignore next */ []));
1268
+ spec = computed(() => LAYOUTS[this.layout()], /* @ts-ignore */
1269
+ ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
1270
+ headerClasses = computed(() => `flex gap-2 pl-4 lg:gap-3 lg:pl-6 ${this.spec().header} ${this.closeLabel() ? 'pr-2 lg:pr-4' : 'pr-4 lg:pr-6'}`, /* @ts-ignore */
1271
+ ...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
1272
+ titleClasses = computed(() => this.spec().title, /* @ts-ignore */
1273
+ ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
1274
+ crossClasses = computed(() => `rounded-pill lg:rounded-control grid size-11 flex-none cursor-pointer place-items-center text-(--muted-foreground) transition-[transform,background-color,color] duration-(--duration-press) ease-out outline-none focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-(--ring) active:scale-(--press-scale) active:bg-(--soft) lg:size-9 lg:hover:bg-(--glow) lg:hover:text-(--foreground) ${this.spec().cross}`, /* @ts-ignore */
1275
+ ...(ngDevMode ? [{ debugName: "crossClasses" }] : /* istanbul ignore next */ []));
1276
+ iconClasses = computed(() => this.spec().icon, /* @ts-ignore */
1277
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
1278
+ footerClasses = computed(() => `max-lg:[&>[ui-button]]:text-body flex justify-end gap-2 px-4 empty:hidden max-lg:flex-col-reverse max-lg:[&>[ui-button]]:h-[50px] max-lg:[&>[ui-button]]:w-full lg:px-6 ${this.spec().footer}`, /* @ts-ignore */
1279
+ ...(ngDevMode ? [{ debugName: "footerClasses" }] : /* istanbul ignore next */ []));
1280
+ classes = computed(() => `open:flex w-full flex-col overflow-hidden m-auto mt-auto max-h-[calc(100dvh-2rem)] max-lg:mb-0 max-lg:max-w-none max-lg:rounded-b-none lg:max-w-(--dialog-width) rounded-container bg-(--card) text-(--foreground) shadow-[0_0_0_1px_var(--border),0_24px_64px_rgb(0_0_0/0.24)] max-lg:shadow-[0_-1px_0_var(--border),0_-12px_32px_rgb(0_0_0/0.16)] backdrop:bg-black/[0.36] ${this.spec().dialog}`, /* @ts-ignore */
505
1281
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
506
1282
  #host = inject(ElementRef);
507
1283
  constructor() {
508
- effect(() => {
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
+ });
1295
+ afterRenderEffect(() => {
509
1296
  // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
510
1297
  const dialog = this.#host.nativeElement.querySelector('dialog');
511
1298
  if (this.open() && !dialog.open) {
@@ -516,6 +1303,23 @@ class UiDialog {
516
1303
  }
517
1304
  });
518
1305
  }
1306
+ #pressedOnBackdrop = false;
1307
+ onPointerDown(event) {
1308
+ this.#pressedOnBackdrop = event.target === this.#dialog && this.#outside(event);
1309
+ }
1310
+ onClick(event) {
1311
+ if (this.#pressedOnBackdrop && event.target === this.#dialog && this.#outside(event)) {
1312
+ this.#dialog.close();
1313
+ }
1314
+ this.#pressedOnBackdrop = false;
1315
+ }
1316
+ get #dialog() {
1317
+ return this.#host.nativeElement.querySelector('dialog');
1318
+ }
1319
+ #outside(event) {
1320
+ const rect = this.#dialog.getBoundingClientRect();
1321
+ return (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom);
1322
+ }
519
1323
  onNativeClose() {
520
1324
  // The owner already knows about a close it asked for; only a close it did not ask for is news.
521
1325
  if (this.open()) {
@@ -523,87 +1327,159 @@ class UiDialog {
523
1327
  }
524
1328
  }
525
1329
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
526
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDialog, isStandalone: true, selector: "ui-dialog", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
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: `
527
1331
  <dialog
528
- [attr.aria-describedby]="description() ? descriptionId : null"
1332
+ #dlg
1333
+ [attr.aria-describedby]="describedBy()"
529
1334
  [attr.aria-labelledby]="headingId"
1335
+ [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
530
1336
  [class]="classes()"
1337
+ [style.--dialog-width]="widthValue()"
531
1338
  (close)="onNativeClose()"
532
1339
  >
533
- <div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
534
- <span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
1340
+ <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1341
+ <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
535
1342
  </div>
536
1343
 
537
- <div class="px-6 pt-1 lg:pt-6">
538
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1344
+ <div data-dialog-header [class]="headerClasses()">
1345
+ <div class="flex min-w-0 flex-1 flex-col" [class]="titleClasses()">
1346
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1347
+
1348
+ @if (description()) {
1349
+ <p
1350
+ class="text-label text-(--muted-foreground)"
1351
+ [class.truncate]="truncateDescription()"
1352
+ [id]="descriptionId"
1353
+ >
1354
+ {{ description() }}
1355
+ </p>
1356
+ }
1357
+ </div>
539
1358
 
540
- @if (description()) {
541
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
1359
+ @if (closeLabel()) {
1360
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1361
+ <svg
1362
+ aria-hidden="true"
1363
+ class="block size-[22px] fill-none stroke-current stroke-[1.75]"
1364
+ stroke-linecap="round"
1365
+ stroke-linejoin="round"
1366
+ viewBox="0 0 24 24"
1367
+ [class]="iconClasses()"
1368
+ >
1369
+ <path d="M18 6 6 18" />
1370
+ <path d="m6 6 12 12" />
1371
+ </svg>
1372
+ </button>
542
1373
  }
543
1374
  </div>
544
1375
 
545
- <div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
546
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
547
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
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
+ >
1384
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1385
+ <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
548
1386
  <ng-content />
549
1387
  </div>
550
1388
  </div>
551
1389
 
552
- <div
553
- class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
554
- data-dialog-footer
555
- >
1390
+ <div data-dialog-footer [class]="footerClasses()">
556
1391
  <ng-content select="[dialogActions]" />
557
1392
  </div>
558
1393
  </dialog>
559
- `, isInline: true, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] });
1394
+ `, isInline: true, styles: ["dialog{--dialog-ease: var(--ease-sheet);transition:opacity var(--duration-base) var(--dialog-ease),transform var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{--dialog-ease: var(--ease-out);transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] });
560
1395
  }
561
1396
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
562
1397
  type: Component,
563
- args: [{ selector: 'ui-dialog', template: `
1398
+ args: [{ selector: 'ui-dialog', host: { '(pointerdown)': 'onPointerDown($event)', '(click)': 'onClick($event)' }, template: `
564
1399
  <dialog
565
- [attr.aria-describedby]="description() ? descriptionId : null"
1400
+ #dlg
1401
+ [attr.aria-describedby]="describedBy()"
566
1402
  [attr.aria-labelledby]="headingId"
1403
+ [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
567
1404
  [class]="classes()"
1405
+ [style.--dialog-width]="widthValue()"
568
1406
  (close)="onNativeClose()"
569
1407
  >
570
- <div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
571
- <span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
1408
+ <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1409
+ <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
572
1410
  </div>
573
1411
 
574
- <div class="px-6 pt-1 lg:pt-6">
575
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1412
+ <div data-dialog-header [class]="headerClasses()">
1413
+ <div class="flex min-w-0 flex-1 flex-col" [class]="titleClasses()">
1414
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1415
+
1416
+ @if (description()) {
1417
+ <p
1418
+ class="text-label text-(--muted-foreground)"
1419
+ [class.truncate]="truncateDescription()"
1420
+ [id]="descriptionId"
1421
+ >
1422
+ {{ description() }}
1423
+ </p>
1424
+ }
1425
+ </div>
576
1426
 
577
- @if (description()) {
578
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
1427
+ @if (closeLabel()) {
1428
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1429
+ <svg
1430
+ aria-hidden="true"
1431
+ class="block size-[22px] fill-none stroke-current stroke-[1.75]"
1432
+ stroke-linecap="round"
1433
+ stroke-linejoin="round"
1434
+ viewBox="0 0 24 24"
1435
+ [class]="iconClasses()"
1436
+ >
1437
+ <path d="M18 6 6 18" />
1438
+ <path d="m6 6 12 12" />
1439
+ </svg>
1440
+ </button>
579
1441
  }
580
1442
  </div>
581
1443
 
582
- <div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
583
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
584
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
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
+ >
1452
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1453
+ <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
585
1454
  <ng-content />
586
1455
  </div>
587
1456
  </div>
588
1457
 
589
- <div
590
- class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
591
- data-dialog-footer
592
- >
1458
+ <div data-dialog-footer [class]="footerClasses()">
593
1459
  <ng-content select="[dialogActions]" />
594
1460
  </div>
595
1461
  </dialog>
596
- `, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] }]
597
- }], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
598
-
599
- const OUTER_RADIUS = 93;
600
- const INNER_RADIUS = 63;
601
- const pieLayout = pie()
602
- .value((slice) => slice.value)
603
- .sort(null);
604
- const arcGenerator = arc().innerRadius(INNER_RADIUS).outerRadius(OUTER_RADIUS);
1462
+ `, styles: ["dialog{--dialog-ease: var(--ease-sheet);transition:opacity var(--duration-base) var(--dialog-ease),transform var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{--dialog-ease: var(--ease-out);transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] }]
1463
+ }], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], truncateDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "truncateDescription", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
1464
+
1465
+ const RADIUS = 78;
1466
+ const RING_WIDTH = 30;
1467
+ const ACTIVE_RING_WIDTH = 38;
1468
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1469
+ const GAP = 2;
605
1470
  function positionSlices(slices) {
606
- return pieLayout(slices).map((arc) => ({ ...arc.data, path: arcGenerator(arc) }));
1471
+ let travelled = 0;
1472
+ return slices.map((slice) => {
1473
+ const length = slice.share * CIRCUMFERENCE;
1474
+ const drawn = Math.max(0, length - GAP);
1475
+ const positioned = {
1476
+ ...slice,
1477
+ dash: `${drawn.toFixed(2)} ${(CIRCUMFERENCE - drawn).toFixed(2)}`,
1478
+ offset: (-travelled).toFixed(2),
1479
+ };
1480
+ travelled += length;
1481
+ return positioned;
1482
+ });
607
1483
  }
608
1484
 
609
1485
  const MAX_RAMP = 6;
@@ -635,16 +1511,7 @@ function rankSlices(slices, othersLabel) {
635
1511
 
636
1512
  const identityValue$1 = (value) => `${value}`;
637
1513
  const identityShare = (share) => `${Math.round(share * 100)}%`;
638
- const ACTIVE_SCALE = 1.08;
639
- const ROW_CLASSES = 'flex w-full items-center gap-3 min-h-12 px-2 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)';
640
- const RAMP_FILL_CLASSES = {
641
- 1: 'fill-(--ramp-1)',
642
- 2: 'fill-(--ramp-2)',
643
- 3: 'fill-(--ramp-3)',
644
- 4: 'fill-(--ramp-4)',
645
- 5: 'fill-(--ramp-5)',
646
- 6: 'fill-(--ramp-6)',
647
- };
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)';
648
1515
  const RAMP_DOT_CLASSES = {
649
1516
  1: 'bg-(--ramp-1)',
650
1517
  2: 'bg-(--ramp-2)',
@@ -671,6 +1538,8 @@ class UiDonut {
671
1538
  ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
672
1539
  shareFormat = input(identityShare, /* @ts-ignore */
673
1540
  ...(ngDevMode ? [{ debugName: "shareFormat" }] : /* istanbul ignore next */ []));
1541
+ legendHref = input(/* @ts-ignore */
1542
+ ...(ngDevMode ? [undefined, { debugName: "legendHref" }] : /* istanbul ignore next */ []));
674
1543
  sliceSelect = output();
675
1544
  activeId = signal(null, /* @ts-ignore */
676
1545
  ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
@@ -691,22 +1560,23 @@ class UiDonut {
691
1560
  return detail ? `${this.label()} : ${detail}` : this.label();
692
1561
  }, /* @ts-ignore */
693
1562
  ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
694
- pathClasses(slice) {
695
- return `cursor-pointer stroke-(--card) ${RAMP_FILL_CLASSES[slice.ramp]}`;
1563
+ radius = RADIUS;
1564
+ ringWidth = RING_WIDTH;
1565
+ strokeColor(slice) {
1566
+ return `var(--ramp-${slice.ramp})`;
1567
+ }
1568
+ strokeWidth(slice) {
1569
+ return this.isActive(slice) ? ACTIVE_RING_WIDTH : RING_WIDTH;
696
1570
  }
697
1571
  dotClasses(slice) {
698
1572
  return `size-3 flex-none rounded-pill shadow-[inset_0_0_0_1px_var(--border)] ${RAMP_DOT_CLASSES[slice.ramp]}`;
699
1573
  }
700
1574
  rowClasses(slice) {
701
- return `${ROW_CLASSES}${this.isActive(slice) ? ' bg-(--soft)' : ''}`;
1575
+ return `${ROW_CLASSES$1}${this.isActive(slice) ? ' bg-(--soft)' : ''}`;
702
1576
  }
703
1577
  subLabel(slice) {
704
1578
  return slice.members.length > 0 ? slice.members.join(', ') : (slice.sublabel ?? '');
705
1579
  }
706
- arcTransform(slice) {
707
- const scale = this.isActive(slice) ? ACTIVE_SCALE : 1;
708
- return `translate(100px, 100px) scale(${scale})`;
709
- }
710
1580
  isActive(slice) {
711
1581
  return this.active()?.id === slice.id;
712
1582
  }
@@ -718,64 +1588,111 @@ class UiDonut {
718
1588
  this.activeId.set(null);
719
1589
  }
720
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
+ }
721
1601
  select(slice) {
722
1602
  this.activeId.set(slice.id);
723
1603
  this.sliceSelect.emit(slice.id);
724
1604
  }
725
1605
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, deps: [], target: i0.ɵɵFactoryTarget.Component });
726
- 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-6 sm:flex-row sm:items-center" }, 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: `
727
1607
  <div class="relative size-[232px] flex-none sm:size-60">
728
- <svg class="block h-full w-full" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1608
+ <svg class="block h-full w-full -rotate-90" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1609
+ <circle
1610
+ class="stroke-(--muted)"
1611
+ cx="100"
1612
+ cy="100"
1613
+ fill="none"
1614
+ [attr.r]="radius"
1615
+ [attr.stroke-width]="ringWidth"
1616
+ />
729
1617
  @for (slice of positioned(); track slice.id) {
730
- <g class="transition-transform duration-(--duration-fast) ease-out" [style.transform]="arcTransform(slice)">
731
- <path
732
- stroke-width="1"
733
- [attr.d]="slice.path"
734
- [class]="pathClasses(slice)"
735
- (click)="select(slice)"
736
- (pointerenter)="highlight(slice.id)"
737
- (pointerleave)="clearHighlight(slice.id)"
738
- />
739
- </g>
1618
+ <circle
1619
+ class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1620
+ cx="100"
1621
+ cy="100"
1622
+ data-slice
1623
+ fill="none"
1624
+ [attr.r]="radius"
1625
+ [attr.stroke-dasharray]="slice.dash"
1626
+ [attr.stroke-dashoffset]="slice.offset"
1627
+ [style.stroke]="strokeColor(slice)"
1628
+ [style.stroke-width.px]="strokeWidth(slice)"
1629
+ (click)="select(slice)"
1630
+ (pointerenter)="highlight(slice.id)"
1631
+ (pointerleave)="clearHighlight(slice.id)"
1632
+ />
740
1633
  }
741
1634
  </svg>
742
1635
 
743
1636
  @if (active(); as slice) {
744
1637
  <div
745
- class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
1638
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-12 text-center"
746
1639
  data-testid="donut-centre"
747
1640
  >
748
- <span class="text-caption max-w-full truncate text-(--muted-foreground)">{{ slice.label }}</span>
749
- <span class="text-title font-semibold">{{ shareFormat()(slice.share) }}</span>
750
- <span class="text-caption">{{ valueFormat()(slice.value) }}</span>
1641
+ <span class="text-label max-w-full truncate font-medium whitespace-nowrap text-(--muted-foreground)">{{
1642
+ slice.label
1643
+ }}</span>
1644
+ <span class="text-heading font-semibold tracking-(--tracking-display)">{{ shareFormat()(slice.share) }}</span>
1645
+ <span class="text-caption tracking-normal text-(--muted-foreground) [font-variant-numeric:var(--numeric)]">{{
1646
+ valueFormat()(slice.value)
1647
+ }}</span>
751
1648
  </div>
752
1649
  }
753
1650
  </div>
754
1651
 
755
- <div class="flex min-w-0 flex-1 flex-col">
1652
+ <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
756
1653
  @for (slice of ranked(); track slice.id) {
757
- <button
758
- type="button"
759
- [class]="rowClasses(slice)"
760
- (blur)="clearHighlight(slice.id)"
761
- (click)="select(slice)"
762
- (focus)="highlight(slice.id)"
763
- (pointerenter)="highlight(slice.id)"
764
- (pointerleave)="clearHighlight(slice.id)"
765
- >
766
- <span [class]="dotClasses(slice)"></span>
767
- <span class="flex min-w-0 flex-1 flex-col">
768
- <span class="text-body truncate font-medium">{{ slice.label }}</span>
769
- <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
770
- </span>
771
- <span class="flex flex-none flex-col items-end whitespace-nowrap">
772
- <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
773
- <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
774
- </span>
775
- </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
+ }
776
1679
  }
777
1680
  </div>
778
- `, 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"] }] });
779
1696
  }
780
1697
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, decorators: [{
781
1698
  type: Component,
@@ -783,67 +1700,301 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
783
1700
  selector: 'ui-donut',
784
1701
  template: `
785
1702
  <div class="relative size-[232px] flex-none sm:size-60">
786
- <svg class="block h-full w-full" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1703
+ <svg class="block h-full w-full -rotate-90" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1704
+ <circle
1705
+ class="stroke-(--muted)"
1706
+ cx="100"
1707
+ cy="100"
1708
+ fill="none"
1709
+ [attr.r]="radius"
1710
+ [attr.stroke-width]="ringWidth"
1711
+ />
787
1712
  @for (slice of positioned(); track slice.id) {
788
- <g class="transition-transform duration-(--duration-fast) ease-out" [style.transform]="arcTransform(slice)">
789
- <path
790
- stroke-width="1"
791
- [attr.d]="slice.path"
792
- [class]="pathClasses(slice)"
793
- (click)="select(slice)"
794
- (pointerenter)="highlight(slice.id)"
795
- (pointerleave)="clearHighlight(slice.id)"
796
- />
797
- </g>
1713
+ <circle
1714
+ class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1715
+ cx="100"
1716
+ cy="100"
1717
+ data-slice
1718
+ fill="none"
1719
+ [attr.r]="radius"
1720
+ [attr.stroke-dasharray]="slice.dash"
1721
+ [attr.stroke-dashoffset]="slice.offset"
1722
+ [style.stroke]="strokeColor(slice)"
1723
+ [style.stroke-width.px]="strokeWidth(slice)"
1724
+ (click)="select(slice)"
1725
+ (pointerenter)="highlight(slice.id)"
1726
+ (pointerleave)="clearHighlight(slice.id)"
1727
+ />
798
1728
  }
799
1729
  </svg>
800
1730
 
801
1731
  @if (active(); as slice) {
802
1732
  <div
803
- class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
1733
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-12 text-center"
804
1734
  data-testid="donut-centre"
805
1735
  >
806
- <span class="text-caption max-w-full truncate text-(--muted-foreground)">{{ slice.label }}</span>
807
- <span class="text-title font-semibold">{{ shareFormat()(slice.share) }}</span>
808
- <span class="text-caption">{{ valueFormat()(slice.value) }}</span>
1736
+ <span class="text-label max-w-full truncate font-medium whitespace-nowrap text-(--muted-foreground)">{{
1737
+ slice.label
1738
+ }}</span>
1739
+ <span class="text-heading font-semibold tracking-(--tracking-display)">{{ shareFormat()(slice.share) }}</span>
1740
+ <span class="text-caption tracking-normal text-(--muted-foreground) [font-variant-numeric:var(--numeric)]">{{
1741
+ valueFormat()(slice.value)
1742
+ }}</span>
809
1743
  </div>
810
1744
  }
811
1745
  </div>
812
1746
 
813
- <div class="flex min-w-0 flex-1 flex-col">
1747
+ <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
814
1748
  @for (slice of ranked(); track slice.id) {
815
- <button
816
- type="button"
817
- [class]="rowClasses(slice)"
818
- (blur)="clearHighlight(slice.id)"
819
- (click)="select(slice)"
820
- (focus)="highlight(slice.id)"
821
- (pointerenter)="highlight(slice.id)"
822
- (pointerleave)="clearHighlight(slice.id)"
823
- >
824
- <span [class]="dotClasses(slice)"></span>
825
- <span class="flex min-w-0 flex-1 flex-col">
826
- <span class="text-body truncate font-medium">{{ slice.label }}</span>
827
- <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
828
- </span>
829
- <span class="flex flex-none flex-col items-end whitespace-nowrap">
830
- <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
831
- <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
832
- </span>
833
- </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
+ }
834
1774
  }
835
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>
836
1790
  `,
1791
+ imports: [NgTemplateOutlet],
837
1792
  host: {
838
- class: 'flex flex-col items-center gap-6 sm:flex-row sm:items-center',
1793
+ class: 'flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8',
839
1794
  },
840
1795
  }]
841
- }], 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"] }] } });
842
1797
 
843
- const nextId$1 = (() => {
1798
+ /**
1799
+ * Link to another site: the label followed by an external-link icon. A 44px target on touch,
1800
+ * an underline on hover with a mouse.
1801
+ *
1802
+ * @example
1803
+ * <a ui-external-link href="https://example.com" target="_blank" rel="noopener">Page on example.com</a>
1804
+ */
1805
+ class UiExternalLink {
1806
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiExternalLink, deps: [], target: i0.ɵɵFactoryTarget.Component });
1807
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiExternalLink, isStandalone: true, selector: "a[ui-external-link]", host: { classAttribute: "inline-flex min-h-11 items-center gap-1.5 rounded-[4px] text-label leading-[17px] font-medium lg:min-h-0 hover:underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) [&>svg]:flex-none" }, ngImport: i0, template: `
1808
+ <ng-content />
1809
+ <svg
1810
+ aria-hidden="true"
1811
+ fill="none"
1812
+ height="14"
1813
+ stroke="currentColor"
1814
+ stroke-linecap="round"
1815
+ stroke-linejoin="round"
1816
+ stroke-width="1.75"
1817
+ viewBox="0 0 24 24"
1818
+ width="14"
1819
+ >
1820
+ <path d="M15 3h6v6" />
1821
+ <path d="M10 14 21 3" />
1822
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
1823
+ </svg>
1824
+ `, isInline: true });
1825
+ }
1826
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiExternalLink, decorators: [{
1827
+ type: Component,
1828
+ args: [{
1829
+ selector: 'a[ui-external-link]',
1830
+ template: `
1831
+ <ng-content />
1832
+ <svg
1833
+ aria-hidden="true"
1834
+ fill="none"
1835
+ height="14"
1836
+ stroke="currentColor"
1837
+ stroke-linecap="round"
1838
+ stroke-linejoin="round"
1839
+ stroke-width="1.75"
1840
+ viewBox="0 0 24 24"
1841
+ width="14"
1842
+ >
1843
+ <path d="M15 3h6v6" />
1844
+ <path d="M10 14 21 3" />
1845
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
1846
+ </svg>
1847
+ `,
1848
+ host: {
1849
+ class: 'inline-flex min-h-11 items-center gap-1.5 rounded-[4px] text-label leading-[17px] font-medium lg:min-h-0 hover:underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) [&>svg]:flex-none',
1850
+ },
1851
+ }]
1852
+ }] });
1853
+
1854
+ /** Available fact row sizes. `FactSize` is derived from this tuple. */
1855
+ const FACT_SIZES = ['md', 'sm', 'auto'];
1856
+ /** Colour of the line under a fact value. `stale` flags an out-of-date figure. */
1857
+ const FACT_SUB_TONES = ['subtle', 'stale'];
1858
+ const SIZE_CLASSES$2 = {
1859
+ md: { row: 'py-3 [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-body' },
1860
+ sm: { row: 'py-2.5', value: 'text-label' },
1861
+ auto: {
1862
+ row: 'py-3 lg:py-2.5 max-lg:[&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]',
1863
+ value: 'text-body lg:text-label',
1864
+ },
1865
+ };
1866
+ const SUB_TONE_CLASSES$1 = {
1867
+ subtle: 'text-(--subtle-foreground)',
1868
+ stale: 'text-(--stale)',
1869
+ };
1870
+ /**
1871
+ * The list that holds `div[ui-fact]` rows, as a native description list.
1872
+ *
1873
+ * @example
1874
+ * <dl uiFacts>
1875
+ * <div ui-fact label="Quantity">500</div>
1876
+ * <div ui-fact label="Price" sub="24/09 · Yahoo">28,64 €</div>
1877
+ * </dl>
1878
+ */
1879
+ class UiFacts {
1880
+ ruled = input(false, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1881
+ classes = computed(() => `m-0 flex flex-col${this.ruled() ? ' shadow-[inset_0_1px_0_var(--hairline)]' : ''}`, /* @ts-ignore */
1882
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1884
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiFacts, isStandalone: true, selector: "dl[uiFacts]", inputs: { ruled: { classPropertyName: "ruled", publicName: "ruled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1885
+ }
1886
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, decorators: [{
1887
+ type: Directive,
1888
+ args: [{
1889
+ selector: 'dl[uiFacts]',
1890
+ host: { '[class]': 'classes()' },
1891
+ }]
1892
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
1893
+ /**
1894
+ * One fact of a `dl[uiFacts]`: a label on the left, its value on the right, an optional sub-line
1895
+ * under the value, and a hairline above every row but the first at the `md` size (the `sm` desktop list is separated by space only). The projected content is the value.
1896
+ *
1897
+ * @example
1898
+ * <div ui-fact label="Price" sub="24/09 · Yahoo" subTone="stale">28,64 €</div>
1899
+ */
1900
+ class UiFact {
1901
+ label = input.required(/* @ts-ignore */
1902
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1903
+ sub = input(/* @ts-ignore */
1904
+ ...(ngDevMode ? [undefined, { debugName: "sub" }] : /* istanbul ignore next */ []));
1905
+ subTone = input('subtle', /* @ts-ignore */
1906
+ ...(ngDevMode ? [{ debugName: "subTone" }] : /* istanbul ignore next */ []));
1907
+ size = input('md', /* @ts-ignore */
1908
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1909
+ classes = computed(() => `flex items-baseline justify-between gap-4 ${SIZE_CLASSES$2[this.size()].row}`, /* @ts-ignore */
1910
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1911
+ valueClasses = computed(() => `${SIZE_CLASSES$2[this.size()].value} tabular-nums`, /* @ts-ignore */
1912
+ ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
1913
+ subClasses = computed(() => `text-caption ${SUB_TONE_CLASSES$1[this.subTone()]}`, /* @ts-ignore */
1914
+ ...(ngDevMode ? [{ debugName: "subClasses" }] : /* istanbul ignore next */ []));
1915
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, deps: [], target: i0.ɵɵFactoryTarget.Component });
1916
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFact, isStandalone: true, selector: "div[ui-fact]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sub: { classPropertyName: "sub", publicName: "sub", isSignal: true, isRequired: false, transformFunction: null }, subTone: { classPropertyName: "subTone", publicName: "subTone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
1917
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
1918
+ <dd class="m-0 flex flex-col items-end text-right">
1919
+ <span [class]="valueClasses()"><ng-content /></span>
1920
+ @if (sub()) {
1921
+ <span [class]="subClasses()">{{ sub() }}</span>
1922
+ }
1923
+ </dd>
1924
+ `, isInline: true });
1925
+ }
1926
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, decorators: [{
1927
+ type: Component,
1928
+ args: [{
1929
+ selector: 'div[ui-fact]',
1930
+ template: `
1931
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
1932
+ <dd class="m-0 flex flex-col items-end text-right">
1933
+ <span [class]="valueClasses()"><ng-content /></span>
1934
+ @if (sub()) {
1935
+ <span [class]="subClasses()">{{ sub() }}</span>
1936
+ }
1937
+ </dd>
1938
+ `,
1939
+ host: { '[class]': 'classes()' },
1940
+ }]
1941
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sub: [{ type: i0.Input, args: [{ isSignal: true, alias: "sub", required: false }] }], subTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "subTone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1942
+
1943
+ const nextId$2 = (() => {
844
1944
  let count = 0;
845
1945
  return () => `ui-field-${++count}`;
846
1946
  })();
1947
+ /**
1948
+ * Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`: it sits
1949
+ * inside the control's left edge and the control's text is inset to clear it. Decorative, so hidden
1950
+ * from assistive technologies.
1951
+ *
1952
+ * @example
1953
+ * <ui-field label="Search" labelHidden>
1954
+ * <svg uiFieldLeading width="18" height="18" viewBox="0 0 24 24">...</svg>
1955
+ * <input uiInput type="search" />
1956
+ * </ui-field>
1957
+ */
1958
+ class UiFieldLeading {
1959
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1960
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldLeading, isStandalone: true, selector: "[uiFieldLeading]", host: { attributes: { "aria-hidden": "true" }, classAttribute: "pointer-events-none absolute left-3 flex text-(--muted-foreground)" }, ngImport: i0 });
1961
+ }
1962
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, decorators: [{
1963
+ type: Directive,
1964
+ args: [{
1965
+ selector: '[uiFieldLeading]',
1966
+ host: {
1967
+ class: 'pointer-events-none absolute left-3 flex text-(--muted-foreground)',
1968
+ 'aria-hidden': 'true',
1969
+ },
1970
+ }]
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
+ }] });
847
1998
  /**
848
1999
  * Label, control, hint and error as one unit. The control stays a plain native element:
849
2000
  * the field finds it in its own projected content and wires the ARIA attributes onto it.
@@ -876,13 +2027,22 @@ class UiField {
876
2027
  ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
877
2028
  unit = input(/* @ts-ignore */
878
2029
  ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
879
- #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();
880
2034
  hintId = `${this.#id}-hint`;
881
2035
  errorId = `${this.#id}-error`;
882
2036
  #host = inject(ElementRef);
883
2037
  control = contentChild(UI_CONTROL, /* @ts-ignore */
884
2038
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
885
- labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label font-medium text-(--muted-foreground)', /* @ts-ignore */
2039
+ leading = contentChild(UiFieldLeading, /* @ts-ignore */
2040
+ ...(ngDevMode ? [{ debugName: "leading" }] : /* istanbul ignore next */ []));
2041
+ trailing = contentChild(UiFieldTrailing, /* @ts-ignore */
2042
+ ...(ngDevMode ? [{ debugName: "trailing" }] : /* istanbul ignore next */ []));
2043
+ rowClasses = computed(() => `relative flex items-center${this.leading() ? ' [&>input]:pl-[38px]' : ''}${this.trailing() ? ' [&>input]:pr-12 lg:[&>input]:pr-11' : ''}`, /* @ts-ignore */
2044
+ ...(ngDevMode ? [{ debugName: "rowClasses" }] : /* istanbul ignore next */ []));
2045
+ labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)', /* @ts-ignore */
886
2046
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
887
2047
  /**
888
2048
  * The caller's own `error` wins: it is the only way to report something the control does not
@@ -901,6 +2061,7 @@ class UiField {
901
2061
  }, /* @ts-ignore */
902
2062
  ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
903
2063
  constructor() {
2064
+ let markedByField = false;
904
2065
  afterRenderEffect(() => {
905
2066
  // querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
906
2067
  const control = this.#host.nativeElement.querySelector('input, select, textarea');
@@ -916,23 +2077,31 @@ class UiField {
916
2077
  else {
917
2078
  control.removeAttribute('aria-describedby');
918
2079
  }
919
- if (this.message()) {
2080
+ if (this.message() || this.invalid()) {
920
2081
  control.setAttribute('aria-invalid', 'true');
2082
+ markedByField = true;
921
2083
  }
922
- else {
2084
+ else if (markedByField) {
923
2085
  control.removeAttribute('aria-invalid');
2086
+ markedByField = false;
924
2087
  }
925
2088
  });
926
2089
  }
927
2090
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
928
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }], ngImport: i0, template: `
2091
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null }, optional: { classPropertyName: "optional", publicName: "optional", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }, { propertyName: "leading", first: true, predicate: UiFieldLeading, descendants: true, isSignal: true }, { propertyName: "trailing", first: true, predicate: UiFieldTrailing, descendants: true, isSignal: true }], ngImport: i0, template: `
929
2092
  <div class="group flex flex-col gap-1.5">
930
2093
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
931
- <label [class]="labelClasses()">{{ label() }}</label>
2094
+ <label [class]="labelClasses()"
2095
+ >{{ label() }}
2096
+ @if (optional()) {
2097
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2098
+ }
2099
+ </label>
932
2100
 
933
- <div class="relative flex items-center">
2101
+ <div [class]="rowClasses()">
2102
+ <ng-content select="[uiFieldLeading]" />
934
2103
  <ng-content />
935
-
2104
+ <ng-content select="[uiFieldTrailing]" />
936
2105
  @if (unit()) {
937
2106
  <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
938
2107
  }
@@ -955,11 +2124,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
955
2124
  template: `
956
2125
  <div class="group flex flex-col gap-1.5">
957
2126
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
958
- <label [class]="labelClasses()">{{ label() }}</label>
2127
+ <label [class]="labelClasses()"
2128
+ >{{ label() }}
2129
+ @if (optional()) {
2130
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
2131
+ }
2132
+ </label>
959
2133
 
960
- <div class="relative flex items-center">
2134
+ <div [class]="rowClasses()">
2135
+ <ng-content select="[uiFieldLeading]" />
961
2136
  <ng-content />
962
-
2137
+ <ng-content select="[uiFieldTrailing]" />
963
2138
  @if (unit()) {
964
2139
  <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
965
2140
  }
@@ -975,77 +2150,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
975
2150
  </div>
976
2151
  `,
977
2152
  }]
978
- }], 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 }] }] } });
2153
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], optional: [{ type: i0.Input, args: [{ isSignal: true, alias: "optional", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }], leading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldLeading), { isSignal: true }] }], trailing: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldTrailing), { isSignal: true }] }] } });
979
2154
 
980
- /** Available control sizes. `md` is the 44/40px touch target. */
981
- const CONTROL_SIZES = ['sm', 'md'];
982
- const CONTROL_BASE_CLASSES = 'w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40';
983
- const CONTROL_SIZE_CLASSES = {
984
- sm: 'h-9',
985
- md: 'min-h-(--row-min)',
986
- };
987
- /**
988
- * Styled native text input. A directive (not a wrapper component) so the host
989
- * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
990
- * and every native attribute keep working without any plumbing.
991
- *
992
- * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
993
- * it can show the message without any binding here.
994
- *
995
- * @example
996
- * <input uiInput type="email" placeholder="you@example.com" />
997
- */
998
- class UiInput {
999
- size = input('md', /* @ts-ignore */
1000
- ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1001
- errors = input([], /* @ts-ignore */
1002
- ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
1003
- touched = input(false, /* @ts-ignore */
1004
- ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
1005
- classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
1006
- ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1007
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1008
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
1009
- }
1010
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
1011
- type: Directive,
1012
- args: [{
1013
- selector: 'input[uiInput]',
1014
- host: {
1015
- '[class]': 'classes()',
1016
- },
1017
- providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }],
1018
- }]
1019
- }], 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 }] }] } });
2155
+ const CHIP_CLASSES = 'flex-none flex h-11 items-center rounded-pill border-0 bg-transparent p-0 font-[inherit] cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) pointer-fine:h-8 pointer-fine:focus-visible:outline-offset-2';
2156
+ const PILL_CLASSES = 'flex h-[34px] items-center gap-1.5 rounded-pill px-3.5 text-label font-medium whitespace-nowrap pointer-fine:h-8 pointer-fine:px-3 pointer-fine:group-hover/chip:shadow-[inset_0_0_0_1px_var(--muted-foreground)]';
2157
+ const SELECTED_PILL_CLASSES = 'bg-(--primary) text-(--primary-foreground)';
2158
+ const UNSELECTED_PILL_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
1020
2159
  /**
1021
- * Styled native textarea. Lives beside the input because it shares every one of its
1022
- * classes; it takes no size, since a textarea grows from a floor instead of sitting
1023
- * at a fixed height.
1024
- *
1025
- * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
1026
- * it can show the message without any binding here.
2160
+ * A row of single-choice filter pills, each with an optional count, that narrows a list in place:
2161
+ * an asset class, a status. On touch it scrolls on one line, bleeding to the screen edges; with a
2162
+ * fine pointer it wraps.
1027
2163
  *
1028
2164
  * @example
1029
- * <textarea uiTextarea maxlength="280"></textarea>
2165
+ * <ui-filter-chips ariaLabel="Filter by class" [options]="classes()" [(value)]="selectedClass" />
1030
2166
  */
1031
- class UiTextarea {
1032
- errors = input([], /* @ts-ignore */
1033
- ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
1034
- touched = input(false, /* @ts-ignore */
1035
- ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
1036
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1037
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40 min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
2167
+ class UiFilterChips {
2168
+ options = input.required(/* @ts-ignore */
2169
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2170
+ value = model.required(/* @ts-ignore */
2171
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2172
+ ariaLabel = input.required(/* @ts-ignore */
2173
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2174
+ chipClasses = CHIP_CLASSES;
2175
+ pillClasses(optionValue) {
2176
+ return `${PILL_CLASSES} ${optionValue === this.value() ? SELECTED_PILL_CLASSES : UNSELECTED_PILL_CLASSES}`;
2177
+ }
2178
+ countClasses(optionValue) {
2179
+ return optionValue === this.value() ? 'text-(--primary-foreground)' : 'text-(--subtle-foreground)';
2180
+ }
2181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
2182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFilterChips, isStandalone: true, selector: "ui-filter-chips", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "ariaLabel()" }, classAttribute: "flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0" }, ngImport: i0, template: `
2183
+ @for (option of options(); track option.value) {
2184
+ <button
2185
+ class="group/chip"
2186
+ type="button"
2187
+ [attr.aria-pressed]="option.value === value()"
2188
+ [class]="chipClasses"
2189
+ (click)="value.set(option.value)"
2190
+ >
2191
+ <span data-chip [class]="pillClasses(option.value)"
2192
+ >{{ option.label }}
2193
+ @if (option.count !== undefined && option.count !== null) {
2194
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2195
+ option.count
2196
+ }}</span>
2197
+ }
2198
+ </span>
2199
+ </button>
2200
+ }
2201
+ `, isInline: true });
1038
2202
  }
1039
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
1040
- type: Directive,
2203
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, decorators: [{
2204
+ type: Component,
1041
2205
  args: [{
1042
- selector: 'textarea[uiTextarea]',
2206
+ selector: 'ui-filter-chips',
2207
+ template: `
2208
+ @for (option of options(); track option.value) {
2209
+ <button
2210
+ class="group/chip"
2211
+ type="button"
2212
+ [attr.aria-pressed]="option.value === value()"
2213
+ [class]="chipClasses"
2214
+ (click)="value.set(option.value)"
2215
+ >
2216
+ <span data-chip [class]="pillClasses(option.value)"
2217
+ >{{ option.label }}
2218
+ @if (option.count !== undefined && option.count !== null) {
2219
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
2220
+ option.count
2221
+ }}</span>
2222
+ }
2223
+ </span>
2224
+ </button>
2225
+ }
2226
+ `,
1043
2227
  host: {
1044
- class: `${CONTROL_BASE_CLASSES} min-h-24 py-2`,
2228
+ role: 'group',
2229
+ class: 'flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0',
2230
+ '[attr.aria-label]': 'ariaLabel()',
1045
2231
  },
1046
- providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],
1047
2232
  }]
1048
- }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
2233
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }] } });
1049
2234
 
1050
2235
  const axisTicks = (points, maxTicks = 5) => {
1051
2236
  if (points.length <= maxTicks) {
@@ -1059,6 +2244,13 @@ const axisTicks = (points, maxTicks = 5) => {
1059
2244
  }
1060
2245
  return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
1061
2246
  };
2247
+ const isCoreTick = (index, count) => {
2248
+ if (count <= 3) {
2249
+ return true;
2250
+ }
2251
+ const middle = Math.floor((count - 1) / 2);
2252
+ return index === 0 || index === count - 1 || index === middle;
2253
+ };
1062
2254
 
1063
2255
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
1064
2256
  const nearestPointIndex = (points, x) => {
@@ -1090,6 +2282,36 @@ const navigateIndex = (key, current, last) => {
1090
2282
  }
1091
2283
  };
1092
2284
 
2285
+ const START_LABEL_HEIGHT = 16;
2286
+ const AXIS_ROW = 17;
2287
+ const TOOLTIP_SIZES = ['compact', 'large'];
2288
+ const TOOLTIP_CLASSES = {
2289
+ compact: { box: '-top-3 px-2.5', value: 'text-label', delta: 'text-caption' },
2290
+ large: { box: 'top-0 px-3', value: 'text-body', delta: 'text-label' },
2291
+ };
2292
+ const PLOT_PADDING_RATIO = 16 / 240;
2293
+ const SPARKLINE_PADDING = 4;
2294
+ const START_LABEL_BASELINE_OFFSET = 4;
2295
+ const TOOLTIP_OFFSET = 14;
2296
+ const TOOLTIP_FLIP_RATIO = 0.55;
2297
+ const plotHeightFor = (height, sparkline, axisGap, wide) => sparkline ? height : Math.max(height - resolveGap(axisGap, wide) - AXIS_ROW, 1);
2298
+ const plotPaddingFor = (plotHeight, sparkline) => sparkline ? SPARKLINE_PADDING : plotHeight * PLOT_PADDING_RATIO;
2299
+ const startLabelBaseline = (boxBottom) => boxBottom - START_LABEL_BASELINE_OFFSET;
2300
+ const tooltipPlacement = (ratio) => {
2301
+ const flipped = ratio > TOOLTIP_FLIP_RATIO;
2302
+ return `left: calc(${ratio * 100}% ${flipped ? '-' : '+'} ${TOOLTIP_OFFSET}px); transform: ${flipped ? 'translateX(-100%)' : 'none'}`;
2303
+ };
2304
+ const deltaTone = (delta) => delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
2305
+ const DEFAULT_WIDTH = 640;
2306
+ const DEFAULT_HEIGHT = 240;
2307
+ const identityValue = (value) => `${value}`;
2308
+ const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
2309
+ const identityTime = (time) => new Date(time).toISOString();
2310
+ const parseAxisTicks = (value) => value === 'auto' ? 'auto' : Number(value);
2311
+ const resolveGap = (gap, wide) => (gap === 'auto' ? (wide ? 16 : 12) : gap);
2312
+ const resolveTooltip = (size, wide) => size === 'auto' ? (wide ? 'large' : 'compact') : size;
2313
+ const tooltipClassesFor = (size, wide) => TOOLTIP_CLASSES[resolveTooltip(size, wide)];
2314
+
1093
2315
  const CURVE = curveMonotoneX;
1094
2316
  const buildGeometry = (points, width, height, padding = {}) => {
1095
2317
  if (points.length === 0) {
@@ -1125,8 +2347,8 @@ const buildGeometry = (points, width, height, padding = {}) => {
1125
2347
  };
1126
2348
  };
1127
2349
 
1128
- const LINE_GAP = 3;
1129
- const COLLISION_MARGIN = 3;
2350
+ const LINE_GAP = 6;
2351
+ const COLLISION_MARGIN = 6;
1130
2352
  const EDGE_TOLERANCE = 3;
1131
2353
  const candidateSlots = (startY, box, label) => {
1132
2354
  const aboveTop = startY - LINE_GAP - label.height;
@@ -1239,13 +2461,19 @@ const interpolateLine = (from, to, progress) => {
1239
2461
  return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
1240
2462
  };
1241
2463
 
1242
- const DEFAULT_WIDTH = 640;
1243
- const DEFAULT_HEIGHT = 240;
1244
- const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
1245
- const START_LABEL_HEIGHT = 17;
1246
- const identityValue = (value) => `${value}`;
1247
- const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1248
- const identityTime = (time) => new Date(time).toISOString();
2464
+ const WIDE_QUERY = '(min-width: 64rem)';
2465
+ const wideViewport = () => {
2466
+ const query = typeof matchMedia === 'function' ? matchMedia(WIDE_QUERY) : null;
2467
+ const wide = signal(query?.matches ?? false, /* @ts-ignore */
2468
+ ...(ngDevMode ? [{ debugName: "wide" }] : /* istanbul ignore next */ []));
2469
+ const onChange = (event) => wide.set(event.matches);
2470
+ if (query) {
2471
+ query.onchange = onChange;
2472
+ inject(DestroyRef).onDestroy(() => (query.onchange = null));
2473
+ }
2474
+ return wide;
2475
+ };
2476
+
1249
2477
  /**
1250
2478
  * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1251
2479
  *
@@ -1259,6 +2487,8 @@ class UiLineChart {
1259
2487
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1260
2488
  valueFormat = input(identityValue, /* @ts-ignore */
1261
2489
  ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
2490
+ tooltipFormat = input(/* @ts-ignore */
2491
+ ...(ngDevMode ? [undefined, { debugName: "tooltipFormat" }] : /* istanbul ignore next */ []));
1262
2492
  deltaFormat = input(identityDelta, /* @ts-ignore */
1263
2493
  ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1264
2494
  timeFormat = input(identityTime, /* @ts-ignore */
@@ -1271,10 +2501,19 @@ class UiLineChart {
1271
2501
  ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1272
2502
  valueColumnLabel = input('Value', /* @ts-ignore */
1273
2503
  ...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
2504
+ sparkline = input(false, { ...(ngDevMode ? { debugName: "sparkline" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2505
+ tooltip = input('compact', /* @ts-ignore */
2506
+ ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
2507
+ tooltipDelta = input(true, { ...(ngDevMode ? { debugName: "tooltipDelta" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2508
+ axisGap = input(12, /* @ts-ignore */
2509
+ ...(ngDevMode ? [{ debugName: "axisGap" }] : /* istanbul ignore next */ []));
2510
+ axisTicks = input('auto', { ...(ngDevMode ? { debugName: "axisTicks" } : /* istanbul ignore next */ {}), transform: parseAxisTicks });
1274
2511
  #destroyRef = inject(DestroyRef);
2512
+ #wide = wideViewport();
1275
2513
  #frame = null;
1276
2514
  #hasRenderedOnce = false;
1277
2515
  #previousGeometry = null;
2516
+ isCoreTick = isCoreTick;
1278
2517
  svgRef = viewChild.required('svgRef', /* @ts-ignore */
1279
2518
  ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1280
2519
  activeIndex = signal(null, /* @ts-ignore */
@@ -1287,7 +2526,16 @@ class UiLineChart {
1287
2526
  ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1288
2527
  effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1289
2528
  ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1290
- geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
2529
+ plotHeight = computed(() => plotHeightFor(this.effectiveHeight(), this.sparkline(), this.axisGap(), this.#wide()), /* @ts-ignore */
2530
+ ...(ngDevMode ? [{ debugName: "plotHeight" }] : /* istanbul ignore next */ []));
2531
+ tooltipStyle = (point) => tooltipPlacement(point.x / this.effectiveWidth());
2532
+ tooltipClasses = computed(() => tooltipClassesFor(this.tooltip(), this.#wide()), /* @ts-ignore */
2533
+ ...(ngDevMode ? [{ debugName: "tooltipClasses" }] : /* istanbul ignore next */ []));
2534
+ geometry = computed(() => {
2535
+ const plotHeight = this.plotHeight();
2536
+ const padding = plotPaddingFor(plotHeight, this.sparkline());
2537
+ return buildGeometry(this.points(), this.effectiveWidth(), plotHeight, { x: 0, top: padding, bottom: padding });
2538
+ }, /* @ts-ignore */
1291
2539
  ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1292
2540
  displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1293
2541
  ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
@@ -1307,8 +2555,8 @@ class UiLineChart {
1307
2555
  }
1308
2556
  const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1309
2557
  const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
1310
- const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
1311
- return { label, placement };
2558
+ const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.plotHeight() }, size);
2559
+ return { label, placement: { ...placement, y: startLabelBaseline(placement.y) } };
1312
2560
  }, /* @ts-ignore */
1313
2561
  ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1314
2562
  #renderRef = afterNextRender(() => this.#observeSize());
@@ -1353,10 +2601,8 @@ class UiLineChart {
1353
2601
  this.#destroyRef.onDestroy(() => observer.disconnect());
1354
2602
  }
1355
2603
  #cancelAnimation() {
1356
- if (this.#frame !== null) {
1357
- cancelAnimationFrame(this.#frame);
1358
- this.#frame = null;
1359
- }
2604
+ cancelAnimationFrame(this.#frame ?? 0);
2605
+ this.#frame = null;
1360
2606
  }
1361
2607
  #animate(from, to) {
1362
2608
  if (from.points.length < 2 || to.points.length < 2) {
@@ -1366,37 +2612,20 @@ class UiLineChart {
1366
2612
  const startTime = performance.now();
1367
2613
  const step = (now) => {
1368
2614
  const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
1369
- this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
1370
- if (t < 1) {
1371
- this.#frame = requestAnimationFrame(step);
1372
- }
1373
- else {
1374
- this.#frame = null;
1375
- this.animatedLine.set(null);
1376
- }
2615
+ this.animatedLine.set(t < 1 ? interpolateLine(from, to, easeOutQuint(t)) : null);
2616
+ this.#frame = t < 1 ? requestAnimationFrame(step) : null;
1377
2617
  };
1378
2618
  this.#frame = requestAnimationFrame(step);
1379
2619
  }
1380
- coreTick(index, count) {
1381
- if (count <= 3) {
1382
- return true;
1383
- }
1384
- const middle = Math.floor((count - 1) / 2);
1385
- return index === 0 || index === count - 1 || index === middle;
1386
- }
1387
2620
  deltaFor(point) {
1388
2621
  return point.v - this.points()[0].v;
1389
2622
  }
1390
2623
  deltaClasses(point) {
1391
- const delta = this.deltaFor(point);
1392
- return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1393
- }
1394
- tooltipStyle(point) {
1395
- return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
2624
+ return `${this.tooltipClasses().delta} ${deltaTone(this.deltaFor(point))}`;
1396
2625
  }
1397
2626
  onPointerMove(event) {
1398
2627
  const geometry = this.geometry();
1399
- if (!geometry || geometry.points.length === 0) {
2628
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1400
2629
  return;
1401
2630
  }
1402
2631
  const rect = this.svgRef().nativeElement.getBoundingClientRect();
@@ -1409,7 +2638,7 @@ class UiLineChart {
1409
2638
  }
1410
2639
  onKeydown(event) {
1411
2640
  const geometry = this.geometry();
1412
- if (!geometry || geometry.points.length === 0) {
2641
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1413
2642
  return;
1414
2643
  }
1415
2644
  const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
@@ -1422,15 +2651,15 @@ class UiLineChart {
1422
2651
  this.activeIndex.set(navigation.index);
1423
2652
  }
1424
2653
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1425
- 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 } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
2654
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, tooltipFormat: { classPropertyName: "tooltipFormat", publicName: "tooltipFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
1426
2655
  <div class="relative h-full">
1427
2656
  <svg
1428
2657
  #svgRef
1429
- class="block h-full w-full touch-pan-y"
2658
+ class="block h-full w-full touch-pan-y overflow-visible"
1430
2659
  role="img"
1431
- tabindex="0"
1432
2660
  [attr.aria-label]="label()"
1433
2661
  [attr.height]="effectiveHeight()"
2662
+ [attr.tabindex]="sparkline() ? null : 0"
1434
2663
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1435
2664
  [attr.width]="effectiveWidth()"
1436
2665
  (keydown)="onKeydown($event)"
@@ -1444,119 +2673,124 @@ class UiLineChart {
1444
2673
  fill="none"
1445
2674
  stroke-linecap="round"
1446
2675
  stroke-linejoin="round"
1447
- stroke-width="2"
1448
2676
  [attr.d]="displayLine()"
2677
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1449
2678
  />
1450
2679
 
1451
- <line
1452
- class="stroke-(--border)"
1453
- data-chart-start-line
1454
- stroke-dasharray="4 4"
1455
- stroke-width="1"
1456
- x1="0"
1457
- [attr.x2]="effectiveWidth()"
1458
- [attr.y1]="geometry.start.y"
1459
- [attr.y2]="geometry.start.y"
1460
- />
1461
-
1462
- @if (startLabelInfo(); as info) {
1463
- <text
1464
- class="text-caption fill-(--subtle-foreground)"
1465
- data-chart-start-label
1466
- [attr.text-anchor]="info.placement.anchor"
1467
- [attr.x]="info.placement.x"
1468
- [attr.y]="info.placement.y"
1469
- >
1470
- {{ info.label }}
1471
- </text>
1472
- }
1473
-
1474
- <circle
1475
- class="fill-(--foreground) stroke-(--card)"
1476
- data-chart-end
1477
- r="4"
1478
- stroke-width="2"
1479
- [attr.cx]="geometry.end.x"
1480
- [attr.cy]="geometry.end.y"
1481
- />
1482
-
1483
- @for (
1484
- tick of ticks();
1485
- track tick.t;
1486
- let index = $index;
1487
- let count = $count;
1488
- let first = $first;
1489
- let last = $last
1490
- ) {
1491
- <text
1492
- class="text-caption fill-(--subtle-foreground)"
1493
- data-chart-axis-tick
1494
- [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1495
- [attr.x]="tick.x"
1496
- [attr.y]="effectiveHeight() - 4"
1497
- [class.max-sm:hidden]="!coreTick(index, count)"
1498
- >
1499
- {{ axisFormat()(tick.t) }}
1500
- </text>
1501
- }
1502
-
1503
- @if (active(); as point) {
2680
+ @if (!sparkline()) {
1504
2681
  <line
1505
2682
  class="stroke-(--border)"
1506
- data-chart-crosshair
2683
+ data-chart-start-line
2684
+ stroke-dasharray="3 4"
1507
2685
  stroke-width="1"
1508
- y1="0"
1509
- [attr.x1]="point.x"
1510
- [attr.x2]="point.x"
1511
- [attr.y2]="effectiveHeight()"
2686
+ x1="0"
2687
+ [attr.x2]="effectiveWidth()"
2688
+ [attr.y1]="geometry.start.y"
2689
+ [attr.y2]="geometry.start.y"
1512
2690
  />
2691
+
2692
+ @if (startLabelInfo(); as info) {
2693
+ <text
2694
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2695
+ data-chart-start-label
2696
+ [attr.text-anchor]="info.placement.anchor"
2697
+ [attr.x]="info.placement.x"
2698
+ [attr.y]="info.placement.y"
2699
+ >
2700
+ {{ info.label }}
2701
+ </text>
2702
+ }
2703
+
1513
2704
  <circle
1514
- class="fill-(--foreground) stroke-(--card)"
1515
- data-chart-active
2705
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2706
+ data-chart-end
1516
2707
  r="4"
1517
- stroke-width="2"
1518
- [attr.cx]="point.x"
1519
- [attr.cy]="point.y"
2708
+ stroke-width="6"
2709
+ [attr.cx]="geometry.end.x"
2710
+ [attr.cy]="geometry.end.y"
1520
2711
  />
2712
+
2713
+ @for (
2714
+ tick of ticks();
2715
+ track tick.t;
2716
+ let index = $index;
2717
+ let count = $count;
2718
+ let first = $first;
2719
+ let last = $last
2720
+ ) {
2721
+ <text
2722
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2723
+ data-chart-axis-tick
2724
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
2725
+ [attr.x]="tick.x"
2726
+ [attr.y]="effectiveHeight() - 4"
2727
+ [class.hidden]="axisTicks() === 3 && !isCoreTick(index, count)"
2728
+ [class.max-sm:hidden]="axisTicks() === 'auto' && !isCoreTick(index, count)"
2729
+ >
2730
+ {{ axisFormat()(tick.t) }}
2731
+ </text>
2732
+ }
2733
+
2734
+ @if (active(); as point) {
2735
+ <line
2736
+ class="stroke-(--border)"
2737
+ data-chart-crosshair
2738
+ stroke-width="1"
2739
+ y1="0"
2740
+ [attr.x1]="point.x"
2741
+ [attr.x2]="point.x"
2742
+ [attr.y2]="plotHeight()"
2743
+ />
2744
+ <circle
2745
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2746
+ data-chart-active
2747
+ r="5"
2748
+ stroke-width="6"
2749
+ [attr.cx]="point.x"
2750
+ [attr.cy]="point.y"
2751
+ />
2752
+ }
1521
2753
  }
1522
2754
  }
1523
2755
  </svg>
1524
2756
 
1525
2757
  @if (active(); as point) {
1526
2758
  <div
1527
- class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
2759
+ class="rounded-container pointer-events-none absolute bg-(--elevated) py-2 whitespace-nowrap [font-variant-numeric:var(--numeric)] shadow-[0_4px_16px_rgb(0_0_0/0.12),inset_0_0_0_1px_var(--border)]"
1528
2760
  data-testid="chart-tooltip"
1529
2761
  role="status"
2762
+ [class]="tooltipClasses().box"
1530
2763
  [style]="tooltipStyle(point)"
1531
2764
  >
1532
- <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1533
- <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1534
- <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
2765
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2766
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2767
+ </p>
2768
+ @if (tooltipDelta()) {
2769
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2770
+ }
2771
+ <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
1535
2772
  </div>
1536
2773
  }
1537
-
1538
2774
  @if (points().length > 0) {
1539
- <table class="sr-only">
1540
- <caption>
1541
- {{
1542
- label()
1543
- }}
1544
- </caption>
1545
- <thead>
1546
- <tr>
1547
- <th scope="col">{{ timeColumnLabel() }}</th>
1548
- <th scope="col">{{ valueColumnLabel() }}</th>
1549
- </tr>
1550
- </thead>
1551
- <tbody>
1552
- @for (point of points(); track point.t) {
2775
+ <div class="sr-only">
2776
+ <table>
2777
+ <caption [textContent]="label()"></caption>
2778
+ <thead>
1553
2779
  <tr>
1554
- <td>{{ timeFormat()(point.t) }}</td>
1555
- <td>{{ valueFormat()(point.v) }}</td>
2780
+ <th scope="col">{{ timeColumnLabel() }}</th>
2781
+ <th scope="col">{{ valueColumnLabel() }}</th>
1556
2782
  </tr>
1557
- }
1558
- </tbody>
1559
- </table>
2783
+ </thead>
2784
+ <tbody>
2785
+ @for (point of points(); track point.t) {
2786
+ <tr>
2787
+ <td>{{ timeFormat()(point.t) }}</td>
2788
+ <td>{{ valueFormat()(point.v) }}</td>
2789
+ </tr>
2790
+ }
2791
+ </tbody>
2792
+ </table>
2793
+ </div>
1560
2794
  }
1561
2795
  </div>
1562
2796
  `, isInline: true });
@@ -1569,11 +2803,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1569
2803
  <div class="relative h-full">
1570
2804
  <svg
1571
2805
  #svgRef
1572
- class="block h-full w-full touch-pan-y"
2806
+ class="block h-full w-full touch-pan-y overflow-visible"
1573
2807
  role="img"
1574
- tabindex="0"
1575
2808
  [attr.aria-label]="label()"
1576
2809
  [attr.height]="effectiveHeight()"
2810
+ [attr.tabindex]="sparkline() ? null : 0"
1577
2811
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1578
2812
  [attr.width]="effectiveWidth()"
1579
2813
  (keydown)="onKeydown($event)"
@@ -1587,119 +2821,124 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1587
2821
  fill="none"
1588
2822
  stroke-linecap="round"
1589
2823
  stroke-linejoin="round"
1590
- stroke-width="2"
1591
2824
  [attr.d]="displayLine()"
2825
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1592
2826
  />
1593
2827
 
1594
- <line
1595
- class="stroke-(--border)"
1596
- data-chart-start-line
1597
- stroke-dasharray="4 4"
1598
- stroke-width="1"
1599
- x1="0"
1600
- [attr.x2]="effectiveWidth()"
1601
- [attr.y1]="geometry.start.y"
1602
- [attr.y2]="geometry.start.y"
1603
- />
1604
-
1605
- @if (startLabelInfo(); as info) {
1606
- <text
1607
- class="text-caption fill-(--subtle-foreground)"
1608
- data-chart-start-label
1609
- [attr.text-anchor]="info.placement.anchor"
1610
- [attr.x]="info.placement.x"
1611
- [attr.y]="info.placement.y"
1612
- >
1613
- {{ info.label }}
1614
- </text>
1615
- }
1616
-
1617
- <circle
1618
- class="fill-(--foreground) stroke-(--card)"
1619
- data-chart-end
1620
- r="4"
1621
- stroke-width="2"
1622
- [attr.cx]="geometry.end.x"
1623
- [attr.cy]="geometry.end.y"
1624
- />
1625
-
1626
- @for (
1627
- tick of ticks();
1628
- track tick.t;
1629
- let index = $index;
1630
- let count = $count;
1631
- let first = $first;
1632
- let last = $last
1633
- ) {
1634
- <text
1635
- class="text-caption fill-(--subtle-foreground)"
1636
- data-chart-axis-tick
1637
- [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1638
- [attr.x]="tick.x"
1639
- [attr.y]="effectiveHeight() - 4"
1640
- [class.max-sm:hidden]="!coreTick(index, count)"
1641
- >
1642
- {{ axisFormat()(tick.t) }}
1643
- </text>
1644
- }
1645
-
1646
- @if (active(); as point) {
2828
+ @if (!sparkline()) {
1647
2829
  <line
1648
2830
  class="stroke-(--border)"
1649
- data-chart-crosshair
2831
+ data-chart-start-line
2832
+ stroke-dasharray="3 4"
1650
2833
  stroke-width="1"
1651
- y1="0"
1652
- [attr.x1]="point.x"
1653
- [attr.x2]="point.x"
1654
- [attr.y2]="effectiveHeight()"
2834
+ x1="0"
2835
+ [attr.x2]="effectiveWidth()"
2836
+ [attr.y1]="geometry.start.y"
2837
+ [attr.y2]="geometry.start.y"
1655
2838
  />
2839
+
2840
+ @if (startLabelInfo(); as info) {
2841
+ <text
2842
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2843
+ data-chart-start-label
2844
+ [attr.text-anchor]="info.placement.anchor"
2845
+ [attr.x]="info.placement.x"
2846
+ [attr.y]="info.placement.y"
2847
+ >
2848
+ {{ info.label }}
2849
+ </text>
2850
+ }
2851
+
1656
2852
  <circle
1657
- class="fill-(--foreground) stroke-(--card)"
1658
- data-chart-active
2853
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2854
+ data-chart-end
1659
2855
  r="4"
1660
- stroke-width="2"
1661
- [attr.cx]="point.x"
1662
- [attr.cy]="point.y"
2856
+ stroke-width="6"
2857
+ [attr.cx]="geometry.end.x"
2858
+ [attr.cy]="geometry.end.y"
1663
2859
  />
2860
+
2861
+ @for (
2862
+ tick of ticks();
2863
+ track tick.t;
2864
+ let index = $index;
2865
+ let count = $count;
2866
+ let first = $first;
2867
+ let last = $last
2868
+ ) {
2869
+ <text
2870
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2871
+ data-chart-axis-tick
2872
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
2873
+ [attr.x]="tick.x"
2874
+ [attr.y]="effectiveHeight() - 4"
2875
+ [class.hidden]="axisTicks() === 3 && !isCoreTick(index, count)"
2876
+ [class.max-sm:hidden]="axisTicks() === 'auto' && !isCoreTick(index, count)"
2877
+ >
2878
+ {{ axisFormat()(tick.t) }}
2879
+ </text>
2880
+ }
2881
+
2882
+ @if (active(); as point) {
2883
+ <line
2884
+ class="stroke-(--border)"
2885
+ data-chart-crosshair
2886
+ stroke-width="1"
2887
+ y1="0"
2888
+ [attr.x1]="point.x"
2889
+ [attr.x2]="point.x"
2890
+ [attr.y2]="plotHeight()"
2891
+ />
2892
+ <circle
2893
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2894
+ data-chart-active
2895
+ r="5"
2896
+ stroke-width="6"
2897
+ [attr.cx]="point.x"
2898
+ [attr.cy]="point.y"
2899
+ />
2900
+ }
1664
2901
  }
1665
2902
  }
1666
2903
  </svg>
1667
2904
 
1668
2905
  @if (active(); as point) {
1669
2906
  <div
1670
- class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
2907
+ class="rounded-container pointer-events-none absolute bg-(--elevated) py-2 whitespace-nowrap [font-variant-numeric:var(--numeric)] shadow-[0_4px_16px_rgb(0_0_0/0.12),inset_0_0_0_1px_var(--border)]"
1671
2908
  data-testid="chart-tooltip"
1672
2909
  role="status"
2910
+ [class]="tooltipClasses().box"
1673
2911
  [style]="tooltipStyle(point)"
1674
2912
  >
1675
- <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1676
- <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1677
- <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
2913
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">
2914
+ {{ tooltipFormat()?.(point) ?? valueFormat()(point.v) }}
2915
+ </p>
2916
+ @if (tooltipDelta()) {
2917
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2918
+ }
2919
+ <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
1678
2920
  </div>
1679
2921
  }
1680
-
1681
2922
  @if (points().length > 0) {
1682
- <table class="sr-only">
1683
- <caption>
1684
- {{
1685
- label()
1686
- }}
1687
- </caption>
1688
- <thead>
1689
- <tr>
1690
- <th scope="col">{{ timeColumnLabel() }}</th>
1691
- <th scope="col">{{ valueColumnLabel() }}</th>
1692
- </tr>
1693
- </thead>
1694
- <tbody>
1695
- @for (point of points(); track point.t) {
2923
+ <div class="sr-only">
2924
+ <table>
2925
+ <caption [textContent]="label()"></caption>
2926
+ <thead>
1696
2927
  <tr>
1697
- <td>{{ timeFormat()(point.t) }}</td>
1698
- <td>{{ valueFormat()(point.v) }}</td>
2928
+ <th scope="col">{{ timeColumnLabel() }}</th>
2929
+ <th scope="col">{{ valueColumnLabel() }}</th>
1699
2930
  </tr>
1700
- }
1701
- </tbody>
1702
- </table>
2931
+ </thead>
2932
+ <tbody>
2933
+ @for (point of points(); track point.t) {
2934
+ <tr>
2935
+ <td>{{ timeFormat()(point.t) }}</td>
2936
+ <td>{{ valueFormat()(point.v) }}</td>
2937
+ </tr>
2938
+ }
2939
+ </tbody>
2940
+ </table>
2941
+ </div>
1703
2942
  }
1704
2943
  </div>
1705
2944
  `,
@@ -1707,11 +2946,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1707
2946
  class: 'block h-full',
1708
2947
  },
1709
2948
  }]
1710
- }], 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 }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
2949
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], tooltipFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
1711
2950
 
1712
- const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
1713
- const ITEM_CLASSES = 'flex w-full items-center gap-2 min-h-(--row-min) px-3 rounded-control text-label hover:bg-(--soft) focus-visible:bg-(--soft) outline-none';
1714
- const nextId = (() => {
2951
+ const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16),inset_0_0_0_1px_var(--border)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
2952
+ const SHEET_CLASSES = 'max-lg:fixed max-lg:inset-x-4 max-lg:top-auto max-lg:w-auto max-lg:bottom-[calc(76px+env(safe-area-inset-bottom))] max-lg:min-w-0 max-lg:p-1.5 max-lg:bg-(--card) max-lg:shadow-[0_12px_32px_rgb(0_0_0/0.2),inset_0_0_0_1px_var(--border)] max-lg:backdrop:bg-[rgb(0_0_0/0.36)] max-lg:transition-[opacity,translate,overlay,display] max-lg:scale-100 max-lg:translate-y-4 max-lg:open:translate-y-0 max-lg:starting:open:scale-100 max-lg:starting:open:translate-y-4';
2953
+ const SHEET_ITEM_CLASSES = 'flex w-full items-center gap-3 lg:pointer-fine:gap-2.5 min-h-12 lg:min-h-11 lg:pointer-fine:min-h-9 px-3 lg:pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-6px)] lg:rounded-[calc(var(--radius-container)-4px)] text-body lg:pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-5 [:where(&>svg)]:flex-none lg:[:where(&>svg)]:size-[18px] lg:pointer-fine:[:where(&>svg)]:size-4';
2954
+ const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-[18px] [:where(&>svg)]:flex-none pointer-fine:[:where(&>svg)]:size-4';
2955
+ const nextId$1 = (() => {
1715
2956
  let count = 0;
1716
2957
  return () => `ui-menu-${++count}`;
1717
2958
  })();
@@ -1725,7 +2966,7 @@ class UiMenuItem {
1725
2966
  destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1726
2967
  #host = inject(ElementRef).nativeElement;
1727
2968
  #menu = inject(UiMenu);
1728
- classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
2969
+ classes = computed(() => `${this.#menu.sheet() ? SHEET_ITEM_CLASSES : ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
1729
2970
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1730
2971
  focus() {
1731
2972
  this.#host.focus();
@@ -1752,7 +2993,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1752
2993
  }]
1753
2994
  }], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
1754
2995
  /**
1755
- * Secondary actions menu, opened from its trigger through the native Popover API.
2996
+ * Secondary actions menu, opened from its trigger through the native Popover API. With `sheet`, below
2997
+ * 64rem it opens as a bottom action sheet above the tab bar instead of beside its trigger, and
2998
+ * `heading` names what the actions apply to. `width` fixes the outer popover width in pixels (the sheet
2999
+ * always spans the screen below 64rem); without it the menu fits its content.
1756
3000
  *
1757
3001
  * @example
1758
3002
  * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
@@ -1764,10 +3008,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1764
3008
  class UiMenu {
1765
3009
  label = input.required(/* @ts-ignore */
1766
3010
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1767
- menuId = nextId();
3011
+ sheet = input(false, { ...(ngDevMode ? { debugName: "sheet" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3012
+ heading = input(/* @ts-ignore */
3013
+ ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
3014
+ width = input(undefined, { ...(ngDevMode ? { debugName: "width" } : /* istanbul ignore next */ {}), transform: numberAttribute });
3015
+ menuId = nextId$1();
1768
3016
  isOpen = signal(false, /* @ts-ignore */
1769
3017
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1770
- classes = MENU_CLASSES;
3018
+ hasWidth = computed(() => Number.isFinite(this.width()), /* @ts-ignore */
3019
+ ...(ngDevMode ? [{ debugName: "hasWidth" }] : /* istanbul ignore next */ []));
3020
+ widthVar = computed(() => (this.hasWidth() ? `${this.width()}px` : null), /* @ts-ignore */
3021
+ ...(ngDevMode ? [{ debugName: "widthVar" }] : /* istanbul ignore next */ []));
3022
+ classes = computed(() => {
3023
+ const width = !this.hasWidth() ? '' : this.sheet() ? ' lg:w-(--ui-menu-width)' : ' w-(--ui-menu-width)';
3024
+ return `${MENU_CLASSES}${this.sheet() ? ` ${SHEET_CLASSES}` : ''}${width}`;
3025
+ }, /* @ts-ignore */
3026
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1771
3027
  items = contentChildren(UiMenuItem, /* @ts-ignore */
1772
3028
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1773
3029
  #host = inject(ElementRef).nativeElement;
@@ -1778,7 +3034,14 @@ class UiMenu {
1778
3034
  if (typeof this.#host.showPopover === 'function') {
1779
3035
  this.#host.showPopover();
1780
3036
  }
1781
- this.#position(trigger);
3037
+ if (this.#isSheet()) {
3038
+ this.#host.style.position = '';
3039
+ this.#host.style.top = '';
3040
+ this.#host.style.left = '';
3041
+ }
3042
+ else {
3043
+ this.#position(trigger);
3044
+ }
1782
3045
  this.items()[0]?.focus();
1783
3046
  }
1784
3047
  close() {
@@ -1828,6 +3091,9 @@ class UiMenu {
1828
3091
  event.preventDefault();
1829
3092
  items[nextIndex]?.focus();
1830
3093
  }
3094
+ #isSheet() {
3095
+ return this.sheet() && typeof window.matchMedia === 'function' && !window.matchMedia('(min-width: 64rem)').matches;
3096
+ }
1831
3097
  #position(trigger) {
1832
3098
  const margin = 8;
1833
3099
  const triggerRect = trigger.getBoundingClientRect();
@@ -1841,24 +3107,39 @@ class UiMenu {
1841
3107
  this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
1842
3108
  }
1843
3109
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1844
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: UiMenu, isStandalone: true, selector: "ui-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" }, listeners: { "toggle": "onToggle($event)", "keydown": "onKeydown($event)" }, properties: { "id": "menuId", "class": "classes", "attr.aria-label": "label()" } }, queries: [{ propertyName: "items", predicate: UiMenuItem, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true });
3110
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiMenu, isStandalone: true, selector: "ui-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sheet: { classPropertyName: "sheet", publicName: "sheet", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" }, listeners: { "toggle": "onToggle($event)", "keydown": "onKeydown($event)" }, properties: { "id": "menuId", "style.--ui-menu-width": "widthVar()", "class": "classes()", "attr.aria-label": "label()" } }, queries: [{ propertyName: "items", predicate: UiMenuItem, isSignal: true }], ngImport: i0, template: `
3111
+ @if (sheet() && heading()) {
3112
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3113
+ {{ heading() }}
3114
+ </div>
3115
+ }
3116
+ <ng-content />
3117
+ `, isInline: true });
1845
3118
  }
1846
3119
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
1847
3120
  type: Component,
1848
3121
  args: [{
1849
3122
  selector: 'ui-menu',
1850
- template: '<ng-content />',
3123
+ template: `
3124
+ @if (sheet() && heading()) {
3125
+ <div aria-hidden="true" class="text-label px-3 pt-2 pb-1.5 leading-[17px] text-(--muted-foreground) lg:hidden">
3126
+ {{ heading() }}
3127
+ </div>
3128
+ }
3129
+ <ng-content />
3130
+ `,
1851
3131
  host: {
1852
3132
  role: 'menu',
1853
3133
  popover: 'auto',
1854
3134
  '[id]': 'menuId',
1855
- '[class]': 'classes',
3135
+ '[style.--ui-menu-width]': 'widthVar()',
3136
+ '[class]': 'classes()',
1856
3137
  '[attr.aria-label]': 'label()',
1857
3138
  '(toggle)': 'onToggle($event)',
1858
3139
  '(keydown)': 'onKeydown($event)',
1859
3140
  },
1860
3141
  }]
1861
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
3142
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sheet: [{ type: i0.Input, args: [{ isSignal: true, alias: "sheet", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
1862
3143
  /**
1863
3144
  * Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
1864
3145
  * `aria-controls` on it.
@@ -1895,6 +3176,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1895
3176
  }]
1896
3177
  }], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
1897
3178
 
3179
+ /** Track thicknesses. `MeterSize` is derived from this tuple. */
3180
+ const METER_SIZES = ['md', 'sm'];
3181
+ const TRACK_CLASSES = {
3182
+ md: 'h-1.5',
3183
+ sm: 'h-1',
3184
+ };
1898
3185
  /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
1899
3186
  const METER_TONES = [1, 2, 3, 4, 5, 6];
1900
3187
  const TONE_CLASSES = {
@@ -1921,20 +3208,25 @@ class UiMeter {
1921
3208
  ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
1922
3209
  tone = input(1, /* @ts-ignore */
1923
3210
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3211
+ size = input('md', /* @ts-ignore */
3212
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3213
+ trackClasses = computed(() => TRACK_CLASSES[this.size()], /* @ts-ignore */
3214
+ ...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
1924
3215
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
1925
3216
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
1926
- fillClasses = computed(() => `h-full rounded-control ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
3217
+ fillClasses = computed(() => `h-full rounded-pill ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
1927
3218
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
1928
3219
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1929
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
3220
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1930
3221
  <div
1931
- class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
3222
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
1932
3223
  role="meter"
1933
3224
  [attr.aria-label]="label()"
1934
3225
  [attr.aria-valuemax]="1"
1935
3226
  [attr.aria-valuemin]="0"
1936
3227
  [attr.aria-valuenow]="value()"
1937
3228
  [attr.aria-valuetext]="valueText()"
3229
+ [class]="trackClasses()"
1938
3230
  >
1939
3231
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
1940
3232
  </div>
@@ -1946,22 +3238,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1946
3238
  selector: 'ui-meter',
1947
3239
  template: `
1948
3240
  <div
1949
- class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
3241
+ class="rounded-pill w-full overflow-hidden bg-(--muted)"
1950
3242
  role="meter"
1951
3243
  [attr.aria-label]="label()"
1952
3244
  [attr.aria-valuemax]="1"
1953
3245
  [attr.aria-valuemin]="0"
1954
3246
  [attr.aria-valuenow]="value()"
1955
3247
  [attr.aria-valuetext]="valueText()"
3248
+ [class]="trackClasses()"
1956
3249
  >
1957
3250
  <div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
1958
3251
  </div>
1959
3252
  `,
1960
3253
  }]
1961
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueText", required: true }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
3254
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueText", required: true }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1962
3255
 
1963
- const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label font-medium text-(--muted-foreground) transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3256
+ const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label leading-normal font-medium transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1964
3257
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
3258
+ const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
1965
3259
  /**
1966
3260
  * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
1967
3261
  *
@@ -1973,7 +3267,7 @@ const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
1973
3267
  */
1974
3268
  class UiNavItem {
1975
3269
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1976
- classes = computed(() => `${BASE_CLASSES$1}${this.active() ? ` ${ACTIVE_CLASSES}` : ''}`, /* @ts-ignore */
3270
+ classes = computed(() => `${BASE_CLASSES$1} ${this.active() ? ACTIVE_CLASSES : IDLE_CLASSES}`, /* @ts-ignore */
1977
3271
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1978
3272
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1979
3273
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiNavItem, isStandalone: true, selector: "a[ui-nav-item]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: '<ng-content select="[navIcon]" /><ng-content />', isInline: true });
@@ -1990,7 +3284,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1990
3284
  }]
1991
3285
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1992
3286
 
1993
- const BASE_CLASSES = 'flex w-full items-center gap-2.5 min-h-14 px-2.5 py-1.5 rounded-control text-left select-none touch-manipulation transition-colors duration-(--duration-press) hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3287
+ const ROW_SIZES = ['md', 'lg', 'xl', 'card', 'dense'];
3288
+ const ROW_PADDINGS = ['md', 'sm', 'none'];
3289
+ const ROW_GAPS = ['default', 'sm'];
3290
+ const SIZE_CLASSES$1 = {
3291
+ md: 'min-h-14',
3292
+ lg: 'min-h-15',
3293
+ xl: 'min-h-18 pointer-fine:min-h-17',
3294
+ card: 'min-h-17',
3295
+ dense: 'min-h-14 lg:min-h-12',
3296
+ };
3297
+ const SIZE_GAPS = { md: 'gap-2.5', lg: 'gap-2.5', xl: 'gap-3', card: 'gap-2', dense: 'gap-3' };
3298
+ const PADDING_CLASSES = { md: 'px-2.5', sm: 'px-2', none: 'px-0' };
3299
+ const BASE_CLASSES = 'flex w-full items-center py-1.5 rounded-control text-left select-none touch-manipulation transition-colors duration-(--duration-press) hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1994
3300
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
1995
3301
  /**
1996
3302
  * Clickable row of a list: a holding, an envelope, an account.
@@ -2002,10 +3308,16 @@ const SELECTED_CLASSES$1 = 'bg-(--soft)';
2002
3308
  */
2003
3309
  class UiRow {
2004
3310
  selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2005
- classes = computed(() => `${BASE_CLASSES}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
3311
+ size = input('md', /* @ts-ignore */
3312
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3313
+ padding = input('md', /* @ts-ignore */
3314
+ ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
3315
+ gap = input('default', /* @ts-ignore */
3316
+ ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
3317
+ classes = computed(() => `${BASE_CLASSES} ${SIZE_CLASSES$1[this.size()]} ${this.gap() === 'sm' ? 'gap-2' : SIZE_GAPS[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
2006
3318
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2007
3319
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
2008
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
3320
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
2009
3321
  }
2010
3322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
2011
3323
  type: Component,
@@ -2017,9 +3329,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2017
3329
  '[attr.aria-current]': "selected() ? 'true' : null",
2018
3330
  },
2019
3331
  }]
2020
- }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }] } });
3332
+ }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }] } });
3333
+ /**
3334
+ * Lays out a `ui-row` with a trailing action beside it, such as a "more" icon button: the row takes
3335
+ * the room the action leaves and the action keeps its own size.
3336
+ *
3337
+ * @example
3338
+ * <div uiRowItem>
3339
+ * <a ui-row size="xl" padding="sm" routerLink="/accounts/1">...</a>
3340
+ * <button ui-button variant="quiet" size="icon-sm" aria-label="Account actions">...</button>
3341
+ * </div>
3342
+ */
3343
+ class UiRowItem {
3344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3345
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiRowItem, isStandalone: true, selector: "[uiRowItem]", host: { classAttribute: "flex items-center gap-1 [&>[ui-row]]:w-auto [&>[ui-row]]:min-w-0 [&>[ui-row]]:flex-1 [&>:not([ui-row])]:flex-none" }, ngImport: i0 });
3346
+ }
3347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowItem, decorators: [{
3348
+ type: Directive,
3349
+ args: [{
3350
+ selector: '[uiRowItem]',
3351
+ host: {
3352
+ class: 'flex items-center gap-1 [&>[ui-row]]:w-auto [&>[ui-row]]:min-w-0 [&>[ui-row]]:flex-1 [&>:not([ui-row])]:flex-none',
3353
+ },
3354
+ }]
3355
+ }] });
3356
+ /**
3357
+ * Non-interactive row of a list, separated from the next by a hairline: a passkey with its delete
3358
+ * button, a device, a setting. It is a 72px row (68px from 64rem) with a 12px gap; put it on an
3359
+ * `li` or a `div`. Use `ui-row` instead when the whole row is a link or an action.
3360
+ *
3361
+ * @example
3362
+ * <ul>
3363
+ * <li uiListRow>
3364
+ * <span class="flex-1">MacBook Air</span>
3365
+ * <button ui-button variant="quiet-destructive" size="icon-sm" aria-label="Delete MacBook Air">...</button>
3366
+ * </li>
3367
+ * </ul>
3368
+ */
3369
+ class UiListRow {
3370
+ ruled = input(true, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3371
+ classes = computed(() => `flex min-h-18 items-center gap-3 py-1.5 lg:min-h-17${this.ruled() ? ' shadow-[inset_0_-1px_0_var(--hairline)]' : ''}`, /* @ts-ignore */
3372
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3373
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3374
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiListRow, isStandalone: true, selector: "[uiListRow]", inputs: { ruled: { classPropertyName: "ruled", publicName: "ruled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
3375
+ }
3376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiListRow, decorators: [{
3377
+ type: Directive,
3378
+ args: [{
3379
+ selector: '[uiListRow]',
3380
+ host: { '[class]': 'classes()' },
3381
+ }]
3382
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
2021
3383
 
2022
- const OPTION_CLASSES = "relative z-10 min-h-9 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)";
3384
+ const nextId = (() => {
3385
+ let count = 0;
3386
+ return () => `ui-segmented-${++count}`;
3387
+ })();
3388
+ const OPTION_CLASSES = "relative z-10 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) active:scale-(--press-scale)";
3389
+ const SEGMENTED_SIZES = ['md', 'sm'];
3390
+ const SIZE_CLASSES = {
3391
+ md: 'h-10 pointer-fine:h-8',
3392
+ sm: 'h-10 pointer-fine:h-[30px]',
3393
+ };
2023
3394
  const SELECTED_CLASSES = 'text-(--foreground)';
2024
3395
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
2025
3396
  /**
@@ -2034,8 +3405,12 @@ class UiSegmented {
2034
3405
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2035
3406
  label = input.required(/* @ts-ignore */
2036
3407
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3408
+ showLabel = input(false, { ...(ngDevMode ? { debugName: "showLabel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3409
+ labelId = nextId();
2037
3410
  value = model.required(/* @ts-ignore */
2038
3411
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3412
+ size = input('md', /* @ts-ignore */
3413
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2039
3414
  radios = viewChildren('radio', /* @ts-ignore */
2040
3415
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
2041
3416
  thumbIndex = computed(() => {
@@ -2044,7 +3419,7 @@ class UiSegmented {
2044
3419
  }, /* @ts-ignore */
2045
3420
  ...(ngDevMode ? [{ debugName: "thumbIndex" }] : /* istanbul ignore next */ []));
2046
3421
  optionClasses(optionValue) {
2047
- return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
3422
+ return `${OPTION_CLASSES} ${SIZE_CLASSES[this.size()]} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
2048
3423
  }
2049
3424
  tabIndexFor(optionValue) {
2050
3425
  const opts = this.options();
@@ -2068,37 +3443,45 @@ class UiSegmented {
2068
3443
  this.radios()[nextIndex]?.nativeElement.focus();
2069
3444
  }
2070
3445
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
2071
- 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: `
2072
- <div
2073
- class="rounded-control relative grid bg-(--muted) p-0.5"
2074
- role="radiogroup"
2075
- [attr.aria-label]="label()"
2076
- [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
2077
- >
2078
- @if (thumbIndex(); as index) {
2079
- <span
2080
- aria-hidden="true"
2081
- class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
2082
- data-thumb
2083
- [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2084
- [style.width]="'calc(100% / ' + options().length + ')'"
2085
- ></span>
3446
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSegmented, isStandalone: true, selector: "ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, showLabel: { classPropertyName: "showLabel", publicName: "showLabel", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
3447
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3448
+ @if (showLabel()) {
3449
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3450
+ label()
3451
+ }}</span>
2086
3452
  }
3453
+ <div
3454
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3455
+ role="radiogroup"
3456
+ [attr.aria-label]="showLabel() ? null : label()"
3457
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3458
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3459
+ >
3460
+ @if (thumbIndex(); as index) {
3461
+ <span
3462
+ aria-hidden="true"
3463
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
3464
+ data-thumb
3465
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3466
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3467
+ ></span>
3468
+ }
2087
3469
 
2088
- @for (option of options(); track option.value) {
2089
- <button
2090
- #radio
2091
- role="radio"
2092
- type="button"
2093
- [attr.aria-checked]="option.value === value()"
2094
- [attr.tabindex]="tabIndexFor(option.value)"
2095
- [class]="optionClasses(option.value)"
2096
- (click)="select(option.value)"
2097
- (keydown)="onKeydown($event)"
2098
- >
2099
- {{ option.label }}
2100
- </button>
2101
- }
3470
+ @for (option of options(); track option.value) {
3471
+ <button
3472
+ #radio
3473
+ role="radio"
3474
+ type="button"
3475
+ [attr.aria-checked]="option.value === value()"
3476
+ [attr.tabindex]="tabIndexFor(option.value)"
3477
+ [class]="optionClasses(option.value)"
3478
+ (click)="select(option.value)"
3479
+ (keydown)="onKeydown($event)"
3480
+ >
3481
+ {{ option.label }}
3482
+ </button>
3483
+ }
3484
+ </div>
2102
3485
  </div>
2103
3486
  `, isInline: true });
2104
3487
  }
@@ -2107,48 +3490,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2107
3490
  args: [{
2108
3491
  selector: 'ui-segmented',
2109
3492
  template: `
2110
- <div
2111
- class="rounded-control relative grid bg-(--muted) p-0.5"
2112
- role="radiogroup"
2113
- [attr.aria-label]="label()"
2114
- [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
2115
- >
2116
- @if (thumbIndex(); as index) {
2117
- <span
2118
- aria-hidden="true"
2119
- class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
2120
- data-thumb
2121
- [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2122
- [style.width]="'calc(100% / ' + options().length + ')'"
2123
- ></span>
3493
+ <div [class]="showLabel() ? 'flex flex-col gap-1.5' : ''">
3494
+ @if (showLabel()) {
3495
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
3496
+ label()
3497
+ }}</span>
2124
3498
  }
3499
+ <div
3500
+ class="rounded-control relative grid bg-(--muted) p-0.5"
3501
+ role="radiogroup"
3502
+ [attr.aria-label]="showLabel() ? null : label()"
3503
+ [attr.aria-labelledby]="showLabel() ? labelId : null"
3504
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
3505
+ >
3506
+ @if (thumbIndex(); as index) {
3507
+ <span
3508
+ aria-hidden="true"
3509
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
3510
+ data-thumb
3511
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
3512
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
3513
+ ></span>
3514
+ }
2125
3515
 
2126
- @for (option of options(); track option.value) {
2127
- <button
2128
- #radio
2129
- role="radio"
2130
- type="button"
2131
- [attr.aria-checked]="option.value === value()"
2132
- [attr.tabindex]="tabIndexFor(option.value)"
2133
- [class]="optionClasses(option.value)"
2134
- (click)="select(option.value)"
2135
- (keydown)="onKeydown($event)"
2136
- >
2137
- {{ option.label }}
2138
- </button>
2139
- }
3516
+ @for (option of options(); track option.value) {
3517
+ <button
3518
+ #radio
3519
+ role="radio"
3520
+ type="button"
3521
+ [attr.aria-checked]="option.value === value()"
3522
+ [attr.tabindex]="tabIndexFor(option.value)"
3523
+ [class]="optionClasses(option.value)"
3524
+ (click)="select(option.value)"
3525
+ (keydown)="onKeydown($event)"
3526
+ >
3527
+ {{ option.label }}
3528
+ </button>
3529
+ }
3530
+ </div>
2140
3531
  </div>
2141
3532
  `,
2142
3533
  }]
2143
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
3534
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], showLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabel", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
2144
3535
 
2145
- const SELECT_CLASSES = 'cursor-pointer pr-8';
3536
+ const SELECT_CLASSES = 'cursor-pointer appearance-none pr-10 bg-(image:--chevron-down) bg-no-repeat bg-[position:right_0.75rem_center] bg-[size:1.125rem]';
2146
3537
  // Spelled out prefix included: Tailwind scans for literal strings, so a prefix applied in code
2147
- // emits nothing. Dropping `supports` leaves Firefox the padding of a chevron it does not lay out.
3538
+ // emits nothing.
2148
3539
  const PICKER_CLASSES = [
2149
3540
  'pointer-fine:supports-[appearance:base-select]:[appearance:base-select]',
2150
3541
  'pointer-fine:supports-[appearance:base-select]:items-center',
2151
- 'pointer-fine:supports-[appearance:base-select]:pr-3',
2152
3542
  'pointer-fine:supports-[appearance:base-select]:[&_option]:cursor-pointer',
2153
3543
  'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-control',
2154
3544
  'pointer-fine:supports-[appearance:base-select]:[&_option]:px-2.5',
@@ -2156,8 +3546,7 @@ const PICKER_CLASSES = [
2156
3546
  'pointer-fine:supports-[appearance:base-select]:[&_option:hover]:bg-(--soft)',
2157
3547
  'pointer-fine:supports-[appearance:base-select]:[&_option:focus]:bg-(--soft)',
2158
3548
  'pointer-fine:supports-[appearance:base-select]:[&_option:checked]:font-medium',
2159
- '[&::picker-icon]:text-caption',
2160
- '[&::picker-icon]:text-(--muted-foreground)',
3549
+ '[&::picker-icon]:hidden',
2161
3550
  '[&::picker(select)]:[appearance:base-select]',
2162
3551
  '[&::picker(select)]:my-1.5',
2163
3552
  '[&::picker(select)]:rounded-control',
@@ -2186,7 +3575,7 @@ class UiSelect {
2186
3575
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2187
3576
  touched = input(false, /* @ts-ignore */
2188
3577
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2189
- classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
3578
+ classes = computed(() => `${CONTROL_BASE_CLASSES} bg-(--background) ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
2190
3579
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2191
3580
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2192
3581
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiSelect) }], ngImport: i0 });
@@ -2203,7 +3592,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2203
3592
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
2204
3593
 
2205
3594
  /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
2206
- const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
3595
+ const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring', 'dot'];
2207
3596
  const LAST_LINE_WIDTH = '62%';
2208
3597
  const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
2209
3598
  const BAR_CLASSES = 'block rounded-control bg-(--muted)';
@@ -2222,6 +3611,8 @@ class UiSkeleton {
2222
3611
  ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
2223
3612
  height = input(12, /* @ts-ignore */
2224
3613
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3614
+ size = input(232, /* @ts-ignore */
3615
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2225
3616
  animation = PULSE_ANIMATION;
2226
3617
  bars = computed(() => {
2227
3618
  const count = Math.max(1, this.lines());
@@ -2229,28 +3620,36 @@ class UiSkeleton {
2229
3620
  }, /* @ts-ignore */
2230
3621
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
2231
3622
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
2232
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-11 w-56\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 flex-col justify-center gap-2\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-2/5\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-container h-48 w-full bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill size-40 border-[24px] border-(--muted) bg-transparent\"\n [style.animation]=\"animation\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
3623
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('dot') {\n <span class=\"rounded-pill size-3 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
2233
3624
  }
2234
3625
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
2235
3626
  type: Component,
2236
3627
  args: [{ selector: 'ui-skeleton', template: `
2237
3628
  @switch (shape()) {
2238
3629
  @case ('figure') {
2239
- <span class="${BAR_CLASSES} h-11 w-56" [style.animation]="animation"></span>
3630
+ <span class="${BAR_CLASSES} h-10 w-65" [style.animation]="animation"></span>
2240
3631
  }
2241
3632
  @case ('row') {
2242
- <span class="flex h-14 flex-col justify-center gap-2">
2243
- <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
2244
- <span class="${BAR_CLASSES} h-3 w-2/5" [style.animation]="animation"></span>
3633
+ <span class="flex h-14 items-center justify-between gap-4">
3634
+ <span class="flex min-w-0 flex-1 flex-col gap-1.5">
3635
+ <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
3636
+ <span class="${BAR_CLASSES} h-3 w-20" [style.animation]="animation"></span>
3637
+ </span>
3638
+ <span class="${BAR_CLASSES} h-4 w-[90px] flex-none" [style.animation]="animation"></span>
2245
3639
  </span>
2246
3640
  }
2247
3641
  @case ('chart') {
2248
- <span class="rounded-container h-48 w-full bg-(--muted)" [style.animation]="animation"></span>
3642
+ <span class="rounded-control min-h-50 w-full flex-1 bg-(--muted)" [style.animation]="animation"></span>
3643
+ }
3644
+ @case ('dot') {
3645
+ <span class="rounded-pill size-3 bg-(--muted)" [style.animation]="animation"></span>
2249
3646
  }
2250
3647
  @case ('ring') {
2251
3648
  <span
2252
- class="rounded-pill size-40 border-[24px] border-(--muted) bg-transparent"
3649
+ class="rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]"
2253
3650
  [style.animation]="animation"
3651
+ [style.height.px]="size()"
3652
+ [style.width.px]="size()"
2254
3653
  ></span>
2255
3654
  }
2256
3655
  @default {
@@ -2268,15 +3667,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2268
3667
  'aria-hidden': 'true',
2269
3668
  class: 'flex flex-col gap-2',
2270
3669
  }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
2271
- }], propDecorators: { shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
3670
+ }], propDecorators: { shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2272
3671
 
2273
3672
  /** Available stat sizes. `StatSize` is derived from this tuple. */
2274
3673
  const STAT_SIZES = ['hero', 'tile'];
2275
3674
  const VALUE_CLASSES = {
2276
- hero: 'text-display font-semibold text-(--foreground)',
2277
- tile: 'text-title font-semibold text-(--foreground)',
3675
+ hero: 'text-display font-semibold tracking-(--tracking-display) text-(--foreground)',
3676
+ tile: 'text-title font-semibold tabular-nums text-(--foreground)',
3677
+ };
3678
+ const LABEL_CLASSES = {
3679
+ hero: 'text-label font-medium text-(--muted-foreground)',
3680
+ tile: 'text-label font-normal text-(--muted-foreground)',
3681
+ };
3682
+ const STACK_CLASSES = {
3683
+ hero: 'flex flex-col gap-1',
3684
+ tile: 'flex flex-col gap-0.5',
2278
3685
  };
2279
- const LABEL_CLASSES = 'text-label font-medium text-(--subtle-foreground)';
2280
3686
  /**
2281
3687
  * Label, figure and a named period of change, at two sizes: a `hero` for a screen's single
2282
3688
  * headline number, or a smaller `tile` repeated in a grid. The figure itself, its delta and its
@@ -2297,21 +3703,24 @@ class UiStat {
2297
3703
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2298
3704
  size = input('tile', /* @ts-ignore */
2299
3705
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2300
- labelClasses = LABEL_CLASSES;
3706
+ labelClasses = computed(() => LABEL_CLASSES[this.size()], /* @ts-ignore */
3707
+ ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
3708
+ stackClasses = computed(() => STACK_CLASSES[this.size()], /* @ts-ignore */
3709
+ ...(ngDevMode ? [{ debugName: "stackClasses" }] : /* istanbul ignore next */ []));
2301
3710
  valueClasses = computed(() => VALUE_CLASSES[this.size()], /* @ts-ignore */
2302
3711
  ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
2303
3712
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
2304
3713
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiStat, isStandalone: true, selector: "ui-stat", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2305
3714
  <div class="flex items-start justify-between gap-4">
2306
- <div class="flex flex-col gap-1">
2307
- <dl>
2308
- <dt [class]="labelClasses">{{ label() }}</dt>
3715
+ <div [class]="stackClasses()">
3716
+ <dl [class]="stackClasses()">
3717
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2309
3718
  <dd [class]="valueClasses()">
2310
3719
  <ng-content select="[uiStatValue]" />
2311
3720
  </dd>
2312
3721
  </dl>
2313
3722
 
2314
- <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
3723
+ <div class="text-label flex flex-wrap items-center gap-x-1.5 gap-y-1 text-(--muted-foreground)">
2315
3724
  <ng-content select="[uiStatDelta]" />
2316
3725
  <ng-content select="[uiStatCaption]" />
2317
3726
  </div>
@@ -2327,15 +3736,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2327
3736
  selector: 'ui-stat',
2328
3737
  template: `
2329
3738
  <div class="flex items-start justify-between gap-4">
2330
- <div class="flex flex-col gap-1">
2331
- <dl>
2332
- <dt [class]="labelClasses">{{ label() }}</dt>
3739
+ <div [class]="stackClasses()">
3740
+ <dl [class]="stackClasses()">
3741
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2333
3742
  <dd [class]="valueClasses()">
2334
3743
  <ng-content select="[uiStatValue]" />
2335
3744
  </dd>
2336
3745
  </dl>
2337
3746
 
2338
- <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
3747
+ <div class="text-label flex flex-wrap items-center gap-x-1.5 gap-y-1 text-(--muted-foreground)">
2339
3748
  <ng-content select="[uiStatDelta]" />
2340
3749
  <ng-content select="[uiStatCaption]" />
2341
3750
  </div>
@@ -2347,9 +3756,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2347
3756
  }]
2348
3757
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2349
3758
 
2350
- const CLASSES = "relative shrink-0 w-[51px] h-[31px] appearance-none cursor-pointer rounded-pill bg-(--muted) checked:bg-(--primary) transition-colors duration-(--duration-fast) ease-out disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2 before:h-(--row-min) before:content-[''] after:absolute after:top-0.5 after:left-0.5 after:size-[27px] after:rounded-pill after:bg-(--card) after:shadow-[0_1px_3px_rgb(0_0_0/0.2)] after:transition-transform after:duration-(--duration-fast) after:ease-out after:content-[''] checked:after:translate-x-[20px]";
3759
+ const CLASSES = "relative shrink-0 w-[51px] h-[31px] pointer-fine:w-10 pointer-fine:h-6 appearance-none cursor-pointer rounded-pill bg-(--muted) checked:bg-(--primary) transition-colors duration-(--duration-fast) ease-out disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2 before:h-(--row-min) before:content-[''] after:absolute after:top-0.5 after:left-0.5 after:size-[27px] pointer-fine:after:size-5 after:rounded-pill after:bg-(--card) after:shadow-[0_1px_3px_rgb(0_0_0/0.2)] after:transition-transform after:duration-(--duration-fast) after:ease-out after:content-[''] checked:after:translate-x-[20px] pointer-fine:checked:after:translate-x-4";
2351
3760
  /**
2352
3761
  * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
3762
+ * 51 by 31 on touch, 40 by 24 with a mouse.
2353
3763
  *
2354
3764
  * @example
2355
3765
  * <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
@@ -2370,44 +3780,154 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2370
3780
  }]
2371
3781
  }] });
2372
3782
 
3783
+ const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-[3px] touch-manipulation select-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3784
+ const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3785
+ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3786
+ /**
3787
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
3788
+ *
3789
+ * @example
3790
+ * <nav ui-tab-bar>
3791
+ * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3792
+ * </nav>
3793
+ */
3794
+ class UiTabBar {
3795
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3796
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiTabBar, isStandalone: true, selector: "nav[ui-tab-bar]", host: { classAttribute: "grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]" }, ngImport: i0, template: '<ng-content />', isInline: true });
3797
+ }
3798
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
3799
+ type: Component,
3800
+ args: [{
3801
+ selector: 'nav[ui-tab-bar]',
3802
+ template: '<ng-content />',
3803
+ host: {
3804
+ class: 'grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]',
3805
+ },
3806
+ }]
3807
+ }] });
3808
+ /**
3809
+ * One destination of `ui-tab-bar`.
3810
+ *
3811
+ * @example
3812
+ * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
3813
+ */
3814
+ class UiTab {
3815
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3816
+ classes = computed(() => `${TAB_CLASSES} ${this.active() ? ACTIVE_TAB_CLASSES : INACTIVE_TAB_CLASSES}`, /* @ts-ignore */
3817
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3818
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
3819
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiTab, isStandalone: true, selector: "a[ui-tab]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: `
3820
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3821
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3822
+ `, isInline: true });
3823
+ }
3824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
3825
+ type: Component,
3826
+ args: [{
3827
+ selector: 'a[ui-tab]',
3828
+ template: `
3829
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
3830
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
3831
+ `,
3832
+ host: {
3833
+ '[class]': 'classes()',
3834
+ '[attr.aria-current]': "active() ? 'page' : null",
3835
+ },
3836
+ }]
3837
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3838
+
2373
3839
  /** Breakpoints a secondary column can be held back until. */
2374
3840
  const CELL_BREAKPOINTS = ['md', 'lg'];
3841
+ /** Body row heights. */
3842
+ const TABLE_ROWS = ['48', '52', '60'];
3843
+ const ROW_CLASSES = {
3844
+ '48': '[--table-row:3rem]',
3845
+ '52': '[--table-row:3.25rem]',
3846
+ '60': '[--table-row:3.75rem]',
3847
+ };
2375
3848
  const FROM_CLASSES = {
2376
3849
  md: 'hidden md:table-cell',
2377
3850
  lg: 'hidden lg:table-cell',
2378
3851
  };
2379
- const TH_CLASSES = 'h-9 px-2 align-bottom text-caption font-medium text-(--subtle-foreground) shadow-[inset_0_-1px_0_var(--hairline)]';
2380
- const TD_CLASSES = 'h-12 px-2 align-middle text-body';
2381
- const NUMERIC_CLASSES = 'text-right tabular-nums';
3852
+ const TH_CLASSES = 'h-[calc(var(--table-head)+var(--table-head-gap))] border-b-(length:--table-head-gap) border-transparent px-2 align-middle text-caption font-medium text-(--subtle-foreground) shadow-(--table-rule)';
3853
+ const TD_CLASSES = 'h-(--table-row) px-2 align-middle text-body';
3854
+ const NUMERIC_CLASSES = 'text-right tabular-nums whitespace-nowrap';
2382
3855
  const TEXTUAL_CLASSES = 'text-left';
2383
- /** Styled native table. The rows and cells stay plain <tr> and <td>. */
3856
+ const PRIMARY_CLASSES = 'w-full max-w-0';
3857
+ /**
3858
+ * Styled native table. The rows and cells stay plain <tr> and <td>. `row` sets the height of every
3859
+ * body row, `rule` draws or drops the hairline under the header, `spaced` leaves 4px between the
3860
+ * header and the first row.
3861
+ *
3862
+ * @example
3863
+ * <table uiTable row="60" [rule]="false">
3864
+ */
2384
3865
  class UiTable {
3866
+ row = input('48', /* @ts-ignore */
3867
+ ...(ngDevMode ? [{ debugName: "row" }] : /* istanbul ignore next */ []));
3868
+ rule = input(true, { ...(ngDevMode ? { debugName: "rule" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3869
+ spaced = input(false, { ...(ngDevMode ? { debugName: "spaced" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3870
+ classes = computed(() => [
3871
+ 'w-full border-separate border-spacing-0',
3872
+ ROW_CLASSES[this.row()],
3873
+ this.rule() ? '[--table-rule:inset_0_-1px_0_var(--hairline)]' : '[--table-rule:none]',
3874
+ this.spaced() ? '[--table-head-gap:0.25rem]' : '[--table-head-gap:0px]',
3875
+ ].join(' '), /* @ts-ignore */
3876
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2385
3877
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2386
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
3878
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTable, isStandalone: true, selector: "table[uiTable]", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: false, transformFunction: null }, rule: { classPropertyName: "rule", publicName: "rule", isSignal: true, isRequired: false, transformFunction: null }, spaced: { classPropertyName: "spaced", publicName: "spaced", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
2387
3879
  }
2388
3880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, decorators: [{
2389
3881
  type: Directive,
2390
3882
  args: [{
2391
3883
  selector: 'table[uiTable]',
2392
3884
  host: {
2393
- class: 'w-full border-collapse',
3885
+ '[class]': 'classes()',
2394
3886
  },
2395
3887
  }]
3888
+ }], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: false }] }], rule: [{ type: i0.Input, args: [{ isSignal: true, alias: "rule", required: false }] }], spaced: [{ type: i0.Input, args: [{ isSignal: true, alias: "spaced", required: false }] }] } });
3889
+ /** Body of one group of rows: leaves a 4px tail after its last row. */
3890
+ class UiGroup {
3891
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3892
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiGroup, isStandalone: true, selector: "tbody[uiGroup]", host: { classAttribute: "after:table-row after:h-1 after:content-[\"\"]" }, ngImport: i0 });
3893
+ }
3894
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroup, decorators: [{
3895
+ type: Directive,
3896
+ args: [{
3897
+ selector: 'tbody[uiGroup]',
3898
+ host: { class: 'after:table-row after:h-1 after:content-[""]' },
3899
+ }]
2396
3900
  }] });
2397
3901
  /**
2398
- * Body or group row. `group` draws the muted banner that opens each account inside a list of
2399
- * holdings: it carries no hover state of its own, unlike an ordinary row.
3902
+ * Body or group row. An ordinary row glows on hover and takes the soft fill when `selected`; an
3903
+ * `interactive` row (one that opens something) also presses to the soft fill. A row with no action keeps a
3904
+ * plain name: `<td uiTd primary class="truncate font-medium">`.
3905
+ * `group` marks the row that holds a group band: it carries no hover state.
2400
3906
  *
2401
3907
  * @example
2402
- * <tr uiTr>...
2403
- * <tr uiTr group>...
3908
+ * <tr uiTr [selected]="open">...
3909
+ * <tr uiTr group><td ui-group-cell name="Brokerage">...
2404
3910
  */
2405
3911
  class UiTr {
2406
3912
  group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2407
- classes = computed(() => this.group() ? 'min-h-(--row-min) rounded-control bg-(--muted) font-semibold' : 'rounded-control hover:bg-(--glow)', /* @ts-ignore */
3913
+ selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3914
+ interactive = input(false, { ...(ngDevMode ? { debugName: "interactive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3915
+ classes = computed(() => {
3916
+ if (this.group()) {
3917
+ return '';
3918
+ }
3919
+ return [
3920
+ 'relative [&>td:first-child]:rounded-l-control [&>td:last-child]:rounded-r-control',
3921
+ this.selected() ? '[&>td]:bg-(--soft)' : '',
3922
+ 'hover:[&>td]:bg-(--glow)',
3923
+ this.interactive() ? 'active:[&>td]:bg-(--soft)' : '',
3924
+ ]
3925
+ .filter(Boolean)
3926
+ .join(' ');
3927
+ }, /* @ts-ignore */
2408
3928
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2409
3929
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2410
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTr, isStandalone: true, selector: "tr[uiTr]", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
3930
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTr, isStandalone: true, selector: "tr[uiTr]", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-selected": "selected() ? \"true\" : null" } }, ngImport: i0 });
2411
3931
  }
2412
3932
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, decorators: [{
2413
3933
  type: Directive,
@@ -2415,25 +3935,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2415
3935
  selector: 'tr[uiTr]',
2416
3936
  host: {
2417
3937
  '[class]': 'classes()',
3938
+ '[attr.aria-selected]': 'selected() ? "true" : null',
2418
3939
  },
2419
3940
  }]
2420
- }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }] } });
2421
- const cellClasses = (base, numeric, from) => [base, numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES, from ? FROM_CLASSES[from] : ''].filter(Boolean).join(' ');
3941
+ }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }] } });
3942
+ const cellClasses = (base, numeric, primary, from, secondary) => {
3943
+ const breakpoint = from ?? (secondary ? 'lg' : null);
3944
+ return [
3945
+ base,
3946
+ numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES,
3947
+ primary ? PRIMARY_CLASSES : '',
3948
+ breakpoint ? FROM_CLASSES[breakpoint] : '',
3949
+ ]
3950
+ .filter(Boolean)
3951
+ .join(' ');
3952
+ };
2422
3953
  /**
2423
- * Column header. `numeric` right-aligns and lines the digits up; `from` holds a
2424
- * secondary column back until the viewport is wide enough for it.
3954
+ * Column header, centred in its 36px. `numeric` right-aligns and lines the digits up; `secondary`
3955
+ * (or `from`) holds a column back until the viewport is wide enough for it; `primary` makes it
3956
+ * the column that absorbs the free width and truncates; `tall` is a 40px header; `width` fixes the column width and keeps it from shrinking.
2425
3957
  *
2426
3958
  * @example
2427
- * <th uiTh numeric from="md">Average cost</th>
3959
+ * <th uiTh numeric secondary width="108px">Quantity</th>
2428
3960
  */
2429
3961
  class UiTh {
3962
+ width = input(null, /* @ts-ignore */
3963
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2430
3964
  numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3965
+ primary = input(false, { ...(ngDevMode ? { debugName: "primary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3966
+ secondary = input(false, { ...(ngDevMode ? { debugName: "secondary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3967
+ tall = input(false, { ...(ngDevMode ? { debugName: "tall" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2431
3968
  from = input(null, /* @ts-ignore */
2432
3969
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2433
- classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
3970
+ classes = computed(() => cellClasses(`${this.tall() ? '[--table-head:2.5rem]' : '[--table-head:2.25rem]'} ${TH_CLASSES}`, this.numeric(), this.primary(), this.from(), this.secondary()), /* @ts-ignore */
2434
3971
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2435
3972
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2436
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTh, isStandalone: true, selector: "th[uiTh]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
3973
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTh, isStandalone: true, selector: "th[uiTh]", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, secondary: { classPropertyName: "secondary", publicName: "secondary", isSignal: true, isRequired: false, transformFunction: null }, tall: { classPropertyName: "tall", publicName: "tall", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "style.width": "width()", "style.min-width": "width()" } }, ngImport: i0 });
2437
3974
  }
2438
3975
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, decorators: [{
2439
3976
  type: Directive,
@@ -2441,18 +3978,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2441
3978
  selector: 'th[uiTh]',
2442
3979
  host: {
2443
3980
  '[class]': 'classes()',
3981
+ '[style.width]': 'width()',
3982
+ '[style.min-width]': 'width()',
2444
3983
  },
2445
3984
  }]
2446
- }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
2447
- /** Body cell. Same two inputs as `UiTh`, and they must be set on both to match. */
3985
+ }], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], secondary: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondary", required: false }] }], tall: [{ type: i0.Input, args: [{ isSignal: true, alias: "tall", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
3986
+ /** Body cell. Same inputs as `UiTh` (bar `tall` and `width`), and they must be set on both to match. */
2448
3987
  class UiTd {
2449
3988
  numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3989
+ primary = input(false, { ...(ngDevMode ? { debugName: "primary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3990
+ secondary = input(false, { ...(ngDevMode ? { debugName: "secondary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2450
3991
  from = input(null, /* @ts-ignore */
2451
3992
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2452
- classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
3993
+ classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.primary(), this.from(), this.secondary()), /* @ts-ignore */
2453
3994
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2454
3995
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2455
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTd, isStandalone: true, selector: "td[uiTd]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
3996
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTd, isStandalone: true, selector: "td[uiTd]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, secondary: { classPropertyName: "secondary", publicName: "secondary", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
2456
3997
  }
2457
3998
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, decorators: [{
2458
3999
  type: Directive,
@@ -2462,62 +4003,144 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2462
4003
  '[class]': 'classes()',
2463
4004
  },
2464
4005
  }]
2465
- }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
2466
-
2467
- const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-0.5 touch-manipulation select-none text-(--muted-foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
2468
- const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
4006
+ }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], secondary: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondary", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
4007
+ /** Group band sizes. */
4008
+ const GROUP_SIZES = ['md', 'lg'];
2469
4009
  /**
2470
- * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
4010
+ * The band that opens a group of rows: name in 600 (a heading), meta beside it, the projected
4011
+ * total pinned to the right. Spans the whole row through `colspan`.
2471
4012
  *
2472
4013
  * @example
2473
- * <nav ui-tab-bar>
2474
- * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
2475
- * </nav>
4014
+ * <tr uiTr group><td ui-group-cell colspan="4" name="Brokerage" meta="Savings plan">...total...</td></tr>
2476
4015
  */
2477
- class UiTabBar {
2478
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2479
- 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 });
4016
+ class UiGroupCell {
4017
+ name = input.required(/* @ts-ignore */
4018
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
4019
+ meta = input(/* @ts-ignore */
4020
+ ...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
4021
+ size = input('md', /* @ts-ignore */
4022
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
4023
+ hostClasses = computed(() => `px-0 ${this.size() === 'lg' ? 'pt-3' : 'pt-2.5'} pb-1 align-middle text-body`, /* @ts-ignore */
4024
+ ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
4025
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroupCell, deps: [], target: i0.ɵɵFactoryTarget.Component });
4026
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiGroupCell, isStandalone: true, selector: "td[ui-group-cell]", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
4027
+ <div
4028
+ class="rounded-control flex items-center justify-between gap-4 bg-(--muted) px-2"
4029
+ [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
4030
+ >
4031
+ <div class="flex min-w-0 items-baseline gap-3">
4032
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4033
+ {{ name() }}
4034
+ </h2>
4035
+ @if (meta()) {
4036
+ <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
4037
+ }
4038
+ </div>
4039
+ <span class="font-semibold whitespace-nowrap tabular-nums"><ng-content /></span>
4040
+ </div>
4041
+ `, isInline: true });
2480
4042
  }
2481
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
4043
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroupCell, decorators: [{
2482
4044
  type: Component,
2483
4045
  args: [{
2484
- selector: 'nav[ui-tab-bar]',
2485
- template: '<ng-content />',
4046
+ selector: 'td[ui-group-cell]',
2486
4047
  host: {
2487
- 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)]',
4048
+ '[class]': 'hostClasses()',
4049
+ },
4050
+ template: `
4051
+ <div
4052
+ class="rounded-control flex items-center justify-between gap-4 bg-(--muted) px-2"
4053
+ [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
4054
+ >
4055
+ <div class="flex min-w-0 items-baseline gap-3">
4056
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap outline-none" data-group-heading tabindex="-1">
4057
+ {{ name() }}
4058
+ </h2>
4059
+ @if (meta()) {
4060
+ <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
4061
+ }
4062
+ </div>
4063
+ <span class="font-semibold whitespace-nowrap tabular-nums"><ng-content /></span>
4064
+ </div>
4065
+ `,
4066
+ }]
4067
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
4068
+ /**
4069
+ * Makes a link or button the way into its row. By default an invisible layer stretches over the
4070
+ * whole row, so the row is one target for the pointer, the keyboard and a screen reader, and its
4071
+ * focus ring wraps the row. With `[stretch]="false"` only the name is the
4072
+ * target: it underlines on hover and takes its own ring. `current` marks the open line.
4073
+ *
4074
+ * @example
4075
+ * <td uiTd primary><a uiRowLink href="/lines/42">Ferrari</a></td>
4076
+ * <td uiTd primary><a uiRowLink [stretch]="false" href="/accounts/3">Brokerage</a></td>
4077
+ */
4078
+ class UiRowLink {
4079
+ stretch = input(true, { ...(ngDevMode ? { debugName: "stretch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4080
+ current = input(false, { ...(ngDevMode ? { debugName: "current" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4081
+ classes = computed(() => this.stretch()
4082
+ ? 'block max-w-full truncate rounded-control text-left font-medium outline-none after:absolute after:inset-0 after:rounded-control after:content-[""] focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-(--ring)'
4083
+ : 'block max-w-full truncate rounded-sm text-left font-medium underline-offset-[3px] outline-none hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)', /* @ts-ignore */
4084
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4085
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4086
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiRowLink, isStandalone: true, selector: "a[uiRowLink], button[uiRowLink]", inputs: { stretch: { classPropertyName: "stretch", publicName: "stretch", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "current() ? \"true\" : null" } }, ngImport: i0 });
4087
+ }
4088
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowLink, decorators: [{
4089
+ type: Directive,
4090
+ args: [{
4091
+ selector: 'a[uiRowLink], button[uiRowLink]',
4092
+ host: {
4093
+ '[class]': 'classes()',
4094
+ '[attr.aria-current]': 'current() ? "true" : null',
2488
4095
  },
2489
4096
  }]
4097
+ }], propDecorators: { stretch: [{ type: i0.Input, args: [{ isSignal: true, alias: "stretch", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }] } });
4098
+ /** A control in another cell of a stretched-link row: lifts it above the layer that covers the row. */
4099
+ class UiRowAction {
4100
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4101
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiRowAction, isStandalone: true, selector: "[uiRowAction]", host: { classAttribute: "relative z-1" }, ngImport: i0 });
4102
+ }
4103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowAction, decorators: [{
4104
+ type: Directive,
4105
+ args: [{
4106
+ selector: '[uiRowAction]',
4107
+ host: { class: 'relative z-1' },
4108
+ }]
2490
4109
  }] });
4110
+ /** Tones of a subtitle: subtle (default), `--stale` at 500, or the color of the cell. */
4111
+ const SUB_TONES = ['subtle', 'stale', 'inherit'];
4112
+ const SUB_TONE_CLASSES = {
4113
+ subtle: 'font-normal text-(--subtle-foreground)',
4114
+ stale: 'font-medium text-(--stale)',
4115
+ inherit: 'font-normal',
4116
+ };
2491
4117
  /**
2492
- * One destination of `ui-tab-bar`.
4118
+ * A second line under a cell's main text. `narrow` shows it only under 1024px, where it carries
4119
+ * the quantity, average cost and quote that were held back. Its amounts go through `ui-amount`
4120
+ * so they mask.
2493
4121
  *
2494
4122
  * @example
2495
- * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
4123
+ * <span uiCellSub narrow>10 × 421.26 € · cost 388.10 €</span>
4124
+ * <span uiCellSub tone="stale">Quote of 24/09</span>
2496
4125
  */
2497
- class UiTab {
2498
- active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2499
- classes = computed(() => `${TAB_CLASSES}${this.active() ? ` ${ACTIVE_TAB_CLASSES}` : ''}`, /* @ts-ignore */
4126
+ class UiCellSub {
4127
+ narrow = input(false, { ...(ngDevMode ? { debugName: "narrow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4128
+ tone = input('subtle', /* @ts-ignore */
4129
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4130
+ classes = computed(() => `${this.narrow() ? 'lg:hidden ' : ''}block truncate text-caption tabular-nums ${SUB_TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
2500
4131
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2501
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
2502
- 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: `
2503
- <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2504
- <span class="text-caption" data-tab-label><ng-content /></span>
2505
- `, isInline: true });
4132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCellSub, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4133
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiCellSub, isStandalone: true, selector: "[uiCellSub]", inputs: { narrow: { classPropertyName: "narrow", publicName: "narrow", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
2506
4134
  }
2507
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
2508
- type: Component,
4135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCellSub, decorators: [{
4136
+ type: Directive,
2509
4137
  args: [{
2510
- selector: 'a[ui-tab]',
2511
- template: `
2512
- <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2513
- <span class="text-caption" data-tab-label><ng-content /></span>
2514
- `,
4138
+ selector: '[uiCellSub]',
2515
4139
  host: {
2516
4140
  '[class]': 'classes()',
2517
- '[attr.aria-current]': "active() ? 'page' : null",
2518
4141
  },
2519
4142
  }]
2520
- }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
4143
+ }], propDecorators: { narrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "narrow", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
2521
4144
 
2522
4145
  /*
2523
4146
  * Public API surface of the ui library.
@@ -2527,5 +4150,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2527
4150
  * Generated bundle index. Do not edit.
2528
4151
  */
2529
4152
 
2530
- export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiDonut, UiField, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
4153
+ export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_ALIGNS, ASYNC_STATES, ASYNC_VARIANTS, AVATAR_SIZES, BACK_LINK_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_SURFACES, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DELTA_SIZES, DELTA_WEIGHTS, DIALOG_LAYOUTS, DIALOG_WIDTHS, FACT_SIZES, FACT_SUB_TONES, GROUP_SIZES, METER_SIZES, METER_TONES, ROW_GAPS, ROW_PADDINGS, ROW_SIZES, SEGMENTED_SIZES, SKELETON_SHAPES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiActionBar, UiAlert, UiAmount, UiAsync, UiAvatar, UiAvatarLink, UiBackLink, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiExternalLink, UiFact, UiFacts, UiField, UiFieldLeading, UiFieldTrailing, UiFilterChips, UiGroup, UiGroupCell, UiInput, UiLineChart, UiListRow, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowItem, UiRowLink, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
2531
4154
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map