@joanroucoux/cairn-ui 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,16 @@
1
+ import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
1
2
  import * as i0 from '@angular/core';
2
- import { Component, input, computed, InjectionToken, signal, inject, LOCALE_ID, booleanAttribute, output, Directive, ElementRef, DestroyRef, forwardRef, model, viewChildren, afterNextRender, afterRenderEffect, contentChild, viewChild, effect, numberAttribute, contentChildren } from '@angular/core';
3
- import { NgTemplateOutlet } from '@angular/common';
3
+ import { inject, ElementRef, DestroyRef, afterNextRender, Component, input, booleanAttribute, computed, InjectionToken, signal, LOCALE_ID, effect, untracked, output, Directive, forwardRef, model, viewChildren, afterRenderEffect, contentChild, viewChild, numberAttribute, contentChildren, Injectable } from '@angular/core';
4
4
  import { scaleLinear } from 'd3-scale';
5
5
  import { curveMonotoneX, line } from 'd3-shape';
6
6
 
7
+ const HEIGHT_PROPERTY$1 = '--action-bar-height';
7
8
  /**
8
9
  * Bar of actions fixed above the tab bar on iPhone: the primary actions of a detail screen, within
9
- * thumb reach. It sits 52px plus the safe area above the bottom edge, so it clears `ui-tab-bar`, and
10
+ * thumb reach. It sits `--tab-bar-height` (52px plus the safe area) above the bottom edge, so it clears `ui-tab-bar`, and
10
11
  * it is hidden from `64rem`, where the same actions live in the page itself. Its children share the
11
- * width equally.
12
+ * width equally. While it is on the page it publishes its height as `--action-bar-height` on the
13
+ * root element, which `ui-toaster` adds to its offset so a confirmation never covers the actions.
12
14
  *
13
15
  * @example
14
16
  * <ui-action-bar>
@@ -17,8 +19,23 @@ import { curveMonotoneX, line } from 'd3-shape';
17
19
  * </ui-action-bar>
18
20
  */
19
21
  class UiActionBar {
22
+ constructor() {
23
+ const host = inject(ElementRef).nativeElement;
24
+ const root = inject(DOCUMENT).documentElement;
25
+ const destroyRef = inject(DestroyRef);
26
+ const publish = () => root.style.setProperty(HEIGHT_PROPERTY$1, `${host.offsetHeight}px`);
27
+ afterNextRender(() => {
28
+ publish();
29
+ if (typeof ResizeObserver !== 'undefined') {
30
+ const observer = new ResizeObserver(publish);
31
+ observer.observe(host);
32
+ destroyRef.onDestroy(() => observer.disconnect());
33
+ }
34
+ });
35
+ destroyRef.onDestroy(() => root.style.removeProperty(HEIGHT_PROPERTY$1));
36
+ }
20
37
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiActionBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
21
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiActionBar, isStandalone: true, selector: "ui-action-bar", host: { classAttribute: "fixed inset-x-0 bottom-[calc(52px+env(safe-area-inset-bottom))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden" }, ngImport: i0, template: '<ng-content />', isInline: true });
38
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiActionBar, isStandalone: true, selector: "ui-action-bar", host: { classAttribute: "fixed inset-x-0 bottom-[var(--tab-bar-height,calc(52px+env(safe-area-inset-bottom)))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden" }, ngImport: i0, template: '<ng-content />', isInline: true });
22
39
  }
23
40
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiActionBar, decorators: [{
24
41
  type: Component,
@@ -26,19 +43,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
26
43
  selector: 'ui-action-bar',
27
44
  template: '<ng-content />',
28
45
  host: {
29
- class: 'fixed inset-x-0 bottom-[calc(52px+env(safe-area-inset-bottom))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden',
46
+ class: 'fixed inset-x-0 bottom-[var(--tab-bar-height,calc(52px+env(safe-area-inset-bottom)))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden',
30
47
  },
31
48
  }]
32
- }] });
49
+ }], ctorParameters: () => [] });
33
50
 
34
51
  const ALERT_VARIANTS = ['error', 'warning'];
35
- const BASE_CLASSES$8 = 'flex items-start gap-2.5 text-left text-label';
52
+ const BASE_CLASSES$9 = 'flex items-start gap-2.5 text-left text-label';
36
53
  const VARIANT_CLASSES$3 = {
37
54
  error: 'rounded-control bg-(--card) px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--border)]',
38
55
  warning: 'text-(--foreground)',
39
56
  };
40
57
  /**
41
58
  * Inline message explaining a failure or a consequence: an icon, an optional heading and a text.
59
+ * It fades in when it appears; set `[fadeIn]="false"` on an alert that is part of the page as it
60
+ * opens (an error state after a load), which shows without motion.
42
61
  *
43
62
  * @example
44
63
  * <ui-alert heading="Sign-in failed">Try again.</ui-alert>
@@ -48,10 +67,11 @@ class UiAlert {
48
67
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
49
68
  heading = input(/* @ts-ignore */
50
69
  ...(ngDevMode ? [undefined, { debugName: "heading" }] : /* istanbul ignore next */ []));
51
- classes = computed(() => `${BASE_CLASSES$8} ${VARIANT_CLASSES$3[this.variant()]}`, /* @ts-ignore */
70
+ fadeIn = input(true, { ...(ngDevMode ? { debugName: "fadeIn" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
71
+ classes = computed(() => `${BASE_CLASSES$9} ${VARIANT_CLASSES$3[this.variant()]}${this.fadeIn() ? ' ui-enter-fade' : ''}`, /* @ts-ignore */
52
72
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
53
73
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAlert, deps: [], target: i0.ɵɵFactoryTarget.Component });
54
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiAlert, isStandalone: true, selector: "ui-alert", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.role": "variant() === 'warning' ? 'status' : 'alert'" } }, ngImport: i0, template: `
74
+ 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 }, fadeIn: { classPropertyName: "fadeIn", publicName: "fadeIn", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "animate.leave": "ui-leave-fade" }, properties: { "class": "classes()", "attr.role": "variant() === 'warning' ? 'status' : 'alert'" } }, ngImport: i0, template: `
55
75
  @if (variant() === 'warning') {
56
76
  <svg
57
77
  aria-hidden="true"
@@ -141,11 +161,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
141
161
  </div>
142
162
  `,
143
163
  host: {
164
+ 'animate.leave': 'ui-leave-fade',
144
165
  '[class]': 'classes()',
145
166
  '[attr.role]': "variant() === 'warning' ? 'status' : 'alert'",
146
167
  },
147
168
  }]
148
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }] } });
169
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], fadeIn: [{ type: i0.Input, args: [{ isSignal: true, alias: "fadeIn", required: false }] }] } });
149
170
 
150
171
  const MINUS = '−';
151
172
  const EM_DASH = '—';
@@ -252,13 +273,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
252
273
  }]
253
274
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], signed: [{ type: i0.Input, args: [{ isSignal: true, alias: "signed", required: false }] }], fractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "fractionDigits", required: false }] }], numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], whenMasked: [{ type: i0.Input, args: [{ isSignal: true, alias: "whenMasked", required: false }] }] } });
254
275
 
276
+ const SKELETON_DELAY_MS = 150;
277
+ const SKELETON_MIN_MS = 400;
278
+ /**
279
+ * Turns the live state of a call into the state to draw: `loading` only appears once the call has
280
+ * lasted 150 ms and, once shown, stays at least 400 ms, so a fast call never flashes a skeleton and a
281
+ * slow one never blinks. Until then the previous state stays on screen (a reload keeps its content);
282
+ * `null` means nothing to draw yet, for a first load still under 150 ms. Call it in an injection context.
283
+ *
284
+ * @example
285
+ * protected readonly shown = delayedState(this.state);
286
+ */
287
+ function delayedState(source) {
288
+ const displayed = signal(null, /* @ts-ignore */
289
+ ...(ngDevMode ? [{ debugName: "displayed" }] : /* istanbul ignore next */ []));
290
+ let timer;
291
+ let shownAt = 0;
292
+ const clear = () => {
293
+ clearTimeout(timer);
294
+ timer = undefined;
295
+ };
296
+ inject(DestroyRef).onDestroy(clear);
297
+ effect(() => {
298
+ const state = source();
299
+ untracked(() => {
300
+ if (state === 'loading') {
301
+ if (displayed() === 'loading') {
302
+ clear();
303
+ }
304
+ else {
305
+ timer = setTimeout(() => {
306
+ timer = undefined;
307
+ shownAt = Date.now();
308
+ displayed.set('loading');
309
+ }, SKELETON_DELAY_MS);
310
+ }
311
+ return;
312
+ }
313
+ clear();
314
+ const remaining = shownAt + SKELETON_MIN_MS - Date.now();
315
+ if (displayed() === 'loading' && remaining > 0) {
316
+ timer = setTimeout(() => {
317
+ timer = undefined;
318
+ displayed.set(untracked(source));
319
+ }, remaining);
320
+ }
321
+ else {
322
+ displayed.set(state);
323
+ }
324
+ });
325
+ });
326
+ return displayed.asReadonly();
327
+ }
328
+
255
329
  /** Available async block states. `AsyncState` is derived from this tuple. */
256
330
  const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'];
257
331
  /** Looks of the error block. `AsyncVariant` is derived from this tuple. */
258
332
  const ASYNC_VARIANTS = ['elevated', 'plain', 'emphasis', 'inline'];
259
333
  /** Alignments of the error block; `auto` is centred from 64rem. `AsyncAlign` is derived from this tuple. */
260
334
  const ASYNC_ALIGNS = ['start', 'center', 'auto'];
261
- const RETRY_BASE_CLASSES = 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) text-label';
335
+ const RETRY_BASE_CLASSES = 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[scale,background-color,opacity] [transition-duration:var(--duration-press),var(--duration-fast),var(--duration-fast)] ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) text-label';
262
336
  const RETRY_OUTLINE_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
263
337
  const RETRY_PRIMARY_CLASSES = 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)';
264
338
  const RETRY_BLOCK_CLASSES = 'min-h-(--row-min) gap-2 pl-3 pr-4';
