@joanroucoux/cairn-ui 0.1.4 → 0.2.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,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Component, InjectionToken, output, inject, ElementRef, effect, booleanAttribute, contentChild, afterRenderEffect, forwardRef, Directive, model, viewChildren } from '@angular/core';
2
+ import { input, computed, Component, booleanAttribute, inject, ElementRef, DestroyRef, InjectionToken, output, effect, contentChild, afterRenderEffect, forwardRef, Directive, model, viewChildren } from '@angular/core';
3
3
 
4
4
  /** Available avatar sizes. `AvatarSize` is derived from this tuple. */
5
5
  const AVATAR_SIZES = ['sm', 'md'];
6
- const BASE_CLASSES$4 = 'inline-flex items-center justify-center rounded-full border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
6
+ const BASE_CLASSES$4 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
7
7
  const SIZE_CLASSES$1 = {
8
- sm: 'size-8 text-xs',
9
- md: 'size-11 text-sm',
8
+ sm: 'size-8 text-caption',
9
+ md: 'size-11 text-label',
10
10
  };
11
11
  /**
12
12
  * Initials in a disc, standing in for a person. Single-user application: there is exactly one.
@@ -23,10 +23,10 @@ class UiAvatar {
23
23
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
24
24
  classes = computed(() => `${BASE_CLASSES$4} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
25
25
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
26
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
27
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", 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 });
26
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
27
+ 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 });
28
28
  }
29
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiAvatar, decorators: [{
29
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, decorators: [{
30
30
  type: Component,
31
31
  args: [{
32
32
  selector: 'ui-avatar',
@@ -40,29 +40,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
40
40
  }], propDecorators: { initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
41
41
 
42
42
  /** Available badge variants. `BadgeVariant` is derived from this tuple. */
43
- const BADGE_VARIANTS = ['primary', 'secondary', 'destructive', 'outline'];
44
- const BASE_CLASSES$3 = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium';
43
+ const BADGE_VARIANTS = ['neutral', 'outline'];
44
+ const BASE_CLASSES$3 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
45
45
  const VARIANT_CLASSES$2 = {
46
- primary: 'bg-(--primary) text-(--primary-foreground)',
47
- secondary: 'bg-(--secondary) text-(--secondary-foreground)',
48
- destructive: 'bg-(--destructive) text-(--destructive-foreground)',
49
- outline: 'border border-(--border) text-(--foreground)',
46
+ neutral: 'bg-(--muted) text-(--foreground)',
47
+ outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
50
48
  };
51
49
  /**
52
- * Small status descriptor for counts, states or labels.
50
+ * Small status descriptor for counts, states or labels, such as an instrument's asset class.
53
51
  *
54
52
  * @example
55
- * <ui-badge variant="secondary">Draft</ui-badge>
53
+ * <ui-badge>ETF</ui-badge>
56
54
  */
