@rokkit/ui 1.0.0-next.99 → 1.0.2

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 (83) hide show
  1. package/README.md +159 -1
  2. package/package.json +52 -53
  3. package/src/MarkdownRenderer.svelte +106 -0
  4. package/src/components/AlertList.svelte +59 -0
  5. package/src/components/Avatar.svelte +68 -0
  6. package/src/components/Badge.svelte +57 -0
  7. package/src/components/BreadCrumbs.svelte +94 -0
  8. package/src/components/Button.svelte +91 -0
  9. package/src/components/ButtonGroup.svelte +9 -0
  10. package/src/components/Card.svelte +71 -0
  11. package/src/components/Carousel.svelte +169 -0
  12. package/src/components/Code.svelte +189 -0
  13. package/src/components/Connector.svelte +46 -0
  14. package/src/components/Divider.svelte +24 -0
  15. package/src/components/Dropdown.svelte +202 -0
  16. package/src/components/FloatingAction.svelte +367 -0
  17. package/src/components/FloatingNavigation.svelte +256 -0
  18. package/src/components/Grid.svelte +128 -0
  19. package/src/components/ItemContent.svelte +25 -0
  20. package/src/components/LazyTree.svelte +171 -0
  21. package/src/components/List.svelte +224 -0
  22. package/src/components/Menu.svelte +270 -0
  23. package/src/components/Message.svelte +75 -0
  24. package/src/components/MultiSelect.svelte +367 -0
  25. package/src/components/NavContent.svelte +42 -0
  26. package/src/components/PaletteManager.svelte +360 -0
  27. package/src/components/Pill.svelte +83 -0
  28. package/src/components/ProgressBar.svelte +31 -0
  29. package/src/components/Range.svelte +346 -0
  30. package/src/components/Rating.svelte +115 -0
  31. package/src/components/ResponsiveGrid.svelte +34 -0
  32. package/src/components/Reveal.svelte +46 -0
  33. package/src/components/SearchFilter.svelte +88 -0
  34. package/src/components/Select.svelte +424 -0
  35. package/src/components/Shine.svelte +87 -0
  36. package/src/components/Stack.svelte +38 -0
  37. package/src/components/StatusList.svelte +18 -0
  38. package/src/components/Stepper.svelte +174 -0
  39. package/src/components/Swatch.svelte +102 -0
  40. package/src/components/Switch.svelte +71 -0
  41. package/src/components/Table.svelte +245 -0
  42. package/src/components/Tabs.svelte +202 -0
  43. package/src/components/Tilt.svelte +68 -0
  44. package/src/components/Timeline.svelte +61 -0
  45. package/src/components/Toggle.svelte +93 -0
  46. package/src/components/Toolbar.svelte +306 -0
  47. package/src/components/ToolbarGroup.svelte +17 -0
  48. package/src/components/Tooltip.svelte +121 -0
  49. package/src/components/Tree.svelte +143 -0
  50. package/src/components/UploadFileStatus.svelte +102 -0
  51. package/src/components/UploadProgress.svelte +127 -0
  52. package/src/components/UploadTarget.svelte +126 -0
  53. package/src/components/index.ts +50 -0
  54. package/src/index.ts +61 -0
  55. package/src/markdown-plugin.ts +12 -0
  56. package/src/types/button.ts +86 -0
  57. package/src/types/code.ts +46 -0
  58. package/src/types/floating-action.ts +123 -0
  59. package/src/types/floating-navigation.ts +80 -0
  60. package/src/types/index.ts +58 -0
  61. package/src/types/list.ts +200 -0
  62. package/src/types/menu.ts +95 -0
  63. package/src/types/palette.ts +160 -0
  64. package/src/types/range.ts +51 -0
  65. package/src/types/search-filter.ts +67 -0
  66. package/src/types/select.ts +176 -0
  67. package/src/types/switch.ts +68 -0
  68. package/src/types/table.ts +219 -0
  69. package/src/types/tabs.ts +103 -0
  70. package/src/types/timeline.ts +53 -0
  71. package/src/types/toggle.ts +68 -0
  72. package/src/types/toolbar.ts +167 -0
  73. package/src/types/tooltip.ts +21 -0
  74. package/src/types/tree.ts +250 -0
  75. package/src/types/upload-file-status.ts +45 -0
  76. package/src/types/upload-progress.ts +111 -0
  77. package/src/types/upload-target.ts +71 -0
  78. package/src/utils/palette.ts +618 -0
  79. package/src/utils/shiki.ts +114 -0
  80. package/src/utils/upload.js +145 -0
  81. package/LICENSE +0 -21
  82. package/src/index.js +0 -8
  83. package/src/wrappers.js +0 -2
