@rokkit/ui 1.0.0-next.98 → 1.0.1
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/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/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 +48 -0
- package/src/index.ts +59 -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,424 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Select — Trigger + dropdown with List-style flatView content.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Trigger — manages open/close (click, Enter, Escape, click-outside)
|
|
7
|
+
* Wrapper — owns focusedKey $state + flatView $derived
|
|
8
|
+
* Navigator — attaches DOM event handlers on dropdown
|
|
9
|
+
* flatView loop — single flat {#each}, groups rendered as non-interactive labels
|
|
10
|
+
*
|
|
11
|
+
* Groups are pre-processed with expanded:true (always show children) and
|
|
12
|
+
* disabled:true (excluded from keyboard navigation). Group labels have no
|
|
13
|
+
* data-path so Navigator ignores them entirely.
|
|
14
|
+
*
|
|
15
|
+
* Data attributes:
|
|
16
|
+
* data-select — root container
|
|
17
|
+
* data-select-trigger — trigger button
|
|
18
|
+
* data-select-value — selected value display area
|
|
19
|
+
* data-select-value-text — selected item text
|
|
20
|
+
* data-select-value-icon — selected item icon
|
|
21
|
+
* data-select-placeholder — placeholder text
|
|
22
|
+
* data-select-arrow — dropdown arrow icon
|
|
23
|
+
* data-select-dropdown — dropdown container
|
|
24
|
+
* data-select-filter — filter input wrapper
|
|
25
|
+
* data-select-filter-input — filter text input
|
|
26
|
+
* data-select-option — leaf option items
|
|
27
|
+
* data-select-group-label — group header label (non-interactive)
|
|
28
|
+
* data-select-group-icon — icon inside group label
|
|
29
|
+
* data-select-divider — divider between groups
|
|
30
|
+
* data-select-check — check icon on selected item
|
|
31
|
+
* data-select-empty — no results message
|
|
32
|
+
* data-path — required by Navigator
|
|
33
|
+
* data-selected — selected state
|
|
34
|
+
* data-disabled — disabled state
|
|
35
|
+
* data-open — dropdown is open
|
|
36
|
+
* data-size — size variant
|
|
37
|
+
* data-align — dropdown alignment
|
|
38
|
+
* data-direction — dropdown direction
|
|
39
|
+
*/
|
|
40
|
+
// @ts-nocheck
|
|
41
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
42
|
+
import { Wrapper, ProxyTree, messages } from '@rokkit/states'
|
|
43
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
44
|
+
import { Navigator, Trigger } from '@rokkit/actions'
|
|
45
|
+
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
|
|
46
|
+
import ItemContent from './ItemContent.svelte'
|
|
47
|
+
|
|
48
|
+
interface SelectIcons {
|
|
49
|
+
opened?: string
|
|
50
|
+
closed?: string
|
|
51
|
+
checked?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
items = [],
|
|
56
|
+
fields = {},
|
|
57
|
+
value = $bindable(),
|
|
58
|
+
selected = $bindable<unknown>(null),
|
|
59
|
+
placeholder = messages.select,
|
|
60
|
+
size = 'md',
|
|
61
|
+
disabled = false,
|
|
62
|
+
filterable = false,
|
|
63
|
+
filterPlaceholder = 'Search...',
|
|
64
|
+
align = 'start',
|
|
65
|
+
direction = 'down',
|
|
66
|
+
icons: userIcons = {} as SelectIcons,
|
|
67
|
+
onchange,
|
|
68
|
+
class: className = '',
|
|
69
|
+
...snippets
|
|
70
|
+
}: {
|
|
71
|
+
items?: unknown[]
|
|
72
|
+
fields?: Record<string, string>
|
|
73
|
+
value?: unknown
|
|
74
|
+
selected?: unknown
|
|
75
|
+
placeholder?: string
|
|
76
|
+
size?: string
|
|
77
|
+
disabled?: boolean
|
|
78
|
+
filterable?: boolean
|
|
79
|
+
filterPlaceholder?: string
|
|
80
|
+
align?: 'start' | 'end'
|
|
81
|
+
direction?: 'up' | 'down'
|
|
82
|
+
icons?: SelectIcons
|
|
83
|
+
onchange?: (value: unknown, item: unknown) => void
|
|
84
|
+
class?: string
|
|
85
|
+
[key: string]: unknown
|
|
86
|
+
} = $props()
|
|
87
|
+
|
|
88
|
+
const icons = $derived({
|
|
89
|
+
...DEFAULT_STATE_ICONS.selector,
|
|
90
|
+
...DEFAULT_STATE_ICONS.checkbox,
|
|
91
|
+
...userIcons
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ─── Dropdown state ───────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
let isOpen = $state(false)
|
|
97
|
+
let selectRef = $state<HTMLElement | null>(null)
|
|
98
|
+
let triggerRef = $state<HTMLElement | null>(null)
|
|
99
|
+
let dropdownRef = $state<HTMLElement | null>(null)
|
|
100
|
+
|
|
101
|
+
// ─── Filter ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
let filterQuery = $state('')
|
|
104
|
+
let filterInputRef = $state<HTMLInputElement | null>(null)
|
|
105
|
+
|
|
106
|
+
const textField = $derived(fields?.label || 'label')
|
|
107
|
+
const childrenField = $derived(fields?.children || 'children')
|
|
108
|
+
|
|
109
|
+
function childMatchesQuery(child: unknown, query: string): boolean {
|
|
110
|
+
return String((child as Record<string, unknown>)[textField] ?? '')
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.includes(query)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function filterGroupChildren(asRecord: Record<string, unknown>, query: string): unknown | null {
|
|
116
|
+
const children = asRecord[childrenField] as unknown[]
|
|
117
|
+
const matching = children.filter((child: unknown) => childMatchesQuery(child, query))
|
|
118
|
+
return matching.length > 0 ? { ...asRecord, [childrenField]: matching } : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function filterItem(item: unknown, query: string): unknown | null {
|
|
122
|
+
const asRecord = item as Record<string, unknown>
|
|
123
|
+
const children = asRecord[childrenField]
|
|
124
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
125
|
+
return filterGroupChildren(asRecord, query)
|
|
126
|
+
}
|
|
127
|
+
const text = String(asRecord[textField] ?? '').toLowerCase()
|
|
128
|
+
return text.includes(query) ? item : null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const filteredItems = $derived.by(() => {
|
|
132
|
+
if (!filterable || !filterQuery) return items
|
|
133
|
+
const query = filterQuery.toLowerCase()
|
|
134
|
+
return items.map((item) => filterItem(item, query)).filter(Boolean)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Pre-process: force groups expanded + disabled (non-navigable labels)
|
|
138
|
+
const processedItems = $derived(
|
|
139
|
+
filteredItems.map((item) => {
|
|
140
|
+
const children = item[childrenField]
|
|
141
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
142
|
+
return { ...item, expanded: true, disabled: true }
|
|
143
|
+
}
|
|
144
|
+
return item
|
|
145
|
+
})
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
|
|
151
|
+
if (proxy.disabled) return
|
|
152
|
+
value = extractedValue
|
|
153
|
+
selected = proxy.original
|
|
154
|
+
onchange?.(extractedValue, proxy.original)
|
|
155
|
+
isOpen = false
|
|
156
|
+
filterQuery = ''
|
|
157
|
+
triggerRef?.focus()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const proxyTree = $derived(new ProxyTree(processedItems, fields))
|
|
161
|
+
const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
|
|
162
|
+
|
|
163
|
+
// Override cancel/blur to close dropdown
|
|
164
|
+
$effect(() => {
|
|
165
|
+
const w = wrapper
|
|
166
|
+
w.cancel = () => {
|
|
167
|
+
isOpen = false
|
|
168
|
+
filterQuery = ''
|
|
169
|
+
triggerRef?.focus()
|
|
170
|
+
}
|
|
171
|
+
w.blur = () => {
|
|
172
|
+
isOpen = false
|
|
173
|
+
filterQuery = ''
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// When wrapper recreates while open, focus first item
|
|
178
|
+
$effect(() => {
|
|
179
|
+
const _w = wrapper
|
|
180
|
+
if (isOpen && !filterable) _w.first(null)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// ─── Selected proxy for trigger display ───────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function isValueUnset(): boolean {
|
|
186
|
+
return value === undefined || value === null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isMatchingLeaf(proxy: ProxyItem): boolean {
|
|
190
|
+
return !proxy.hasChildren && proxy.value === value
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function findSelectedProxy(): ProxyItem | null {
|
|
194
|
+
if (isValueUnset()) return null
|
|
195
|
+
for (const [, proxy] of wrapper.lookup) {
|
|
196
|
+
if (isMatchingLeaf(proxy)) return proxy
|
|
197
|
+
}
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const selectedProxy = $derived.by(findSelectedProxy)
|
|
202
|
+
|
|
203
|
+
// Sync selected raw item
|
|
204
|
+
$effect(() => {
|
|
205
|
+
selected = selectedProxy?.original ?? null
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// ─── Trigger action ───────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
$effect(() => {
|
|
211
|
+
if (!triggerRef || !selectRef || disabled) return
|
|
212
|
+
const t = new Trigger(triggerRef, selectRef, {
|
|
213
|
+
isOpen: () => isOpen,
|
|
214
|
+
onopen: () => {
|
|
215
|
+
isOpen = true
|
|
216
|
+
requestAnimationFrame(() => {
|
|
217
|
+
if (filterable) {
|
|
218
|
+
filterInputRef?.focus()
|
|
219
|
+
} else {
|
|
220
|
+
focusSelectedOrFirst()
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
onclose: () => {
|
|
225
|
+
isOpen = false
|
|
226
|
+
filterQuery = ''
|
|
227
|
+
},
|
|
228
|
+
onlast: () => requestAnimationFrame(() => wrapper.last(null))
|
|
229
|
+
})
|
|
230
|
+
return () => t.destroy()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
function moveToSelectedValue(): boolean {
|
|
234
|
+
for (const node of wrapper.flatView) {
|
|
235
|
+
if (!node.proxy.disabled && node.proxy.value === value) {
|
|
236
|
+
wrapper.moveTo(node.key)
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function focusSelectedOrFirst() {
|
|
244
|
+
if (value !== undefined && value !== null && moveToSelectedValue()) return
|
|
245
|
+
wrapper.first(null)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Navigator on dropdown ────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
$effect(() => {
|
|
251
|
+
if (!isOpen || !dropdownRef) return
|
|
252
|
+
const dir = getComputedStyle(dropdownRef).direction || 'ltr'
|
|
253
|
+
const nav = new Navigator(dropdownRef, wrapper, { dir, containScroll: true })
|
|
254
|
+
return () => nav.destroy()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// DOM focus sync
|
|
258
|
+
$effect(() => {
|
|
259
|
+
const key = wrapper.focusedKey
|
|
260
|
+
if (!isOpen || !dropdownRef || !key) return
|
|
261
|
+
requestAnimationFrame(() => {
|
|
262
|
+
const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
263
|
+
if (target && target !== document.activeElement) {
|
|
264
|
+
target.focus()
|
|
265
|
+
target.scrollIntoView?.({ block: 'nearest' })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// ─── Filter keyboard (native listener, fires before Navigator) ───────────
|
|
271
|
+
|
|
272
|
+
function selectFocused() {
|
|
273
|
+
if (wrapper.focusedKey) wrapper.select(null)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleFilterKeyDown(event: KeyboardEvent) {
|
|
277
|
+
if (event.key === 'ArrowDown') {
|
|
278
|
+
event.preventDefault()
|
|
279
|
+
event.stopPropagation()
|
|
280
|
+
wrapper.first(null)
|
|
281
|
+
} else if (event.key === 'Escape' && filterQuery) {
|
|
282
|
+
event.preventDefault()
|
|
283
|
+
event.stopPropagation()
|
|
284
|
+
filterQuery = ''
|
|
285
|
+
} else if (event.key === 'Enter') {
|
|
286
|
+
event.preventDefault()
|
|
287
|
+
event.stopPropagation()
|
|
288
|
+
selectFocused()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
$effect(() => {
|
|
293
|
+
if (!isOpen || !filterable || !filterInputRef) return
|
|
294
|
+
const el = filterInputRef
|
|
295
|
+
el.addEventListener('keydown', handleFilterKeyDown)
|
|
296
|
+
return () => el.removeEventListener('keydown', handleFilterKeyDown)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/** Set of group keys that need a divider before them (not the first group) */
|
|
302
|
+
const groupDividers = $derived.by(() => {
|
|
303
|
+
const set = new SvelteSet<string>()
|
|
304
|
+
let foundFirst = false
|
|
305
|
+
for (const node of wrapper.flatView) {
|
|
306
|
+
if (node.hasChildren) {
|
|
307
|
+
if (foundFirst) set.add(node.key)
|
|
308
|
+
foundFirst = true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return set
|
|
312
|
+
})
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
{#snippet defaultOptionContent(proxy: ProxyItem)}
|
|
316
|
+
<ItemContent {proxy} />
|
|
317
|
+
{/snippet}
|
|
318
|
+
|
|
319
|
+
{#snippet defaultGroupContent(proxy: ProxyItem)}
|
|
320
|
+
{#if proxy.get('icon')}
|
|
321
|
+
<span data-select-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
|
|
322
|
+
{/if}
|
|
323
|
+
<span>{proxy.label}</span>
|
|
324
|
+
{/snippet}
|
|
325
|
+
|
|
326
|
+
<div
|
|
327
|
+
bind:this={selectRef}
|
|
328
|
+
data-select
|
|
329
|
+
data-open={isOpen || undefined}
|
|
330
|
+
data-size={size}
|
|
331
|
+
data-disabled={disabled || undefined}
|
|
332
|
+
data-align={align}
|
|
333
|
+
data-direction={direction}
|
|
334
|
+
class={className || undefined}
|
|
335
|
+
>
|
|
336
|
+
<button
|
|
337
|
+
bind:this={triggerRef}
|
|
338
|
+
type="button"
|
|
339
|
+
data-select-trigger
|
|
340
|
+
{disabled}
|
|
341
|
+
aria-haspopup="listbox"
|
|
342
|
+
aria-expanded={isOpen}
|
|
343
|
+
>
|
|
344
|
+
<span data-select-value>
|
|
345
|
+
{#if selectedProxy}
|
|
346
|
+
{#if selectedProxy.get('icon')}
|
|
347
|
+
<span data-select-value-icon class={selectedProxy.get('icon')} aria-hidden="true"></span>
|
|
348
|
+
{/if}
|
|
349
|
+
<span data-select-value-text>{selectedProxy.label}</span>
|
|
350
|
+
{:else}
|
|
351
|
+
<span data-select-placeholder>{placeholder}</span>
|
|
352
|
+
{/if}
|
|
353
|
+
</span>
|
|
354
|
+
<span data-select-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"></span>
|
|
355
|
+
</button>
|
|
356
|
+
|
|
357
|
+
{#if isOpen}
|
|
358
|
+
<div bind:this={dropdownRef} data-select-dropdown role="listbox" aria-orientation="vertical">
|
|
359
|
+
{#if filterable}
|
|
360
|
+
<div data-select-filter>
|
|
361
|
+
<input
|
|
362
|
+
bind:this={filterInputRef}
|
|
363
|
+
type="text"
|
|
364
|
+
data-select-filter-input
|
|
365
|
+
placeholder={filterPlaceholder}
|
|
366
|
+
bind:value={filterQuery}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
{/if}
|
|
370
|
+
|
|
371
|
+
{#each wrapper.flatView as node (node.key)}
|
|
372
|
+
{@const proxy = node.proxy}
|
|
373
|
+
{@const sel = !node.hasChildren && proxy.value === value}
|
|
374
|
+
{@const content = resolveSnippet(
|
|
375
|
+
snippets as Record<string, unknown>,
|
|
376
|
+
proxy,
|
|
377
|
+
node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{#if node.type === 'separator'}
|
|
381
|
+
<hr data-select-separator />
|
|
382
|
+
{:else if node.hasChildren}
|
|
383
|
+
{#if groupDividers.has(node.key)}
|
|
384
|
+
<div data-select-divider></div>
|
|
385
|
+
{/if}
|
|
386
|
+
<div data-select-group-label role="presentation">
|
|
387
|
+
{#if content}
|
|
388
|
+
{@render content(proxy)}
|
|
389
|
+
{:else}
|
|
390
|
+
{@render defaultGroupContent(proxy)}
|
|
391
|
+
{/if}
|
|
392
|
+
</div>
|
|
393
|
+
{:else}
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
title={proxy.get('tooltip')}
|
|
397
|
+
data-select-option
|
|
398
|
+
data-path={node.key}
|
|
399
|
+
data-level={node.level}
|
|
400
|
+
data-selected={sel || undefined}
|
|
401
|
+
data-disabled={proxy.disabled || undefined}
|
|
402
|
+
role="option"
|
|
403
|
+
aria-selected={sel}
|
|
404
|
+
disabled={proxy.disabled || disabled}
|
|
405
|
+
tabindex="-1"
|
|
406
|
+
>
|
|
407
|
+
{#if content}
|
|
408
|
+
{@render content(proxy)}
|
|
409
|
+
{:else}
|
|
410
|
+
{@render defaultOptionContent(proxy)}
|
|
411
|
+
{/if}
|
|
412
|
+
{#if sel}
|
|
413
|
+
<span data-select-check class={icons.checked} aria-hidden="true"></span>
|
|
414
|
+
{/if}
|
|
415
|
+
</button>
|
|
416
|
+
{/if}
|
|
417
|
+
{/each}
|
|
418
|
+
|
|
419
|
+
{#if filterable && filterQuery && filteredItems.length === 0}
|
|
420
|
+
<div data-select-empty>No results</div>
|
|
421
|
+
{/if}
|
|
422
|
+
</div>
|
|
423
|
+
{/if}
|
|
424
|
+
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import { id } from '@rokkit/core'
|
|
4
|
+
|
|
5
|
+
interface ShineProps {
|
|
6
|
+
/** Light color (default: 'rgb(var(--primary-500))') */
|
|
7
|
+
color?: string
|
|
8
|
+
/** Light source distance/height — controls spread (default: 300) */
|
|
9
|
+
radius?: number
|
|
10
|
+
/** Gaussian blur depth (default: 1) */
|
|
11
|
+
depth?: number
|
|
12
|
+
/** Height of the surface for the light filter (default: 2) */
|
|
13
|
+
surfaceScale?: number
|
|
14
|
+
/** The bigger the value the bigger the reflection (default: 0.75) */
|
|
15
|
+
specularConstant?: number
|
|
16
|
+
/** Controls focus for the light source — bigger = brighter (default: 120) */
|
|
17
|
+
specularExponent?: number
|
|
18
|
+
/** Additional CSS class */
|
|
19
|
+
class?: string
|
|
20
|
+
children?: Snippet
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
color = 'rgb(var(--primary-500))',
|
|
25
|
+
radius = 300,
|
|
26
|
+
depth = 1,
|
|
27
|
+
surfaceScale = 2,
|
|
28
|
+
specularConstant = 0.75,
|
|
29
|
+
specularExponent = 120,
|
|
30
|
+
class: className = '',
|
|
31
|
+
children
|
|
32
|
+
}: ShineProps = $props()
|
|
33
|
+
|
|
34
|
+
const filterId = id('filter')
|
|
35
|
+
|
|
36
|
+
let mouse = $state({ x: 0, y: 0 })
|
|
37
|
+
let wrapperBox = $state({ left: 0, top: 0 })
|
|
38
|
+
let wrapperEl: HTMLDivElement | null = $state(null)
|
|
39
|
+
|
|
40
|
+
function onPointerMove(e: PointerEvent) {
|
|
41
|
+
wrapperBox = wrapperEl?.getBoundingClientRect() ?? { left: 0, top: 0 }
|
|
42
|
+
mouse = { x: e.clientX, y: e.clientY }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onScroll() {
|
|
46
|
+
wrapperBox = wrapperEl?.getBoundingClientRect() ?? { left: 0, top: 0 }
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<svelte:window onpointermove={onPointerMove} onscroll={onScroll} />
|
|
51
|
+
|
|
52
|
+
<svg data-shine-filter>
|
|
53
|
+
<filter id={filterId} color-interpolation-filters="sRGB">
|
|
54
|
+
<feGaussianBlur in="SourceAlpha" stdDeviation={depth} />
|
|
55
|
+
|
|
56
|
+
<feSpecularLighting
|
|
57
|
+
result="light-source"
|
|
58
|
+
{surfaceScale}
|
|
59
|
+
{specularConstant}
|
|
60
|
+
{specularExponent}
|
|
61
|
+
lighting-color={color}
|
|
62
|
+
>
|
|
63
|
+
<fePointLight x={mouse.x - wrapperBox.left} y={mouse.y - wrapperBox.top} z={radius} />
|
|
64
|
+
</feSpecularLighting>
|
|
65
|
+
|
|
66
|
+
<feComposite result="reflections" in="light-source" in2="SourceAlpha" operator="in" />
|
|
67
|
+
|
|
68
|
+
<feComposite
|
|
69
|
+
in="SourceGraphic"
|
|
70
|
+
in2="reflections"
|
|
71
|
+
operator="arithmetic"
|
|
72
|
+
k1="0"
|
|
73
|
+
k2="1"
|
|
74
|
+
k3="1"
|
|
75
|
+
k4="0"
|
|
76
|
+
/>
|
|
77
|
+
</filter>
|
|
78
|
+
</svg>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
data-shine
|
|
82
|
+
style:filter="url(#{filterId})"
|
|
83
|
+
class={className || undefined}
|
|
84
|
+
bind:this={wrapperEl}
|
|
85
|
+
>
|
|
86
|
+
{@render children?.()}
|
|
87
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface StackProps {
|
|
5
|
+
/** Layout direction */
|
|
6
|
+
direction?: 'vertical' | 'horizontal'
|
|
7
|
+
/** Gap size between children */
|
|
8
|
+
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
9
|
+
/** Cross-axis alignment */
|
|
10
|
+
align?: 'start' | 'center' | 'end' | 'stretch'
|
|
11
|
+
/** Main-axis justification */
|
|
12
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
|
|
13
|
+
/** Stack children */
|
|
14
|
+
children: Snippet
|
|
15
|
+
/** Additional CSS class */
|
|
16
|
+
class?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
direction = 'vertical',
|
|
21
|
+
gap = 'md',
|
|
22
|
+
align,
|
|
23
|
+
justify,
|
|
24
|
+
children,
|
|
25
|
+
class: className = ''
|
|
26
|
+
}: StackProps = $props()
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
data-stack
|
|
31
|
+
data-direction={direction}
|
|
32
|
+
data-gap={gap}
|
|
33
|
+
data-align={align || undefined}
|
|
34
|
+
data-justify={justify || undefined}
|
|
35
|
+
class={className || undefined}
|
|
36
|
+
>
|
|
37
|
+
{@render children()}
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ICONS = DEFAULT_STATE_ICONS.badge
|
|
5
|
+
|
|
6
|
+
let { class: className = '', items, icons = {} } = $props()
|
|
7
|
+
|
|
8
|
+
const resolvedIcons = $derived({ ...DEFAULT_ICONS, ...icons })
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div data-status-list class={className} role="status">
|
|
12
|
+
{#each items as { text, status }, index (index)}
|
|
13
|
+
<div data-status-item data-status={status}>
|
|
14
|
+
<span class={resolvedIcons[status]} aria-hidden="true"></span>
|
|
15
|
+
<p>{text}</p>
|
|
16
|
+
</div>
|
|
17
|
+
{/each}
|
|
18
|
+
</div>
|