@viniaraujo68/plinth 0.0.0-stage → 0.3.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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -2
  3. package/dist/anchoring.d.ts +61 -0
  4. package/dist/anchoring.js +151 -0
  5. package/dist/async-action.svelte.d.ts +37 -0
  6. package/dist/async-action.svelte.js +64 -0
  7. package/dist/attachments/index.d.ts +2 -0
  8. package/dist/attachments/index.js +2 -0
  9. package/dist/attachments/sortable.d.ts +6 -0
  10. package/dist/attachments/sortable.js +227 -0
  11. package/dist/attachments/tooltip.d.ts +37 -0
  12. package/dist/attachments/tooltip.js +166 -0
  13. package/dist/components/AsyncButton.svelte +49 -0
  14. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  15. package/dist/components/Combobox.svelte +380 -0
  16. package/dist/components/Combobox.svelte.d.ts +74 -0
  17. package/dist/components/Copyable.svelte +116 -0
  18. package/dist/components/Copyable.svelte.d.ts +37 -0
  19. package/dist/components/DateRangePicker.svelte +266 -0
  20. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  21. package/dist/components/Dialog.svelte +102 -0
  22. package/dist/components/Dialog.svelte.d.ts +38 -0
  23. package/dist/components/DragHandle.svelte +26 -0
  24. package/dist/components/DragHandle.svelte.d.ts +7 -0
  25. package/dist/components/Dropdown.svelte +204 -0
  26. package/dist/components/Dropdown.svelte.d.ts +49 -0
  27. package/dist/components/ErrorDisplay.svelte +59 -0
  28. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  29. package/dist/components/ErrorPage.svelte +119 -0
  30. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  31. package/dist/components/JsonInput.svelte +106 -0
  32. package/dist/components/JsonInput.svelte.d.ts +40 -0
  33. package/dist/components/Listbox.svelte +257 -0
  34. package/dist/components/Listbox.svelte.d.ts +78 -0
  35. package/dist/components/LoadingButton.svelte +36 -0
  36. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  37. package/dist/components/Logo.svelte +112 -0
  38. package/dist/components/Logo.svelte.d.ts +29 -0
  39. package/dist/components/Modal.svelte +187 -0
  40. package/dist/components/Modal.svelte.d.ts +62 -0
  41. package/dist/components/MultiSelect.svelte +256 -0
  42. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  43. package/dist/components/OptionPanel.svelte +241 -0
  44. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  45. package/dist/components/Paginator.svelte +205 -0
  46. package/dist/components/Paginator.svelte.d.ts +48 -0
  47. package/dist/components/RefreshButton.svelte +246 -0
  48. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  49. package/dist/components/SegmentedControl.svelte +147 -0
  50. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  51. package/dist/components/Select.svelte +312 -0
  52. package/dist/components/Select.svelte.d.ts +68 -0
  53. package/dist/components/Skeleton.svelte +97 -0
  54. package/dist/components/Skeleton.svelte.d.ts +32 -0
  55. package/dist/components/Tooltip.svelte +60 -0
  56. package/dist/components/Tooltip.svelte.d.ts +58 -0
  57. package/dist/components/date-range.d.ts +101 -0
  58. package/dist/components/date-range.js +144 -0
  59. package/dist/components/index.d.ts +26 -0
  60. package/dist/components/index.js +38 -0
  61. package/dist/components/json-text.d.ts +22 -0
  62. package/dist/components/json-text.js +22 -0
  63. package/dist/components/pick.d.ts +18 -0
  64. package/dist/components/pick.js +18 -0
  65. package/dist/components/segmented.d.ts +7 -0
  66. package/dist/components/segmented.js +1 -0
  67. package/dist/components/select.d.ts +66 -0
  68. package/dist/components/select.js +90 -0
  69. package/dist/confirm/Confirmer.svelte +194 -0
  70. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  71. package/dist/confirm/challenge.d.ts +16 -0
  72. package/dist/confirm/challenge.js +16 -0
  73. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  74. package/dist/confirm/confirm-manager.svelte.js +149 -0
  75. package/dist/confirm/index.d.ts +3 -0
  76. package/dist/confirm/index.js +3 -0
  77. package/dist/error.d.ts +25 -0
  78. package/dist/error.js +39 -0
  79. package/dist/formatters.d.ts +95 -0
  80. package/dist/formatters.js +146 -0
  81. package/dist/http.d.ts +68 -0
  82. package/dist/http.js +138 -0
  83. package/dist/index.d.ts +10 -0
  84. package/dist/index.js +14 -0
  85. package/dist/routing.svelte.d.ts +169 -0
  86. package/dist/routing.svelte.js +208 -0
  87. package/dist/shell/AppShell.svelte +874 -0
  88. package/dist/shell/AppShell.svelte.d.ts +93 -0
  89. package/dist/shell/Breadcrumbs.svelte +78 -0
  90. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  91. package/dist/shell/crumbs.d.ts +23 -0
  92. package/dist/shell/crumbs.js +1 -0
  93. package/dist/shell/index.d.ts +3 -0
  94. package/dist/shell/index.js +6 -0
  95. package/dist/table/ColumnMenu.svelte +249 -0
  96. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  97. package/dist/table/DataTable.svelte +643 -0
  98. package/dist/table/DataTable.svelte.d.ts +153 -0
  99. package/dist/table/columns.d.ts +55 -0
  100. package/dist/table/columns.js +153 -0
  101. package/dist/table/index.d.ts +4 -0
  102. package/dist/table/index.js +8 -0
  103. package/dist/table/sort.d.ts +48 -0
  104. package/dist/table/sort.js +139 -0
  105. package/dist/table/types.d.ts +93 -0
  106. package/dist/table/types.js +1 -0
  107. package/dist/theme/ThemeController.svelte +54 -0
  108. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  109. package/dist/theme/ThemeToggle.svelte +119 -0
  110. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  111. package/dist/theme/css-color.d.ts +23 -0
  112. package/dist/theme/css-color.js +31 -0
  113. package/dist/theme/index.d.ts +4 -0
  114. package/dist/theme/index.js +5 -0
  115. package/dist/theme/theme.css +410 -0
  116. package/dist/theme/theme.svelte.d.ts +54 -0
  117. package/dist/theme/theme.svelte.js +58 -0
  118. package/dist/toast/Toaster.svelte +257 -0
  119. package/dist/toast/Toaster.svelte.d.ts +35 -0
  120. package/dist/toast/index.d.ts +2 -0
  121. package/dist/toast/index.js +2 -0
  122. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  123. package/dist/toast/toast-manager.svelte.js +176 -0
  124. package/dist/user/ScopedComponent.svelte +47 -0
  125. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  126. package/dist/user/anonymous.d.ts +21 -0
  127. package/dist/user/anonymous.js +26 -0
  128. package/dist/user/context.d.ts +55 -0
  129. package/dist/user/context.js +13 -0
  130. package/dist/user/index.d.ts +3 -0
  131. package/dist/user/index.js +6 -0
  132. package/package.json +133 -4
