@joanroucoux/cairn-ui 0.5.1 → 0.6.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,7 +1,7 @@
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 { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, forwardRef, Directive, model, viewChildren, afterRenderEffect, contentChild, viewChild, afterNextRender, effect, contentChildren } from '@angular/core';
4
3
  import { scaleLinear } from 'd3-scale';
4
+ import { curveMonotoneX, line } from 'd3-shape';
5
5
 
6
6
  const MINUS = '−';
7
7
  const EM_DASH = '—';
@@ -108,9 +108,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
108
108
  }]
109
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
110
 
111
+ const ALERT_VARIANTS = ['error', 'warning'];
112
+ const BASE_CLASSES$7 = 'flex items-start gap-2.5 text-left text-label';
113
+ const VARIANT_CLASSES$3 = {
114
+ error: 'rounded-control bg-(--card) px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--border)]',
115
+ warning: 'text-(--foreground)',
116
+ };
117
+ /**
118
+ * Inline message explaining a failure or a consequence: an icon, an optional heading and a text.
119
+ *
120
+ * @example
121
+ * <ui-alert heading="Sign-in failed">Try again.</ui-alert>
122
+ */
123
+ class UiAlert {
124
+ variant = input('error', /* @ts-ignore */
125
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
126
+ heading = input(/* @ts-ignore */
127
+ ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
128
+ classes = computed(() => `${BASE_CLASSES$7} ${VARIANT_CLASSES$3[this.variant()]}`, /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
130
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, deps: [], target: i0.ɵɵFactoryTarget.Component });
131
+ 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: `
132
+ @if (variant() === 'warning') {
133
+ <svg
134
+ aria-hidden="true"
135
+ class="mt-px block flex-none stroke-(--negative)"
136
+ fill="none"
137
+ height="18"
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="1.75"
141
+ viewBox="0 0 24 24"
142
+ width="18"
143
+ >
144
+ <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" />
145
+ <path d="M12 9v4" />
146
+ <path d="M12 17h.01" />
147
+ </svg>
148
+ } @else {
149
+ <svg
150
+ aria-hidden="true"
151
+ class="block flex-none stroke-(--negative)"
152
+ fill="none"
153
+ height="18"
154
+ stroke-linecap="round"
155
+ stroke-linejoin="round"
156
+ stroke-width="1.75"
157
+ viewBox="0 0 24 24"
158
+ width="18"
159
+ [class.mt-px]="heading()"
160
+ >
161
+ <circle cx="12" cy="12" r="10" />
162
+ <line x1="12" x2="12" y1="8" y2="12" />
163
+ <line x1="12" x2="12.01" y1="16" y2="16" />
164
+ </svg>
165
+ }
166
+ <div class="flex flex-col gap-0.5">
167
+ @if (heading()) {
168
+ <span class="font-medium">{{ heading() }}</span>
169
+ }
170
+ <span class="text-pretty" [class.text-(--muted-foreground)]="heading()"><ng-content /></span>
171
+ </div>
172
+ `, isInline: true });
173
+ }
174
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, decorators: [{
175
+ type: Component,
176
+ args: [{
177
+ selector: 'ui-alert',
178
+ template: `
179
+ @if (variant() === 'warning') {
180
+ <svg
181
+ aria-hidden="true"
182
+ class="mt-px block flex-none stroke-(--negative)"
183
+ fill="none"
184
+ height="18"
185
+ stroke-linecap="round"
186
+ stroke-linejoin="round"
187
+ stroke-width="1.75"
188
+ viewBox="0 0 24 24"
189
+ width="18"
190
+ >
191
+ <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" />
192
+ <path d="M12 9v4" />
193
+ <path d="M12 17h.01" />
194
+ </svg>
195
+ } @else {
196
+ <svg
197
+ aria-hidden="true"
198
+ class="block flex-none stroke-(--negative)"
199
+ fill="none"
200
+ height="18"
201
+ stroke-linecap="round"
202
+ stroke-linejoin="round"
203
+ stroke-width="1.75"
204
+ viewBox="0 0 24 24"
205
+ width="18"
206
+ [class.mt-px]="heading()"
207
+ >
208
+ <circle cx="12" cy="12" r="10" />
209
+ <line x1="12" x2="12" y1="8" y2="12" />
210
+ <line x1="12" x2="12.01" y1="16" y2="16" />
211
+ </svg>
212
+ }
213
+ <div class="flex flex-col gap-0.5">
214
+ @if (heading()) {
215
+ <span class="font-medium">{{ heading() }}</span>
216
+ }
217
+ <span class="text-pretty" [class.text-(--muted-foreground)]="heading()"><ng-content /></span>
218
+ </div>
219
+ `,
220
+ host: {
221
+ '[class]': 'classes()',
222
+ '[attr.role]': "variant() === 'warning' ? 'status' : 'alert'",
223
+ },
224
+ }]
225
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }] } });
226
+
111
227
  /** Available async block states. `AsyncState` is derived from this tuple. */
112
228
  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)';
229
+ const RETRY_BUTTON_CLASSES = 'mt-2 relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) min-h-(--row-min) pl-3 pr-4 text-label bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
114
230
  /**
115
231
  * The three states of a block that depends on a service call, next to the ready content.
116
232
  *
@@ -141,12 +257,25 @@ class UiAsync {
141
257
  @case ('error') {
142
258
  <div
143
259
  aria-live="polite"
144
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
260
+ class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-4"
145
261
  role="alert"
146
262
  >
147
263
  <span class="text-body font-medium">{{ errorTitle() }}</span>
148
264
  <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
149
265
  <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
266
+ <svg
267
+ aria-hidden="true"
268
+ class="block flex-none fill-none stroke-current"
269
+ height="18"
270
+ stroke-linecap="round"
271
+ stroke-linejoin="round"
272
+ stroke-width="1.75"
273
+ viewBox="0 0 24 24"
274
+ width="18"
275
+ >
276
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
277
+ <path d="M21 3v5h-5" />
278
+ </svg>
150
279
  {{ retryLabel() }}
151
280
  </button>
152
281
  </div>
@@ -172,12 +301,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
172
301
  @case ('error') {
173
302
  <div
174
303
  aria-live="polite"
175
- class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
304
+ class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-4"
176
305
  role="alert"
177
306
  >
178
307
  <span class="text-body font-medium">{{ errorTitle() }}</span>
179
308
  <span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
180
309
  <button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
310
+ <svg
311
+ aria-hidden="true"
312
+ class="block flex-none fill-none stroke-current"
313
+ height="18"
314
+ stroke-linecap="round"
315
+ stroke-linejoin="round"
316
+ stroke-width="1.75"
317
+ viewBox="0 0 24 24"
318
+ width="18"
319
+ >
320
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
321
+ <path d="M21 3v5h-5" />
322
+ </svg>
181
323
  {{ retryLabel() }}
182
324
  </button>
183
325
  </div>
@@ -197,11 +339,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
197
339
  }], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], errorTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTitle", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], retryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "retryLabel", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
198
340
 
199
341
  /** 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 = {
342
+ const AVATAR_SIZES = ['sm', 'md', 'lg'];
343
+ const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill bg-(--muted) font-medium tabular-nums';
344
+ const SIZE_CLASSES$3 = {
203
345
  sm: 'size-8 text-caption',
204
- md: 'size-11 text-label',
346
+ md: 'size-[34px] text-label',
347
+ lg: 'size-14 text-title',
205
348
  };
206
349
  /**
207
350
  * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
@@ -216,7 +359,7 @@ class UiAvatar {
216
359
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
217
360
  size = input('md', /* @ts-ignore */
218
361
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
219
- classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
362
+ classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$3[this.size()]}`, /* @ts-ignore */
220
363
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
221
364
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
222
365
  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 });
@@ -236,7 +379,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
236
379
 
237
380
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
238
381
  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';
382
+ const BADGE_SIZES = ['md', 'sm'];
383
+ const BASE_CLASSES$5 = 'inline-flex items-center rounded-pill text-caption font-medium';
384
+ const SIZE_CLASSES$2 = {
385
+ md: 'h-6 px-2.5',
386
+ sm: 'h-[22px] px-2',
387
+ };
240
388
  const VARIANT_CLASSES$2 = {
241
389
  neutral: 'bg-(--muted) text-(--foreground)',
242
390
  outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
@@ -250,10 +398,12 @@ const VARIANT_CLASSES$2 = {
250
398
  class UiBadge {
251
399
  variant = input('neutral', /* @ts-ignore */
252
400
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
253
- classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
401
+ size = input('md', /* @ts-ignore */
402
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
403
+ classes = computed(() => `${BASE_CLASSES$5} ${SIZE_CLASSES$2[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
254
404
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
255
405
  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 });
406
+ 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
407
  }
258
408
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, decorators: [{
259
409
  type: Component,
@@ -264,24 +414,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
264
414
  '[class]': 'classes()',
265
415
  },
266
416
  }]
267
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
417
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
268
418
 
269
419
  /** Available button variants. `ButtonVariant` is derived from this tuple. */
270
420
  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';
421
+ /** Available button sizes. */
422
+ const BUTTON_SIZES = ['md', 'lg', 'xl', 'xxl', 'icon'];
423
+ const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
274
424
  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',
425
+ primary: 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)',
426
+ outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow) active:bg-(--soft)',
427
+ ghost: 'bg-transparent text-(--foreground) hover:bg-(--glow) active:bg-(--soft)',
428
+ destructive: 'bg-(--destructive) text-(--destructive-foreground)',
279
429
  };
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',
430
+ const SIZE_CLASSES$1 = {
431
+ md: 'min-h-(--row-min) gap-2 px-4 text-label has-[>svg:first-child]:pl-3',
432
+ lg: 'h-11 gap-2 px-[18px] text-label',
433
+ xl: 'h-[50px] gap-2 px-6 text-body',
434
+ xxl: 'h-[52px] gap-2.5 px-6 text-body',
435
+ icon: 'size-(--row-min) gap-2 p-0',
285
436
  };
286
437
  /**
287
438
  * Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
@@ -296,7 +447,7 @@ class UiButton {
296
447
  size = input('md', /* @ts-ignore */
297
448
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
298
449
  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 */
