@joanroucoux/cairn-ui 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 = '--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
10
  * thumb reach. It sits 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,6 +19,21 @@ 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, `${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));
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
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-[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
  }
@@ -29,16 +46,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
29
46
  class: 'fixed inset-x-0 bottom-[calc(52px+env(safe-area-inset-bottom))] z-40 grid grid-flow-col auto-cols-fr gap-2 bg-(--background) px-(--gutter) py-3 shadow-[0_-1px_0_var(--hairline)] lg:hidden',
30
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,242 @@ 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
+ #gesture = null;
1445
+ constructor(panel, grips, dismiss) {
1446
+ this.#panel = panel;
1447
+ this.#grips = grips;
1448
+ this.#dismiss = dismiss;
1449
+ for (const grip of grips) {
1450
+ grip.addEventListener('pointerdown', this.#onDown);
1451
+ grip.addEventListener('pointermove', this.#onMove);
1452
+ grip.addEventListener('pointerup', this.#onUp);
1453
+ grip.addEventListener('pointercancel', this.#onCancel);
1454
+ }
1455
+ }
1456
+ reset() {
1457
+ this.#gesture = null;
1458
+ this.#panel.removeAttribute('data-dragging');
1459
+ this.#panel.style.removeProperty('--drag-y');
1460
+ }
1461
+ destroy() {
1462
+ for (const grip of this.#grips) {
1463
+ grip.removeEventListener('pointerdown', this.#onDown);
1464
+ grip.removeEventListener('pointermove', this.#onMove);
1465
+ grip.removeEventListener('pointerup', this.#onUp);
1466
+ grip.removeEventListener('pointercancel', this.#onCancel);
1467
+ }
1468
+ }
1469
+ #onDown = (event) => {
1470
+ const grip = event.currentTarget;
1471
+ if (event.button !== 0 || !isSheet() || event.target.closest(CONTROLS)) {
1472
+ return;
1473
+ }
1474
+ grip.setPointerCapture(event.pointerId);
1475
+ this.#gesture = {
1476
+ id: event.pointerId,
1477
+ startX: event.clientX,
1478
+ startY: event.clientY,
1479
+ dragging: false,
1480
+ height: 0,
1481
+ offset: 0,
1482
+ samples: [{ y: event.clientY, at: event.timeStamp }],
1483
+ };
1484
+ };
1485
+ #onMove = (event) => {
1486
+ const gesture = this.#current(event);
1487
+ if (!gesture) {
1488
+ return;
1489
+ }
1490
+ const dx = event.clientX - gesture.startX;
1491
+ const dy = event.clientY - gesture.startY;
1492
+ if (!gesture.dragging) {
1493
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < SLOP) {
1494
+ return;
1495
+ }
1496
+ if (Math.abs(dx) > Math.abs(dy)) {
1497
+ this.#gesture = null;
1498
+ return;
1499
+ }
1500
+ gesture.dragging = true;
1501
+ gesture.height = this.#panel.getBoundingClientRect().height;
1502
+ this.#panel.setAttribute('data-dragging', '');
1503
+ }
1504
+ gesture.offset = Math.max(0, dy);
1505
+ gesture.samples.push({ y: event.clientY, at: event.timeStamp });
1506
+ this.#panel.style.setProperty('--drag-y', `${gesture.offset}px`);
1507
+ };
1508
+ #onUp = (event) => {
1509
+ const gesture = this.#current(event);
1510
+ if (!gesture) {
1511
+ return;
1512
+ }
1513
+ this.#gesture = null;
1514
+ if (!gesture.dragging) {
1515
+ return;
1516
+ }
1517
+ this.#panel.removeAttribute('data-dragging');
1518
+ const release = { y: event.clientY, at: event.timeStamp };
1519
+ if (gesture.offset > gesture.height * DISMISS_RATIO || releaseSpeed(gesture.samples, release) > DISMISS_SPEED) {
1520
+ this.#dismiss();
1521
+ }
1522
+ else {
1523
+ this.reset();
1524
+ }
1525
+ };
1526
+ #onCancel = (event) => {
1527
+ if (this.#current(event)) {
1528
+ this.#gesture = null;
1529
+ this.#panel.removeAttribute('data-dragging');
1530
+ this.reset();
1531
+ }
1532
+ };
1533
+ #current(event) {
1534
+ return this.#gesture?.id === event.pointerId ? this.#gesture : null;
1535
+ }
1536
+ }
1537
+ function isSheet() {
1538
+ return typeof matchMedia === 'function' && !matchMedia('(min-width: 64rem)').matches;
1539
+ }
1540
+ function releaseSpeed(samples, last) {
1541
+ const first = samples.find((sample) => sample.at >= last.at - SPEED_WINDOW) ?? last;
1542
+ const elapsed = last.at - first.at;
1543
+ return elapsed > 0 ? (last.y - first.y) / elapsed : 0;
1544
+ }
1545
+
1233
1546
  /** Named dialog widths; any other CSS length is accepted as is. `DialogWidth` is derived from this tuple. */
1234
1547
  const DIALOG_WIDTHS = ['md', 'lg'];
1235
1548
  const WIDTH_VALUES = {
@@ -1280,6 +1593,8 @@ const LAYOUTS = {
1280
1593
  footer: 'max-lg:pb-[calc(8px+env(safe-area-inset-bottom))] lg:pb-6',
1281
1594
  },
1282
1595
  };
1596
+ /** What started a close, as `closed` reports it. `DialogCloseReason` is derived from this tuple. */
1597
+ const DIALOG_CLOSE_REASONS = ['escape', 'backdrop', 'cross', 'drag', 'programmatic'];
1283
1598
  const nextId$3 = (() => {
1284
1599
  let count = 0;
1285
1600
  return () => `ui-dialog-${++count}`;
@@ -1308,6 +1623,7 @@ class UiDialog {
1308
1623
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
1309
1624
  open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1310
1625
  dismissed = output();
1626
+ closed = output();
1311
1627
  headingId = nextId$3();
1312
1628
  descriptionId = `${this.headingId}-description`;
1313
1629
  bodyId = `${this.headingId}-body`;
@@ -1327,11 +1643,11 @@ class UiDialog {
1327
1643
  ...(ngDevMode ? [{ debugName: "widthValue" }] : /* istanbul ignore next */ []));
1328
1644
  spec = computed(() => LAYOUTS[this.layout()], /* @ts-ignore */
1329
1645
  ...(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 */
1646
+ 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
1647
  ...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
1332
1648
  titleClasses = computed(() => this.spec().title, /* @ts-ignore */
1333
1649
  ...(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 */
1650
+ 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) lg:size-9 lg:hover:bg-(--glow) lg:hover:text-(--foreground) ${this.spec().cross}`, /* @ts-ignore */
1335
1651
  ...(ngDevMode ? [{ debugName: "crossClasses" }] : /* istanbul ignore next */ []));
