@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,170 @@
|
|
|
1
|
+
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { lastIndex, formatNumber, niceTicks, seriesColor, textWidth, type Series } from './chart.js';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
data: Row[];
|
|
7
|
+
/** The field naming each bar's category. */
|
|
8
|
+
x: keyof Row & string;
|
|
9
|
+
series: Series<Row>[];
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
level?: 2 | 3 | 4;
|
|
13
|
+
/** Stack the series instead of grouping them side by side. */
|
|
14
|
+
stacked?: boolean;
|
|
15
|
+
/** Bars grow sideways, for long category names or many categories. */
|
|
16
|
+
horizontal?: boolean;
|
|
17
|
+
height?: number;
|
|
18
|
+
format?: (v: number) => string;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
let { data, x, series, title, description, level, stacked = false, horizontal = false, height, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
|
|
22
|
+
|
|
23
|
+
let width = $state(640);
|
|
24
|
+
let active = $state<number | null>(null);
|
|
25
|
+
|
|
26
|
+
const field = (r: Row, k: string) => (r as Record<string, unknown>)[k];
|
|
27
|
+
const cats = $derived(data.map((r) => String(field(r, x) ?? '')));
|
|
28
|
+
const values = $derived(
|
|
29
|
+
data.map((r) =>
|
|
30
|
+
series.map((s) => {
|
|
31
|
+
const v = field(r, s.key);
|
|
32
|
+
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, v) : 0;
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
const totals = $derived(values.map((v) => (stacked ? v.reduce((a, b) => a + b, 0) : Math.max(0, ...v))));
|
|
37
|
+
const h = $derived(height ?? (horizontal ? Math.max(120, data.length * (stacked || series.length === 1 ? 32 : 18 * series.length + 14) + 30) : 240));
|
|
38
|
+
const ticks = $derived(niceTicks(0, Math.max(0, ...totals), horizontal ? Math.max(2, Math.floor(width / 110)) : Math.max(3, Math.round(h / 60))));
|
|
39
|
+
const max = $derived(ticks[ticks.length - 1]!);
|
|
40
|
+
|
|
41
|
+
const top = 10;
|
|
42
|
+
const labelW = $derived(horizontal ? Math.min(180, Math.max(...cats.map(textWidth)) + 12) : Math.max(...ticks.map((t) => textWidth(format(t)))) + 10);
|
|
43
|
+
const left = $derived(labelW);
|
|
44
|
+
const right = $derived(horizontal && series.length === 1 ? Math.max(...totals.map((t) => textWidth(format(t)))) + 10 : 12);
|
|
45
|
+
const bottom = $derived(horizontal ? 22 : 24);
|
|
46
|
+
const plotW = $derived(Math.max(10, width - left - right));
|
|
47
|
+
const plotH = $derived(h - top - bottom);
|
|
48
|
+
const band = $derived((horizontal ? plotH : plotW) / Math.max(1, data.length));
|
|
49
|
+
const groups = $derived(stacked ? 1 : series.length);
|
|
50
|
+
// Bars are at most 24px thick; the rest of the band is air.
|
|
51
|
+
const thick = $derived(Math.max(2, Math.min(24, (band * 0.7 - (groups - 1) * 2) / groups)));
|
|
52
|
+
const scale = (v: number) => (v / (max || 1)) * (horizontal ? plotW : plotH);
|
|
53
|
+
|
|
54
|
+
type Rect = { s: number; x: number; y: number; w: number; h: number; end: boolean };
|
|
55
|
+
const rects = $derived(
|
|
56
|
+
values.map((v, i) => {
|
|
57
|
+
const out: Rect[] = [];
|
|
58
|
+
const groupW = groups * thick + (groups - 1) * 2;
|
|
59
|
+
const start = (horizontal ? top : left) + i * band + (band - groupW) / 2;
|
|
60
|
+
let acc = 0;
|
|
61
|
+
const topSeries = lastIndex(v, (n) => n > 0);
|
|
62
|
+
v.forEach((n, s) => {
|
|
63
|
+
if (n <= 0) return;
|
|
64
|
+
const len = scale(n);
|
|
65
|
+
const pos = stacked ? start : start + s * (thick + 2);
|
|
66
|
+
const from = scale(acc) + (stacked && acc > 0 ? 2 : 0);
|
|
67
|
+
const l = Math.max(0, len - (stacked && acc > 0 ? 2 : 0));
|
|
68
|
+
if (horizontal) out.push({ s, x: left + from, y: pos, w: l, h: thick, end: !stacked || s === topSeries });
|
|
69
|
+
else out.push({ s, x: pos, y: top + plotH - from - l, w: thick, h: l, end: !stacked || s === topSeries });
|
|
70
|
+
if (stacked) acc += n;
|
|
71
|
+
});
|
|
72
|
+
return out;
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// A bar with a 4px rounded data end and a square baseline end.
|
|
77
|
+
function barPath(r: Rect) {
|
|
78
|
+
const rad = r.end ? Math.min(4, horizontal ? r.h / 2 : r.w / 2, horizontal ? r.w : r.h) : 0;
|
|
79
|
+
if (horizontal) return `M${r.x},${r.y}H${r.x + r.w - rad}Q${r.x + r.w},${r.y} ${r.x + r.w},${r.y + rad}V${r.y + r.h - rad}Q${r.x + r.w},${r.y + r.h} ${r.x + r.w - rad},${r.y + r.h}H${r.x}Z`;
|
|
80
|
+
return `M${r.x},${r.y + r.h}V${r.y + rad}Q${r.x},${r.y} ${r.x + rad},${r.y}H${r.x + r.w - rad}Q${r.x + r.w},${r.y} ${r.x + r.w},${r.y + rad}V${r.y + r.h}Z`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onkeydown(e: KeyboardEvent) {
|
|
84
|
+
const last = data.length - 1;
|
|
85
|
+
const next = horizontal ? 'ArrowDown' : 'ArrowRight';
|
|
86
|
+
const prev = horizontal ? 'ArrowUp' : 'ArrowLeft';
|
|
87
|
+
const moves: Record<string, number> = { [next]: Math.min(last, (active ?? -1) + 1), [prev]: Math.max(0, (active ?? last + 1) - 1), Home: 0, End: last };
|
|
88
|
+
if (e.key in moves) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
active = moves[e.key]!;
|
|
91
|
+
} else if (e.key === 'Escape') active = null;
|
|
92
|
+
}
|
|
93
|
+
function hit(e: PointerEvent, el: Element) {
|
|
94
|
+
const r = el.getBoundingClientRect();
|
|
95
|
+
const at = horizontal ? ((e.clientY - r.top) / r.height) * h - top : ((e.clientX - r.left) / r.width) * width - left;
|
|
96
|
+
const i = Math.floor(at / band);
|
|
97
|
+
active = i >= 0 && i < data.length ? i : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const describe = (i: number) => `${cats[i]}: ` + series.map((s, k) => `${s.label} ${format(values[i]![k]!)}`).join(', ') + (stacked && series.length > 1 ? `, total ${format(totals[i]!)}` : '');
|
|
101
|
+
const table = $derived({
|
|
102
|
+
headers: [x, ...series.map((s) => s.label), ...(stacked && series.length > 1 ? ['Total'] : [])],
|
|
103
|
+
rows: data.map((_, i) => [cats[i]!, ...values[i]!.map(format), ...(stacked && series.length > 1 ? [format(totals[i]!)] : [])])
|
|
104
|
+
});
|
|
105
|
+
const labelEvery = $derived(horizontal ? 1 : Math.max(1, Math.ceil(data.length / Math.max(2, Math.floor(plotW / 64)))));
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<ChartFrame {title} {description} {level} class={klass} legend={series.map((s, i) => ({ label: s.label, color: seriesColor(i), kind: 'rect' }))} {table} readout={active === null ? '' : describe(active)}>
|
|
109
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
110
|
+
<div
|
|
111
|
+
class="tern-plot"
|
|
112
|
+
bind:clientWidth={width}
|
|
113
|
+
role="group"
|
|
114
|
+
aria-roledescription="chart"
|
|
115
|
+
aria-label="{title}. Use the arrow keys to read values."
|
|
116
|
+
tabindex="0"
|
|
117
|
+
onpointermove={(e) => hit(e, e.currentTarget)}
|
|
118
|
+
onpointerleave={() => (active = null)}
|
|
119
|
+
onfocus={() => active === null && (active = 0)}
|
|
120
|
+
onblur={() => (active = null)}
|
|
121
|
+
{onkeydown}
|
|
122
|
+
>
|
|
123
|
+
<svg {width} height={h} viewBox="0 0 {width} {h}" aria-hidden="true">
|
|
124
|
+
{#each ticks as t (t)}
|
|
125
|
+
{#if horizontal}
|
|
126
|
+
<line class="grid" x1={left + scale(t)} x2={left + scale(t)} y1={top} y2={top + plotH} />
|
|
127
|
+
<text class="tick" x={left + scale(t)} y={h - 6} text-anchor="middle">{format(t)}</text>
|
|
128
|
+
{:else}
|
|
129
|
+
<line class="grid" x1={left} x2={left + plotW} y1={top + plotH - scale(t)} y2={top + plotH - scale(t)} />
|
|
130
|
+
<text class="tick" x={left - 8} y={top + plotH - scale(t)} dy="0.32em" text-anchor="end">{format(t)}</text>
|
|
131
|
+
{/if}
|
|
132
|
+
{/each}
|
|
133
|
+
{#each cats as c, i (i)}
|
|
134
|
+
{#if active === i}
|
|
135
|
+
<rect x={horizontal ? left : left + i * band} y={horizontal ? top + i * band : top} width={horizontal ? plotW : band} height={horizontal ? band : plotH} fill="var(--tern-surface-2)" />
|
|
136
|
+
{/if}
|
|
137
|
+
{#if horizontal}
|
|
138
|
+
<text class="tick" x={left - 8} y={top + i * band + band / 2} dy="0.32em" text-anchor="end">{c.length > 26 ? c.slice(0, 25) + '…' : c}</text>
|
|
139
|
+
{:else if i % labelEvery === 0}
|
|
140
|
+
<text class="tick" x={left + i * band + band / 2} y={h - 6} text-anchor="middle">{c}</text>
|
|
141
|
+
{/if}
|
|
142
|
+
{/each}
|
|
143
|
+
{#each rects as group, i (i)}
|
|
144
|
+
{#each group as r (r.s)}
|
|
145
|
+
<path d={barPath(r)} fill={seriesColor(r.s)} opacity={active === null || active === i ? 1 : 0.55} />
|
|
146
|
+
{/each}
|
|
147
|
+
{#if series.length === 1 && group[0]}
|
|
148
|
+
{#if horizontal}
|
|
149
|
+
<text class="direct" x={group[0].x + group[0].w + 6} y={group[0].y + group[0].h / 2} dy="0.32em">{format(totals[i]!)}</text>
|
|
150
|
+
{:else if data.length <= 12 && band > textWidth(format(totals[i]!))}
|
|
151
|
+
<text class="direct" x={group[0].x + group[0].w / 2} y={group[0].y - 6} text-anchor="middle">{format(totals[i]!)}</text>
|
|
152
|
+
{/if}
|
|
153
|
+
{/if}
|
|
154
|
+
{/each}
|
|
155
|
+
</svg>
|
|
156
|
+
{#if active !== null}
|
|
157
|
+
<div
|
|
158
|
+
class="tooltip"
|
|
159
|
+
style:left="{horizontal ? Math.min(left + scale(totals[active]!) + 12, width - 170) : Math.min(left + (active + 1) * band, width - 170)}px"
|
|
160
|
+
style:top="{horizontal ? top + active * band : top}px"
|
|
161
|
+
>
|
|
162
|
+
<div class="head">{cats[active]}</div>
|
|
163
|
+
{#each series as s, k (s.key)}
|
|
164
|
+
<div class="row"><span class="swatch" style:background={seriesColor(k)}></span><strong>{format(values[active]![k]!)}</strong><span>{s.label}</span></div>
|
|
165
|
+
{/each}
|
|
166
|
+
{#if stacked && series.length > 1}<div class="row"><strong>{format(totals[active]!)}</strong><span>Total</span></div>{/if}
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
</ChartFrame>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Series } from './chart.js';
|
|
2
|
+
declare function $$render<Row extends object>(): {
|
|
3
|
+
props: {
|
|
4
|
+
data: Row[];
|
|
5
|
+
/** The field naming each bar's category. */
|
|
6
|
+
x: keyof Row & string;
|
|
7
|
+
series: Series<Row>[];
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
level?: 2 | 3 | 4;
|
|
11
|
+
/** Stack the series instead of grouping them side by side. */
|
|
12
|
+
stacked?: boolean;
|
|
13
|
+
/** Bars grow sideways, for long category names or many categories. */
|
|
14
|
+
horizontal?: boolean;
|
|
15
|
+
height?: number;
|
|
16
|
+
format?: (v: number) => string;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
exports: {};
|
|
20
|
+
bindings: "";
|
|
21
|
+
slots: {};
|
|
22
|
+
events: {};
|
|
23
|
+
};
|
|
24
|
+
declare class __sveltets_Render<Row extends object> {
|
|
25
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
26
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
27
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
28
|
+
bindings(): "";
|
|
29
|
+
exports(): {};
|
|
30
|
+
}
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
new <Row extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Row>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Row>['props']>, ReturnType<__sveltets_Render<Row>['events']>, ReturnType<__sveltets_Render<Row>['slots']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
35
|
+
<Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
declare const BarChart: $$IsomorphicComponent;
|
|
39
|
+
type BarChart<Row extends object> = InstanceType<typeof BarChart<Row>>;
|
|
40
|
+
export default BarChart;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Menu from './Menu.svelte';
|
|
3
|
+
|
|
4
|
+
type Crumb = { label: string; href?: string };
|
|
5
|
+
type Props = {
|
|
6
|
+
/** From the top level down to the current page, which is last. */
|
|
7
|
+
items: Crumb[];
|
|
8
|
+
/** With more items than this, the middle ones move into a menu. */
|
|
9
|
+
max?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
};
|
|
12
|
+
let { items, max = 4, class: klass = '' }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const collapsed = $derived(items.length > max);
|
|
15
|
+
const head = $derived(collapsed ? items.slice(0, 1) : items);
|
|
16
|
+
const middle = $derived(collapsed ? items.slice(1, items.length - (max - 2)) : []);
|
|
17
|
+
const tail = $derived(collapsed ? items.slice(items.length - (max - 2)) : []);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#snippet crumb(item: Crumb, last: boolean)}
|
|
21
|
+
<li>
|
|
22
|
+
{#if last}
|
|
23
|
+
<span aria-current="page">{item.label}</span>
|
|
24
|
+
{:else if item.href}
|
|
25
|
+
<a href={item.href}>{item.label}</a>
|
|
26
|
+
{:else}
|
|
27
|
+
<span>{item.label}</span>
|
|
28
|
+
{/if}
|
|
29
|
+
</li>
|
|
30
|
+
{/snippet}
|
|
31
|
+
|
|
32
|
+
<nav class="tern-breadcrumb {klass}" aria-label="Breadcrumb">
|
|
33
|
+
<ol>
|
|
34
|
+
{#each head as item, i (i)}{@render crumb(item, !collapsed && i === items.length - 1)}{/each}
|
|
35
|
+
{#if collapsed}
|
|
36
|
+
<li>
|
|
37
|
+
<Menu variant="ghost" items={middle.map((m) => ({ label: m.label, href: m.href }))} class="more">
|
|
38
|
+
<span class="tern-sr-only">Show {middle.length} more</span><span aria-hidden="true">…</span>
|
|
39
|
+
</Menu>
|
|
40
|
+
</li>
|
|
41
|
+
{#each tail as item, i (i)}{@render crumb(item, i === tail.length - 1)}{/each}
|
|
42
|
+
{/if}
|
|
43
|
+
</ol>
|
|
44
|
+
</nav>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
ol {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 0.25rem;
|
|
52
|
+
margin: 0;
|
|
53
|
+
padding: 0;
|
|
54
|
+
list-style: none;
|
|
55
|
+
font-size: 0.875rem;
|
|
56
|
+
color: var(--tern-muted);
|
|
57
|
+
}
|
|
58
|
+
li {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 0.25rem;
|
|
62
|
+
}
|
|
63
|
+
li + li::before {
|
|
64
|
+
content: '/';
|
|
65
|
+
margin-inline-end: 0.25rem;
|
|
66
|
+
color: var(--tern-border-strong);
|
|
67
|
+
}
|
|
68
|
+
a {
|
|
69
|
+
color: var(--tern-muted);
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
border-radius: 3px;
|
|
72
|
+
}
|
|
73
|
+
a:hover {
|
|
74
|
+
color: var(--tern-fg);
|
|
75
|
+
text-decoration: underline;
|
|
76
|
+
}
|
|
77
|
+
a:focus-visible {
|
|
78
|
+
outline: 2px solid var(--tern-ring);
|
|
79
|
+
outline-offset: 2px;
|
|
80
|
+
}
|
|
81
|
+
[aria-current='page'] {
|
|
82
|
+
color: var(--tern-fg);
|
|
83
|
+
font-weight: 550;
|
|
84
|
+
}
|
|
85
|
+
li :global(.more .trigger) {
|
|
86
|
+
height: 1.5rem;
|
|
87
|
+
padding: 0 0.4rem;
|
|
88
|
+
}
|
|
89
|
+
li :global(.more .trigger svg) {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Crumb = {
|
|
2
|
+
label: string;
|
|
3
|
+
href?: string;
|
|
4
|
+
};
|
|
5
|
+
type Props = {
|
|
6
|
+
/** From the top level down to the current page, which is last. */
|
|
7
|
+
items: Crumb[];
|
|
8
|
+
/** With more items than this, the middle ones move into a menu. */
|
|
9
|
+
max?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const Breadcrumb: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type Breadcrumb = ReturnType<typeof Breadcrumb>;
|
|
14
|
+
export default Breadcrumb;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** Names the group for screen readers, such as "Text alignment". */
|
|
6
|
+
label: string;
|
|
7
|
+
orientation?: 'horizontal' | 'vertical';
|
|
8
|
+
class?: string;
|
|
9
|
+
/** Buttons, Menus or Inputs, drawn as one joined control. */
|
|
10
|
+
children: Snippet;
|
|
11
|
+
};
|
|
12
|
+
let { label, orientation = 'horizontal', class: klass = '', children }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="tern-button-group {orientation} {klass}" role="group" aria-label={label}>
|
|
16
|
+
{@render children()}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.tern-button-group {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
isolation: isolate;
|
|
23
|
+
}
|
|
24
|
+
.vertical {
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
}
|
|
27
|
+
.tern-button-group > :global(*) {
|
|
28
|
+
position: relative;
|
|
29
|
+
}
|
|
30
|
+
.tern-button-group > :global(*:focus-visible),
|
|
31
|
+
.tern-button-group > :global(*:has(:focus-visible)) {
|
|
32
|
+
z-index: 1;
|
|
33
|
+
}
|
|
34
|
+
.horizontal > :global(* + *) {
|
|
35
|
+
margin-inline-start: -1px;
|
|
36
|
+
}
|
|
37
|
+
.vertical > :global(* + *) {
|
|
38
|
+
margin-block-start: -1px;
|
|
39
|
+
}
|
|
40
|
+
.horizontal > :global(:not(:first-child)),
|
|
41
|
+
.horizontal > :global(:not(:first-child) .trigger) {
|
|
42
|
+
border-start-start-radius: 0;
|
|
43
|
+
border-end-start-radius: 0;
|
|
44
|
+
}
|
|
45
|
+
.horizontal > :global(:not(:last-child)),
|
|
46
|
+
.horizontal > :global(:not(:last-child) .trigger) {
|
|
47
|
+
border-start-end-radius: 0;
|
|
48
|
+
border-end-end-radius: 0;
|
|
49
|
+
}
|
|
50
|
+
.vertical > :global(:not(:first-child)) {
|
|
51
|
+
border-start-start-radius: 0;
|
|
52
|
+
border-start-end-radius: 0;
|
|
53
|
+
}
|
|
54
|
+
.vertical > :global(:not(:last-child)) {
|
|
55
|
+
border-end-start-radius: 0;
|
|
56
|
+
border-end-end-radius: 0;
|
|
57
|
+
}
|
|
58
|
+
.tern-button-group > :global(.tern-btn) {
|
|
59
|
+
box-shadow: none;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Names the group for screen readers, such as "Text alignment". */
|
|
4
|
+
label: string;
|
|
5
|
+
orientation?: 'horizontal' | 'vertical';
|
|
6
|
+
class?: string;
|
|
7
|
+
/** Buttons, Menus or Inputs, drawn as one joined control. */
|
|
8
|
+
children: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const ButtonGroup: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type ButtonGroup = ReturnType<typeof ButtonGroup>;
|
|
12
|
+
export default ButtonGroup;
|
|
@@ -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;
|