@ternkit/tern 1.2.0 → 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,76 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLSelectAttributes } from 'svelte/elements';
4
+ import { getField } from './field.js';
5
+
6
+ type Option = { value: string; label: string; disabled?: boolean };
7
+ type Props = HTMLSelectAttributes & {
8
+ value?: unknown;
9
+ /** The options, or pass <option> elements as children. */
10
+ options?: Option[];
11
+ /** Shown, and not selectable, while nothing is chosen. */
12
+ placeholder?: string;
13
+ children?: Snippet;
14
+ };
15
+ let { value = $bindable(), options, placeholder, class: klass = '', children, ...rest }: Props = $props();
16
+ const field = getField();
17
+ </script>
18
+
19
+ <span class="tern-select {klass}">
20
+ <select
21
+ id={field?.id}
22
+ aria-describedby={field?.describedBy}
23
+ aria-invalid={field?.invalid || undefined}
24
+ required={field?.required}
25
+ disabled={field?.disabled}
26
+ {...rest}
27
+ bind:value
28
+ >
29
+ {#if placeholder}<option value="" disabled selected={value === undefined || value === ''}>{placeholder}</option>{/if}
30
+ {#each options ?? [] as o (o.value)}
31
+ <option value={o.value} disabled={o.disabled}>{o.label}</option>
32
+ {/each}
33
+ {@render children?.()}
34
+ </select>
35
+ <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>
36
+ </span>
37
+
38
+ <style>
39
+ .tern-select {
40
+ position: relative;
41
+ display: block;
42
+ }
43
+ select {
44
+ appearance: none;
45
+ width: 100%;
46
+ height: var(--tern-control-height);
47
+ padding: 0 2.25rem 0 0.75rem;
48
+ font: inherit;
49
+ font-size: 0.875rem;
50
+ color: var(--tern-fg);
51
+ background: var(--tern-surface);
52
+ border: 1px solid var(--tern-border-strong);
53
+ border-radius: var(--tern-radius-sm);
54
+ cursor: pointer;
55
+ }
56
+ select:focus-visible {
57
+ outline: none;
58
+ border-color: var(--tern-accent);
59
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
60
+ }
61
+ select[aria-invalid='true'] {
62
+ border-color: var(--tern-danger);
63
+ }
64
+ select:disabled {
65
+ opacity: 0.6;
66
+ cursor: not-allowed;
67
+ }
68
+ svg {
69
+ position: absolute;
70
+ right: 0.7rem;
71
+ top: 50%;
72
+ translate: 0 -50%;
73
+ pointer-events: none;
74
+ color: var(--tern-muted);
75
+ }
76
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLSelectAttributes } from 'svelte/elements';
3
+ type Option = {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ };
8
+ type Props = HTMLSelectAttributes & {
9
+ value?: unknown;
10
+ /** The options, or pass <option> elements as children. */
11
+ options?: Option[];
12
+ /** Shown, and not selectable, while nothing is chosen. */
13
+ placeholder?: string;
14
+ children?: Snippet;
15
+ };
16
+ declare const Select: import("svelte").Component<Props, {}, "value">;
17
+ type Select = ReturnType<typeof Select>;
18
+ export default Select;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ type Props = { vertical?: boolean; class?: string };
3
+ let { vertical = false, class: klass = '' }: Props = $props();
4
+ </script>
5
+
6
+ <div class="tern-separator {klass}" class:vertical role="separator" aria-orientation={vertical ? 'vertical' : 'horizontal'}></div>
7
+
8
+ <style>
9
+ .tern-separator {
10
+ height: 1px;
11
+ width: 100%;
12
+ background: var(--tern-border);
13
+ }
14
+ .vertical {
15
+ width: 1px;
16
+ height: auto;
17
+ align-self: stretch;
18
+ }
19
+ </style>
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ vertical?: boolean;
3
+ class?: string;
4
+ };
5
+ declare const Separator: import("svelte").Component<Props, {}, "">;
6
+ type Separator = ReturnType<typeof Separator>;
7
+ export default Separator;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ type Props = { width?: string; height?: string; radius?: string; class?: string };
3
+ let { width = '100%', height = '1rem', radius = 'var(--tern-radius-sm)', class: klass = '' }: Props = $props();
4
+ </script>
5
+
6
+ <!-- Decorative: pair it with a status message, such as aria-busy on the region it stands in for. -->
7
+ <span class="tern-skeleton {klass}" aria-hidden="true" style:width style:height style:border-radius={radius}></span>
8
+
9
+ <style>
10
+ .tern-skeleton {
11
+ display: block;
12
+ background: linear-gradient(90deg, var(--tern-surface-2) 25%, color-mix(in oklch, var(--tern-surface-2) 60%, var(--tern-border)) 50%, var(--tern-surface-2) 75%);
13
+ background-size: 200% 100%;
14
+ animation: shimmer 1.4s ease-in-out infinite;
15
+ }
16
+ @keyframes shimmer {
17
+ from {
18
+ background-position: 100% 0;
19
+ }
20
+ to {
21
+ background-position: -100% 0;
22
+ }
23
+ }
24
+ @media (prefers-reduced-motion: reduce) {
25
+ .tern-skeleton {
26
+ animation: none;
27
+ }
28
+ }
29
+ </style>
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ width?: string;
3
+ height?: string;
4
+ radius?: string;
5
+ class?: string;
6
+ };
7
+ declare const Skeleton: import("svelte").Component<Props, {}, "">;
8
+ type Skeleton = ReturnType<typeof Skeleton>;
9
+ export default Skeleton;
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ type Props = { label?: string; size?: string; class?: string };
3
+ let { label = 'Loading', size = '1.25rem', class: klass = '' }: Props = $props();
4
+ </script>
5
+
6
+ <span class="tern-spinner {klass}" role="status" style:--size={size}>
7
+ <span class="ring" aria-hidden="true"></span>
8
+ <span class="tern-sr-only">{label}</span>
9
+ </span>
10
+
11
+ <style>
12
+ .tern-spinner {
13
+ display: inline-flex;
14
+ color: var(--tern-accent);
15
+ }
16
+ .ring {
17
+ width: var(--size);
18
+ height: var(--size);
19
+ border-radius: 50%;
20
+ border: 2px solid color-mix(in oklch, currentColor 20%, transparent);
21
+ border-top-color: currentColor;
22
+ animation: spin 0.75s linear infinite;
23
+ }
24
+ .tern-sr-only {
25
+ position: absolute;
26
+ width: 1px;
27
+ height: 1px;
28
+ overflow: hidden;
29
+ clip: rect(0, 0, 0, 0);
30
+ white-space: nowrap;
31
+ }
32
+ @keyframes spin {
33
+ to {
34
+ transform: rotate(1turn);
35
+ }
36
+ }
37
+ @media (prefers-reduced-motion: reduce) {
38
+ .ring {
39
+ animation-duration: 2s;
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ label?: string;
3
+ size?: string;
4
+ class?: string;
5
+ };
6
+ declare const Spinner: import("svelte").Component<Props, {}, "">;
7
+ type Spinner = ReturnType<typeof Spinner>;
8
+ export default Spinner;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLButtonAttributes, 'role'> & {
6
+ checked?: boolean;
7
+ /** The label. */
8
+ children?: Snippet;
9
+ /** Submitted with a form as on/off under this name. */
10
+ name?: string;
11
+ };
12
+ let { checked = $bindable(false), children, name, disabled, class: klass = '', ...rest }: Props = $props();
13
+ const uid = $props.id();
14
+ </script>
15
+
16
+ <span class="tern-switch {klass}">
17
+ <button
18
+ type="button"
19
+ role="switch"
20
+ aria-checked={checked}
21
+ aria-labelledby={children ? `tern-switch-${uid}` : undefined}
22
+ {disabled}
23
+ {...rest}
24
+ onclick={(e) => {
25
+ checked = !checked;
26
+ rest.onclick?.(e as never);
27
+ }}
28
+ >
29
+ <span class="thumb" aria-hidden="true"></span>
30
+ </button>
31
+ {#if children}
32
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
33
+ <span class="label" id="tern-switch-{uid}" onclick={() => !disabled && (checked = !checked)}>{@render children()}</span>
34
+ {/if}
35
+ {#if name}<input type="hidden" {name} value={checked ? 'on' : 'off'} />{/if}
36
+ </span>
37
+
38
+ <style>
39
+ .tern-switch {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ gap: 0.6rem;
43
+ font-size: 0.875rem;
44
+ }
45
+ button {
46
+ position: relative;
47
+ flex: none;
48
+ width: 2.25rem;
49
+ height: 1.3rem;
50
+ padding: 0;
51
+ border-radius: 999px;
52
+ border: 1px solid var(--tern-border-strong);
53
+ background: var(--tern-surface-2);
54
+ cursor: pointer;
55
+ transition:
56
+ background-color var(--tern-duration),
57
+ border-color var(--tern-duration);
58
+ }
59
+ .thumb {
60
+ position: absolute;
61
+ top: 50%;
62
+ left: 0.14rem;
63
+ width: 0.95rem;
64
+ height: 0.95rem;
65
+ border-radius: 50%;
66
+ background: var(--tern-surface);
67
+ box-shadow: 0 1px 2px oklch(0 0 0 / 0.25);
68
+ translate: 0 -50%;
69
+ transition: left var(--tern-duration);
70
+ }
71
+ button[aria-checked='true'] {
72
+ background: var(--tern-accent);
73
+ border-color: var(--tern-accent);
74
+ }
75
+ button[aria-checked='true'] .thumb {
76
+ left: calc(100% - 0.14rem - 0.95rem);
77
+ }
78
+ button:focus-visible {
79
+ outline: 2px solid var(--tern-ring);
80
+ outline-offset: 2px;
81
+ }
82
+ button:disabled {
83
+ opacity: 0.55;
84
+ cursor: not-allowed;
85
+ }
86
+ .label {
87
+ cursor: pointer;
88
+ user-select: none;
89
+ }
90
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLButtonAttributes, 'role'> & {
4
+ checked?: boolean;
5
+ /** The label. */
6
+ children?: Snippet;
7
+ /** Submitted with a form as on/off under this name. */
8
+ name?: string;
9
+ };
10
+ declare const Switch: import("svelte").Component<Props, {}, "checked">;
11
+ type Switch = ReturnType<typeof Switch>;
12
+ export default Switch;
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Tab = { id: string; label: string; disabled?: boolean };
5
+ type Props = {
6
+ tabs: Tab[];
7
+ /** The selected tab's id; bind it. Defaults to the first tab. */
8
+ value?: string;
9
+ /** Names the tab list for screen readers. */
10
+ label?: string;
11
+ class?: string;
12
+ /** Renders the selected tab's panel. */
13
+ panel: Snippet<[string]>;
14
+ };
15
+ let { tabs, value = $bindable(), label, class: klass = '', panel }: Props = $props();
16
+ const uid = $props.id();
17
+ const selected = $derived(value ?? tabs.find((t) => !t.disabled)?.id);
18
+ const buttons: HTMLButtonElement[] = [];
19
+
20
+ // The ARIA tabs pattern: arrow keys move and select, Home and End jump.
21
+ function onkeydown(e: KeyboardEvent, i: number) {
22
+ const enabled = tabs.map((t, j) => (t.disabled ? -1 : j)).filter((j) => j >= 0);
23
+ const at = enabled.indexOf(i);
24
+ let next: number | undefined;
25
+ if (e.key === 'ArrowRight') next = enabled[(at + 1) % enabled.length];
26
+ else if (e.key === 'ArrowLeft') next = enabled[(at - 1 + enabled.length) % enabled.length];
27
+ else if (e.key === 'Home') next = enabled[0];
28
+ else if (e.key === 'End') next = enabled[enabled.length - 1];
29
+ if (next === undefined) return;
30
+ e.preventDefault();
31
+ value = tabs[next]!.id;
32
+ buttons[next]?.focus();
33
+ }
34
+ </script>
35
+
36
+ <div class="tern-tabs {klass}">
37
+ <div class="list" role="tablist" aria-label={label}>
38
+ {#each tabs as t, i (t.id)}
39
+ <button
40
+ bind:this={buttons[i]}
41
+ type="button"
42
+ role="tab"
43
+ id="tern-tab-{uid}-{t.id}"
44
+ aria-selected={selected === t.id}
45
+ aria-controls="tern-tabpanel-{uid}-{t.id}"
46
+ tabindex={selected === t.id ? 0 : -1}
47
+ disabled={t.disabled}
48
+ onclick={() => (value = t.id)}
49
+ onkeydown={(e) => onkeydown(e, i)}>{t.label}</button
50
+ >
51
+ {/each}
52
+ </div>
53
+ {#if selected}
54
+ <div class="panel" role="tabpanel" id="tern-tabpanel-{uid}-{selected}" aria-labelledby="tern-tab-{uid}-{selected}" tabindex="0">
55
+ {@render panel(selected)}
56
+ </div>
57
+ {/if}
58
+ </div>
59
+
60
+ <style>
61
+ .list {
62
+ display: flex;
63
+ gap: 0.25rem;
64
+ border-bottom: 1px solid var(--tern-border);
65
+ }
66
+ [role='tab'] {
67
+ position: relative;
68
+ padding: 0.55rem 0.8rem;
69
+ margin-bottom: -1px;
70
+ border: 0;
71
+ background: transparent;
72
+ font: inherit;
73
+ font-size: 0.875rem;
74
+ font-weight: 550;
75
+ color: var(--tern-muted);
76
+ cursor: pointer;
77
+ border-radius: var(--tern-radius-sm) var(--tern-radius-sm) 0 0;
78
+ transition: color var(--tern-duration);
79
+ }
80
+ [role='tab']:hover:not(:disabled) {
81
+ color: var(--tern-fg);
82
+ }
83
+ [role='tab'][aria-selected='true'] {
84
+ color: var(--tern-fg);
85
+ }
86
+ [role='tab'][aria-selected='true']::after {
87
+ content: '';
88
+ position: absolute;
89
+ left: 0.5rem;
90
+ right: 0.5rem;
91
+ bottom: 0;
92
+ height: 2px;
93
+ border-radius: 2px;
94
+ background: var(--tern-accent);
95
+ }
96
+ [role='tab']:focus-visible {
97
+ outline: 2px solid var(--tern-ring);
98
+ outline-offset: -2px;
99
+ }
100
+ [role='tab']:disabled {
101
+ opacity: 0.45;
102
+ cursor: not-allowed;
103
+ }
104
+ .panel {
105
+ padding-top: 1rem;
106
+ }
107
+ .panel:focus-visible {
108
+ outline: 2px solid var(--tern-ring);
109
+ outline-offset: 4px;
110
+ border-radius: 2px;
111
+ }
112
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Tab = {
3
+ id: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ };
7
+ type Props = {
8
+ tabs: Tab[];
9
+ /** The selected tab's id; bind it. Defaults to the first tab. */
10
+ value?: string;
11
+ /** Names the tab list for screen readers. */
12
+ label?: string;
13
+ class?: string;
14
+ /** Renders the selected tab's panel. */
15
+ panel: Snippet<[string]>;
16
+ };
17
+ declare const Tabs: import("svelte").Component<Props, {}, "value">;
18
+ type Tabs = ReturnType<typeof Tabs>;
19
+ export default Tabs;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
+ import { getField } from './field.js';
4
+
5
+ type Props = HTMLTextareaAttributes & { value?: unknown };
6
+ let { value = $bindable(), class: klass = '', rows = 4, ...rest }: Props = $props();
7
+ const field = getField();
8
+ </script>
9
+
10
+ <textarea
11
+ class="tern-textarea {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
+ {rows}
18
+ {...rest}
19
+ bind:value
20
+ ></textarea>
21
+
22
+ <style>
23
+ .tern-textarea {
24
+ width: 100%;
25
+ box-sizing: border-box;
26
+ padding: 0.5rem 0.75rem;
27
+ font: inherit;
28
+ font-size: 0.875rem;
29
+ line-height: 1.5;
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
+ resize: vertical;
35
+ transition:
36
+ border-color var(--tern-duration),
37
+ box-shadow var(--tern-duration);
38
+ }
39
+ .tern-textarea:focus-visible {
40
+ outline: none;
41
+ border-color: var(--tern-accent);
42
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
43
+ }
44
+ .tern-textarea[aria-invalid='true'] {
45
+ border-color: var(--tern-danger);
46
+ }
47
+ .tern-textarea:disabled {
48
+ opacity: 0.6;
49
+ cursor: not-allowed;
50
+ }
51
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
+ type Props = HTMLTextareaAttributes & {
3
+ value?: unknown;
4
+ };
5
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
6
+ type Textarea = ReturnType<typeof Textarea>;
7
+ export default Textarea;
@@ -0,0 +1,146 @@
1
+ <script lang="ts">
2
+ import { dismiss, toasts, type Toast } from './toast.svelte.js';
3
+
4
+ type Props = { position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' };
5
+ let { position = 'bottom-right' }: Props = $props();
6
+
7
+ // Each toast's timer pauses while it is hovered or focused.
8
+ function timed(node: HTMLElement, t: Toast) {
9
+ if (!t.duration) return;
10
+ let left = t.duration;
11
+ let started = Date.now();
12
+ let timer = setTimeout(() => dismiss(t.id), left);
13
+ const pause = () => {
14
+ clearTimeout(timer);
15
+ left -= Date.now() - started;
16
+ };
17
+ const resume = () => {
18
+ started = Date.now();
19
+ clearTimeout(timer);
20
+ timer = setTimeout(() => dismiss(t.id), Math.max(left, 1000));
21
+ };
22
+ node.addEventListener('pointerenter', pause);
23
+ node.addEventListener('pointerleave', resume);
24
+ node.addEventListener('focusin', pause);
25
+ node.addEventListener('focusout', resume);
26
+ return { destroy: () => clearTimeout(timer) };
27
+ }
28
+ const polite = $derived(toasts.filter((t) => t.tone !== 'danger'));
29
+ const urgent = $derived(toasts.filter((t) => t.tone === 'danger'));
30
+ </script>
31
+
32
+ {#snippet list(items: Toast[])}
33
+ {#each items as t (t.id)}
34
+ <li class="toast {t.tone}" use:timed={t}>
35
+ <div class="text">
36
+ <p class="message">{t.message}</p>
37
+ {#if t.description}<p class="description">{t.description}</p>{/if}
38
+ </div>
39
+ <button type="button" aria-label="Dismiss" onclick={() => dismiss(t.id)}>
40
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
41
+ </button>
42
+ </li>
43
+ {/each}
44
+ {/snippet}
45
+
46
+ <section class="tern-toaster {position}" aria-label="Notifications">
47
+ <!-- Errors are announced at once; the rest when the reader is idle. -->
48
+ <ol aria-live="assertive" aria-relevant="additions">{@render list(urgent)}</ol>
49
+ <ol aria-live="polite" aria-relevant="additions">{@render list(polite)}</ol>
50
+ </section>
51
+
52
+ <style>
53
+ .tern-toaster {
54
+ position: fixed;
55
+ z-index: 100;
56
+ display: grid;
57
+ gap: 0.5rem;
58
+ width: min(22rem, calc(100vw - 2rem));
59
+ pointer-events: none;
60
+ }
61
+ .bottom-right {
62
+ right: 1rem;
63
+ bottom: 1rem;
64
+ }
65
+ .bottom-left {
66
+ left: 1rem;
67
+ bottom: 1rem;
68
+ }
69
+ .top-right {
70
+ right: 1rem;
71
+ top: 1rem;
72
+ }
73
+ .top-left {
74
+ left: 1rem;
75
+ top: 1rem;
76
+ }
77
+ ol {
78
+ display: grid;
79
+ gap: 0.5rem;
80
+ margin: 0;
81
+ padding: 0;
82
+ list-style: none;
83
+ }
84
+ .toast {
85
+ display: flex;
86
+ align-items: flex-start;
87
+ gap: 0.75rem;
88
+ padding: 0.8rem 0.9rem;
89
+ background: var(--tern-surface);
90
+ color: var(--tern-fg);
91
+ border: 1px solid var(--tern-border);
92
+ border-left: 3px solid var(--tern-border-strong);
93
+ border-radius: var(--tern-radius);
94
+ box-shadow: var(--tern-shadow-lg);
95
+ pointer-events: auto;
96
+ animation: slide var(--tern-duration) ease-out;
97
+ }
98
+ .success {
99
+ border-left-color: var(--tern-success);
100
+ }
101
+ .danger {
102
+ border-left-color: var(--tern-danger);
103
+ }
104
+ .warning {
105
+ border-left-color: var(--tern-warning);
106
+ }
107
+ .text {
108
+ flex: 1;
109
+ min-width: 0;
110
+ }
111
+ p {
112
+ margin: 0;
113
+ }
114
+ .message {
115
+ font-size: 0.875rem;
116
+ font-weight: 550;
117
+ }
118
+ .description {
119
+ margin-top: 0.15rem;
120
+ font-size: 0.8125rem;
121
+ color: var(--tern-muted);
122
+ }
123
+ button {
124
+ display: grid;
125
+ place-items: center;
126
+ width: 1.5rem;
127
+ height: 1.5rem;
128
+ border: 0;
129
+ border-radius: var(--tern-radius-sm);
130
+ background: transparent;
131
+ color: var(--tern-muted);
132
+ cursor: pointer;
133
+ }
134
+ button:hover {
135
+ background: var(--tern-surface-2);
136
+ }
137
+ button:focus-visible {
138
+ outline: 2px solid var(--tern-ring);
139
+ }
140
+ @keyframes slide {
141
+ from {
142
+ opacity: 0;
143
+ transform: translateY(6px);
144
+ }
145
+ }
146
+ </style>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
3
+ };
4
+ declare const Toaster: import("svelte").Component<Props, {}, "">;
5
+ type Toaster = ReturnType<typeof Toaster>;
6
+ export default Toaster;