@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,18 @@
1
+ type Link = {
2
+ label: string;
3
+ href: string;
4
+ description?: string;
5
+ };
6
+ type Entry = {
7
+ label: string;
8
+ href?: string;
9
+ items?: Link[];
10
+ };
11
+ type Props = {
12
+ items: Entry[];
13
+ label?: string;
14
+ class?: string;
15
+ };
16
+ declare const NavigationMenu: import("svelte").Component<Props, {}, "">;
17
+ type NavigationMenu = ReturnType<typeof NavigationMenu>;
18
+ export default NavigationMenu;
@@ -0,0 +1,114 @@
1
+ <script lang="ts">
2
+ type Props = {
3
+ /** The current page, from 1; bind it. */
4
+ page?: number;
5
+ /** How many pages there are. */
6
+ pages: number;
7
+ /** Makes pages links, such as (p) => `?page=${p}`, so they work without JavaScript and can be opened in new tabs. */
8
+ href?: (page: number) => string;
9
+ /** Pages shown on each side of the current one. */
10
+ siblings?: number;
11
+ label?: string;
12
+ class?: string;
13
+ onchange?: (page: number) => void;
14
+ };
15
+ let { page = $bindable(1), pages, href, siblings = 1, label = 'Pagination', class: klass = '', onchange }: Props = $props();
16
+
17
+ // 1 … 4 5 6 … 20: the ends, the current page and its siblings.
18
+ const shown = $derived.by(() => {
19
+ const out: (number | 'gap')[] = [];
20
+ const from = Math.max(2, page - siblings);
21
+ const to = Math.min(pages - 1, page + siblings);
22
+ out.push(1);
23
+ if (from > 2) out.push(from === 3 ? 2 : 'gap');
24
+ for (let p = from; p <= to; p++) out.push(p);
25
+ if (to < pages - 1) out.push(to === pages - 2 ? pages - 1 : 'gap');
26
+ if (pages > 1) out.push(pages);
27
+ return out;
28
+ });
29
+
30
+ function go(p: number, e?: MouseEvent) {
31
+ if (p < 1 || p > pages || p === page) return;
32
+ if (!href) e?.preventDefault();
33
+ page = p;
34
+ onchange?.(p);
35
+ }
36
+ </script>
37
+
38
+ {#snippet link(p: number, content: string, aria: string | undefined, disabled: boolean, cls: string)}
39
+ {#if href && !disabled}
40
+ <a href={href(p)} class={cls} aria-label={aria} aria-current={p === page && !aria ? 'page' : undefined} onclick={(e) => go(p, e)}>{content}</a>
41
+ {:else}
42
+ <button type="button" class={cls} aria-label={aria} aria-current={p === page && !aria ? 'page' : undefined} {disabled} onclick={(e) => go(p, e)}>{content}</button>
43
+ {/if}
44
+ {/snippet}
45
+
46
+ <nav class="tern-pagination {klass}" aria-label={label}>
47
+ <ul>
48
+ <li>{@render link(page - 1, '‹ Previous', 'Previous page', page <= 1, 'step')}</li>
49
+ {#each shown as p, i (i)}
50
+ <li>
51
+ {#if p === 'gap'}
52
+ <span class="gap" aria-hidden="true">…</span>
53
+ {:else}
54
+ {@render link(p, String(p), undefined, false, 'num')}
55
+ {/if}
56
+ </li>
57
+ {/each}
58
+ <li>{@render link(page + 1, 'Next ›', 'Next page', page >= pages, 'step')}</li>
59
+ </ul>
60
+ </nav>
61
+
62
+ <style>
63
+ ul {
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ align-items: center;
67
+ gap: 0.25rem;
68
+ margin: 0;
69
+ padding: 0;
70
+ list-style: none;
71
+ }
72
+ a,
73
+ button {
74
+ display: inline-grid;
75
+ place-items: center;
76
+ min-width: 2.25rem;
77
+ height: 2.25rem;
78
+ padding: 0 0.6rem;
79
+ box-sizing: border-box;
80
+ font: inherit;
81
+ font-size: 0.875rem;
82
+ font-weight: 550;
83
+ font-variant-numeric: tabular-nums;
84
+ color: var(--tern-fg);
85
+ text-decoration: none;
86
+ background: transparent;
87
+ border: 1px solid transparent;
88
+ border-radius: var(--tern-radius-sm);
89
+ cursor: pointer;
90
+ }
91
+ a:hover,
92
+ button:hover:not(:disabled) {
93
+ background: var(--tern-surface-2);
94
+ }
95
+ [aria-current='page'] {
96
+ background: var(--tern-surface);
97
+ border-color: var(--tern-border-strong);
98
+ box-shadow: var(--tern-shadow);
99
+ }
100
+ button:disabled {
101
+ color: var(--tern-muted);
102
+ opacity: 0.5;
103
+ cursor: not-allowed;
104
+ }
105
+ a:focus-visible,
106
+ button:focus-visible {
107
+ outline: 2px solid var(--tern-ring);
108
+ outline-offset: 1px;
109
+ }
110
+ .gap {
111
+ padding: 0 0.25rem;
112
+ color: var(--tern-muted);
113
+ }
114
+ </style>
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ /** The current page, from 1; bind it. */
3
+ page?: number;
4
+ /** How many pages there are. */
5
+ pages: number;
6
+ /** Makes pages links, such as (p) => `?page=${p}`, so they work without JavaScript and can be opened in new tabs. */
7
+ href?: (page: number) => string;
8
+ /** Pages shown on each side of the current one. */
9
+ siblings?: number;
10
+ label?: string;
11
+ class?: string;
12
+ onchange?: (page: number) => void;
13
+ };
14
+ declare const Pagination: import("svelte").Component<Props, {}, "page">;
15
+ type Pagination = ReturnType<typeof Pagination>;
16
+ export default Pagination;
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { getField } from './field.js';
4
+
5
+ type Props = Omit<HTMLInputAttributes, 'type'> & {
6
+ value?: string;
7
+ /** Whether the password is shown; bind it. */
8
+ visible?: boolean;
9
+ /** current-password on sign-in forms, new-password where it's set, so password managers help. */
10
+ autocomplete?: 'current-password' | 'new-password';
11
+ };
12
+ let { value = $bindable(''), visible = $bindable(false), autocomplete = 'current-password', class: klass = '', ...rest }: Props = $props();
13
+
14
+ const uid = $props.id();
15
+ const field = getField();
16
+ const id = field?.id ?? `tern-password-${uid}`;
17
+ let input = $state<HTMLInputElement>();
18
+ </script>
19
+
20
+ <div class="tern-password {klass}">
21
+ <input
22
+ bind:this={input}
23
+ class="input"
24
+ {id}
25
+ type={visible ? 'text' : 'password'}
26
+ {autocomplete}
27
+ autocapitalize="off"
28
+ spellcheck="false"
29
+ aria-describedby={field?.describedBy}
30
+ aria-invalid={field?.invalid || undefined}
31
+ required={field?.required}
32
+ disabled={field?.disabled}
33
+ name={field?.name}
34
+ {...rest}
35
+ bind:value
36
+ />
37
+ <button
38
+ type="button"
39
+ class="toggle"
40
+ aria-label="Show password"
41
+ aria-pressed={visible}
42
+ aria-controls={id}
43
+ disabled={field?.disabled || rest.disabled}
44
+ onclick={() => {
45
+ // Keep the caret where it was when the type changes.
46
+ const at = input?.selectionStart ?? null;
47
+ visible = !visible;
48
+ requestAnimationFrame(() => at !== null && input === document.activeElement && input.setSelectionRange(at, at));
49
+ }}
50
+ >
51
+ {#if visible}
52
+ <svg aria-hidden="true" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
53
+ <path d="M3 3l14 14" />
54
+ <path d="M8.6 8.6a2 2 0 002.8 2.8" />
55
+ <path d="M6.2 6.3C4.4 7.4 3.1 8.9 2.5 10c1.3 2.4 4.2 5 7.5 5 1.4 0 2.7-.4 3.8-1.1" />
56
+ <path d="M9 5.1c.3 0 .7-.1 1-.1 3.3 0 6.2 2.6 7.5 5-.4.8-1 1.6-1.7 2.4" />
57
+ </svg>
58
+ {:else}
59
+ <svg aria-hidden="true" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
60
+ <path d="M2.5 10c1.3-2.4 4.2-5 7.5-5s6.2 2.6 7.5 5c-1.3 2.4-4.2 5-7.5 5s-6.2-2.6-7.5-5z" />
61
+ <circle cx="10" cy="10" r="2.25" />
62
+ </svg>
63
+ {/if}
64
+ </button>
65
+ </div>
66
+
67
+ <style>
68
+ .tern-password {
69
+ position: relative;
70
+ display: flex;
71
+ align-items: center;
72
+ }
73
+ .input {
74
+ width: 100%;
75
+ box-sizing: border-box;
76
+ height: var(--tern-control-height);
77
+ padding: 0 2.5rem 0 0.75rem;
78
+ font: inherit;
79
+ font-size: 0.875rem;
80
+ color: var(--tern-fg);
81
+ background: var(--tern-surface);
82
+ border: 1px solid var(--tern-border-strong);
83
+ border-radius: var(--tern-radius-sm);
84
+ transition:
85
+ border-color var(--tern-duration),
86
+ box-shadow var(--tern-duration);
87
+ }
88
+ /* Browsers' own reveal buttons would sit next to ours. */
89
+ .input::-ms-reveal,
90
+ .input::-ms-clear {
91
+ display: none;
92
+ }
93
+ .input:hover:not(:disabled) {
94
+ border-color: var(--tern-muted);
95
+ }
96
+ .input:focus-visible {
97
+ outline: none;
98
+ border-color: var(--tern-accent);
99
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
100
+ }
101
+ .input[aria-invalid='true'] {
102
+ border-color: var(--tern-danger);
103
+ }
104
+ .input:disabled {
105
+ opacity: 0.6;
106
+ cursor: not-allowed;
107
+ }
108
+ .toggle {
109
+ position: absolute;
110
+ right: 0.3rem;
111
+ display: grid;
112
+ place-items: center;
113
+ width: 1.875rem;
114
+ height: 1.875rem;
115
+ padding: 0;
116
+ color: var(--tern-muted);
117
+ background: transparent;
118
+ border: 0;
119
+ border-radius: calc(var(--tern-radius-sm) - 2px);
120
+ cursor: pointer;
121
+ }
122
+ .toggle:hover:not(:disabled) {
123
+ color: var(--tern-fg);
124
+ background: var(--tern-surface-2);
125
+ }
126
+ .toggle:focus-visible {
127
+ outline: 2px solid var(--tern-ring);
128
+ outline-offset: 0;
129
+ }
130
+ .toggle:disabled {
131
+ opacity: 0.5;
132
+ cursor: not-allowed;
133
+ }
134
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type Props = Omit<HTMLInputAttributes, 'type'> & {
3
+ value?: string;
4
+ /** Whether the password is shown; bind it. */
5
+ visible?: boolean;
6
+ /** current-password on sign-in forms, new-password where it's set, so password managers help. */
7
+ autocomplete?: 'current-password' | 'new-password';
8
+ };
9
+ declare const PasswordInput: import("svelte").Component<Props, {}, "value" | "visible">;
10
+ type PasswordInput = ReturnType<typeof PasswordInput>;
11
+ export default PasswordInput;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { float, focusables, type Align, type Side } from './floating.js';
4
+
5
+ export type PopoverTrigger = {
6
+ id: string;
7
+ 'aria-haspopup': 'dialog';
8
+ 'aria-expanded': boolean;
9
+ 'aria-controls': string;
10
+ onclick: () => void;
11
+ };
12
+
13
+ type Props = {
14
+ /** Whether it's open; bind it. */
15
+ open?: boolean;
16
+ /** A heading shown at the top, which labels the popover. */
17
+ title?: string;
18
+ /** Labels the popover when it has no title. */
19
+ label?: string;
20
+ side?: Side;
21
+ align?: Align;
22
+ /** The button that opens it. Spread the props onto it: {#snippet trigger(props)}<Button {...props}>…</Button>{/snippet} */
23
+ trigger: Snippet<[PopoverTrigger]>;
24
+ children: Snippet;
25
+ class?: string;
26
+ /** The trigger's id, when it needs a particular one, such as a Field's. */
27
+ triggerId?: string;
28
+ onclose?: () => void;
29
+ };
30
+ let { open = $bindable(false), title, label, side = 'bottom', align = 'center', trigger, children, class: klass = '', triggerId, onclose }: Props = $props();
31
+
32
+ const uid = $props.id();
33
+ const id = `tern-popover-${uid}`;
34
+ let panel = $state<HTMLDivElement>();
35
+ const tid = $derived(triggerId ?? `${id}-trigger`);
36
+ const anchor = () => document.getElementById(tid);
37
+
38
+ function close(returnFocus: boolean) {
39
+ if (!open) return;
40
+ open = false;
41
+ onclose?.();
42
+ if (returnFocus) anchor()?.focus();
43
+ }
44
+
45
+ $effect(() => {
46
+ if (!open || !panel) return;
47
+ const a = anchor();
48
+ if (!a) return;
49
+ panel.showPopover?.();
50
+ const stop = float(panel, a, { side, align, offset: 8 });
51
+ (focusables(panel)[0] ?? panel).focus({ preventScroll: true });
52
+ const away = (e: PointerEvent) => {
53
+ const t = e.target as Node;
54
+ if (!panel?.contains(t) && !a.contains(t)) close(false);
55
+ };
56
+ document.addEventListener('pointerdown', away, true);
57
+ return () => {
58
+ stop();
59
+ document.removeEventListener('pointerdown', away, true);
60
+ };
61
+ });
62
+ </script>
63
+
64
+ {@render trigger({
65
+ id: tid,
66
+ 'aria-haspopup': 'dialog',
67
+ 'aria-expanded': open,
68
+ 'aria-controls': id,
69
+ onclick: () => (open ? close(false) : (open = true))
70
+ })}
71
+ {#if open}
72
+ <div
73
+ bind:this={panel}
74
+ {id}
75
+ class="tern-popover {klass}"
76
+ role="dialog"
77
+ aria-labelledby={title ? `${id}-title` : undefined}
78
+ aria-label={title ? undefined : label}
79
+ tabindex="-1"
80
+ popover="manual"
81
+ onkeydown={(e) => {
82
+ if (e.key === 'Escape') {
83
+ e.stopPropagation();
84
+ close(true);
85
+ }
86
+ }}
87
+ onfocusout={(e) => {
88
+ const to = e.relatedTarget as Node | null;
89
+ if (to && !panel?.contains(to) && !anchor()?.contains(to)) close(false);
90
+ }}
91
+ >
92
+ {#if title}<h2 class="title" id="{id}-title">{title}</h2>{/if}
93
+ {@render children()}
94
+ </div>
95
+ {/if}
96
+
97
+ <style>
98
+ .tern-popover {
99
+ width: max-content;
100
+ max-width: min(22rem, calc(100vw - 1rem));
101
+ padding: 1rem;
102
+ color: var(--tern-fg);
103
+ background: var(--tern-surface);
104
+ border: 1px solid var(--tern-border);
105
+ border-radius: var(--tern-radius);
106
+ box-shadow: var(--tern-shadow-lg);
107
+ font-size: 0.875rem;
108
+ outline: none;
109
+ animation: tern-pop-in var(--tern-duration) ease-out;
110
+ }
111
+ .title {
112
+ margin: 0 0 0.5rem;
113
+ font-size: 0.9375rem;
114
+ font-weight: 600;
115
+ }
116
+ @keyframes tern-pop-in {
117
+ from {
118
+ opacity: 0;
119
+ transform: scale(0.97);
120
+ }
121
+ }
122
+ @media (prefers-reduced-motion: reduce) {
123
+ .tern-popover {
124
+ animation: none;
125
+ }
126
+ }
127
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Align, type Side } from './floating.js';
3
+ export type PopoverTrigger = {
4
+ id: string;
5
+ 'aria-haspopup': 'dialog';
6
+ 'aria-expanded': boolean;
7
+ 'aria-controls': string;
8
+ onclick: () => void;
9
+ };
10
+ type Props = {
11
+ /** Whether it's open; bind it. */
12
+ open?: boolean;
13
+ /** A heading shown at the top, which labels the popover. */
14
+ title?: string;
15
+ /** Labels the popover when it has no title. */
16
+ label?: string;
17
+ side?: Side;
18
+ align?: Align;
19
+ /** The button that opens it. Spread the props onto it: {#snippet trigger(props)}<Button {...props}>…</Button>{/snippet} */
20
+ trigger: Snippet<[PopoverTrigger]>;
21
+ children: Snippet;
22
+ class?: string;
23
+ /** The trigger's id, when it needs a particular one, such as a Field's. */
24
+ triggerId?: string;
25
+ onclose?: () => void;
26
+ };
27
+ declare const Popover: import("svelte").Component<Props, {}, "open">;
28
+ type Popover = ReturnType<typeof Popover>;
29
+ export default Popover;
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ type Props = {
3
+ /** The progress, from 0 to max; leave it out while the amount is unknown. */
4
+ value?: number | null;
5
+ max?: number;
6
+ /** What is progressing, for screen readers and shown above the bar. */
7
+ label: string;
8
+ showLabel?: boolean;
9
+ class?: string;
10
+ };
11
+ let { value = null, max = 100, label, showLabel = false, class: klass = '' }: Props = $props();
12
+ const pct = $derived(value === null ? null : Math.max(0, Math.min(100, (value / max) * 100)));
13
+ const uid = $props.id();
14
+ </script>
15
+
16
+ <div class="tern-progress {klass}">
17
+ {#if showLabel}
18
+ <div class="head">
19
+ <span id="tern-progress-{uid}">{label}</span>
20
+ {#if pct !== null}<span class="pct">{Math.round(pct)}%</span>{/if}
21
+ </div>
22
+ {/if}
23
+ <div
24
+ class="track"
25
+ role="progressbar"
26
+ aria-label={showLabel ? undefined : label}
27
+ aria-labelledby={showLabel ? `tern-progress-${uid}` : undefined}
28
+ aria-valuemin={0}
29
+ aria-valuemax={max}
30
+ aria-valuenow={value ?? undefined}
31
+ >
32
+ <div class="bar" class:indeterminate={pct === null} style:width={pct === null ? undefined : `${pct}%`}></div>
33
+ </div>
34
+ </div>
35
+
36
+ <style>
37
+ .head {
38
+ display: flex;
39
+ justify-content: space-between;
40
+ margin-bottom: 0.35rem;
41
+ font-size: 0.8125rem;
42
+ font-weight: 550;
43
+ }
44
+ .pct {
45
+ color: var(--tern-muted);
46
+ font-variant-numeric: tabular-nums;
47
+ }
48
+ .track {
49
+ height: 0.5rem;
50
+ border-radius: 999px;
51
+ background: var(--tern-surface-2);
52
+ border: 1px solid var(--tern-border);
53
+ overflow: hidden;
54
+ }
55
+ .bar {
56
+ height: 100%;
57
+ border-radius: inherit;
58
+ background: var(--tern-accent);
59
+ transition: width var(--tern-duration) ease-out;
60
+ }
61
+ .indeterminate {
62
+ width: 35%;
63
+ animation: slide 1.2s ease-in-out infinite;
64
+ }
65
+ @keyframes slide {
66
+ from {
67
+ translate: -100% 0;
68
+ }
69
+ to {
70
+ translate: 300% 0;
71
+ }
72
+ }
73
+ </style>
@@ -0,0 +1,12 @@
1
+ type Props = {
2
+ /** The progress, from 0 to max; leave it out while the amount is unknown. */
3
+ value?: number | null;
4
+ max?: number;
5
+ /** What is progressing, for screen readers and shown above the bar. */
6
+ label: string;
7
+ showLabel?: boolean;
8
+ class?: string;
9
+ };
10
+ declare const Progress: import("svelte").Component<Props, {}, "">;
11
+ type Progress = ReturnType<typeof Progress>;
12
+ export default Progress;
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ type Option = { value: string; label: string; description?: string; disabled?: boolean };
3
+ type Props = {
4
+ /** The group's label, read as the fieldset's legend. */
5
+ label: string;
6
+ name: string;
7
+ options: Option[];
8
+ value?: string;
9
+ /** Lay the options out in a row. */
10
+ inline?: boolean;
11
+ required?: boolean;
12
+ disabled?: boolean;
13
+ class?: string;
14
+ };
15
+ let { label, name, options, value = $bindable(), inline = false, required = false, disabled = false, class: klass = '' }: Props = $props();
16
+ const uid = $props.id();
17
+ </script>
18
+
19
+ <!-- Native radios: arrow keys move between options, as screen readers expect. -->
20
+ <fieldset class="tern-radios {klass}" class:inline {disabled}>
21
+ <legend>{label}</legend>
22
+ <div class="options">
23
+ {#each options as o, i (o.value)}
24
+ <label class="option">
25
+ <input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} {required} aria-describedby={o.description ? `tern-radio-${uid}-${i}` : undefined} />
26
+ <span class="dot" aria-hidden="true"></span>
27
+ <span class="text">
28
+ {o.label}
29
+ {#if o.description}<span class="description" id="tern-radio-{uid}-{i}">{o.description}</span>{/if}
30
+ </span>
31
+ </label>
32
+ {/each}
33
+ </div>
34
+ </fieldset>
35
+
36
+ <style>
37
+ fieldset {
38
+ border: 0;
39
+ padding: 0;
40
+ margin: 0;
41
+ min-width: 0;
42
+ }
43
+ legend {
44
+ font-size: 0.875rem;
45
+ font-weight: 550;
46
+ margin-bottom: 0.5rem;
47
+ padding: 0;
48
+ }
49
+ .options {
50
+ display: grid;
51
+ gap: 0.5rem;
52
+ }
53
+ .inline .options {
54
+ display: flex;
55
+ flex-wrap: wrap;
56
+ gap: 1.25rem;
57
+ }
58
+ .option {
59
+ position: relative;
60
+ display: inline-flex;
61
+ align-items: flex-start;
62
+ gap: 0.6rem;
63
+ font-size: 0.875rem;
64
+ cursor: pointer;
65
+ }
66
+ input {
67
+ position: absolute;
68
+ opacity: 0;
69
+ width: 1.125rem;
70
+ height: 1.125rem;
71
+ margin: 0.1rem 0 0;
72
+ cursor: inherit;
73
+ }
74
+ .dot {
75
+ flex: none;
76
+ width: 1.125rem;
77
+ height: 1.125rem;
78
+ margin-top: 0.1rem;
79
+ box-sizing: border-box;
80
+ border-radius: 50%;
81
+ border: 1px solid var(--tern-border-strong);
82
+ background: var(--tern-surface);
83
+ transition:
84
+ border-width var(--tern-duration),
85
+ border-color var(--tern-duration);
86
+ }
87
+ input:checked + .dot {
88
+ border: 0.33rem solid var(--tern-accent);
89
+ }
90
+ input:focus-visible + .dot {
91
+ outline: 2px solid var(--tern-ring);
92
+ outline-offset: 2px;
93
+ }
94
+ input:disabled ~ * {
95
+ opacity: 0.55;
96
+ }
97
+ .text {
98
+ display: grid;
99
+ gap: 0.125rem;
100
+ }
101
+ .description {
102
+ color: var(--tern-muted);
103
+ font-size: 0.8125rem;
104
+ }
105
+ </style>
@@ -0,0 +1,21 @@
1
+ type Option = {
2
+ value: string;
3
+ label: string;
4
+ description?: string;
5
+ disabled?: boolean;
6
+ };
7
+ type Props = {
8
+ /** The group's label, read as the fieldset's legend. */
9
+ label: string;
10
+ name: string;
11
+ options: Option[];
12
+ value?: string;
13
+ /** Lay the options out in a row. */
14
+ inline?: boolean;
15
+ required?: boolean;
16
+ disabled?: boolean;
17
+ class?: string;
18
+ };
19
+ declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
20
+ type RadioGroup = ReturnType<typeof RadioGroup>;
21
+ export default RadioGroup;