@joanroucoux/cairn-ui 0.5.0 → 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;
@@ -1163,6 +1722,9 @@ const placeStartLabel = (points, startY, box, label) => {
1163
1722
  break;
1164
1723
  }
1165
1724
  }
1725
+ if (!best) {
1726
+ return { anchor: 'end', x: box.width, y: label.height };
1727
+ }
1166
1728
  const { anchor, x, y } = best.slot;
1167
1729
  return { anchor, x, y };
1168
1730
  };
@@ -1236,13 +1798,19 @@ const interpolateLine = (from, to, progress) => {
1236
1798
  return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
1237
1799
  };
1238
1800
 
1239
- const DEFAULT_WIDTH = 640;
1240
- const DEFAULT_HEIGHT = 240;
1241
- const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
1242
- const START_LABEL_HEIGHT = 17;
1243
- const identityValue = (value) => `${value}`;
1244
- const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1245
- 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
+
1246
1814
  /**
1247
1815
  * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1248
1816
  *
@@ -1268,10 +1836,19 @@ class UiLineChart {
1268
1836
  ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1269
1837
  valueColumnLabel = input('Value', /* @ts-ignore */
1270
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 });
1271
1846
  #destroyRef = inject(DestroyRef);
1847
+ #wide = wideViewport();
1272
1848
  #frame = null;
1273
1849
  #hasRenderedOnce = false;
1274
1850
  #previousGeometry = null;
1851
+ isCoreTick = isCoreTick;
1275
1852
  svgRef = viewChild.required('svgRef', /* @ts-ignore */
1276
1853
  ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1277
1854
  activeIndex = signal(null, /* @ts-ignore */
@@ -1284,7 +1861,16 @@ class UiLineChart {
1284
1861
  ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1285
1862
  effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1286
1863
  ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1287
- 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 */
1288
1874
  ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1289
1875
  displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1290
1876
  ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
@@ -1304,8 +1890,8 @@ class UiLineChart {
1304
1890
  }
1305
1891
  const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1306
1892
  const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
1307
- const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
1308
- 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) } };
1309
1895
  }, /* @ts-ignore */
1310
1896
  ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1311
1897
  #renderRef = afterNextRender(() => this.#observeSize());
@@ -1350,10 +1936,8 @@ class UiLineChart {
1350
1936
  this.#destroyRef.onDestroy(() => observer.disconnect());
1351
1937
  }
1352
1938
  #cancelAnimation() {
