@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,138 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Calendar, { type DateRange } from './Calendar.svelte';
|
|
3
|
+
import Popover from './Popover.svelte';
|
|
4
|
+
import { format, type ISODate } from './dates.js';
|
|
5
|
+
import { getField } from './field.js';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
mode?: 'single' | 'range';
|
|
9
|
+
/** The chosen day (single) or { start, end } (range), as ISO dates; bind it. */
|
|
10
|
+
value?: ISODate | null | DateRange;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** Labels it when it isn't in a Field. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Posts the date as YYYY-MM-DD under this name; a range posts name and name + "_end". */
|
|
15
|
+
name?: string;
|
|
16
|
+
min?: ISODate;
|
|
17
|
+
max?: ISODate;
|
|
18
|
+
isDisabled?: (date: ISODate) => boolean;
|
|
19
|
+
locale?: string;
|
|
20
|
+
weekStart?: number;
|
|
21
|
+
/** How the chosen date is shown. */
|
|
22
|
+
display?: Intl.DateTimeFormatOptions;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
onchange?: (value: ISODate | DateRange) => void;
|
|
26
|
+
};
|
|
27
|
+
const field = getField();
|
|
28
|
+
let {
|
|
29
|
+
mode = 'single',
|
|
30
|
+
value = $bindable(mode === 'range' ? { start: null, end: null } : null),
|
|
31
|
+
placeholder = mode === 'range' ? 'Pick dates' : 'Pick a date',
|
|
32
|
+
label,
|
|
33
|
+
name: nameProp,
|
|
34
|
+
min,
|
|
35
|
+
max,
|
|
36
|
+
isDisabled,
|
|
37
|
+
locale,
|
|
38
|
+
weekStart,
|
|
39
|
+
display = { year: 'numeric', month: 'short', day: 'numeric' },
|
|
40
|
+
disabled = false,
|
|
41
|
+
class: klass = '',
|
|
42
|
+
onchange
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
const name = $derived(nameProp ?? field?.name);
|
|
45
|
+
|
|
46
|
+
const uid = $props.id();
|
|
47
|
+
field?.custom();
|
|
48
|
+
let open = $state(false);
|
|
49
|
+
const triggerId = field?.id ?? `tern-datepicker-${uid}-trigger`;
|
|
50
|
+
|
|
51
|
+
const text = $derived.by(() => {
|
|
52
|
+
if (mode === 'single') return value ? format(value as ISODate, locale, display) : '';
|
|
53
|
+
const r = value as DateRange | null;
|
|
54
|
+
if (!r?.start) return '';
|
|
55
|
+
return `${format(r.start, locale, display)} – ${r.end ? format(r.end, locale, display) : '…'}`;
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<Popover bind:open label={label ?? 'Choose a date'} align="start" {triggerId}>
|
|
60
|
+
{#snippet trigger(props)}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
class="tern-datepicker {klass}"
|
|
64
|
+
{...props}
|
|
65
|
+
aria-labelledby="{field?.labelId ?? `tern-datepicker-${uid}-label`} tern-datepicker-{uid}-value"
|
|
66
|
+
aria-describedby={field?.describedBy}
|
|
67
|
+
disabled={disabled || field?.disabled}
|
|
68
|
+
>
|
|
69
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="2.5" y="3.5" width="11" height="10" rx="2" /><path d="M2.5 6.5h11M5.5 2v3M10.5 2v3" /></svg>
|
|
70
|
+
<span class:placeholder={!text} id="tern-datepicker-{uid}-value">{text || placeholder}</span>
|
|
71
|
+
</button>
|
|
72
|
+
{#if !field}<span class="tern-sr-only" id="tern-datepicker-{uid}-label">{label ?? 'Date'}</span>{/if}
|
|
73
|
+
{/snippet}
|
|
74
|
+
<Calendar
|
|
75
|
+
bind:value
|
|
76
|
+
{mode}
|
|
77
|
+
{min}
|
|
78
|
+
{max}
|
|
79
|
+
{isDisabled}
|
|
80
|
+
{locale}
|
|
81
|
+
{weekStart}
|
|
82
|
+
label={label ?? 'Choose a date'}
|
|
83
|
+
onselect={(v) => {
|
|
84
|
+
onchange?.(v);
|
|
85
|
+
open = false;
|
|
86
|
+
document.getElementById(triggerId)?.focus();
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</Popover>
|
|
90
|
+
{#if name}
|
|
91
|
+
{#if mode === 'single'}
|
|
92
|
+
<input type="hidden" {name} value={(value as ISODate | null) ?? ''} />
|
|
93
|
+
{:else}
|
|
94
|
+
<input type="hidden" {name} value={(value as DateRange | null)?.start ?? ''} />
|
|
95
|
+
<input type="hidden" name="{name}_end" value={(value as DateRange | null)?.end ?? ''} />
|
|
96
|
+
{/if}
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.tern-datepicker {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 0.5rem;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: var(--tern-control-height);
|
|
106
|
+
padding: 0 0.75rem;
|
|
107
|
+
font: inherit;
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
color: var(--tern-fg);
|
|
110
|
+
text-align: start;
|
|
111
|
+
background: var(--tern-surface);
|
|
112
|
+
border: 1px solid var(--tern-border-strong);
|
|
113
|
+
border-radius: var(--tern-radius-sm);
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
116
|
+
.tern-datepicker svg {
|
|
117
|
+
flex: none;
|
|
118
|
+
color: var(--tern-muted);
|
|
119
|
+
}
|
|
120
|
+
.tern-datepicker:hover:not(:disabled) {
|
|
121
|
+
border-color: var(--tern-muted);
|
|
122
|
+
}
|
|
123
|
+
.tern-datepicker:focus-visible {
|
|
124
|
+
outline: none;
|
|
125
|
+
border-color: var(--tern-accent);
|
|
126
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
|
|
127
|
+
}
|
|
128
|
+
.tern-datepicker[aria-invalid='true'] {
|
|
129
|
+
border-color: var(--tern-danger);
|
|
130
|
+
}
|
|
131
|
+
.tern-datepicker:disabled {
|
|
132
|
+
opacity: 0.6;
|
|
133
|
+
cursor: not-allowed;
|
|
134
|
+
}
|
|
135
|
+
.placeholder {
|
|
136
|
+
color: var(--tern-muted);
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DateRange } from './Calendar.svelte';
|
|
2
|
+
import { type ISODate } from './dates.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
mode?: 'single' | 'range';
|
|
5
|
+
/** The chosen day (single) or { start, end } (range), as ISO dates; bind it. */
|
|
6
|
+
value?: ISODate | null | DateRange;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Labels it when it isn't in a Field. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Posts the date as YYYY-MM-DD under this name; a range posts name and name + "_end". */
|
|
11
|
+
name?: string;
|
|
12
|
+
min?: ISODate;
|
|
13
|
+
max?: ISODate;
|
|
14
|
+
isDisabled?: (date: ISODate) => boolean;
|
|
15
|
+
locale?: string;
|
|
16
|
+
weekStart?: number;
|
|
17
|
+
/** How the chosen date is shown. */
|
|
18
|
+
display?: Intl.DateTimeFormatOptions;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
class?: string;
|
|
21
|
+
onchange?: (value: ISODate | DateRange) => void;
|
|
22
|
+
};
|
|
23
|
+
declare const DatePicker: import("svelte").Component<Props, {}, "value">;
|
|
24
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
25
|
+
export default DatePicker;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { trapTab } from './floating.js';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
/** Whether the dialog is open; bind it. */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
/** The heading, which labels the dialog. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** A sentence under the title, read when the dialog opens. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Close when the backdrop is clicked. Escape always closes. */
|
|
13
|
+
dismissable?: boolean;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
/** center for a dialog; an edge slides it in from there, as Sheet and Drawer do. */
|
|
16
|
+
side?: 'center' | 'left' | 'right' | 'top' | 'bottom';
|
|
17
|
+
/** alertdialog for confirmations that interrupt, as AlertDialog uses. */
|
|
18
|
+
role?: 'dialog' | 'alertdialog';
|
|
19
|
+
/** Show the close button in the corner. */
|
|
20
|
+
closeButton?: boolean;
|
|
21
|
+
/** Drag the panel down to close it, with a handle at the top. For bottom sheets on touch screens. */
|
|
22
|
+
draggable?: boolean;
|
|
23
|
+
/** The element to focus when it opens, instead of the first focusable one. */
|
|
24
|
+
initialFocus?: () => HTMLElement | null | undefined;
|
|
25
|
+
class?: string;
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
/** Buttons at the bottom, such as Cancel and Save. */
|
|
28
|
+
footer?: Snippet;
|
|
29
|
+
onclose?: () => void;
|
|
30
|
+
};
|
|
31
|
+
let {
|
|
32
|
+
open = $bindable(false),
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
dismissable = true,
|
|
36
|
+
size = 'md',
|
|
37
|
+
side = 'center',
|
|
38
|
+
role = 'dialog',
|
|
39
|
+
closeButton = true,
|
|
40
|
+
draggable = false,
|
|
41
|
+
initialFocus,
|
|
42
|
+
class: klass = '',
|
|
43
|
+
children,
|
|
44
|
+
footer,
|
|
45
|
+
onclose
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const uid = $props.id();
|
|
49
|
+
let dialog = $state<HTMLDialogElement>();
|
|
50
|
+
|
|
51
|
+
// The native dialog makes the page inert, closes on Escape and returns
|
|
52
|
+
// focus to what opened it.
|
|
53
|
+
$effect(() => {
|
|
54
|
+
if (!dialog) return;
|
|
55
|
+
if (open && !dialog.open) {
|
|
56
|
+
dialog.showModal();
|
|
57
|
+
const el = initialFocus?.();
|
|
58
|
+
if (el) el.focus();
|
|
59
|
+
} else if (!open && dialog.open) dialog.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Dragging a bottom sheet down closes it once it passes a quarter of its height.
|
|
63
|
+
let drag = $state(0);
|
|
64
|
+
let dragStart: number | null = null;
|
|
65
|
+
function dragDown(e: PointerEvent) {
|
|
66
|
+
if (!draggable) return;
|
|
67
|
+
dragStart = e.clientY;
|
|
68
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
69
|
+
}
|
|
70
|
+
function dragMove(e: PointerEvent) {
|
|
71
|
+
if (dragStart !== null) drag = Math.max(0, e.clientY - dragStart);
|
|
72
|
+
}
|
|
73
|
+
function dragEnd(e: PointerEvent) {
|
|
74
|
+
if (dragStart === null) return;
|
|
75
|
+
const panel = (e.currentTarget as HTMLElement).closest('.panel') as HTMLElement | null;
|
|
76
|
+
if (panel && drag > panel.offsetHeight / 4) open = false;
|
|
77
|
+
dragStart = null;
|
|
78
|
+
drag = 0;
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<dialog
|
|
83
|
+
bind:this={dialog}
|
|
84
|
+
class="tern-dialog {size} {side} {klass}"
|
|
85
|
+
{role}
|
|
86
|
+
aria-labelledby="tern-dialog-{uid}-title"
|
|
87
|
+
aria-describedby={description ? `tern-dialog-${uid}-description` : undefined}
|
|
88
|
+
onclose={() => {
|
|
89
|
+
open = false;
|
|
90
|
+
onclose?.();
|
|
91
|
+
}}
|
|
92
|
+
onclick={(e) => {
|
|
93
|
+
if (dismissable && e.target === dialog) open = false;
|
|
94
|
+
}}
|
|
95
|
+
onkeydown={(e) => dialog && trapTab(dialog, e)}
|
|
96
|
+
>
|
|
97
|
+
<div class="panel" style:translate={drag ? `0 ${drag}px` : undefined}>
|
|
98
|
+
{#if draggable}
|
|
99
|
+
<div class="handle" aria-hidden="true" onpointerdown={dragDown} onpointermove={dragMove} onpointerup={dragEnd} onpointercancel={dragEnd}><span></span></div>
|
|
100
|
+
{/if}
|
|
101
|
+
<header>
|
|
102
|
+
<h2 id="tern-dialog-{uid}-title">{title}</h2>
|
|
103
|
+
{#if closeButton}
|
|
104
|
+
<button type="button" class="close" aria-label="Close" onclick={() => (open = false)}>
|
|
105
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
|
|
106
|
+
</button>
|
|
107
|
+
{/if}
|
|
108
|
+
</header>
|
|
109
|
+
{#if description}<p class="description" id="tern-dialog-{uid}-description">{description}</p>{/if}
|
|
110
|
+
{#if children}<div class="body">{@render children()}</div>{/if}
|
|
111
|
+
{#if footer}<footer>{@render footer()}</footer>{/if}
|
|
112
|
+
</div>
|
|
113
|
+
</dialog>
|
|
114
|
+
|
|
115
|
+
<style>
|
|
116
|
+
.tern-dialog {
|
|
117
|
+
padding: 0;
|
|
118
|
+
border: 0;
|
|
119
|
+
background: transparent;
|
|
120
|
+
color: var(--tern-fg);
|
|
121
|
+
max-width: calc(100vw - 2rem);
|
|
122
|
+
width: 28rem;
|
|
123
|
+
overflow: visible;
|
|
124
|
+
}
|
|
125
|
+
.sm {
|
|
126
|
+
width: 22rem;
|
|
127
|
+
}
|
|
128
|
+
.lg {
|
|
129
|
+
width: 40rem;
|
|
130
|
+
}
|
|
131
|
+
.tern-dialog::backdrop {
|
|
132
|
+
background: oklch(0.2 0.02 240 / 0.45);
|
|
133
|
+
backdrop-filter: blur(2px);
|
|
134
|
+
}
|
|
135
|
+
.tern-dialog[open] .panel {
|
|
136
|
+
animation: rise var(--tern-duration) ease-out;
|
|
137
|
+
}
|
|
138
|
+
.panel {
|
|
139
|
+
display: grid;
|
|
140
|
+
gap: 1rem;
|
|
141
|
+
padding: 1.25rem 1.5rem 1.5rem;
|
|
142
|
+
background: var(--tern-surface);
|
|
143
|
+
border: 1px solid var(--tern-border);
|
|
144
|
+
border-radius: calc(var(--tern-radius) + 4px);
|
|
145
|
+
box-shadow: var(--tern-shadow-lg);
|
|
146
|
+
}
|
|
147
|
+
header {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: flex-start;
|
|
150
|
+
justify-content: space-between;
|
|
151
|
+
gap: 1rem;
|
|
152
|
+
}
|
|
153
|
+
h2 {
|
|
154
|
+
margin: 0;
|
|
155
|
+
font-size: 1.0625rem;
|
|
156
|
+
font-weight: 650;
|
|
157
|
+
letter-spacing: -0.01em;
|
|
158
|
+
}
|
|
159
|
+
.description {
|
|
160
|
+
margin: -0.5rem 0 0;
|
|
161
|
+
color: var(--tern-muted);
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
}
|
|
164
|
+
.close {
|
|
165
|
+
display: grid;
|
|
166
|
+
place-items: center;
|
|
167
|
+
width: 1.75rem;
|
|
168
|
+
height: 1.75rem;
|
|
169
|
+
margin: -0.25rem -0.5rem 0 0;
|
|
170
|
+
border: 0;
|
|
171
|
+
border-radius: var(--tern-radius-sm);
|
|
172
|
+
background: transparent;
|
|
173
|
+
color: var(--tern-muted);
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
}
|
|
176
|
+
.close:hover {
|
|
177
|
+
background: var(--tern-surface-2);
|
|
178
|
+
color: var(--tern-fg);
|
|
179
|
+
}
|
|
180
|
+
.close:focus-visible {
|
|
181
|
+
outline: 2px solid var(--tern-ring);
|
|
182
|
+
outline-offset: 1px;
|
|
183
|
+
}
|
|
184
|
+
footer {
|
|
185
|
+
display: flex;
|
|
186
|
+
justify-content: flex-end;
|
|
187
|
+
gap: 0.5rem;
|
|
188
|
+
margin-top: 0.25rem;
|
|
189
|
+
}
|
|
190
|
+
/* Sheets: full height or width, against an edge. */
|
|
191
|
+
.tern-dialog:not(.center) {
|
|
192
|
+
margin: 0;
|
|
193
|
+
max-width: 100vw;
|
|
194
|
+
max-height: 100vh;
|
|
195
|
+
}
|
|
196
|
+
.tern-dialog.left,
|
|
197
|
+
.tern-dialog.right {
|
|
198
|
+
height: 100vh;
|
|
199
|
+
width: min(24rem, 100vw - 3rem);
|
|
200
|
+
}
|
|
201
|
+
.tern-dialog.left {
|
|
202
|
+
margin-right: auto;
|
|
203
|
+
}
|
|
204
|
+
.tern-dialog.right {
|
|
205
|
+
margin-left: auto;
|
|
206
|
+
}
|
|
207
|
+
.tern-dialog.top,
|
|
208
|
+
.tern-dialog.bottom {
|
|
209
|
+
width: 100vw;
|
|
210
|
+
}
|
|
211
|
+
.tern-dialog.top {
|
|
212
|
+
margin-bottom: auto;
|
|
213
|
+
}
|
|
214
|
+
.tern-dialog.bottom {
|
|
215
|
+
margin-top: auto;
|
|
216
|
+
}
|
|
217
|
+
.tern-dialog:not(.center) .panel {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
height: 100%;
|
|
221
|
+
box-sizing: border-box;
|
|
222
|
+
overflow: auto;
|
|
223
|
+
border-radius: 0;
|
|
224
|
+
}
|
|
225
|
+
.tern-dialog.left footer,
|
|
226
|
+
.tern-dialog.right footer {
|
|
227
|
+
margin-top: auto;
|
|
228
|
+
}
|
|
229
|
+
.left .panel {
|
|
230
|
+
border-width: 0 1px 0 0;
|
|
231
|
+
}
|
|
232
|
+
.right .panel {
|
|
233
|
+
border-width: 0 0 0 1px;
|
|
234
|
+
}
|
|
235
|
+
.top .panel {
|
|
236
|
+
border-width: 0 0 1px;
|
|
237
|
+
border-radius: 0 0 calc(var(--tern-radius) + 4px) calc(var(--tern-radius) + 4px);
|
|
238
|
+
}
|
|
239
|
+
.bottom .panel {
|
|
240
|
+
max-height: 85vh;
|
|
241
|
+
border-width: 1px 0 0;
|
|
242
|
+
border-radius: calc(var(--tern-radius) + 4px) calc(var(--tern-radius) + 4px) 0 0;
|
|
243
|
+
}
|
|
244
|
+
.tern-dialog.left[open] .panel {
|
|
245
|
+
animation-name: from-left;
|
|
246
|
+
}
|
|
247
|
+
.tern-dialog.right[open] .panel {
|
|
248
|
+
animation-name: from-right;
|
|
249
|
+
}
|
|
250
|
+
.tern-dialog.top[open] .panel {
|
|
251
|
+
animation-name: from-top;
|
|
252
|
+
}
|
|
253
|
+
.tern-dialog.bottom[open] .panel {
|
|
254
|
+
animation-name: from-bottom;
|
|
255
|
+
}
|
|
256
|
+
.handle {
|
|
257
|
+
display: grid;
|
|
258
|
+
place-items: center;
|
|
259
|
+
margin: -0.75rem 0 -0.25rem;
|
|
260
|
+
padding: 0.5rem;
|
|
261
|
+
cursor: grab;
|
|
262
|
+
touch-action: none;
|
|
263
|
+
}
|
|
264
|
+
.handle span {
|
|
265
|
+
width: 2.5rem;
|
|
266
|
+
height: 0.3rem;
|
|
267
|
+
border-radius: 1rem;
|
|
268
|
+
background: var(--tern-border-strong);
|
|
269
|
+
}
|
|
270
|
+
@keyframes from-left {
|
|
271
|
+
from {
|
|
272
|
+
transform: translateX(-100%);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
@keyframes from-right {
|
|
276
|
+
from {
|
|
277
|
+
transform: translateX(100%);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
@keyframes from-top {
|
|
281
|
+
from {
|
|
282
|
+
transform: translateY(-100%);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
@keyframes from-bottom {
|
|
286
|
+
from {
|
|
287
|
+
transform: translateY(100%);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
@media (prefers-reduced-motion: reduce) {
|
|
291
|
+
.tern-dialog[open] .panel {
|
|
292
|
+
animation: none !important;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
@keyframes rise {
|
|
296
|
+
from {
|
|
297
|
+
opacity: 0;
|
|
298
|
+
transform: translateY(6px) scale(0.985);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Whether the dialog is open; bind it. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** The heading, which labels the dialog. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** A sentence under the title, read when the dialog opens. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Close when the backdrop is clicked. Escape always closes. */
|
|
10
|
+
dismissable?: boolean;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
/** center for a dialog; an edge slides it in from there, as Sheet and Drawer do. */
|
|
13
|
+
side?: 'center' | 'left' | 'right' | 'top' | 'bottom';
|
|
14
|
+
/** alertdialog for confirmations that interrupt, as AlertDialog uses. */
|
|
15
|
+
role?: 'dialog' | 'alertdialog';
|
|
16
|
+
/** Show the close button in the corner. */
|
|
17
|
+
closeButton?: boolean;
|
|
18
|
+
/** Drag the panel down to close it, with a handle at the top. For bottom sheets on touch screens. */
|
|
19
|
+
draggable?: boolean;
|
|
20
|
+
/** The element to focus when it opens, instead of the first focusable one. */
|
|
21
|
+
initialFocus?: () => HTMLElement | null | undefined;
|
|
22
|
+
class?: string;
|
|
23
|
+
children?: Snippet;
|
|
24
|
+
/** Buttons at the bottom, such as Cancel and Save. */
|
|
25
|
+
footer?: Snippet;
|
|
26
|
+
onclose?: () => void;
|
|
27
|
+
};
|
|
28
|
+
declare const Dialog: import("svelte").Component<Props, {}, "open">;
|
|
29
|
+
type Dialog = ReturnType<typeof Dialog>;
|
|
30
|
+
export default Dialog;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { formatNumber, seriesColor } from './chart.js';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
data: Row[];
|
|
7
|
+
/** The field naming each part. */
|
|
8
|
+
label: keyof Row & string;
|
|
9
|
+
/** The field with each part's amount. */
|
|
10
|
+
value: keyof Row & string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
level?: 2 | 3 | 4;
|
|
14
|
+
/** Parts after this many fold into "Other". */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** The total's caption in the middle, such as "visits". */
|
|
17
|
+
totalLabel?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
format?: (v: number) => string;
|
|
20
|
+
class?: string;
|
|
21
|
+
};
|
|
22
|
+
let { data, label, value, title, description, level, max = 6, totalLabel = 'total', size = 180, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
|
|
23
|
+
|
|
24
|
+
let active = $state<number | null>(null);
|
|
25
|
+
|
|
26
|
+
// Biggest first; the tail folds into Other so colors stay distinct.
|
|
27
|
+
const parts = $derived.by(() => {
|
|
28
|
+
const rows = data
|
|
29
|
+
.map((r) => {
|
|
30
|
+
const o = r as Record<string, unknown>;
|
|
31
|
+
return { label: String(o[label] ?? ''), value: Math.max(0, Number(o[value]) || 0) };
|
|
32
|
+
})
|
|
33
|
+
.sort((a, b) => b.value - a.value);
|
|
34
|
+
if (rows.length <= max) return rows;
|
|
35
|
+
const keep = rows.slice(0, max - 1);
|
|
36
|
+
return [...keep, { label: 'Other', value: rows.slice(max - 1).reduce((a, r) => a + r.value, 0) }];
|
|
37
|
+
});
|
|
38
|
+
const total = $derived(parts.reduce((a, p) => a + p.value, 0));
|
|
39
|
+
const r = $derived(size / 2);
|
|
40
|
+
const inner = $derived(r * 0.62);
|
|
41
|
+
|
|
42
|
+
const arcs = $derived.by(() => {
|
|
43
|
+
let a = -Math.PI / 2;
|
|
44
|
+
const gap = parts.length > 1 ? 2 / r : 0; // a 2px surface gap between parts
|
|
45
|
+
return parts.map((p, i) => {
|
|
46
|
+
const sweep = total ? (p.value / total) * Math.PI * 2 : 0;
|
|
47
|
+
const from = a + gap / 2;
|
|
48
|
+
const to = a + sweep - gap / 2;
|
|
49
|
+
a += sweep;
|
|
50
|
+
return { i, d: sweep > gap ? arc(from, to) : '' };
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
function arc(from: number, to: number) {
|
|
54
|
+
const large = to - from > Math.PI ? 1 : 0;
|
|
55
|
+
const p = (rad: number, a: number) => `${(r + rad * Math.cos(a)).toFixed(2)},${(r + rad * Math.sin(a)).toFixed(2)}`;
|
|
56
|
+
if (to - from >= Math.PI * 2 - 0.001) return `M${p(r, from)}A${r},${r} 0 1 1 ${p(r, from + Math.PI)}A${r},${r} 0 1 1 ${p(r, from)}M${p(inner, from)}A${inner},${inner} 0 1 0 ${p(inner, from + Math.PI)}A${inner},${inner} 0 1 0 ${p(inner, from)}Z`;
|
|
57
|
+
return `M${p(r, from)}A${r},${r} 0 ${large} 1 ${p(r, to)}L${p(inner, to)}A${inner},${inner} 0 ${large} 0 ${p(inner, from)}Z`;
|
|
58
|
+
}
|
|
59
|
+
const pct = (v: number) => (total ? Math.round((v / total) * 1000) / 10 : 0);
|
|
60
|
+
|
|
61
|
+
function onkeydown(e: KeyboardEvent) {
|
|
62
|
+
const last = parts.length - 1;
|
|
63
|
+
const moves: Record<string, number> = { ArrowRight: Math.min(last, (active ?? -1) + 1), ArrowDown: Math.min(last, (active ?? -1) + 1), ArrowLeft: Math.max(0, (active ?? 1) - 1), ArrowUp: Math.max(0, (active ?? 1) - 1), Home: 0, End: last };
|
|
64
|
+
if (e.key in moves) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
active = moves[e.key]!;
|
|
67
|
+
} else if (e.key === 'Escape') active = null;
|
|
68
|
+
}
|
|
69
|
+
const table = $derived({ headers: [label, value, 'Share'], rows: parts.map((p) => [p.label, format(p.value), `${pct(p.value)}%`]) });
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<ChartFrame
|
|
73
|
+
{title}
|
|
74
|
+
{description}
|
|
75
|
+
{level}
|
|
76
|
+
class={klass}
|
|
77
|
+
legend={parts.map((p, i) => ({ label: `${p.label} · ${pct(p.value)}%`, color: seriesColor(i), kind: 'rect' }))}
|
|
78
|
+
{table}
|
|
79
|
+
readout={active === null ? '' : `${parts[active]!.label}: ${format(parts[active]!.value)}, ${pct(parts[active]!.value)}%`}
|
|
80
|
+
>
|
|
81
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
82
|
+
<div
|
|
83
|
+
class="tern-plot donut"
|
|
84
|
+
role="group"
|
|
85
|
+
aria-roledescription="chart"
|
|
86
|
+
aria-label="{title}. Use the arrow keys to read values."
|
|
87
|
+
tabindex="0"
|
|
88
|
+
style:width="{size}px"
|
|
89
|
+
onpointerleave={() => (active = null)}
|
|
90
|
+
onfocus={() => active === null && (active = 0)}
|
|
91
|
+
onblur={() => (active = null)}
|
|
92
|
+
{onkeydown}
|
|
93
|
+
>
|
|
94
|
+
<svg width={size} height={size} viewBox="0 0 {size} {size}" aria-hidden="true">
|
|
95
|
+
{#each arcs as a (a.i)}
|
|
96
|
+
<path d={a.d} fill={seriesColor(a.i)} fill-rule="evenodd" opacity={active === null || active === a.i ? 1 : 0.5} onpointerenter={() => (active = a.i)} role="presentation" />
|
|
97
|
+
{/each}
|
|
98
|
+
<text x={r} y={r - 4} text-anchor="middle" class="total">{active === null ? format(total) : format(parts[active]!.value)}</text>
|
|
99
|
+
<text x={r} y={r + 14} text-anchor="middle" class="tick">{active === null ? totalLabel : parts[active]!.label}</text>
|
|
100
|
+
</svg>
|
|
101
|
+
</div>
|
|
102
|
+
</ChartFrame>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.donut {
|
|
106
|
+
justify-self: start;
|
|
107
|
+
max-width: 100%;
|
|
108
|
+
}
|
|
109
|
+
.total {
|
|
110
|
+
fill: var(--tern-fg);
|
|
111
|
+
font-size: 20px;
|
|
112
|
+
font-weight: 650;
|
|
113
|
+
font-variant-numeric: tabular-nums;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare function $$render<Row extends object>(): {
|
|
2
|
+
props: {
|
|
3
|
+
data: Row[];
|
|
4
|
+
/** The field naming each part. */
|
|
5
|
+
label: keyof Row & string;
|
|
6
|
+
/** The field with each part's amount. */
|
|
7
|
+
value: keyof Row & string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
level?: 2 | 3 | 4;
|
|
11
|
+
/** Parts after this many fold into "Other". */
|
|
12
|
+
max?: number;
|
|
13
|
+
/** The total's caption in the middle, such as "visits". */
|
|
14
|
+
totalLabel?: string;
|
|
15
|
+
size?: number;
|
|
16
|
+
format?: (v: number) => string;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
exports: {};
|
|
20
|
+
bindings: "";
|
|
21
|
+
slots: {};
|
|
22
|
+
events: {};
|
|
23
|
+
};
|
|
24
|
+
declare class __sveltets_Render<Row extends object> {
|
|
25
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
26
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
27
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
28
|
+
bindings(): "";
|
|
29
|
+
exports(): {};
|
|
30
|
+
}
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
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']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
35
|
+
<Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
declare const DonutChart: $$IsomorphicComponent;
|
|
39
|
+
type DonutChart<Row extends object> = InstanceType<typeof DonutChart<Row>>;
|
|
40
|
+
export default DonutChart;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Dialog from './Dialog.svelte';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
/** Whether it's open; bind it. */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
footer?: Snippet;
|
|
13
|
+
onclose?: () => void;
|
|
14
|
+
};
|
|
15
|
+
let { open = $bindable(false), ...rest }: Props = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<!-- A bottom sheet: slides up, and closes when dragged down, tapped outside or on Escape. -->
|
|
19
|
+
<Dialog bind:open side="bottom" draggable {...rest} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Whether it's open; bind it. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
footer?: Snippet;
|
|
10
|
+
onclose?: () => void;
|
|
11
|
+
};
|
|
12
|
+
declare const Drawer: import("svelte").Component<Props, {}, "open">;
|
|
13
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
14
|
+
export default Drawer;
|