@@ -0,0 +1,246 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import type { ClassValue } from "svelte/elements";
4
+ import { AsyncAction } from "../async-action.svelte.js";
5
+ import { reportError } from "../error.js";
6
+
7
+ interface IntervalOption {
8
+ value: number;
9
+ label: string;
10
+ }
11
+
12
+ interface Props {
13
+ /** The reload itself. Awaited, so the spinner and the countdown follow the real request. */
14
+ onRefresh: () => unknown | Promise<unknown>;
15
+ /** Auto-refresh periods offered by the picker, in seconds. Pass `[]` for a manual-only control. */
16
+ intervals?: IntervalOption[];
17
+ /**
18
+ * Period selected when auto-refresh is switched on, one of `intervals`. Defaults to the second
19
+ * option, and so does a period the picker does not offer.
20
+ */
21
+ defaultInterval?: number;
22
+ /** Whether auto-refresh is armed. Pass `true` to start with it on; bind to follow the toggle. */
23
+ autoRefresh?: boolean;
24
+ /**
25
+ * When the data on screen last landed, as an epoch millisecond count. Left out, the control
26
+ * times its own successful refreshes — which is wrong the moment the data also arrives from
27
+ * somewhere else, such as a server-rendered first load.
28
+ */
29
+ lastSuccessAt?: number | null;
30
+ /** Formatting locale for the timestamp. Defaults to the browser's. */
31
+ locale?: string;
32
+ refreshLabel?: string;
33
+ startLabel?: string;
34
+ pauseLabel?: string;
35
+ intervalLabel?: string;
36
+ class?: ClassValue;
37
+ }
38
+
39
+ const defaultIntervals: IntervalOption[] = [
40
+ { value: 10, label: "10s" },
41
+ { value: 30, label: "30s" },
42
+ { value: 60, label: "1m" },
43
+ { value: 300, label: "5m" },
44
+ ];
45
+
46
+ let {
47
+ onRefresh,
48
+ intervals = defaultIntervals,
49
+ defaultInterval,
50
+ autoRefresh = $bindable(false),
51
+ lastSuccessAt,
52
+ locale,
53
+ refreshLabel = "Refresh now",
54
+ startLabel = "Start auto-refresh",
55
+ pauseLabel = "Pause auto-refresh",
56
+ intervalLabel = "Auto-refresh interval",
57
+ class: cls,
58
+ }: Props = $props();
59
+
60
+ // A seed, not a binding: once the user has picked a period, a re-render of the parent must not
61
+ // yank it back to the default. `untrack` states that and keeps the compiler from warning.
62
+ let periodSec = $state(
63
+ untrack(() =>
64
+ intervals.some((option) => option.value === defaultInterval)
65
+ ? defaultInterval!
66
+ : (intervals[1]?.value ?? intervals[0]?.value ?? null),
67
+ ),
68
+ );
69
+ const scheduled = $derived(intervals.length > 0 && periodSec !== null);
70
+ let ownLastSuccessAt = $state<number | null>(null);
71
+ let nextRefreshAt = $state<number | null>(null);
72
+ let now = $state(Date.now());
73
+ let visible = $state(true);
74
+
75
+ const action = new AsyncAction(async () => {
76
+ await onRefresh();
77
+ ownLastSuccessAt = Date.now();
78
+ });
79
+
80
+ const refresh = async () => {
81
+ await action.run();
82
+ if (action.isError) reportError(action.error);
83
+ };
84
+
85
+ const shownAt = $derived(lastSuccessAt === undefined ? ownLastSuccessAt : lastSuccessAt);
86
+
87
+ // A hidden tab gets its timers throttled to once a minute or worse, so an unattended schedule
88
+ // would drift and then fire a burst on focus. Suspending it outright is both honest and cheaper.
89
+ $effect(() => {
90
+ const track = () => {
91
+ visible = document.visibilityState === "visible";
92
+ };
93
+ track();
94
+ document.addEventListener("visibilitychange", track);
95
+ return () => {
96
+ document.removeEventListener("visibilitychange", track);
97
+ };
98
+ });
99
+
100
+ // One second-resolution timer drives both the countdown and the firing, so what the user reads
101
+ // is the schedule itself rather than a second clock that agrees with it only approximately.
102
+ // The deadline is a plain local: writing it to state would make this effect depend on its own
103
+ // output and re-run forever.
104
+ $effect(() => {
105
+ if (!autoRefresh || !scheduled || !visible) {
106
+ nextRefreshAt = null;
107
+ return;
108
+ }
109
+
110
+ const period = periodSec! * 1000;
111
+ let deadline = Date.now() + period;
112
+ nextRefreshAt = deadline;
113
+ now = Date.now();
114
+
115
+ const id = setInterval(() => {
116
+ now = Date.now();
117
+ if (now < deadline) return;
118
+ deadline = now + period;
119
+ nextRefreshAt = deadline;
120
+ void refresh();
121
+ }, 1000);
122
+
123
+ return () => {
124
+ clearInterval(id);
125
+ nextRefreshAt = null;
126
+ };
127
+ });
128
+
129
+ const secondsLeft = $derived(
130
+ nextRefreshAt === null ? null : Math.max(0, Math.ceil((nextRefreshAt - now) / 1000)),
131
+ );
132
+
133
+ const formatTime = (ms: number | null): string =>
134
+ ms === null
135
+ ? "--:--:--"
136
+ : new Date(ms).toLocaleTimeString(locale, {
137
+ hour: "2-digit",
138
+ minute: "2-digit",
139
+ second: "2-digit",
140
+ });
141
+ </script>
142
+
143
+ <!--
144
+ @component
145
+ Freshness control: when the data last landed, a "refresh now" button, and an auto-refresh toggle
146
+ with a period picker.
147
+
148
+ It owns its schedule — this library ships no query layer to delegate one to — and suspends it
149
+ while the tab is hidden, resuming on the next visible second rather than firing a backlog. The
150
+ countdown replaces the timestamp while auto-refresh is armed, so the strip never changes width.
151
+
152
+ ```svelte
153
+ <RefreshButton onRefresh={() => reload()} lastSuccessAt={loadedAt} />
154
+ ```
155
+ -->
156
+
157
+ <div
158
+ class={[
159
+ "inline-flex h-8 items-center rounded-field border border-base-content/15 bg-base-100 pr-1 pl-2.5",
160
+ cls,
161
+ ]}
162
+ >
163
+ <span class="flex items-center gap-1.5 text-base-content/55">
164
+ <span
165
+ class={[
166
+ "size-1.5 rounded-full transition",
167
+ secondsLeft === null
168
+ ? "bg-base-content/30"
169
+ : "animate-pulse bg-success ring-2 ring-success/25",
170
+ ]}
171
+ ></span>
172
+ <span class="font-mono text-xs tabular-nums" data-testid="refresh-clock">
173
+ {secondsLeft === null ? formatTime(shownAt) : `${secondsLeft}s`}
174
+ </span>
175
+ </span>
176
+
177
+ <!-- Icons are inline SVG throughout the library: a component that renders an icon-font class
178
+ would silently render nothing unless the consumer also installs that plugin in its own
179
+ Tailwind chain. -->
180
+ <button
181
+ type="button"
182
+ class="btn ml-1.5 btn-square btn-ghost btn-xs"
183
+ title={refreshLabel}
184
+ aria-label={refreshLabel}
185
+ onclick={refresh}
186
+ >
187
+ <svg
188
+ class={["size-3.5", action.isPending && "animate-spin"]}
189
+ viewBox="0 0 24 24"
190
+ fill="none"
191
+ stroke="currentColor"
192
+ stroke-width="2"
193
+ stroke-linecap="round"
194
+ stroke-linejoin="round"
195
+ aria-hidden="true"
196
+ >
197
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
198
+ <path d="M21 3v5h-5" />
199
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
200
+ <path d="M8 16H3v5" />
201
+ </svg>
202
+ </button>
203
+
204
+ {#if scheduled}
205
+ <span class="mx-0.5 h-5 w-px bg-base-content/15"></span>
206
+
207
+ <button
208
+ type="button"
209
+ class={[
210
+ "btn btn-square btn-ghost btn-xs",
211
+ autoRefresh ? "text-primary" : "text-base-content/55",
212
+ ]}
213
+ title={autoRefresh ? pauseLabel : startLabel}
214
+ aria-label={autoRefresh ? pauseLabel : startLabel}
215
+ aria-pressed={autoRefresh}
216
+ onclick={() => {
217
+ autoRefresh = !autoRefresh;
218
+ }}
219
+ >
220
+ {#if autoRefresh}
221
+ <svg class="size-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
222
+ <rect x="6" y="4" width="4" height="16" rx="1" />
223
+ <rect x="14" y="4" width="4" height="16" rx="1" />
224
+ </svg>
225
+ {:else}
226
+ <svg class="size-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
227
+ <path
228
+ d="M7 4.5v15a1 1 0 0 0 1.53.85l11.5-7.5a1 1 0 0 0 0-1.7L8.53 3.65A1 1 0 0 0 7 4.5Z"
229
+ />
230
+ </svg>
231
+ {/if}
232
+ </button>
233
+ {/if}
234
+
235
+ {#if scheduled && autoRefresh}
236
+ <select
237
+ class="select ml-0.5 w-18 select-ghost font-mono select-xs"
238
+ aria-label={intervalLabel}
239
+ bind:value={periodSec}
240
+ >
241
+ {#each intervals as option (option.value)}
242
+ <option value={option.value}>{option.label}</option>
243
+ {/each}
244
+ </select>
245
+ {/if}
246
+ </div>
@@ -0,0 +1,46 @@
1
+ import type { ClassValue } from "svelte/elements";
2
+ interface IntervalOption {
3
+ value: number;
4
+ label: string;
5
+ }
6
+ interface Props {
7
+ /** The reload itself. Awaited, so the spinner and the countdown follow the real request. */
8
+ onRefresh: () => unknown | Promise<unknown>;
9
+ /** Auto-refresh periods offered by the picker, in seconds. Pass `[]` for a manual-only control. */
10
+ intervals?: IntervalOption[];
11
+ /**
12
+ * Period selected when auto-refresh is switched on, one of `intervals`. Defaults to the second
13
+ * option, and so does a period the picker does not offer.
14
+ */
15
+ defaultInterval?: number;
16
+ /** Whether auto-refresh is armed. Pass `true` to start with it on; bind to follow the toggle. */
17
+ autoRefresh?: boolean;
18
+ /**
19
+ * When the data on screen last landed, as an epoch millisecond count. Left out, the control
20
+ * times its own successful refreshes — which is wrong the moment the data also arrives from
21
+ * somewhere else, such as a server-rendered first load.
22
+ */
23
+ lastSuccessAt?: number | null;
24
+ /** Formatting locale for the timestamp. Defaults to the browser's. */
25
+ locale?: string;
26
+ refreshLabel?: string;
27
+ startLabel?: string;
28
+ pauseLabel?: string;
29
+ intervalLabel?: string;
30
+ class?: ClassValue;
31
+ }
32
+ /**
33
+ * Freshness control: when the data last landed, a "refresh now" button, and an auto-refresh toggle
34
+ * with a period picker.
35
+ *
36
+ * It owns its schedule — this library ships no query layer to delegate one to — and suspends it
37
+ * while the tab is hidden, resuming on the next visible second rather than firing a backlog. The
38
+ * countdown replaces the timestamp while auto-refresh is armed, so the strip never changes width.
39
+ *
40
+ * ```svelte
41
+ * <RefreshButton onRefresh={() => reload()} lastSuccessAt={loadedAt} />
42
+ * ```
43
+ */
44
+ declare const RefreshButton: import("svelte").Component<Props, {}, "autoRefresh">;
45
+ type RefreshButton = ReturnType<typeof RefreshButton>;
46
+ export default RefreshButton;
@@ -0,0 +1,147 @@
1
+ <script lang="ts" generics="Id extends string">
2
+ import type { ClassValue } from "svelte/elements";
3
+ import type { SegmentedOption } from "./segmented.js";
4
+
5
+ interface Props {
6
+ /** The choices, in the order they are shown. `id` is the value, so it has to be unique. */
7
+ options: readonly SegmentedOption<Id>[];
8
+ /** The chosen `id`. Bindable, and also reported through {@link Props.onchange}. */
9
+ value: Id;
10
+ /** Accessible name of the group. A prop because the library ships no translations. */
11
+ label: string;
12
+ /**
13
+ * Visible text before the buttons — "Show:", "Goals and assists:". It is not the accessible
14
+ * name; `label` is, so a caption can be as terse as it likes without a screen reader hearing
15
+ * half a sentence.
16
+ */
17
+ caption?: string;
18
+ /** `sm` is for a control inside a card header, where the default height would dominate. */
19
+ size?: "sm" | "md";
20
+ /** Classes for the wrapper — spacing, alignment. */
21
+ class?: ClassValue;
22
+ /** Called with the new `id` only when it actually changes; re-picking the current one is silent. */
23
+ onchange?: (id: Id) => void;
24
+ }
25
+
26
+ let {
27
+ options,
28
+ value = $bindable(),
29
+ label,
30
+ caption,
31
+ size = "md",
32
+ class: className,
33
+ onchange,
34
+ }: Props = $props();
35
+
36
+ const pick = (id: Id) => {
37
+ if (id === value) return;
38
+ value = id;
39
+ onchange?.(id);
40
+ };
41
+ </script>
42
+
43
+ <!--
44
+ @component
45
+ A row of mutually exclusive buttons: one of them is the current choice, and pressing another
46
+ moves the choice there. It is the control for a small, fixed set of views of the same thing —
47
+ "total, per match, per day" — where a `Select` would hide options that are cheaper to see.
48
+
49
+ Each button is a toggle button (`aria-pressed`) inside a labelled `group`, rather than a radio
50
+ group: a radio group moves the choice with the arrow keys as focus moves, which re-renders
51
+ whatever the control drives on every keystroke, where Tab and Enter let a keyboard user look at the
52
+ options before committing to one.
53
+
54
+ The colours come from two custom properties with library defaults, so an app with its own
55
+ contrast-tuned inks can hand them in without restyling the component:
56
+ `--segmented-ink` for the resting text, `--segmented-ink-selected` for the chosen one.
57
+
58
+ ```svelte
59
+ <SegmentedControl
60
+ bind:value={unit}
61
+ label="Unit"
62
+ caption="Goals:"
63
+ options={[
64
+ { id: "total", label: "Total" },
65
+ { id: "match", label: "Per match" },
66
+ ]}
67
+ />
68
+ ```
69
+ -->
70
+
71
+ <div class={["plinth-segmented", className]} data-size={size}>
72
+ {#if caption}<span class="plinth-segmented-caption">{caption}</span>{/if}
73
+ <div class="plinth-segmented-options" role="group" aria-label={label}>
74
+ {#each options as option (option.id)}
75
+ <button
76
+ type="button"
77
+ class="plinth-segment"
78
+ class:plinth-segment-selected={option.id === value}
79
+ aria-pressed={option.id === value}
80
+ disabled={option.disabled}
81
+ onclick={() => pick(option.id)}
82
+ >
83
+ {option.label}
84
+ </button>
85
+ {/each}
86
+ </div>
87
+ </div>
88
+
89
+ <style>
90
+ .plinth-segmented {
91
+ --segmented-ink-default: color-mix(in oklch, var(--color-base-content) 70%, transparent);
92
+
93
+ display: flex;
94
+ flex-wrap: wrap;
95
+ align-items: center;
96
+ gap: 0.375rem 0.5rem;
97
+ }
98
+
99
+ .plinth-segmented-caption {
100
+ font-size: 0.72rem;
101
+ color: var(--segmented-ink, var(--segmented-ink-default));
102
+ }
103
+
104
+ .plinth-segmented-options {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: 0.3125rem;
108
+ }
109
+
110
+ .plinth-segment {
111
+ min-height: 2rem;
112
+ padding: 0.25rem 0.6875rem;
113
+ border: 1px solid color-mix(in oklch, var(--color-base-content) 15%, transparent);
114
+ /* The theme's field radius rather than a pill, like the table's sort pills: these are
115
+ controls, and they should match the buttons and inputs they sit among. */
116
+ border-radius: var(--radius-field, 0.5rem);
117
+ background: var(--color-base-100);
118
+ color: var(--segmented-ink, var(--segmented-ink-default));
119
+ font-size: 0.78rem;
120
+ cursor: pointer;
121
+ }
122
+
123
+ .plinth-segment:disabled {
124
+ opacity: 0.4;
125
+ cursor: not-allowed;
126
+ }
127
+
128
+ .plinth-segment-selected {
129
+ border-color: var(--color-primary);
130
+ background: color-mix(in oklch, var(--color-primary) 14%, transparent);
131
+ color: var(--segmented-ink-selected, var(--color-primary));
132
+ font-weight: 600;
133
+ }
134
+
135
+ [data-size="sm"] .plinth-segment {
136
+ min-height: 1.625rem;
137
+ padding: 0.125rem 0.5rem;
138
+ font-size: 0.7rem;
139
+ }
140
+
141
+ /* 40px on a phone: at the default height a segment is too small a target for a thumb. */
142
+ @media (max-width: 35rem) {
143
+ [data-size="md"] .plinth-segment {
144
+ min-height: 2.5rem;
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,71 @@
1
+ import type { ClassValue } from "svelte/elements";
2
+ import type { SegmentedOption } from "./segmented.js";
3
+ declare function $$render<Id extends string>(): {
4
+ props: {
5
+ /** The choices, in the order they are shown. `id` is the value, so it has to be unique. */
6
+ options: readonly SegmentedOption<Id>[];
7
+ /** The chosen `id`. Bindable, and also reported through {@link Props.onchange}. */
8
+ value: Id;
9
+ /** Accessible name of the group. A prop because the library ships no translations. */
10
+ label: string;
11
+ /**
12
+ * Visible text before the buttons — "Show:", "Goals and assists:". It is not the accessible
13
+ * name; `label` is, so a caption can be as terse as it likes without a screen reader hearing
14
+ * half a sentence.
15
+ */
16
+ caption?: string;
17
+ /** `sm` is for a control inside a card header, where the default height would dominate. */
18
+ size?: "sm" | "md";
19
+ /** Classes for the wrapper — spacing, alignment. */
20
+ class?: ClassValue;
21
+ /** Called with the new `id` only when it actually changes; re-picking the current one is silent. */
22
+ onchange?: (id: Id) => void;
23
+ };
24
+ exports: {};
25
+ bindings: "value";
26
+ slots: {};
27
+ events: {};
28
+ };
29
+ declare class __sveltets_Render<Id extends string> {
30
+ props(): ReturnType<typeof $$render<Id>>['props'];
31
+ events(): ReturnType<typeof $$render<Id>>['events'];
32
+ slots(): ReturnType<typeof $$render<Id>>['slots'];
33
+ bindings(): "value";
34
+ exports(): {};
35
+ }
36
+ interface $$IsomorphicComponent {
37
+ new <Id extends string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Id>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Id>['props']>, ReturnType<__sveltets_Render<Id>['events']>, ReturnType<__sveltets_Render<Id>['slots']>> & {
38
+ $$bindings?: ReturnType<__sveltets_Render<Id>['bindings']>;
39
+ } & ReturnType<__sveltets_Render<Id>['exports']>;
40
+ <Id extends string>(internal: unknown, props: ReturnType<__sveltets_Render<Id>['props']> & {}): ReturnType<__sveltets_Render<Id>['exports']>;
41
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
42
+ }
43
+ /**
44
+ * A row of mutually exclusive buttons: one of them is the current choice, and pressing another
45
+ * moves the choice there. It is the control for a small, fixed set of views of the same thing —
46
+ * "total, per match, per day" — where a `Select` would hide options that are cheaper to see.
47
+ *
48
+ * Each button is a toggle button (`aria-pressed`) inside a labelled `group`, rather than a radio
49
+ * group: a radio group moves the choice with the arrow keys as focus moves, which re-renders
50
+ * whatever the control drives on every keystroke, where Tab and Enter let a keyboard user look at the
51
+ * options before committing to one.
52
+ *
53
+ * The colours come from two custom properties with library defaults, so an app with its own
54
+ * contrast-tuned inks can hand them in without restyling the component:
55
+ * `--segmented-ink` for the resting text, `--segmented-ink-selected` for the chosen one.
56
+ *
57
+ * ```svelte
58
+ * <SegmentedControl
59
+ * bind:value={unit}
60
+ * label="Unit"
61
+ * caption="Goals:"
62
+ * options={[
63
+ * { id: "total", label: "Total" },
64
+ * { id: "match", label: "Per match" },
65
+ * ]}
66
+ * />
67
+ * ```
68
+ */
69
+ declare const SegmentedControl: $$IsomorphicComponent;
70
+ type SegmentedControl<Id extends string> = InstanceType<typeof SegmentedControl<Id>>;
71
+ export default SegmentedControl;