1353
- if (this.#frame !== null) {
1354
- cancelAnimationFrame(this.#frame);
1355
- this.#frame = null;
1356
- }
1939
+ cancelAnimationFrame(this.#frame ?? 0);
1940
+ this.#frame = null;
1357
1941
  }
1358
1942
  #animate(from, to) {
1359
1943
  if (from.points.length < 2 || to.points.length < 2) {
@@ -1363,37 +1947,20 @@ class UiLineChart {
1363
1947
  const startTime = performance.now();
1364
1948
  const step = (now) => {
1365
1949
  const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
1366
- this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
1367
- if (t < 1) {
1368
- this.#frame = requestAnimationFrame(step);
1369
- }
1370
- else {
1371
- this.#frame = null;
1372
- this.animatedLine.set(null);
1373
- }
1950
+ this.animatedLine.set(t < 1 ? interpolateLine(from, to, easeOutQuint(t)) : null);
1951
+ this.#frame = t < 1 ? requestAnimationFrame(step) : null;
1374
1952
  };
1375
1953
  this.#frame = requestAnimationFrame(step);
1376
1954
  }
1377
- coreTick(index, count) {
1378
- if (count <= 3) {
1379
- return true;
1380
- }
1381
- const middle = Math.floor((count - 1) / 2);
1382
- return index === 0 || index === count - 1 || index === middle;
1383
- }
1384
1955
  deltaFor(point) {
1385
1956
  return point.v - this.points()[0].v;
1386
1957
  }
1387
1958
  deltaClasses(point) {
1388
- const delta = this.deltaFor(point);
1389
- return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1390
- }
1391
- tooltipStyle(point) {
1392
- return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
1959
+ return `${this.tooltipClasses().delta} ${deltaTone(this.deltaFor(point))}`;
1393
1960
  }
1394
1961
  onPointerMove(event) {
1395
1962
  const geometry = this.geometry();
1396
- if (!geometry || geometry.points.length === 0) {
1963
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1397
1964
  return;
1398
1965
  }
1399
1966
  const rect = this.svgRef().nativeElement.getBoundingClientRect();
@@ -1406,7 +1973,7 @@ class UiLineChart {
1406
1973
  }
1407
1974
  onKeydown(event) {
1408
1975
  const geometry = this.geometry();
1409
- if (!geometry || geometry.points.length === 0) {
1976
+ if (this.sparkline() || !geometry || geometry.points.length === 0) {
1410
1977
  return;
1411
1978
  }
1412
1979
  const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
@@ -1419,15 +1986,15 @@ class UiLineChart {
1419
1986
  this.activeIndex.set(navigation.index);
1420
1987
  }
1421
1988
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1422
- 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: `
1423
1990
  <div class="relative h-full">
1424
1991
  <svg
1425
1992
  #svgRef
1426
- class="block h-full w-full touch-pan-y"
1993
+ class="block h-full w-full touch-pan-y overflow-visible"
1427
1994
  role="img"
1428
- tabindex="0"
1429
1995
  [attr.aria-label]="label()"
1430
1996
  [attr.height]="effectiveHeight()"
1997
+ [attr.tabindex]="sparkline() ? null : 0"
1431
1998
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1432
1999
  [attr.width]="effectiveWidth()"
1433
2000
  (keydown)="onKeydown($event)"
@@ -1441,94 +2008,100 @@ class UiLineChart {
1441
2008
  fill="none"
1442
2009
  stroke-linecap="round"
1443
2010
  stroke-linejoin="round"
1444
- stroke-width="2"
1445
2011
  [attr.d]="displayLine()"
2012
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1446
2013
  />
1447
2014
 
1448
- <line
1449
- class="stroke-(--border)"
1450
- data-chart-start-line
1451
- stroke-dasharray="4 4"
1452
- stroke-width="1"
1453
- x1="0"
1454
- [attr.x2]="effectiveWidth()"
1455
- [attr.y1]="geometry.start.y"
1456
- [attr.y2]="geometry.start.y"
1457
- />
1458
-
1459
- @if (startLabelInfo(); as info) {
1460
- <text
1461
- class="text-caption fill-(--subtle-foreground)"
1462
- data-chart-start-label
1463
- [attr.text-anchor]="info.placement.anchor"
1464
- [attr.x]="info.placement.x"
1465
- [attr.y]="info.placement.y"
1466
- >
1467
- {{ info.label }}
1468
- </text>
1469
- }
1470
-
1471
- <circle
1472
- class="fill-(--foreground) stroke-(--card)"
1473
- data-chart-end
1474
- r="4"
1475
- stroke-width="2"
1476
- [attr.cx]="geometry.end.x"
1477
- [attr.cy]="geometry.end.y"
1478
- />
1479
-
1480
- @for (
1481
- tick of ticks();
1482
- track tick.t;
1483
- let index = $index;
1484
- let count = $count;
1485
- let first = $first;
1486
- let last = $last
1487
- ) {
1488
- <text
1489
- class="text-caption fill-(--subtle-foreground)"
1490
- data-chart-axis-tick
1491
- [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1492
- [attr.x]="tick.x"
1493
- [attr.y]="effectiveHeight() - 4"
1494
- [class.max-sm:hidden]="!coreTick(index, count)"
1495
- >
1496
- {{ axisFormat()(tick.t) }}
1497
- </text>
1498
- }
1499
-
1500
- @if (active(); as point) {
2015
+ @if (!sparkline()) {
1501
2016
  <line
1502
2017
  class="stroke-(--border)"
1503
- data-chart-crosshair
2018
+ data-chart-start-line
2019
+ stroke-dasharray="3 4"
1504
2020
  stroke-width="1"
1505
- y1="0"
1506
- [attr.x1]="point.x"
1507
- [attr.x2]="point.x"
1508
- [attr.y2]="effectiveHeight()"
2021
+ x1="0"
2022
+ [attr.x2]="effectiveWidth()"
2023
+ [attr.y1]="geometry.start.y"
2024
+ [attr.y2]="geometry.start.y"
1509
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
+
1510
2039
  <circle
1511
- class="fill-(--foreground) stroke-(--card)"
1512
- data-chart-active
2040
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2041
+ data-chart-end
1513
2042
  r="4"
1514
- stroke-width="2"
1515
- [attr.cx]="point.x"
1516
- [attr.cy]="point.y"
2043
+ stroke-width="6"
2044
+ [attr.cx]="geometry.end.x"
2045
+ [attr.cy]="geometry.end.y"
1517
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
+ }
1518
2088
  }
1519
2089
  }
1520
2090
  </svg>
1521
2091
 
1522
2092
  @if (active(); as point) {
1523
2093
  <div
1524
- 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)]"
1525
2095
  data-testid="chart-tooltip"
1526
2096
  role="status"
2097
+ [class]="tooltipClasses().box"
1527
2098
  [style]="tooltipStyle(point)"
1528
2099
  >
1529
- <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1530
- <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1531
- <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>
1532
2105
  </div>
1533
2106
  }
1534
2107
 
@@ -1566,11 +2139,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1566
2139
  <div class="relative h-full">
1567
2140
  <svg
1568
2141
  #svgRef
1569
- class="block h-full w-full touch-pan-y"
2142
+ class="block h-full w-full touch-pan-y overflow-visible"
1570
2143
  role="img"
1571
- tabindex="0"
1572
2144
  [attr.aria-label]="label()"
1573
2145
  [attr.height]="effectiveHeight()"
2146
+ [attr.tabindex]="sparkline() ? null : 0"
1574
2147
  [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1575
2148
  [attr.width]="effectiveWidth()"
1576
2149
  (keydown)="onKeydown($event)"
@@ -1584,94 +2157,100 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1584
2157
  fill="none"
1585
2158
  stroke-linecap="round"
1586
2159
  stroke-linejoin="round"
1587
- stroke-width="2"
1588
2160
  [attr.d]="displayLine()"
2161
+ [attr.stroke-width]="sparkline() ? 1.5 : 2"
1589
2162
  />
1590
2163
 
1591
- <line
1592
- class="stroke-(--border)"
1593
- data-chart-start-line
1594
- stroke-dasharray="4 4"
1595
- stroke-width="1"
1596
- x1="0"
1597
- [attr.x2]="effectiveWidth()"
1598
- [attr.y1]="geometry.start.y"
1599
- [attr.y2]="geometry.start.y"
1600
- />
1601
-
1602
- @if (startLabelInfo(); as info) {
1603
- <text
1604
- class="text-caption fill-(--subtle-foreground)"
1605
- data-chart-start-label
1606
- [attr.text-anchor]="info.placement.anchor"
1607
- [attr.x]="info.placement.x"
1608
- [attr.y]="info.placement.y"
1609
- >
1610
- {{ info.label }}
1611
- </text>
1612
- }
1613
-
1614
- <circle
1615
- class="fill-(--foreground) stroke-(--card)"
1616
- data-chart-end
1617
- r="4"
1618
- stroke-width="2"
1619
- [attr.cx]="geometry.end.x"
1620
- [attr.cy]="geometry.end.y"
1621
- />
1622
-
1623
- @for (
1624
- tick of ticks();
1625
- track tick.t;
1626
- let index = $index;
1627
- let count = $count;
1628
- let first = $first;
1629
- let last = $last
1630
- ) {
1631
- <text
1632
- class="text-caption fill-(--subtle-foreground)"
1633
- data-chart-axis-tick
1634
- [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1635
- [attr.x]="tick.x"
1636
- [attr.y]="effectiveHeight() - 4"
1637
- [class.max-sm:hidden]="!coreTick(index, count)"
1638
- >
1639
- {{ axisFormat()(tick.t) }}
1640
- </text>
1641
- }
1642
-
1643
- @if (active(); as point) {
2164
+ @if (!sparkline()) {
1644
2165
  <line
1645
2166
  class="stroke-(--border)"
1646
- data-chart-crosshair
2167
+ data-chart-start-line
2168
+ stroke-dasharray="3 4"
1647
2169
  stroke-width="1"
1648
- y1="0"
1649
- [attr.x1]="point.x"
1650
- [attr.x2]="point.x"
1651
- [attr.y2]="effectiveHeight()"
2170
+ x1="0"
2171
+ [attr.x2]="effectiveWidth()"
2172
+ [attr.y1]="geometry.start.y"
2173
+ [attr.y2]="geometry.start.y"
1652
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
+
1653
2188
  <circle
1654
- class="fill-(--foreground) stroke-(--card)"
1655
- data-chart-active
2189
+ class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2190
+ data-chart-end
1656
2191
  r="4"
1657
- stroke-width="2"
1658
- [attr.cx]="point.x"
1659
- [attr.cy]="point.y"
2192
+ stroke-width="6"
2193
+ [attr.cx]="geometry.end.x"
2194
+ [attr.cy]="geometry.end.y"
1660
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
+ }
1661
2237
  }
1662
2238
  }
1663
2239
  </svg>
1664
2240
 
1665
2241
  @if (active(); as point) {
1666
2242
  <div
1667
- 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)]"
1668
2244
  data-testid="chart-tooltip"
1669
2245
  role="status"
2246
+ [class]="tooltipClasses().box"
1670
2247
  [style]="tooltipStyle(point)"
1671
2248
  >
1672
- <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1673
- <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1674
- <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>
1675
2254
  </div>
1676
2255
  }
1677
2256
 
@@ -1704,10 +2283,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1704
2283
  class: 'block h-full',
1705
2284
  },
1706
2285
  }]
1707
- }], 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 }] }] } });
1708
2287
 
1709
- 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';
1710
- 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)';
1711
2290
  const nextId = (() => {
1712
2291
  let count = 0;
1713
2292
  return () => `ui-menu-${++count}`;
@@ -1920,12 +2499,12 @@ class UiMeter {
1920
2499
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
1921
2500
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
1922
2501
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
1923
- 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 */
1924
2503
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
1925
2504
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1926
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: `
1927
2506
  <div
1928
- 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)"
1929
2508
  role="meter"
1930
2509
  [attr.aria-label]="label()"
1931
2510
  [attr.aria-valuemax]="1"
@@ -1943,7 +2522,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1943
2522
  selector: 'ui-meter',
1944
2523
  template: `
1945
2524
  <div
1946
- 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)"
1947
2526
  role="meter"
1948
2527
  [attr.aria-label]="label()"
1949
2528
  [attr.aria-valuemax]="1"
@@ -1957,8 +2536,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1957
2536
  }]
1958
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 }] }] } });
1959
2538
 
1960
- 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)';
1961
2540
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
2541
+ const IDLE_CLASSES = 'text-(--muted-foreground)';
1962
2542
  /**
1963
2543
  * One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
1964
2544
  *
@@ -1970,7 +2550,7 @@ const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
1970
2550
  */
1971
2551
  class UiNavItem {
1972
2552
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1973
- 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 */
1974
2554
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1975
2555
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1976
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 });
@@ -1987,7 +2567,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1987
2567
  }]
1988
2568
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
1989
2569
 
1990
- 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)';
1991
2579
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
1992
2580
  /**
1993
2581
  * Clickable row of a list: a holding, an envelope, an account.
@@ -1999,10 +2587,14 @@ const SELECTED_CLASSES$1 = 'bg-(--soft)';
1999
2587
  */
2000
2588
  class UiRow {
2001
2589
  selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2002
- 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 */
2003
2595
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2004
2596
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
2005
- 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 });
2006
2598
  }
2007
2599
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
2008
2600
  type: Component,
@@ -2014,9 +2606,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2014
2606
  '[attr.aria-current]': "selected() ? 'true' : null",
2015
2607
  },
2016
2608
  }]
2017
- }], 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 }] }] } });
2018
2610
 
2019
- 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)";
2020
2612
  const SELECTED_CLASSES = 'text-(--foreground)';
2021
2613
  const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
2022
2614
  /**
@@ -2075,10 +2667,10 @@ class UiSegmented {
2075
2667
  @if (thumbIndex(); as index) {
2076
2668
  <span
2077
2669
  aria-hidden="true"
2078
- 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"
2079
2671
  data-thumb
2080
2672
  [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2081
- [style.width]="'calc(100% / ' + options().length + ')'"
2673
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2082
2674
  ></span>
2083
2675
  }
2084
2676
 
@@ -2113,10 +2705,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2113
2705
  @if (thumbIndex(); as index) {
2114
2706
  <span
2115
2707
  aria-hidden="true"
2116
- 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"
2117
2709
  data-thumb
2118
2710
  [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
2119
- [style.width]="'calc(100% / ' + options().length + ')'"
2711
+ [style.width]="'calc((100% - 4px) / ' + options().length + ')'"
2120
2712
  ></span>
2121
2713
  }
2122
2714
 
@@ -2139,13 +2731,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2139
2731
  }]
2140
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 }] }] } });
2141
2733
 
2142
- 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]';
2143
2735
  // Spelled out prefix included: Tailwind scans for literal strings, so a prefix applied in code
2144
- // emits nothing. Dropping `supports` leaves Firefox the padding of a chevron it does not lay out.
2736
+ // emits nothing.
2145
2737
  const PICKER_CLASSES = [
2146
2738
  'pointer-fine:supports-[appearance:base-select]:[appearance:base-select]',
2147
2739
  'pointer-fine:supports-[appearance:base-select]:items-center',
2148
- 'pointer-fine:supports-[appearance:base-select]:pr-3',
2149
2740
  'pointer-fine:supports-[appearance:base-select]:[&_option]:cursor-pointer',
2150
2741
  'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-control',
2151
2742
  'pointer-fine:supports-[appearance:base-select]:[&_option]:px-2.5',
@@ -2153,8 +2744,7 @@ const PICKER_CLASSES = [
2153
2744
  'pointer-fine:supports-[appearance:base-select]:[&_option:hover]:bg-(--soft)',
2154
2745
  'pointer-fine:supports-[appearance:base-select]:[&_option:focus]:bg-(--soft)',
2155
2746
  'pointer-fine:supports-[appearance:base-select]:[&_option:checked]:font-medium',
2156
- '[&::picker-icon]:text-caption',
2157
- '[&::picker-icon]:text-(--muted-foreground)',
2747
+ '[&::picker-icon]:hidden',
2158
2748
  '[&::picker(select)]:[appearance:base-select]',
2159
2749
  '[&::picker(select)]:my-1.5',
2160
2750
  '[&::picker(select)]:rounded-control',
@@ -2183,7 +2773,7 @@ class UiSelect {
2183
2773
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2184
2774
  touched = input(false, /* @ts-ignore */
2185
2775
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2186
- 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 */
2187
2777
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2188
2778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2189
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 });
@@ -2219,6 +2809,8 @@ class UiSkeleton {
2219
2809
  ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
2220
2810
  height = input(12, /* @ts-ignore */
2221
2811
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
2812
+ size = input(232, /* @ts-ignore */
2813
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2222
2814
  animation = PULSE_ANIMATION;
2223
2815
  bars = computed(() => {
2224
2816
  const count = Math.max(1, this.lines());
@@ -2226,28 +2818,33 @@ class UiSkeleton {
2226
2818
  }, /* @ts-ignore */
2227
2819
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
2228
2820
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
2229
- 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"] });
2230
2822
  }
2231
2823
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
2232
2824
  type: Component,
2233
2825
  args: [{ selector: 'ui-skeleton', template: `
2234
2826
  @switch (shape()) {
2235
2827
  @case ('figure') {
2236
- <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>
2237
2829
  }
2238
2830
  @case ('row') {
2239
- <span class="flex h-14 flex-col justify-center gap-2">
2240
- <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
2241
- <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>
2242
2837
  </span>
2243
2838
  }
2244
2839
  @case ('chart') {
2245
- <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>
2246
2841
  }
2247
2842
  @case ('ring') {
2248
2843
  <span
2249
- class="rounded-pill size-40 border-[24px] border-(--muted) bg-transparent"
2844
+ class="rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]"
2250
2845
  [style.animation]="animation"
2846
+ [style.height.px]="size()"
2847
+ [style.width.px]="size()"
2251
2848
  ></span>
2252
2849
  }
2253
2850
  @default {
@@ -2265,15 +2862,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2265
2862
  'aria-hidden': 'true',
2266
2863
  class: 'flex flex-col gap-2',
2267
2864
  }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
2268
- }], 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 }] }] } });
2269
2866
 