1336
1652
  iconClasses = computed(() => this.spec().icon, /* @ts-ignore */
1337
1653
  ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
@@ -1343,6 +1659,13 @@ class UiDialog {
1343
1659
  constructor() {
1344
1660
  const destroyRef = inject(DestroyRef);
1345
1661
  afterNextRender(() => {
1662
+ const host = this.#host.nativeElement;
1663
+ const grips = ['[data-dialog-handle]', '[data-dialog-header]'].map((grip) => host.querySelector(grip));
1664
+ this.#drag = new SheetDrag(this.#dialog, grips, () => this.close('drag'));
1665
+ destroyRef.onDestroy(() => {
1666
+ this.#drag?.destroy();
1667
+ this.#cancelExit();
1668
+ });
1346
1669
  if (typeof ResizeObserver === 'undefined') {
1347
1670
  return;
1348
1671
  }
@@ -1356,20 +1679,38 @@ class UiDialog {
1356
1679
  // viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
1357
1680
  const dialog = this.#host.nativeElement.querySelector('dialog');
1358
1681
  if (this.open() && !dialog.open) {
1682
+ this.#cancelExit();
1683
+ this.#drag?.reset();
1359
1684
  dialog.showModal();
1360
1685
  }
1361
1686
  else if (!this.open() && dialog.open) {
1362
- dialog.close();
1687
+ this.close('programmatic');
1363
1688
  }
1364
1689
  });
1365
1690
  }
1366
1691
  #pressedOnBackdrop = false;
1692
+ #drag = null;
1693
+ #reason = null;
1694
+ #cancelExit = () => undefined;
1695
+ close(reason) {
1696
+ if (this.#dialog.open) {
1697
+ this.#reason = reason;
1698
+ this.#dialog.close();
1699
+ }
1700
+ }
1701
+ onCancel(event) {
1702
+ if (event.target !== event.currentTarget) {
1703
+ return;
1704
+ }
1705
+ event.preventDefault();
1706
+ this.close('escape');
1707
+ }
1367
1708
  onPointerDown(event) {
1368
1709
  this.#pressedOnBackdrop = event.target === this.#dialog && this.#outside(event);
1369
1710
  }
1370
1711
  onClick(event) {
1371
1712
  if (this.#pressedOnBackdrop && event.target === this.#dialog && this.#outside(event)) {
1372
- this.#dialog.close();
1713
+ this.close('backdrop');
1373
1714
  }
1374
1715
  this.#pressedOnBackdrop = false;
1375
1716
  }
@@ -1381,13 +1722,26 @@ class UiDialog {
1381
1722
  return (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom);
1382
1723
  }
1383
1724
  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()) {
1725
+ const reason = this.#reason ?? 'escape';
1726
+ this.#reason = null;
1727
+ if (this.#dialog.open) {
1728
+ return;
1729
+ }
1730
+ if (reason !== 'programmatic') {
1386
1731
  this.dismissed.emit();
1387
1732
  }
1733
+ this.#awaitExit(reason);
1734
+ }
1735
+ #awaitExit(reason) {
1736
+ this.#cancelExit();
1737
+ this.#cancelExit = afterExit(this.#dialog, () => {
1738
+ this.#cancelExit = () => undefined;
1739
+ this.#drag?.reset();
1740
+ this.closed.emit(reason);
1741
+ });
1388
1742
  }
1389
1743
  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: `
1744
+ 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", closed: "closed" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)" } }, ngImport: i0, template: `
1391
1745
  <dialog
1392
1746
  #dlg
1393
1747
  [attr.aria-describedby]="describedBy()"
@@ -1395,9 +1749,15 @@ class UiDialog {
1395
1749
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1396
1750
  [class]="classes()"
1397
1751
  [style.--dialog-width]="widthValue()"
1752
+ (cancel)="onCancel($event)"
1398
1753
  (close)="onNativeClose()"
1399
1754
  >
1400
- <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1755
+ <div
1756
+ aria-hidden="true"
1757
+ class="flex h-5 touch-none justify-center lg:hidden"
1758
+ data-dialog-handle
1759
+ [class]="spec().handle"
1760
+ >
1401
1761
  <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
1402
1762
  </div>
1403
1763
 
@@ -1417,7 +1777,7 @@ class UiDialog {
1417
1777
  </div>
1418
1778
 
1419
1779
  @if (closeLabel()) {
1420
- <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1780
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="close('cross')">
1421
1781
  <svg
1422
1782
  aria-hidden="true"
1423
1783
  class="block size-[22px] fill-none stroke-current stroke-[1.75]"
@@ -1451,7 +1811,7 @@ class UiDialog {
1451
1811
  <ng-content select="[dialogActions]" />
1452
1812
  </div>
1453
1813
  </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"] });
1814
+ `, 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
1815
  }
1456
1816
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
1457
1817
  type: Component,
@@ -1463,9 +1823,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1463
1823
  [attr.role]="layout() === 'confirm' ? 'alertdialog' : 'dialog'"
1464
1824
  [class]="classes()"
1465
1825
  [style.--dialog-width]="widthValue()"
1826
+ (cancel)="onCancel($event)"
1466
1827
  (close)="onNativeClose()"
1467
1828
  >
1468
- <div aria-hidden="true" class="flex h-5 justify-center lg:hidden" data-dialog-handle [class]="spec().handle">
1829
+ <div
1830
+ aria-hidden="true"
1831
+ class="flex h-5 touch-none justify-center lg:hidden"
1832
+ data-dialog-handle
1833
+ [class]="spec().handle"
1834
+ >
1469
1835
  <span class="rounded-pill h-[5px] w-9 bg-(--border)"></span>
1470
1836
  </div>
1471
1837
 
@@ -1485,7 +1851,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1485
1851
  </div>
1486
1852
 
1487
1853
  @if (closeLabel()) {
1488
- <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="dlg.close()">
1854
+ <button type="button" [attr.aria-label]="closeLabel()" [class]="crossClasses()" (click)="close('cross')">
1489
1855
  <svg
1490
1856
  aria-hidden="true"
1491
1857
  class="block size-[22px] fill-none stroke-current stroke-[1.75]"
@@ -1519,12 +1885,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1519
1885
  <ng-content select="[dialogActions]" />
1520
1886
  </div>
1521
1887
  </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"] }] } });
1888
+ `, 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"] }]
1889
+ }], 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"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1524
1890
 
1525
1891
  const RADIUS = 78;
1526
1892
  const RING_WIDTH = 30;
1527
- const ACTIVE_RING_WIDTH = 38;
1528
1893
  const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
1529
1894
  const GAP = 2;
1530
1895
  function positionSlices(slices) {
@@ -1571,7 +1936,8 @@ function rankSlices(slices, othersLabel) {
1571
1936
 
1572
1937
  const identityValue$1 = (value) => `${value}`;
1573
1938
  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)';
1939
+ 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)';
1940
+ const SLICE_CLASSES = 'cursor-pointer origin-center [transform-box:view-box] transition-[scale,opacity] duration-(--duration-fast) ease-out';
1575
1941
  const RAMP_DOT_CLASSES = {
1576
1942
  1: 'bg-(--ramp-1)',
1577
1943
  2: 'bg-(--ramp-2)',
@@ -1625,8 +1991,12 @@ class UiDonut {
1625
1991
  strokeColor(slice) {
1626
1992
  return `var(--ramp-${slice.ramp})`;
1627
1993
  }
1628
- strokeWidth(slice) {
1629
- return this.isActive(slice) ? ACTIVE_RING_WIDTH : RING_WIDTH;
1994
+ sliceClasses(slice) {
1995
+ const id = this.activeId();
1996
+ if (id === null) {
1997
+ return SLICE_CLASSES;
1998
+ }
1999
+ return `${SLICE_CLASSES} ${id === slice.id ? 'scale-104 motion-reduce:scale-100' : 'opacity-50'}`;
1630
2000
  }
1631
2001
  dotClasses(slice) {
1632
2002
  return `size-3 flex-none rounded-pill shadow-[inset_0_0_0_1px_var(--border)] ${RAMP_DOT_CLASSES[slice.ramp]}`;
@@ -1676,7 +2046,6 @@ class UiDonut {
1676
2046
  />
1677
2047
  @for (slice of positioned(); track slice.id) {
1678
2048
  <circle
1679
- class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1680
2049
  cx="100"
1681
2050
  cy="100"
1682
2051
  data-slice
@@ -1684,8 +2053,9 @@ class UiDonut {
1684
2053
  [attr.r]="radius"
1685
2054
  [attr.stroke-dasharray]="slice.dash"
1686
2055
  [attr.stroke-dashoffset]="slice.offset"
2056
+ [attr.stroke-width]="ringWidth"
2057
+ [class]="sliceClasses(slice)"
1687
2058
  [style.stroke]="strokeColor(slice)"
1688
- [style.stroke-width.px]="strokeWidth(slice)"
1689
2059
  (click)="select(slice)"
1690
2060
  (pointerenter)="highlight(slice.id)"
1691
2061
  (pointerleave)="clearHighlight(slice.id)"
@@ -1771,7 +2141,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1771
2141
  />
1772
2142
  @for (slice of positioned(); track slice.id) {
1773
2143
  <circle
1774
- class="cursor-pointer transition-[stroke-width] duration-(--duration-fast) ease-out"
1775
2144
  cx="100"
1776
2145
  cy="100"
1777
2146
  data-slice
@@ -1779,8 +2148,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1779
2148
  [attr.r]="radius"
1780
2149
  [attr.stroke-dasharray]="slice.dash"
1781
2150
  [attr.stroke-dashoffset]="slice.offset"
2151
+ [attr.stroke-width]="ringWidth"
2152
+ [class]="sliceClasses(slice)"
1782
2153
  [style.stroke]="strokeColor(slice)"
1783
- [style.stroke-width.px]="strokeWidth(slice)"
1784
2154
  (click)="select(slice)"
1785
2155
  (pointerenter)="highlight(slice.id)"
1786
2156
  (pointerleave)="clearHighlight(slice.id)"
@@ -1855,6 +2225,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1855
2225
  }]
1856
2226
  }], 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
2227
 
2228
+ /**
2229
+ * Centred empty state: a title, a hint and an action slot, such as a catalogue search that matches
2230
+ * nothing. The padding is 24px, 48px from 64rem.
2231
+ *
2232
+ * @example
2233
+ * <ui-empty heading="Aucun instrument ne correspond à « zzz »" hint="La recherche porte sur le nom et l'ISIN.">
2234
+ * <button ui-button variant="outline" size="md" type="button">Ajouter une ligne</button>
2235
+ * </ui-empty>
2236
+ */
2237
+ class UiEmpty {
2238
+ heading = input.required(/* @ts-ignore */
2239
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2240
+ hint = input('', /* @ts-ignore */
2241
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiEmpty, deps: [], target: i0.ɵɵFactoryTarget.Component });
2243
+ 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: `
2244
+ <span class="text-body font-medium text-pretty">{{ heading() }}</span>
2245
+ @if (hint()) {
2246
+ <span class="text-label text-(--muted-foreground)">{{ hint() }}</span>
2247
+ }
2248
+ <div class="mt-2 empty:hidden"><ng-content /></div>
2249
+ `, isInline: true });
2250
+ }
2251
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiEmpty, decorators: [{
2252
+ type: Component,
2253
+ args: [{
2254
+ selector: 'ui-empty',
2255
+ template: `
2256
+ <span class="text-body font-medium text-pretty">{{ heading() }}</span>
2257
+ @if (hint()) {
2258
+ <span class="text-label text-(--muted-foreground)">{{ hint() }}</span>
2259
+ }
2260
+ <div class="mt-2 empty:hidden"><ng-content /></div>
2261
+ `,
2262
+ host: { class: 'flex flex-col items-center gap-1.5 px-2 py-6 text-center lg:py-12' },
2263
+ }]
2264
+ }], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
2265
+
1858
2266
  /**
1859
2267
  * Link to another site: the label followed by an external-link icon. A 44px target on touch,
1860
2268
  * an underline on hover with a mouse.
@@ -2045,14 +2453,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2045
2453
  */
2046
2454
  class UiFieldTrailing {
2047
2455
  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 });
2456
+ 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
2457
  }
2050
2458
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, decorators: [{
2051
2459
  type: Directive,
2052
2460
  args: [{
2053
2461
  selector: '[uiFieldTrailing]',
2054
2462
  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)',
2463
+ 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
2464
  },
2057
2465
  }]
2058
2466
  }] });