57
55
  class UiBadge {
58
- variant = input('primary', /* @ts-ignore */
56
+ variant = input('neutral', /* @ts-ignore */
59
57
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
60
58
  classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
61
59
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
62
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
63
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: UiBadge, isStandalone: true, selector: "ui-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
60
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
61
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiBadge, isStandalone: true, selector: "ui-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
64
62
  }
65
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiBadge, decorators: [{
63
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, decorators: [{
66
64
  type: Component,
67
65
  args: [{
68
66
  selector: 'ui-badge',
@@ -74,63 +72,99 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
74
72
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
75
73
 
76
74
  /** Available button variants. `ButtonVariant` is derived from this tuple. */
77
- const BUTTON_VARIANTS = ['primary', 'secondary', 'outline', 'destructive', 'ghost'];
78
- /** Available button sizes. `md` is the 44px touch target Cairn asks for everywhere. */
79
- const BUTTON_SIZES = ['sm', 'md', 'lg'];
75
+ const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
76
+ /** Available button sizes. `md` is the touch target Cairn asks for everywhere. */
77
+ const BUTTON_SIZES = ['md', 'lg', 'xl', 'icon'];
80
78
  const BASE_CLASSES$2 =
81
- // Tailwind's preflight sets buttons to cursor: default (matching the native element) since v3.4:
82
- // cursor-pointer restores the hand cursor consumers expect from a clickable control.
83
- 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-50';
79
+ // Tailwind's preflight gives buttons cursor: default; the hand cursor is what users expect here.
80
+ 'relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
84
81
  const VARIANT_CLASSES$1 = {
85
- primary: 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground) hover:opacity-90',
86
- secondary: 'bg-(--secondary) text-(--secondary-foreground) hover:bg-(--secondary)/80',
87
- outline: 'border border-(--border) bg-transparent hover:bg-(--soft)',
88
- destructive: 'bg-(--destructive) text-(--destructive-foreground) hover:bg-(--destructive)/90',
89
- ghost: 'hover:bg-(--accent) hover:text-(--accent-foreground)',
82
+ primary: 'bg-(--primary) text-(--primary-foreground) hover:opacity-90',
83
+ outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)',
84
+ ghost: 'bg-transparent text-(--foreground) hover:bg-(--glow)',
85
+ destructive: 'bg-(--destructive) text-(--destructive-foreground) hover:opacity-90',
90
86
  };
91
87
  const SIZE_CLASSES = {
92
- sm: 'h-9 px-3 text-[13px]',
93
- md: 'h-11 px-4 text-sm',
94
- lg: 'h-12 px-6 text-base',
88
+ md: 'min-h-(--row-min) px-4 text-label',
89
+ lg: 'h-11 px-[18px] text-label',
90
+ xl: 'h-[50px] px-6 text-body',
91
+ icon: 'size-(--row-min) p-0',
95
92
  };
96
93
  /**
97
94
  * Styled native button. Applied as an attribute so the host keeps every native
98
95
  * button behavior (type, disabled, form submission, accessibility) for free.
99
96
  *
100
97
  * Also applies to an anchor, for the one case where the action really is a navigation:
101
- * a download the browser has to perform itself. The element stays a link, and so does
102
- * its role - never use this to make an ordinary link look like a form control.
98
+ * a download the browser has to perform itself.
99
+ *
100
+ * `size="icon"` has no visible text: give it an `aria-label`.
101
+ *
102
+ * A `loading` button shows a spinner before its label and swallows clicks, so a slow first
103
+ * response cannot turn into a double submission.
103
104
  *
104
105
  * @example
105
- * <button ui-button variant="destructive" size="sm">Delete</button>
106
- * <a ui-button variant="outline" href="/api/holdings/export" download>Export</a>
106
+ * <button ui-button variant="destructive">Delete</button>
107
+ * <button ui-button size="icon" variant="ghost" aria-label="More actions">...</button>
107
108
  */
108
109
  class UiButton {
109
110
  variant = input('primary', /* @ts-ignore */
110
111
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
111
112
  size = input('md', /* @ts-ignore */
112
113
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
113
- classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
114
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
115
+ classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
114
116
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
115
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
116
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: UiButton, isStandalone: true, selector: "button[ui-button], a[ui-button]", 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 });
117
+ constructor() {
118
+ // A host `(click)` binding runs after the template's own listener, so a loading button would
119
+ // still fire the consumer's handler once before this guard got a say. Capturing from the
120
+ // constructor puts this listener first regardless of binding order.
121
+ const host = inject(ElementRef).nativeElement;
122
+ const guard = (event) => {
123
+ if (this.loading()) {
124
+ event.preventDefault();
125
+ event.stopImmediatePropagation();
126
+ }
127
+ };
128
+ host.addEventListener('click', guard, { capture: true });
129
+ inject(DestroyRef).onDestroy(() => host.removeEventListener('click', guard, { capture: true }));
130
+ }
131
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
132
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiButton, isStandalone: true, selector: "button[ui-button], a[ui-button]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-busy": "loading() || null", "attr.aria-disabled": "loading() || null" } }, ngImport: i0, template: `
133
+ @if (loading()) {
134
+ <span
135
+ aria-hidden="true"
136
+ class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
137
+ ></span>
138
+ }
139
+ <ng-content />
140
+ `, isInline: true });
117
141
  }
118
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiButton, decorators: [{
142
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiButton, decorators: [{
119
143
  type: Component,
120
144
  args: [{
121
145
  selector: 'button[ui-button], a[ui-button]',
122
- template: '<ng-content />',
146
+ template: `
147
+ @if (loading()) {
148
+ <span
149
+ aria-hidden="true"
150
+ class="rounded-pill size-4 border-2 border-current border-r-transparent motion-safe:animate-spin"
151
+ ></span>
152
+ }
153
+ <ng-content />
154
+ `,
123
155
  host: {
124
156
  '[class]': 'classes()',
157
+ '[attr.aria-busy]': 'loading() || null',
158
+ '[attr.aria-disabled]': 'loading() || null',
125
159
  },
126
160
  }]
127
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
161
+ }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
128
162
 
129
163
  /** Available card variants. `CardVariant` is derived from this tuple. */
130
164
  const CARD_VARIANTS = ['default', 'elevated'];
131
165
  /** Available card paddings. `CardPadding` is derived from this tuple. */
132
166
  const CARD_PADDINGS = ['none', 'sm', 'md'];
133
- const BASE_CLASSES$1 = 'block rounded-xl border';
167
+ const BASE_CLASSES$1 = 'block rounded-container border';
134
168
  const VARIANT_CLASSES = {
135
169
  default: 'bg-(--card) border-(--border) text-(--card-foreground)',
136
170
  elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
@@ -153,10 +187,10 @@ class UiCard {
153
187
  ...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
154
188
  classes = computed(() => `${BASE_CLASSES$1} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
155
189
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
156
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
157
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: UiCard, isStandalone: true, selector: "ui-card", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
190
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
191
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiCard, isStandalone: true, selector: "ui-card", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
158
192
  }
159
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiCard, decorators: [{
193
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, decorators: [{
160
194
  type: Component,
161
195
  args: [{
162
196
  selector: 'ui-card',
@@ -179,14 +213,17 @@ const UI_CONTROL = new InjectionToken('UI_CONTROL');
179
213
 
180
214
  /** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
181
215
  const DELTA_EMPHASES = ['text', 'pill'];
182
- const BASE_CLASSES = 'inline-flex items-center gap-1 font-semibold tabular-nums';
183
- const PILL_CLASSES = 'rounded-md px-2.5 py-1';
216
+ const BASE_CLASSES = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
217
+ const PILL_CLASSES = 'h-6 rounded-pill px-2';
184
218
  const TEXT_CLASSES = {
185
219
  positive: 'text-(--positive)',
186
220
  negative: 'text-(--negative)',
187
221
  neutral: 'text-(--muted-foreground)',
188
222
  unknown: 'text-(--subtle-foreground)',
189
223
  };
224
+ // The handoff's `Cairn Composants.dc.html` uses --soft here, but --positive text on --soft only
225
+ // reaches 4.33:1 (axe flags it): --soft is a touch darker than --background, and --positive sits
226
+ // right at the edge of AA at 4.77:1 against --background alone. --muted keeps the chip legible.
190
227
  const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
191
228
  /**
192
229
  * Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
@@ -227,8 +264,8 @@ class UiDelta {
227
264
  return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
228
265
  }, /* @ts-ignore */
229
266
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
230
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
231
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UiDelta, isStandalone: true, selector: "ui-delta", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, unknownLabel: { classPropertyName: "unknownLabel", publicName: "unknownLabel", isSignal: true, isRequired: true, transformFunction: null }, emphasis: { classPropertyName: "emphasis", publicName: "emphasis", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDelta, isStandalone: true, selector: "ui-delta", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, unknownLabel: { classPropertyName: "unknownLabel", publicName: "unknownLabel", isSignal: true, isRequired: true, transformFunction: null }, emphasis: { classPropertyName: "emphasis", publicName: "emphasis", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
232
269
  @if (sign() === 'unknown') {
233
270
  <span aria-hidden="true">&mdash;</span>
234
271
  <span class="sr-only">{{ unknownLabel() }}</span>
@@ -237,7 +274,7 @@ class UiDelta {
237
274
  }
238
275
  `, isInline: true });
239
276
  }
240
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiDelta, decorators: [{
277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, decorators: [{
241
278
  type: Component,
242
279
  args: [{
243
280
  selector: 'ui-delta',
@@ -287,7 +324,7 @@ class UiDialog {
287
324
  dismissed = output();
288
325
  headingId = nextId$1();
289
326
  descriptionId = `${this.headingId}-description`;
290
- classes = computed(() => `w-full m-auto ${WIDTH_CLASSES[this.width()]} rounded-xl border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
327
+ classes = computed(() => `w-full m-auto ${WIDTH_CLASSES[this.width()]} rounded-container border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
291
328
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
292
329
  #host = inject(ElementRef);
293
330
  constructor() {
@@ -308,18 +345,18 @@ class UiDialog {
308
345
  this.dismissed.emit();
309
346
  }
310
347
  }
311
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UiDialog, isStandalone: true, selector: "ui-dialog", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
348
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
349
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDialog, isStandalone: true, selector: "ui-dialog", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
313
350
  <dialog
314
351
  [attr.aria-describedby]="description() ? descriptionId : null"
315
352
  [attr.aria-labelledby]="headingId"
316
353
  [class]="classes()"
317
354
  (close)="onNativeClose()"
318
355
  >
319
- <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
356
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
320
357
 
321
358
  @if (description()) {
322
- <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
359
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
323
360
  }
324
361
 
325
362
  <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
@@ -333,7 +370,7 @@ class UiDialog {
333
370
  </dialog>
334
371
  `, isInline: true });
335
372
  }
336
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiDialog, decorators: [{
373
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
337
374
  type: Component,
338
375
  args: [{
339
376
  selector: 'ui-dialog',
@@ -344,10 +381,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
344
381
  [class]="classes()"
345
382
  (close)="onNativeClose()"
346
383
  >
347
- <h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
384
+ <h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
348
385
 
349
386
  @if (description()) {
350
- <p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
387
+ <p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
351
388
  }
352
389
 
353
390
  <!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
@@ -397,13 +434,16 @@ class UiField {
397
434
  ...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
398
435
  error = input(/* @ts-ignore */
399
436
  ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
437
+ /** Unit shown inside the control's right edge, such as `EUR` or `parts`. Not part of the control's value. */
438
+ unit = input(/* @ts-ignore */
439
+ ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
400
440
  #id = nextId();
401
441
  hintId = `${this.#id}-hint`;
402
442
  errorId = `${this.#id}-error`;
403
443
  #host = inject(ElementRef);
404
444
  control = contentChild(UI_CONTROL, /* @ts-ignore */
405
445
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
406
- labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-[13px] font-medium text-(--foreground)', /* @ts-ignore */
446
+ labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label font-medium text-(--muted-foreground)', /* @ts-ignore */
407
447
  ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
408
448
  /**
409
449
  * The caller's own `error` wins: it is the only way to report something the control does not
@@ -445,25 +485,31 @@ class UiField {
445
485
  }
446
486
  });
447
487
  }
448
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
449
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }], ngImport: i0, template: `
488
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
489
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }], ngImport: i0, template: `
450
490
  <div class="group flex flex-col gap-1.5">
451
491
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
452
492
  <label [class]="labelClasses()">{{ label() }}</label>
453
493
 
454
- <ng-content />
494
+ <div class="relative flex items-center">
495
+ <ng-content />
496
+
497
+ @if (unit()) {
498
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
499
+ }
500
+ </div>
455
501
 
456
502
  @if (hint()) {
457
- <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
503
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
458
504
  }
459
505
 
460
506
  @if (message()) {
461
- <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
507
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
462
508
  }
463
509
  </div>
464
510
  `, isInline: true });
465
511
  }
466
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiField, decorators: [{
512
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, decorators: [{
467
513
  type: Component,
468
514
  args: [{
469
515
  selector: 'ui-field',
@@ -472,26 +518,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
472
518
  <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
473
519
  <label [class]="labelClasses()">{{ label() }}</label>
474
520
 
475
- <ng-content />
521
+ <div class="relative flex items-center">
522
+ <ng-content />
523
+
524
+ @if (unit()) {
525
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
526
+ }
527
+ </div>
476
528
 
477
529
  @if (hint()) {
478
- <span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
530
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
479
531
  }
480
532
 
481
533
  @if (message()) {
482
- <p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
534
+ <p class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">{{ message() }}</p>
483
535
  }
484
536
  </div>
485
537
  `,
486
538
  }]
487
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }] } });
539
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }] } });
488
540
 
489
- /** Available control sizes. `md` is the 44px touch target. */
541
+ /** Available control sizes. `md` is the 44/40px touch target. */
490
542
  const CONTROL_SIZES = ['sm', 'md'];
491
- const CONTROL_BASE_CLASSES = 'w-full rounded-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) enabled:hover:outline-2 enabled:hover:outline-offset-2 enabled:hover:outline-(--ring) enabled:group-hover:outline-2 enabled:group-hover:outline-offset-2 enabled:group-hover:outline-(--ring) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50';
543
+ const CONTROL_BASE_CLASSES = 'w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40';
492
544
  const CONTROL_SIZE_CLASSES = {
493
545
  sm: 'h-9',
494
- md: 'h-11',
546
+ md: 'min-h-(--row-min)',
495
547
  };
496
548
  /**
497
549
  * Styled native text input. A directive (not a wrapper component) so the host
@@ -513,10 +565,10 @@ class UiInput {
513
565
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
514
566
  classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
515
567
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
516
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
517
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
568
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
569
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
518
570
  }
519
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiInput, decorators: [{
571
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
520
572
  type: Directive,
521
573
  args: [{
522
574
  selector: 'input[uiInput]',
@@ -542,10 +594,10 @@ class UiTextarea {
542
594
  ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
543
595
  touched = input(false, /* @ts-ignore */
544
596
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
545
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
546
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", 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-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) enabled:hover:outline-2 enabled:hover:outline-offset-2 enabled:hover:outline-(--ring) enabled:group-hover:outline-2 enabled:group-hover:outline-offset-2 enabled:group-hover:outline-(--ring) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50 min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
597
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
598
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-control bg-(--background) px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] transition-shadow duration-(--duration-fast) ease-out placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-40 min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
547
599
  }
548
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTextarea, decorators: [{
600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
549
601
  type: Directive,
550
602
  args: [{
551
603
  selector: 'textarea[uiTextarea]',
@@ -584,12 +636,12 @@ class UiMeter {
584
636
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
585
637
  percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
586
638
  ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
587
- fillClasses = computed(() => `h-full rounded-sm ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
639
+ fillClasses = computed(() => `h-full rounded-control ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
588
640
  ...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
589
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
590
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
641
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
642
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiMeter, isStandalone: true, selector: "ui-meter", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueText: { classPropertyName: "valueText", publicName: "valueText", isSignal: true, isRequired: true, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
591
643
  <div
592
- class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
644
+ class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
593
645
  role="meter"
594
646
  [attr.aria-label]="label()"
595
647
  [attr.aria-valuemax]="1"
@@ -601,13 +653,13 @@ class UiMeter {
601
653
  </div>
602
654
  `, isInline: true });
603
655
  }
604
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiMeter, decorators: [{
656
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMeter, decorators: [{
605
657
  type: Component,
606
658
  args: [{
607
659
  selector: 'ui-meter',
608
660
  template: `
609
661
  <div
610
- class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
662
+ class="rounded-control h-2.5 w-full overflow-hidden bg-(--elevated)"
611
663
  role="meter"
612
664
  [attr.aria-label]="label()"
613
665
  [attr.aria-valuemax]="1"
@@ -621,11 +673,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
621
673
  }]
622
674
  }], 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 }] }] } });
623
675
 
624
- const OPTION_CLASSES = 'min-h-11 flex-1 cursor-pointer rounded-md px-3 text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)';
625
- const SELECTED_CLASSES = 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground)';
626
- const UNSELECTED_CLASSES = 'bg-(--elevated) text-(--muted-foreground) hover:text-(--foreground)';
676
+ const OPTION_CLASSES =
677
+ // The visible track stays the handoff's 36px; the hit area still reaches --row-min (44px touch,
678
+ // 40px pointer: fine) through an ::after enlarged only vertically, centered on the option, and
679
+ // constrained to its own horizontal bounds so it never reaches into a neighboring option.
680
+ "relative z-10 min-h-9 flex-1 cursor-pointer text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)";
681
+ const SELECTED_CLASSES = 'text-(--foreground)';
682
+ const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
627
683
  /**
628
- * Exclusive choice within a small, known set: a time range, a unit, a mode.
684
+ * Exclusive choice within a small, known set: a time range, a unit, a mode. The selected option's
685
+ * background is a single thumb that slides under it in `transform`, instead of being redrawn.
629
686
  *
630
687
  * @example
631
688
  * <ui-segmented [options]="ranges" label="Time range" [(value)]="range" />
@@ -639,6 +696,12 @@ class UiSegmented {
639
696
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
640
697
  radios = viewChildren('radio', /* @ts-ignore */
641
698
  ...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
699
+ // 1-based so 0 (falsy) reliably means "no match", letting @if skip the thumb entirely.
700
+ thumbIndex = computed(() => {
701
+ const index = this.options().findIndex((option) => option.value === this.value());
702
+ return index === -1 ? 0 : index + 1;
703
+ }, /* @ts-ignore */
704
+ ...(ngDevMode ? [{ debugName: "thumbIndex" }] : /* istanbul ignore next */ []));
642
705
  optionClasses(optionValue) {
643
706
  return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
644
707
  }
@@ -663,9 +726,24 @@ class UiSegmented {
663
726
  this.value.set(next.value);
664
727
  this.radios()[nextIndex]?.nativeElement.focus();
665
728
  }
666
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
667
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UiSegmented, isStandalone: true, selector: "ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
668
- <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
729
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
730
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSegmented, isStandalone: true, selector: "ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "radios", predicate: ["radio"], descendants: true, isSignal: true }], ngImport: i0, template: `
731
+ <div
732
+ class="rounded-control relative grid bg-(--muted) p-0.5"
733
+ role="radiogroup"
734
+ [attr.aria-label]="label()"
735
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
736
+ >
737
+ @if (thumbIndex(); as index) {
738
+ <span
739
+ aria-hidden="true"
740
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
741
+ data-thumb
742
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
743
+ [style.width]="'calc(100% / ' + options().length + ')'"
744
+ ></span>
745
+ }
746
+
669
747
  @for (option of options(); track option.value) {
670
748
  <button
671
749
  #radio
@@ -683,12 +761,27 @@ class UiSegmented {
683
761
  </div>
684
762
  `, isInline: true });
685
763
  }
686
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSegmented, decorators: [{
764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSegmented, decorators: [{
687
765
  type: Component,
688
766
  args: [{
689
767
  selector: 'ui-segmented',
690
768
  template: `
691
- <div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
769
+ <div
770
+ class="rounded-control relative grid bg-(--muted) p-0.5"
771
+ role="radiogroup"
772
+ [attr.aria-label]="label()"
773
+ [style.grid-template-columns]="'repeat(' + options().length + ', 1fr)'"
774
+ >
775
+ @if (thumbIndex(); as index) {
776
+ <span
777
+ aria-hidden="true"
778
+ class="absolute top-0.5 bottom-0.5 left-0.5 rounded-[calc(var(--radius-control)-2px)] bg-(--card) shadow-[0_1px_2px_rgb(0_0_0/0.08)] transition-transform duration-(--duration-fast) ease-out"
779
+ data-thumb
780
+ [style.transform]="'translateX(' + (index - 1) * 100 + '%)'"
781
+ [style.width]="'calc(100% / ' + options().length + ')'"
782
+ ></span>
783
+ }
784
+
692
785
  @for (option of options(); track option.value) {
693
786
  <button
694
787
  #radio
@@ -716,17 +809,17 @@ const PICKER_CLASSES = [
716
809
  'pointer-fine:supports-[appearance:base-select]:items-center',
717
810
  'pointer-fine:supports-[appearance:base-select]:pr-3',
718
811
  'pointer-fine:supports-[appearance:base-select]:[&_option]:cursor-pointer',
719
- 'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-md',
812
+ 'pointer-fine:supports-[appearance:base-select]:[&_option]:rounded-control',
720
813
  'pointer-fine:supports-[appearance:base-select]:[&_option]:px-2.5',
721
814
  'pointer-fine:supports-[appearance:base-select]:[&_option]:py-2',
722
815
  'pointer-fine:supports-[appearance:base-select]:[&_option:hover]:bg-(--soft)',
723
816
  'pointer-fine:supports-[appearance:base-select]:[&_option:focus]:bg-(--soft)',
724
817
  'pointer-fine:supports-[appearance:base-select]:[&_option:checked]:font-medium',
725
- '[&::picker-icon]:text-[11px]',
818
+ '[&::picker-icon]:text-caption',
726
819
  '[&::picker-icon]:text-(--muted-foreground)',
727
820
  '[&::picker(select)]:[appearance:base-select]',
728
821
  '[&::picker(select)]:my-1.5',
729
- '[&::picker(select)]:rounded-lg',
822
+ '[&::picker(select)]:rounded-control',
730
823
  '[&::picker(select)]:border',
731
824
  '[&::picker(select)]:border-(--border)',
732
825
  '[&::picker(select)]:bg-(--card)',
@@ -754,10 +847,10 @@ class UiSelect {
754
847
  ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
755
848
  classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${PICKER_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
756
849
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
757
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
758
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiSelect) }], ngImport: i0 });
850
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
851
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiSelect) }], ngImport: i0 });
759
852
  }
760
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSelect, decorators: [{
853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSelect, decorators: [{
761
854
  type: Directive,
762
855
  args: [{
763
856
  selector: 'select[uiSelect]',
@@ -768,55 +861,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
768
861
  }]
769
862
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
770
863
 
864
+ /** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
865
+ const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
771
866
  const LAST_LINE_WIDTH = '62%';
867
+ // Declared here, not in tokens.css (a pure token sheet holds no element styles or keyframes).
868
+ const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
869
+ const BAR_CLASSES = 'block rounded-control bg-(--muted)';
772
870
  /**
773
- * Decorative placeholder for content that is still loading. Deliberately hidden from
774
- * assistive technology: the calling screen keeps its own role="status" sentence.
871
+ * Decorative placeholder for content that is still loading, at the shape of the content it stands
872
+ * in for. Deliberately hidden from assistive technology: the calling screen keeps its own
873
+ * `role="status"` sentence.
874
+ *
875
+ * Pulses opacity from 1 to 0.45 and back over `--pulse-duration`, which `tokens.css` sets to `0ms`
876
+ * under `prefers-reduced-motion: reduce`: the pulse stops there with no branching in this component.
775
877
  *
776
878
  * @example
879
+ * <ui-skeleton shape="figure" />
777
880
  * <ui-skeleton [lines]="3" />
778
- * <ui-skeleton [height]="196" />
779
881
  */
780
882
  class UiSkeleton {
883
+ shape = input('text', /* @ts-ignore */
884
+ ...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
781
885
  lines = input(1, /* @ts-ignore */
782
886
  ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
783
887
  height = input(12, /* @ts-ignore */
784
888
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
889
+ animation = PULSE_ANIMATION;
785
890
  bars = computed(() => {
786
891
  const count = Math.max(1, this.lines());
787
892
  return Array.from({ length: count }, (_, index) => (index === count - 1 && count > 1 ? LAST_LINE_WIDTH : '100%'));
788
893
  }, /* @ts-ignore */
789
894
  ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
790
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
791
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: `
792
- @for (line of bars(); track $index) {
793
- <span
794
- class="block rounded bg-(--soft) motion-safe:animate-pulse"
795
- [style.height.px]="height()"
796
- [style.width]="line"
797
- ></span>
895
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
896
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiSkeleton, isStandalone: true, selector: "ui-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, classAttribute: "flex flex-col gap-2" }, ngImport: i0, template: "\n @switch (shape()) {\n @case ('figure') {\n <span class=\"block rounded-control bg-(--muted) h-11 w-56\" [style.animation]=\"animation\"></span>\n }\n @case ('row') {\n <span class=\"flex h-14 flex-col justify-center gap-2\">\n <span class=\"block rounded-control bg-(--muted) h-4 w-3/5\" [style.animation]=\"animation\"></span>\n <span class=\"block rounded-control bg-(--muted) h-3 w-2/5\" [style.animation]=\"animation\"></span>\n </span>\n }\n @case ('chart') {\n <span class=\"rounded-container h-48 w-full bg-(--muted)\" [style.animation]=\"animation\"></span>\n }\n @case ('ring') {\n <span\n class=\"rounded-pill size-40 border-[24px] border-(--muted) bg-transparent\"\n [style.animation]=\"animation\"\n ></span>\n }\n @default {\n @for (line of bars(); track $index) {\n <span\n class=\"block rounded-control bg-(--muted)\"\n [style.animation]=\"animation\"\n [style.height.px]=\"height()\"\n [style.width]=\"line\"\n ></span>\n }\n }\n }\n ", isInline: true, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] });
897
+ }
898
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSkeleton, decorators: [{
899
+ type: Component,
900
+ args: [{ selector: 'ui-skeleton', template: `
901
+ @switch (shape()) {
902
+ @case ('figure') {
903
+ <span class="${BAR_CLASSES} h-11 w-56" [style.animation]="animation"></span>
904
+ }
905
+ @case ('row') {
906
+ <span class="flex h-14 flex-col justify-center gap-2">
907
+ <span class="${BAR_CLASSES} h-4 w-3/5" [style.animation]="animation"></span>
908
+ <span class="${BAR_CLASSES} h-3 w-2/5" [style.animation]="animation"></span>
909
+ </span>
910
+ }
911
+ @case ('chart') {
912
+ <span class="rounded-container h-48 w-full bg-(--muted)" [style.animation]="animation"></span>
913
+ }
914
+ @case ('ring') {
915
+ <span
916
+ class="rounded-pill size-40 border-[24px] border-(--muted) bg-transparent"
917
+ [style.animation]="animation"
918
+ ></span>
919
+ }
920
+ @default {
921
+ @for (line of bars(); track $index) {
922
+ <span
923
+ class="${BAR_CLASSES}"
924
+ [style.animation]="animation"
925
+ [style.height.px]="height()"
926
+ [style.width]="line"
927
+ ></span>
928
+ }
929
+ }
798
930
  }
931
+ `, host: {
932
+ 'aria-hidden': 'true',
933
+ class: 'flex flex-col gap-2',
934
+ }, styles: ["@keyframes cairn-pulse{0%{opacity:1}to{opacity:.45}}\n"] }]
935
+ }], 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 }] }] } });
936
+
937
+ /** Available stat sizes. `StatSize` is derived from this tuple. */
938
+ const STAT_SIZES = ['hero', 'tile'];
939
+ const VALUE_CLASSES = {
940
+ hero: 'text-display font-semibold text-(--foreground)',
941
+ tile: 'text-title font-semibold text-(--foreground)',
942
+ };
943
+ const LABEL_CLASSES = 'text-label font-medium text-(--subtle-foreground)';
944
+ /**
945
+ * Label, figure and a named period of change, at two sizes: a `hero` for a screen's single
946
+ * headline number, or a smaller `tile` repeated in a grid. The figure itself, its delta and its
947
+ * caption are all projected, so the component carries no formatting and no currency of its own.
948
+ *
949
+ * The `hero` size never sets `tabular-nums` on the figure: a lone headline number reads better
950
+ * with its digits at their natural width, unlike a column of aligned figures.
951
+ *
952
+ * @example
953
+ * <ui-stat label="Net worth" size="hero">
954
+ * <span uiStatValue>128,656 EUR</span>
955
+ * <ui-delta uiStatDelta [value]="dayChange()" unknownLabel="Not available">+316.54 EUR</ui-delta>
956
+ * <span uiStatCaption>today</span>
957
+ * </ui-stat>
958
+ */
959
+ class UiStat {
960
+ label = input.required(/* @ts-ignore */
961
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
962
+ size = input('tile', /* @ts-ignore */
963
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
964
+ labelClasses = LABEL_CLASSES;
965
+ valueClasses = computed(() => VALUE_CLASSES[this.size()], /* @ts-ignore */
966
+ ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
967
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, deps: [], target: i0.ɵɵFactoryTarget.Component });
968
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiStat, isStandalone: true, selector: "ui-stat", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
969
+ <div class="flex items-start justify-between gap-4">
970
+ <div class="flex flex-col gap-1">
971
+ <dl>
972
+ <dt [class]="labelClasses">{{ label() }}</dt>
973
+ <dd [class]="valueClasses()">
974
+ <ng-content select="[uiStatValue]" />
975
+ </dd>
976
+ </dl>
977
+
978
+ <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
979
+ <ng-content select="[uiStatDelta]" />
980
+ <ng-content select="[uiStatCaption]" />
981
+ </div>
982
+ </div>
983
+
984
+ <ng-content select="[uiStatAside]" />
985
+ </div>
799
986
  `, isInline: true });
800
987
  }
801
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiSkeleton, decorators: [{
988
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiStat, decorators: [{
802
989
  type: Component,
803
990
  args: [{
804
- selector: 'ui-skeleton',
991
+ selector: 'ui-stat',
805
992
  template: `
806
- @for (line of bars(); track $index) {
807
- <span
808
- class="block rounded bg-(--soft) motion-safe:animate-pulse"
809
- [style.height.px]="height()"
810
- [style.width]="line"
811
- ></span>
812
- }
993
+ <div class="flex items-start justify-between gap-4">
994
+ <div class="flex flex-col gap-1">
995
+ <dl>
996
+ <dt [class]="labelClasses">{{ label() }}</dt>
997
+ <dd [class]="valueClasses()">
998
+ <ng-content select="[uiStatValue]" />
999
+ </dd>
1000
+ </dl>
1001
+
1002
+ <div class="text-label flex flex-wrap items-center gap-x-2 gap-y-1 text-(--muted-foreground)">
1003
+ <ng-content select="[uiStatDelta]" />
1004
+ <ng-content select="[uiStatCaption]" />
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <ng-content select="[uiStatAside]" />
1009
+ </div>
813
1010
  `,
814
- host: {
815
- 'aria-hidden': 'true',
816
- class: 'flex flex-col gap-2',
817
- },
818
1011
  }]
819
- }], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
1012
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
820
1013
 
821
1014
  /** Breakpoints a secondary column can be held back until. */
822
1015
  const CELL_BREAKPOINTS = ['md', 'lg'];
@@ -824,16 +1017,16 @@ const FROM_CLASSES = {
824
1017
  md: 'hidden md:table-cell',
825
1018
  lg: 'hidden lg:table-cell',
826
1019
  };
827
- const TH_CLASSES = 'px-3 py-2 align-bottom text-[11px] font-medium uppercase tracking-[0.05em] text-(--subtle-foreground)';
828
- const TD_CLASSES = 'border-t border-(--hairline) px-3 py-2.5 align-middle text-sm';
1020
+ const TH_CLASSES = 'h-9 px-2 align-bottom text-caption font-medium text-(--subtle-foreground) shadow-[inset_0_-1px_0_var(--hairline)]';
1021
+ const TD_CLASSES = 'h-12 px-2 align-middle text-body';
829
1022
  const NUMERIC_CLASSES = 'text-right tabular-nums';
830
1023
  const TEXTUAL_CLASSES = 'text-left';
831
1024
  /** Styled native table. The rows and cells stay plain <tr> and <td>. */
832
1025
  class UiTable {
833
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
834
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
1026
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1027
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
835
1028
  }
836
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTable, decorators: [{
1029
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTable, decorators: [{
837
1030
  type: Directive,
838
1031
  args: [{
839
1032
  selector: 'table[uiTable]',
@@ -842,6 +1035,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
842
1035
  },
843
1036
  }]
844
1037
  }] });
1038
+ /**
1039
+ * Body or group row. `group` draws the muted banner that opens each account inside a list of
1040
+ * holdings: it carries no hover state of its own, unlike an ordinary row.
1041
+ *
1042
+ * @example
1043
+ * <tr uiTr>...
1044
+ * <tr uiTr group>...
1045
+ */
1046
+ class UiTr {
1047
+ group = input(false, { ...(ngDevMode ? { debugName: "group" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1048
+ classes = computed(() => this.group() ? 'min-h-(--row-min) rounded-control bg-(--muted) font-semibold' : 'rounded-control hover:bg-(--glow)', /* @ts-ignore */
1049
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1050
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1051
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTr, isStandalone: true, selector: "tr[uiTr]", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1052
+ }
1053
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTr, decorators: [{
1054
+ type: Directive,
1055
+ args: [{
1056
+ selector: 'tr[uiTr]',
1057
+ host: {
1058
+ '[class]': 'classes()',
1059
+ },
1060
+ }]
1061
+ }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }] } });
845
1062
  const cellClasses = (base, numeric, from) => [base, numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES, from ? FROM_CLASSES[from] : ''].filter(Boolean).join(' ');
846
1063
  /**
847
1064
  * Column header. `numeric` right-aligns and lines the digits up; `from` holds a
@@ -856,10 +1073,10 @@ class UiTh {
856
1073
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
857
1074
  classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
858
1075
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
859
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
860
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: UiTh, isStandalone: true, selector: "th[uiTh]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1076
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1077
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTh, isStandalone: true, selector: "th[uiTh]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
861
1078
  }
862
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTh, decorators: [{
1079
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTh, decorators: [{
863
1080
  type: Directive,
864
1081
  args: [{
865
1082
  selector: 'th[uiTh]',
@@ -875,10 +1092,10 @@ class UiTd {
875
1092
  ...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
876
1093
  classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
877
1094
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
878
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
879
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: UiTd, isStandalone: true, selector: "td[uiTd]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1095
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1096
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTd, isStandalone: true, selector: "td[uiTd]", inputs: { numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, from: { classPropertyName: "from", publicName: "from", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
880
1097
  }
881
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UiTd, decorators: [{
1098
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTd, decorators: [{
882
1099
  type: Directive,
883
1100
  args: [{
884
1101
  selector: 'td[uiTd]',
@@ -896,5 +1113,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
896
1113
  * Generated bundle index. Do not edit.
897
1114
  */
898
1115
 
899
- export { AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMeter, UiSegmented, UiSelect, UiSkeleton, UiTable, UiTd, UiTextarea, UiTh };
1116
+ export { AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMeter, UiSegmented, UiSelect, UiSkeleton, UiStat, UiTable, UiTd, UiTextarea, UiTh, UiTr };
900
1117
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map