@viniaraujo68/plinth 0.0.0-stage → 0.2.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 (128) 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 +1 -0
  8. package/dist/attachments/index.js +1 -0
  9. package/dist/attachments/tooltip.d.ts +37 -0
  10. package/dist/attachments/tooltip.js +166 -0
  11. package/dist/components/AsyncButton.svelte +49 -0
  12. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  13. package/dist/components/Combobox.svelte +380 -0
  14. package/dist/components/Combobox.svelte.d.ts +74 -0
  15. package/dist/components/Copyable.svelte +116 -0
  16. package/dist/components/Copyable.svelte.d.ts +37 -0
  17. package/dist/components/DateRangePicker.svelte +266 -0
  18. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  19. package/dist/components/Dialog.svelte +102 -0
  20. package/dist/components/Dialog.svelte.d.ts +38 -0
  21. package/dist/components/Dropdown.svelte +204 -0
  22. package/dist/components/Dropdown.svelte.d.ts +49 -0
  23. package/dist/components/ErrorDisplay.svelte +59 -0
  24. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  25. package/dist/components/ErrorPage.svelte +119 -0
  26. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  27. package/dist/components/JsonInput.svelte +106 -0
  28. package/dist/components/JsonInput.svelte.d.ts +40 -0
  29. package/dist/components/Listbox.svelte +257 -0
  30. package/dist/components/Listbox.svelte.d.ts +78 -0
  31. package/dist/components/LoadingButton.svelte +36 -0
  32. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  33. package/dist/components/Logo.svelte +112 -0
  34. package/dist/components/Logo.svelte.d.ts +29 -0
  35. package/dist/components/Modal.svelte +187 -0
  36. package/dist/components/Modal.svelte.d.ts +62 -0
  37. package/dist/components/MultiSelect.svelte +256 -0
  38. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  39. package/dist/components/OptionPanel.svelte +241 -0
  40. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  41. package/dist/components/Paginator.svelte +205 -0
  42. package/dist/components/Paginator.svelte.d.ts +48 -0
  43. package/dist/components/RefreshButton.svelte +246 -0
  44. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  45. package/dist/components/SegmentedControl.svelte +147 -0
  46. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  47. package/dist/components/Select.svelte +312 -0
  48. package/dist/components/Select.svelte.d.ts +68 -0
  49. package/dist/components/Skeleton.svelte +97 -0
  50. package/dist/components/Skeleton.svelte.d.ts +32 -0
  51. package/dist/components/Tooltip.svelte +60 -0
  52. package/dist/components/Tooltip.svelte.d.ts +58 -0
  53. package/dist/components/date-range.d.ts +101 -0
  54. package/dist/components/date-range.js +144 -0
  55. package/dist/components/index.d.ts +25 -0
  56. package/dist/components/index.js +37 -0
  57. package/dist/components/json-text.d.ts +22 -0
  58. package/dist/components/json-text.js +22 -0
  59. package/dist/components/pick.d.ts +18 -0
  60. package/dist/components/pick.js +18 -0
  61. package/dist/components/segmented.d.ts +7 -0
  62. package/dist/components/segmented.js +1 -0
  63. package/dist/components/select.d.ts +66 -0
  64. package/dist/components/select.js +90 -0
  65. package/dist/confirm/Confirmer.svelte +194 -0
  66. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  67. package/dist/confirm/challenge.d.ts +16 -0
  68. package/dist/confirm/challenge.js +16 -0
  69. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  70. package/dist/confirm/confirm-manager.svelte.js +149 -0
  71. package/dist/confirm/index.d.ts +3 -0
  72. package/dist/confirm/index.js +3 -0
  73. package/dist/error.d.ts +25 -0
  74. package/dist/error.js +39 -0
  75. package/dist/formatters.d.ts +95 -0
  76. package/dist/formatters.js +146 -0
  77. package/dist/http.d.ts +68 -0
  78. package/dist/http.js +138 -0
  79. package/dist/index.d.ts +10 -0
  80. package/dist/index.js +14 -0
  81. package/dist/routing.svelte.d.ts +169 -0
  82. package/dist/routing.svelte.js +208 -0
  83. package/dist/shell/AppShell.svelte +874 -0
  84. package/dist/shell/AppShell.svelte.d.ts +93 -0
  85. package/dist/shell/Breadcrumbs.svelte +78 -0
  86. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  87. package/dist/shell/crumbs.d.ts +23 -0
  88. package/dist/shell/crumbs.js +1 -0
  89. package/dist/shell/index.d.ts +3 -0
  90. package/dist/shell/index.js +6 -0
  91. package/dist/table/ColumnMenu.svelte +249 -0
  92. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  93. package/dist/table/DataTable.svelte +643 -0
  94. package/dist/table/DataTable.svelte.d.ts +153 -0
  95. package/dist/table/columns.d.ts +55 -0
  96. package/dist/table/columns.js +153 -0
  97. package/dist/table/index.d.ts +4 -0
  98. package/dist/table/index.js +8 -0
  99. package/dist/table/sort.d.ts +48 -0
  100. package/dist/table/sort.js +139 -0
  101. package/dist/table/types.d.ts +93 -0
  102. package/dist/table/types.js +1 -0
  103. package/dist/theme/ThemeController.svelte +54 -0
  104. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  105. package/dist/theme/ThemeToggle.svelte +119 -0
  106. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  107. package/dist/theme/css-color.d.ts +23 -0
  108. package/dist/theme/css-color.js +31 -0
  109. package/dist/theme/index.d.ts +4 -0
  110. package/dist/theme/index.js +5 -0
  111. package/dist/theme/theme.css +410 -0
  112. package/dist/theme/theme.svelte.d.ts +54 -0
  113. package/dist/theme/theme.svelte.js +58 -0
  114. package/dist/toast/Toaster.svelte +257 -0
  115. package/dist/toast/Toaster.svelte.d.ts +35 -0
  116. package/dist/toast/index.d.ts +2 -0
  117. package/dist/toast/index.js +2 -0
  118. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  119. package/dist/toast/toast-manager.svelte.js +176 -0
  120. package/dist/user/ScopedComponent.svelte +47 -0
  121. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  122. package/dist/user/anonymous.d.ts +21 -0
  123. package/dist/user/anonymous.js +26 -0
  124. package/dist/user/context.d.ts +55 -0
  125. package/dist/user/context.js +13 -0
  126. package/dist/user/index.d.ts +3 -0
  127. package/dist/user/index.js +6 -0
  128. package/package.json +133 -4