450
+ classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES$1[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
300
451
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
301
452
  constructor() {
302
453
  const host = inject(ElementRef).nativeElement;
@@ -344,16 +495,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
344
495
  /** Available card variants. `CardVariant` is derived from this tuple. */
345
496
  const CARD_VARIANTS = ['default', 'elevated'];
346
497
  /** 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';
498
+ const CARD_PADDINGS = ['none', 'sm', 'md', 'list'];
499
+ const BASE_CLASSES$3 = 'block rounded-container';
349
500
  const VARIANT_CLASSES = {
350
- default: 'bg-(--card) border-(--border) text-(--card-foreground)',
351
- elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
501
+ default: 'bg-(--card) shadow-[inset_0_0_0_1px_var(--border)] text-(--card-foreground)',
502
+ elevated: 'bg-(--elevated) shadow-[inset_0_0_0_1px_var(--hairline)] text-(--foreground)',
352
503
  };
353
- const PADDING_CLASSES = {
504
+ const PADDING_CLASSES$1 = {
354
505
  none: 'p-0',
355
506
  sm: 'p-3',
356
- md: 'p-5',
507
+ md: 'p-(--inset-card)',
508
+ list: 'px-2 py-3',
357
509
  };
358
510
  /**
359
511
  * Surface that groups related content: a summary block, a table, a side panel.
@@ -366,7 +518,7 @@ class UiCard {
366
518
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
367
519
  padding = input('md', /* @ts-ignore */
368
520
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
369
- classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
521
+ classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES$1[this.padding()]}`, /* @ts-ignore */
370
522
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
371
523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
372
524
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiCard, isStandalone: true, selector: "ui-card", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
@@ -392,6 +544,244 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
392
544
  */
393
545
  const UI_CONTROL = new InjectionToken('UI_CONTROL');
394
546
 
547
+ /** Available control sizes. `md` is the 44/40px touch target. */
548
+ const CONTROL_SIZES = ['sm', 'md'];
549
+ const CONTROL_BASE_CLASSES = 'w-full rounded-control px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-50';
550
+ const CONTROL_SURFACES = ['background', 'card'];
551
+ const CONTROL_SURFACE_CLASSES = {
552
+ background: 'bg-(--background)',
553
+ card: 'bg-(--card)',
554
+ };
555
+ const CONTROL_SIZE_CLASSES = {
556
+ sm: 'h-9',
557
+ md: 'min-h-(--row-min)',
558
+ };
559
+ /**
560
+ * Styled native text input. A directive (not a wrapper component) so the host
561
+ * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
562
+ * and every native attribute keep working without any plumbing.
563
+ *
564
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
565
+ * it can show the message without any binding here.
566
+ *
567
+ * @example
568
+ * <input uiInput type="email" placeholder="you@example.com" />
569
+ */
570
+ class UiInput {
571
+ size = input('md', /* @ts-ignore */
572
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
573
+ surface = input('background', /* @ts-ignore */
574
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
575
+ errors = input([], /* @ts-ignore */
576
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
577
+ touched = input(false, /* @ts-ignore */
578
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
579
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
580
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
581
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
582
+ 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 });
583
+ }
584
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
585
+ type: Directive,
586
+ args: [{
587
+ selector: 'input[uiInput]',
588
+ host: {
589
+ '[class]': 'classes()',
590
+ },
591
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }],
592
+ }]
593
+ }], 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 }] }] } });
594
+ /**
595
+ * Styled native textarea. Lives beside the input because it shares every one of its
596
+ * classes; it takes no size, since a textarea grows from a floor instead of sitting
597
+ * at a fixed height.
598
+ *
599
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
600
+ * it can show the message without any binding here.
601
+ *
602
+ * @example
603
+ * <textarea uiTextarea maxlength="280"></textarea>
604
+ */
605
+ class UiTextarea {
606
+ errors = input([], /* @ts-ignore */
607
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
608
+ touched = input(false, /* @ts-ignore */
609
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
610
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
611
+ 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 });
612
+ }
613
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
614
+ type: Directive,
615
+ args: [{
616
+ selector: 'textarea[uiTextarea]',
617
+ host: {
618
+ class: `${CONTROL_BASE_CLASSES} bg-(--background) min-h-24 py-2`,
619
+ },
620
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],
621
+ }]
622
+ }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
623
+
624
+ const CHIP_CLASSES = 'h-10 px-3.5 pointer-fine:h-8 pointer-fine:px-3 rounded-pill text-label font-medium cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)';
625
+ const SELECTED_CLASSES$2 = 'bg-(--primary) text-(--primary-foreground)';
626
+ const UNSELECTED_CLASSES$1 = 'text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
627
+ const nextId$3 = (() => {
628
+ let count = 0;
629
+ return () => `ui-choice-chips-${++count}`;
630
+ })();
631
+ /**
632
+ * Exclusive choice among a handful of named options, shown as wrapping pills: an account envelope,
633
+ * a category, a frequency. Binds to Signal Forms with `[formField]`.
634
+ *
635
+ * @example
636
+ * <ui-choice-chips label="Envelope" [options]="envelopes" [(value)]="envelope" />
637
+ *
638
+ * @example
639
+ * <ui-choice-chips label="Envelope" [options]="envelopes" [formField]="form.envelope" />
640
+ */
641
+ class UiChoiceChips {
642
+ options = input.required(/* @ts-ignore */
643
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
644
+ value = model.required(/* @ts-ignore */
645
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
646
+ label = input(/* @ts-ignore */
647
+ ...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
648
+ ariaLabel = input(/* @ts-ignore */
649
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
650
+ ariaLabelledby = input(/* @ts-ignore */
651
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []));
652
+ surface = input('background', /* @ts-ignore */
653
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
654
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
655
+ errors = input([], /* @ts-ignore */
656
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
657
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
658
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
659
+ touched = input(false, /* @ts-ignore */
660
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
661
+ touch = output();
662
+ labelId = nextId$3();
663
+ #host = inject(ElementRef);
664
+ radios = viewChildren('radio', /* @ts-ignore */
665
+ ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
666
+ labelledBy = computed(() => this.ariaLabelledby() ?? (this.label() ? this.labelId : null), /* @ts-ignore */
667
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
668
+ chipClasses(optionValue) {
669
+ const state = optionValue === this.value()
670
+ ? SELECTED_CLASSES$2
671
+ : `${CONTROL_SURFACE_CLASSES[this.surface()]} ${UNSELECTED_CLASSES$1}`;
672
+ return `${CHIP_CLASSES} ${state}`;
673
+ }
674
+ tabIndexFor(optionValue) {
675
+ const opts = this.options();
676
+ const tabStopValue = opts.some((option) => option.value === this.value()) ? this.value() : opts[0]?.value;
677
+ return optionValue === tabStopValue ? 0 : -1;
678
+ }
679
+ select(optionValue) {
680
+ this.value.set(optionValue);
681
+ }
682
+ onFocusOut(event) {
683
+ if (!this.#host.nativeElement.contains(event.relatedTarget)) {
684
+ this.touch.emit();
685
+ }
686
+ }
687
+ onKeydown(event, index) {
688
+ const opts = this.options();
689
+ const target = {
690
+ ArrowRight: index + 1,
691
+ ArrowDown: index + 1,
692
+ ArrowLeft: index - 1,
693
+ ArrowUp: index - 1,
694
+ Home: 0,
695
+ End: opts.length - 1,
696
+ }[event.key];
697
+ if (target === undefined) {
698
+ return;
699
+ }
700
+ event.preventDefault();
701
+ const nextIndex = ((target % opts.length) + opts.length) % opts.length;
702
+ this.value.set(opts[nextIndex].value);
703
+ this.radios()[nextIndex].nativeElement.focus();
704
+ }
705
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiChoiceChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
706
+ 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: `
707
+ <div
708
+ class="flex flex-col gap-1.5"
709
+ role="radiogroup"
710
+ [attr.aria-invalid]="invalid() || null"
711
+ [attr.aria-label]="ariaLabel() ?? null"
712
+ [attr.aria-labelledby]="labelledBy()"
713
+ [attr.aria-required]="required() || null"
714
+ (focusout)="onFocusOut($event)"
715
+ >
716
+ @if (label()) {
717
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
718
+ label()
719
+ }}</span>
720
+ }
721
+
722
+ <div class="flex flex-wrap gap-1.5">
723
+ @for (option of options(); track option.value; let index = $index) {
724
+ <button
725
+ #radio
726
+ role="radio"
727
+ type="button"
728
+ [attr.aria-checked]="option.value === value()"
729
+ [attr.tabindex]="tabIndexFor(option.value)"
730
+ [class]="chipClasses(option.value)"
731
+ [disabled]="disabled()"
732
+ (click)="select(option.value)"
733
+ (keydown)="onKeydown($event, index)"
734
+ >
735
+ {{ option.label }}
736
+ </button>
737
+ }
738
+ </div>
739
+ </div>
740
+ `, isInline: true });
741
+ }
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiChoiceChips, decorators: [{
743
+ type: Component,
744
+ args: [{
745
+ selector: 'ui-choice-chips',
746
+ template: `
747
+ <div
748
+ class="flex flex-col gap-1.5"
749
+ role="radiogroup"
750
+ [attr.aria-invalid]="invalid() || null"
751
+ [attr.aria-label]="ariaLabel() ?? null"
752
+ [attr.aria-labelledby]="labelledBy()"
753
+ [attr.aria-required]="required() || null"
754
+ (focusout)="onFocusOut($event)"
755
+ >
756
+ @if (label()) {
757
+ <span class="text-label leading-[17px] font-medium text-(--muted-foreground)" [id]="labelId">{{
758
+ label()
759
+ }}</span>
760
+ }
761
+
762
+ <div class="flex flex-wrap gap-1.5">
763
+ @for (option of options(); track option.value; let index = $index) {
764
+ <button
765
+ #radio
766
+ role="radio"
767
+ type="button"
768
+ [attr.aria-checked]="option.value === value()"
769
+ [attr.tabindex]="tabIndexFor(option.value)"
770
+ [class]="chipClasses(option.value)"
771
+ [disabled]="disabled()"
772
+ (click)="select(option.value)"
773
+ (keydown)="onKeydown($event, index)"
774
+ >
775
+ {{ option.label }}
776
+ </button>
777
+ }
778
+ </div>
779
+ </div>
780
+ `,
781
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiChoiceChips) }],
782
+ }]
783
+ }], 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 }] }] } });
784
+
395
785
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
396
786
  const DELTA_EMPHASES = ['text', 'pill'];
397
787
  const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
@@ -402,7 +792,7 @@ const TEXT_CLASSES = {
402
792
  neutral: 'text-(--muted-foreground)',
403
793
  unknown: 'text-(--subtle-foreground)',
404
794
  };
405
- const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
795
+ const PILL_BACKGROUND_CLASS = 'bg-(--soft)';
406
796
  /**
407
797
  * Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
408
798
  *
@@ -470,24 +860,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
470
860
  }]
471
861
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unknownLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unknownLabel", required: true }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }] } });
472
862
 
473
- /** Available dialog widths. `DialogWidth` is derived from this tuple. */
863
+ /** Named dialog widths; any other CSS length is accepted as is. `DialogWidth` is derived from this tuple. */
474
864
  const DIALOG_WIDTHS = ['md', 'lg'];
475
- const WIDTH_CLASSES = {
476
- md: 'max-w-md',
477
- lg: 'max-w-[560px]',
865
+ const WIDTH_VALUES = {
866
+ md: '28rem',
867
+ lg: '560px',
868
+ };
869
+ /** Spacing presets. `DialogLayout` is derived from this tuple. */
870
+ const DIALOG_LAYOUTS = ['trade', 'form', 'list', 'confirm'];
871
+ const LAYOUTS = {
872
+ trade: {
873
+ dialog: '',
874
+ header: 'items-start lg:pt-6',
875
+ title: 'pt-1 lg:pt-0',
876
+ cross: 'lg:-mt-1.5',
877
+ icon: 'lg:size-5',
878
+ handle: 'pt-[7.5px]',
879
+ body: 'pt-3 pb-4 lg:pt-5 lg:pb-5',
880
+ footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
881
+ },
882
+ form: {
883
+ dialog: '',
884
+ header: 'items-center lg:pt-6',
885
+ title: '',
886
+ cross: '',
887
+ icon: 'lg:size-[22px]',
888
+ handle: 'pt-[7.5px]',
889
+ body: 'pt-2 pb-5 lg:pt-4 lg:pb-5',
890
+ footer: 'max-lg:pb-[calc(12px+env(safe-area-inset-bottom))] lg:pb-5',
891
+ },
892
+ list: {
893
+ dialog: 'max-lg:h-[calc(100dvh-58px)] lg:mt-24 lg:mb-auto lg:max-h-[min(760px,calc(100dvh-96px-32px))]',
894
+ header: 'items-center lg:pt-5',
895
+ title: '',
896
+ cross: '',
897
+ icon: 'lg:size-[22px]',
898
+ handle: 'pt-[7.5px]',
899
+ body: 'pt-2 pb-4 lg:pt-4 lg:pb-5',
900
+ 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',
901
+ },
902
+ confirm: {
903
+ dialog: '',
904
+ header: 'items-center pt-3 lg:pt-6',
905
+ title: '',
906
+ cross: '',
907
+ icon: 'lg:size-[22px]',
908
+ handle: 'pt-[11.5px]',
909
+ body: 'pt-3 pb-5 lg:pt-3 lg:pb-6',
910
+ footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
911
+ },
478
912
  };
479
913
  const nextId$2 = (() => {
480
914
  let count = 0;
481
915
  return () => `ui-dialog-${++count}`;
482
916
  })();
483
917
  /**
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.
918
+ * Modal dialog on the native <dialog>: a bottom sheet under `64rem`, centered above it.
486
919
  *
487
920
  * @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>
921
+ * <ui-dialog heading="Buy" description="Ferrari" closeLabel="Close" width="528px" [open]="buying()" (dismissed)="buying.set(false)">
922
+ * <p>Body</p>
923
+ * <button dialogActions ui-button variant="outline" class="max-lg:hidden" (click)="buying.set(false)">Cancel</button>
924
+ * <button dialogActions ui-button>Buy 40 shares</button>
491
925
  * </ui-dialog>
492
926
  */
493
927
  class UiDialog {
@@ -495,17 +929,39 @@ class UiDialog {
495
929
  ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
496
930
  description = input(/* @ts-ignore */
497
931
  ...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
932
+ closeLabel = input(/* @ts-ignore */
933
+ ...(ngDevMode ? [undefined, { debugName: "closeLabel" }] : /* istanbul ignore next */ []));
934
+ layout = input('trade', /* @ts-ignore */
935
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
936
+ truncateDescription = input(false, { ...(ngDevMode ? { debugName: "truncateDescription" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
498
937
  width = input('lg', /* @ts-ignore */
499
938
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
500
939
  open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
501
940
  dismissed = output();
502
941
  headingId = nextId$2();
503
942
  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 */
943
+ widthValue = computed(() => {
944
+ const width = this.width();
945
+ return Object.hasOwn(WIDTH_VALUES, width) ? WIDTH_VALUES[width] : width;
946
+ }, /* @ts-ignore */
947
+ ...(ngDevMode ? [{ debugName: "widthValue" }] : /* istanbul ignore next */ []));
948
+ spec = computed(() => LAYOUTS[this.layout()], /* @ts-ignore */
949
+ ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
950
+ 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 */
951
+ ...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
952
+ titleClasses = computed(() => this.spec().title, /* @ts-ignore */
953
+ ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
954
+ 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 */
955
+ ...(ngDevMode ? [{ debugName: "crossClasses" }] : /* istanbul ignore next */ []));
956
+ iconClasses = computed(() => this.spec().icon, /* @ts-ignore */
957
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
958
+ 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 */
959
+ ...(ngDevMode ? [{ debugName: "footerClasses" }] : /* istanbul ignore next */ []));
960
+ 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
961
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
506
962
  #host = inject(ElementRef);
507
963
  constructor() {
508
- effect(() => {
964
+ afterRenderEffect(() => {
509
965
  // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
510
966
  const dialog = this.#host.nativeElement.querySelector('dialog');
511
967
  if (this.open() && !dialog.open) {
@@ -516,6 +972,23 @@ class UiDialog {
516
972
  }
517
973
  });
518
974
  }
975
+ #pressedOnBackdrop = false;
976
+ onPointerDown(event) {
977
+ this.#pressedOnBackdrop = event.target === this.#dialog && this.#outside(event);
978
+ }
979
+ onClick(event) {
980
+ if (this.#pressedOnBackdrop && event.target === this.#dialog && this.#outside(event)) {
981
+ this.#dialog.close();
982
+ }
983
+ this.#pressedOnBackdrop = false;
984
+ }
985
+ get #dialog() {
986
+ return this.#host.nativeElement.querySelector('dialog');
987
+ }
988
+ #outside(event) {
989
+ const rect = this.#dialog.getBoundingClientRect();
990
+ return (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom);
991
+ }
519
992
  onNativeClose() {
520
993
  // The owner already knows about a close it asked for; only a close it did not ask for is news.
521
994
  if (this.open()) {
@@ -523,87 +996,145 @@ class UiDialog {
523
996
  }
524
997
  }
525
998
  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: `
