@ternkit/tern 1.3.0 → 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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.js +168 -0
- package/dist/server/intl.js +55 -12
- package/dist/ui/AlertDialog.svelte +79 -0
- package/dist/ui/AlertDialog.svelte.d.ts +24 -0
- package/dist/ui/AreaChart.svelte +9 -0
- package/dist/ui/AreaChart.svelte.d.ts +26 -0
- package/dist/ui/AspectRatio.svelte +33 -0
- package/dist/ui/AspectRatio.svelte.d.ts +10 -0
- package/dist/ui/BarChart.svelte +170 -0
- package/dist/ui/BarChart.svelte.d.ts +40 -0
- package/dist/ui/Breadcrumb.svelte +92 -0
- package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
- package/dist/ui/ButtonGroup.svelte +61 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
- package/dist/ui/Calendar.svelte +265 -0
- package/dist/ui/Calendar.svelte.d.ts +28 -0
- package/dist/ui/Carousel.svelte +169 -0
- package/dist/ui/Carousel.svelte.d.ts +36 -0
- package/dist/ui/ChartFrame.svelte +174 -0
- package/dist/ui/ChartFrame.svelte.d.ts +24 -0
- package/dist/ui/Checkbox.svelte +13 -1
- package/dist/ui/Collapsible.svelte +59 -0
- package/dist/ui/Collapsible.svelte.d.ts +12 -0
- package/dist/ui/Combobox.svelte +350 -0
- package/dist/ui/Combobox.svelte.d.ts +24 -0
- package/dist/ui/Command.svelte +268 -0
- package/dist/ui/Command.svelte.d.ts +28 -0
- package/dist/ui/ConsentScreen.svelte +125 -0
- package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
- package/dist/ui/ContextMenu.svelte +78 -0
- package/dist/ui/ContextMenu.svelte.d.ts +13 -0
- package/dist/ui/DataTable.svelte +277 -0
- package/dist/ui/DataTable.svelte.d.ts +53 -0
- package/dist/ui/DatePicker.svelte +138 -0
- package/dist/ui/DatePicker.svelte.d.ts +25 -0
- package/dist/ui/Dialog.svelte +168 -27
- package/dist/ui/Dialog.svelte.d.ts +10 -0
- package/dist/ui/DonutChart.svelte +115 -0
- package/dist/ui/DonutChart.svelte.d.ts +40 -0
- package/dist/ui/Drawer.svelte +19 -0
- package/dist/ui/Drawer.svelte.d.ts +14 -0
- package/dist/ui/Empty.svelte +62 -0
- package/dist/ui/Empty.svelte.d.ts +15 -0
- package/dist/ui/Field.svelte +22 -2
- package/dist/ui/Field.svelte.d.ts +2 -0
- package/dist/ui/Form.svelte +159 -0
- package/dist/ui/Form.svelte.d.ts +21 -0
- package/dist/ui/HoverCard.svelte +106 -0
- package/dist/ui/HoverCard.svelte.d.ts +26 -0
- package/dist/ui/Input.svelte +2 -1
- package/dist/ui/InputGroup.svelte +80 -0
- package/dist/ui/InputGroup.svelte.d.ts +13 -0
- package/dist/ui/InputOTP.svelte +140 -0
- package/dist/ui/InputOTP.svelte.d.ts +18 -0
- package/dist/ui/Item.svelte +86 -0
- package/dist/ui/Item.svelte.d.ts +17 -0
- package/dist/ui/Label.svelte +29 -0
- package/dist/ui/Label.svelte.d.ts +10 -0
- package/dist/ui/LineChart.svelte +171 -0
- package/dist/ui/LineChart.svelte.d.ts +42 -0
- package/dist/ui/Menu.svelte +22 -151
- package/dist/ui/Menu.svelte.d.ts +3 -10
- package/dist/ui/MenuList.svelte +317 -0
- package/dist/ui/MenuList.svelte.d.ts +29 -0
- package/dist/ui/Menubar.svelte +133 -0
- package/dist/ui/Menubar.svelte.d.ts +14 -0
- package/dist/ui/NavigationMenu.svelte +208 -0
- package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
- package/dist/ui/Pagination.svelte +114 -0
- package/dist/ui/Pagination.svelte.d.ts +16 -0
- package/dist/ui/PasswordInput.svelte +134 -0
- package/dist/ui/PasswordInput.svelte.d.ts +11 -0
- package/dist/ui/Popover.svelte +127 -0
- package/dist/ui/Popover.svelte.d.ts +29 -0
- package/dist/ui/Resizable.svelte +122 -0
- package/dist/ui/Resizable.svelte.d.ts +16 -0
- package/dist/ui/ScrollArea.svelte +43 -0
- package/dist/ui/ScrollArea.svelte.d.ts +13 -0
- package/dist/ui/Select.svelte +3 -2
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Sheet.svelte +21 -0
- package/dist/ui/Sheet.svelte.d.ts +16 -0
- package/dist/ui/Sidebar.svelte +263 -0
- package/dist/ui/Sidebar.svelte.d.ts +35 -0
- package/dist/ui/SignIn.svelte +182 -0
- package/dist/ui/SignIn.svelte.d.ts +38 -0
- package/dist/ui/SignUp.svelte +106 -0
- package/dist/ui/SignUp.svelte.d.ts +23 -0
- package/dist/ui/Slider.svelte +196 -0
- package/dist/ui/Slider.svelte.d.ts +19 -0
- package/dist/ui/Sparkline.svelte +23 -0
- package/dist/ui/Sparkline.svelte.d.ts +11 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Textarea.svelte +2 -1
- package/dist/ui/Toggle.svelte +82 -0
- package/dist/ui/Toggle.svelte.d.ts +17 -0
- package/dist/ui/ToggleGroup.svelte +143 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
- package/dist/ui/Tooltip.svelte +27 -19
- package/dist/ui/Tooltip.svelte.d.ts +2 -1
- package/dist/ui/chart.d.ts +26 -0
- package/dist/ui/chart.js +89 -0
- package/dist/ui/dates.d.ts +13 -0
- package/dist/ui/dates.js +64 -0
- package/dist/ui/field.d.ts +8 -0
- package/dist/ui/floating.d.ts +44 -0
- package/dist/ui/floating.js +97 -0
- package/dist/ui/form.d.ts +12 -0
- package/dist/ui/form.js +8 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +43 -0
- package/dist/ui/menu.d.ts +25 -0
- package/dist/ui/menu.js +1 -0
- package/dist/ui/options.d.ts +17 -0
- package/dist/ui/options.js +22 -0
- package/dist/ui/table.d.ts +26 -0
- package/dist/ui/table.js +1 -0
- package/dist/ui/theme.css +25 -0
- package/dist/vite/index.js +10 -2
- package/package.json +1 -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,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** The first pane's share, in percent; bind it. */
|
|
6
|
+
size?: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
direction?: 'horizontal' | 'vertical';
|
|
10
|
+
/** Names the handle, such as "Resize sidebar". */
|
|
11
|
+
label?: string;
|
|
12
|
+
first: Snippet;
|
|
13
|
+
second: Snippet;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
let { size = $bindable(50), min = 15, max = 85, direction = 'horizontal', label = 'Resize', first, second, class: klass = '' }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
let root = $state<HTMLDivElement>();
|
|
20
|
+
let dragging = $state(false);
|
|
21
|
+
const clamp = (v: number) => Math.round(Math.min(max, Math.max(min, v)) * 10) / 10;
|
|
22
|
+
|
|
23
|
+
function move(e: PointerEvent) {
|
|
24
|
+
if (!dragging || !root) return;
|
|
25
|
+
const r = root.getBoundingClientRect();
|
|
26
|
+
size = clamp(direction === 'horizontal' ? ((e.clientX - r.left) / r.width) * 100 : ((e.clientY - r.top) / r.height) * 100);
|
|
27
|
+
}
|
|
28
|
+
function onkeydown(e: KeyboardEvent) {
|
|
29
|
+
const back = direction === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
|
|
30
|
+
const fwd = direction === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
|
|
31
|
+
const moves: Record<string, number> = { [back]: size - 5, [fwd]: size + 5, Home: min, End: max };
|
|
32
|
+
if (!(e.key in moves)) return;
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
size = clamp(moves[e.key]!);
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div bind:this={root} class="tern-resizable {direction} {klass}" class:dragging>
|
|
39
|
+
<div class="pane" id="tern-resizable-{uid}-first" style:flex-basis="{size}%">{@render first()}</div>
|
|
40
|
+
<!-- A focusable window splitter, as the ARIA pattern has it. -->
|
|
41
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
42
|
+
<div
|
|
43
|
+
class="handle"
|
|
44
|
+
role="separator"
|
|
45
|
+
tabindex="0"
|
|
46
|
+
aria-label={label}
|
|
47
|
+
aria-controls="tern-resizable-{uid}-first"
|
|
48
|
+
aria-orientation={direction === 'horizontal' ? 'vertical' : 'horizontal'}
|
|
49
|
+
aria-valuenow={size}
|
|
50
|
+
aria-valuemin={min}
|
|
51
|
+
aria-valuemax={max}
|
|
52
|
+
onpointerdown={(e) => {
|
|
53
|
+
dragging = true;
|
|
54
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
55
|
+
}}
|
|
56
|
+
onpointermove={move}
|
|
57
|
+
onpointerup={() => (dragging = false)}
|
|
58
|
+
onpointercancel={() => (dragging = false)}
|
|
59
|
+
{onkeydown}
|
|
60
|
+
></div>
|
|
61
|
+
<div class="pane second">{@render second()}</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.tern-resizable {
|
|
66
|
+
display: flex;
|
|
67
|
+
width: 100%;
|
|
68
|
+
height: 100%;
|
|
69
|
+
min-height: 0;
|
|
70
|
+
border: 1px solid var(--tern-border);
|
|
71
|
+
border-radius: var(--tern-radius);
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
.vertical {
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
}
|
|
77
|
+
.pane {
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
min-height: 0;
|
|
81
|
+
overflow: auto;
|
|
82
|
+
}
|
|
83
|
+
.second {
|
|
84
|
+
flex: 1;
|
|
85
|
+
}
|
|
86
|
+
.handle {
|
|
87
|
+
position: relative;
|
|
88
|
+
flex: none;
|
|
89
|
+
background: var(--tern-border);
|
|
90
|
+
touch-action: none;
|
|
91
|
+
outline: none;
|
|
92
|
+
}
|
|
93
|
+
.horizontal > .handle {
|
|
94
|
+
width: 1px;
|
|
95
|
+
cursor: col-resize;
|
|
96
|
+
}
|
|
97
|
+
.vertical > .handle {
|
|
98
|
+
height: 1px;
|
|
99
|
+
cursor: row-resize;
|
|
100
|
+
}
|
|
101
|
+
/* A wider target than the line. */
|
|
102
|
+
.handle::before {
|
|
103
|
+
content: '';
|
|
104
|
+
position: absolute;
|
|
105
|
+
inset: -4px;
|
|
106
|
+
}
|
|
107
|
+
.handle:hover,
|
|
108
|
+
.handle:focus-visible,
|
|
109
|
+
.dragging > .handle {
|
|
110
|
+
background: var(--tern-accent);
|
|
111
|
+
}
|
|
112
|
+
.handle:focus-visible::after {
|
|
113
|
+
content: '';
|
|
114
|
+
position: absolute;
|
|
115
|
+
inset: -2px;
|
|
116
|
+
border-radius: 2px;
|
|
117
|
+
box-shadow: 0 0 0 2px var(--tern-ring);
|
|
118
|
+
}
|
|
119
|
+
.dragging {
|
|
120
|
+
user-select: none;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** The first pane's share, in percent; bind it. */
|
|
4
|
+
size?: number;
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
direction?: 'horizontal' | 'vertical';
|
|
8
|
+
/** Names the handle, such as "Resize sidebar". */
|
|
9
|
+
label?: string;
|
|
10
|
+
first: Snippet;
|
|
11
|
+
second: Snippet;
|
|
12
|
+
class?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const Resizable: import("svelte").Component<Props, {}, "size">;
|
|
15
|
+
type Resizable = ReturnType<typeof Resizable>;
|
|
16
|
+
export default Resizable;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** Names the region, so keyboard and screen reader users know what scrolls. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The most it may grow before scrolling, such as "20rem". */
|
|
8
|
+
maxHeight?: string;
|
|
9
|
+
orientation?: 'vertical' | 'horizontal' | 'both';
|
|
10
|
+
children: Snippet;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
let { label, maxHeight = '20rem', orientation = 'vertical', children, class: klass = '' }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- Focusable, so keyboards can scroll it, with thin scrollbars in the theme's colors. -->
|
|
17
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
18
|
+
<div class="tern-scroll {orientation} {klass}" role="region" aria-label={label} tabindex="0" style:max-height={orientation === 'horizontal' ? undefined : maxHeight}>
|
|
19
|
+
{@render children()}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
.tern-scroll {
|
|
24
|
+
scrollbar-width: thin;
|
|
25
|
+
scrollbar-color: var(--tern-border-strong) transparent;
|
|
26
|
+
border-radius: var(--tern-radius-sm);
|
|
27
|
+
}
|
|
28
|
+
.vertical {
|
|
29
|
+
overflow-y: auto;
|
|
30
|
+
overflow-x: hidden;
|
|
31
|
+
}
|
|
32
|
+
.horizontal {
|
|
33
|
+
overflow-x: auto;
|
|
34
|
+
overflow-y: hidden;
|
|
35
|
+
}
|
|
36
|
+
.both {
|
|
37
|
+
overflow: auto;
|
|
38
|
+
}
|
|
39
|
+
.tern-scroll:focus-visible {
|
|
40
|
+
outline: 2px solid var(--tern-ring);
|
|
41
|
+
outline-offset: 2px;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Names the region, so keyboard and screen reader users know what scrolls. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** The most it may grow before scrolling, such as "20rem". */
|
|
6
|
+
maxHeight?: string;
|
|
7
|
+
orientation?: 'vertical' | 'horizontal' | 'both';
|
|
8
|
+
children: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
};
|
|
11
|
+
declare const ScrollArea: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type ScrollArea = ReturnType<typeof ScrollArea>;
|
|
13
|
+
export default ScrollArea;
|
package/dist/ui/Select.svelte
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
placeholder?: string;
|
|
13
13
|
children?: Snippet;
|
|
14
14
|
};
|
|
15
|
-
let { value = $bindable(), options, placeholder, class: klass = '', children, ...rest }: Props = $props();
|
|
16
15
|
const field = getField();
|
|
16
|
+
let { value = $bindable(field?.value), options, placeholder, class: klass = '', children, ...rest }: Props = $props();
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<span class="tern-select {klass}">
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
aria-invalid={field?.invalid || undefined}
|
|
24
24
|
required={field?.required}
|
|
25
25
|
disabled={field?.disabled}
|
|
26
|
-
{
|
|
26
|
+
name={field?.name}
|
|
27
|
+
{...rest}
|
|
27
28
|
bind:value
|
|
28
29
|
>
|
|
29
30
|
{#if placeholder}<option value="" disabled selected={value === undefined || value === ''}>{placeholder}</option>{/if}
|