2270
2867
  /** Available stat sizes. `StatSize` is derived from this tuple. */
2271
2868
  const STAT_SIZES = ['hero', 'tile'];
2272
2869
  const VALUE_CLASSES = {
2273
- hero: 'text-display font-semibold text-(--foreground)',
2274
- 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',
2275
2880
  };
2276
- const LABEL_CLASSES = 'text-label font-medium text-(--subtle-foreground)';
2277
2881
  /**
2278
2882
  * Label, figure and a named period of change, at two sizes: a `hero` for a screen's single
2279
2883
  * headline number, or a smaller `tile` repeated in a grid. The figure itself, its delta and its
@@ -2294,21 +2898,24 @@ class UiStat {
2294
2898
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2295
2899
  size = input('tile', /* @ts-ignore */
2296
2900
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2297
- 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 */ []));
2298
2905
  valueClasses = computed(() => VALUE_CLASSES[this.size()], /* @ts-ignore */
2299
2906
  ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
2300
2907
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
2301
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: `
2302
2909
  <div class="flex items-start justify-between gap-4">
2303
- <div class="flex flex-col gap-1">
2304
- <dl>
2305
- <dt [class]="labelClasses">{{ label() }}</dt>
2910
+ <div [class]="stackClasses()">
2911
+ <dl [class]="stackClasses()">
2912
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2306
2913
  <dd [class]="valueClasses()">
2307
2914
  <ng-content select="[uiStatValue]" />
2308
2915
  </dd>
2309
2916
  </dl>
2310
2917
 
2311
- <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)">
2312
2919
  <ng-content select="[uiStatDelta]" />
2313
2920
  <ng-content select="[uiStatCaption]" />
2314
2921
  </div>
@@ -2324,15 +2931,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2324
2931
  selector: 'ui-stat',
2325
2932
  template: `
2326
2933
  <div class="flex items-start justify-between gap-4">
2327
- <div class="flex flex-col gap-1">
2328
- <dl>
2329
- <dt [class]="labelClasses">{{ label() }}</dt>
2934
+ <div [class]="stackClasses()">
2935
+ <dl [class]="stackClasses()">
2936
+ <dt [class]="labelClasses()">{{ label() }}</dt>
2330
2937
  <dd [class]="valueClasses()">
2331
2938
  <ng-content select="[uiStatValue]" />
2332
2939
  </dd>
2333
2940
  </dl>
2334
2941
 
2335
- <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)">
2336
2943
  <ng-content select="[uiStatDelta]" />
