@joanroucoux/cairn-ui 0.1.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.
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, computed, Component, output, inject, ElementRef, effect, afterRenderEffect, Directive, model, viewChildren, booleanAttribute } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/** Available avatar sizes. `AvatarSize` is derived from this tuple. */
|
|
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)';
|
|
7
|
+
const SIZE_CLASSES$1 = {
|
|
8
|
+
sm: 'size-8 text-xs',
|
|
9
|
+
md: 'size-11 text-sm',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Initials in a disc, standing in for a person. Single-user application: there is exactly one.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <ui-avatar initials="JR" label="My account" />
|
|
16
|
+
*/
|
|
17
|
+
class UiAvatar {
|
|
18
|
+
initials = input.required(/* @ts-ignore */
|
|
19
|
+
...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
|
|
20
|
+
label = input.required(/* @ts-ignore */
|
|
21
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
22
|
+
size = input('md', /* @ts-ignore */
|
|
23
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
24
|
+
classes = computed(() => `${BASE_CLASSES$4} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
|
|
25
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
26
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
27
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", 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
|
+
}
|
|
29
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiAvatar, decorators: [{
|
|
30
|
+
type: Component,
|
|
31
|
+
args: [{
|
|
32
|
+
selector: 'ui-avatar',
|
|
33
|
+
template: '<span aria-hidden="true">{{ initials() }}</span>',
|
|
34
|
+
host: {
|
|
35
|
+
role: 'img',
|
|
36
|
+
'[attr.aria-label]': 'label()',
|
|
37
|
+
'[class]': 'classes()',
|
|
38
|
+
},
|
|
39
|
+
}]
|
|
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
|
+
|
|
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';
|
|
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)',
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Small status descriptor for counts, states or labels.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <ui-badge variant="secondary">Draft</ui-badge>
|
|
56
|
+
*/
|
|
57
|
+
class UiBadge {
|
|
58
|
+
variant = input('primary', /* @ts-ignore */
|
|
59
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
60
|
+
classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
|
|
61
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
62
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
63
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", 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
|
+
}
|
|
65
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiBadge, decorators: [{
|
|
66
|
+
type: Component,
|
|
67
|
+
args: [{
|
|
68
|
+
selector: 'ui-badge',
|
|
69
|
+
template: '<ng-content />',
|
|
70
|
+
host: {
|
|
71
|
+
'[class]': 'classes()',
|
|
72
|
+
},
|
|
73
|
+
}]
|
|
74
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
|
|
75
|
+
|
|
76
|
+
/** 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'];
|
|
80
|
+
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';
|
|
84
|
+
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)',
|
|
90
|
+
};
|
|
91
|
+
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',
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Styled native button. Applied as an attribute so the host keeps every native
|
|
98
|
+
* button behavior (type, disabled, form submission, accessibility) for free.
|
|
99
|
+
*
|
|
100
|
+
* 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.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* <button ui-button variant="destructive" size="sm">Delete</button>
|
|
106
|
+
* <a ui-button variant="outline" href="/api/holdings/export" download>Export</a>
|
|
107
|
+
*/
|
|
108
|
+
class UiButton {
|
|
109
|
+
variant = input('primary', /* @ts-ignore */
|
|
110
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
111
|
+
size = input('md', /* @ts-ignore */
|
|
112
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
113
|
+
classes = computed(() => `${BASE_CLASSES$2} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
|
|
114
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
115
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
116
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", 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
|
+
}
|
|
118
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiButton, decorators: [{
|
|
119
|
+
type: Component,
|
|
120
|
+
args: [{
|
|
121
|
+
selector: 'button[ui-button], a[ui-button]',
|
|
122
|
+
template: '<ng-content />',
|
|
123
|
+
host: {
|
|
124
|
+
'[class]': 'classes()',
|
|
125
|
+
},
|
|
126
|
+
}]
|
|
127
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
128
|
+
|
|
129
|
+
/** Available card variants. `CardVariant` is derived from this tuple. */
|
|
130
|
+
const CARD_VARIANTS = ['default', 'elevated'];
|
|
131
|
+
/** Available card paddings. `CardPadding` is derived from this tuple. */
|
|
132
|
+
const CARD_PADDINGS = ['none', 'sm', 'md'];
|
|
133
|
+
const BASE_CLASSES$1 = 'block rounded-xl border';
|
|
134
|
+
const VARIANT_CLASSES = {
|
|
135
|
+
default: 'bg-(--card) border-(--border) text-(--card-foreground)',
|
|
136
|
+
elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
|
|
137
|
+
};
|
|
138
|
+
const PADDING_CLASSES = {
|
|
139
|
+
none: 'p-0',
|
|
140
|
+
sm: 'p-3',
|
|
141
|
+
md: 'p-5',
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Surface that groups related content: a summary block, a table, a side panel.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* <ui-card variant="elevated" padding="sm">Total</ui-card>
|
|
148
|
+
*/
|
|
149
|
+
class UiCard {
|
|
150
|
+
variant = input('default', /* @ts-ignore */
|
|
151
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
152
|
+
padding = input('md', /* @ts-ignore */
|
|
153
|
+
...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
|
|
154
|
+
classes = computed(() => `${BASE_CLASSES$1} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
|
|
155
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
156
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
157
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", 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
|
+
}
|
|
159
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiCard, decorators: [{
|
|
160
|
+
type: Component,
|
|
161
|
+
args: [{
|
|
162
|
+
selector: 'ui-card',
|
|
163
|
+
template: '<ng-content />',
|
|
164
|
+
host: {
|
|
165
|
+
'[class]': 'classes()',
|
|
166
|
+
},
|
|
167
|
+
}]
|
|
168
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }] } });
|
|
169
|
+
|
|
170
|
+
/** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
|
|
171
|
+
const DELTA_EMPHASES = ['text', 'pill'];
|
|
172
|
+
const BASE_CLASSES = 'inline-flex items-center gap-1 font-semibold tabular-nums';
|
|
173
|
+
const PILL_CLASSES = 'rounded-md px-2.5 py-1';
|
|
174
|
+
const TEXT_CLASSES = {
|
|
175
|
+
positive: 'text-(--positive)',
|
|
176
|
+
negative: 'text-(--negative)',
|
|
177
|
+
neutral: 'text-(--muted-foreground)',
|
|
178
|
+
unknown: 'text-(--subtle-foreground)',
|
|
179
|
+
};
|
|
180
|
+
const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
|
|
181
|
+
/**
|
|
182
|
+
* Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
|
|
183
|
+
*
|
|
184
|
+
* A null value is not a zero: a holding with no cost basis exists, and showing 0 would falsely
|
|
185
|
+
* claim a known gain of zero. Color never carries the sign on its own: the projected text must
|
|
186
|
+
* already contain a + or a minus sign.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* <ui-delta [value]="holding.dayChangeEur" unknownLabel="Not available">
|
|
190
|
+
* {{ holding.dayChangeEur | signedMoney }}
|
|
191
|
+
* </ui-delta>
|
|
192
|
+
*/
|
|
193
|
+
class UiDelta {
|
|
194
|
+
value = input.required(/* @ts-ignore */
|
|
195
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
196
|
+
unknownLabel = input.required(/* @ts-ignore */
|
|
197
|
+
...(ngDevMode ? [{ debugName: "unknownLabel" }] : /* istanbul ignore next */ []));
|
|
198
|
+
emphasis = input('text', /* @ts-ignore */
|
|
199
|
+
...(ngDevMode ? [{ debugName: "emphasis" }] : /* istanbul ignore next */ []));
|
|
200
|
+
sign = computed(() => {
|
|
201
|
+
const value = this.value();
|
|
202
|
+
if (value === null || value === undefined) {
|
|
203
|
+
return 'unknown';
|
|
204
|
+
}
|
|
205
|
+
if (value > 0) {
|
|
206
|
+
return 'positive';
|
|
207
|
+
}
|
|
208
|
+
if (value < 0) {
|
|
209
|
+
return 'negative';
|
|
210
|
+
}
|
|
211
|
+
return 'neutral';
|
|
212
|
+
}, /* @ts-ignore */
|
|
213
|
+
...(ngDevMode ? [{ debugName: "sign" }] : /* istanbul ignore next */ []));
|
|
214
|
+
classes = computed(() => {
|
|
215
|
+
const sign = this.sign();
|
|
216
|
+
const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
|
|
217
|
+
return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
|
|
218
|
+
}, /* @ts-ignore */
|
|
219
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
220
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
221
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", 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: `
|
|
222
|
+
@if (sign() === 'unknown') {
|
|
223
|
+
<span aria-hidden="true">—</span>
|
|
224
|
+
<span class="sr-only">{{ unknownLabel() }}</span>
|
|
225
|
+
} @else {
|
|
226
|
+
<ng-content />
|
|
227
|
+
}
|
|
228
|
+
`, isInline: true });
|
|
229
|
+
}
|
|
230
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDelta, decorators: [{
|
|
231
|
+
type: Component,
|
|
232
|
+
args: [{
|
|
233
|
+
selector: 'ui-delta',
|
|
234
|
+
template: `
|
|
235
|
+
@if (sign() === 'unknown') {
|
|
236
|
+
<span aria-hidden="true">—</span>
|
|
237
|
+
<span class="sr-only">{{ unknownLabel() }}</span>
|
|
238
|
+
} @else {
|
|
239
|
+
<ng-content />
|
|
240
|
+
}
|
|
241
|
+
`,
|
|
242
|
+
host: {
|
|
243
|
+
'[class]': 'classes()',
|
|
244
|
+
},
|
|
245
|
+
}]
|
|
246
|
+
}], 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 }] }] } });
|
|
247
|
+
|
|
248
|
+
/** Available dialog widths. `DialogWidth` is derived from this tuple. */
|
|
249
|
+
const DIALOG_WIDTHS = ['md', 'lg'];
|
|
250
|
+
const WIDTH_CLASSES = {
|
|
251
|
+
md: 'max-w-md',
|
|
252
|
+
lg: 'max-w-lg',
|
|
253
|
+
};
|
|
254
|
+
const nextId$1 = (() => {
|
|
255
|
+
let count = 0;
|
|
256
|
+
return () => `ui-dialog-${++count}`;
|
|
257
|
+
})();
|
|
258
|
+
/**
|
|
259
|
+
* Modal dialog on the native <dialog>: focus trap, focus return, Escape and the top layer
|
|
260
|
+
* come from the platform. Two projection slots - the body, and [dialogActions] for the footer.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
|
|
264
|
+
* <p>This cannot be undone.</p>
|
|
265
|
+
* <button dialogActions ui-button variant="outline" (click)="confirming.set(false)">Cancel</button>
|
|
266
|
+
* </ui-dialog>
|
|
267
|
+
*/
|
|
268
|
+
class UiDialog {
|
|
269
|
+
heading = input.required(/* @ts-ignore */
|
|
270
|
+
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
271
|
+
description = input(/* @ts-ignore */
|
|
272
|
+
...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
|
|
273
|
+
width = input('lg', /* @ts-ignore */
|
|
274
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
275
|
+
open = input(false, /* @ts-ignore */
|
|
276
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
277
|
+
dismissed = output();
|
|
278
|
+
headingId = nextId$1();
|
|
279
|
+
descriptionId = `${this.headingId}-description`;
|
|
280
|
+
classes = computed(() => `w-full ${WIDTH_CLASSES[this.width()]} rounded-xl border border-(--border) bg-(--card) p-6 text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
|
|
281
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
282
|
+
#host = inject(ElementRef);
|
|
283
|
+
constructor() {
|
|
284
|
+
effect(() => {
|
|
285
|
+
// viewChild.required can't target a #private field (NG1053); safe because the template has exactly one <dialog>.
|
|
286
|
+
const dialog = this.#host.nativeElement.querySelector('dialog');
|
|
287
|
+
if (this.open() && !dialog.open) {
|
|
288
|
+
dialog.showModal();
|
|
289
|
+
}
|
|
290
|
+
else if (!this.open() && dialog.open) {
|
|
291
|
+
dialog.close();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
onNativeClose() {
|
|
296
|
+
// The owner already knows about a close it asked for; only a close it did not ask for is news.
|
|
297
|
+
if (this.open()) {
|
|
298
|
+
this.dismissed.emit();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
302
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", 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: `
|
|
303
|
+
<dialog
|
|
304
|
+
[attr.aria-describedby]="description() ? descriptionId : null"
|
|
305
|
+
[attr.aria-labelledby]="headingId"
|
|
306
|
+
[class]="classes()"
|
|
307
|
+
(close)="onNativeClose()"
|
|
308
|
+
>
|
|
309
|
+
<h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
|
|
310
|
+
|
|
311
|
+
@if (description()) {
|
|
312
|
+
<p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
<!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
|
|
316
|
+
<div class="mt-4 flex flex-col gap-4 empty:hidden">
|
|
317
|
+
<ng-content />
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div class="mt-6 flex justify-end gap-3">
|
|
321
|
+
<ng-content select="[dialogActions]" />
|
|
322
|
+
</div>
|
|
323
|
+
</dialog>
|
|
324
|
+
`, isInline: true });
|
|
325
|
+
}
|
|
326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiDialog, decorators: [{
|
|
327
|
+
type: Component,
|
|
328
|
+
args: [{
|
|
329
|
+
selector: 'ui-dialog',
|
|
330
|
+
template: `
|
|
331
|
+
<dialog
|
|
332
|
+
[attr.aria-describedby]="description() ? descriptionId : null"
|
|
333
|
+
[attr.aria-labelledby]="headingId"
|
|
334
|
+
[class]="classes()"
|
|
335
|
+
(close)="onNativeClose()"
|
|
336
|
+
>
|
|
337
|
+
<h2 class="text-lg font-semibold tracking-tight" [id]="headingId">{{ heading() }}</h2>
|
|
338
|
+
|
|
339
|
+
@if (description()) {
|
|
340
|
+
<p class="mt-2 text-sm text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
<!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
|
|
344
|
+
<div class="mt-4 flex flex-col gap-4 empty:hidden">
|
|
345
|
+
<ng-content />
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div class="mt-6 flex justify-end gap-3">
|
|
349
|
+
<ng-content select="[dialogActions]" />
|
|
350
|
+
</div>
|
|
351
|
+
</dialog>
|
|
352
|
+
`,
|
|
353
|
+
}]
|
|
354
|
+
}], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
|
|
355
|
+
|
|
356
|
+
const nextId = (() => {
|
|
357
|
+
let count = 0;
|
|
358
|
+
return () => `ui-field-${++count}`;
|
|
359
|
+
})();
|
|
360
|
+
/**
|
|
361
|
+
* Label, control, hint and error as one unit. The control stays a plain native element:
|
|
362
|
+
* the field finds it in its own projected content and wires the ARIA attributes onto it.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* <ui-field label="Quantity" hint="Leave empty if unknown">
|
|
366
|
+
* <input uiInput type="number" />
|
|
367
|
+
* </ui-field>
|
|
368
|
+
*/
|
|
369
|
+
class UiField {
|
|
370
|
+
label = input.required(/* @ts-ignore */
|
|
371
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
372
|
+
hint = input(/* @ts-ignore */
|
|
373
|
+
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
374
|
+
error = input(/* @ts-ignore */
|
|
375
|
+
...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
|
|
376
|
+
#id = nextId();
|
|
377
|
+
hintId = `${this.#id}-hint`;
|
|
378
|
+
errorId = `${this.#id}-error`;
|
|
379
|
+
#host = inject(ElementRef);
|
|
380
|
+
constructor() {
|
|
381
|
+
afterRenderEffect(() => {
|
|
382
|
+
// querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
|
|
383
|
+
const control = this.#host.nativeElement.querySelector('input, select, textarea');
|
|
384
|
+
if (!control) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
control.id ||= this.#id;
|
|
388
|
+
this.#host.nativeElement.querySelector('label').htmlFor = control.id;
|
|
389
|
+
const describedBy = [this.hint() ? this.hintId : null, this.error() ? this.errorId : null].filter(Boolean);
|
|
390
|
+
if (describedBy.length > 0) {
|
|
391
|
+
control.setAttribute('aria-describedby', describedBy.join(' '));
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
control.removeAttribute('aria-describedby');
|
|
395
|
+
}
|
|
396
|
+
if (this.error()) {
|
|
397
|
+
control.setAttribute('aria-invalid', 'true');
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
control.removeAttribute('aria-invalid');
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
405
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
406
|
+
<div class="flex flex-col gap-1.5">
|
|
407
|
+
<!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
|
|
408
|
+
<label class="text-[13px] font-medium text-(--foreground)">{{ label() }}</label>
|
|
409
|
+
|
|
410
|
+
<ng-content />
|
|
411
|
+
|
|
412
|
+
@if (hint()) {
|
|
413
|
+
<span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@if (error()) {
|
|
417
|
+
<p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ error() }}</p>
|
|
418
|
+
}
|
|
419
|
+
</div>
|
|
420
|
+
`, isInline: true });
|
|
421
|
+
}
|
|
422
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiField, decorators: [{
|
|
423
|
+
type: Component,
|
|
424
|
+
args: [{
|
|
425
|
+
selector: 'ui-field',
|
|
426
|
+
template: `
|
|
427
|
+
<div class="flex flex-col gap-1.5">
|
|
428
|
+
<!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
|
|
429
|
+
<label class="text-[13px] font-medium text-(--foreground)">{{ label() }}</label>
|
|
430
|
+
|
|
431
|
+
<ng-content />
|
|
432
|
+
|
|
433
|
+
@if (hint()) {
|
|
434
|
+
<span class="text-[11.5px] text-(--subtle-foreground)" [id]="hintId">{{ hint() }}</span>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@if (error()) {
|
|
438
|
+
<p class="text-[11.5px] text-(--negative)" role="alert" [id]="errorId">{{ error() }}</p>
|
|
439
|
+
}
|
|
440
|
+
</div>
|
|
441
|
+
`,
|
|
442
|
+
}]
|
|
443
|
+
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
|
|
444
|
+
|
|
445
|
+
/** Available control sizes. `md` is the 44px touch target. */
|
|
446
|
+
const CONTROL_SIZES = ['sm', 'md'];
|
|
447
|
+
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) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:cursor-not-allowed disabled:opacity-50';
|
|
448
|
+
const CONTROL_SIZE_CLASSES = {
|
|
449
|
+
sm: 'h-9',
|
|
450
|
+
md: 'h-11',
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Styled native text input. A directive (not a wrapper component) so the host
|
|
454
|
+
* stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
|
|
455
|
+
* and every native attribute keep working without any plumbing.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* <input uiInput type="email" placeholder="you@example.com" />
|
|
459
|
+
*/
|
|
460
|
+
class UiInput {
|
|
461
|
+
size = input('md', /* @ts-ignore */
|
|
462
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
463
|
+
classes = computed(() => `${CONTROL_BASE_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
|
|
464
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
466
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
467
|
+
}
|
|
468
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiInput, decorators: [{
|
|
469
|
+
type: Directive,
|
|
470
|
+
args: [{
|
|
471
|
+
selector: 'input[uiInput]',
|
|
472
|
+
host: {
|
|
473
|
+
'[class]': 'classes()',
|
|
474
|
+
},
|
|
475
|
+
}]
|
|
476
|
+
}], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
477
|
+
/**
|
|
478
|
+
* Styled native textarea. Lives beside the input because it shares every one of its
|
|
479
|
+
* classes; it takes no size, since a textarea grows from a floor instead of sitting
|
|
480
|
+
* at a fixed height.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* <textarea uiTextarea maxlength="280"></textarea>
|
|
484
|
+
*/
|
|
485
|
+
class UiTextarea {
|
|
486
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
487
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", host: { classAttribute: "w-full rounded-md border border-(--border) bg-transparent px-3 text-sm text-(--foreground) transition-colors placeholder:text-(--muted-foreground) 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" }, ngImport: i0 });
|
|
488
|
+
}
|
|
489
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTextarea, decorators: [{
|
|
490
|
+
type: Directive,
|
|
491
|
+
args: [{
|
|
492
|
+
selector: 'textarea[uiTextarea]',
|
|
493
|
+
host: {
|
|
494
|
+
class: `${CONTROL_BASE_CLASSES} min-h-24 py-2`,
|
|
495
|
+
},
|
|
496
|
+
}]
|
|
497
|
+
}] });
|
|
498
|
+
|
|
499
|
+
/** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
|
|
500
|
+
const METER_TONES = [1, 2, 3, 4, 5, 6];
|
|
501
|
+
const TONE_CLASSES = {
|
|
502
|
+
1: 'bg-(--ramp-1)',
|
|
503
|
+
2: 'bg-(--ramp-2)',
|
|
504
|
+
3: 'bg-(--ramp-3)',
|
|
505
|
+
4: 'bg-(--ramp-4)',
|
|
506
|
+
5: 'bg-(--ramp-5)',
|
|
507
|
+
6: 'bg-(--ramp-6)',
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* Proportion of a whole, drawn as a single bar. One bar per category, never a stacked
|
|
511
|
+
* strip: without hue to separate segments, length has to do the work alone.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* <ui-meter [value]="0.463" label="Funds" valueText="46.3% - 128,656.00 EUR" [tone]="1" />
|
|
515
|
+
*/
|
|
516
|
+
class UiMeter {
|
|
517
|
+
value = input.required(/* @ts-ignore */
|
|
518
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
519
|
+
label = input.required(/* @ts-ignore */
|
|
520
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
521
|
+
valueText = input.required(/* @ts-ignore */
|
|
522
|
+
...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
|
|
523
|
+
tone = input(1, /* @ts-ignore */
|
|
524
|
+
...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
525
|
+
percent = computed(() => Math.min(100, Math.max(0, this.value() * 100)), /* @ts-ignore */
|
|
526
|
+
...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
|
|
527
|
+
fillClasses = computed(() => `h-full rounded-sm ${TONE_CLASSES[this.tone()]}`, /* @ts-ignore */
|
|
528
|
+
...(ngDevMode ? [{ debugName: "fillClasses" }] : /* istanbul ignore next */ []));
|
|
529
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiMeter, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
530
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", 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: `
|
|
531
|
+
<div
|
|
532
|
+
class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
|
|
533
|
+
role="meter"
|
|
534
|
+
[attr.aria-label]="label()"
|
|
535
|
+
[attr.aria-valuemax]="1"
|
|
536
|
+
[attr.aria-valuemin]="0"
|
|
537
|
+
[attr.aria-valuenow]="value()"
|
|
538
|
+
[attr.aria-valuetext]="valueText()"
|
|
539
|
+
>
|
|
540
|
+
<div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
|
|
541
|
+
</div>
|
|
542
|
+
`, isInline: true });
|
|
543
|
+
}
|
|
544
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiMeter, decorators: [{
|
|
545
|
+
type: Component,
|
|
546
|
+
args: [{
|
|
547
|
+
selector: 'ui-meter',
|
|
548
|
+
template: `
|
|
549
|
+
<div
|
|
550
|
+
class="h-2.5 w-full overflow-hidden rounded-sm bg-(--elevated)"
|
|
551
|
+
role="meter"
|
|
552
|
+
[attr.aria-label]="label()"
|
|
553
|
+
[attr.aria-valuemax]="1"
|
|
554
|
+
[attr.aria-valuemin]="0"
|
|
555
|
+
[attr.aria-valuenow]="value()"
|
|
556
|
+
[attr.aria-valuetext]="valueText()"
|
|
557
|
+
>
|
|
558
|
+
<div data-testid="meter-fill" [class]="fillClasses()" [style.width.%]="percent()"></div>
|
|
559
|
+
</div>
|
|
560
|
+
`,
|
|
561
|
+
}]
|
|
562
|
+
}], 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 }] }] } });
|
|
563
|
+
|
|
564
|
+
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)';
|
|
565
|
+
const SELECTED_CLASSES = 'bg-(--primary) bg-linear-135 from-(--primary) to-(--primary-to) text-(--primary-foreground)';
|
|
566
|
+
const UNSELECTED_CLASSES = 'bg-(--elevated) text-(--muted-foreground) hover:text-(--foreground)';
|
|
567
|
+
/**
|
|
568
|
+
* Exclusive choice within a small, known set: a time range, a unit, a mode.
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* <ui-segmented [options]="ranges" label="Time range" [(value)]="range" />
|
|
572
|
+
*/
|
|
573
|
+
class UiSegmented {
|
|
574
|
+
options = input.required(/* @ts-ignore */
|
|
575
|
+
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
576
|
+
label = input.required(/* @ts-ignore */
|
|
577
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
578
|
+
value = model.required(/* @ts-ignore */
|
|
579
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
580
|
+
radios = viewChildren('radio', /* @ts-ignore */
|
|
581
|
+
...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
|
|
582
|
+
optionClasses(optionValue) {
|
|
583
|
+
return `${OPTION_CLASSES} ${optionValue === this.value() ? SELECTED_CLASSES : UNSELECTED_CLASSES}`;
|
|
584
|
+
}
|
|
585
|
+
tabIndexFor(optionValue) {
|
|
586
|
+
const opts = this.options();
|
|
587
|
+
const tabStopValue = opts.some((option) => option.value === this.value()) ? this.value() : opts[0]?.value;
|
|
588
|
+
return optionValue === tabStopValue ? 0 : -1;
|
|
589
|
+
}
|
|
590
|
+
select(optionValue) {
|
|
591
|
+
this.value.set(optionValue);
|
|
592
|
+
}
|
|
593
|
+
onKeydown(event) {
|
|
594
|
+
const delta = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];
|
|
595
|
+
if (delta === undefined) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
const opts = this.options();
|
|
600
|
+
const currentIndex = opts.findIndex((option) => option.value === this.value());
|
|
601
|
+
const nextIndex = (currentIndex + delta + opts.length) % opts.length;
|
|
602
|
+
const next = opts[nextIndex];
|
|
603
|
+
this.value.set(next.value);
|
|
604
|
+
this.radios()[nextIndex]?.nativeElement.focus();
|
|
605
|
+
}
|
|
606
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
607
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", 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: `
|
|
608
|
+
<div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
|
|
609
|
+
@for (option of options(); track option.value) {
|
|
610
|
+
<button
|
|
611
|
+
#radio
|
|
612
|
+
role="radio"
|
|
613
|
+
type="button"
|
|
614
|
+
[attr.aria-checked]="option.value === value()"
|
|
615
|
+
[attr.tabindex]="tabIndexFor(option.value)"
|
|
616
|
+
[class]="optionClasses(option.value)"
|
|
617
|
+
(click)="select(option.value)"
|
|
618
|
+
(keydown)="onKeydown($event)"
|
|
619
|
+
>
|
|
620
|
+
{{ option.label }}
|
|
621
|
+
</button>
|
|
622
|
+
}
|
|
623
|
+
</div>
|
|
624
|
+
`, isInline: true });
|
|
625
|
+
}
|
|
626
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSegmented, decorators: [{
|
|
627
|
+
type: Component,
|
|
628
|
+
args: [{
|
|
629
|
+
selector: 'ui-segmented',
|
|
630
|
+
template: `
|
|
631
|
+
<div class="flex gap-1" role="radiogroup" [attr.aria-label]="label()">
|
|
632
|
+
@for (option of options(); track option.value) {
|
|
633
|
+
<button
|
|
634
|
+
#radio
|
|
635
|
+
role="radio"
|
|
636
|
+
type="button"
|
|
637
|
+
[attr.aria-checked]="option.value === value()"
|
|
638
|
+
[attr.tabindex]="tabIndexFor(option.value)"
|
|
639
|
+
[class]="optionClasses(option.value)"
|
|
640
|
+
(click)="select(option.value)"
|
|
641
|
+
(keydown)="onKeydown($event)"
|
|
642
|
+
>
|
|
643
|
+
{{ option.label }}
|
|
644
|
+
</button>
|
|
645
|
+
}
|
|
646
|
+
</div>
|
|
647
|
+
`,
|
|
648
|
+
}]
|
|
649
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], radios: [{ type: i0.ViewChildren, args: ['radio', { isSignal: true }] }] } });
|
|
650
|
+
|
|
651
|
+
const SELECT_CLASSES = 'bg-(--elevated) pr-8';
|
|
652
|
+
/**
|
|
653
|
+
* Styled native select. Deliberately keeps the platform's own chevron and picker:
|
|
654
|
+
* they are what makes the control usable on a phone.
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* <select uiSelect><option value="pea">PEA</option></select>
|
|
658
|
+
*/
|
|
659
|
+
class UiSelect {
|
|
660
|
+
size = input('md', /* @ts-ignore */
|
|
661
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
662
|
+
classes = computed(() => `${CONTROL_BASE_CLASSES} ${SELECT_CLASSES} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
|
|
663
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
664
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
665
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: UiSelect, isStandalone: true, selector: "select[uiSelect]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
666
|
+
}
|
|
667
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSelect, decorators: [{
|
|
668
|
+
type: Directive,
|
|
669
|
+
args: [{
|
|
670
|
+
selector: 'select[uiSelect]',
|
|
671
|
+
host: {
|
|
672
|
+
'[class]': 'classes()',
|
|
673
|
+
},
|
|
674
|
+
}]
|
|
675
|
+
}], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
676
|
+
|
|
677
|
+
const LAST_LINE_WIDTH = '62%';
|
|
678
|
+
/**
|
|
679
|
+
* Decorative placeholder for content that is still loading. Deliberately hidden from
|
|
680
|
+
* assistive technology: the calling screen keeps its own role="status" sentence.
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* <ui-skeleton [lines]="3" />
|
|
684
|
+
* <ui-skeleton [height]="196" />
|
|
685
|
+
*/
|
|
686
|
+
class UiSkeleton {
|
|
687
|
+
lines = input(1, /* @ts-ignore */
|
|
688
|
+
...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
|
|
689
|
+
height = input(12, /* @ts-ignore */
|
|
690
|
+
...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
691
|
+
bars = computed(() => {
|
|
692
|
+
const count = Math.max(1, this.lines());
|
|
693
|
+
return Array.from({ length: count }, (_, index) => (index === count - 1 && count > 1 ? LAST_LINE_WIDTH : '100%'));
|
|
694
|
+
}, /* @ts-ignore */
|
|
695
|
+
...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
|
|
696
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
697
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", 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: `
|
|
698
|
+
@for (line of bars(); track $index) {
|
|
699
|
+
<span
|
|
700
|
+
class="block rounded bg-(--soft) motion-safe:animate-pulse"
|
|
701
|
+
[style.height.px]="height()"
|
|
702
|
+
[style.width]="line"
|
|
703
|
+
></span>
|
|
704
|
+
}
|
|
705
|
+
`, isInline: true });
|
|
706
|
+
}
|
|
707
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiSkeleton, decorators: [{
|
|
708
|
+
type: Component,
|
|
709
|
+
args: [{
|
|
710
|
+
selector: 'ui-skeleton',
|
|
711
|
+
template: `
|
|
712
|
+
@for (line of bars(); track $index) {
|
|
713
|
+
<span
|
|
714
|
+
class="block rounded bg-(--soft) motion-safe:animate-pulse"
|
|
715
|
+
[style.height.px]="height()"
|
|
716
|
+
[style.width]="line"
|
|
717
|
+
></span>
|
|
718
|
+
}
|
|
719
|
+
`,
|
|
720
|
+
host: {
|
|
721
|
+
'aria-hidden': 'true',
|
|
722
|
+
class: 'flex flex-col gap-2',
|
|
723
|
+
},
|
|
724
|
+
}]
|
|
725
|
+
}], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
|
|
726
|
+
|
|
727
|
+
/** Breakpoints a secondary column can be held back until. */
|
|
728
|
+
const CELL_BREAKPOINTS = ['md', 'lg'];
|
|
729
|
+
const FROM_CLASSES = {
|
|
730
|
+
md: 'hidden md:table-cell',
|
|
731
|
+
lg: 'hidden lg:table-cell',
|
|
732
|
+
};
|
|
733
|
+
const TH_CLASSES = 'px-3 py-2 align-bottom text-[11px] font-medium uppercase tracking-[0.05em] text-(--subtle-foreground)';
|
|
734
|
+
const TD_CLASSES = 'border-t border-(--hairline) px-3 py-2.5 align-middle text-sm';
|
|
735
|
+
const NUMERIC_CLASSES = 'text-right tabular-nums';
|
|
736
|
+
const TEXTUAL_CLASSES = 'text-left';
|
|
737
|
+
/** Styled native table. The rows and cells stay plain <tr> and <td>. */
|
|
738
|
+
class UiTable {
|
|
739
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
740
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: UiTable, isStandalone: true, selector: "table[uiTable]", host: { classAttribute: "w-full border-collapse" }, ngImport: i0 });
|
|
741
|
+
}
|
|
742
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTable, decorators: [{
|
|
743
|
+
type: Directive,
|
|
744
|
+
args: [{
|
|
745
|
+
selector: 'table[uiTable]',
|
|
746
|
+
host: {
|
|
747
|
+
class: 'w-full border-collapse',
|
|
748
|
+
},
|
|
749
|
+
}]
|
|
750
|
+
}] });
|
|
751
|
+
const cellClasses = (base, numeric, from) => [base, numeric ? NUMERIC_CLASSES : TEXTUAL_CLASSES, from ? FROM_CLASSES[from] : ''].filter(Boolean).join(' ');
|
|
752
|
+
/**
|
|
753
|
+
* Column header. `numeric` right-aligns and lines the digits up; `from` holds a
|
|
754
|
+
* secondary column back until the viewport is wide enough for it.
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* <th uiTh numeric from="md">Prix de revient</th>
|
|
758
|
+
*/
|
|
759
|
+
class UiTh {
|
|
760
|
+
numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
761
|
+
from = input(null, /* @ts-ignore */
|
|
762
|
+
...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
|
|
763
|
+
classes = computed(() => cellClasses(TH_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
|
|
764
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
765
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
766
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", 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 });
|
|
767
|
+
}
|
|
768
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTh, decorators: [{
|
|
769
|
+
type: Directive,
|
|
770
|
+
args: [{
|
|
771
|
+
selector: 'th[uiTh]',
|
|
772
|
+
host: {
|
|
773
|
+
'[class]': 'classes()',
|
|
774
|
+
},
|
|
775
|
+
}]
|
|
776
|
+
}], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
|
|
777
|
+
/** Body cell. Same two inputs as `UiTh`, and they must be set on both to match. */
|
|
778
|
+
class UiTd {
|
|
779
|
+
numeric = input(false, { ...(ngDevMode ? { debugName: "numeric" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
780
|
+
from = input(null, /* @ts-ignore */
|
|
781
|
+
...(ngDevMode ? [{ debugName: "from" }] : /* istanbul ignore next */ []));
|
|
782
|
+
classes = computed(() => cellClasses(TD_CLASSES, this.numeric(), this.from()), /* @ts-ignore */
|
|
783
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
784
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
785
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", 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 });
|
|
786
|
+
}
|
|
787
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UiTd, decorators: [{
|
|
788
|
+
type: Directive,
|
|
789
|
+
args: [{
|
|
790
|
+
selector: 'td[uiTd]',
|
|
791
|
+
host: {
|
|
792
|
+
'[class]': 'classes()',
|
|
793
|
+
},
|
|
794
|
+
}]
|
|
795
|
+
}], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
|
|
796
|
+
|
|
797
|
+
/*
|
|
798
|
+
* Public API surface of the ui library.
|
|
799
|
+
*/
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Generated bundle index. Do not edit.
|
|
803
|
+
*/
|
|
804
|
+
|
|
805
|
+
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 };
|
|
806
|
+
//# sourceMappingURL=joanroucoux-cairn-ui.mjs.map
|