@spaethtech/svelte-ui 0.1.10 → 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 (208) hide show
  1. package/README.md +166 -42
  2. package/dist/components/Alert.svelte +119 -0
  3. package/dist/components/Alert.svelte.d.ts +29 -0
  4. package/dist/components/Badge/Badge.svelte +142 -69
  5. package/dist/components/Badge/Badge.svelte.d.ts +26 -6
  6. package/dist/components/Badge/index.d.ts +1 -1
  7. package/dist/components/Badge/index.js +1 -1
  8. package/dist/components/Banner.svelte +133 -0
  9. package/dist/components/Banner.svelte.d.ts +31 -0
  10. package/dist/components/Button.svelte +240 -0
  11. package/dist/components/Button.svelte.d.ts +33 -0
  12. package/dist/components/ButtonDropdown.svelte +145 -0
  13. package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
  14. package/dist/components/Calendar.svelte +259 -0
  15. package/dist/components/Calendar.svelte.d.ts +27 -0
  16. package/dist/components/Card.svelte +78 -0
  17. package/dist/components/Card.svelte.d.ts +17 -0
  18. package/dist/components/CardBody.svelte +41 -0
  19. package/dist/components/CardBody.svelte.d.ts +17 -0
  20. package/dist/components/CardFooter.svelte +49 -0
  21. package/dist/components/CardFooter.svelte.d.ts +16 -0
  22. package/dist/components/CardHeader.svelte +49 -0
  23. package/dist/components/CardHeader.svelte.d.ts +16 -0
  24. package/dist/components/Checkbox.svelte +131 -0
  25. package/dist/components/Checkbox.svelte.d.ts +16 -0
  26. package/dist/components/DataTable.svelte +518 -0
  27. package/dist/components/DataTable.svelte.d.ts +74 -0
  28. package/dist/components/DatePicker.svelte +312 -0
  29. package/dist/components/DatePicker.svelte.d.ts +32 -0
  30. package/dist/components/DateTimeInput.svelte +93 -0
  31. package/dist/components/DateTimeInput.svelte.d.ts +20 -0
  32. package/dist/components/Dialog.svelte +132 -0
  33. package/dist/components/Dialog.svelte.d.ts +24 -0
  34. package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
  35. package/dist/components/EmailInput.svelte.d.ts +21 -0
  36. package/dist/components/Input.svelte +369 -0
  37. package/dist/components/Input.svelte.d.ts +35 -0
  38. package/dist/components/{List/List.svelte → List.svelte} +194 -133
  39. package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
  40. package/dist/components/Menu.svelte +117 -0
  41. package/dist/components/Menu.svelte.d.ts +20 -0
  42. package/dist/components/NotesEditor.svelte +127 -0
  43. package/dist/components/NotesEditor.svelte.d.ts +17 -0
  44. package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
  45. package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
  46. package/dist/components/PasswordInput.svelte +52 -0
  47. package/dist/components/PasswordInput.svelte.d.ts +20 -0
  48. package/dist/components/Popup.svelte +140 -0
  49. package/dist/components/Popup.svelte.d.ts +31 -0
  50. package/dist/components/Query.svelte +284 -0
  51. package/dist/components/Query.svelte.d.ts +15 -0
  52. package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
  53. package/dist/components/Rating.svelte.d.ts +13 -0
  54. package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
  55. package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
  56. package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
  57. package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
  58. package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
  59. package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
  60. package/dist/components/SideBarMenu/index.d.ts +2 -0
  61. package/dist/components/SideBarMenu/index.js +1 -0
  62. package/dist/components/TabStrip/TabStrip.svelte +384 -0
  63. package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
  64. package/dist/components/TabStrip/index.d.ts +3 -0
  65. package/dist/components/TabStrip/index.js +2 -0
  66. package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
  67. package/dist/components/TextArea.svelte.d.ts +38 -0
  68. package/dist/components/ThemeSelector.svelte +81 -0
  69. package/dist/components/ThemeSelector.svelte.d.ts +10 -0
  70. package/dist/components/TimePicker.svelte +148 -0
  71. package/dist/components/TimePicker.svelte.d.ts +28 -0
  72. package/dist/components/TimeRangeInput.svelte +203 -0
  73. package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
  74. package/dist/components/TimeSpinner.svelte +202 -0
  75. package/dist/components/TimeSpinner.svelte.d.ts +26 -0
  76. package/dist/components/Toast/Toaster.svelte +51 -0
  77. package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
  78. package/dist/components/Toast/toast.svelte.d.ts +29 -0
  79. package/dist/components/Toast/toast.svelte.js +27 -0
  80. package/dist/components/Toggle.svelte +93 -0
  81. package/dist/components/Toggle.svelte.d.ts +15 -0
  82. package/dist/data/dataset.d.ts +42 -0
  83. package/dist/data/dataset.js +3 -0
  84. package/dist/data/index.d.ts +3 -0
  85. package/dist/data/index.js +3 -0
  86. package/dist/data/query/ast.d.ts +62 -0
  87. package/dist/data/query/ast.js +12 -0
  88. package/dist/data/query/index.d.ts +3 -0
  89. package/dist/data/query/index.js +3 -0
  90. package/dist/data/query/lexer.d.ts +33 -0
  91. package/dist/data/query/lexer.js +225 -0
  92. package/dist/data/query/parser.d.ts +11 -0
  93. package/dist/data/query/parser.js +252 -0
  94. package/dist/data/table/grid.svelte.d.ts +57 -0
  95. package/dist/data/table/grid.svelte.js +139 -0
  96. package/dist/data/table/index.d.ts +2 -0
  97. package/dist/data/table/index.js +2 -0
  98. package/dist/data/table/types.d.ts +79 -0
  99. package/dist/index.d.ts +48 -22
  100. package/dist/index.js +45 -21
  101. package/dist/positioning/anchored.d.ts +61 -0
  102. package/dist/positioning/anchored.js +122 -0
  103. package/dist/positioning/tooltip.d.ts +41 -0
  104. package/dist/positioning/tooltip.js +147 -0
  105. package/dist/theme.css +205 -0
  106. package/dist/types/breakpoints.d.ts +24 -0
  107. package/dist/types/breakpoints.js +13 -0
  108. package/dist/types/responsive.d.ts +32 -0
  109. package/dist/types/responsive.js +54 -0
  110. package/dist/types/sizes.d.ts +10 -3
  111. package/dist/types/time.d.ts +19 -0
  112. package/dist/types/time.js +10 -0
  113. package/dist/types/variants.d.ts +8 -1
  114. package/dist/types/variants.js +16 -1
  115. package/package.json +93 -63
  116. package/dist/components/Button/Button.svelte +0 -172
  117. package/dist/components/Button/Button.svelte.d.ts +0 -32
  118. package/dist/components/Button/index.d.ts +0 -1
  119. package/dist/components/Button/index.js +0 -1
  120. package/dist/components/Dialog/Dialog.svelte +0 -101
  121. package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
  122. package/dist/components/Dialog/index.d.ts +0 -1
  123. package/dist/components/Dialog/index.js +0 -1
  124. package/dist/components/ElementManager/ElementManager.svelte +0 -397
  125. package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
  126. package/dist/components/ElementManager/index.d.ts +0 -1
  127. package/dist/components/ElementManager/index.js +0 -1
  128. package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
  129. package/dist/components/EmailInput/index.d.ts +0 -1
  130. package/dist/components/EmailInput/index.js +0 -1
  131. package/dist/components/Icon/Icon.svelte +0 -74
  132. package/dist/components/Icon/Icon.svelte.d.ts +0 -13
  133. package/dist/components/Icon/index.d.ts +0 -1
  134. package/dist/components/Icon/index.js +0 -1
  135. package/dist/components/Input/Input.svelte +0 -268
  136. package/dist/components/Input/Input.svelte.d.ts +0 -18
  137. package/dist/components/Input/index.d.ts +0 -1
  138. package/dist/components/Input/index.js +0 -1
  139. package/dist/components/List/index.d.ts +0 -1
  140. package/dist/components/List/index.js +0 -1
  141. package/dist/components/ListItem/ListItem.svelte +0 -175
  142. package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
  143. package/dist/components/ListItem/index.d.ts +0 -2
  144. package/dist/components/ListItem/index.js +0 -2
  145. package/dist/components/ListView/ListView.svelte +0 -463
  146. package/dist/components/ListView/ListView.svelte.d.ts +0 -37
  147. package/dist/components/ListView/index.d.ts +0 -2
  148. package/dist/components/ListView/index.js +0 -2
  149. package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
  150. package/dist/components/NodeGraph/BaseNode.js +0 -30
  151. package/dist/components/NodeGraph/Edge.svelte +0 -60
  152. package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
  153. package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
  154. package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
  155. package/dist/components/NodeGraph/Node.svelte +0 -100
  156. package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
  157. package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
  158. package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
  159. package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
  160. package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
  161. package/dist/components/NodeGraph/NodePort.svelte +0 -75
  162. package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
  163. package/dist/components/NodeGraph/decorators.d.ts +0 -81
  164. package/dist/components/NodeGraph/decorators.js +0 -148
  165. package/dist/components/NodeGraph/index.d.ts +0 -9
  166. package/dist/components/NodeGraph/index.js +0 -9
  167. package/dist/components/NodeGraph/types.d.ts +0 -94
  168. package/dist/components/NodeGraph/types.js +0 -33
  169. package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
  170. package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
  171. package/dist/components/NotesEditor/index.d.ts +0 -1
  172. package/dist/components/NotesEditor/index.js +0 -1
  173. package/dist/components/NumberInput/index.d.ts +0 -1
  174. package/dist/components/NumberInput/index.js +0 -1
  175. package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
  176. package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
  177. package/dist/components/PasswordInput/index.d.ts +0 -1
  178. package/dist/components/PasswordInput/index.js +0 -1
  179. package/dist/components/Popup/index.d.ts +0 -1
  180. package/dist/components/Popup/index.js +0 -1
  181. package/dist/components/Rating/Rating.svelte.d.ts +0 -7
  182. package/dist/components/Rating/index.d.ts +0 -1
  183. package/dist/components/Rating/index.js +0 -1
  184. package/dist/components/ScrollView/ScrollView.svelte +0 -285
  185. package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
  186. package/dist/components/ScrollView/index.d.ts +0 -1
  187. package/dist/components/ScrollView/index.js +0 -1
  188. package/dist/components/SearchInput/index.d.ts +0 -1
  189. package/dist/components/SearchInput/index.js +0 -1
  190. package/dist/components/Select/index.d.ts +0 -1
  191. package/dist/components/Select/index.js +0 -1
  192. package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
  193. package/dist/components/TextArea/index.d.ts +0 -1
  194. package/dist/components/TextArea/index.js +0 -1
  195. package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
  196. package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
  197. package/dist/components/ThemeSelector/index.d.ts +0 -1
  198. package/dist/components/ThemeSelector/index.js +0 -1
  199. package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
  200. package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
  201. package/dist/components/TooltipHandler/index.d.ts +0 -1
  202. package/dist/components/TooltipHandler/index.js +0 -1
  203. package/dist/styles/sizes.d.ts +0 -78
  204. package/dist/styles/sizes.js +0 -120
  205. package/dist/styles/variants.d.ts +0 -106
  206. package/dist/styles/variants.js +0 -194
  207. package/dist/types/ManagerTypes.d.ts +0 -42
  208. /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