@@ -334,7 +408,8 @@ class UiAsync {
334
408
  card = input(false, { ...(ngDevMode ? { debugName: "card" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
335
409
  fill = input(false, { ...(ngDevMode ? { debugName: "fill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
336
410
  retry = output();
337
- filling = computed(() => this.fill() && this.state() === 'error', /* @ts-ignore */
411
+ shown = delayedState(this.state);
412
+ filling = computed(() => this.fill() && this.shown() === 'error', /* @ts-ignore */
338
413
  ...(ngDevMode ? [{ debugName: "filling" }] : /* istanbul ignore next */ []));
339
414
  iconSize = computed(() => (this.variant() === 'inline' ? 16 : 18), /* @ts-ignore */
340
415
  ...(ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []));
@@ -353,9 +428,9 @@ class UiAsync {
353
428
  return `flex flex-col ${surface} ${spec.gap} ${ALIGN_CLASSES[this.align()]} ${spec.padding[this.align()]}${fill}`;
354
429
  }, /* @ts-ignore */
355
430
  ...(ngDevMode ? [{ debugName: "boxClasses" }] : /* istanbul ignore next */ []));
356
- titleClasses = computed(() => this.#spec()?.title ?? '', /* @ts-ignore */
431
+ titleClasses = computed(() => this.#spec().title, /* @ts-ignore */
357
432
  ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
358
- messageClasses = computed(() => this.#spec()?.message[this.align()] ?? '', /* @ts-ignore */
433
+ messageClasses = computed(() => this.#spec().message[this.align()], /* @ts-ignore */
359
434
  ...(ngDevMode ? [{ debugName: "messageClasses" }] : /* istanbul ignore next */ []));
360
435
  retryButtonClasses = computed(() => {
361
436
  const spec = this.#spec();
@@ -367,7 +442,8 @@ class UiAsync {
367
442
  ...(ngDevMode ? [{ debugName: "retryButtonClasses" }] : /* istanbul ignore next */ []));
368
443
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, deps: [], target: i0.ɵɵFactoryTarget.Component });
369
444
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiAsync, isStandalone: true, selector: "ui-async", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, errorTitle: { classPropertyName: "errorTitle", publicName: "errorTitle", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, retryLabel: { classPropertyName: "retryLabel", publicName: "retryLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { properties: { "attr.aria-busy": "state() === 'loading' || null", "class.flex": "filling()", "class.flex-col": "filling()" } }, ngImport: i0, template: `
370
- @switch (state()) {
445
+ @switch (shown()) {
446
+ @case (null) {}
371
447
  @case ('loading') {
372
448
  <ng-content select="[asyncLoading]" />
373
449
  }
@@ -415,7 +491,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
415
491
  args: [{
416
492
  selector: 'ui-async',
417
493
  template: `
418
- @switch (state()) {
494
+ @switch (shown()) {
495
+ @case (null) {}
419
496
  @case ('loading') {
420
497
  <ng-content select="[asyncLoading]" />
421
498
  }
@@ -467,7 +544,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
467
544
 
468
545
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
469
546
  const AVATAR_SIZES = ['sm', 'md', 'lg', 'auto'];
470
- const BASE_CLASSES$7 = 'inline-flex items-center justify-center rounded-pill bg-(--muted) font-medium tabular-nums';
547
+ const BASE_CLASSES$8 = 'inline-flex items-center justify-center rounded-pill bg-(--muted) font-medium tabular-nums';
471
548
  const SIZE_CLASSES$7 = {
472
549
  sm: 'size-8 text-caption',
473
550
  md: 'size-[34px] text-label',
@@ -487,7 +564,7 @@ class UiAvatar {
487
564
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
488
565
  size = input('md', /* @ts-ignore */
489
566
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
490
- classes = computed(() => `${BASE_CLASSES$7} ${SIZE_CLASSES$7[this.size()]}`, /* @ts-ignore */
567
+ classes = computed(() => `${BASE_CLASSES$8} ${SIZE_CLASSES$7[this.size()]}`, /* @ts-ignore */
491
568
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
492
569
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
493
570
  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 });
@@ -514,21 +591,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
514
591
  */
515
592
  class UiAvatarLink {
516
593
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatarLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
517
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiAvatarLink, isStandalone: true, selector: "a[uiAvatarLink], button[uiAvatarLink]", host: { classAttribute: "rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[transform,background-color] duration-(--duration-press) ease-out hover:bg-(--glow) aria-[current=page]:bg-(--soft) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)" }, ngImport: i0 });
594
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiAvatarLink, isStandalone: true, selector: "a[uiAvatarLink], button[uiAvatarLink]", host: { classAttribute: "rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out hover:bg-(--glow) aria-[current=page]:bg-(--soft) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)" }, ngImport: i0 });
518
595
  }
519
596
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatarLink, decorators: [{
520
597
  type: Directive,
521
598
  args: [{
522
599
  selector: 'a[uiAvatarLink], button[uiAvatarLink]',
523
600
  host: {
524
- class: 'rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[transform,background-color] duration-(--duration-press) ease-out hover:bg-(--glow) aria-[current=page]:bg-(--soft) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)',
601
+ class: 'rounded-pill grid size-11 lg:size-10 place-items-center cursor-pointer select-none touch-manipulation transition-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out hover:bg-(--glow) aria-[current=page]:bg-(--soft) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)',
525
602
  },
526
603
  }]
527
604
  }] });
528
605
 
529
606
  /** Available back link sizes. `BackLinkSize` is derived from this tuple. */
530
607
  const BACK_LINK_SIZES = ['md', 'sm', 'header'];
531
- const BASE_CLASSES$6 = 'inline-flex items-center gap-0.5 rounded-control text-body font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,color] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-(--ring)';
608
+ const BASE_CLASSES$7 = 'inline-flex items-center gap-0.5 rounded-control text-body font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[scale,background-color,color] [transition-duration:var(--duration-press),var(--duration-fast),var(--duration-fast)] ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-(--ring)';
532
609
  const SIZE_CLASSES$6 = {
533
610
  md: 'h-11 pr-3 pl-1 text-(--foreground) focus-visible:outline-offset-2',
534
611
  sm: 'h-9 pr-2 pl-0.5 text-(--muted-foreground) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-offset-2',
@@ -546,7 +623,7 @@ class UiBackLink {
546
623
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
547
624
  chevron = computed(() => CHEVRON[this.size()], /* @ts-ignore */
548
625
  ...(ngDevMode ? [{ debugName: "chevron" }] : /* istanbul ignore next */ []));
549
- classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$6[this.size()]}`, /* @ts-ignore */
626
+ classes = computed(() => `${BASE_CLASSES$7} ${SIZE_CLASSES$6[this.size()]}`, /* @ts-ignore */
550
627
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
551
628
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBackLink, deps: [], target: i0.ɵɵFactoryTarget.Component });
552
629
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiBackLink, isStandalone: true, selector: "a[ui-back-link]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
@@ -595,7 +672,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
595
672
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
596
673
  const BADGE_VARIANTS = ['neutral', 'outline'];
597
674
  const BADGE_SIZES = ['md', 'sm'];
598
- const BASE_CLASSES$5 = 'inline-flex items-center rounded-pill text-caption font-medium';
675
+ const BASE_CLASSES$6 = 'inline-flex items-center rounded-pill text-caption font-medium';
599
676
  const SIZE_CLASSES$5 = {
600
677
  md: 'h-6 px-2.5',
601
678
  sm: 'h-[22px] px-2',
@@ -615,7 +692,7 @@ class UiBadge {
615
692
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
616
693
  size = input('md', /* @ts-ignore */
617
694
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
618
- classes = computed(() => `${BASE_CLASSES$5} ${SIZE_CLASSES$5[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
695
+ classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$5[this.size()]} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
619
696
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
620
697
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
621
698
  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 });
@@ -658,7 +735,7 @@ const BUTTON_SIZES = [
658
735
  'icon',
659
736
  'icon-sm',
660
737
  ];
661
- const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
738
+ const BASE_CLASSES$5 = 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[scale,background-color,opacity] [transition-duration:var(--duration-press),var(--duration-fast),var(--duration-fast)] ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
662
739
  const RING_OUTSIDE = 'focus-visible:outline-offset-2';
663
740
  const RING_INSIDE = 'focus-visible:-outline-offset-2';
664
741
  const OUTLINE = 'bg-(--card) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow) active:bg-(--soft)';
@@ -678,10 +755,10 @@ const SIZE_CLASSES$4 = {
678
755
  slim: 'h-11 lg:h-8 gap-1.5 px-2.5 text-label',
679
756
  xs: 'h-8 gap-1.5 px-2.5 text-label',
680
757
  sm: 'h-9 gap-2 px-3 text-label',
681
- md: 'min-h-(--row-min) gap-2 px-4 text-label has-[>svg:first-child]:pl-3',
758
+ md: 'min-h-(--row-min) gap-2 px-4 text-label has-[>span>svg:first-child]:pl-3',
682
759
  lg: 'h-11 gap-2 px-[18px] text-label',
683
760
  xl: 'h-[50px] gap-2 px-6 text-body',
684
- xxl: 'h-[52px] lg:h-11 gap-2.5 px-6 lg:px-5 text-body lg:has-[>svg:first-child]:pl-4',
761
+ xxl: 'h-[52px] lg:h-11 gap-2.5 px-6 lg:px-5 text-body lg:has-[>span>svg:first-child]:pl-4',
685
762
  tall: 'h-12 lg:h-10 gap-2 px-4 text-body',
686
763
  block: 'h-[50px] lg:h-11 gap-2 px-4 text-body',
687
764
  icon: 'size-(--row-min) gap-2 p-0',
@@ -703,7 +780,7 @@ class UiButton {
703
780
  busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
704
781
  inactive = computed(() => this.loading() || this.busy(), /* @ts-ignore */
705
782
  ...(ngDevMode ? [{ debugName: "inactive" }] : /* istanbul ignore next */ []));
706
- classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES$4[this.size()]}${this.inactive() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
783
+ classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES$4[this.size()]}${this.inactive() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
707
784
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
708
785
  constructor() {
709
786
  const host = inject(ElementRef).nativeElement;
@@ -721,10 +798,10 @@ class UiButton {
721
798
  @if (loading()) {
722
799
  <span
723
800
  aria-hidden="true"
724
- class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
801
+ class="rounded-pill animate-cairn-spin absolute inset-0 m-auto size-4 border-2 border-current border-r-transparent"
725
802
  ></span>
726
803
  }
727
- <ng-content />
804
+ <span class="contents" [class.text-transparent]="loading()"><ng-content /></span>
728
805
  `, isInline: true });
729
806
  }
730
807
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, decorators: [{
@@ -735,10 +812,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
735
812
  @if (loading()) {
736
813
  <span
737
814
  aria-hidden="true"
738
- class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
815
+ class="rounded-pill animate-cairn-spin absolute inset-0 m-auto size-4 border-2 border-current border-r-transparent"
739
816
  ></span>
740
817
  }
741
- <ng-content />
818
+ <span class="contents" [class.text-transparent]="loading()"><ng-content /></span>
742
819
  `,
743
820
  host: {
744
821
  '[class]': 'classes()',
@@ -756,7 +833,7 @@ const CARD_PADDINGS = ['none', 'xs', 'sm', 'md', 'list', 'rows', 'recap', 'panel
756
833
  const CARD_SURFACES = ['always', 'lg', 'max-lg'];
757
834
  /** Border of the card surface; `auto` follows the variant. `CardBorder` is derived from this tuple. */
758
835
  const CARD_BORDERS = ['auto', 'none', 'border', 'hairline'];
759
- const BASE_CLASSES$3 = 'block';
836
+ const BASE_CLASSES$4 = 'block';
760
837
  const VARIANT_CLASSES = {
761
838
  default: {
762
839
  always: 'rounded-container bg-(--card) text-(--card-foreground)',
@@ -861,7 +938,7 @@ class UiCard {
861
938
  const surface = this.surface();
862
939
  const border = this.border();
863
940
  return [
864
- BASE_CLASSES$3,
941
+ BASE_CLASSES$4,
865
942
  VARIANT_CLASSES[variant][surface],
866
943
  BORDER_CLASSES[border === 'auto' ? AUTO_BORDERS[variant] : border][surface],
867
944
  PADDING_CLASSES$1[this.padding()][surface],
@@ -897,7 +974,7 @@ const UI_CONTROL = new InjectionToken('UI_CONTROL');
897
974
 
898
975
  /** Available control sizes. `md` is the 44/40px touch target; `lg` and `xl` are the 48px sheet and sign-in fields. */
899
976
  const CONTROL_SIZES = ['sm', 'md', 'lg', 'xl'];
900
- 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';
977
+ const CONTROL_BASE_CLASSES = 'w-full rounded-control px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] 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';
901
978
  const CONTROL_SURFACES = ['background', 'card'];
902
979
  const CONTROL_SURFACE_CLASSES = {
903
980
  background: 'bg-(--background)',
@@ -962,7 +1039,7 @@ class UiTextarea {
962
1039
  touched = input(false, /* @ts-ignore */
963
1040
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
964
1041
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
965
- 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 });
1042
+ 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)] 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 });
966
1043
  }
967
1044
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
968
1045
  type: Directive,
@@ -1142,7 +1219,7 @@ const DELTA_EMPHASES = ['text', 'pill'];
1142
1219
  const DELTA_SIZES = ['label', 'inherit'];
1143
1220
  /** Font weights. `DeltaWeight` is derived from this tuple. */
1144
1221
  const DELTA_WEIGHTS = ['medium', 'regular'];
1145
- const BASE_CLASSES$2 = 'inline-flex items-center gap-1 tabular-nums';
1222
+ const BASE_CLASSES$3 = 'inline-flex items-center gap-1 tabular-nums';
1146
1223
  const SIZE_CLASSES$3 = {
1147
1224
  label: 'text-label',
1148
1225
  inherit: '',
@@ -1199,7 +1276,7 @@ class UiDelta {
1199
1276
  classes = computed(() => {
1200
1277
  const sign = this.sign();
1201
1278
  const background = this.emphasis() === 'pill' ? `${PILL_CLASSES$1} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
1202
- return `${BASE_CLASSES$2} ${SIZE_CLASSES$3[this.size()]} ${WEIGHT_CLASSES[this.weight()]} ${TEXT_CLASSES[sign]} ${background}`;
1279
+ return `${BASE_CLASSES$3} ${SIZE_CLASSES$3[this.size()]} ${WEIGHT_CLASSES[this.weight()]} ${TEXT_CLASSES[sign]} ${background}`;
1203
1280
  }, /* @ts-ignore */
1204
1281
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1205
1282
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -1230,6 +1307,244 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1230
1307
  }]
1231
1308
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unknownLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unknownLabel", required: true }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], weight: [{ type: i0.Input, args: [{ isSignal: true, alias: "weight", required: false }] }] } });
1232
1309
 
1310
+ const GRACE = 50;
1311
+ function afterExit(element, done) {
1312
+ const style = getComputedStyle(element);
1313
+ const wait = longest(style.transitionDuration) + longest(style.transitionDelay);
1314
+ if (wait === 0) {
1315
+ done();
1316
+ return () => undefined;
1317
+ }
1318
+ const onEnd = (event) => {
1319
+ if (event.target === element && !event.pseudoElement) {
1320
+ cancel();
1321
+ done();
1322
+ }
1323
+ };
1324
+ const timer = setTimeout(() => {
1325
+ cancel();
1326
+ done();
1327
+ }, wait + GRACE);
1328
+ const cancel = () => {
1329
+ clearTimeout(timer);
1330
+ element.removeEventListener('transitionend', onEnd);
1331
+ };
1332
+ element.addEventListener('transitionend', onEnd);
1333
+ return cancel;
1334
+ }
1335
+ function longest(list) {
1336
+ return Math.max(0, ...list.split(',').map(toMs));
1337
+ }
1338
+ function toMs(value) {
1339
+ const amount = Number.parseFloat(value);
1340
+ if (Number.isNaN(amount)) {
1341
+ return 0;
1342
+ }
1343
+ return value.trim().endsWith('ms') ? amount : amount * 1000;
1344
+ }
1345
+
1346
+ const DIALOG_STYLES = `
1347
+ dialog {
1348
+ --dialog-ease: var(--ease-sheet);
1349
+ transition:
1350
+ transform var(--duration-base) var(--dialog-ease),
1351
+ overlay var(--duration-base) allow-discrete,
1352
+ display var(--duration-base) allow-discrete;
1353
+ transform: translateY(var(--drag-y, 0px));
1354
+ }
1355
+
1356
+ dialog:not([open]) {
1357
+ transform: translateY(100%);
1358
+ transition-duration: var(--duration-exit);
1359
+ }
1360
+
1361
+ @starting-style {
1362
+ dialog[open] {
1363
+ transform: translateY(100%);
1364
+ }
1365
+ }
1366
+
1367
+ dialog[data-dragging] {
1368
+ transition: none;
1369
+ }
1370
+
1371
+ @media (prefers-reduced-motion: reduce) {
1372
+ dialog {
1373
+ transition:
1374
+ opacity var(--duration-base) var(--dialog-ease),
1375
+ overlay var(--duration-base) allow-discrete,
1376
+ display var(--duration-base) allow-discrete;
1377
+ opacity: 1;
1378
+ }
1379
+
1380
+ dialog:not([open]) {
1381
+ opacity: 0;
1382
+ transform: translateY(var(--drag-y, 0px));
1383
+ }
1384
+
1385
+ @starting-style {
1386
+ dialog[open] {
1387
+ opacity: 0;
1388
+ transform: translateY(0);
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ @media (min-width: 64rem) {
1394
+ dialog {
1395
+ --dialog-ease: var(--ease-out);
1396
+ transition:
1397
+ opacity var(--duration-base) var(--dialog-ease),
1398
+ transform var(--duration-base) var(--dialog-ease),
1399
+ overlay var(--duration-base) allow-discrete,
1400
+ display var(--duration-base) allow-discrete;
1401
+ opacity: 1;
1402
+ transform: scale(1);
1403
+ }
1404
+
1405
+ dialog:not([open]) {
1406
+ opacity: 0;
1407
+ transform: scale(var(--enter-scale));
1408
+ }
1409
+
1410
+ @starting-style {
1411
+ dialog[open] {
1412
+ opacity: 0;
1413
+ transform: scale(var(--enter-scale));
1414
+ }
1415
+ }
1416
+ }
1417
+
1418
+ dialog::backdrop {
1419
+ transition: opacity var(--duration-base) var(--ease-out);
1420
+ opacity: 1;
1421
+ }
1422
+
1423
+ dialog:not([open])::backdrop {
1424
+ opacity: 0;
1425
+ transition-duration: var(--duration-exit);
1426
+ }
1427
+
1428
+ @starting-style {
1429
+ dialog[open]::backdrop {
1430
+ opacity: 0;
1431
+ }
1432
+ }
1433
+ `;
1434
+
1435
+ const DISMISS_RATIO = 0.3;
1436
+ const DISMISS_SPEED = 0.5;
1437
+ const SPEED_WINDOW = 100;
1438
+ const SLOP = 6;
1439
+ const CONTROLS = 'button, a, input, select, textarea, [role="button"]';
1440
+ class SheetDrag {
1441
+ #panel;
1442
+ #grips;
1443
+ #dismiss;
1444
+ #enabled;
1445
+ #gesture = null;
1446
+ constructor(panel, grips, dismiss, enabled) {
1447
+ this.#panel = panel;
1448
+ this.#grips = grips;
1449
+ this.#dismiss = dismiss;
1450
+ this.#enabled = enabled;
1451
+ for (const grip of grips) {
1452
+ grip.addEventListener('pointerdown', this.#onDown);
1453
+ grip.addEventListener('pointermove', this.#onMove);
1454
+ grip.addEventListener('pointerup', this.#onUp);
1455
+ grip.addEventListener('pointercancel', this.#onCancel);
1456
+ }
1457
+ }
1458
+ reset() {
1459
+ this.#gesture = null;
1460
+ this.#panel.removeAttribute('data-dragging');
1461
+ this.#panel.style.removeProperty('--drag-y');
1462
+ }
1463
+ destroy() {
1464
+ for (const grip of this.#grips) {
1465
+ grip.removeEventListener('pointerdown', this.#onDown);
1466
+ grip.removeEventListener('pointermove', this.#onMove);
1467
+ grip.removeEventListener('pointerup', this.#onUp);
1468
+ grip.removeEventListener('pointercancel', this.#onCancel);
1469
+ }
1470
+ }
1471
+ #onDown = (event) => {
1472
+ const grip = event.currentTarget;
1473
+ if (event.button !== 0 || !this.#enabled() || !isSheet() || event.target.closest(CONTROLS)) {
1474
+ return;
1475
+ }
1476
+ grip.setPointerCapture(event.pointerId);
1477
+ this.#gesture = {
1478
+ id: event.pointerId,
1479
+ startX: event.clientX,
1480
+ startY: event.clientY,
1481
+ dragging: false,
1482
+ height: 0,
1483
+ offset: 0,
1484
+ samples: [{ y: event.clientY, at: event.timeStamp }],
1485
+ };
1486
+ };
1487
+ #onMove = (event) => {
1488
+ const gesture = this.#current(event);
1489
+ if (!gesture) {
1490
+ return;
1491
+ }
1492
+ const dx = event.clientX - gesture.startX;
1493
+ const dy = event.clientY - gesture.startY;
1494
+ if (!gesture.dragging) {
1495
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < SLOP) {
1496
+ return;
1497
+ }
1498
+ if (Math.abs(dx) > Math.abs(dy)) {
1499
+ this.#gesture = null;
1500
+ return;
1501
+ }
1502
+ gesture.dragging = true;
1503
+ gesture.height = this.#panel.getBoundingClientRect().height;
1504
+ this.#panel.setAttribute('data-dragging', '');
1505
+ }
1506
+ gesture.offset = Math.max(0, dy);
1507
+ gesture.samples.push({ y: event.clientY, at: event.timeStamp });
1508
+ this.#panel.style.setProperty('--drag-y', `${gesture.offset}px`);
1509
+ };
1510
+ #onUp = (event) => {
1511
+ const gesture = this.#current(event);
1512
+ if (!gesture) {
1513
+ return;
1514
+ }
1515
+ this.#gesture = null;
1516
+ if (!gesture.dragging) {
1517
+ return;
1518
+ }
1519
+ this.#panel.removeAttribute('data-dragging');
1520
+ const release = { y: event.clientY, at: event.timeStamp };
1521
+ if (gesture.offset > gesture.height * DISMISS_RATIO || releaseSpeed(gesture.samples, release) > DISMISS_SPEED) {
1522
+ this.#dismiss();
1523
+ }
1524
+ else {
1525
+ this.reset();
1526
+ }
1527
+ };
1528
+ #onCancel = (event) => {
1529
+ if (this.#current(event)) {
1530
+ this.#gesture = null;
1531
+ this.#panel.removeAttribute('data-dragging');
1532
+ this.reset();
1533
+ }
1534
+ };
1535
+ #current(event) {
1536
+ return this.#gesture?.id === event.pointerId ? this.#gesture : null;
1537
+ }
1538
+ }
1539
+ function isSheet() {
1540
+ return typeof matchMedia === 'function' && !matchMedia('(min-width: 64rem)').matches;
1541
+ }
1542
+ function releaseSpeed(samples, last) {
1543
+ const first = samples.find((sample) => sample.at >= last.at - SPEED_WINDOW) ?? last;
1544
+ const elapsed = last.at - first.at;
1545
+ return elapsed > 0 ? (last.y - first.y) / elapsed : 0;
1546
+ }
1547
+
1233
1548
  /** Named dialog widths; any other CSS length is accepted as is. `DialogWidth` is derived from this tuple. */
1234
1549
  const DIALOG_WIDTHS = ['md', 'lg'];
1235
1550
  const WIDTH_VALUES = {
@@ -1280,6 +1595,8 @@ const LAYOUTS = {
1280
1595
  footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
1281
1596
  },
1282
1597
  };
1598
+ /** What started a close, as `closed` reports it. `DialogCloseReason` is derived from this tuple. */
1599
+ const DIALOG_CLOSE_REASONS = ['escape', 'backdrop', 'cross', 'drag', 'programmatic'];
1283
1600
  const nextId$3 = (() => {
1284
1601
  let count = 0;
1285
1602
  return () => `ui-dialog-${++count}`;
@@ -1287,6 +1604,9 @@ const nextId$3 = (() => {
1287
1604
  /**
1288
1605
  * Modal dialog on the native <dialog>: a bottom sheet under `64rem`, centered above it.
1289
1606
  *
1607
+ * `busy` keeps it open while its action runs: Escape, a click on the backdrop and a drag of the sheet do
1608
+ * nothing and the cross is disabled. The owner disables its own buttons and still closes it through `open`.
1609
+ *
1290
1610
  * @example
1291
1611
  * <ui-dialog heading="Buy" description="Ferrari" closeLabel="Close" width="528px" [open]="buying()" (dismissed)="buying.set(false)">
1292
1612
  * <p>Body</p>
@@ -1307,7 +1627,9 @@ class UiDialog {
1307
1627
  width = input('lg', /* @ts-ignore */
1308
1628
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
1309
1629
  open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1630
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1310
1631
  dismissed = output();
1632
+ closed = output();
1311
1633
  headingId = nextId$3();
1312
1634
  descriptionId = `${this.headingId}-description`;
1313
1635
  bodyId = `${this.headingId}-body`;
@@ -1327,11 +1649,11 @@ class UiDialog {
1327
1649
  ...(ngDevMode ? [{ debugName: "widthValue" }] : /* istanbul ignore next */ []));
1328
1650
  spec = computed(() => LAYOUTS[this.layout()], /* @ts-ignore */
1329
1651
  ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
1330
- 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 */
1652
+ headerClasses = computed(() => `flex gap-2 pl-4 max-lg:touch-none lg:gap-3 lg:pl-6 ${this.spec().header} ${this.closeLabel() ? 'pr-2 lg:pr-4' : 'pr-4 lg:pr-6'}`, /* @ts-ignore */
1331
1653
  ...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
1332
1654
  titleClasses = computed(() => this.spec().title, /* @ts-ignore */
1333
1655
  ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
1334
- 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 */
1656
+ crossClasses = computed(() => `rounded-pill lg:rounded-control grid size-11 flex-none cursor-pointer place-items-center text-(--muted-foreground) transition-[scale,background-color,color] [transition-duration:var(--duration-press),var(--duration-fast),var(--duration-fast)] ease-out outline-none focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-(--ring) active:scale-(--press-scale) active:bg-(--soft) disabled:pointer-events-none disabled:opacity-40 lg:size-9 lg:hover:bg-(--glow) lg:hover:text-(--foreground) ${this.spec().cross}`, /* @ts-ignore */
1335
1657
  ...(ngDevMode ? [{ debugName: "crossClasses" }] : /* istanbul ignore next */ []));
1336
1658
  iconClasses = computed(() => this.spec().icon, /* @ts-ignore */
1337
1659
  ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
@@ -1343,6 +1665,13 @@ class UiDialog {
1343
1665
  constructor() {
1344
1666
  const destroyRef = inject(DestroyRef);
1345
1667
  afterNextRender(() => {
1668
+ const host = this.#host.nativeElement;
1669
+ const grips = ['[data-dialog-handle]', '[data-dialog-header]'].map((grip) => host.querySelector(grip));
1670
+ this.#drag = new SheetDrag(this.#dialog, grips, () => this.close('drag'), () => !this.busy());
1671
+ destroyRef.onDestroy(() => {
1672
+ this.#drag?.destroy();
1673
+ this.#cancelExit();
1674
+ });
1346
1675
  if (typeof ResizeObserver === 'undefined') {
1347
1676
  return;
1348
1677
  }
@@ -1356,20 +1685,45 @@ class UiDialog {
1356
1685
  // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
1357
1686
  const dialog = this.#host.nativeElement.querySelector('dialog');
1358
1687
  if (this.open() && !dialog.open) {
1688
+ this.#cancelExit();
1689
+ this.#drag?.reset();
1359
1690
  dialog.showModal();
1360
1691
  }
1361
1692
  else if (!this.open() && dialog.open) {
1362
- dialog.close();
1693
+ this.close('programmatic');
1363
1694
  }
1364
1695
  });
1365
1696
  }
1366
1697
  #pressedOnBackdrop = false;
1698
+ #drag = null;
1699
+ #reason = null;
1700
+ #cancelExit = () => undefined;
1701
+ close(reason) {
1702
+ if (this.#dialog.open) {
1703
+ this.#reason = reason;
1704
+ this.#dialog.close();
1705
+ }
1706
+ }
1707
+ onCancel(event) {
1708
+ if (event.target !== event.currentTarget) {
1709
+ return;
1710
+ }
1711
+ event.preventDefault();
1712
+ if (!this.busy()) {
1713
+ this.close('escape');
1714
+ }
1715
+ }
1716
+ onKeydown(event) {
1717
+ if (event.key === 'Escape' && this.busy()) {
1718
+ event.preventDefault();
1719
+ }
1720
+ }
1367
1721
  onPointerDown(event) {
1368
1722
  this.#pressedOnBackdrop = event.target === this.#dialog && this.#outside(event);
1369
1723
  }
1370
1724
  onClick(event) {
1371
- if (this.#pressedOnBackdrop && event.target === this.#dialog && this.#outside(event)) {
1372
- this.#dialog.close();
1725
+ if (this.#pressedOnBackdrop && !this.busy() && event.target === this.#dialog && this.#outside(event)) {
1726
+ this.close('backdrop');
1373
1727
  }
1374
1728
  this.#pressedOnBackdrop = false;
1375
1729
  }
@@ -1381,23 +1735,48 @@ class UiDialog {
1381
1735
  return (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom);
1382
1736
  }
1383
1737
  onNativeClose() {
1384
- // The owner already knows about a close it asked for; only a close it did not ask for is news.
1385
- if (this.open()) {
1738
+ const asked = this.#reason;
1739
+ this.#reason = null;
1740
+ if (this.#dialog.open) {
1741
+ return;
1742
+ }
1743
+ if (asked === null && this.busy()) {
1744
+ this.#dialog.showModal();
1745
+ return;
1746
+ }
1747
+ const reason = asked ?? 'escape';
1748
+ if (reason !== 'programmatic') {
1386
1749
  this.dismissed.emit();
1387
1750
  }
1751
+ this.#awaitExit(reason);
1752
+ }
1753
+ #awaitExit(reason) {
1754
+ this.#cancelExit();
1755
+ this.#cancelExit = afterExit(this.#dialog, () => {
1756
+ this.#cancelExit = () => undefined;
1757
+ this.#drag?.reset();
1758
+ this.closed.emit(reason);
1759
+ });
1388
1760
  }
1389
1761
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
1390
- 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: `
1762
+ 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 }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed", closed: "closed" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)", "keydown": "onKeydown($event)" } }, ngImport: i0, template: `
1391
1763
  <dialog
1392
1764
  #dlg
1765
+ [attr.aria-busy]="busy() || null"
1393
1766
  [attr.aria-describedby]="describedBy()"
1394
1767
  [attr.aria-labelledby]="headingId"
1395
1768
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1396
1769
  [class]="classes()"
1397
1770
  [style.--dialog-width]="widthValue()"
1771
+ (cancel)="onCancel($event)"
1398
1772
  (close)="onNativeClose()"
1399
1773
  >
1400
- <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1774
+ <div
1775
+ aria-hidden="true"
1776
+ class="flex h-5 touch-none justify-center lg:hidden"
1777
+ data-dialog-handle
1778
+ [class]="spec().handle"
1779
+ >
1401
1780
  <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
1402
1781
  </div>
1403
1782
 
@@ -1417,7 +1796,13 @@ class UiDialog {
1417
1796
  </div>
1418
1797
 
1419
1798
  @if (closeLabel()) {
1420
- <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1799
+ <button
1800
+ type="button"
1801
+ [attr.aria-label]="closeLabel()"
1802
+ [class]="crossClasses()"
1803
+ [disabled]="busy()"
1804
+ (click)="close('cross')"
1805
+ >
1421
1806
  <svg
1422
1807
  aria-hidden="true"
1423
1808
  class="block size-[22px] fill-none stroke-current stroke-[1.75]"
@@ -1451,21 +1836,32 @@ class UiDialog {
1451
1836
  <ng-content select="[dialogActions]" />
1452
1837
  </div>
1453
1838
  </dialog>
1454
- `, 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"] });
1839
+ `, isInline: true, styles: ["dialog{--dialog-ease: var(--ease-sheet);transition:transform var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;transform:translateY(var(--drag-y, 0px))}dialog:not([open]){transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{transform:translateY(100%)}}dialog[data-dragging]{transition:none}@media(prefers-reduced-motion:reduce){dialog{transition:opacity var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1}dialog:not([open]){opacity:0;transform:translateY(var(--drag-y, 0px))}@starting-style{dialog[open]{opacity:0;transform:translateY(0)}}}@media(min-width:64rem){dialog{--dialog-ease: var(--ease-out);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:scale(1)}dialog:not([open]){opacity:0;transform:scale(var(--enter-scale))}@starting-style{dialog[open]{opacity:0;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"] });
1455
1840
  }
1456
1841
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
1457
1842
  type: Component,
1458
- args: [{ selector: 'ui-dialog', host: { '(pointerdown)': 'onPointerDown($event)', '(click)': 'onClick($event)' }, template: `
1843
+ args: [{ selector: 'ui-dialog', host: {
1844
+ '(pointerdown)': 'onPointerDown($event)',
1845
+ '(click)': 'onClick($event)',
1846
+ '(keydown)': 'onKeydown($event)',
1847
+ }, template: `
1459
1848
  <dialog
1460
1849
  #dlg
1850
+ [attr.aria-busy]="busy() || null"
1461
1851
  [attr.aria-describedby]="describedBy()"
1462
1852
  [attr.aria-labelledby]="headingId"
1463
1853
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1464
1854
  [class]="classes()"
1465
1855
  [style.--dialog-width]="widthValue()"
1856
+ (cancel)="onCancel($event)"
1466
1857
  (close)="onNativeClose()"
1467
1858
  >
1468
- <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1859
+ <div
1860
+ aria-hidden="true"
1861
+ class="flex h-5 touch-none justify-center lg:hidden"
1862
+ data-dialog-handle
1863
+ [class]="spec().handle"
1864
+ >
1469
1865
  <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
1470
1866
  </div>
1471
1867
 
@@ -1485,7 +1881,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1485
1881
  </div>
1486
1882
 
1487
1883
  @if (closeLabel()) {
1488
- <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1884
+ <button
1885
+ type="button"
1886
+ [attr.aria-label]="closeLabel()"
1887
+ [class]="crossClasses()"
1888
+ [disabled]="busy()"
1889
+ (click)="close('cross')"
1890
+ >
1489
1891
  <svg
1490
1892
  aria-hidden="true"
1491
1893
  class="block size-[22px] fill-none stroke-current stroke-[1.75]"
@@ -1519,12 +1921,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1519
1921
  <ng-content select="[dialogActions]" />
1520
1922
  </div>
1521
1923
  </dialog>
1522
- `, 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"] }]
1523
- }], 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"] }] } });
1924
+ `, styles: ["dialog{--dialog-ease: var(--ease-sheet);transition:transform var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;transform:translateY(var(--drag-y, 0px))}dialog:not([open]){transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{transform:translateY(100%)}}dialog[data-dragging]{transition:none}@media(prefers-reduced-motion:reduce){dialog{transition:opacity var(--duration-base) var(--dialog-ease),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1}dialog:not([open]){opacity:0;transform:translateY(var(--drag-y, 0px))}@starting-style{dialog[open]{opacity:0;transform:translateY(0)}}}@media(min-width:64rem){dialog{--dialog-ease: var(--ease-out);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:scale(1)}dialog:not([open]){opacity:0;transform:scale(var(--enter-scale))}@starting-style{dialog[open]{opacity:0;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"] }]
1925
+ }], 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 }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1524
1926
 
1525
1927
  const RADIUS = 78;
1526
1928
  const RING_WIDTH = 30;
1527
- const ACTIVE_RING_WIDTH = 38;
1528
1929
  const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1529
1930
  const GAP = 2;
1530
1931
  function positionSlices(slices) {
@@ -1571,7 +1972,8 @@ function rankSlices(slices, othersLabel) {
1571
1972
 
1572
1973
  const identityValue$1 = (value) => `${value}`;
1573
1974
  const identityShare = (share) => `${Math.round(share * 100)}%`;
1574
- const ROW_CLASSES$1 = 'flex w-full items-center gap-3 min-h-14 px-2 py-1 [font-variant-numeric:var(--numeric)] rounded-control text-left transition-colors duration-(--duration-fast) ease-out hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1975
+ 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-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out hover:bg-(--glow) active:bg-(--soft) active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
1976
+ const SLICE_CLASSES = 'cursor-pointer origin-center [transform-box:view-box] transition-[scale,opacity] duration-(--duration-fast) ease-out';
1575
1977
  const RAMP_DOT_CLASSES = {
1576
1978
  1: 'bg-(--ramp-1)',
1577
1979
  2: 'bg-(--ramp-2)',
@@ -1625,8 +2027,12 @@ class UiDonut {
1625
2027
  strokeColor(slice) {
1626
2028
  return `var(--ramp-${slice.ramp})`;
1627
2029
  }
1628
- strokeWidth(slice) {
1629
- return this.isActive(slice) ? ACTIVE_RING_WIDTH : RING_WIDTH;
2030
+ sliceClasses(slice) {
2031
+ const id = this.activeId();
2032
+ if (id === null) {
2033
+ return SLICE_CLASSES;
2034
+ }
2035
+ return `${SLICE_CLASSES} ${id === slice.id ? 'scale-104 motion-reduce:scale-100' : 'opacity-50'}`;
1630
2036
  }
1631
2037
  dotClasses(slice) {
1632
2038
  return `size-3 flex-none rounded-pill shadow-[inset_0_0_0_1px_var(--border)] ${RAMP_DOT_CLASSES[slice.ramp]}`;
@@ -1676,7 +2082,6 @@ class UiDonut {
1676
2082
  />
1677
2083
  @for (slice of positioned(); track slice.id) {
1678
2084
  <circle
1679
- class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1680
2085
  cx="100"
1681
2086
  cy="100"
1682
2087
  data-slice
@@ -1684,8 +2089,9 @@ class UiDonut {
1684
2089
  [attr.r]="radius"
1685
2090
  [attr.stroke-dasharray]="slice.dash"
1686
2091
  [attr.stroke-dashoffset]="slice.offset"
2092
+ [attr.stroke-width]="ringWidth"
2093
+ [class]="sliceClasses(slice)"
1687
2094
  [style.stroke]="strokeColor(slice)"
1688
- [style.stroke-width.px]="strokeWidth(slice)"
1689
2095
  (click)="select(slice)"
1690
2096
  (pointerenter)="highlight(slice.id)"
1691
2097
  (pointerleave)="clearHighlight(slice.id)"
@@ -1771,7 +2177,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1771
2177
  />
1772
2178
  @for (slice of positioned(); track slice.id) {
1773
2179
  <circle
1774
- class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1775
2180
  cx="100"
1776
2181
  cy="100"
1777
2182
  data-slice
@@ -1779,8 +2184,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1779
2184
  [attr.r]="radius"
1780
2185
  [attr.stroke-dasharray]="slice.dash"
1781
2186
  [attr.stroke-dashoffset]="slice.offset"
2187
+ [attr.stroke-width]="ringWidth"
2188
+ [class]="sliceClasses(slice)"
1782
2189
  [style.stroke]="strokeColor(slice)"
1783
- [style.stroke-width.px]="strokeWidth(slice)"
1784
2190
  (click)="select(slice)"
1785
2191
  (pointerenter)="highlight(slice.id)"
1786
2192
  (pointerleave)="clearHighlight(slice.id)"
@@ -1855,6 +2261,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1855
2261
  }]
1856
2262
  }], propDecorators: { slices: [{ type: i0.Input, args: [{ isSignal: true, alias: "slices", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], othersLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "othersLabel", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], shareFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareFormat", required: false }] }], legendHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "legendHref", required: false }] }], sliceSelect: [{ type: i0.Output, args: ["sliceSelect"] }] } });
1857
2263
 
2264
+ /**
2265
+ * Centred empty state: a title, a hint and an action slot, such as a catalogue search that matches
2266
+ * nothing. The padding is 24px, 48px from 64rem.
2267
+ *
2268
+ * @example
2269
+ * <ui-empty heading="Aucun instrument ne correspond à « zzz »" hint="La recherche porte sur le nom et l'ISIN.">
2270
+ * <button ui-button variant="outline" size="md" type="button">Ajouter une ligne</button>
2271
+ * </ui-empty>
2272
+ */
2273
+ class UiEmpty {
2274
+ heading = input.required(/* @ts-ignore */
2275
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2276
+ hint = input('', /* @ts-ignore */
2277
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiEmpty, deps: [], target: i0.ɵɵFactoryTarget.Component });
2279
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiEmpty, isStandalone: true, selector: "ui-empty", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col items-center gap-1.5 px-2 py-6 text-center lg:py-12" }, ngImport: i0, template: `
2280
+ <span class="text-body font-medium text-pretty">{{ heading() }}</span>
2281
+ @if (hint()) {
2282
+ <span class="text-label text-(--muted-foreground)">{{ hint() }}</span>
2283
+ }
2284
+ <div class="mt-2 empty:hidden"><ng-content /></div>
2285
+ `, isInline: true });
2286
+ }
2287
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiEmpty, decorators: [{
2288
+ type: Component,
2289
+ args: [{
2290
+ selector: 'ui-empty',
2291
+ template: `
2292
+ <span class="text-body font-medium text-pretty">{{ heading() }}</span>
2293
+ @if (hint()) {
2294
+ <span class="text-label text-(--muted-foreground)">{{ hint() }}</span>
2295
+ }
2296
+ <div class="mt-2 empty:hidden"><ng-content /></div>
2297
+ `,
2298
+ host: { class: 'flex flex-col items-center gap-1.5 px-2 py-6 text-center lg:py-12' },
2299
+ }]
2300
+ }], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
2301
+
1858
2302
  /**
1859
2303
  * Link to another site: the label followed by an external-link icon. A 44px target on touch,
1860
2304
  * an underline on hover with a mouse.
@@ -2045,14 +2489,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2045
2489
  */
2046
2490
  class UiFieldTrailing {
2047
2491
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2048
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldTrailing, isStandalone: true, selector: "[uiFieldTrailing]", host: { classAttribute: "absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)" }, ngImport: i0 });
2492
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldTrailing, isStandalone: true, selector: "[uiFieldTrailing]", host: { classAttribute: "absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) transition-[color] duration-(--duration-fast) ease-out hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)" }, ngImport: i0 });
2049
2493
  }
2050
2494
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, decorators: [{
2051
2495
  type: Directive,
2052
2496
  args: [{
2053
2497
  selector: '[uiFieldTrailing]',
2054
2498
  host: {
2055
- class: 'absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)',
2499
+ class: 'absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) transition-[color] duration-(--duration-fast) ease-out hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)',
2056
2500
  },
2057
2501
  }]
2058
2502
  }] });
@@ -2179,7 +2623,9 @@ class UiField {
2179
2623
  }
2180
2624
 
2181
2625
  @if (message()) {
2182
- <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2626
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
2627
+ {{ message() }}
2628
+ </p>
2183
2629
  }
2184
2630
  </div>
2185
2631
  `, isInline: true });
@@ -2212,7 +2658,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2212
2658
  }
2213
2659
 
2214
2660
  @if (message()) {
2215
- <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2661
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
2662
+ {{ message() }}
2663
+ </p>
2216
2664
  }
2217
2665
  </div>
2218
2666
  `,
@@ -2299,6 +2747,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2299
2747
  }]
2300
2748
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }] } });
2301
2749
 
2750
+ const QUERY = '(prefers-reduced-motion: reduce)';
2751
+ /**
2752
+ * A signal that follows the user's `prefers-reduced-motion` preference live.
2753
+ * Call it in an injection context; the listener is removed with that context.
2754
+ *
2755
+ * @example
2756
+ * protected readonly reduced = injectReducedMotion();
2757
+ */
2758
+ function injectReducedMotion() {
2759
+ if (typeof matchMedia === 'undefined') {
2760
+ return signal(false).asReadonly();
2761
+ }
2762
+ const query = matchMedia(QUERY);
2763
+ const reduced = signal(query.matches, /* @ts-ignore */
2764
+ ...(ngDevMode ? [{ debugName: "reduced" }] : /* istanbul ignore next */ []));
2765
+ const listener = (event) => reduced.set(event.matches);
2766
+ query.addEventListener('change', listener);
2767
+ inject(DestroyRef).onDestroy(() => query.removeEventListener('change', listener));
2768
+ return reduced.asReadonly();
2769
+ }
2770
+
2302
2771
  const axisTicks = (points, maxTicks = 5) => {
2303
2772
  if (points.length <= maxTicks) {
2304
2773
  return [...points];
@@ -2441,6 +2910,23 @@ const buildGeometry = (points, width, height, padding = {}) => {
2441
2910
  };
2442
2911
  };
2443
2912
 
2913
+ const observeSize = (element, destroyRef, measured) => {
2914
+ if (typeof ResizeObserver === 'undefined') {
2915
+ return;
2916
+ }
2917
+ const { clientWidth: width, clientHeight: height } = element;
2918
+ if (width > 0 && height > 0) {
2919
+ measured({ width, height });
2920
+ }
2921
+ const observer = new ResizeObserver(([entry]) => {
2922
+ if (entry) {
2923
+ measured({ width: entry.contentRect.width, height: entry.contentRect.height });
2924
+ }
2925
+ });
2926
+ observer.observe(element);
2927
+ destroyRef.onDestroy(() => observer.disconnect());
2928
+ };
2929
+
2444
2930
  const LINE_GAP = 6;
2445
2931
  const COLLISION_MARGIN = 6;
2446
2932
  const EDGE_TOLERANCE = 3;
@@ -2489,7 +2975,15 @@ const placeStartLabel = (points, startY, box, label) => {
2489
2975
  const TRANSITION_SAMPLES = 120;
2490
2976
  const TRANSITION_DURATION = 260;
2491
2977
  const easeOutQuint = (t) => 1 - (1 - t) ** 5;
2492
- const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
2978
+ const durationFrom = (value) => {
2979
+ const match = /^\s*([\d.]+)(ms|s)\s*$/.exec(value);
2980
+ const duration = match ? Number(match[1]) * (match[2] === 's' ? 1000 : 1) : Number.NaN;
2981
+ return Number.isFinite(duration) ? duration : TRANSITION_DURATION;
2982
+ };
2983
+ const sameSeries = (a, b) => a === b ||
2984
+ (b !== null &&
2985
+ a.length === b.length &&
2986
+ a.every((point, index) => point.t === b[index].t && point.v === b[index].v));
2493
2987
  const monotoneTangents = (xs, ys) => {
2494
2988
  const n = xs.length;
2495
2989
  const slopes = [];
@@ -2545,15 +3039,85 @@ const sampleMonotone = (points, sampleXs) => {
2545
3039
  return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
2546
3040
  });
2547
3041
  };
2548
- const buildPolyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
2549
- const interpolateLine = (from, to, progress) => {
2550
- const left = to.points[0].x;
2551
- const right = to.points.at(-1).x;
2552
- const samples = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
2553
- const fromYs = sampleMonotone(from.points, samples);
2554
- const toYs = sampleMonotone(to.points, samples);
2555
- return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
3042
+ const polyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
3043
+ const shapeTransition = (from, to) => {
3044
+ const left = to[0].x;
3045
+ const right = to.at(-1).x;
3046
+ const xs = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
3047
+ return { xs, from: sampleMonotone(from, xs), to: sampleMonotone(to, xs) };
2556
3048
  };
3049
+ const shapeAt = (transition, progress) => transition.from.map((fromY, index) => fromY + (transition.to[index] - fromY) * progress);
3050
+ const animateShape = (from, to, duration, draw, done) => {
3051
+ const transition = shapeTransition(from, to);
3052
+ const start = performance.now();
3053
+ let frame = 0;
3054
+ const step = (now) => {
3055
+ const t = Math.min(1, Math.max(0, (now - start) / duration));
3056
+ if (t === 1) {
3057
+ done();
3058
+ return;
3059
+ }
3060
+ const ys = shapeAt(transition, easeOutQuint(t));
3061
+ draw(transition.xs.map((x, index) => ({ x, y: ys[index] })), polyline(transition.xs, ys));
3062
+ frame = requestAnimationFrame(step);
3063
+ };
3064
+ frame = requestAnimationFrame(step);
3065
+ return () => cancelAnimationFrame(frame);
3066
+ };
3067
+ class ShapeMotion {
3068
+ #view;
3069
+ #points = null;
3070
+ #target = null;
3071
+ #held = null;
3072
+ #drawn = null;
3073
+ #stop = null;
3074
+ constructor(view) {
3075
+ this.#view = view;
3076
+ }
3077
+ follow(points, geometry, options) {
3078
+ const newSeries = !sameSeries(points, this.#points);
3079
+ this.#points = points;
3080
+ if (this.#stop && !newSeries && !options.reduced && geometry?.line === this.#target?.line) {
3081
+ this.#target = geometry;
3082
+ return;
3083
+ }
3084
+ const from = this.#drawn;
3085
+ const held = this.#held ?? this.#target;
3086
+ this.#target = geometry;
3087
+ this.stop();
3088
+ if (!newSeries || !geometry || !from || from.length < 2 || geometry.points.length < 2 || options.reduced) {
3089
+ this.#settle(geometry);
3090
+ return;
3091
+ }
3092
+ const duration = options.duration();
3093
+ if (duration <= 0) {
3094
+ this.#settle(geometry);
3095
+ return;
3096
+ }
3097
+ this.#hold(held);
3098
+ this.#stop = animateShape(from, geometry.points, duration, (shape, line) => this.#draw(shape, line, shape.at(-1)), () => this.#settle(geometry));
3099
+ }
3100
+ stop() {
3101
+ this.#stop?.();
3102
+ this.#stop = null;
3103
+ }
3104
+ #hold(geometry) {
3105
+ this.#held = geometry;
3106
+ this.#view.hold(geometry);
3107
+ }
3108
+ #settle(geometry) {
3109
+ this.#stop = null;
3110
+ this.#hold(null);
3111
+ this.#drawn = null;
3112
+ if (geometry) {
3113
+ this.#draw(geometry.points, geometry.line, geometry.end);
3114
+ }
3115
+ }
3116
+ #draw(shape, line, end) {
3117
+ this.#drawn = shape;
3118
+ this.#view.draw(line, end);
3119
+ }
3120
+ }
2557
3121
 
2558
3122
  const WIDE_QUERY = '(min-width: 64rem)';
2559
3123
  const wideViewport = () => {
@@ -2608,16 +3172,30 @@ class UiLineChart {
2608
3172
  ...(ngDevMode ? [{ debugName: "axisGap" }] : /* istanbul ignore next */ []));
2609
3173
  axisTicks = input('auto', { ...(ngDevMode ? { debugName: "axisTicks" } : /* istanbul ignore next */ {}), transform: parseAxisTicks });
2610
3174
  #destroyRef = inject(DestroyRef);
3175
+ #host = inject(ElementRef);
2611
3176
  #wide = wideViewport();
2612
- #frame = null;
2613
- #hasRenderedOnce = false;
2614
- #previousGeometry = null;
3177
+ #reducedMotion = injectReducedMotion();
3178
+ #motion = new ShapeMotion({
3179
+ hold: (geometry) => {
3180
+ this.held.set(geometry);
3181
+ this.activeIndex.update((index) => (geometry ? null : index));
3182
+ },
3183
+ draw: (line, end) => {
3184
+ this.lineRef()?.nativeElement.setAttribute('d', line);
3185
+ this.endRef()?.nativeElement.setAttribute('cx', String(end.x));
3186
+ this.endRef()?.nativeElement.setAttribute('cy', String(end.y));
3187
+ },
3188
+ });
2615
3189
  svgRef = viewChild.required('svgRef', /* @ts-ignore */
2616
3190
  ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
3191
+ lineRef = viewChild('lineRef', /* @ts-ignore */
3192
+ ...(ngDevMode ? [{ debugName: "lineRef" }] : /* istanbul ignore next */ []));
3193
+ endRef = viewChild('endRef', /* @ts-ignore */
3194
+ ...(ngDevMode ? [{ debugName: "endRef" }] : /* istanbul ignore next */ []));
2617
3195
  activeIndex = signal(null, /* @ts-ignore */
2618
3196
  ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
2619
- animatedLine = signal(null, /* @ts-ignore */
2620
- ...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
3197
+ held = signal(null, /* @ts-ignore */
3198
+ ...(ngDevMode ? [{ debugName: "held" }] : /* istanbul ignore next */ []));
2621
3199
  measuredSize = signal(null, /* @ts-ignore */
2622
3200
  ...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
2623
3201
  effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
@@ -2635,8 +3213,10 @@ class UiLineChart {
2635
3213
  return buildGeometry(this.points(), this.effectiveWidth(), plotHeight, { x: 0, top: padding, bottom: padding });
2636
3214
  }, /* @ts-ignore */
2637
3215
  ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
2638
- displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
2639
- ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
3216
+ shown = computed(() => this.held() ?? this.geometry(), /* @ts-ignore */
3217
+ ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
3218
+ startFade = computed(() => this.held() ? 'opacity-0 duration-(--duration-exit)' : 'duration-(--duration-fast)', /* @ts-ignore */
3219
+ ...(ngDevMode ? [{ debugName: "startFade" }] : /* istanbul ignore next */ []));
2640
3220
  ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
2641
3221
  ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
2642
3222
  tickTexts = computed(() => tickLabels(this.ticks(), this.axisFormat(), this.axisTicks()), /* @ts-ignore */
@@ -2644,12 +3224,12 @@ class UiLineChart {
2644
3224
  active = computed(() => {
2645
3225
  const geometry = this.geometry();
2646
3226
  const index = this.activeIndex();
2647
- return geometry && index !== null ? geometry.points[index] : null;
3227
+ return geometry && index !== null && !this.held() ? geometry.points[index] : null;
2648
3228
  }, /* @ts-ignore */
2649
3229
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
2650
3230
  startLabelInfo = computed(() => {
2651
3231
  const text = this.startLabel();
2652
- const geometry = this.geometry();
3232
+ const geometry = this.shown();
2653
3233
  if (!text || !geometry) {
2654
3234
  return null;
2655
3235
  }
@@ -2659,63 +3239,21 @@ class UiLineChart {
2659
3239
  return { label, placement: { ...placement, y: startLabelBaseline(placement.y) } };
2660
3240
  }, /* @ts-ignore */
2661
3241
  ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
2662
- #renderRef = afterNextRender(() => this.#observeSize());
3242
+ #renderRef = afterNextRender(() => {
3243
+ this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
3244
+ observeSize(this.svgRef().nativeElement, this.#destroyRef, (size) => this.measuredSize.set(size));
3245
+ });
2663
3246
  constructor() {
2664
3247
  effect(() => {
3248
+ const points = this.points();
2665
3249
  const geometry = this.geometry();
2666
- const previous = this.#previousGeometry;
2667
- if (previous === geometry) {
2668
- return;
2669
- }
2670
- this.#previousGeometry = geometry;
2671
- this.#cancelAnimation();
2672
- if (!this.#hasRenderedOnce) {
2673
- this.#hasRenderedOnce = true;
2674
- return;
2675
- }
2676
- if (!previous || !geometry) {
2677
- this.animatedLine.set(null);
2678
- return;
2679
- }
2680
- if (prefersReducedMotion()) {
2681
- this.animatedLine.set(null);
2682
- return;
2683
- }
2684
- this.#animate(previous, geometry);
2685
- });
2686
- this.#destroyRef.onDestroy(() => this.#cancelAnimation());
2687
- }
2688
- #observeSize() {
2689
- this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
2690
- if (typeof ResizeObserver === 'undefined') {
2691
- return;
2692
- }
2693
- const element = this.svgRef().nativeElement;
2694
- const observer = new ResizeObserver(([entry]) => {
2695
- if (entry) {
2696
- const { width, height } = entry.contentRect;
2697
- this.measuredSize.set({ width, height });
2698
- }
3250
+ const reduced = this.#reducedMotion();
3251
+ untracked(() => this.#motion.follow(points, geometry, {
3252
+ reduced,
3253
+ duration: () => durationFrom(getComputedStyle(this.#host.nativeElement).getPropertyValue('--duration-base')),
3254
+ }));
2699
3255
  });
2700
- observer.observe(element);
2701
- this.#destroyRef.onDestroy(() => observer.disconnect());
2702
- }
2703
- #cancelAnimation() {
2704
- cancelAnimationFrame(this.#frame ?? 0);
2705
- this.#frame = null;
2706
- }
2707
- #animate(from, to) {
2708
- if (from.points.length < 2 || to.points.length < 2) {
2709
- this.animatedLine.set(null);
2710
- return;
2711
- }
2712
- const startTime = performance.now();
2713
- const step = (now) => {
2714
- const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
2715
- this.animatedLine.set(t < 1 ? interpolateLine(from, to, easeOutQuint(t)) : null);
2716
- this.#frame = t < 1 ? requestAnimationFrame(step) : null;
2717
- };
2718
- this.#frame = requestAnimationFrame(step);
3256
+ this.#destroyRef.onDestroy(() => this.#motion.stop());
2719
3257
  }
2720
3258
  deltaFor(point) {
2721
3259
  return point.v - this.points()[0].v;
@@ -2751,7 +3289,7 @@ class UiLineChart {
2751
3289
  this.activeIndex.set(navigation.index);
2752
3290
  }
2753
3291
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
2754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, tooltipFormat: { classPropertyName: "tooltipFormat", publicName: "tooltipFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
3292
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, tooltipFormat: { classPropertyName: "tooltipFormat", publicName: "tooltipFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipDelta: { classPropertyName: "tooltipDelta", publicName: "tooltipDelta", isSignal: true, isRequired: false, transformFunction: null }, axisGap: { classPropertyName: "axisGap", publicName: "axisGap", isSignal: true, isRequired: false, transformFunction: null }, axisTicks: { classPropertyName: "axisTicks", publicName: "axisTicks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }, { propertyName: "lineRef", first: true, predicate: ["lineRef"], descendants: true, isSignal: true }, { propertyName: "endRef", first: true, predicate: ["endRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
2755
3293
  <div class="relative h-full">
2756
3294
  <svg
2757
3295
  #svgRef
@@ -2766,48 +3304,52 @@ class UiLineChart {
2766
3304
  (pointerleave)="onPointerLeave()"
2767
3305
  (pointermove)="onPointerMove($event)"
2768
3306
  >
2769
- @if (geometry(); as geometry) {
3307
+ @if (shown(); as shape) {
2770
3308
  <path
3309
+ #lineRef
2771
3310
  class="stroke-(--foreground)"
2772
3311
  data-chart-line
2773
3312
  fill="none"
2774
3313
  stroke-linecap="round"
2775
3314
  stroke-linejoin="round"
2776
- [attr.d]="displayLine()"
3315
+ [attr.d]="shape.line"
2777
3316
  [attr.stroke-width]="sparkline() ? 1.5 : 2"
2778
3317
  />
2779
3318
 
2780
3319
  @if (!sparkline()) {
2781
3320
  <line
2782
- class="stroke-(--border)"
3321
+ class="stroke-(--border) transition-opacity ease-(--ease-out)"
2783
3322
  data-chart-start-line
2784
3323
  stroke-dasharray="3 4"
2785
3324
  stroke-width="1"
2786
3325
  x1="0"
2787
3326
  [attr.x2]="effectiveWidth()"
2788
- [attr.y1]="geometry.start.y"
2789
- [attr.y2]="geometry.start.y"
3327
+ [attr.y1]="shape.start.y"
3328
+ [attr.y2]="shape.start.y"
3329
+ [class]="startFade()"
2790
3330
  />
2791
3331
 
2792
3332
  @if (startLabelInfo(); as info) {
2793
3333
  <text
2794
- class="text-caption fill-(--subtle-foreground) tabular-nums"
3334
+ class="text-caption fill-(--subtle-foreground) tabular-nums transition-opacity ease-(--ease-out)"
2795
3335
  data-chart-start-label
2796
3336
  [attr.text-anchor]="info.placement.anchor"
2797
3337
  [attr.x]="info.placement.x"
2798
3338
  [attr.y]="info.placement.y"
3339
+ [class]="startFade()"
2799
3340
  >
2800
3341
  {{ info.label }}
2801
3342
  </text>
2802
3343
  }
2803
3344
 
2804
3345
  <circle
3346
+ #endRef
2805
3347
  class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2806
3348
  data-chart-end
2807
3349
  r="4"
2808
3350
  stroke-width="6"
2809
- [attr.cx]="geometry.end.x"
2810
- [attr.cy]="geometry.end.y"
3351
+ [attr.cx]="shape.end.x"
3352
+ [attr.cy]="shape.end.y"
2811
3353
  />
2812
3354
 
2813
3355
  @for (tick of ticks(); track tick.t; let index = $index; let first = $first; let last = $last) {
@@ -2908,48 +3450,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2908
3450
  (pointerleave)="onPointerLeave()"
2909
3451
  (pointermove)="onPointerMove($event)"
2910
3452
  >
2911
- @if (geometry(); as geometry) {
3453
+ @if (shown(); as shape) {
2912
3454
  <path
3455
+ #lineRef
2913
3456
  class="stroke-(--foreground)"
2914
3457
  data-chart-line
2915
3458
  fill="none"
2916
3459
  stroke-linecap="round"
2917
3460
  stroke-linejoin="round"
2918
- [attr.d]="displayLine()"
3461
+ [attr.d]="shape.line"
2919
3462
  [attr.stroke-width]="sparkline() ? 1.5 : 2"
2920
3463
  />
2921
3464
 
2922
3465
  @if (!sparkline()) {
2923
3466
  <line
2924
- class="stroke-(--border)"
3467
+ class="stroke-(--border) transition-opacity ease-(--ease-out)"
2925
3468
  data-chart-start-line
2926
3469
  stroke-dasharray="3 4"
2927
3470
  stroke-width="1"
2928
3471
  x1="0"
2929
3472
  [attr.x2]="effectiveWidth()"
2930
- [attr.y1]="geometry.start.y"
2931
- [attr.y2]="geometry.start.y"
3473
+ [attr.y1]="shape.start.y"
3474
+ [attr.y2]="shape.start.y"
3475
+ [class]="startFade()"
2932
3476
  />
2933
3477
 
2934
3478
  @if (startLabelInfo(); as info) {
2935
3479
  <text
2936
- class="text-caption fill-(--subtle-foreground) tabular-nums"
3480
+ class="text-caption fill-(--subtle-foreground) tabular-nums transition-opacity ease-(--ease-out)"
2937
3481
  data-chart-start-label
2938
3482
  [attr.text-anchor]="info.placement.anchor"
2939
3483
  [attr.x]="info.placement.x"
2940
3484
  [attr.y]="info.placement.y"
3485
+ [class]="startFade()"
2941
3486
  >
2942
3487
  {{ info.label }}
2943
3488
  </text>
2944
3489
  }
2945
3490
 
2946
3491
  <circle
3492
+ #endRef
2947
3493
  class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2948
3494
  data-chart-end
2949
3495
  r="4"
2950
3496
  stroke-width="6"
2951
- [attr.cx]="geometry.end.x"
2952
- [attr.cy]="geometry.end.y"
3497
+ [attr.cx]="shape.end.x"
3498
+ [attr.cy]="shape.end.y"
2953
3499
  />
2954
3500
 
2955
3501
  @for (tick of ticks(); track tick.t; let index = $index; let first = $first; let last = $last) {
@@ -3034,12 +3580,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3034
3580
  class: 'block h-full',
3035
3581
  },
3036
3582
  }]
3037
- }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], tooltipFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
3583
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], tooltipFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipDelta: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelta", required: false }] }], axisGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisGap", required: false }] }], axisTicks: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisTicks", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }], lineRef: [{ type: i0.ViewChild, args: ['lineRef', { isSignal: true }] }], endRef: [{ type: i0.ViewChild, args: ['endRef', { isSignal: true }] }] } });
3038
3584
 
3039
- 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';
3040
- const SHEET_CLASSES = 'max-lg:fixed max-lg:inset-x-4 max-lg:top-auto max-lg:w-auto max-lg:bottom-[calc(76px+env(safe-area-inset-bottom))] max-lg:min-w-0 max-lg:p-1.5 max-lg:bg-(--card) max-lg:shadow-[0_12px_32px_rgb(0_0_0/0.2),inset_0_0_0_1px_var(--border)] max-lg:backdrop:bg-[rgb(0_0_0/0.36)] max-lg:transition-[opacity,translate,overlay,display] max-lg:scale-100 max-lg:translate-y-4 max-lg:open:translate-y-0 max-lg:starting:open:scale-100 max-lg:starting:open:translate-y-4';
3041
- const SHEET_ITEM_CLASSES = 'flex w-full items-center gap-3 lg:pointer-fine:gap-2.5 min-h-12 lg:min-h-11 lg:pointer-fine:min-h-9 px-3 lg:pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-6px)] lg:rounded-[calc(var(--radius-container)-4px)] text-body lg:pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-5 [:where(&>svg)]:flex-none lg:[:where(&>svg)]:size-[18px] lg:pointer-fine:[:where(&>svg)]:size-4';
3042
- const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-[18px] [:where(&>svg)]:flex-none pointer-fine:[:where(&>svg)]:size-4';
3585
+ 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,scale,overlay,display] transition-discrete duration-(--duration-fast) not-open:duration-[120ms] ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
3586
+ const SHEET_CLASSES = 'max-lg:fixed max-lg:inset-x-4 max-lg:top-auto max-lg:w-auto max-lg:bottom-[calc(76px+env(safe-area-inset-bottom))] max-lg:min-w-0 max-lg:p-1.5 max-lg:bg-(--card) max-lg:shadow-[0_12px_32px_rgb(0_0_0/0.2),inset_0_0_0_1px_var(--border)] max-lg:backdrop:bg-[rgb(0_0_0/0.36)] max-lg:transition-[opacity,translate,overlay,display] max-lg:scale-100 max-lg:translate-y-4 max-lg:open:translate-y-0 max-lg:starting:open:scale-100 max-lg:starting:open:translate-y-4 max-lg:motion-reduce:translate-y-0 max-lg:motion-reduce:starting:open:translate-y-0';
3587
+ const SHEET_ITEM_CLASSES = 'flex w-full items-center gap-3 lg:pointer-fine:gap-2.5 min-h-12 lg:min-h-11 lg:pointer-fine:min-h-9 px-3 lg:pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-6px)] lg:rounded-[calc(var(--radius-container)-4px)] text-body lg:pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) transition-[background-color] duration-(--duration-fast) ease-out outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-5 [:where(&>svg)]:flex-none lg:[:where(&>svg)]:size-[18px] lg:pointer-fine:[:where(&>svg)]:size-4';
3588
+ const ITEM_CLASSES = 'flex w-full items-center gap-3 pointer-fine:gap-2.5 min-h-11 pointer-fine:min-h-9 px-3 pointer-fine:px-2.5 rounded-[calc(var(--radius-container)-4px)] text-body pointer-fine:text-label hover:bg-(--glow) focus-visible:bg-(--glow) active:bg-(--soft) transition-[background-color] duration-(--duration-fast) ease-out outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) [:where(&>svg)]:size-[18px] [:where(&>svg)]:flex-none pointer-fine:[:where(&>svg)]:size-4';
3043
3589
  const nextId$1 = (() => {
3044
3590
  let count = 0;
3045
3591
  return () => `ui-menu-${++count}`;
@@ -3341,7 +3887,369 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3341
3887
  }]
3342
3888
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueText", required: true }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3343
3889
 
3344
- const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label leading-normal font-medium transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3890
+ class FlipItems {
3891
+ items = new Set();
3892
+ destroyed = new Set();
3893
+ used = false;
3894
+ get marked() {
3895
+ return this.used;
3896
+ }
3897
+ add(item) {
3898
+ this.used = true;
3899
+ this.items.add(item);
3900
+ }
3901
+ remove(item) {
3902
+ this.destroyed.add(item);
3903
+ }
3904
+ inside(host) {
3905
+ const inside = [];
3906
+ for (const item of this.items) {
3907
+ if (host.contains(item)) {
3908
+ inside.push(item);
3909
+ }
3910
+ else if (this.destroyed.has(item)) {
3911
+ this.items.delete(item);
3912
+ this.destroyed.delete(item);
3913
+ }
3914
+ }
3915
+ return inside;
3916
+ }
3917
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FlipItems, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3918
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FlipItems });
3919
+ }
3920
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FlipItems, decorators: [{
3921
+ type: Injectable
3922
+ }] });
3923
+
3924
+ function readDuration$1(element, name, fallback) {
3925
+ const raw = getComputedStyle(element).getPropertyValue(name).trim();
3926
+ const value = parseFloat(raw);
3927
+ if (Number.isNaN(value)) {
3928
+ return fallback;
3929
+ }
3930
+ return raw.endsWith('ms') ? value : value * 1000;
3931
+ }
3932
+ function readEasing(element, name, fallback) {
3933
+ return getComputedStyle(element).getPropertyValue(name).trim() || fallback;
3934
+ }
3935
+
3936
+ function translateY(element) {
3937
+ const matrix = /^matrix\((.+)\)$/.exec(getComputedStyle(element).transform);
3938
+ return matrix ? parseFloat(matrix[1].split(',')[5]) : 0;
3939
+ }
3940
+ /**
3941
+ * Slides the items of a list into place when one is removed or inserted (FLIP): it measures every item
3942
+ * before and after the change, from the top of the list itself, then animates the shift in `translateY`
3943
+ * over `--duration-base` `--ease-out`. Pair it with `animate.leave="ui-leave-fade"` on the item so it fades
3944
+ * out first. Nothing moves under `prefers-reduced-motion`: the list closes up at once.
3945
+ *
3946
+ * By default the items are the direct children of the list. Mark them with `uiFlipItem` instead when they
3947
+ * sit deeper, for instance a page of several groups: put `uiFlipList` on the element around every group and
3948
+ * `uiFlipItem` on each group heading and row, so a removal also slides the groups below. An item may sit
3949
+ * inside another item (a row inside a card): it then moves by its own share and its card carries the rest.
3950
+ * Only an item added or removed starts a slide; a change inside an item does not. Positions leave out the
3951
+ * translate of any animation running inside the list (a slide in progress, an enter fade-up).
3952
+ *
3953
+ * @example
3954
+ * <ul uiFlipList>
3955
+ * @for (key of keys(); track key.id) {
3956
+ * <li uiListRow animate.leave="ui-leave-fade">...</li>
3957
+ * }
3958
+ * </ul>
3959
+ *
3960
+ * <table uiTable uiFlipList>
3961
+ * @for (group of groups(); track group.id) {
3962
+ * <tbody>
3963
+ * <tr uiTr group uiFlipItem>...</tr>
3964
+ * @for (row of group.rows; track row.id) {
3965
+ * <tr uiTr uiFlipItem animate.leave="ui-leave-fade">...</tr>
3966
+ * }
3967
+ * </tbody>
3968
+ * }
3969
+ * </table>
3970
+ */
3971
+ class UiFlipList {
3972
+ host = inject(ElementRef).nativeElement;
3973
+ reduced = injectReducedMotion();
3974
+ mutations = new MutationObserver(() => this.onChange());
3975
+ resizes = new ResizeObserver(() => {
3976
+ this.tops = this.measure();
3977
+ });
3978
+ running = new Map();
3979
+ watched = new Set();
3980
+ items = inject(FlipItems, { self: true });
3981
+ tops = new Map();
3982
+ shifts = new Map();
3983
+ constructor() {
3984
+ afterNextRender(() => {
3985
+ this.tops = this.measure();
3986
+ this.mutations.observe(this.host, { childList: true, subtree: true });
3987
+ this.resizes.observe(this.host);
3988
+ });
3989
+ inject(DestroyRef).onDestroy(() => {
3990
+ this.mutations.disconnect();
3991
+ this.resizes.disconnect();
3992
+ });
3993
+ }
3994
+ tracked() {
3995
+ if (!this.items.marked) {
3996
+ return Array.from(this.host.children).filter((child) => child instanceof HTMLElement);
3997
+ }
3998
+ return this.items.inside(this.host);
3999
+ }
4000
+ measure() {
4001
+ const elements = this.tracked();
4002
+ const origin = this.host.getBoundingClientRect().top - this.host.scrollTop;
4003
+ const tops = new Map(elements.map((element) => [element, element.getBoundingClientRect().top - origin]));
4004
+ this.shifts = new Map();
4005
+ for (const animation of this.host.getAnimations({ subtree: true })) {
4006
+ const target = animation.effect.target;
4007
+ if (target !== this.host) {
4008
+ this.shifts.set(target, translateY(target));
4009
+ }
4010
+ }
4011
+ for (const [moving, shift] of this.shifts) {
4012
+ for (const [element, top] of tops) {
4013
+ if (moving.contains(element)) {
4014
+ tops.set(element, top - shift);
4015
+ }
4016
+ }
4017
+ }
4018
+ for (const element of elements) {
4019
+ if (!this.watched.has(element)) {
4020
+ this.watched.add(element);
4021
+ this.resizes.observe(element);
4022
+ }
4023
+ }
4024
+ for (const element of this.watched) {
4025
+ if (!tops.has(element)) {
4026
+ this.watched.delete(element);
4027
+ this.running.delete(element);
4028
+ this.resizes.unobserve(element);
4029
+ }
4030
+ }
4031
+ return tops;
4032
+ }
4033
+ onChange() {
4034
+ const before = this.tops;
4035
+ const after = this.measure();
4036
+ this.tops = after;
4037
+ const changed = after.size !== before.size || Array.from(after.keys()).some((element) => !before.has(element));
4038
+ if (this.reduced() || !changed) {
4039
+ return;
4040
+ }
4041
+ const duration = readDuration$1(this.host, '--duration-base', 260);
4042
+ const easing = readEasing(this.host, '--ease-out', 'ease-out');
4043
+ const moved = (element) => (before.get(element) ?? 0) - (after.get(element) ?? 0);
4044
+ for (const [element] of after) {
4045
+ if (!before.has(element)) {
4046
+ continue;
4047
+ }
4048
+ const own = moved(element) - moved(this.parentItem(element, before, after));
4049
+ if (Math.abs(own) < 0.5) {
4050
+ continue;
4051
+ }
4052
+ const from = own + (this.shifts.get(element) ?? 0);
4053
+ this.running.get(element)?.cancel();
4054
+ const animation = element.animate([{ transform: `translateY(${from}px)` }, { transform: 'translateY(0)' }], {
4055
+ duration,
4056
+ easing,
4057
+ });
4058
+ animation.onfinish = () => {
4059
+ if (this.running.get(element) === animation) {
4060
+ this.running.delete(element);
4061
+ }
4062
+ };
4063
+ this.running.set(element, animation);
4064
+ }
4065
+ }
4066
+ parentItem(element, before, after) {
4067
+ let node = element.parentElement;
4068
+ while (node !== this.host && !(before.has(node) && after.has(node))) {
4069
+ node = node.parentElement;
4070
+ }
4071
+ return node;
4072
+ }
4073
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipList, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4074
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFlipList, isStandalone: true, selector: "[uiFlipList]", providers: [FlipItems], ngImport: i0 });
4075
+ }
4076
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipList, decorators: [{
4077
+ type: Directive,
4078
+ args: [{
4079
+ selector: '[uiFlipList]',
4080
+ providers: [FlipItems],
4081
+ }]
4082
+ }], ctorParameters: () => [] });
4083
+ /**
4084
+ * Marks an element as an item of the nearest `uiFlipList`, at any depth: a group heading, a row inside a
4085
+ * group, a card. Once one item is marked, the list follows its marked items only.
4086
+ *
4087
+ * @example
4088
+ * <tr uiTr uiFlipItem animate.leave="ui-leave-fade">...</tr>
4089
+ */
4090
+ class UiFlipItem {
4091
+ constructor() {
4092
+ const items = inject(FlipItems, { optional: true });
4093
+ if (items) {
4094
+ const element = inject(ElementRef).nativeElement;
4095
+ items.add(element);
4096
+ inject(DestroyRef).onDestroy(() => items.remove(element));
4097
+ }
4098
+ }
4099
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4100
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFlipItem, isStandalone: true, selector: "[uiFlipItem]", ngImport: i0 });
4101
+ }
4102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipItem, decorators: [{
4103
+ type: Directive,
4104
+ args: [{
4105
+ selector: '[uiFlipItem]',
4106
+ }]
4107
+ }], ctorParameters: () => [] });
4108
+
4109
+ const HOLD_MS = 200;
4110
+ const SCROLL_SETTLE_MS = 200;
4111
+ const SCROLL_START_MS = 300;
4112
+ function paintedElements(host) {
4113
+ if (host.tagName === 'TR') {
4114
+ return Array.from(host.children).filter((cell) => cell instanceof HTMLElement);
4115
+ }
4116
+ if (host.hasAttribute('ui-group-cell') && host.firstElementChild instanceof HTMLElement) {
4117
+ return [host.firstElementChild];
4118
+ }
4119
+ return [host];
4120
+ }
4121
+ /**
4122
+ * Draws the eye to an element that just changed: scrolls it into view when it is off screen, holds the
4123
+ * `--soft` fill for 200 ms, then fades back to the element's own background over `--duration-highlight`.
4124
+ * Each new non-null token plays once, so pass a fresh value per event (an object or a counter), not a
4125
+ * boolean. A non-null token already present at first render plays on arrival. On a `tr` it paints the
4126
+ * cells, on a `td[ui-group-cell]` the band, anywhere else the host. Under `prefers-reduced-motion` the
4127
+ * scroll is instant and the fade lasts 600 ms.
4128
+ *
4129
+ * It starts on the next frame and, when the element is still playing a finite animation of its own (an
4130
+ * `animate.enter` fade on a new row), once that animation has finished, waiting no longer than
4131
+ * `--duration-base`: the element fades in, then lights up. Both can sit on the same element.
4132
+ *
4133
+ * Give the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and
4134
+ * below it (header, tab bar, action bar): a row under them counts as off screen and is scrolled out.
4135
+ * For an arrival on a page opened at the element, scroll to it at once first (`scrollIntoView` in
4136
+ * `afterNextRender`) and set the token afterwards: a token present at first render scrolls smoothly
4137
+ * from where the page is.
4138
+ *
4139
+ * @example
4140
+ * <a ui-row [uiHighlight]="changed() === holding.id ? token() : null" routerLink="/holdings/1">...</a>
4141
+ */
4142
+ class UiHighlight {
4143
+ uiHighlight = input(null, /* @ts-ignore */
4144
+ ...(ngDevMode ? [{ debugName: "uiHighlight" }] : /* istanbul ignore next */ []));
4145
+ host = inject(ElementRef).nativeElement;
4146
+ reduced = injectReducedMotion();
4147
+ cancel = null;
4148
+ constructor() {
4149
+ effect(() => {
4150
+ const token = this.uiHighlight();
4151
+ if (token === null || token === undefined) {
4152
+ return;
4153
+ }
4154
+ untracked(() => this.play());
4155
+ });
4156
+ inject(DestroyRef).onDestroy(() => this.cancel?.());
4157
+ }
4158
+ play() {
4159
+ this.cancel?.();
4160
+ if (this.isVisible()) {
4161
+ this.cancel = this.flash();
4162
+ return;
4163
+ }
4164
+ const reduced = this.reduced();
4165
+ this.host.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' });
4166
+ if (reduced) {
4167
+ this.cancel = this.flash();
4168
+ return;
4169
+ }
4170
+ let stopFlash = null;
4171
+ let timer;
4172
+ const moves = (event) => event.target === document || (event.target instanceof Node && event.target.contains(this.host));
4173
+ const start = () => {
4174
+ stopListening();
4175
+ stopFlash = this.flash();
4176
+ };
4177
+ const onScrollEnd = (event) => {
4178
+ if (moves(event)) {
4179
+ start();
4180
+ }
4181
+ };
4182
+ const onScroll = (event) => {
4183
+ if (moves(event)) {
4184
+ clearTimeout(timer);
4185
+ timer = setTimeout(start, SCROLL_SETTLE_MS);
4186
+ }
4187
+ };
4188
+ const stopListening = () => {
4189
+ clearTimeout(timer);
4190
+ window.removeEventListener('scrollend', onScrollEnd, true);
4191
+ window.removeEventListener('scroll', onScroll, true);
4192
+ };
4193
+ timer = setTimeout(start, SCROLL_START_MS);
4194
+ window.addEventListener('scrollend', onScrollEnd, true);
4195
+ window.addEventListener('scroll', onScroll, true);
4196
+ this.cancel = () => {
4197
+ stopListening();
4198
+ stopFlash?.();
4199
+ };
4200
+ }
4201
+ isVisible() {
4202
+ const rect = this.host.getBoundingClientRect();
4203
+ const style = getComputedStyle(this.host);
4204
+ const top = parseFloat(style.scrollMarginTop) || 0;
4205
+ const bottom = parseFloat(style.scrollMarginBottom) || 0;
4206
+ return rect.top >= top && rect.bottom <= window.innerHeight - bottom;
4207
+ }
4208
+ flash() {
4209
+ const painted = paintedElements(this.host);
4210
+ let animations = [];
4211
+ let timer;
4212
+ const paint = () => {
4213
+ const fade = readDuration$1(this.host, '--duration-highlight', 1200);
4214
+ const easing = readEasing(this.host, '--ease-out', 'ease-out');
4215
+ const total = HOLD_MS + fade;
4216
+ animations = painted.map((element) => element.animate([
4217
+ { backgroundColor: 'var(--soft)', offset: 0 },
4218
+ { backgroundColor: 'var(--soft)', offset: HOLD_MS / total, easing },
4219
+ { offset: 1 },
4220
+ ], { duration: total }));
4221
+ };
4222
+ const frame = requestAnimationFrame(() => {
4223
+ const remaining = Array.from(new Set([this.host, ...painted]))
4224
+ .flatMap((element) => element.getAnimations())
4225
+ .map((animation) => {
4226
+ const timing = animation.effect.getComputedTiming();
4227
+ return Number(timing.endTime) - Number(timing.localTime ?? 0);
4228
+ })
4229
+ .filter((left) => Number.isFinite(left));
4230
+ if (remaining.length === 0) {
4231
+ paint();
4232
+ return;
4233
+ }
4234
+ timer = setTimeout(paint, Math.min(Math.max(...remaining), readDuration$1(this.host, '--duration-base', 260)));
4235
+ });
4236
+ return () => {
4237
+ cancelAnimationFrame(frame);
4238
+ clearTimeout(timer);
4239
+ animations.forEach((animation) => animation.cancel());
4240
+ };
4241
+ }
4242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiHighlight, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4243
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiHighlight, isStandalone: true, selector: "[uiHighlight]", inputs: { uiHighlight: { classPropertyName: "uiHighlight", publicName: "uiHighlight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
4244
+ }
4245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiHighlight, decorators: [{
4246
+ type: Directive,
4247
+ args: [{
4248
+ selector: '[uiHighlight]',
4249
+ }]
4250
+ }], ctorParameters: () => [], propDecorators: { uiHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiHighlight", required: false }] }] } });
4251
+
4252
+ const BASE_CLASSES$2 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label leading-normal font-medium transition-[scale,background-color,color] [transition-duration:var(--duration-press),var(--duration-fast),var(--duration-fast)] ease-out active:scale-(--press-scale) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3345
4253
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
3346
4254
  const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
3347
4255
  /**
@@ -3355,7 +4263,7 @@ const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
3355
4263
  */
3356
4264
  class UiNavItem {
3357
4265
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3358
- classes = computed(() => `${BASE_CLASSES$1} ${this.active() ? ACTIVE_CLASSES : IDLE_CLASSES}`, /* @ts-ignore */
4266
+ classes = computed(() => `${BASE_CLASSES$2} ${this.active() ? ACTIVE_CLASSES : IDLE_CLASSES}`, /* @ts-ignore */
3359
4267
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3360
4268
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
3361
4269
  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 });
@@ -3384,10 +4292,12 @@ const SIZE_CLASSES$1 = {
3384
4292
  };
3385
4293
  const SIZE_GAPS = { md: 'gap-2.5', lg: 'gap-2.5', xl: 'gap-3', card: 'gap-2', dense: 'gap-3' };
3386
4294
  const PADDING_CLASSES = { md: 'px-2.5', sm: 'px-2', none: 'px-0' };
3387
- 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)';
4295
+ const BASE_CLASSES$1 = 'flex w-full items-center py-1.5 rounded-control text-left select-none touch-manipulation focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
4296
+ const INTERACTIVE_CLASSES = 'transition-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out hover:bg-(--glow) active:bg-(--soft) active:scale-(--press-scale)';
4297
+ const UNAVAILABLE_CLASSES = 'opacity-50 cursor-not-allowed';
3388
4298
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
3389
4299
  /**
3390
- * Clickable row of a list: a holding, an envelope, an account.
4300
+ * Clickable row of a list: a holding, an account, a search result.
3391
4301
  *
3392
4302
  * @example
3393
4303
  * <a ui-row [selected]="holding.id === openId()" [routerLink]="['/holdings', holding.id]">
@@ -3396,28 +4306,59 @@ const SELECTED_CLASSES$1 = 'bg-(--soft)';
3396
4306
  */
3397
4307
  class UiRow {
3398
4308
  selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4309
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4310
+ unavailable = input(false, { ...(ngDevMode ? { debugName: "unavailable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3399
4311
  size = input('md', /* @ts-ignore */
3400
4312
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3401
4313
  padding = input('md', /* @ts-ignore */
3402
4314
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
3403
4315
  gap = input('default', /* @ts-ignore */
3404
4316
  ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
3405
- classes = computed(() => `${BASE_CLASSES} ${SIZE_CLASSES$1[this.size()]} ${this.gap() === 'sm' ? 'gap-2' : SIZE_GAPS[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
4317
+ classes = computed(() => `${BASE_CLASSES$1} ${SIZE_CLASSES$1[this.size()]} ${this.gap() === 'sm' ? 'gap-2' : SIZE_GAPS[this.size()]} ${PADDING_CLASSES[this.padding()]}${this.unavailable() ? ` ${UNAVAILABLE_CLASSES}` : ` ${INTERACTIVE_CLASSES}`}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}${this.busy() ? ' pointer-events-none' : ''}`, /* @ts-ignore */
3406
4318
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4319
+ constructor() {
4320
+ const host = inject(ElementRef).nativeElement;
4321
+ const guard = (event) => {
4322
+ if (this.busy() || this.unavailable()) {
4323
+ event.preventDefault();
4324
+ event.stopImmediatePropagation();
4325
+ }
4326
+ };
4327
+ host.addEventListener('click', guard, { capture: true });
4328
+ inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
4329
+ }
3407
4330
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
3408
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
4331
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.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 }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, unavailable: { classPropertyName: "unavailable", publicName: "unavailable", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-busy": "busy() || null", "attr.aria-disabled": "busy() || unavailable() ? 'true' : null", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: `
4332
+ <ng-content />
4333
+ @if (busy()) {
4334
+ <span
4335
+ aria-hidden="true"
4336
+ class="rounded-pill animate-cairn-spin size-4 flex-none border-2 border-current border-r-transparent"
4337
+ ></span>
4338
+ }
4339
+ `, isInline: true });
3409
4340
  }
3410
4341
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
3411
4342
  type: Component,
3412
4343
  args: [{
3413
4344
  selector: 'a[ui-row], button[ui-row]',
3414
- template: '<ng-content />',
4345
+ template: `
4346
+ <ng-content />
4347
+ @if (busy()) {
4348
+ <span
4349
+ aria-hidden="true"
4350
+ class="rounded-pill animate-cairn-spin size-4 flex-none border-2 border-current border-r-transparent"
4351
+ ></span>
4352
+ }
4353
+ `,
3415
4354
  host: {
3416
4355
  '[class]': 'classes()',
4356
+ '[attr.aria-busy]': 'busy() || null',
4357
+ '[attr.aria-disabled]': "busy() || unavailable() ? 'true' : null",
3417
4358
  '[attr.aria-current]': "selected() ? 'true' : null",
3418
4359
  },
3419
4360
  }]
3420
- }], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }] } });
4361
+ }], ctorParameters: () => [], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], unavailable: [{ type: i0.Input, args: [{ isSignal: true, alias: "unavailable", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }] } });
3421
4362
  /**
3422
4363
  * Lays out a `ui-row` with a trailing action beside it, such as a "more" icon button: the row takes
3423
4364
  * the room the action leaves and the action keeps its own size.
@@ -3496,7 +4437,7 @@ const nextId = (() => {
3496
4437
  let count = 0;
3497
4438
  return () => `ui-segmented-${++count}`;
3498
4439
  })();
3499
- const OPTION_CLASSES = "relative z-10 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) active:scale-(--press-scale)";
4440
+ const OPTION_CLASSES = "relative z-10 flex-1 cursor-pointer px-3 text-label font-medium transition-[scale,color] duration-(--duration-press) ease-out 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)";
3500
4441
  const SEGMENTED_SIZES = ['md', 'sm'];
3501
4442
  const SIZE_CLASSES = {
3502
4443
  md: 'h-10 pointer-fine:h-8',
@@ -3731,11 +4672,13 @@ class UiSkeleton {
3731
4672
  }, /* @ts-ignore */
3732
4673
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
3733
4674
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
3734
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('dot') {\n <span class=\"rounded-pill size-3 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
4675
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-10 w-65\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 items-center justify-between gap-4\">\n <span class=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-20\" [style.animation]=\"animation\"></span>\n </span>\n <span class=\"block rounded-control bg-(--muted) h-4 w-[90px] flex-none\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-control min-h-50 w-full flex-1 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('dot') {\n <span class=\"rounded-pill size-3 bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill shadow-[inset_0_0_0_30px_var(--muted)]\"\n [style.animation]=\"animation\"\n [style.height.px]=\"size()\"\n [style.width.px]=\"size()\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true });
3735
4676
  }
3736
4677
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
3737
4678
  type: Component,
3738
- args: [{ selector: 'ui-skeleton', template: `
4679
+ args: [{
4680
+ selector: 'ui-skeleton',
4681
+ template: `
3739
4682
  @switch (shape()) {
3740
4683
  @case ('figure') {
3741
4684
  <span class="${BAR_CLASSES} h-10 w-65" [style.animation]="animation"></span>
@@ -3774,12 +4717,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3774
4717
  }
3775
4718
  }
3776
4719
  }
3777
- `, host: {
4720
+ `,
4721
+ host: {
3778
4722
  'aria-hidden': 'true',
3779
4723
  class: 'flex flex-col gap-2',
3780
- }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
4724
+ },
4725
+ }]
3781
4726
  }], 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 }] }] } });
3782
4727
 
4728
+ const BASE_CLASSES = "relative inline-flex items-center gap-0.5 rounded-[4px] text-caption font-medium text-(--stale) hover:underline underline-offset-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) before:absolute before:inset-x-0 before:top-1/2 before:h-11 pointer-fine:before:h-10 before:-translate-y-1/2 before:content-['']";
4729
+ const CHEVRON_CLASSES = "after:size-4 after:flex-none after:bg-current after:content-[''] after:mask-center after:mask-no-repeat after:mask-[url(data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27black%27%20stroke-width%3D%271.75%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%27m9%2018%206-6-6-6%27%2F%3E%3C%2Fsvg%3E)]";
4730
+ /**
4731
+ * Link in the stale tone, for a caption about data that is late or left out of a total, such as
4732
+ * "1 ligne sans cours, non comptée". Caption size, weight 500, underlined on hover, with an optional
4733
+ * trailing chevron.
4734
+ *
4735
+ * @example
4736
+ * <a uiStaleLink chevron routerLink="/holdings">1 cours en retard</a>
4737
+ */
4738
+ class UiStaleLink {
4739
+ chevron = input(false, { ...(ngDevMode ? { debugName: "chevron" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4740
+ classes = computed(() => (this.chevron() ? `${BASE_CLASSES} ${CHEVRON_CLASSES}` : BASE_CLASSES), /* @ts-ignore */
4741
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4742
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStaleLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4743
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiStaleLink, isStandalone: true, selector: "a[uiStaleLink]", inputs: { chevron: { classPropertyName: "chevron", publicName: "chevron", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
4744
+ }
4745
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStaleLink, decorators: [{
4746
+ type: Directive,
4747
+ args: [{
4748
+ selector: 'a[uiStaleLink]',
4749
+ host: { '[class]': 'classes()' },
4750
+ }]
4751
+ }], propDecorators: { chevron: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevron", required: false }] }] } });
4752
+
3783
4753
  /** Available stat sizes. `StatSize` is derived from this tuple. */
3784
4754
  const STAT_SIZES = ['hero', 'tile'];
3785
4755
  const VALUE_CLASSES = {
@@ -3867,7 +4837,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3867
4837
  }]
3868
4838
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3869
4839
 
3870
- 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";
4840
+ 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-[background-color] 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";
3871
4841
  /**
3872
4842
  * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
3873
4843
  * 51 by 31 on touch, 40 by 24 with a mouse.
@@ -3891,11 +4861,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3891
4861
  }]
3892
4862
  }] });
3893
4863
 
3894
- 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)';
4864
+ const HEIGHT_PROPERTY = '--tab-bar-height';
4865
+ const HEIGHT = 'calc(52px + env(safe-area-inset-bottom))';
4866
+ const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-[3px] touch-manipulation select-none transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
3895
4867
  const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3896
4868
  const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3897
4869
  /**
3898
- * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
4870
+ * Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area. While it is on the
4871
+ * page it publishes that height as `--tab-bar-height` on the root element, which `ui-action-bar` and
4872
+ * `ui-toaster` sit above; use it rather than restating 52px for anything that must clear the bar.
3899
4873
  *
3900
4874
  * @example
3901
4875
  * <nav ui-tab-bar>
@@ -3903,6 +4877,11 @@ const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3903
4877
  * </nav>
3904
4878
  */
3905
4879
  class UiTabBar {
4880
+ constructor() {
4881
+ const root = inject(DOCUMENT).documentElement;
4882
+ root.style.setProperty(HEIGHT_PROPERTY, HEIGHT);
4883
+ inject(DestroyRef).onDestroy(() => root.style.removeProperty(HEIGHT_PROPERTY));
4884
+ }
3906
4885
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
3907
4886
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiTabBar, isStandalone: true, selector: "nav[ui-tab-bar]", host: { classAttribute: "grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]" }, ngImport: i0, template: '<ng-content />', isInline: true });
3908
4887
  }
@@ -3915,7 +4894,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3915
4894
  class: 'grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]',
3916
4895
  },
3917
4896
  }]
3918
- }] });
4897
+ }], ctorParameters: () => [] });
3919
4898
  /**
3920
4899
  * One destination of `ui-tab-bar`.
3921
4900
  *
@@ -3947,6 +4926,150 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3947
4926
  }]
3948
4927
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3949
4928
 
4929
+ /**
4930
+ * One-slot confirmation message queue read by `ui-toaster`.
4931
+ *
4932
+ * @example
4933
+ * inject(UiToasts).show('Achat enregistré');
4934
+ */
4935
+ class UiToasts {
4936
+ current = signal(null, /* @ts-ignore */
4937
+ ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
4938
+ sequence = 0;
4939
+ toast = this.current.asReadonly();
4940
+ show(message) {
4941
+ this.current.set({ id: ++this.sequence, text: message });
4942
+ }
4943
+ dismiss() {
4944
+ this.current.set(null);
4945
+ }
4946
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4947
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, providedIn: 'root' });
4948
+ }
4949
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, decorators: [{
4950
+ type: Injectable,
4951
+ args: [{ providedIn: 'root' }]
4952
+ }] });
4953
+
4954
+ const DEFAULT_DURATION = 5000;
4955
+ const TOAST_CLASSES = 'pointer-events-auto max-w-[min(28rem,calc(100vw-2rem))] rounded-container bg-(--elevated) px-4 py-3 text-label text-(--foreground) shadow-[0_8px_24px_rgb(0_0_0/0.16),inset_0_0_0_1px_var(--border)] transition-[opacity,translate] duration-(--duration-base) ease-out starting:opacity-0 starting:translate-y-2 motion-reduce:starting:translate-y-0';
4956
+ const readDuration = (host) => {
4957
+ const raw = getComputedStyle(host).getPropertyValue('--toast-duration').trim();
4958
+ const value = parseFloat(raw);
4959
+ if (Number.isNaN(value)) {
4960
+ return DEFAULT_DURATION;
4961
+ }
4962
+ return raw.endsWith('ms') ? value : value * 1000;
4963
+ };
4964
+ /**
4965
+ * Confirmation message shown by `UiToasts.show`: one at a time, no action, gone after `--toast-duration`.
4966
+ * Place it once in the app shell.
4967
+ *
4968
+ * @example
4969
+ * <ui-toaster />
4970
+ */
4971
+ class UiToaster {
4972
+ toasts = inject(UiToasts);
4973
+ toastClasses = TOAST_CLASSES;
4974
+ host = inject(ElementRef).nativeElement;
4975
+ hovered = signal(false, /* @ts-ignore */
4976
+ ...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
4977
+ focused = signal(false, /* @ts-ignore */
4978
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
4979
+ lastId = 0;
4980
+ remaining = DEFAULT_DURATION;
4981
+ startedAt = 0;
4982
+ timer;
4983
+ constructor() {
4984
+ effect(() => {
4985
+ const toast = this.toasts.toast();
4986
+ const paused = this.hovered() || this.focused();
4987
+ untracked(() => {
4988
+ if (!toast) {
4989
+ this.stop();
4990
+ this.hovered.set(false);
4991
+ this.focused.set(false);
4992
+ return;
4993
+ }
4994
+ this.host.querySelectorAll('.ui-leave-fade').forEach((leaving) => leaving.remove());
4995
+ if (paused) {
4996
+ this.pause();
4997
+ }
4998
+ else {
4999
+ this.resume(toast.id);
5000
+ }
5001
+ });
5002
+ });
5003
+ inject(DestroyRef).onDestroy(() => this.stop());
5004
+ }
5005
+ setHovered(value) {
5006
+ this.hovered.set(value);
5007
+ }
5008
+ setFocused(value) {
5009
+ this.focused.set(value);
5010
+ }
5011
+ resume(id) {
5012
+ if (id !== this.lastId) {
5013
+ this.lastId = id;
5014
+ this.remaining = readDuration(this.host);
5015
+ }
5016
+ clearTimeout(this.timer);
5017
+ this.startedAt = Date.now();
5018
+ this.timer = setTimeout(() => this.toasts.dismiss(), this.remaining);
5019
+ }
5020
+ pause() {
5021
+ if (this.timer !== undefined) {
5022
+ this.remaining -= Date.now() - this.startedAt;
5023
+ clearTimeout(this.timer);
5024
+ this.timer = undefined;
5025
+ }
5026
+ }
5027
+ stop() {
5028
+ clearTimeout(this.timer);
5029
+ this.timer = undefined;
5030
+ this.lastId = 0;
5031
+ }
5032
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToaster, deps: [], target: i0.ɵɵFactoryTarget.Component });
5033
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiToaster, isStandalone: true, selector: "ui-toaster", host: { attributes: { "role": "status" }, classAttribute: "pointer-events-none fixed inset-x-0 z-50 flex justify-center bottom-[calc(var(--tab-bar-height,calc(52px+env(safe-area-inset-bottom)))+var(--action-bar-height,0px)+8px)] lg:inset-x-auto lg:right-6 lg:bottom-6" }, ngImport: i0, template: `
5034
+ @if (toasts.toast(); as toast) {
5035
+ <div
5036
+ animate.leave="ui-leave-fade"
5037
+ [class]="toastClasses"
5038
+ (focusin)="setFocused(true)"
5039
+ (focusout)="setFocused(false)"
5040
+ (mouseenter)="setHovered(true)"
5041
+ (mouseleave)="setHovered(false)"
5042
+ >
5043
+ {{ toast.text }}
5044
+ </div>
5045
+ }
5046
+ `, isInline: true });
5047
+ }
5048
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToaster, decorators: [{
5049
+ type: Component,
5050
+ args: [{
5051
+ selector: 'ui-toaster',
5052
+ template: `
5053
+ @if (toasts.toast(); as toast) {
5054
+ <div
5055
+ animate.leave="ui-leave-fade"
5056
+ [class]="toastClasses"
5057
+ (focusin)="setFocused(true)"
5058
+ (focusout)="setFocused(false)"
5059
+ (mouseenter)="setHovered(true)"
5060
+ (mouseleave)="setHovered(false)"
5061
+ >
5062
+ {{ toast.text }}
5063
+ </div>
5064
+ }
5065
+ `,
5066
+ host: {
5067
+ role: 'status',
5068
+ class: 'pointer-events-none fixed inset-x-0 z-50 flex justify-center bottom-[calc(var(--tab-bar-height,calc(52px+env(safe-area-inset-bottom)))+var(--action-bar-height,0px)+8px)] lg:inset-x-auto lg:right-6 lg:bottom-6',
5069
+ },
5070
+ }]
5071
+ }], ctorParameters: () => [] });
5072
+
3950
5073
  /** Breakpoints a secondary column can be held back until. */
3951
5074
  const CELL_BREAKPOINTS = ['md', 'lg'];
3952
5075
  /** Body row heights. */
@@ -4030,8 +5153,10 @@ class UiTr {
4030
5153
  return [
4031
5154
  'relative [&>td:first-child]:rounded-l-control [&>td:last-child]:rounded-r-control',
4032
5155
  this.selected() ? '[&>td]:bg-(--soft)' : '',
4033
- 'hover:[&>td]:bg-(--glow)',
4034
- this.interactive() ? 'active:[&>td]:bg-(--soft)' : '',
5156
+ 'hover:[&>td]:bg-(--glow) [&>td]:transition-colors [&>td]:duration-(--duration-fast) [&>td]:ease-out',
5157
+ this.interactive()
5158
+ ? 'active:[&>td]:bg-(--soft) transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale)'
5159
+ : '',
4035
5160
  ]
4036
5161
  .filter(Boolean)
4037
5162
  .join(' ');
@@ -4261,5 +5386,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
4261
5386
  * Generated bundle index. Do not edit.
4262
5387
  */
4263
5388
 
4264
- export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_ALIGNS, ASYNC_STATES, ASYNC_VARIANTS, AVATAR_SIZES, BACK_LINK_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_SURFACES, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DELTA_SIZES, DELTA_WEIGHTS, DIALOG_LAYOUTS, DIALOG_WIDTHS, FACT_SIZES, FACT_SUB_TONES, GROUP_SIZES, METER_SIZES, METER_TONES, ROW_GAPS, ROW_PADDINGS, ROW_SIZES, SEGMENTED_SIZES, SKELETON_SHAPES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiActionBar, UiAlert, UiAmount, UiAsync, UiAvatar, UiAvatarLink, UiBackLink, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiExternalLink, UiFact, UiFacts, UiField, UiFieldLeading, UiFieldTrailing, UiFilterChips, UiGroup, UiGroupCell, UiInput, UiLineChart, UiListRow, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowItem, UiRowLink, UiRowTile, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
5389
+ export { ALERT_VARIANTS, AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_ALIGNS, ASYNC_STATES, ASYNC_VARIANTS, AVATAR_SIZES, BACK_LINK_SIZES, BADGE_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_SURFACES, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, CONTROL_SURFACES, DELTA_EMPHASES, DELTA_SIZES, DELTA_WEIGHTS, DIALOG_CLOSE_REASONS, DIALOG_LAYOUTS, DIALOG_WIDTHS, FACT_SIZES, FACT_SUB_TONES, GROUP_SIZES, METER_SIZES, METER_TONES, ROW_GAPS, ROW_PADDINGS, ROW_SIZES, SEGMENTED_SIZES, SKELETON_SHAPES, STAT_SIZES, SUB_TONES, TABLE_ROWS, UI_AMOUNT_MASKED, UiActionBar, UiAlert, UiAmount, UiAsync, UiAvatar, UiAvatarLink, UiBackLink, UiBadge, UiButton, UiCard, UiCellSub, UiChoiceChips, UiDelta, UiDialog, UiDonut, UiEmpty, UiExternalLink, UiFact, UiFacts, UiField, UiFieldLeading, UiFieldTrailing, UiFilterChips, UiFlipItem, UiFlipList, UiGroup, UiGroupCell, UiHighlight, UiInput, UiLineChart, UiListRow, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiRowAction, UiRowItem, UiRowLink, UiRowTile, UiSegmented, UiSelect, UiSkeleton, UiStaleLink, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiToaster, UiToasts, UiTr, delayedState, formatAmount, injectReducedMotion };
4265
5390
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map