@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,265 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { addDays, addMonths, format, localeWeekStart, monthGrid, startOfMonth, today, type ISODate } from './dates.js';
|
|
4
|
+
|
|
5
|
+
export type DateRange = { start: ISODate | null; end: ISODate | null };
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
/** single picks a day; range picks a start and an end. */
|
|
9
|
+
mode?: 'single' | 'range';
|
|
10
|
+
/** The chosen day (single) or { start, end } (range); bind it. */
|
|
11
|
+
value?: ISODate | null | DateRange;
|
|
12
|
+
/** The earliest and latest days that can be picked. */
|
|
13
|
+
min?: ISODate;
|
|
14
|
+
max?: ISODate;
|
|
15
|
+
/** Other days that can't be picked, such as weekends. */
|
|
16
|
+
isDisabled?: (date: ISODate) => boolean;
|
|
17
|
+
/** The locale for month and day names; the browser's by default. */
|
|
18
|
+
locale?: string;
|
|
19
|
+
/** The first day of the week, 0 for Sunday; the locale's by default. */
|
|
20
|
+
weekStart?: number;
|
|
21
|
+
/** Names the calendar for screen readers. */
|
|
22
|
+
label?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
/** Runs when a day (single) or a whole range is picked. */
|
|
25
|
+
onselect?: (value: ISODate | DateRange) => void;
|
|
26
|
+
};
|
|
27
|
+
let { mode = 'single', value = $bindable(mode === 'range' ? { start: null, end: null } : null), min, max, isDisabled, locale, weekStart, label, class: klass = '', onselect }: Props = $props();
|
|
28
|
+
|
|
29
|
+
const uid = $props.id();
|
|
30
|
+
const id = `tern-calendar-${uid}`;
|
|
31
|
+
const first = $derived(weekStart ?? localeWeekStart(locale));
|
|
32
|
+
const range = $derived(mode === 'range' ? ((value as DateRange | null) ?? { start: null, end: null }) : null);
|
|
33
|
+
const single = $derived(mode === 'single' ? (value as ISODate | null) : null);
|
|
34
|
+
const now = today();
|
|
35
|
+
let focused = $state<ISODate>(today());
|
|
36
|
+
let hovered = $state<ISODate | null>(null);
|
|
37
|
+
let grid = $state<HTMLTableElement>();
|
|
38
|
+
|
|
39
|
+
// Start where the value is.
|
|
40
|
+
$effect.pre(() => {
|
|
41
|
+
const start = single ?? range?.start;
|
|
42
|
+
if (start) focused = start;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const weeks = $derived(monthGrid(focused, first));
|
|
46
|
+
const month = $derived(focused.slice(0, 7));
|
|
47
|
+
const weekdays = $derived(weeks[0]!.map((d) => ({ short: format(d, locale, { weekday: 'short' }), long: format(d, locale, { weekday: 'long' }) })));
|
|
48
|
+
|
|
49
|
+
const blocked = (d: ISODate) => (min !== undefined && d < min) || (max !== undefined && d > max) || !!isDisabled?.(d);
|
|
50
|
+
const selected = (d: ISODate) => (single ? d === single : !!range && (d === range.start || d === range.end));
|
|
51
|
+
const between = (d: ISODate) => {
|
|
52
|
+
if (!range?.start) return false;
|
|
53
|
+
const end = range.end ?? hovered;
|
|
54
|
+
if (!end) return false;
|
|
55
|
+
const [a, b] = range.start <= end ? [range.start, end] : [end, range.start];
|
|
56
|
+
return d > a && d < b;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async function moveTo(d: ISODate) {
|
|
60
|
+
if (min && d < min) d = min;
|
|
61
|
+
if (max && d > max) d = max;
|
|
62
|
+
focused = d;
|
|
63
|
+
await tick();
|
|
64
|
+
grid?.querySelector<HTMLElement>(`[data-date="${d}"]`)?.focus();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pick(d: ISODate) {
|
|
68
|
+
if (blocked(d)) return;
|
|
69
|
+
focused = d;
|
|
70
|
+
if (mode === 'single') {
|
|
71
|
+
value = d;
|
|
72
|
+
onselect?.(d);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const r = range!;
|
|
76
|
+
if (!r.start || r.end) value = { start: d, end: null };
|
|
77
|
+
else {
|
|
78
|
+
const next = d < r.start ? { start: d, end: r.start } : { start: r.start, end: d };
|
|
79
|
+
value = next;
|
|
80
|
+
onselect?.(next);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onkeydown(e: KeyboardEvent) {
|
|
85
|
+
const dow = (new Date(`${focused}T00:00:00Z`).getUTCDay() - first + 7) % 7;
|
|
86
|
+
const moves: Record<string, () => ISODate> = {
|
|
87
|
+
ArrowRight: () => addDays(focused, 1),
|
|
88
|
+
ArrowLeft: () => addDays(focused, -1),
|
|
89
|
+
ArrowDown: () => addDays(focused, 7),
|
|
90
|
+
ArrowUp: () => addDays(focused, -7),
|
|
91
|
+
Home: () => addDays(focused, -dow),
|
|
92
|
+
End: () => addDays(focused, 6 - dow),
|
|
93
|
+
PageDown: () => addMonths(focused, e.shiftKey ? 12 : 1),
|
|
94
|
+
PageUp: () => addMonths(focused, e.shiftKey ? -12 : -1)
|
|
95
|
+
};
|
|
96
|
+
if (e.key in moves) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
moveTo(moves[e.key]!());
|
|
99
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
pick(focused);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const canPrev = $derived(!min || startOfMonth(focused) > min);
|
|
106
|
+
const canNext = $derived(!max || addMonths(startOfMonth(focused), 1) <= max);
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div class="tern-calendar {klass}" role="group" aria-label={label}>
|
|
110
|
+
<div class="header">
|
|
111
|
+
<button type="button" class="nav" aria-label="Previous month" disabled={!canPrev} onclick={() => (focused = addMonths(focused, -1))}>
|
|
112
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M10 4l-4 4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
113
|
+
</button>
|
|
114
|
+
<div class="title" id="{id}-title" aria-live="polite">{format(focused, locale, { month: 'long', year: 'numeric' })}</div>
|
|
115
|
+
<button type="button" class="nav" aria-label="Next month" disabled={!canNext} onclick={() => (focused = addMonths(focused, 1))}>
|
|
116
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
<table bind:this={grid} role="grid" aria-labelledby="{id}-title" aria-multiselectable={mode === 'range' || undefined}>
|
|
120
|
+
<thead>
|
|
121
|
+
<tr>
|
|
122
|
+
{#each weekdays as w (w.long)}<th scope="col" abbr={w.long}>{w.short}</th>{/each}
|
|
123
|
+
</tr>
|
|
124
|
+
</thead>
|
|
125
|
+
<tbody>
|
|
126
|
+
{#each weeks as week (week[0])}
|
|
127
|
+
<tr>
|
|
128
|
+
{#each week as d (d)}
|
|
129
|
+
{@const outside = d.slice(0, 7) !== month}
|
|
130
|
+
<td
|
|
131
|
+
aria-selected={selected(d)}
|
|
132
|
+
class:start={range?.start === d && !!(range.end || hovered)}
|
|
133
|
+
class:end={range?.end === d || (!range?.end && hovered === d && range?.start && range.start < d)}
|
|
134
|
+
class:between={between(d)}
|
|
135
|
+
>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
data-date={d}
|
|
139
|
+
tabindex={d === focused ? 0 : -1}
|
|
140
|
+
class="day"
|
|
141
|
+
class:outside
|
|
142
|
+
class:selected={selected(d)}
|
|
143
|
+
aria-label={format(d, locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
|
144
|
+
aria-current={d === now ? 'date' : undefined}
|
|
145
|
+
aria-disabled={blocked(d) || undefined}
|
|
146
|
+
onclick={() => pick(d)}
|
|
147
|
+
onfocus={() => (focused = d)}
|
|
148
|
+
onpointerenter={() => (hovered = d)}
|
|
149
|
+
onpointerleave={() => (hovered = null)}
|
|
150
|
+
{onkeydown}
|
|
151
|
+
>
|
|
152
|
+
{Number(d.slice(8))}
|
|
153
|
+
</button>
|
|
154
|
+
</td>
|
|
155
|
+
{/each}
|
|
156
|
+
</tr>
|
|
157
|
+
{/each}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<style>
|
|
163
|
+
.tern-calendar {
|
|
164
|
+
display: inline-grid;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
color: var(--tern-fg);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
}
|
|
169
|
+
.header {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: space-between;
|
|
173
|
+
}
|
|
174
|
+
.title {
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
}
|
|
177
|
+
.nav {
|
|
178
|
+
display: grid;
|
|
179
|
+
place-items: center;
|
|
180
|
+
width: 2rem;
|
|
181
|
+
height: 2rem;
|
|
182
|
+
color: var(--tern-muted);
|
|
183
|
+
background: transparent;
|
|
184
|
+
border: 1px solid var(--tern-border);
|
|
185
|
+
border-radius: var(--tern-radius-sm);
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
}
|
|
188
|
+
.nav:hover:not(:disabled) {
|
|
189
|
+
color: var(--tern-fg);
|
|
190
|
+
background: var(--tern-surface-2);
|
|
191
|
+
}
|
|
192
|
+
.nav:disabled {
|
|
193
|
+
opacity: 0.4;
|
|
194
|
+
cursor: not-allowed;
|
|
195
|
+
}
|
|
196
|
+
.nav:focus-visible {
|
|
197
|
+
outline: 2px solid var(--tern-ring);
|
|
198
|
+
outline-offset: 1px;
|
|
199
|
+
}
|
|
200
|
+
table {
|
|
201
|
+
border-collapse: collapse;
|
|
202
|
+
}
|
|
203
|
+
th {
|
|
204
|
+
width: 2.25rem;
|
|
205
|
+
padding-bottom: 0.25rem;
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
font-weight: 500;
|
|
208
|
+
color: var(--tern-muted);
|
|
209
|
+
}
|
|
210
|
+
td {
|
|
211
|
+
padding: 1px 0;
|
|
212
|
+
}
|
|
213
|
+
td.between,
|
|
214
|
+
td.start,
|
|
215
|
+
td.end {
|
|
216
|
+
background: var(--tern-accent-soft);
|
|
217
|
+
}
|
|
218
|
+
td.start {
|
|
219
|
+
border-radius: 50% 0 0 50%;
|
|
220
|
+
}
|
|
221
|
+
td.end {
|
|
222
|
+
border-radius: 0 50% 50% 0;
|
|
223
|
+
}
|
|
224
|
+
td.start.end {
|
|
225
|
+
border-radius: 50%;
|
|
226
|
+
}
|
|
227
|
+
.day {
|
|
228
|
+
display: grid;
|
|
229
|
+
place-items: center;
|
|
230
|
+
width: 2.25rem;
|
|
231
|
+
height: 2.25rem;
|
|
232
|
+
padding: 0;
|
|
233
|
+
font: inherit;
|
|
234
|
+
font-variant-numeric: tabular-nums;
|
|
235
|
+
color: var(--tern-fg);
|
|
236
|
+
background: transparent;
|
|
237
|
+
border: 0;
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
}
|
|
241
|
+
.day:hover:not([aria-disabled='true']):not(.selected) {
|
|
242
|
+
background: var(--tern-surface-2);
|
|
243
|
+
}
|
|
244
|
+
.day.outside {
|
|
245
|
+
color: var(--tern-muted);
|
|
246
|
+
}
|
|
247
|
+
.day[aria-current='date'] {
|
|
248
|
+
font-weight: 700;
|
|
249
|
+
box-shadow: inset 0 0 0 1px var(--tern-border-strong);
|
|
250
|
+
}
|
|
251
|
+
.day.selected {
|
|
252
|
+
color: var(--tern-accent-fg);
|
|
253
|
+
background: var(--tern-accent);
|
|
254
|
+
}
|
|
255
|
+
.day[aria-disabled='true'] {
|
|
256
|
+
color: var(--tern-muted);
|
|
257
|
+
opacity: 0.45;
|
|
258
|
+
text-decoration: line-through;
|
|
259
|
+
cursor: not-allowed;
|
|
260
|
+
}
|
|
261
|
+
.day:focus-visible {
|
|
262
|
+
outline: 2px solid var(--tern-ring);
|
|
263
|
+
outline-offset: 1px;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ISODate } from './dates.js';
|
|
2
|
+
export type DateRange = {
|
|
3
|
+
start: ISODate | null;
|
|
4
|
+
end: ISODate | null;
|
|
5
|
+
};
|
|
6
|
+
type Props = {
|
|
7
|
+
/** single picks a day; range picks a start and an end. */
|
|
8
|
+
mode?: 'single' | 'range';
|
|
9
|
+
/** The chosen day (single) or { start, end } (range); bind it. */
|
|
10
|
+
value?: ISODate | null | DateRange;
|
|
11
|
+
/** The earliest and latest days that can be picked. */
|
|
12
|
+
min?: ISODate;
|
|
13
|
+
max?: ISODate;
|
|
14
|
+
/** Other days that can't be picked, such as weekends. */
|
|
15
|
+
isDisabled?: (date: ISODate) => boolean;
|
|
16
|
+
/** The locale for month and day names; the browser's by default. */
|
|
17
|
+
locale?: string;
|
|
18
|
+
/** The first day of the week, 0 for Sunday; the locale's by default. */
|
|
19
|
+
weekStart?: number;
|
|
20
|
+
/** Names the calendar for screen readers. */
|
|
21
|
+
label?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Runs when a day (single) or a whole range is picked. */
|
|
24
|
+
onselect?: (value: ISODate | DateRange) => void;
|
|
25
|
+
};
|
|
26
|
+
declare const Calendar: import("svelte").Component<Props, {}, "value">;
|
|
27
|
+
type Calendar = ReturnType<typeof Calendar>;
|
|
28
|
+
export default Calendar;
|
|
@@ -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,169 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
items: T[];
|
|
6
|
+
/** Names the carousel, such as "Featured projects". */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Draws one slide. */
|
|
9
|
+
slide: Snippet<[T, number]>;
|
|
10
|
+
/** Slides visible at once on wide screens. */
|
|
11
|
+
perView?: number;
|
|
12
|
+
/** Moves on by itself every this many milliseconds, with a pause button. Stops on hover and focus. */
|
|
13
|
+
autoplay?: number;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
let { items, label, slide, perView = 1, autoplay, class: klass = '' }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
let track = $state<HTMLDivElement>();
|
|
20
|
+
let current = $state(0);
|
|
21
|
+
// svelte-ignore state_referenced_locally
|
|
22
|
+
let playing = $state(!!autoplay);
|
|
23
|
+
let paused = $state(false); // by hover or focus
|
|
24
|
+
|
|
25
|
+
function go(i: number) {
|
|
26
|
+
const n = Math.max(0, items.length - perView);
|
|
27
|
+
current = ((i % (n + 1)) + n + 1) % (n + 1);
|
|
28
|
+
const el = track?.children[current] as HTMLElement | undefined;
|
|
29
|
+
track?.scrollTo({ left: el?.offsetLeft ?? 0, behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (!autoplay || !playing || paused) return;
|
|
34
|
+
const t = setInterval(() => go(current + 1), autoplay);
|
|
35
|
+
return () => clearInterval(t);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Keep current in step when the user scrolls or swipes.
|
|
39
|
+
function onscroll() {
|
|
40
|
+
if (!track) return;
|
|
41
|
+
const w = (track.children[0] as HTMLElement | undefined)?.offsetWidth || 1;
|
|
42
|
+
current = Math.round(track.scrollLeft / w);
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<section
|
|
47
|
+
class="tern-carousel {klass}"
|
|
48
|
+
aria-roledescription="carousel"
|
|
49
|
+
aria-label={label}
|
|
50
|
+
onpointerenter={() => (paused = true)}
|
|
51
|
+
onpointerleave={() => (paused = false)}
|
|
52
|
+
onfocusin={() => (paused = true)}
|
|
53
|
+
onfocusout={() => (paused = false)}
|
|
54
|
+
style:--per-view={perView}
|
|
55
|
+
>
|
|
56
|
+
<div class="controls">
|
|
57
|
+
{#if autoplay}
|
|
58
|
+
<button type="button" class="control" aria-label={playing ? 'Stop slides' : 'Start slides'} onclick={() => (playing = !playing)}>
|
|
59
|
+
{#if playing}
|
|
60
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M5 3.5v9M11 3.5v9" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
61
|
+
{:else}
|
|
62
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M5 3.5l7 4.5-7 4.5z" fill="currentColor" /></svg>
|
|
63
|
+
{/if}
|
|
64
|
+
</button>
|
|
65
|
+
{/if}
|
|
66
|
+
<button type="button" class="control" aria-label="Previous slide" aria-controls="tern-carousel-{uid}" onclick={() => go(current - 1)}>
|
|
67
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M10 4l-4 4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
68
|
+
</button>
|
|
69
|
+
<button type="button" class="control" aria-label="Next slide" aria-controls="tern-carousel-{uid}" onclick={() => go(current + 1)}>
|
|
70
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
74
|
+
<div
|
|
75
|
+
bind:this={track}
|
|
76
|
+
class="track"
|
|
77
|
+
id="tern-carousel-{uid}"
|
|
78
|
+
role="group"
|
|
79
|
+
aria-label="Slides"
|
|
80
|
+
tabindex="0"
|
|
81
|
+
aria-live={playing && !paused ? 'off' : 'polite'}
|
|
82
|
+
{onscroll}
|
|
83
|
+
onkeydown={(e) => {
|
|
84
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
go(current + (e.key === 'ArrowRight' ? 1 : -1));
|
|
87
|
+
}
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{#each items as item, i (i)}
|
|
91
|
+
<div class="slide" role="group" aria-roledescription="slide" aria-label="{i + 1} of {items.length}" inert={i < current || i >= current + perView ? true : undefined}>
|
|
92
|
+
{@render slide(item, i)}
|
|
93
|
+
</div>
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
96
|
+
<div class="dots" aria-hidden="true">
|
|
97
|
+
{#each { length: Math.max(1, items.length - perView + 1) }, i (i)}<span class:on={i === current}></span>{/each}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.tern-carousel {
|
|
103
|
+
display: grid;
|
|
104
|
+
gap: 0.75rem;
|
|
105
|
+
}
|
|
106
|
+
.controls {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: flex-end;
|
|
109
|
+
gap: 0.375rem;
|
|
110
|
+
}
|
|
111
|
+
.track {
|
|
112
|
+
display: flex;
|
|
113
|
+
gap: 1rem;
|
|
114
|
+
overflow-x: auto;
|
|
115
|
+
scroll-snap-type: x mandatory;
|
|
116
|
+
scrollbar-width: none;
|
|
117
|
+
border-radius: var(--tern-radius);
|
|
118
|
+
}
|
|
119
|
+
.track:focus-visible {
|
|
120
|
+
outline: 2px solid var(--tern-ring);
|
|
121
|
+
outline-offset: 2px;
|
|
122
|
+
}
|
|
123
|
+
.track::-webkit-scrollbar {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
.slide {
|
|
127
|
+
flex: 0 0 calc((100% - (var(--per-view) - 1) * 1rem) / var(--per-view));
|
|
128
|
+
scroll-snap-align: start;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
}
|
|
131
|
+
@media (max-width: 40rem) {
|
|
132
|
+
.slide {
|
|
133
|
+
flex-basis: 85%;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
.control {
|
|
137
|
+
display: grid;
|
|
138
|
+
place-items: center;
|
|
139
|
+
width: 2rem;
|
|
140
|
+
height: 2rem;
|
|
141
|
+
color: var(--tern-fg);
|
|
142
|
+
background: var(--tern-surface);
|
|
143
|
+
border: 1px solid var(--tern-border-strong);
|
|
144
|
+
border-radius: 50%;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
}
|
|
147
|
+
.control:hover {
|
|
148
|
+
background: var(--tern-surface-2);
|
|
149
|
+
}
|
|
150
|
+
.control:focus-visible {
|
|
151
|
+
outline: 2px solid var(--tern-ring);
|
|
152
|
+
outline-offset: 2px;
|
|
153
|
+
}
|
|
154
|
+
.dots {
|
|
155
|
+
display: flex;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
gap: 0.375rem;
|
|
158
|
+
}
|
|
159
|
+
.dots span {
|
|
160
|
+
width: 0.4rem;
|
|
161
|
+
height: 0.4rem;
|
|
162
|
+
border-radius: 50%;
|
|
163
|
+
background: var(--tern-border-strong);
|
|
164
|
+
transition: background var(--tern-duration);
|
|
165
|
+
}
|
|
166
|
+
.dots span.on {
|
|
167
|
+
background: var(--tern-accent);
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
declare function $$render<T>(): {
|
|
3
|
+
props: {
|
|
4
|
+
items: T[];
|
|
5
|
+
/** Names the carousel, such as "Featured projects". */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Draws one slide. */
|
|
8
|
+
slide: Snippet<[T, number]>;
|
|
9
|
+
/** Slides visible at once on wide screens. */
|
|
10
|
+
perView?: number;
|
|
11
|
+
/** Moves on by itself every this many milliseconds, with a pause button. Stops on hover and focus. */
|
|
12
|
+
autoplay?: number;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
exports: {};
|
|
16
|
+
bindings: "";
|
|
17
|
+
slots: {};
|
|
18
|
+
events: {};
|
|
19
|
+
};
|
|
20
|
+
declare class __sveltets_Render<T> {
|
|
21
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
22
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
23
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
24
|
+
bindings(): "";
|
|
25
|
+
exports(): {};
|
|
26
|
+
}
|
|
27
|
+
interface $$IsomorphicComponent {
|
|
28
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
29
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
30
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
31
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
32
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
33
|
+
}
|
|
34
|
+
declare const Carousel: $$IsomorphicComponent;
|
|
35
|
+
type Carousel<T> = InstanceType<typeof Carousel<T>>;
|
|
36
|
+
export default Carousel;
|