@rokkit/ui 1.0.0-next.125 → 1.0.0-next.128
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 +198 -101
- package/package.json +42 -34
- package/src/components/BreadCrumbs.svelte +90 -0
- package/src/components/Button.svelte +93 -0
- package/src/components/ButtonGroup.svelte +18 -0
- package/src/components/Card.svelte +61 -0
- package/src/components/Carousel.svelte +174 -0
- package/src/components/Code.svelte +189 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/FloatingAction.svelte +334 -0
- package/src/components/FloatingNavigation.svelte +235 -0
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +25 -0
- package/src/components/LazyTree.svelte +165 -0
- package/src/components/List.svelte +188 -0
- package/src/components/Menu.svelte +270 -0
- package/src/components/MultiSelect.svelte +369 -0
- package/src/components/PaletteManager.svelte +364 -0
- package/src/components/Pill.svelte +83 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +330 -0
- package/src/components/Rating.svelte +101 -0
- package/src/components/Reveal.svelte +58 -0
- package/src/components/SearchFilter.svelte +88 -0
- package/src/components/Select.svelte +396 -0
- package/src/{Shine.svelte → components/Shine.svelte} +29 -21
- package/src/components/Stepper.svelte +172 -0
- package/src/components/Switch.svelte +75 -0
- package/src/components/Table.svelte +242 -0
- package/src/components/Tabs.svelte +192 -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 +308 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tree.svelte +144 -0
- package/src/components/UploadFileStatus.svelte +83 -0
- package/src/components/UploadProgress.svelte +131 -0
- package/src/components/UploadTarget.svelte +124 -0
- package/src/components/index.ts +38 -0
- package/src/index.ts +46 -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 +55 -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 +210 -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 +164 -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 +68 -0
- package/src/utils/palette.ts +582 -0
- package/src/utils/shiki.ts +122 -0
- package/src/utils/upload.js +128 -0
- package/dist/constants.d.ts +0 -2
- package/dist/index.d.ts +0 -41
- package/dist/lib/fields.d.ts +0 -16
- package/dist/lib/form.d.ts +0 -95
- package/dist/lib/index.d.ts +0 -6
- package/dist/lib/layout.d.ts +0 -7
- package/dist/lib/nested.d.ts +0 -48
- package/dist/lib/schema.d.ts +0 -7
- package/dist/lib/select.d.ts +0 -8
- package/dist/lib/tree.d.ts +0 -9
- package/dist/tree/List.spec.svelte.d.ts +0 -1
- package/dist/tree/Node.spec.svelte.d.ts +0 -1
- package/dist/tree/Root.spec.svelte.d.ts +0 -1
- package/dist/types.d.ts +0 -5
- package/dist/wrappers/index.d.ts +0 -3
- package/src/Accordion.svelte +0 -118
- package/src/BreadCrumbs.svelte +0 -32
- package/src/Button.svelte +0 -57
- package/src/Calendar.svelte +0 -93
- package/src/Card.svelte +0 -45
- package/src/Carousel.svelte +0 -49
- package/src/CheckBox.svelte +0 -56
- package/src/Connector.svelte +0 -40
- package/src/DropDown.svelte +0 -68
- package/src/DropSearch.svelte +0 -37
- package/src/Fillable.svelte +0 -19
- package/src/GraphPaper.svelte +0 -43
- package/src/Icon.svelte +0 -81
- package/src/Item.svelte +0 -25
- package/src/Link.svelte +0 -21
- package/src/List.svelte +0 -89
- package/src/ListBody.svelte +0 -43
- package/src/Message.svelte +0 -11
- package/src/MultiSelect.svelte +0 -48
- package/src/NestedList.svelte +0 -78
- package/src/NestedPaginator.svelte +0 -63
- package/src/Node.svelte +0 -76
- package/src/Overlay.svelte +0 -21
- package/src/PageNavigator.svelte +0 -94
- package/src/PickOne.svelte +0 -60
- package/src/Pill.svelte +0 -41
- package/src/ProgressBar.svelte +0 -21
- package/src/ProgressDots.svelte +0 -53
- package/src/RadioGroup.svelte +0 -52
- package/src/Range.svelte +0 -45
- package/src/RangeMinMax.svelte +0 -124
- package/src/RangeSlider.svelte +0 -79
- package/src/RangeTick.svelte +0 -28
- package/src/Rating.svelte +0 -95
- package/src/ResponsiveGrid.svelte +0 -88
- package/src/Scrollable.svelte +0 -7
- package/src/Select.svelte +0 -114
- package/src/Separator.svelte +0 -1
- package/src/Slider.svelte +0 -14
- package/src/SlidingColumns.svelte +0 -50
- package/src/Stage.svelte +0 -41
- package/src/Stepper.svelte +0 -66
- package/src/Summary.svelte +0 -22
- package/src/Switch.svelte +0 -106
- package/src/TableCell.svelte +0 -51
- package/src/TableHeaderCell.svelte +0 -54
- package/src/Tabs.svelte +0 -176
- package/src/Tilt.svelte +0 -66
- package/src/Toggle.svelte +0 -58
- package/src/ToggleThemeMode.svelte +0 -23
- package/src/Tree.svelte +0 -80
- package/src/TreeTable.svelte +0 -171
- package/src/ValidationReport.svelte +0 -23
- package/src/constants.js +0 -4
- package/src/index.js +0 -48
- package/src/lib/fields.js +0 -118
- package/src/lib/form.js +0 -72
- package/src/lib/index.js +0 -13
- package/src/lib/layout.js +0 -63
- package/src/lib/nested.js +0 -192
- package/src/lib/schema.js +0 -32
- package/src/lib/select.js +0 -38
- package/src/lib/tree.js +0 -22
- package/src/tree/List.spec.svelte.js +0 -84
- package/src/tree/List.svelte +0 -78
- package/src/tree/Node.spec.svelte.js +0 -104
- package/src/tree/Node.svelte +0 -80
- package/src/tree/Root.spec.svelte.js +0 -63
- package/src/tree/Root.svelte +0 -81
- package/src/types.js +0 -9
- package/src/wrappers/Category.svelte +0 -27
- package/src/wrappers/Section.svelte +0 -16
- package/src/wrappers/Wrapper.svelte +0 -12
- package/src/wrappers/index.js +0 -3
|
@@ -0,0 +1,396 @@
|
|
|
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 } from '@rokkit/states'
|
|
43
|
+
import { Navigator, Trigger } from '@rokkit/actions'
|
|
44
|
+
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
|
|
45
|
+
import ItemContent from './ItemContent.svelte'
|
|
46
|
+
|
|
47
|
+
interface SelectIcons {
|
|
48
|
+
opened?: string
|
|
49
|
+
closed?: string
|
|
50
|
+
checked?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
items = [],
|
|
55
|
+
fields = {},
|
|
56
|
+
value = $bindable(),
|
|
57
|
+
selected = $bindable<unknown>(null),
|
|
58
|
+
placeholder = 'Select...',
|
|
59
|
+
size = 'md',
|
|
60
|
+
disabled = false,
|
|
61
|
+
filterable = false,
|
|
62
|
+
filterPlaceholder = 'Search...',
|
|
63
|
+
align = 'start',
|
|
64
|
+
direction = 'down',
|
|
65
|
+
icons: userIcons = {} as SelectIcons,
|
|
66
|
+
onchange,
|
|
67
|
+
class: className = '',
|
|
68
|
+
...snippets
|
|
69
|
+
}: {
|
|
70
|
+
items?: unknown[]
|
|
71
|
+
fields?: Record<string, string>
|
|
72
|
+
value?: unknown
|
|
73
|
+
selected?: unknown
|
|
74
|
+
placeholder?: string
|
|
75
|
+
size?: string
|
|
76
|
+
disabled?: boolean
|
|
77
|
+
filterable?: boolean
|
|
78
|
+
filterPlaceholder?: string
|
|
79
|
+
align?: 'start' | 'end'
|
|
80
|
+
direction?: 'up' | 'down'
|
|
81
|
+
icons?: SelectIcons
|
|
82
|
+
onchange?: (value: unknown, item: unknown) => void
|
|
83
|
+
class?: string
|
|
84
|
+
[key: string]: unknown
|
|
85
|
+
} = $props()
|
|
86
|
+
|
|
87
|
+
const icons = $derived({ ...DEFAULT_STATE_ICONS.selector, ...DEFAULT_STATE_ICONS.checkbox, ...userIcons })
|
|
88
|
+
|
|
89
|
+
// ─── Dropdown state ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
let isOpen = $state(false)
|
|
92
|
+
let selectRef = $state<HTMLElement | null>(null)
|
|
93
|
+
let triggerRef = $state<HTMLElement | null>(null)
|
|
94
|
+
let dropdownRef = $state<HTMLElement | null>(null)
|
|
95
|
+
|
|
96
|
+
// ─── Filter ───────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
let filterQuery = $state('')
|
|
99
|
+
let filterInputRef = $state<HTMLInputElement | null>(null)
|
|
100
|
+
|
|
101
|
+
const textField = $derived(fields?.text || 'text')
|
|
102
|
+
const childrenField = $derived(fields?.children || 'children')
|
|
103
|
+
|
|
104
|
+
const filteredItems = $derived.by(() => {
|
|
105
|
+
if (!filterable || !filterQuery) return items
|
|
106
|
+
const query = filterQuery.toLowerCase()
|
|
107
|
+
return items
|
|
108
|
+
.map((item) => {
|
|
109
|
+
const children = item[childrenField]
|
|
110
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
111
|
+
const matching = children.filter((child) =>
|
|
112
|
+
String(child[textField] ?? '').toLowerCase().includes(query)
|
|
113
|
+
)
|
|
114
|
+
return matching.length > 0 ? { ...item, [childrenField]: matching } : null
|
|
115
|
+
}
|
|
116
|
+
return String(item[textField] ?? '').toLowerCase().includes(query) ? item : null
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Pre-process: force groups expanded + disabled (non-navigable labels)
|
|
122
|
+
const processedItems = $derived(
|
|
123
|
+
filteredItems.map((item) => {
|
|
124
|
+
const children = item[childrenField]
|
|
125
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
126
|
+
return { ...item, expanded: true, disabled: true }
|
|
127
|
+
}
|
|
128
|
+
return item
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
|
|
135
|
+
if (proxy.disabled) return
|
|
136
|
+
value = extractedValue
|
|
137
|
+
selected = proxy.original
|
|
138
|
+
onchange?.(extractedValue, proxy.original)
|
|
139
|
+
isOpen = false
|
|
140
|
+
filterQuery = ''
|
|
141
|
+
triggerRef?.focus()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const proxyTree = $derived(new ProxyTree(processedItems, fields))
|
|
145
|
+
const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
|
|
146
|
+
|
|
147
|
+
// Override cancel/blur to close dropdown
|
|
148
|
+
$effect(() => {
|
|
149
|
+
const w = wrapper
|
|
150
|
+
w.cancel = () => {
|
|
151
|
+
isOpen = false
|
|
152
|
+
filterQuery = ''
|
|
153
|
+
triggerRef?.focus()
|
|
154
|
+
}
|
|
155
|
+
w.blur = () => {
|
|
156
|
+
isOpen = false
|
|
157
|
+
filterQuery = ''
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// When wrapper recreates while open, focus first item
|
|
162
|
+
$effect(() => {
|
|
163
|
+
const _w = wrapper
|
|
164
|
+
if (isOpen && !filterable) _w.first(null)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// ─── Selected proxy for trigger display ───────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const selectedProxy = $derived.by(() => {
|
|
170
|
+
if (value === undefined || value === null) return null
|
|
171
|
+
for (const [, proxy] of wrapper.lookup) {
|
|
172
|
+
if (!proxy.hasChildren && proxy.value === value) return proxy
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Sync selected raw item
|
|
178
|
+
$effect(() => {
|
|
179
|
+
selected = selectedProxy?.original ?? null
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ─── Trigger action ───────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
$effect(() => {
|
|
185
|
+
if (!triggerRef || !selectRef || disabled) return
|
|
186
|
+
const t = new Trigger(triggerRef, selectRef, {
|
|
187
|
+
isOpen: () => isOpen,
|
|
188
|
+
onopen: () => {
|
|
189
|
+
isOpen = true
|
|
190
|
+
requestAnimationFrame(() => {
|
|
191
|
+
if (filterable) {
|
|
192
|
+
filterInputRef?.focus()
|
|
193
|
+
} else {
|
|
194
|
+
focusSelectedOrFirst()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
onclose: () => {
|
|
199
|
+
isOpen = false
|
|
200
|
+
filterQuery = ''
|
|
201
|
+
},
|
|
202
|
+
onlast: () => requestAnimationFrame(() => wrapper.last(null))
|
|
203
|
+
})
|
|
204
|
+
return () => t.destroy()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
function focusSelectedOrFirst() {
|
|
208
|
+
if (value !== undefined && value !== null) {
|
|
209
|
+
for (const node of wrapper.flatView) {
|
|
210
|
+
if (!node.proxy.disabled && node.proxy.value === value) {
|
|
211
|
+
wrapper.moveTo(node.key)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
wrapper.first(null)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Navigator on dropdown ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
$effect(() => {
|
|
222
|
+
if (!isOpen || !dropdownRef) return
|
|
223
|
+
const dir = getComputedStyle(dropdownRef).direction || 'ltr'
|
|
224
|
+
const nav = new Navigator(dropdownRef, wrapper, { dir })
|
|
225
|
+
return () => nav.destroy()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// DOM focus sync
|
|
229
|
+
$effect(() => {
|
|
230
|
+
const key = wrapper.focusedKey
|
|
231
|
+
if (!isOpen || !dropdownRef || !key) return
|
|
232
|
+
requestAnimationFrame(() => {
|
|
233
|
+
const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
234
|
+
if (target && target !== document.activeElement) {
|
|
235
|
+
target.focus()
|
|
236
|
+
target.scrollIntoView?.({ block: 'nearest' })
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// ─── Filter keyboard (native listener, fires before Navigator) ───────────
|
|
242
|
+
|
|
243
|
+
$effect(() => {
|
|
244
|
+
if (!isOpen || !filterable || !filterInputRef) return
|
|
245
|
+
const el = filterInputRef
|
|
246
|
+
const handler = (event: KeyboardEvent) => {
|
|
247
|
+
if (event.key === 'ArrowDown') {
|
|
248
|
+
event.preventDefault()
|
|
249
|
+
event.stopPropagation()
|
|
250
|
+
wrapper.first(null)
|
|
251
|
+
} else if (event.key === 'Escape') {
|
|
252
|
+
if (filterQuery) {
|
|
253
|
+
event.preventDefault()
|
|
254
|
+
event.stopPropagation()
|
|
255
|
+
filterQuery = ''
|
|
256
|
+
}
|
|
257
|
+
// Empty filter: let event bubble to Navigator/Trigger for close
|
|
258
|
+
} else if (event.key === 'Enter') {
|
|
259
|
+
event.preventDefault()
|
|
260
|
+
event.stopPropagation()
|
|
261
|
+
if (wrapper.focusedKey) wrapper.select(null)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
el.addEventListener('keydown', handler)
|
|
265
|
+
return () => el.removeEventListener('keydown', handler)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/** Set of group keys that need a divider before them (not the first group) */
|
|
271
|
+
const groupDividers = $derived.by(() => {
|
|
272
|
+
const set = new Set<string>()
|
|
273
|
+
let foundFirst = false
|
|
274
|
+
for (const node of wrapper.flatView) {
|
|
275
|
+
if (node.hasChildren) {
|
|
276
|
+
if (foundFirst) set.add(node.key)
|
|
277
|
+
foundFirst = true
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return set
|
|
281
|
+
})
|
|
282
|
+
</script>
|
|
283
|
+
|
|
284
|
+
{#snippet defaultOptionContent(proxy: ProxyItem)}
|
|
285
|
+
<ItemContent {proxy} />
|
|
286
|
+
{/snippet}
|
|
287
|
+
|
|
288
|
+
{#snippet defaultGroupContent(proxy: ProxyItem)}
|
|
289
|
+
{#if proxy.get('icon')}
|
|
290
|
+
<span data-select-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
|
|
291
|
+
{/if}
|
|
292
|
+
<span>{proxy.label}</span>
|
|
293
|
+
{/snippet}
|
|
294
|
+
|
|
295
|
+
<div
|
|
296
|
+
bind:this={selectRef}
|
|
297
|
+
data-select
|
|
298
|
+
data-open={isOpen || undefined}
|
|
299
|
+
data-size={size}
|
|
300
|
+
data-disabled={disabled || undefined}
|
|
301
|
+
data-align={align}
|
|
302
|
+
data-direction={direction}
|
|
303
|
+
class={className || undefined}
|
|
304
|
+
>
|
|
305
|
+
<button
|
|
306
|
+
bind:this={triggerRef}
|
|
307
|
+
type="button"
|
|
308
|
+
data-select-trigger
|
|
309
|
+
{disabled}
|
|
310
|
+
aria-haspopup="listbox"
|
|
311
|
+
aria-expanded={isOpen}
|
|
312
|
+
>
|
|
313
|
+
<span data-select-value>
|
|
314
|
+
{#if selectedProxy}
|
|
315
|
+
{#if selectedProxy.get('icon')}
|
|
316
|
+
<span data-select-value-icon class={selectedProxy.get('icon')} aria-hidden="true"></span>
|
|
317
|
+
{/if}
|
|
318
|
+
<span data-select-value-text>{selectedProxy.label}</span>
|
|
319
|
+
{:else}
|
|
320
|
+
<span data-select-placeholder>{placeholder}</span>
|
|
321
|
+
{/if}
|
|
322
|
+
</span>
|
|
323
|
+
<span data-select-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"></span>
|
|
324
|
+
</button>
|
|
325
|
+
|
|
326
|
+
{#if isOpen}
|
|
327
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
328
|
+
<div
|
|
329
|
+
bind:this={dropdownRef}
|
|
330
|
+
data-select-dropdown
|
|
331
|
+
role="listbox"
|
|
332
|
+
aria-orientation="vertical"
|
|
333
|
+
>
|
|
334
|
+
{#if filterable}
|
|
335
|
+
<div data-select-filter>
|
|
336
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
337
|
+
<input
|
|
338
|
+
bind:this={filterInputRef}
|
|
339
|
+
type="text"
|
|
340
|
+
data-select-filter-input
|
|
341
|
+
placeholder={filterPlaceholder}
|
|
342
|
+
bind:value={filterQuery}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
{/if}
|
|
346
|
+
|
|
347
|
+
{#each wrapper.flatView as node (node.key)}
|
|
348
|
+
{@const proxy = node.proxy}
|
|
349
|
+
{@const sel = !node.hasChildren && proxy.value === value}
|
|
350
|
+
{@const content = resolveSnippet(snippets as Record<string, unknown>, proxy, node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET)}
|
|
351
|
+
|
|
352
|
+
{#if node.type === 'separator'}
|
|
353
|
+
<hr data-select-separator />
|
|
354
|
+
{:else if node.hasChildren}
|
|
355
|
+
{#if groupDividers.has(node.key)}
|
|
356
|
+
<div data-select-divider></div>
|
|
357
|
+
{/if}
|
|
358
|
+
<div data-select-group-label role="presentation">
|
|
359
|
+
{#if content}
|
|
360
|
+
{@render content(proxy)}
|
|
361
|
+
{:else}
|
|
362
|
+
{@render defaultGroupContent(proxy)}
|
|
363
|
+
{/if}
|
|
364
|
+
</div>
|
|
365
|
+
{:else}
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
title={proxy.get('tooltip')}
|
|
369
|
+
data-select-option
|
|
370
|
+
data-path={node.key}
|
|
371
|
+
data-level={node.level}
|
|
372
|
+
data-selected={sel || undefined}
|
|
373
|
+
data-disabled={proxy.disabled || undefined}
|
|
374
|
+
role="option"
|
|
375
|
+
aria-selected={sel}
|
|
376
|
+
disabled={proxy.disabled || disabled}
|
|
377
|
+
tabindex="-1"
|
|
378
|
+
>
|
|
379
|
+
{#if content}
|
|
380
|
+
{@render content(proxy)}
|
|
381
|
+
{:else}
|
|
382
|
+
{@render defaultOptionContent(proxy)}
|
|
383
|
+
{/if}
|
|
384
|
+
{#if sel}
|
|
385
|
+
<span data-select-check class={icons.checked} aria-hidden="true"></span>
|
|
386
|
+
{/if}
|
|
387
|
+
</button>
|
|
388
|
+
{/if}
|
|
389
|
+
{/each}
|
|
390
|
+
|
|
391
|
+
{#if filterable && filterQuery && filteredItems.length === 0}
|
|
392
|
+
<div data-select-empty>No results</div>
|
|
393
|
+
{/if}
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
</div>
|
|
@@ -1,34 +1,43 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
2
3
|
import { id } from '@rokkit/core'
|
|
3
|
-
// import { clsx } from 'clsx'
|
|
4
4
|
|
|
5
|
-
|
|
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 {
|
|
6
24
|
color = 'rgb(var(--primary-500))',
|
|
7
25
|
radius = 300,
|
|
8
|
-
/** Depth of effect */
|
|
9
26
|
depth = 1,
|
|
10
|
-
/** Represents the height of the surface for a light filter primitive */
|
|
11
27
|
surfaceScale = 2,
|
|
12
|
-
/** The bigger the value the bigger the reflection */
|
|
13
28
|
specularConstant = 0.75,
|
|
14
|
-
/** controls the focus for the light source. The bigger the value the brighter the light */
|
|
15
29
|
specularExponent = 120,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} = $props()
|
|
30
|
+
class: className = '',
|
|
31
|
+
children
|
|
32
|
+
}: ShineProps = $props()
|
|
19
33
|
|
|
20
34
|
const filterId = id('filter')
|
|
21
35
|
|
|
22
36
|
let mouse = $state({ x: 0, y: 0 })
|
|
23
37
|
let wrapperBox = $state({ left: 0, top: 0 })
|
|
24
|
-
|
|
25
|
-
let wrapperEl = null
|
|
38
|
+
let wrapperEl: HTMLDivElement | null = $state(null)
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
* @param {PointerEvent} e
|
|
30
|
-
*/
|
|
31
|
-
function onPointerMove(e) {
|
|
40
|
+
function onPointerMove(e: PointerEvent) {
|
|
32
41
|
wrapperBox = wrapperEl?.getBoundingClientRect() ?? { left: 0, top: 0 }
|
|
33
42
|
mouse = { x: e.clientX, y: e.clientY }
|
|
34
43
|
}
|
|
@@ -40,7 +49,7 @@
|
|
|
40
49
|
|
|
41
50
|
<svelte:window onpointermove={onPointerMove} onscroll={onScroll} />
|
|
42
51
|
|
|
43
|
-
<svg data-shine-filter
|
|
52
|
+
<svg data-shine-filter>
|
|
44
53
|
<filter id={filterId} color-interpolation-filters="sRGB">
|
|
45
54
|
<feGaussianBlur in="SourceAlpha" stdDeviation={depth} />
|
|
46
55
|
|
|
@@ -69,10 +78,9 @@
|
|
|
69
78
|
</svg>
|
|
70
79
|
|
|
71
80
|
<div
|
|
72
|
-
data-shine
|
|
81
|
+
data-shine
|
|
73
82
|
style:filter="url(#{filterId})"
|
|
74
|
-
{
|
|
75
|
-
class="inline-block"
|
|
83
|
+
class={className || undefined}
|
|
76
84
|
bind:this={wrapperEl}
|
|
77
85
|
>
|
|
78
86
|
{@render children?.()}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
4
|
+
import { messages } from '@rokkit/states'
|
|
5
|
+
|
|
6
|
+
interface StepperStep {
|
|
7
|
+
/** Step label (shown below circle) */
|
|
8
|
+
text: string
|
|
9
|
+
/** Short text inside circle (default: step number) */
|
|
10
|
+
label?: string
|
|
11
|
+
/** Step is finished (shows checkmark) */
|
|
12
|
+
completed?: boolean
|
|
13
|
+
/** Step cannot be navigated to */
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
/** Number of sub-stages within this step (default: 1) */
|
|
16
|
+
stages?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface StepperIcons {
|
|
20
|
+
/** Icon class for check/completed state */
|
|
21
|
+
check?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StepperProps {
|
|
25
|
+
/** Array of step definitions */
|
|
26
|
+
steps?: StepperStep[]
|
|
27
|
+
/** Current step index (bindable) */
|
|
28
|
+
current?: number
|
|
29
|
+
/** Current sub-stage within step (bindable, 0-based) */
|
|
30
|
+
currentStage?: number
|
|
31
|
+
/** Only allow clicking completed steps + first incomplete */
|
|
32
|
+
linear?: boolean
|
|
33
|
+
/** Layout orientation */
|
|
34
|
+
orientation?: 'horizontal' | 'vertical'
|
|
35
|
+
/** Custom icons */
|
|
36
|
+
icons?: StepperIcons
|
|
37
|
+
/** Callback when a step or dot is clicked */
|
|
38
|
+
onclick?: (step: number, stage?: number) => void
|
|
39
|
+
/** Content snippet rendered below the stepper */
|
|
40
|
+
content?: Snippet<[StepperStep, number]>
|
|
41
|
+
/** Additional CSS class */
|
|
42
|
+
class?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultIcons: StepperIcons = {
|
|
46
|
+
check: DEFAULT_STATE_ICONS.action.check
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let {
|
|
50
|
+
steps = [],
|
|
51
|
+
current = $bindable(0),
|
|
52
|
+
currentStage = $bindable(0),
|
|
53
|
+
linear = false,
|
|
54
|
+
orientation = 'horizontal',
|
|
55
|
+
label = messages.current.stepper.label,
|
|
56
|
+
icons: userIcons,
|
|
57
|
+
onclick,
|
|
58
|
+
content,
|
|
59
|
+
class: className = ''
|
|
60
|
+
}: StepperProps & { label?: string } = $props()
|
|
61
|
+
|
|
62
|
+
const icons = $derived<StepperIcons>({ ...defaultIcons, ...userIcons })
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a step can be clicked
|
|
66
|
+
*/
|
|
67
|
+
function isClickable(index: number): boolean {
|
|
68
|
+
const step = steps[index]
|
|
69
|
+
if (step.disabled) return false
|
|
70
|
+
if (!linear) return true
|
|
71
|
+
// Linear mode: allow completed steps and the first incomplete step
|
|
72
|
+
if (step.completed) return true
|
|
73
|
+
// First incomplete step = first step where index >= first non-completed
|
|
74
|
+
const firstIncomplete = steps.findIndex((s) => !s.completed)
|
|
75
|
+
return index === firstIncomplete
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether the connector line before step N should look "completed"
|
|
80
|
+
*/
|
|
81
|
+
function isConnectorCompleted(index: number): boolean {
|
|
82
|
+
return index > 0 && Boolean(steps[index - 1]?.completed)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle step circle click
|
|
87
|
+
*/
|
|
88
|
+
function handleStepClick(index: number) {
|
|
89
|
+
if (!isClickable(index)) return
|
|
90
|
+
current = index
|
|
91
|
+
currentStage = 0
|
|
92
|
+
onclick?.(index)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle sub-stage dot click
|
|
97
|
+
*/
|
|
98
|
+
function handleDotClick(stepIndex: number, stageIndex: number) {
|
|
99
|
+
if (!isClickable(stepIndex)) return
|
|
100
|
+
current = stepIndex
|
|
101
|
+
currentStage = stageIndex
|
|
102
|
+
onclick?.(stepIndex, stageIndex)
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<div
|
|
107
|
+
data-stepper
|
|
108
|
+
data-orientation={orientation}
|
|
109
|
+
class={className || undefined}
|
|
110
|
+
role="group"
|
|
111
|
+
aria-label={label}
|
|
112
|
+
>
|
|
113
|
+
{#each steps as step, index (index)}
|
|
114
|
+
<!-- Connector line before step (except first) -->
|
|
115
|
+
{#if index > 0}
|
|
116
|
+
<div
|
|
117
|
+
data-stepper-connector
|
|
118
|
+
data-completed={isConnectorCompleted(index) || undefined}
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
></div>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
<!-- Step -->
|
|
124
|
+
<div
|
|
125
|
+
data-stepper-step
|
|
126
|
+
data-completed={step.completed || undefined}
|
|
127
|
+
data-active={index === current || undefined}
|
|
128
|
+
data-disabled={step.disabled || undefined}
|
|
129
|
+
>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
data-stepper-circle
|
|
133
|
+
disabled={!isClickable(index)}
|
|
134
|
+
aria-label="{step.text}{step.completed ? ' (completed)' : ''}"
|
|
135
|
+
aria-current={index === current ? 'step' : undefined}
|
|
136
|
+
onclick={() => handleStepClick(index)}
|
|
137
|
+
>
|
|
138
|
+
{#if step.completed}
|
|
139
|
+
<span data-stepper-check-icon class={icons.check} aria-hidden="true"></span>
|
|
140
|
+
{:else}
|
|
141
|
+
{step.label ?? index + 1}
|
|
142
|
+
{/if}
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<span data-stepper-label>{step.text}</span>
|
|
146
|
+
|
|
147
|
+
<!-- Sub-stage dots -->
|
|
148
|
+
{#if step.stages && step.stages > 1}
|
|
149
|
+
<div data-stepper-dots aria-label="Sub-stages for {step.text}">
|
|
150
|
+
{#each Array(step.stages) as _, stageIndex (stageIndex)}
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
data-stepper-dot
|
|
154
|
+
data-active={index === current && stageIndex === currentStage || undefined}
|
|
155
|
+
data-completed={step.completed || (index === current && stageIndex < currentStage) || undefined}
|
|
156
|
+
disabled={!isClickable(index)}
|
|
157
|
+
aria-label="Stage {stageIndex + 1}"
|
|
158
|
+
onclick={() => handleDotClick(index, stageIndex)}
|
|
159
|
+
></button>
|
|
160
|
+
{/each}
|
|
161
|
+
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
164
|
+
{/each}
|
|
165
|
+
|
|
166
|
+
<!-- Content area -->
|
|
167
|
+
{#if content && steps[current]}
|
|
168
|
+
<div data-stepper-content>
|
|
169
|
+
{@render content(steps[current], current)}
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|