@joanroucoux/cairn-ui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,204 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Component, booleanAttribute, inject, ElementRef, DestroyRef, InjectionToken, output, effect, contentChild, afterRenderEffect, forwardRef, Directive, model, viewChildren } from '@angular/core';
2
+ import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, viewChild, afterNextRender, contentChildren, model, viewChildren } from '@angular/core';
3
+ import { scaleLinear } from 'd3-scale';
4
+ import { curveMonotoneX, line } from 'd3-shape';
5
+
6
+ const MINUS = '−';
7
+ const EM_DASH = '—';
8
+ const BULLETS = '••••';
9
+ /** Formats a number or an amount for the locale, with a missing value as an em dash and masking behind four bullets. */
10
+ function formatAmount(value, options, masked = false) {
11
+ if (value === null || value === undefined || Number.isNaN(value)) {
12
+ return EM_DASH;
13
+ }
14
+ const digits = options.fractionDigits ?? 2;
15
+ const format = new Intl.NumberFormat(options.locale, {
16
+ ...(options.currency ? { style: 'currency', currency: options.currency } : {}),
17
+ minimumFractionDigits: digits,
18
+ maximumFractionDigits: digits,
19
+ });
20
+ const magnitude = Math.abs(value);
21
+ const parts = format.formatToParts(magnitude);
22
+ if (masked) {
23
+ let replaced = false;
24
+ return parts
25
+ .map((part) => {
26
+ if (['integer', 'group', 'decimal', 'fraction'].includes(part.type)) {
27
+ if (replaced) {
28
+ return '';
29
+ }
30
+ replaced = true;
31
+ return BULLETS;
32
+ }
33
+ return part.value;
34
+ })
35
+ .join('');
36
+ }
37
+ const text = parts.map((part) => part.value).join('');
38
+ const isZero = Number(format.format(magnitude).replace(/[^\d]/g, '')) === 0;
39
+ if (!options.signed || isZero) {
40
+ return value < 0 && !isZero ? `${MINUS}${text}` : text;
41
+ }
42
+ return `${value > 0 ? '+' : MINUS}${text}`;
43
+ }
44
+
45
+ /** App-wide "amounts are hidden" state. Defaults to never masked; the app overrides it. */
46
+ const UI_AMOUNT_MASKED = new InjectionToken('UI_AMOUNT_MASKED', {
47
+ providedIn: 'root',
48
+ factory: () => signal(false),
49
+ });
50
+ /** Available figure styles. `AmountNumeric` is derived from this tuple. */
51
+ const AMOUNT_NUMERICS = ['tabular', 'proportional'];
52
+ /** Available masking behaviors. `AmountMasking` is derived from this tuple. */
53
+ const AMOUNT_MASKINGS = ['dots', 'hide'];
54
+ /**
55
+ * A number or an amount of money, formatted for the locale and masked when the app hides amounts.
56
+ *
57
+ * @example
58
+ * <ui-amount [value]="total" currency="EUR" numeric="proportional" />
59
+ * <ui-amount [value]="dayChange" currency="EUR" signed whenMasked="hide" />
60
+ */
61
+ class UiAmount {
62
+ value = input(/* @ts-ignore */
63
+ ...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
64
+ currency = input(/* @ts-ignore */
65
+ ...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
66
+ locale = input(inject(LOCALE_ID), /* @ts-ignore */
67
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
68
+ signed = input(false, { ...(ngDevMode ? { debugName: "signed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
69
+ fractionDigits = input(2, /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "fractionDigits" }] : /* istanbul ignore next */ []));
71
+ numeric = input('tabular', /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "numeric" }] : /* istanbul ignore next */ []));
73
+ whenMasked = input('dots', /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "whenMasked" }] : /* istanbul ignore next */ []));
75
+ #masked = inject(UI_AMOUNT_MASKED);
76
+ hidden = computed(() => this.#masked() && this.whenMasked() === 'hide', /* @ts-ignore */
77
+ ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
78
+ text = computed(() => formatAmount(this.value(), {
79
+ locale: this.locale(),
80
+ currency: this.currency(),
81
+ signed: this.signed(),
82
+ fractionDigits: this.fractionDigits(),
83
+ }, this.#masked()), /* @ts-ignore */
84
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
85
+ classes = computed(() => {
86
+ const missing = this.value() === null || this.value() === undefined;
87
+ return [
88
+ 'whitespace-nowrap',
89
+ this.numeric() === 'tabular' ? 'tabular-nums' : '',
90
+ missing ? 'text-(--subtle-foreground)' : '',
91
+ ]
92
+ .filter(Boolean)
93
+ .join(' ');
94
+ }, /* @ts-ignore */
95
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
96
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, deps: [], target: i0.ɵɵFactoryTarget.Component });
97
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiAmount, isStandalone: true, selector: "ui-amount", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, signed: { classPropertyName: "signed", publicName: "signed", isSignal: true, isRequired: false, transformFunction: null }, fractionDigits: { classPropertyName: "fractionDigits", publicName: "fractionDigits", isSignal: true, isRequired: false, transformFunction: null }, numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, whenMasked: { classPropertyName: "whenMasked", publicName: "whenMasked", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: '{{ text() }}', isInline: true });
98
+ }
99
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, decorators: [{
100
+ type: Component,
101
+ args: [{
102
+ selector: 'ui-amount',
103
+ template: '{{ text() }}',
104
+ host: {
105
+ '[class]': 'classes()',
106
+ '[attr.hidden]': 'hidden() || null',
107
+ },
108
+ }]
109
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], signed: [{ type: i0.Input, args: [{ isSignal: true, alias: "signed", required: false }] }], fractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "fractionDigits", required: false }] }], numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], whenMasked: [{ type: i0.Input, args: [{ isSignal: true, alias: "whenMasked", required: false }] }] } });
110
+
111
+ /** Available async block states. `AsyncState` is derived from this tuple. */
112
+ 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)';
114
+ /**
115
+ * The three states of a block that depends on a service call, next to the ready content.
116
+ *
117
+ * @example
118
+ * <ui-async [state]="totalState()" errorTitle="Total could not be loaded" errorMessage="The server did not answer." retryLabel="Retry" (retry)="reload()">
119
+ * <span asyncLoading><ui-skeleton shape="figure" /></span>
120
+ * <span asyncEmpty>Add a holding to see your total.</span>
121
+ * <span>{{ total() }}</span>
122
+ * </ui-async>
123
+ */
124
+ class UiAsync {
125
+ state = input.required(/* @ts-ignore */
126
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
127
+ errorTitle = input(/* @ts-ignore */
128
+ ...(ngDevMode ? [undefined, { debugName: "errorTitle" }] : /* istanbul ignore next */ []));
129
+ errorMessage = input(/* @ts-ignore */
130
+ ...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
131
+ retryLabel = input(/* @ts-ignore */
132
+ ...(ngDevMode ? [undefined, { debugName: "retryLabel" }] : /* istanbul ignore next */ []));
133
+ retry = output();
134
+ retryButtonClasses = RETRY_BUTTON_CLASSES;
135
+ 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: `
137
+ @switch (state()) {
138
+ @case ('loading') {
139
+ <ng-content select="[asyncLoading]" />
140
+ }
141
+ @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()">
150
+ {{ retryLabel() }}
151
+ </button>
152
+ </div>
153
+ }
154
+ @case ('empty') {
155
+ <ng-content select="[asyncEmpty]" />
156
+ }
157
+ @default {
158
+ <ng-content />
159
+ }
160
+ }
161
+ `, isInline: true });
162
+ }
163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, decorators: [{
164
+ type: Component,
165
+ args: [{
166
+ selector: 'ui-async',
167
+ template: `
168
+ @switch (state()) {
169
+ @case ('loading') {
170
+ <ng-content select="[asyncLoading]" />
171
+ }
172
+ @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()">
181
+ {{ retryLabel() }}
182
+ </button>
183
+ </div>
184
+ }
185
+ @case ('empty') {
186
+ <ng-content select="[asyncEmpty]" />
187
+ }
188
+ @default {
189
+ <ng-content />
190
+ }
191
+ }
192
+ `,
193
+ host: {
194
+ '[attr.aria-busy]': "state() === 'loading' || null",
195
+ },
196
+ }]
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"] }] } });
3
198
 
4
199
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
5
200
  const AVATAR_SIZES = ['sm', 'md'];
6
- const BASE_CLASSES$4 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
201
+ const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
7
202
  const SIZE_CLASSES$1 = {
8
203
  sm: 'size-8 text-caption',
9
204
  md: 'size-11 text-label',
@@ -21,7 +216,7 @@ class UiAvatar {
21
216
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
22
217
  size = input('md', /* @ts-ignore */
23
218
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
24
- classes = computed(() => `${BASE_CLASSES$4} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
219
+ classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
25
220
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
26
221
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
27
222
  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 });
@@ -41,7 +236,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
41
236
 
42
237
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
43
238
  const BADGE_VARIANTS = ['neutral', 'outline'];
44
- const BASE_CLASSES$3 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
239
+ const BASE_CLASSES$5 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
45
240
  const VARIANT_CLASSES$2 = {
46
241
  neutral: 'bg-(--muted) text-(--foreground)',
47
242
  outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
@@ -55,7 +250,7 @@ const VARIANT_CLASSES$2 = {
55
250
  class UiBadge {
56
251
  variant = input('neutral', /* @ts-ignore */
57
252
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
58
- classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
253
+ classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
59
254
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
60
255
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
61
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 });
@@ -75,9 +270,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
75
270
  const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
76
271
  /** Available button sizes. `md` is the touch target Cairn asks for everywhere. */
77
272
  const BUTTON_SIZES = ['md', 'lg', 'xl', 'icon'];
78
- const BASE_CLASSES$2 =
79
- // Tailwind's preflight gives buttons cursor: default; the hand cursor is what users expect here.
80
- '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';
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';
81
274
  const VARIANT_CLASSES$1 = {
82
275
  primary: 'bg-(--primary) text-(--primary-foreground) hover:opacity-90',
83
276
  outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)',
@@ -91,16 +284,7 @@ const SIZE_CLASSES = {
91
284
  icon: 'size-(--row-min) p-0',
92
285
  };
93
286
  /**
94
- * Styled native button. Applied as an attribute so the host keeps every native
95
- * button behavior (type, disabled, form submission, accessibility) for free.
96
- *
97
- * Also applies to an anchor, for the one case where the action really is a navigation:
98
- * a download the browser has to perform itself.
99
- *
100
- * `size="icon"` has no visible text: give it an `aria-label`.
101
- *
102
- * A `loading` button shows a spinner before its label and swallows clicks, so a slow first
103
- * response cannot turn into a double submission.
287
+ * Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
104
288
  *
105
289
  * @example
106
290
  * <button ui-button variant="destructive">Delete</button>
@@ -112,12 +296,9 @@ class UiButton {
112
296
  size = input('md', /* @ts-ignore */
113
297
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
114
298
  loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
115
- classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
299
+ classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
116
300
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
117
301
  constructor() {
118
- // A host `(click)` binding runs after the template's own listener, so a loading button would
119
- // still fire the consumer's handler once before this guard got a say. Capturing from the
120
- // constructor puts this listener first regardless of binding order.
121
302
  const host = inject(ElementRef).nativeElement;
122
303
  const guard = (event) => {
123
304
  if (this.loading()) {
@@ -164,7 +345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
164
345
  const CARD_VARIANTS = ['default', 'elevated'];
165
346
  /** Available card paddings. `CardPadding` is derived from this tuple. */
166
347
  const CARD_PADDINGS = ['none', 'sm', 'md'];
167
- const BASE_CLASSES$1 = 'block rounded-container border';
348
+ const BASE_CLASSES$3 = 'block rounded-container border';
168
349
  const VARIANT_CLASSES = {
169
350
  default: 'bg-(--card) border-(--border) text-(--card-foreground)',
170
351
  elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
@@ -185,7 +366,7 @@ class UiCard {
185
366
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
186
367
  padding = input('md', /* @ts-ignore */
187
368
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
188
- classes = computed(() => `${BASE_CLASSES$1} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
369
+ classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
189
370
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
190
371
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
191
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 });
@@ -213,7 +394,7 @@ const UI_CONTROL = new InjectionToken('UI_CONTROL');
213
394
 
214
395
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
215
396
  const DELTA_EMPHASES = ['text', 'pill'];
216
- const BASE_CLASSES = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
397
+ const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
217
398
  const PILL_CLASSES = 'h-6 rounded-pill px-2';
218
399
  const TEXT_CLASSES = {
219
400
  positive: 'text-(--positive)',
@@ -221,9 +402,6 @@ const TEXT_CLASSES = {
221
402
  neutral: 'text-(--muted-foreground)',
222
403
  unknown: 'text-(--subtle-foreground)',
223
404
  };
224
- // The handoff's `Cairn Composants.dc.html` uses --soft here, but --positive text on --soft only
225
- // reaches 4.33:1 (axe flags it): --soft is a touch darker than --background, and --positive sits
226
- // right at the edge of AA at 4.77:1 against --background alone. --muted keeps the chip legible.
227
405
  const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
228
406
  /**
229
407
  * Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
@@ -261,7 +439,7 @@ class UiDelta {
261
439
  classes = computed(() => {
262
440
  const sign = this.sign();
263
441
  const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
264
- return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
442
+ return `${BASE_CLASSES$2} ${TEXT_CLASSES[sign]} ${background}`;
265
443
  }, /* @ts-ignore */
266
444
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
267
445
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -296,15 +474,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
296
474
  const DIALOG_WIDTHS = ['md', 'lg'];
297
475
  const WIDTH_CLASSES = {
298
476
  md: 'max-w-md',
299
- lg: 'max-w-lg',
477
+ lg: 'max-w-[560px]',
300
478
  };
301
- const nextId$1 = (() => {
479
+ const nextId$2 = (() => {
302
480
  let count = 0;
303
481
  return () => `ui-dialog-${++count}`;
304
482
  })();
305
483
  /**
306
- * Modal dialog on the native <dialog>: focus trap, focus return, Escape and the top layer
307
- * come from the platform. Two projection slots - the body, and [dialogActions] for the footer.
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.
308
486
  *
309
487
  * @example
310
488
  * <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
@@ -319,12 +497,11 @@ class UiDialog {
319
497
  ...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
320
498
  width = input('lg', /* @ts-ignore */
321
499
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
322
- open = input(false, /* @ts-ignore */
323
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
500
+ open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
324
501
  dismissed = output();
325
- headingId = nextId$1();
502
+ headingId = nextId$2();
326
503
  descriptionId = `${this.headingId}-description`;
327
- classes = computed(() => `w-full m-auto ${WIDTH_CLASSES[this.width()]} rounded-container border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
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 */
328
505
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
329
506
  #host = inject(ElementRef);
330
507
  constructor() {
@@ -353,54 +530,73 @@ class UiDialog {
353
530
  [class]="classes()"
354
531
  (close)="onNativeClose()"
355
532
  >
356
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
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>
535
+ </div>
357
536
 
358
- @if (description()) {
359
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
360
- }
537
+ <div class="px-6 pt-1 lg:pt-6">
538
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
361
539
 
362
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
363
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
364
- <ng-content />
540
+ @if (description()) {
541
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
542
+ }
365
543
  </div>
366
544
 
367
- <div class="mt-6 flex justify-end gap-3">
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">
548
+ <ng-content />
549
+ </div>
550
+ </div>
551
+
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
+ >
368
556
  <ng-content select="[dialogActions]" />
369
557
  </div>
370
558
  </dialog>
371
- `, isInline: true });
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"] });
372
560
  }
373
561
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
374
562
  type: Component,
375
- args: [{
376
- selector: 'ui-dialog',
377
- template: `
563
+ args: [{ selector: 'ui-dialog', template: `
378
564
  <dialog
379
565
  [attr.aria-describedby]="description() ? descriptionId : null"
380
566
  [attr.aria-labelledby]="headingId"
381
567
  [class]="classes()"
382
568
  (close)="onNativeClose()"
383
569
  >
384
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
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>
572
+ </div>
385
573
 
386
- @if (description()) {
387
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
388
- }
574
+ <div class="px-6 pt-1 lg:pt-6">
575
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
389
576
 
390
- <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
391
- <div class="mt-4 flex flex-col gap-4 empty:hidden">
392
- <ng-content />
577
+ @if (description()) {
578
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
579
+ }
580
+ </div>
581
+
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">
585
+ <ng-content />
586
+ </div>
393
587
  </div>
394
588
 
395
- <div class="mt-6 flex justify-end gap-3">
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
+ >
396
593
  <ng-content select="[dialogActions]" />
397
594
  </div>
398
595
  </dialog>
399
- `,
400
- }]
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"] }]
401
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"] }] } });
402
598
 
403
- const nextId = (() => {
599
+ const nextId$1 = (() => {
404
600
  let count = 0;
405
601
  return () => `ui-field-${++count}`;
406
602
  })();
@@ -434,10 +630,9 @@ class UiField {
434
630
  ...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
435
631
  error = input(/* @ts-ignore */
436
632
  ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
437
- /** Unit shown inside the control's right edge, such as `EUR` or `parts`. Not part of the control's value. */
438
633
  unit = input(/* @ts-ignore */
439
634
  ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
440
- #id = nextId();
635
+ #id = nextId$1();
441
636
  hintId = `${this.#id}-hint`;
442
637
  errorId = `${this.#id}-error`;
443
638
  #host = inject(ElementRef);
@@ -608,6 +803,851 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
608
803
  }]
609
804
  }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
610
805
 
806
+ const axisTicks = (points, maxTicks = 5) => {
807
+ if (points.length <= maxTicks) {
808
+ return [...points];
809
+ }
810
+ const count = Math.max(3, Math.min(maxTicks, points.length));
811
+ const step = (points.length - 1) / (count - 1);
812
+ const indexes = new Set();
813
+ for (let tick = 0; tick < count; tick++) {
814
+ indexes.add(Math.round(tick * step));
815
+ }
816
+ return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
817
+ };
818
+
819
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
820
+ const nearestPointIndex = (points, x) => {
821
+ let nearest = 0;
822
+ let nearestDistance = Infinity;
823
+ points.forEach((point, index) => {
824
+ const distance = Math.abs(point.x - x);
825
+ if (distance < nearestDistance) {
826
+ nearestDistance = distance;
827
+ nearest = index;
828
+ }
829
+ });
830
+ return nearest;
831
+ };
832
+ const navigateIndex = (key, current, last) => {
833
+ switch (key) {
834
+ case 'ArrowRight':
835
+ return { index: clamp((current ?? -1) + 1, 0, last), preventDefault: true };
836
+ case 'ArrowLeft':
837
+ return { index: clamp((current ?? last + 1) - 1, 0, last), preventDefault: true };
838
+ case 'Home':
839
+ return { index: 0, preventDefault: true };
840
+ case 'End':
841
+ return { index: last, preventDefault: true };
842
+ case 'Escape':
843
+ return { index: null, preventDefault: false };
844
+ default:
845
+ return null;
846
+ }
847
+ };
848
+
849
+ const CURVE = curveMonotoneX;
850
+ const buildGeometry = (points, width, height, padding = {}) => {
851
+ if (points.length === 0) {
852
+ return null;
853
+ }
854
+ const { x: paddingX = 4, top: paddingTop = 4, bottom: paddingBottom = 4 } = padding;
855
+ const plotTop = paddingTop;
856
+ const plotBottom = height - paddingBottom;
857
+ const times = points.map((point) => point.t);
858
+ const values = points.map((point) => point.v);
859
+ const minValue = Math.min(...values);
860
+ const maxValue = Math.max(...values);
861
+ const x = scaleLinear()
862
+ .domain([Math.min(...times), Math.max(...times)])
863
+ .range([paddingX, width - paddingX]);
864
+ const y = minValue === maxValue
865
+ ? () => (plotTop + plotBottom) / 2
866
+ : scaleLinear().domain([minValue, maxValue]).range([plotBottom, plotTop]);
867
+ const toX = (point) => (points.length === 1 ? paddingX : x(point.t));
868
+ const lineGenerator = line()
869
+ .x(toX)
870
+ .y((point) => y(point.v))
871
+ .curve(CURVE);
872
+ const first = points[0];
873
+ const last = points.at(-1);
874
+ return {
875
+ line: lineGenerator(points),
876
+ end: { x: toX(last), y: y(last.v) },
877
+ start: { y: y(first.v) },
878
+ points: points.map((point) => ({ ...point, x: toX(point), y: y(point.v) })),
879
+ plotBottom,
880
+ plotTop,
881
+ };
882
+ };
883
+
884
+ const LINE_GAP = 3;
885
+ const COLLISION_MARGIN = 3;
886
+ const EDGE_TOLERANCE = 3;
887
+ const candidateSlots = (startY, box, label) => {
888
+ const aboveTop = startY - LINE_GAP - label.height;
889
+ const aboveBottom = startY - LINE_GAP;
890
+ const belowTop = startY + LINE_GAP;
891
+ const belowBottom = startY + LINE_GAP + label.height;
892
+ const slots = [
893
+ { anchor: 'end', x: box.width, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
894
+ { anchor: 'end', x: box.width, y: belowBottom, top: belowTop, bottom: belowBottom },
895
+ { anchor: 'start', x: 0, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
896
+ { anchor: 'start', x: 0, y: belowBottom, top: belowTop, bottom: belowBottom },
897
+ { anchor: 'end', x: box.width, y: label.height, top: 0, bottom: label.height },
898
+ { anchor: 'start', x: 0, y: label.height, top: 0, bottom: label.height },
899
+ { anchor: 'end', x: box.width, y: box.height, top: box.height - label.height, bottom: box.height },
900
+ { anchor: 'start', x: 0, y: box.height, top: box.height - label.height, bottom: box.height },
901
+ ];
902
+ return slots.filter((slot) => slot.top >= -EDGE_TOLERANCE && slot.bottom <= box.height + EDGE_TOLERANCE);
903
+ };
904
+ const hitsFor = (slot, label, points) => {
905
+ const left = slot.anchor === 'end' ? slot.x - label.width : slot.x;
906
+ const right = slot.anchor === 'end' ? slot.x : slot.x + label.width;
907
+ const top = slot.top - COLLISION_MARGIN;
908
+ const bottom = slot.bottom + COLLISION_MARGIN;
909
+ return points.filter((point) => point.x >= left && point.x <= right && point.y >= top && point.y <= bottom).length;
910
+ };
911
+ const placeStartLabel = (points, startY, box, label) => {
912
+ let best = null;
913
+ for (const slot of candidateSlots(startY, box, label)) {
914
+ const hits = hitsFor(slot, label, points);
915
+ if (!best || hits < best.hits) {
916
+ best = { slot, hits };
917
+ }
918
+ if (hits === 0) {
919
+ break;
920
+ }
921
+ }
922
+ const { anchor, x, y } = best.slot;
923
+ return { anchor, x, y };
924
+ };
925
+
926
+ const TRANSITION_SAMPLES = 120;
927
+ const TRANSITION_DURATION = 260;
928
+ const easeOutQuint = (t) => 1 - (1 - t) ** 5;
929
+ const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
930
+ const monotoneTangents = (xs, ys) => {
931
+ const n = xs.length;
932
+ const slopes = [];
933
+ for (let i = 0; i < n - 1; i++) {
934
+ const dx = xs[i + 1] - xs[i];
935
+ slopes.push(dx === 0 ? 0 : (ys[i + 1] - ys[i]) / dx);
936
+ }
937
+ const tangents = new Array(n).fill(0);
938
+ tangents[0] = slopes[0];
939
+ tangents[n - 1] = slopes[n - 2];
940
+ for (let i = 1; i < n - 1; i++) {
941
+ const a = slopes[i - 1];
942
+ const b = slopes[i];
943
+ tangents[i] = a * b <= 0 ? 0 : (a + b) / 2;
944
+ }
945
+ for (let i = 0; i < n - 1; i++) {
946
+ const slope = slopes[i];
947
+ if (slope === 0) {
948
+ tangents[i] = 0;
949
+ tangents[i + 1] = 0;
950
+ continue;
951
+ }
952
+ const a = tangents[i] / slope;
953
+ const b = tangents[i + 1] / slope;
954
+ const s = a * a + b * b;
955
+ if (s > 9) {
956
+ const t = 3 / Math.sqrt(s);
957
+ tangents[i] = t * a * slope;
958
+ tangents[i + 1] = t * b * slope;
959
+ }
960
+ }
961
+ return tangents;
962
+ };
963
+ const sampleMonotone = (points, sampleXs) => {
964
+ const xs = points.map((point) => point.x);
965
+ const ys = points.map((point) => point.y);
966
+ const tangents = monotoneTangents(xs, ys);
967
+ let segment = 0;
968
+ return sampleXs.map((x) => {
969
+ while (segment < xs.length - 2 && x > xs[segment + 1]) {
970
+ segment++;
971
+ }
972
+ const x0 = xs[segment];
973
+ const x1 = xs[segment + 1];
974
+ const dx = x1 - x0;
975
+ const f = dx === 0 ? 0 : (x - x0) / dx;
976
+ const f2 = f * f;
977
+ const f3 = f2 * f;
978
+ const y0 = ys[segment];
979
+ const y1 = ys[segment + 1];
980
+ const m0 = tangents[segment];
981
+ const m1 = tangents[segment + 1];
982
+ return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
983
+ });
984
+ };
985
+ const buildPolyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
986
+ const interpolateLine = (from, to, progress) => {
987
+ const left = to.points[0].x;
988
+ const right = to.points.at(-1).x;
989
+ const samples = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
990
+ const fromYs = sampleMonotone(from.points, samples);
991
+ const toYs = sampleMonotone(to.points, samples);
992
+ return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
993
+ };
994
+
995
+ const DEFAULT_WIDTH = 640;
996
+ const DEFAULT_HEIGHT = 240;
997
+ const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
998
+ const START_LABEL_HEIGHT = 17;
999
+ const identityValue = (value) => `${value}`;
1000
+ const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1001
+ const identityTime = (time) => new Date(time).toISOString();
1002
+ /**
1003
+ * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1004
+ *
1005
+ * @example
1006
+ * <ui-line-chart [points]="points" label="Net worth over one month" startLabel="Since" [valueFormat]="formatEur" />
1007
+ */
1008
+ class UiLineChart {
1009
+ points = input.required(/* @ts-ignore */
1010
+ ...(ngDevMode ? [{ debugName: "points" }] : /* istanbul ignore next */ []));
1011
+ label = input.required(/* @ts-ignore */
1012
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1013
+ valueFormat = input(identityValue, /* @ts-ignore */
1014
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
1015
+ deltaFormat = input(identityDelta, /* @ts-ignore */
1016
+ ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1017
+ timeFormat = input(identityTime, /* @ts-ignore */
1018
+ ...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
1019
+ axisFormat = input(identityTime, /* @ts-ignore */
1020
+ ...(ngDevMode ? [{ debugName: "axisFormat" }] : /* istanbul ignore next */ []));
1021
+ startLabel = input('', /* @ts-ignore */
1022
+ ...(ngDevMode ? [{ debugName: "startLabel" }] : /* istanbul ignore next */ []));
1023
+ timeColumnLabel = input('Time', /* @ts-ignore */
1024
+ ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1025
+ valueColumnLabel = input('Value', /* @ts-ignore */
1026
+ ...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
1027
+ #destroyRef = inject(DestroyRef);
1028
+ #frame = null;
1029
+ #hasRenderedOnce = false;
1030
+ #previousGeometry = null;
1031
+ svgRef = viewChild.required('svgRef', /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1033
+ activeIndex = signal(null, /* @ts-ignore */
1034
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1035
+ animatedLine = signal(null, /* @ts-ignore */
1036
+ ...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
1037
+ measuredSize = signal(null, /* @ts-ignore */
1038
+ ...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
1039
+ effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
1040
+ ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1041
+ effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1042
+ ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1043
+ geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
1044
+ ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1045
+ displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1046
+ ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
1047
+ ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
1048
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
1049
+ active = computed(() => {
1050
+ const geometry = this.geometry();
1051
+ const index = this.activeIndex();
1052
+ return geometry && index !== null ? geometry.points[index] : null;
1053
+ }, /* @ts-ignore */
1054
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1055
+ startLabelInfo = computed(() => {
1056
+ const text = this.startLabel();
1057
+ const geometry = this.geometry();
1058
+ if (!text || !geometry) {
1059
+ return null;
1060
+ }
1061
+ const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1062
+ const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
1063
+ const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
1064
+ return { label, placement };
1065
+ }, /* @ts-ignore */
1066
+ ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1067
+ #renderRef = afterNextRender(() => this.#observeSize());
1068
+ constructor() {
1069
+ effect(() => {
1070
+ const geometry = this.geometry();
1071
+ const previous = this.#previousGeometry;
1072
+ if (previous === geometry) {
1073
+ return;
1074
+ }
1075
+ this.#previousGeometry = geometry;
1076
+ this.#cancelAnimation();
1077
+ if (!this.#hasRenderedOnce) {
1078
+ this.#hasRenderedOnce = true;
1079
+ return;
1080
+ }
1081
+ if (!previous || !geometry) {
1082
+ this.animatedLine.set(null);
1083
+ return;
1084
+ }
1085
+ if (prefersReducedMotion()) {
1086
+ this.animatedLine.set(null);
1087
+ return;
1088
+ }
1089
+ this.#animate(previous, geometry);
1090
+ });
1091
+ this.#destroyRef.onDestroy(() => this.#cancelAnimation());
1092
+ }
1093
+ #observeSize() {
1094
+ this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
1095
+ if (typeof ResizeObserver === 'undefined') {
1096
+ return;
1097
+ }
1098
+ const element = this.svgRef().nativeElement;
1099
+ const observer = new ResizeObserver(([entry]) => {
1100
+ if (entry) {
1101
+ const { width, height } = entry.contentRect;
1102
+ this.measuredSize.set({ width, height });
1103
+ }
1104
+ });
1105
+ observer.observe(element);
1106
+ this.#destroyRef.onDestroy(() => observer.disconnect());
1107
+ }
1108
+ #cancelAnimation() {
1109
+ if (this.#frame !== null) {
1110
+ cancelAnimationFrame(this.#frame);
1111
+ this.#frame = null;
1112
+ }
1113
+ }
1114
+ #animate(from, to) {
1115
+ if (from.points.length < 2 || to.points.length < 2) {
1116
+ this.animatedLine.set(null);
1117
+ return;
1118
+ }
1119
+ const startTime = performance.now();
1120
+ const step = (now) => {
1121
+ const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
1122
+ this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
1123
+ if (t < 1) {
1124
+ this.#frame = requestAnimationFrame(step);
1125
+ }
1126
+ else {
1127
+ this.#frame = null;
1128
+ this.animatedLine.set(null);
1129
+ }
1130
+ };
1131
+ this.#frame = requestAnimationFrame(step);
1132
+ }
1133
+ coreTick(index, count) {
1134
+ if (count <= 3) {
1135
+ return true;
1136
+ }
1137
+ const middle = Math.floor((count - 1) / 2);
1138
+ return index === 0 || index === count - 1 || index === middle;
1139
+ }
1140
+ deltaFor(point) {
1141
+ return point.v - this.points()[0].v;
1142
+ }
1143
+ deltaClasses(point) {
1144
+ const delta = this.deltaFor(point);
1145
+ return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1146
+ }
1147
+ tooltipStyle(point) {
1148
+ return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
1149
+ }
1150
+ onPointerMove(event) {
1151
+ const geometry = this.geometry();
1152
+ if (!geometry || geometry.points.length === 0) {
1153
+ return;
1154
+ }
1155
+ const rect = this.svgRef().nativeElement.getBoundingClientRect();
1156
+ const ratio = rect.width === 0 ? 1 : this.effectiveWidth() / rect.width;
1157
+ const x = (event.clientX - rect.left) * ratio;
1158
+ this.activeIndex.set(nearestPointIndex(geometry.points, x));
1159
+ }
1160
+ onPointerLeave() {
1161
+ this.activeIndex.set(null);
1162
+ }
1163
+ onKeydown(event) {
1164
+ const geometry = this.geometry();
1165
+ if (!geometry || geometry.points.length === 0) {
1166
+ return;
1167
+ }
1168
+ const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
1169
+ if (!navigation) {
1170
+ return;
1171
+ }
1172
+ if (navigation.preventDefault) {
1173
+ event.preventDefault();
1174
+ }
1175
+ this.activeIndex.set(navigation.index);
1176
+ }
1177
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1178
+ 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: `
1179
+ <div class="relative h-full">
1180
+ <svg
1181
+ #svgRef
1182
+ class="block h-full w-full touch-pan-y"
1183
+ role="img"
1184
+ tabindex="0"
1185
+ [attr.aria-label]="label()"
1186
+ [attr.height]="effectiveHeight()"
1187
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1188
+ [attr.width]="effectiveWidth()"
1189
+ (keydown)="onKeydown($event)"
1190
+ (pointerleave)="onPointerLeave()"
1191
+ (pointermove)="onPointerMove($event)"
1192
+ >
1193
+ @if (geometry(); as geometry) {
1194
+ <path
1195
+ class="stroke-(--foreground)"
1196
+ data-chart-line
1197
+ fill="none"
1198
+ stroke-linecap="round"
1199
+ stroke-linejoin="round"
1200
+ stroke-width="2"
1201
+ [attr.d]="displayLine()"
1202
+ />
1203
+
1204
+ <line
1205
+ class="stroke-(--border)"
1206
+ data-chart-start-line
1207
+ stroke-dasharray="4 4"
1208
+ stroke-width="1"
1209
+ x1="0"
1210
+ [attr.x2]="effectiveWidth()"
1211
+ [attr.y1]="geometry.start.y"
1212
+ [attr.y2]="geometry.start.y"
1213
+ />
1214
+
1215
+ @if (startLabelInfo(); as info) {
1216
+ <text
1217
+ class="text-caption fill-(--subtle-foreground)"
1218
+ data-chart-start-label
1219
+ [attr.text-anchor]="info.placement.anchor"
1220
+ [attr.x]="info.placement.x"
1221
+ [attr.y]="info.placement.y"
1222
+ >
1223
+ {{ info.label }}
1224
+ </text>
1225
+ }
1226
+
1227
+ <circle
1228
+ class="fill-(--foreground) stroke-(--card)"
1229
+ data-chart-end
1230
+ r="4"
1231
+ stroke-width="2"
1232
+ [attr.cx]="geometry.end.x"
1233
+ [attr.cy]="geometry.end.y"
1234
+ />
1235
+
1236
+ @for (
1237
+ tick of ticks();
1238
+ track tick.t;
1239
+ let index = $index;
1240
+ let count = $count;
1241
+ let first = $first;
1242
+ let last = $last
1243
+ ) {
1244
+ <text
1245
+ class="text-caption fill-(--subtle-foreground)"
1246
+ data-chart-axis-tick
1247
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1248
+ [attr.x]="tick.x"
1249
+ [attr.y]="effectiveHeight() - 4"
1250
+ [class.max-sm:hidden]="!coreTick(index, count)"
1251
+ >
1252
+ {{ axisFormat()(tick.t) }}
1253
+ </text>
1254
+ }
1255
+
1256
+ @if (active(); as point) {
1257
+ <line
1258
+ class="stroke-(--border)"
1259
+ data-chart-crosshair
1260
+ stroke-width="1"
1261
+ y1="0"
1262
+ [attr.x1]="point.x"
1263
+ [attr.x2]="point.x"
1264
+ [attr.y2]="effectiveHeight()"
1265
+ />
1266
+ <circle
1267
+ class="fill-(--foreground) stroke-(--card)"
1268
+ data-chart-active
1269
+ r="4"
1270
+ stroke-width="2"
1271
+ [attr.cx]="point.x"
1272
+ [attr.cy]="point.y"
1273
+ />
1274
+ }
1275
+ }
1276
+ </svg>
1277
+
1278
+ @if (active(); as point) {
1279
+ <div
1280
+ 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)]"
1281
+ data-testid="chart-tooltip"
1282
+ role="status"
1283
+ [style]="tooltipStyle(point)"
1284
+ >
1285
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1286
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1287
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1288
+ </div>
1289
+ }
1290
+
1291
+ @if (points().length > 0) {
1292
+ <table class="sr-only">
1293
+ <caption>
1294
+ {{
1295
+ label()
1296
+ }}
1297
+ </caption>
1298
+ <thead>
1299
+ <tr>
1300
+ <th scope="col">{{ timeColumnLabel() }}</th>
1301
+ <th scope="col">{{ valueColumnLabel() }}</th>
1302
+ </tr>
1303
+ </thead>
1304
+ <tbody>
1305
+ @for (point of points(); track point.t) {
1306
+ <tr>
1307
+ <td>{{ timeFormat()(point.t) }}</td>
1308
+ <td>{{ valueFormat()(point.v) }}</td>
1309
+ </tr>
1310
+ }
1311
+ </tbody>
1312
+ </table>
1313
+ }
1314
+ </div>
1315
+ `, isInline: true });
1316
+ }
1317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, decorators: [{
1318
+ type: Component,
1319
+ args: [{
1320
+ selector: 'ui-line-chart',
1321
+ template: `
1322
+ <div class="relative h-full">
1323
+ <svg
1324
+ #svgRef
1325
+ class="block h-full w-full touch-pan-y"
1326
+ role="img"
1327
+ tabindex="0"
1328
+ [attr.aria-label]="label()"
1329
+ [attr.height]="effectiveHeight()"
1330
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1331
+ [attr.width]="effectiveWidth()"
1332
+ (keydown)="onKeydown($event)"
1333
+ (pointerleave)="onPointerLeave()"
1334
+ (pointermove)="onPointerMove($event)"
1335
+ >
1336
+ @if (geometry(); as geometry) {
1337
+ <path
1338
+ class="stroke-(--foreground)"
1339
+ data-chart-line
1340
+ fill="none"
1341
+ stroke-linecap="round"
1342
+ stroke-linejoin="round"
1343
+ stroke-width="2"
1344
+ [attr.d]="displayLine()"
1345
+ />
1346
+
1347
+ <line
1348
+ class="stroke-(--border)"
1349
+ data-chart-start-line
1350
+ stroke-dasharray="4 4"
1351
+ stroke-width="1"
1352
+ x1="0"
1353
+ [attr.x2]="effectiveWidth()"
1354
+ [attr.y1]="geometry.start.y"
1355
+ [attr.y2]="geometry.start.y"
1356
+ />
1357
+
1358
+ @if (startLabelInfo(); as info) {
1359
+ <text
1360
+ class="text-caption fill-(--subtle-foreground)"
1361
+ data-chart-start-label
1362
+ [attr.text-anchor]="info.placement.anchor"
1363
+ [attr.x]="info.placement.x"
1364
+ [attr.y]="info.placement.y"
1365
+ >
1366
+ {{ info.label }}
1367
+ </text>
1368
+ }
1369
+
1370
+ <circle
1371
+ class="fill-(--foreground) stroke-(--card)"
1372
+ data-chart-end
1373
+ r="4"
1374
+ stroke-width="2"
1375
+ [attr.cx]="geometry.end.x"
1376
+ [attr.cy]="geometry.end.y"
1377
+ />
1378
+
1379
+ @for (
1380
+ tick of ticks();
1381
+ track tick.t;
1382
+ let index = $index;
1383
+ let count = $count;
1384
+ let first = $first;
1385
+ let last = $last
1386
+ ) {
1387
+ <text
1388
+ class="text-caption fill-(--subtle-foreground)"
1389
+ data-chart-axis-tick
1390
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1391
+ [attr.x]="tick.x"
1392
+ [attr.y]="effectiveHeight() - 4"
1393
+ [class.max-sm:hidden]="!coreTick(index, count)"
1394
+ >
1395
+ {{ axisFormat()(tick.t) }}
1396
+ </text>
1397
+ }
1398
+
1399
+ @if (active(); as point) {
1400
+ <line
1401
+ class="stroke-(--border)"
1402
+ data-chart-crosshair
1403
+ stroke-width="1"
1404
+ y1="0"
1405
+ [attr.x1]="point.x"
1406
+ [attr.x2]="point.x"
1407
+ [attr.y2]="effectiveHeight()"
1408
+ />
1409
+ <circle
1410
+ class="fill-(--foreground) stroke-(--card)"
1411
+ data-chart-active
1412
+ r="4"
1413
+ stroke-width="2"
1414
+ [attr.cx]="point.x"
1415
+ [attr.cy]="point.y"
1416
+ />
1417
+ }
1418
+ }
1419
+ </svg>
1420
+
1421
+ @if (active(); as point) {
1422
+ <div
1423
+ 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)]"
1424
+ data-testid="chart-tooltip"
1425
+ role="status"
1426
+ [style]="tooltipStyle(point)"
1427
+ >
1428
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1429
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1430
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1431
+ </div>
1432
+ }
1433
+
1434
+ @if (points().length > 0) {
1435
+ <table class="sr-only">
1436
+ <caption>
1437
+ {{
1438
+ label()
1439
+ }}
1440
+ </caption>
1441
+ <thead>
1442
+ <tr>
1443
+ <th scope="col">{{ timeColumnLabel() }}</th>
1444
+ <th scope="col">{{ valueColumnLabel() }}</th>
1445
+ </tr>
1446
+ </thead>
1447
+ <tbody>
1448
+ @for (point of points(); track point.t) {
1449
+ <tr>
1450
+ <td>{{ timeFormat()(point.t) }}</td>
1451
+ <td>{{ valueFormat()(point.v) }}</td>
1452
+ </tr>
1453
+ }
1454
+ </tbody>
1455
+ </table>
1456
+ }
1457
+ </div>
1458
+ `,
1459
+ host: {
1460
+ class: 'block h-full',
1461
+ },
1462
+ }]
1463
+ }], 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 }] }] } });
1464
+
1465
+ 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';
1466
+ 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';
1467
+ const nextId = (() => {
1468
+ let count = 0;
1469
+ return () => `ui-menu-${++count}`;
1470
+ })();
1471
+ /**
1472
+ * One action of a `ui-menu`; `destructive` paints it in `--negative`.
1473
+ *
1474
+ * @example
1475
+ * <button uiMenuItem destructive (click)="remove()">Delete the line</button>
1476
+ */
1477
+ class UiMenuItem {
1478
+ destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1479
+ #host = inject(ElementRef).nativeElement;
1480
+ #menu = inject(UiMenu);
1481
+ classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
1482
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1483
+ focus() {
1484
+ this.#host.focus();
1485
+ }
1486
+ hasFocus() {
1487
+ return this.#host === document.activeElement;
1488
+ }
1489
+ onClick() {
1490
+ this.#menu.close();
1491
+ }
1492
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1493
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuItem, isStandalone: true, selector: "button[uiMenuItem]", inputs: { destructive: { classPropertyName: "destructive", publicName: "destructive", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
1494
+ }
1495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, decorators: [{
1496
+ type: Directive,
1497
+ args: [{
1498
+ selector: 'button[uiMenuItem]',
1499
+ host: {
1500
+ role: 'menuitem',
1501
+ tabindex: '-1',
1502
+ '[class]': 'classes()',
1503
+ '(click)': 'onClick()',
1504
+ },
1505
+ }]
1506
+ }], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
1507
+ /**
1508
+ * Secondary actions menu, opened from its trigger through the native Popover API.
1509
+ *
1510
+ * @example
1511
+ * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
1512
+ * <ui-menu #menu label="Line actions">
1513
+ * <button uiMenuItem (click)="edit()">Edit</button>
1514
+ * <button uiMenuItem destructive (click)="remove()">Delete the line</button>
1515
+ * </ui-menu>
1516
+ */
1517
+ class UiMenu {
1518
+ label = input.required(/* @ts-ignore */
1519
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1520
+ menuId = nextId();
1521
+ isOpen = signal(false, /* @ts-ignore */
1522
+ ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1523
+ classes = MENU_CLASSES;
1524
+ items = contentChildren(UiMenuItem, /* @ts-ignore */
1525
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1526
+ #host = inject(ElementRef).nativeElement;
1527
+ #trigger = null;
1528
+ open(trigger) {
1529
+ this.#trigger = trigger;
1530
+ this.isOpen.set(true);
1531
+ if (typeof this.#host.showPopover === 'function') {
1532
+ this.#host.showPopover();
1533
+ }
1534
+ this.#position(trigger);
1535
+ this.items()[0]?.focus();
1536
+ }
1537
+ close() {
1538
+ const wasOpen = this.isOpen();
1539
+ this.#applyClosed();
1540
+ if (wasOpen && typeof this.#host.hidePopover === 'function') {
1541
+ this.#host.hidePopover();
1542
+ }
1543
+ }
1544
+ #applyClosed() {
1545
+ this.isOpen.set(false);
1546
+ this.#trigger?.focus();
1547
+ this.#trigger = null;
1548
+ }
1549
+ onToggle(event) {
1550
+ if (event.newState === 'closed') {
1551
+ this.#applyClosed();
1552
+ }
1553
+ }
1554
+ onKeydown(event) {
1555
+ if (event.key === 'Tab' || event.key === 'Escape') {
1556
+ event.preventDefault();
1557
+ this.close();
1558
+ return;
1559
+ }
1560
+ const items = this.items();
1561
+ const currentIndex = items.findIndex((item) => item.hasFocus());
1562
+ const lastIndex = items.length - 1;
1563
+ let nextIndex;
1564
+ switch (event.key) {
1565
+ case 'ArrowDown':
1566
+ nextIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;
1567
+ break;
1568
+ case 'ArrowUp':
1569
+ nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;
1570
+ break;
1571
+ case 'Home':
1572
+ nextIndex = 0;
1573
+ break;
1574
+ case 'End':
1575
+ nextIndex = lastIndex;
1576
+ break;
1577
+ }
1578
+ if (nextIndex === undefined) {
1579
+ return;
1580
+ }
1581
+ event.preventDefault();
1582
+ items[nextIndex]?.focus();
1583
+ }
1584
+ #position(trigger) {
1585
+ const margin = 8;
1586
+ const triggerRect = trigger.getBoundingClientRect();
1587
+ const menuRect = this.#host.getBoundingClientRect();
1588
+ const fitsBelow = window.innerHeight - triggerRect.bottom >= menuRect.height + margin;
1589
+ const top = fitsBelow ? triggerRect.bottom + 4 : Math.max(margin, triggerRect.top - menuRect.height - 4);
1590
+ const left = Math.min(Math.max(triggerRect.right - menuRect.width, margin), window.innerWidth - menuRect.width - margin);
1591
+ this.#host.style.position = 'fixed';
1592
+ this.#host.style.top = `${Math.min(top, window.innerHeight - menuRect.height - margin)}px`;
1593
+ this.#host.style.left = `${left}px`;
1594
+ this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
1595
+ }
1596
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1597
+ 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 });
1598
+ }
1599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
1600
+ type: Component,
1601
+ args: [{
1602
+ selector: 'ui-menu',
1603
+ template: '<ng-content />',
1604
+ host: {
1605
+ role: 'menu',
1606
+ popover: 'auto',
1607
+ '[id]': 'menuId',
1608
+ '[class]': 'classes',
1609
+ '[attr.aria-label]': 'label()',
1610
+ '(toggle)': 'onToggle($event)',
1611
+ '(keydown)': 'onKeydown($event)',
1612
+ },
1613
+ }]
1614
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
1615
+ /**
1616
+ * Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
1617
+ * `aria-controls` on it.
1618
+ *
1619
+ * @example
1620
+ * <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
1621
+ */
1622
+ class UiMenuTrigger {
1623
+ uiMenuTrigger = input.required(/* @ts-ignore */
1624
+ ...(ngDevMode ? [{ debugName: "uiMenuTrigger" }] : /* istanbul ignore next */ []));
1625
+ #host = inject(ElementRef).nativeElement;
1626
+ toggle() {
1627
+ const menu = this.uiMenuTrigger();
1628
+ if (menu.isOpen()) {
1629
+ menu.close();
1630
+ }
1631
+ else {
1632
+ menu.open(this.#host);
1633
+ }
1634
+ }
1635
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1636
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuTrigger, isStandalone: true, selector: "[uiMenuTrigger]", inputs: { uiMenuTrigger: { classPropertyName: "uiMenuTrigger", publicName: "uiMenuTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "menu" }, listeners: { "click": "toggle()" }, properties: { "attr.aria-expanded": "uiMenuTrigger().isOpen()", "attr.aria-controls": "uiMenuTrigger().menuId" } }, ngImport: i0 });
1637
+ }
1638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, decorators: [{
1639
+ type: Directive,
1640
+ args: [{
1641
+ selector: '[uiMenuTrigger]',
1642
+ host: {
1643
+ 'aria-haspopup': 'menu',
1644
+ '[attr.aria-expanded]': 'uiMenuTrigger().isOpen()',
1645
+ '[attr.aria-controls]': 'uiMenuTrigger().menuId',
1646
+ '(click)': 'toggle()',
1647
+ },
1648
+ }]
1649
+ }], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
1650
+
611
1651
  /** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
612
1652
  const METER_TONES = [1, 2, 3, 4, 5, 6];
613
1653
  const TONE_CLASSES = {
@@ -673,11 +1713,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
673
1713
  }]
674
1714
  }], 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 }] }] } });
675
1715
 
676
- const OPTION_CLASSES =
677
- // The visible track stays the handoff's 36px; the hit area still reaches --row-min (44px touch,
678
- // 40px pointer: fine) through an ::after enlarged only vertically, centered on the option, and
679
- // constrained to its own horizontal bounds so it never reaches into a neighboring option.
680
- "relative z-10 min-h-9 flex-1 cursor-pointer 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)";
1716
+ 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)';
1717
+ const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
1718
+ /**
1719
+ * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
1720
+ *
1721
+ * @example
1722
+ * <a ui-nav-item [active]="url === '/'" routerLink="/">
1723
+ * <lucide-icon navIcon name="house" />
1724
+ * Portfolio
1725
+ * </a>
1726
+ */
1727
+ class UiNavItem {
1728
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1729
+ classes = computed(() => `${BASE_CLASSES$1}${this.active() ? ` ${ACTIVE_CLASSES}` : ''}`, /* @ts-ignore */
1730
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1732
+ 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 });
1733
+ }
1734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, decorators: [{
1735
+ type: Component,
1736
+ args: [{
1737
+ selector: 'a[ui-nav-item]',
1738
+ template: '<ng-content select="[navIcon]" /><ng-content />',
1739
+ host: {
1740
+ '[class]': 'classes()',
1741
+ '[attr.aria-current]': "active() ? 'page' : null",
1742
+ },
1743
+ }]
1744
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1745
+
1746
+ 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)';
1747
+ const SELECTED_CLASSES$1 = 'bg-(--soft)';
1748
+ /**
1749
+ * Clickable row of a list: a holding, an envelope, an account.
1750
+ *
1751
+ * @example
1752
+ * <a ui-row [selected]="holding.id === openId()" [routerLink]="['/holdings', holding.id]">
1753
+ * {{ holding.name }}
1754
+ * </a>
1755
+ */
1756
+ class UiRow {
1757
+ selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1758
+ classes = computed(() => `${BASE_CLASSES}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
1759
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1760
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
1761
+ 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 });
1762
+ }
1763
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
1764
+ type: Component,
1765
+ args: [{
1766
+ selector: 'a[ui-row], button[ui-row]',
1767
+ template: '<ng-content />',
1768
+ host: {
1769
+ '[class]': 'classes()',
1770
+ '[attr.aria-current]': "selected() ? 'true' : null",
1771
+ },
1772
+ }]
1773
+ }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }] } });
1774
+
1775
+ 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)";
681
1776
  const SELECTED_CLASSES = 'text-(--foreground)';
682
1777
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
683
1778
  /**
@@ -696,7 +1791,6 @@ class UiSegmented {
696
1791
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
697
1792
  radios = viewChildren('radio', /* @ts-ignore */
698
1793
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
699
- // 1-based so 0 (falsy) reliably means "no match", letting @if skip the thumb entirely.
700
1794
  thumbIndex = computed(() => {
701
1795
  const index = this.options().findIndex((option) => option.value === this.value());
702
1796
  return index === -1 ? 0 : index + 1;
@@ -864,16 +1958,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
864
1958
  /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
865
1959
  const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
866
1960
  const LAST_LINE_WIDTH = '62%';
867
- // Declared here, not in tokens.css (a pure token sheet holds no element styles or keyframes).
868
1961
  const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
869
1962
  const BAR_CLASSES = 'block rounded-control bg-(--muted)';
870
1963
  /**
871
- * Decorative placeholder for content that is still loading, at the shape of the content it stands
872
- * in for. Deliberately hidden from assistive technology: the calling screen keeps its own
873
- * `role="status"` sentence.
874
- *
875
- * Pulses opacity from 1 to 0.45 and back over `--pulse-duration`, which `tokens.css` sets to `0ms`
876
- * under `prefers-reduced-motion: reduce`: the pulse stops there with no branching in this component.
1964
+ * Decorative, pulsing placeholder for content that is still loading, at the shape of the content
1965
+ * it stands in for.
877
1966
  *
878
1967
  * @example
879
1968
  * <ui-skeleton shape="figure" />
@@ -1011,6 +2100,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1011
2100
  }]
1012
2101
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1013
2102
 
2103
+ 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]";
2104
+ /**
2105
+ * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
2106
+ *
2107
+ * @example
2108
+ * <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
2109
+ */
2110
+ class UiSwitch {
2111
+ classes = CLASSES;
2112
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2113
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiSwitch, isStandalone: true, selector: "input[type=checkbox][uiSwitch]", host: { attributes: { "role": "switch" }, properties: { "class": "classes" } }, ngImport: i0 });
2114
+ }
2115
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, decorators: [{
2116
+ type: Directive,
2117
+ args: [{
2118
+ selector: 'input[type=checkbox][uiSwitch]',
2119
+ host: {
2120
+ role: 'switch',
2121
+ '[class]': 'classes',
2122
+ },
2123
+ }]
2124
+ }] });
2125
+
1014
2126
  /** Breakpoints a secondary column can be held back until. */
1015
2127
  const CELL_BREAKPOINTS = ['md', 'lg'];
1016
2128
  const FROM_CLASSES = {
@@ -1105,6 +2217,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1105
2217
  }]
1106
2218
  }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
1107
2219
 
2220
+ 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)';
2221
+ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
2222
+ /**
2223
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
2224
+ *
2225
+ * @example
2226
+ * <nav ui-tab-bar>
2227
+ * <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
2228
+ * </nav>
2229
+ */
2230
+ class UiTabBar {
2231
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2232
+ 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 });
2233
+ }
2234
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
2235
+ type: Component,
2236
+ args: [{
2237
+ selector: 'nav[ui-tab-bar]',
2238
+ template: '<ng-content />',
2239
+ host: {
2240
+ 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)]',
2241
+ },
2242
+ }]
2243
+ }] });
2244
+ /**
2245
+ * One destination of `ui-tab-bar`.
2246
+ *
2247
+ * @example
2248
+ * <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
2249
+ */
2250
+ class UiTab {
2251
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2252
+ classes = computed(() => `${TAB_CLASSES}${this.active() ? ` ${ACTIVE_TAB_CLASSES}` : ''}`, /* @ts-ignore */
2253
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2254
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
2255
+ 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: `
2256
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2257
+ <span class="text-caption" data-tab-label><ng-content /></span>
2258
+ `, isInline: true });
2259
+ }
2260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
2261
+ type: Component,
2262
+ args: [{
2263
+ selector: 'a[ui-tab]',
2264
+ template: `
2265
+ <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2266
+ <span class="text-caption" data-tab-label><ng-content /></span>
2267
+ `,
2268
+ host: {
2269
+ '[class]': 'classes()',
2270
+ '[attr.aria-current]': "active() ? 'page' : null",
2271
+ },
2272
+ }]
2273
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
2274
+
1108
2275
  /*
1109
2276
  * Public API surface of the ui library.
1110
2277
  */
@@ -1113,5 +2280,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1113
2280
  * Generated bundle index. Do not edit.
1114
2281
  */
1115
2282
 
1116
- export { AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMeter, UiSegmented, UiSelect, UiSkeleton, UiStat, UiTable, UiTd, UiTextarea, UiTh, UiTr };
2283
+ export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
1117
2284
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map