@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,18 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** What it counts, in sentence case, such as "Unique visitors". */
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
/** The change, in percent, against the period named in period. */
|
|
6
|
+
delta?: number;
|
|
7
|
+
/** Such as "vs last week". */
|
|
8
|
+
period?: string;
|
|
9
|
+
/** Whether up is good (visits) or bad (errors), which picks the delta's color. */
|
|
10
|
+
upIsGood?: boolean;
|
|
11
|
+
/** Recent values, drawn as a sparkline. */
|
|
12
|
+
trend?: number[];
|
|
13
|
+
format?: (v: number) => string;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const Stat: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type Stat = ReturnType<typeof Stat>;
|
|
18
|
+
export default Stat;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts" generics="Row">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { Column } from './table.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
columns: Column<Row>[];
|
|
8
|
+
rows: Row[];
|
|
9
|
+
/** A caption for the table; say what it lists. Hidden visually with hideCaption. */
|
|
10
|
+
caption: string;
|
|
11
|
+
hideCaption?: boolean;
|
|
12
|
+
/** A stable key for each row. */
|
|
13
|
+
key?: (row: Row) => string | number;
|
|
14
|
+
/** Shown in place of rows when there are none. */
|
|
15
|
+
empty?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
};
|
|
18
|
+
let { columns, rows, caption, hideCaption = false, key = (r) => JSON.stringify(r), empty = 'Nothing to show', class: klass = '' }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<!-- A semantic table, with a caption, column headers and numeric alignment. -->
|
|
22
|
+
<div class="tern-table-wrap {klass}">
|
|
23
|
+
<table class="tern-table">
|
|
24
|
+
<caption class:tern-sr-only={hideCaption}>{caption}</caption>
|
|
25
|
+
<thead>
|
|
26
|
+
<tr>
|
|
27
|
+
{#each columns as col (col.key)}<th scope="col" style:text-align={col.align} style:width={col.width}>{col.header}</th>{/each}
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
{#each rows as row (key(row))}
|
|
32
|
+
<tr>
|
|
33
|
+
{#each columns as col (col.key)}
|
|
34
|
+
<td style:text-align={col.align}>
|
|
35
|
+
{#if col.cell}{@render col.cell(row)}{:else}{row[col.key] ?? ''}{/if}
|
|
36
|
+
</td>
|
|
37
|
+
{/each}
|
|
38
|
+
</tr>
|
|
39
|
+
{:else}
|
|
40
|
+
<tr><td colspan={columns.length} class="empty">{empty}</td></tr>
|
|
41
|
+
{/each}
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.tern-table-wrap {
|
|
48
|
+
overflow-x: auto;
|
|
49
|
+
border: 1px solid var(--tern-border);
|
|
50
|
+
border-radius: var(--tern-radius);
|
|
51
|
+
background: var(--tern-surface);
|
|
52
|
+
}
|
|
53
|
+
caption {
|
|
54
|
+
padding: 0.75rem;
|
|
55
|
+
text-align: start;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
color: var(--tern-fg);
|
|
58
|
+
}
|
|
59
|
+
td {
|
|
60
|
+
font-variant-numeric: tabular-nums;
|
|
61
|
+
}
|
|
62
|
+
.tern-table-wrap :global(tbody tr:last-child td) {
|
|
63
|
+
border-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
.empty {
|
|
66
|
+
padding: 2rem;
|
|
67
|
+
text-align: center;
|
|
68
|
+
color: var(--tern-muted);
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Column } from './table.js';
|
|
2
|
+
declare function $$render<Row>(): {
|
|
3
|
+
props: {
|
|
4
|
+
columns: Column<Row>[];
|
|
5
|
+
rows: Row[];
|
|
6
|
+
/** A caption for the table; say what it lists. Hidden visually with hideCaption. */
|
|
7
|
+
caption: string;
|
|
8
|
+
hideCaption?: boolean;
|
|
9
|
+
/** A stable key for each row. */
|
|
10
|
+
key?: (row: Row) => string | number;
|
|
11
|
+
/** Shown in place of rows when there are none. */
|
|
12
|
+
empty?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
exports: {};
|
|
16
|
+
bindings: "";
|
|
17
|
+
slots: {};
|
|
18
|
+
events: {};
|
|
19
|
+
};
|
|
20
|
+
declare class __sveltets_Render<Row> {
|
|
21
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
22
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
23
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
24
|
+
bindings(): "";
|
|
25
|
+
exports(): {};
|
|
26
|
+
}
|
|
27
|
+
interface $$IsomorphicComponent {
|
|
28
|
+
new <Row>(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']>> & {
|
|
29
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
30
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
31
|
+
<Row>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
32
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
33
|
+
}
|
|
34
|
+
declare const Table: $$IsomorphicComponent;
|
|
35
|
+
type Table<Row> = InstanceType<typeof Table<Row>>;
|
|
36
|
+
export default Table;
|
package/dist/ui/Textarea.svelte
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { getField } from './field.js';
|
|
4
4
|
|
|
5
5
|
type Props = HTMLTextareaAttributes & { value?: unknown };
|
|
6
|
-
let { value = $bindable(), class: klass = '', rows = 4, ...rest }: Props = $props();
|
|
7
6
|
const field = getField();
|
|
7
|
+
let { value = $bindable(field?.value), class: klass = '', rows = 4, ...rest }: Props = $props();
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
<textarea
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
required={field?.required}
|
|
16
16
|
disabled={field?.disabled}
|
|
17
17
|
{rows}
|
|
18
|
+
name={field?.name}
|
|
18
19
|
{...rest}
|
|
19
20
|
bind:value
|
|
20
21
|
></textarea>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = Omit<HTMLButtonAttributes, 'value'> & {
|
|
6
|
+
/** Whether it's on; bind it. */
|
|
7
|
+
pressed?: boolean;
|
|
8
|
+
variant?: 'secondary' | 'ghost';
|
|
9
|
+
size?: 'sm' | 'md';
|
|
10
|
+
/** Posts value (or "on") under this name while pressed. */
|
|
11
|
+
name?: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
/** The label. For an icon alone, set aria-label. */
|
|
14
|
+
children: Snippet;
|
|
15
|
+
onchange?: (pressed: boolean) => void;
|
|
16
|
+
};
|
|
17
|
+
let { pressed = $bindable(false), variant = 'secondary', size = 'md', name, value = 'on', class: klass = '', children, onchange, ...rest }: Props = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="tern-toggle {variant} {size} {klass}"
|
|
23
|
+
aria-pressed={pressed}
|
|
24
|
+
{...rest}
|
|
25
|
+
onclick={() => {
|
|
26
|
+
pressed = !pressed;
|
|
27
|
+
onchange?.(pressed);
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{@render children()}
|
|
31
|
+
</button>
|
|
32
|
+
{#if name && pressed}<input type="hidden" {name} {value} />{/if}
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.tern-toggle {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
gap: 0.4rem;
|
|
40
|
+
height: var(--tern-control-height);
|
|
41
|
+
min-width: var(--tern-control-height);
|
|
42
|
+
padding: 0 0.7rem;
|
|
43
|
+
font: inherit;
|
|
44
|
+
font-size: 0.875rem;
|
|
45
|
+
font-weight: 550;
|
|
46
|
+
color: var(--tern-muted);
|
|
47
|
+
border: 1px solid transparent;
|
|
48
|
+
border-radius: var(--tern-radius-sm);
|
|
49
|
+
background: transparent;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
transition:
|
|
52
|
+
background-color var(--tern-duration),
|
|
53
|
+
color var(--tern-duration);
|
|
54
|
+
}
|
|
55
|
+
.secondary {
|
|
56
|
+
border-color: var(--tern-border-strong);
|
|
57
|
+
background: var(--tern-surface);
|
|
58
|
+
}
|
|
59
|
+
.sm {
|
|
60
|
+
height: calc(var(--tern-control-height) - 0.5rem);
|
|
61
|
+
min-width: calc(var(--tern-control-height) - 0.5rem);
|
|
62
|
+
padding: 0 0.5rem;
|
|
63
|
+
font-size: 0.8125rem;
|
|
64
|
+
}
|
|
65
|
+
.tern-toggle:hover:not(:disabled) {
|
|
66
|
+
color: var(--tern-fg);
|
|
67
|
+
background: var(--tern-surface-2);
|
|
68
|
+
}
|
|
69
|
+
.tern-toggle[aria-pressed='true'] {
|
|
70
|
+
color: var(--tern-fg);
|
|
71
|
+
background: var(--tern-accent-soft);
|
|
72
|
+
border-color: color-mix(in oklch, var(--tern-accent) 45%, transparent);
|
|
73
|
+
}
|
|
74
|
+
.tern-toggle:focus-visible {
|
|
75
|
+
outline: 2px solid var(--tern-ring);
|
|
76
|
+
outline-offset: 2px;
|
|
77
|
+
}
|
|
78
|
+
.tern-toggle:disabled {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<HTMLButtonAttributes, 'value'> & {
|
|
4
|
+
/** Whether it's on; bind it. */
|
|
5
|
+
pressed?: boolean;
|
|
6
|
+
variant?: 'secondary' | 'ghost';
|
|
7
|
+
size?: 'sm' | 'md';
|
|
8
|
+
/** Posts value (or "on") under this name while pressed. */
|
|
9
|
+
name?: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
/** The label. For an icon alone, set aria-label. */
|
|
12
|
+
children: Snippet;
|
|
13
|
+
onchange?: (pressed: boolean) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const Toggle: import("svelte").Component<Props, {}, "pressed">;
|
|
16
|
+
type Toggle = ReturnType<typeof Toggle>;
|
|
17
|
+
export default Toggle;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getField } from './field.js';
|
|
4
|
+
|
|
5
|
+
type Option = { value: string; label: string; disabled?: boolean; icon?: Snippet };
|
|
6
|
+
type Props = {
|
|
7
|
+
options: Option[];
|
|
8
|
+
/** single: one at a time, like radio buttons; multiple: any number, like checkboxes. */
|
|
9
|
+
type?: 'single' | 'multiple';
|
|
10
|
+
/** The chosen value (single) or values (multiple); bind it. */
|
|
11
|
+
value?: string | string[] | null;
|
|
12
|
+
/** Labels the group when it isn't in a Field. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Show only the icons; the labels stay for screen readers. */
|
|
15
|
+
iconsOnly?: boolean;
|
|
16
|
+
/** Posts the chosen values under this name. */
|
|
17
|
+
name?: string;
|
|
18
|
+
size?: 'sm' | 'md';
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
class?: string;
|
|
21
|
+
onchange?: (value: string | string[] | null) => void;
|
|
22
|
+
};
|
|
23
|
+
const field = getField();
|
|
24
|
+
let { options, type = 'single', value = $bindable(type === 'multiple' ? [] : null), label, iconsOnly = false, name: nameProp, size = 'md', disabled = false, class: klass = '', onchange }: Props = $props();
|
|
25
|
+
const name = $derived(nameProp ?? field?.name);
|
|
26
|
+
|
|
27
|
+
field?.custom();
|
|
28
|
+
const buttons: HTMLButtonElement[] = [];
|
|
29
|
+
const chosen = (v: string) => (Array.isArray(value) ? value.includes(v) : value === v);
|
|
30
|
+
const enabled = $derived(options.map((o, i) => ({ o, i })).filter(({ o }) => !o.disabled && !disabled));
|
|
31
|
+
// Roving tabindex: the chosen item, else the first enabled one.
|
|
32
|
+
const tabStop = $derived(enabled.find(({ o }) => chosen(o.value))?.i ?? enabled[0]?.i ?? -1);
|
|
33
|
+
|
|
34
|
+
function pick(v: string) {
|
|
35
|
+
if (type === 'multiple') {
|
|
36
|
+
const list = Array.isArray(value) ? value : [];
|
|
37
|
+
value = list.includes(v) ? list.filter((x) => x !== v) : [...list, v];
|
|
38
|
+
} else value = value === v ? null : v;
|
|
39
|
+
onchange?.(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onkeydown(e: KeyboardEvent, i: number) {
|
|
43
|
+
const pos = enabled.findIndex((x) => x.i === i);
|
|
44
|
+
let next: number | undefined;
|
|
45
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = enabled[(pos + 1) % enabled.length]?.i;
|
|
46
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = enabled[(pos - 1 + enabled.length) % enabled.length]?.i;
|
|
47
|
+
else if (e.key === 'Home') next = enabled[0]?.i;
|
|
48
|
+
else if (e.key === 'End') next = enabled[enabled.length - 1]?.i;
|
|
49
|
+
if (next === undefined) return;
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
buttons[next]?.focus();
|
|
52
|
+
// Like radio buttons, moving selects in single mode.
|
|
53
|
+
if (type === 'single') {
|
|
54
|
+
value = options[next]!.value;
|
|
55
|
+
onchange?.(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class="tern-toggle-group {size} {klass}"
|
|
62
|
+
role={type === 'single' ? 'radiogroup' : 'group'}
|
|
63
|
+
aria-label={field ? undefined : label}
|
|
64
|
+
aria-labelledby={field?.labelId}
|
|
65
|
+
aria-describedby={field?.describedBy}
|
|
66
|
+
>
|
|
67
|
+
{#each options as option, i (option.value)}
|
|
68
|
+
<button
|
|
69
|
+
bind:this={buttons[i]}
|
|
70
|
+
id={i === tabStop ? field?.id : undefined}
|
|
71
|
+
type="button"
|
|
72
|
+
role={type === 'single' ? 'radio' : undefined}
|
|
73
|
+
aria-checked={type === 'single' ? chosen(option.value) : undefined}
|
|
74
|
+
aria-pressed={type === 'multiple' ? chosen(option.value) : undefined}
|
|
75
|
+
aria-label={iconsOnly ? option.label : undefined}
|
|
76
|
+
title={iconsOnly ? option.label : undefined}
|
|
77
|
+
tabindex={i === tabStop ? 0 : -1}
|
|
78
|
+
disabled={option.disabled || disabled || field?.disabled}
|
|
79
|
+
onclick={() => pick(option.value)}
|
|
80
|
+
onkeydown={(e) => onkeydown(e, i)}
|
|
81
|
+
>
|
|
82
|
+
{#if option.icon}{@render option.icon()}{/if}
|
|
83
|
+
{#if !iconsOnly}{option.label}{/if}
|
|
84
|
+
</button>
|
|
85
|
+
{/each}
|
|
86
|
+
</div>
|
|
87
|
+
{#if name}
|
|
88
|
+
{#each options.filter((o) => chosen(o.value)) as o (o.value)}<input type="hidden" {name} value={o.value} />{/each}
|
|
89
|
+
{/if}
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.tern-toggle-group {
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
gap: 0.125rem;
|
|
95
|
+
padding: 0.2rem;
|
|
96
|
+
background: var(--tern-surface-2);
|
|
97
|
+
border: 1px solid var(--tern-border);
|
|
98
|
+
border-radius: var(--tern-radius);
|
|
99
|
+
width: fit-content;
|
|
100
|
+
}
|
|
101
|
+
button {
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
gap: 0.4rem;
|
|
106
|
+
height: calc(var(--tern-control-height) - 0.4rem);
|
|
107
|
+
min-width: calc(var(--tern-control-height) - 0.4rem);
|
|
108
|
+
padding: 0 0.7rem;
|
|
109
|
+
font: inherit;
|
|
110
|
+
font-size: 0.875rem;
|
|
111
|
+
font-weight: 550;
|
|
112
|
+
color: var(--tern-muted);
|
|
113
|
+
background: transparent;
|
|
114
|
+
border: 0;
|
|
115
|
+
border-radius: var(--tern-radius-sm);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
transition:
|
|
118
|
+
background-color var(--tern-duration),
|
|
119
|
+
color var(--tern-duration);
|
|
120
|
+
}
|
|
121
|
+
.sm button {
|
|
122
|
+
height: calc(var(--tern-control-height) - 0.8rem);
|
|
123
|
+
padding: 0 0.5rem;
|
|
124
|
+
font-size: 0.8125rem;
|
|
125
|
+
}
|
|
126
|
+
button:hover:not(:disabled) {
|
|
127
|
+
color: var(--tern-fg);
|
|
128
|
+
}
|
|
129
|
+
button[aria-checked='true'],
|
|
130
|
+
button[aria-pressed='true'] {
|
|
131
|
+
color: var(--tern-fg);
|
|
132
|
+
background: var(--tern-surface);
|
|
133
|
+
box-shadow: var(--tern-shadow);
|
|
134
|
+
}
|
|
135
|
+
button:focus-visible {
|
|
136
|
+
outline: 2px solid var(--tern-ring);
|
|
137
|
+
outline-offset: 1px;
|
|
138
|
+
}
|
|
139
|
+
button:disabled {
|
|
140
|
+
opacity: 0.45;
|
|
141
|
+
cursor: not-allowed;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Option = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
icon?: Snippet;
|
|
7
|
+
};
|
|
8
|
+
type Props = {
|
|
9
|
+
options: Option[];
|
|
10
|
+
/** single: one at a time, like radio buttons; multiple: any number, like checkboxes. */
|
|
11
|
+
type?: 'single' | 'multiple';
|
|
12
|
+
/** The chosen value (single) or values (multiple); bind it. */
|
|
13
|
+
value?: string | string[] | null;
|
|
14
|
+
/** Labels the group when it isn't in a Field. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Show only the icons; the labels stay for screen readers. */
|
|
17
|
+
iconsOnly?: boolean;
|
|
18
|
+
/** Posts the chosen values under this name. */
|
|
19
|
+
name?: string;
|
|
20
|
+
size?: 'sm' | 'md';
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
onchange?: (value: string | string[] | null) => void;
|
|
24
|
+
};
|
|
25
|
+
declare const ToggleGroup: import("svelte").Component<Props, {}, "value">;
|
|
26
|
+
type ToggleGroup = ReturnType<typeof ToggleGroup>;
|
|
27
|
+
export default ToggleGroup;
|
package/dist/ui/Tooltip.svelte
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { float, type Side } from './floating.js';
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
5
6
|
/** The tooltip's text. It describes the trigger; don't put anything interactive in it. */
|
|
6
7
|
text: string;
|
|
7
|
-
side?:
|
|
8
|
+
side?: Side;
|
|
8
9
|
/** How long to wait before showing, in milliseconds. */
|
|
9
10
|
delay?: number;
|
|
10
11
|
/** The trigger, which must be focusable, such as a Button. It receives aria-describedby through the snippet's props. */
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
let { text, side = 'top', delay = 300, children }: Props = $props();
|
|
14
15
|
const uid = $props.id();
|
|
15
16
|
let open = $state(false);
|
|
17
|
+
let wrap = $state<HTMLSpanElement>();
|
|
18
|
+
let tip = $state<HTMLSpanElement>();
|
|
16
19
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
17
20
|
|
|
18
21
|
const show = (now = false) => {
|
|
@@ -24,10 +27,21 @@
|
|
|
24
27
|
clearTimeout(timer);
|
|
25
28
|
open = false;
|
|
26
29
|
};
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (!open || !tip || !wrap) return;
|
|
33
|
+
tip.showPopover?.();
|
|
34
|
+
const stop = float(tip, wrap.firstElementChild ?? wrap, { side, align: 'center', offset: 6 });
|
|
35
|
+
return () => {
|
|
36
|
+
stop();
|
|
37
|
+
tip?.hidePopover?.();
|
|
38
|
+
};
|
|
39
|
+
});
|
|
27
40
|
</script>
|
|
28
41
|
|
|
29
42
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
30
43
|
<span
|
|
44
|
+
bind:this={wrap}
|
|
31
45
|
class="tern-tooltip"
|
|
32
46
|
onpointerenter={() => show()}
|
|
33
47
|
onpointerleave={hide}
|
|
@@ -36,42 +50,36 @@
|
|
|
36
50
|
onkeydown={(e) => e.key === 'Escape' && hide()}
|
|
37
51
|
>
|
|
38
52
|
{@render children({ 'aria-describedby': `tern-tooltip-${uid}` })}
|
|
39
|
-
<span role="tooltip" id="tern-tooltip-{uid}" class="tip
|
|
53
|
+
<span bind:this={tip} role="tooltip" id="tern-tooltip-{uid}" class="tip" class:open popover="manual">{text}</span>
|
|
40
54
|
</span>
|
|
41
55
|
|
|
42
56
|
<style>
|
|
43
57
|
.tern-tooltip {
|
|
44
|
-
position: relative;
|
|
45
58
|
display: inline-flex;
|
|
46
59
|
}
|
|
47
60
|
.tip {
|
|
48
|
-
position: absolute;
|
|
49
|
-
left: 50%;
|
|
50
|
-
z-index: 60;
|
|
51
61
|
width: max-content;
|
|
52
62
|
max-width: 16rem;
|
|
53
63
|
padding: 0.35rem 0.55rem;
|
|
64
|
+
border: 0;
|
|
54
65
|
border-radius: var(--tern-radius-sm);
|
|
55
66
|
background: var(--tern-fg);
|
|
56
67
|
color: var(--tern-bg);
|
|
57
68
|
font-size: 0.75rem;
|
|
58
69
|
line-height: 1.35;
|
|
59
70
|
pointer-events: none;
|
|
60
|
-
opacity: 0;
|
|
61
|
-
visibility: hidden;
|
|
62
|
-
translate: -50% 0;
|
|
63
|
-
transition:
|
|
64
|
-
opacity var(--tern-duration),
|
|
65
|
-
visibility var(--tern-duration);
|
|
66
71
|
}
|
|
67
|
-
.
|
|
68
|
-
|
|
72
|
+
.tip.open {
|
|
73
|
+
animation: tern-tip-in var(--tern-duration) ease-out;
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
@keyframes tern-tip-in {
|
|
76
|
+
from {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
@media (prefers-reduced-motion: reduce) {
|
|
81
|
+
.tip.open {
|
|
82
|
+
animation: none;
|
|
83
|
+
}
|
|
76
84
|
}
|
|
77
85
|
</style>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type Side } from './floating.js';
|
|
2
3
|
type Props = {
|
|
3
4
|
/** The tooltip's text. It describes the trigger; don't put anything interactive in it. */
|
|
4
5
|
text: string;
|
|
5
|
-
side?:
|
|
6
|
+
side?: Side;
|
|
6
7
|
/** How long to wait before showing, in milliseconds. */
|
|
7
8
|
delay?: number;
|
|
8
9
|
/** The trigger, which must be focusable, such as a Button. It receives aria-describedby through the snippet's props. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type XValue = number | string | Date;
|
|
2
|
+
export type Series<Row> = {
|
|
3
|
+
/** The field holding this series' numbers. */
|
|
4
|
+
key: keyof Row & string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
/** The series colors, in their fixed order. A ninth series folds into "Other". */
|
|
8
|
+
export declare const seriesColor: (i: number) => string;
|
|
9
|
+
/** Turns x values into numbers for time and number axes, or null for categories. */
|
|
10
|
+
export declare function numericX(values: XValue[]): number[] | null;
|
|
11
|
+
export declare const isTime: (values: XValue[]) => boolean;
|
|
12
|
+
/** Round ticks covering [min, max], about count of them. */
|
|
13
|
+
export declare function niceTicks(min: number, max: number, count?: number): number[];
|
|
14
|
+
/** 1,284 in full under 10,000; 12K, 4.2M above. */
|
|
15
|
+
export declare function formatNumber(n: number, compact?: boolean): string;
|
|
16
|
+
/** Evenly spaced time ticks with a format that fits their spacing. */
|
|
17
|
+
export declare function timeTicks(min: number, max: number, count?: number): {
|
|
18
|
+
ticks: number[];
|
|
19
|
+
format: (t: number) => string;
|
|
20
|
+
};
|
|
21
|
+
/** A readable label for an x value in tooltips and tables. */
|
|
22
|
+
export declare function formatX(v: XValue, time: boolean): string;
|
|
23
|
+
/** An estimate of how wide a label is, for margins; the charts' text is 12px. */
|
|
24
|
+
export declare const textWidth: (s: string) => number;
|
|
25
|
+
/** Array.prototype.findLastIndex, for older targets. */
|
|
26
|
+
export declare function lastIndex<T>(a: T[], ok: (v: T) => boolean): number;
|
package/dist/ui/chart.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Scales, ticks and formatting shared by the charts.
|
|
2
|
+
/** The series colors, in their fixed order. A ninth series folds into "Other". */
|
|
3
|
+
export const seriesColor = (i) => `var(--tern-chart-${(i % 8) + 1})`;
|
|
4
|
+
const isoDate = /^\d{4}-\d{2}-\d{2}([T ][\d:.]+(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
5
|
+
/** Turns x values into numbers for time and number axes, or null for categories. */
|
|
6
|
+
export function numericX(values) {
|
|
7
|
+
if (values.every((v) => typeof v === 'number'))
|
|
8
|
+
return values;
|
|
9
|
+
if (values.every((v) => v instanceof Date))
|
|
10
|
+
return values.map((d) => d.getTime());
|
|
11
|
+
if (values.every((v) => typeof v === 'string' && isoDate.test(v)))
|
|
12
|
+
return values.map((s) => Date.parse(s.length === 10 ? s + 'T00:00:00Z' : s));
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export const isTime = (values) => values.length > 0 && !values.every((v) => typeof v === 'number') && numericX(values) !== null;
|
|
16
|
+
/** Round ticks covering [min, max], about count of them. */
|
|
17
|
+
export function niceTicks(min, max, count = 5) {
|
|
18
|
+
if (min === max) {
|
|
19
|
+
if (min === 0)
|
|
20
|
+
return [0, 1];
|
|
21
|
+
[min, max] = min > 0 ? [0, min] : [min, 0];
|
|
22
|
+
}
|
|
23
|
+
const span = max - min;
|
|
24
|
+
const raw = span / Math.max(1, count);
|
|
25
|
+
const mag = 10 ** Math.floor(Math.log10(raw));
|
|
26
|
+
const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => span / s <= count) ?? 10 * mag;
|
|
27
|
+
const start = Math.floor(min / step) * step;
|
|
28
|
+
const end = Math.ceil(max / step) * step;
|
|
29
|
+
const out = [];
|
|
30
|
+
for (let v = start; v <= end + step / 2; v += step)
|
|
31
|
+
out.push(Number(v.toFixed(10)));
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** 1,284 in full under 10,000; 12K, 4.2M above. */
|
|
35
|
+
export function formatNumber(n, compact = Math.abs(n) >= 10_000) {
|
|
36
|
+
return new Intl.NumberFormat(undefined, compact ? { notation: 'compact', maximumFractionDigits: 1 } : { maximumFractionDigits: 2 }).format(n);
|
|
37
|
+
}
|
|
38
|
+
const DAY = 86_400_000;
|
|
39
|
+
/** Evenly spaced time ticks with a format that fits their spacing. */
|
|
40
|
+
export function timeTicks(min, max, count = 6) {
|
|
41
|
+
const span = max - min;
|
|
42
|
+
const fmt = (o) => {
|
|
43
|
+
const f = new Intl.DateTimeFormat(undefined, { timeZone: 'UTC', ...o });
|
|
44
|
+
return (t) => f.format(new Date(t));
|
|
45
|
+
};
|
|
46
|
+
if (span <= 2 * DAY) {
|
|
47
|
+
const step = [1, 2, 3, 6, 12].map((h) => h * 3_600_000).find((s) => span / s <= count) ?? 24 * 3_600_000;
|
|
48
|
+
return { ticks: stepTicks(min, max, step), format: fmt({ hour: '2-digit', minute: '2-digit' }) };
|
|
49
|
+
}
|
|
50
|
+
if (span <= 120 * DAY) {
|
|
51
|
+
const step = [1, 2, 7, 14].map((d) => d * DAY).find((s) => span / s <= count) ?? 28 * DAY;
|
|
52
|
+
return { ticks: stepTicks(min, max, step), format: fmt({ month: 'short', day: 'numeric' }) };
|
|
53
|
+
}
|
|
54
|
+
const months = [];
|
|
55
|
+
const d = new Date(min);
|
|
56
|
+
d.setUTCDate(1);
|
|
57
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
58
|
+
const every = Math.max(1, Math.ceil(span / (30 * DAY) / count));
|
|
59
|
+
while (d.getTime() <= max) {
|
|
60
|
+
if (d.getTime() >= min)
|
|
61
|
+
months.push(d.getTime());
|
|
62
|
+
d.setUTCMonth(d.getUTCMonth() + every);
|
|
63
|
+
}
|
|
64
|
+
return { ticks: months, format: fmt(span > 400 * DAY ? { month: 'short', year: 'numeric' } : { month: 'short' }) };
|
|
65
|
+
}
|
|
66
|
+
function stepTicks(min, max, step) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (let t = Math.ceil(min / step) * step; t <= max; t += step)
|
|
69
|
+
out.push(t);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** A readable label for an x value in tooltips and tables. */
|
|
73
|
+
export function formatX(v, time) {
|
|
74
|
+
if (!time)
|
|
75
|
+
return v instanceof Date ? v.toISOString() : String(v);
|
|
76
|
+
const t = v instanceof Date ? v.getTime() : typeof v === 'number' ? v : Date.parse(v.length === 10 ? v + 'T00:00:00Z' : v);
|
|
77
|
+
const d = new Date(t);
|
|
78
|
+
const hasTime = d.getUTCHours() || d.getUTCMinutes();
|
|
79
|
+
return new Intl.DateTimeFormat(undefined, { timeZone: 'UTC', dateStyle: 'medium', ...(hasTime ? { timeStyle: 'short' } : {}) }).format(d);
|
|
80
|
+
}
|
|
81
|
+
/** An estimate of how wide a label is, for margins; the charts' text is 12px. */
|
|
82
|
+
export const textWidth = (s) => s.length * 6.6;
|
|
83
|
+
/** Array.prototype.findLastIndex, for older targets. */
|
|
84
|
+
export function lastIndex(a, ok) {
|
|
85
|
+
for (let i = a.length - 1; i >= 0; i--)
|
|
86
|
+
if (ok(a[i]))
|
|
87
|
+
return i;
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ISODate = string;
|
|
2
|
+
export declare function parse(iso: ISODate): Date;
|
|
3
|
+
export declare function iso(d: Date): ISODate;
|
|
4
|
+
export declare function today(): ISODate;
|
|
5
|
+
export declare function addDays(date: ISODate, n: number): ISODate;
|
|
6
|
+
/** Adds months, keeping the day where the month has it, else its last day. */
|
|
7
|
+
export declare function addMonths(date: ISODate, n: number): ISODate;
|
|
8
|
+
export declare function startOfMonth(date: ISODate): ISODate;
|
|
9
|
+
/** The weeks shown for date's month, each 7 ISO dates, starting on weekStart (0 = Sunday). */
|
|
10
|
+
export declare function monthGrid(date: ISODate, weekStart: number): ISODate[][];
|
|
11
|
+
/** The first day of the week for a locale, from Intl where the browser knows it. */
|
|
12
|
+
export declare function localeWeekStart(locale?: string): number;
|
|
13
|
+
export declare function format(date: ISODate, locale: string | undefined, options: Intl.DateTimeFormatOptions): string;
|