@rokkit/ui 1.0.0-next.99 → 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,202 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Dropdown — Trigger that shows the selected value + dropdown panel with options.
|
|
4
|
+
*
|
|
5
|
+
* Similar to Menu but the trigger label reflects the currently selected value
|
|
6
|
+
* instead of a static label prop.
|
|
7
|
+
*
|
|
8
|
+
* Data attributes:
|
|
9
|
+
* data-dropdown — root container
|
|
10
|
+
* data-dropdown-trigger — trigger button
|
|
11
|
+
* data-dropdown-icon — optional icon in trigger
|
|
12
|
+
* data-dropdown-label — selected value text in trigger
|
|
13
|
+
* data-dropdown-arrow — expand/collapse chevron
|
|
14
|
+
* data-dropdown-panel — dropdown options panel
|
|
15
|
+
* data-dropdown-option — each option item
|
|
16
|
+
* data-dropdown-separator — separator between options
|
|
17
|
+
* data-active — marks the currently selected option
|
|
18
|
+
* data-open — present on root when panel is open
|
|
19
|
+
* data-size — size variant (sm | md | lg)
|
|
20
|
+
* data-align — panel alignment (start | end)
|
|
21
|
+
* data-direction — panel direction (down | up)
|
|
22
|
+
*/
|
|
23
|
+
// @ts-nocheck
|
|
24
|
+
import { ProxyTree, Wrapper, messages } from '@rokkit/states'
|
|
25
|
+
import { Navigator, Trigger } from '@rokkit/actions'
|
|
26
|
+
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
27
|
+
|
|
28
|
+
interface DropdownIcons {
|
|
29
|
+
opened?: string
|
|
30
|
+
closed?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
items = [],
|
|
35
|
+
fields = {},
|
|
36
|
+
value = $bindable(),
|
|
37
|
+
placeholder = messages.select,
|
|
38
|
+
icon,
|
|
39
|
+
size = 'md',
|
|
40
|
+
disabled = false,
|
|
41
|
+
showArrow = true,
|
|
42
|
+
align = 'start',
|
|
43
|
+
direction = 'down',
|
|
44
|
+
icons: userIcons = {} as DropdownIcons,
|
|
45
|
+
onchange,
|
|
46
|
+
class: className = ''
|
|
47
|
+
}: {
|
|
48
|
+
items?: unknown[]
|
|
49
|
+
fields?: Record<string, string>
|
|
50
|
+
value?: unknown
|
|
51
|
+
placeholder?: string
|
|
52
|
+
icon?: string
|
|
53
|
+
size?: string
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
showArrow?: boolean
|
|
56
|
+
align?: 'start' | 'end'
|
|
57
|
+
direction?: 'up' | 'down'
|
|
58
|
+
icons?: DropdownIcons
|
|
59
|
+
onchange?: (value: unknown, item: unknown) => void
|
|
60
|
+
class?: string
|
|
61
|
+
} = $props()
|
|
62
|
+
|
|
63
|
+
const icons = $derived({ ...DEFAULT_STATE_ICONS.selector, ...userIcons })
|
|
64
|
+
|
|
65
|
+
// ─── State ────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
let isOpen = $state(false)
|
|
68
|
+
let rootRef = $state<HTMLElement | null>(null)
|
|
69
|
+
let triggerRef = $state<HTMLElement | null>(null)
|
|
70
|
+
let panelRef = $state<HTMLElement | null>(null)
|
|
71
|
+
|
|
72
|
+
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function handleSelect(v: unknown, proxy: unknown) {
|
|
75
|
+
if ((proxy as { disabled?: boolean }).disabled) return
|
|
76
|
+
value = v
|
|
77
|
+
onchange?.(v, (proxy as { original: unknown }).original)
|
|
78
|
+
isOpen = false
|
|
79
|
+
triggerRef?.focus()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const proxyTree = $derived(new ProxyTree(items, fields))
|
|
83
|
+
const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
const w = wrapper
|
|
87
|
+
w.cancel = () => {
|
|
88
|
+
isOpen = false
|
|
89
|
+
triggerRef?.focus()
|
|
90
|
+
}
|
|
91
|
+
w.blur = () => {
|
|
92
|
+
isOpen = false
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
const _w = wrapper
|
|
98
|
+
if (isOpen) _w.first(null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ─── Trigger action ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (!triggerRef || !rootRef || disabled) return
|
|
105
|
+
const t = new Trigger(triggerRef, rootRef, {
|
|
106
|
+
isOpen: () => isOpen,
|
|
107
|
+
onopen: () => {
|
|
108
|
+
isOpen = true
|
|
109
|
+
requestAnimationFrame(() => wrapper.first(null))
|
|
110
|
+
},
|
|
111
|
+
onclose: () => {
|
|
112
|
+
isOpen = false
|
|
113
|
+
},
|
|
114
|
+
onlast: () => requestAnimationFrame(() => wrapper.last(null))
|
|
115
|
+
})
|
|
116
|
+
return () => t.destroy()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ─── Navigator on panel ───────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (!isOpen || !panelRef) return
|
|
123
|
+
const nav = new Navigator(panelRef, wrapper, { containScroll: true })
|
|
124
|
+
return () => nav.destroy()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Focus sync — move DOM focus to focusedKey in panel
|
|
128
|
+
$effect(() => {
|
|
129
|
+
const key = wrapper.focusedKey
|
|
130
|
+
if (!isOpen || !panelRef || !key) return
|
|
131
|
+
requestAnimationFrame(() => {
|
|
132
|
+
const target = panelRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
133
|
+
if (target && target !== document.activeElement) target.focus()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ─── Selected label ───────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const selectedLabel = $derived.by(() => {
|
|
140
|
+
if (value === undefined || value === null) return null
|
|
141
|
+
for (const node of wrapper.flatView) {
|
|
142
|
+
if (node.proxy.value === value) return node.proxy.label
|
|
143
|
+
}
|
|
144
|
+
return String(value)
|
|
145
|
+
})
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<div
|
|
149
|
+
bind:this={rootRef}
|
|
150
|
+
data-dropdown
|
|
151
|
+
data-open={isOpen || undefined}
|
|
152
|
+
data-size={size}
|
|
153
|
+
data-disabled={disabled || undefined}
|
|
154
|
+
data-align={align}
|
|
155
|
+
data-direction={direction}
|
|
156
|
+
class={className || undefined}
|
|
157
|
+
>
|
|
158
|
+
<button
|
|
159
|
+
bind:this={triggerRef}
|
|
160
|
+
type="button"
|
|
161
|
+
data-dropdown-trigger
|
|
162
|
+
{disabled}
|
|
163
|
+
aria-haspopup="listbox"
|
|
164
|
+
aria-expanded={isOpen}
|
|
165
|
+
>
|
|
166
|
+
{#if icon}
|
|
167
|
+
<span data-dropdown-icon class={icon} aria-hidden="true"></span>
|
|
168
|
+
{/if}
|
|
169
|
+
<span data-dropdown-label>{selectedLabel ?? placeholder}</span>
|
|
170
|
+
{#if showArrow}
|
|
171
|
+
<span data-dropdown-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"
|
|
172
|
+
></span>
|
|
173
|
+
{/if}
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
{#if isOpen}
|
|
177
|
+
<div bind:this={panelRef} data-dropdown-panel role="listbox">
|
|
178
|
+
{#each wrapper.flatView as node (node.key)}
|
|
179
|
+
{@const proxy = node.proxy}
|
|
180
|
+
{@const isActive = proxy.value === value}
|
|
181
|
+
|
|
182
|
+
{#if node.type === 'separator'}
|
|
183
|
+
<hr data-dropdown-separator />
|
|
184
|
+
{:else}
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
data-dropdown-option
|
|
188
|
+
data-path={node.key}
|
|
189
|
+
data-active={isActive || undefined}
|
|
190
|
+
data-disabled={proxy.disabled || undefined}
|
|
191
|
+
disabled={proxy.disabled || disabled}
|
|
192
|
+
role="option"
|
|
193
|
+
aria-selected={isActive}
|
|
194
|
+
tabindex="-1"
|
|
195
|
+
>
|
|
196
|
+
<span data-dropdown-option-label>{proxy.label}</span>
|
|
197
|
+
</button>
|
|
198
|
+
{/if}
|
|
199
|
+
{/each}
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
</div>
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
FloatingActionProps,
|
|
4
|
+
FloatingActionItem,
|
|
5
|
+
FloatingActionItemSnippet,
|
|
6
|
+
FloatingActionItemHandlers,
|
|
7
|
+
FloatingActionIcons
|
|
8
|
+
} from '../types/floating-action.js'
|
|
9
|
+
import { getSnippet } from '../types/floating-action.js'
|
|
10
|
+
import { ProxyItem } from '@rokkit/states'
|
|
11
|
+
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
items = [],
|
|
15
|
+
fields: userFields,
|
|
16
|
+
icons: userIcons = {} as FloatingActionIcons,
|
|
17
|
+
label = 'Actions',
|
|
18
|
+
size = 'md',
|
|
19
|
+
position = 'bottom-right',
|
|
20
|
+
expand = 'vertical',
|
|
21
|
+
itemAlign = 'center',
|
|
22
|
+
disabled = false,
|
|
23
|
+
open = $bindable(false),
|
|
24
|
+
backdrop = true,
|
|
25
|
+
contained = false,
|
|
26
|
+
onselect,
|
|
27
|
+
onopen,
|
|
28
|
+
onclose,
|
|
29
|
+
class: className = '',
|
|
30
|
+
item: itemSnippet,
|
|
31
|
+
...snippets
|
|
32
|
+
}: FloatingActionProps & { [key: string]: FloatingActionItemSnippet | unknown } = $props()
|
|
33
|
+
|
|
34
|
+
const icons = $derived({
|
|
35
|
+
add: DEFAULT_STATE_ICONS.action.add,
|
|
36
|
+
close: DEFAULT_STATE_ICONS.action.close,
|
|
37
|
+
...userIcons
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a ProxyItem for the given item
|
|
42
|
+
*/
|
|
43
|
+
function createProxy(item: FloatingActionItem): ProxyItem {
|
|
44
|
+
return new ProxyItem(item, userFields)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let fabRef = $state<HTMLDivElement | null>(null)
|
|
48
|
+
let focusedIndex = $state(-1)
|
|
49
|
+
|
|
50
|
+
// Flatten items for keyboard navigation (excluding disabled)
|
|
51
|
+
const flatItems = $derived.by(() => {
|
|
52
|
+
return items
|
|
53
|
+
.map((item) => ({ proxy: createProxy(item), original: item }))
|
|
54
|
+
.filter((item) => !item.proxy.disabled)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Toggle the FAB open/closed
|
|
59
|
+
*/
|
|
60
|
+
function toggle() {
|
|
61
|
+
if (disabled) return
|
|
62
|
+
if (open) {
|
|
63
|
+
close()
|
|
64
|
+
} else {
|
|
65
|
+
openMenu()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Open the FAB menu
|
|
71
|
+
*/
|
|
72
|
+
function openMenu() {
|
|
73
|
+
if (disabled || open) return
|
|
74
|
+
open = true
|
|
75
|
+
focusedIndex = 0
|
|
76
|
+
onopen?.()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Close the FAB menu
|
|
81
|
+
*/
|
|
82
|
+
function close() {
|
|
83
|
+
if (!open) return
|
|
84
|
+
open = false
|
|
85
|
+
focusedIndex = -1
|
|
86
|
+
onclose?.()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle item selection
|
|
91
|
+
*/
|
|
92
|
+
function handleItemClick(item: { proxy: ProxyItem; original: FloatingActionItem }) {
|
|
93
|
+
if (item.proxy.disabled) return
|
|
94
|
+
onselect?.(item.proxy.value, item.original)
|
|
95
|
+
close()
|
|
96
|
+
// Return focus to trigger
|
|
97
|
+
const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
|
|
98
|
+
trigger?.focus()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Move DOM focus to the menu item at the given index
|
|
103
|
+
*/
|
|
104
|
+
function focusDomItem(index: number) {
|
|
105
|
+
const menu = fabRef?.querySelector('[data-fab-menu]')
|
|
106
|
+
if (!menu) return
|
|
107
|
+
const menuItems = menu.querySelectorAll('[data-fab-item]:not([data-disabled])')
|
|
108
|
+
const menuItem = menuItems[index] as HTMLElement | undefined
|
|
109
|
+
menuItem?.focus()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Focus an item by index
|
|
114
|
+
*/
|
|
115
|
+
function focusItem(index: number) {
|
|
116
|
+
if (index < 0 || index >= flatItems.length) return
|
|
117
|
+
focusedIndex = index
|
|
118
|
+
focusDomItem(index)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle keyboard navigation on trigger
|
|
123
|
+
*/
|
|
124
|
+
function handleTriggerKeyDown(event: KeyboardEvent) {
|
|
125
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
126
|
+
event.preventDefault()
|
|
127
|
+
toggle()
|
|
128
|
+
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
openMenu()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Close menu and return focus to trigger
|
|
136
|
+
*/
|
|
137
|
+
function closeAndFocusTrigger() {
|
|
138
|
+
close()
|
|
139
|
+
const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
|
|
140
|
+
trigger?.focus()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Activate the currently focused item
|
|
145
|
+
*/
|
|
146
|
+
function activateFocusedItem() {
|
|
147
|
+
if (focusedIndex >= 0 && focusedIndex < flatItems.length) {
|
|
148
|
+
handleItemClick(flatItems[focusedIndex])
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function wrapNext(current: number, last: number): number {
|
|
153
|
+
return current < last ? current + 1 : 0
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function wrapPrev(current: number, last: number): number {
|
|
157
|
+
return current > 0 ? current - 1 : last
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nextFabIndex(key: string, current: number, last: number): number {
|
|
161
|
+
if (key === 'ArrowDown') return wrapNext(current, last)
|
|
162
|
+
if (key === 'ArrowUp') return wrapPrev(current, last)
|
|
163
|
+
if (key === 'Home') return 0
|
|
164
|
+
if (key === 'End') return last
|
|
165
|
+
return -1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleFabNavMove(event: KeyboardEvent): boolean {
|
|
169
|
+
const last = flatItems.length - 1
|
|
170
|
+
const next = nextFabIndex(event.key, focusedIndex, last)
|
|
171
|
+
if (next === -1) return false
|
|
172
|
+
focusItem(next)
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isActivateKey(key: string): boolean {
|
|
177
|
+
return key === 'Enter' || key === ' '
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handle keyboard navigation when menu is open
|
|
182
|
+
*/
|
|
183
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
184
|
+
if (!open) return
|
|
185
|
+
if (event.key === 'Escape') {
|
|
186
|
+
event.preventDefault()
|
|
187
|
+
closeAndFocusTrigger()
|
|
188
|
+
} else if (isActivateKey(event.key)) {
|
|
189
|
+
event.preventDefault()
|
|
190
|
+
activateFocusedItem()
|
|
191
|
+
} else if (handleFabNavMove(event)) {
|
|
192
|
+
event.preventDefault()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle item-specific keyboard events
|
|
198
|
+
*/
|
|
199
|
+
function handleItemKeyDown(
|
|
200
|
+
event: KeyboardEvent,
|
|
201
|
+
item: { proxy: ProxyItem; original: FloatingActionItem }
|
|
202
|
+
) {
|
|
203
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
204
|
+
event.preventDefault()
|
|
205
|
+
handleItemClick(item)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handle click outside to close
|
|
211
|
+
*/
|
|
212
|
+
function handleClickOutside(event: MouseEvent) {
|
|
213
|
+
if (fabRef && !fabRef.contains(event.target as Node)) {
|
|
214
|
+
close()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle backdrop click
|
|
220
|
+
*/
|
|
221
|
+
function handleBackdropClick() {
|
|
222
|
+
close()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create handlers object for custom snippets
|
|
227
|
+
*/
|
|
228
|
+
function createHandlers(item: {
|
|
229
|
+
proxy: ProxyItem
|
|
230
|
+
original: FloatingActionItem
|
|
231
|
+
}): FloatingActionItemHandlers {
|
|
232
|
+
return {
|
|
233
|
+
onclick: () => handleItemClick(item),
|
|
234
|
+
onkeydown: (event: KeyboardEvent) => handleItemKeyDown(event, item)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve which snippet to use for an item
|
|
240
|
+
*/
|
|
241
|
+
function resolveItemSnippet(proxy: ProxyItem): FloatingActionItemSnippet | null {
|
|
242
|
+
const snippetName = proxy.get('snippet')
|
|
243
|
+
if (snippetName) {
|
|
244
|
+
const namedSnippet = getSnippet(snippets, snippetName)
|
|
245
|
+
if (namedSnippet) {
|
|
246
|
+
return namedSnippet as FloatingActionItemSnippet
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return itemSnippet ?? null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Calculate item animation delay for stagger effect
|
|
254
|
+
*/
|
|
255
|
+
function getItemDelay(index: number): string {
|
|
256
|
+
return `${index * 50}ms`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Set up click outside listener when open
|
|
260
|
+
$effect(() => {
|
|
261
|
+
if (open) {
|
|
262
|
+
document.addEventListener('click', handleClickOutside, true)
|
|
263
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
264
|
+
// Focus first item after animation starts
|
|
265
|
+
requestAnimationFrame(() => {
|
|
266
|
+
if (flatItems.length > 0) {
|
|
267
|
+
focusItem(0)
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
return () => {
|
|
272
|
+
document.removeEventListener('click', handleClickOutside, true)
|
|
273
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
{#snippet defaultItem(
|
|
279
|
+
proxy: ProxyItem,
|
|
280
|
+
handlers: FloatingActionItemHandlers,
|
|
281
|
+
index: number,
|
|
282
|
+
total: number
|
|
283
|
+
)}
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
data-fab-item
|
|
287
|
+
data-fab-index={index}
|
|
288
|
+
data-disabled={proxy.disabled || undefined}
|
|
289
|
+
disabled={proxy.disabled || disabled}
|
|
290
|
+
aria-label={proxy.label}
|
|
291
|
+
style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
|
|
292
|
+
onclick={handlers.onclick}
|
|
293
|
+
onkeydown={handlers.onkeydown}
|
|
294
|
+
>
|
|
295
|
+
{#if proxy.get('icon')}
|
|
296
|
+
<span data-fab-item-icon class={proxy.get('icon')} aria-hidden="true"></span>
|
|
297
|
+
{/if}
|
|
298
|
+
{#if proxy.label}
|
|
299
|
+
<span data-fab-item-label>{proxy.label}</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</button>
|
|
302
|
+
{/snippet}
|
|
303
|
+
|
|
304
|
+
{#snippet renderItem(
|
|
305
|
+
item: { proxy: ProxyItem; original: FloatingActionItem },
|
|
306
|
+
index: number,
|
|
307
|
+
total: number
|
|
308
|
+
)}
|
|
309
|
+
{@const customSnippet = resolveItemSnippet(item.proxy)}
|
|
310
|
+
{@const handlers = createHandlers(item)}
|
|
311
|
+
{#if customSnippet}
|
|
312
|
+
<div
|
|
313
|
+
data-fab-item
|
|
314
|
+
data-fab-item-custom
|
|
315
|
+
data-fab-index={index}
|
|
316
|
+
data-disabled={item.proxy.disabled || undefined}
|
|
317
|
+
style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
|
|
318
|
+
>
|
|
319
|
+
<svelte:boundary>
|
|
320
|
+
{@render customSnippet(item.original, item.proxy.fields, handlers)}
|
|
321
|
+
{#snippet failed()}
|
|
322
|
+
{@render defaultItem(item.proxy, handlers, index, total)}
|
|
323
|
+
{/snippet}
|
|
324
|
+
</svelte:boundary>
|
|
325
|
+
</div>
|
|
326
|
+
{:else}
|
|
327
|
+
{@render defaultItem(item.proxy, handlers, index, total)}
|
|
328
|
+
{/if}
|
|
329
|
+
{/snippet}
|
|
330
|
+
|
|
331
|
+
<div
|
|
332
|
+
bind:this={fabRef}
|
|
333
|
+
data-fab
|
|
334
|
+
data-open={open || undefined}
|
|
335
|
+
data-size={size}
|
|
336
|
+
data-position={position}
|
|
337
|
+
data-expand={expand}
|
|
338
|
+
data-item-align={itemAlign}
|
|
339
|
+
data-disabled={disabled || undefined}
|
|
340
|
+
data-contained={contained || undefined}
|
|
341
|
+
class={className || undefined}
|
|
342
|
+
>
|
|
343
|
+
{#if backdrop && open}
|
|
344
|
+
<div data-fab-backdrop role="presentation" onclick={handleBackdropClick}></div>
|
|
345
|
+
{/if}
|
|
346
|
+
|
|
347
|
+
{#if open}
|
|
348
|
+
<div data-fab-menu role="menu" aria-label={label}>
|
|
349
|
+
{#each flatItems as item, index (index)}
|
|
350
|
+
{@render renderItem(item, index, flatItems.length)}
|
|
351
|
+
{/each}
|
|
352
|
+
</div>
|
|
353
|
+
{/if}
|
|
354
|
+
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
data-fab-trigger
|
|
358
|
+
{disabled}
|
|
359
|
+
aria-label={label}
|
|
360
|
+
aria-haspopup="menu"
|
|
361
|
+
aria-expanded={open}
|
|
362
|
+
onclick={toggle}
|
|
363
|
+
onkeydown={handleTriggerKeyDown}
|
|
364
|
+
>
|
|
365
|
+
<span data-fab-icon class={open ? icons.close : icons.add} aria-hidden="true"></span>
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|