@ternkit/tern 1.3.0 → 1.5.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/deferred.d.ts +5 -1
- package/dist/runtime/deferred.js +27 -11
- 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 +12 -3
- package/dist/vite/native.d.ts +3 -0
- package/dist/vite/native.js +122 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { goto, invalidate, invalidateAll, preloadData, applyAction } from './run
|
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.js';
|
|
14
14
|
export { enhance } from './runtime/forms.js';
|
|
15
|
+
export { validate, checkField, type FormField, type FormErrors } from './runtime/validate.js';
|
|
15
16
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
16
17
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
17
18
|
export { ApiError, createApi } from './runtime/api.js';
|
|
@@ -28,4 +29,4 @@ export type { MessageKey } from './runtime/i18n.svelte.js';
|
|
|
28
29
|
export interface Messages {
|
|
29
30
|
}
|
|
30
31
|
/** The version of the Tern client runtime. */
|
|
31
|
-
export declare const VERSION = "1.
|
|
32
|
+
export declare const VERSION = "1.5.0";
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ export { page, navigating, env } from './runtime/state.svelte.js';
|
|
|
11
11
|
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export { enhance } from './runtime/forms.js';
|
|
14
|
+
export { validate, checkField } from './runtime/validate.js';
|
|
14
15
|
export { ApiError, createApi } from './runtime/api.js';
|
|
15
16
|
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
16
17
|
export { default as Image } from './image/Image.svelte';
|
|
17
18
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
18
19
|
/** The version of the Tern client runtime. */
|
|
19
|
-
export const VERSION = '1.
|
|
20
|
+
export const VERSION = '1.5.0';
|
|
@@ -17,7 +17,11 @@ export declare class DeferredScope {
|
|
|
17
17
|
/** Rejects every value still pending, e.g. when the stream ends early. */
|
|
18
18
|
close(reason: string): void;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Replaces deferred placeholders in data with promises from scope. The data
|
|
22
|
+
* must be fresh from JSON.parse: it is changed in place, which saves
|
|
23
|
+
* copying every page's data on every render.
|
|
24
|
+
*/
|
|
21
25
|
export declare function revive<T>(value: T, promise: (id: number) => Promise<unknown>): T;
|
|
22
26
|
/**
|
|
23
27
|
* Reads a response that holds one JSON payload line, optionally followed
|
package/dist/runtime/deferred.js
CHANGED
|
@@ -32,21 +32,37 @@ export class DeferredScope {
|
|
|
32
32
|
this.#pending.clear();
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Replaces deferred placeholders in data with promises from scope. The data
|
|
37
|
+
* must be fresh from JSON.parse: it is changed in place, which saves
|
|
38
|
+
* copying every page's data on every render.
|
|
39
|
+
*/
|
|
36
40
|
export function revive(value, promise) {
|
|
41
|
+
if (value === null || typeof value !== 'object')
|
|
42
|
+
return value;
|
|
37
43
|
if (Array.isArray(value)) {
|
|
38
|
-
|
|
44
|
+
for (let i = 0; i < value.length; i++) {
|
|
45
|
+
const v = value[i];
|
|
46
|
+
if (v !== null && typeof v === 'object') {
|
|
47
|
+
const r = revive(v, promise);
|
|
48
|
+
if (r !== v)
|
|
49
|
+
value[i] = r;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
const obj = value;
|
|
55
|
+
if (typeof obj.__tern_deferred === 'number' && Object.keys(obj).length === 1) {
|
|
56
|
+
return promise(obj.__tern_deferred);
|
|
39
57
|
}
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
for (const k in obj) {
|
|
59
|
+
const v = obj[k];
|
|
60
|
+
if (v !== null && typeof v === 'object') {
|
|
61
|
+
const r = revive(v, promise);
|
|
62
|
+
// defineProperty, so a key named __proto__ stays a plain property.
|
|
63
|
+
if (r !== v)
|
|
64
|
+
Object.defineProperty(obj, k, { value: r, writable: true, enumerable: true, configurable: true });
|
|
45
65
|
}
|
|
46
|
-
const out = {};
|
|
47
|
-
for (const k of keys)
|
|
48
|
-
out[k] = revive(obj[k], promise);
|
|
49
|
-
return out;
|
|
50
66
|
}
|
|
51
67
|
return value;
|
|
52
68
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A form field's rules, as tern gen writes them. */
|
|
2
|
+
export interface FormField<Name extends string = string> {
|
|
3
|
+
name: Name;
|
|
4
|
+
label: string;
|
|
5
|
+
kind: 'string' | 'int' | 'float' | 'bool' | 'date' | 'datetime' | 'strings';
|
|
6
|
+
required?: boolean;
|
|
7
|
+
rules?: {
|
|
8
|
+
name: string;
|
|
9
|
+
arg?: string;
|
|
10
|
+
}[];
|
|
11
|
+
message?: string;
|
|
12
|
+
secret?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Errors by field name; empty when the form is valid. */
|
|
15
|
+
export type FormErrors<Name extends string = string> = Partial<Record<Name, string>>;
|
|
16
|
+
/** Reads a date or a date and time as Go does, or null. Times without a zone are UTC. */
|
|
17
|
+
export declare function parseFormTime(kind: 'date' | 'datetime', s: string): number | null;
|
|
18
|
+
/**
|
|
19
|
+
* Returns why a field's values are invalid, or "". values are its posted
|
|
20
|
+
* values; other reads another field's first value, for same=.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkField(f: FormField, values: string[], other?: (name: string) => string, labelOf?: (name: string) => string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Checks a form against its rules, as kit.FormAction will on the server:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { forms } from './$forms';
|
|
28
|
+
* const errors = validate(forms.create, new FormData(formElement));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function validate<Name extends string>(fields: FormField<Name>[], data: FormData | Record<string, unknown>): FormErrors<Name>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Checks form values against the rules tern gen writes to ./$forms from a
|
|
2
|
+
// kit.FormAction's validate tags. The checks and messages match
|
|
3
|
+
// internal/formrules in Go, which checks the same rules on the server;
|
|
4
|
+
// internal/formrules/testdata/cases.json keeps the two in step.
|
|
5
|
+
const emailRe = /^[^\s@<>()",;:]+@[^\s@<>()",;:]+\.[^\s@<>()",;:]+$/;
|
|
6
|
+
const intRe = /^[+-]?\d+$/;
|
|
7
|
+
const floatRe = /^[+-]?(\d+\.?\d*|\.\d+)$/;
|
|
8
|
+
const patterns = new Map();
|
|
9
|
+
function truthy(s) {
|
|
10
|
+
return !['', 'off', 'false', '0', 'no'].includes(s.trim().toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
/** Reads a date or a date and time as Go does, or null. Times without a zone are UTC. */
|
|
13
|
+
export function parseFormTime(kind, s) {
|
|
14
|
+
if (kind === 'date') {
|
|
15
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
16
|
+
if (!m)
|
|
17
|
+
return null;
|
|
18
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
19
|
+
const t = Date.UTC(y, mo - 1, d);
|
|
20
|
+
const back = new Date(t);
|
|
21
|
+
return back.getUTCFullYear() === y && back.getUTCMonth() === mo - 1 && back.getUTCDate() === d ? t : null;
|
|
22
|
+
}
|
|
23
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(s);
|
|
24
|
+
if (m) {
|
|
25
|
+
const [y, mo, d, h, mi, se] = m.slice(1).map((x) => Number(x ?? 0));
|
|
26
|
+
const t = Date.UTC(y, mo - 1, d, h, mi, se);
|
|
27
|
+
const b = new Date(t);
|
|
28
|
+
return b.getUTCDate() === d && b.getUTCHours() === h && b.getUTCMinutes() === mi ? t : null;
|
|
29
|
+
}
|
|
30
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(s)) {
|
|
31
|
+
const t = Date.parse(s);
|
|
32
|
+
return Number.isNaN(t) ? null : t;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function joinOr(list) {
|
|
37
|
+
return list.length === 1 ? list[0] : `${list.slice(0, -1).join(', ')} or ${list[list.length - 1]}`;
|
|
38
|
+
}
|
|
39
|
+
const bound = { min: 'at least', max: 'at most', len: 'exactly' };
|
|
40
|
+
/**
|
|
41
|
+
* Returns why a field's values are invalid, or "". values are its posted
|
|
42
|
+
* values; other reads another field's first value, for same=.
|
|
43
|
+
*/
|
|
44
|
+
export function checkField(f, values, other = () => '', labelOf = (n) => n) {
|
|
45
|
+
const msg = (m) => f.message || m;
|
|
46
|
+
const list = values.filter((v) => v.trim() !== '');
|
|
47
|
+
const v = values[0] ?? '';
|
|
48
|
+
const empty = f.kind === 'strings' ? list.length === 0 : f.kind === 'bool' ? !truthy(v) : v.trim() === '';
|
|
49
|
+
if (empty) {
|
|
50
|
+
if (!f.required)
|
|
51
|
+
return '';
|
|
52
|
+
return msg(f.kind === 'bool' ? `${f.label} must be checked.` : `${f.label} is required.`);
|
|
53
|
+
}
|
|
54
|
+
let num = 0;
|
|
55
|
+
let when = 0;
|
|
56
|
+
if (f.kind === 'int') {
|
|
57
|
+
if (!intRe.test(v.trim()))
|
|
58
|
+
return msg(`${f.label} must be a whole number.`);
|
|
59
|
+
num = Number(v.trim());
|
|
60
|
+
}
|
|
61
|
+
else if (f.kind === 'float') {
|
|
62
|
+
if (!floatRe.test(v.trim()))
|
|
63
|
+
return msg(`${f.label} must be a number.`);
|
|
64
|
+
num = Number(v.trim());
|
|
65
|
+
}
|
|
66
|
+
else if (f.kind === 'date' || f.kind === 'datetime') {
|
|
67
|
+
const t = parseFormTime(f.kind, v.trim());
|
|
68
|
+
if (t === null)
|
|
69
|
+
return msg(f.kind === 'date' ? `${f.label} must be a date.` : `${f.label} must be a date and time.`);
|
|
70
|
+
when = t;
|
|
71
|
+
}
|
|
72
|
+
for (const r of f.rules ?? []) {
|
|
73
|
+
const arg = r.arg ?? '';
|
|
74
|
+
const n = Number(arg);
|
|
75
|
+
switch (r.name) {
|
|
76
|
+
case 'min':
|
|
77
|
+
case 'max':
|
|
78
|
+
case 'len': {
|
|
79
|
+
const low = r.name === 'min';
|
|
80
|
+
const fails = (c) => (r.name === 'len' && c !== n) || (low && c < n) || (r.name === 'max' && c > n);
|
|
81
|
+
if (f.kind === 'string') {
|
|
82
|
+
if (fails([...v].length))
|
|
83
|
+
return msg(`${f.label} must be ${bound[r.name]} ${arg} ${n === 1 ? 'character' : 'characters'}.`);
|
|
84
|
+
}
|
|
85
|
+
else if (f.kind === 'int' || f.kind === 'float') {
|
|
86
|
+
if (fails(num))
|
|
87
|
+
return msg(`${f.label} must be ${bound[r.name]} ${arg}.`);
|
|
88
|
+
}
|
|
89
|
+
else if (f.kind === 'date' || f.kind === 'datetime') {
|
|
90
|
+
const b = parseFormTime(f.kind, arg) ?? 0;
|
|
91
|
+
if ((low && when < b) || (r.name === 'max' && when > b))
|
|
92
|
+
return msg(`${f.label} must be on or ${low ? 'after' : 'before'} ${arg}.`);
|
|
93
|
+
}
|
|
94
|
+
else if (f.kind === 'strings') {
|
|
95
|
+
if (fails(list.length))
|
|
96
|
+
return msg(`Choose ${bound[r.name]} ${arg} for ${f.label.toLowerCase()}.`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'email':
|
|
101
|
+
if (!emailRe.test(v))
|
|
102
|
+
return msg(`${f.label} must be an email address.`);
|
|
103
|
+
break;
|
|
104
|
+
case 'url': {
|
|
105
|
+
let ok = false;
|
|
106
|
+
try {
|
|
107
|
+
const u = new URL(v);
|
|
108
|
+
ok = (u.protocol === 'http:' || u.protocol === 'https:') && u.host !== '' && !/[ \t\n]/.test(v);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
ok = false;
|
|
112
|
+
}
|
|
113
|
+
if (!ok)
|
|
114
|
+
return msg(`${f.label} must be a web address starting with http:// or https://.`);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'pattern': {
|
|
118
|
+
let re = patterns.get(arg);
|
|
119
|
+
if (!re)
|
|
120
|
+
patterns.set(arg, (re = new RegExp(`^(?:${arg})$`, 'u')));
|
|
121
|
+
for (const s of f.kind === 'strings' ? list : [v])
|
|
122
|
+
if (!re.test(s))
|
|
123
|
+
return msg(`${f.label} isn't in the expected format.`);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'oneof': {
|
|
127
|
+
const opts = arg.split('|');
|
|
128
|
+
for (const s of f.kind === 'strings' ? list : [v])
|
|
129
|
+
if (!opts.includes(s))
|
|
130
|
+
return msg(`${f.label} must be one of ${joinOr(opts)}.`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'same':
|
|
134
|
+
if (other(arg) !== v)
|
|
135
|
+
return msg(`${f.label} must match ${labelOf(arg).toLowerCase()}.`);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Checks a form against its rules, as kit.FormAction will on the server:
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* import { forms } from './$forms';
|
|
146
|
+
* const errors = validate(forms.create, new FormData(formElement));
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function validate(fields, data) {
|
|
150
|
+
const read = (name) => {
|
|
151
|
+
if (data instanceof FormData)
|
|
152
|
+
return data.getAll(name).map((v) => (typeof v === 'string' ? v : v.name));
|
|
153
|
+
const v = data[name];
|
|
154
|
+
if (v === undefined || v === null || v === false)
|
|
155
|
+
return [];
|
|
156
|
+
if (v === true)
|
|
157
|
+
return ['on'];
|
|
158
|
+
return Array.isArray(v) ? v.map(String) : [String(v)];
|
|
159
|
+
};
|
|
160
|
+
const labels = new Map(fields.map((f) => [f.name, f.label]));
|
|
161
|
+
const errors = {};
|
|
162
|
+
for (const f of fields) {
|
|
163
|
+
const m = checkField(f, read(f.name), (n) => read(n)[0] ?? '', (n) => labels.get(n) ?? n);
|
|
164
|
+
if (m)
|
|
165
|
+
errors[f.name] = m;
|
|
166
|
+
}
|
|
167
|
+
return errors;
|
|
168
|
+
}
|
package/dist/server/intl.js
CHANGED
|
@@ -15,40 +15,83 @@ class NumberFormat {
|
|
|
15
15
|
format(value) {
|
|
16
16
|
let n = Number(value);
|
|
17
17
|
const o = this.#o;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const sign = String(o.signDisplay ?? 'auto');
|
|
19
|
+
if (!Number.isFinite(n)) {
|
|
20
|
+
if (Number.isNaN(n))
|
|
21
|
+
return 'NaN';
|
|
22
|
+
return (n < 0 ? (sign === 'never' ? '' : '-') : sign === 'always' || sign === 'exceptZero' ? '+' : '') + '∞';
|
|
23
|
+
}
|
|
24
|
+
const negative = n < 0 || Object.is(n, -0);
|
|
20
25
|
let suffix = '';
|
|
21
26
|
if (o.style === 'percent') {
|
|
22
27
|
n *= 100;
|
|
23
28
|
suffix = '%';
|
|
24
29
|
}
|
|
30
|
+
const isCurrency = o.style === 'currency';
|
|
31
|
+
const explicit = o.maximumFractionDigits !== undefined || o.minimumFractionDigits !== undefined;
|
|
32
|
+
let max = o.maximumFractionDigits ?? (isCurrency ? 2 : o.style === 'percent' ? 0 : 3);
|
|
33
|
+
const min = o.minimumFractionDigits ?? (isCurrency ? 2 : 0);
|
|
34
|
+
let abs = Math.abs(n);
|
|
25
35
|
if (o.notation === 'compact') {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const long = o.compactDisplay === 'long';
|
|
37
|
+
const units = [
|
|
38
|
+
[1e12, 'T', ' trillion'],
|
|
39
|
+
[1e9, 'B', ' billion'],
|
|
40
|
+
[1e6, 'M', ' million'],
|
|
41
|
+
[1e3, 'K', ' thousand'],
|
|
42
|
+
];
|
|
43
|
+
// Like en-US: two significant digits, but never fewer than the
|
|
44
|
+
// integer part needs.
|
|
45
|
+
const digits = (x) => (explicit ? Math.max(min, Math.min(max, 20)) : x >= 10 || x === 0 ? 0 : Math.max(0, 1 - Math.floor(Math.log10(x))));
|
|
46
|
+
const rounded = (x) => Number(x.toFixed(digits(x)));
|
|
47
|
+
let unit = '';
|
|
48
|
+
for (let i = 0; i < units.length; i++) {
|
|
49
|
+
const [limit, short, name] = units[i];
|
|
50
|
+
if (rounded(abs) >= limit) {
|
|
51
|
+
abs /= limit;
|
|
52
|
+
unit = long ? name : short;
|
|
53
|
+
// 999,999 rounds to 1000K, which is 1M.
|
|
54
|
+
if (rounded(abs) >= 1000 && i > 0) {
|
|
55
|
+
abs /= 1000;
|
|
56
|
+
unit = long ? units[i - 1][2] : units[i - 1][1];
|
|
57
|
+
}
|
|
31
58
|
break;
|
|
32
59
|
}
|
|
33
60
|
}
|
|
61
|
+
suffix = unit + suffix;
|
|
62
|
+
max = digits(abs);
|
|
34
63
|
}
|
|
35
|
-
|
|
36
|
-
const max = o.maximumFractionDigits ?? (isCurrency ? 2 : o.style === 'percent' ? 0 : o.notation === 'compact' ? 1 : 3);
|
|
37
|
-
const min = o.minimumFractionDigits ?? (isCurrency ? 2 : 0);
|
|
38
|
-
let s = Math.abs(n).toFixed(Math.max(min, Math.min(max, 20)));
|
|
64
|
+
let s = abs.toFixed(o.notation === 'compact' && !explicit ? max : Math.max(min, Math.min(max, 20)));
|
|
39
65
|
if (s.includes('.')) {
|
|
40
66
|
let [int, frac = ''] = s.split('.');
|
|
41
67
|
while (frac.length > min && frac.endsWith('0'))
|
|
42
68
|
frac = frac.slice(0, -1);
|
|
43
69
|
s = frac ? `${int}.${frac}` : int;
|
|
44
70
|
}
|
|
71
|
+
const zero = !/[1-9]/.test(s);
|
|
45
72
|
const [int, frac] = s.split('.');
|
|
46
73
|
s = (o.useGrouping === false ? int : group(int)) + (frac ? `.${frac}` : '');
|
|
47
74
|
if (isCurrency) {
|
|
48
75
|
const code = String(o.currency ?? 'USD').toUpperCase();
|
|
49
76
|
s = (code === 'USD' ? '$' : code === 'EUR' ? '€' : code === 'GBP' ? '£' : `${code} `) + s;
|
|
50
77
|
}
|
|
51
|
-
|
|
78
|
+
let prefix = '';
|
|
79
|
+
switch (sign) {
|
|
80
|
+
case 'never':
|
|
81
|
+
break;
|
|
82
|
+
case 'always':
|
|
83
|
+
prefix = negative ? '-' : '+';
|
|
84
|
+
break;
|
|
85
|
+
case 'exceptZero':
|
|
86
|
+
prefix = zero ? '' : negative ? '-' : '+';
|
|
87
|
+
break;
|
|
88
|
+
case 'negative':
|
|
89
|
+
prefix = negative && !zero ? '-' : '';
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
prefix = negative ? '-' : '';
|
|
93
|
+
}
|
|
94
|
+
return prefix + s + suffix;
|
|
52
95
|
}
|
|
53
96
|
formatToParts(value) {
|
|
54
97
|
return [{ type: 'literal', value: this.format(value) }];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte';
|
|
3
|
+
import Dialog from './Dialog.svelte';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
/** Whether it's open; bind it. */
|
|
8
|
+
open?: boolean;
|
|
9
|
+
/** The question, such as "Delete this project?" */
|
|
10
|
+
title: string;
|
|
11
|
+
/** What happens, such as "This can't be undone." */
|
|
12
|
+
description?: string;
|
|
13
|
+
confirmLabel?: string;
|
|
14
|
+
cancelLabel?: string;
|
|
15
|
+
/** Styles the confirm button for destructive actions. */
|
|
16
|
+
danger?: boolean;
|
|
17
|
+
/** Runs on confirm. If it returns a promise, the button shows a spinner until it settles, and the dialog stays open if it rejects. */
|
|
18
|
+
onconfirm: () => void | Promise<unknown>;
|
|
19
|
+
oncancel?: () => void;
|
|
20
|
+
/** More content, such as a field to type a name into. */
|
|
21
|
+
children?: Snippet;
|
|
22
|
+
/** Disables the confirm button, for example until a name is typed. */
|
|
23
|
+
confirmDisabled?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
};
|
|
26
|
+
let {
|
|
27
|
+
open = $bindable(false),
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
confirmLabel = 'Continue',
|
|
31
|
+
cancelLabel = 'Cancel',
|
|
32
|
+
danger = false,
|
|
33
|
+
onconfirm,
|
|
34
|
+
oncancel,
|
|
35
|
+
children,
|
|
36
|
+
confirmDisabled = false,
|
|
37
|
+
class: klass = ''
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const uid = $props.id();
|
|
41
|
+
let busy = $state(false);
|
|
42
|
+
let confirmed = false;
|
|
43
|
+
|
|
44
|
+
async function confirm() {
|
|
45
|
+
busy = true;
|
|
46
|
+
try {
|
|
47
|
+
await onconfirm();
|
|
48
|
+
confirmed = true;
|
|
49
|
+
open = false;
|
|
50
|
+
} catch {
|
|
51
|
+
// Stay open: the caller shows what went wrong.
|
|
52
|
+
} finally {
|
|
53
|
+
busy = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!-- Interrupts to confirm an action. Focus starts on Cancel, so Enter doesn't confirm by accident. -->
|
|
59
|
+
<Dialog
|
|
60
|
+
bind:open
|
|
61
|
+
{title}
|
|
62
|
+
{description}
|
|
63
|
+
role="alertdialog"
|
|
64
|
+
size="sm"
|
|
65
|
+
dismissable={false}
|
|
66
|
+
closeButton={false}
|
|
67
|
+
initialFocus={() => document.getElementById(`tern-alert-${uid}-cancel`)}
|
|
68
|
+
onclose={() => {
|
|
69
|
+
if (!confirmed) oncancel?.();
|
|
70
|
+
confirmed = false;
|
|
71
|
+
}}
|
|
72
|
+
class={klass}
|
|
73
|
+
{children}
|
|
74
|
+
>
|
|
75
|
+
{#snippet footer()}
|
|
76
|
+
<Button variant="secondary" id="tern-alert-{uid}-cancel" onclick={() => (open = false)} disabled={busy}>{cancelLabel}</Button>
|
|
77
|
+
<Button variant={danger ? 'danger' : 'primary'} onclick={confirm} loading={busy} disabled={confirmDisabled}>{confirmLabel}</Button>
|
|
78
|
+
{/snippet}
|
|
79
|
+
</Dialog>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Whether it's open; bind it. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** The question, such as "Delete this project?" */
|
|
6
|
+
title: string;
|
|
7
|
+
/** What happens, such as "This can't be undone." */
|
|
8
|
+
description?: string;
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
cancelLabel?: string;
|
|
11
|
+
/** Styles the confirm button for destructive actions. */
|
|
12
|
+
danger?: boolean;
|
|
13
|
+
/** Runs on confirm. If it returns a promise, the button shows a spinner until it settles, and the dialog stays open if it rejects. */
|
|
14
|
+
onconfirm: () => void | Promise<unknown>;
|
|
15
|
+
oncancel?: () => void;
|
|
16
|
+
/** More content, such as a field to type a name into. */
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
/** Disables the confirm button, for example until a name is typed. */
|
|
19
|
+
confirmDisabled?: boolean;
|
|
20
|
+
class?: string;
|
|
21
|
+
};
|
|
22
|
+
declare const AlertDialog: import("svelte").Component<Props, {}, "open">;
|
|
23
|
+
type AlertDialog = ReturnType<typeof AlertDialog>;
|
|
24
|
+
export default AlertDialog;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import type { ComponentProps } from 'svelte';
|
|
3
|
+
import LineChart from './LineChart.svelte';
|
|
4
|
+
|
|
5
|
+
let props: Omit<ComponentProps<typeof LineChart<Row>>, 'area'> = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<!-- A LineChart with each series' area washed in under its line. -->
|
|
9
|
+
<LineChart {...props} area />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import LineChart from './LineChart.svelte';
|
|
3
|
+
declare function $$render<Row extends object>(): {
|
|
4
|
+
props: Omit<ComponentProps<typeof LineChart<Row>>, "area">;
|
|
5
|
+
exports: {};
|
|
6
|
+
bindings: "";
|
|
7
|
+
slots: {};
|
|
8
|
+
events: {};
|
|
9
|
+
};
|
|
10
|
+
declare class __sveltets_Render<Row extends object> {
|
|
11
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
12
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
13
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
14
|
+
bindings(): "";
|
|
15
|
+
exports(): {};
|
|
16
|
+
}
|
|
17
|
+
interface $$IsomorphicComponent {
|
|
18
|
+
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']>> & {
|
|
19
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
20
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
21
|
+
<Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
22
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
23
|
+
}
|
|
24
|
+
declare const AreaChart: $$IsomorphicComponent;
|
|
25
|
+
type AreaChart<Row extends object> = InstanceType<typeof AreaChart<Row>>;
|
|
26
|
+
export default AreaChart;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** Width over height, such as 16 / 9. */
|
|
6
|
+
ratio?: number;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
let { ratio = 16 / 9, children, class: klass = '' }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<!-- Keeps its content, such as an image or video, at a ratio as it resizes. -->
|
|
14
|
+
<div class="tern-aspect {klass}" style:aspect-ratio={ratio}>{@render children()}</div>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.tern-aspect {
|
|
18
|
+
position: relative;
|
|
19
|
+
width: 100%;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
border-radius: var(--tern-radius);
|
|
22
|
+
}
|
|
23
|
+
.tern-aspect > :global(img),
|
|
24
|
+
.tern-aspect > :global(video),
|
|
25
|
+
.tern-aspect > :global(iframe) {
|
|
26
|
+
position: absolute;
|
|
27
|
+
inset: 0;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
object-fit: cover;
|
|
31
|
+
border: 0;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Width over height, such as 16 / 9. */
|
|
4
|
+
ratio?: number;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const AspectRatio: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type AspectRatio = ReturnType<typeof AspectRatio>;
|
|
10
|
+
export default AspectRatio;
|