@joanroucoux/cairn-ui 0.2.0 → 0.4.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,9 +1,204 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input,
|
|
2
|
+
import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, viewChild, afterNextRender, contentChildren, model, viewChildren } from '@angular/core';
|
|
3
|
+
import { scaleLinear } from 'd3-scale';
|
|
4
|
+
import { curveMonotoneX, line } from 'd3-shape';
|
|
5
|
+
|
|
6
|
+
const MINUS = '−';
|
|
7
|
+
const EM_DASH = '—';
|
|
8
|
+
const BULLETS = '••••';
|
|
9
|
+
/** Formats a number or an amount for the locale, with a missing value as an em dash and masking behind four bullets. */
|
|
10
|
+
function formatAmount(value, options, masked = false) {
|
|
11
|
+
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
12
|
+
return EM_DASH;
|
|
13
|
+
}
|
|
14
|
+
const digits = options.fractionDigits ?? 2;
|
|
15
|
+
const format = new Intl.NumberFormat(options.locale, {
|
|
16
|
+
...(options.currency ? { style: 'currency', currency: options.currency } : {}),
|
|
17
|
+
minimumFractionDigits: digits,
|
|
18
|
+
maximumFractionDigits: digits,
|
|
19
|
+
});
|
|
20
|
+
const magnitude = Math.abs(value);
|
|
21
|
+
const parts = format.formatToParts(magnitude);
|
|
22
|
+
if (masked) {
|
|
23
|
+
let replaced = false;
|
|
24
|
+
return parts
|
|
25
|
+
.map((part) => {
|
|
26
|
+
if (['integer', 'group', 'decimal', 'fraction'].includes(part.type)) {
|
|
27
|
+
if (replaced) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
replaced = true;
|
|
31
|
+
return BULLETS;
|
|
32
|
+
}
|
|
33
|
+
return part.value;
|
|
34
|
+
})
|
|
35
|
+
.join('');
|
|
36
|
+
}
|
|
37
|
+
const text = parts.map((part) => part.value).join('');
|
|
38
|
+
const isZero = Number(format.format(magnitude).replace(/[^\d]/g, '')) === 0;
|
|
39
|
+
if (!options.signed || isZero) {
|
|
40
|
+
return value < 0 && !isZero ? `${MINUS}${text}` : text;
|
|
41
|
+
}
|
|
42
|
+
return `${value > 0 ? '+' : MINUS}${text}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** App-wide "amounts are hidden" state. Defaults to never masked; the app overrides it. */
|
|
46
|
+
const UI_AMOUNT_MASKED = new InjectionToken('UI_AMOUNT_MASKED', {
|
|
47
|
+
providedIn: 'root',
|
|
48
|
+
factory: () => signal(false),
|
|
49
|
+
});
|
|
50
|
+
/** Available figure styles. `AmountNumeric` is derived from this tuple. */
|
|
51
|
+
const AMOUNT_NUMERICS = ['tabular', 'proportional'];
|
|
52
|
+
/** Available masking behaviors. `AmountMasking` is derived from this tuple. */
|
|
53
|
+
const AMOUNT_MASKINGS = ['dots', 'hide'];
|
|
54
|
+
/**
|
|
55
|
+
* A number or an amount of money, formatted for the locale and masked when the app hides amounts.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <ui-amount [value]="total" currency="EUR" numeric="proportional" />
|
|
59
|
+
* <ui-amount [value]="dayChange" currency="EUR" signed whenMasked="hide" />
|
|
60
|
+
*/
|
|
61
|
+
class UiAmount {
|
|
62
|
+
value = input(/* @ts-ignore */
|
|
63
|
+
...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
64
|
+
currency = input(/* @ts-ignore */
|
|
65
|
+
...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
|
|
66
|
+
locale = input(inject(LOCALE_ID), /* @ts-ignore */
|
|
67
|
+
...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
68
|
+
signed = input(false, { ...(ngDevMode ? { debugName: "signed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
69
|
+
fractionDigits = input(2, /* @ts-ignore */
|
|
70
|
+
...(ngDevMode ? [{ debugName: "fractionDigits" }] : /* istanbul ignore next */ []));
|
|
71
|
+
numeric = input('tabular', /* @ts-ignore */
|
|
72
|
+
...(ngDevMode ? [{ debugName: "numeric" }] : /* istanbul ignore next */ []));
|
|
73
|
+
whenMasked = input('dots', /* @ts-ignore */
|
|
74
|
+
...(ngDevMode ? [{ debugName: "whenMasked" }] : /* istanbul ignore next */ []));
|
|
75
|
+
#masked = inject(UI_AMOUNT_MASKED);
|
|
76
|
+
hidden = computed(() => this.#masked() && this.whenMasked() === 'hide', /* @ts-ignore */
|
|
77
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
78
|
+
text = computed(() => formatAmount(this.value(), {
|
|
79
|
+
locale: this.locale(),
|
|
80
|
+
currency: this.currency(),
|
|
81
|
+
signed: this.signed(),
|
|
82
|
+
fractionDigits: this.fractionDigits(),
|
|
83
|
+
}, this.#masked()), /* @ts-ignore */
|
|
84
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
85
|
+
classes = computed(() => {
|
|
86
|
+
const missing = this.value() === null || this.value() === undefined;
|
|
87
|
+
return [
|
|
88
|
+
'whitespace-nowrap',
|
|
89
|
+
this.numeric() === 'tabular' ? 'tabular-nums' : '',
|
|
90
|
+
missing ? 'text-(--subtle-foreground)' : '',
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(' ');
|
|
94
|
+
}, /* @ts-ignore */
|
|
95
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
96
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
97
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiAmount, isStandalone: true, selector: "ui-amount", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, signed: { classPropertyName: "signed", publicName: "signed", isSignal: true, isRequired: false, transformFunction: null }, fractionDigits: { classPropertyName: "fractionDigits", publicName: "fractionDigits", isSignal: true, isRequired: false, transformFunction: null }, numeric: { classPropertyName: "numeric", publicName: "numeric", isSignal: true, isRequired: false, transformFunction: null }, whenMasked: { classPropertyName: "whenMasked", publicName: "whenMasked", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: '{{ text() }}', isInline: true });
|
|
98
|
+
}
|
|
99
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAmount, decorators: [{
|
|
100
|
+
type: Component,
|
|
101
|
+
args: [{
|
|
102
|
+
selector: 'ui-amount',
|
|
103
|
+
template: '{{ text() }}',
|
|
104
|
+
host: {
|
|
105
|
+
'[class]': 'classes()',
|
|
106
|
+
'[attr.hidden]': 'hidden() || null',
|
|
107
|
+
},
|
|
108
|
+
}]
|
|
109
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], signed: [{ type: i0.Input, args: [{ isSignal: true, alias: "signed", required: false }] }], fractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "fractionDigits", required: false }] }], numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], whenMasked: [{ type: i0.Input, args: [{ isSignal: true, alias: "whenMasked", required: false }] }] } });
|
|
110
|
+
|
|
111
|
+
/** Available async block states. `AsyncState` is derived from this tuple. */
|
|
112
|
+
const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'];
|
|
113
|
+
const RETRY_BUTTON_CLASSES = 'mt-2 relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) min-h-(--row-min) px-4 text-label bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';
|
|
114
|
+
/**
|
|
115
|
+
* The three states of a block that depends on a service call, next to the ready content.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* <ui-async [state]="totalState()" errorTitle="Total could not be loaded" errorMessage="The server did not answer." retryLabel="Retry" (retry)="reload()">
|
|
119
|
+
* <span asyncLoading><ui-skeleton shape="figure" /></span>
|
|
120
|
+
* <span asyncEmpty>Add a holding to see your total.</span>
|
|
121
|
+
* <span>{{ total() }}</span>
|
|
122
|
+
* </ui-async>
|
|
123
|
+
*/
|
|
124
|
+
class UiAsync {
|
|
125
|
+
state = input.required(/* @ts-ignore */
|
|
126
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
127
|
+
errorTitle = input(/* @ts-ignore */
|
|
128
|
+
...(ngDevMode ? [undefined, { debugName: "errorTitle" }] : /* istanbul ignore next */ []));
|
|
129
|
+
errorMessage = input(/* @ts-ignore */
|
|
130
|
+
...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
131
|
+
retryLabel = input(/* @ts-ignore */
|
|
132
|
+
...(ngDevMode ? [undefined, { debugName: "retryLabel" }] : /* istanbul ignore next */ []));
|
|
133
|
+
retry = output();
|
|
134
|
+
retryButtonClasses = RETRY_BUTTON_CLASSES;
|
|
135
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
136
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiAsync, isStandalone: true, selector: "ui-async", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, errorTitle: { classPropertyName: "errorTitle", publicName: "errorTitle", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, retryLabel: { classPropertyName: "retryLabel", publicName: "retryLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { properties: { "attr.aria-busy": "state() === 'loading' || null" } }, ngImport: i0, template: `
|
|
137
|
+
@switch (state()) {
|
|
138
|
+
@case ('loading') {
|
|
139
|
+
<ng-content select="[asyncLoading]" />
|
|
140
|
+
}
|
|
141
|
+
@case ('error') {
|
|
142
|
+
<div
|
|
143
|
+
aria-live="polite"
|
|
144
|
+
class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
|
|
145
|
+
role="alert"
|
|
146
|
+
>
|
|
147
|
+
<span class="text-body font-medium">{{ errorTitle() }}</span>
|
|
148
|
+
<span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
|
|
149
|
+
<button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
|
|
150
|
+
{{ retryLabel() }}
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
}
|
|
154
|
+
@case ('empty') {
|
|
155
|
+
<ng-content select="[asyncEmpty]" />
|
|
156
|
+
}
|
|
157
|
+
@default {
|
|
158
|
+
<ng-content />
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`, isInline: true });
|
|
162
|
+
}
|
|
163
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAsync, decorators: [{
|
|
164
|
+
type: Component,
|
|
165
|
+
args: [{
|
|
166
|
+
selector: 'ui-async',
|
|
167
|
+
template: `
|
|
168
|
+
@switch (state()) {
|
|
169
|
+
@case ('loading') {
|
|
170
|
+
<ng-content select="[asyncLoading]" />
|
|
171
|
+
}
|
|
172
|
+
@case ('error') {
|
|
173
|
+
<div
|
|
174
|
+
aria-live="polite"
|
|
175
|
+
class="rounded-control flex flex-col items-start gap-1 bg-(--elevated) p-(--inset-card)"
|
|
176
|
+
role="alert"
|
|
177
|
+
>
|
|
178
|
+
<span class="text-body font-medium">{{ errorTitle() }}</span>
|
|
179
|
+
<span class="text-label text-(--muted-foreground)">{{ errorMessage() }}</span>
|
|
180
|
+
<button type="button" [class]="retryButtonClasses" (click)="retry.emit()">
|
|
181
|
+
{{ retryLabel() }}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
}
|
|
185
|
+
@case ('empty') {
|
|
186
|
+
<ng-content select="[asyncEmpty]" />
|
|
187
|
+
}
|
|
188
|
+
@default {
|
|
189
|
+
<ng-content />
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
host: {
|
|
194
|
+
'[attr.aria-busy]': "state() === 'loading' || null",
|
|
195
|
+
},
|
|
196
|
+
}]
|
|
197
|
+
}], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], errorTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTitle", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], retryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "retryLabel", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
|
|
3
198
|
|
|
4
199
|
/** Available avatar sizes. `AvatarSize` is derived from this tuple. */
|
|
5
200
|
const AVATAR_SIZES = ['sm', 'md'];
|
|
6
|
-
const BASE_CLASSES$
|
|
201
|
+
const BASE_CLASSES$6 = 'inline-flex items-center justify-center rounded-pill border border-(--primary) bg-(--soft) font-semibold tabular-nums text-(--primary)';
|
|
7
202
|
const SIZE_CLASSES$1 = {
|
|
8
203
|
sm: 'size-8 text-caption',
|
|
9
204
|
md: 'size-11 text-label',
|
|
@@ -21,7 +216,7 @@ class UiAvatar {
|
|
|
21
216
|
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
22
217
|
size = input('md', /* @ts-ignore */
|
|
23
218
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
24
|
-
classes = computed(() => `${BASE_CLASSES$
|
|
219
|
+
classes = computed(() => `${BASE_CLASSES$6} ${SIZE_CLASSES$1[this.size()]}`, /* @ts-ignore */
|
|
25
220
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
26
221
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
27
222
|
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 });
|
|
@@ -41,7 +236,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
41
236
|
|
|
42
237
|
/** Available badge variants. `BadgeVariant` is derived from this tuple. */
|
|
43
238
|
const BADGE_VARIANTS = ['neutral', 'outline'];
|
|
44
|
-
const BASE_CLASSES$
|
|
239
|
+
const BASE_CLASSES$5 = 'inline-flex h-6 items-center rounded-pill px-2.5 text-caption font-medium';
|
|
45
240
|
const VARIANT_CLASSES$2 = {
|
|
46
241
|
neutral: 'bg-(--muted) text-(--foreground)',
|
|
47
242
|
outline: 'shadow-[inset_0_0_0_1px_var(--border)] text-(--foreground)',
|
|
@@ -55,7 +250,7 @@ const VARIANT_CLASSES$2 = {
|
|
|
55
250
|
class UiBadge {
|
|
56
251
|
variant = input('neutral', /* @ts-ignore */
|
|
57
252
|
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
58
|
-
classes = computed(() => `${BASE_CLASSES$
|
|
253
|
+
classes = computed(() => `${BASE_CLASSES$5} ${VARIANT_CLASSES$2[this.variant()]}`, /* @ts-ignore */
|
|
59
254
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
60
255
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
61
256
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiBadge, isStandalone: true, selector: "ui-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
|
|
@@ -75,9 +270,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
75
270
|
const BUTTON_VARIANTS = ['primary', 'outline', 'ghost', 'destructive'];
|
|
76
271
|
/** Available button sizes. `md` is the touch target Cairn asks for everywhere. */
|
|
77
272
|
const BUTTON_SIZES = ['md', 'lg', 'xl', 'icon'];
|
|
78
|
-
const BASE_CLASSES$
|
|
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';
|
|
273
|
+
const BASE_CLASSES$4 = 'relative inline-flex items-center justify-center gap-2 rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[transform,background-color,opacity] duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) disabled:pointer-events-none disabled:opacity-40 aria-disabled:not-aria-busy:opacity-40';
|
|
81
274
|
const VARIANT_CLASSES$1 = {
|
|
82
275
|
primary: 'bg-(--primary) text-(--primary-foreground) hover:opacity-90',
|
|
83
276
|
outline: 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)',
|
|
@@ -91,16 +284,7 @@ const SIZE_CLASSES = {
|
|
|
91
284
|
icon: 'size-(--row-min) p-0',
|
|
92
285
|
};
|
|
93
286
|
/**
|
|
94
|
-
* Styled native button
|
|
95
|
-
* button behavior (type, disabled, form submission, accessibility) for free.
|
|
96
|
-
*
|
|
97
|
-
* Also applies to an anchor, for the one case where the action really is a navigation:
|
|
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.
|
|
287
|
+
* Styled native button or anchor, applied as an attribute so the host keeps every native behavior.
|
|
104
288
|
*
|
|
105
289
|
* @example
|
|
106
290
|
* <button ui-button variant="destructive">Delete</button>
|
|
@@ -112,12 +296,9 @@ class UiButton {
|
|
|
112
296
|
size = input('md', /* @ts-ignore */
|
|
113
297
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
114
298
|
loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
115
|
-
classes = computed(() => `${BASE_CLASSES$
|
|
299
|
+
classes = computed(() => `${BASE_CLASSES$4} ${VARIANT_CLASSES$1[this.variant()]} ${SIZE_CLASSES[this.size()]}${this.loading() ? ' pointer-events-none opacity-70' : ''}`, /* @ts-ignore */
|
|
116
300
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
117
301
|
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
302
|
const host = inject(ElementRef).nativeElement;
|
|
122
303
|
const guard = (event) => {
|
|
123
304
|
if (this.loading()) {
|
|
@@ -164,7 +345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
164
345
|
const CARD_VARIANTS = ['default', 'elevated'];
|
|
165
346
|
/** Available card paddings. `CardPadding` is derived from this tuple. */
|
|
166
347
|
const CARD_PADDINGS = ['none', 'sm', 'md'];
|
|
167
|
-
const BASE_CLASSES$
|
|
348
|
+
const BASE_CLASSES$3 = 'block rounded-container border';
|
|
168
349
|
const VARIANT_CLASSES = {
|
|
169
350
|
default: 'bg-(--card) border-(--border) text-(--card-foreground)',
|
|
170
351
|
elevated: 'bg-(--elevated) border-(--hairline) text-(--foreground)',
|
|
@@ -185,7 +366,7 @@ class UiCard {
|
|
|
185
366
|
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
186
367
|
padding = input('md', /* @ts-ignore */
|
|
187
368
|
...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
|
|
188
|
-
classes = computed(() => `${BASE_CLASSES$
|
|
369
|
+
classes = computed(() => `${BASE_CLASSES$3} ${VARIANT_CLASSES[this.variant()]} ${PADDING_CLASSES[this.padding()]}`, /* @ts-ignore */
|
|
189
370
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
190
371
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
191
372
|
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 });
|
|
@@ -213,7 +394,7 @@ const UI_CONTROL = new InjectionToken('UI_CONTROL');
|
|
|
213
394
|
|
|
214
395
|
/** Available delta emphases. `DeltaEmphasis` is derived from this tuple. */
|
|
215
396
|
const DELTA_EMPHASES = ['text', 'pill'];
|
|
216
|
-
const BASE_CLASSES = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
|
|
397
|
+
const BASE_CLASSES$2 = 'inline-flex items-center gap-1 text-label font-medium tabular-nums';
|
|
217
398
|
const PILL_CLASSES = 'h-6 rounded-pill px-2';
|
|
218
399
|
const TEXT_CLASSES = {
|
|
219
400
|
positive: 'text-(--positive)',
|
|
@@ -221,9 +402,6 @@ const TEXT_CLASSES = {
|
|
|
221
402
|
neutral: 'text-(--muted-foreground)',
|
|
222
403
|
unknown: 'text-(--subtle-foreground)',
|
|
223
404
|
};
|
|
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.
|
|
227
405
|
const PILL_BACKGROUND_CLASS = 'bg-(--muted)';
|
|
228
406
|
/**
|
|
229
407
|
* Signed amount whose sign carries meaning: a gain, a loss, or an unknown.
|
|
@@ -261,7 +439,7 @@ class UiDelta {
|
|
|
261
439
|
classes = computed(() => {
|
|
262
440
|
const sign = this.sign();
|
|
263
441
|
const background = this.emphasis() === 'pill' ? `${PILL_CLASSES} ${PILL_BACKGROUND_CLASS}` : 'bg-transparent';
|
|
264
|
-
return `${BASE_CLASSES} ${TEXT_CLASSES[sign]} ${background}`;
|
|
442
|
+
return `${BASE_CLASSES$2} ${TEXT_CLASSES[sign]} ${background}`;
|
|
265
443
|
}, /* @ts-ignore */
|
|
266
444
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
267
445
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDelta, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
@@ -296,15 +474,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
296
474
|
const DIALOG_WIDTHS = ['md', 'lg'];
|
|
297
475
|
const WIDTH_CLASSES = {
|
|
298
476
|
md: 'max-w-md',
|
|
299
|
-
lg: 'max-w-
|
|
477
|
+
lg: 'max-w-[560px]',
|
|
300
478
|
};
|
|
301
|
-
const nextId$
|
|
479
|
+
const nextId$2 = (() => {
|
|
302
480
|
let count = 0;
|
|
303
481
|
return () => `ui-dialog-${++count}`;
|
|
304
482
|
})();
|
|
305
483
|
/**
|
|
306
|
-
* Modal dialog on the native <dialog>:
|
|
307
|
-
*
|
|
484
|
+
* Modal dialog on the native <dialog>: a sheet rising from the bottom under `64rem`, centered
|
|
485
|
+
* above it. Two projection slots - the body, and [dialogActions] for the footer.
|
|
308
486
|
*
|
|
309
487
|
* @example
|
|
310
488
|
* <ui-dialog heading="Delete this holding?" [open]="confirming()" (dismissed)="confirming.set(false)">
|
|
@@ -319,12 +497,11 @@ class UiDialog {
|
|
|
319
497
|
...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
|
|
320
498
|
width = input('lg', /* @ts-ignore */
|
|
321
499
|
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
322
|
-
open = input(false, /*
|
|
323
|
-
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
500
|
+
open = input(false, { ...(ngDevMode ? { debugName: "open" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
324
501
|
dismissed = output();
|
|
325
|
-
headingId = nextId$
|
|
502
|
+
headingId = nextId$2();
|
|
326
503
|
descriptionId = `${this.headingId}-description`;
|
|
327
|
-
classes = computed(() => `w-full m-auto ${WIDTH_CLASSES[this.width()]}
|
|
504
|
+
classes = computed(() => `flex w-full flex-col m-auto mt-auto max-h-[calc(100dvh-2rem)] max-lg:mb-0 max-lg:max-w-none max-lg:rounded-b-none rounded-container ${WIDTH_CLASSES[this.width()]} border border-(--border) bg-(--card) text-(--foreground) backdrop:bg-black/60`, /* @ts-ignore */
|
|
328
505
|
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
329
506
|
#host = inject(ElementRef);
|
|
330
507
|
constructor() {
|
|
@@ -353,54 +530,73 @@ class UiDialog {
|
|
|
353
530
|
[class]="classes()"
|
|
354
531
|
(close)="onNativeClose()"
|
|
355
532
|
>
|
|
356
|
-
<
|
|
533
|
+
<div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
|
|
534
|
+
<span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
|
|
535
|
+
</div>
|
|
357
536
|
|
|
358
|
-
|
|
359
|
-
<
|
|
360
|
-
}
|
|
537
|
+
<div class="px-6 pt-1 lg:pt-6">
|
|
538
|
+
<h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
|
|
361
539
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
540
|
+
@if (description()) {
|
|
541
|
+
<p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
|
|
542
|
+
}
|
|
365
543
|
</div>
|
|
366
544
|
|
|
367
|
-
<div class="
|
|
545
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
|
|
546
|
+
<!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
|
|
547
|
+
<div class="mt-4 flex flex-col gap-4 empty:hidden">
|
|
548
|
+
<ng-content />
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div
|
|
553
|
+
class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
|
|
554
|
+
data-dialog-footer
|
|
555
|
+
>
|
|
368
556
|
<ng-content select="[dialogActions]" />
|
|
369
557
|
</div>
|
|
370
558
|
</dialog>
|
|
371
|
-
`, isInline: true });
|
|
559
|
+
`, isInline: true, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] });
|
|
372
560
|
}
|
|
373
561
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDialog, decorators: [{
|
|
374
562
|
type: Component,
|
|
375
|
-
args: [{
|
|
376
|
-
selector: 'ui-dialog',
|
|
377
|
-
template: `
|
|
563
|
+
args: [{ selector: 'ui-dialog', template: `
|
|
378
564
|
<dialog
|
|
379
565
|
[attr.aria-describedby]="description() ? descriptionId : null"
|
|
380
566
|
[attr.aria-labelledby]="headingId"
|
|
381
567
|
[class]="classes()"
|
|
382
568
|
(close)="onNativeClose()"
|
|
383
569
|
>
|
|
384
|
-
<
|
|
570
|
+
<div aria-hidden="true" class="flex h-7 items-center justify-center lg:hidden" data-dialog-handle>
|
|
571
|
+
<span class="rounded-pill h-1.5 w-9 bg-(--border)"></span>
|
|
572
|
+
</div>
|
|
385
573
|
|
|
386
|
-
|
|
387
|
-
<
|
|
388
|
-
}
|
|
574
|
+
<div class="px-6 pt-1 lg:pt-6">
|
|
575
|
+
<h2 class="text-title font-semibold" [id]="headingId">{{ heading() }}</h2>
|
|
389
576
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
577
|
+
@if (description()) {
|
|
578
|
+
<p class="text-label mt-2 text-(--muted-foreground)" [id]="descriptionId">{{ description() }}</p>
|
|
579
|
+
}
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-6" data-dialog-body tabindex="0">
|
|
583
|
+
<!-- empty:hidden keeps a body-less dialog from carrying the body's top margin. -->
|
|
584
|
+
<div class="mt-4 flex flex-col gap-4 empty:hidden">
|
|
585
|
+
<ng-content />
|
|
586
|
+
</div>
|
|
393
587
|
</div>
|
|
394
588
|
|
|
395
|
-
<div
|
|
589
|
+
<div
|
|
590
|
+
class="flex justify-end gap-3 px-6 pt-6 pb-6 max-lg:pb-[calc(1rem+env(safe-area-inset-bottom))]"
|
|
591
|
+
data-dialog-footer
|
|
592
|
+
>
|
|
396
593
|
<ng-content select="[dialogActions]" />
|
|
397
594
|
</div>
|
|
398
595
|
</dialog>
|
|
399
|
-
`,
|
|
400
|
-
}]
|
|
596
|
+
`, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] }]
|
|
401
597
|
}], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
|
|
402
598
|
|
|
403
|
-
const nextId = (() => {
|
|
599
|
+
const nextId$1 = (() => {
|
|
404
600
|
let count = 0;
|
|
405
601
|
return () => `ui-field-${++count}`;
|
|
406
602
|
})();
|
|
@@ -434,10 +630,9 @@ class UiField {
|
|
|
434
630
|
...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
435
631
|
error = input(/* @ts-ignore */
|
|
436
632
|
...(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
633
|
unit = input(/* @ts-ignore */
|
|
439
634
|
...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
|
|
440
|
-
#id = nextId();
|
|
635
|
+
#id = nextId$1();
|
|
441
636
|
hintId = `${this.#id}-hint`;
|
|
442
637
|
errorId = `${this.#id}-error`;
|
|
443
638
|
#host = inject(ElementRef);
|
|
@@ -608,6 +803,851 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
608
803
|
}]
|
|
609
804
|
}], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
|
|
610
805
|
|
|
806
|
+
const axisTicks = (points, maxTicks = 5) => {
|
|
807
|
+
if (points.length <= maxTicks) {
|
|
808
|
+
return [...points];
|
|
809
|
+
}
|
|
810
|
+
const count = Math.max(3, Math.min(maxTicks, points.length));
|
|
811
|
+
const step = (points.length - 1) / (count - 1);
|
|
812
|
+
const indexes = new Set();
|
|
813
|
+
for (let tick = 0; tick < count; tick++) {
|
|
814
|
+
indexes.add(Math.round(tick * step));
|
|
815
|
+
}
|
|
816
|
+
return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
820
|
+
const nearestPointIndex = (points, x) => {
|
|
821
|
+
let nearest = 0;
|
|
822
|
+
let nearestDistance = Infinity;
|
|
823
|
+
points.forEach((point, index) => {
|
|
824
|
+
const distance = Math.abs(point.x - x);
|
|
825
|
+
if (distance < nearestDistance) {
|
|
826
|
+
nearestDistance = distance;
|
|
827
|
+
nearest = index;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
return nearest;
|
|
831
|
+
};
|
|
832
|
+
const navigateIndex = (key, current, last) => {
|
|
833
|
+
switch (key) {
|
|
834
|
+
case 'ArrowRight':
|
|
835
|
+
return { index: clamp((current ?? -1) + 1, 0, last), preventDefault: true };
|
|
836
|
+
case 'ArrowLeft':
|
|
837
|
+
return { index: clamp((current ?? last + 1) - 1, 0, last), preventDefault: true };
|
|
838
|
+
case 'Home':
|
|
839
|
+
return { index: 0, preventDefault: true };
|
|
840
|
+
case 'End':
|
|
841
|
+
return { index: last, preventDefault: true };
|
|
842
|
+
case 'Escape':
|
|
843
|
+
return { index: null, preventDefault: false };
|
|
844
|
+
default:
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const CURVE = curveMonotoneX;
|
|
850
|
+
const buildGeometry = (points, width, height, padding = {}) => {
|
|
851
|
+
if (points.length === 0) {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
const { x: paddingX = 4, top: paddingTop = 4, bottom: paddingBottom = 4 } = padding;
|
|
855
|
+
const plotTop = paddingTop;
|
|
856
|
+
const plotBottom = height - paddingBottom;
|
|
857
|
+
const times = points.map((point) => point.t);
|
|
858
|
+
const values = points.map((point) => point.v);
|
|
859
|
+
const minValue = Math.min(...values);
|
|
860
|
+
const maxValue = Math.max(...values);
|
|
861
|
+
const x = scaleLinear()
|
|
862
|
+
.domain([Math.min(...times), Math.max(...times)])
|
|
863
|
+
.range([paddingX, width - paddingX]);
|
|
864
|
+
const y = minValue === maxValue
|
|
865
|
+
? () => (plotTop + plotBottom) / 2
|
|
866
|
+
: scaleLinear().domain([minValue, maxValue]).range([plotBottom, plotTop]);
|
|
867
|
+
const toX = (point) => (points.length === 1 ? paddingX : x(point.t));
|
|
868
|
+
const lineGenerator = line()
|
|
869
|
+
.x(toX)
|
|
870
|
+
.y((point) => y(point.v))
|
|
871
|
+
.curve(CURVE);
|
|
872
|
+
const first = points[0];
|
|
873
|
+
const last = points.at(-1);
|
|
874
|
+
return {
|
|
875
|
+
line: lineGenerator(points),
|
|
876
|
+
end: { x: toX(last), y: y(last.v) },
|
|
877
|
+
start: { y: y(first.v) },
|
|
878
|
+
points: points.map((point) => ({ ...point, x: toX(point), y: y(point.v) })),
|
|
879
|
+
plotBottom,
|
|
880
|
+
plotTop,
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const LINE_GAP = 3;
|
|
885
|
+
const COLLISION_MARGIN = 3;
|
|
886
|
+
const EDGE_TOLERANCE = 3;
|
|
887
|
+
const candidateSlots = (startY, box, label) => {
|
|
888
|
+
const aboveTop = startY - LINE_GAP - label.height;
|
|
889
|
+
const aboveBottom = startY - LINE_GAP;
|
|
890
|
+
const belowTop = startY + LINE_GAP;
|
|
891
|
+
const belowBottom = startY + LINE_GAP + label.height;
|
|
892
|
+
const slots = [
|
|
893
|
+
{ anchor: 'end', x: box.width, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
|
|
894
|
+
{ anchor: 'end', x: box.width, y: belowBottom, top: belowTop, bottom: belowBottom },
|
|
895
|
+
{ anchor: 'start', x: 0, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
|
|
896
|
+
{ anchor: 'start', x: 0, y: belowBottom, top: belowTop, bottom: belowBottom },
|
|
897
|
+
{ anchor: 'end', x: box.width, y: label.height, top: 0, bottom: label.height },
|
|
898
|
+
{ anchor: 'start', x: 0, y: label.height, top: 0, bottom: label.height },
|
|
899
|
+
{ anchor: 'end', x: box.width, y: box.height, top: box.height - label.height, bottom: box.height },
|
|
900
|
+
{ anchor: 'start', x: 0, y: box.height, top: box.height - label.height, bottom: box.height },
|
|
901
|
+
];
|
|
902
|
+
return slots.filter((slot) => slot.top >= -EDGE_TOLERANCE && slot.bottom <= box.height + EDGE_TOLERANCE);
|
|
903
|
+
};
|
|
904
|
+
const hitsFor = (slot, label, points) => {
|
|
905
|
+
const left = slot.anchor === 'end' ? slot.x - label.width : slot.x;
|
|
906
|
+
const right = slot.anchor === 'end' ? slot.x : slot.x + label.width;
|
|
907
|
+
const top = slot.top - COLLISION_MARGIN;
|
|
908
|
+
const bottom = slot.bottom + COLLISION_MARGIN;
|
|
909
|
+
return points.filter((point) => point.x >= left && point.x <= right && point.y >= top && point.y <= bottom).length;
|
|
910
|
+
};
|
|
911
|
+
const placeStartLabel = (points, startY, box, label) => {
|
|
912
|
+
let best = null;
|
|
913
|
+
for (const slot of candidateSlots(startY, box, label)) {
|
|
914
|
+
const hits = hitsFor(slot, label, points);
|
|
915
|
+
if (!best || hits < best.hits) {
|
|
916
|
+
best = { slot, hits };
|
|
917
|
+
}
|
|
918
|
+
if (hits === 0) {
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const { anchor, x, y } = best.slot;
|
|
923
|
+
return { anchor, x, y };
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const TRANSITION_SAMPLES = 120;
|
|
927
|
+
const TRANSITION_DURATION = 260;
|
|
928
|
+
const easeOutQuint = (t) => 1 - (1 - t) ** 5;
|
|
929
|
+
const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
930
|
+
const monotoneTangents = (xs, ys) => {
|
|
931
|
+
const n = xs.length;
|
|
932
|
+
const slopes = [];
|
|
933
|
+
for (let i = 0; i < n - 1; i++) {
|
|
934
|
+
const dx = xs[i + 1] - xs[i];
|
|
935
|
+
slopes.push(dx === 0 ? 0 : (ys[i + 1] - ys[i]) / dx);
|
|
936
|
+
}
|
|
937
|
+
const tangents = new Array(n).fill(0);
|
|
938
|
+
tangents[0] = slopes[0];
|
|
939
|
+
tangents[n - 1] = slopes[n - 2];
|
|
940
|
+
for (let i = 1; i < n - 1; i++) {
|
|
941
|
+
const a = slopes[i - 1];
|
|
942
|
+
const b = slopes[i];
|
|
943
|
+
tangents[i] = a * b <= 0 ? 0 : (a + b) / 2;
|
|
944
|
+
}
|
|
945
|
+
for (let i = 0; i < n - 1; i++) {
|
|
946
|
+
const slope = slopes[i];
|
|
947
|
+
if (slope === 0) {
|
|
948
|
+
tangents[i] = 0;
|
|
949
|
+
tangents[i + 1] = 0;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const a = tangents[i] / slope;
|
|
953
|
+
const b = tangents[i + 1] / slope;
|
|
954
|
+
const s = a * a + b * b;
|
|
955
|
+
if (s > 9) {
|
|
956
|
+
const t = 3 / Math.sqrt(s);
|
|
957
|
+
tangents[i] = t * a * slope;
|
|
958
|
+
tangents[i + 1] = t * b * slope;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return tangents;
|
|
962
|
+
};
|
|
963
|
+
const sampleMonotone = (points, sampleXs) => {
|
|
964
|
+
const xs = points.map((point) => point.x);
|
|
965
|
+
const ys = points.map((point) => point.y);
|
|
966
|
+
const tangents = monotoneTangents(xs, ys);
|
|
967
|
+
let segment = 0;
|
|
968
|
+
return sampleXs.map((x) => {
|
|
969
|
+
while (segment < xs.length - 2 && x > xs[segment + 1]) {
|
|
970
|
+
segment++;
|
|
971
|
+
}
|
|
972
|
+
const x0 = xs[segment];
|
|
973
|
+
const x1 = xs[segment + 1];
|
|
974
|
+
const dx = x1 - x0;
|
|
975
|
+
const f = dx === 0 ? 0 : (x - x0) / dx;
|
|
976
|
+
const f2 = f * f;
|
|
977
|
+
const f3 = f2 * f;
|
|
978
|
+
const y0 = ys[segment];
|
|
979
|
+
const y1 = ys[segment + 1];
|
|
980
|
+
const m0 = tangents[segment];
|
|
981
|
+
const m1 = tangents[segment + 1];
|
|
982
|
+
return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
|
|
983
|
+
});
|
|
984
|
+
};
|
|
985
|
+
const buildPolyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
|
|
986
|
+
const interpolateLine = (from, to, progress) => {
|
|
987
|
+
const left = to.points[0].x;
|
|
988
|
+
const right = to.points.at(-1).x;
|
|
989
|
+
const samples = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
|
|
990
|
+
const fromYs = sampleMonotone(from.points, samples);
|
|
991
|
+
const toYs = sampleMonotone(to.points, samples);
|
|
992
|
+
return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
const DEFAULT_WIDTH = 640;
|
|
996
|
+
const DEFAULT_HEIGHT = 240;
|
|
997
|
+
const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
|
|
998
|
+
const START_LABEL_HEIGHT = 17;
|
|
999
|
+
const identityValue = (value) => `${value}`;
|
|
1000
|
+
const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
|
|
1001
|
+
const identityTime = (time) => new Date(time).toISOString();
|
|
1002
|
+
/**
|
|
1003
|
+
* Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* <ui-line-chart [points]="points" label="Net worth over one month" startLabel="Since" [valueFormat]="formatEur" />
|
|
1007
|
+
*/
|
|
1008
|
+
class UiLineChart {
|
|
1009
|
+
points = input.required(/* @ts-ignore */
|
|
1010
|
+
...(ngDevMode ? [{ debugName: "points" }] : /* istanbul ignore next */ []));
|
|
1011
|
+
label = input.required(/* @ts-ignore */
|
|
1012
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1013
|
+
valueFormat = input(identityValue, /* @ts-ignore */
|
|
1014
|
+
...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
|
|
1015
|
+
deltaFormat = input(identityDelta, /* @ts-ignore */
|
|
1016
|
+
...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
|
|
1017
|
+
timeFormat = input(identityTime, /* @ts-ignore */
|
|
1018
|
+
...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
|
|
1019
|
+
axisFormat = input(identityTime, /* @ts-ignore */
|
|
1020
|
+
...(ngDevMode ? [{ debugName: "axisFormat" }] : /* istanbul ignore next */ []));
|
|
1021
|
+
startLabel = input('', /* @ts-ignore */
|
|
1022
|
+
...(ngDevMode ? [{ debugName: "startLabel" }] : /* istanbul ignore next */ []));
|
|
1023
|
+
timeColumnLabel = input('Time', /* @ts-ignore */
|
|
1024
|
+
...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
|
|
1025
|
+
valueColumnLabel = input('Value', /* @ts-ignore */
|
|
1026
|
+
...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
|
|
1027
|
+
#destroyRef = inject(DestroyRef);
|
|
1028
|
+
#frame = null;
|
|
1029
|
+
#hasRenderedOnce = false;
|
|
1030
|
+
#previousGeometry = null;
|
|
1031
|
+
svgRef = viewChild.required('svgRef', /* @ts-ignore */
|
|
1032
|
+
...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
|
|
1033
|
+
activeIndex = signal(null, /* @ts-ignore */
|
|
1034
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
1035
|
+
animatedLine = signal(null, /* @ts-ignore */
|
|
1036
|
+
...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
|
|
1037
|
+
measuredSize = signal(null, /* @ts-ignore */
|
|
1038
|
+
...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
|
|
1039
|
+
effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
|
|
1040
|
+
...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
|
|
1041
|
+
effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
|
|
1042
|
+
...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
|
|
1043
|
+
geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
|
|
1044
|
+
...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
|
|
1045
|
+
displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
|
|
1046
|
+
...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
|
|
1047
|
+
ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
|
|
1048
|
+
...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
|
|
1049
|
+
active = computed(() => {
|
|
1050
|
+
const geometry = this.geometry();
|
|
1051
|
+
const index = this.activeIndex();
|
|
1052
|
+
return geometry && index !== null ? geometry.points[index] : null;
|
|
1053
|
+
}, /* @ts-ignore */
|
|
1054
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
1055
|
+
startLabelInfo = computed(() => {
|
|
1056
|
+
const text = this.startLabel();
|
|
1057
|
+
const geometry = this.geometry();
|
|
1058
|
+
if (!text || !geometry) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
|
|
1062
|
+
const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
|
|
1063
|
+
const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
|
|
1064
|
+
return { label, placement };
|
|
1065
|
+
}, /* @ts-ignore */
|
|
1066
|
+
...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
|
|
1067
|
+
#renderRef = afterNextRender(() => this.#observeSize());
|
|
1068
|
+
constructor() {
|
|
1069
|
+
effect(() => {
|
|
1070
|
+
const geometry = this.geometry();
|
|
1071
|
+
const previous = this.#previousGeometry;
|
|
1072
|
+
if (previous === geometry) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
this.#previousGeometry = geometry;
|
|
1076
|
+
this.#cancelAnimation();
|
|
1077
|
+
if (!this.#hasRenderedOnce) {
|
|
1078
|
+
this.#hasRenderedOnce = true;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (!previous || !geometry) {
|
|
1082
|
+
this.animatedLine.set(null);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (prefersReducedMotion()) {
|
|
1086
|
+
this.animatedLine.set(null);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.#animate(previous, geometry);
|
|
1090
|
+
});
|
|
1091
|
+
this.#destroyRef.onDestroy(() => this.#cancelAnimation());
|
|
1092
|
+
}
|
|
1093
|
+
#observeSize() {
|
|
1094
|
+
this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
|
|
1095
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const element = this.svgRef().nativeElement;
|
|
1099
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
1100
|
+
if (entry) {
|
|
1101
|
+
const { width, height } = entry.contentRect;
|
|
1102
|
+
this.measuredSize.set({ width, height });
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
observer.observe(element);
|
|
1106
|
+
this.#destroyRef.onDestroy(() => observer.disconnect());
|
|
1107
|
+
}
|
|
1108
|
+
#cancelAnimation() {
|
|
1109
|
+
if (this.#frame !== null) {
|
|
1110
|
+
cancelAnimationFrame(this.#frame);
|
|
1111
|
+
this.#frame = null;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
#animate(from, to) {
|
|
1115
|
+
if (from.points.length < 2 || to.points.length < 2) {
|
|
1116
|
+
this.animatedLine.set(null);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const startTime = performance.now();
|
|
1120
|
+
const step = (now) => {
|
|
1121
|
+
const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
|
|
1122
|
+
this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
|
|
1123
|
+
if (t < 1) {
|
|
1124
|
+
this.#frame = requestAnimationFrame(step);
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
this.#frame = null;
|
|
1128
|
+
this.animatedLine.set(null);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
this.#frame = requestAnimationFrame(step);
|
|
1132
|
+
}
|
|
1133
|
+
coreTick(index, count) {
|
|
1134
|
+
if (count <= 3) {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
const middle = Math.floor((count - 1) / 2);
|
|
1138
|
+
return index === 0 || index === count - 1 || index === middle;
|
|
1139
|
+
}
|
|
1140
|
+
deltaFor(point) {
|
|
1141
|
+
return point.v - this.points()[0].v;
|
|
1142
|
+
}
|
|
1143
|
+
deltaClasses(point) {
|
|
1144
|
+
const delta = this.deltaFor(point);
|
|
1145
|
+
return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
|
|
1146
|
+
}
|
|
1147
|
+
tooltipStyle(point) {
|
|
1148
|
+
return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
|
|
1149
|
+
}
|
|
1150
|
+
onPointerMove(event) {
|
|
1151
|
+
const geometry = this.geometry();
|
|
1152
|
+
if (!geometry || geometry.points.length === 0) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const rect = this.svgRef().nativeElement.getBoundingClientRect();
|
|
1156
|
+
const ratio = rect.width === 0 ? 1 : this.effectiveWidth() / rect.width;
|
|
1157
|
+
const x = (event.clientX - rect.left) * ratio;
|
|
1158
|
+
this.activeIndex.set(nearestPointIndex(geometry.points, x));
|
|
1159
|
+
}
|
|
1160
|
+
onPointerLeave() {
|
|
1161
|
+
this.activeIndex.set(null);
|
|
1162
|
+
}
|
|
1163
|
+
onKeydown(event) {
|
|
1164
|
+
const geometry = this.geometry();
|
|
1165
|
+
if (!geometry || geometry.points.length === 0) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
|
|
1169
|
+
if (!navigation) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (navigation.preventDefault) {
|
|
1173
|
+
event.preventDefault();
|
|
1174
|
+
}
|
|
1175
|
+
this.activeIndex.set(navigation.index);
|
|
1176
|
+
}
|
|
1177
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1178
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1179
|
+
<div class="relative h-full">
|
|
1180
|
+
<svg
|
|
1181
|
+
#svgRef
|
|
1182
|
+
class="block h-full w-full touch-pan-y"
|
|
1183
|
+
role="img"
|
|
1184
|
+
tabindex="0"
|
|
1185
|
+
[attr.aria-label]="label()"
|
|
1186
|
+
[attr.height]="effectiveHeight()"
|
|
1187
|
+
[attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
|
|
1188
|
+
[attr.width]="effectiveWidth()"
|
|
1189
|
+
(keydown)="onKeydown($event)"
|
|
1190
|
+
(pointerleave)="onPointerLeave()"
|
|
1191
|
+
(pointermove)="onPointerMove($event)"
|
|
1192
|
+
>
|
|
1193
|
+
@if (geometry(); as geometry) {
|
|
1194
|
+
<path
|
|
1195
|
+
class="stroke-(--foreground)"
|
|
1196
|
+
data-chart-line
|
|
1197
|
+
fill="none"
|
|
1198
|
+
stroke-linecap="round"
|
|
1199
|
+
stroke-linejoin="round"
|
|
1200
|
+
stroke-width="2"
|
|
1201
|
+
[attr.d]="displayLine()"
|
|
1202
|
+
/>
|
|
1203
|
+
|
|
1204
|
+
<line
|
|
1205
|
+
class="stroke-(--border)"
|
|
1206
|
+
data-chart-start-line
|
|
1207
|
+
stroke-dasharray="4 4"
|
|
1208
|
+
stroke-width="1"
|
|
1209
|
+
x1="0"
|
|
1210
|
+
[attr.x2]="effectiveWidth()"
|
|
1211
|
+
[attr.y1]="geometry.start.y"
|
|
1212
|
+
[attr.y2]="geometry.start.y"
|
|
1213
|
+
/>
|
|
1214
|
+
|
|
1215
|
+
@if (startLabelInfo(); as info) {
|
|
1216
|
+
<text
|
|
1217
|
+
class="text-caption fill-(--subtle-foreground)"
|
|
1218
|
+
data-chart-start-label
|
|
1219
|
+
[attr.text-anchor]="info.placement.anchor"
|
|
1220
|
+
[attr.x]="info.placement.x"
|
|
1221
|
+
[attr.y]="info.placement.y"
|
|
1222
|
+
>
|
|
1223
|
+
{{ info.label }}
|
|
1224
|
+
</text>
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
<circle
|
|
1228
|
+
class="fill-(--foreground) stroke-(--card)"
|
|
1229
|
+
data-chart-end
|
|
1230
|
+
r="4"
|
|
1231
|
+
stroke-width="2"
|
|
1232
|
+
[attr.cx]="geometry.end.x"
|
|
1233
|
+
[attr.cy]="geometry.end.y"
|
|
1234
|
+
/>
|
|
1235
|
+
|
|
1236
|
+
@for (
|
|
1237
|
+
tick of ticks();
|
|
1238
|
+
track tick.t;
|
|
1239
|
+
let index = $index;
|
|
1240
|
+
let count = $count;
|
|
1241
|
+
let first = $first;
|
|
1242
|
+
let last = $last
|
|
1243
|
+
) {
|
|
1244
|
+
<text
|
|
1245
|
+
class="text-caption fill-(--subtle-foreground)"
|
|
1246
|
+
data-chart-axis-tick
|
|
1247
|
+
[attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
|
|
1248
|
+
[attr.x]="tick.x"
|
|
1249
|
+
[attr.y]="effectiveHeight() - 4"
|
|
1250
|
+
[class.max-sm:hidden]="!coreTick(index, count)"
|
|
1251
|
+
>
|
|
1252
|
+
{{ axisFormat()(tick.t) }}
|
|
1253
|
+
</text>
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
@if (active(); as point) {
|
|
1257
|
+
<line
|
|
1258
|
+
class="stroke-(--border)"
|
|
1259
|
+
data-chart-crosshair
|
|
1260
|
+
stroke-width="1"
|
|
1261
|
+
y1="0"
|
|
1262
|
+
[attr.x1]="point.x"
|
|
1263
|
+
[attr.x2]="point.x"
|
|
1264
|
+
[attr.y2]="effectiveHeight()"
|
|
1265
|
+
/>
|
|
1266
|
+
<circle
|
|
1267
|
+
class="fill-(--foreground) stroke-(--card)"
|
|
1268
|
+
data-chart-active
|
|
1269
|
+
r="4"
|
|
1270
|
+
stroke-width="2"
|
|
1271
|
+
[attr.cx]="point.x"
|
|
1272
|
+
[attr.cy]="point.y"
|
|
1273
|
+
/>
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
</svg>
|
|
1277
|
+
|
|
1278
|
+
@if (active(); as point) {
|
|
1279
|
+
<div
|
|
1280
|
+
class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
|
|
1281
|
+
data-testid="chart-tooltip"
|
|
1282
|
+
role="status"
|
|
1283
|
+
[style]="tooltipStyle(point)"
|
|
1284
|
+
>
|
|
1285
|
+
<p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
|
|
1286
|
+
<p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
|
|
1287
|
+
<p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
|
|
1288
|
+
</div>
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
@if (points().length > 0) {
|
|
1292
|
+
<table class="sr-only">
|
|
1293
|
+
<caption>
|
|
1294
|
+
{{
|
|
1295
|
+
label()
|
|
1296
|
+
}}
|
|
1297
|
+
</caption>
|
|
1298
|
+
<thead>
|
|
1299
|
+
<tr>
|
|
1300
|
+
<th scope="col">{{ timeColumnLabel() }}</th>
|
|
1301
|
+
<th scope="col">{{ valueColumnLabel() }}</th>
|
|
1302
|
+
</tr>
|
|
1303
|
+
</thead>
|
|
1304
|
+
<tbody>
|
|
1305
|
+
@for (point of points(); track point.t) {
|
|
1306
|
+
<tr>
|
|
1307
|
+
<td>{{ timeFormat()(point.t) }}</td>
|
|
1308
|
+
<td>{{ valueFormat()(point.v) }}</td>
|
|
1309
|
+
</tr>
|
|
1310
|
+
}
|
|
1311
|
+
</tbody>
|
|
1312
|
+
</table>
|
|
1313
|
+
}
|
|
1314
|
+
</div>
|
|
1315
|
+
`, isInline: true });
|
|
1316
|
+
}
|
|
1317
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, decorators: [{
|
|
1318
|
+
type: Component,
|
|
1319
|
+
args: [{
|
|
1320
|
+
selector: 'ui-line-chart',
|
|
1321
|
+
template: `
|
|
1322
|
+
<div class="relative h-full">
|
|
1323
|
+
<svg
|
|
1324
|
+
#svgRef
|
|
1325
|
+
class="block h-full w-full touch-pan-y"
|
|
1326
|
+
role="img"
|
|
1327
|
+
tabindex="0"
|
|
1328
|
+
[attr.aria-label]="label()"
|
|
1329
|
+
[attr.height]="effectiveHeight()"
|
|
1330
|
+
[attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
|
|
1331
|
+
[attr.width]="effectiveWidth()"
|
|
1332
|
+
(keydown)="onKeydown($event)"
|
|
1333
|
+
(pointerleave)="onPointerLeave()"
|
|
1334
|
+
(pointermove)="onPointerMove($event)"
|
|
1335
|
+
>
|
|
1336
|
+
@if (geometry(); as geometry) {
|
|
1337
|
+
<path
|
|
1338
|
+
class="stroke-(--foreground)"
|
|
1339
|
+
data-chart-line
|
|
1340
|
+
fill="none"
|
|
1341
|
+
stroke-linecap="round"
|
|
1342
|
+
stroke-linejoin="round"
|
|
1343
|
+
stroke-width="2"
|
|
1344
|
+
[attr.d]="displayLine()"
|
|
1345
|
+
/>
|
|
1346
|
+
|
|
1347
|
+
<line
|
|
1348
|
+
class="stroke-(--border)"
|
|
1349
|
+
data-chart-start-line
|
|
1350
|
+
stroke-dasharray="4 4"
|
|
1351
|
+
stroke-width="1"
|
|
1352
|
+
x1="0"
|
|
1353
|
+
[attr.x2]="effectiveWidth()"
|
|
1354
|
+
[attr.y1]="geometry.start.y"
|
|
1355
|
+
[attr.y2]="geometry.start.y"
|
|
1356
|
+
/>
|
|
1357
|
+
|
|
1358
|
+
@if (startLabelInfo(); as info) {
|
|
1359
|
+
<text
|
|
1360
|
+
class="text-caption fill-(--subtle-foreground)"
|
|
1361
|
+
data-chart-start-label
|
|
1362
|
+
[attr.text-anchor]="info.placement.anchor"
|
|
1363
|
+
[attr.x]="info.placement.x"
|
|
1364
|
+
[attr.y]="info.placement.y"
|
|
1365
|
+
>
|
|
1366
|
+
{{ info.label }}
|
|
1367
|
+
</text>
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
<circle
|
|
1371
|
+
class="fill-(--foreground) stroke-(--card)"
|
|
1372
|
+
data-chart-end
|
|
1373
|
+
r="4"
|
|
1374
|
+
stroke-width="2"
|
|
1375
|
+
[attr.cx]="geometry.end.x"
|
|
1376
|
+
[attr.cy]="geometry.end.y"
|
|
1377
|
+
/>
|
|
1378
|
+
|
|
1379
|
+
@for (
|
|
1380
|
+
tick of ticks();
|
|
1381
|
+
track tick.t;
|
|
1382
|
+
let index = $index;
|
|
1383
|
+
let count = $count;
|
|
1384
|
+
let first = $first;
|
|
1385
|
+
let last = $last
|
|
1386
|
+
) {
|
|
1387
|
+
<text
|
|
1388
|
+
class="text-caption fill-(--subtle-foreground)"
|
|
1389
|
+
data-chart-axis-tick
|
|
1390
|
+
[attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
|
|
1391
|
+
[attr.x]="tick.x"
|
|
1392
|
+
[attr.y]="effectiveHeight() - 4"
|
|
1393
|
+
[class.max-sm:hidden]="!coreTick(index, count)"
|
|
1394
|
+
>
|
|
1395
|
+
{{ axisFormat()(tick.t) }}
|
|
1396
|
+
</text>
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
@if (active(); as point) {
|
|
1400
|
+
<line
|
|
1401
|
+
class="stroke-(--border)"
|
|
1402
|
+
data-chart-crosshair
|
|
1403
|
+
stroke-width="1"
|
|
1404
|
+
y1="0"
|
|
1405
|
+
[attr.x1]="point.x"
|
|
1406
|
+
[attr.x2]="point.x"
|
|
1407
|
+
[attr.y2]="effectiveHeight()"
|
|
1408
|
+
/>
|
|
1409
|
+
<circle
|
|
1410
|
+
class="fill-(--foreground) stroke-(--card)"
|
|
1411
|
+
data-chart-active
|
|
1412
|
+
r="4"
|
|
1413
|
+
stroke-width="2"
|
|
1414
|
+
[attr.cx]="point.x"
|
|
1415
|
+
[attr.cy]="point.y"
|
|
1416
|
+
/>
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
</svg>
|
|
1420
|
+
|
|
1421
|
+
@if (active(); as point) {
|
|
1422
|
+
<div
|
|
1423
|
+
class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
|
|
1424
|
+
data-testid="chart-tooltip"
|
|
1425
|
+
role="status"
|
|
1426
|
+
[style]="tooltipStyle(point)"
|
|
1427
|
+
>
|
|
1428
|
+
<p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
|
|
1429
|
+
<p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
|
|
1430
|
+
<p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
|
|
1431
|
+
</div>
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
@if (points().length > 0) {
|
|
1435
|
+
<table class="sr-only">
|
|
1436
|
+
<caption>
|
|
1437
|
+
{{
|
|
1438
|
+
label()
|
|
1439
|
+
}}
|
|
1440
|
+
</caption>
|
|
1441
|
+
<thead>
|
|
1442
|
+
<tr>
|
|
1443
|
+
<th scope="col">{{ timeColumnLabel() }}</th>
|
|
1444
|
+
<th scope="col">{{ valueColumnLabel() }}</th>
|
|
1445
|
+
</tr>
|
|
1446
|
+
</thead>
|
|
1447
|
+
<tbody>
|
|
1448
|
+
@for (point of points(); track point.t) {
|
|
1449
|
+
<tr>
|
|
1450
|
+
<td>{{ timeFormat()(point.t) }}</td>
|
|
1451
|
+
<td>{{ valueFormat()(point.v) }}</td>
|
|
1452
|
+
</tr>
|
|
1453
|
+
}
|
|
1454
|
+
</tbody>
|
|
1455
|
+
</table>
|
|
1456
|
+
}
|
|
1457
|
+
</div>
|
|
1458
|
+
`,
|
|
1459
|
+
host: {
|
|
1460
|
+
class: 'block h-full',
|
|
1461
|
+
},
|
|
1462
|
+
}]
|
|
1463
|
+
}], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
|
|
1464
|
+
|
|
1465
|
+
const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
|
|
1466
|
+
const ITEM_CLASSES = 'flex w-full items-center gap-2 min-h-(--row-min) px-3 rounded-control text-label hover:bg-(--soft) focus-visible:bg-(--soft) outline-none';
|
|
1467
|
+
const nextId = (() => {
|
|
1468
|
+
let count = 0;
|
|
1469
|
+
return () => `ui-menu-${++count}`;
|
|
1470
|
+
})();
|
|
1471
|
+
/**
|
|
1472
|
+
* One action of a `ui-menu`; `destructive` paints it in `--negative`.
|
|
1473
|
+
*
|
|
1474
|
+
* @example
|
|
1475
|
+
* <button uiMenuItem destructive (click)="remove()">Delete the line</button>
|
|
1476
|
+
*/
|
|
1477
|
+
class UiMenuItem {
|
|
1478
|
+
destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1479
|
+
#host = inject(ElementRef).nativeElement;
|
|
1480
|
+
#menu = inject(UiMenu);
|
|
1481
|
+
classes = computed(() => `${ITEM_CLASSES}${this.destructive() ? ' text-(--negative)' : ''}`, /* @ts-ignore */
|
|
1482
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
1483
|
+
focus() {
|
|
1484
|
+
this.#host.focus();
|
|
1485
|
+
}
|
|
1486
|
+
hasFocus() {
|
|
1487
|
+
return this.#host === document.activeElement;
|
|
1488
|
+
}
|
|
1489
|
+
onClick() {
|
|
1490
|
+
this.#menu.close();
|
|
1491
|
+
}
|
|
1492
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1493
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuItem, isStandalone: true, selector: "button[uiMenuItem]", inputs: { destructive: { classPropertyName: "destructive", publicName: "destructive", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
|
|
1494
|
+
}
|
|
1495
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuItem, decorators: [{
|
|
1496
|
+
type: Directive,
|
|
1497
|
+
args: [{
|
|
1498
|
+
selector: 'button[uiMenuItem]',
|
|
1499
|
+
host: {
|
|
1500
|
+
role: 'menuitem',
|
|
1501
|
+
tabindex: '-1',
|
|
1502
|
+
'[class]': 'classes()',
|
|
1503
|
+
'(click)': 'onClick()',
|
|
1504
|
+
},
|
|
1505
|
+
}]
|
|
1506
|
+
}], propDecorators: { destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }] } });
|
|
1507
|
+
/**
|
|
1508
|
+
* Secondary actions menu, opened from its trigger through the native Popover API.
|
|
1509
|
+
*
|
|
1510
|
+
* @example
|
|
1511
|
+
* <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
|
|
1512
|
+
* <ui-menu #menu label="Line actions">
|
|
1513
|
+
* <button uiMenuItem (click)="edit()">Edit</button>
|
|
1514
|
+
* <button uiMenuItem destructive (click)="remove()">Delete the line</button>
|
|
1515
|
+
* </ui-menu>
|
|
1516
|
+
*/
|
|
1517
|
+
class UiMenu {
|
|
1518
|
+
label = input.required(/* @ts-ignore */
|
|
1519
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1520
|
+
menuId = nextId();
|
|
1521
|
+
isOpen = signal(false, /* @ts-ignore */
|
|
1522
|
+
...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
1523
|
+
classes = MENU_CLASSES;
|
|
1524
|
+
items = contentChildren(UiMenuItem, /* @ts-ignore */
|
|
1525
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1526
|
+
#host = inject(ElementRef).nativeElement;
|
|
1527
|
+
#trigger = null;
|
|
1528
|
+
open(trigger) {
|
|
1529
|
+
this.#trigger = trigger;
|
|
1530
|
+
this.isOpen.set(true);
|
|
1531
|
+
if (typeof this.#host.showPopover === 'function') {
|
|
1532
|
+
this.#host.showPopover();
|
|
1533
|
+
}
|
|
1534
|
+
this.#position(trigger);
|
|
1535
|
+
this.items()[0]?.focus();
|
|
1536
|
+
}
|
|
1537
|
+
close() {
|
|
1538
|
+
const wasOpen = this.isOpen();
|
|
1539
|
+
this.#applyClosed();
|
|
1540
|
+
if (wasOpen && typeof this.#host.hidePopover === 'function') {
|
|
1541
|
+
this.#host.hidePopover();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
#applyClosed() {
|
|
1545
|
+
this.isOpen.set(false);
|
|
1546
|
+
this.#trigger?.focus();
|
|
1547
|
+
this.#trigger = null;
|
|
1548
|
+
}
|
|
1549
|
+
onToggle(event) {
|
|
1550
|
+
if (event.newState === 'closed') {
|
|
1551
|
+
this.#applyClosed();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
onKeydown(event) {
|
|
1555
|
+
if (event.key === 'Tab' || event.key === 'Escape') {
|
|
1556
|
+
event.preventDefault();
|
|
1557
|
+
this.close();
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const items = this.items();
|
|
1561
|
+
const currentIndex = items.findIndex((item) => item.hasFocus());
|
|
1562
|
+
const lastIndex = items.length - 1;
|
|
1563
|
+
let nextIndex;
|
|
1564
|
+
switch (event.key) {
|
|
1565
|
+
case 'ArrowDown':
|
|
1566
|
+
nextIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;
|
|
1567
|
+
break;
|
|
1568
|
+
case 'ArrowUp':
|
|
1569
|
+
nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;
|
|
1570
|
+
break;
|
|
1571
|
+
case 'Home':
|
|
1572
|
+
nextIndex = 0;
|
|
1573
|
+
break;
|
|
1574
|
+
case 'End':
|
|
1575
|
+
nextIndex = lastIndex;
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
if (nextIndex === undefined) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
event.preventDefault();
|
|
1582
|
+
items[nextIndex]?.focus();
|
|
1583
|
+
}
|
|
1584
|
+
#position(trigger) {
|
|
1585
|
+
const margin = 8;
|
|
1586
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
1587
|
+
const menuRect = this.#host.getBoundingClientRect();
|
|
1588
|
+
const fitsBelow = window.innerHeight - triggerRect.bottom >= menuRect.height + margin;
|
|
1589
|
+
const top = fitsBelow ? triggerRect.bottom + 4 : Math.max(margin, triggerRect.top - menuRect.height - 4);
|
|
1590
|
+
const left = Math.min(Math.max(triggerRect.right - menuRect.width, margin), window.innerWidth - menuRect.width - margin);
|
|
1591
|
+
this.#host.style.position = 'fixed';
|
|
1592
|
+
this.#host.style.top = `${Math.min(top, window.innerHeight - menuRect.height - margin)}px`;
|
|
1593
|
+
this.#host.style.left = `${left}px`;
|
|
1594
|
+
this.#host.style.transformOrigin = fitsBelow ? 'top right' : 'bottom right';
|
|
1595
|
+
}
|
|
1596
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1597
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.7", type: UiMenu, isStandalone: true, selector: "ui-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" }, listeners: { "toggle": "onToggle($event)", "keydown": "onKeydown($event)" }, properties: { "id": "menuId", "class": "classes", "attr.aria-label": "label()" } }, queries: [{ propertyName: "items", predicate: UiMenuItem, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true });
|
|
1598
|
+
}
|
|
1599
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenu, decorators: [{
|
|
1600
|
+
type: Component,
|
|
1601
|
+
args: [{
|
|
1602
|
+
selector: 'ui-menu',
|
|
1603
|
+
template: '<ng-content />',
|
|
1604
|
+
host: {
|
|
1605
|
+
role: 'menu',
|
|
1606
|
+
popover: 'auto',
|
|
1607
|
+
'[id]': 'menuId',
|
|
1608
|
+
'[class]': 'classes',
|
|
1609
|
+
'[attr.aria-label]': 'label()',
|
|
1610
|
+
'(toggle)': 'onToggle($event)',
|
|
1611
|
+
'(keydown)': 'onKeydown($event)',
|
|
1612
|
+
},
|
|
1613
|
+
}]
|
|
1614
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => UiMenuItem), { isSignal: true }] }] } });
|
|
1615
|
+
/**
|
|
1616
|
+
* Opens a `ui-menu` from a trigger element and wires `aria-haspopup`, `aria-expanded` and
|
|
1617
|
+
* `aria-controls` on it.
|
|
1618
|
+
*
|
|
1619
|
+
* @example
|
|
1620
|
+
* <button ui-button size="icon" aria-label="More" [uiMenuTrigger]="menu">...</button>
|
|
1621
|
+
*/
|
|
1622
|
+
class UiMenuTrigger {
|
|
1623
|
+
uiMenuTrigger = input.required(/* @ts-ignore */
|
|
1624
|
+
...(ngDevMode ? [{ debugName: "uiMenuTrigger" }] : /* istanbul ignore next */ []));
|
|
1625
|
+
#host = inject(ElementRef).nativeElement;
|
|
1626
|
+
toggle() {
|
|
1627
|
+
const menu = this.uiMenuTrigger();
|
|
1628
|
+
if (menu.isOpen()) {
|
|
1629
|
+
menu.close();
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
menu.open(this.#host);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1636
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiMenuTrigger, isStandalone: true, selector: "[uiMenuTrigger]", inputs: { uiMenuTrigger: { classPropertyName: "uiMenuTrigger", publicName: "uiMenuTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "menu" }, listeners: { "click": "toggle()" }, properties: { "attr.aria-expanded": "uiMenuTrigger().isOpen()", "attr.aria-controls": "uiMenuTrigger().menuId" } }, ngImport: i0 });
|
|
1637
|
+
}
|
|
1638
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiMenuTrigger, decorators: [{
|
|
1639
|
+
type: Directive,
|
|
1640
|
+
args: [{
|
|
1641
|
+
selector: '[uiMenuTrigger]',
|
|
1642
|
+
host: {
|
|
1643
|
+
'aria-haspopup': 'menu',
|
|
1644
|
+
'[attr.aria-expanded]': 'uiMenuTrigger().isOpen()',
|
|
1645
|
+
'[attr.aria-controls]': 'uiMenuTrigger().menuId',
|
|
1646
|
+
'(click)': 'toggle()',
|
|
1647
|
+
},
|
|
1648
|
+
}]
|
|
1649
|
+
}], propDecorators: { uiMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiMenuTrigger", required: true }] }] } });
|
|
1650
|
+
|
|
611
1651
|
/** Ramp step used to paint the fill. `MeterTone` is derived from this tuple. */
|
|
612
1652
|
const METER_TONES = [1, 2, 3, 4, 5, 6];
|
|
613
1653
|
const TONE_CLASSES = {
|
|
@@ -673,11 +1713,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
673
1713
|
}]
|
|
674
1714
|
}], 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 }] }] } });
|
|
675
1715
|
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1716
|
+
const BASE_CLASSES$1 = 'flex items-center gap-3 h-10 px-3 rounded-control text-label font-medium text-(--muted-foreground) transition-colors duration-(--duration-press) hover:bg-(--glow) hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
|
|
1717
|
+
const ACTIVE_CLASSES = 'bg-(--soft) text-(--foreground)';
|
|
1718
|
+
/**
|
|
1719
|
+
* One destination of the sidebar navigation (Portfolio, Holdings, Allocation, Accounts).
|
|
1720
|
+
*
|
|
1721
|
+
* @example
|
|
1722
|
+
* <a ui-nav-item [active]="url === '/'" routerLink="/">
|
|
1723
|
+
* <lucide-icon navIcon name="house" />
|
|
1724
|
+
* Portfolio
|
|
1725
|
+
* </a>
|
|
1726
|
+
*/
|
|
1727
|
+
class UiNavItem {
|
|
1728
|
+
active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1729
|
+
classes = computed(() => `${BASE_CLASSES$1}${this.active() ? ` ${ACTIVE_CLASSES}` : ''}`, /* @ts-ignore */
|
|
1730
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
1731
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1732
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiNavItem, isStandalone: true, selector: "a[ui-nav-item]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: '<ng-content select="[navIcon]" /><ng-content />', isInline: true });
|
|
1733
|
+
}
|
|
1734
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiNavItem, decorators: [{
|
|
1735
|
+
type: Component,
|
|
1736
|
+
args: [{
|
|
1737
|
+
selector: 'a[ui-nav-item]',
|
|
1738
|
+
template: '<ng-content select="[navIcon]" /><ng-content />',
|
|
1739
|
+
host: {
|
|
1740
|
+
'[class]': 'classes()',
|
|
1741
|
+
'[attr.aria-current]': "active() ? 'page' : null",
|
|
1742
|
+
},
|
|
1743
|
+
}]
|
|
1744
|
+
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
|
|
1745
|
+
|
|
1746
|
+
const BASE_CLASSES = 'flex w-full items-center gap-2.5 min-h-14 px-2.5 py-1.5 rounded-control text-left select-none touch-manipulation transition-colors duration-(--duration-press) hover:bg-(--glow) active:bg-(--soft) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
|
|
1747
|
+
const SELECTED_CLASSES$1 = 'bg-(--soft)';
|
|
1748
|
+
/**
|
|
1749
|
+
* Clickable row of a list: a holding, an envelope, an account.
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* <a ui-row [selected]="holding.id === openId()" [routerLink]="['/holdings', holding.id]">
|
|
1753
|
+
* {{ holding.name }}
|
|
1754
|
+
* </a>
|
|
1755
|
+
*/
|
|
1756
|
+
class UiRow {
|
|
1757
|
+
selected = input(false, { ...(ngDevMode ? { debugName: "selected" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1758
|
+
classes = computed(() => `${BASE_CLASSES}${this.selected() ? ` ${SELECTED_CLASSES$1}` : ''}`, /* @ts-ignore */
|
|
1759
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
1760
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1761
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiRow, isStandalone: true, selector: "a[ui-row], button[ui-row]", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "selected() ? 'true' : null" } }, ngImport: i0, template: '<ng-content />', isInline: true });
|
|
1762
|
+
}
|
|
1763
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiRow, decorators: [{
|
|
1764
|
+
type: Component,
|
|
1765
|
+
args: [{
|
|
1766
|
+
selector: 'a[ui-row], button[ui-row]',
|
|
1767
|
+
template: '<ng-content />',
|
|
1768
|
+
host: {
|
|
1769
|
+
'[class]': 'classes()',
|
|
1770
|
+
'[attr.aria-current]': "selected() ? 'true' : null",
|
|
1771
|
+
},
|
|
1772
|
+
}]
|
|
1773
|
+
}], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }] } });
|
|
1774
|
+
|
|
1775
|
+
const OPTION_CLASSES = "relative z-10 min-h-9 flex-1 cursor-pointer px-3 text-label font-medium transition-colors after:absolute after:inset-x-0 after:top-1/2 after:h-(--row-min) after:-translate-y-1/2 after:content-[''] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring)";
|
|
681
1776
|
const SELECTED_CLASSES = 'text-(--foreground)';
|
|
682
1777
|
const UNSELECTED_CLASSES = 'text-(--muted-foreground) hover:text-(--foreground)';
|
|
683
1778
|
/**
|
|
@@ -696,7 +1791,6 @@ class UiSegmented {
|
|
|
696
1791
|
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
697
1792
|
radios = viewChildren('radio', /* @ts-ignore */
|
|
698
1793
|
...(ngDevMode ? [{ debugName: "radios" }] : /* istanbul ignore next */ []));
|
|
699
|
-
// 1-based so 0 (falsy) reliably means "no match", letting @if skip the thumb entirely.
|
|
700
1794
|
thumbIndex = computed(() => {
|
|
701
1795
|
const index = this.options().findIndex((option) => option.value === this.value());
|
|
702
1796
|
return index === -1 ? 0 : index + 1;
|
|
@@ -864,16 +1958,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
864
1958
|
/** Available skeleton shapes. `SkeletonShape` is derived from this tuple. */
|
|
865
1959
|
const SKELETON_SHAPES = ['text', 'figure', 'row', 'chart', 'ring'];
|
|
866
1960
|
const LAST_LINE_WIDTH = '62%';
|
|
867
|
-
// Declared here, not in tokens.css (a pure token sheet holds no element styles or keyframes).
|
|
868
1961
|
const PULSE_ANIMATION = 'cairn-pulse var(--pulse-duration) var(--ease-out) infinite alternate';
|
|
869
1962
|
const BAR_CLASSES = 'block rounded-control bg-(--muted)';
|
|
870
1963
|
/**
|
|
871
|
-
* Decorative placeholder for content that is still loading, at the shape of the content
|
|
872
|
-
* in for.
|
|
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.
|
|
1964
|
+
* Decorative, pulsing placeholder for content that is still loading, at the shape of the content
|
|
1965
|
+
* it stands in for.
|
|
877
1966
|
*
|
|
878
1967
|
* @example
|
|
879
1968
|
* <ui-skeleton shape="figure" />
|
|
@@ -1011,6 +2100,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1011
2100
|
}]
|
|
1012
2101
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
1013
2102
|
|
|
2103
|
+
const CLASSES = "relative shrink-0 w-[51px] h-[31px] appearance-none cursor-pointer rounded-pill bg-(--muted) checked:bg-(--primary) transition-colors duration-(--duration-fast) ease-out disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2 before:h-(--row-min) before:content-[''] after:absolute after:top-0.5 after:left-0.5 after:size-[27px] after:rounded-pill after:bg-(--card) after:shadow-[0_1px_3px_rgb(0_0_0/0.2)] after:transition-transform after:duration-(--duration-fast) after:ease-out after:content-[''] checked:after:translate-x-[20px]";
|
|
2104
|
+
/**
|
|
2105
|
+
* Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role="switch"`.
|
|
2106
|
+
*
|
|
2107
|
+
* @example
|
|
2108
|
+
* <input type="checkbox" uiSwitch [checked]="hidden()" (change)="toggle()" aria-label="Hide amounts" />
|
|
2109
|
+
*/
|
|
2110
|
+
class UiSwitch {
|
|
2111
|
+
classes = CLASSES;
|
|
2112
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2113
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiSwitch, isStandalone: true, selector: "input[type=checkbox][uiSwitch]", host: { attributes: { "role": "switch" }, properties: { "class": "classes" } }, ngImport: i0 });
|
|
2114
|
+
}
|
|
2115
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiSwitch, decorators: [{
|
|
2116
|
+
type: Directive,
|
|
2117
|
+
args: [{
|
|
2118
|
+
selector: 'input[type=checkbox][uiSwitch]',
|
|
2119
|
+
host: {
|
|
2120
|
+
role: 'switch',
|
|
2121
|
+
'[class]': 'classes',
|
|
2122
|
+
},
|
|
2123
|
+
}]
|
|
2124
|
+
}] });
|
|
2125
|
+
|
|
1014
2126
|
/** Breakpoints a secondary column can be held back until. */
|
|
1015
2127
|
const CELL_BREAKPOINTS = ['md', 'lg'];
|
|
1016
2128
|
const FROM_CLASSES = {
|
|
@@ -1105,6 +2217,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1105
2217
|
}]
|
|
1106
2218
|
}], propDecorators: { numeric: [{ type: i0.Input, args: [{ isSignal: true, alias: "numeric", required: false }] }], from: [{ type: i0.Input, args: [{ isSignal: true, alias: "from", required: false }] }] } });
|
|
1107
2219
|
|
|
2220
|
+
const TAB_CLASSES = 'flex h-[52px] flex-col items-center justify-center gap-0.5 touch-manipulation select-none text-(--muted-foreground) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
|
|
2221
|
+
const ACTIVE_TAB_CLASSES = 'font-semibold text-(--foreground)';
|
|
2222
|
+
/**
|
|
2223
|
+
* Bottom navigation bar for iPhone: 52px of tabs plus the home indicator's safe area.
|
|
2224
|
+
*
|
|
2225
|
+
* @example
|
|
2226
|
+
* <nav ui-tab-bar>
|
|
2227
|
+
* <a ui-tab [active]="url === '/'" routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
|
|
2228
|
+
* </nav>
|
|
2229
|
+
*/
|
|
2230
|
+
class UiTabBar {
|
|
2231
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2232
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: UiTabBar, isStandalone: true, selector: "nav[ui-tab-bar]", host: { classAttribute: "grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]" }, ngImport: i0, template: '<ng-content />', isInline: true });
|
|
2233
|
+
}
|
|
2234
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTabBar, decorators: [{
|
|
2235
|
+
type: Component,
|
|
2236
|
+
args: [{
|
|
2237
|
+
selector: 'nav[ui-tab-bar]',
|
|
2238
|
+
template: '<ng-content />',
|
|
2239
|
+
host: {
|
|
2240
|
+
class: 'grid grid-flow-col auto-cols-fr items-stretch bg-(--background)/90 backdrop-blur [@media(prefers-reduced-transparency:reduce)]:bg-(--background) shadow-[inset_0_1px_0_var(--hairline)] pb-[env(safe-area-inset-bottom)]',
|
|
2241
|
+
},
|
|
2242
|
+
}]
|
|
2243
|
+
}] });
|
|
2244
|
+
/**
|
|
2245
|
+
* One destination of `ui-tab-bar`.
|
|
2246
|
+
*
|
|
2247
|
+
* @example
|
|
2248
|
+
* <a ui-tab active routerLink="/"><lucide-icon tabIcon name="house" />Portfolio</a>
|
|
2249
|
+
*/
|
|
2250
|
+
class UiTab {
|
|
2251
|
+
active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2252
|
+
classes = computed(() => `${TAB_CLASSES}${this.active() ? ` ${ACTIVE_TAB_CLASSES}` : ''}`, /* @ts-ignore */
|
|
2253
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
2254
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2255
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.7", type: UiTab, isStandalone: true, selector: "a[ui-tab]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.aria-current": "active() ? 'page' : null" } }, ngImport: i0, template: `
|
|
2256
|
+
<span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
|
|
2257
|
+
<span class="text-caption" data-tab-label><ng-content /></span>
|
|
2258
|
+
`, isInline: true });
|
|
2259
|
+
}
|
|
2260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTab, decorators: [{
|
|
2261
|
+
type: Component,
|
|
2262
|
+
args: [{
|
|
2263
|
+
selector: 'a[ui-tab]',
|
|
2264
|
+
template: `
|
|
2265
|
+
<span class="size-6" data-tab-icon><ng-content select="[tabIcon]" /></span>
|
|
2266
|
+
<span class="text-caption" data-tab-label><ng-content /></span>
|
|
2267
|
+
`,
|
|
2268
|
+
host: {
|
|
2269
|
+
'[class]': 'classes()',
|
|
2270
|
+
'[attr.aria-current]': "active() ? 'page' : null",
|
|
2271
|
+
},
|
|
2272
|
+
}]
|
|
2273
|
+
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }] } });
|
|
2274
|
+
|
|
1108
2275
|
/*
|
|
1109
2276
|
* Public API surface of the ui library.
|
|
1110
2277
|
*/
|
|
@@ -1113,5 +2280,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1113
2280
|
* Generated bundle index. Do not edit.
|
|
1114
2281
|
*/
|
|
1115
2282
|
|
|
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 };
|
|
2283
|
+
export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
|
|
1117
2284
|
//# sourceMappingURL=joanroucoux-cairn-ui.mjs.map
|