999
+ 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
1000
  <dialog
1001
+ #dlg
528
1002
  [attr.aria-describedby]="description() ? descriptionId : null"
529
1003
  [attr.aria-labelledby]="headingId"
1004
+ [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
530
1005
  [class]="classes()"
1006
+ [style.--dialog-width]="widthValue()"
531
1007
  (close)="onNativeClose()"
532
1008
  >
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>
1009
+ <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1010
+ <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
535
1011
  </div>
536
1012
 
537
- <div class="px-6 pt-1 lg:pt-6">
538
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1013
+ <div data-dialog-header [class]="headerClasses()">
1014
+ <div class="flex min-w-0 flex-1 flex-col" [class]="titleClasses()">
1015
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1016
+
1017
+ @if (description()) {
1018
+ <p
1019
+ class="text-label text-(--muted-foreground)"
1020
+ [class.truncate]="truncateDescription()"
1021
+ [id]="descriptionId"
1022
+ >
1023
+ {{ description() }}
1024
+ </p>
1025
+ }
1026
+ </div>
539
1027
 
540
- @if (description()) {
541
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
1028
+ @if (closeLabel()) {
1029
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1030
+ <svg
1031
+ aria-hidden="true"
1032
+ class="block size-[22px] fill-none stroke-current stroke-[1.75]"
1033
+ stroke-linecap="round"
1034
+ stroke-linejoin="round"
1035
+ viewBox="0 0 24 24"
1036
+ [class]="iconClasses()"
1037
+ >
1038
+ <path d="M18 6 6 18" />
1039
+ <path d="m6 6 12 12" />
1040
+ </svg>
1041
+ </button>
542
1042
  }
543
1043
  </div>
544
1044
 
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">
1045
+ <div class="min-h-0 flex-1 overflow-y-auto px-4 lg:px-6" data-dialog-body>
1046
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1047
+ <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
548
1048
  <ng-content />
549
1049
  </div>
550
1050
  </div>
551
1051
 
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
- >
1052
+ <div data-dialog-footer [class]="footerClasses()">
556
1053
  <ng-content select="[dialogActions]" />
557
1054
  </div>
558
1055
  </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"] });
1056
+ `, 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
1057
  }
561
1058
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
562
1059
  type: Component,
563
- args: [{ selector: 'ui-dialog', template: `
1060
+ args: [{ selector: 'ui-dialog', host: { '(pointerdown)': 'onPointerDown($event)', '(click)': 'onClick($event)' }, template: `
564
1061
  <dialog
1062
+ #dlg
565
1063
  [attr.aria-describedby]="description() ? descriptionId : null"
566
1064
  [attr.aria-labelledby]="headingId"
1065
+ [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
567
1066
  [class]="classes()"
1067
+ [style.--dialog-width]="widthValue()"
568
1068
  (close)="onNativeClose()"
569
1069
  >
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>
1070
+ <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1071
+ <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
572
1072
  </div>
573
1073
 
574
- <div class="px-6 pt-1 lg:pt-6">
575
- <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1074
+ <div data-dialog-header [class]="headerClasses()">
1075
+ <div class="flex min-w-0 flex-1 flex-col" [class]="titleClasses()">
1076
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
1077
+
1078
+ @if (description()) {
1079
+ <p
1080
+ class="text-label text-(--muted-foreground)"
1081
+ [class.truncate]="truncateDescription()"
1082
+ [id]="descriptionId"
1083
+ >
1084
+ {{ description() }}
1085
+ </p>
1086
+ }
1087
+ </div>
576
1088
 
577
- @if (description()) {
578
- <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
1089
+ @if (closeLabel()) {
1090
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1091
+ <svg
1092
+ aria-hidden="true"
1093
+ class="block size-[22px] fill-none stroke-current stroke-[1.75]"
1094
+ stroke-linecap="round"
1095
+ stroke-linejoin="round"
1096
+ viewBox="0 0 24 24"
1097
+ [class]="iconClasses()"
1098
+ >
1099
+ <path d="M18 6 6 18" />
1100
+ <path d="m6 6 12 12" />
1101
+ </svg>
1102
+ </button>
579
1103
  }
580
1104
  </div>
581
1105
 
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">
1106
+ <div class="min-h-0 flex-1 overflow-y-auto px-4 lg:px-6" data-dialog-body>
1107
+ <!-- empty:hidden keeps a body-less dialog from carrying the body's vertical padding. -->
1108
+ <div class="flex flex-col gap-4 empty:hidden" [class]="spec().body">
585
1109
  <ng-content />
586
1110
  </div>
587
1111
  </div>
588
1112
 
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
- >
1113
+ <div data-dialog-footer [class]="footerClasses()">
593
1114
  <ng-content select="[dialogActions]" />
594
1115
  </div>
595
1116
  </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);
1117
+ `, 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"] }]
1118
+ }], 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"] }] } });
1119
+
1120
+ const RADIUS = 78;
1121
+ const RING_WIDTH = 30;
1122
+ const ACTIVE_RING_WIDTH = 38;
1123
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1124
+ const GAP = 2;
605
1125
  function positionSlices(slices) {
606
- return pieLayout(slices).map((arc) => ({ ...arc.data, path: arcGenerator(arc) }));
1126
+ let travelled = 0;
1127
+ return slices.map((slice) => {
1128
+ const length = slice.share * CIRCUMFERENCE;
1129
+ const drawn = Math.max(0, length - GAP);
1130
+ const positioned = {
1131
+ ...slice,
1132
+ dash: `${drawn.toFixed(2)} ${(CIRCUMFERENCE - drawn).toFixed(2)}`,
1133
+ offset: (-travelled).toFixed(2),
1134
+ };
1135
+ travelled += length;
1136
+ return positioned;
1137
+ });
607
1138
  }
608
1139
 
609
1140
  const MAX_RAMP = 6;
@@ -635,16 +1166,7 @@ function rankSlices(slices, othersLabel) {
635
1166
 
636
1167
  const identityValue$1 = (value) => `${value}`;
637
1168
  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
- };
1169
+ const ROW_CLASSES$1 = 'flex w-full items-center gap-3 min-h-14 px-2 py-1 [font-variant-numeric:var(--numeric)] rounded-control text-left transition-colors duration-(--duration-fast) ease-out hover:bg-(--glow) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
648
1170
  const RAMP_DOT_CLASSES = {
649
1171
  1: 'bg-(--ramp-1)',
650
1172
  2: 'bg-(--ramp-2)',
@@ -691,22 +1213,23 @@ class UiDonut {
691
1213
  return detail ? `${this.label()} : ${detail}` : this.label();
692
1214
  }, /* @ts-ignore */
693
1215
  ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
694
- pathClasses(slice) {
695
- return `cursor-pointer stroke-(--card) ${RAMP_FILL_CLASSES[slice.ramp]}`;
1216
+ radius = RADIUS;
1217
+ ringWidth = RING_WIDTH;
1218
+ strokeColor(slice) {
1219
+ return `var(--ramp-${slice.ramp})`;
1220
+ }
1221
+ strokeWidth(slice) {
1222
+ return this.isActive(slice) ? ACTIVE_RING_WIDTH : RING_WIDTH;
696
1223
  }
697
1224
  dotClasses(slice) {
698
1225
  return `size-3 flex-none rounded-pill shadow-[inset_0_0_0_1px_var(--border)] ${RAMP_DOT_CLASSES[slice.ramp]}`;
699
1226
  }
700
1227
  rowClasses(slice) {
701
- return `${ROW_CLASSES}${this.isActive(slice) ? ' bg-(--soft)' : ''}`;
1228
+ return `${ROW_CLASSES$1}${this.isActive(slice) ? ' bg-(--soft)' : ''}`;
702
1229
  }
703
1230
  subLabel(slice) {
704
1231
  return slice.members.length > 0 ? slice.members.join(', ') : (slice.sublabel ?? '');
705
1232
  }
706
- arcTransform(slice) {
707
- const scale = this.isActive(slice) ? ACTIVE_SCALE : 1;
708
- return `translate(100px, 100px) scale(${scale})`;
709
- }
710
1233
  isActive(slice) {
711
1234
  return this.active()?.id === slice.id;
712
1235
  }
@@ -723,36 +1246,53 @@ class UiDonut {
723
1246
  this.sliceSelect.emit(slice.id);
724
1247
  }
725
1248
  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: `
1249
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDonut, isStandalone: true, selector: "ui-donut", inputs: { slices: { classPropertyName: "slices", publicName: "slices", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, othersLabel: { classPropertyName: "othersLabel", publicName: "othersLabel", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, shareFormat: { classPropertyName: "shareFormat", publicName: "shareFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sliceSelect: "sliceSelect" }, host: { classAttribute: "flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8" }, ngImport: i0, template: `
727
1250
  <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()">
1251
+ <svg class="block h-full w-full -rotate-90" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1252
+ <circle
1253
+ class="stroke-(--muted)"
1254
+ cx="100"
1255
+ cy="100"
1256
+ fill="none"
1257
+ [attr.r]="radius"
1258
+ [attr.stroke-width]="ringWidth"
1259
+ />
729
1260
  @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>
1261
+ <circle
1262
+ class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1263
+ cx="100"
1264
+ cy="100"
1265
+ data-slice
1266
+ fill="none"
1267
+ [attr.r]="radius"
1268
+ [attr.stroke-dasharray]="slice.dash"
1269
+ [attr.stroke-dashoffset]="slice.offset"
1270
+ [style.stroke]="strokeColor(slice)"
1271
+ [style.stroke-width.px]="strokeWidth(slice)"
1272
+ (click)="select(slice)"
1273
+ (pointerenter)="highlight(slice.id)"
1274
+ (pointerleave)="clearHighlight(slice.id)"
1275
+ />
740
1276
  }
741
1277
  </svg>
742
1278
 
743
1279
  @if (active(); as slice) {
744
1280
  <div
745
- class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
1281
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-12 text-center"
746
1282
  data-testid="donut-centre"
747
1283
  >
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>
1284
+ <span class="text-label max-w-full truncate font-medium whitespace-nowrap text-(--muted-foreground)">{{
1285
+ slice.label
1286
+ }}</span>
1287
+ <span class="text-heading font-semibold tracking-(--tracking-display)">{{ shareFormat()(slice.share) }}</span>
1288
+ <span class="text-caption tracking-normal text-(--muted-foreground) [font-variant-numeric:var(--numeric)]">{{
1289
+ valueFormat()(slice.value)
1290
+ }}</span>
751
1291
  </div>
752
1292
  }
753
1293
  </div>
754
1294
 
755
- <div class="flex min-w-0 flex-1 flex-col">
1295
+ <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
756
1296
  @for (slice of ranked(); track slice.id) {
757
1297
  <button
758
1298
  type="button"
@@ -766,11 +1306,13 @@ class UiDonut {
766
1306
  <span [class]="dotClasses(slice)"></span>
767
1307
  <span class="flex min-w-0 flex-1 flex-col">
768
1308
  <span class="text-body truncate font-medium">{{ slice.label }}</span>
769
- <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
1309
+ <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1310
+ subLabel(slice)
1311
+ }}</span>
770
1312
  </span>
771
1313
  <span class="flex flex-none flex-col items-end whitespace-nowrap">
772
1314
  <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
773
- <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1315
+ <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
774
1316
  </span>
775
1317
  </button>
776
1318
  }
@@ -783,34 +1325,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
783
1325
  selector: 'ui-donut',
784
1326
  template: `
785
1327
  <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()">
1328
+ <svg class="block h-full w-full -rotate-90" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
1329
+ <circle
1330
+ class="stroke-(--muted)"
1331
+ cx="100"
1332
+ cy="100"
1333
+ fill="none"
1334
+ [attr.r]="radius"
1335
+ [attr.stroke-width]="ringWidth"
1336
+ />
787
1337
  @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>
1338
+ <circle
1339
+ class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1340
+ cx="100"
1341
+ cy="100"
1342
+ data-slice
1343
+ fill="none"
1344
+ [attr.r]="radius"
1345
+ [attr.stroke-dasharray]="slice.dash"
1346
+ [attr.stroke-dashoffset]="slice.offset"
1347
+ [style.stroke]="strokeColor(slice)"
1348
+ [style.stroke-width.px]="strokeWidth(slice)"
1349
+ (click)="select(slice)"
1350
+ (pointerenter)="highlight(slice.id)"
1351
+ (pointerleave)="clearHighlight(slice.id)"
1352
+ />
798
1353
  }
799
1354
  </svg>
800
1355
 
801
1356
  @if (active(); as slice) {
802
1357
  <div
803
- class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
1358
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-12 text-center"
804
1359
  data-testid="donut-centre"
805
1360
  >
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>
1361
+ <span class="text-label max-w-full truncate font-medium whitespace-nowrap text-(--muted-foreground)">{{
1362
+ slice.label
1363
+ }}</span>
1364
+ <span class="text-heading font-semibold tracking-(--tracking-display)">{{ shareFormat()(slice.share) }}</span>
1365
+ <span class="text-caption tracking-normal text-(--muted-foreground) [font-variant-numeric:var(--numeric)]">{{
1366
+ valueFormat()(slice.value)
1367
+ }}</span>
809
1368
  </div>
810
1369
  }
811
1370
  </div>
812
1371
 
813
- <div class="flex min-w-0 flex-1 flex-col">
1372
+ <div class="flex w-full min-w-0 flex-1 flex-col sm:w-auto" data-testid="donut-legend">
814
1373
  @for (slice of ranked(); track slice.id) {
815
1374
  <button
816
1375
  type="button"
@@ -824,18 +1383,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
824
1383
  <span [class]="dotClasses(slice)"></span>
825
1384
  <span class="flex min-w-0 flex-1 flex-col">
826
1385
  <span class="text-body truncate font-medium">{{ slice.label }}</span>
827
- <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
1386
+ <span class="text-caption truncate tracking-(--tracking-caption) text-(--subtle-foreground)">{{
1387
+ subLabel(slice)
1388
+ }}</span>
828
1389
  </span>
829
1390
  <span class="flex flex-none flex-col items-end whitespace-nowrap">
830
1391
  <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
831
- <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
1392
+ <span class="text-caption tracking-normal text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
832
1393
  </span>
833
1394
  </button>
834
1395
  }
835
1396
  </div>
836
1397
  `,
837
1398
  host: {
838
- class: 'flex flex-col items-center gap-6 sm:flex-row sm:items-center',
1399
+ class: 'flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:gap-8',
839
1400
  },
840
1401
  }]
