@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.
Files changed (170) hide show
  1. package/dist/goscript/index.svelte.d.ts +32 -2
  2. package/dist/goscript/index.svelte.js +257 -31
  3. package/dist/goscript/patch.d.ts +6 -0
  4. package/dist/goscript/patch.js +25 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +2 -1
  7. package/dist/runtime/validate.d.ts +31 -0
  8. package/dist/runtime/validate.js +168 -0
  9. package/dist/server/intl.js +55 -12
  10. package/dist/ui/Accordion.svelte +84 -0
  11. package/dist/ui/Accordion.svelte.d.ts +20 -0
  12. package/dist/ui/Alert.svelte +76 -0
  13. package/dist/ui/Alert.svelte.d.ts +12 -0
  14. package/dist/ui/AlertDialog.svelte +79 -0
  15. package/dist/ui/AlertDialog.svelte.d.ts +24 -0
  16. package/dist/ui/AreaChart.svelte +9 -0
  17. package/dist/ui/AreaChart.svelte.d.ts +26 -0
  18. package/dist/ui/AspectRatio.svelte +33 -0
  19. package/dist/ui/AspectRatio.svelte.d.ts +10 -0
  20. package/dist/ui/Avatar.svelte +48 -0
  21. package/dist/ui/Avatar.svelte.d.ts +10 -0
  22. package/dist/ui/Badge.svelte +54 -0
  23. package/dist/ui/Badge.svelte.d.ts +10 -0
  24. package/dist/ui/BarChart.svelte +170 -0
  25. package/dist/ui/BarChart.svelte.d.ts +40 -0
  26. package/dist/ui/Breadcrumb.svelte +92 -0
  27. package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
  28. package/dist/ui/Button.svelte +136 -0
  29. package/dist/ui/Button.svelte.d.ts +18 -0
  30. package/dist/ui/ButtonGroup.svelte +61 -0
  31. package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
  32. package/dist/ui/Calendar.svelte +265 -0
  33. package/dist/ui/Calendar.svelte.d.ts +28 -0
  34. package/dist/ui/Card.svelte +74 -0
  35. package/dist/ui/Card.svelte.d.ts +18 -0
  36. package/dist/ui/Carousel.svelte +169 -0
  37. package/dist/ui/Carousel.svelte.d.ts +36 -0
  38. package/dist/ui/ChartFrame.svelte +174 -0
  39. package/dist/ui/ChartFrame.svelte.d.ts +24 -0
  40. package/dist/ui/Checkbox.svelte +112 -0
  41. package/dist/ui/Checkbox.svelte.d.ts +14 -0
  42. package/dist/ui/Collapsible.svelte +59 -0
  43. package/dist/ui/Collapsible.svelte.d.ts +12 -0
  44. package/dist/ui/Combobox.svelte +350 -0
  45. package/dist/ui/Combobox.svelte.d.ts +24 -0
  46. package/dist/ui/Command.svelte +268 -0
  47. package/dist/ui/Command.svelte.d.ts +28 -0
  48. package/dist/ui/ConsentScreen.svelte +125 -0
  49. package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
  50. package/dist/ui/ContextMenu.svelte +78 -0
  51. package/dist/ui/ContextMenu.svelte.d.ts +13 -0
  52. package/dist/ui/DataTable.svelte +277 -0
  53. package/dist/ui/DataTable.svelte.d.ts +53 -0
  54. package/dist/ui/DatePicker.svelte +138 -0
  55. package/dist/ui/DatePicker.svelte.d.ts +25 -0
  56. package/dist/ui/Dialog.svelte +301 -0
  57. package/dist/ui/Dialog.svelte.d.ts +30 -0
  58. package/dist/ui/DonutChart.svelte +115 -0
  59. package/dist/ui/DonutChart.svelte.d.ts +40 -0
  60. package/dist/ui/Drawer.svelte +19 -0
  61. package/dist/ui/Drawer.svelte.d.ts +14 -0
  62. package/dist/ui/Empty.svelte +62 -0
  63. package/dist/ui/Empty.svelte.d.ts +15 -0
  64. package/dist/ui/Field.svelte +97 -0
  65. package/dist/ui/Field.svelte.d.ts +19 -0
  66. package/dist/ui/Form.svelte +159 -0
  67. package/dist/ui/Form.svelte.d.ts +21 -0
  68. package/dist/ui/HoverCard.svelte +106 -0
  69. package/dist/ui/HoverCard.svelte.d.ts +26 -0
  70. package/dist/ui/Input.svelte +62 -0
  71. package/dist/ui/Input.svelte.d.ts +7 -0
  72. package/dist/ui/InputGroup.svelte +80 -0
  73. package/dist/ui/InputGroup.svelte.d.ts +13 -0
  74. package/dist/ui/InputOTP.svelte +140 -0
  75. package/dist/ui/InputOTP.svelte.d.ts +18 -0
  76. package/dist/ui/Item.svelte +86 -0
  77. package/dist/ui/Item.svelte.d.ts +17 -0
  78. package/dist/ui/Kbd.svelte +24 -0
  79. package/dist/ui/Kbd.svelte.d.ts +7 -0
  80. package/dist/ui/Label.svelte +29 -0
  81. package/dist/ui/Label.svelte.d.ts +10 -0
  82. package/dist/ui/LineChart.svelte +171 -0
  83. package/dist/ui/LineChart.svelte.d.ts +42 -0
  84. package/dist/ui/Menu.svelte +103 -0
  85. package/dist/ui/Menu.svelte.d.ts +15 -0
  86. package/dist/ui/MenuList.svelte +317 -0
  87. package/dist/ui/MenuList.svelte.d.ts +29 -0
  88. package/dist/ui/Menubar.svelte +133 -0
  89. package/dist/ui/Menubar.svelte.d.ts +14 -0
  90. package/dist/ui/NavigationMenu.svelte +208 -0
  91. package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
  92. package/dist/ui/Pagination.svelte +114 -0
  93. package/dist/ui/Pagination.svelte.d.ts +16 -0
  94. package/dist/ui/PasswordInput.svelte +134 -0
  95. package/dist/ui/PasswordInput.svelte.d.ts +11 -0
  96. package/dist/ui/Popover.svelte +127 -0
  97. package/dist/ui/Popover.svelte.d.ts +29 -0
  98. package/dist/ui/Progress.svelte +73 -0
  99. package/dist/ui/Progress.svelte.d.ts +12 -0
  100. package/dist/ui/RadioGroup.svelte +105 -0
  101. package/dist/ui/RadioGroup.svelte.d.ts +21 -0
  102. package/dist/ui/Resizable.svelte +122 -0
  103. package/dist/ui/Resizable.svelte.d.ts +16 -0
  104. package/dist/ui/ScrollArea.svelte +43 -0
  105. package/dist/ui/ScrollArea.svelte.d.ts +13 -0
  106. package/dist/ui/Select.svelte +77 -0
  107. package/dist/ui/Select.svelte.d.ts +18 -0
  108. package/dist/ui/SelectMenu.svelte +301 -0
  109. package/dist/ui/SelectMenu.svelte.d.ts +18 -0
  110. package/dist/ui/Separator.svelte +19 -0
  111. package/dist/ui/Separator.svelte.d.ts +7 -0
  112. package/dist/ui/Sheet.svelte +21 -0
  113. package/dist/ui/Sheet.svelte.d.ts +16 -0
  114. package/dist/ui/Sidebar.svelte +263 -0
  115. package/dist/ui/Sidebar.svelte.d.ts +35 -0
  116. package/dist/ui/SignIn.svelte +182 -0
  117. package/dist/ui/SignIn.svelte.d.ts +38 -0
  118. package/dist/ui/SignUp.svelte +106 -0
  119. package/dist/ui/SignUp.svelte.d.ts +23 -0
  120. package/dist/ui/Skeleton.svelte +29 -0
  121. package/dist/ui/Skeleton.svelte.d.ts +9 -0
  122. package/dist/ui/Slider.svelte +196 -0
  123. package/dist/ui/Slider.svelte.d.ts +19 -0
  124. package/dist/ui/Sparkline.svelte +23 -0
  125. package/dist/ui/Sparkline.svelte.d.ts +11 -0
  126. package/dist/ui/Spinner.svelte +42 -0
  127. package/dist/ui/Spinner.svelte.d.ts +8 -0
  128. package/dist/ui/Stat.svelte +91 -0
  129. package/dist/ui/Stat.svelte.d.ts +18 -0
  130. package/dist/ui/Switch.svelte +90 -0
  131. package/dist/ui/Switch.svelte.d.ts +12 -0
  132. package/dist/ui/Table.svelte +70 -0
  133. package/dist/ui/Table.svelte.d.ts +36 -0
  134. package/dist/ui/Tabs.svelte +112 -0
  135. package/dist/ui/Tabs.svelte.d.ts +19 -0
  136. package/dist/ui/Textarea.svelte +52 -0
  137. package/dist/ui/Textarea.svelte.d.ts +7 -0
  138. package/dist/ui/Toaster.svelte +146 -0
  139. package/dist/ui/Toaster.svelte.d.ts +6 -0
  140. package/dist/ui/Toggle.svelte +82 -0
  141. package/dist/ui/Toggle.svelte.d.ts +17 -0
  142. package/dist/ui/ToggleGroup.svelte +143 -0
  143. package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
  144. package/dist/ui/Tooltip.svelte +85 -0
  145. package/dist/ui/Tooltip.svelte.d.ts +16 -0
  146. package/dist/ui/chart.d.ts +26 -0
  147. package/dist/ui/chart.js +89 -0
  148. package/dist/ui/dates.d.ts +13 -0
  149. package/dist/ui/dates.js +64 -0
  150. package/dist/ui/field.d.ts +18 -0
  151. package/dist/ui/field.js +8 -0
  152. package/dist/ui/floating.d.ts +44 -0
  153. package/dist/ui/floating.js +97 -0
  154. package/dist/ui/form.d.ts +12 -0
  155. package/dist/ui/form.js +8 -0
  156. package/dist/ui/index.d.ts +78 -0
  157. package/dist/ui/index.js +70 -0
  158. package/dist/ui/menu.d.ts +25 -0
  159. package/dist/ui/menu.js +1 -0
  160. package/dist/ui/options.d.ts +17 -0
  161. package/dist/ui/options.js +22 -0
  162. package/dist/ui/table.d.ts +26 -0
  163. package/dist/ui/table.js +1 -0
  164. package/dist/ui/theme.css +176 -0
  165. package/dist/ui/toast.svelte.d.ts +19 -0
  166. package/dist/ui/toast.svelte.js +18 -0
  167. package/dist/vite/goscript.d.ts +14 -0
  168. package/dist/vite/goscript.js +45 -16
  169. package/dist/vite/index.js +15 -4
  170. package/package.json +7 -1
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Props = {
5
+ title: string;
6
+ description?: string;
7
+ /** The heading level, to fit the page's outline. */
8
+ level?: 2 | 3 | 4;
9
+ /** An illustration or icon above the title. */
10
+ icon?: Snippet;
11
+ /** What to do next, such as a Create button. */
12
+ actions?: Snippet;
13
+ class?: string;
14
+ };
15
+ let { title, description, level = 3, icon, actions, class: klass = '' }: Props = $props();
16
+ </script>
17
+
18
+ <!-- What a list or page shows when it has nothing yet: what it is for and what to do. -->
19
+ <div class="tern-empty {klass}">
20
+ {#if icon}<div class="icon" aria-hidden="true">{@render icon()}</div>{/if}
21
+ <svelte:element this={`h${level}`} class="title">{title}</svelte:element>
22
+ {#if description}<p>{description}</p>{/if}
23
+ {#if actions}<div class="actions">{@render actions()}</div>{/if}
24
+ </div>
25
+
26
+ <style>
27
+ .tern-empty {
28
+ display: grid;
29
+ justify-items: center;
30
+ gap: 0.5rem;
31
+ padding: 2.5rem 1.5rem;
32
+ text-align: center;
33
+ border: 1px dashed var(--tern-border-strong);
34
+ border-radius: var(--tern-radius);
35
+ }
36
+ .icon {
37
+ display: grid;
38
+ place-items: center;
39
+ width: 2.75rem;
40
+ height: 2.75rem;
41
+ margin-bottom: 0.25rem;
42
+ color: var(--tern-accent);
43
+ background: var(--tern-accent-soft);
44
+ border-radius: 50%;
45
+ }
46
+ .title {
47
+ margin: 0;
48
+ font-size: 1rem;
49
+ font-weight: 600;
50
+ }
51
+ p {
52
+ max-width: 28rem;
53
+ margin: 0;
54
+ color: var(--tern-muted);
55
+ font-size: 0.875rem;
56
+ }
57
+ .actions {
58
+ display: flex;
59
+ gap: 0.5rem;
60
+ margin-top: 0.5rem;
61
+ }
62
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ title: string;
4
+ description?: string;
5
+ /** The heading level, to fit the page's outline. */
6
+ level?: 2 | 3 | 4;
7
+ /** An illustration or icon above the title. */
8
+ icon?: Snippet;
9
+ /** What to do next, such as a Create button. */
10
+ actions?: Snippet;
11
+ class?: string;
12
+ };
13
+ declare const Empty: import("svelte").Component<Props, {}, "">;
14
+ type Empty = ReturnType<typeof Empty>;
15
+ export default Empty;
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { setField } from './field.js';
4
+ import { getForm } from './form.js';
5
+
6
+ type Props = {
7
+ /** The visible label. */
8
+ label: string;
9
+ /** Help text under the control, read by screen readers with it. */
10
+ description?: string;
11
+ /** An error message; marks the control invalid and announces the message. */
12
+ error?: string | null;
13
+ required?: boolean;
14
+ disabled?: boolean;
15
+ /** The control's name. Inside a Form, the Field shows that field's error and knows if it's required, and the control posts under it. */
16
+ name?: string;
17
+ class?: string;
18
+ /** The control, such as an Input, Textarea or Select. */
19
+ children: Snippet;
20
+ };
21
+ let { label, description, error: errorProp = null, required: requiredProp = false, disabled = false, name, class: klass = '', children }: Props = $props();
22
+
23
+ const form = getForm();
24
+ const error = $derived(errorProp ?? (name ? form?.error(name) : undefined) ?? null);
25
+ const required = $derived(requiredProp || (name ? !!form?.field(name)?.required : false));
26
+
27
+ const uid = $props.id();
28
+ let custom = $state(false);
29
+ const id = `tern-field-${uid}`;
30
+ setField({
31
+ id,
32
+ labelId: `${id}-label`,
33
+ get describedBy() {
34
+ return [description ? `${id}-description` : '', error ? `${id}-error` : ''].filter(Boolean).join(' ') || undefined;
35
+ },
36
+ get invalid() {
37
+ return !!error;
38
+ },
39
+ get required() {
40
+ return required;
41
+ },
42
+ get disabled() {
43
+ return disabled;
44
+ },
45
+ custom() {
46
+ custom = true;
47
+ },
48
+ get name() {
49
+ return name;
50
+ },
51
+ get value() {
52
+ return name && !form?.field(name)?.secret ? form?.value(name) : undefined;
53
+ }
54
+ });
55
+ </script>
56
+
57
+ <div class="tern-field {klass}" class:disabled>
58
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
59
+ <label for={custom ? undefined : id} id="{id}-label" onclick={custom ? () => document.getElementById(id)?.focus() : undefined}>
60
+ {label}
61
+ {#if required}<span class="required" aria-hidden="true">*</span>{/if}
62
+ </label>
63
+ {@render children()}
64
+ {#if description}<p class="description" id="{id}-description">{description}</p>{/if}
65
+ {#if error}<p class="error" id="{id}-error" role="alert">{error}</p>{/if}
66
+ </div>
67
+
68
+ <style>
69
+ .tern-field {
70
+ display: grid;
71
+ align-content: start;
72
+ gap: 0.375rem;
73
+ }
74
+ label {
75
+ font-size: 0.875rem;
76
+ font-weight: 550;
77
+ color: var(--tern-fg);
78
+ }
79
+ .required {
80
+ color: var(--tern-danger);
81
+ margin-inline-start: 0.125rem;
82
+ }
83
+ .description {
84
+ margin: 0;
85
+ font-size: 0.8125rem;
86
+ color: var(--tern-muted);
87
+ }
88
+ .error {
89
+ margin: 0;
90
+ font-size: 0.8125rem;
91
+ color: var(--tern-danger);
92
+ font-weight: 500;
93
+ }
94
+ .disabled label {
95
+ opacity: 0.6;
96
+ }
97
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ /** The visible label. */
4
+ label: string;
5
+ /** Help text under the control, read by screen readers with it. */
6
+ description?: string;
7
+ /** An error message; marks the control invalid and announces the message. */
8
+ error?: string | null;
9
+ required?: boolean;
10
+ disabled?: boolean;
11
+ /** The control's name. Inside a Form, the Field shows that field's error and knows if it's required, and the control posts under it. */
12
+ name?: string;
13
+ class?: string;
14
+ /** The control, such as an Input, Textarea or Select. */
15
+ children: Snippet;
16
+ };
17
+ declare const Field: import("svelte").Component<Props, {}, "">;
18
+ type Field = ReturnType<typeof Field>;
19
+ export default Field;
@@ -0,0 +1,159 @@
1
+ <script lang="ts">
2
+ import { tick, type Snippet } from 'svelte';
3
+ import type { HTMLFormAttributes } from 'svelte/elements';
4
+ import { enhance as enhanceAction } from '../runtime/forms.js';
5
+ import { validate, type FormField } from '../runtime/validate.js';
6
+ import Alert from './Alert.svelte';
7
+ import { setForm } from './form.js';
8
+
9
+ type Failure = { errors?: Record<string, string>; values?: Record<string, unknown>; message?: string };
10
+
11
+ type Props = Omit<HTMLFormAttributes, 'onsubmit'> & {
12
+ /** The action's rules, from ./$forms: forms.create. The browser checks them as the user types and before posting. */
13
+ rules?: FormField[];
14
+ /** The page's form prop, so the server's errors and the posted values show up. */
15
+ form?: unknown;
16
+ /** Post in the background with use:enhance. Without it, or without JavaScript, the browser posts the form. */
17
+ enhance?: boolean;
18
+ /** Whether a post is in flight; bind it to show a spinner. */
19
+ submitting?: boolean;
20
+ /** Clear the form after a successful post. */
21
+ reset?: boolean;
22
+ /** Runs after a successful post with the action's result. */
23
+ onsuccess?: (data: unknown) => void;
24
+ children: Snippet;
25
+ };
26
+ let { rules = [], form, enhance = true, submitting = $bindable(false), reset = true, onsuccess, method = 'POST', children, class: klass = '', ...rest }: Props = $props();
27
+
28
+ const uid = $props.id();
29
+ let el = $state<HTMLFormElement>();
30
+ let client = $state<Record<string, string>>({});
31
+ let touched = $state<Record<string, boolean>>({});
32
+ let edited = $state<Record<string, boolean>>({});
33
+ let submitted = $state(false);
34
+ let hydrated = $state(false);
35
+ let summary = $state('');
36
+
37
+ const failure = $derived((form && typeof form === 'object' && 'errors' in form ? form : null) as Failure | null);
38
+ // A new answer from the server replaces what the user had edited since the last one.
39
+ $effect(() => {
40
+ void failure;
41
+ edited = {};
42
+ });
43
+ $effect(() => {
44
+ hydrated = true;
45
+ });
46
+
47
+ const byName = $derived(new Map(rules.map((f) => [f.name, f])));
48
+ setForm({
49
+ error: (name) => client[name] ?? (edited[name] ? undefined : failure?.errors?.[name]),
50
+ value: (name) => {
51
+ const v = failure?.values?.[name];
52
+ return typeof v === 'string' ? v : undefined;
53
+ },
54
+ field: (name) => byName.get(name)
55
+ });
56
+
57
+ function check(names: string[]) {
58
+ if (!el || rules.length === 0) return;
59
+ const all = validate(rules, new FormData(el));
60
+ const next = { ...client };
61
+ for (const n of names) {
62
+ if (all[n]) next[n] = all[n]!;
63
+ else delete next[n];
64
+ }
65
+ client = next;
66
+ }
67
+ // A field and the fields that must match it, such as a password and its confirmation.
68
+ const related = (name: string) => [name, ...rules.filter((f) => f.rules?.some((r) => r.name === 'same' && r.arg === name)).map((f) => f.name)];
69
+
70
+ function oninput(e: Event) {
71
+ const name = (e.target as HTMLInputElement | null)?.name;
72
+ if (!name) return;
73
+ edited = { ...edited, [name]: true };
74
+ if (touched[name] || submitted) check(related(name).filter((n) => touched[n] || submitted || n === name));
75
+ }
76
+ function onfocusout(e: FocusEvent) {
77
+ const name = (e.target as HTMLInputElement | null)?.name;
78
+ // Don't complain about a field the user only passed through.
79
+ if (!name || !byName.has(name) || (!edited[name] && !submitted)) return;
80
+ touched = { ...touched, [name]: true };
81
+ check([name]);
82
+ }
83
+
84
+ /** Checks everything; returns false and focuses the first problem if anything is wrong. */
85
+ function checkAll(data: FormData) {
86
+ submitted = true;
87
+ const errors = validate(rules, data) as Record<string, string>;
88
+ client = errors;
89
+ const names = Object.keys(errors);
90
+ if (names.length === 0) {
91
+ summary = '';
92
+ return true;
93
+ }
94
+ summary = names.length === 1 ? '1 field needs attention.' : `${names.length} fields need attention.`;
95
+ void tick().then(() => el?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus());
96
+ return false;
97
+ }
98
+
99
+ const submit = ({ formData, cancel }: { formData: FormData; cancel(): void }) => {
100
+ const errors = validate(rules, formData);
101
+ if (Object.keys(errors).length > 0) {
102
+ cancel();
103
+ checkAll(formData);
104
+ return;
105
+ }
106
+ submitted = true;
107
+ client = {};
108
+ summary = '';
109
+ submitting = true;
110
+ return async ({ update, result }: { update: (o?: { reset?: boolean }) => Promise<void>; result: { type: string; data?: unknown } }) => {
111
+ await update({ reset });
112
+ submitting = false;
113
+ if (result.type === 'success') {
114
+ submitted = false;
115
+ touched = {};
116
+ onsuccess?.(result.data);
117
+ } else if (result.type === 'failure') {
118
+ await tick();
119
+ el?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus();
120
+ }
121
+ };
122
+ };
123
+
124
+ function attach(node: HTMLFormElement) {
125
+ if (!enhance) return;
126
+ const action = enhanceAction(node, submit as never);
127
+ return () => action?.destroy?.();
128
+ }
129
+ </script>
130
+
131
+ <form
132
+ bind:this={el}
133
+ class="tern-form {klass}"
134
+ {method}
135
+ novalidate={hydrated && rules.length > 0}
136
+ aria-describedby={failure?.message ? `tern-form-${uid}-message` : undefined}
137
+ {...rest}
138
+ {oninput}
139
+ onchange={oninput}
140
+ {onfocusout}
141
+ onsubmit={(e) => {
142
+ if (enhance) return; // use:enhance checks first
143
+ if (!checkAll(new FormData(e.currentTarget))) e.preventDefault();
144
+ }}
145
+ {@attach attach}
146
+ >
147
+ {#if failure?.message}
148
+ <div id="tern-form-{uid}-message"><Alert tone="danger" live>{failure.message}</Alert></div>
149
+ {/if}
150
+ {@render children()}
151
+ <p class="tern-sr-only" role="status">{summary}</p>
152
+ </form>
153
+
154
+ <style>
155
+ .tern-form {
156
+ display: grid;
157
+ gap: 1rem;
158
+ }
159
+ </style>
@@ -0,0 +1,21 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLFormAttributes } from 'svelte/elements';
3
+ import { type FormField } from '../runtime/validate.js';
4
+ type Props = Omit<HTMLFormAttributes, 'onsubmit'> & {
5
+ /** The action's rules, from ./$forms: forms.create. The browser checks them as the user types and before posting. */
6
+ rules?: FormField[];
7
+ /** The page's form prop, so the server's errors and the posted values show up. */
8
+ form?: unknown;
9
+ /** Post in the background with use:enhance. Without it, or without JavaScript, the browser posts the form. */
10
+ enhance?: boolean;
11
+ /** Whether a post is in flight; bind it to show a spinner. */
12
+ submitting?: boolean;
13
+ /** Clear the form after a successful post. */
14
+ reset?: boolean;
15
+ /** Runs after a successful post with the action's result. */
16
+ onsuccess?: (data: unknown) => void;
17
+ children: Snippet;
18
+ };
19
+ declare const Form: import("svelte").Component<Props, {}, "submitting">;
20
+ type Form = ReturnType<typeof Form>;
21
+ export default Form;
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { float, type Align, type Side } from './floating.js';
4
+
5
+ export type HoverCardTrigger = {
6
+ 'aria-describedby'?: string;
7
+ onpointerenter: () => void;
8
+ onpointerleave: () => void;
9
+ onfocus: () => void;
10
+ onblur: () => void;
11
+ onkeydown: (e: KeyboardEvent) => void;
12
+ };
13
+
14
+ type Props = {
15
+ /** The link or button it previews. Spread the props onto it. */
16
+ trigger: Snippet<[HoverCardTrigger]>;
17
+ /** The preview. Pointer and keyboard users see it; it is also linked as the trigger's description, so keep it short or make sure what it shows is reachable elsewhere. */
18
+ children: Snippet;
19
+ side?: Side;
20
+ align?: Align;
21
+ /** Milliseconds before it opens. */
22
+ openDelay?: number;
23
+ /** Milliseconds before it closes once the pointer leaves. */
24
+ closeDelay?: number;
25
+ class?: string;
26
+ };
27
+ let { trigger, children, side = 'bottom', align = 'center', openDelay = 500, closeDelay = 200, class: klass = '' }: Props = $props();
28
+
29
+ const uid = $props.id();
30
+ const id = `tern-hovercard-${uid}`;
31
+ let open = $state(false);
32
+ let card = $state<HTMLDivElement>();
33
+ let anchorEl: Element | null = null;
34
+ let timer: ReturnType<typeof setTimeout> | undefined;
35
+
36
+ function later(fn: () => void, ms: number) {
37
+ clearTimeout(timer);
38
+ timer = setTimeout(fn, ms);
39
+ }
40
+ const show = (now = false) => {
41
+ anchorEl = document.activeElement?.matches(`[aria-describedby~="${id}"]`) ? document.activeElement : (document.querySelector(`[aria-describedby~="${id}"]`) ?? anchorEl);
42
+ if (now) {
43
+ clearTimeout(timer);
44
+ open = true;
45
+ } else later(() => (open = true), openDelay);
46
+ };
47
+ const hide = (now = false) => (now ? ((open = false), clearTimeout(timer)) : later(() => (open = false), closeDelay));
48
+
49
+ $effect(() => {
50
+ if (!open || !card || !anchorEl) return;
51
+ card.showPopover?.();
52
+ return float(card, anchorEl, { side, align, offset: 8 });
53
+ });
54
+ </script>
55
+
56
+ {@render trigger({
57
+ 'aria-describedby': id,
58
+ onpointerenter: () => show(),
59
+ onpointerleave: () => hide(),
60
+ onfocus: () => show(true),
61
+ onblur: () => hide(true),
62
+ onkeydown: (e) => e.key === 'Escape' && hide(true)
63
+ })}
64
+ <div
65
+ bind:this={card}
66
+ {id}
67
+ class="tern-hovercard {klass}"
68
+ class:open
69
+ popover="manual"
70
+ role="none"
71
+ onpointerenter={() => clearTimeout(timer)}
72
+ onpointerleave={() => hide()}
73
+ >
74
+ {#if open}{@render children()}{/if}
75
+ </div>
76
+
77
+ <style>
78
+ .tern-hovercard {
79
+ width: max-content;
80
+ max-width: min(20rem, calc(100vw - 1rem));
81
+ padding: 1rem;
82
+ color: var(--tern-fg);
83
+ background: var(--tern-surface);
84
+ border: 1px solid var(--tern-border);
85
+ border-radius: var(--tern-radius);
86
+ box-shadow: var(--tern-shadow-lg);
87
+ font-size: 0.875rem;
88
+ }
89
+ .tern-hovercard:not(.open) {
90
+ display: none;
91
+ }
92
+ .open {
93
+ animation: tern-hover-in var(--tern-duration) ease-out;
94
+ }
95
+ @keyframes tern-hover-in {
96
+ from {
97
+ opacity: 0;
98
+ transform: translateY(2px);
99
+ }
100
+ }
101
+ @media (prefers-reduced-motion: reduce) {
102
+ .open {
103
+ animation: none;
104
+ }
105
+ }
106
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Align, type Side } from './floating.js';
3
+ export type HoverCardTrigger = {
4
+ 'aria-describedby'?: string;
5
+ onpointerenter: () => void;
6
+ onpointerleave: () => void;
7
+ onfocus: () => void;
8
+ onblur: () => void;
9
+ onkeydown: (e: KeyboardEvent) => void;
10
+ };
11
+ type Props = {
12
+ /** The link or button it previews. Spread the props onto it. */
13
+ trigger: Snippet<[HoverCardTrigger]>;
14
+ /** The preview. Pointer and keyboard users see it; it is also linked as the trigger's description, so keep it short or make sure what it shows is reachable elsewhere. */
15
+ children: Snippet;
16
+ side?: Side;
17
+ align?: Align;
18
+ /** Milliseconds before it opens. */
19
+ openDelay?: number;
20
+ /** Milliseconds before it closes once the pointer leaves. */
21
+ closeDelay?: number;
22
+ class?: string;
23
+ };
24
+ declare const HoverCard: import("svelte").Component<Props, {}, "">;
25
+ type HoverCard = ReturnType<typeof HoverCard>;
26
+ export default HoverCard;
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { getField } from './field.js';
4
+
5
+ type Props = HTMLInputAttributes & { value?: unknown };
6
+ const field = getField();
7
+ let { value = $bindable(field?.value), class: klass = '', ...rest }: Props = $props();
8
+ </script>
9
+
10
+ <input
11
+ class="tern-input {klass}"
12
+ id={field?.id}
13
+ aria-describedby={field?.describedBy}
14
+ aria-invalid={field?.invalid || undefined}
15
+ required={field?.required}
16
+ disabled={field?.disabled}
17
+ name={field?.name}
18
+ {...rest}
19
+ bind:value
20
+ />
21
+
22
+ <style>
23
+ .tern-input {
24
+ width: 100%;
25
+ box-sizing: border-box;
26
+ height: var(--tern-control-height);
27
+ padding: 0 0.75rem;
28
+ font: inherit;
29
+ font-size: 0.875rem;
30
+ color: var(--tern-fg);
31
+ background: var(--tern-surface);
32
+ border: 1px solid var(--tern-border-strong);
33
+ border-radius: var(--tern-radius-sm);
34
+ box-shadow: inset 0 1px 1px oklch(0.24 0.02 240 / 0.04);
35
+ transition:
36
+ border-color var(--tern-duration),
37
+ box-shadow var(--tern-duration);
38
+ }
39
+ .tern-input::placeholder {
40
+ color: var(--tern-muted);
41
+ opacity: 0.8;
42
+ }
43
+ .tern-input:hover:not(:disabled) {
44
+ border-color: var(--tern-muted);
45
+ }
46
+ .tern-input:focus-visible {
47
+ outline: none;
48
+ border-color: var(--tern-accent);
49
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
50
+ }
51
+ .tern-input[aria-invalid='true'] {
52
+ border-color: var(--tern-danger);
53
+ }
54
+ .tern-input[aria-invalid='true']:focus-visible {
55
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-danger) 25%, transparent);
56
+ }
57
+ .tern-input:disabled {
58
+ opacity: 0.6;
59
+ cursor: not-allowed;
60
+ background: var(--tern-surface-2);
61
+ }
62
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type Props = HTMLInputAttributes & {
3
+ value?: unknown;
4
+ };
5
+ declare const Input: import("svelte").Component<Props, {}, "value">;
6
+ type Input = ReturnType<typeof Input>;
7
+ export default Input;
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Props = {
5
+ /** Before the input: text such as "https://", an icon, or a button. */
6
+ start?: Snippet;
7
+ /** After the input: text such as ".com", a unit, or a button. */
8
+ end?: Snippet;
9
+ class?: string;
10
+ /** The Input. */
11
+ children: Snippet;
12
+ };
13
+ let { start, end, class: klass = '', children }: Props = $props();
14
+ </script>
15
+
16
+ <!-- An Input with text, icons or buttons inside its border. Text in start and end is read with the label when you add it to the Field's description, not on its own. -->
17
+ <div class="tern-input-group {klass}">
18
+ {#if start}<span class="addon start">{@render start()}</span>{/if}
19
+ {@render children()}
20
+ {#if end}<span class="addon end">{@render end()}</span>{/if}
21
+ </div>
22
+
23
+ <style>
24
+ .tern-input-group {
25
+ display: flex;
26
+ align-items: center;
27
+ height: var(--tern-control-height);
28
+ background: var(--tern-surface);
29
+ border: 1px solid var(--tern-border-strong);
30
+ border-radius: var(--tern-radius-sm);
31
+ transition:
32
+ border-color var(--tern-duration),
33
+ box-shadow var(--tern-duration);
34
+ }
35
+ .tern-input-group:hover {
36
+ border-color: var(--tern-muted);
37
+ }
38
+ .tern-input-group:has(:global(input:focus-visible)) {
39
+ border-color: var(--tern-accent);
40
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
41
+ }
42
+ .tern-input-group:has(:global([aria-invalid='true'])) {
43
+ border-color: var(--tern-danger);
44
+ }
45
+ .tern-input-group > :global(.tern-input) {
46
+ flex: 1;
47
+ min-width: 0;
48
+ height: 100%;
49
+ border: 0;
50
+ box-shadow: none;
51
+ background: transparent;
52
+ }
53
+ .tern-input-group > :global(.tern-input:focus-visible) {
54
+ box-shadow: none;
55
+ }
56
+ .addon {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 0.25rem;
60
+ height: 100%;
61
+ color: var(--tern-muted);
62
+ font-size: 0.875rem;
63
+ white-space: nowrap;
64
+ }
65
+ .start {
66
+ padding-inline-start: 0.75rem;
67
+ }
68
+ .end {
69
+ padding-inline-end: 0.75rem;
70
+ }
71
+ .start + :global(.tern-input) {
72
+ padding-inline-start: 0.4rem;
73
+ }
74
+ .addon :global(.tern-btn) {
75
+ height: calc(var(--tern-control-height) - 0.6rem);
76
+ }
77
+ .end:has(:global(.tern-btn)) {
78
+ padding-inline-end: 0.25rem;
79
+ }
80
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ /** Before the input: text such as "https://", an icon, or a button. */
4
+ start?: Snippet;
5
+ /** After the input: text such as ".com", a unit, or a button. */
6
+ end?: Snippet;
7
+ class?: string;
8
+ /** The Input. */
9
+ children: Snippet;
10
+ };
11
+ declare const InputGroup: import("svelte").Component<Props, {}, "">;
12
+ type InputGroup = ReturnType<typeof InputGroup>;
13
+ export default InputGroup;