simplesvelte 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,23 @@
1
- <script lang="ts">
2
- import { clickOutside } from './utils.js'
3
- import Input from './Input.svelte'
4
- import Label from './Label.svelte'
5
- type Option = {
1
+ <script lang="ts" module>
2
+ export type SelectOption = {
6
3
  value: any
7
4
  label: any
8
5
  group?: string
9
6
  [key: string]: any // Allow additional properties
10
7
  }
11
8
 
9
+ export type SelectFetchParams = { filter: string | undefined }
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import Input from './Input.svelte'
14
+ import Label from './Label.svelte'
15
+
12
16
  type Props = {
13
17
  value?: string | number | undefined | null | (string | number)[]
14
- options: Option[]
18
+ options?: SelectOption[]
19
+ fetchOptions?: (params: SelectFetchParams) => Promise<SelectOption[]>
20
+ debounceMs?: number
15
21
  name?: string
16
22
  label?: string
17
23
  class?: string
@@ -31,7 +37,9 @@
31
37
 
32
38
  let {
33
39
  value = $bindable(undefined),
34
- options: items,
40
+ options: staticOptions = [],
41
+ fetchOptions,
42
+ debounceMs = 300,
35
43
  name,
36
44
  label,
37
45
  class: className = '',
@@ -44,7 +52,50 @@
44
52
  onchange,
45
53
  }: Props = $props()
46
54
 
47
- let detailsOpen = $state(false)
55
+ // Async fetch state
56
+ let asyncItems = $state<SelectOption[]>([])
57
+ let isLoading = $state(false)
58
+ let fetchError = $state<string | undefined>(undefined)
59
+ let debounceTimeout: ReturnType<typeof setTimeout> | undefined
60
+
61
+ // Use async items if fetchOptions is provided, otherwise use static options
62
+ let items = $derived(fetchOptions ? asyncItems : staticOptions)
63
+
64
+ // Fetch options when filter changes (debounced)
65
+ async function fetchWithFilter(filterValue: string | undefined) {
66
+ if (!fetchOptions) return
67
+
68
+ // Clear previous timeout
69
+ if (debounceTimeout) clearTimeout(debounceTimeout)
70
+
71
+ debounceTimeout = setTimeout(async () => {
72
+ isLoading = true
73
+ fetchError = undefined
74
+ try {
75
+ asyncItems = await fetchOptions({ filter: filterValue })
76
+ } catch (err) {
77
+ fetchError = err instanceof Error ? err.message : 'Failed to fetch options'
78
+ asyncItems = []
79
+ } finally {
80
+ isLoading = false
81
+ }
82
+ }, debounceMs)
83
+ }
84
+
85
+ // Trigger fetch when dropdown opens or filter input changes (if using async)
86
+ $effect(() => {
87
+ if (fetchOptions && dropdownOpen) {
88
+ // Track filterInput to re-run when it changes
89
+ const currentFilter = filterInput
90
+ fetchWithFilter(currentFilter || undefined)
91
+ }
92
+ })
93
+
94
+ let dropdownOpen = $state(false)
95
+
96
+ // Generate unique ID for popover
97
+ const popoverId = `select-popover-${Math.random().toString(36).slice(2, 9)}`
98
+ const anchorName = `--anchor-${popoverId}`
48
99
 
49
100
  // Ensure value is properly typed for single/multiple mode
50
101
  // Normalize on access rather than maintaining separate state
@@ -77,7 +128,7 @@
77
128
  if (!multiple) {
78
129
  // Close dropdown and update filter immediately
79
130
  filterInput = items.find((item) => item.value === itemValue)?.label || ''
80
- detailsOpen = false
131
+ closeDropdown()
81
132
  value = itemValue
82
133
  if (onchange) onchange(value)
83
134
  return
@@ -107,7 +158,7 @@
107
158
  function clearAll() {
108
159
  value = multiple ? [] : null
109
160
  filterInput = ''
110
- detailsOpen = false
161
+ closeDropdown()
111
162
  if (onchange) onchange(value)
112
163
  }
113
164
 
@@ -117,7 +168,7 @@
117
168
  // Display value in filter box for single-select when closed
118
169
  let filter = $derived.by(() => {
119
170
  // In single select mode when dropdown is closed, show selected item
120
- if (!multiple && !detailsOpen && selectedItem) {
171
+ if (!multiple && !dropdownOpen && selectedItem) {
121
172
  return selectedItem.label
122
173
  }
123
174
  // Otherwise use the user's filter input
@@ -125,16 +176,19 @@
125
176
  })
126
177
 
127
178
  let filteredItems = $derived.by(() => {
179
+ // When using async fetch, server handles filtering - return items as-is
180
+ if (fetchOptions) return items
181
+ // Client-side filtering for static options
128
182
  if (filter.length === 0) return items
129
183
  return items.filter((item) => item.label.toLowerCase().includes(filter.toLowerCase()))
130
184
  })
131
185
 
132
186
  // Flatten filteredItems into a list with group headers and options for virtual scroll
133
- type FlatListItem = { type: 'header'; group: string } | { type: 'option'; item: Option }
187
+ type FlatListItem = { type: 'header'; group: string } | { type: 'option'; item: SelectOption }
134
188
  let flatList = $derived.by(() => {
135
189
  const result: FlatListItem[] = []
136
- const groups: Record<string, Option[]> = {}
137
- const ungrouped: Option[] = []
190
+ const groups: Record<string, SelectOption[]> = {}
191
+ const ungrouped: SelectOption[] = []
138
192
 
139
193
  for (const item of filteredItems) {
140
194
  if (item.group) {
@@ -147,8 +201,8 @@
147
201
 
148
202
  // In multiple mode, separate selected and unselected items
149
203
  if (multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) {
150
- const selectedUngrouped: Option[] = []
151
- const unselectedUngrouped: Option[] = []
204
+ const selectedUngrouped: SelectOption[] = []
205
+ const unselectedUngrouped: SelectOption[] = []
152
206
 
153
207
  for (const item of ungrouped) {
154
208
  if (normalizedValue.includes(item.value)) {
@@ -198,6 +252,7 @@
198
252
  })
199
253
 
200
254
  let searchEL: HTMLInputElement | undefined = $state(undefined)
255
+ let popoverEl: HTMLElement | undefined = $state(undefined)
201
256
 
202
257
  // Virtual list implementation
203
258
  let scrollTop = $state(0)
@@ -239,7 +294,7 @@
239
294
  function scrollToSelected(node: HTMLDivElement) {
240
295
  const unsubscribe = $effect.root(() => {
241
296
  $effect(() => {
242
- if (detailsOpen) {
297
+ if (dropdownOpen) {
243
298
  // Only scroll on initial open, not on subsequent selection changes
244
299
  if (hasScrolledOnOpen) return
245
300
  hasScrolledOnOpen = true
@@ -296,6 +351,26 @@
296
351
  }
297
352
  return undefined
298
353
  })
354
+
355
+ function openDropdown() {
356
+ if (!popoverEl) return
357
+ popoverEl.showPopover()
358
+ }
359
+
360
+ function closeDropdown() {
361
+ if (!popoverEl) return
362
+ popoverEl.hidePopover()
363
+ }
364
+
365
+ // Handle popover toggle event to sync state
366
+ function handlePopoverToggle(e: ToggleEvent) {
367
+ dropdownOpen = e.newState === 'open'
368
+ if (dropdownOpen) {
369
+ searchEL?.focus()
370
+ } else {
371
+ hasScrolledOnOpen = false
372
+ }
373
+ }
299
374
  </script>
300
375
 
301
376
  <!-- Data inputs for form submission -->
@@ -309,167 +384,204 @@
309
384
 
310
385
  <Label {label} {name} optional={!required} class={className} error={errorText}>
311
386
  {#if !disabled}
312
- <details
313
- class="dropdown w-full"
314
- bind:open={detailsOpen}
315
- use:clickOutside={() => {
316
- if (!detailsOpen) return
317
- detailsOpen = false
387
+ <!-- Trigger button with popover target and anchor positioning -->
388
+ <button
389
+ type="button"
390
+ popovertarget={popoverId}
391
+ role="combobox"
392
+ aria-expanded={dropdownOpen}
393
+ aria-haspopup="listbox"
394
+ aria-controls={popoverId}
395
+ class="select relative h-max min-h-10 w-full min-w-12 cursor-pointer bg-none! pr-1 text-left"
396
+ style="anchor-name: {anchorName}"
397
+ title={tooltipText}
398
+ onclick={() => {
399
+ searchEL?.focus()
400
+ // Only clear filter in single-select mode; in multi-select, keep filter for continued searching
401
+ if (!multiple) filterInput = ''
402
+ }}
403
+ onkeydown={(e) => {
404
+ if (e.key === 'Escape') {
405
+ closeDropdown()
406
+ }
318
407
  }}>
319
- <summary
320
- class="select h-max min-h-10 w-full min-w-12 cursor-pointer bg-none! pr-1"
321
- title={tooltipText}
322
- onclick={() => {
323
- searchEL?.focus()
324
- // Only clear filter in single-select mode; in multi-select, keep filter for continued searching
325
- if (!multiple) filterInput = ''
326
- }}>
327
- {#if multiple}
328
- <!-- Multi-select display with condensed chips -->
329
- <div class="flex min-h-8 flex-wrap gap-1 p-1">
330
- {#if selectedItems.length > 0}
331
- <!-- Show first selected item -->
332
- <div class="badge badge-neutral bg-base-200 text-base-content gap-1">
333
- <span class="max-w-[200px] truncate">{selectedItems[0].label}</span>
334
- <button
335
- type="button"
336
- class="btn btn-xs btn-circle btn-ghost hover:bg-base-300"
337
- onclick={(e) => {
408
+ {#if multiple}
409
+ <!-- Multi-select display with condensed chips -->
410
+ <div class="flex min-h-8 flex-wrap gap-1 p-1">
411
+ {#if selectedItems.length > 0}
412
+ <!-- Show first selected item -->
413
+ <div class="badge badge-neutral bg-base-200 text-base-content gap-1">
414
+ <span class="max-w-[200px] truncate">{selectedItems[0].label}</span>
415
+ <span
416
+ role="button"
417
+ tabindex="0"
418
+ class="btn btn-xs btn-circle btn-ghost hover:bg-base-300"
419
+ onclick={(e) => {
420
+ e.stopPropagation()
421
+ removeSelectedItem(selectedItems[0].value)
422
+ }}
423
+ onkeydown={(e) => {
424
+ if (e.key === 'Enter' || e.key === ' ') {
425
+ e.preventDefault()
338
426
  e.stopPropagation()
339
427
  removeSelectedItem(selectedItems[0].value)
340
- }}>
341
-
342
- </button>
428
+ }
429
+ }}>
430
+
431
+ </span>
432
+ </div>
433
+
434
+ {#if selectedItems.length > 1}
435
+ <!-- Show count indicator for remaining items -->
436
+ <div class="badge badge-ghost text-base-content/70">
437
+ (+{selectedItems.length - 1} more)
343
438
  </div>
344
-
345
- {#if selectedItems.length > 1}
346
- <!-- Show count indicator for remaining items -->
347
- <div class="badge badge-ghost text-base-content/70">
348
- (+{selectedItems.length - 1} more)
349
- </div>
350
- {/if}
351
439
  {/if}
352
- <!-- Search input for filtering in multi-select -->
353
- <input
354
- type="text"
355
- class="h-full min-w-[120px] flex-1 outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
356
- bind:this={searchEL}
357
- bind:value={filterInput}
358
- onclick={() => {
359
- detailsOpen = true
360
- }}
361
- placeholder="Search..."
362
- required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
363
- </div>
364
- {:else}
365
- <!-- Single-select display -->
440
+ {/if}
441
+ <!-- Search input for filtering in multi-select -->
366
442
  <input
367
443
  type="text"
368
- class="h-full w-full outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
444
+ class="h-full min-w-[120px] flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
369
445
  bind:this={searchEL}
370
- value={filter}
371
- oninput={(e) => (filterInput = e.currentTarget.value)}
372
- onclick={() => {
373
- detailsOpen = true
374
- }}
375
- {placeholder}
376
- required={required && !normalizedValue} />
377
- {/if}
378
- {#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
379
- <button
380
- type="button"
381
- class="btn btn-sm btn-circle btn-ghost absolute top-1 right-1"
446
+ bind:value={filterInput}
382
447
  onclick={(e) => {
448
+ e.stopPropagation()
449
+ openDropdown()
450
+ }}
451
+ placeholder="Search..."
452
+ required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
453
+ </div>
454
+ {:else}
455
+ <!-- Single-select display -->
456
+ <input
457
+ type="text"
458
+ class="h-full w-full outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
459
+ bind:this={searchEL}
460
+ value={filter}
461
+ oninput={(e) => (filterInput = e.currentTarget.value)}
462
+ onclick={(e) => {
463
+ e.stopPropagation()
464
+ openDropdown()
465
+ }}
466
+ {placeholder}
467
+ required={required && !normalizedValue} />
468
+ {/if}
469
+ {#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
470
+ <span
471
+ role="button"
472
+ tabindex="0"
473
+ class="btn btn-sm btn-circle btn-ghost bg-base-100 absolute top-1 right-1"
474
+ onclick={(e) => {
475
+ e.stopPropagation()
476
+ clearAll()
477
+ }}
478
+ onkeydown={(e) => {
479
+ if (e.key === 'Enter' || e.key === ' ') {
480
+ e.preventDefault()
383
481
  e.stopPropagation()
384
482
  clearAll()
483
+ }
484
+ }}>
485
+
486
+ </span>
487
+ {/if}
488
+ </button>
489
+
490
+ <!-- Dropdown content using popover API -->
491
+ <ul
492
+ bind:this={popoverEl}
493
+ id={popoverId}
494
+ popover
495
+ role="listbox"
496
+ class="dropdown menu bg-base-100 rounded-box z-50 mt-2 flex flex-col flex-nowrap gap-1 p-2 shadow outline m-0"
497
+ style="position-anchor: {anchorName}; position: absolute; top: anchor(bottom); left: anchor(left); width: anchor-size(width)"
498
+ ontoggle={handlePopoverToggle}>
499
+ {#if multiple && filteredItems.length > 1}
500
+ <!-- Select All / Clear All options for multi-select -->
501
+ <div class="flex gap-2">
502
+ <button
503
+ type="button"
504
+ class="btn btn-sm hover:bg-base-content/10 grow"
505
+ onclick={() => {
506
+ const allValues = filteredItems.map((item) => item.value)
507
+ value = [...allValues]
508
+ if (onchange) onchange(value)
385
509
  }}>
386
-
510
+ Select All
387
511
  </button>
388
- {/if}
389
- </summary>
390
- <ul
391
- class="menu dropdown-content bg-base-100 rounded-box z-10 mt-2 flex w-full flex-col flex-nowrap gap-1 p-2 shadow outline">
392
- {#if multiple && filteredItems.length > 1}
393
- <!-- Select All / Clear All options for multi-select -->
394
-
395
- <div class="flex gap-2">
396
- <button
397
- type="button"
398
- class="btn btn-sm hover:bg-base-content/10 grow"
399
- onclick={() => {
400
- const allValues = filteredItems.map((item) => item.value)
401
- value = [...allValues]
402
- if (onchange) onchange(value)
403
- }}>
404
- Select All
405
- </button>
406
- <button
407
- type="button"
408
- class="btn btn-sm hover:bg-base-content/10 grow"
409
- onclick={() => {
410
- value = []
411
- if (onchange) onchange(value)
412
- }}>
413
- Clear All
414
- </button>
415
- </div>
416
- {/if}
417
- {#if filteredItems.length === 0}
418
- <li class="m-2 text-center text-sm text-gray-500">No items found</li>
419
- {/if}
420
-
421
- {#if flatList.length > 0}
422
- <div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
423
- <!-- Virtual spacer for items before visible range -->
424
- {#if visibleItems.startIndex > 0}
425
- <div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
426
- {/if}
427
-
428
- <!-- Render only visible items (headers and options) -->
429
- {#each visibleItems.items as entry, idx (entry.type === 'header' ? 'header-' + entry.group + '-' + idx : 'option-' + entry.item.value + '-' + idx)}
430
- {#if entry.type === 'header'}
431
- <li
432
- class="bg-base-200 top-0 z-10 flex items-center justify-center px-2 text-lg font-bold text-gray-700"
433
- style="height: {itemHeight}px;">
434
- {entry.group}
435
- </li>
436
- {:else}
437
- {@const item = entry.item}
438
- {@const isSelected = multiple
439
- ? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
440
- : item.value === normalizedValue}
441
- <li style="height: {itemHeight}px;">
442
- <button
443
- class="flex h-full w-full items-center gap-2 {isSelected
444
- ? ' bg-primary text-primary-content hover:bg-primary/70!'
445
- : ''}"
446
- type="button"
447
- onclick={(e) => {
448
- e.stopPropagation()
449
- toggleItemSelection(item.value)
450
- searchEL?.focus()
451
- }}>
452
- {#if multiple}
453
- <input
454
- type="checkbox"
455
- class="checkbox checkbox-sm text-primary-content! pointer-events-none"
456
- checked={isSelected}
457
- readonly />
458
- {/if}
459
- <span class="flex-1 overflow-hidden text-left text-nowrap text-ellipsis">{item.label}</span>
460
- </button>
461
- </li>
462
- {/if}
463
- {/each}
464
-
465
- <!-- Virtual spacer for items after visible range -->
466
- {#if visibleItems.endIndex < visibleItems.total}
467
- <div style="height: {(visibleItems.total - visibleItems.endIndex) * itemHeight}px;"></div>
512
+ <button
513
+ type="button"
514
+ class="btn btn-sm hover:bg-base-content/10 grow"
515
+ onclick={() => {
516
+ value = []
517
+ if (onchange) onchange(value)
518
+ }}>
519
+ Clear All
520
+ </button>
521
+ </div>
522
+ {/if}
523
+ {#if isLoading}
524
+ <li class="m-2 flex items-center justify-center gap-2 text-sm text-gray-500">
525
+ <span class="loading loading-spinner loading-sm"></span>
526
+ Loading...
527
+ </li>
528
+ {:else if fetchError}
529
+ <li class="m-2 text-center text-sm text-error">{fetchError}</li>
530
+ {:else if filteredItems.length === 0}
531
+ <li class="m-2 text-center text-sm text-gray-500">No items found</li>
532
+ {/if}
533
+
534
+ {#if flatList.length > 0}
535
+ <div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
536
+ <!-- Virtual spacer for items before visible range -->
537
+ {#if visibleItems.startIndex > 0}
538
+ <div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
539
+ {/if}
540
+
541
+ <!-- Render only visible items (headers and options) -->
542
+ {#each visibleItems.items as entry, idx (entry.type === 'header' ? 'header-' + entry.group + '-' + idx : 'option-' + entry.item.value + '-' + idx)}
543
+ {#if entry.type === 'header'}
544
+ <li
545
+ class="bg-base-200 top-0 z-10 flex items-center justify-center px-2 text-lg font-bold text-gray-700"
546
+ style="height: {itemHeight}px;">
547
+ {entry.group}
548
+ </li>
549
+ {:else}
550
+ {@const item = entry.item}
551
+ {@const isSelected = multiple
552
+ ? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
553
+ : item.value === normalizedValue}
554
+ <li style="height: {itemHeight}px;" role="option" aria-selected={isSelected}>
555
+ <button
556
+ class="flex h-full w-full items-center gap-2 {isSelected
557
+ ? ' bg-primary text-primary-content hover:bg-primary/70!'
558
+ : ''}"
559
+ type="button"
560
+ onclick={(e) => {
561
+ e.stopPropagation()
562
+ toggleItemSelection(item.value)
563
+ searchEL?.focus()
564
+ }}>
565
+ {#if multiple}
566
+ <input
567
+ type="checkbox"
568
+ class="checkbox checkbox-sm text-primary-content! pointer-events-none"
569
+ checked={isSelected}
570
+ readonly />
571
+ {/if}
572
+ <span class="flex-1 overflow-hidden text-left text-nowrap text-ellipsis">{item.label}</span>
573
+ </button>
574
+ </li>
468
575
  {/if}
469
- </div>
470
- {/if}
471
- </ul>
472
- </details>
576
+ {/each}
577
+
578
+ <!-- Virtual spacer for items after visible range -->
579
+ {#if visibleItems.endIndex < visibleItems.total}
580
+ <div style="height: {(visibleItems.total - visibleItems.endIndex) * itemHeight}px;"></div>
581
+ {/if}
582
+ </div>
583
+ {/if}
584
+ </ul>
473
585
  {:else}
474
586
  <!-- Disabled state -->
475
587
  {#if multiple}
@@ -1,12 +1,17 @@
1
- type Option = {
1
+ export type SelectOption = {
2
2
  value: any;
3
3
  label: any;
4
4
  group?: string;
5
5
  [key: string]: any;
6
6
  };
7
+ export type SelectFetchParams = {
8
+ filter: string | undefined;
9
+ };
7
10
  type Props = {
8
11
  value?: string | number | undefined | null | (string | number)[];
9
- options: Option[];
12
+ options?: SelectOption[];
13
+ fetchOptions?: (params: SelectFetchParams) => Promise<SelectOption[]>;
14
+ debounceMs?: number;
10
15
  name?: string;
11
16
  label?: string;
12
17
  class?: string;
package/dist/styles.css CHANGED
@@ -8,3 +8,8 @@ body {
8
8
  height: 100%;
9
9
  background-color: var(--color-base-200);
10
10
  }
11
+
12
+ /* Ensure popover elements are hidden by default - use !important to override menu display:flex */
13
+ [popover]:not(:popover-open) {
14
+ display: none !important;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",
@@ -36,31 +36,31 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/compat": "^1.4.1",
39
- "@eslint/js": "^9.39.0",
40
- "@sveltejs/adapter-cloudflare": "^7.2.4",
41
- "@sveltejs/kit": "^2.48.4",
42
- "@sveltejs/package": "^2.5.4",
43
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
44
- "@tailwindcss/cli": "^4.1.16",
45
- "@tailwindcss/vite": "^4.1.16",
46
- "@testing-library/svelte": "^5.2.8",
39
+ "@eslint/js": "^9.39.2",
40
+ "@sveltejs/adapter-cloudflare": "^7.2.5",
41
+ "@sveltejs/kit": "^2.50.0",
42
+ "@sveltejs/package": "^2.5.7",
43
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
44
+ "@tailwindcss/cli": "^4.1.18",
45
+ "@tailwindcss/vite": "^4.1.18",
46
+ "@testing-library/svelte": "^5.3.1",
47
47
  "@testing-library/user-event": "^14.6.1",
48
- "daisyui": "^5.4.2",
49
- "eslint": "^9.39.0",
48
+ "daisyui": "^5.5.14",
49
+ "eslint": "^9.39.2",
50
50
  "eslint-config-prettier": "^10.1.8",
51
- "eslint-plugin-svelte": "^3.13.0",
51
+ "eslint-plugin-svelte": "^3.14.0",
52
52
  "globals": "^16.5.0",
53
- "jsdom": "^27.1.0",
54
- "prettier": "^3.6.2",
55
- "prettier-plugin-svelte": "^3.4.0",
53
+ "jsdom": "^27.4.0",
54
+ "prettier": "^3.8.1",
55
+ "prettier-plugin-svelte": "^3.4.1",
56
56
  "prettier-plugin-tailwindcss": "^0.6.14",
57
- "publint": "^0.3.15",
58
- "svelte": "^5.43.2",
59
- "svelte-check": "^4.3.3",
60
- "tailwindcss": "^4.1.16",
57
+ "publint": "^0.3.17",
58
+ "svelte": "^5.47.1",
59
+ "svelte-check": "^4.3.5",
60
+ "tailwindcss": "^4.1.18",
61
61
  "typescript": "^5.9.3",
62
- "typescript-eslint": "^8.46.3",
63
- "vite": "^7.1.12",
62
+ "typescript-eslint": "^8.53.1",
63
+ "vite": "^7.3.1",
64
64
  "vitest": "^3.2.4"
65
65
  },
66
66
  "keywords": [