@@ -2179,7 +2587,9 @@ class UiField {
2179
2587
  }
2180
2588
 
2181
2589
  @if (message()) {
2182
- <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2590
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
2591
+ {{ message() }}
2592
+ </p>
2183
2593
  }
2184
2594
  </div>
2185
2595
  `, isInline: true });
@@ -2212,7 +2622,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2212
2622
  }
2213
2623
 
2214
2624
  @if (message()) {
2215
- <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
2625
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
2626
+ {{ message() }}
2627
+ </p>
2216
2628
  }
2217
2629
  </div>
2218
2630
  `,
@@ -2299,6 +2711,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2299
2711
  }]
2300
2712
  }], 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
2713
 
2714
+ const QUERY = '(prefers-reduced-motion: reduce)';
2715
+ /**
2716
+ * A signal that follows the user's `prefers-reduced-motion` preference live.
2717
+ * Call it in an injection context; the listener is removed with that context.
2718
+ *
2719
+ * @example
2720
+ * protected readonly reduced = injectReducedMotion();
2721
+ */
2722
+ function injectReducedMotion() {
2723
+ if (typeof matchMedia === 'undefined') {
2724
+ return signal(false).asReadonly();
2725
+ }
2726
+ const query = matchMedia(QUERY);
2727
+ const reduced = signal(query.matches, /* @ts-ignore */
2728
+ ...(ngDevMode ? [{ debugName: "reduced" }] : /* istanbul ignore next */ []));
2729
+ const listener = (event) => reduced.set(event.matches);
2730
+ query.addEventListener('change', listener);
2731
+ inject(DestroyRef).onDestroy(() => query.removeEventListener('change', listener));
2732
+ return reduced.asReadonly();
2733
+ }
2734
+
2302
2735
  const axisTicks = (points, maxTicks = 5) => {
2303
2736
  if (points.length <= maxTicks) {
2304
2737
  return [...points];
@@ -2441,6 +2874,23 @@ const buildGeometry = (points, width, height, padding = {}) => {
2441
2874
  };
2442
2875
  };
2443
2876
 
2877
+ const observeSize = (element, destroyRef, measured) => {
2878
+ if (typeof ResizeObserver === 'undefined') {
2879
+ return;
2880
+ }
2881
+ const { clientWidth: width, clientHeight: height } = element;
2882
+ if (width > 0 && height > 0) {
2883
+ measured({ width, height });
2884
+ }
2885
+ const observer = new ResizeObserver(([entry]) => {
2886
+ if (entry) {
2887
+ measured({ width: entry.contentRect.width, height: entry.contentRect.height });
2888
+ }
2889
+ });
2890
+ observer.observe(element);
2891
+ destroyRef.onDestroy(() => observer.disconnect());
2892
+ };
2893
+
2444
2894
  const LINE_GAP = 6;
2445
2895
  const COLLISION_MARGIN = 6;
2446
2896
  const EDGE_TOLERANCE = 3;
@@ -2489,7 +2939,15 @@ const placeStartLabel = (points, startY, box, label) => {
2489
2939
  const TRANSITION_SAMPLES = 120;
2490
2940
  const TRANSITION_DURATION = 260;
2491
2941
  const easeOutQuint = (t) => 1 - (1 - t) ** 5;
2492
- const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
2942
+ const durationFrom = (value) => {
2943
+ const match = /^\s*([\d.]+)(ms|s)\s*$/.exec(value);
2944
+ const duration = match ? Number(match[1]) * (match[2] === 's' ? 1000 : 1) : Number.NaN;
2945
+ return Number.isFinite(duration) ? duration : TRANSITION_DURATION;
2946
+ };
2947
+ const sameSeries = (a, b) => a === b ||
2948
+ (b !== null &&
2949
+ a.length === b.length &&
2950
+ a.every((point, index) => point.t === b[index].t && point.v === b[index].v));
2493
2951
  const monotoneTangents = (xs, ys) => {
2494
2952
  const n = xs.length;
2495
2953
  const slopes = [];
@@ -2545,15 +3003,85 @@ const sampleMonotone = (points, sampleXs) => {
2545
3003
  return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
2546
3004
  });
2547
3005
  };
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));
3006
+ const polyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
3007
+ const shapeTransition = (from, to) => {
3008
+ const left = to[0].x;
3009
+ const right = to.at(-1).x;
3010
+ const xs = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
3011
+ return { xs, from: sampleMonotone(from, xs), to: sampleMonotone(to, xs) };
3012
+ };
3013
+ const shapeAt = (transition, progress) => transition.from.map((fromY, index) => fromY + (transition.to[index] - fromY) * progress);
3014
+ const animateShape = (from, to, duration, draw, done) => {
3015
+ const transition = shapeTransition(from, to);
3016
+ const start = performance.now();
3017
+ let frame = 0;
3018
+ const step = (now) => {
3019
+ const t = Math.min(1, Math.max(0, (now - start) / duration));
3020
+ if (t === 1) {
3021
+ done();
3022
+ return;
3023
+ }
3024
+ const ys = shapeAt(transition, easeOutQuint(t));
3025
+ draw(transition.xs.map((x, index) => ({ x, y: ys[index] })), polyline(transition.xs, ys));
3026
+ frame = requestAnimationFrame(step);
3027
+ };
3028
+ frame = requestAnimationFrame(step);
3029
+ return () => cancelAnimationFrame(frame);
2556
3030
  };
