@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.
- package/dist/goscript/index.svelte.d.ts +32 -2
- package/dist/goscript/index.svelte.js +257 -31
- package/dist/goscript/patch.d.ts +6 -0
- package/dist/goscript/patch.js +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ui/Accordion.svelte +84 -0
- package/dist/ui/Accordion.svelte.d.ts +20 -0
- package/dist/ui/Alert.svelte +76 -0
- package/dist/ui/Alert.svelte.d.ts +12 -0
- package/dist/ui/Avatar.svelte +48 -0
- package/dist/ui/Avatar.svelte.d.ts +10 -0
- package/dist/ui/Badge.svelte +54 -0
- package/dist/ui/Badge.svelte.d.ts +10 -0
- package/dist/ui/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -0
- package/dist/ui/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -0
- package/dist/ui/Checkbox.svelte +100 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- package/dist/ui/Dialog.svelte +160 -0
- package/dist/ui/Dialog.svelte.d.ts +20 -0
- package/dist/ui/Field.svelte +77 -0
- package/dist/ui/Field.svelte.d.ts +17 -0
- package/dist/ui/Input.svelte +61 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- package/dist/ui/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -0
- package/dist/ui/Menu.svelte +232 -0
- package/dist/ui/Menu.svelte.d.ts +22 -0
- package/dist/ui/Progress.svelte +73 -0
- package/dist/ui/Progress.svelte.d.ts +12 -0
- package/dist/ui/RadioGroup.svelte +105 -0
- package/dist/ui/RadioGroup.svelte.d.ts +21 -0
- package/dist/ui/Select.svelte +76 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -0
- package/dist/ui/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -0
- package/dist/ui/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +51 -0
- package/dist/ui/Textarea.svelte.d.ts +7 -0
- package/dist/ui/Toaster.svelte +146 -0
- package/dist/ui/Toaster.svelte.d.ts +6 -0
- package/dist/ui/Tooltip.svelte +77 -0
- package/dist/ui/Tooltip.svelte.d.ts +15 -0
- package/dist/ui/field.d.ts +10 -0
- package/dist/ui/field.js +8 -0
- package/dist/ui/index.d.ts +24 -0
- package/dist/ui/index.js +27 -0
- package/dist/ui/theme.css +151 -0
- package/dist/ui/toast.svelte.d.ts +19 -0
- package/dist/ui/toast.svelte.js +18 -0
- package/dist/vite/goscript.d.ts +14 -0
- package/dist/vite/goscript.js +45 -16
- package/dist/vite/index.js +5 -2
- package/package.json +7 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Props = {
|
|
3
|
+
/** The person's name: the image's alt text, and the initials when there is no image. */
|
|
4
|
+
name: string;
|
|
5
|
+
src?: string | null;
|
|
6
|
+
size?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
};
|
|
9
|
+
let { name, src = null, size = '2.25rem', class: klass = '' }: Props = $props();
|
|
10
|
+
let failed = $state(false);
|
|
11
|
+
const initials = $derived(
|
|
12
|
+
name
|
|
13
|
+
.split(/\s+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.slice(0, 2)
|
|
16
|
+
.map((w) => w[0]!.toUpperCase())
|
|
17
|
+
.join('')
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<span class="tern-avatar {klass}" style:--size={size}>
|
|
22
|
+
{#if src && !failed}
|
|
23
|
+
<img {src} alt={name} onerror={() => (failed = true)} />
|
|
24
|
+
{:else}
|
|
25
|
+
<span role="img" aria-label={name}>{initials}</span>
|
|
26
|
+
{/if}
|
|
27
|
+
</span>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.tern-avatar {
|
|
31
|
+
display: inline-grid;
|
|
32
|
+
place-items: center;
|
|
33
|
+
width: var(--size);
|
|
34
|
+
height: var(--size);
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
background: var(--tern-accent-soft);
|
|
38
|
+
color: var(--tern-accent);
|
|
39
|
+
font-size: calc(var(--size) * 0.38);
|
|
40
|
+
font-weight: 650;
|
|
41
|
+
flex: none;
|
|
42
|
+
}
|
|
43
|
+
img {
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
object-fit: cover;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** The person's name: the image's alt text, and the initials when there is no image. */
|
|
3
|
+
name: string;
|
|
4
|
+
src?: string | null;
|
|
5
|
+
size?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
10
|
+
export default Avatar;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = { tone?: 'neutral' | 'accent' | 'success' | 'danger' | 'warning'; dot?: boolean; class?: string; children: Snippet };
|
|
5
|
+
let { tone = 'neutral', dot = false, class: klass = '', children }: Props = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<span class="tern-badge {tone} {klass}">
|
|
9
|
+
{#if dot}<span class="dot" aria-hidden="true"></span>{/if}
|
|
10
|
+
{@render children()}
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
.tern-badge {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 0.35rem;
|
|
18
|
+
height: 1.375rem;
|
|
19
|
+
padding: 0 0.55rem;
|
|
20
|
+
border-radius: 999px;
|
|
21
|
+
font-size: 0.75rem;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
line-height: 1;
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
border: 1px solid transparent;
|
|
26
|
+
}
|
|
27
|
+
.dot {
|
|
28
|
+
width: 0.4rem;
|
|
29
|
+
height: 0.4rem;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
background: currentColor;
|
|
32
|
+
}
|
|
33
|
+
.neutral {
|
|
34
|
+
background: var(--tern-surface-2);
|
|
35
|
+
color: var(--tern-fg);
|
|
36
|
+
border-color: var(--tern-border);
|
|
37
|
+
}
|
|
38
|
+
.accent {
|
|
39
|
+
background: var(--tern-accent-soft);
|
|
40
|
+
color: var(--tern-accent);
|
|
41
|
+
}
|
|
42
|
+
.success {
|
|
43
|
+
background: var(--tern-success-soft);
|
|
44
|
+
color: color-mix(in oklch, var(--tern-success) 80%, var(--tern-fg));
|
|
45
|
+
}
|
|
46
|
+
.danger {
|
|
47
|
+
background: var(--tern-danger-soft);
|
|
48
|
+
color: color-mix(in oklch, var(--tern-danger) 85%, var(--tern-fg));
|
|
49
|
+
}
|
|
50
|
+
.warning {
|
|
51
|
+
background: var(--tern-warning-soft);
|
|
52
|
+
color: color-mix(in oklch, var(--tern-warning) 55%, var(--tern-fg));
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
tone?: 'neutral' | 'accent' | 'success' | 'danger' | 'warning';
|
|
4
|
+
dot?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Badge: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Badge = ReturnType<typeof Badge>;
|
|
10
|
+
export default Badge;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Common = {
|
|
6
|
+
/** primary for the main action, secondary for the others, ghost for toolbars, danger for destructive actions */
|
|
7
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
/** Shows a spinner and blocks clicks, while staying focusable. */
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
};
|
|
13
|
+
type Props = Common & ((HTMLButtonAttributes & { href?: undefined }) | (HTMLAnchorAttributes & { href: string }));
|
|
14
|
+
|
|
15
|
+
let { variant = 'primary', size = 'md', loading = false, children, class: klass = '', ...rest }: Props = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if rest.href !== undefined}
|
|
19
|
+
<a class="tern-btn {variant} {size} {klass}" {...rest as HTMLAnchorAttributes}>{@render children?.()}</a>
|
|
20
|
+
{:else}
|
|
21
|
+
<button
|
|
22
|
+
class="tern-btn {variant} {size} {klass}"
|
|
23
|
+
type="button"
|
|
24
|
+
{...rest as HTMLButtonAttributes}
|
|
25
|
+
aria-busy={loading || undefined}
|
|
26
|
+
aria-disabled={loading || (rest as HTMLButtonAttributes).disabled || undefined}
|
|
27
|
+
onclick={(e) => {
|
|
28
|
+
if (loading) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
(rest as HTMLButtonAttributes).onclick?.(e as never);
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{#if loading}<span class="spinner" aria-hidden="true"></span>{/if}
|
|
36
|
+
{@render children?.()}
|
|
37
|
+
</button>
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.tern-btn {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
gap: 0.5em;
|
|
46
|
+
height: var(--tern-control-height);
|
|
47
|
+
padding: 0 1rem;
|
|
48
|
+
border-radius: var(--tern-radius-sm);
|
|
49
|
+
border: 1px solid transparent;
|
|
50
|
+
font: inherit;
|
|
51
|
+
font-size: 0.875rem;
|
|
52
|
+
font-weight: 550;
|
|
53
|
+
line-height: 1;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
user-select: none;
|
|
58
|
+
transition:
|
|
59
|
+
background-color var(--tern-duration),
|
|
60
|
+
border-color var(--tern-duration),
|
|
61
|
+
box-shadow var(--tern-duration),
|
|
62
|
+
transform var(--tern-duration);
|
|
63
|
+
}
|
|
64
|
+
.tern-btn:focus-visible {
|
|
65
|
+
outline: 2px solid var(--tern-ring);
|
|
66
|
+
outline-offset: 2px;
|
|
67
|
+
}
|
|
68
|
+
.tern-btn:active:not([aria-disabled='true']) {
|
|
69
|
+
transform: translateY(0.5px);
|
|
70
|
+
}
|
|
71
|
+
.tern-btn[aria-disabled='true'],
|
|
72
|
+
.tern-btn:disabled {
|
|
73
|
+
opacity: 0.55;
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
}
|
|
76
|
+
.tern-btn[aria-busy='true'] {
|
|
77
|
+
cursor: progress;
|
|
78
|
+
}
|
|
79
|
+
.sm {
|
|
80
|
+
height: calc(var(--tern-control-height) - 0.5rem);
|
|
81
|
+
padding: 0 0.7rem;
|
|
82
|
+
font-size: 0.8125rem;
|
|
83
|
+
}
|
|
84
|
+
.lg {
|
|
85
|
+
height: calc(var(--tern-control-height) + 0.5rem);
|
|
86
|
+
padding: 0 1.35rem;
|
|
87
|
+
font-size: 0.9375rem;
|
|
88
|
+
}
|
|
89
|
+
.primary {
|
|
90
|
+
background: var(--tern-accent);
|
|
91
|
+
color: var(--tern-accent-fg);
|
|
92
|
+
box-shadow:
|
|
93
|
+
var(--tern-shadow),
|
|
94
|
+
inset 0 1px 0 oklch(1 0 0 / 0.14);
|
|
95
|
+
}
|
|
96
|
+
.primary:hover:not([aria-disabled='true']) {
|
|
97
|
+
background: var(--tern-accent-hover);
|
|
98
|
+
}
|
|
99
|
+
.secondary {
|
|
100
|
+
background: var(--tern-surface);
|
|
101
|
+
color: var(--tern-fg);
|
|
102
|
+
border-color: var(--tern-border-strong);
|
|
103
|
+
box-shadow: var(--tern-shadow);
|
|
104
|
+
}
|
|
105
|
+
.secondary:hover:not([aria-disabled='true']) {
|
|
106
|
+
background: var(--tern-surface-2);
|
|
107
|
+
}
|
|
108
|
+
.ghost {
|
|
109
|
+
background: transparent;
|
|
110
|
+
color: var(--tern-fg);
|
|
111
|
+
}
|
|
112
|
+
.ghost:hover:not([aria-disabled='true']) {
|
|
113
|
+
background: var(--tern-surface-2);
|
|
114
|
+
}
|
|
115
|
+
.danger {
|
|
116
|
+
background: var(--tern-danger);
|
|
117
|
+
color: var(--tern-danger-fg);
|
|
118
|
+
box-shadow: var(--tern-shadow);
|
|
119
|
+
}
|
|
120
|
+
.danger:hover:not([aria-disabled='true']) {
|
|
121
|
+
background: var(--tern-danger-hover);
|
|
122
|
+
}
|
|
123
|
+
.spinner {
|
|
124
|
+
width: 1em;
|
|
125
|
+
height: 1em;
|
|
126
|
+
border-radius: 50%;
|
|
127
|
+
border: 2px solid currentColor;
|
|
128
|
+
border-right-color: transparent;
|
|
129
|
+
animation: spin 0.7s linear infinite;
|
|
130
|
+
}
|
|
131
|
+
@keyframes spin {
|
|
132
|
+
to {
|
|
133
|
+
transform: rotate(1turn);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type Common = {
|
|
4
|
+
/** primary for the main action, secondary for the others, ghost for toolbars, danger for destructive actions */
|
|
5
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
/** Shows a spinner and blocks clicks, while staying focusable. */
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
};
|
|
11
|
+
type Props = Common & ((HTMLButtonAttributes & {
|
|
12
|
+
href?: undefined;
|
|
13
|
+
}) | (HTMLAnchorAttributes & {
|
|
14
|
+
href: string;
|
|
15
|
+
}));
|
|
16
|
+
declare const Button: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type Button = ReturnType<typeof Button>;
|
|
18
|
+
export default Button;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<HTMLElement> & {
|
|
6
|
+
/** A heading for the card. */
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** The heading level, to fit the page's outline. */
|
|
10
|
+
level?: 2 | 3 | 4;
|
|
11
|
+
/** The element to render: section for standalone content, article for items in a feed. */
|
|
12
|
+
as?: 'div' | 'section' | 'article';
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
/** Actions in the header's corner. */
|
|
15
|
+
actions?: Snippet;
|
|
16
|
+
footer?: Snippet;
|
|
17
|
+
};
|
|
18
|
+
let { title, description, level = 3, as = 'div', class: klass = '', children, actions, footer, ...rest }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<svelte:element this={as} class="tern-card {klass}" {...rest}>
|
|
22
|
+
{#if title || actions}
|
|
23
|
+
<header>
|
|
24
|
+
<div>
|
|
25
|
+
{#if title}<svelte:element this={`h${level}`} class="title">{title}</svelte:element>{/if}
|
|
26
|
+
{#if description}<p class="description">{description}</p>{/if}
|
|
27
|
+
</div>
|
|
28
|
+
{#if actions}<div class="actions">{@render actions()}</div>{/if}
|
|
29
|
+
</header>
|
|
30
|
+
{/if}
|
|
31
|
+
{#if children}<div class="body">{@render children()}</div>{/if}
|
|
32
|
+
{#if footer}<footer>{@render footer()}</footer>{/if}
|
|
33
|
+
</svelte:element>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.tern-card {
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: 1rem;
|
|
39
|
+
padding: 1.25rem;
|
|
40
|
+
background: var(--tern-surface);
|
|
41
|
+
color: var(--tern-fg);
|
|
42
|
+
border: 1px solid var(--tern-border);
|
|
43
|
+
border-radius: calc(var(--tern-radius) + 2px);
|
|
44
|
+
box-shadow: var(--tern-shadow);
|
|
45
|
+
}
|
|
46
|
+
header {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
gap: 1rem;
|
|
51
|
+
}
|
|
52
|
+
.title {
|
|
53
|
+
margin: 0;
|
|
54
|
+
font-size: 1rem;
|
|
55
|
+
font-weight: 650;
|
|
56
|
+
letter-spacing: -0.01em;
|
|
57
|
+
}
|
|
58
|
+
.description {
|
|
59
|
+
margin: 0.2rem 0 0;
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
color: var(--tern-muted);
|
|
62
|
+
}
|
|
63
|
+
.actions {
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: 0.5rem;
|
|
66
|
+
}
|
|
67
|
+
footer {
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: flex-end;
|
|
70
|
+
gap: 0.5rem;
|
|
71
|
+
padding-top: 1rem;
|
|
72
|
+
border-top: 1px solid var(--tern-border);
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = HTMLAttributes<HTMLElement> & {
|
|
4
|
+
/** A heading for the card. */
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
/** The heading level, to fit the page's outline. */
|
|
8
|
+
level?: 2 | 3 | 4;
|
|
9
|
+
/** The element to render: section for standalone content, article for items in a feed. */
|
|
10
|
+
as?: 'div' | 'section' | 'article';
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
/** Actions in the header's corner. */
|
|
13
|
+
actions?: Snippet;
|
|
14
|
+
footer?: Snippet;
|
|
15
|
+
};
|
|
16
|
+
declare const Card: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type Card = ReturnType<typeof Card>;
|
|
18
|
+
export default Card;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = Omit<HTMLInputAttributes, 'type'> & {
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
/** Shows a dash, for "some selected". */
|
|
8
|
+
indeterminate?: boolean;
|
|
9
|
+
/** The label. */
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
/** Help text under the label. */
|
|
12
|
+
description?: string;
|
|
13
|
+
};
|
|
14
|
+
let { checked = $bindable(false), indeterminate = $bindable(false), children, description, class: klass = '', ...rest }: Props = $props();
|
|
15
|
+
const uid = $props.id();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<label class="tern-check {klass}">
|
|
19
|
+
<input type="checkbox" {...rest} bind:checked bind:indeterminate aria-describedby={description ? `tern-check-${uid}` : undefined} />
|
|
20
|
+
<span class="box" aria-hidden="true">
|
|
21
|
+
<svg viewBox="0 0 16 16"><path class="tick" d="M3.5 8.5l3 3 6-7" /><path class="dash" d="M4 8h8" /></svg>
|
|
22
|
+
</span>
|
|
23
|
+
<span class="text">
|
|
24
|
+
{@render children?.()}
|
|
25
|
+
{#if description}<span class="description" id="tern-check-{uid}">{description}</span>{/if}
|
|
26
|
+
</span>
|
|
27
|
+
</label>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.tern-check {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: flex-start;
|
|
33
|
+
gap: 0.6rem;
|
|
34
|
+
font-size: 0.875rem;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
position: relative;
|
|
37
|
+
}
|
|
38
|
+
input {
|
|
39
|
+
position: absolute;
|
|
40
|
+
opacity: 0;
|
|
41
|
+
width: 1.125rem;
|
|
42
|
+
height: 1.125rem;
|
|
43
|
+
margin: 0.1rem 0 0;
|
|
44
|
+
cursor: inherit;
|
|
45
|
+
}
|
|
46
|
+
.box {
|
|
47
|
+
flex: none;
|
|
48
|
+
display: grid;
|
|
49
|
+
place-items: center;
|
|
50
|
+
width: 1.125rem;
|
|
51
|
+
height: 1.125rem;
|
|
52
|
+
margin-top: 0.1rem;
|
|
53
|
+
border: 1px solid var(--tern-border-strong);
|
|
54
|
+
border-radius: 5px;
|
|
55
|
+
background: var(--tern-surface);
|
|
56
|
+
transition:
|
|
57
|
+
background-color var(--tern-duration),
|
|
58
|
+
border-color var(--tern-duration);
|
|
59
|
+
}
|
|
60
|
+
svg {
|
|
61
|
+
width: 0.8rem;
|
|
62
|
+
height: 0.8rem;
|
|
63
|
+
fill: none;
|
|
64
|
+
stroke: var(--tern-accent-fg);
|
|
65
|
+
stroke-width: 2.2;
|
|
66
|
+
stroke-linecap: round;
|
|
67
|
+
stroke-linejoin: round;
|
|
68
|
+
}
|
|
69
|
+
.tick,
|
|
70
|
+
.dash {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
input:checked + .box,
|
|
74
|
+
input:indeterminate + .box {
|
|
75
|
+
background: var(--tern-accent);
|
|
76
|
+
border-color: var(--tern-accent);
|
|
77
|
+
}
|
|
78
|
+
input:checked:not(:indeterminate) + .box .tick,
|
|
79
|
+
input:indeterminate + .box .dash {
|
|
80
|
+
display: block;
|
|
81
|
+
}
|
|
82
|
+
input:focus-visible + .box {
|
|
83
|
+
outline: 2px solid var(--tern-ring);
|
|
84
|
+
outline-offset: 2px;
|
|
85
|
+
}
|
|
86
|
+
input:disabled ~ * {
|
|
87
|
+
opacity: 0.55;
|
|
88
|
+
}
|
|
89
|
+
.tern-check:has(input:disabled) {
|
|
90
|
+
cursor: not-allowed;
|
|
91
|
+
}
|
|
92
|
+
.text {
|
|
93
|
+
display: grid;
|
|
94
|
+
gap: 0.125rem;
|
|
95
|
+
}
|
|
96
|
+
.description {
|
|
97
|
+
color: var(--tern-muted);
|
|
98
|
+
font-size: 0.8125rem;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<HTMLInputAttributes, 'type'> & {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
/** Shows a dash, for "some selected". */
|
|
6
|
+
indeterminate?: boolean;
|
|
7
|
+
/** The label. */
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
/** Help text under the label. */
|
|
10
|
+
description?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const Checkbox: import("svelte").Component<Props, {}, "checked" | "indeterminate">;
|
|
13
|
+
type Checkbox = ReturnType<typeof Checkbox>;
|
|
14
|
+
export default Checkbox;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** Whether the dialog is open; bind it. */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** The heading, which labels the dialog. */
|
|
8
|
+
title: string;
|
|
9
|
+
/** A sentence under the title, read when the dialog opens. */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Close when the backdrop is clicked. Escape always closes. */
|
|
12
|
+
dismissable?: boolean;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
class?: string;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
/** Buttons at the bottom, such as Cancel and Save. */
|
|
17
|
+
footer?: Snippet;
|
|
18
|
+
onclose?: () => void;
|
|
19
|
+
};
|
|
20
|
+
let { open = $bindable(false), title, description, dismissable = true, size = 'md', class: klass = '', children, footer, onclose }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const uid = $props.id();
|
|
23
|
+
let dialog = $state<HTMLDialogElement>();
|
|
24
|
+
|
|
25
|
+
// The native dialog traps focus, makes the page inert, closes on Escape
|
|
26
|
+
// and returns focus to what opened it.
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (!dialog) return;
|
|
29
|
+
if (open && !dialog.open) dialog.showModal();
|
|
30
|
+
else if (!open && dialog.open) dialog.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Tab cycles inside the dialog instead of leaving for the browser's UI.
|
|
34
|
+
function onkeydown(e: KeyboardEvent) {
|
|
35
|
+
if (e.key !== 'Tab' || !dialog) return;
|
|
36
|
+
const focusable = Array.from(
|
|
37
|
+
dialog.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')
|
|
38
|
+
).filter((el) => el.offsetParent !== null || el === document.activeElement);
|
|
39
|
+
if (focusable.length === 0) return;
|
|
40
|
+
const first = focusable[0]!;
|
|
41
|
+
const last = focusable[focusable.length - 1]!;
|
|
42
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
last.focus();
|
|
45
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
first.focus();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<dialog
|
|
53
|
+
bind:this={dialog}
|
|
54
|
+
class="tern-dialog {size} {klass}"
|
|
55
|
+
aria-labelledby="tern-dialog-{uid}-title"
|
|
56
|
+
aria-describedby={description ? `tern-dialog-${uid}-description` : undefined}
|
|
57
|
+
onclose={() => {
|
|
58
|
+
open = false;
|
|
59
|
+
onclose?.();
|
|
60
|
+
}}
|
|
61
|
+
onclick={(e) => {
|
|
62
|
+
if (dismissable && e.target === dialog) open = false;
|
|
63
|
+
}}
|
|
64
|
+
{onkeydown}
|
|
65
|
+
>
|
|
66
|
+
<div class="panel">
|
|
67
|
+
<header>
|
|
68
|
+
<h2 id="tern-dialog-{uid}-title">{title}</h2>
|
|
69
|
+
<button type="button" class="close" aria-label="Close" onclick={() => (open = false)}>
|
|
70
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
|
|
71
|
+
</button>
|
|
72
|
+
</header>
|
|
73
|
+
{#if description}<p class="description" id="tern-dialog-{uid}-description">{description}</p>{/if}
|
|
74
|
+
{#if children}<div class="body">{@render children()}</div>{/if}
|
|
75
|
+
{#if footer}<footer>{@render footer()}</footer>{/if}
|
|
76
|
+
</div>
|
|
77
|
+
</dialog>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.tern-dialog {
|
|
81
|
+
padding: 0;
|
|
82
|
+
border: 0;
|
|
83
|
+
background: transparent;
|
|
84
|
+
color: var(--tern-fg);
|
|
85
|
+
max-width: calc(100vw - 2rem);
|
|
86
|
+
width: 28rem;
|
|
87
|
+
overflow: visible;
|
|
88
|
+
}
|
|
89
|
+
.sm {
|
|
90
|
+
width: 22rem;
|
|
91
|
+
}
|
|
92
|
+
.lg {
|
|
93
|
+
width: 40rem;
|
|
94
|
+
}
|
|
95
|
+
.tern-dialog::backdrop {
|
|
96
|
+
background: oklch(0.2 0.02 240 / 0.45);
|
|
97
|
+
backdrop-filter: blur(2px);
|
|
98
|
+
}
|
|
99
|
+
.tern-dialog[open] .panel {
|
|
100
|
+
animation: rise var(--tern-duration) ease-out;
|
|
101
|
+
}
|
|
102
|
+
.panel {
|
|
103
|
+
display: grid;
|
|
104
|
+
gap: 1rem;
|
|
105
|
+
padding: 1.25rem 1.5rem 1.5rem;
|
|
106
|
+
background: var(--tern-surface);
|
|
107
|
+
border: 1px solid var(--tern-border);
|
|
108
|
+
border-radius: calc(var(--tern-radius) + 4px);
|
|
109
|
+
box-shadow: var(--tern-shadow-lg);
|
|
110
|
+
}
|
|
111
|
+
header {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: flex-start;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
gap: 1rem;
|
|
116
|
+
}
|
|
117
|
+
h2 {
|
|
118
|
+
margin: 0;
|
|
119
|
+
font-size: 1.0625rem;
|
|
120
|
+
font-weight: 650;
|
|
121
|
+
letter-spacing: -0.01em;
|
|
122
|
+
}
|
|
123
|
+
.description {
|
|
124
|
+
margin: -0.5rem 0 0;
|
|
125
|
+
color: var(--tern-muted);
|
|
126
|
+
font-size: 0.875rem;
|
|
127
|
+
}
|
|
128
|
+
.close {
|
|
129
|
+
display: grid;
|
|
130
|
+
place-items: center;
|
|
131
|
+
width: 1.75rem;
|
|
132
|
+
height: 1.75rem;
|
|
133
|
+
margin: -0.25rem -0.5rem 0 0;
|
|
134
|
+
border: 0;
|
|
135
|
+
border-radius: var(--tern-radius-sm);
|
|
136
|
+
background: transparent;
|
|
137
|
+
color: var(--tern-muted);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
}
|
|
140
|
+
.close:hover {
|
|
141
|
+
background: var(--tern-surface-2);
|
|
142
|
+
color: var(--tern-fg);
|
|
143
|
+
}
|
|
144
|
+
.close:focus-visible {
|
|
145
|
+
outline: 2px solid var(--tern-ring);
|
|
146
|
+
outline-offset: 1px;
|
|
147
|
+
}
|
|
148
|
+
footer {
|
|
149
|
+
display: flex;
|
|
150
|
+
justify-content: flex-end;
|
|
151
|
+
gap: 0.5rem;
|
|
152
|
+
margin-top: 0.25rem;
|
|
153
|
+
}
|
|
154
|
+
@keyframes rise {
|
|
155
|
+
from {
|
|
156
|
+
opacity: 0;
|
|
157
|
+
transform: translateY(6px) scale(0.985);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Whether the dialog is open; bind it. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** The heading, which labels the dialog. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** A sentence under the title, read when the dialog opens. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Close when the backdrop is clicked. Escape always closes. */
|
|
10
|
+
dismissable?: boolean;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
class?: string;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
/** Buttons at the bottom, such as Cancel and Save. */
|
|
15
|
+
footer?: Snippet;
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
};
|
|
18
|
+
declare const Dialog: import("svelte").Component<Props, {}, "open">;
|
|
19
|
+
type Dialog = ReturnType<typeof Dialog>;
|
|
20
|
+
export default Dialog;
|