@ternkit/tern 1.2.1 → 1.3.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 (63) 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 +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/ui/Accordion.svelte +84 -0
  8. package/dist/ui/Accordion.svelte.d.ts +20 -0
  9. package/dist/ui/Alert.svelte +76 -0
  10. package/dist/ui/Alert.svelte.d.ts +12 -0
  11. package/dist/ui/Avatar.svelte +48 -0
  12. package/dist/ui/Avatar.svelte.d.ts +10 -0
  13. package/dist/ui/Badge.svelte +54 -0
  14. package/dist/ui/Badge.svelte.d.ts +10 -0
  15. package/dist/ui/Button.svelte +136 -0
  16. package/dist/ui/Button.svelte.d.ts +18 -0
  17. package/dist/ui/Card.svelte +74 -0
  18. package/dist/ui/Card.svelte.d.ts +18 -0
  19. package/dist/ui/Checkbox.svelte +100 -0
  20. package/dist/ui/Checkbox.svelte.d.ts +14 -0
  21. package/dist/ui/Dialog.svelte +160 -0
  22. package/dist/ui/Dialog.svelte.d.ts +20 -0
  23. package/dist/ui/Field.svelte +77 -0
  24. package/dist/ui/Field.svelte.d.ts +17 -0
  25. package/dist/ui/Input.svelte +61 -0
  26. package/dist/ui/Input.svelte.d.ts +7 -0
  27. package/dist/ui/Kbd.svelte +24 -0
  28. package/dist/ui/Kbd.svelte.d.ts +7 -0
  29. package/dist/ui/Menu.svelte +232 -0
  30. package/dist/ui/Menu.svelte.d.ts +22 -0
  31. package/dist/ui/Progress.svelte +73 -0
  32. package/dist/ui/Progress.svelte.d.ts +12 -0
  33. package/dist/ui/RadioGroup.svelte +105 -0
  34. package/dist/ui/RadioGroup.svelte.d.ts +21 -0
  35. package/dist/ui/Select.svelte +76 -0
  36. package/dist/ui/Select.svelte.d.ts +18 -0
  37. package/dist/ui/Separator.svelte +19 -0
  38. package/dist/ui/Separator.svelte.d.ts +7 -0
  39. package/dist/ui/Skeleton.svelte +29 -0
  40. package/dist/ui/Skeleton.svelte.d.ts +9 -0
  41. package/dist/ui/Spinner.svelte +42 -0
  42. package/dist/ui/Spinner.svelte.d.ts +8 -0
  43. package/dist/ui/Switch.svelte +90 -0
  44. package/dist/ui/Switch.svelte.d.ts +12 -0
  45. package/dist/ui/Tabs.svelte +112 -0
  46. package/dist/ui/Tabs.svelte.d.ts +19 -0
  47. package/dist/ui/Textarea.svelte +51 -0
  48. package/dist/ui/Textarea.svelte.d.ts +7 -0
  49. package/dist/ui/Toaster.svelte +146 -0
  50. package/dist/ui/Toaster.svelte.d.ts +6 -0
  51. package/dist/ui/Tooltip.svelte +77 -0
  52. package/dist/ui/Tooltip.svelte.d.ts +15 -0
  53. package/dist/ui/field.d.ts +10 -0
  54. package/dist/ui/field.js +8 -0
  55. package/dist/ui/index.d.ts +24 -0
  56. package/dist/ui/index.js +27 -0
  57. package/dist/ui/theme.css +151 -0
  58. package/dist/ui/toast.svelte.d.ts +19 -0
  59. package/dist/ui/toast.svelte.js +18 -0
  60. package/dist/vite/goscript.d.ts +14 -0
  61. package/dist/vite/goscript.js +45 -16
  62. package/dist/vite/index.js +5 -2
  63. package/package.json +7 -1
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { setField } from './field.js';
4
+
5
+ type Props = {
6
+ /** The visible label. */
7
+ label: string;
8
+ /** Help text under the control, read by screen readers with it. */
9
+ description?: string;
10
+ /** An error message; marks the control invalid and announces the message. */
11
+ error?: string | null;
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ class?: string;
15
+ /** The control, such as an Input, Textarea or Select. */
16
+ children: Snippet;
17
+ };
18
+ let { label, description, error = null, required = false, disabled = false, class: klass = '', children }: Props = $props();
19
+
20
+ const uid = $props.id();
21
+ const id = `tern-field-${uid}`;
22
+ setField({
23
+ id,
24
+ get describedBy() {
25
+ return [description ? `${id}-description` : '', error ? `${id}-error` : ''].filter(Boolean).join(' ') || undefined;
26
+ },
27
+ get invalid() {
28
+ return !!error;
29
+ },
30
+ get required() {
31
+ return required;
32
+ },
33
+ get disabled() {
34
+ return disabled;
35
+ }
36
+ });
37
+ </script>
38
+
39
+ <div class="tern-field {klass}" class:disabled>
40
+ <label for={id}>
41
+ {label}
42
+ {#if required}<span class="required" aria-hidden="true">*</span>{/if}
43
+ </label>
44
+ {@render children()}
45
+ {#if description}<p class="description" id="{id}-description">{description}</p>{/if}
46
+ {#if error}<p class="error" id="{id}-error" role="alert">{error}</p>{/if}
47
+ </div>
48
+
49
+ <style>
50
+ .tern-field {
51
+ display: grid;
52
+ gap: 0.375rem;
53
+ }
54
+ label {
55
+ font-size: 0.875rem;
56
+ font-weight: 550;
57
+ color: var(--tern-fg);
58
+ }
59
+ .required {
60
+ color: var(--tern-danger);
61
+ margin-inline-start: 0.125rem;
62
+ }
63
+ .description {
64
+ margin: 0;
65
+ font-size: 0.8125rem;
66
+ color: var(--tern-muted);
67
+ }
68
+ .error {
69
+ margin: 0;
70
+ font-size: 0.8125rem;
71
+ color: var(--tern-danger);
72
+ font-weight: 500;
73
+ }
74
+ .disabled label {
75
+ opacity: 0.6;
76
+ }
77
+ </style>
@@ -0,0 +1,17 @@
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
+ class?: string;
12
+ /** The control, such as an Input, Textarea or Select. */
13
+ children: Snippet;
14
+ };
15
+ declare const Field: import("svelte").Component<Props, {}, "">;
16
+ type Field = ReturnType<typeof Field>;
17
+ export default Field;
@@ -0,0 +1,61 @@
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
+ let { value = $bindable(), class: klass = '', ...rest }: Props = $props();
7
+ const field = getField();
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
+ {...rest}
18
+ bind:value
19
+ />
20
+
21
+ <style>
22
+ .tern-input {
23
+ width: 100%;
24
+ box-sizing: border-box;
25
+ height: var(--tern-control-height);
26
+ padding: 0 0.75rem;
27
+ font: inherit;
28
+ font-size: 0.875rem;
29
+ color: var(--tern-fg);
30
+ background: var(--tern-surface);
31
+ border: 1px solid var(--tern-border-strong);
32
+ border-radius: var(--tern-radius-sm);
33
+ box-shadow: inset 0 1px 1px oklch(0.24 0.02 240 / 0.04);
34
+ transition:
35
+ border-color var(--tern-duration),
36
+ box-shadow var(--tern-duration);
37
+ }
38
+ .tern-input::placeholder {
39
+ color: var(--tern-muted);
40
+ opacity: 0.8;
41
+ }
42
+ .tern-input:hover:not(:disabled) {
43
+ border-color: var(--tern-muted);
44
+ }
45
+ .tern-input:focus-visible {
46
+ outline: none;
47
+ border-color: var(--tern-accent);
48
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
49
+ }
50
+ .tern-input[aria-invalid='true'] {
51
+ border-color: var(--tern-danger);
52
+ }
53
+ .tern-input[aria-invalid='true']:focus-visible {
54
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-danger) 25%, transparent);
55
+ }
56
+ .tern-input:disabled {
57
+ opacity: 0.6;
58
+ cursor: not-allowed;
59
+ background: var(--tern-surface-2);
60
+ }
61
+ </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,24 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ let { children }: { children: Snippet } = $props();
4
+ </script>
5
+
6
+ <kbd class="tern-kbd">{@render children()}</kbd>
7
+
8
+ <style>
9
+ .tern-kbd {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ min-width: 1.4em;
13
+ height: 1.4em;
14
+ padding: 0 0.35em;
15
+ justify-content: center;
16
+ border: 1px solid var(--tern-border-strong);
17
+ border-bottom-width: 2px;
18
+ border-radius: 5px;
19
+ background: var(--tern-surface);
20
+ font-family: var(--tern-font-mono);
21
+ font-size: 0.75em;
22
+ color: var(--tern-fg);
23
+ }
24
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ children: Snippet;
4
+ };
5
+ declare const Kbd: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Kbd = ReturnType<typeof Kbd>;
7
+ export default Kbd;
@@ -0,0 +1,232 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Item = { label: string; onselect?: () => void; href?: string; danger?: boolean; disabled?: boolean; separator?: boolean };
5
+ type Props = {
6
+ /** The items; an item with separator: true draws a line. */
7
+ items: Item[];
8
+ /** The button's content. */
9
+ children: Snippet;
10
+ /** Opens to the start or end edge of the button. */
11
+ align?: 'start' | 'end';
12
+ variant?: 'secondary' | 'ghost';
13
+ class?: string;
14
+ };
15
+ let { items, children, align = 'start', variant = 'secondary', class: klass = '' }: Props = $props();
16
+
17
+ const uid = $props.id();
18
+ let open = $state(false);
19
+ let button = $state<HTMLButtonElement>();
20
+ let menu = $state<HTMLDivElement>();
21
+ let typed = '';
22
+ let typedTimer: ReturnType<typeof setTimeout> | undefined;
23
+
24
+ const entries = () => Array.from(menu?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])') ?? []);
25
+
26
+ async function show(focus: 'first' | 'last') {
27
+ open = true;
28
+ await Promise.resolve();
29
+ requestAnimationFrame(() => {
30
+ const list = entries();
31
+ (focus === 'first' ? list[0] : list[list.length - 1])?.focus();
32
+ });
33
+ }
34
+
35
+ function hide(returnFocus = true) {
36
+ open = false;
37
+ if (returnFocus) button?.focus();
38
+ }
39
+
40
+ function select(item: Item) {
41
+ if (item.disabled) return;
42
+ hide();
43
+ item.onselect?.();
44
+ }
45
+
46
+ // The ARIA menu button pattern.
47
+ function onmenukey(e: KeyboardEvent) {
48
+ const list = entries();
49
+ const i = list.indexOf(document.activeElement as HTMLElement);
50
+ const move = (j: number) => {
51
+ e.preventDefault();
52
+ list[(j + list.length) % list.length]?.focus();
53
+ };
54
+ switch (e.key) {
55
+ case 'ArrowDown':
56
+ return move(i + 1);
57
+ case 'ArrowUp':
58
+ return move(i - 1);
59
+ case 'Home':
60
+ return move(0);
61
+ case 'End':
62
+ return move(list.length - 1);
63
+ case 'Escape':
64
+ e.preventDefault();
65
+ return hide();
66
+ case 'Tab':
67
+ return hide(false);
68
+ }
69
+ if (e.key.length === 1 && /\S/.test(e.key)) {
70
+ // Type to jump to an item.
71
+ typed += e.key.toLowerCase();
72
+ clearTimeout(typedTimer);
73
+ typedTimer = setTimeout(() => (typed = ''), 500);
74
+ const start = typed.length === 1 ? i + 1 : i;
75
+ const order = [...list.slice(start), ...list.slice(0, start)];
76
+ order.find((el) => el.textContent?.trim().toLowerCase().startsWith(typed))?.focus();
77
+ }
78
+ }
79
+
80
+ $effect(() => {
81
+ if (!open) return;
82
+ const away = (e: PointerEvent) => {
83
+ if (!menu?.contains(e.target as Node) && !button?.contains(e.target as Node)) hide(false);
84
+ };
85
+ document.addEventListener('pointerdown', away);
86
+ return () => document.removeEventListener('pointerdown', away);
87
+ });
88
+ </script>
89
+
90
+ <div class="tern-menu {klass}">
91
+ <button
92
+ bind:this={button}
93
+ type="button"
94
+ class="trigger {variant}"
95
+ id="tern-menu-{uid}-button"
96
+ aria-haspopup="menu"
97
+ aria-expanded={open}
98
+ aria-controls={open ? `tern-menu-${uid}` : undefined}
99
+ onclick={() => (open ? hide() : show('first'))}
100
+ onkeydown={(e) => {
101
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
102
+ e.preventDefault();
103
+ show('first');
104
+ } else if (e.key === 'ArrowUp') {
105
+ e.preventDefault();
106
+ show('last');
107
+ }
108
+ }}
109
+ >
110
+ {@render children()}
111
+ <svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
112
+ </button>
113
+ {#if open}
114
+ <div bind:this={menu} class="menu {align}" role="menu" id="tern-menu-{uid}" aria-labelledby="tern-menu-{uid}-button" tabindex="-1" onkeydown={onmenukey}>
115
+ {#each items as item, i (i)}
116
+ {#if item.separator}
117
+ <div class="separator" role="separator"></div>
118
+ {:else if item.href && !item.disabled}
119
+ <a role="menuitem" tabindex="-1" class:danger={item.danger} href={item.href} onclick={() => hide(false)}>{item.label}</a>
120
+ {:else}
121
+ <div
122
+ role="menuitem"
123
+ tabindex="-1"
124
+ class:danger={item.danger}
125
+ aria-disabled={item.disabled || undefined}
126
+ onclick={() => select(item)}
127
+ onkeydown={(e) => {
128
+ if (e.key === 'Enter' || e.key === ' ') {
129
+ e.preventDefault();
130
+ select(item);
131
+ }
132
+ }}
133
+ >
134
+ {item.label}
135
+ </div>
136
+ {/if}
137
+ {/each}
138
+ </div>
139
+ {/if}
140
+ </div>
141
+
142
+ <style>
143
+ .tern-menu {
144
+ position: relative;
145
+ display: inline-block;
146
+ }
147
+ .trigger {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ gap: 0.4rem;
151
+ height: var(--tern-control-height);
152
+ padding: 0 0.75rem 0 0.9rem;
153
+ font: inherit;
154
+ font-size: 0.875rem;
155
+ font-weight: 550;
156
+ color: var(--tern-fg);
157
+ border-radius: var(--tern-radius-sm);
158
+ cursor: pointer;
159
+ }
160
+ .secondary {
161
+ background: var(--tern-surface);
162
+ border: 1px solid var(--tern-border-strong);
163
+ box-shadow: var(--tern-shadow);
164
+ }
165
+ .ghost {
166
+ background: transparent;
167
+ border: 1px solid transparent;
168
+ }
169
+ .trigger:hover,
170
+ .trigger[aria-expanded='true'] {
171
+ background: var(--tern-surface-2);
172
+ }
173
+ .trigger:focus-visible {
174
+ outline: 2px solid var(--tern-ring);
175
+ outline-offset: 2px;
176
+ }
177
+ .menu {
178
+ position: absolute;
179
+ top: calc(100% + 0.375rem);
180
+ z-index: 50;
181
+ min-width: 11rem;
182
+ padding: 0.3rem;
183
+ background: var(--tern-surface);
184
+ border: 1px solid var(--tern-border);
185
+ border-radius: var(--tern-radius);
186
+ box-shadow: var(--tern-shadow-lg);
187
+ animation: pop var(--tern-duration) ease-out;
188
+ }
189
+ .start {
190
+ left: 0;
191
+ }
192
+ .end {
193
+ right: 0;
194
+ }
195
+ [role='menuitem'] {
196
+ display: block;
197
+ padding: 0.45rem 0.65rem;
198
+ border-radius: calc(var(--tern-radius-sm) - 2px);
199
+ font-size: 0.875rem;
200
+ color: var(--tern-fg);
201
+ text-decoration: none;
202
+ cursor: pointer;
203
+ outline: none;
204
+ }
205
+ [role='menuitem']:hover,
206
+ [role='menuitem']:focus {
207
+ background: var(--tern-accent-soft);
208
+ }
209
+ [role='menuitem'].danger {
210
+ color: var(--tern-danger);
211
+ }
212
+ [role='menuitem'].danger:hover,
213
+ [role='menuitem'].danger:focus {
214
+ background: var(--tern-danger-soft);
215
+ }
216
+ [role='menuitem'][aria-disabled='true'] {
217
+ opacity: 0.45;
218
+ cursor: not-allowed;
219
+ background: none;
220
+ }
221
+ .separator {
222
+ height: 1px;
223
+ margin: 0.3rem 0.2rem;
224
+ background: var(--tern-border);
225
+ }
226
+ @keyframes pop {
227
+ from {
228
+ opacity: 0;
229
+ transform: translateY(-4px);
230
+ }
231
+ }
232
+ </style>
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Item = {
3
+ label: string;
4
+ onselect?: () => void;
5
+ href?: string;
6
+ danger?: boolean;
7
+ disabled?: boolean;
8
+ separator?: boolean;
9
+ };
10
+ type Props = {
11
+ /** The items; an item with separator: true draws a line. */
12
+ items: Item[];
13
+ /** The button's content. */
14
+ children: Snippet;
15
+ /** Opens to the start or end edge of the button. */
16
+ align?: 'start' | 'end';
17
+ variant?: 'secondary' | 'ghost';
18
+ class?: string;
19
+ };
20
+ declare const Menu: import("svelte").Component<Props, {}, "">;
21
+ type Menu = ReturnType<typeof Menu>;
22
+ export default Menu;
@@ -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;