@ternkit/tern 1.3.0 → 1.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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.js +168 -0
- package/dist/server/intl.js +55 -12
- package/dist/ui/AlertDialog.svelte +79 -0
- package/dist/ui/AlertDialog.svelte.d.ts +24 -0
- package/dist/ui/AreaChart.svelte +9 -0
- package/dist/ui/AreaChart.svelte.d.ts +26 -0
- package/dist/ui/AspectRatio.svelte +33 -0
- package/dist/ui/AspectRatio.svelte.d.ts +10 -0
- package/dist/ui/BarChart.svelte +170 -0
- package/dist/ui/BarChart.svelte.d.ts +40 -0
- package/dist/ui/Breadcrumb.svelte +92 -0
- package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
- package/dist/ui/ButtonGroup.svelte +61 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
- package/dist/ui/Calendar.svelte +265 -0
- package/dist/ui/Calendar.svelte.d.ts +28 -0
- package/dist/ui/Carousel.svelte +169 -0
- package/dist/ui/Carousel.svelte.d.ts +36 -0
- package/dist/ui/ChartFrame.svelte +174 -0
- package/dist/ui/ChartFrame.svelte.d.ts +24 -0
- package/dist/ui/Checkbox.svelte +13 -1
- package/dist/ui/Collapsible.svelte +59 -0
- package/dist/ui/Collapsible.svelte.d.ts +12 -0
- package/dist/ui/Combobox.svelte +350 -0
- package/dist/ui/Combobox.svelte.d.ts +24 -0
- package/dist/ui/Command.svelte +268 -0
- package/dist/ui/Command.svelte.d.ts +28 -0
- package/dist/ui/ConsentScreen.svelte +125 -0
- package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
- package/dist/ui/ContextMenu.svelte +78 -0
- package/dist/ui/ContextMenu.svelte.d.ts +13 -0
- package/dist/ui/DataTable.svelte +277 -0
- package/dist/ui/DataTable.svelte.d.ts +53 -0
- package/dist/ui/DatePicker.svelte +138 -0
- package/dist/ui/DatePicker.svelte.d.ts +25 -0
- package/dist/ui/Dialog.svelte +168 -27
- package/dist/ui/Dialog.svelte.d.ts +10 -0
- package/dist/ui/DonutChart.svelte +115 -0
- package/dist/ui/DonutChart.svelte.d.ts +40 -0
- package/dist/ui/Drawer.svelte +19 -0
- package/dist/ui/Drawer.svelte.d.ts +14 -0
- package/dist/ui/Empty.svelte +62 -0
- package/dist/ui/Empty.svelte.d.ts +15 -0
- package/dist/ui/Field.svelte +22 -2
- package/dist/ui/Field.svelte.d.ts +2 -0
- package/dist/ui/Form.svelte +159 -0
- package/dist/ui/Form.svelte.d.ts +21 -0
- package/dist/ui/HoverCard.svelte +106 -0
- package/dist/ui/HoverCard.svelte.d.ts +26 -0
- package/dist/ui/Input.svelte +2 -1
- package/dist/ui/InputGroup.svelte +80 -0
- package/dist/ui/InputGroup.svelte.d.ts +13 -0
- package/dist/ui/InputOTP.svelte +140 -0
- package/dist/ui/InputOTP.svelte.d.ts +18 -0
- package/dist/ui/Item.svelte +86 -0
- package/dist/ui/Item.svelte.d.ts +17 -0
- package/dist/ui/Label.svelte +29 -0
- package/dist/ui/Label.svelte.d.ts +10 -0
- package/dist/ui/LineChart.svelte +171 -0
- package/dist/ui/LineChart.svelte.d.ts +42 -0
- package/dist/ui/Menu.svelte +22 -151
- package/dist/ui/Menu.svelte.d.ts +3 -10
- package/dist/ui/MenuList.svelte +317 -0
- package/dist/ui/MenuList.svelte.d.ts +29 -0
- package/dist/ui/Menubar.svelte +133 -0
- package/dist/ui/Menubar.svelte.d.ts +14 -0
- package/dist/ui/NavigationMenu.svelte +208 -0
- package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
- package/dist/ui/Pagination.svelte +114 -0
- package/dist/ui/Pagination.svelte.d.ts +16 -0
- package/dist/ui/PasswordInput.svelte +134 -0
- package/dist/ui/PasswordInput.svelte.d.ts +11 -0
- package/dist/ui/Popover.svelte +127 -0
- package/dist/ui/Popover.svelte.d.ts +29 -0
- package/dist/ui/Resizable.svelte +122 -0
- package/dist/ui/Resizable.svelte.d.ts +16 -0
- package/dist/ui/ScrollArea.svelte +43 -0
- package/dist/ui/ScrollArea.svelte.d.ts +13 -0
- package/dist/ui/Select.svelte +3 -2
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Sheet.svelte +21 -0
- package/dist/ui/Sheet.svelte.d.ts +16 -0
- package/dist/ui/Sidebar.svelte +263 -0
- package/dist/ui/Sidebar.svelte.d.ts +35 -0
- package/dist/ui/SignIn.svelte +182 -0
- package/dist/ui/SignIn.svelte.d.ts +38 -0
- package/dist/ui/SignUp.svelte +106 -0
- package/dist/ui/SignUp.svelte.d.ts +23 -0
- package/dist/ui/Slider.svelte +196 -0
- package/dist/ui/Slider.svelte.d.ts +19 -0
- package/dist/ui/Sparkline.svelte +23 -0
- package/dist/ui/Sparkline.svelte.d.ts +11 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Textarea.svelte +2 -1
- package/dist/ui/Toggle.svelte +82 -0
- package/dist/ui/Toggle.svelte.d.ts +17 -0
- package/dist/ui/ToggleGroup.svelte +143 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
- package/dist/ui/Tooltip.svelte +27 -19
- package/dist/ui/Tooltip.svelte.d.ts +2 -1
- package/dist/ui/chart.d.ts +26 -0
- package/dist/ui/chart.js +89 -0
- package/dist/ui/dates.d.ts +13 -0
- package/dist/ui/dates.js +64 -0
- package/dist/ui/field.d.ts +8 -0
- package/dist/ui/floating.d.ts +44 -0
- package/dist/ui/floating.js +97 -0
- package/dist/ui/form.d.ts +12 -0
- package/dist/ui/form.js +8 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +43 -0
- package/dist/ui/menu.d.ts +25 -0
- package/dist/ui/menu.js +1 -0
- package/dist/ui/options.d.ts +17 -0
- package/dist/ui/options.js +22 -0
- package/dist/ui/table.d.ts +26 -0
- package/dist/ui/table.js +1 -0
- package/dist/ui/theme.css +25 -0
- package/dist/vite/index.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
/** Makes the whole item a link; actions stay separate buttons. */
|
|
8
|
+
href?: string;
|
|
9
|
+
/** An avatar, icon or thumbnail at the start. */
|
|
10
|
+
media?: Snippet;
|
|
11
|
+
/** Buttons or a badge at the end. */
|
|
12
|
+
actions?: Snippet;
|
|
13
|
+
/** li inside a ul or ol, div elsewhere. */
|
|
14
|
+
as?: 'div' | 'li';
|
|
15
|
+
class?: string;
|
|
16
|
+
};
|
|
17
|
+
let { title, description, href, media, actions, as = 'div', class: klass = '' }: Props = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<!-- A row with media, a title, a description and actions, for lists of people, files or settings. -->
|
|
21
|
+
<svelte:element this={as} class="tern-item {klass}" class:link={!!href}>
|
|
22
|
+
{#if media}<div class="media">{@render media()}</div>{/if}
|
|
23
|
+
<div class="text">
|
|
24
|
+
{#if href}<a {href} class="title">{title}</a>{:else}<span class="title">{title}</span>{/if}
|
|
25
|
+
{#if description}<span class="description">{description}</span>{/if}
|
|
26
|
+
</div>
|
|
27
|
+
{#if actions}<div class="actions">{@render actions()}</div>{/if}
|
|
28
|
+
</svelte:element>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.tern-item {
|
|
32
|
+
position: relative;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 0.75rem;
|
|
36
|
+
padding: 0.75rem;
|
|
37
|
+
border-radius: var(--tern-radius-sm);
|
|
38
|
+
list-style: none;
|
|
39
|
+
}
|
|
40
|
+
.link:hover {
|
|
41
|
+
background: var(--tern-surface-2);
|
|
42
|
+
}
|
|
43
|
+
.text {
|
|
44
|
+
display: grid;
|
|
45
|
+
flex: 1;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
}
|
|
48
|
+
.title {
|
|
49
|
+
font-weight: 550;
|
|
50
|
+
color: var(--tern-fg);
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
/* The link covers the item, so it's one big target. */
|
|
57
|
+
a.title::after {
|
|
58
|
+
content: '';
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: 0;
|
|
61
|
+
border-radius: inherit;
|
|
62
|
+
}
|
|
63
|
+
a.title:focus-visible::after {
|
|
64
|
+
outline: 2px solid var(--tern-ring);
|
|
65
|
+
outline-offset: -2px;
|
|
66
|
+
}
|
|
67
|
+
a.title:focus-visible {
|
|
68
|
+
outline: none;
|
|
69
|
+
}
|
|
70
|
+
.description {
|
|
71
|
+
font-size: 0.8125rem;
|
|
72
|
+
color: var(--tern-muted);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
.actions {
|
|
78
|
+
position: relative;
|
|
79
|
+
z-index: 1;
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 0.375rem;
|
|
82
|
+
}
|
|
83
|
+
.media {
|
|
84
|
+
flex: none;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
/** Makes the whole item a link; actions stay separate buttons. */
|
|
6
|
+
href?: string;
|
|
7
|
+
/** An avatar, icon or thumbnail at the start. */
|
|
8
|
+
media?: Snippet;
|
|
9
|
+
/** Buttons or a badge at the end. */
|
|
10
|
+
actions?: Snippet;
|
|
11
|
+
/** li inside a ul or ol, div elsewhere. */
|
|
12
|
+
as?: 'div' | 'li';
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const Item: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type Item = ReturnType<typeof Item>;
|
|
17
|
+
export default Item;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLLabelAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = HTMLLabelAttributes & {
|
|
6
|
+
/** Marks the control as required, with an asterisk screen readers skip (the control says it's required). */
|
|
7
|
+
required?: boolean;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
};
|
|
10
|
+
let { required = false, class: klass = '', children, ...rest }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<!-- A label on its own, for layouts Field doesn't fit. Point it at the control with for. -->
|
|
14
|
+
<label class="tern-label {klass}" {...rest}>
|
|
15
|
+
{@render children()}
|
|
16
|
+
{#if required}<span class="required" aria-hidden="true">*</span>{/if}
|
|
17
|
+
</label>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.tern-label {
|
|
21
|
+
font-size: 0.875rem;
|
|
22
|
+
font-weight: 550;
|
|
23
|
+
color: var(--tern-fg);
|
|
24
|
+
}
|
|
25
|
+
.required {
|
|
26
|
+
color: var(--tern-danger);
|
|
27
|
+
margin-inline-start: 0.125rem;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLLabelAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = HTMLLabelAttributes & {
|
|
4
|
+
/** Marks the control as required, with an asterisk screen readers skip (the control says it's required). */
|
|
5
|
+
required?: boolean;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Label: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Label = ReturnType<typeof Label>;
|
|
10
|
+
export default Label;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { lastIndex, formatNumber, formatX, isTime, niceTicks, numericX, seriesColor, textWidth, timeTicks, type Series, type XValue } from './chart.js';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
data: Row[];
|
|
7
|
+
/** The field on the x axis: dates (Date or ISO strings), numbers or categories. */
|
|
8
|
+
x: keyof Row & string;
|
|
9
|
+
/** Up to eight series; each is a field of numbers. */
|
|
10
|
+
series: Series<Row>[];
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
level?: 2 | 3 | 4;
|
|
14
|
+
/** Fill under the lines. */
|
|
15
|
+
area?: boolean;
|
|
16
|
+
/** Start the y axis at zero. Turn it off for values far from zero, such as a temperature. */
|
|
17
|
+
zero?: boolean;
|
|
18
|
+
height?: number;
|
|
19
|
+
/** Formats y values on the axis, in tooltips and in the table. */
|
|
20
|
+
format?: (v: number) => string;
|
|
21
|
+
class?: string;
|
|
22
|
+
};
|
|
23
|
+
let { data, x, series, title, description, level, area = false, zero = true, height = 240, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
let width = $state(640);
|
|
26
|
+
let active = $state<number | null>(null);
|
|
27
|
+
|
|
28
|
+
const field = (r: Row, k: string) => (r as Record<string, unknown>)[k];
|
|
29
|
+
const xs = $derived(data.map((r) => field(r, x) as XValue));
|
|
30
|
+
const time = $derived(isTime(xs));
|
|
31
|
+
const nx = $derived(numericX(xs));
|
|
32
|
+
const values = $derived(
|
|
33
|
+
series.map((s) =>
|
|
34
|
+
data.map((r) => {
|
|
35
|
+
const v = field(r, s.key);
|
|
36
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
const all = $derived(values.flat().filter((v): v is number => v !== null));
|
|
41
|
+
const yTicks = $derived(niceTicks(zero || area ? Math.min(0, ...all) : Math.min(...all), Math.max(zero || area ? 0 : -Infinity, ...all), Math.max(3, Math.round(height / 60))));
|
|
42
|
+
const yMin = $derived(yTicks[0]!);
|
|
43
|
+
const yMax = $derived(yTicks[yTicks.length - 1]!);
|
|
44
|
+
|
|
45
|
+
// Direct labels at the line ends for up to four series, unless they'd collide.
|
|
46
|
+
const top = 10;
|
|
47
|
+
const bottom = 24;
|
|
48
|
+
const left = $derived(Math.max(...yTicks.map((t) => textWidth(format(t)))) + 10);
|
|
49
|
+
const ends = $derived(values.map((v) => [...v].reverse().find((n) => n !== null) ?? null));
|
|
50
|
+
const endY = (v: number) => top + (1 - (v - yMin) / (yMax - yMin || 1)) * (height - top - bottom);
|
|
51
|
+
const direct = $derived.by(() => {
|
|
52
|
+
if (series.length > 4) return false;
|
|
53
|
+
const ys = ends.filter((e): e is number => e !== null).map(endY).sort((a, b) => a - b);
|
|
54
|
+
return ys.every((y, i) => i === 0 || y - ys[i - 1]! >= 14);
|
|
55
|
+
});
|
|
56
|
+
const endLabels = $derived(series.map((s, i) => (series.length === 1 ? (ends[i] === null ? '' : format(ends[i]!)) : s.label)));
|
|
57
|
+
const right = $derived(direct ? Math.max(...endLabels.map(textWidth)) + 14 : 8);
|
|
58
|
+
|
|
59
|
+
const plotW = $derived(Math.max(10, width - left - right));
|
|
60
|
+
const plotH = $derived(height - top - bottom);
|
|
61
|
+
const xMin = $derived(nx ? Math.min(...nx) : 0);
|
|
62
|
+
const xMax = $derived(nx ? Math.max(...nx) : Math.max(1, data.length - 1));
|
|
63
|
+
const px = (i: number) => left + (((nx ? nx[i]! : i) - xMin) / (xMax - xMin || 1)) * plotW;
|
|
64
|
+
const py = (v: number) => top + (1 - (v - yMin) / (yMax - yMin || 1)) * plotH;
|
|
65
|
+
|
|
66
|
+
const xTicks = $derived.by(() => {
|
|
67
|
+
if (time && nx) {
|
|
68
|
+
const t = timeTicks(xMin, xMax, Math.max(2, Math.floor(plotW / 90)));
|
|
69
|
+
return t.ticks.map((v) => ({ at: left + ((v - xMin) / (xMax - xMin || 1)) * plotW, label: t.format(v) }));
|
|
70
|
+
}
|
|
71
|
+
if (nx) return niceTicks(xMin, xMax, Math.max(2, Math.floor(plotW / 80))).filter((v) => v >= xMin && v <= xMax).map((v) => ({ at: left + ((v - xMin) / (xMax - xMin || 1)) * plotW, label: formatNumber(v) }));
|
|
72
|
+
const every = Math.max(1, Math.ceil(data.length / Math.max(2, Math.floor(plotW / 70))));
|
|
73
|
+
return xs.map((v, i) => ({ at: px(i), label: String(v), i })).filter((t) => t.i % every === 0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function path(v: (number | null)[]) {
|
|
77
|
+
let d = '';
|
|
78
|
+
let pen = false;
|
|
79
|
+
v.forEach((n, i) => {
|
|
80
|
+
if (n === null) return void (pen = false);
|
|
81
|
+
d += `${pen ? 'L' : 'M'}${px(i).toFixed(1)},${py(n).toFixed(1)}`;
|
|
82
|
+
pen = true;
|
|
83
|
+
});
|
|
84
|
+
return d;
|
|
85
|
+
}
|
|
86
|
+
function areaPath(v: (number | null)[]) {
|
|
87
|
+
const base = py(Math.max(yMin, Math.min(0, yMax)));
|
|
88
|
+
const runs: string[] = [];
|
|
89
|
+
let run: number[] = [];
|
|
90
|
+
const flush = () => {
|
|
91
|
+
if (run.length) runs.push(`M${px(run[0]!)},${base}` + run.map((i) => `L${px(i).toFixed(1)},${py(v[i]!).toFixed(1)}`).join('') + `L${px(run[run.length - 1]!)},${base}Z`);
|
|
92
|
+
run = [];
|
|
93
|
+
};
|
|
94
|
+
v.forEach((n, i) => (n === null ? flush() : run.push(i)));
|
|
95
|
+
flush();
|
|
96
|
+
return runs.join('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function nearest(clientX: number, el: Element) {
|
|
100
|
+
const r = el.getBoundingClientRect();
|
|
101
|
+
const at = ((clientX - r.left) / r.width) * width;
|
|
102
|
+
let best = 0;
|
|
103
|
+
for (let i = 1; i < data.length; i++) if (Math.abs(px(i) - at) < Math.abs(px(best) - at)) best = i;
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
function onkeydown(e: KeyboardEvent) {
|
|
107
|
+
const last = data.length - 1;
|
|
108
|
+
const moves: Record<string, number> = { ArrowRight: Math.min(last, (active ?? -1) + 1), ArrowLeft: Math.max(0, (active ?? last + 1) - 1), Home: 0, End: last };
|
|
109
|
+
if (e.key in moves) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
active = moves[e.key]!;
|
|
112
|
+
} else if (e.key === 'Escape') active = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const describe = (i: number) => `${formatX(xs[i]!, time)}: ` + series.map((s, k) => `${s.label} ${values[k]![i] === null ? 'no data' : format(values[k]![i]!)}`).join(', ');
|
|
116
|
+
const table = $derived({
|
|
117
|
+
headers: [x, ...series.map((s) => s.label)],
|
|
118
|
+
rows: data.map((_, i) => [formatX(xs[i]!, time), ...values.map((v) => (v[i] === null ? '–' : format(v[i]!)))])
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<ChartFrame {title} {description} {level} class={klass} legend={series.map((s, i) => ({ label: s.label, color: seriesColor(i), kind: area ? 'rect' : 'line' }))} {table} readout={active === null ? '' : describe(active)}>
|
|
123
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
124
|
+
<div
|
|
125
|
+
class="tern-plot"
|
|
126
|
+
bind:clientWidth={width}
|
|
127
|
+
role="group"
|
|
128
|
+
aria-roledescription="chart"
|
|
129
|
+
aria-label="{title}. Use the arrow keys to read values."
|
|
130
|
+
tabindex="0"
|
|
131
|
+
onpointermove={(e) => (active = nearest(e.clientX, e.currentTarget))}
|
|
132
|
+
onpointerleave={() => (active = null)}
|
|
133
|
+
onfocus={() => active === null && (active = data.length - 1)}
|
|
134
|
+
onblur={() => (active = null)}
|
|
135
|
+
{onkeydown}
|
|
136
|
+
>
|
|
137
|
+
<svg {width} {height} viewBox="0 0 {width} {height}" aria-hidden="true">
|
|
138
|
+
{#each yTicks as t (t)}
|
|
139
|
+
<line class="grid" x1={left} x2={left + plotW} y1={py(t)} y2={py(t)} />
|
|
140
|
+
<text class="tick" x={left - 8} y={py(t)} dy="0.32em" text-anchor="end">{format(t)}</text>
|
|
141
|
+
{/each}
|
|
142
|
+
{#each xTicks as t, i (i)}
|
|
143
|
+
<text class="tick" x={t.at} y={height - 6} text-anchor={i === 0 && t.at - left < 20 ? 'start' : 'middle'}>{t.label}</text>
|
|
144
|
+
{/each}
|
|
145
|
+
{#each values as v, k (series[k]!.key)}
|
|
146
|
+
{#if area}<path d={areaPath(v)} fill={seriesColor(k)} fill-opacity="0.1" />{/if}
|
|
147
|
+
<path d={path(v)} fill="none" stroke={seriesColor(k)} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
|
|
148
|
+
{/each}
|
|
149
|
+
{#if active !== null}
|
|
150
|
+
<line class="grid" x1={px(active)} x2={px(active)} y1={top} y2={top + plotH} style="stroke: var(--tern-border-strong)" />
|
|
151
|
+
{/if}
|
|
152
|
+
{#each values as v, k (series[k]!.key)}
|
|
153
|
+
{@const i = active ?? lastIndex(v, (n) => n !== null)}
|
|
154
|
+
{#if i >= 0 && v[i] !== null}
|
|
155
|
+
<circle cx={px(i)} cy={py(v[i]!)} r="4" fill={seriesColor(k)} stroke="var(--tern-surface)" stroke-width="2" />
|
|
156
|
+
{/if}
|
|
157
|
+
{#if direct && ends[k] !== null}
|
|
158
|
+
<text class="direct" x={left + plotW + 10} y={py(ends[k]!)} dy="0.32em">{endLabels[k]}</text>
|
|
159
|
+
{/if}
|
|
160
|
+
{/each}
|
|
161
|
+
</svg>
|
|
162
|
+
{#if active !== null}
|
|
163
|
+
<div class="tooltip" style:left="{Math.min(px(active) + 12, width - 170)}px" style:top="{top}px">
|
|
164
|
+
<div class="head">{formatX(xs[active]!, time)}</div>
|
|
165
|
+
{#each series as s, k (s.key)}
|
|
166
|
+
<div class="row"><span class="swatch" style:background={seriesColor(k)}></span><strong>{values[k]![active] === null ? '–' : format(values[k]![active]!)}</strong><span>{s.label}</span></div>
|
|
167
|
+
{/each}
|
|
168
|
+
</div>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
</ChartFrame>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Series } from './chart.js';
|
|
2
|
+
declare function $$render<Row extends object>(): {
|
|
3
|
+
props: {
|
|
4
|
+
data: Row[];
|
|
5
|
+
/** The field on the x axis: dates (Date or ISO strings), numbers or categories. */
|
|
6
|
+
x: keyof Row & string;
|
|
7
|
+
/** Up to eight series; each is a field of numbers. */
|
|
8
|
+
series: Series<Row>[];
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
level?: 2 | 3 | 4;
|
|
12
|
+
/** Fill under the lines. */
|
|
13
|
+
area?: boolean;
|
|
14
|
+
/** Start the y axis at zero. Turn it off for values far from zero, such as a temperature. */
|
|
15
|
+
zero?: boolean;
|
|
16
|
+
height?: number;
|
|
17
|
+
/** Formats y values on the axis, in tooltips and in the table. */
|
|
18
|
+
format?: (v: number) => string;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
exports: {};
|
|
22
|
+
bindings: "";
|
|
23
|
+
slots: {};
|
|
24
|
+
events: {};
|
|
25
|
+
};
|
|
26
|
+
declare class __sveltets_Render<Row extends object> {
|
|
27
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
28
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
29
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
30
|
+
bindings(): "";
|
|
31
|
+
exports(): {};
|
|
32
|
+
}
|
|
33
|
+
interface $$IsomorphicComponent {
|
|
34
|
+
new <Row extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Row>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Row>['props']>, ReturnType<__sveltets_Render<Row>['events']>, ReturnType<__sveltets_Render<Row>['slots']>> & {
|
|
35
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
36
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
37
|
+
<Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
38
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
39
|
+
}
|
|
40
|
+
declare const LineChart: $$IsomorphicComponent;
|
|
41
|
+
type LineChart<Row extends object> = InstanceType<typeof LineChart<Row>>;
|
|
42
|
+
export default LineChart;
|
package/dist/ui/Menu.svelte
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import MenuList from './MenuList.svelte';
|
|
4
|
+
import type { MenuItem } from './menu.js';
|
|
3
5
|
|
|
4
|
-
type Item = { label: string; onselect?: () => void; href?: string; danger?: boolean; disabled?: boolean; separator?: boolean };
|
|
5
6
|
type Props = {
|
|
6
|
-
/** The items
|
|
7
|
-
items:
|
|
7
|
+
/** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
|
|
8
|
+
items: MenuItem[];
|
|
8
9
|
/** The button's content. */
|
|
9
10
|
children: Snippet;
|
|
10
11
|
/** Opens to the start or end edge of the button. */
|
|
@@ -16,75 +17,13 @@
|
|
|
16
17
|
|
|
17
18
|
const uid = $props.id();
|
|
18
19
|
let open = $state(false);
|
|
20
|
+
let focus = $state<'first' | 'last'>('first');
|
|
19
21
|
let button = $state<HTMLButtonElement>();
|
|
20
|
-
let menu = $state<HTMLDivElement>();
|
|
21
|
-
let typed = '';
|
|
22
|
-
let typedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async function show(focus: 'first' | 'last') {
|
|
23
|
+
function show(where: 'first' | 'last') {
|
|
24
|
+
focus = where;
|
|
27
25
|
open = true;
|
|
28
|
-
await Promise.resolve();
|
|
29
|
-
requestAnimationFrame(() => {
|
|
30
|
-
const list = entries();
|
|
31
|
-
(focus === 'first' ? list[0] : list[list.length - 1])?.focus();
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function hide(returnFocus = true) {
|
|
36
|
-
open = false;
|
|
37
|
-
if (returnFocus) button?.focus();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function select(item: Item) {
|
|
41
|
-
if (item.disabled) return;
|
|
42
|
-
hide();
|
|
43
|
-
item.onselect?.();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The ARIA menu button pattern.
|
|
47
|
-
function onmenukey(e: KeyboardEvent) {
|
|
48
|
-
const list = entries();
|
|
49
|
-
const i = list.indexOf(document.activeElement as HTMLElement);
|
|
50
|
-
const move = (j: number) => {
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
list[(j + list.length) % list.length]?.focus();
|
|
53
|
-
};
|
|
54
|
-
switch (e.key) {
|
|
55
|
-
case 'ArrowDown':
|
|
56
|
-
return move(i + 1);
|
|
57
|
-
case 'ArrowUp':
|
|
58
|
-
return move(i - 1);
|
|
59
|
-
case 'Home':
|
|
60
|
-
return move(0);
|
|
61
|
-
case 'End':
|
|
62
|
-
return move(list.length - 1);
|
|
63
|
-
case 'Escape':
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
return hide();
|
|
66
|
-
case 'Tab':
|
|
67
|
-
return hide(false);
|
|
68
|
-
}
|
|
69
|
-
if (e.key.length === 1 && /\S/.test(e.key)) {
|
|
70
|
-
// Type to jump to an item.
|
|
71
|
-
typed += e.key.toLowerCase();
|
|
72
|
-
clearTimeout(typedTimer);
|
|
73
|
-
typedTimer = setTimeout(() => (typed = ''), 500);
|
|
74
|
-
const start = typed.length === 1 ? i + 1 : i;
|
|
75
|
-
const order = [...list.slice(start), ...list.slice(0, start)];
|
|
76
|
-
order.find((el) => el.textContent?.trim().toLowerCase().startsWith(typed))?.focus();
|
|
77
|
-
}
|
|
78
26
|
}
|
|
79
|
-
|
|
80
|
-
$effect(() => {
|
|
81
|
-
if (!open) return;
|
|
82
|
-
const away = (e: PointerEvent) => {
|
|
83
|
-
if (!menu?.contains(e.target as Node) && !button?.contains(e.target as Node)) hide(false);
|
|
84
|
-
};
|
|
85
|
-
document.addEventListener('pointerdown', away);
|
|
86
|
-
return () => document.removeEventListener('pointerdown', away);
|
|
87
|
-
});
|
|
88
27
|
</script>
|
|
89
28
|
|
|
90
29
|
<div class="tern-menu {klass}">
|
|
@@ -96,7 +35,7 @@
|
|
|
96
35
|
aria-haspopup="menu"
|
|
97
36
|
aria-expanded={open}
|
|
98
37
|
aria-controls={open ? `tern-menu-${uid}` : undefined}
|
|
99
|
-
onclick={() => (open ?
|
|
38
|
+
onclick={() => (open ? (open = false) : show('first'))}
|
|
100
39
|
onkeydown={(e) => {
|
|
101
40
|
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
102
41
|
e.preventDefault();
|
|
@@ -110,38 +49,25 @@
|
|
|
110
49
|
{@render children()}
|
|
111
50
|
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
112
51
|
</button>
|
|
113
|
-
{#if open}
|
|
114
|
-
<
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
onkeydown={(e) => {
|
|
128
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
129
|
-
e.preventDefault();
|
|
130
|
-
select(item);
|
|
131
|
-
}
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
{item.label}
|
|
135
|
-
</div>
|
|
136
|
-
{/if}
|
|
137
|
-
{/each}
|
|
138
|
-
</div>
|
|
52
|
+
{#if open && button}
|
|
53
|
+
<MenuList
|
|
54
|
+
{items}
|
|
55
|
+
id="tern-menu-{uid}"
|
|
56
|
+
labelledby="tern-menu-{uid}-button"
|
|
57
|
+
anchor={button}
|
|
58
|
+
{align}
|
|
59
|
+
{focus}
|
|
60
|
+
inside={() => [button]}
|
|
61
|
+
onclose={(how) => {
|
|
62
|
+
open = false;
|
|
63
|
+
if (how.returnFocus) button?.focus();
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
139
66
|
{/if}
|
|
140
67
|
</div>
|
|
141
68
|
|
|
142
69
|
<style>
|
|
143
70
|
.tern-menu {
|
|
144
|
-
position: relative;
|
|
145
71
|
display: inline-block;
|
|
146
72
|
}
|
|
147
73
|
.trigger {
|
|
@@ -174,59 +100,4 @@
|
|
|
174
100
|
outline: 2px solid var(--tern-ring);
|
|
175
101
|
outline-offset: 2px;
|
|
176
102
|
}
|
|
177
|
-
.menu {
|
|
178
|
-
position: absolute;
|
|
179
|
-
top: calc(100% + 0.375rem);
|
|
180
|
-
z-index: 50;
|
|
181
|
-
min-width: 11rem;
|
|
182
|
-
padding: 0.3rem;
|
|
183
|
-
background: var(--tern-surface);
|
|
184
|
-
border: 1px solid var(--tern-border);
|
|
185
|
-
border-radius: var(--tern-radius);
|
|
186
|
-
box-shadow: var(--tern-shadow-lg);
|
|
187
|
-
animation: pop var(--tern-duration) ease-out;
|
|
188
|
-
}
|
|
189
|
-
.start {
|
|
190
|
-
left: 0;
|
|
191
|
-
}
|
|
192
|
-
.end {
|
|
193
|
-
right: 0;
|
|
194
|
-
}
|
|
195
|
-
[role='menuitem'] {
|
|
196
|
-
display: block;
|
|
197
|
-
padding: 0.45rem 0.65rem;
|
|
198
|
-
border-radius: calc(var(--tern-radius-sm) - 2px);
|
|
199
|
-
font-size: 0.875rem;
|
|
200
|
-
color: var(--tern-fg);
|
|
201
|
-
text-decoration: none;
|
|
202
|
-
cursor: pointer;
|
|
203
|
-
outline: none;
|
|
204
|
-
}
|
|
205
|
-
[role='menuitem']:hover,
|
|
206
|
-
[role='menuitem']:focus {
|
|
207
|
-
background: var(--tern-accent-soft);
|
|
208
|
-
}
|
|
209
|
-
[role='menuitem'].danger {
|
|
210
|
-
color: var(--tern-danger);
|
|
211
|
-
}
|
|
212
|
-
[role='menuitem'].danger:hover,
|
|
213
|
-
[role='menuitem'].danger:focus {
|
|
214
|
-
background: var(--tern-danger-soft);
|
|
215
|
-
}
|
|
216
|
-
[role='menuitem'][aria-disabled='true'] {
|
|
217
|
-
opacity: 0.45;
|
|
218
|
-
cursor: not-allowed;
|
|
219
|
-
background: none;
|
|
220
|
-
}
|
|
221
|
-
.separator {
|
|
222
|
-
height: 1px;
|
|
223
|
-
margin: 0.3rem 0.2rem;
|
|
224
|
-
background: var(--tern-border);
|
|
225
|
-
}
|
|
226
|
-
@keyframes pop {
|
|
227
|
-
from {
|
|
228
|
-
opacity: 0;
|
|
229
|
-
transform: translateY(-4px);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
103
|
</style>
|
package/dist/ui/Menu.svelte.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
type
|
|
3
|
-
label: string;
|
|
4
|
-
onselect?: () => void;
|
|
5
|
-
href?: string;
|
|
6
|
-
danger?: boolean;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
separator?: boolean;
|
|
9
|
-
};
|
|
2
|
+
import type { MenuItem } from './menu.js';
|
|
10
3
|
type Props = {
|
|
11
|
-
/** The items
|
|
12
|
-
items:
|
|
4
|
+
/** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
|
|
5
|
+
items: MenuItem[];
|
|
13
6
|
/** The button's content. */
|
|
14
7
|
children: Snippet;
|
|
15
8
|
/** Opens to the start or end edge of the button. */
|