@@ -0,0 +1,346 @@
1
+ <script lang="ts">
2
+ import type { RangeProps } from '../types/range.js'
3
+ import { generateTicks } from '@rokkit/core'
4
+ import { pannable } from '@rokkit/actions'
5
+ import { messages } from '@rokkit/states'
6
+
7
+ let {
8
+ value = $bindable(50),
9
+ lower = $bindable(0),
10
+ upper = $bindable(100),
11
+ min = 0,
12
+ max = 100,
13
+ step = 1,
14
+ range: rangeMode = false,
15
+ ticks = 0,
16
+ labelSkip = 0,
17
+ disabled = false,
18
+ labels: userLabels = {},
19
+ onchange,
20
+ class: className = ''
21
+ }: RangeProps & { labels?: Record<string, string> } = $props()
22
+
23
+ const labels = $derived({ ...messages.range, ...userLabels })
24
+
25
+ // ─── Pixel state ────────────────────────────────────────────────
26
+ let trackWidth = $state(0)
27
+
28
+ // ─── Helpers ────────────────────────────────────────────────────
29
+
30
+ function inverseLerp(val: number, lo: number, hi: number): number {
31
+ if (hi === lo) return 0
32
+ return (val - lo) / (hi - lo)
33
+ }
34
+
35
+ function valueToPercent(val: number): number {
36
+ return inverseLerp(val, min, max) * 100
37
+ }
38
+
39
+ function pixelToValue(px: number): number {
40
+ if (trackWidth === 0) return min
41
+ return min + (px / trackWidth) * (max - min)
42
+ }
43
+
44
+ function clamp(val: number, lo: number, hi: number): number {
45
+ return Math.min(Math.max(val, lo), hi)
46
+ }
47
+
48
+ function snapToStep(val: number): number {
49
+ if (step <= 0) return val
50
+ return Math.round((val - min) / step) * step + min
51
+ }
52
+
53
+ function fireChange() {
54
+ if (rangeMode) {
55
+ onchange?.([lower, upper])
56
+ } else {
57
+ onchange?.(value)
58
+ }
59
+ }
60
+
61
+ // ─── Derived positions (percentages) ────────────────────────────
62
+
63
+ let selectedLeft = $derived(rangeMode ? valueToPercent(lower) : 0)
64
+ let selectedWidth = $derived(
65
+ rangeMode ? valueToPercent(upper) - valueToPercent(lower) : valueToPercent(value)
66
+ )
67
+
68
+ // ─── Ticks ──────────────────────────────────────────────────────
69
+
70
+ let tickStep = $derived(ticks > 0 ? Math.max(1, Math.round((max - min) / ticks)) : 0)
71
+ let tickItems = $derived(
72
+ ticks > 0 ? generateTicks(min, max, tickStep, labelSkip > 0 ? labelSkip + 1 : 1) : []
73
+ )
74
+
75
+ // ─── Thumb drag — upper (or single) ─────────────────────────────
76
+
77
+ let slidingUpper = $state(false)
78
+
79
+ function handleUpperPanStart() {
80
+ if (disabled) return
81
+ slidingUpper = true
82
+ }
83
+
84
+ function panUpperBy(dx: number) {
85
+ const currentPx = inverseLerp(rangeMode ? upper : value, min, max) * trackWidth
86
+ const minPx = rangeMode ? inverseLerp(lower, min, max) * trackWidth : 0
87
+ const newPx = clamp(currentPx + dx, minPx, trackWidth)
88
+ const snapped = snapToStep(clamp(pixelToValue(newPx), rangeMode ? lower : min, max))
89
+ if (rangeMode) upper = snapped
90
+ else value = snapped
91
+ }
92
+
93
+ function handleUpperPanMove(event: CustomEvent) {
94
+ if (disabled || trackWidth === 0) return
95
+ panUpperBy(event.detail.dx as number)
96
+ fireChange()
97
+ }
98
+
99
+ function handleUpperPanEnd() {
100
+ slidingUpper = false
101
+ // Final snap
102
+ if (rangeMode) {
103
+ upper = snapToStep(clamp(upper, lower, max))
104
+ } else {
105
+ value = snapToStep(clamp(value, min, max))
106
+ }
107
+ fireChange()
108
+ }
109
+
110
+ function nudgeUpper(delta: number) {
111
+ const increment = step > 0 ? step : (max - min) / 10
112
+ if (rangeMode) upper = clamp(snapToStep(upper + delta * increment), lower, max)
113
+ else value = clamp(snapToStep(value + delta * increment), min, max)
114
+ }
115
+
116
+ function jumpUpper(toEnd: boolean) {
117
+ if (rangeMode) upper = toEnd ? max : lower
118
+ else value = toEnd ? max : min
119
+ }
120
+
121
+ function isIncreaseKey(key: string): boolean {
122
+ return key === 'ArrowRight' || key === 'ArrowUp'
123
+ }
124
+
125
+ function isDecreaseKey(key: string): boolean {
126
+ return key === 'ArrowLeft' || key === 'ArrowDown'
127
+ }
128
+
129
+ function applyUpperKey(key: string): boolean {
130
+ if (isIncreaseKey(key)) {
131
+ nudgeUpper(1)
132
+ return true
133
+ }
134
+ if (isDecreaseKey(key)) {
135
+ nudgeUpper(-1)
136
+ return true
137
+ }
138
+ if (key === 'Home') {
139
+ jumpUpper(false)
140
+ return true
141
+ }
142
+ if (key === 'End') {
143
+ jumpUpper(true)
144
+ return true
145
+ }
146
+ return false
147
+ }
148
+
149
+ function handleUpperKeyDown(event: KeyboardEvent) {
150
+ if (disabled) return
151
+ if (applyUpperKey(event.key)) {
152
+ event.preventDefault()
153
+ fireChange()
154
+ }
155
+ }
156
+
157
+ // ─── Thumb drag — lower (range mode only) ───────────────────────
158
+
159
+ let slidingLower = $state(false)
160
+
161
+ function handleLowerPanStart() {
162
+ if (disabled) return
163
+ slidingLower = true
164
+ }
165
+
166
+ function handleLowerPanMove(event: CustomEvent) {
167
+ if (disabled || trackWidth === 0) return
168
+ const dx = event.detail.dx as number
169
+ const currentPx = inverseLerp(lower, min, max) * trackWidth
170
+ const newPx = clamp(currentPx + dx, 0, inverseLerp(upper, min, max) * trackWidth)
171
+ const raw = pixelToValue(newPx)
172
+ lower = snapToStep(clamp(raw, min, upper))
173
+ fireChange()
174
+ }
175
+
176
+ function handleLowerPanEnd() {
177
+ slidingLower = false
178
+ lower = snapToStep(clamp(lower, min, upper))
179
+ fireChange()
180
+ }
181
+
182
+ function nudgeLower(delta: number) {
183
+ const increment = step > 0 ? step : (max - min) / 10
184
+ lower = clamp(snapToStep(lower + delta * increment), min, upper)
185
+ }
186
+
187
+ function applyLowerKey(key: string): boolean {
188
+ if (isIncreaseKey(key)) {
189
+ nudgeLower(1)
190
+ return true
191
+ }
192
+ if (isDecreaseKey(key)) {
193
+ nudgeLower(-1)
194
+ return true
195
+ }
196
+ if (key === 'Home') {
197
+ lower = min
198
+ return true
199
+ }
200
+ if (key === 'End') {
201
+ lower = upper
202
+ return true
203
+ }
204
+ return false
205
+ }
206
+
207
+ function handleLowerKeyDown(event: KeyboardEvent) {
208
+ if (disabled) return
209
+ if (applyLowerKey(event.key)) {
210
+ event.preventDefault()
211
+ fireChange()
212
+ }
213
+ }
214
+
215
+ // ─── Track click ────────────────────────────────────────────────
216
+
217
+ function handleTrackClick(event: MouseEvent) {
218
+ if (disabled) return
219
+ const target = event.currentTarget as HTMLElement
220
+ const rect = target.getBoundingClientRect()
221
+ const px = event.clientX - rect.left
222
+ const raw = pixelToValue(px)
223
+ const snapped = snapToStep(clamp(raw, min, max))
224
+
225
+ if (rangeMode) {
226
+ // Click closer to lower → adjust lower, else adjust upper
227
+ const distLower = Math.abs(snapped - lower)
228
+ const distUpper = Math.abs(snapped - upper)
229
+ if (distLower < distUpper) {
230
+ lower = clamp(snapped, min, upper)
231
+ } else {
232
+ upper = clamp(snapped, lower, max)
233
+ }
234
+ } else {
235
+ value = snapped
236
+ }
237
+ fireChange()
238
+ }
239
+
240
+ // ─── Tick click ─────────────────────────────────────────────────
241
+
242
+ function handleTickClick(tickValue: number) {
243
+ if (disabled) return
244
+ const snapped = snapToStep(clamp(tickValue, min, max))
245
+
246
+ if (rangeMode) {
247
+ const distLower = Math.abs(snapped - lower)
248
+ const distUpper = Math.abs(snapped - upper)
249
+ if (distLower < distUpper) {
250
+ lower = clamp(snapped, min, upper)
251
+ } else {
252
+ upper = clamp(snapped, lower, max)
253
+ }
254
+ } else {
255
+ value = snapped
256
+ }
257
+ fireChange()
258
+ }
259
+ </script>
260
+
261
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
262
+ <div data-range data-disabled={disabled || undefined} class={className || undefined}>
263
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
264
+ <div data-range-track onclick={handleTrackClick}>
265
+ <div data-range-bar bind:clientWidth={trackWidth}></div>
266
+ <div data-range-selected style:left="{selectedLeft}%" style:width="{selectedWidth}%"></div>
267
+
268
+ {#if rangeMode}
269
+ <!-- Lower thumb -->
270
+ <div
271
+ data-range-thumb
272
+ data-thumb="lower"
273
+ data-sliding={slidingLower || undefined}
274
+ style:left="{valueToPercent(lower)}%"
275
+ tabindex={disabled ? -1 : 0}
276
+ use:pannable
277
+ onpanstart={handleLowerPanStart}
278
+ onpanmove={handleLowerPanMove}
279
+ onpanend={handleLowerPanEnd}
280
+ onkeydown={handleLowerKeyDown}
281
+ onfocus={() => {
282
+ if (!disabled) slidingLower = true
283
+ }}
284
+ onblur={() => {
285
+ slidingLower = false
286
+ }}
287
+ role="slider"
288
+ aria-valuenow={lower}
289
+ aria-valuemin={min}
290
+ aria-valuemax={upper}
291
+ aria-disabled={disabled || undefined}
292
+ aria-label={labels.lower}
293
+ ></div>
294
+ {/if}
295
+
296
+ <!-- Upper thumb (or single thumb) -->
297
+ <div
298
+ data-range-thumb
299
+ data-thumb={rangeMode ? 'upper' : 'value'}
300
+ data-sliding={slidingUpper || undefined}
301
+ style:left="{valueToPercent(rangeMode ? upper : value)}%"
302
+ tabindex={disabled ? -1 : 0}
303
+ use:pannable
304
+ onpanstart={handleUpperPanStart}
305
+ onpanmove={handleUpperPanMove}
306
+ onpanend={handleUpperPanEnd}
307
+ onkeydown={handleUpperKeyDown}
308
+ onfocus={() => {
309
+ if (!disabled) slidingUpper = true
310
+ }}
311
+ onblur={() => {
312
+ slidingUpper = false
313
+ }}
314
+ role="slider"
315
+ aria-valuenow={rangeMode ? upper : value}
316
+ aria-valuemin={rangeMode ? lower : min}
317
+ aria-valuemax={max}
318
+ aria-disabled={disabled || undefined}
319
+ aria-label={rangeMode ? labels.upper : labels.value}
320
+ ></div>
321
+ </div>
322
+
323
+ {#if tickItems.length > 0}
324
+ <div data-range-ticks style:--count={tickItems.length - 1}>
325
+ {#each tickItems as tick (tick.value)}
326
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
327
+ <div
328
+ data-range-tick
329
+ data-disabled={disabled || undefined}
330
+ role="option"
331
+ aria-selected={false}
332
+ aria-disabled={disabled || undefined}
333
+ tabindex={disabled ? -1 : 0}
334
+ onclick={() => handleTickClick(tick.value)}
335
+ >
336
+ <div data-tick-bar></div>
337
+ {#if tick.label !== ''}
338
+ <span data-tick-label>{tick.label}</span>
339
+ {:else}
340
+ <span data-tick-label></span>
341
+ {/if}
342
+ </div>
343
+ {/each}
344
+ </div>
345
+ {/if}
346
+ </div>
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
3
+ import { messages } from '@rokkit/states'
4
+
5
+ interface RatingIcons {
6
+ filled?: string
7
+ empty?: string
8
+ half?: string
9
+ }
10
+
11
+ interface RatingProps {
12
+ /** Current rating value (bindable) */
13
+ value?: number
14
+ /** Maximum number of stars (default: 5) */
15
+ max?: number
16
+ /** Disabled state */
17
+ disabled?: boolean
18
+ /** Custom icons for rating states */
19
+ icons?: RatingIcons
20
+ /** Called when value changes */
21
+ onchange?: (value: number) => void
22
+ /** Additional CSS class */
23
+ class?: string
24
+ }
25
+
26
+ let {
27
+ value = $bindable(0),
28
+ max = 5,
29
+ disabled = false,
30
+ label = messages.rating.label,
31
+ icons: userIcons = {} as RatingIcons,
32
+ onchange,
33
+ class: className = ''
34
+ }: RatingProps & { label?: string } = $props()
35
+
36
+ const icons = $derived({ ...DEFAULT_STATE_ICONS.rating, ...userIcons })
37
+
38
+ let hoverIndex = $state(-1)
39
+
40
+ function handleClick(index: number) {
41
+ if (disabled) return
42
+ // Clicking same star toggles to 0, otherwise set to index + 1
43
+ const newValue = value === index + 1 ? 0 : index + 1
44
+ value = newValue
45
+ onchange?.(newValue)
46
+ }
47
+
48
+ function setRating(newValue: number) {
49
+ value = newValue
50
+ onchange?.(newValue)
51
+ }
52
+
53
+ function tryDigitKey(key: string): boolean {
54
+ const digit = parseInt(key, 10)
55
+ if (isNaN(digit) || digit < 0 || digit > max) return false
56
+ setRating(digit)
57
+ return true
58
+ }
59
+
60
+ function isIncreaseKey(key: string): boolean {
61
+ return key === 'ArrowRight' || key === 'ArrowUp'
62
+ }
63
+
64
+ function isDecreaseKey(key: string): boolean {
65
+ return key === 'ArrowLeft' || key === 'ArrowDown'
66
+ }
67
+
68
+ function handleKeyDown(event: KeyboardEvent) {
69
+ if (disabled) return
70
+
71
+ if (isIncreaseKey(event.key)) {
72
+ event.preventDefault()
73
+ setRating(Math.min(value + 1, max))
74
+ } else if (isDecreaseKey(event.key)) {
75
+ event.preventDefault()
76
+ setRating(Math.max(value - 1, 0))
77
+ } else if (tryDigitKey(event.key)) {
78
+ event.preventDefault()
79
+ }
80
+ }
81
+ </script>
82
+
83
+ <div
84
+ data-rating
85
+ data-rating-disabled={disabled || undefined}
86
+ class={className || undefined}
87
+ role="radiogroup"
88
+ aria-label={label}
89
+ aria-disabled={disabled || undefined}
90
+ tabindex={disabled ? undefined : 0}
91
+ onkeydown={handleKeyDown}
92
+ >
93
+ {#each Array(max) as _, index (index)}
94
+ {@const filled = index < value}
95
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
96
+ <span
97
+ data-rating-item
98
+ data-filled={filled || undefined}
99
+ data-hovering={index <= hoverIndex || undefined}
100
+ role="radio"
101
+ tabindex={-1}
102
+ aria-checked={filled}
103
+ aria-label="Rate {index + 1} of {max}"
104
+ onclick={() => handleClick(index)}
105
+ onmouseenter={() => {
106
+ if (!disabled) hoverIndex = index
107
+ }}
108
+ onmouseleave={() => {
109
+ hoverIndex = -1
110
+ }}
111
+ >
112
+ <span data-rating-icon class={filled ? icons.filled : icons.empty} aria-hidden="true"></span>
113
+ </span>
114
+ {/each}
115
+ </div>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ interface ResponsiveGridProps {
5
+ /** Minimum column width (CSS value) */
6
+ minWidth?: string
7
+ /** Gap between cells (CSS value) */
8
+ gap?: string
9
+ /** Maximum number of columns */
10
+ maxCols?: number
11
+ /** Grid children */
12
+ children: Snippet
13
+ /** Additional CSS class */
14
+ class?: string
15
+ }
16
+
17
+ const {
18
+ minWidth = '240px',
19
+ gap = '1rem',
20
+ maxCols,
21
+ children,
22
+ class: className = ''
23
+ }: ResponsiveGridProps = $props()
24
+ </script>
25
+
26
+ <div
27
+ data-responsive-grid
28
+ class={className || undefined}
29
+ style:--grid-min-width={minWidth}
30
+ style:--grid-gap={gap}
31
+ style:--grid-max-cols={maxCols || undefined}
32
+ >
33
+ {@render children()}
34
+ </div>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { reveal } from '@rokkit/actions'
4
+
5
+ interface RevealProps {
6
+ /** Slide direction (default: 'up') */
7
+ direction?: 'up' | 'down' | 'left' | 'right' | 'none'
8
+ /** Slide distance CSS value (default: '1.5rem') */
9
+ distance?: string
10
+ /** Transition duration in ms (default: 600) */
11
+ duration?: number
12
+ /** Delay before animation starts in ms (default: 0) */
13
+ delay?: number
14
+ /** Delay increment per direct child in ms for stagger (default: 0) */
15
+ stagger?: number
16
+ /** Only animate once (default: true) */
17
+ once?: boolean
18
+ /** IntersectionObserver threshold 0–1 (default: 0.1) */
19
+ threshold?: number
20
+ /** CSS easing function */
21
+ easing?: string
22
+ /** Additional CSS class */
23
+ class?: string
24
+ children?: Snippet
25
+ }
26
+
27
+ const {
28
+ direction = 'up',
29
+ distance = '1.5rem',
30
+ duration = 600,
31
+ delay = 0,
32
+ stagger = 0,
33
+ once = true,
34
+ threshold = 0.1,
35
+ easing = 'cubic-bezier(0.4, 0, 0.2, 1)',
36
+ class: className = '',
37
+ children
38
+ }: RevealProps = $props()
39
+ </script>
40
+
41
+ <div
42
+ class={className || undefined}
43
+ use:reveal={{ direction, distance, duration, delay, stagger, once, threshold, easing }}
44
+ >
45
+ {@render children?.()}
46
+ </div>
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import type { SearchFilterProps, FilterObject } from '../types/search-filter.js'
3
+ import { parseFilters } from '@rokkit/data'
4
+ import { messages } from '@rokkit/states'
5
+
6
+ let {
7
+ filters = $bindable([]),
8
+ debounce: delay = 300,
9
+ placeholder = 'Search...',
10
+ columns: _columns,
11
+ labels: userLabels = {},
12
+ onfilter,
13
+ class: className = '',
14
+ size = 'md',
15
+ tag: tagSnippet
16
+ }: SearchFilterProps & { labels?: Record<string, string> } = $props()
17
+
18
+ const labels = $derived({
19
+ clear: messages.search_.clear,
20
+ remove: messages.filter.remove,
21
+ ...userLabels
22
+ })
23
+
24
+ let inputText = $state('')
25
+ let timer: ReturnType<typeof setTimeout> | undefined
26
+
27
+ function handleInput() {
28
+ clearTimeout(timer)
29
+ timer = setTimeout(() => {
30
+ filters = parseFilters(inputText)
31
+ onfilter?.(filters)
32
+ }, delay)
33
+ }
34
+
35
+ function removeFilter(index: number) {
36
+ filters = filters.filter((_: FilterObject, i: number) => i !== index)
37
+ onfilter?.(filters)
38
+ }
39
+
40
+ function clear() {
41
+ inputText = ''
42
+ filters = []
43
+ onfilter?.(filters)
44
+ }
45
+
46
+ function formatFilter(filter: FilterObject): string {
47
+ const col = filter.column ? `${filter.column} ${filter.operator} ` : ''
48
+ const val = filter.value instanceof RegExp ? filter.value.source : String(filter.value)
49
+ return `${col}${val}`
50
+ }
51
+ </script>
52
+
53
+ <div data-search-filter data-size={size} class={className || undefined}>
54
+ <div data-search-input-wrapper>
55
+ <input
56
+ type="search"
57
+ data-search-input
58
+ bind:value={inputText}
59
+ oninput={handleInput}
60
+ {placeholder}
61
+ />
62
+ {#if inputText}
63
+ <button data-search-clear onclick={clear} aria-label={labels.clear} type="button">
64
+ &times;
65
+ </button>
66
+ {/if}
67
+ </div>
68
+
69
+ {#if filters.length > 0}
70
+ <div data-search-tags>
71
+ {#each filters as filter, i (i)}
72
+ {#if tagSnippet}
73
+ {@render tagSnippet(filter, () => removeFilter(i))}
74
+ {:else}
75
+ <span data-search-tag>
76
+ <span data-search-tag-text>{formatFilter(filter)}</span>
77
+ <button
78
+ data-search-tag-remove
79
+ onclick={() => removeFilter(i)}
80
+ aria-label={labels.remove}
81
+ type="button">&times;</button
82
+ >
83
+ </span>
84
+ {/if}
85
+ {/each}
86
+ </div>
87
+ {/if}
88
+ </div>