@rokkit/ui 1.0.0-next.124 → 1.0.0-next.127
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 +52 -34
- package/src/components/BreadCrumbs.svelte +82 -0
- package/src/components/Button.svelte +87 -0
- package/src/components/ButtonGroup.svelte +18 -0
- package/src/components/Card.svelte +61 -0
- package/src/components/Carousel.svelte +169 -0
- package/src/components/Code.svelte +185 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/FloatingAction.svelte +331 -0
- package/src/components/FloatingNavigation.svelte +228 -0
- package/src/components/ItemContent.svelte +24 -0
- package/src/components/List.svelte +476 -0
- package/src/components/Menu.svelte +421 -0
- package/src/components/MultiSelect.svelte +521 -0
- package/src/components/PaletteManager.svelte +354 -0
- package/src/components/Pill.svelte +78 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +325 -0
- package/src/components/Rating.svelte +91 -0
- package/src/components/Reveal.svelte +58 -0
- package/src/components/SearchFilter.svelte +80 -0
- package/src/components/Select.svelte +585 -0
- package/src/{Shine.svelte → components/Shine.svelte} +29 -21
- package/src/components/Stepper.svelte +169 -0
- package/src/components/Switch.svelte +75 -0
- package/src/components/Table.svelte +243 -0
- package/src/components/Tabs.svelte +268 -0
- package/src/components/Tilt.svelte +68 -0
- package/src/components/Timeline.svelte +61 -0
- package/src/components/Toggle.svelte +157 -0
- package/src/components/Toolbar.svelte +307 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tree.svelte +613 -0
- package/src/components/index.ts +33 -0
- package/src/index.ts +41 -0
- package/src/types/button.ts +83 -0
- package/src/types/code.ts +46 -0
- package/src/types/floating-action.ts +118 -0
- package/src/types/floating-navigation.ts +68 -0
- package/src/types/index.ts +53 -0
- package/src/types/item-proxy.ts +358 -0
- package/src/types/list.ts +196 -0
- package/src/types/menu.ts +195 -0
- package/src/types/palette.ts +143 -0
- package/src/types/range.ts +51 -0
- package/src/types/search-filter.ts +67 -0
- package/src/types/select.ts +206 -0
- package/src/types/switch.ts +64 -0
- package/src/types/table.ts +210 -0
- package/src/types/tabs.ts +124 -0
- package/src/types/timeline.ts +51 -0
- package/src/types/toggle.ts +109 -0
- package/src/types/toolbar.ts +164 -0
- package/src/types/tree.ts +259 -0
- package/src/utils/palette.ts +582 -0
- package/src/utils/shiki.ts +122 -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,331 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
FloatingActionProps,
|
|
4
|
+
FloatingActionItem,
|
|
5
|
+
FloatingActionItemSnippet,
|
|
6
|
+
FloatingActionItemHandlers
|
|
7
|
+
} from '../types/floating-action.js'
|
|
8
|
+
import { getSnippet } from '../types/floating-action.js'
|
|
9
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
items = [],
|
|
13
|
+
fields: userFields,
|
|
14
|
+
icon = 'i-lucide:plus',
|
|
15
|
+
closeIcon = 'i-lucide:x',
|
|
16
|
+
label = 'Actions',
|
|
17
|
+
size = 'md',
|
|
18
|
+
position = 'bottom-right',
|
|
19
|
+
expand = 'vertical',
|
|
20
|
+
itemAlign = 'center',
|
|
21
|
+
disabled = false,
|
|
22
|
+
open = $bindable(false),
|
|
23
|
+
backdrop = true,
|
|
24
|
+
contained = false,
|
|
25
|
+
onselect,
|
|
26
|
+
onopen,
|
|
27
|
+
onclose,
|
|
28
|
+
class: className = '',
|
|
29
|
+
item: itemSnippet,
|
|
30
|
+
...snippets
|
|
31
|
+
}: FloatingActionProps & { [key: string]: FloatingActionItemSnippet | unknown } = $props()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an ItemProxy for the given item
|
|
35
|
+
*/
|
|
36
|
+
function createProxy(item: FloatingActionItem): ItemProxy {
|
|
37
|
+
return new ItemProxy(item, userFields)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let fabRef = $state<HTMLDivElement | null>(null)
|
|
41
|
+
let focusedIndex = $state(-1)
|
|
42
|
+
|
|
43
|
+
// Flatten items for keyboard navigation (excluding disabled)
|
|
44
|
+
const flatItems = $derived.by(() => {
|
|
45
|
+
return items
|
|
46
|
+
.map((item) => ({ proxy: createProxy(item), original: item }))
|
|
47
|
+
.filter((item) => !item.proxy.disabled)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Toggle the FAB open/closed
|
|
52
|
+
*/
|
|
53
|
+
function toggle() {
|
|
54
|
+
if (disabled) return
|
|
55
|
+
if (open) {
|
|
56
|
+
close()
|
|
57
|
+
} else {
|
|
58
|
+
openMenu()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Open the FAB menu
|
|
64
|
+
*/
|
|
65
|
+
function openMenu() {
|
|
66
|
+
if (disabled || open) return
|
|
67
|
+
open = true
|
|
68
|
+
focusedIndex = 0
|
|
69
|
+
onopen?.()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close the FAB menu
|
|
74
|
+
*/
|
|
75
|
+
function close() {
|
|
76
|
+
if (!open) return
|
|
77
|
+
open = false
|
|
78
|
+
focusedIndex = -1
|
|
79
|
+
onclose?.()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle item selection
|
|
84
|
+
*/
|
|
85
|
+
function handleItemClick(item: { proxy: ItemProxy; original: FloatingActionItem }) {
|
|
86
|
+
if (item.proxy.disabled) return
|
|
87
|
+
onselect?.(item.proxy.itemValue, item.original)
|
|
88
|
+
close()
|
|
89
|
+
// Return focus to trigger
|
|
90
|
+
const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
|
|
91
|
+
trigger?.focus()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Focus an item by index
|
|
96
|
+
*/
|
|
97
|
+
function focusItem(index: number) {
|
|
98
|
+
if (index < 0 || index >= flatItems.length) return
|
|
99
|
+
focusedIndex = index
|
|
100
|
+
const menu = fabRef?.querySelector('[data-fab-menu]')
|
|
101
|
+
if (menu) {
|
|
102
|
+
const menuItems = menu.querySelectorAll('[data-fab-item]:not([data-disabled])')
|
|
103
|
+
const menuItem = menuItems[index] as HTMLElement | undefined
|
|
104
|
+
menuItem?.focus()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle keyboard navigation on trigger
|
|
110
|
+
*/
|
|
111
|
+
function handleTriggerKeyDown(event: KeyboardEvent) {
|
|
112
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
toggle()
|
|
115
|
+
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
openMenu()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle keyboard navigation when menu is open
|
|
123
|
+
*/
|
|
124
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
125
|
+
if (!open) return
|
|
126
|
+
|
|
127
|
+
switch (event.key) {
|
|
128
|
+
case 'Escape':
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
close()
|
|
131
|
+
const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
|
|
132
|
+
trigger?.focus()
|
|
133
|
+
break
|
|
134
|
+
case 'ArrowDown':
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
focusItem(focusedIndex < flatItems.length - 1 ? focusedIndex + 1 : 0)
|
|
137
|
+
break
|
|
138
|
+
case 'ArrowUp':
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
focusItem(focusedIndex > 0 ? focusedIndex - 1 : flatItems.length - 1)
|
|
141
|
+
break
|
|
142
|
+
case 'Home':
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
focusItem(0)
|
|
145
|
+
break
|
|
146
|
+
case 'End':
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
focusItem(flatItems.length - 1)
|
|
149
|
+
break
|
|
150
|
+
case 'Enter':
|
|
151
|
+
case ' ':
|
|
152
|
+
event.preventDefault()
|
|
153
|
+
if (focusedIndex >= 0 && focusedIndex < flatItems.length) {
|
|
154
|
+
handleItemClick(flatItems[focusedIndex])
|
|
155
|
+
}
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handle item-specific keyboard events
|
|
162
|
+
*/
|
|
163
|
+
function handleItemKeyDown(
|
|
164
|
+
event: KeyboardEvent,
|
|
165
|
+
item: { proxy: ItemProxy; original: FloatingActionItem }
|
|
166
|
+
) {
|
|
167
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
168
|
+
event.preventDefault()
|
|
169
|
+
handleItemClick(item)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle click outside to close
|
|
175
|
+
*/
|
|
176
|
+
function handleClickOutside(event: MouseEvent) {
|
|
177
|
+
if (fabRef && !fabRef.contains(event.target as Node)) {
|
|
178
|
+
close()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle backdrop click
|
|
184
|
+
*/
|
|
185
|
+
function handleBackdropClick() {
|
|
186
|
+
close()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create handlers object for custom snippets
|
|
191
|
+
*/
|
|
192
|
+
function createHandlers(item: {
|
|
193
|
+
proxy: ItemProxy
|
|
194
|
+
original: FloatingActionItem
|
|
195
|
+
}): FloatingActionItemHandlers {
|
|
196
|
+
return {
|
|
197
|
+
onclick: () => handleItemClick(item),
|
|
198
|
+
onkeydown: (event: KeyboardEvent) => handleItemKeyDown(event, item)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve which snippet to use for an item
|
|
204
|
+
*/
|
|
205
|
+
function resolveItemSnippet(proxy: ItemProxy): FloatingActionItemSnippet | null {
|
|
206
|
+
const snippetName = proxy.snippetName
|
|
207
|
+
if (snippetName) {
|
|
208
|
+
const namedSnippet = getSnippet(snippets, snippetName)
|
|
209
|
+
if (namedSnippet) {
|
|
210
|
+
return namedSnippet as FloatingActionItemSnippet
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return itemSnippet ?? null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Calculate item animation delay for stagger effect
|
|
218
|
+
*/
|
|
219
|
+
function getItemDelay(index: number): string {
|
|
220
|
+
return `${index * 50}ms`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Set up click outside listener when open
|
|
224
|
+
$effect(() => {
|
|
225
|
+
if (open) {
|
|
226
|
+
document.addEventListener('click', handleClickOutside, true)
|
|
227
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
228
|
+
// Focus first item after animation starts
|
|
229
|
+
requestAnimationFrame(() => {
|
|
230
|
+
if (flatItems.length > 0) {
|
|
231
|
+
focusItem(0)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
return () => {
|
|
236
|
+
document.removeEventListener('click', handleClickOutside, true)
|
|
237
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
{#snippet defaultItem(
|
|
243
|
+
proxy: ItemProxy,
|
|
244
|
+
handlers: FloatingActionItemHandlers,
|
|
245
|
+
index: number,
|
|
246
|
+
total: number
|
|
247
|
+
)}
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
data-fab-item
|
|
251
|
+
data-fab-index={index}
|
|
252
|
+
data-disabled={proxy.disabled || undefined}
|
|
253
|
+
disabled={proxy.disabled || disabled}
|
|
254
|
+
aria-label={proxy.label || proxy.text}
|
|
255
|
+
style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
|
|
256
|
+
onclick={handlers.onclick}
|
|
257
|
+
onkeydown={handlers.onkeydown}
|
|
258
|
+
>
|
|
259
|
+
{#if proxy.icon}
|
|
260
|
+
<span data-fab-item-icon class={proxy.icon} aria-hidden="true"></span>
|
|
261
|
+
{/if}
|
|
262
|
+
{#if proxy.text}
|
|
263
|
+
<span data-fab-item-label>{proxy.text}</span>
|
|
264
|
+
{/if}
|
|
265
|
+
</button>
|
|
266
|
+
{/snippet}
|
|
267
|
+
|
|
268
|
+
{#snippet renderItem(
|
|
269
|
+
item: { proxy: ItemProxy; original: FloatingActionItem },
|
|
270
|
+
index: number,
|
|
271
|
+
total: number
|
|
272
|
+
)}
|
|
273
|
+
{@const customSnippet = resolveItemSnippet(item.proxy)}
|
|
274
|
+
{@const handlers = createHandlers(item)}
|
|
275
|
+
{#if customSnippet}
|
|
276
|
+
<div
|
|
277
|
+
data-fab-item
|
|
278
|
+
data-fab-item-custom
|
|
279
|
+
data-fab-index={index}
|
|
280
|
+
data-disabled={item.proxy.disabled || undefined}
|
|
281
|
+
style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
|
|
282
|
+
>
|
|
283
|
+
<svelte:boundary>
|
|
284
|
+
{@render customSnippet(item.original, item.proxy.fields, handlers)}
|
|
285
|
+
{#snippet failed()}
|
|
286
|
+
{@render defaultItem(item.proxy, handlers, index, total)}
|
|
287
|
+
{/snippet}
|
|
288
|
+
</svelte:boundary>
|
|
289
|
+
</div>
|
|
290
|
+
{:else}
|
|
291
|
+
{@render defaultItem(item.proxy, handlers, index, total)}
|
|
292
|
+
{/if}
|
|
293
|
+
{/snippet}
|
|
294
|
+
|
|
295
|
+
<div
|
|
296
|
+
bind:this={fabRef}
|
|
297
|
+
data-fab
|
|
298
|
+
data-open={open || undefined}
|
|
299
|
+
data-size={size}
|
|
300
|
+
data-position={position}
|
|
301
|
+
data-expand={expand}
|
|
302
|
+
data-item-align={itemAlign}
|
|
303
|
+
data-disabled={disabled || undefined}
|
|
304
|
+
data-contained={contained || undefined}
|
|
305
|
+
class={className || undefined}
|
|
306
|
+
>
|
|
307
|
+
{#if backdrop && open}
|
|
308
|
+
<div data-fab-backdrop role="presentation" onclick={handleBackdropClick}></div>
|
|
309
|
+
{/if}
|
|
310
|
+
|
|
311
|
+
{#if open}
|
|
312
|
+
<div data-fab-menu role="menu" aria-label={label}>
|
|
313
|
+
{#each flatItems as item, index (index)}
|
|
314
|
+
{@render renderItem(item, index, flatItems.length)}
|
|
315
|
+
{/each}
|
|
316
|
+
</div>
|
|
317
|
+
{/if}
|
|
318
|
+
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
data-fab-trigger
|
|
322
|
+
{disabled}
|
|
323
|
+
aria-label={label}
|
|
324
|
+
aria-haspopup="menu"
|
|
325
|
+
aria-expanded={open}
|
|
326
|
+
onclick={toggle}
|
|
327
|
+
onkeydown={handleTriggerKeyDown}
|
|
328
|
+
>
|
|
329
|
+
<span data-fab-icon class={open ? closeIcon : icon} aria-hidden="true"></span>
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FloatingNavigationProps } from '../types/floating-navigation.js'
|
|
3
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
items = [],
|
|
7
|
+
fields: userFields,
|
|
8
|
+
value = $bindable(),
|
|
9
|
+
position = 'right',
|
|
10
|
+
pinned = $bindable(false),
|
|
11
|
+
observe = true,
|
|
12
|
+
observerOptions = { rootMargin: '-20% 0px -70% 0px', threshold: 0 },
|
|
13
|
+
size = 'md',
|
|
14
|
+
label = 'Page navigation',
|
|
15
|
+
onselect,
|
|
16
|
+
onpinchange,
|
|
17
|
+
item: itemSnippet,
|
|
18
|
+
class: className = ''
|
|
19
|
+
}: FloatingNavigationProps = $props()
|
|
20
|
+
|
|
21
|
+
let navRef = $state<HTMLElement | null>(null)
|
|
22
|
+
let expanded = $state(false)
|
|
23
|
+
let focusedIndex = $state(-1)
|
|
24
|
+
|
|
25
|
+
const isVertical = $derived(position === 'left' || position === 'right')
|
|
26
|
+
|
|
27
|
+
const itemProxies = $derived(
|
|
28
|
+
items.map((item) => ({
|
|
29
|
+
proxy: new ItemProxy(item, userFields),
|
|
30
|
+
original: item
|
|
31
|
+
}))
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const activeIndex = $derived(
|
|
35
|
+
itemProxies.findIndex((item) => item.proxy.itemValue === value)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
function togglePin() {
|
|
39
|
+
pinned = !pinned
|
|
40
|
+
if (!pinned) expanded = false
|
|
41
|
+
onpinchange?.(pinned)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleMouseEnter() {
|
|
45
|
+
if (!pinned) expanded = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleMouseLeave() {
|
|
49
|
+
if (!pinned) expanded = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleItemClick(item: { proxy: ItemProxy; original: Record<string, unknown> }) {
|
|
53
|
+
value = item.proxy.itemValue
|
|
54
|
+
onselect?.(item.proxy.itemValue, item.original)
|
|
55
|
+
|
|
56
|
+
// Smooth scroll to target section
|
|
57
|
+
const href = item.proxy.has('href') ? String(item.original[userFields?.href ?? 'href'] ?? '') : ''
|
|
58
|
+
const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.itemValue)
|
|
59
|
+
const el = document.getElementById(targetId)
|
|
60
|
+
el?.scrollIntoView({ behavior: 'smooth' })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
64
|
+
const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight'
|
|
65
|
+
const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft'
|
|
66
|
+
|
|
67
|
+
switch (event.key) {
|
|
68
|
+
case nextKey:
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
focusItem(focusedIndex < itemProxies.length - 1 ? focusedIndex + 1 : 0)
|
|
71
|
+
break
|
|
72
|
+
case prevKey:
|
|
73
|
+
event.preventDefault()
|
|
74
|
+
focusItem(focusedIndex > 0 ? focusedIndex - 1 : itemProxies.length - 1)
|
|
75
|
+
break
|
|
76
|
+
case 'Home':
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
focusItem(0)
|
|
79
|
+
break
|
|
80
|
+
case 'End':
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
focusItem(itemProxies.length - 1)
|
|
83
|
+
break
|
|
84
|
+
case 'Enter':
|
|
85
|
+
case ' ':
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
if (focusedIndex >= 0 && focusedIndex < itemProxies.length) {
|
|
88
|
+
handleItemClick(itemProxies[focusedIndex])
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
case 'Escape':
|
|
92
|
+
if (!pinned) {
|
|
93
|
+
event.preventDefault()
|
|
94
|
+
expanded = false
|
|
95
|
+
}
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function focusItem(index: number) {
|
|
101
|
+
if (index < 0 || index >= itemProxies.length) return
|
|
102
|
+
focusedIndex = index
|
|
103
|
+
const itemsContainer = navRef?.querySelector('[data-floating-nav-items]')
|
|
104
|
+
if (itemsContainer) {
|
|
105
|
+
const navItems = itemsContainer.querySelectorAll('[data-floating-nav-item]')
|
|
106
|
+
const item = navItems[index] as HTMLElement | undefined
|
|
107
|
+
item?.focus()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// IntersectionObserver for active section tracking
|
|
112
|
+
$effect(() => {
|
|
113
|
+
if (!observe || itemProxies.length === 0) return
|
|
114
|
+
|
|
115
|
+
const observer = new IntersectionObserver((entries) => {
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (entry.isIntersecting) {
|
|
118
|
+
const match = itemProxies.find((item) => {
|
|
119
|
+
const href = item.proxy.has('href')
|
|
120
|
+
? String(item.original[userFields?.href ?? 'href'] ?? '')
|
|
121
|
+
: ''
|
|
122
|
+
const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.itemValue)
|
|
123
|
+
return targetId === entry.target.id
|
|
124
|
+
})
|
|
125
|
+
if (match) {
|
|
126
|
+
value = match.proxy.itemValue
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, observerOptions)
|
|
131
|
+
|
|
132
|
+
for (const item of itemProxies) {
|
|
133
|
+
const href = item.proxy.has('href')
|
|
134
|
+
? String(item.original[userFields?.href ?? 'href'] ?? '')
|
|
135
|
+
: ''
|
|
136
|
+
const targetId = href.startsWith('#') ? href.slice(1) : String(item.proxy.itemValue)
|
|
137
|
+
const el = document.getElementById(targetId)
|
|
138
|
+
if (el) observer.observe(el)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return () => observer.disconnect()
|
|
142
|
+
})
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
146
|
+
<nav
|
|
147
|
+
bind:this={navRef}
|
|
148
|
+
data-floating-nav
|
|
149
|
+
data-position={position}
|
|
150
|
+
data-expanded={expanded || pinned || undefined}
|
|
151
|
+
data-pinned={pinned || undefined}
|
|
152
|
+
data-size={size}
|
|
153
|
+
aria-label={label}
|
|
154
|
+
class={className || undefined}
|
|
155
|
+
onmouseenter={handleMouseEnter}
|
|
156
|
+
onmouseleave={handleMouseLeave}
|
|
157
|
+
onkeydown={handleKeyDown}
|
|
158
|
+
>
|
|
159
|
+
<div data-floating-nav-header>
|
|
160
|
+
{#if expanded || pinned}
|
|
161
|
+
<span data-floating-nav-title>{label}</span>
|
|
162
|
+
{/if}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
data-floating-nav-pin
|
|
166
|
+
aria-pressed={pinned}
|
|
167
|
+
aria-label={pinned ? 'Unpin navigation' : 'Pin navigation'}
|
|
168
|
+
onclick={togglePin}
|
|
169
|
+
>
|
|
170
|
+
<span class={pinned ? 'i-lucide:pin-off' : 'i-lucide:pin'} aria-hidden="true"></span>
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div data-floating-nav-items>
|
|
175
|
+
{#each itemProxies as item, index (item.proxy.itemValue ?? index)}
|
|
176
|
+
{@const isActive = item.proxy.itemValue === value}
|
|
177
|
+
{@const isLink = item.proxy.has('href')}
|
|
178
|
+
{#if itemSnippet}
|
|
179
|
+
{@render itemSnippet(item.original, {
|
|
180
|
+
text: item.proxy.text,
|
|
181
|
+
icon: item.proxy.icon,
|
|
182
|
+
active: isActive
|
|
183
|
+
})}
|
|
184
|
+
{:else if isLink}
|
|
185
|
+
<a
|
|
186
|
+
data-floating-nav-item
|
|
187
|
+
data-active={isActive || undefined}
|
|
188
|
+
href={String(item.original[userFields?.href ?? 'href'] ?? '')}
|
|
189
|
+
aria-current={isActive ? 'true' : undefined}
|
|
190
|
+
tabindex={index === 0 ? 0 : -1}
|
|
191
|
+
style="--fn-index: {index}; --fn-total: {itemProxies.length}"
|
|
192
|
+
onclick={(e) => {
|
|
193
|
+
e.preventDefault()
|
|
194
|
+
handleItemClick(item)
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{#if item.proxy.icon}
|
|
198
|
+
<span data-floating-nav-icon class={item.proxy.icon} aria-hidden="true"></span>
|
|
199
|
+
{/if}
|
|
200
|
+
<span data-floating-nav-label>{item.proxy.text}</span>
|
|
201
|
+
</a>
|
|
202
|
+
{:else}
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
data-floating-nav-item
|
|
206
|
+
data-active={isActive || undefined}
|
|
207
|
+
aria-current={isActive ? 'true' : undefined}
|
|
208
|
+
tabindex={index === 0 ? 0 : -1}
|
|
209
|
+
style="--fn-index: {index}; --fn-total: {itemProxies.length}"
|
|
210
|
+
onclick={() => handleItemClick(item)}
|
|
211
|
+
>
|
|
212
|
+
{#if item.proxy.icon}
|
|
213
|
+
<span data-floating-nav-icon class={item.proxy.icon} aria-hidden="true"></span>
|
|
214
|
+
{/if}
|
|
215
|
+
<span data-floating-nav-label>{item.proxy.text}</span>
|
|
216
|
+
</button>
|
|
217
|
+
{/if}
|
|
218
|
+
{/each}
|
|
219
|
+
|
|
220
|
+
{#if activeIndex >= 0}
|
|
221
|
+
<span
|
|
222
|
+
data-floating-nav-indicator
|
|
223
|
+
style="--fn-active-index: {activeIndex}"
|
|
224
|
+
aria-hidden="true"
|
|
225
|
+
></span>
|
|
226
|
+
{/if}
|
|
227
|
+
</div>
|
|
228
|
+
</nav>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ItemProxy } from '../types/item-proxy.js'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
proxy: ItemProxy
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { proxy }: Props = $props()
|
|
9
|
+
|
|
10
|
+
const badge = $derived(proxy.get<string>('badge'))
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{#if proxy.icon}
|
|
14
|
+
<span data-item-icon class={proxy.icon} aria-hidden="true"></span>
|
|
15
|
+
{/if}
|
|
16
|
+
<span data-item-text>
|
|
17
|
+
<span data-item-label>{proxy.text}</span>
|
|
18
|
+
{#if proxy.description}
|
|
19
|
+
<span data-item-description>{proxy.description}</span>
|
|
20
|
+
{/if}
|
|
21
|
+
</span>
|
|
22
|
+
{#if badge}
|
|
23
|
+
<span data-item-badge>{badge}</span>
|
|
24
|
+
{/if}
|