3031
+ class ShapeMotion {
3032
+ #view;
3033
+ #points = null;
3034
+ #target = null;
3035
+ #held = null;
3036
+ #drawn = null;
3037
+ #stop = null;
3038
+ constructor(view) {
3039
+ this.#view = view;
3040
+ }
3041
+ follow(points, geometry, options) {
3042
+ const newSeries = !sameSeries(points, this.#points);
3043
+ this.#points = points;
3044
+ if (this.#stop && !newSeries && !options.reduced && geometry?.line === this.#target?.line) {
3045
+ this.#target = geometry;
3046
+ return;
3047
+ }
3048
+ const from = this.#drawn;
3049
+ const held = this.#held ?? this.#target;
3050
+ this.#target = geometry;
3051
+ this.stop();
3052
+ if (!newSeries || !geometry || !from || from.length < 2 || geometry.points.length < 2 || options.reduced) {
3053
+ this.#settle(geometry);
3054
+ return;
3055
+ }
3056
+ const duration = options.duration();
3057
+ if (duration <= 0) {
3058
+ this.#settle(geometry);
3059
+ return;
3060
+ }
3061
+ this.#hold(held);
3062
+ this.#stop = animateShape(from, geometry.points, duration, (shape, line) => this.#draw(shape, line, shape.at(-1)), () => this.#settle(geometry));
3063
+ }
3064
+ stop() {
3065
+ this.#stop?.();
3066
+ this.#stop = null;
3067
+ }
3068
+ #hold(geometry) {
3069
+ this.#held = geometry;
3070
+ this.#view.hold(geometry);
3071
+ }
3072
+ #settle(geometry) {
3073
+ this.#stop = null;
3074
+ this.#hold(null);
3075
+ this.#drawn = null;
3076
+ if (geometry) {
3077
+ this.#draw(geometry.points, geometry.line, geometry.end);
3078
+ }
3079
+ }
3080
+ #draw(shape, line, end) {
3081
+ this.#drawn = shape;
3082
+ this.#view.draw(line, end);
3083
+ }
3084
+ }
2557
3085
 
2558
3086
  const WIDE_QUERY = '(min-width: 64rem)';