2337
2944
  <ng-content select="[uiStatCaption]" />
2338
2945
  </div>
@@ -2344,9 +2951,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2344
2951
  }]
2345
2952
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2346
2953
 
2347
- 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";
2348
2955
  /**
2349
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.
2350
2958
  *
2351
2959
  * @example
2352
2960
  * <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
@@ -2369,42 +2977,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2369
2977
 
2370
2978
  /** Breakpoints a secondary column can be held back until. */
2371
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
+ };
2372
2987
  const FROM_CLASSES = {
2373
2988
  md: 'hidden md:table-cell',
2374
2989
  lg: 'hidden lg:table-cell',
2375
2990
  };
2376
- const TH_CLASSES = 'h-9 px-2 align-bottom text-caption font-medium text-(--subtle-foreground) shadow-[inset_0_-1px_0_var(--hairline)]';
2377
- const TD_CLASSES = 'h-12 px-2 align-middle text-body';
2378
- 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';
2379
2994
  const TEXTUAL_CLASSES = 'text-left';
2380
- /** 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
+ */
2381
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 */ []));
2382
3016
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2383
- 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 });
2384
3018
  }
2385
3019
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, decorators: [{
2386
3020
  type: Directive,
2387
3021
  args: [{
2388
3022
  selector: 'table[uiTable]',
2389
3023
  host: {
2390
- class: 'w-full border-collapse',
3024
+ '[class]': 'classes()',
2391
3025
  },
2392
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
+ }]
2393
3039
  }] });
