@ternkit/tern 1.3.0 → 1.5.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.
Files changed (131) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +2 -1
  3. package/dist/runtime/deferred.d.ts +5 -1
  4. package/dist/runtime/deferred.js +27 -11
  5. package/dist/runtime/validate.d.ts +31 -0
  6. package/dist/runtime/validate.js +168 -0
  7. package/dist/server/intl.js +55 -12
  8. package/dist/ui/AlertDialog.svelte +79 -0
  9. package/dist/ui/AlertDialog.svelte.d.ts +24 -0
  10. package/dist/ui/AreaChart.svelte +9 -0
  11. package/dist/ui/AreaChart.svelte.d.ts +26 -0
  12. package/dist/ui/AspectRatio.svelte +33 -0
  13. package/dist/ui/AspectRatio.svelte.d.ts +10 -0
  14. package/dist/ui/BarChart.svelte +170 -0
  15. package/dist/ui/BarChart.svelte.d.ts +40 -0
  16. package/dist/ui/Breadcrumb.svelte +92 -0
  17. package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
  18. package/dist/ui/ButtonGroup.svelte +61 -0
  19. package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
  20. package/dist/ui/Calendar.svelte +265 -0
  21. package/dist/ui/Calendar.svelte.d.ts +28 -0
  22. package/dist/ui/Carousel.svelte +169 -0
  23. package/dist/ui/Carousel.svelte.d.ts +36 -0
  24. package/dist/ui/ChartFrame.svelte +174 -0
  25. package/dist/ui/ChartFrame.svelte.d.ts +24 -0
  26. package/dist/ui/Checkbox.svelte +13 -1
  27. package/dist/ui/Collapsible.svelte +59 -0
  28. package/dist/ui/Collapsible.svelte.d.ts +12 -0
  29. package/dist/ui/Combobox.svelte +350 -0
  30. package/dist/ui/Combobox.svelte.d.ts +24 -0
  31. package/dist/ui/Command.svelte +268 -0
  32. package/dist/ui/Command.svelte.d.ts +28 -0
  33. package/dist/ui/ConsentScreen.svelte +125 -0
  34. package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
  35. package/dist/ui/ContextMenu.svelte +78 -0
  36. package/dist/ui/ContextMenu.svelte.d.ts +13 -0
  37. package/dist/ui/DataTable.svelte +277 -0
  38. package/dist/ui/DataTable.svelte.d.ts +53 -0
  39. package/dist/ui/DatePicker.svelte +138 -0
  40. package/dist/ui/DatePicker.svelte.d.ts +25 -0
  41. package/dist/ui/Dialog.svelte +168 -27
  42. package/dist/ui/Dialog.svelte.d.ts +10 -0
  43. package/dist/ui/DonutChart.svelte +115 -0
  44. package/dist/ui/DonutChart.svelte.d.ts +40 -0
  45. package/dist/ui/Drawer.svelte +19 -0
  46. package/dist/ui/Drawer.svelte.d.ts +14 -0
  47. package/dist/ui/Empty.svelte +62 -0
  48. package/dist/ui/Empty.svelte.d.ts +15 -0
  49. package/dist/ui/Field.svelte +22 -2
  50. package/dist/ui/Field.svelte.d.ts +2 -0
  51. package/dist/ui/Form.svelte +159 -0
  52. package/dist/ui/Form.svelte.d.ts +21 -0
  53. package/dist/ui/HoverCard.svelte +106 -0
  54. package/dist/ui/HoverCard.svelte.d.ts +26 -0
  55. package/dist/ui/Input.svelte +2 -1
  56. package/dist/ui/InputGroup.svelte +80 -0
  57. package/dist/ui/InputGroup.svelte.d.ts +13 -0
  58. package/dist/ui/InputOTP.svelte +140 -0
  59. package/dist/ui/InputOTP.svelte.d.ts +18 -0
  60. package/dist/ui/Item.svelte +86 -0
  61. package/dist/ui/Item.svelte.d.ts +17 -0
  62. package/dist/ui/Label.svelte +29 -0
  63. package/dist/ui/Label.svelte.d.ts +10 -0
  64. package/dist/ui/LineChart.svelte +171 -0
  65. package/dist/ui/LineChart.svelte.d.ts +42 -0
  66. package/dist/ui/Menu.svelte +22 -151
  67. package/dist/ui/Menu.svelte.d.ts +3 -10
  68. package/dist/ui/MenuList.svelte +317 -0
  69. package/dist/ui/MenuList.svelte.d.ts +29 -0
  70. package/dist/ui/Menubar.svelte +133 -0
  71. package/dist/ui/Menubar.svelte.d.ts +14 -0
  72. package/dist/ui/NavigationMenu.svelte +208 -0
  73. package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
  74. package/dist/ui/Pagination.svelte +114 -0
  75. package/dist/ui/Pagination.svelte.d.ts +16 -0
  76. package/dist/ui/PasswordInput.svelte +134 -0
  77. package/dist/ui/PasswordInput.svelte.d.ts +11 -0
  78. package/dist/ui/Popover.svelte +127 -0
  79. package/dist/ui/Popover.svelte.d.ts +29 -0
  80. package/dist/ui/Resizable.svelte +122 -0
  81. package/dist/ui/Resizable.svelte.d.ts +16 -0
  82. package/dist/ui/ScrollArea.svelte +43 -0
  83. package/dist/ui/ScrollArea.svelte.d.ts +13 -0
  84. package/dist/ui/Select.svelte +3 -2
  85. package/dist/ui/SelectMenu.svelte +301 -0
  86. package/dist/ui/SelectMenu.svelte.d.ts +18 -0
  87. package/dist/ui/Sheet.svelte +21 -0
  88. package/dist/ui/Sheet.svelte.d.ts +16 -0
  89. package/dist/ui/Sidebar.svelte +263 -0
  90. package/dist/ui/Sidebar.svelte.d.ts +35 -0
  91. package/dist/ui/SignIn.svelte +182 -0
  92. package/dist/ui/SignIn.svelte.d.ts +38 -0
  93. package/dist/ui/SignUp.svelte +106 -0
  94. package/dist/ui/SignUp.svelte.d.ts +23 -0
  95. package/dist/ui/Slider.svelte +196 -0
  96. package/dist/ui/Slider.svelte.d.ts +19 -0
  97. package/dist/ui/Sparkline.svelte +23 -0
  98. package/dist/ui/Sparkline.svelte.d.ts +11 -0
  99. package/dist/ui/Stat.svelte +91 -0
  100. package/dist/ui/Stat.svelte.d.ts +18 -0
  101. package/dist/ui/Table.svelte +70 -0
  102. package/dist/ui/Table.svelte.d.ts +36 -0
  103. package/dist/ui/Textarea.svelte +2 -1
  104. package/dist/ui/Toggle.svelte +82 -0
  105. package/dist/ui/Toggle.svelte.d.ts +17 -0
  106. package/dist/ui/ToggleGroup.svelte +143 -0
  107. package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
  108. package/dist/ui/Tooltip.svelte +27 -19
  109. package/dist/ui/Tooltip.svelte.d.ts +2 -1
  110. package/dist/ui/chart.d.ts +26 -0
  111. package/dist/ui/chart.js +89 -0
  112. package/dist/ui/dates.d.ts +13 -0
  113. package/dist/ui/dates.js +64 -0
  114. package/dist/ui/field.d.ts +8 -0
  115. package/dist/ui/floating.d.ts +44 -0
  116. package/dist/ui/floating.js +97 -0
  117. package/dist/ui/form.d.ts +12 -0
  118. package/dist/ui/form.js +8 -0
  119. package/dist/ui/index.d.ts +54 -0
  120. package/dist/ui/index.js +43 -0
  121. package/dist/ui/menu.d.ts +25 -0
  122. package/dist/ui/menu.js +1 -0
  123. package/dist/ui/options.d.ts +17 -0
  124. package/dist/ui/options.js +22 -0
  125. package/dist/ui/table.d.ts +26 -0
  126. package/dist/ui/table.js +1 -0
  127. package/dist/ui/theme.css +25 -0
  128. package/dist/vite/index.js +12 -3
  129. package/dist/vite/native.d.ts +3 -0
  130. package/dist/vite/native.js +122 -0
  131. package/package.json +3 -2
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Props = {
5
+ title: string;
6
+ description?: string;
7
+ /** Makes the whole item a link; actions stay separate buttons. */
8
+ href?: string;
9
+ /** An avatar, icon or thumbnail at the start. */
10
+ media?: Snippet;
11
+ /** Buttons or a badge at the end. */
12
+ actions?: Snippet;
13
+ /** li inside a ul or ol, div elsewhere. */
14
+ as?: 'div' | 'li';
15
+ class?: string;
16
+ };
17
+ let { title, description, href, media, actions, as = 'div', class: klass = '' }: Props = $props();
18
+ </script>
19
+
20
+ <!-- A row with media, a title, a description and actions, for lists of people, files or settings. -->
21
+ <svelte:element this={as} class="tern-item {klass}" class:link={!!href}>
22
+ {#if media}<div class="media">{@render media()}</div>{/if}
23
+ <div class="text">
24
+ {#if href}<a {href} class="title">{title}</a>{:else}<span class="title">{title}</span>{/if}
25
+ {#if description}<span class="description">{description}</span>{/if}
26
+ </div>
27
+ {#if actions}<div class="actions">{@render actions()}</div>{/if}
28
+ </svelte:element>
29
+
30
+ <style>
31
+ .tern-item {
32
+ position: relative;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 0.75rem;
36
+ padding: 0.75rem;
37
+ border-radius: var(--tern-radius-sm);
38
+ list-style: none;
39
+ }
40
+ .link:hover {
41
+ background: var(--tern-surface-2);
42
+ }
43
+ .text {
44
+ display: grid;
45
+ flex: 1;
46
+ min-width: 0;
47
+ }
48
+ .title {
49
+ font-weight: 550;
50
+ color: var(--tern-fg);
51
+ text-decoration: none;
52
+ overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ white-space: nowrap;
55
+ }
56
+ /* The link covers the item, so it's one big target. */
57
+ a.title::after {
58
+ content: '';
59
+ position: absolute;
60
+ inset: 0;
61
+ border-radius: inherit;
62
+ }
63
+ a.title:focus-visible::after {
64
+ outline: 2px solid var(--tern-ring);
65
+ outline-offset: -2px;
66
+ }
67
+ a.title:focus-visible {
68
+ outline: none;
69
+ }
70
+ .description {
71
+ font-size: 0.8125rem;
72
+ color: var(--tern-muted);
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ }
77
+ .actions {
78
+ position: relative;
79
+ z-index: 1;
80
+ display: flex;
81
+ gap: 0.375rem;
82
+ }
83
+ .media {
84
+ flex: none;
85
+ }
86
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ title: string;
4
+ description?: string;
5
+ /** Makes the whole item a link; actions stay separate buttons. */
6
+ href?: string;
7
+ /** An avatar, icon or thumbnail at the start. */
8
+ media?: Snippet;
9
+ /** Buttons or a badge at the end. */
10
+ actions?: Snippet;
11
+ /** li inside a ul or ol, div elsewhere. */
12
+ as?: 'div' | 'li';
13
+ class?: string;
14
+ };
15
+ declare const Item: import("svelte").Component<Props, {}, "">;
16
+ type Item = ReturnType<typeof Item>;
17
+ export default Item;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLLabelAttributes } from 'svelte/elements';
4
+
5
+ type Props = HTMLLabelAttributes & {
6
+ /** Marks the control as required, with an asterisk screen readers skip (the control says it's required). */
7
+ required?: boolean;
8
+ children: Snippet;
9
+ };
10
+ let { required = false, class: klass = '', children, ...rest }: Props = $props();
11
+ </script>
12
+
13
+ <!-- A label on its own, for layouts Field doesn't fit. Point it at the control with for. -->
14
+ <label class="tern-label {klass}" {...rest}>
15
+ {@render children()}
16
+ {#if required}<span class="required" aria-hidden="true">*</span>{/if}
17
+ </label>
18
+
19
+ <style>
20
+ .tern-label {
21
+ font-size: 0.875rem;
22
+ font-weight: 550;
23
+ color: var(--tern-fg);
24
+ }
25
+ .required {
26
+ color: var(--tern-danger);
27
+ margin-inline-start: 0.125rem;
28
+ }
29
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLLabelAttributes } from 'svelte/elements';
3
+ type Props = HTMLLabelAttributes & {
4
+ /** Marks the control as required, with an asterisk screen readers skip (the control says it's required). */
5
+ required?: boolean;
6
+ children: Snippet;
7
+ };
8
+ declare const Label: import("svelte").Component<Props, {}, "">;
9
+ type Label = ReturnType<typeof Label>;
10
+ export default Label;
@@ -0,0 +1,171 @@
1
+ <script lang="ts" generics="Row extends object">
2
+ import ChartFrame from './ChartFrame.svelte';
3
+ import { lastIndex, formatNumber, formatX, isTime, niceTicks, numericX, seriesColor, textWidth, timeTicks, type Series, type XValue } from './chart.js';
4
+
5
+ type Props = {
6
+ data: Row[];
7
+ /** The field on the x axis: dates (Date or ISO strings), numbers or categories. */
8
+ x: keyof Row & string;
9
+ /** Up to eight series; each is a field of numbers. */
10
+ series: Series<Row>[];
11
+ title: string;
12
+ description?: string;
13
+ level?: 2 | 3 | 4;
14
+ /** Fill under the lines. */
15
+ area?: boolean;
16
+ /** Start the y axis at zero. Turn it off for values far from zero, such as a temperature. */
17
+ zero?: boolean;
18
+ height?: number;
19
+ /** Formats y values on the axis, in tooltips and in the table. */
20
+ format?: (v: number) => string;
21
+ class?: string;
22
+ };
23
+ let { data, x, series, title, description, level, area = false, zero = true, height = 240, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
24
+
25
+ let width = $state(640);
26
+ let active = $state<number | null>(null);
27
+
28
+ const field = (r: Row, k: string) => (r as Record<string, unknown>)[k];
29
+ const xs = $derived(data.map((r) => field(r, x) as XValue));
30
+ const time = $derived(isTime(xs));
31
+ const nx = $derived(numericX(xs));
32
+ const values = $derived(
33
+ series.map((s) =>
34
+ data.map((r) => {
35
+ const v = field(r, s.key);
36
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
37
+ })
38
+ )
39
+ );
40
+ const all = $derived(values.flat().filter((v): v is number => v !== null));
41
+ const yTicks = $derived(niceTicks(zero || area ? Math.min(0, ...all) : Math.min(...all), Math.max(zero || area ? 0 : -Infinity, ...all), Math.max(3, Math.round(height / 60))));
42
+ const yMin = $derived(yTicks[0]!);
43
+ const yMax = $derived(yTicks[yTicks.length - 1]!);
44
+
45
+ // Direct labels at the line ends for up to four series, unless they'd collide.
46
+ const top = 10;
47
+ const bottom = 24;
48
+ const left = $derived(Math.max(...yTicks.map((t) => textWidth(format(t)))) + 10);
49
+ const ends = $derived(values.map((v) => [...v].reverse().find((n) => n !== null) ?? null));
50
+ const endY = (v: number) => top + (1 - (v - yMin) / (yMax - yMin || 1)) * (height - top - bottom);
51
+ const direct = $derived.by(() => {
52
+ if (series.length > 4) return false;
53
+ const ys = ends.filter((e): e is number => e !== null).map(endY).sort((a, b) => a - b);
54
+ return ys.every((y, i) => i === 0 || y - ys[i - 1]! >= 14);
55
+ });
56
+ const endLabels = $derived(series.map((s, i) => (series.length === 1 ? (ends[i] === null ? '' : format(ends[i]!)) : s.label)));
57
+ const right = $derived(direct ? Math.max(...endLabels.map(textWidth)) + 14 : 8);
58
+
59
+ const plotW = $derived(Math.max(10, width - left - right));
60
+ const plotH = $derived(height - top - bottom);
61
+ const xMin = $derived(nx ? Math.min(...nx) : 0);
62
+ const xMax = $derived(nx ? Math.max(...nx) : Math.max(1, data.length - 1));
63
+ const px = (i: number) => left + (((nx ? nx[i]! : i) - xMin) / (xMax - xMin || 1)) * plotW;
64
+ const py = (v: number) => top + (1 - (v - yMin) / (yMax - yMin || 1)) * plotH;
65
+
66
+ const xTicks = $derived.by(() => {
67
+ if (time && nx) {
68
+ const t = timeTicks(xMin, xMax, Math.max(2, Math.floor(plotW / 90)));
69
+ return t.ticks.map((v) => ({ at: left + ((v - xMin) / (xMax - xMin || 1)) * plotW, label: t.format(v) }));
70
+ }
71
+ if (nx) return niceTicks(xMin, xMax, Math.max(2, Math.floor(plotW / 80))).filter((v) => v >= xMin && v <= xMax).map((v) => ({ at: left + ((v - xMin) / (xMax - xMin || 1)) * plotW, label: formatNumber(v) }));
72
+ const every = Math.max(1, Math.ceil(data.length / Math.max(2, Math.floor(plotW / 70))));
73
+ return xs.map((v, i) => ({ at: px(i), label: String(v), i })).filter((t) => t.i % every === 0);
74
+ });
75
+
76
+ function path(v: (number | null)[]) {
77
+ let d = '';
78
+ let pen = false;
79
+ v.forEach((n, i) => {
80
+ if (n === null) return void (pen = false);
81
+ d += `${pen ? 'L' : 'M'}${px(i).toFixed(1)},${py(n).toFixed(1)}`;
82
+ pen = true;
83
+ });
84
+ return d;
85
+ }
86
+ function areaPath(v: (number | null)[]) {
87
+ const base = py(Math.max(yMin, Math.min(0, yMax)));
88
+ const runs: string[] = [];
89
+ let run: number[] = [];
90
+ const flush = () => {
91
+ if (run.length) runs.push(`M${px(run[0]!)},${base}` + run.map((i) => `L${px(i).toFixed(1)},${py(v[i]!).toFixed(1)}`).join('') + `L${px(run[run.length - 1]!)},${base}Z`);
92
+ run = [];
93
+ };
94
+ v.forEach((n, i) => (n === null ? flush() : run.push(i)));
95
+ flush();
96
+ return runs.join('');
97
+ }
98
+
99
+ function nearest(clientX: number, el: Element) {
100
+ const r = el.getBoundingClientRect();
101
+ const at = ((clientX - r.left) / r.width) * width;
102
+ let best = 0;
103
+ for (let i = 1; i < data.length; i++) if (Math.abs(px(i) - at) < Math.abs(px(best) - at)) best = i;
104
+ return best;
105
+ }
106
+ function onkeydown(e: KeyboardEvent) {
107
+ const last = data.length - 1;
108
+ const moves: Record<string, number> = { ArrowRight: Math.min(last, (active ?? -1) + 1), ArrowLeft: Math.max(0, (active ?? last + 1) - 1), Home: 0, End: last };
109
+ if (e.key in moves) {
110
+ e.preventDefault();
111
+ active = moves[e.key]!;
112
+ } else if (e.key === 'Escape') active = null;
113
+ }
114
+
115
+ const describe = (i: number) => `${formatX(xs[i]!, time)}: ` + series.map((s, k) => `${s.label} ${values[k]![i] === null ? 'no data' : format(values[k]![i]!)}`).join(', ');
116
+ const table = $derived({
117
+ headers: [x, ...series.map((s) => s.label)],
118
+ rows: data.map((_, i) => [formatX(xs[i]!, time), ...values.map((v) => (v[i] === null ? '–' : format(v[i]!)))])
119
+ });
120
+ </script>
121
+
122
+ <ChartFrame {title} {description} {level} class={klass} legend={series.map((s, i) => ({ label: s.label, color: seriesColor(i), kind: area ? 'rect' : 'line' }))} {table} readout={active === null ? '' : describe(active)}>
123
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
124
+ <div
125
+ class="tern-plot"
126
+ bind:clientWidth={width}
127
+ role="group"
128
+ aria-roledescription="chart"
129
+ aria-label="{title}. Use the arrow keys to read values."
130
+ tabindex="0"
131
+ onpointermove={(e) => (active = nearest(e.clientX, e.currentTarget))}
132
+ onpointerleave={() => (active = null)}
133
+ onfocus={() => active === null && (active = data.length - 1)}
134
+ onblur={() => (active = null)}
135
+ {onkeydown}
136
+ >
137
+ <svg {width} {height} viewBox="0 0 {width} {height}" aria-hidden="true">
138
+ {#each yTicks as t (t)}
139
+ <line class="grid" x1={left} x2={left + plotW} y1={py(t)} y2={py(t)} />
140
+ <text class="tick" x={left - 8} y={py(t)} dy="0.32em" text-anchor="end">{format(t)}</text>
141
+ {/each}
142
+ {#each xTicks as t, i (i)}
143
+ <text class="tick" x={t.at} y={height - 6} text-anchor={i === 0 && t.at - left < 20 ? 'start' : 'middle'}>{t.label}</text>
144
+ {/each}
145
+ {#each values as v, k (series[k]!.key)}
146
+ {#if area}<path d={areaPath(v)} fill={seriesColor(k)} fill-opacity="0.1" />{/if}
147
+ <path d={path(v)} fill="none" stroke={seriesColor(k)} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
148
+ {/each}
149
+ {#if active !== null}
150
+ <line class="grid" x1={px(active)} x2={px(active)} y1={top} y2={top + plotH} style="stroke: var(--tern-border-strong)" />
151
+ {/if}
152
+ {#each values as v, k (series[k]!.key)}
153
+ {@const i = active ?? lastIndex(v, (n) => n !== null)}
154
+ {#if i >= 0 && v[i] !== null}
155
+ <circle cx={px(i)} cy={py(v[i]!)} r="4" fill={seriesColor(k)} stroke="var(--tern-surface)" stroke-width="2" />
156
+ {/if}
157
+ {#if direct && ends[k] !== null}
158
+ <text class="direct" x={left + plotW + 10} y={py(ends[k]!)} dy="0.32em">{endLabels[k]}</text>
159
+ {/if}
160
+ {/each}
161
+ </svg>
162
+ {#if active !== null}
163
+ <div class="tooltip" style:left="{Math.min(px(active) + 12, width - 170)}px" style:top="{top}px">
164
+ <div class="head">{formatX(xs[active]!, time)}</div>
165
+ {#each series as s, k (s.key)}
166
+ <div class="row"><span class="swatch" style:background={seriesColor(k)}></span><strong>{values[k]![active] === null ? '–' : format(values[k]![active]!)}</strong><span>{s.label}</span></div>
167
+ {/each}
168
+ </div>
169
+ {/if}
170
+ </div>
171
+ </ChartFrame>
@@ -0,0 +1,42 @@
1
+ import { type Series } from './chart.js';
2
+ declare function $$render<Row extends object>(): {
3
+ props: {
4
+ data: Row[];
5
+ /** The field on the x axis: dates (Date or ISO strings), numbers or categories. */
6
+ x: keyof Row & string;
7
+ /** Up to eight series; each is a field of numbers. */
8
+ series: Series<Row>[];
9
+ title: string;
10
+ description?: string;
11
+ level?: 2 | 3 | 4;
12
+ /** Fill under the lines. */
13
+ area?: boolean;
14
+ /** Start the y axis at zero. Turn it off for values far from zero, such as a temperature. */
15
+ zero?: boolean;
16
+ height?: number;
17
+ /** Formats y values on the axis, in tooltips and in the table. */
18
+ format?: (v: number) => string;
19
+ class?: string;
20
+ };
21
+ exports: {};
22
+ bindings: "";
23
+ slots: {};
24
+ events: {};
25
+ };
26
+ declare class __sveltets_Render<Row extends object> {
27
+ props(): ReturnType<typeof $$render<Row>>['props'];
28
+ events(): ReturnType<typeof $$render<Row>>['events'];
29
+ slots(): ReturnType<typeof $$render<Row>>['slots'];
30
+ bindings(): "";
31
+ exports(): {};
32
+ }
33
+ interface $$IsomorphicComponent {
34
+ 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']>> & {
35
+ $$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
36
+ } & ReturnType<__sveltets_Render<Row>['exports']>;
37
+ <Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
38
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
39
+ }
40
+ declare const LineChart: $$IsomorphicComponent;
41
+ type LineChart<Row extends object> = InstanceType<typeof LineChart<Row>>;
42
+ export default LineChart;
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import MenuList from './MenuList.svelte';
4
+ import type { MenuItem } from './menu.js';
3
5
 
4
- type Item = { label: string; onselect?: () => void; href?: string; danger?: boolean; disabled?: boolean; separator?: boolean };
5
6
  type Props = {
6
- /** The items; an item with separator: true draws a line. */
7
- items: Item[];
7
+ /** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
8
+ items: MenuItem[];
8
9
  /** The button's content. */
9
10
  children: Snippet;
10
11
  /** Opens to the start or end edge of the button. */
@@ -16,75 +17,13 @@
16
17
 
17
18
  const uid = $props.id();
18
19
  let open = $state(false);
20
+ let focus = $state<'first' | 'last'>('first');
19
21
  let button = $state<HTMLButtonElement>();
20
- let menu = $state<HTMLDivElement>();
21
- let typed = '';
22
- let typedTimer: ReturnType<typeof setTimeout> | undefined;
23
22
 
24
- const entries = () => Array.from(menu?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])') ?? []);
25
-
26
- async function show(focus: 'first' | 'last') {
23
+ function show(where: 'first' | 'last') {
24
+ focus = where;
27
25
  open = true;
28
- await Promise.resolve();
29
- requestAnimationFrame(() => {
30
- const list = entries();
31
- (focus === 'first' ? list[0] : list[list.length - 1])?.focus();
32
- });
33
- }
34
-
35
- function hide(returnFocus = true) {
36
- open = false;
37
- if (returnFocus) button?.focus();
38
- }
39
-
40
- function select(item: Item) {
41
- if (item.disabled) return;
42
- hide();
43
- item.onselect?.();
44
- }
45
-
46
- // The ARIA menu button pattern.
47
- function onmenukey(e: KeyboardEvent) {
48
- const list = entries();
49
- const i = list.indexOf(document.activeElement as HTMLElement);
50
- const move = (j: number) => {
51
- e.preventDefault();
52
- list[(j + list.length) % list.length]?.focus();
53
- };
54
- switch (e.key) {
55
- case 'ArrowDown':
56
- return move(i + 1);
57
- case 'ArrowUp':
58
- return move(i - 1);
59
- case 'Home':
60
- return move(0);
61
- case 'End':
62
- return move(list.length - 1);
63
- case 'Escape':
64
- e.preventDefault();
65
- return hide();
66
- case 'Tab':
67
- return hide(false);
68
- }
69
- if (e.key.length === 1 && /\S/.test(e.key)) {
70
- // Type to jump to an item.
71
- typed += e.key.toLowerCase();
72
- clearTimeout(typedTimer);
73
- typedTimer = setTimeout(() => (typed = ''), 500);
74
- const start = typed.length === 1 ? i + 1 : i;
75
- const order = [...list.slice(start), ...list.slice(0, start)];
76
- order.find((el) => el.textContent?.trim().toLowerCase().startsWith(typed))?.focus();
77
- }
78
26
  }
79
-
80
- $effect(() => {
81
- if (!open) return;
82
- const away = (e: PointerEvent) => {
83
- if (!menu?.contains(e.target as Node) && !button?.contains(e.target as Node)) hide(false);
84
- };
85
- document.addEventListener('pointerdown', away);
86
- return () => document.removeEventListener('pointerdown', away);
87
- });
88
27
  </script>
89
28
 
90
29
  <div class="tern-menu {klass}">
@@ -96,7 +35,7 @@
96
35
  aria-haspopup="menu"
97
36
  aria-expanded={open}
98
37
  aria-controls={open ? `tern-menu-${uid}` : undefined}
99
- onclick={() => (open ? hide() : show('first'))}
38
+ onclick={() => (open ? (open = false) : show('first'))}
100
39
  onkeydown={(e) => {
101
40
  if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
102
41
  e.preventDefault();
@@ -110,38 +49,25 @@
110
49
  {@render children()}
111
50
  <svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
112
51
  </button>
113
- {#if open}
114
- <div bind:this={menu} class="menu {align}" role="menu" id="tern-menu-{uid}" aria-labelledby="tern-menu-{uid}-button" tabindex="-1" onkeydown={onmenukey}>
115
- {#each items as item, i (i)}
116
- {#if item.separator}
117
- <div class="separator" role="separator"></div>
118
- {:else if item.href && !item.disabled}
119
- <a role="menuitem" tabindex="-1" class:danger={item.danger} href={item.href} onclick={() => hide(false)}>{item.label}</a>
120
- {:else}
121
- <div
122
- role="menuitem"
123
- tabindex="-1"
124
- class:danger={item.danger}
125
- aria-disabled={item.disabled || undefined}
126
- onclick={() => select(item)}
127
- onkeydown={(e) => {
128
- if (e.key === 'Enter' || e.key === ' ') {
129
- e.preventDefault();
130
- select(item);
131
- }
132
- }}
133
- >
134
- {item.label}
135
- </div>
136
- {/if}
137
- {/each}
138
- </div>
52
+ {#if open && button}
53
+ <MenuList
54
+ {items}
55
+ id="tern-menu-{uid}"
56
+ labelledby="tern-menu-{uid}-button"
57
+ anchor={button}
58
+ {align}
59
+ {focus}
60
+ inside={() => [button]}
61
+ onclose={(how) => {
62
+ open = false;
63
+ if (how.returnFocus) button?.focus();
64
+ }}
65
+ />
139
66
  {/if}
140
67
  </div>
141
68
 
142
69
  <style>
143
70
  .tern-menu {
144
- position: relative;
145
71
  display: inline-block;
146
72
  }
147
73
  .trigger {
@@ -174,59 +100,4 @@
174
100
  outline: 2px solid var(--tern-ring);
175
101
  outline-offset: 2px;
176
102
  }
177
- .menu {
178
- position: absolute;
179
- top: calc(100% + 0.375rem);
180
- z-index: 50;
181
- min-width: 11rem;
182
- padding: 0.3rem;
183
- background: var(--tern-surface);
184
- border: 1px solid var(--tern-border);
185
- border-radius: var(--tern-radius);
186
- box-shadow: var(--tern-shadow-lg);
187
- animation: pop var(--tern-duration) ease-out;
188
- }
189
- .start {
190
- left: 0;
191
- }
192
- .end {
193
- right: 0;
194
- }
195
- [role='menuitem'] {
196
- display: block;
197
- padding: 0.45rem 0.65rem;
198
- border-radius: calc(var(--tern-radius-sm) - 2px);
199
- font-size: 0.875rem;
200
- color: var(--tern-fg);
201
- text-decoration: none;
202
- cursor: pointer;
203
- outline: none;
204
- }
205
- [role='menuitem']:hover,
206
- [role='menuitem']:focus {
207
- background: var(--tern-accent-soft);
208
- }
209
- [role='menuitem'].danger {
210
- color: var(--tern-danger);
211
- }
212
- [role='menuitem'].danger:hover,
213
- [role='menuitem'].danger:focus {
214
- background: var(--tern-danger-soft);
215
- }
216
- [role='menuitem'][aria-disabled='true'] {
217
- opacity: 0.45;
218
- cursor: not-allowed;
219
- background: none;
220
- }
221
- .separator {
222
- height: 1px;
223
- margin: 0.3rem 0.2rem;
224
- background: var(--tern-border);
225
- }
226
- @keyframes pop {
227
- from {
228
- opacity: 0;
229
- transform: translateY(-4px);
230
- }
231
- }
232
103
  </style>
@@ -1,15 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
- type Item = {
3
- label: string;
4
- onselect?: () => void;
5
- href?: string;
6
- danger?: boolean;
7
- disabled?: boolean;
8
- separator?: boolean;
9
- };
2
+ import type { MenuItem } from './menu.js';
10
3
  type Props = {
11
- /** The items; an item with separator: true draws a line. */
12
- items: Item[];
4
+ /** The items. Items can have submenus, checkboxes, shortcuts and group headings. */
5
+ items: MenuItem[];
13
6
  /** The button's content. */
14
7
  children: Snippet;
15
8
  /** Opens to the start or end edge of the button. */