@@ -0,0 +1,257 @@
1
+ <script lang="ts" generics="O extends SelectOption = SelectOption">
2
+ import type { Snippet } from "svelte";
3
+ import type { Attachment } from "svelte/attachments";
4
+ import type { ClassValue, HTMLAttributes } from "svelte/elements";
5
+ import {
6
+ filterOptions,
7
+ isTypedKey,
8
+ nearestSelectableIndex,
9
+ nextSelectableIndex,
10
+ Typeahead,
11
+ typeaheadMatch,
12
+ type SelectOption,
13
+ } from "./select.js";
14
+
15
+ type Props = Omit<
16
+ HTMLAttributes<HTMLDivElement>,
17
+ "children" | "class" | "onkeydown" | "role" | "tabindex"
18
+ > & {
19
+ /** The rows, in order. A new list keeps the highlighted row, or its position once it is gone. */
20
+ options: readonly O[];
21
+ /**
22
+ * The checked rows: one value, or an array for a list that picks several. A single value is
23
+ * where the highlight starts, and it goes back there whenever the value changes.
24
+ */
25
+ value?: string | readonly string[] | null;
26
+ /** A row was picked, by a click, Enter or Space. A disabled row is never reported. */
27
+ onpick: (option: O) => void;
28
+ /** Adds a search box above the list, which then holds the focus. */
29
+ searchable?: boolean;
30
+ /** The search text. Bindable. */
31
+ query?: string;
32
+ /** What the search reads for a row. Defaults to its label. */
33
+ searchText?: (option: O) => string;
34
+ searchPlaceholder?: string;
35
+ /** Shown in place of the rows when there are none to show. */
36
+ emptyLabel?: string;
37
+ /** Marks the list busy, and shows `loadingLabel` while it has no rows. */
38
+ loading?: boolean;
39
+ loadingLabel?: string;
40
+ /** Takes the focus as the popover or dialog holding the list opens. */
41
+ autofocus?: boolean;
42
+ /** Marks the checked rows with a ✓. Turn it off for a list of actions rather than a choice. */
43
+ checkmarks?: boolean;
44
+ /**
45
+ * Renders a row in place of its label. The row is already a `role="option"` button, so the
46
+ * snippet must not hold buttons, links or anything else interactive.
47
+ */
48
+ option?: Snippet<[O]>;
49
+ /** Classes for the whole control. The list is where every other attribute goes. */
50
+ class?: ClassValue;
51
+ };
52
+
53
+ let {
54
+ options,
55
+ value = null,
56
+ onpick,
57
+ searchable = false,
58
+ query = $bindable(""),
59
+ searchText = (candidate) => candidate.label,
60
+ searchPlaceholder = "Search…",
61
+ emptyLabel = "No matches",
62
+ loading = false,
63
+ loadingLabel = "Loading…",
64
+ autofocus = false,
65
+ checkmarks = true,
66
+ option,
67
+ class: className,
68
+ ...rest
69
+ }: Props = $props();
70
+
71
+ const uid = $props.id();
72
+ const listId = $derived(rest.id ?? `plinth-listbox-${uid}`);
73
+ const optionId = (index: number) => `${listId}-option-${index}`;
74
+
75
+ const visible = $derived(searchable ? filterOptions(options, query, searchText) : options);
76
+ const multiple = $derived(Array.isArray(value));
77
+ const checked = $derived(new Set<string | null>([value].flat()));
78
+
79
+ let highlighted = $state.raw<{ value: string; index: number; from: typeof value }>();
80
+
81
+ const activeIndex = $derived.by(() => {
82
+ if (highlighted && (multiple || highlighted.from === value)) {
83
+ const kept = visible.findIndex(
84
+ (candidate) => candidate.value === highlighted!.value && !candidate.disabled,
85
+ );
86
+ return kept >= 0 ? kept : nearestSelectableIndex(visible, highlighted.index);
87
+ }
88
+ const start = multiple
89
+ ? -1
90
+ : visible.findIndex((candidate) => candidate.value === value && !candidate.disabled);
91
+ return start >= 0 ? start : nearestSelectableIndex(visible, 0);
92
+ });
93
+
94
+ const activeId = $derived(activeIndex >= 0 ? optionId(activeIndex) : undefined);
95
+
96
+ const scrollToActive: Attachment<HTMLElement> = (list) => {
97
+ const row = list.children[activeIndex] as HTMLElement | undefined;
98
+ if (!row) return;
99
+ const bottom = row.offsetTop + row.offsetHeight - list.clientHeight;
100
+ if (row.offsetTop < list.scrollTop) list.scrollTop = row.offsetTop;
101
+ else if (bottom > list.scrollTop) list.scrollTop = bottom;
102
+ };
103
+
104
+ const scrollOnResize: Attachment<HTMLElement> = (list) => {
105
+ const observer = new ResizeObserver(() => scrollToActive(list));
106
+ observer.observe(list);
107
+ return () => observer.disconnect();
108
+ };
109
+
110
+ const highlight = (index: number) => {
111
+ const candidate = visible[index];
112
+ if (candidate) highlighted = { value: candidate.value, index, from: value };
113
+ };
114
+
115
+ const pick = (index: number) => {
116
+ const candidate = visible[index];
117
+ if (!candidate || candidate.disabled) return;
118
+ highlight(index);
119
+ onpick(candidate);
120
+ };
121
+
122
+ const typeahead = new Typeahead();
123
+ const jumpTo = (char: string) =>
124
+ highlight(typeaheadMatch(visible, activeIndex, typeahead.add(char)));
125
+
126
+ const onkeydown = (event: KeyboardEvent) => {
127
+ if (event.isComposing) return;
128
+ switch (event.key) {
129
+ case "ArrowDown":
130
+ highlight(nextSelectableIndex(visible, activeIndex + 1, 1));
131
+ break;
132
+ case "ArrowUp":
133
+ highlight(nextSelectableIndex(visible, activeIndex - 1, -1));
134
+ break;
135
+ case "Home":
136
+ if (searchable) return;
137
+ highlight(nextSelectableIndex(visible, 0, 1));
138
+ break;
139
+ case "End":
140
+ if (searchable) return;
141
+ highlight(nextSelectableIndex(visible, visible.length - 1, -1));
142
+ break;
143
+ case " ":
144
+ if (searchable) return;
145
+ if (typeahead.typing) {
146
+ jumpTo(" ");
147
+ break;
148
+ }
149
+ pick(activeIndex);
150
+ break;
151
+ case "Enter":
152
+ pick(activeIndex);
153
+ break;
154
+ default:
155
+ if (searchable || !isTypedKey(event)) return;
156
+ jumpTo(event.key);
157
+ }
158
+ event.preventDefault();
159
+ };
160
+ </script>
161
+
162
+ <!--
163
+ @component
164
+ A list to pick from in place: click a row, or walk it with the arrows, Home and End or the first
165
+ letters of a label, and pick with Enter or Space. `value` checks the selected row and the highlight
166
+ starts on it; an array of values checks several, for a list that picks many. Disabled rows show,
167
+ and the keyboard and the pointer step over them.
168
+
169
+ `searchable` adds a search box that keeps the focus while the arrows walk the list under it. It
170
+ folds case and accents, and a row matches when it contains every word typed; `searchText` decides
171
+ what it reads, so a row can be found by text it does not show.
172
+
173
+ The focus stays inside: on the search box, or on the list itself. The highlighted row is pointed at
174
+ with `aria-activedescendant` and never focused. Put the list in a popover with `autofocus` and it
175
+ takes the focus as the popover opens — which is what a `Dropdown` does with it.
176
+
177
+ ```svelte
178
+ <Listbox options={statuses} value={status} onpick={(row) => (status = row.value)} aria-label="Status" />
179
+ ```
180
+ -->
181
+
182
+ <div class={["plinth-listbox flex flex-col", className]}>
183
+ {#if searchable}
184
+ <input
185
+ bind:value={query}
186
+ type="text"
187
+ class="input mb-1 w-full input-sm"
188
+ placeholder={searchPlaceholder}
189
+ autocomplete="off"
190
+ spellcheck="false"
191
+ {autofocus}
192
+ role="combobox"
193
+ aria-expanded="true"
194
+ aria-controls={listId}
195
+ aria-activedescendant={activeId}
196
+ aria-autocomplete="list"
197
+ aria-label={searchPlaceholder}
198
+ oninput={() => (highlighted = undefined)}
199
+ {onkeydown}
200
+ />
201
+ {/if}
202
+
203
+ <div
204
+ {...rest}
205
+ {@attach scrollToActive}
206
+ {@attach scrollOnResize}
207
+ id={listId}
208
+ role="listbox"
209
+ tabindex={searchable ? -1 : 0}
210
+ aria-busy={loading || undefined}
211
+ aria-multiselectable={multiple || undefined}
212
+ aria-activedescendant={searchable ? undefined : activeId}
213
+ autofocus={!searchable && autofocus}
214
+ class="pointer-events-auto relative max-h-64 overflow-y-auto overscroll-contain p-1 focus-visible:outline-none"
215
+ onkeydown={searchable ? undefined : onkeydown}
216
+ onmousedown={(event) => {
217
+ event.preventDefault();
218
+ if (!searchable) event.currentTarget.focus();
219
+ }}
220
+ >
221
+ {#each visible as candidate, index (candidate.value)}
222
+ <button
223
+ type="button"
224
+ id={optionId(index)}
225
+ role="option"
226
+ tabindex="-1"
227
+ disabled={candidate.disabled}
228
+ aria-selected={checked.has(candidate.value)}
229
+ aria-disabled={candidate.disabled}
230
+ class={[
231
+ "flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-field px-3 py-2 text-start text-sm",
232
+ index === activeIndex && "bg-base-content/10",
233
+ candidate.disabled && "cursor-not-allowed opacity-40",
234
+ ]}
235
+ onpointermove={() => {
236
+ if (!candidate.disabled && index !== activeIndex) highlight(index);
237
+ }}
238
+ onclick={() => pick(index)}
239
+ >
240
+ <span class="min-w-0 flex-1 wrap-anywhere">
241
+ {#if option}{@render option(candidate)}{:else}{candidate.label}{/if}
242
+ </span>
243
+ {#if checkmarks}
244
+ <span class="flex-none text-xs" aria-hidden="true">
245
+ {checked.has(candidate.value) ? "✓" : ""}
246
+ </span>
247
+ {/if}
248
+ </button>
249
+ {/each}
250
+ </div>
251
+
252
+ {#if visible.length === 0}
253
+ <p role="status" class="px-3 py-4 text-center text-sm text-base-content/60">
254
+ {loading ? loadingLabel : emptyLabel}
255
+ </p>
256
+ {/if}
257
+ </div>
@@ -0,0 +1,78 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ClassValue, HTMLAttributes } from "svelte/elements";
3
+ import { type SelectOption } from "./select.js";
4
+ declare function $$render<O extends SelectOption = SelectOption>(): {
5
+ props: Omit<HTMLAttributes<HTMLDivElement>, "children" | "class" | "tabindex" | "role" | "onkeydown"> & {
6
+ /** The rows, in order. A new list keeps the highlighted row, or its position once it is gone. */
7
+ options: readonly O[];
8
+ /**
9
+ * The checked rows: one value, or an array for a list that picks several. A single value is
10
+ * where the highlight starts, and it goes back there whenever the value changes.
11
+ */
12
+ value?: string | readonly string[] | null;
13
+ /** A row was picked, by a click, Enter or Space. A disabled row is never reported. */
14
+ onpick: (option: O) => void;
15
+ /** Adds a search box above the list, which then holds the focus. */
16
+ searchable?: boolean;
17
+ /** The search text. Bindable. */
18
+ query?: string;
19
+ /** What the search reads for a row. Defaults to its label. */
20
+ searchText?: (option: O) => string;
21
+ searchPlaceholder?: string;
22
+ /** Shown in place of the rows when there are none to show. */
23
+ emptyLabel?: string;
24
+ /** Marks the list busy, and shows `loadingLabel` while it has no rows. */
25
+ loading?: boolean;
26
+ loadingLabel?: string;
27
+ /** Takes the focus as the popover or dialog holding the list opens. */
28
+ autofocus?: boolean;
29
+ /** Marks the checked rows with a ✓. Turn it off for a list of actions rather than a choice. */
30
+ checkmarks?: boolean;
31
+ /**
32
+ * Renders a row in place of its label. The row is already a `role="option"` button, so the
33
+ * snippet must not hold buttons, links or anything else interactive.
34
+ */
35
+ option?: Snippet<[O]>;
36
+ /** Classes for the whole control. The list is where every other attribute goes. */
37
+ class?: ClassValue;
38
+ };
39
+ exports: {};
40
+ bindings: "query";
41
+ slots: {};
42
+ events: {};
43
+ };
44
+ declare class __sveltets_Render<O extends SelectOption = SelectOption> {
45
+ props(): ReturnType<typeof $$render<O>>['props'];
46
+ events(): ReturnType<typeof $$render<O>>['events'];
47
+ slots(): ReturnType<typeof $$render<O>>['slots'];
48
+ bindings(): "query";
49
+ exports(): {};
50
+ }
51
+ interface $$IsomorphicComponent {
52
+ new <O extends SelectOption = SelectOption>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<O>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<O>['props']>, ReturnType<__sveltets_Render<O>['events']>, ReturnType<__sveltets_Render<O>['slots']>> & {
53
+ $$bindings?: ReturnType<__sveltets_Render<O>['bindings']>;
54
+ } & ReturnType<__sveltets_Render<O>['exports']>;
55
+ <O extends SelectOption = SelectOption>(internal: unknown, props: ReturnType<__sveltets_Render<O>['props']> & {}): ReturnType<__sveltets_Render<O>['exports']>;
56
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
57
+ }
58
+ /**
59
+ * A list to pick from in place: click a row, or walk it with the arrows, Home and End or the first
60
+ * letters of a label, and pick with Enter or Space. `value` checks the selected row and the highlight
61
+ * starts on it; an array of values checks several, for a list that picks many. Disabled rows show,
62
+ * and the keyboard and the pointer step over them.
63
+ *
64
+ * `searchable` adds a search box that keeps the focus while the arrows walk the list under it. It
65
+ * folds case and accents, and a row matches when it contains every word typed; `searchText` decides
66
+ * what it reads, so a row can be found by text it does not show.
67
+ *
68
+ * The focus stays inside: on the search box, or on the list itself. The highlighted row is pointed at
69
+ * with `aria-activedescendant` and never focused. Put the list in a popover with `autofocus` and it
70
+ * takes the focus as the popover opens — which is what a `Dropdown` does with it.
71
+ *
72
+ * ```svelte
73
+ * <Listbox options={statuses} value={status} onpick={(row) => (status = row.value)} aria-label="Status" />
74
+ * ```
75
+ */
76
+ declare const Listbox: $$IsomorphicComponent;
77
+ type Listbox<O extends SelectOption = SelectOption> = InstanceType<typeof Listbox<O>>;
78
+ export default Listbox;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
+
5
+ interface Props extends HTMLButtonAttributes {
6
+ children: Snippet;
7
+ /** Shows the spinner and blocks the button. The caller owns this flag. */
8
+ loading?: boolean;
9
+ }
10
+
11
+ // `type` defaults to "submit" in HTML, so a button dropped into a form to run a handler also
12
+ // submits that form. This one asks to be told when that is what you meant.
13
+ let { children, loading = false, disabled, type = "button", ...rest }: Props = $props();
14
+ </script>
15
+
16
+ <!--
17
+ @component
18
+ A button that grows a spinner and stops accepting clicks while `loading` is set.
19
+
20
+ It is a plain `<button>` with everything forwarded, so it takes the app's own classes — the
21
+ library never decides that a button is `btn-primary`. The label is kept on screen next to the
22
+ spinner instead of being swapped out, because a button that changes width mid-click moves whatever
23
+ sits next to it.
24
+
25
+ ```svelte
26
+ <LoadingButton class="btn btn-primary" loading={saving} onclick={save}>Save</LoadingButton>
27
+ ```
28
+ -->
29
+
30
+ <button {...rest} {type} disabled={loading || disabled} aria-busy={loading}>
31
+ {#if loading}
32
+ <!-- Decorative: `aria-busy` above is what a screen reader announces. -->
33
+ <span class="loading loading-xs loading-spinner" aria-hidden="true"></span>
34
+ {/if}
35
+ {@render children()}
36
+ </button>
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+ interface Props extends HTMLButtonAttributes {
4
+ children: Snippet;
5
+ /** Shows the spinner and blocks the button. The caller owns this flag. */
6
+ loading?: boolean;
7
+ }
8
+ /**
9
+ * A button that grows a spinner and stops accepting clicks while `loading` is set.
10
+ *
11
+ * It is a plain `<button>` with everything forwarded, so it takes the app's own classes — the
12
+ * library never decides that a button is `btn-primary`. The label is kept on screen next to the
13
+ * spinner instead of being swapped out, because a button that changes width mid-click moves whatever
14
+ * sits next to it.
15
+ *
16
+ * ```svelte
17
+ * <LoadingButton class="btn btn-primary" loading={saving} onclick={save}>Save</LoadingButton>
18
+ * ```
19
+ */
20
+ declare const LoadingButton: import("svelte").Component<Props, {}, "">;
21
+ type LoadingButton = ReturnType<typeof LoadingButton>;
22
+ export default LoadingButton;
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import { PICK_PATH } from "./pick.js";
4
+
5
+ type Props = HTMLAttributes<HTMLDivElement> & {
6
+ /** Runs the orbit. Leave it off for a static placeholder mark. */
7
+ animated?: boolean;
8
+ /** Stroke color. Defaults to the current theme's ink. */
9
+ color?: string;
10
+ };
11
+
12
+ let { class: className, animated = false, color, ...rest }: Props = $props();
13
+ </script>
14
+
15
+ <!--
16
+ @component
17
+ A guitar pick drawn as a single outline stroke — the library's loading indicator, and the mark it
18
+ falls back to when there is nothing to show yet. Animated, a lit third of the outline orbits the
19
+ perimeter; standing still, the outline is unbroken.
20
+
21
+ It sizes itself from the inherited font size, so `class="text-4xl"` is the whole API for making it
22
+ bigger, and it colors itself from `--color-base-content`, so it stays legible in either theme and
23
+ inside a `data-theme` subtree.
24
+
25
+ It renders no text and carries no role, which makes it silent to assistive technology on purpose:
26
+ whatever it is standing in for is what should be announced, so wrap it in the live region or
27
+ `aria-busy` container that owns the loading state.
28
+
29
+ ```svelte
30
+ <div class="grid place-items-center" role="status" aria-label="Loading releases">
31
+ <Logo class="text-4xl" animated />
32
+ </div>
33
+ ```
34
+ -->
35
+
36
+ <div
37
+ {...rest}
38
+ class={["plinth-logo", className]}
39
+ data-animated={animated ? "" : undefined}
40
+ style:--logo-color={color}
41
+ >
42
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
43
+ <path d={PICK_PATH} />
44
+ </svg>
45
+ </div>
46
+
47
+ <style>
48
+ @keyframes orbit {
49
+ from {
50
+ stroke-dashoffset: 0px;
51
+ }
52
+ to {
53
+ stroke-dashoffset: -71.648px;
54
+ }
55
+ }
56
+
57
+ @keyframes pulse {
58
+ 0%,
59
+ 100% {
60
+ opacity: 1;
61
+ }
62
+ 50% {
63
+ opacity: 0.35;
64
+ }
65
+ }
66
+
67
+ .plinth-logo {
68
+ /* Full ink rather than the faint wash a solid mark could afford: a 3.5-wide stroke in a 32 box
69
+ covers little area, and the animated state lights only a third of that perimeter at a time,
70
+ so a single-digit-percent wash would leave the running dash invisible. The outline stays
71
+ quiet at full strength because it is hollow — the surface reads through the middle, which is
72
+ what keeps it from being mistaken for content. */
73
+ --logo-color: var(--color-base-content);
74
+
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+
79
+ svg {
80
+ display: block;
81
+ width: 1em;
82
+ height: 1em;
83
+ }
84
+
85
+ path {
86
+ fill: none;
87
+ stroke: var(--logo-color);
88
+ stroke-width: 3.5;
89
+ stroke-linecap: round;
90
+ stroke-linejoin: round;
91
+ }
92
+
93
+ /* Presence, not value: `data-animated={false}` would still serialise to the string "false"
94
+ and match an attribute selector, so the attribute is omitted entirely when it is off. */
95
+ &[data-animated] path {
96
+ /* 23.883 + 47.765 = 71.648, the path's measured length via `getTotalLength` in chromium,
97
+ which is also the distance the keyframes travel. The three numbers are one fact stated
98
+ three times and have to move together: a dash period that is not exactly the path length
99
+ makes the pattern restart at the path's start point — the tip — so the dash visibly snaps
100
+ there once per cycle. Change the `d` and all three are wrong until re-measured. */
101
+ stroke-dasharray: 23.883px 47.765px;
102
+ animation: orbit 1.7s linear infinite;
103
+
104
+ /* A fully static mark would remove the only feedback that something is loading, so the
105
+ orbit degrades to a pulse rather than to nothing. */
106
+ @media (prefers-reduced-motion: reduce) {
107
+ stroke-dasharray: none;
108
+ animation: pulse 1.7s ease-in-out infinite;
109
+ }
110
+ }
111
+ }
112
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ type Props = HTMLAttributes<HTMLDivElement> & {
3
+ /** Runs the orbit. Leave it off for a static placeholder mark. */
4
+ animated?: boolean;
5
+ /** Stroke color. Defaults to the current theme's ink. */
6
+ color?: string;
7
+ };
8
+ /**
9
+ * A guitar pick drawn as a single outline stroke — the library's loading indicator, and the mark it
10
+ * falls back to when there is nothing to show yet. Animated, a lit third of the outline orbits the
11
+ * perimeter; standing still, the outline is unbroken.
12
+ *
13
+ * It sizes itself from the inherited font size, so `class="text-4xl"` is the whole API for making it
14
+ * bigger, and it colors itself from `--color-base-content`, so it stays legible in either theme and
15
+ * inside a `data-theme` subtree.
16
+ *
17
+ * It renders no text and carries no role, which makes it silent to assistive technology on purpose:
18
+ * whatever it is standing in for is what should be announced, so wrap it in the live region or
19
+ * `aria-busy` container that owns the loading state.
20
+ *
21
+ * ```svelte
22
+ * <div class="grid place-items-center" role="status" aria-label="Loading releases">
23
+ * <Logo class="text-4xl" animated />
24
+ * </div>
25
+ * ```
26
+ */
27
+ declare const Logo: import("svelte").Component<Props, {}, "">;
28
+ type Logo = ReturnType<typeof Logo>;
29
+ export default Logo;