@ternkit/tern 1.2.1 → 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/goscript/index.svelte.d.ts +32 -2
- package/dist/goscript/index.svelte.js +257 -31
- package/dist/goscript/patch.d.ts +6 -0
- package/dist/goscript/patch.js +25 -0
- 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/Accordion.svelte +84 -0
- package/dist/ui/Accordion.svelte.d.ts +20 -0
- package/dist/ui/Alert.svelte +76 -0
- package/dist/ui/Alert.svelte.d.ts +12 -0
- 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/Avatar.svelte +48 -0
- package/dist/ui/Avatar.svelte.d.ts +10 -0
- package/dist/ui/Badge.svelte +54 -0
- package/dist/ui/Badge.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/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -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/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -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 +112 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- 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 +301 -0
- package/dist/ui/Dialog.svelte.d.ts +30 -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 +97 -0
- package/dist/ui/Field.svelte.d.ts +19 -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 +62 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- 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/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -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 +103 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- 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/Progress.svelte +73 -0
- package/dist/ui/Progress.svelte.d.ts +12 -0
- package/dist/ui/RadioGroup.svelte +105 -0
- package/dist/ui/RadioGroup.svelte.d.ts +21 -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 +77 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -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/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -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/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +52 -0
- package/dist/ui/Textarea.svelte.d.ts +7 -0
- package/dist/ui/Toaster.svelte +146 -0
- package/dist/ui/Toaster.svelte.d.ts +6 -0
- 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 +85 -0
- package/dist/ui/Tooltip.svelte.d.ts +16 -0
- 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 +18 -0
- package/dist/ui/field.js +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 +78 -0
- package/dist/ui/index.js +70 -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 +176 -0
- package/dist/ui/toast.svelte.d.ts +19 -0
- package/dist/ui/toast.svelte.js +18 -0
- package/dist/vite/goscript.d.ts +14 -0
- package/dist/vite/goscript.js +45 -16
- package/dist/vite/index.js +15 -4
- package/package.json +7 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getField } from './field.js';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** The code; bind it. */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** How many characters. */
|
|
8
|
+
length?: number;
|
|
9
|
+
/** numeric for digits only, or alphanumeric. */
|
|
10
|
+
mode?: 'numeric' | 'alphanumeric';
|
|
11
|
+
/** Labels it when it isn't in a Field. */
|
|
12
|
+
label?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Runs once every character is entered, such as to submit. */
|
|
17
|
+
oncomplete?: (code: string) => void;
|
|
18
|
+
};
|
|
19
|
+
const field = getField();
|
|
20
|
+
let { value = $bindable(field?.value ?? ''), length = 6, mode = 'numeric', label, name: nameProp, disabled = false, class: klass = '', oncomplete }: Props = $props();
|
|
21
|
+
const name = $derived(nameProp ?? field?.name);
|
|
22
|
+
|
|
23
|
+
let input = $state<HTMLInputElement>();
|
|
24
|
+
let focused = $state(false);
|
|
25
|
+
let caret = $state(0);
|
|
26
|
+
const pattern = $derived(mode === 'numeric' ? /[^0-9]/g : /[^0-9a-z]/gi);
|
|
27
|
+
|
|
28
|
+
function oninput() {
|
|
29
|
+
const clean = input!.value.replace(pattern, '').slice(0, length);
|
|
30
|
+
input!.value = clean;
|
|
31
|
+
value = clean;
|
|
32
|
+
caret = clean.length;
|
|
33
|
+
if (clean.length === length) oncomplete?.(clean);
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<!-- One real input, drawn as boxes: pasting, autofill from SMS and screen readers all just work. -->
|
|
38
|
+
<div class="tern-otp {klass}" class:focused class:disabled={disabled || field?.disabled} style:--length={length}>
|
|
39
|
+
<input
|
|
40
|
+
bind:this={input}
|
|
41
|
+
{value}
|
|
42
|
+
{name}
|
|
43
|
+
id={field?.id}
|
|
44
|
+
aria-label={field ? undefined : label}
|
|
45
|
+
aria-describedby={field?.describedBy}
|
|
46
|
+
aria-invalid={field?.invalid || undefined}
|
|
47
|
+
required={field?.required}
|
|
48
|
+
disabled={disabled || field?.disabled}
|
|
49
|
+
inputmode={mode === 'numeric' ? 'numeric' : 'text'}
|
|
50
|
+
autocomplete="one-time-code"
|
|
51
|
+
autocapitalize="characters"
|
|
52
|
+
spellcheck="false"
|
|
53
|
+
pattern={mode === 'numeric' ? `[0-9]{${length}}` : `[0-9A-Za-z]{${length}}`}
|
|
54
|
+
{oninput}
|
|
55
|
+
onfocus={() => {
|
|
56
|
+
focused = true;
|
|
57
|
+
caret = value.length;
|
|
58
|
+
}}
|
|
59
|
+
onblur={() => (focused = false)}
|
|
60
|
+
onselect={() => (caret = Math.min(input?.selectionStart ?? 0, length - 1))}
|
|
61
|
+
/>
|
|
62
|
+
<div class="boxes" aria-hidden="true">
|
|
63
|
+
{#each { length }, i (i)}
|
|
64
|
+
<span class="box" class:active={focused && i === Math.min(caret, length - 1)} class:filled={i < value.length}>
|
|
65
|
+
{value[i] ?? ''}
|
|
66
|
+
{#if focused && i === value.length}<span class="caret"></span>{/if}
|
|
67
|
+
</span>
|
|
68
|
+
{/each}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
.tern-otp {
|
|
74
|
+
position: relative;
|
|
75
|
+
width: fit-content;
|
|
76
|
+
}
|
|
77
|
+
input {
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: 0;
|
|
80
|
+
width: 100%;
|
|
81
|
+
height: 100%;
|
|
82
|
+
opacity: 0;
|
|
83
|
+
font-size: 16px; /* no zoom on iOS */
|
|
84
|
+
letter-spacing: 2.6rem;
|
|
85
|
+
caret-color: transparent;
|
|
86
|
+
border: 0;
|
|
87
|
+
padding: 0;
|
|
88
|
+
cursor: text;
|
|
89
|
+
z-index: 1;
|
|
90
|
+
}
|
|
91
|
+
.boxes {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 0.5rem;
|
|
94
|
+
}
|
|
95
|
+
.box {
|
|
96
|
+
position: relative;
|
|
97
|
+
display: grid;
|
|
98
|
+
place-items: center;
|
|
99
|
+
width: 2.5rem;
|
|
100
|
+
height: 2.875rem;
|
|
101
|
+
font-family: var(--tern-font-mono);
|
|
102
|
+
font-size: 1.25rem;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
text-transform: uppercase;
|
|
105
|
+
color: var(--tern-fg);
|
|
106
|
+
background: var(--tern-surface);
|
|
107
|
+
border: 1px solid var(--tern-border-strong);
|
|
108
|
+
border-radius: var(--tern-radius-sm);
|
|
109
|
+
transition:
|
|
110
|
+
border-color var(--tern-duration),
|
|
111
|
+
box-shadow var(--tern-duration);
|
|
112
|
+
}
|
|
113
|
+
.box.active {
|
|
114
|
+
border-color: var(--tern-accent);
|
|
115
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
|
|
116
|
+
}
|
|
117
|
+
.caret {
|
|
118
|
+
position: absolute;
|
|
119
|
+
width: 1px;
|
|
120
|
+
height: 1.25rem;
|
|
121
|
+
background: var(--tern-fg);
|
|
122
|
+
animation: blink 1s steps(1) infinite;
|
|
123
|
+
}
|
|
124
|
+
.tern-otp:has(input[aria-invalid='true']) .box {
|
|
125
|
+
border-color: var(--tern-danger);
|
|
126
|
+
}
|
|
127
|
+
.disabled {
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
}
|
|
130
|
+
@keyframes blink {
|
|
131
|
+
50% {
|
|
132
|
+
opacity: 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
@media (prefers-reduced-motion: reduce) {
|
|
136
|
+
.caret {
|
|
137
|
+
animation: none;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** The code; bind it. */
|
|
3
|
+
value?: string;
|
|
4
|
+
/** How many characters. */
|
|
5
|
+
length?: number;
|
|
6
|
+
/** numeric for digits only, or alphanumeric. */
|
|
7
|
+
mode?: 'numeric' | 'alphanumeric';
|
|
8
|
+
/** Labels it when it isn't in a Field. */
|
|
9
|
+
label?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Runs once every character is entered, such as to submit. */
|
|
14
|
+
oncomplete?: (code: string) => void;
|
|
15
|
+
};
|
|
16
|
+
declare const InputOTP: import("svelte").Component<Props, {}, "value">;
|
|
17
|
+
type InputOTP = ReturnType<typeof InputOTP>;
|
|
18
|
+
export default InputOTP;
|
|
@@ -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,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
let { children }: { children: Snippet } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<kbd class="tern-kbd">{@render children()}</kbd>
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
.tern-kbd {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
min-width: 1.4em;
|
|
13
|
+
height: 1.4em;
|
|
14
|
+
padding: 0 0.35em;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
border: 1px solid var(--tern-border-strong);
|
|
17
|
+
border-bottom-width: 2px;
|
|
18
|
+
border-radius: 5px;
|
|
19
|
+
background: var(--tern-surface);
|
|
20
|
+
font-family: var(--tern-font-mono);
|
|
21
|
+
font-size: 0.75em;
|
|
22
|
+
color: var(--tern-fg);
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
@@ -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;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import MenuList from './MenuList.svelte';
|
|
4
|
+
import type { MenuItem } from './menu.js';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
/** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
|
|
8
|
+
items: MenuItem[];
|
|
9
|
+
/** The button's content. */
|
|
10
|
+
children: Snippet;
|
|
11
|
+
/** Opens to the start or end edge of the button. */
|
|
12
|
+
align?: 'start' | 'end';
|
|
13
|
+
variant?: 'secondary' | 'ghost';
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
let { items, children, align = 'start', variant = 'secondary', class: klass = '' }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
let open = $state(false);
|
|
20
|
+
let focus = $state<'first' | 'last'>('first');
|
|
21
|
+
let button = $state<HTMLButtonElement>();
|
|
22
|
+
|
|
23
|
+
function show(where: 'first' | 'last') {
|
|
24
|
+
focus = where;
|
|
25
|
+
open = true;
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="tern-menu {klass}">
|
|
30
|
+
<button
|
|
31
|
+
bind:this={button}
|
|
32
|
+
type="button"
|
|
33
|
+
class="trigger {variant}"
|
|
34
|
+
id="tern-menu-{uid}-button"
|
|
35
|
+
aria-haspopup="menu"
|
|
36
|
+
aria-expanded={open}
|
|
37
|
+
aria-controls={open ? `tern-menu-${uid}` : undefined}
|
|
38
|
+
onclick={() => (open ? (open = false) : show('first'))}
|
|
39
|
+
onkeydown={(e) => {
|
|
40
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
show('first');
|
|
43
|
+
} else if (e.key === 'ArrowUp') {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
show('last');
|
|
46
|
+
}
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{@render children()}
|
|
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>
|
|
51
|
+
</button>
|
|
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
|
+
/>
|
|
66
|
+
{/if}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.tern-menu {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
}
|
|
73
|
+
.trigger {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 0.4rem;
|
|
77
|
+
height: var(--tern-control-height);
|
|
78
|
+
padding: 0 0.75rem 0 0.9rem;
|
|
79
|
+
font: inherit;
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
font-weight: 550;
|
|
82
|
+
color: var(--tern-fg);
|
|
83
|
+
border-radius: var(--tern-radius-sm);
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
}
|
|
86
|
+
.secondary {
|
|
87
|
+
background: var(--tern-surface);
|
|
88
|
+
border: 1px solid var(--tern-border-strong);
|
|
89
|
+
box-shadow: var(--tern-shadow);
|
|
90
|
+
}
|
|
91
|
+
.ghost {
|
|
92
|
+
background: transparent;
|
|
93
|
+
border: 1px solid transparent;
|
|
94
|
+
}
|
|
95
|
+
.trigger:hover,
|
|
96
|
+
.trigger[aria-expanded='true'] {
|
|
97
|
+
background: var(--tern-surface-2);
|
|
98
|
+
}
|
|
99
|
+
.trigger:focus-visible {
|
|
100
|
+
outline: 2px solid var(--tern-ring);
|
|
101
|
+
outline-offset: 2px;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { MenuItem } from './menu.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
/** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
|
|
5
|
+
items: MenuItem[];
|
|
6
|
+
/** The button's content. */
|
|
7
|
+
children: Snippet;
|
|
8
|
+
/** Opens to the start or end edge of the button. */
|
|
9
|
+
align?: 'start' | 'end';
|
|
10
|
+
variant?: 'secondary' | 'ghost';
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const Menu: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Menu = ReturnType<typeof Menu>;
|
|
15
|
+
export default Menu;
|