@rokkit/ui 1.0.0-next.99 → 1.0.2
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/NavContent.svelte +42 -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/ResponsiveGrid.svelte +34 -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 +50 -0
- package/src/index.ts +61 -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,256 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
FloatingNavigationProps,
|
|
4
|
+
FloatingNavigationIcons
|
|
5
|
+
} from '../types/floating-navigation.js'
|
|
6
|
+
import { ProxyItem, messages } from '@rokkit/states'
|
|
7
|
+
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
items = [],
|
|
11
|
+
fields: userFields,
|
|
12
|
+
icons: userIcons = {} as FloatingNavigationIcons,
|
|
13
|
+
value = $bindable(),
|
|
14
|
+
position = 'right',
|
|
15
|
+
pinned = $bindable(false),
|
|
16
|
+
observe = true,
|
|
17
|
+
observerOptions = { rootMargin: '-20% 0px -70% 0px', threshold: 0 },
|
|
18
|
+
size = 'md',
|
|
19
|
+
label = messages.floatingNav.label,
|
|
20
|
+
labels: userLabels = {},
|
|
21
|
+
onselect,
|
|
22
|
+
onpinchange,
|
|
23
|
+
item: itemSnippet,
|
|
24
|
+
class: className = ''
|
|
25
|
+
}: FloatingNavigationProps & { labels?: Record<string, string> } = $props()
|
|
26
|
+
|
|
27
|
+
const labels = $derived({ ...messages.floatingNav, ...userLabels })
|
|
28
|
+
|
|
29
|
+
const icons = $derived({
|
|
30
|
+
pin: DEFAULT_STATE_ICONS.action.pin,
|
|
31
|
+
unpin: DEFAULT_STATE_ICONS.action.unpin,
|
|
32
|
+
...userIcons
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let navRef = $state<HTMLElement | null>(null)
|
|
36
|
+
let expanded = $state(false)
|
|
37
|
+
let focusedIndex = $state(-1)
|
|
38
|
+
|
|
39
|
+
const isVertical = $derived(position === 'left' || position === 'right')
|
|
40
|
+
|
|
41
|
+
const itemProxies = $derived(
|
|
42
|
+
items.map((item) => ({
|
|
43
|
+
proxy: new ProxyItem(item, userFields),
|
|
44
|
+
original: item
|
|
45
|
+
}))
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const activeIndex = $derived(itemProxies.findIndex((item) => item.proxy.value === value))
|
|
49
|
+
|
|
50
|
+
function togglePin() {
|
|
51
|
+
pinned = !pinned
|
|
52
|
+
if (!pinned) expanded = false
|
|
53
|
+
onpinchange?.(pinned)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleMouseEnter() {
|
|
57
|
+
if (!pinned) expanded = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleMouseLeave() {
|
|
61
|
+
if (!pinned) expanded = false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getItemHref(item: { proxy: ProxyItem; original: Record<string, unknown> }): string {
|
|
65
|
+
if (item.proxy.get('href') === undefined) return ''
|
|
66
|
+
return String(item.original[userFields?.href ?? 'href'] ?? '')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveTargetId(item: { proxy: ProxyItem; original: Record<string, unknown> }): string {
|
|
70
|
+
const href = getItemHref(item)
|
|
71
|
+
return href.startsWith('#') ? href.slice(1) : String(item.proxy.value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleItemClick(item: { proxy: ProxyItem; original: Record<string, unknown> }) {
|
|
75
|
+
value = item.proxy.value
|
|
76
|
+
onselect?.(item.proxy.value, item.original)
|
|
77
|
+
const el = document.getElementById(resolveTargetId(item))
|
|
78
|
+
el?.scrollIntoView({ behavior: 'smooth' })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function activateFocusedItem() {
|
|
82
|
+
if (focusedIndex >= 0 && focusedIndex < itemProxies.length) {
|
|
83
|
+
handleItemClick(itemProxies[focusedIndex])
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nextKey = $derived(isVertical ? 'ArrowDown' : 'ArrowRight')
|
|
88
|
+
const prevKey = $derived(isVertical ? 'ArrowUp' : 'ArrowLeft')
|
|
89
|
+
|
|
90
|
+
function wrapNext(current: number, last: number): number {
|
|
91
|
+
return current < last ? current + 1 : 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wrapPrev(current: number, last: number): number {
|
|
95
|
+
return current > 0 ? current - 1 : last
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getNextNavIndex(key: string, current: number, last: number): number {
|
|
99
|
+
if (key === nextKey) return wrapNext(current, last)
|
|
100
|
+
if (key === prevKey) return wrapPrev(current, last)
|
|
101
|
+
if (key === 'Home') return 0
|
|
102
|
+
if (key === 'End') return last
|
|
103
|
+
return -1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleNavMove(event: KeyboardEvent): boolean {
|
|
107
|
+
const last = itemProxies.length - 1
|
|
108
|
+
const next = getNextNavIndex(event.key, focusedIndex, last)
|
|
109
|
+
if (next === -1) return false
|
|
110
|
+
focusItem(next)
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isActivateKey(key: string): boolean {
|
|
115
|
+
return key === 'Enter' || key === ' '
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
119
|
+
if (isActivateKey(event.key)) {
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
activateFocusedItem()
|
|
122
|
+
} else if (event.key === 'Escape' && !pinned) {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
expanded = false
|
|
125
|
+
} else if (handleNavMove(event)) {
|
|
126
|
+
event.preventDefault()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function focusDomItem(index: number) {
|
|
131
|
+
const itemsContainer = navRef?.querySelector('[data-floating-nav-items]')
|
|
132
|
+
if (!itemsContainer) return
|
|
133
|
+
const navItems = itemsContainer.querySelectorAll('[data-floating-nav-item]')
|
|
134
|
+
const item = navItems[index] as HTMLElement | undefined
|
|
135
|
+
item?.focus()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function focusItem(index: number) {
|
|
139
|
+
if (index < 0 || index >= itemProxies.length) return
|
|
140
|
+
focusedIndex = index
|
|
141
|
+
focusDomItem(index)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function matchesEntryId(
|
|
145
|
+
item: { proxy: ProxyItem; original: Record<string, unknown> },
|
|
146
|
+
id: string
|
|
147
|
+
): boolean {
|
|
148
|
+
return resolveTargetId(item) === id
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleIntersection(entries: IntersectionObserverEntry[]) {
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (!entry.isIntersecting) continue
|
|
154
|
+
const match = itemProxies.find((item) => matchesEntryId(item, entry.target.id))
|
|
155
|
+
if (match) value = match.proxy.value
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// IntersectionObserver for active section tracking
|
|
160
|
+
$effect(() => {
|
|
161
|
+
if (!observe || itemProxies.length === 0) return
|
|
162
|
+
|
|
163
|
+
const observer = new IntersectionObserver(handleIntersection, observerOptions)
|
|
164
|
+
|
|
165
|
+
for (const item of itemProxies) {
|
|
166
|
+
const targetId = resolveTargetId(item)
|
|
167
|
+
const el = document.getElementById(targetId)
|
|
168
|
+
if (el) observer.observe(el)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return () => observer.disconnect()
|
|
172
|
+
})
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
176
|
+
<nav
|
|
177
|
+
bind:this={navRef}
|
|
178
|
+
data-floating-nav
|
|
179
|
+
data-position={position}
|
|
180
|
+
data-expanded={expanded || pinned || undefined}
|
|
181
|
+
data-pinned={pinned || undefined}
|
|
182
|
+
data-size={size}
|
|
183
|
+
aria-label={label}
|
|
184
|
+
class={className || undefined}
|
|
185
|
+
onmouseenter={handleMouseEnter}
|
|
186
|
+
onmouseleave={handleMouseLeave}
|
|
187
|
+
onkeydown={handleKeyDown}
|
|
188
|
+
>
|
|
189
|
+
<div data-floating-nav-header>
|
|
190
|
+
{#if expanded || pinned}
|
|
191
|
+
<span data-floating-nav-title>{label}</span>
|
|
192
|
+
{/if}
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
data-floating-nav-pin
|
|
196
|
+
aria-pressed={pinned}
|
|
197
|
+
aria-label={pinned ? labels.unpin : labels.pin}
|
|
198
|
+
onclick={togglePin}
|
|
199
|
+
>
|
|
200
|
+
<span data-floating-nav-pin-icon class={pinned ? icons.unpin : icons.pin} aria-hidden="true"
|
|
201
|
+
></span>
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div data-floating-nav-items>
|
|
206
|
+
{#each itemProxies as item, index (item.proxy.value ?? index)}
|
|
207
|
+
{@const isActive = item.proxy.value === value}
|
|
208
|
+
{@const isLink = item.proxy.get('href') !== undefined}
|
|
209
|
+
{#if itemSnippet}
|
|
210
|
+
{@render itemSnippet(item.original, {
|
|
211
|
+
text: item.proxy.label,
|
|
212
|
+
icon: item.proxy.get('icon'),
|
|
213
|
+
active: isActive
|
|
214
|
+
})}
|
|
215
|
+
{:else if isLink}
|
|
216
|
+
<a
|
|
217
|
+
data-floating-nav-item
|
|
218
|
+
data-active={isActive || undefined}
|
|
219
|
+
href={String(item.original[userFields?.href ?? 'href'] ?? '')}
|
|
220
|
+
aria-current={isActive ? 'true' : undefined}
|
|
221
|
+
tabindex={index === 0 ? 0 : -1}
|
|
222
|
+
style="--fn-index: {index}; --fn-total: {itemProxies.length}"
|
|
223
|
+
onclick={(e) => {
|
|
224
|
+
e.preventDefault()
|
|
225
|
+
handleItemClick(item)
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{#if item.proxy.get('icon')}
|
|
229
|
+
<span data-floating-nav-icon class={item.proxy.get('icon')} aria-hidden="true"></span>
|
|
230
|
+
{/if}
|
|
231
|
+
<span data-floating-nav-label>{item.proxy.label}</span>
|
|
232
|
+
</a>
|
|
233
|
+
{:else}
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
data-floating-nav-item
|
|
237
|
+
data-active={isActive || undefined}
|
|
238
|
+
aria-current={isActive ? 'true' : undefined}
|
|
239
|
+
tabindex={index === 0 ? 0 : -1}
|
|
240
|
+
style="--fn-index: {index}; --fn-total: {itemProxies.length}"
|
|
241
|
+
onclick={() => handleItemClick(item)}
|
|
242
|
+
>
|
|
243
|
+
{#if item.proxy.get('icon')}
|
|
244
|
+
<span data-floating-nav-icon class={item.proxy.get('icon')} aria-hidden="true"></span>
|
|
245
|
+
{/if}
|
|
246
|
+
<span data-floating-nav-label>{item.proxy.label}</span>
|
|
247
|
+
</button>
|
|
248
|
+
{/if}
|
|
249
|
+
{/each}
|
|
250
|
+
|
|
251
|
+
{#if activeIndex >= 0}
|
|
252
|
+
<span data-floating-nav-indicator style="--fn-active-index: {activeIndex}" aria-hidden="true"
|
|
253
|
+
></span>
|
|
254
|
+
{/if}
|
|
255
|
+
</div>
|
|
256
|
+
</nav>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Grid — Responsive tile grid with Wrapper + Navigator horizontal navigation.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Wrapper — owns focusedKey $state + flatView $derived
|
|
7
|
+
* Navigator — attaches DOM event handlers, calls wrapper[action](path)
|
|
8
|
+
* horizontal orientation (ArrowLeft/ArrowRight navigation)
|
|
9
|
+
* flatView loop — single flat {#each} rendering items as grid tiles
|
|
10
|
+
*
|
|
11
|
+
* Snippet customization:
|
|
12
|
+
* itemContent — replaces inner content of <button> for each tile
|
|
13
|
+
* [named] — per-item override via item.snippet = 'name'; falls back to itemContent
|
|
14
|
+
*
|
|
15
|
+
* Snippets receive (proxy) only — the <button> wrapper with data-path is
|
|
16
|
+
* always rendered by this component, so snippets never need to handle navigation.
|
|
17
|
+
*
|
|
18
|
+
* Data attributes on rendered elements:
|
|
19
|
+
* data-grid — root container
|
|
20
|
+
* data-grid-item — each tile button
|
|
21
|
+
* data-path — required by Navigator for click detection + scroll
|
|
22
|
+
* data-active — highlights current value match
|
|
23
|
+
* data-disabled — disabled state
|
|
24
|
+
* data-size — size variant
|
|
25
|
+
*
|
|
26
|
+
* CSS custom properties:
|
|
27
|
+
* --grid-min-size — minimum tile width for auto-fill
|
|
28
|
+
* --grid-gap — grid gap
|
|
29
|
+
*/
|
|
30
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
31
|
+
import { Wrapper, ProxyTree, messages } from '@rokkit/states'
|
|
32
|
+
import { Navigator } from '@rokkit/actions'
|
|
33
|
+
import { resolveSnippet, ITEM_SNIPPET } from '@rokkit/core'
|
|
34
|
+
import ItemContent from './ItemContent.svelte'
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
items = [],
|
|
38
|
+
fields = {},
|
|
39
|
+
value = $bindable(),
|
|
40
|
+
size = 'md',
|
|
41
|
+
disabled = false,
|
|
42
|
+
minSize = '120px',
|
|
43
|
+
gap = '1rem',
|
|
44
|
+
label = messages.grid.label,
|
|
45
|
+
onselect,
|
|
46
|
+
class: className = '',
|
|
47
|
+
...snippets
|
|
48
|
+
}: {
|
|
49
|
+
items?: unknown[]
|
|
50
|
+
fields?: Record<string, string>
|
|
51
|
+
value?: unknown
|
|
52
|
+
size?: string
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
minSize?: string
|
|
55
|
+
gap?: string
|
|
56
|
+
label?: string
|
|
57
|
+
onselect?: (value: unknown, proxy: ProxyItem) => void
|
|
58
|
+
class?: string
|
|
59
|
+
[key: string]: unknown
|
|
60
|
+
} = $props()
|
|
61
|
+
|
|
62
|
+
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const proxyTree = $derived(new ProxyTree(items, fields))
|
|
65
|
+
const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
|
|
66
|
+
|
|
67
|
+
// ─── Navigator (horizontal) ───────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
let gridRef = $state<HTMLElement | null>(null)
|
|
70
|
+
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (!gridRef || disabled) return
|
|
73
|
+
const dir = getComputedStyle(gridRef).direction || 'ltr'
|
|
74
|
+
const nav = new Navigator(gridRef, wrapper, { orientation: 'horizontal', dir })
|
|
75
|
+
return () => nav.destroy()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ─── Sync external value → focused key ────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
$effect(() => {
|
|
81
|
+
wrapper.moveToValue(value)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ─── Selection handler ─────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
|
|
87
|
+
if (proxy.disabled || disabled) return
|
|
88
|
+
value = extractedValue
|
|
89
|
+
onselect?.(extractedValue, proxy)
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<div
|
|
94
|
+
bind:this={gridRef}
|
|
95
|
+
data-grid
|
|
96
|
+
data-grid-min-size={minSize}
|
|
97
|
+
data-size={size}
|
|
98
|
+
data-disabled={disabled || undefined}
|
|
99
|
+
class={className || undefined}
|
|
100
|
+
role="grid"
|
|
101
|
+
aria-label={label}
|
|
102
|
+
aria-disabled={disabled || undefined}
|
|
103
|
+
style:--grid-min-size={minSize}
|
|
104
|
+
style:--grid-gap={gap}
|
|
105
|
+
>
|
|
106
|
+
{#each wrapper.flatView as node (node.key)}
|
|
107
|
+
{@const proxy = node.proxy}
|
|
108
|
+
{@const sel = proxy.value === value}
|
|
109
|
+
{@const content = resolveSnippet(snippets as Record<string, unknown>, proxy, ITEM_SNIPPET)}
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
data-grid-item
|
|
113
|
+
data-path={node.key}
|
|
114
|
+
data-active={sel || undefined}
|
|
115
|
+
data-disabled={proxy.disabled || undefined}
|
|
116
|
+
role="gridcell"
|
|
117
|
+
aria-selected={sel}
|
|
118
|
+
aria-label={proxy.label}
|
|
119
|
+
disabled={proxy.disabled || disabled}
|
|
120
|
+
>
|
|
121
|
+
{#if content}
|
|
122
|
+
{@render content(proxy)}
|
|
123
|
+
{:else}
|
|
124
|
+
<ItemContent {proxy} />
|
|
125
|
+
{/if}
|
|
126
|
+
</button>
|
|
127
|
+
{/each}
|
|
128
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const { proxy, showIcon = true, showSubtext = true } = $props()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
{#if showIcon}
|
|
6
|
+
{#if proxy.get('avatar')}
|
|
7
|
+
<img data-item-avatar src={proxy.get('avatar')} alt={proxy.get('tooltip') ?? proxy.label} />
|
|
8
|
+
{:else if proxy.get('icon')}
|
|
9
|
+
<span data-item-icon class={proxy.get('icon')} aria-hidden="true"></span>
|
|
10
|
+
{/if}
|
|
11
|
+
{/if}
|
|
12
|
+
{#if proxy.label || (showSubtext && proxy.get('subtext'))}
|
|
13
|
+
<span data-item-text>
|
|
14
|
+
<span data-item-label>{proxy.label}</span>
|
|
15
|
+
{#if showSubtext && proxy.get('subtext')}
|
|
16
|
+
<span data-item-description>{proxy.get('subtext')}</span>
|
|
17
|
+
{/if}
|
|
18
|
+
</span>
|
|
19
|
+
{/if}
|
|
20
|
+
{#if proxy.get('badge')}
|
|
21
|
+
<span data-item-badge>{proxy.get('badge')}</span>
|
|
22
|
+
{/if}
|
|
23
|
+
{#if proxy.get('shortcut')}
|
|
24
|
+
<kbd data-item-shortcut>{proxy.get('shortcut')}</kbd>
|
|
25
|
+
{/if}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
/**
|
|
4
|
+
* LazyTree — Tree + lazy loading.
|
|
5
|
+
*
|
|
6
|
+
* Same as Tree but uses LazyProxyItem via createProxy factory
|
|
7
|
+
* so nodes with children: true fetch on expand.
|
|
8
|
+
*
|
|
9
|
+
* Default item content: icon + label + badge + shortcut (no avatar/description).
|
|
10
|
+
* Parent nodes hide their icon by default.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
13
|
+
import { LazyWrapper, LazyProxyItem, ProxyTree, messages } from '@rokkit/states'
|
|
14
|
+
import { Navigator } from '@rokkit/actions'
|
|
15
|
+
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET } from '@rokkit/core'
|
|
16
|
+
import ItemContent from './ItemContent.svelte'
|
|
17
|
+
import Connector from './Connector.svelte'
|
|
18
|
+
import Button from './Button.svelte'
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
items = [],
|
|
22
|
+
fields = {},
|
|
23
|
+
value,
|
|
24
|
+
size = 'md',
|
|
25
|
+
lineStyle = 'solid',
|
|
26
|
+
labels: userLabels = {},
|
|
27
|
+
icons: userIcons = {},
|
|
28
|
+
onselect,
|
|
29
|
+
onlazyload,
|
|
30
|
+
hasMore = false,
|
|
31
|
+
class: className = '',
|
|
32
|
+
...snippets
|
|
33
|
+
}: {
|
|
34
|
+
items?: unknown[]
|
|
35
|
+
fields?: Record<string, string>
|
|
36
|
+
value?: unknown
|
|
37
|
+
size?: string
|
|
38
|
+
lineStyle?: 'none' | 'solid' | 'dashed' | 'dotted'
|
|
39
|
+
labels?: Record<string, string>
|
|
40
|
+
icons?: { opened?: string; closed?: string }
|
|
41
|
+
onselect?: (value: unknown, proxy: ProxyItem) => void
|
|
42
|
+
onlazyload?: (current?: unknown) => Promise<unknown[]>
|
|
43
|
+
hasMore?: boolean
|
|
44
|
+
class?: string
|
|
45
|
+
[key: string]: unknown
|
|
46
|
+
} = $props()
|
|
47
|
+
|
|
48
|
+
const labels = $derived({ ...messages.tree, ...userLabels })
|
|
49
|
+
|
|
50
|
+
const icons = $derived({ ...DEFAULT_STATE_ICONS.folder, ...userIcons })
|
|
51
|
+
|
|
52
|
+
const proxyTree = $derived(
|
|
53
|
+
new ProxyTree(items, fields, {
|
|
54
|
+
createProxy: (raw, f, key, level) =>
|
|
55
|
+
new LazyProxyItem(
|
|
56
|
+
raw,
|
|
57
|
+
f,
|
|
58
|
+
key,
|
|
59
|
+
level,
|
|
60
|
+
onlazyload ? (_value, rawItem) => onlazyload(rawItem) : null
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
const wrapper = $derived(new LazyWrapper(proxyTree, { onselect, onlazyload }))
|
|
65
|
+
|
|
66
|
+
let treeRef = $state<HTMLElement | null>(null)
|
|
67
|
+
|
|
68
|
+
$effect(() => {
|
|
69
|
+
if (!treeRef) return
|
|
70
|
+
const dir = getComputedStyle(treeRef).direction || 'ltr'
|
|
71
|
+
const nav = new Navigator(treeRef, wrapper, { collapsible: true, dir })
|
|
72
|
+
return () => nav.destroy()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
$effect(() => {
|
|
76
|
+
wrapper.moveToValue(value)
|
|
77
|
+
})
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
bind:this={treeRef}
|
|
82
|
+
data-tree
|
|
83
|
+
data-size={size}
|
|
84
|
+
data-line-style={lineStyle}
|
|
85
|
+
class={className || undefined}
|
|
86
|
+
role="tree"
|
|
87
|
+
tabindex="0"
|
|
88
|
+
aria-label={labels.label}
|
|
89
|
+
>
|
|
90
|
+
{#each wrapper.flatView as node (node.key)}
|
|
91
|
+
{@const proxy = node.proxy}
|
|
92
|
+
{@const isActive = proxy.value === value}
|
|
93
|
+
{@const isLoading = proxy.loading}
|
|
94
|
+
{@const content = resolveSnippet(snippets, proxy, ITEM_SNIPPET)}
|
|
95
|
+
|
|
96
|
+
<div
|
|
97
|
+
data-tree-node
|
|
98
|
+
data-tree-path={node.key}
|
|
99
|
+
data-tree-level={node.level - 1}
|
|
100
|
+
data-tree-has-children={node.isExpandable || undefined}
|
|
101
|
+
data-tree-loading={isLoading || undefined}
|
|
102
|
+
data-active={isActive || undefined}
|
|
103
|
+
role="treeitem"
|
|
104
|
+
aria-expanded={node.isExpandable ? proxy.expanded : undefined}
|
|
105
|
+
aria-selected={isActive}
|
|
106
|
+
aria-busy={isLoading || undefined}
|
|
107
|
+
aria-level={node.level}
|
|
108
|
+
>
|
|
109
|
+
<div data-tree-node-row>
|
|
110
|
+
{#each node.lineTypes as lineType, lineIndex (lineIndex)}
|
|
111
|
+
{#if lineType === 'icon'}
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
data-tree-toggle-btn
|
|
115
|
+
onclick={() => wrapper.toggle(node.key)}
|
|
116
|
+
aria-label={isLoading
|
|
117
|
+
? labels.loading
|
|
118
|
+
: proxy.expanded
|
|
119
|
+
? labels.collapse
|
|
120
|
+
: labels.expand}
|
|
121
|
+
tabindex={-1}
|
|
122
|
+
>
|
|
123
|
+
{#if isLoading}
|
|
124
|
+
<span data-tree-spinner aria-hidden="true"></span>
|
|
125
|
+
{:else}
|
|
126
|
+
<span class={proxy.expanded ? icons.opened : icons.closed} aria-hidden="true"
|
|
127
|
+
></span>
|
|
128
|
+
{/if}
|
|
129
|
+
</button>
|
|
130
|
+
{:else}
|
|
131
|
+
<Connector type={lineType} />
|
|
132
|
+
{/if}
|
|
133
|
+
{/each}
|
|
134
|
+
|
|
135
|
+
{#if proxy.get('href')}
|
|
136
|
+
<a
|
|
137
|
+
href={proxy.get('href')}
|
|
138
|
+
data-tree-item-content
|
|
139
|
+
data-path={node.key}
|
|
140
|
+
data-active={isActive || undefined}
|
|
141
|
+
aria-label={proxy.label}
|
|
142
|
+
aria-current={isActive ? 'page' : undefined}
|
|
143
|
+
>
|
|
144
|
+
{#if content}
|
|
145
|
+
{@render content(proxy)}
|
|
146
|
+
{:else}
|
|
147
|
+
<ItemContent {proxy} showIcon={!node.isExpandable} showSubtext={false} />
|
|
148
|
+
{/if}
|
|
149
|
+
</a>
|
|
150
|
+
{:else}
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
data-tree-item-content
|
|
154
|
+
data-path={node.key}
|
|
155
|
+
data-active={isActive || undefined}
|
|
156
|
+
aria-label={proxy.label}
|
|
157
|
+
>
|
|
158
|
+
{#if content}
|
|
159
|
+
{@render content(proxy)}
|
|
160
|
+
{:else}
|
|
161
|
+
<ItemContent {proxy} showIcon={!node.isExpandable} showSubtext={false} />
|
|
162
|
+
{/if}
|
|
163
|
+
</button>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{/each}
|
|
168
|
+
{#if hasMore}
|
|
169
|
+
<Button label={labels.loadMore} style="ghost" {size} onclick={() => wrapper.loadMore()} />
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|