2394
3040
  /**
2395
- * Body or group row. `group` draws the muted banner that opens each account inside a list of
2396
- * 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.
2397
3045
  *
2398
3046
  * @example
2399
- * <tr uiTr>...
2400
- * <tr uiTr group>...
3047
+ * <tr uiTr [selected]="open">...
3048
+ * <tr uiTr group><td ui-group-cell name="Brokerage">...
2401
3049
  */
2402
3050
  class UiTr {
2403
3051
  group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2404
- 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 */
2405
3067
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2406
3068
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2407
- 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 });
2408
3070
  }
2409
3071
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, decorators: [{
2410
3072
  type: Directive,
@@ -2412,25 +3074,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2412
3074
  selector: 'tr[uiTr]',
2413
3075
  host: {
2414
3076
  '[class]': 'classes()',
3077
+ '[attr.aria-selected]': 'selected() ? "true" : null',
2415
3078
  },
2416
3079
  }]
2417
- }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }] } });
2418
- 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
+ };
2419
3092
  /**
2420
- * Column header. `numeric` right-aligns and lines the digits up; `from` holds a
2421
- * 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.
2422
3096
  *
2423
3097
  * @example
2424
- * <th uiTh numeric from="md">Average cost</th>
3098
+ * <th uiTh numeric secondary width="108px">Quantity</th>
2425
3099
  */