841
1402
  }], propDecorators: { slices: [{ type: i0.Input, args: [{ isSignal: true, alias: "slices", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], othersLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "othersLabel", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], shareFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareFormat", required: false }] }], sliceSelect: [{ type: i0.Output, args: ["sliceSelect"] }] } });
@@ -844,6 +1405,31 @@ const nextId$1 = (() => {
844
1405
  let count = 0;
845
1406
  return () => `ui-field-${++count}`;
846
1407
  })();
1408
+ /**
1409
+ * Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`: it sits
1410
+ * inside the control's left edge and the control's text is inset to clear it. Decorative, so hidden
1411
+ * from assistive technologies.
1412
+ *
1413
+ * @example
1414
+ * <ui-field label="Search" labelHidden>
1415
+ * <svg uiFieldLeading width="18" height="18" viewBox="0 0 24 24">...</svg>
1416
+ * <input uiInput type="search" />
1417
+ * </ui-field>
1418
+ */
1419
+ class UiFieldLeading {
1420
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1421
+ 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 });
1422
+ }
1423
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, decorators: [{
1424
+ type: Directive,
1425
+ args: [{
1426
+ selector: '[uiFieldLeading]',
1427
+ host: {
1428
+ class: 'pointer-events-none absolute left-3 flex text-(--muted-foreground)',
1429
+ 'aria-hidden': 'true',
1430
+ },
1431
+ }]
1432
+ }] });
847
1433
  /**
848
1434
  * Label, control, hint and error as one unit. The control stays a plain native element:
849
1435
  * the field finds it in its own projected content and wires the ARIA attributes onto it.
@@ -882,7 +1468,11 @@ class UiField {
882
1468
  #host = inject(ElementRef);
883
1469
  control = contentChild(UI_CONTROL, /* @ts-ignore */
884
1470
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
885
- labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label font-medium text-(--muted-foreground)', /* @ts-ignore */
1471
+ leading = contentChild(UiFieldLeading, /* @ts-ignore */
1472
+ ...(ngDevMode ? [{ debugName: "leading" }] : /* istanbul ignore next */ []));
1473
+ rowClasses = computed(() => this.leading() ? 'relative flex items-center [&>input]:pl-[38px]' : 'relative flex items-center', /* @ts-ignore */
1474
+ ...(ngDevMode ? [{ debugName: "rowClasses" }] : /* istanbul ignore next */ []));
1475
+ labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)', /* @ts-ignore */
886
1476
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
887
1477
  /**
888
1478
  * The caller's own `error` wins: it is the only way to report something the control does not
@@ -925,12 +1515,13 @@ class UiField {
925
1515
  });
926
1516
  }
927
1517
  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: `
1518
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }, { propertyName: "leading", first: true, predicate: UiFieldLeading, descendants: true, isSignal: true }], ngImport: i0, template: `
929
1519
  <div class="group flex flex-col gap-1.5">
930
1520
  <!-- 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
1521
  <label [class]="labelClasses()">{{ label() }}</label>
932
1522
 
933
- <div class="relative flex items-center">
1523
+ <div [class]="rowClasses()">
1524
+ <ng-content select="[uiFieldLeading]" />
934
1525
  <ng-content />
935
1526
 
936
1527
  @if (unit()) {
@@ -957,7 +1548,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
957
1548
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
958
1549
  <label [class]="labelClasses()">{{ label() }}</label>
959
1550
 
960
- <div class="relative flex items-center">
1551
+ <div [class]="rowClasses()">
1552
+ <ng-content select="[uiFieldLeading]" />
961
1553
  <ng-content />
962
1554
 
963
1555
  @if (unit()) {
@@ -975,77 +1567,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
975
1567
  </div>
976
1568
  `,
977
1569
  }]
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 }] }] } });
979
-
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 }] }] } });
1020
- /**
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.
1027
- *
1028
- * @example
1029
- * <textarea uiTextarea maxlength="280"></textarea>
1030
- */
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 });
1038
- }
1039
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
1040
- type: Directive,
1041
- args: [{
1042
- selector: 'textarea[uiTextarea]',
1043
- host: {
1044
- class: `${CONTROL_BASE_CLASSES} min-h-24 py-2`,
1045
- },
1046
- providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],
1047
- }]
1048
- }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
1570
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }], leading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldLeading), { isSignal: true }] }] } });
1049
1571
 
