@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.
Files changed (146) hide show
  1. package/README.md +198 -101
  2. package/package.json +52 -34
  3. package/src/components/BreadCrumbs.svelte +82 -0
  4. package/src/components/Button.svelte +87 -0
  5. package/src/components/ButtonGroup.svelte +18 -0
  6. package/src/components/Card.svelte +61 -0
  7. package/src/components/Carousel.svelte +169 -0
  8. package/src/components/Code.svelte +185 -0
  9. package/src/components/Connector.svelte +46 -0
  10. package/src/components/FloatingAction.svelte +331 -0
  11. package/src/components/FloatingNavigation.svelte +228 -0
  12. package/src/components/ItemContent.svelte +24 -0
  13. package/src/components/List.svelte +476 -0
  14. package/src/components/Menu.svelte +421 -0
  15. package/src/components/MultiSelect.svelte +521 -0
  16. package/src/components/PaletteManager.svelte +354 -0
  17. package/src/components/Pill.svelte +78 -0
  18. package/src/components/ProgressBar.svelte +31 -0
  19. package/src/components/Range.svelte +325 -0
  20. package/src/components/Rating.svelte +91 -0
  21. package/src/components/Reveal.svelte +58 -0
  22. package/src/components/SearchFilter.svelte +80 -0
  23. package/src/components/Select.svelte +585 -0
  24. package/src/{Shine.svelte → components/Shine.svelte} +29 -21
  25. package/src/components/Stepper.svelte +169 -0
  26. package/src/components/Switch.svelte +75 -0
  27. package/src/components/Table.svelte +243 -0
  28. package/src/components/Tabs.svelte +268 -0
  29. package/src/components/Tilt.svelte +68 -0
  30. package/src/components/Timeline.svelte +61 -0
  31. package/src/components/Toggle.svelte +157 -0
  32. package/src/components/Toolbar.svelte +307 -0
  33. package/src/components/ToolbarGroup.svelte +17 -0
  34. package/src/components/Tree.svelte +613 -0
  35. package/src/components/index.ts +33 -0
  36. package/src/index.ts +41 -0
  37. package/src/types/button.ts +83 -0
  38. package/src/types/code.ts +46 -0
  39. package/src/types/floating-action.ts +118 -0
  40. package/src/types/floating-navigation.ts +68 -0
  41. package/src/types/index.ts +53 -0
  42. package/src/types/item-proxy.ts +358 -0
  43. package/src/types/list.ts +196 -0
  44. package/src/types/menu.ts +195 -0
  45. package/src/types/palette.ts +143 -0
  46. package/src/types/range.ts +51 -0
  47. package/src/types/search-filter.ts +67 -0
  48. package/src/types/select.ts +206 -0
  49. package/src/types/switch.ts +64 -0
  50. package/src/types/table.ts +210 -0
  51. package/src/types/tabs.ts +124 -0
  52. package/src/types/timeline.ts +51 -0
  53. package/src/types/toggle.ts +109 -0
  54. package/src/types/toolbar.ts +164 -0
  55. package/src/types/tree.ts +259 -0
  56. package/src/utils/palette.ts +582 -0
  57. package/src/utils/shiki.ts +122 -0
  58. package/dist/constants.d.ts +0 -2
  59. package/dist/index.d.ts +0 -41
  60. package/dist/lib/fields.d.ts +0 -16
  61. package/dist/lib/form.d.ts +0 -95
  62. package/dist/lib/index.d.ts +0 -6
  63. package/dist/lib/layout.d.ts +0 -7
  64. package/dist/lib/nested.d.ts +0 -48
  65. package/dist/lib/schema.d.ts +0 -7
  66. package/dist/lib/select.d.ts +0 -8
  67. package/dist/lib/tree.d.ts +0 -9
  68. package/dist/tree/List.spec.svelte.d.ts +0 -1
  69. package/dist/tree/Node.spec.svelte.d.ts +0 -1
  70. package/dist/tree/Root.spec.svelte.d.ts +0 -1
  71. package/dist/types.d.ts +0 -5
  72. package/dist/wrappers/index.d.ts +0 -3
  73. package/src/Accordion.svelte +0 -118
  74. package/src/BreadCrumbs.svelte +0 -32
  75. package/src/Button.svelte +0 -57
  76. package/src/Calendar.svelte +0 -93
  77. package/src/Card.svelte +0 -45
  78. package/src/Carousel.svelte +0 -49
  79. package/src/CheckBox.svelte +0 -56
  80. package/src/Connector.svelte +0 -40
  81. package/src/DropDown.svelte +0 -68
  82. package/src/DropSearch.svelte +0 -37
  83. package/src/Fillable.svelte +0 -19
  84. package/src/GraphPaper.svelte +0 -43
  85. package/src/Icon.svelte +0 -81
  86. package/src/Item.svelte +0 -25
  87. package/src/Link.svelte +0 -21
  88. package/src/List.svelte +0 -89
  89. package/src/ListBody.svelte +0 -43
  90. package/src/Message.svelte +0 -11
  91. package/src/MultiSelect.svelte +0 -48
  92. package/src/NestedList.svelte +0 -78
  93. package/src/NestedPaginator.svelte +0 -63
  94. package/src/Node.svelte +0 -76
  95. package/src/Overlay.svelte +0 -21
  96. package/src/PageNavigator.svelte +0 -94
  97. package/src/PickOne.svelte +0 -60
  98. package/src/Pill.svelte +0 -41
  99. package/src/ProgressBar.svelte +0 -21
  100. package/src/ProgressDots.svelte +0 -53
  101. package/src/RadioGroup.svelte +0 -52
  102. package/src/Range.svelte +0 -45
  103. package/src/RangeMinMax.svelte +0 -124
  104. package/src/RangeSlider.svelte +0 -79
  105. package/src/RangeTick.svelte +0 -28
  106. package/src/Rating.svelte +0 -95
  107. package/src/ResponsiveGrid.svelte +0 -88
  108. package/src/Scrollable.svelte +0 -7
  109. package/src/Select.svelte +0 -114
  110. package/src/Separator.svelte +0 -1
  111. package/src/Slider.svelte +0 -14
  112. package/src/SlidingColumns.svelte +0 -50
  113. package/src/Stage.svelte +0 -41
  114. package/src/Stepper.svelte +0 -66
  115. package/src/Summary.svelte +0 -22
  116. package/src/Switch.svelte +0 -106
  117. package/src/TableCell.svelte +0 -51
  118. package/src/TableHeaderCell.svelte +0 -54
  119. package/src/Tabs.svelte +0 -176
  120. package/src/Tilt.svelte +0 -66
  121. package/src/Toggle.svelte +0 -58
  122. package/src/ToggleThemeMode.svelte +0 -23
  123. package/src/Tree.svelte +0 -80
  124. package/src/TreeTable.svelte +0 -171
  125. package/src/ValidationReport.svelte +0 -23
  126. package/src/constants.js +0 -4
  127. package/src/index.js +0 -48
  128. package/src/lib/fields.js +0 -118
  129. package/src/lib/form.js +0 -72
  130. package/src/lib/index.js +0 -13
  131. package/src/lib/layout.js +0 -63
  132. package/src/lib/nested.js +0 -192
  133. package/src/lib/schema.js +0 -32
  134. package/src/lib/select.js +0 -38
  135. package/src/lib/tree.js +0 -22
  136. package/src/tree/List.spec.svelte.js +0 -84
  137. package/src/tree/List.svelte +0 -78
  138. package/src/tree/Node.spec.svelte.js +0 -104
  139. package/src/tree/Node.svelte +0 -80
  140. package/src/tree/Root.spec.svelte.js +0 -63
  141. package/src/tree/Root.svelte +0 -81
  142. package/src/types.js +0 -9
  143. package/src/wrappers/Category.svelte +0 -27
  144. package/src/wrappers/Section.svelte +0 -16
  145. package/src/wrappers/Wrapper.svelte +0 -12
  146. 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}