@@ -1,6 +1,9 @@
1
1
  <script lang="ts">
2
- import { untrack } from 'svelte';
3
- import type { Snippet } from 'svelte';
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 = 28, // Default: var(--ui-height) - (var(--ui-border) * 2) = 32px - 4px = 28px
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: var(--ui-border) * 2 for normal state, 1px * 2 for disabled
138
- const borderOffset = disabled ? 2 : 4; // TODO: Use CSS var(--ui-border) when possible
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 = Math.min(options.length, maxVisibleItems) * actualItemHeight + borderOffset;
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
- 'Content-Type': 'application/json',
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 !== 'AbortError') {
279
- console.error('List API fetch error:', 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 === 'Tab') {
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 === 'ArrowDown' || event.key === 'ArrowUp') {
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 === 'ArrowDown') {
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 === 'ArrowDown') {
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('mousemove', handleThumbMouseMove);
413
- document.addEventListener('mouseup', handleThumbMouseUp);
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(0, Math.min(maxScrollTop, dragStartScrollTop + scrollRatio * maxScrollTop));
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('mousemove', handleThumbMouseMove);
430
- document.removeEventListener('mouseup', handleThumbMouseUp);
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(20, Math.min(currentMaxHeight - (trackPadding * 2) - 4, visibleRatio * currentMaxHeight));
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 - (trackPadding * 2);
457
- thumbTop = trackPadding + (scrollRatio * trackHeight);
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('scroll', updateScrollPosition);
489
+
490
+ listElement.addEventListener("scroll", updateScrollPosition);
467
491
  return () => {
468
492
  if (listElement) {
469
- listElement.removeEventListener('scroll', updateScrollPosition);
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="{disabled ? '[background-color:color-mix(in_srgb,var(--color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--color-secondary)_50%,transparent)] border cursor-not-allowed' : '[background-color:var(--color-background)] [border-color:var(--color-secondary)] hover:[border-color:color-mix(in_srgb,var(--color-secondary)_80%,black)] focus:[border-color:color-mix(in_srgb,var(--color-secondary)_90%,black)] focus:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus:[outline-offset:0px]'} rounded-md overflow-y-scroll custom-list {dropdownClass}"
481
- style="height: {maxHeight()}px; scrollbar-width: none; -ms-overflow-style: none; overscroll-behavior: contain; {disabled ? 'border-width: 1px;' : 'border-width: var(--ui-border);'}"
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
- {#if isLoading && showLoading}
493
- <div class="flex items-center justify-center py-4 [color:var(--color-secondary)]">
494
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 [border-color:var(--color-secondary)] mr-2"></div>
495
- {loadingText}
496
- </div>
497
- {:else if hasError && showError}
498
- <div class="flex items-center justify-center py-4 [color:var(--color-danger)]">
499
- <span class="text-sm [color:var(--color-danger)]">{errorText}</span>
500
- </div>
501
- {:else if allOptions().length === 0}
502
- <div class="flex items-center justify-center [color:var(--color-secondary)] select-none" style="height: {itemHeight}px;">
503
- <span class="px-4 text-sm [color:var(--color-secondary)]">{emptyText}</span>
504
- </div>
505
- {:else}
506
- {#each allOptions() as option, index (option.value)}
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
- {#if itemSnippet}
520
- {@render itemSnippet(option, index, isSelected(option.value), focusedIndex === index)}
521
- {:else if showCheckboxes}
522
- <div class="flex items-center gap-3 px-2 py-2 w-full">
523
- <div class="relative">
524
- <input
525
- type="checkbox"
526
- checked={isSelected(option.value)}
527
- disabled={disabled}
528
- class="w-4 h-4 rounded [border-color:var(--color-secondary)] {isSelected(option.value) ? '[background-color:var(--color-primary)] [border-color:var(--color-primary)]' : '[background-color:var(--color-background)]'} {disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
529
- tabindex="-1"
530
- readonly
531
- />
532
- {#if isSelected(option.value)}
533
- <svg class="absolute inset-0 w-4 h-4 text-white pointer-events-none" fill="currentColor" viewBox="0 0 20 20">
534
- <path fill-rule="evenodd" 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" clip-rule="evenodd" />
535
- </svg>
536
- {/if}
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
- <span class="flex-1 text-left text-sm">{option.label}</span>
539
- </div>
540
- {:else}
541
- <span class="px-2 w-full text-sm leading-tight">{option.label}</span>
542
- {/if}
543
- </button>
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 ? Math.round((listElement.scrollTop / (listElement.scrollHeight - listElement.clientHeight)) * 100) : 0}
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 'svelte';
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[];