1050
1572
  const axisTicks = (points, maxTicks = 5) => {
1051
1573
  if (points.length <= maxTicks) {
@@ -1059,6 +1581,13 @@ const axisTicks = (points, maxTicks = 5) => {
1059
1581
  }
1060
1582
  return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
1061
1583
  };
1584
+ const isCoreTick = (index, count) => {
1585
+ if (count <= 3) {
1586
+ return true;
1587
+ }
1588
+ const middle = Math.floor((count - 1) / 2);
1589
+ return index === 0 || index === count - 1 || index === middle;
1590
+ };
1062
1591
 
1063
1592
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
1064
1593
  const nearestPointIndex = (points, x) => {
@@ -1090,6 +1619,36 @@ const navigateIndex = (key, current, last) => {
1090
1619
  }
1091
1620
  };
1092
1621
 
1622
+ const START_LABEL_HEIGHT = 16;
1623
+ const AXIS_ROW = 17;
1624
+ const TOOLTIP_SIZES = ['compact', 'large'];
1625
+ const TOOLTIP_CLASSES = {
1626
+ compact: { box: '-top-3 px-2.5', value: 'text-label', delta: 'text-caption' },
1627
+ large: { box: 'top-0 px-3', value: 'text-body', delta: 'text-label' },
1628
+ };
1629
+ const PLOT_PADDING_RATIO = 16 / 240;
1630
+ const SPARKLINE_PADDING = 4;
1631
+ const START_LABEL_BASELINE_OFFSET = 4;
1632
+ const TOOLTIP_OFFSET = 14;
1633
+ const TOOLTIP_FLIP_RATIO = 0.55;
1634
+ const plotHeightFor = (height, sparkline, axisGap, wide) => sparkline ? height : Math.max(height - resolveGap(axisGap, wide) - AXIS_ROW, 1);
1635
+ const plotPaddingFor = (plotHeight, sparkline) => sparkline ? SPARKLINE_PADDING : plotHeight * PLOT_PADDING_RATIO;
1636
+ const startLabelBaseline = (boxBottom) => boxBottom - START_LABEL_BASELINE_OFFSET;
1637
+ const tooltipPlacement = (ratio) => {
1638
+ const flipped = ratio > TOOLTIP_FLIP_RATIO;
1639
+ return `left: calc(${ratio * 100}% ${flipped ? '-' : '+'} ${TOOLTIP_OFFSET}px); transform: ${flipped ? 'translateX(-100%)' : 'none'}`;
1640
+ };
1641
+ const deltaTone = (delta) => delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1642
+ const DEFAULT_WIDTH = 640;
1643
+ const DEFAULT_HEIGHT = 240;
1644
+ const identityValue = (value) => `${value}`;
1645
+ const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1646
+ const identityTime = (time) => new Date(time).toISOString();
1647
+ const parseAxisTicks = (value) => value === 'auto' ? 'auto' : Number(value);
1648
+ const resolveGap = (gap, wide) => (gap === 'auto' ? (wide ? 16 : 12) : gap);
1649
+ const resolveTooltip = (size, wide) => size === 'auto' ? (wide ? 'large' : 'compact') : size;
1650
+ const tooltipClassesFor = (size, wide) => TOOLTIP_CLASSES[resolveTooltip(size, wide)];
1651
+
1093
1652
  const CURVE = curveMonotoneX;
1094
1653
  const buildGeometry = (points, width, height, padding = {}) => {
1095
1654
  if (points.length === 0) {
@@ -1125,8 +1684,8 @@ const buildGeometry = (points, width, height, padding = {}) => {
1125
1684
  };
1126
1685
  };
1127
1686
 
1128
- const LINE_GAP = 3;
1129
- const COLLISION_MARGIN = 3;
1687
+ const LINE_GAP = 6;
1688
+ const COLLISION_MARGIN = 6;
1130
1689
  const EDGE_TOLERANCE = 3;
1131
1690
  const candidateSlots = (startY, box, label) => {
1132
1691
  const aboveTop = startY - LINE_GAP - label.height;
@@ -1239,13 +1798,19 @@ const interpolateLine = (from, to, progress) => {
1239
1798
  return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
1240
1799
  };
1241
1800
 
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();
1801
+ const WIDE_QUERY = '(min-width: 64rem)';
1802
+ const wideViewport = () => {
1803
+ const query = typeof matchMedia === 'function' ? matchMedia(WIDE_QUERY) : null;
1804
+ const wide = signal(query?.matches ?? false, /* @ts-ignore */
1805
+ ...(ngDevMode ? [{ debugName: "wide" }] : /* istanbul ignore next */ []));
1806
+ const onChange = (event) => wide.set(event.matches);
1807
+ if (query) {
1808
+ query.onchange = onChange;
1809
+ inject(DestroyRef).onDestroy(() => (query.onchange = null));
1810
+ }
1811
+ return wide;
1812
+ };
1813
+
1249
1814
  /**
1250
1815
  * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1251
1816
  *
@@ -1271,10 +1836,19 @@ class UiLineChart {
1271
1836
  ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1272
1837
  valueColumnLabel = input('Value', /* @ts-ignore */
1273
1838
  ...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
1839
+ sparkline = input(false, { ...(ngDevMode ? { debugName: "sparkline" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1840
+ tooltip = input('compact', /* @ts-ignore */
1841
+ ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
1842
+ tooltipDelta = input(true, { ...(ngDevMode ? { debugName: "tooltipDelta" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1843
+ axisGap = input(12, /* @ts-ignore */
1844
+ ...(ngDevMode ? [{ debugName: "axisGap" }] : /* istanbul ignore next */ []));
1845
+ axisTicks = input('auto', { ...(ngDevMode ? { debugName: "axisTicks" } : /* istanbul ignore next */ {}), transform: parseAxisTicks });
1274
1846
  #destroyRef = inject(DestroyRef);
1847
+ #wide = wideViewport();
1275
1848
  #frame = null;
1276
1849
  #hasRenderedOnce = false;
1277
1850
  #previousGeometry = null;
1851
+ isCoreTick = isCoreTick;
1278
1852
  svgRef = viewChild.required('svgRef', /* @ts-ignore */
1279
1853
  ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1280
1854
  activeIndex = signal(null, /* @ts-ignore */
@@ -1287,7 +1861,16 @@ class UiLineChart {
1287
1861
  ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1288
1862
  effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1289
1863
  ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1290
- geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
1864
+ plotHeight = computed(() => plotHeightFor(this.effectiveHeight(), this.sparkline(), this.axisGap(), this.#wide()), /* @ts-ignore */
1865
+ ...(ngDevMode ? [{ debugName: "plotHeight" }] : /* istanbul ignore next */ []));
1866
+ tooltipStyle = (point) => tooltipPlacement(point.x / this.effectiveWidth());
1867
+ tooltipClasses = computed(() => tooltipClassesFor(this.tooltip(), this.#wide()), /* @ts-ignore */
1868
+ ...(ngDevMode ? [{ debugName: "tooltipClasses" }] : /* istanbul ignore next */ []));
1869
+ geometry = computed(() => {
1870
+ const plotHeight = this.plotHeight();
1871
+ const padding = plotPaddingFor(plotHeight, this.sparkline());
1872
+ return buildGeometry(this.points(), this.effectiveWidth(), plotHeight, { x: 0, top: padding, bottom: padding });
1873
+ }, /* @ts-ignore */
1291
1874
  ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1292
1875
  displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1293
1876
  ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
@@ -1307,8 +1890,8 @@ class UiLineChart {
1307
1890
  }
1308
1891
  const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1309
1892
  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 };
1893
+ const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.plotHeight() }, size);
1894
+ return { label, placement: { ...placement, y: startLabelBaseline(placement.y) } };
1312
1895
  }, /* @ts-ignore */
1313
1896
  ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1314
1897
  #renderRef = afterNextRender(() => this.#observeSize());
@@ -1353,10 +1936,8 @@ class UiLineChart {
1353
1936
  this.#destroyRef.onDestroy(() => observer.disconnect());
1354
1937
  }
1355
1938
  #cancelAnimation() {
1356
- if (this.#frame !== null) {
1357
- cancelAnimationFrame(this.#frame);
1358
- this.#frame = null;
1359
- }
1939
+ cancelAnimationFrame(this.#frame ?? 0);
1940
+ this.#frame = null;
1360
1941
  }
1361
1942
  #animate(from, to) {
1362
1943
  if (from.points.length < 2 || to.points.length < 2) {
@@ -1366,37 +1947,20 @@ class UiLineChart {
1366
1947
  const startTime = performance.now();
1367
1948
  const step = (now) => {
1368
1949
  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
- }
1950
+ this.animatedLine.set(t < 1 ? interpolateLine(from, to, easeOutQuint(t)) : null);
1951
+ this.#frame = t < 1 ? requestAnimationFrame(step) : null;
1377
1952
  };
1378
1953
  this.#frame = requestAnimationFrame(step);
1379
1954
  }
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
1955
  deltaFor(point) {
1388
1956
  return point.v - this.points()[0].v;
1389
1957
  }
1390
1958
  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}%`;
1959
+ return `${this.tooltipClasses().delta} ${deltaTone(this.deltaFor(point))}`;
1396
1960
  }
1397
1961
  onPointerMove(event) {
1398
1962
  const geometry = this.geometry();
1399
- if (!geometry || geometry.points.length === 0) {
1963
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1400
1964
  return;
1401
1965
  }
1402
1966
  const rect = this.svgRef().nativeElement.getBoundingClientRect();
@@ -1409,7 +1973,7 @@ class UiLineChart {
1409
1973
  }
1410
1974
  onKeydown(event) {
1411
1975
  const geometry = this.geometry();
1412
- if (!geometry || geometry.points.length === 0) {
1976
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1413
1977
  return;
1414
1978
  }
1415
1979
  const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
@@ -1422,15 +1986,15 @@ class UiLineChart {
1422
1986
  this.activeIndex.set(navigation.index);
1423
1987
  }
1424
1988
  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: `
1989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
1426
1990
  <div class="relative h-full">
1427
1991
  <svg
1428
1992
  #svgRef
1429
- class="block h-full w-full touch-pan-y"
1993
+ class="block h-full w-full touch-pan-y overflow-visible"
1430
1994
  role="img"
1431
- tabindex="0"
1432
1995
  [attr.aria-label]="label()"
1433
1996
  [attr.height]="effectiveHeight()"
1997
+ [attr.tabindex]="sparkline() ? null : 0"
1434
1998
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1435
1999
  [attr.width]="effectiveWidth()"
1436
2000
  (keydown)="onKeydown($event)"
@@ -1444,94 +2008,100 @@ class UiLineChart {
1444
2008
  fill="none"
1445
2009
  stroke-linecap="round"
1446
2010
  stroke-linejoin="round"
1447
- stroke-width="2"
1448
2011
  [attr.d]="displayLine()"
2012
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1449
2013
  />
1450
2014
 
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) {
2015
+ @if (!sparkline()) {
1504
2016
  <line
1505
2017
  class="stroke-(--border)"
1506
- data-chart-crosshair
2018
+ data-chart-start-line
2019
+ stroke-dasharray="3 4"
1507
2020
  stroke-width="1"
1508
- y1="0"
1509
- [attr.x1]="point.x"
1510
- [attr.x2]="point.x"
1511
- [attr.y2]="effectiveHeight()"
2021
+ x1="0"
2022
+ [attr.x2]="effectiveWidth()"
2023
+ [attr.y1]="geometry.start.y"
2024
+ [attr.y2]="geometry.start.y"
1512
2025
  />
2026
+
2027
+ @if (startLabelInfo(); as info) {
2028
+ <text
2029
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2030
+ data-chart-start-label
2031
+ [attr.text-anchor]="info.placement.anchor"
2032
+ [attr.x]="info.placement.x"
2033
+ [attr.y]="info.placement.y"
2034
+ >
2035
+ {{ info.label }}
2036
+ </text>
2037
+ }
2038
+
1513
2039
  <circle
1514
- class="fill-(--foreground) stroke-(--card)"
1515
- data-chart-active
2040
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2041
+ data-chart-end
1516
2042
  r="4"
1517
- stroke-width="2"
1518
- [attr.cx]="point.x"
1519
- [attr.cy]="point.y"
2043
+ stroke-width="6"
2044
+ [attr.cx]="geometry.end.x"
2045
+ [attr.cy]="geometry.end.y"
1520
2046
  />
2047
+
2048
+ @for (
2049
+ tick of ticks();
2050
+ track tick.t;
2051
+ let index = $index;
2052
+ let count = $count;
2053
+ let first = $first;
2054
+ let last = $last
2055
+ ) {
2056
+ <text
2057
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2058
+ data-chart-axis-tick
2059
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
2060
+ [attr.x]="tick.x"
2061
+ [attr.y]="effectiveHeight() - 4"
2062
+ [class.hidden]="axisTicks() === 3 && !isCoreTick(index, count)"
2063
+ [class.max-sm:hidden]="axisTicks() === 'auto' && !isCoreTick(index, count)"
2064
+ >
2065
+ {{ axisFormat()(tick.t) }}
2066
+ </text>
2067
+ }
2068
+
2069
+ @if (active(); as point) {
2070
+ <line
2071
+ class="stroke-(--border)"
2072
+ data-chart-crosshair
2073
+ stroke-width="1"
2074
+ y1="0"
2075
+ [attr.x1]="point.x"
2076
+ [attr.x2]="point.x"
2077
+ [attr.y2]="plotHeight()"
2078
+ />
2079
+ <circle
2080
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2081
+ data-chart-active
2082
+ r="5"
2083
+ stroke-width="6"
2084
+ [attr.cx]="point.x"
2085
+ [attr.cy]="point.y"
2086
+ />
2087
+ }
1521
2088
  }
1522
2089
  }
1523
2090
  </svg>
1524
2091
 
1525
2092
  @if (active(); as point) {
1526
2093
  <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)]"
2094
+ 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
2095
  data-testid="chart-tooltip"
1529
2096
  role="status"
2097
+ [class]="tooltipClasses().box"
1530
2098
  [style]="tooltipStyle(point)"
1531
2099
  >
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>
2100
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2101
+ @if (tooltipDelta()) {
2102
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2103
+ }
2104
+ <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
1535
2105
  </div>
1536
2106
  }
1537
2107
 
@@ -1569,11 +2139,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1569
2139
  <div class="relative h-full">
1570
2140
  <svg
1571
2141
  #svgRef
1572
- class="block h-full w-full touch-pan-y"
2142
+ class="block h-full w-full touch-pan-y overflow-visible"
1573
2143
  role="img"
1574
- tabindex="0"
1575
2144
  [attr.aria-label]="label()"
1576
2145
  [attr.height]="effectiveHeight()"
2146
+ [attr.tabindex]="sparkline() ? null : 0"
1577
2147
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1578
2148
  [attr.width]="effectiveWidth()"
1579
2149
  (keydown)="onKeydown($event)"
@@ -1587,94 +2157,100 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1587
2157
  fill="none"
1588
2158
  stroke-linecap="round"
1589
2159
  stroke-linejoin="round"
1590
- stroke-width="2"
1591
2160
  [attr.d]="displayLine()"
2161
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1592
2162
  />
1593
2163
 
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) {
2164
+ @if (!sparkline()) {
1647
2165
  <line
1648
2166
  class="stroke-(--border)"
1649
- data-chart-crosshair
2167
+ data-chart-start-line
2168
+ stroke-dasharray="3 4"
1650
2169
  stroke-width="1"
1651
- y1="0"
1652
- [attr.x1]="point.x"
1653
- [attr.x2]="point.x"
1654
- [attr.y2]="effectiveHeight()"
2170
+ x1="0"
2171
+ [attr.x2]="effectiveWidth()"
2172
+ [attr.y1]="geometry.start.y"
2173
+ [attr.y2]="geometry.start.y"
1655
2174
  />
2175
+
2176
+ @if (startLabelInfo(); as info) {
2177
+ <text
2178
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2179
+ data-chart-start-label
2180
+ [attr.text-anchor]="info.placement.anchor"
2181
+ [attr.x]="info.placement.x"
2182
+ [attr.y]="info.placement.y"
2183
+ >
2184
+ {{ info.label }}
2185
+ </text>
2186
+ }
2187
+
1656
2188
  <circle
1657
- class="fill-(--foreground) stroke-(--card)"
1658
- data-chart-active
2189
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2190
+ data-chart-end
1659
2191
  r="4"
1660
- stroke-width="2"
1661
- [attr.cx]="point.x"
1662
- [attr.cy]="point.y"
2192
+ stroke-width="6"
2193
+ [attr.cx]="geometry.end.x"
2194
+ [attr.cy]="geometry.end.y"
1663
2195
  />
2196
+
2197
+ @for (
2198
+ tick of ticks();
2199
+ track tick.t;
2200
+ let index = $index;
2201
+ let count = $count;
2202
+ let first = $first;
2203
+ let last = $last
2204
+ ) {
2205
+ <text
2206
+ class="text-caption fill-(--subtle-foreground) tabular-nums"
2207
+ data-chart-axis-tick
2208
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
2209
+ [attr.x]="tick.x"
2210
+ [attr.y]="effectiveHeight() - 4"
2211
+ [class.hidden]="axisTicks() === 3 && !isCoreTick(index, count)"
2212
+ [class.max-sm:hidden]="axisTicks() === 'auto' && !isCoreTick(index, count)"
2213
+ >
2214
+ {{ axisFormat()(tick.t) }}
2215
+ </text>
2216
+ }
2217
+
2218
+ @if (active(); as point) {
2219
+ <line
2220
+ class="stroke-(--border)"
2221
+ data-chart-crosshair
2222
+ stroke-width="1"
2223
+ y1="0"
2224
+ [attr.x1]="point.x"
2225
+ [attr.x2]="point.x"
2226
+ [attr.y2]="plotHeight()"
2227
+ />
2228
+ <circle
2229
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2230
+ data-chart-active
2231
+ r="5"
2232
+ stroke-width="6"
2233
+ [attr.cx]="point.x"
2234
+ [attr.cy]="point.y"
2235
+ />
2236
+ }
1664
2237
  }
1665
2238
  }
1666
2239
  </svg>
1667
2240
 
1668
2241
  @if (active(); as point) {
1669
2242
  <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)]"
2243
+ 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
2244
  data-testid="chart-tooltip"
1672
2245
  role="status"
2246
+ [class]="tooltipClasses().box"
1673
2247
  [style]="tooltipStyle(point)"
1674
2248
  >
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>
2249
+ <p class="font-medium text-(--foreground)" [class]="tooltipClasses().value">{{ valueFormat()(point.v) }}</p>
2250
+ @if (tooltipDelta()) {
2251
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
2252
+ }
2253
+ <p class="text-caption text-(--subtle-foreground)" data-chart-date>{{ timeFormat()(point.t) }}</p>
1678
2254
  </div>
1679
2255
  }
1680
2256
 
@@ -1707,10 +2283,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1707
2283
  class: 'block h-full',
1708
2284
  },
1709
2285
  }]
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 }] }] } });
2286
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
1711
2287
 
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';
2288
+ const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16),inset_0_0_0_1px_var(--border)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
2289
+ const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1714
2290
  const nextId = (() => {
1715
2291
  let count = 0;
1716
2292
  return () => `ui-menu-${++count}`;
@@ -1923,12 +2499,12 @@ class UiMeter {
1923
2499
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
1924
2500
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
1925
2501
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
1926
- fillClasses = computed(() => `h-full rounded-control ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
2502
+ fillClasses = computed(() => `h-full rounded-pill ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
1927
2503
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
1928
2504
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1929
2505
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1930
2506
  <div
1931
- class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
2507
+ class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
1932
2508
  role="meter"
1933
2509
  [attr.aria-label]="label()"
1934
2510
  [attr.aria-valuemax]="1"
@@ -1946,7 +2522,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1946
2522
  selector: 'ui-meter',
1947
2523
  template: `
1948
2524
  <div
1949
- class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
2525
+ class="rounded-pill h-1.5 w-full overflow-hidden bg-(--muted)"
1950
2526
  role="meter"
1951
2527
  [attr.aria-label]="label()"
1952
2528
  [attr.aria-valuemax]="1"
@@ -1960,8 +2536,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1960
2536
  }]
1961
2537
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueText", required: true }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
1962
2538
 
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)';
2539
+ const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label font-medium transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1964
2540
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
2541
+ const IDLE_CLASSES = 'text-(--muted-foreground)';
1965
2542
  /**
1966
2543
  * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
1967
2544
  *
@@ -1973,7 +2550,7 @@ const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
1973
2550
  */
1974
2551
  class UiNavItem {
1975
2552
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1976
- classes = computed(() => `${BASE_CLASSES$1}${this.active() ? ` ${ACTIVE_CLASSES}` : ''}`, /* @ts-ignore */
2553
+ classes = computed(() => `${BASE_CLASSES$1} ${this.active() ? ACTIVE_CLASSES : IDLE_CLASSES}`, /* @ts-ignore */
1977
2554
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1978
2555
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1979
2556
  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 +2567,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1990
2567
  }]
1991
2568
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1992
2569
 
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)';
2570
+ const ROW_SIZES = ['md', 'lg', 'xl'];
2571
+ const ROW_PADDINGS = ['md', 'sm', 'none'];
2572
+ const SIZE_CLASSES = {
2573
+ md: 'min-h-14 gap-2.5',
2574
+ lg: 'min-h-15 gap-2.5',
2575
+ xl: 'min-h-18 pointer-fine:min-h-17 gap-3',
2576
+ };
2577
+ const PADDING_CLASSES = { md: 'px-2.5', sm: 'px-2', none: 'px-0' };
2578
+ 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
2579
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
1995
2580
  /**
1996
2581
  * Clickable row of a list: a holding, an envelope, an account.
@@ -2002,10 +2587,14 @@ const SELECTED_CLASSES$1 = 'bg-(--soft)';
2002
2587
  */
2003
2588
  class UiRow {
2004
2589
  selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2005
- classes = computed(() => `${BASE_CLASSES}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
2590
+ size = input('md', /* @ts-ignore */
2591
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2592
+ padding = input('md', /* @ts-ignore */
2593
+ ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
2594
+ classes = computed(() => `${BASE_CLASSES} ${SIZE_CLASSES[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
2006
2595
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2007
2596
  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 });
2597
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
2009
2598
  }
2010
2599
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
2011
2600
  type: Component,
@@ -2017,9 +2606,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2017
2606
  '[attr.aria-current]': "selected() ? 'true' : null",
2018
2607
  },
2019
2608
  }]
2020
- }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }] } });
2609
+ }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }] } });
2021
2610
 
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)";
2611
+ const OPTION_CLASSES = "relative z-10 h-10 pointer-fine:h-8 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) active:scale-(--press-scale)";
2023
2612
  const SELECTED_CLASSES = 'text-(--foreground)';
2024
2613
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
2025
2614
  /**
@@ -2078,10 +2667,10 @@ class UiSegmented {
2078
2667
  @if (thumbIndex(); as index) {
2079
2668
  <span
2080
2669
  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"
2670
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
2082
2671
  data-thumb
2083
2672
  [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2084
- [style.width]="'calc(100% / ' + options().length + ')'"
2673
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2085
2674
  ></span>
2086
2675
  }
2087
2676
 
@@ -2116,10 +2705,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2116
2705
  @if (thumbIndex(); as index) {
2117
2706
  <span
2118
2707
  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"
2708
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08),inset_0_0_0_1px_var(--border)] transition-transform duration-(--duration-fast) ease-out"
2120
2709
  data-thumb
2121
2710
  [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2122
- [style.width]="'calc(100% / ' + options().length + ')'"
2711
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2123
2712
  ></span>
2124
2713
  }
2125
2714
 
@@ -2142,13 +2731,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2142
2731
  }]
2143
2732
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
2144
2733
 
2145
- const SELECT_CLASSES = 'cursor-pointer pr-8';
2734
+ 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
2735
  // 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.
2736
+ // emits nothing.
2148
2737
  const PICKER_CLASSES = [
2149
2738
  'pointer-fine:supports-[appearance:base-select]:[appearance:base-select]',
2150
2739
  'pointer-fine:supports-[appearance:base-select]:items-center',
2151
- 'pointer-fine:supports-[appearance:base-select]:pr-3',
2152
2740
  'pointer-fine:supports-[appearance:base-select]:[&_option]:cursor-pointer',
2153
2741
  'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-control',
2154
2742
  'pointer-fine:supports-[appearance:base-select]:[&_option]:px-2.5',
@@ -2156,8 +2744,7 @@ const PICKER_CLASSES = [
2156
2744
  'pointer-fine:supports-[appearance:base-select]:[&_option:hover]:bg-(--soft)',
2157
2745
  'pointer-fine:supports-[appearance:base-select]:[&_option:focus]:bg-(--soft)',
2158
2746
  'pointer-fine:supports-[appearance:base-select]:[&_option:checked]:font-medium',
2159
- '[&::picker-icon]:text-caption',
2160
- '[&::picker-icon]:text-(--muted-foreground)',
2747
+ '[&::picker-icon]:hidden',
2161
2748
  '[&::picker(select)]:[appearance:base-select]',
2162
2749
  '[&::picker(select)]:my-1.5',
2163
2750
  '[&::picker(select)]:rounded-control',
@@ -2186,7 +2773,7 @@ class UiSelect {
2186
2773
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2187
2774
  touched = input(false, /* @ts-ignore */
2188
2775
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2189
- classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
2776
+ classes = computed(() => `${CONTROL_BASE_CLASSES} bg-(--background) ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
2190
2777
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2191
2778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2192
2779
  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 });
@@ -2222,6 +2809,8 @@ class UiSkeleton {
2222
2809
  ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
2223
2810
  height = input(12, /* @ts-ignore */
2224
2811
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
2812
+ size = input(232, /* @ts-ignore */
2813
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2225
2814
  animation = PULSE_ANIMATION;
2226
2815
  bars = computed(() => {
2227
2816
  const count = Math.max(1, this.lines());
@@ -2229,28 +2818,33 @@ class UiSkeleton {
2229
2818
  }, /* @ts-ignore */
2230
2819
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
2231
2820
  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"] });
2821
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
2233
2822
  }
2234
2823
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
2235
2824
  type: Component,
2236
2825
  args: [{ selector: 'ui-skeleton', template: `
2237
2826
  @switch (shape()) {
2238
2827
  @case ('figure') {
2239
- <span class="${BAR_CLASSES} h-11 w-56" [style.animation]="animation"></span>
2828
+ <span class="${BAR_CLASSES} h-10 w-65" [style.animation]="animation"></span>
2240
2829
  }
2241
2830
  @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>
2831
+ <span class="flex h-14 items-center justify-between gap-4">
2832
+ <span class="flex min-w-0 flex-1 flex-col gap-1.5">
2833
+ <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
2834
+ <span class="${BAR_CLASSES} h-3 w-20" [style.animation]="animation"></span>
2835
+ </span>
2836
+ <span class="${BAR_CLASSES} h-4 w-[90px] flex-none" [style.animation]="animation"></span>
2245
2837
  </span>
2246
2838
  }
2247
2839
  @case ('chart') {
2248
- <span class="rounded-container h-48 w-full bg-(--muted)" [style.animation]="animation"></span>
2840
+ <span class="rounded-control min-h-50 w-full flex-1 bg-(--muted)" [style.animation]="animation"></span>
2249
2841
  }
2250
2842
  @case ('ring') {
2251
2843
  <span
2252
- class="rounded-pill size-40 border-[24px] border-(--muted) bg-transparent"
2844
+ class="rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]"
2253
2845
  [style.animation]="animation"
2846
+ [style.height.px]="size()"
2847
+ [style.width.px]="size()"
2254
2848
  ></span>
2255
2849
  }
2256
2850
  @default {
@@ -2268,15 +2862,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2268
2862
  'aria-hidden': 'true',
2269
2863
  class: 'flex flex-col gap-2',
2270
2864
  }, 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 }] }] } });
2865
+ }], 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
2866
 
2273
2867
  /** Available stat sizes. `StatSize` is derived from this tuple. */
2274
2868
  const STAT_SIZES = ['hero', 'tile'];
2275
2869
  const VALUE_CLASSES = {
2276
- hero: 'text-display font-semibold text-(--foreground)',
2277
- tile: 'text-title font-semibold text-(--foreground)',
2870
+ hero: 'text-display font-semibold tracking-(--tracking-display) text-(--foreground)',
2871
+ tile: 'text-title font-semibold tabular-nums text-(--foreground)',
2872
+ };
2873
+ const LABEL_CLASSES = {
2874
+ hero: 'text-label font-medium text-(--muted-foreground)',
2875
+ tile: 'text-label font-normal text-(--muted-foreground)',
2876
+ };
2877
+ const STACK_CLASSES = {
2878
+ hero: 'flex flex-col gap-1',
2879
+ tile: 'flex flex-col gap-0.5',
2278
2880
  };
2279
- const LABEL_CLASSES = 'text-label font-medium text-(--subtle-foreground)';
2280
2881
  /**
2281
2882
  * Label, figure and a named period of change, at two sizes: a `hero` for a screen's single
2282
2883
  * headline number, or a smaller `tile` repeated in a grid. The figure itself, its delta and its
@@ -2297,21 +2898,24 @@ class UiStat {
2297
2898
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2298
2899
  size = input('tile', /* @ts-ignore */
2299
2900
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2300
- labelClasses = LABEL_CLASSES;
2901
+ labelClasses = computed(() => LABEL_CLASSES[this.size()], /* @ts-ignore */
2902
+ ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
2903
+ stackClasses = computed(() => STACK_CLASSES[this.size()], /* @ts-ignore */
2904
+ ...(ngDevMode ? [{ debugName: "stackClasses" }] : /* istanbul ignore next */ []));
2301
2905
  valueClasses = computed(() => VALUE_CLASSES[this.size()], /* @ts-ignore */
2302
2906
  ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
2303
2907
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
2304
2908
  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
2909
  <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>
2910
+ <div [class]="stackClasses()">
2911
+ <dl [class]="stackClasses()">
2912
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2309
2913
  <dd [class]="valueClasses()">
2310
2914
  <ng-content select="[uiStatValue]" />
2311
2915
  </dd>
2312
2916
  </dl>
2313
2917
 
2314
- <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
2918
+ <div class="text-label flex flex-wrap items-center gap-x-1.5 gap-y-1 text-(--muted-foreground)">
2315
2919
  <ng-content select="[uiStatDelta]" />
2316
2920
  <ng-content select="[uiStatCaption]" />
2317
2921
  </div>
@@ -2327,15 +2931,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2327
2931
  selector: 'ui-stat',
2328
2932
  template: `
2329
2933
  <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>
2934
+ <div [class]="stackClasses()">
2935
+ <dl [class]="stackClasses()">
2936
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2333
2937
  <dd [class]="valueClasses()">
2334
2938
  <ng-content select="[uiStatValue]" />
2335
2939
  </dd>
2336
2940
  </dl>
2337
2941
 
2338
- <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
2942
+ <div class="text-label flex flex-wrap items-center gap-x-1.5 gap-y-1 text-(--muted-foreground)">
2339
2943
  <ng-content select="[uiStatDelta]" />
2340
2944
  <ng-content select="[uiStatCaption]" />
2341
2945
  </div>
@@ -2347,9 +2951,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2347
2951
  }]
2348
2952
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2349
2953
 
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]";
2954
+ 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
2955
  /**
2352
2956
  * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
2957
+ * 51 by 31 on touch, 40 by 24 with a mouse.
2353
2958
  *
2354
2959
  * @example
2355
2960
  * <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
@@ -2372,42 +2977,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2372
2977
 
2373
2978
  /** Breakpoints a secondary column can be held back until. */
2374
2979
  const CELL_BREAKPOINTS = ['md', 'lg'];
2980
+ /** Body row heights. */
2981
+ const TABLE_ROWS = ['48', '52', '60'];
2982
+ const ROW_CLASSES = {
2983
+ '48': '[--table-row:3rem]',
2984
+ '52': '[--table-row:3.25rem]',
2985
+ '60': '[--table-row:3.75rem]',
2986
+ };
2375
2987
  const FROM_CLASSES = {
2376
2988
  md: 'hidden md:table-cell',
2377
2989
  lg: 'hidden lg:table-cell',
2378
2990
  };
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';
2991
+ 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)';
2992
+ const TD_CLASSES = 'h-(--table-row) px-2 align-middle text-body';
2993
+ const NUMERIC_CLASSES = 'text-right tabular-nums whitespace-nowrap';
2382
2994
  const TEXTUAL_CLASSES = 'text-left';
2383
- /** Styled native table. The rows and cells stay plain <tr> and <td>. */
2995
+ const PRIMARY_CLASSES = 'w-full max-w-0';
2996
+ /**
2997
+ * Styled native table. The rows and cells stay plain <tr> and <td>. `row` sets the height of every
2998
+ * body row, `rule` draws or drops the hairline under the header, `spaced` leaves 4px between the
2999
+ * header and the first row.
3000
+ *
3001
+ * @example
3002
+ * <table uiTable row="60" [rule]="false">
3003
+ */
2384
3004
  class UiTable {
3005
+ row = input('48', /* @ts-ignore */
3006
+ ...(ngDevMode ? [{ debugName: "row" }] : /* istanbul ignore next */ []));
3007
+ rule = input(true, { ...(ngDevMode ? { debugName: "rule" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3008
+ spaced = input(false, { ...(ngDevMode ? { debugName: "spaced" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3009
+ classes = computed(() => [
3010
+ 'w-full border-separate border-spacing-0',
3011
+ ROW_CLASSES[this.row()],
3012
+ this.rule() ? '[--table-rule:inset_0_-1px_0_var(--hairline)]' : '[--table-rule:none]',
3013
+ this.spaced() ? '[--table-head-gap:0.25rem]' : '[--table-head-gap:0px]',
3014
+ ].join(' '), /* @ts-ignore */
3015
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2385
3016
  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 });
3017
+ 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
3018
  }
2388
3019
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, decorators: [{
2389
3020
  type: Directive,
2390
3021
  args: [{
2391
3022
  selector: 'table[uiTable]',
2392
3023
  host: {
2393
- class: 'w-full border-collapse',
3024
+ '[class]': 'classes()',
2394
3025
  },
2395
3026
  }]
3027
+ }], 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 }] }] } });
3028
+ /** Body of one group of rows: leaves a 4px tail after its last row. */
3029
+ class UiGroup {
3030
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3031
+ 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 });
3032
+ }
3033
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroup, decorators: [{
3034
+ type: Directive,
3035
+ args: [{
3036
+ selector: 'tbody[uiGroup]',
3037
+ host: { class: 'after:table-row after:h-1 after:content-[""]' },
3038
+ }]
2396
3039
  }] });
2397
3040
  /**
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.
3041
+ * Body or group row. An ordinary row glows on hover and takes the soft fill when `selected`; an
3042
+ * `interactive` row (one that opens something) also presses to the soft fill. A row with no action keeps a
3043
+ * plain name: `<td uiTd primary class="truncate font-medium">`.
3044
+ * `group` marks the row that holds a group band: it carries no hover state.
2400
3045
  *
2401
3046
  * @example
2402
- * <tr uiTr>...
2403
- * <tr uiTr group>...
3047
+ * <tr uiTr [selected]="open">...
3048
+ * <tr uiTr group><td ui-group-cell name="Brokerage">...
2404
3049
  */
2405
3050
  class UiTr {
2406
3051
  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 */
3052
+ selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3053
+ interactive = input(false, { ...(ngDevMode ? { debugName: "interactive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3054
+ classes = computed(() => {
3055
+ if (this.group()) {
3056
+ return '';
3057
+ }
3058
+ return [
3059
+ 'relative [&>td:first-child]:rounded-l-control [&>td:last-child]:rounded-r-control',
3060
+ this.selected() ? '[&>td]:bg-(--soft)' : '',
3061
+ 'hover:[&>td]:bg-(--glow)',
3062
+ this.interactive() ? 'active:[&>td]:bg-(--soft)' : '',
3063
+ ]
3064
+ .filter(Boolean)
3065
+ .join(' ');
3066
+ }, /* @ts-ignore */
2408
3067
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2409
3068
  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 });
3069
+ 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
3070
  }
2412
3071
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, decorators: [{
2413
3072
  type: Directive,
@@ -2415,25 +3074,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2415
3074
  selector: 'tr[uiTr]',
2416
3075
  host: {
2417
3076
  '[class]': 'classes()',
3077
+ '[attr.aria-selected]': 'selected() ? "true" : null',
2418
3078
  },
2419
3079
  }]
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(' ');
3080
+ }], 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 }] }] } });
3081
+ const cellClasses = (base, numeric, primary, from, secondary) => {
3082
+ const breakpoint = from ?? (secondary ? 'lg' : null);
3083
+ return [
3084
+ base,
3085
+ numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES,
3086
+ primary ? PRIMARY_CLASSES : '',
3087
+ breakpoint ? FROM_CLASSES[breakpoint] : '',
3088
+ ]
3089
+ .filter(Boolean)
3090
+ .join(' ');
3091
+ };
2422
3092
  /**
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.
3093
+ * Column header, centred in its 36px. `numeric` right-aligns and lines the digits up; `secondary`
3094
+ * (or `from`) holds a column back until the viewport is wide enough for it; `primary` makes it
3095
+ * 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
3096
  *
2426
3097
  * @example
2427
- * <th uiTh numeric from="md">Average cost</th>
3098
+ * <th uiTh numeric secondary width="108px">Quantity</th>
2428
3099
  */
