@ternkit/tern 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +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/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/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/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/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/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -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/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -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 +112 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- 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 +301 -0
- package/dist/ui/Dialog.svelte.d.ts +30 -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 +97 -0
- package/dist/ui/Field.svelte.d.ts +19 -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 +62 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- 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/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -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 +103 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- 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/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/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 +77 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -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/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -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/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +52 -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/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 +85 -0
- package/dist/ui/Tooltip.svelte.d.ts +16 -0
- 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 +18 -0
- package/dist/ui/field.js +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 +78 -0
- package/dist/ui/index.js +70 -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 +176 -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 +15 -4
- package/package.json +7 -1
|
@@ -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;
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
const field = getField();
|
|
16
|
+
let { value = $bindable(field?.value), options, placeholder, class: klass = '', children, ...rest }: Props = $props();
|
|
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
|
+
name={field?.name}
|
|
27
|
+
{...rest}
|
|
28
|
+
bind:value
|
|
29
|
+
>
|
|
30
|
+
{#if placeholder}<option value="" disabled selected={value === undefined || value === ''}>{placeholder}</option>{/if}
|
|
31
|
+
{#each options ?? [] as o (o.value)}
|
|
32
|
+
<option value={o.value} disabled={o.disabled}>{o.label}</option>
|
|
33
|
+
{/each}
|
|
34
|
+
{@render children?.()}
|
|
35
|
+
</select>
|
|
36
|
+
<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>
|
|
37
|
+
</span>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.tern-select {
|
|
41
|
+
position: relative;
|
|
42
|
+
display: block;
|
|
43
|
+
}
|
|
44
|
+
select {
|
|
45
|
+
appearance: none;
|
|
46
|
+
width: 100%;
|
|
47
|
+
height: var(--tern-control-height);
|
|
48
|
+
padding: 0 2.25rem 0 0.75rem;
|
|
49
|
+
font: inherit;
|
|
50
|
+
font-size: 0.875rem;
|
|
51
|
+
color: var(--tern-fg);
|
|
52
|
+
background: var(--tern-surface);
|
|
53
|
+
border: 1px solid var(--tern-border-strong);
|
|
54
|
+
border-radius: var(--tern-radius-sm);
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
select:focus-visible {
|
|
58
|
+
outline: none;
|
|
59
|
+
border-color: var(--tern-accent);
|
|
60
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
|
|
61
|
+
}
|
|
62
|
+
select[aria-invalid='true'] {
|
|
63
|
+
border-color: var(--tern-danger);
|
|
64
|
+
}
|
|
65
|
+
select:disabled {
|
|
66
|
+
opacity: 0.6;
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
}
|
|
69
|
+
svg {
|
|
70
|
+
position: absolute;
|
|
71
|
+
right: 0.7rem;
|
|
72
|
+
top: 50%;
|
|
73
|
+
translate: 0 -50%;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
color: var(--tern-muted);
|
|
76
|
+
}
|
|
77
|
+
</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,301 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getField } from './field.js';
|
|
3
|
+
import { float } from './floating.js';
|
|
4
|
+
import { grouped, type Option } from './options.js';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
options: Option[];
|
|
8
|
+
/** The chosen value; bind it. */
|
|
9
|
+
value?: string | null;
|
|
10
|
+
/** Shown when nothing is chosen. */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** Labels it when it isn't in a Field. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Posts the value under this name. */
|
|
15
|
+
name?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
onchange?: (value: string, option: Option) => void;
|
|
19
|
+
};
|
|
20
|
+
const field = getField();
|
|
21
|
+
let { options, value = $bindable(field?.value ?? null), placeholder = 'Choose…', label, name: nameProp, disabled = false, class: klass = '', onchange }: Props = $props();
|
|
22
|
+
const name = $derived(nameProp ?? field?.name);
|
|
23
|
+
|
|
24
|
+
const uid = $props.id();
|
|
25
|
+
const id = `tern-selectmenu-${uid}`;
|
|
26
|
+
field?.custom();
|
|
27
|
+
let button = $state<HTMLButtonElement>();
|
|
28
|
+
let list = $state<HTMLDivElement>();
|
|
29
|
+
let open = $state(false);
|
|
30
|
+
let active = $state(-1);
|
|
31
|
+
let typed = '';
|
|
32
|
+
let typedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
33
|
+
|
|
34
|
+
const selectedIndex = $derived(options.findIndex((o) => o.value === value));
|
|
35
|
+
const selected = $derived(options[selectedIndex]);
|
|
36
|
+
const enabledIdx = $derived(options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0));
|
|
37
|
+
|
|
38
|
+
function show() {
|
|
39
|
+
if (disabled || field?.disabled) return;
|
|
40
|
+
active = selectedIndex >= 0 && !options[selectedIndex]?.disabled ? selectedIndex : (enabledIdx[0] ?? -1);
|
|
41
|
+
open = true;
|
|
42
|
+
}
|
|
43
|
+
function choose(i: number) {
|
|
44
|
+
const o = options[i];
|
|
45
|
+
if (!o || o.disabled) return;
|
|
46
|
+
value = o.value;
|
|
47
|
+
open = false;
|
|
48
|
+
onchange?.(o.value, o);
|
|
49
|
+
button?.focus();
|
|
50
|
+
}
|
|
51
|
+
function step(by: number | 'first' | 'last') {
|
|
52
|
+
const list = enabledIdx;
|
|
53
|
+
if (!list.length) return;
|
|
54
|
+
const pos = list.indexOf(active);
|
|
55
|
+
const next = by === 'first' ? 0 : by === 'last' ? list.length - 1 : Math.max(0, Math.min(list.length - 1, (pos < 0 ? 0 : pos) + by));
|
|
56
|
+
active = list[next]!;
|
|
57
|
+
document.getElementById(`${id}-opt-${active}`)?.scrollIntoView?.({ block: 'nearest' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onkeydown(e: KeyboardEvent) {
|
|
61
|
+
const opening = ['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End'];
|
|
62
|
+
if (!open) {
|
|
63
|
+
if (opening.includes(e.key)) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
show();
|
|
66
|
+
if (e.key === 'Home') step('first');
|
|
67
|
+
if (e.key === 'End') step('last');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
switch (e.key) {
|
|
72
|
+
case 'ArrowDown':
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
return e.altKey ? choose(active) : step(1);
|
|
75
|
+
case 'ArrowUp':
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
return e.altKey ? choose(active) : step(-1);
|
|
78
|
+
case 'Home':
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
return step('first');
|
|
81
|
+
case 'End':
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
return step('last');
|
|
84
|
+
case 'PageDown':
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
return step(10);
|
|
87
|
+
case 'PageUp':
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
return step(-10);
|
|
90
|
+
case 'Enter':
|
|
91
|
+
case ' ':
|
|
92
|
+
if (e.key === ' ' && typed) break; // a space inside a typed name
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
return choose(active);
|
|
95
|
+
case 'Escape':
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
open = false;
|
|
99
|
+
return;
|
|
100
|
+
case 'Tab':
|
|
101
|
+
open = false;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
106
|
+
// Type to jump, open or closed, like a native select.
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
typed += e.key.toLowerCase();
|
|
109
|
+
clearTimeout(typedTimer);
|
|
110
|
+
typedTimer = setTimeout(() => (typed = ''), 500);
|
|
111
|
+
const from = open ? active : selectedIndex;
|
|
112
|
+
const start = typed.length === 1 ? from + 1 : Math.max(from, 0);
|
|
113
|
+
const order = [...enabledIdx.filter((i) => i >= start), ...enabledIdx.filter((i) => i < start)];
|
|
114
|
+
const hit = order.find((i) => options[i]!.label.toLowerCase().startsWith(typed));
|
|
115
|
+
if (hit === undefined) return;
|
|
116
|
+
if (open) active = hit;
|
|
117
|
+
else choose(hit);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (!open || !list || !button) return;
|
|
123
|
+
list.showPopover?.();
|
|
124
|
+
const stop = float(list, button, { side: 'bottom', align: 'start', offset: 4, matchWidth: true });
|
|
125
|
+
document.getElementById(`${id}-opt-${active}`)?.scrollIntoView?.({ block: 'nearest' });
|
|
126
|
+
const away = (e: PointerEvent) => {
|
|
127
|
+
const t = e.target as Node;
|
|
128
|
+
if (!list?.contains(t) && !button?.contains(t)) open = false;
|
|
129
|
+
};
|
|
130
|
+
document.addEventListener('pointerdown', away, true);
|
|
131
|
+
return () => {
|
|
132
|
+
stop();
|
|
133
|
+
document.removeEventListener('pointerdown', away, true);
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
bind:this={button}
|
|
140
|
+
type="button"
|
|
141
|
+
class="tern-selectmenu {klass}"
|
|
142
|
+
role="combobox"
|
|
143
|
+
id={field?.id ?? id}
|
|
144
|
+
aria-haspopup="listbox"
|
|
145
|
+
aria-expanded={open}
|
|
146
|
+
aria-controls="{id}-list"
|
|
147
|
+
aria-activedescendant={open && active >= 0 ? `${id}-opt-${active}` : undefined}
|
|
148
|
+
aria-label={field ? undefined : label}
|
|
149
|
+
aria-labelledby={field ? `${field.labelId} ${id}-value` : undefined}
|
|
150
|
+
aria-describedby={field?.describedBy}
|
|
151
|
+
aria-invalid={field?.invalid || undefined}
|
|
152
|
+
aria-required={field?.required || undefined}
|
|
153
|
+
disabled={disabled || field?.disabled}
|
|
154
|
+
onclick={() => (open ? (open = false) : show())}
|
|
155
|
+
{onkeydown}
|
|
156
|
+
onblur={(e) => !list?.contains(e.relatedTarget as Node) && (open = false)}
|
|
157
|
+
>
|
|
158
|
+
<span class="value" class:placeholder={!selected} id="{id}-value">{selected?.label ?? placeholder}</span>
|
|
159
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M5 6.5l3-3 3 3M5 9.5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
160
|
+
</button>
|
|
161
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
162
|
+
<div bind:this={list} id="{id}-list" class="tern-selectmenu-list" class:open role="listbox" aria-label={label} aria-labelledby={field?.labelId} tabindex="-1" popover="manual">
|
|
163
|
+
{#if open}
|
|
164
|
+
{#each grouped(options) as g, gi (gi)}
|
|
165
|
+
{#snippet items()}
|
|
166
|
+
{#each g.options as o (o.value)}
|
|
167
|
+
{@const i = options.indexOf(o)}
|
|
168
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
169
|
+
<div
|
|
170
|
+
id="{id}-opt-{i}"
|
|
171
|
+
class="option"
|
|
172
|
+
class:active={i === active}
|
|
173
|
+
role="option"
|
|
174
|
+
tabindex="-1"
|
|
175
|
+
aria-selected={o.value === value}
|
|
176
|
+
aria-disabled={o.disabled || undefined}
|
|
177
|
+
onpointerdown={(e) => e.preventDefault()}
|
|
178
|
+
onpointermove={() => !o.disabled && (active = i)}
|
|
179
|
+
onclick={() => choose(i)}
|
|
180
|
+
>
|
|
181
|
+
<span class="check" aria-hidden="true">
|
|
182
|
+
{#if o.value === value}<svg viewBox="0 0 16 16" width="14" height="14"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>{/if}
|
|
183
|
+
</span>
|
|
184
|
+
<span class="text">
|
|
185
|
+
<span>{o.label}</span>
|
|
186
|
+
{#if o.description}<span class="description">{o.description}</span>{/if}
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
{/each}
|
|
190
|
+
{/snippet}
|
|
191
|
+
{#if g.group}
|
|
192
|
+
<div role="group" aria-labelledby="{id}-group-{gi}">
|
|
193
|
+
<div class="heading" id="{id}-group-{gi}" role="presentation">{g.group}</div>
|
|
194
|
+
{@render items()}
|
|
195
|
+
</div>
|
|
196
|
+
{:else}
|
|
197
|
+
{@render items()}
|
|
198
|
+
{/if}
|
|
199
|
+
{/each}
|
|
200
|
+
{/if}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<style>
|
|
204
|
+
.tern-selectmenu {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: space-between;
|
|
208
|
+
gap: 0.5rem;
|
|
209
|
+
width: 100%;
|
|
210
|
+
height: var(--tern-control-height);
|
|
211
|
+
padding: 0 0.6rem 0 0.75rem;
|
|
212
|
+
font: inherit;
|
|
213
|
+
font-size: 0.875rem;
|
|
214
|
+
color: var(--tern-fg);
|
|
215
|
+
text-align: start;
|
|
216
|
+
background: var(--tern-surface);
|
|
217
|
+
border: 1px solid var(--tern-border-strong);
|
|
218
|
+
border-radius: var(--tern-radius-sm);
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition:
|
|
221
|
+
border-color var(--tern-duration),
|
|
222
|
+
box-shadow var(--tern-duration);
|
|
223
|
+
}
|
|
224
|
+
.tern-selectmenu svg {
|
|
225
|
+
flex: none;
|
|
226
|
+
color: var(--tern-muted);
|
|
227
|
+
}
|
|
228
|
+
.tern-selectmenu:hover:not(:disabled) {
|
|
229
|
+
border-color: var(--tern-muted);
|
|
230
|
+
}
|
|
231
|
+
.tern-selectmenu:focus-visible {
|
|
232
|
+
outline: none;
|
|
233
|
+
border-color: var(--tern-accent);
|
|
234
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--tern-ring) 30%, transparent);
|
|
235
|
+
}
|
|
236
|
+
.tern-selectmenu[aria-invalid='true'] {
|
|
237
|
+
border-color: var(--tern-danger);
|
|
238
|
+
}
|
|
239
|
+
.tern-selectmenu:disabled {
|
|
240
|
+
opacity: 0.6;
|
|
241
|
+
cursor: not-allowed;
|
|
242
|
+
}
|
|
243
|
+
.value {
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
text-overflow: ellipsis;
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
}
|
|
248
|
+
.placeholder {
|
|
249
|
+
color: var(--tern-muted);
|
|
250
|
+
}
|
|
251
|
+
.tern-selectmenu-list {
|
|
252
|
+
max-height: 18rem;
|
|
253
|
+
overflow: auto;
|
|
254
|
+
padding: 0.3rem;
|
|
255
|
+
color: var(--tern-fg);
|
|
256
|
+
background: var(--tern-surface);
|
|
257
|
+
border: 1px solid var(--tern-border);
|
|
258
|
+
border-radius: var(--tern-radius);
|
|
259
|
+
box-shadow: var(--tern-shadow-lg);
|
|
260
|
+
font-size: 0.875rem;
|
|
261
|
+
outline: none;
|
|
262
|
+
}
|
|
263
|
+
.tern-selectmenu-list:not(.open) {
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
.option {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: flex-start;
|
|
269
|
+
gap: 0.4rem;
|
|
270
|
+
padding: 0.4rem 0.6rem 0.4rem 0.35rem;
|
|
271
|
+
border-radius: calc(var(--tern-radius-sm) - 2px);
|
|
272
|
+
cursor: pointer;
|
|
273
|
+
}
|
|
274
|
+
.option.active {
|
|
275
|
+
background: var(--tern-accent-soft);
|
|
276
|
+
}
|
|
277
|
+
.option[aria-disabled='true'] {
|
|
278
|
+
opacity: 0.45;
|
|
279
|
+
cursor: not-allowed;
|
|
280
|
+
}
|
|
281
|
+
.check {
|
|
282
|
+
display: inline-flex;
|
|
283
|
+
width: 1rem;
|
|
284
|
+
padding-top: 0.15rem;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
color: var(--tern-accent);
|
|
287
|
+
}
|
|
288
|
+
.text {
|
|
289
|
+
display: grid;
|
|
290
|
+
}
|
|
291
|
+
.description {
|
|
292
|
+
font-size: 0.8125rem;
|
|
293
|
+
color: var(--tern-muted);
|
|
294
|
+
}
|
|
295
|
+
.heading {
|
|
296
|
+
padding: 0.45rem 0.6rem 0.25rem 1.75rem;
|
|
297
|
+
font-size: 0.75rem;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
color: var(--tern-muted);
|
|
300
|
+
}
|
|
301
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Option } from './options.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
options: Option[];
|
|
4
|
+
/** The chosen value; bind it. */
|
|
5
|
+
value?: string | null;
|
|
6
|
+
/** Shown when nothing is chosen. */
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Labels it when it isn't in a Field. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Posts the value under this name. */
|
|
11
|
+
name?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
onchange?: (value: string, option: Option) => void;
|
|
15
|
+
};
|
|
16
|
+
declare const SelectMenu: import("svelte").Component<Props, {}, "value">;
|
|
17
|
+
type SelectMenu = ReturnType<typeof SelectMenu>;
|
|
18
|
+
export default SelectMenu;
|
|
@@ -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,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Dialog from './Dialog.svelte';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
/** Whether it's open; bind it. */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** The edge it slides in from. */
|
|
11
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
12
|
+
class?: string;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
footer?: Snippet;
|
|
15
|
+
onclose?: () => void;
|
|
16
|
+
};
|
|
17
|
+
let { open = $bindable(false), side = 'right', ...rest }: Props = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<!-- A modal panel against an edge of the screen, for navigation, filters or details. -->
|
|
21
|
+
<Dialog bind:open {side} {...rest} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Whether it's open; bind it. */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
/** The edge it slides in from. */
|
|
8
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
9
|
+
class?: string;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
footer?: Snippet;
|
|
12
|
+
onclose?: () => void;
|
|
13
|
+
};
|
|
14
|
+
declare const Sheet: import("svelte").Component<Props, {}, "open">;
|
|
15
|
+
type Sheet = ReturnType<typeof Sheet>;
|
|
16
|
+
export default Sheet;
|