@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.
- package/README.md +159 -1
- package/package.json +52 -53
- package/src/MarkdownRenderer.svelte +106 -0
- package/src/components/AlertList.svelte +59 -0
- package/src/components/Avatar.svelte +68 -0
- package/src/components/Badge.svelte +57 -0
- package/src/components/BreadCrumbs.svelte +94 -0
- package/src/components/Button.svelte +91 -0
- package/src/components/ButtonGroup.svelte +9 -0
- package/src/components/Card.svelte +71 -0
- package/src/components/Carousel.svelte +169 -0
- package/src/components/Code.svelte +189 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/Divider.svelte +24 -0
- package/src/components/Dropdown.svelte +202 -0
- package/src/components/FloatingAction.svelte +367 -0
- package/src/components/FloatingNavigation.svelte +256 -0
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +25 -0
- package/src/components/LazyTree.svelte +171 -0
- package/src/components/List.svelte +224 -0
- package/src/components/Menu.svelte +270 -0
- package/src/components/Message.svelte +75 -0
- package/src/components/MultiSelect.svelte +367 -0
- package/src/components/NavContent.svelte +42 -0
- package/src/components/PaletteManager.svelte +360 -0
- package/src/components/Pill.svelte +83 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +346 -0
- package/src/components/Rating.svelte +115 -0
- package/src/components/ResponsiveGrid.svelte +34 -0
- package/src/components/Reveal.svelte +46 -0
- package/src/components/SearchFilter.svelte +88 -0
- package/src/components/Select.svelte +424 -0
- package/src/components/Shine.svelte +87 -0
- package/src/components/Stack.svelte +38 -0
- package/src/components/StatusList.svelte +18 -0
- package/src/components/Stepper.svelte +174 -0
- package/src/components/Swatch.svelte +102 -0
- package/src/components/Switch.svelte +71 -0
- package/src/components/Table.svelte +245 -0
- package/src/components/Tabs.svelte +202 -0
- package/src/components/Tilt.svelte +68 -0
- package/src/components/Timeline.svelte +61 -0
- package/src/components/Toggle.svelte +93 -0
- package/src/components/Toolbar.svelte +306 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tooltip.svelte +121 -0
- package/src/components/Tree.svelte +143 -0
- package/src/components/UploadFileStatus.svelte +102 -0
- package/src/components/UploadProgress.svelte +127 -0
- package/src/components/UploadTarget.svelte +126 -0
- package/src/components/index.ts +50 -0
- package/src/index.ts +61 -0
- package/src/markdown-plugin.ts +12 -0
- package/src/types/button.ts +86 -0
- package/src/types/code.ts +46 -0
- package/src/types/floating-action.ts +123 -0
- package/src/types/floating-navigation.ts +80 -0
- package/src/types/index.ts +58 -0
- package/src/types/list.ts +200 -0
- package/src/types/menu.ts +95 -0
- package/src/types/palette.ts +160 -0
- package/src/types/range.ts +51 -0
- package/src/types/search-filter.ts +67 -0
- package/src/types/select.ts +176 -0
- package/src/types/switch.ts +68 -0
- package/src/types/table.ts +219 -0
- package/src/types/tabs.ts +103 -0
- package/src/types/timeline.ts +53 -0
- package/src/types/toggle.ts +68 -0
- package/src/types/toolbar.ts +167 -0
- package/src/types/tooltip.ts +21 -0
- package/src/types/tree.ts +250 -0
- package/src/types/upload-file-status.ts +45 -0
- package/src/types/upload-progress.ts +111 -0
- package/src/types/upload-target.ts +71 -0
- package/src/utils/palette.ts +618 -0
- package/src/utils/shiki.ts +114 -0
- package/src/utils/upload.js +145 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -8
- 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
|
+
×
|
|
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">×</button
|
|
82
|
+
>
|
|
83
|
+
</span>
|
|
84
|
+
{/if}
|
|
85
|
+
{/each}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|