2426
3100
  class UiTh {
3101
+ width = input(null, /* @ts-ignore */
3102
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2427
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 });
2428
3107
  from = input(null, /* @ts-ignore */
2429
3108
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2430
- 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 */
2431
3110
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2432
3111
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2433
- 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 });
2434
3113
  }
2435
3114
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, decorators: [{
2436
3115
  type: Directive,
@@ -2438,18 +3117,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2438
3117
  selector: 'th[uiTh]',
2439
3118
  host: {
2440
3119
  '[class]': 'classes()',
3120
+ '[style.width]': 'width()',
3121
+ '[style.min-width]': 'width()',
2441
3122
  },
2442
3123
  }]
2443
- }], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
2444
- /** 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. */
2445
3126
  class UiTd {
2446
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 });
2447
3130
  from = input(null, /* @ts-ignore */
2448
3131
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
2449
- 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 */
2450
3133
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2451
3134
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2452
- 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 });
2453
3136
  }
2454
3137
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, decorators: [{
2455
3138
  type: Directive,
@@ -2459,9 +3142,143 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2459
3142
  '[class]': 'classes()',
2460
3143
  },
2461
3144
  }]
2462
- }], 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 }] }] } });
2463
3279
 
2464
- 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)';
2465
3282
  const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
2466
3283
  /**
2467
3284
  * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
@@ -2493,12 +3310,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2493
3310
  */
2494
3311
  class UiTab {
2495
3312
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2496
- 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 */
2497
3314
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2498
3315
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
2499
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: `
2500
3317
  <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2501
- <span class="text-caption" data-tab-label><ng-content /></span>
3318
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
2502
3319
  `, isInline: true });
2503
3320
  }
2504
3321
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
@@ -2507,7 +3324,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2507
3324
  selector: 'a[ui-tab]',
2508
3325
  template: `
2509
3326
  <span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
2510
- <span class="text-caption" data-tab-label><ng-content /></span>
3327
+ <span class="text-caption leading-none" data-tab-label><ng-content /></span>
2511
3328
  `,
2512
3329
  host: {
2513
3330
  '[class]': 'classes()',
@@ -2524,5 +3341,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2524
3341
  * Generated bundle index. Do not edit.
2525
3342
  */
2526
3343
 
2527
- 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 };
2528
3345
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map