@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,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,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Item = { id: string; title: string };
|
|
5
|
+
type Props = {
|
|
6
|
+
items: Item[];
|
|
7
|
+
/** The open items' ids; bind it. */
|
|
8
|
+
open?: string[];
|
|
9
|
+
/** Let several items be open at once. */
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
/** The heading level of the item titles, to fit the page's outline. */
|
|
12
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** Renders an item's content. */
|
|
15
|
+
content: Snippet<[string]>;
|
|
16
|
+
};
|
|
17
|
+
let { items, open = $bindable([]), multiple = false, level = 3, class: klass = '', content }: Props = $props();
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
|
|
20
|
+
function toggle(id: string) {
|
|
21
|
+
if (open.includes(id)) open = open.filter((x) => x !== id);
|
|
22
|
+
else open = multiple ? [...open, id] : [id];
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="tern-accordion {klass}">
|
|
27
|
+
{#each items as item (item.id)}
|
|
28
|
+
{@const expanded = open.includes(item.id)}
|
|
29
|
+
<div class="item">
|
|
30
|
+
<div role="heading" aria-level={level}>
|
|
31
|
+
<button type="button" id="tern-acc-{uid}-{item.id}" aria-expanded={expanded} aria-controls="tern-acc-{uid}-{item.id}-panel" onclick={() => toggle(item.id)}>
|
|
32
|
+
{item.title}
|
|
33
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="panel" role="region" id="tern-acc-{uid}-{item.id}-panel" aria-labelledby="tern-acc-{uid}-{item.id}" hidden={!expanded}>
|
|
37
|
+
{#if expanded}{@render content(item.id)}{/if}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.tern-accordion {
|
|
45
|
+
border-top: 1px solid var(--tern-border);
|
|
46
|
+
}
|
|
47
|
+
.item {
|
|
48
|
+
border-bottom: 1px solid var(--tern-border);
|
|
49
|
+
}
|
|
50
|
+
button {
|
|
51
|
+
display: flex;
|
|
52
|
+
width: 100%;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
gap: 1rem;
|
|
56
|
+
padding: 0.85rem 0.25rem;
|
|
57
|
+
border: 0;
|
|
58
|
+
background: transparent;
|
|
59
|
+
font: inherit;
|
|
60
|
+
font-size: 0.9375rem;
|
|
61
|
+
font-weight: 550;
|
|
62
|
+
color: var(--tern-fg);
|
|
63
|
+
text-align: start;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
button:focus-visible {
|
|
67
|
+
outline: 2px solid var(--tern-ring);
|
|
68
|
+
outline-offset: -2px;
|
|
69
|
+
border-radius: var(--tern-radius-sm);
|
|
70
|
+
}
|
|
71
|
+
svg {
|
|
72
|
+
flex: none;
|
|
73
|
+
color: var(--tern-muted);
|
|
74
|
+
transition: rotate var(--tern-duration);
|
|
75
|
+
}
|
|
76
|
+
[aria-expanded='true'] svg {
|
|
77
|
+
rotate: 180deg;
|
|
78
|
+
}
|
|
79
|
+
.panel {
|
|
80
|
+
padding: 0 0.25rem 1rem;
|
|
81
|
+
color: var(--tern-muted);
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Item = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
type Props = {
|
|
7
|
+
items: Item[];
|
|
8
|
+
/** The open items' ids; bind it. */
|
|
9
|
+
open?: string[];
|
|
10
|
+
/** Let several items be open at once. */
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
/** The heading level of the item titles, to fit the page's outline. */
|
|
13
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
14
|
+
class?: string;
|
|
15
|
+
/** Renders an item's content. */
|
|
16
|
+
content: Snippet<[string]>;
|
|
17
|
+
};
|
|
18
|
+
declare const Accordion: import("svelte").Component<Props, {}, "open">;
|
|
19
|
+
type Accordion = ReturnType<typeof Accordion>;
|
|
20
|
+
export default Accordion;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Announce it when it appears: true for messages shown in response to something, false for ones on the page from the start. */
|
|
8
|
+
live?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
};
|
|
12
|
+
let { tone = 'info', title, live = false, class: klass = '', children }: Props = $props();
|
|
13
|
+
const role = $derived(live ? (tone === 'danger' || tone === 'warning' ? 'alert' : 'status') : undefined);
|
|
14
|
+
const icons = {
|
|
15
|
+
info: 'M8 7v4M8 5h.01',
|
|
16
|
+
success: 'M5 8.5l2 2 4-4.5',
|
|
17
|
+
warning: 'M8 5v3.5M8 11h.01',
|
|
18
|
+
danger: 'M6 6l4 4M10 6l-4 4'
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="tern-alert {tone} {klass}" {role}>
|
|
23
|
+
<svg class="icon" viewBox="0 0 16 16" width="18" height="18" aria-hidden="true">
|
|
24
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4" />
|
|
25
|
+
<path d={icons[tone]} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
|
26
|
+
</svg>
|
|
27
|
+
<div class="text">
|
|
28
|
+
{#if title}<p class="title">{title}</p>{/if}
|
|
29
|
+
{#if children}<div class="body">{@render children()}</div>{/if}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.tern-alert {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 0.7rem;
|
|
37
|
+
padding: 0.85rem 1rem;
|
|
38
|
+
border-radius: var(--tern-radius);
|
|
39
|
+
border: 1px solid;
|
|
40
|
+
font-size: 0.875rem;
|
|
41
|
+
}
|
|
42
|
+
.icon {
|
|
43
|
+
flex: none;
|
|
44
|
+
margin-top: 0.05rem;
|
|
45
|
+
}
|
|
46
|
+
.title {
|
|
47
|
+
margin: 0;
|
|
48
|
+
font-weight: 650;
|
|
49
|
+
}
|
|
50
|
+
.body {
|
|
51
|
+
color: var(--tern-fg);
|
|
52
|
+
}
|
|
53
|
+
.title + .body {
|
|
54
|
+
margin-top: 0.2rem;
|
|
55
|
+
}
|
|
56
|
+
.info {
|
|
57
|
+
background: var(--tern-accent-soft);
|
|
58
|
+
border-color: color-mix(in oklch, var(--tern-accent) 30%, transparent);
|
|
59
|
+
color: var(--tern-accent);
|
|
60
|
+
}
|
|
61
|
+
.success {
|
|
62
|
+
background: var(--tern-success-soft);
|
|
63
|
+
border-color: color-mix(in oklch, var(--tern-success) 30%, transparent);
|
|
64
|
+
color: color-mix(in oklch, var(--tern-success) 80%, var(--tern-fg));
|
|
65
|
+
}
|
|
66
|
+
.warning {
|
|
67
|
+
background: var(--tern-warning-soft);
|
|
68
|
+
border-color: color-mix(in oklch, var(--tern-warning) 35%, transparent);
|
|
69
|
+
color: color-mix(in oklch, var(--tern-warning) 55%, var(--tern-fg));
|
|
70
|
+
}
|
|
71
|
+
.danger {
|
|
72
|
+
background: var(--tern-danger-soft);
|
|
73
|
+
border-color: color-mix(in oklch, var(--tern-danger) 30%, transparent);
|
|
74
|
+
color: color-mix(in oklch, var(--tern-danger) 85%, var(--tern-fg));
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Announce it when it appears: true for messages shown in response to something, false for ones on the page from the start. */
|
|
6
|
+
live?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const Alert: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Alert = ReturnType<typeof Alert>;
|
|
12
|
+
export default Alert;
|
|
@@ -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;
|