@spaethtech/svelte-ui 0.1.10 → 0.3.1-dev.10.ff03a8d
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.
- package/.claude/skills/svelte-ui/SKILL.md +118 -0
- package/.claude/skills/themes/SKILL.md +303 -0
- package/CLAUDE.md +248 -0
- package/README.md +180 -42
- package/dist/components/Alert.svelte +119 -0
- package/dist/components/Alert.svelte.d.ts +29 -0
- package/dist/components/Badge/Badge.svelte +142 -69
- package/dist/components/Badge/Badge.svelte.d.ts +26 -6
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Banner.svelte +133 -0
- package/dist/components/Banner.svelte.d.ts +31 -0
- package/dist/components/Button.svelte +240 -0
- package/dist/components/Button.svelte.d.ts +33 -0
- package/dist/components/ButtonDropdown.svelte +145 -0
- package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
- package/dist/components/Calendar.svelte +259 -0
- package/dist/components/Calendar.svelte.d.ts +27 -0
- package/dist/components/Card.svelte +79 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CardBody.svelte +41 -0
- package/dist/components/CardBody.svelte.d.ts +17 -0
- package/dist/components/CardFooter.svelte +51 -0
- package/dist/components/CardFooter.svelte.d.ts +16 -0
- package/dist/components/CardHeader.svelte +51 -0
- package/dist/components/CardHeader.svelte.d.ts +16 -0
- package/dist/components/Checkbox.svelte +131 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/ConfirmDialog.svelte +103 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +33 -0
- package/dist/components/DataTable.svelte +518 -0
- package/dist/components/DataTable.svelte.d.ts +74 -0
- package/dist/components/DatePicker.svelte +312 -0
- package/dist/components/DatePicker.svelte.d.ts +32 -0
- package/dist/components/DateTimeInput.svelte +93 -0
- package/dist/components/DateTimeInput.svelte.d.ts +20 -0
- package/dist/components/Dialog.svelte +151 -0
- package/dist/components/Dialog.svelte.d.ts +30 -0
- package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
- package/dist/components/EmailInput.svelte.d.ts +21 -0
- package/dist/components/Input.svelte +369 -0
- package/dist/components/Input.svelte.d.ts +35 -0
- package/dist/components/{List/List.svelte → List.svelte} +194 -133
- package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
- package/dist/components/Menu.svelte +117 -0
- package/dist/components/Menu.svelte.d.ts +20 -0
- package/dist/components/NotesEditor.svelte +127 -0
- package/dist/components/NotesEditor.svelte.d.ts +17 -0
- package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
- package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
- package/dist/components/PasswordInput.svelte +52 -0
- package/dist/components/PasswordInput.svelte.d.ts +20 -0
- package/dist/components/Popup.svelte +140 -0
- package/dist/components/Popup.svelte.d.ts +31 -0
- package/dist/components/Query.svelte +284 -0
- package/dist/components/Query.svelte.d.ts +15 -0
- package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
- package/dist/components/Rating.svelte.d.ts +13 -0
- package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
- package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
- package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
- package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
- package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
- package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
- package/dist/components/SideBarMenu/index.d.ts +2 -0
- package/dist/components/SideBarMenu/index.js +1 -0
- package/dist/components/TabStrip/TabStrip.svelte +384 -0
- package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
- package/dist/components/TabStrip/index.d.ts +3 -0
- package/dist/components/TabStrip/index.js +2 -0
- package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
- package/dist/components/TextArea.svelte.d.ts +38 -0
- package/dist/components/ThemeSelector.svelte +81 -0
- package/dist/components/ThemeSelector.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +148 -0
- package/dist/components/TimePicker.svelte.d.ts +28 -0
- package/dist/components/TimeRangeInput.svelte +203 -0
- package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
- package/dist/components/TimeSpinner.svelte +202 -0
- package/dist/components/TimeSpinner.svelte.d.ts +26 -0
- package/dist/components/Toast/Toaster.svelte +51 -0
- package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
- package/dist/components/Toast/toast.svelte.d.ts +29 -0
- package/dist/components/Toast/toast.svelte.js +27 -0
- package/dist/components/Toggle.svelte +93 -0
- package/dist/components/Toggle.svelte.d.ts +15 -0
- package/dist/data/dataset.d.ts +42 -0
- package/dist/data/dataset.js +3 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/query/ast.d.ts +62 -0
- package/dist/data/query/ast.js +12 -0
- package/dist/data/query/index.d.ts +3 -0
- package/dist/data/query/index.js +3 -0
- package/dist/data/query/lexer.d.ts +33 -0
- package/dist/data/query/lexer.js +225 -0
- package/dist/data/query/parser.d.ts +11 -0
- package/dist/data/query/parser.js +252 -0
- package/dist/data/table/grid.svelte.d.ts +57 -0
- package/dist/data/table/grid.svelte.js +139 -0
- package/dist/data/table/index.d.ts +2 -0
- package/dist/data/table/index.js +2 -0
- package/dist/data/table/types.d.ts +79 -0
- package/dist/index.d.ts +49 -22
- package/dist/index.js +46 -21
- package/dist/positioning/anchored.d.ts +61 -0
- package/dist/positioning/anchored.js +122 -0
- package/dist/positioning/tooltip.d.ts +41 -0
- package/dist/positioning/tooltip.js +147 -0
- package/dist/theme.css +205 -0
- package/dist/types/breakpoints.d.ts +24 -0
- package/dist/types/breakpoints.js +13 -0
- package/dist/types/responsive.d.ts +32 -0
- package/dist/types/responsive.js +54 -0
- package/dist/types/sizes.d.ts +10 -3
- package/dist/types/time.d.ts +19 -0
- package/dist/types/time.js +10 -0
- package/dist/types/variants.d.ts +8 -1
- package/dist/types/variants.js +16 -1
- package/docs/components.md +255 -0
- package/docs/examples.md +323 -0
- package/docs/paradigm.md +240 -0
- package/docs/themes.md +170 -0
- package/docs/usage.md +450 -0
- package/package.json +97 -63
- package/dist/components/Button/Button.svelte +0 -172
- package/dist/components/Button/Button.svelte.d.ts +0 -32
- package/dist/components/Button/index.d.ts +0 -1
- package/dist/components/Button/index.js +0 -1
- package/dist/components/Dialog/Dialog.svelte +0 -101
- package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
- package/dist/components/Dialog/index.d.ts +0 -1
- package/dist/components/Dialog/index.js +0 -1
- package/dist/components/ElementManager/ElementManager.svelte +0 -397
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
- package/dist/components/ElementManager/index.d.ts +0 -1
- package/dist/components/ElementManager/index.js +0 -1
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
- package/dist/components/EmailInput/index.d.ts +0 -1
- package/dist/components/EmailInput/index.js +0 -1
- package/dist/components/Icon/Icon.svelte +0 -74
- package/dist/components/Icon/Icon.svelte.d.ts +0 -13
- package/dist/components/Icon/index.d.ts +0 -1
- package/dist/components/Icon/index.js +0 -1
- package/dist/components/Input/Input.svelte +0 -268
- package/dist/components/Input/Input.svelte.d.ts +0 -18
- package/dist/components/Input/index.d.ts +0 -1
- package/dist/components/Input/index.js +0 -1
- package/dist/components/List/index.d.ts +0 -1
- package/dist/components/List/index.js +0 -1
- package/dist/components/ListItem/ListItem.svelte +0 -175
- package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
- package/dist/components/ListItem/index.d.ts +0 -2
- package/dist/components/ListItem/index.js +0 -2
- package/dist/components/ListView/ListView.svelte +0 -463
- package/dist/components/ListView/ListView.svelte.d.ts +0 -37
- package/dist/components/ListView/index.d.ts +0 -2
- package/dist/components/ListView/index.js +0 -2
- package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
- package/dist/components/NodeGraph/BaseNode.js +0 -30
- package/dist/components/NodeGraph/Edge.svelte +0 -60
- package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
- package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
- package/dist/components/NodeGraph/Node.svelte +0 -100
- package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
- package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
- package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
- package/dist/components/NodeGraph/NodePort.svelte +0 -75
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
- package/dist/components/NodeGraph/decorators.d.ts +0 -81
- package/dist/components/NodeGraph/decorators.js +0 -148
- package/dist/components/NodeGraph/index.d.ts +0 -9
- package/dist/components/NodeGraph/index.js +0 -9
- package/dist/components/NodeGraph/types.d.ts +0 -94
- package/dist/components/NodeGraph/types.js +0 -33
- package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
- package/dist/components/NotesEditor/index.d.ts +0 -1
- package/dist/components/NotesEditor/index.js +0 -1
- package/dist/components/NumberInput/index.d.ts +0 -1
- package/dist/components/NumberInput/index.js +0 -1
- package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
- package/dist/components/PasswordInput/index.d.ts +0 -1
- package/dist/components/PasswordInput/index.js +0 -1
- package/dist/components/Popup/index.d.ts +0 -1
- package/dist/components/Popup/index.js +0 -1
- package/dist/components/Rating/Rating.svelte.d.ts +0 -7
- package/dist/components/Rating/index.d.ts +0 -1
- package/dist/components/Rating/index.js +0 -1
- package/dist/components/ScrollView/ScrollView.svelte +0 -285
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
- package/dist/components/ScrollView/index.d.ts +0 -1
- package/dist/components/ScrollView/index.js +0 -1
- package/dist/components/SearchInput/index.d.ts +0 -1
- package/dist/components/SearchInput/index.js +0 -1
- package/dist/components/Select/index.d.ts +0 -1
- package/dist/components/Select/index.js +0 -1
- package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
- package/dist/components/TextArea/index.d.ts +0 -1
- package/dist/components/TextArea/index.js +0 -1
- package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
- package/dist/components/ThemeSelector/index.d.ts +0 -1
- package/dist/components/ThemeSelector/index.js +0 -1
- package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
- package/dist/components/TooltipHandler/index.d.ts +0 -1
- package/dist/components/TooltipHandler/index.js +0 -1
- package/dist/styles/sizes.d.ts +0 -78
- package/dist/styles/sizes.js +0 -120
- package/dist/styles/variants.d.ts +0 -106
- package/dist/styles/variants.js +0 -194
- package/dist/types/ManagerTypes.d.ts +0 -42
- /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from
|
|
3
|
-
import type { Snippet } from
|
|
2
|
+
import { untrack } from "svelte";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { Size } from "../types/sizes.js";
|
|
5
|
+
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
6
|
+
import { variantToken, type Variant } from "../types/variants.js";
|
|
4
7
|
|
|
5
8
|
type Option = Record<string, any> & {
|
|
6
9
|
value: string;
|
|
@@ -20,6 +23,11 @@
|
|
|
20
23
|
itemClass?: string;
|
|
21
24
|
itemSnippet?: Snippet<[Option, number, boolean, boolean]>; // option, index, isSelected, isFocused
|
|
22
25
|
disabled?: boolean;
|
|
26
|
+
size?: Responsive<Size>;
|
|
27
|
+
/** Tints the interactive surfaces (hover / keyboard-cursor / selected) with a semantic colour
|
|
28
|
+
* instead of neutral grey — points `--ui-accent` at the variant token via `.ui-accent`, same as
|
|
29
|
+
* DataTable. The bordered frame stays neutral. Omit for the default grey tint. */
|
|
30
|
+
variant?: Variant;
|
|
23
31
|
// API integration
|
|
24
32
|
url?: string;
|
|
25
33
|
searchParam?: string;
|
|
@@ -45,13 +53,15 @@
|
|
|
45
53
|
multiSelect = false,
|
|
46
54
|
showCheckboxes = false,
|
|
47
55
|
maxVisibleItems, // Optional - when not set, grows to fit all items
|
|
48
|
-
itemHeight =
|
|
56
|
+
itemHeight = 30, // Default: var(--ui-height) minus 2px of border = 30px
|
|
49
57
|
minHeight = 32, // Should match var(--ui-height) for standard UI element height
|
|
50
58
|
class: className = "",
|
|
51
59
|
dropdownClass = "",
|
|
52
60
|
itemClass = "",
|
|
53
61
|
itemSnippet,
|
|
54
62
|
disabled = false,
|
|
63
|
+
size = "md",
|
|
64
|
+
variant,
|
|
55
65
|
// API integration props
|
|
56
66
|
url,
|
|
57
67
|
searchParam,
|
|
@@ -68,12 +78,20 @@
|
|
|
68
78
|
onError,
|
|
69
79
|
onLoadStart,
|
|
70
80
|
onLoadEnd,
|
|
71
|
-
onSelection
|
|
81
|
+
onSelection,
|
|
72
82
|
}: Props = $props();
|
|
73
83
|
|
|
84
|
+
// Size axis (see types/sizes.ts): option text scales with the trigger (forwarded by Select).
|
|
85
|
+
const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
|
|
86
|
+
const textClass = $derived(responsiveClasses(size, textMap));
|
|
87
|
+
// When a variant is set, point --ui-accent at its token so the shared interactive tints
|
|
88
|
+
// (--ui-color-hover/surface/active used by the items) re-mix from that colour. The `.ui-accent` class
|
|
89
|
+
// on the list container does the actual re-mix (see the themes skill / DataTable).
|
|
90
|
+
const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : "");
|
|
91
|
+
|
|
74
92
|
// showCheckboxes implies multiSelect
|
|
75
93
|
const effectiveMultiSelect = multiSelect || showCheckboxes;
|
|
76
|
-
|
|
94
|
+
|
|
77
95
|
// Initialize value based on multiSelect mode
|
|
78
96
|
if (value === undefined) {
|
|
79
97
|
value = effectiveMultiSelect ? [] : "";
|
|
@@ -90,7 +108,7 @@
|
|
|
90
108
|
function toggleSelection(optionValue: string) {
|
|
91
109
|
if (effectiveMultiSelect && Array.isArray(value)) {
|
|
92
110
|
if (value.includes(optionValue)) {
|
|
93
|
-
value = value.filter(v => v !== optionValue);
|
|
111
|
+
value = value.filter((v) => v !== optionValue);
|
|
94
112
|
} else {
|
|
95
113
|
value = [...value, optionValue];
|
|
96
114
|
}
|
|
@@ -133,18 +151,19 @@
|
|
|
133
151
|
const options = allOptions();
|
|
134
152
|
const actualItemHeight = getActualItemHeight();
|
|
135
153
|
let calculatedHeight;
|
|
136
|
-
|
|
137
|
-
// Account for border
|
|
138
|
-
const borderOffset =
|
|
139
|
-
|
|
154
|
+
|
|
155
|
+
// Account for top+bottom border. --ui-border is 1px, so 2px total in both states.
|
|
156
|
+
const borderOffset = 2;
|
|
157
|
+
|
|
140
158
|
if (maxVisibleItems !== undefined) {
|
|
141
159
|
// Fixed height mode - use maxVisibleItems
|
|
142
|
-
calculatedHeight =
|
|
160
|
+
calculatedHeight =
|
|
161
|
+
Math.min(options.length, maxVisibleItems) * actualItemHeight + borderOffset;
|
|
143
162
|
} else {
|
|
144
163
|
// Auto-grow mode - fit all items
|
|
145
164
|
calculatedHeight = options.length * actualItemHeight + borderOffset;
|
|
146
165
|
}
|
|
147
|
-
|
|
166
|
+
|
|
148
167
|
// Ensure minimum height is respected
|
|
149
168
|
return Math.max(calculatedHeight, minHeight);
|
|
150
169
|
});
|
|
@@ -163,36 +182,36 @@
|
|
|
163
182
|
|
|
164
183
|
function selectOption(optionValue: string, event: MouseEvent) {
|
|
165
184
|
if (disabled) return;
|
|
166
|
-
|
|
185
|
+
|
|
167
186
|
event.stopPropagation();
|
|
168
|
-
|
|
187
|
+
|
|
169
188
|
// Update keyboard cursor to the clicked item
|
|
170
|
-
const clickedIndex = allOptions().findIndex(option => option.value === optionValue);
|
|
189
|
+
const clickedIndex = allOptions().findIndex((option) => option.value === optionValue);
|
|
171
190
|
if (clickedIndex >= 0) {
|
|
172
191
|
keyboardCursor = clickedIndex;
|
|
173
192
|
}
|
|
174
|
-
|
|
193
|
+
|
|
175
194
|
// Update bindable value
|
|
176
195
|
toggleSelection(optionValue);
|
|
177
|
-
|
|
196
|
+
|
|
178
197
|
// Call onSelection callback with the full option object
|
|
179
|
-
const selectedOption = allOptions().find(option => option.value === optionValue);
|
|
198
|
+
const selectedOption = allOptions().find((option) => option.value === optionValue);
|
|
180
199
|
if (selectedOption && onSelection) {
|
|
181
200
|
onSelection(selectedOption);
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
|
-
|
|
203
|
+
|
|
185
204
|
// Handle list focus - restore keyboard cursor position and scroll into view
|
|
186
205
|
function handleListFocus() {
|
|
187
206
|
// Set keyboard cursor to first item if none exists and options are available
|
|
188
207
|
if (keyboardCursor < 0 && allOptions().length > 0) {
|
|
189
208
|
keyboardCursor = 0;
|
|
190
209
|
}
|
|
191
|
-
|
|
210
|
+
|
|
192
211
|
// If we have a keyboard cursor position, restore focus highlighting and scroll into view
|
|
193
212
|
if (keyboardCursor >= 0) {
|
|
194
213
|
focusedIndex = keyboardCursor;
|
|
195
|
-
|
|
214
|
+
|
|
196
215
|
// Scroll into view if the list is scrollable
|
|
197
216
|
if (listElement) {
|
|
198
217
|
const actualItemHeight = getActualItemHeight();
|
|
@@ -201,7 +220,7 @@
|
|
|
201
220
|
const viewTop = listElement.scrollTop;
|
|
202
221
|
const currentMaxHeight = maxHeight();
|
|
203
222
|
const viewBottom = viewTop + currentMaxHeight;
|
|
204
|
-
|
|
223
|
+
|
|
205
224
|
if (itemTop < viewTop || itemBottom > viewBottom) {
|
|
206
225
|
// Keyboard cursor is not visible, scroll it into view
|
|
207
226
|
listElement.scrollTop = Math.max(0, itemTop - (currentMaxHeight - actualItemHeight) / 2);
|
|
@@ -212,12 +231,12 @@
|
|
|
212
231
|
focusedIndex = -1;
|
|
213
232
|
}
|
|
214
233
|
}
|
|
215
|
-
|
|
234
|
+
|
|
216
235
|
// Handle list blur - reset focused index
|
|
217
236
|
function handleListBlur() {
|
|
218
237
|
focusedIndex = -1;
|
|
219
238
|
}
|
|
220
|
-
|
|
239
|
+
|
|
221
240
|
// API fetch function
|
|
222
241
|
async function fetchOptions(searchQuery: string) {
|
|
223
242
|
if (!url) {
|
|
@@ -236,47 +255,46 @@
|
|
|
236
255
|
if (abortController) {
|
|
237
256
|
abortController.abort();
|
|
238
257
|
}
|
|
239
|
-
|
|
258
|
+
|
|
240
259
|
abortController = new AbortController();
|
|
241
|
-
|
|
260
|
+
|
|
242
261
|
try {
|
|
243
262
|
isLoading = true;
|
|
244
263
|
onLoadStart?.();
|
|
245
|
-
|
|
264
|
+
|
|
246
265
|
// Build URL with optional search parameter
|
|
247
266
|
const apiUrl = new URL(url, window.location.origin);
|
|
248
267
|
if (searchParam && searchQuery.trim()) {
|
|
249
268
|
apiUrl.searchParams.set(searchParam, searchQuery);
|
|
250
269
|
}
|
|
251
|
-
|
|
270
|
+
|
|
252
271
|
const response = await fetch(apiUrl.toString(), {
|
|
253
272
|
signal: abortController.signal,
|
|
254
273
|
headers: {
|
|
255
|
-
|
|
256
|
-
...headers
|
|
257
|
-
}
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
...headers,
|
|
276
|
+
},
|
|
258
277
|
});
|
|
259
|
-
|
|
278
|
+
|
|
260
279
|
if (!response.ok) {
|
|
261
280
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
262
281
|
}
|
|
263
|
-
|
|
282
|
+
|
|
264
283
|
const data = await response.json();
|
|
265
|
-
|
|
284
|
+
|
|
266
285
|
// Transform the response using provided transform function
|
|
267
286
|
let transformedOptions = transform ? transform(data) : data;
|
|
268
|
-
|
|
287
|
+
|
|
269
288
|
// Apply maxResults limit if specified
|
|
270
289
|
if (maxResults && transformedOptions.length > maxResults) {
|
|
271
290
|
transformedOptions = transformedOptions.slice(0, maxResults);
|
|
272
291
|
}
|
|
273
|
-
|
|
292
|
+
|
|
274
293
|
apiOptions = transformedOptions;
|
|
275
294
|
lastSearchTerm = searchQuery;
|
|
276
|
-
|
|
277
295
|
} catch (error) {
|
|
278
|
-
if (error instanceof Error && error.name !==
|
|
279
|
-
console.error(
|
|
296
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
297
|
+
console.error("List API fetch error:", error);
|
|
280
298
|
hasError = true;
|
|
281
299
|
currentError = error;
|
|
282
300
|
onError?.(error);
|
|
@@ -297,7 +315,7 @@
|
|
|
297
315
|
const timeoutId = setTimeout(() => {
|
|
298
316
|
fetchOptions(searchTerm);
|
|
299
317
|
}, debounceMs);
|
|
300
|
-
|
|
318
|
+
|
|
301
319
|
return () => clearTimeout(timeoutId);
|
|
302
320
|
} else if (!searchTerm.trim()) {
|
|
303
321
|
// Clear results when search term is empty
|
|
@@ -326,38 +344,38 @@
|
|
|
326
344
|
// Handle keyboard navigation within list
|
|
327
345
|
function handleListKeydown(event: KeyboardEvent) {
|
|
328
346
|
if (disabled) return;
|
|
329
|
-
|
|
330
|
-
if (event.key ===
|
|
347
|
+
|
|
348
|
+
if (event.key === "Tab") {
|
|
331
349
|
// Allow normal tab navigation to move to next element
|
|
332
350
|
return;
|
|
333
351
|
}
|
|
334
|
-
|
|
335
|
-
if (event.key ===
|
|
352
|
+
|
|
353
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
336
354
|
event.preventDefault();
|
|
337
355
|
event.stopPropagation(); // Prevent parent dialog from scrolling
|
|
338
|
-
|
|
356
|
+
|
|
339
357
|
// If no item is focused yet, start from keyboard cursor position and move from there
|
|
340
358
|
if (focusedIndex === -1) {
|
|
341
359
|
// Start from keyboard cursor if available, otherwise start from beginning
|
|
342
360
|
const startIndex = keyboardCursor >= 0 ? keyboardCursor : 0;
|
|
343
|
-
|
|
344
|
-
if (event.key ===
|
|
361
|
+
|
|
362
|
+
if (event.key === "ArrowDown") {
|
|
345
363
|
focusedIndex = Math.min(startIndex + 1, allOptions().length - 1);
|
|
346
364
|
} else {
|
|
347
365
|
focusedIndex = Math.max(startIndex - 1, 0);
|
|
348
366
|
}
|
|
349
367
|
} else {
|
|
350
368
|
// Normal navigation
|
|
351
|
-
if (event.key ===
|
|
369
|
+
if (event.key === "ArrowDown") {
|
|
352
370
|
focusedIndex = Math.min(focusedIndex + 1, allOptions().length - 1);
|
|
353
371
|
} else {
|
|
354
372
|
focusedIndex = Math.max(focusedIndex - 1, 0);
|
|
355
373
|
}
|
|
356
374
|
}
|
|
357
|
-
|
|
375
|
+
|
|
358
376
|
// Update keyboard cursor to track current navigation position
|
|
359
377
|
keyboardCursor = focusedIndex;
|
|
360
|
-
|
|
378
|
+
|
|
361
379
|
// Scroll to keep focused item visible (only needed when maxVisibleItems is set)
|
|
362
380
|
if (listElement && maxVisibleItems !== undefined) {
|
|
363
381
|
const actualItemHeight = getActualItemHeight();
|
|
@@ -366,7 +384,7 @@
|
|
|
366
384
|
const viewTop = listElement.scrollTop;
|
|
367
385
|
const currentMaxHeight = maxHeight();
|
|
368
386
|
const viewBottom = viewTop + currentMaxHeight;
|
|
369
|
-
|
|
387
|
+
|
|
370
388
|
if (itemTop < viewTop) {
|
|
371
389
|
// Item is above visible area
|
|
372
390
|
listElement.scrollTop = itemTop;
|
|
@@ -375,15 +393,15 @@
|
|
|
375
393
|
listElement.scrollTop = itemBottom - currentMaxHeight;
|
|
376
394
|
}
|
|
377
395
|
}
|
|
378
|
-
} else if (event.key ===
|
|
396
|
+
} else if (event.key === " ") {
|
|
379
397
|
event.preventDefault();
|
|
380
398
|
event.stopPropagation(); // Prevent page scroll
|
|
381
399
|
if (focusedIndex >= 0 && focusedIndex < allOptions().length) {
|
|
382
400
|
const selectedOption = allOptions()[focusedIndex];
|
|
383
|
-
|
|
401
|
+
|
|
384
402
|
// Update bindable value using toggle logic
|
|
385
403
|
toggleSelection(selectedOption.value);
|
|
386
|
-
|
|
404
|
+
|
|
387
405
|
// Call onSelection callback
|
|
388
406
|
if (onSelection) {
|
|
389
407
|
onSelection(selectedOption);
|
|
@@ -408,26 +426,29 @@
|
|
|
408
426
|
isDragging = true;
|
|
409
427
|
dragStartY = event.clientY;
|
|
410
428
|
dragStartScrollTop = listElement.scrollTop;
|
|
411
|
-
|
|
412
|
-
document.addEventListener(
|
|
413
|
-
document.addEventListener(
|
|
429
|
+
|
|
430
|
+
document.addEventListener("mousemove", handleThumbMouseMove);
|
|
431
|
+
document.addEventListener("mouseup", handleThumbMouseUp);
|
|
414
432
|
}
|
|
415
433
|
|
|
416
434
|
function handleThumbMouseMove(event: MouseEvent) {
|
|
417
435
|
if (!isDragging || !listElement || maxVisibleItems === undefined) return;
|
|
418
|
-
|
|
436
|
+
|
|
419
437
|
const deltaY = event.clientY - dragStartY;
|
|
420
438
|
const currentMaxHeight = maxHeight();
|
|
421
439
|
const scrollRatio = deltaY / (currentMaxHeight - scrollThumbHeight());
|
|
422
440
|
const maxScrollTop = listElement.scrollHeight - listElement.clientHeight;
|
|
423
|
-
|
|
424
|
-
listElement.scrollTop = Math.max(
|
|
441
|
+
|
|
442
|
+
listElement.scrollTop = Math.max(
|
|
443
|
+
0,
|
|
444
|
+
Math.min(maxScrollTop, dragStartScrollTop + scrollRatio * maxScrollTop),
|
|
445
|
+
);
|
|
425
446
|
}
|
|
426
447
|
|
|
427
448
|
function handleThumbMouseUp() {
|
|
428
449
|
isDragging = false;
|
|
429
|
-
document.removeEventListener(
|
|
430
|
-
document.removeEventListener(
|
|
450
|
+
document.removeEventListener("mousemove", handleThumbMouseMove);
|
|
451
|
+
document.removeEventListener("mouseup", handleThumbMouseUp);
|
|
431
452
|
}
|
|
432
453
|
|
|
433
454
|
// Update scroll thumb position on scroll
|
|
@@ -442,31 +463,34 @@
|
|
|
442
463
|
if (listElement && maxVisibleItems !== undefined) {
|
|
443
464
|
const updateScrollPosition = () => {
|
|
444
465
|
if (!listElement || maxVisibleItems === undefined) return;
|
|
445
|
-
|
|
466
|
+
|
|
446
467
|
const trackPadding = 4; // 4px padding on top and bottom
|
|
447
468
|
const actualItemHeight = getActualItemHeight();
|
|
448
469
|
const totalContentHeight = allOptions().length * actualItemHeight;
|
|
449
470
|
const currentMaxHeight = maxHeight();
|
|
450
471
|
const visibleRatio = currentMaxHeight / totalContentHeight;
|
|
451
|
-
thumbHeight = Math.max(
|
|
452
|
-
|
|
472
|
+
thumbHeight = Math.max(
|
|
473
|
+
20,
|
|
474
|
+
Math.min(currentMaxHeight - trackPadding * 2 - 4, visibleRatio * currentMaxHeight),
|
|
475
|
+
);
|
|
476
|
+
|
|
453
477
|
const maxScrollTop = listElement.scrollHeight - listElement.clientHeight;
|
|
454
478
|
if (maxScrollTop > 0) {
|
|
455
479
|
const scrollRatio = listElement.scrollTop / maxScrollTop;
|
|
456
|
-
const trackHeight = currentMaxHeight - thumbHeight -
|
|
457
|
-
thumbTop = trackPadding +
|
|
480
|
+
const trackHeight = currentMaxHeight - thumbHeight - trackPadding * 2;
|
|
481
|
+
thumbTop = trackPadding + scrollRatio * trackHeight;
|
|
458
482
|
}
|
|
459
|
-
|
|
483
|
+
|
|
460
484
|
scrollTop = listElement.scrollTop;
|
|
461
485
|
};
|
|
462
|
-
|
|
486
|
+
|
|
463
487
|
// Initial calculation
|
|
464
488
|
setTimeout(updateScrollPosition, 10);
|
|
465
|
-
|
|
466
|
-
listElement.addEventListener(
|
|
489
|
+
|
|
490
|
+
listElement.addEventListener("scroll", updateScrollPosition);
|
|
467
491
|
return () => {
|
|
468
492
|
if (listElement) {
|
|
469
|
-
listElement.removeEventListener(
|
|
493
|
+
listElement.removeEventListener("scroll", updateScrollPosition);
|
|
470
494
|
}
|
|
471
495
|
};
|
|
472
496
|
}
|
|
@@ -475,10 +499,12 @@
|
|
|
475
499
|
|
|
476
500
|
<div class="relative {className}">
|
|
477
501
|
<div class="relative">
|
|
478
|
-
<div
|
|
502
|
+
<div
|
|
479
503
|
bind:this={listElement}
|
|
480
|
-
class="
|
|
481
|
-
|
|
504
|
+
class="ui-accent border {disabled
|
|
505
|
+
? '[background-color:color-mix(in_srgb,var(--ui-color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] cursor-not-allowed'
|
|
506
|
+
: '[background-color:var(--ui-color-background)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border-hover),transparent)] focus:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border-hover),transparent)] focus:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus:[outline-offset:0px]'} rounded-md overflow-y-scroll custom-list {dropdownClass}"
|
|
507
|
+
style="{accentVar} height: {maxHeight()}px; scrollbar-width: none; -ms-overflow-style: none; overscroll-behavior: contain;"
|
|
482
508
|
onscroll={handleScroll}
|
|
483
509
|
onkeydown={handleListKeydown}
|
|
484
510
|
onfocus={handleListFocus}
|
|
@@ -489,73 +515,108 @@
|
|
|
489
515
|
aria-busy={isLoading}
|
|
490
516
|
aria-disabled={disabled}
|
|
491
517
|
>
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
<button
|
|
508
|
-
bind:this={itemElements[index]}
|
|
509
|
-
type="button"
|
|
510
|
-
class="w-full text-left focus:outline-none flex items-center {disabled ? 'cursor-not-allowed [color:color-mix(in_srgb,var(--color-text)_40%,transparent)]' : 'cursor-pointer [color:var(--color-text)]'} {showCheckboxes ? (focusedIndex === index ? '[background-color:var(--color-secondary)] hover:[background-color:var(--color-secondary)] focus:[background-color:var(--color-secondary)]' : 'bg-transparent hover:[background-color:var(--color-secondary)] focus:[background-color:var(--color-secondary)]') : (isSelected(option.value) ? '[background-color:color-mix(in_srgb,var(--color-secondary)_80%,black)] [color:var(--color-text)] hover:[background-color:color-mix(in_srgb,var(--color-secondary)_80%,black)] focus:[background-color:color-mix(in_srgb,var(--color-secondary)_80%,black)]' : focusedIndex === index ? '[background-color:var(--color-secondary)] hover:[background-color:var(--color-secondary)] focus:[background-color:var(--color-secondary)]' : 'bg-transparent hover:[background-color:var(--color-secondary)] focus:[background-color:var(--color-secondary)]')} {itemClass}"
|
|
511
|
-
style="border: none; border-radius: 0; justify-content: flex-start; {itemSnippet ? '' : `height: ${itemHeight}px;`}"
|
|
512
|
-
onmousedown={(event) => selectOption(option.value, event)}
|
|
513
|
-
tabindex="-1"
|
|
514
|
-
role="option"
|
|
515
|
-
aria-selected={isSelected(option.value)}
|
|
516
|
-
aria-label={option.label}
|
|
517
|
-
disabled={disabled}
|
|
518
|
+
{#if isLoading && showLoading}
|
|
519
|
+
<div class="flex items-center justify-center py-4 [color:var(--ui-color-secondary)]">
|
|
520
|
+
<div
|
|
521
|
+
class="animate-spin rounded-full h-4 w-4 border-b-2 [border-color:var(--ui-color-secondary)] mr-2"
|
|
522
|
+
></div>
|
|
523
|
+
{loadingText}
|
|
524
|
+
</div>
|
|
525
|
+
{:else if hasError && showError}
|
|
526
|
+
<div class="flex items-center justify-center py-4 [color:var(--ui-color-error)]">
|
|
527
|
+
<span class="text-sm [color:var(--ui-color-error)]">{errorText}</span>
|
|
528
|
+
</div>
|
|
529
|
+
{:else if allOptions().length === 0}
|
|
530
|
+
<div
|
|
531
|
+
class="flex items-center justify-center [color:var(--ui-color-secondary)] select-none"
|
|
532
|
+
style="height: {itemHeight}px;"
|
|
518
533
|
>
|
|
519
|
-
{
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
534
|
+
<span class="px-4 text-sm [color:var(--ui-color-secondary)]">{emptyText}</span>
|
|
535
|
+
</div>
|
|
536
|
+
{:else}
|
|
537
|
+
{#each allOptions() as option, index (option.value)}
|
|
538
|
+
<button
|
|
539
|
+
bind:this={itemElements[index]}
|
|
540
|
+
type="button"
|
|
541
|
+
class="w-full text-left focus:outline-none flex items-center {disabled
|
|
542
|
+
? 'cursor-not-allowed [color:color-mix(in_srgb,var(--ui-color-text)_40%,transparent)]'
|
|
543
|
+
: 'cursor-pointer [color:var(--ui-color-text)]'} {showCheckboxes
|
|
544
|
+
? focusedIndex === index
|
|
545
|
+
? '[background-color:var(--ui-color-surface)] hover:[background-color:var(--ui-color-hover)]'
|
|
546
|
+
: 'bg-transparent hover:[background-color:var(--ui-color-hover)]'
|
|
547
|
+
: isSelected(option.value)
|
|
548
|
+
? '[background-color:var(--ui-color-active)] [color:var(--ui-color-text)] hover:[background-color:var(--ui-color-active)]'
|
|
549
|
+
: focusedIndex === index
|
|
550
|
+
? '[background-color:var(--ui-color-surface)] hover:[background-color:var(--ui-color-hover)]'
|
|
551
|
+
: 'bg-transparent hover:[background-color:var(--ui-color-hover)]'} {itemClass}"
|
|
552
|
+
style="border: none; border-radius: 0; justify-content: flex-start; {itemSnippet
|
|
553
|
+
? ''
|
|
554
|
+
: `height: ${itemHeight}px;`}"
|
|
555
|
+
onmousedown={(event) => selectOption(option.value, event)}
|
|
556
|
+
tabindex="-1"
|
|
557
|
+
role="option"
|
|
558
|
+
aria-selected={isSelected(option.value)}
|
|
559
|
+
aria-label={option.label}
|
|
560
|
+
{disabled}
|
|
561
|
+
>
|
|
562
|
+
{#if itemSnippet}
|
|
563
|
+
{@render itemSnippet(option, index, isSelected(option.value), focusedIndex === index)}
|
|
564
|
+
{:else if showCheckboxes}
|
|
565
|
+
<div class="flex items-center gap-3 px-2 py-2 w-full">
|
|
566
|
+
<div class="relative">
|
|
567
|
+
<input
|
|
568
|
+
type="checkbox"
|
|
569
|
+
checked={isSelected(option.value)}
|
|
570
|
+
{disabled}
|
|
571
|
+
class="w-4 h-4 rounded [border-color:var(--ui-color-secondary)] {isSelected(
|
|
572
|
+
option.value,
|
|
573
|
+
)
|
|
574
|
+
? '[background-color:var(--ui-color-primary)] [border-color:var(--ui-color-primary)]'
|
|
575
|
+
: '[background-color:var(--ui-color-background)]'} {disabled
|
|
576
|
+
? 'opacity-50 cursor-not-allowed'
|
|
577
|
+
: 'cursor-pointer'}"
|
|
578
|
+
tabindex="-1"
|
|
579
|
+
readonly
|
|
580
|
+
/>
|
|
581
|
+
{#if isSelected(option.value)}
|
|
582
|
+
<svg
|
|
583
|
+
class="absolute inset-0 w-4 h-4 text-white pointer-events-none"
|
|
584
|
+
fill="currentColor"
|
|
585
|
+
viewBox="0 0 20 20"
|
|
586
|
+
>
|
|
587
|
+
<path
|
|
588
|
+
fill-rule="evenodd"
|
|
589
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
590
|
+
clip-rule="evenodd"
|
|
591
|
+
/>
|
|
592
|
+
</svg>
|
|
593
|
+
{/if}
|
|
594
|
+
</div>
|
|
595
|
+
<span class="flex-1 text-left text-sm">{option.label}</span>
|
|
537
596
|
</div>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
{/each}
|
|
545
|
-
{/if}
|
|
597
|
+
{:else}
|
|
598
|
+
<span class="px-2 w-full {textClass} leading-tight">{option.label}</span>
|
|
599
|
+
{/if}
|
|
600
|
+
</button>
|
|
601
|
+
{/each}
|
|
602
|
+
{/if}
|
|
546
603
|
</div>
|
|
547
|
-
|
|
604
|
+
|
|
548
605
|
<!-- Custom scrollbar thumb - only show when maxVisibleItems is set and exceeded -->
|
|
549
606
|
{#if maxVisibleItems !== undefined && allOptions().length > maxVisibleItems && !isLoading && !hasError}
|
|
550
|
-
<div
|
|
551
|
-
class="absolute right-1 [background-color:color-mix(in_srgb,var(--color-text)_60%,transparent)] rounded cursor-default hover:[background-color:color-mix(in_srgb,var(--color-text)_80%,transparent)] transition-colors z-10"
|
|
607
|
+
<div
|
|
608
|
+
class="absolute right-1 [background-color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)] rounded cursor-default hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_80%,transparent)] transition-colors z-10"
|
|
552
609
|
style="top: {thumbTop}px; height: {thumbHeight}px; width: 6px;"
|
|
553
610
|
bind:this={scrollThumb}
|
|
554
611
|
onmousedown={handleThumbMouseDown}
|
|
555
612
|
role="scrollbar"
|
|
556
613
|
aria-orientation="vertical"
|
|
557
614
|
aria-controls="custom-list"
|
|
558
|
-
aria-valuenow={listElement
|
|
615
|
+
aria-valuenow={listElement
|
|
616
|
+
? Math.round(
|
|
617
|
+
(listElement.scrollTop / (listElement.scrollHeight - listElement.clientHeight)) * 100,
|
|
618
|
+
)
|
|
619
|
+
: 0}
|
|
559
620
|
aria-valuemin="0"
|
|
560
621
|
aria-valuemax="100"
|
|
561
622
|
tabindex="-1"
|
|
@@ -570,11 +631,11 @@
|
|
|
570
631
|
.custom-list::-webkit-scrollbar {
|
|
571
632
|
display: none;
|
|
572
633
|
}
|
|
573
|
-
|
|
634
|
+
|
|
574
635
|
.custom-list {
|
|
575
636
|
scrollbar-width: none;
|
|
576
637
|
-ms-overflow-style: none;
|
|
577
638
|
/* Prevent scroll chaining to parent elements */
|
|
578
639
|
overscroll-behavior: contain;
|
|
579
640
|
}
|
|
580
|
-
</style>
|
|
641
|
+
</style>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Snippet } from
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { Size } from "../types/sizes.js";
|
|
3
|
+
import { type Responsive } from "../types/responsive.js";
|
|
4
|
+
import { type Variant } from "../types/variants.js";
|
|
2
5
|
type Option = Record<string, any> & {
|
|
3
6
|
value: string;
|
|
4
7
|
label: string;
|
|
@@ -16,6 +19,11 @@ interface Props {
|
|
|
16
19
|
itemClass?: string;
|
|
17
20
|
itemSnippet?: Snippet<[Option, number, boolean, boolean]>;
|
|
18
21
|
disabled?: boolean;
|
|
22
|
+
size?: Responsive<Size>;
|
|
23
|
+
/** Tints the interactive surfaces (hover / keyboard-cursor / selected) with a semantic colour
|
|
24
|
+
* instead of neutral grey — points `--ui-accent` at the variant token via `.ui-accent`, same as
|
|
25
|
+
* DataTable. The bordered frame stays neutral. Omit for the default grey tint. */
|
|
26
|
+
variant?: Variant;
|
|
19
27
|
url?: string;
|
|
20
28
|
searchParam?: string;
|
|
21
29
|
transform?: (apiResponse: any) => Option[];
|