2559
3087
  const wideViewport = () => {
@@ -2608,16 +3136,30 @@ class UiLineChart {
2608
3136
  ...(ngDevMode ? [{ debugName: "axisGap" }] : /* istanbul ignore next */ []));
2609
3137
  axisTicks = input('auto', { ...(ngDevMode ? { debugName: "axisTicks" } : /* istanbul ignore next */ {}), transform: parseAxisTicks });
2610
3138
  #destroyRef = inject(DestroyRef);
3139
+ #host = inject(ElementRef);
2611
3140
  #wide = wideViewport();
2612
- #frame = null;
2613
- #hasRenderedOnce = false;
2614
- #previousGeometry = null;
3141
+ #reducedMotion = injectReducedMotion();
3142
+ #motion = new ShapeMotion({
3143
+ hold: (geometry) => {
3144
+ this.held.set(geometry);
3145
+ this.activeIndex.update((index) => (geometry ? null : index));
3146
+ },
3147
+ draw: (line, end) => {
3148
+ this.lineRef()?.nativeElement.setAttribute('d', line);
3149
+ this.endRef()?.nativeElement.setAttribute('cx', String(end.x));
3150
+ this.endRef()?.nativeElement.setAttribute('cy', String(end.y));
3151
+ },
3152
+ });
2615
3153
  svgRef = viewChild.required('svgRef', /* @ts-ignore */
2616
3154
  ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
3155
+ lineRef = viewChild('lineRef', /* @ts-ignore */
3156
+ ...(ngDevMode ? [{ debugName: "lineRef" }] : /* istanbul ignore next */ []));
3157
+ endRef = viewChild('endRef', /* @ts-ignore */
3158
+ ...(ngDevMode ? [{ debugName: "endRef" }] : /* istanbul ignore next */ []));
2617
3159
  activeIndex = signal(null, /* @ts-ignore */
2618
3160
  ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
2619
- animatedLine = signal(null, /* @ts-ignore */
2620
- ...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
3161
+ held = signal(null, /* @ts-ignore */
3162
+ ...(ngDevMode ? [{ debugName: "held" }] : /* istanbul ignore next */ []));
2621
3163
  measuredSize = signal(null, /* @ts-ignore */
2622
3164
  ...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
2623
3165
  effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
@@ -2635,8 +3177,10 @@ class UiLineChart {
2635
3177
  return buildGeometry(this.points(), this.effectiveWidth(), plotHeight, { x: 0, top: padding, bottom: padding });
2636
3178
  }, /* @ts-ignore */
2637
3179
  ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
2638
- displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
2639
- ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
3180
+ shown = computed(() => this.held() ?? this.geometry(), /* @ts-ignore */
3181
+ ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
3182
+ startFade = computed(() => this.held() ? 'opacity-0 duration-(--duration-exit)' : 'duration-(--duration-fast)', /* @ts-ignore */
3183
+ ...(ngDevMode ? [{ debugName: "startFade" }] : /* istanbul ignore next */ []));
2640
3184
  ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
2641
3185
  ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
2642
3186
  tickTexts = computed(() => tickLabels(this.ticks(), this.axisFormat(), this.axisTicks()), /* @ts-ignore */
@@ -2644,12 +3188,12 @@ class UiLineChart {
2644
3188
  active = computed(() => {
2645
3189
  const geometry = this.geometry();
2646
3190
  const index = this.activeIndex();
2647
- return geometry && index !== null ? geometry.points[index] : null;
3191
+ return geometry && index !== null && !this.held() ? geometry.points[index] : null;
2648
3192
  }, /* @ts-ignore */
2649
3193
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
2650
3194
  startLabelInfo = computed(() => {
2651
3195
  const text = this.startLabel();
2652
- const geometry = this.geometry();
3196
+ const geometry = this.shown();
2653
3197
  if (!text || !geometry) {
2654
3198
  return null;
2655
3199
  }
@@ -2659,63 +3203,21 @@ class UiLineChart {
2659
3203
  return { label, placement: { ...placement, y: startLabelBaseline(placement.y) } };
2660
3204
  }, /* @ts-ignore */
2661
3205
  ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
2662
- #renderRef = afterNextRender(() => this.#observeSize());
3206
+ #renderRef = afterNextRender(() => {
3207
+ this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
3208
+ observeSize(this.svgRef().nativeElement, this.#destroyRef, (size) => this.measuredSize.set(size));
3209
+ });
2663
3210
  constructor() {
2664
3211
  effect(() => {
3212
+ const points = this.points();
2665
3213
  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
- }
3214
+ const reduced = this.#reducedMotion();
3215
+ untracked(() => this.#motion.follow(points, geometry, {
3216
+ reduced,
3217
+ duration: () => durationFrom(getComputedStyle(this.#host.nativeElement).getPropertyValue('--duration-base')),
3218
+ }));
2699
3219
  });
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);
3220
+ this.#destroyRef.onDestroy(() => this.#motion.stop());
2719
3221
  }
2720
3222
  deltaFor(point) {
2721
3223
  return point.v - this.points()[0].v;
@@ -2751,7 +3253,7 @@ class UiLineChart {
2751
3253
  this.activeIndex.set(navigation.index);
2752
3254
  }
2753
3255
  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: `
3256
+ 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
3257
  <div class="relative h-full">
2756
3258
  <svg
2757
3259
  #svgRef
@@ -2766,48 +3268,52 @@ class UiLineChart {
2766
3268
  (pointerleave)="onPointerLeave()"
2767
3269
  (pointermove)="onPointerMove($event)"
2768
3270
  >
2769
- @if (geometry(); as geometry) {
3271
+ @if (shown(); as shape) {
2770
3272
  <path
3273
+ #lineRef
2771
3274
  class="stroke-(--foreground)"
2772
3275
  data-chart-line
2773
3276
  fill="none"
2774
3277
  stroke-linecap="round"
2775
3278
  stroke-linejoin="round"
2776
- [attr.d]="displayLine()"
3279
+ [attr.d]="shape.line"
2777
3280
  [attr.stroke-width]="sparkline() ? 1.5 : 2"
2778
3281
  />
2779
3282
 
2780
3283
  @if (!sparkline()) {
2781
3284
  <line
2782
- class="stroke-(--border)"
3285
+ class="stroke-(--border) transition-opacity ease-(--ease-out)"
2783
3286
  data-chart-start-line
2784
3287
  stroke-dasharray="3 4"
2785
3288
  stroke-width="1"
2786
3289
  x1="0"
2787
3290
  [attr.x2]="effectiveWidth()"
2788
- [attr.y1]="geometry.start.y"
2789
- [attr.y2]="geometry.start.y"
3291
+ [attr.y1]="shape.start.y"
3292
+ [attr.y2]="shape.start.y"
3293
+ [class]="startFade()"
2790
3294
  />
2791
3295
 
2792
3296
  @if (startLabelInfo(); as info) {
2793
3297
  <text
2794
- class="text-caption fill-(--subtle-foreground) tabular-nums"
3298
+ class="text-caption fill-(--subtle-foreground) tabular-nums transition-opacity ease-(--ease-out)"
2795
3299
  data-chart-start-label
2796
3300
  [attr.text-anchor]="info.placement.anchor"
2797
3301
  [attr.x]="info.placement.x"
2798
3302
  [attr.y]="info.placement.y"
3303
+ [class]="startFade()"
2799
3304
  >
2800
3305
  {{ info.label }}
2801
3306
  </text>
2802
3307
  }
2803
3308
 
2804
3309
  <circle
3310
+ #endRef
2805
3311
  class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2806
3312
  data-chart-end
2807
3313
  r="4"
2808
3314
  stroke-width="6"
2809
- [attr.cx]="geometry.end.x"
2810
- [attr.cy]="geometry.end.y"
3315
+ [attr.cx]="shape.end.x"
3316
+ [attr.cy]="shape.end.y"
2811
3317
  />
2812
3318
 
2813
3319
  @for (tick of ticks(); track tick.t; let index = $index; let first = $first; let last = $last) {
@@ -2908,48 +3414,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2908
3414
  (pointerleave)="onPointerLeave()"
2909
3415
  (pointermove)="onPointerMove($event)"
2910
3416
  >
2911
- @if (geometry(); as geometry) {
3417
+ @if (shown(); as shape) {
2912
3418
  <path
3419
+ #lineRef
2913
3420
  class="stroke-(--foreground)"
2914
3421
  data-chart-line
2915
3422
  fill="none"
2916
3423
  stroke-linecap="round"
2917
3424
  stroke-linejoin="round"
2918
- [attr.d]="displayLine()"
3425
+ [attr.d]="shape.line"
2919
3426
  [attr.stroke-width]="sparkline() ? 1.5 : 2"
2920
3427
  />
2921
3428
 
2922
3429
  @if (!sparkline()) {
2923
3430
  <line
2924
- class="stroke-(--border)"
3431
+ class="stroke-(--border) transition-opacity ease-(--ease-out)"
2925
3432
  data-chart-start-line
2926
3433
  stroke-dasharray="3 4"
2927
3434
  stroke-width="1"
2928
3435
  x1="0"
2929
3436
  [attr.x2]="effectiveWidth()"
2930
- [attr.y1]="geometry.start.y"
2931
- [attr.y2]="geometry.start.y"
3437
+ [attr.y1]="shape.start.y"
3438
+ [attr.y2]="shape.start.y"
3439
+ [class]="startFade()"
2932
3440
  />
2933
3441
 
2934
3442
  @if (startLabelInfo(); as info) {
2935
3443
  <text
2936
- class="text-caption fill-(--subtle-foreground) tabular-nums"
3444
+ class="text-caption fill-(--subtle-foreground) tabular-nums transition-opacity ease-(--ease-out)"
2937
3445
  data-chart-start-label
2938
3446
  [attr.text-anchor]="info.placement.anchor"
2939
3447
  [attr.x]="info.placement.x"
2940
3448
  [attr.y]="info.placement.y"
3449
+ [class]="startFade()"
2941
3450
  >
2942
3451
  {{ info.label }}
2943
3452
  </text>
2944
3453
  }
2945
3454
 
2946
3455
  <circle
3456
+ #endRef
2947
3457
  class="fill-(--foreground) stroke-(--card) [paint-order:stroke]"
2948
3458
  data-chart-end
2949
3459
  r="4"
2950
3460
  stroke-width="6"
2951
- [attr.cx]="geometry.end.x"
2952
- [attr.cy]="geometry.end.y"
3461
+ [attr.cx]="shape.end.x"
3462
+ [attr.cy]="shape.end.y"
2953
3463
  />
2954
3464
 
2955
3465
  @for (tick of ticks(); track tick.t; let index = $index; let first = $first; let last = $last) {
@@ -3034,12 +3544,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3034
3544
  class: 'block h-full',
3035
3545
  },
3036
3546
  }]
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 }] }] } });
3547
+ }], 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
3548
 
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';
3549
+ 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';
3550
+ 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';
3551
+ 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';
3552
+ 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
3553
  const nextId$1 = (() => {
3044
3554
  let count = 0;
3045
3555
  return () => `ui-menu-${++count}`;
@@ -3341,7 +3851,221 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3341
3851
  }]
3342
3852
  }], 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
3853
 
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)';
3854
+ function readDuration$1(element, name, fallback) {
3855
+ const raw = getComputedStyle(element).getPropertyValue(name).trim();
3856
+ const value = parseFloat(raw);
3857
+ if (Number.isNaN(value)) {
3858
+ return fallback;
3859
+ }
3860
+ return raw.endsWith('ms') ? value : value * 1000;
3861
+ }
3862
+ function readEasing(element, name, fallback) {
3863
+ return getComputedStyle(element).getPropertyValue(name).trim() || fallback;
3864
+ }
3865
+
3866
+ /**
3867
+ * Slides the remaining children of a list into place when one is removed or inserted (FLIP): it
3868
+ * measures every child before and after the change, then animates the shift in `translateY` over
3869
+ * `--duration-base` `--ease-out`. Pair it with `animate.leave="ui-leave-fade"` on the item so it fades
3870
+ * out first. Nothing moves under `prefers-reduced-motion`: the list closes up at once.
3871
+ *
3872
+ * @example
3873
+ * <ul uiFlipList>
3874
+ * @for (key of keys(); track key.id) {
3875
+ * <li uiListRow animate.leave="ui-leave-fade">...</li>
3876
+ * }
3877
+ * </ul>
3878
+ */
3879
+ class UiFlipList {
3880
+ host = inject(ElementRef).nativeElement;
3881
+ reduced = injectReducedMotion();
3882
+ mutations = new MutationObserver(() => this.onChange());
3883
+ resizes = new ResizeObserver(() => this.measure());
3884
+ running = new Map();
3885
+ watched = new Set();
3886
+ tops = new Map();
3887
+ constructor() {
3888
+ afterNextRender(() => {
3889
+ this.measure();
3890
+ this.mutations.observe(this.host, { childList: true });
3891
+ this.resizes.observe(this.host);
3892
+ });
3893
+ inject(DestroyRef).onDestroy(() => {
3894
+ this.mutations.disconnect();
3895
+ this.resizes.disconnect();
3896
+ });
3897
+ }
3898
+ measure() {
3899
+ const children = Array.from(this.host.children).filter((child) => child instanceof HTMLElement);
3900
+ this.tops = new Map(children.map((child) => [child, child.offsetTop]));
3901
+ for (const child of children) {
3902
+ if (!this.watched.has(child)) {
3903
+ this.watched.add(child);
3904
+ this.resizes.observe(child);
3905
+ }
3906
+ }
3907
+ for (const child of this.watched) {
3908
+ if (!this.tops.has(child)) {
3909
+ this.watched.delete(child);
3910
+ this.running.delete(child);
3911
+ this.resizes.unobserve(child);
3912
+ }
3913
+ }
3914
+ }
3915
+ interruptedOffset(child) {
3916
+ const running = this.running.get(child);
3917
+ if (!running) {
3918
+ return 0;
3919
+ }
3920
+ const shown = child.getBoundingClientRect().top;
3921
+ running.cancel();
3922
+ this.running.delete(child);
3923
+ return shown - child.getBoundingClientRect().top;
3924
+ }
3925
+ onChange() {
3926
+ const before = this.tops;
3927
+ this.measure();
3928
+ if (this.reduced()) {
3929
+ return;
3930
+ }
3931
+ const duration = readDuration$1(this.host, '--duration-base', 260);
3932
+ const easing = readEasing(this.host, '--ease-out', 'ease-out');
3933
+ for (const [child, top] of this.tops) {
3934
+ const previous = before.get(child);
3935
+ if (previous === undefined || previous === top) {
3936
+ continue;
3937
+ }
3938
+ const from = previous - top + this.interruptedOffset(child);
3939
+ this.running.set(child, child.animate([{ transform: `translateY(${from}px)` }, { transform: 'translateY(0)' }], { duration, easing }));
3940
+ }
3941
+ }
3942
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipList, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3943
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFlipList, isStandalone: true, selector: "[uiFlipList]", ngImport: i0 });
3944
+ }
3945
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFlipList, decorators: [{
3946
+ type: Directive,
3947
+ args: [{
3948
+ selector: '[uiFlipList]',
3949
+ }]
3950
+ }], ctorParameters: () => [] });
3951
+
3952
+ const HOLD_MS = 200;
3953
+ const SCROLL_SETTLE_MS = 200;
3954
+ const SCROLL_START_MS = 300;
3955
+ function paintedElements(host) {
3956
+ if (host.tagName === 'TR') {
3957
+ return Array.from(host.children).filter((cell) => cell instanceof HTMLElement);
3958
+ }
3959
+ if (host.hasAttribute('ui-group-cell') && host.firstElementChild instanceof HTMLElement) {
3960
+ return [host.firstElementChild];
3961
+ }
3962
+ return [host];
3963
+ }
3964
+ /**
3965
+ * Draws the eye to an element that just changed: scrolls it into view when it is off screen, holds the
3966
+ * `--soft` fill for 200 ms, then fades back to the element's own background over `--duration-highlight`.
3967
+ * Each new non-null token plays once, so pass a fresh value per event (an object or a counter), not a
3968
+ * boolean. A non-null token already present at first render plays on arrival. On a `tr` it paints the
3969
+ * cells, on a `td[ui-group-cell]` the band, anywhere else the host. Under `prefers-reduced-motion` the
3970
+ * scroll is instant and the fade lasts 600 ms.
3971
+ *
3972
+ * Give the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and
3973
+ * below it (header, tab bar, action bar): a row under them counts as off screen and is scrolled out.
3974
+ * For an arrival on a page opened at the element, scroll to it at once first (`scrollIntoView` in
3975
+ * `afterNextRender`) and set the token afterwards: a token present at first render scrolls smoothly
3976
+ * from where the page is.
3977
+ *
3978
+ * @example
3979
+ * <a ui-row [uiHighlight]="changed() === holding.id ? token() : null" routerLink="/holdings/1">...</a>
3980
+ */
3981
+ class UiHighlight {
3982
+ uiHighlight = input(null, /* @ts-ignore */
3983
+ ...(ngDevMode ? [{ debugName: "uiHighlight" }] : /* istanbul ignore next */ []));
3984
+ host = inject(ElementRef).nativeElement;
3985
+ reduced = injectReducedMotion();
3986
+ cancel = null;
3987
+ constructor() {
3988
+ effect(() => {
3989
+ const token = this.uiHighlight();
3990
+ if (token === null || token === undefined) {
3991
+ return;
3992
+ }
3993
+ untracked(() => this.play());
3994
+ });
3995
+ inject(DestroyRef).onDestroy(() => this.cancel?.());
3996
+ }
3997
+ play() {
3998
+ this.cancel?.();
3999
+ if (this.isVisible()) {
4000
+ this.cancel = this.flash();
4001
+ return;
4002
+ }
4003
+ const reduced = this.reduced();
4004
+ this.host.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' });
4005
+ if (reduced) {
4006
+ this.cancel = this.flash();
4007
+ return;
4008
+ }
4009
+ let stopFlash = null;
4010
+ let timer;
4011
+ const moves = (event) => event.target === document || (event.target instanceof Node && event.target.contains(this.host));
4012
+ const start = () => {
4013
+ stopListening();
4014
+ stopFlash = this.flash();
4015
+ };
4016
+ const onScrollEnd = (event) => {
4017
+ if (moves(event)) {
4018
+ start();
4019
+ }
4020
+ };
4021
+ const onScroll = (event) => {
4022
+ if (moves(event)) {
4023
+ clearTimeout(timer);
4024
+ timer = setTimeout(start, SCROLL_SETTLE_MS);
4025
+ }
4026
+ };
4027
+ const stopListening = () => {
4028
+ clearTimeout(timer);
4029
+ window.removeEventListener('scrollend', onScrollEnd, true);
4030
+ window.removeEventListener('scroll', onScroll, true);
4031
+ };
4032
+ timer = setTimeout(start, SCROLL_START_MS);
4033
+ window.addEventListener('scrollend', onScrollEnd, true);
4034
+ window.addEventListener('scroll', onScroll, true);
4035
+ this.cancel = () => {
4036
+ stopListening();
4037
+ stopFlash?.();
4038
+ };
4039
+ }
4040
+ isVisible() {
4041
+ const rect = this.host.getBoundingClientRect();
4042
+ const style = getComputedStyle(this.host);
4043
+ const top = parseFloat(style.scrollMarginTop) || 0;
4044
+ const bottom = parseFloat(style.scrollMarginBottom) || 0;
4045
+ return rect.top >= top && rect.bottom <= window.innerHeight - bottom;
4046
+ }
4047
+ flash() {
4048
+ const fade = readDuration$1(this.host, '--duration-highlight', 1200);
4049
+ const easing = readEasing(this.host, '--ease-out', 'ease-out');
4050
+ const total = HOLD_MS + fade;
4051
+ const animations = paintedElements(this.host).map((element) => element.animate([
4052
+ { backgroundColor: 'var(--soft)', offset: 0 },
4053
+ { backgroundColor: 'var(--soft)', offset: HOLD_MS / total, easing },
4054
+ { offset: 1 },
4055
+ ], { duration: total }));
4056
+ return () => animations.forEach((animation) => animation.cancel());
4057
+ }
4058
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiHighlight, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4059
+ 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 });
4060
+ }
4061
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiHighlight, decorators: [{
4062
+ type: Directive,
4063
+ args: [{
4064
+ selector: '[uiHighlight]',
4065
+ }]
4066
+ }], ctorParameters: () => [], propDecorators: { uiHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiHighlight", required: false }] }] } });
4067
+
4068
+ 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
4069
  const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
3346
4070
  const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
3347
4071
  /**
@@ -3355,7 +4079,7 @@ const IDLE_CLASSES = 'text-(--muted-foreground) active:bg-(--soft)';
3355
4079
  */
3356
4080
  class UiNavItem {
3357
4081
  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 */
4082
+ classes = computed(() => `${BASE_CLASSES$2} ${this.active() ? ACTIVE_CLASSES : IDLE_CLASSES}`, /* @ts-ignore */
3359
4083
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3360
4084
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
3361
4085
  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 +4108,12 @@ const SIZE_CLASSES$1 = {
3384
4108
  };
3385
4109
  const SIZE_GAPS = { md: 'gap-2.5', lg: 'gap-2.5', xl: 'gap-3', card: 'gap-2', dense: 'gap-3' };
3386
4110
  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)';
4111
+ 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)';
4112
+ 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)';
4113
+ const UNAVAILABLE_CLASSES = 'opacity-50 cursor-not-allowed';
3388
4114
  const SELECTED_CLASSES$1 = 'bg-(--soft)';
3389
4115
  /**
3390
- * Clickable row of a list: a holding, an envelope, an account.
4116
+ * Clickable row of a list: a holding, an account, a search result.
3391
4117
  *
3392
4118
  * @example
3393
4119
  * <a ui-row [selected]="holding.id === openId()" [routerLink]="['/holdings', holding.id]">
@@ -3396,28 +4122,59 @@ const SELECTED_CLASSES$1 = 'bg-(--soft)';
3396
4122
  */
3397
4123
  class UiRow {
3398
4124
  selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4125
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4126
+ unavailable = input(false, { ...(ngDevMode ? { debugName: "unavailable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3399
4127
  size = input('md', /* @ts-ignore */
3400
4128
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3401
4129
  padding = input('md', /* @ts-ignore */
3402
4130
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
3403
4131
  gap = input('default', /* @ts-ignore */
3404
4132
  ...(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 */
4133
+ 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
4134
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4135
+ constructor() {
4136
+ const host = inject(ElementRef).nativeElement;
4137
+ const guard = (event) => {
4138
+ if (this.busy() || this.unavailable()) {
4139
+ event.preventDefault();
4140
+ event.stopImmediatePropagation();
4141
+ }
4142
+ };
4143
+ host.addEventListener('click', guard, { capture: true });
4144
+ inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
4145
+ }
3407
4146
  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 });
4147
+ 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: `
4148
+ <ng-content />
4149
+ @if (busy()) {
4150
+ <span
4151
+ aria-hidden="true"
4152
+ class="rounded-pill animate-cairn-spin size-4 flex-none border-2 border-current border-r-transparent"
4153
+ ></span>
4154
+ }
4155
+ `, isInline: true });
3409
4156
  }
3410
4157
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
3411
4158
  type: Component,
3412
4159
  args: [{
3413
4160
  selector: 'a[ui-row], button[ui-row]',
3414
- template: '<ng-content />',
4161
+ template: `
4162
+ <ng-content />
4163
+ @if (busy()) {
4164
+ <span
4165
+ aria-hidden="true"
4166
+ class="rounded-pill animate-cairn-spin size-4 flex-none border-2 border-current border-r-transparent"
4167
+ ></span>
4168
+ }
4169
+ `,
3415
4170
  host: {
3416
4171
  '[class]': 'classes()',
4172
+ '[attr.aria-busy]': 'busy() || null',
4173
+ '[attr.aria-disabled]': "busy() || unavailable() ? 'true' : null",
3417
4174
  '[attr.aria-current]': "selected() ? 'true' : null",
3418
4175
  },
3419
4176
  }]
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 }] }] } });
4177
+ }], 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
4178
  /**
3422
4179
  * Lays out a `ui-row` with a trailing action beside it, such as a "more" icon button: the row takes
3423
4180
  * the room the action leaves and the action keeps its own size.
@@ -3496,7 +4253,7 @@ const nextId = (() => {
3496
4253
  let count = 0;
3497
4254
  return () => `ui-segmented-${++count}`;
3498
4255
  })();
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)";
4256
+ 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
4257
  const SEGMENTED_SIZES = ['md', 'sm'];
3501
4258
  const SIZE_CLASSES = {
3502
4259
  md: 'h-10 pointer-fine:h-8',
@@ -3731,11 +4488,13 @@ class UiSkeleton {
3731
4488
  }, /* @ts-ignore */
3732
4489
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
3733
4490
  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"] });
4491
+ 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
4492
  }
3736
4493
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
3737
4494
  type: Component,
3738
- args: [{ selector: 'ui-skeleton', template: `
4495
+ args: [{
4496
+ selector: 'ui-skeleton',
4497
+ template: `
3739
4498
  @switch (shape()) {
3740
4499
  @case ('figure') {
3741
4500
  <span class="${BAR_CLASSES} h-10 w-65" [style.animation]="animation"></span>
@@ -3774,12 +4533,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3774
4533
  }
3775
4534
  }
3776
4535
  }
3777
- `, host: {
4536
+ `,
4537
+ host: {
3778
4538
  'aria-hidden': 'true',
3779
4539
  class: 'flex flex-col gap-2',
3780
- }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
4540
+ },
4541
+ }]
3781
4542
  }], 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
4543
 
4544
+ const BASE_CLASSES = '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)';
4545
+ 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)]";
4546
+ /**
4547
+ * Link in the stale tone, for a caption about data that is late or left out of a total, such as
4548
+ * "1 ligne sans cours, non comptée". Caption size, weight 500, underlined on hover, with an optional
4549
+ * trailing chevron.
4550
+ *
4551
+ * @example
4552
+ * <a uiStaleLink chevron routerLink="/holdings">1 cours en retard</a>
4553
+ */
4554
+ class UiStaleLink {
4555
+ chevron = input(false, { ...(ngDevMode ? { debugName: "chevron" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4556
+ classes = computed(() => (this.chevron() ? `${BASE_CLASSES} ${CHEVRON_CLASSES}` : BASE_CLASSES), /* @ts-ignore */
4557
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
4558
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStaleLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4559
+ 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 });
4560
+ }
4561
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStaleLink, decorators: [{
4562
+ type: Directive,
4563
+ args: [{
4564
+ selector: 'a[uiStaleLink]',
4565
+ host: { '[class]': 'classes()' },
4566
+ }]
4567
+ }], propDecorators: { chevron: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevron", required: false }] }] } });
4568
+
3783
4569
  /** Available stat sizes. `StatSize` is derived from this tuple. */
3784
4570
  const STAT_SIZES = ['hero', 'tile'];
3785
4571
  const VALUE_CLASSES = {
@@ -3867,7 +4653,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3867
4653
  }]
3868
4654
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3869
4655
 
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";
4656
+ 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
4657
  /**
3872
4658
  * Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
3873
4659
  * 51 by 31 on touch, 40 by 24 with a mouse.
@@ -3891,7 +4677,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3891
4677
  }]
3892
4678
  }] });
3893
4679
 
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)';
4680
+ 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
4681
  const INACTIVE_TAB_CLASSES = 'font-medium text-(--muted-foreground)';
3896
4682
  const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
3897
4683
  /**
@@ -3947,6 +4733,150 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3947
4733
  }]
3948
4734
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
3949
4735
 
4736
+ /**
4737
+ * One-slot confirmation message queue read by `ui-toaster`.
4738
+ *
4739
+ * @example
4740
+ * inject(UiToasts).show('Achat enregistré');
4741
+ */
4742
+ class UiToasts {
4743
+ current = signal(null, /* @ts-ignore */
4744
+ ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
4745
+ sequence = 0;
4746
+ toast = this.current.asReadonly();
4747
+ show(message) {
4748
+ this.current.set({ id: ++this.sequence, text: message });
4749
+ }
4750
+ dismiss() {
4751
+ this.current.set(null);
4752
+ }
4753
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4754
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, providedIn: 'root' });
4755
+ }
4756
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToasts, decorators: [{
4757
+ type: Injectable,
4758
+ args: [{ providedIn: 'root' }]
4759
+ }] });
4760
+
4761
+ const DEFAULT_DURATION = 5000;
4762
+ 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';
4763
+ const readDuration = (host) => {
4764
+ const raw = getComputedStyle(host).getPropertyValue('--toast-duration').trim();
4765
+ const value = parseFloat(raw);
4766
+ if (Number.isNaN(value)) {
4767
+ return DEFAULT_DURATION;
4768
+ }
4769
+ return raw.endsWith('ms') ? value : value * 1000;
4770
+ };
4771
+ /**
4772
+ * Confirmation message shown by `UiToasts.show`: one at a time, no action, gone after `--toast-duration`.
4773
+ * Place it once in the app shell.
4774
+ *
4775
+ * @example
4776
+ * <ui-toaster />
4777
+ */
4778
+ class UiToaster {
4779
+ toasts = inject(UiToasts);
4780
+ toastClasses = TOAST_CLASSES;
4781
+ host = inject(ElementRef).nativeElement;
4782
+ hovered = signal(false, /* @ts-ignore */
4783
+ ...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
4784
+ focused = signal(false, /* @ts-ignore */
4785
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
4786
+ lastId = 0;
4787
+ remaining = DEFAULT_DURATION;
4788
+ startedAt = 0;
4789
+ timer;
4790
+ constructor() {
4791
+ effect(() => {
4792
+ const toast = this.toasts.toast();
4793
+ const paused = this.hovered() || this.focused();
4794
+ untracked(() => {
4795
+ if (!toast) {
4796
+ this.stop();
4797
+ this.hovered.set(false);
4798
+ this.focused.set(false);
4799
+ return;
4800
+ }
4801
+ this.host.querySelectorAll('.ui-leave-fade').forEach((leaving) => leaving.remove());
4802
+ if (paused) {
4803
+ this.pause();
4804
+ }
4805
+ else {
4806
+ this.resume(toast.id);
4807
+ }
4808
+ });
4809
+ });
4810
+ inject(DestroyRef).onDestroy(() => this.stop());
4811
+ }
4812
+ setHovered(value) {
4813
+ this.hovered.set(value);
4814
+ }
4815
+ setFocused(value) {
4816
+ this.focused.set(value);
4817
+ }
4818
+ resume(id) {
4819
+ if (id !== this.lastId) {
4820
+ this.lastId = id;
4821
+ this.remaining = readDuration(this.host);
4822
+ }
4823
+ clearTimeout(this.timer);
4824
+ this.startedAt = Date.now();
4825
+ this.timer = setTimeout(() => this.toasts.dismiss(), this.remaining);
4826
+ }
4827
+ pause() {
4828
+ if (this.timer !== undefined) {
4829
+ this.remaining -= Date.now() - this.startedAt;
4830
+ clearTimeout(this.timer);
4831
+ this.timer = undefined;
4832
+ }
4833
+ }
4834
+ stop() {
4835
+ clearTimeout(this.timer);
4836
+ this.timer = undefined;
4837
+ this.lastId = 0;
4838
+ }
4839
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToaster, deps: [], target: i0.ɵɵFactoryTarget.Component });
4840
+ 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(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: `
4841
+ @if (toasts.toast(); as toast) {
4842
+ <div
4843
+ animate.leave="ui-leave-fade"
4844
+ [class]="toastClasses"
4845
+ (focusin)="setFocused(true)"
4846
+ (focusout)="setFocused(false)"
4847
+ (mouseenter)="setHovered(true)"
4848
+ (mouseleave)="setHovered(false)"
4849
+ >
4850
+ {{ toast.text }}
4851
+ </div>
4852
+ }
4853
+ `, isInline: true });
4854
+ }
4855
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiToaster, decorators: [{
4856
+ type: Component,
4857
+ args: [{
4858
+ selector: 'ui-toaster',
4859
+ template: `
4860
+ @if (toasts.toast(); as toast) {
4861
+ <div
4862
+ animate.leave="ui-leave-fade"
4863
+ [class]="toastClasses"
4864
+ (focusin)="setFocused(true)"
4865
+ (focusout)="setFocused(false)"
4866
+ (mouseenter)="setHovered(true)"
4867
+ (mouseleave)="setHovered(false)"
4868
+ >
4869
+ {{ toast.text }}
4870
+ </div>
4871
+ }
4872
+ `,
4873
+ host: {
4874
+ role: 'status',
4875
+ class: 'pointer-events-none fixed inset-x-0 z-50 flex justify-center bottom-[calc(52px+env(safe-area-inset-bottom)+var(--action-bar-height,0px)+8px)] lg:inset-x-auto lg:right-6 lg:bottom-6',
4876
+ },
4877
+ }]
4878
+ }], ctorParameters: () => [] });
4879
+
3950
4880
  /** Breakpoints a secondary column can be held back until. */
3951
4881
  const CELL_BREAKPOINTS = ['md', 'lg'];
3952
4882
  /** Body row heights. */
@@ -4030,8 +4960,10 @@ class UiTr {
4030
4960
  return [
4031
4961
  'relative [&>td:first-child]:rounded-l-control [&>td:last-child]:rounded-r-control',
4032
4962
  this.selected() ? '[&>td]:bg-(--soft)' : '',
4033
- 'hover:[&>td]:bg-(--glow)',
4034
- this.interactive() ? 'active:[&>td]:bg-(--soft)' : '',
4963
+ 'hover:[&>td]:bg-(--glow) [&>td]:transition-colors [&>td]:duration-(--duration-fast) [&>td]:ease-out',
4964
+ this.interactive()
4965
+ ? 'active:[&>td]:bg-(--soft) transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale)'
4966
+ : '',
4035
4967
  ]
4036
4968
  .filter(Boolean)
4037
4969
  .join(' ');
@@ -4261,5 +5193,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
4261
5193
  * Generated bundle index. Do not edit.
4262
5194
  */
4263
5195
 
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 };
5196
+ 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, 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
5197
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map