2429
3100
  class UiTh {
3101
+ width = input(null, /* @ts-ignore */
3102
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2430
3103
  numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3104
+ primary = input(false, { ...(ngDevMode ? { debugName: "primary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3105
+ secondary = input(false, { ...(ngDevMode ? { debugName: "secondary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3106
+ tall = input(false, { ...(ngDevMode ? { debugName: "tall" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2431
3107
  from = input(null, /* @ts-ignore */
2432
3108
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2433
- classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
3109
+ 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
3110
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2435
3111
  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 });
3112
+ 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
3113
  }
2438
3114
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, decorators: [{
2439
3115
  type: Directive,
@@ -2441,18 +3117,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2441
3117
  selector: 'th[uiTh]',
2442
3118
  host: {
2443
3119
  '[class]': 'classes()',
3120
+ '[style.width]': 'width()',
3121
+ '[style.min-width]': 'width()',
2444
3122
  },
2445
3123
  }]
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. */
3124
+ }], 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 }] }] } });
3125
+ /** Body cell. Same inputs as `UiTh` (bar `tall` and `width`), and they must be set on both to match. */
2448
3126
  class UiTd {
2449
3127
  numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3128
+ primary = input(false, { ...(ngDevMode ? { debugName: "primary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3129
+ secondary = input(false, { ...(ngDevMode ? { debugName: "secondary" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2450
3130
  from = input(null, /* @ts-ignore */
2451
3131
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2452
- classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
3132
+ classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.primary(), this.from(), this.secondary()), /* @ts-ignore */
2453
3133
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2454
3134
  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 });
3135
+ 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
3136
  }
2457
3137
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, decorators: [{
2458
3138
  type: Directive,
@@ -2462,9 +3142,143 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2462
3142
  '[class]': 'classes()',
2463
3143
  },
2464
3144
  }]
2465
- }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
3145
+ }], 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 }] }] } });
3146
+ /** Group band sizes. */
3147
+ const GROUP_SIZES = ['md', 'lg'];
3148
+ /**
3149
+ * The band that opens a group of rows: name in 600 (a heading), meta beside it, the projected
3150
+ * total pinned to the right. Spans the whole row through `colspan`.
3151
+ *
3152
+ * @example
3153
+ * <tr uiTr group><td ui-group-cell colspan="4" name="Brokerage" meta="Savings plan">...total...</td></tr>
3154
+ */
3155
+ class UiGroupCell {
3156
+ name = input.required(/* @ts-ignore */
3157
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
3158
+ meta = input(/* @ts-ignore */
3159
+ ...(ngDevMode ? [undefined, { debugName: "meta" }] : /* istanbul ignore next */ []));
3160
+ size = input('md', /* @ts-ignore */
3161
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3162
+ hostClasses = computed(() => `px-0 ${this.size() === 'lg' ? 'pt-3' : 'pt-2.5'} pb-1 align-middle text-body`, /* @ts-ignore */
3163
+ ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
3164
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroupCell, deps: [], target: i0.ɵɵFactoryTarget.Component });
3165
+ 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: `
3166
+ <div
3167
+ class="rounded-control flex items-center justify-between gap-4 bg-(--muted) px-2"
3168
+ [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3169
+ >
3170
+ <div class="flex min-w-0 items-baseline gap-3">
3171
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
3172
+ @if (meta()) {
3173
+ <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3174
+ }
3175
+ </div>
3176
+ <span class="font-semibold whitespace-nowrap tabular-nums"><ng-content /></span>
3177
+ </div>
3178
+ `, isInline: true });
3179
+ }
3180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiGroupCell, decorators: [{
3181
+ type: Component,
3182
+ args: [{
3183
+ selector: 'td[ui-group-cell]',
3184
+ host: {
3185
+ '[class]': 'hostClasses()',
3186
+ },
3187
+ template: `
3188
+ <div
3189
+ class="rounded-control flex items-center justify-between gap-4 bg-(--muted) px-2"
3190
+ [class]="size() === 'lg' ? 'min-h-12' : 'min-h-11'"
3191
+ >
3192
+ <div class="flex min-w-0 items-baseline gap-3">
3193
+ <h2 class="text-body m-0 font-semibold whitespace-nowrap">{{ name() }}</h2>
3194
+ @if (meta()) {
3195
+ <span class="text-label truncate text-(--muted-foreground)">{{ meta() }}</span>
3196
+ }
3197
+ </div>
3198
+ <span class="font-semibold whitespace-nowrap tabular-nums"><ng-content /></span>
3199
+ </div>
3200
+ `,
3201
+ }]
3202
+ }], 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 }] }] } });
3203
+ /**
3204
+ * Makes a link or button the way into its row. By default an invisible layer stretches over the
3205
+ * whole row, so the row is one target for the pointer, the keyboard and a screen reader, and its
3206
+ * focus ring wraps the row. With `[stretch]="false"` only the name is the
3207
+ * target: it underlines on hover and takes its own ring. `current` marks the open line.
3208
+ *
3209
+ * @example
3210
+ * <td uiTd primary><a uiRowLink href="/lines/42">Ferrari</a></td>
3211
+ * <td uiTd primary><a uiRowLink [stretch]="false" href="/accounts/3">Brokerage</a></td>
3212
+ */
3213
+ class UiRowLink {
3214
+ stretch = input(true, { ...(ngDevMode ? { debugName: "stretch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3215
+ current = input(false, { ...(ngDevMode ? { debugName: "current" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3216
+ classes = computed(() => this.stretch()
3217
+ ? '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)'
3218
+ : '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 */
3219
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3220
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3221
+ 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 });
3222
+ }
3223
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowLink, decorators: [{
3224
+ type: Directive,
3225
+ args: [{
3226
+ selector: 'a[uiRowLink], button[uiRowLink]',
3227
+ host: {
3228
+ '[class]': 'classes()',
3229
+ '[attr.aria-current]': 'current() ? "true" : null',
3230
+ },
3231
+ }]
3232
+ }], propDecorators: { stretch: [{ type: i0.Input, args: [{ isSignal: true, alias: "stretch", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }] } });
3233
+ /** A control in another cell of a stretched-link row: lifts it above the layer that covers the row. */
3234
+ class UiRowAction {
3235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3236
+ 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 });
3237
+ }
3238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRowAction, decorators: [{
3239
+ type: Directive,
3240
+ args: [{
3241
+ selector: '[uiRowAction]',
3242
+ host: { class: 'relative z-1' },
3243
+ }]
3244
+ }] });
3245
+ /** Tones of a subtitle: subtle (default), `--stale` at 500, or the color of the cell. */
3246
+ const SUB_TONES = ['subtle', 'stale', 'inherit'];
3247
+ const SUB_TONE_CLASSES = {
3248
+ subtle: 'font-normal text-(--subtle-foreground)',
3249
+ stale: 'font-medium text-(--stale)',
3250
+ inherit: 'font-normal',
3251
+ };
3252
+ /**
3253
+ * A second line under a cell's main text. `narrow` shows it only under 1024px, where it carries
3254
+ * the quantity, average cost and quote that were held back. Its amounts go through `ui-amount`
3255
+ * so they mask.
3256
+ *
3257
+ * @example
3258
+ * <span uiCellSub narrow>10 × 421.26 € · cost 388.10 €</span>
3259
+ * <span uiCellSub tone="stale">Quote of 24/09</span>
3260
+ */
3261
+ class UiCellSub {
3262
+ narrow = input(false, { ...(ngDevMode ? { debugName: "narrow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3263
+ tone = input('subtle', /* @ts-ignore */
3264
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3265
+ classes = computed(() => `${this.narrow() ? 'lg:hidden ' : ''}block truncate text-caption tabular-nums ${SUB_TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
3266
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCellSub, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3268
+ 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 });
3269
+ }
3270
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCellSub, decorators: [{
3271
+ type: Directive,
3272
+ args: [{
3273
+ selector: '[uiCellSub]',
3274
+ host: {
3275
+ '[class]': 'classes()',
3276
+ },
3277
+ }]
3278
+ }], propDecorators: { narrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "narrow", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
2466
3279
 
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)';
3280
+ const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-[3px] touch-manipulation select-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3281
+ const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
2468
3282
  const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
2469
3283
  /**
2470
3284
  * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
@@ -2496,12 +3310,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2496
3310
  */
2497
3311
  class UiTab {
2498
3312
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2499
- classes = computed(() => `${TAB_CLASSES}${this.active() ? ` ${ACTIVE_TAB_CLASSES}` : ''}`, /* @ts-ignore */
3313
+ classes = computed(() => `${TAB_CLASSES} ${this.active() ? ACTIVE_TAB_CLASSES : INACTIVE_TAB_CLASSES}`, /* @ts-ignore */
2500
3314
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2501
3315
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
2502
3316
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiTab, isStandalone: true, selector: "a[ui-tab]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: `
2503
3317
  <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2504
- <span class="text-caption" data-tab-label><ng-content /></span>
3318
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
2505
3319
  `, isInline: true });
2506
3320
  }
2507
3321
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
@@ -2510,7 +3324,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2510
3324
  selector: 'a[ui-tab]',
2511
3325
  template: `
2512
3326
  <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2513
- <span class="text-caption" data-tab-label><ng-content /></span>
3327
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
2514
3328
  `,
2515
3329
  host: {
2516
3330
  '[class]': 'classes()',
@@ -2527,5 +3341,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2527
3341
  * Generated bundle index. Do not edit.
2528
3342
  */
2529
3343
 
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 };
3344
+ export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DIALOG_LAYOUTS, DIALOG_WIDTHS, GROUP_SIZES, METER_TONES, ROW_PADDINGS, ROW_SIZES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiAlert, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiField, UiFieldLeading, UiGroup, UiGroupCell, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowLink, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
2531
3345
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map