@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,196 @@
1
+ <script lang="ts">
2
+ import { getField } from './field.js';
3
+
4
+ type Props = {
5
+ /** A number for one thumb, or [low, high] for a range; bind it. */
6
+ value?: number | [number, number];
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ /** Labels it when it isn't in a Field. */
11
+ label?: string;
12
+ /** Formats values for screen readers and the tooltip, such as (v) => `${v}%`. */
13
+ format?: (v: number) => string;
14
+ /** Posts the value (or both values) under this name. */
15
+ name?: string;
16
+ disabled?: boolean;
17
+ class?: string;
18
+ onchange?: (value: number | [number, number]) => void;
19
+ };
20
+ const field = getField();
21
+ let { value = $bindable(0), min = 0, max = 100, step = 1, label, format = String, name: nameProp, disabled = false, class: klass = '', onchange }: Props = $props();
22
+ const name = $derived(nameProp ?? field?.name);
23
+
24
+ const uid = $props.id();
25
+ field?.custom();
26
+ let track = $state<HTMLDivElement>();
27
+ const thumbs: HTMLElement[] = [];
28
+ let dragging = $state<number | null>(null);
29
+
30
+ const values = $derived<number[]>(Array.isArray(value) ? value : [value]);
31
+ const range = $derived(Array.isArray(value));
32
+ const off = $derived(disabled || !!field?.disabled);
33
+ const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;
34
+
35
+ function snap(v: number) {
36
+ const s = Math.round((v - min) / step) * step + min;
37
+ const decimals = (String(step).split('.')[1] ?? '').length;
38
+ return Math.min(max, Math.max(min, Number(s.toFixed(decimals))));
39
+ }
40
+
41
+ function set(i: number, v: number) {
42
+ v = snap(v);
43
+ if (range) {
44
+ const [lo, hi] = value as [number, number];
45
+ const next: [number, number] = i === 0 ? [Math.min(v, hi), hi] : [lo, Math.max(v, lo)];
46
+ if (next[0] === lo && next[1] === hi) return;
47
+ value = next;
48
+ } else {
49
+ if (v === value) return;
50
+ value = v;
51
+ }
52
+ onchange?.(value);
53
+ }
54
+
55
+ function fromPointer(e: PointerEvent) {
56
+ const r = track!.getBoundingClientRect();
57
+ return min + ((e.clientX - r.left) / r.width) * (max - min);
58
+ }
59
+
60
+ function down(e: PointerEvent) {
61
+ if (off || !track) return;
62
+ e.preventDefault();
63
+ const v = fromPointer(e);
64
+ // The nearest thumb moves; on a tie, the one that can go that way.
65
+ const i = range ? (Math.abs(v - values[0]!) < Math.abs(v - values[1]!) || (v < values[0]! && values[0] === values[1]) ? 0 : 1) : 0;
66
+ dragging = i;
67
+ set(i, v);
68
+ thumbs[i]?.focus();
69
+ track.setPointerCapture(e.pointerId);
70
+ }
71
+
72
+ function onkeydown(e: KeyboardEvent, i: number) {
73
+ const big = Math.max(step, (max - min) / 10);
74
+ const v = values[i]!;
75
+ const moves: Record<string, number> = {
76
+ ArrowRight: v + step,
77
+ ArrowUp: v + step,
78
+ ArrowLeft: v - step,
79
+ ArrowDown: v - step,
80
+ PageUp: v + big,
81
+ PageDown: v - big,
82
+ Home: min,
83
+ End: max
84
+ };
85
+ if (!(e.key in moves)) return;
86
+ e.preventDefault();
87
+ set(i, moves[e.key]!);
88
+ }
89
+ </script>
90
+
91
+ <div class="tern-slider {klass}" class:disabled={off}>
92
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
93
+ <div
94
+ bind:this={track}
95
+ class="track"
96
+ onpointerdown={down}
97
+ onpointermove={(e) => dragging !== null && set(dragging, fromPointer(e))}
98
+ onpointerup={() => (dragging = null)}
99
+ onpointercancel={() => (dragging = null)}
100
+ >
101
+ <div class="fill" style:left="{range ? pct(values[0]!) : 0}%" style:right="{100 - pct(values[values.length - 1]!)}%"></div>
102
+ {#each values as v, i (i)}
103
+ <span
104
+ bind:this={thumbs[i]}
105
+ class="thumb"
106
+ class:active={dragging === i}
107
+ id={i === 0 ? field?.id : undefined}
108
+ role="slider"
109
+ tabindex={off ? -1 : 0}
110
+ style:left="{pct(v)}%"
111
+ aria-valuemin={range && i === 1 ? values[0] : min}
112
+ aria-valuemax={range && i === 0 ? values[1] : max}
113
+ aria-valuenow={v}
114
+ aria-valuetext={format(v)}
115
+ aria-disabled={off || undefined}
116
+ aria-label={field || !label ? undefined : range ? `${label}, ${i === 0 ? 'minimum' : 'maximum'}` : label}
117
+ aria-labelledby={field ? (range ? `${field.labelId} tern-slider-${uid}-${i}` : field.labelId) : undefined}
118
+ aria-describedby={field?.describedBy}
119
+ onkeydown={(e) => onkeydown(e, i)}
120
+ >
121
+ <span class="value" aria-hidden="true">{format(v)}</span>
122
+ </span>
123
+ {#if range && field}<span class="tern-sr-only" id="tern-slider-{uid}-{i}">{i === 0 ? 'minimum' : 'maximum'}</span>{/if}
124
+ {/each}
125
+ </div>
126
+ {#if name}
127
+ {#each values as v, i (i)}<input type="hidden" {name} value={v} />{/each}
128
+ {/if}
129
+ </div>
130
+
131
+ <style>
132
+ .tern-slider {
133
+ padding: 0.6rem 0.5rem;
134
+ touch-action: none;
135
+ }
136
+ .track {
137
+ position: relative;
138
+ height: 0.375rem;
139
+ border-radius: 1rem;
140
+ background: var(--tern-surface-2);
141
+ box-shadow: inset 0 0 0 1px var(--tern-border);
142
+ cursor: pointer;
143
+ }
144
+ .fill {
145
+ position: absolute;
146
+ top: 0;
147
+ bottom: 0;
148
+ border-radius: inherit;
149
+ background: var(--tern-accent);
150
+ }
151
+ .thumb {
152
+ position: absolute;
153
+ top: 50%;
154
+ width: 1.125rem;
155
+ height: 1.125rem;
156
+ translate: -50% -50%;
157
+ border-radius: 50%;
158
+ background: var(--tern-surface);
159
+ border: 2px solid var(--tern-accent);
160
+ box-shadow: var(--tern-shadow);
161
+ cursor: grab;
162
+ outline: none;
163
+ transition: box-shadow var(--tern-duration);
164
+ }
165
+ .thumb:focus-visible,
166
+ .thumb.active {
167
+ box-shadow: 0 0 0 4px color-mix(in oklch, var(--tern-ring) 35%, transparent);
168
+ }
169
+ .value {
170
+ position: absolute;
171
+ bottom: calc(100% + 0.4rem);
172
+ left: 50%;
173
+ translate: -50% 0;
174
+ padding: 0.15rem 0.4rem;
175
+ border-radius: var(--tern-radius-sm);
176
+ background: var(--tern-fg);
177
+ color: var(--tern-bg);
178
+ font-size: 0.75rem;
179
+ white-space: nowrap;
180
+ opacity: 0;
181
+ pointer-events: none;
182
+ transition: opacity var(--tern-duration);
183
+ }
184
+ .thumb:focus-visible .value,
185
+ .thumb.active .value,
186
+ .thumb:hover .value {
187
+ opacity: 1;
188
+ }
189
+ .disabled {
190
+ opacity: 0.5;
191
+ }
192
+ .disabled .track,
193
+ .disabled .thumb {
194
+ cursor: not-allowed;
195
+ }
196
+ </style>
@@ -0,0 +1,19 @@
1
+ type Props = {
2
+ /** A number for one thumb, or [low, high] for a range; bind it. */
3
+ value?: number | [number, number];
4
+ min?: number;
5
+ max?: number;
6
+ step?: number;
7
+ /** Labels it when it isn't in a Field. */
8
+ label?: string;
9
+ /** Formats values for screen readers and the tooltip, such as (v) => `${v}%`. */
10
+ format?: (v: number) => string;
11
+ /** Posts the value (or both values) under this name. */
12
+ name?: string;
13
+ disabled?: boolean;
14
+ class?: string;
15
+ onchange?: (value: number | [number, number]) => void;
16
+ };
17
+ declare const Slider: import("svelte").Component<Props, {}, "value">;
18
+ type Slider = ReturnType<typeof Slider>;
19
+ export default Slider;
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ type Props = {
3
+ values: number[];
4
+ /** Describes the trend for screen readers, such as "Visits rose 12% this week". */
5
+ label: string;
6
+ width?: number;
7
+ height?: number;
8
+ class?: string;
9
+ };
10
+ let { values, label, width = 96, height = 28, class: klass = '' }: Props = $props();
11
+
12
+ const min = $derived(Math.min(...values));
13
+ const max = $derived(Math.max(...values));
14
+ const pts = $derived(values.map((v, i) => [2 + (i / Math.max(1, values.length - 1)) * (width - 6), 2 + (1 - (v - min) / (max - min || 1)) * (height - 4)] as const));
15
+ const d = $derived(pts.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(1)},${y.toFixed(1)}`).join(''));
16
+ const last = $derived(pts[pts.length - 1]);
17
+ </script>
18
+
19
+ <!-- A trend at a glance: the history in a muted line, the latest point in the accent. -->
20
+ <svg class="tern-sparkline {klass}" {width} {height} viewBox="0 0 {width} {height}" role="img" aria-label={label}>
21
+ <path {d} fill="none" stroke="var(--tern-border-strong)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
22
+ {#if last}<circle cx={last[0]} cy={last[1]} r="2.5" fill="var(--tern-accent)" />{/if}
23
+ </svg>
@@ -0,0 +1,11 @@
1
+ type Props = {
2
+ values: number[];
3
+ /** Describes the trend for screen readers, such as "Visits rose 12% this week". */
4
+ label: string;
5
+ width?: number;
6
+ height?: number;
7
+ class?: string;
8
+ };
9
+ declare const Sparkline: import("svelte").Component<Props, {}, "">;
10
+ type Sparkline = ReturnType<typeof Sparkline>;
11
+ export default Sparkline;
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ type Props = { label?: string; size?: string; class?: string };
3
+ let { label = 'Loading', size = '1.25rem', class: klass = '' }: Props = $props();
4
+ </script>
5
+
6
+ <span class="tern-spinner {klass}" role="status" style:--size={size}>
7
+ <span class="ring" aria-hidden="true"></span>
8
+ <span class="tern-sr-only">{label}</span>
9
+ </span>
10
+
11
+ <style>
12
+ .tern-spinner {
13
+ display: inline-flex;
14
+ color: var(--tern-accent);
15
+ }
16
+ .ring {
17
+ width: var(--size);
18
+ height: var(--size);
19
+ border-radius: 50%;
20
+ border: 2px solid color-mix(in oklch, currentColor 20%, transparent);
21
+ border-top-color: currentColor;
22
+ animation: spin 0.75s linear infinite;
23
+ }
24
+ .tern-sr-only {
25
+ position: absolute;
26
+ width: 1px;
27
+ height: 1px;
28
+ overflow: hidden;
29
+ clip: rect(0, 0, 0, 0);
30
+ white-space: nowrap;
31
+ }
32
+ @keyframes spin {
33
+ to {
34
+ transform: rotate(1turn);
35
+ }
36
+ }
37
+ @media (prefers-reduced-motion: reduce) {
38
+ .ring {
39
+ animation-duration: 2s;
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ label?: string;
3
+ size?: string;
4
+ class?: string;
5
+ };
6
+ declare const Spinner: import("svelte").Component<Props, {}, "">;
7
+ type Spinner = ReturnType<typeof Spinner>;
8
+ export default Spinner;
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ import Sparkline from './Sparkline.svelte';
3
+ import { formatNumber } from './chart.js';
4
+
5
+ type Props = {
6
+ /** What it counts, in sentence case, such as "Unique visitors". */
7
+ label: string;
8
+ value: number;
9
+ /** The change, in percent, against the period named in period. */
10
+ delta?: number;
11
+ /** Such as "vs last week". */
12
+ period?: string;
13
+ /** Whether up is good (visits) or bad (errors), which picks the delta's color. */
14
+ upIsGood?: boolean;
15
+ /** Recent values, drawn as a sparkline. */
16
+ trend?: number[];
17
+ format?: (v: number) => string;
18
+ class?: string;
19
+ };
20
+ let { label, value, delta, period = 'vs the previous period', upIsGood = true, trend, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
21
+
22
+ const good = $derived(delta === undefined || delta === 0 ? null : delta > 0 === upIsGood);
23
+ const deltaText = $derived(delta === undefined ? '' : `${delta > 0 ? '+' : delta < 0 ? '−' : ''}${Math.abs(Math.round(delta * 10) / 10)}%`);
24
+ </script>
25
+
26
+ <div class="tern-stat {klass}">
27
+ <div class="label">{label}</div>
28
+ <div class="value">{format(value)}</div>
29
+ <div class="foot">
30
+ {#if delta !== undefined}
31
+ <span class="delta" class:good={good === true} class:bad={good === false}>
32
+ <span aria-hidden="true">{delta > 0 ? '↑' : delta < 0 ? '↓' : '→'}</span>
33
+ <span class="tern-sr-only">{delta > 0 ? 'Up' : delta < 0 ? 'Down' : 'No change'}</span>
34
+ {deltaText}
35
+ </span>
36
+ <span class="period">{period}</span>
37
+ {/if}
38
+ {#if trend && trend.length > 1}
39
+ <Sparkline values={trend} label="{label}, trend over the last {trend.length} periods" class="spark" />
40
+ {/if}
41
+ </div>
42
+ </div>
43
+
44
+ <style>
45
+ .tern-stat {
46
+ display: grid;
47
+ gap: 0.25rem;
48
+ padding: 1rem 1.125rem;
49
+ background: var(--tern-surface);
50
+ border: 1px solid var(--tern-border);
51
+ border-radius: var(--tern-radius);
52
+ }
53
+ .label {
54
+ font-size: 0.8125rem;
55
+ font-weight: 550;
56
+ color: var(--tern-muted);
57
+ }
58
+ .value {
59
+ font-size: 1.75rem;
60
+ font-weight: 650;
61
+ letter-spacing: -0.02em;
62
+ font-variant-numeric: tabular-nums;
63
+ color: var(--tern-fg);
64
+ }
65
+ .foot {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.4rem;
69
+ min-height: 1.75rem;
70
+ font-size: 0.8125rem;
71
+ }
72
+ .delta {
73
+ display: inline-flex;
74
+ gap: 0.15rem;
75
+ font-weight: 600;
76
+ color: var(--tern-muted);
77
+ font-variant-numeric: tabular-nums;
78
+ }
79
+ .delta.good {
80
+ color: var(--tern-success);
81
+ }
82
+ .delta.bad {
83
+ color: var(--tern-danger);
84
+ }
85
+ .period {
86
+ color: var(--tern-muted);
87
+ }
88
+ .foot :global(.spark) {
89
+ margin-inline-start: auto;
90
+ }
91
+ </style>
@@ -0,0 +1,18 @@
1
+ type Props = {
2
+ /** What it counts, in sentence case, such as "Unique visitors". */
3
+ label: string;
4
+ value: number;
5
+ /** The change, in percent, against the period named in period. */
6
+ delta?: number;
7
+ /** Such as "vs last week". */
8
+ period?: string;
9
+ /** Whether up is good (visits) or bad (errors), which picks the delta's color. */
10
+ upIsGood?: boolean;
11
+ /** Recent values, drawn as a sparkline. */
12
+ trend?: number[];
13
+ format?: (v: number) => string;
14
+ class?: string;
15
+ };
16
+ declare const Stat: import("svelte").Component<Props, {}, "">;
17
+ type Stat = ReturnType<typeof Stat>;
18
+ export default Stat;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLButtonAttributes, 'role'> & {
6
+ checked?: boolean;
7
+ /** The label. */
8
+ children?: Snippet;
9
+ /** Submitted with a form as on/off under this name. */
10
+ name?: string;
11
+ };
12
+ let { checked = $bindable(false), children, name, disabled, class: klass = '', ...rest }: Props = $props();
13
+ const uid = $props.id();
14
+ </script>
15
+
16
+ <span class="tern-switch {klass}">
17
+ <button
18
+ type="button"
19
+ role="switch"
20
+ aria-checked={checked}
21
+ aria-labelledby={children ? `tern-switch-${uid}` : undefined}
22
+ {disabled}
23
+ {...rest}
24
+ onclick={(e) => {
25
+ checked = !checked;
26
+ rest.onclick?.(e as never);
27
+ }}
28
+ >
29
+ <span class="thumb" aria-hidden="true"></span>
30
+ </button>
31
+ {#if children}
32
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
33
+ <span class="label" id="tern-switch-{uid}" onclick={() => !disabled && (checked = !checked)}>{@render children()}</span>
34
+ {/if}
35
+ {#if name}<input type="hidden" {name} value={checked ? 'on' : 'off'} />{/if}
36
+ </span>
37
+
38
+ <style>
39
+ .tern-switch {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ gap: 0.6rem;
43
+ font-size: 0.875rem;
44
+ }
45
+ button {
46
+ position: relative;
47
+ flex: none;
48
+ width: 2.25rem;
49
+ height: 1.3rem;
50
+ padding: 0;
51
+ border-radius: 999px;
52
+ border: 1px solid var(--tern-border-strong);
53
+ background: var(--tern-surface-2);
54
+ cursor: pointer;
55
+ transition:
56
+ background-color var(--tern-duration),
57
+ border-color var(--tern-duration);
58
+ }
59
+ .thumb {
60
+ position: absolute;
61
+ top: 50%;
62
+ left: 0.14rem;
63
+ width: 0.95rem;
64
+ height: 0.95rem;
65
+ border-radius: 50%;
66
+ background: var(--tern-surface);
67
+ box-shadow: 0 1px 2px oklch(0 0 0 / 0.25);
68
+ translate: 0 -50%;
69
+ transition: left var(--tern-duration);
70
+ }
71
+ button[aria-checked='true'] {
72
+ background: var(--tern-accent);
73
+ border-color: var(--tern-accent);
74
+ }
75
+ button[aria-checked='true'] .thumb {
76
+ left: calc(100% - 0.14rem - 0.95rem);
77
+ }
78
+ button:focus-visible {
79
+ outline: 2px solid var(--tern-ring);
80
+ outline-offset: 2px;
81
+ }
82
+ button:disabled {
83
+ opacity: 0.55;
84
+ cursor: not-allowed;
85
+ }
86
+ .label {
87
+ cursor: pointer;
88
+ user-select: none;
89
+ }
90
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLButtonAttributes, 'role'> & {
4
+ checked?: boolean;
5
+ /** The label. */
6
+ children?: Snippet;
7
+ /** Submitted with a form as on/off under this name. */
8
+ name?: string;
9
+ };
10
+ declare const Switch: import("svelte").Component<Props, {}, "checked">;
11
+ type Switch = ReturnType<typeof Switch>;
12
+ export default Switch;
@@ -0,0 +1,70 @@
1
+ <script lang="ts" generics="Row">
2
+ import type { Snippet } from 'svelte';
3
+ import type { Column } from './table.js';
4
+
5
+
6
+ type Props = {
7
+ columns: Column<Row>[];
8
+ rows: Row[];
9
+ /** A caption for the table; say what it lists. Hidden visually with hideCaption. */
10
+ caption: string;
11
+ hideCaption?: boolean;
12
+ /** A stable key for each row. */
13
+ key?: (row: Row) => string | number;
14
+ /** Shown in place of rows when there are none. */
15
+ empty?: string;
16
+ class?: string;
17
+ };
18
+ let { columns, rows, caption, hideCaption = false, key = (r) => JSON.stringify(r), empty = 'Nothing to show', class: klass = '' }: Props = $props();
19
+ </script>
20
+
21
+ <!-- A semantic table, with a caption, column headers and numeric alignment. -->
22
+ <div class="tern-table-wrap {klass}">
23
+ <table class="tern-table">
24
+ <caption class:tern-sr-only={hideCaption}>{caption}</caption>
25
+ <thead>
26
+ <tr>
27
+ {#each columns as col (col.key)}<th scope="col" style:text-align={col.align} style:width={col.width}>{col.header}</th>{/each}
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {#each rows as row (key(row))}
32
+ <tr>
33
+ {#each columns as col (col.key)}
34
+ <td style:text-align={col.align}>
35
+ {#if col.cell}{@render col.cell(row)}{:else}{row[col.key] ?? ''}{/if}
36
+ </td>
37
+ {/each}
38
+ </tr>
39
+ {:else}
40
+ <tr><td colspan={columns.length} class="empty">{empty}</td></tr>
41
+ {/each}
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+
46
+ <style>
47
+ .tern-table-wrap {
48
+ overflow-x: auto;
49
+ border: 1px solid var(--tern-border);
50
+ border-radius: var(--tern-radius);
51
+ background: var(--tern-surface);
52
+ }
53
+ caption {
54
+ padding: 0.75rem;
55
+ text-align: start;
56
+ font-weight: 600;
57
+ color: var(--tern-fg);
58
+ }
59
+ td {
60
+ font-variant-numeric: tabular-nums;
61
+ }
62
+ .tern-table-wrap :global(tbody tr:last-child td) {
63
+ border-bottom: 0;
64
+ }
65
+ .empty {
66
+ padding: 2rem;
67
+ text-align: center;
68
+ color: var(--tern-muted);
69
+ }
70
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { Column } from './table.js';
2
+ declare function $$render<Row>(): {
3
+ props: {
4
+ columns: Column<Row>[];
5
+ rows: Row[];
6
+ /** A caption for the table; say what it lists. Hidden visually with hideCaption. */
7
+ caption: string;
8
+ hideCaption?: boolean;
9
+ /** A stable key for each row. */
10
+ key?: (row: Row) => string | number;
11
+ /** Shown in place of rows when there are none. */
12
+ empty?: string;
13
+ class?: string;
14
+ };
15
+ exports: {};
16
+ bindings: "";
17
+ slots: {};
18
+ events: {};
19
+ };
20
+ declare class __sveltets_Render<Row> {
21
+ props(): ReturnType<typeof $$render<Row>>['props'];
22
+ events(): ReturnType<typeof $$render<Row>>['events'];
23
+ slots(): ReturnType<typeof $$render<Row>>['slots'];
24
+ bindings(): "";
25
+ exports(): {};
26
+ }
27
+ interface $$IsomorphicComponent {
28
+ new <Row>(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']>> & {
29
+ $$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
30
+ } & ReturnType<__sveltets_Render<Row>['exports']>;
31
+ <Row>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
32
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
33
+ }
34
+ declare const Table: $$IsomorphicComponent;
35
+ type Table<Row> = InstanceType<typeof Table<Row>>;
36
+ export default Table;