@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.
Files changed (170) hide show
  1. package/dist/goscript/index.svelte.d.ts +32 -2
  2. package/dist/goscript/index.svelte.js +257 -31
  3. package/dist/goscript/patch.d.ts +6 -0
  4. package/dist/goscript/patch.js +25 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +2 -1
  7. package/dist/runtime/validate.d.ts +31 -0
  8. package/dist/runtime/validate.js +168 -0
  9. package/dist/server/intl.js +55 -12
  10. package/dist/ui/Accordion.svelte +84 -0
  11. package/dist/ui/Accordion.svelte.d.ts +20 -0
  12. package/dist/ui/Alert.svelte +76 -0
  13. package/dist/ui/Alert.svelte.d.ts +12 -0
  14. package/dist/ui/AlertDialog.svelte +79 -0
  15. package/dist/ui/AlertDialog.svelte.d.ts +24 -0
  16. package/dist/ui/AreaChart.svelte +9 -0
  17. package/dist/ui/AreaChart.svelte.d.ts +26 -0
  18. package/dist/ui/AspectRatio.svelte +33 -0
  19. package/dist/ui/AspectRatio.svelte.d.ts +10 -0
  20. package/dist/ui/Avatar.svelte +48 -0
  21. package/dist/ui/Avatar.svelte.d.ts +10 -0
  22. package/dist/ui/Badge.svelte +54 -0
  23. package/dist/ui/Badge.svelte.d.ts +10 -0
  24. package/dist/ui/BarChart.svelte +170 -0
  25. package/dist/ui/BarChart.svelte.d.ts +40 -0
  26. package/dist/ui/Breadcrumb.svelte +92 -0
  27. package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
  28. package/dist/ui/Button.svelte +136 -0
  29. package/dist/ui/Button.svelte.d.ts +18 -0
  30. package/dist/ui/ButtonGroup.svelte +61 -0
  31. package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
  32. package/dist/ui/Calendar.svelte +265 -0
  33. package/dist/ui/Calendar.svelte.d.ts +28 -0
  34. package/dist/ui/Card.svelte +74 -0
  35. package/dist/ui/Card.svelte.d.ts +18 -0
  36. package/dist/ui/Carousel.svelte +169 -0
  37. package/dist/ui/Carousel.svelte.d.ts +36 -0
  38. package/dist/ui/ChartFrame.svelte +174 -0
  39. package/dist/ui/ChartFrame.svelte.d.ts +24 -0
  40. package/dist/ui/Checkbox.svelte +112 -0
  41. package/dist/ui/Checkbox.svelte.d.ts +14 -0
  42. package/dist/ui/Collapsible.svelte +59 -0
  43. package/dist/ui/Collapsible.svelte.d.ts +12 -0
  44. package/dist/ui/Combobox.svelte +350 -0
  45. package/dist/ui/Combobox.svelte.d.ts +24 -0
  46. package/dist/ui/Command.svelte +268 -0
  47. package/dist/ui/Command.svelte.d.ts +28 -0
  48. package/dist/ui/ConsentScreen.svelte +125 -0
  49. package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
  50. package/dist/ui/ContextMenu.svelte +78 -0
  51. package/dist/ui/ContextMenu.svelte.d.ts +13 -0
  52. package/dist/ui/DataTable.svelte +277 -0
  53. package/dist/ui/DataTable.svelte.d.ts +53 -0
  54. package/dist/ui/DatePicker.svelte +138 -0
  55. package/dist/ui/DatePicker.svelte.d.ts +25 -0
  56. package/dist/ui/Dialog.svelte +301 -0
  57. package/dist/ui/Dialog.svelte.d.ts +30 -0
  58. package/dist/ui/DonutChart.svelte +115 -0
  59. package/dist/ui/DonutChart.svelte.d.ts +40 -0
  60. package/dist/ui/Drawer.svelte +19 -0
  61. package/dist/ui/Drawer.svelte.d.ts +14 -0
  62. package/dist/ui/Empty.svelte +62 -0
  63. package/dist/ui/Empty.svelte.d.ts +15 -0
  64. package/dist/ui/Field.svelte +97 -0
  65. package/dist/ui/Field.svelte.d.ts +19 -0
  66. package/dist/ui/Form.svelte +159 -0
  67. package/dist/ui/Form.svelte.d.ts +21 -0
  68. package/dist/ui/HoverCard.svelte +106 -0
  69. package/dist/ui/HoverCard.svelte.d.ts +26 -0
  70. package/dist/ui/Input.svelte +62 -0
  71. package/dist/ui/Input.svelte.d.ts +7 -0
  72. package/dist/ui/InputGroup.svelte +80 -0
  73. package/dist/ui/InputGroup.svelte.d.ts +13 -0
  74. package/dist/ui/InputOTP.svelte +140 -0
  75. package/dist/ui/InputOTP.svelte.d.ts +18 -0
  76. package/dist/ui/Item.svelte +86 -0
  77. package/dist/ui/Item.svelte.d.ts +17 -0
  78. package/dist/ui/Kbd.svelte +24 -0
  79. package/dist/ui/Kbd.svelte.d.ts +7 -0
  80. package/dist/ui/Label.svelte +29 -0
  81. package/dist/ui/Label.svelte.d.ts +10 -0
  82. package/dist/ui/LineChart.svelte +171 -0
  83. package/dist/ui/LineChart.svelte.d.ts +42 -0
  84. package/dist/ui/Menu.svelte +103 -0
  85. package/dist/ui/Menu.svelte.d.ts +15 -0
  86. package/dist/ui/MenuList.svelte +317 -0
  87. package/dist/ui/MenuList.svelte.d.ts +29 -0
  88. package/dist/ui/Menubar.svelte +133 -0
  89. package/dist/ui/Menubar.svelte.d.ts +14 -0
  90. package/dist/ui/NavigationMenu.svelte +208 -0
  91. package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
  92. package/dist/ui/Pagination.svelte +114 -0
  93. package/dist/ui/Pagination.svelte.d.ts +16 -0
  94. package/dist/ui/PasswordInput.svelte +134 -0
  95. package/dist/ui/PasswordInput.svelte.d.ts +11 -0
  96. package/dist/ui/Popover.svelte +127 -0
  97. package/dist/ui/Popover.svelte.d.ts +29 -0
  98. package/dist/ui/Progress.svelte +73 -0
  99. package/dist/ui/Progress.svelte.d.ts +12 -0
  100. package/dist/ui/RadioGroup.svelte +105 -0
  101. package/dist/ui/RadioGroup.svelte.d.ts +21 -0
  102. package/dist/ui/Resizable.svelte +122 -0
  103. package/dist/ui/Resizable.svelte.d.ts +16 -0
  104. package/dist/ui/ScrollArea.svelte +43 -0
  105. package/dist/ui/ScrollArea.svelte.d.ts +13 -0
  106. package/dist/ui/Select.svelte +77 -0
  107. package/dist/ui/Select.svelte.d.ts +18 -0
  108. package/dist/ui/SelectMenu.svelte +301 -0
  109. package/dist/ui/SelectMenu.svelte.d.ts +18 -0
  110. package/dist/ui/Separator.svelte +19 -0
  111. package/dist/ui/Separator.svelte.d.ts +7 -0
  112. package/dist/ui/Sheet.svelte +21 -0
  113. package/dist/ui/Sheet.svelte.d.ts +16 -0
  114. package/dist/ui/Sidebar.svelte +263 -0
  115. package/dist/ui/Sidebar.svelte.d.ts +35 -0
  116. package/dist/ui/SignIn.svelte +182 -0
  117. package/dist/ui/SignIn.svelte.d.ts +38 -0
  118. package/dist/ui/SignUp.svelte +106 -0
  119. package/dist/ui/SignUp.svelte.d.ts +23 -0
  120. package/dist/ui/Skeleton.svelte +29 -0
  121. package/dist/ui/Skeleton.svelte.d.ts +9 -0
  122. package/dist/ui/Slider.svelte +196 -0
  123. package/dist/ui/Slider.svelte.d.ts +19 -0
  124. package/dist/ui/Sparkline.svelte +23 -0
  125. package/dist/ui/Sparkline.svelte.d.ts +11 -0
  126. package/dist/ui/Spinner.svelte +42 -0
  127. package/dist/ui/Spinner.svelte.d.ts +8 -0
  128. package/dist/ui/Stat.svelte +91 -0
  129. package/dist/ui/Stat.svelte.d.ts +18 -0
  130. package/dist/ui/Switch.svelte +90 -0
  131. package/dist/ui/Switch.svelte.d.ts +12 -0
  132. package/dist/ui/Table.svelte +70 -0
  133. package/dist/ui/Table.svelte.d.ts +36 -0
  134. package/dist/ui/Tabs.svelte +112 -0
  135. package/dist/ui/Tabs.svelte.d.ts +19 -0
  136. package/dist/ui/Textarea.svelte +52 -0
  137. package/dist/ui/Textarea.svelte.d.ts +7 -0
  138. package/dist/ui/Toaster.svelte +146 -0
  139. package/dist/ui/Toaster.svelte.d.ts +6 -0
  140. package/dist/ui/Toggle.svelte +82 -0
  141. package/dist/ui/Toggle.svelte.d.ts +17 -0
  142. package/dist/ui/ToggleGroup.svelte +143 -0
  143. package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
  144. package/dist/ui/Tooltip.svelte +85 -0
  145. package/dist/ui/Tooltip.svelte.d.ts +16 -0
  146. package/dist/ui/chart.d.ts +26 -0
  147. package/dist/ui/chart.js +89 -0
  148. package/dist/ui/dates.d.ts +13 -0
  149. package/dist/ui/dates.js +64 -0
  150. package/dist/ui/field.d.ts +18 -0
  151. package/dist/ui/field.js +8 -0
  152. package/dist/ui/floating.d.ts +44 -0
  153. package/dist/ui/floating.js +97 -0
  154. package/dist/ui/form.d.ts +12 -0
  155. package/dist/ui/form.js +8 -0
  156. package/dist/ui/index.d.ts +78 -0
  157. package/dist/ui/index.js +70 -0
  158. package/dist/ui/menu.d.ts +25 -0
  159. package/dist/ui/menu.js +1 -0
  160. package/dist/ui/options.d.ts +17 -0
  161. package/dist/ui/options.js +22 -0
  162. package/dist/ui/table.d.ts +26 -0
  163. package/dist/ui/table.js +1 -0
  164. package/dist/ui/theme.css +176 -0
  165. package/dist/ui/toast.svelte.d.ts +19 -0
  166. package/dist/ui/toast.svelte.js +18 -0
  167. package/dist/vite/goscript.d.ts +14 -0
  168. package/dist/vite/goscript.js +45 -16
  169. package/dist/vite/index.js +15 -4
  170. package/package.json +7 -1
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ type Props = {
3
+ /** The person's name: the image's alt text, and the initials when there is no image. */
4
+ name: string;
5
+ src?: string | null;
6
+ size?: string;
7
+ class?: string;
8
+ };
9
+ let { name, src = null, size = '2.25rem', class: klass = '' }: Props = $props();
10
+ let failed = $state(false);
11
+ const initials = $derived(
12
+ name
13
+ .split(/\s+/)
14
+ .filter(Boolean)
15
+ .slice(0, 2)
16
+ .map((w) => w[0]!.toUpperCase())
17
+ .join('')
18
+ );
19
+ </script>
20
+
21
+ <span class="tern-avatar {klass}" style:--size={size}>
22
+ {#if src && !failed}
23
+ <img {src} alt={name} onerror={() => (failed = true)} />
24
+ {:else}
25
+ <span role="img" aria-label={name}>{initials}</span>
26
+ {/if}
27
+ </span>
28
+
29
+ <style>
30
+ .tern-avatar {
31
+ display: inline-grid;
32
+ place-items: center;
33
+ width: var(--size);
34
+ height: var(--size);
35
+ border-radius: 50%;
36
+ overflow: hidden;
37
+ background: var(--tern-accent-soft);
38
+ color: var(--tern-accent);
39
+ font-size: calc(var(--size) * 0.38);
40
+ font-weight: 650;
41
+ flex: none;
42
+ }
43
+ img {
44
+ width: 100%;
45
+ height: 100%;
46
+ object-fit: cover;
47
+ }
48
+ </style>
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ /** The person's name: the image's alt text, and the initials when there is no image. */
3
+ name: string;
4
+ src?: string | null;
5
+ size?: string;
6
+ class?: string;
7
+ };
8
+ declare const Avatar: import("svelte").Component<Props, {}, "">;
9
+ type Avatar = ReturnType<typeof Avatar>;
10
+ export default Avatar;
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Props = { tone?: 'neutral' | 'accent' | 'success' | 'danger' | 'warning'; dot?: boolean; class?: string; children: Snippet };
5
+ let { tone = 'neutral', dot = false, class: klass = '', children }: Props = $props();
6
+ </script>
7
+
8
+ <span class="tern-badge {tone} {klass}">
9
+ {#if dot}<span class="dot" aria-hidden="true"></span>{/if}
10
+ {@render children()}
11
+ </span>
12
+
13
+ <style>
14
+ .tern-badge {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: 0.35rem;
18
+ height: 1.375rem;
19
+ padding: 0 0.55rem;
20
+ border-radius: 999px;
21
+ font-size: 0.75rem;
22
+ font-weight: 600;
23
+ line-height: 1;
24
+ white-space: nowrap;
25
+ border: 1px solid transparent;
26
+ }
27
+ .dot {
28
+ width: 0.4rem;
29
+ height: 0.4rem;
30
+ border-radius: 50%;
31
+ background: currentColor;
32
+ }
33
+ .neutral {
34
+ background: var(--tern-surface-2);
35
+ color: var(--tern-fg);
36
+ border-color: var(--tern-border);
37
+ }
38
+ .accent {
39
+ background: var(--tern-accent-soft);
40
+ color: var(--tern-accent);
41
+ }
42
+ .success {
43
+ background: var(--tern-success-soft);
44
+ color: color-mix(in oklch, var(--tern-success) 80%, var(--tern-fg));
45
+ }
46
+ .danger {
47
+ background: var(--tern-danger-soft);
48
+ color: color-mix(in oklch, var(--tern-danger) 85%, var(--tern-fg));
49
+ }
50
+ .warning {
51
+ background: var(--tern-warning-soft);
52
+ color: color-mix(in oklch, var(--tern-warning) 55%, var(--tern-fg));
53
+ }
54
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ tone?: 'neutral' | 'accent' | 'success' | 'danger' | 'warning';
4
+ dot?: boolean;
5
+ class?: string;
6
+ children: Snippet;
7
+ };
8
+ declare const Badge: import("svelte").Component<Props, {}, "">;
9
+ type Badge = ReturnType<typeof Badge>;
10
+ export default Badge;
@@ -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,136 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
+
5
+ type Common = {
6
+ /** primary for the main action, secondary for the others, ghost for toolbars, danger for destructive actions */
7
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
8
+ size?: 'sm' | 'md' | 'lg';
9
+ /** Shows a spinner and blocks clicks, while staying focusable. */
10
+ loading?: boolean;
11
+ children?: Snippet;
12
+ };
13
+ type Props = Common & ((HTMLButtonAttributes & { href?: undefined }) | (HTMLAnchorAttributes & { href: string }));
14
+
15
+ let { variant = 'primary', size = 'md', loading = false, children, class: klass = '', ...rest }: Props = $props();
16
+ </script>
17
+
18
+ {#if rest.href !== undefined}
19
+ <a class="tern-btn {variant} {size} {klass}" {...rest as HTMLAnchorAttributes}>{@render children?.()}</a>
20
+ {:else}
21
+ <button
22
+ class="tern-btn {variant} {size} {klass}"
23
+ type="button"
24
+ {...rest as HTMLButtonAttributes}
25
+ aria-busy={loading || undefined}
26
+ aria-disabled={loading || (rest as HTMLButtonAttributes).disabled || undefined}
27
+ onclick={(e) => {
28
+ if (loading) {
29
+ e.preventDefault();
30
+ return;
31
+ }
32
+ (rest as HTMLButtonAttributes).onclick?.(e as never);
33
+ }}
34
+ >
35
+ {#if loading}<span class="spinner" aria-hidden="true"></span>{/if}
36
+ {@render children?.()}
37
+ </button>
38
+ {/if}
39
+
40
+ <style>
41
+ .tern-btn {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ gap: 0.5em;
46
+ height: var(--tern-control-height);
47
+ padding: 0 1rem;
48
+ border-radius: var(--tern-radius-sm);
49
+ border: 1px solid transparent;
50
+ font: inherit;
51
+ font-size: 0.875rem;
52
+ font-weight: 550;
53
+ line-height: 1;
54
+ white-space: nowrap;
55
+ text-decoration: none;
56
+ cursor: pointer;
57
+ user-select: none;
58
+ transition:
59
+ background-color var(--tern-duration),
60
+ border-color var(--tern-duration),
61
+ box-shadow var(--tern-duration),
62
+ transform var(--tern-duration);
63
+ }
64
+ .tern-btn:focus-visible {
65
+ outline: 2px solid var(--tern-ring);
66
+ outline-offset: 2px;
67
+ }
68
+ .tern-btn:active:not([aria-disabled='true']) {
69
+ transform: translateY(0.5px);
70
+ }
71
+ .tern-btn[aria-disabled='true'],
72
+ .tern-btn:disabled {
73
+ opacity: 0.55;
74
+ cursor: not-allowed;
75
+ }
76
+ .tern-btn[aria-busy='true'] {
77
+ cursor: progress;
78
+ }
79
+ .sm {
80
+ height: calc(var(--tern-control-height) - 0.5rem);
81
+ padding: 0 0.7rem;
82
+ font-size: 0.8125rem;
83
+ }
84
+ .lg {
85
+ height: calc(var(--tern-control-height) + 0.5rem);
86
+ padding: 0 1.35rem;
87
+ font-size: 0.9375rem;
88
+ }
89
+ .primary {
90
+ background: var(--tern-accent);
91
+ color: var(--tern-accent-fg);
92
+ box-shadow:
93
+ var(--tern-shadow),
94
+ inset 0 1px 0 oklch(1 0 0 / 0.14);
95
+ }
96
+ .primary:hover:not([aria-disabled='true']) {
97
+ background: var(--tern-accent-hover);
98
+ }
99
+ .secondary {
100
+ background: var(--tern-surface);
101
+ color: var(--tern-fg);
102
+ border-color: var(--tern-border-strong);
103
+ box-shadow: var(--tern-shadow);
104
+ }
105
+ .secondary:hover:not([aria-disabled='true']) {
106
+ background: var(--tern-surface-2);
107
+ }
108
+ .ghost {
109
+ background: transparent;
110
+ color: var(--tern-fg);
111
+ }
112
+ .ghost:hover:not([aria-disabled='true']) {
113
+ background: var(--tern-surface-2);
114
+ }
115
+ .danger {
116
+ background: var(--tern-danger);
117
+ color: var(--tern-danger-fg);
118
+ box-shadow: var(--tern-shadow);
119
+ }
120
+ .danger:hover:not([aria-disabled='true']) {
121
+ background: var(--tern-danger-hover);
122
+ }
123
+ .spinner {
124
+ width: 1em;
125
+ height: 1em;
126
+ border-radius: 50%;
127
+ border: 2px solid currentColor;
128
+ border-right-color: transparent;
129
+ animation: spin 0.7s linear infinite;
130
+ }
131
+ @keyframes spin {
132
+ to {
133
+ transform: rotate(1turn);
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
+ type Common = {
4
+ /** primary for the main action, secondary for the others, ghost for toolbars, danger for destructive actions */
5
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
6
+ size?: 'sm' | 'md' | 'lg';
7
+ /** Shows a spinner and blocks clicks, while staying focusable. */
8
+ loading?: boolean;
9
+ children?: Snippet;
10
+ };
11
+ type Props = Common & ((HTMLButtonAttributes & {
12
+ href?: undefined;
13
+ }) | (HTMLAnchorAttributes & {
14
+ href: string;
15
+ }));
16
+ declare const Button: import("svelte").Component<Props, {}, "">;
17
+ type Button = ReturnType<typeof Button>;
18
+ export default Button;
@@ -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;