@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,613 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
TreeProps,
|
|
4
|
+
TreeItem,
|
|
5
|
+
TreeItemSnippet,
|
|
6
|
+
TreeItemHandlers,
|
|
7
|
+
TreeFields,
|
|
8
|
+
TreeLineType,
|
|
9
|
+
TreeStateIcons
|
|
10
|
+
} from '../types/tree.js'
|
|
11
|
+
import {
|
|
12
|
+
defaultTreeFields,
|
|
13
|
+
defaultTreeStateIcons,
|
|
14
|
+
getLineTypes,
|
|
15
|
+
getNodeKey,
|
|
16
|
+
getSnippet
|
|
17
|
+
} from '../types/tree.js'
|
|
18
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
19
|
+
import Connector from './Connector.svelte'
|
|
20
|
+
import ItemContent from './ItemContent.svelte'
|
|
21
|
+
import { NestedController } from '@rokkit/states'
|
|
22
|
+
import { navigator } from '@rokkit/actions'
|
|
23
|
+
import { untrack } from 'svelte'
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
items = [],
|
|
27
|
+
fields: userFields,
|
|
28
|
+
value = $bindable(),
|
|
29
|
+
size = 'md',
|
|
30
|
+
showLines = true,
|
|
31
|
+
multiselect = false,
|
|
32
|
+
expanded = $bindable({}),
|
|
33
|
+
selected = $bindable([]),
|
|
34
|
+
expandAll = false,
|
|
35
|
+
active,
|
|
36
|
+
icons: userIcons,
|
|
37
|
+
onselect,
|
|
38
|
+
onselectedchange,
|
|
39
|
+
onexpandedchange,
|
|
40
|
+
ontoggle,
|
|
41
|
+
onloadchildren,
|
|
42
|
+
class: className = '',
|
|
43
|
+
item: itemSnippet,
|
|
44
|
+
toggle: toggleSnippet,
|
|
45
|
+
connector: connectorSnippet,
|
|
46
|
+
...snippets
|
|
47
|
+
}: TreeProps & { [key: string]: TreeItemSnippet | unknown } = $props()
|
|
48
|
+
|
|
49
|
+
// Merge fields with defaults
|
|
50
|
+
const fields = $derived<TreeFields>({ ...defaultTreeFields, ...userFields })
|
|
51
|
+
const icons = $derived<TreeStateIcons>({ ...defaultTreeStateIcons, ...userIcons })
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create an ItemProxy for the given item
|
|
55
|
+
*/
|
|
56
|
+
function createProxy(item: TreeItem): ItemProxy {
|
|
57
|
+
return new ItemProxy(item, fields)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── NestedController for keyboard navigation ───────────────────
|
|
61
|
+
|
|
62
|
+
let controller = untrack(() => new NestedController(items, value, userFields, { multiselect }))
|
|
63
|
+
let treeRef = $state<HTMLElement | null>(null)
|
|
64
|
+
let loadingPaths = $state(new Set<string>())
|
|
65
|
+
let loadVersion = $state(0)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get expanded state for a node key from the expanded prop.
|
|
69
|
+
* Default to expandAll setting when not explicitly set.
|
|
70
|
+
*/
|
|
71
|
+
function getExpandedState(nodeKey: string): boolean {
|
|
72
|
+
const externalKeys = Object.keys(expanded)
|
|
73
|
+
if (externalKeys.length > 0) {
|
|
74
|
+
if (nodeKey in expanded) return expanded[nodeKey]
|
|
75
|
+
}
|
|
76
|
+
return expandAll
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sync expansion state: expanded prop + expandAll → controller.expandedKeys
|
|
81
|
+
*/
|
|
82
|
+
function syncExpandedToController() {
|
|
83
|
+
for (const [key, proxy] of controller.lookup.entries()) {
|
|
84
|
+
if (!proxy.hasChildren) continue
|
|
85
|
+
const itemProxy = createProxy(proxy.value)
|
|
86
|
+
const nodeKey = getNodeKey(itemProxy)
|
|
87
|
+
const shouldExpand = getExpandedState(nodeKey)
|
|
88
|
+
if (shouldExpand) {
|
|
89
|
+
controller.expandedKeys.add(key)
|
|
90
|
+
} else {
|
|
91
|
+
controller.expandedKeys.delete(key)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sync on init
|
|
97
|
+
syncExpandedToController()
|
|
98
|
+
|
|
99
|
+
$effect(() => {
|
|
100
|
+
controller.update(items)
|
|
101
|
+
syncExpandedToController()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Sync expanded prop / expandAll changes → controller
|
|
105
|
+
$effect(() => {
|
|
106
|
+
void expanded
|
|
107
|
+
void expandAll
|
|
108
|
+
syncExpandedToController()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Derive expanded prop from controller.expandedKeys (pathKey → nodeKey mapping)
|
|
113
|
+
*/
|
|
114
|
+
function deriveExpandedFromController(): Record<string, boolean> {
|
|
115
|
+
const result: Record<string, boolean> = {}
|
|
116
|
+
function walk(treeItems: TreeItem[], pathPrefix: string) {
|
|
117
|
+
treeItems.forEach((item, index) => {
|
|
118
|
+
const proxy = createProxy(item)
|
|
119
|
+
if (!proxy.hasChildren) return
|
|
120
|
+
const pathKey = pathPrefix ? `${pathPrefix}-${index}` : String(index)
|
|
121
|
+
const nodeKey = getNodeKey(proxy)
|
|
122
|
+
result[nodeKey] = controller.expandedKeys.has(pathKey)
|
|
123
|
+
if (controller.expandedKeys.has(pathKey)) {
|
|
124
|
+
walk(proxy.children as TreeItem[], pathKey)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
walk(items, '')
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Focus the element matching controller.focusedKey on navigator action events
|
|
133
|
+
$effect(() => {
|
|
134
|
+
if (!treeRef) return
|
|
135
|
+
const el = treeRef
|
|
136
|
+
|
|
137
|
+
function onAction(event: Event) {
|
|
138
|
+
const detail = (event as CustomEvent).detail
|
|
139
|
+
|
|
140
|
+
if (detail.name === 'move') {
|
|
141
|
+
const key = controller.focusedKey
|
|
142
|
+
if (key) {
|
|
143
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
144
|
+
if (target && target !== document.activeElement) {
|
|
145
|
+
target.focus()
|
|
146
|
+
target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (detail.name === 'select') {
|
|
152
|
+
handleSelectAction()
|
|
153
|
+
syncSelectedFromController()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (detail.name === 'toggle') {
|
|
157
|
+
// Controller already toggled expandedKeys. Derive the expanded prop.
|
|
158
|
+
handleToggleAction()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
el.addEventListener('action', onAction)
|
|
163
|
+
return () => el.removeEventListener('action', onAction)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handle the navigator's select action (Enter/Space or click on data-path item)
|
|
168
|
+
*/
|
|
169
|
+
function handleSelectAction() {
|
|
170
|
+
const key = controller.focusedKey
|
|
171
|
+
if (!key) return
|
|
172
|
+
|
|
173
|
+
const proxy = controller.lookup.get(key)
|
|
174
|
+
if (!proxy) return
|
|
175
|
+
|
|
176
|
+
const itemProxy = createProxy(proxy.value)
|
|
177
|
+
const href = itemProxy.get<string>('href')
|
|
178
|
+
if (!href) {
|
|
179
|
+
onselect?.(itemProxy.itemValue, proxy.value as TreeItem)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle the navigator's toggle action (ArrowLeft collapse / ArrowRight expand)
|
|
185
|
+
*/
|
|
186
|
+
function handleToggleAction() {
|
|
187
|
+
const key = controller.focusedKey
|
|
188
|
+
if (!key) return
|
|
189
|
+
|
|
190
|
+
const proxy = controller.lookup.get(key)
|
|
191
|
+
if (!proxy) return
|
|
192
|
+
|
|
193
|
+
const itemProxy = createProxy(proxy.value)
|
|
194
|
+
const isExpanded = controller.expandedKeys.has(key)
|
|
195
|
+
const newExpanded = deriveExpandedFromController()
|
|
196
|
+
expanded = newExpanded
|
|
197
|
+
onexpandedchange?.(newExpanded)
|
|
198
|
+
ontoggle?.(itemProxy.itemValue, proxy.value as TreeItem, isExpanded)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Lazy loading helpers ──────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load children for a lazy node (children: true → children: [...]).
|
|
205
|
+
* Returns true if children were loaded successfully.
|
|
206
|
+
*/
|
|
207
|
+
async function loadLazyChildren(pathKey: string): Promise<boolean> {
|
|
208
|
+
const proxy = controller.lookup.get(pathKey)
|
|
209
|
+
if (!proxy) return false
|
|
210
|
+
const itemProxy = createProxy(proxy.value)
|
|
211
|
+
if (!itemProxy.canLoadChildren || !onloadchildren) return false
|
|
212
|
+
|
|
213
|
+
loadingPaths = new Set([...loadingPaths, pathKey])
|
|
214
|
+
try {
|
|
215
|
+
const children = await onloadchildren(itemProxy.itemValue, proxy.value as TreeItem)
|
|
216
|
+
const childrenField = fields.children ?? 'children'
|
|
217
|
+
;(proxy.value as Record<string, unknown>)[childrenField] = children
|
|
218
|
+
controller.update(items)
|
|
219
|
+
syncExpandedToController()
|
|
220
|
+
loadVersion++
|
|
221
|
+
} catch {
|
|
222
|
+
loadingPaths = new Set([...loadingPaths].filter((p) => p !== pathKey))
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
loadingPaths = new Set([...loadingPaths].filter((p) => p !== pathKey))
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Toggle expansion of a node by its path key (for toggle button clicks)
|
|
231
|
+
*/
|
|
232
|
+
async function toggleNodeByKey(pathKey: string) {
|
|
233
|
+
// Collapsing — toggle normally
|
|
234
|
+
if (controller.expandedKeys.has(pathKey)) {
|
|
235
|
+
controller.toggleExpansion(pathKey)
|
|
236
|
+
const proxy = controller.lookup.get(pathKey)
|
|
237
|
+
if (!proxy) return
|
|
238
|
+
const itemProxy = createProxy(proxy.value)
|
|
239
|
+
const newExpanded = deriveExpandedFromController()
|
|
240
|
+
expanded = newExpanded
|
|
241
|
+
onexpandedchange?.(newExpanded)
|
|
242
|
+
ontoggle?.(itemProxy.itemValue, proxy.value as TreeItem, false)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Expanding — check if lazy load is needed
|
|
247
|
+
const proxy = controller.lookup.get(pathKey)
|
|
248
|
+
let lazyLoaded = false
|
|
249
|
+
if (proxy) {
|
|
250
|
+
const itemProxy = createProxy(proxy.value)
|
|
251
|
+
if (itemProxy.canLoadChildren && onloadchildren) {
|
|
252
|
+
const loaded = await loadLazyChildren(pathKey)
|
|
253
|
+
if (!loaded) return // Error — stay collapsed
|
|
254
|
+
lazyLoaded = true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// After lazy load, syncExpandedToController may have already expanded the node
|
|
259
|
+
// (e.g. when expandAll=true). Only toggle if not already expanded.
|
|
260
|
+
if (!lazyLoaded || !controller.expandedKeys.has(pathKey)) {
|
|
261
|
+
controller.toggleExpansion(pathKey)
|
|
262
|
+
}
|
|
263
|
+
const updatedProxy = controller.lookup.get(pathKey)
|
|
264
|
+
if (!updatedProxy) return
|
|
265
|
+
const updatedItemProxy = createProxy(updatedProxy.value)
|
|
266
|
+
const isExpanded = controller.expandedKeys.has(pathKey)
|
|
267
|
+
const newExpanded = deriveExpandedFromController()
|
|
268
|
+
expanded = newExpanded
|
|
269
|
+
onexpandedchange?.(newExpanded)
|
|
270
|
+
ontoggle?.(updatedItemProxy.itemValue, updatedProxy.value as TreeItem, isExpanded)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sync DOM focus to controller state
|
|
275
|
+
*/
|
|
276
|
+
function handleFocusIn(event: FocusEvent) {
|
|
277
|
+
const target = event.target as HTMLElement
|
|
278
|
+
if (!target) return
|
|
279
|
+
const path = target.dataset.path
|
|
280
|
+
if (path !== undefined) {
|
|
281
|
+
controller.moveTo(path)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Handle keyboard events the navigator doesn't cover:
|
|
287
|
+
* - Enter/Space on link items: let native <a> behavior through
|
|
288
|
+
* - ArrowRight on lazy nodes: trigger async load before expand
|
|
289
|
+
*/
|
|
290
|
+
function handleTreeKeyDown(event: KeyboardEvent) {
|
|
291
|
+
// ArrowRight on lazy node: intercept before navigator
|
|
292
|
+
if (event.key === 'ArrowRight') {
|
|
293
|
+
const key = controller.focusedKey
|
|
294
|
+
if (!key) return
|
|
295
|
+
const proxy = controller.lookup.get(key)
|
|
296
|
+
if (!proxy) return
|
|
297
|
+
const itemProxy = createProxy(proxy.value)
|
|
298
|
+
if (itemProxy.canLoadChildren && onloadchildren) {
|
|
299
|
+
event.preventDefault()
|
|
300
|
+
event.stopPropagation()
|
|
301
|
+
loadLazyChildren(key).then((loaded) => {
|
|
302
|
+
if (loaded) {
|
|
303
|
+
controller.expand(key)
|
|
304
|
+
handleToggleAction()
|
|
305
|
+
const target = treeRef?.querySelector(`[data-path="${key}"]`) as HTMLElement
|
|
306
|
+
target?.focus()
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
314
|
+
|
|
315
|
+
const key = controller.focusedKey
|
|
316
|
+
if (!key) return
|
|
317
|
+
|
|
318
|
+
const proxy = controller.lookup.get(key)
|
|
319
|
+
if (!proxy) return
|
|
320
|
+
|
|
321
|
+
const itemProxy = createProxy(proxy.value)
|
|
322
|
+
const href = itemProxy.get<string>('href')
|
|
323
|
+
if (href) {
|
|
324
|
+
event.stopPropagation()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Multi-selection helpers ────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Sync the selected bindable prop from controller.selected
|
|
332
|
+
*/
|
|
333
|
+
function syncSelectedFromController() {
|
|
334
|
+
if (!multiselect) return
|
|
335
|
+
selected = [...controller.selected]
|
|
336
|
+
onselectedchange?.(selected)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if an item is in the current selection (for data-selected attribute)
|
|
341
|
+
*/
|
|
342
|
+
function isItemSelected(pathKey: string): boolean {
|
|
343
|
+
if (!multiselect) return false
|
|
344
|
+
return controller.selectedKeys.has(pathKey)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── Check active ──────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if an item is currently active/selected
|
|
351
|
+
*/
|
|
352
|
+
function checkIsActive(proxy: ItemProxy): boolean {
|
|
353
|
+
if (active !== undefined) {
|
|
354
|
+
return proxy.itemValue === active
|
|
355
|
+
}
|
|
356
|
+
return value !== undefined && proxy.itemValue === value
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Handle item click (for button items)
|
|
361
|
+
*/
|
|
362
|
+
function handleItemClick(proxy: ItemProxy) {
|
|
363
|
+
value = proxy.itemValue
|
|
364
|
+
onselect?.(proxy.itemValue, proxy.original as TreeItem)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create handlers object for custom snippets
|
|
369
|
+
*/
|
|
370
|
+
function createHandlers(proxy: ItemProxy, pathKey: string): TreeItemHandlers {
|
|
371
|
+
return {
|
|
372
|
+
onclick: () => handleItemClick(proxy),
|
|
373
|
+
ontoggle: () => toggleNodeByKey(pathKey),
|
|
374
|
+
onkeydown: (event: KeyboardEvent) => {
|
|
375
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
376
|
+
event.preventDefault()
|
|
377
|
+
event.stopPropagation()
|
|
378
|
+
handleItemClick(proxy)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ==========================================================================
|
|
385
|
+
// Flattened tree structure for rendering
|
|
386
|
+
// ==========================================================================
|
|
387
|
+
|
|
388
|
+
interface FlatNode {
|
|
389
|
+
proxy: ItemProxy
|
|
390
|
+
level: number
|
|
391
|
+
lineTypes: TreeLineType[]
|
|
392
|
+
path: string
|
|
393
|
+
isLast: boolean
|
|
394
|
+
isExpandable: boolean
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Flatten the tree into a list of visible nodes with their line types.
|
|
399
|
+
* Reads expansion state from controller.expandedKeys.
|
|
400
|
+
*/
|
|
401
|
+
function flattenTree(
|
|
402
|
+
treeItems: TreeItem[],
|
|
403
|
+
level: number = 0,
|
|
404
|
+
types: TreeLineType[] = [],
|
|
405
|
+
pathPrefix: string = ''
|
|
406
|
+
): FlatNode[] {
|
|
407
|
+
const result: FlatNode[] = []
|
|
408
|
+
|
|
409
|
+
treeItems.forEach((item, index) => {
|
|
410
|
+
const proxy = createProxy(item)
|
|
411
|
+
const isLast = index === treeItems.length - 1
|
|
412
|
+
const nodeType: 'child' | 'last' = isLast ? 'last' : 'child'
|
|
413
|
+
const path = pathPrefix ? `${pathPrefix}-${index}` : String(index)
|
|
414
|
+
const isExpandable = proxy.hasChildren || proxy.canLoadChildren
|
|
415
|
+
|
|
416
|
+
const connectors = getLineTypes(isExpandable, types, nodeType)
|
|
417
|
+
|
|
418
|
+
result.push({
|
|
419
|
+
proxy,
|
|
420
|
+
level,
|
|
421
|
+
lineTypes: connectors,
|
|
422
|
+
path,
|
|
423
|
+
isLast,
|
|
424
|
+
isExpandable
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// If expanded (read from controller), recurse into children
|
|
428
|
+
if (proxy.hasChildren && controller.expandedKeys.has(path)) {
|
|
429
|
+
const childNodes = flattenTree(proxy.children as TreeItem[], level + 1, connectors, path)
|
|
430
|
+
result.push(...childNodes)
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
return result
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
const flatNodes = $derived((void loadVersion, flattenTree(items)))
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolve which snippet to use for an item
|
|
442
|
+
*/
|
|
443
|
+
function resolveItemSnippet(proxy: ItemProxy): TreeItemSnippet | null {
|
|
444
|
+
const snippetName = proxy.snippetName
|
|
445
|
+
if (snippetName) {
|
|
446
|
+
const namedSnippet = getSnippet(snippets, snippetName)
|
|
447
|
+
if (namedSnippet) {
|
|
448
|
+
return namedSnippet as TreeItemSnippet
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return itemSnippet ?? null
|
|
452
|
+
}
|
|
453
|
+
</script>
|
|
454
|
+
|
|
455
|
+
{#snippet defaultToggle(isExpanded: boolean, hasChildren: boolean, stateIcons: TreeStateIcons)}
|
|
456
|
+
<span data-tree-toggle data-tree-has-children={hasChildren || undefined}>
|
|
457
|
+
{#if hasChildren}
|
|
458
|
+
{#if isExpanded}
|
|
459
|
+
<span class={stateIcons.opened} aria-hidden="true"></span>
|
|
460
|
+
{:else}
|
|
461
|
+
<span class={stateIcons.closed} aria-hidden="true"></span>
|
|
462
|
+
{/if}
|
|
463
|
+
{/if}
|
|
464
|
+
</span>
|
|
465
|
+
{/snippet}
|
|
466
|
+
|
|
467
|
+
{#snippet defaultItem(
|
|
468
|
+
proxy: ItemProxy,
|
|
469
|
+
handlers: TreeItemHandlers,
|
|
470
|
+
isActive: boolean,
|
|
471
|
+
_isExpanded: boolean,
|
|
472
|
+
_level: number,
|
|
473
|
+
_path: string
|
|
474
|
+
)}
|
|
475
|
+
{@const href = proxy.get<string>('href')}
|
|
476
|
+
{#if href}
|
|
477
|
+
<a
|
|
478
|
+
{href}
|
|
479
|
+
data-tree-item-content
|
|
480
|
+
data-path={_path}
|
|
481
|
+
data-active={isActive || undefined}
|
|
482
|
+
aria-label={proxy.label}
|
|
483
|
+
aria-current={isActive ? 'page' : undefined}
|
|
484
|
+
>
|
|
485
|
+
<ItemContent {proxy} />
|
|
486
|
+
</a>
|
|
487
|
+
{:else}
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
data-tree-item-content
|
|
491
|
+
data-path={_path}
|
|
492
|
+
data-active={isActive || undefined}
|
|
493
|
+
aria-label={proxy.label}
|
|
494
|
+
aria-pressed={isActive}
|
|
495
|
+
onkeydown={handlers.onkeydown}
|
|
496
|
+
>
|
|
497
|
+
<ItemContent {proxy} />
|
|
498
|
+
</button>
|
|
499
|
+
{/if}
|
|
500
|
+
{/snippet}
|
|
501
|
+
|
|
502
|
+
{#snippet renderNode(node: FlatNode)}
|
|
503
|
+
{@const proxy = node.proxy}
|
|
504
|
+
{@const isActive = checkIsActive(proxy)}
|
|
505
|
+
{@const nodeSelected = isItemSelected(node.path)}
|
|
506
|
+
{@const isExpanded = controller.expandedKeys.has(node.path)}
|
|
507
|
+
{@const isLoading = loadingPaths.has(node.path)}
|
|
508
|
+
{@const handlers = createHandlers(proxy, node.path)}
|
|
509
|
+
{@const customSnippet = resolveItemSnippet(proxy)}
|
|
510
|
+
|
|
511
|
+
<div
|
|
512
|
+
data-tree-node
|
|
513
|
+
data-tree-path={node.path}
|
|
514
|
+
data-tree-level={node.level}
|
|
515
|
+
data-tree-expanded={isExpanded || undefined}
|
|
516
|
+
data-tree-has-children={node.isExpandable || undefined}
|
|
517
|
+
data-tree-loading={isLoading || undefined}
|
|
518
|
+
data-active={isActive || undefined}
|
|
519
|
+
data-selected={nodeSelected || undefined}
|
|
520
|
+
role="treeitem"
|
|
521
|
+
aria-expanded={node.isExpandable ? isExpanded : undefined}
|
|
522
|
+
aria-selected={multiselect ? nodeSelected : isActive}
|
|
523
|
+
aria-busy={isLoading || undefined}
|
|
524
|
+
aria-level={node.level + 1}
|
|
525
|
+
>
|
|
526
|
+
<div data-tree-node-row>
|
|
527
|
+
<!-- Tree lines/connectors -->
|
|
528
|
+
{#if showLines}
|
|
529
|
+
{#each node.lineTypes as lineType, lineIndex (lineIndex)}
|
|
530
|
+
{#if lineType === 'icon'}
|
|
531
|
+
<!-- Render toggle icon instead of connector -->
|
|
532
|
+
<button
|
|
533
|
+
type="button"
|
|
534
|
+
data-tree-toggle-btn
|
|
535
|
+
onclick={() => toggleNodeByKey(node.path)}
|
|
536
|
+
aria-label={isLoading ? 'Loading' : isExpanded ? 'Collapse' : 'Expand'}
|
|
537
|
+
tabindex={-1}
|
|
538
|
+
>
|
|
539
|
+
{#if isLoading}
|
|
540
|
+
<span data-tree-spinner aria-hidden="true"></span>
|
|
541
|
+
{:else if toggleSnippet}
|
|
542
|
+
{@render toggleSnippet(isExpanded, node.isExpandable, icons)}
|
|
543
|
+
{:else}
|
|
544
|
+
{@render defaultToggle(isExpanded, node.isExpandable, icons)}
|
|
545
|
+
{/if}
|
|
546
|
+
</button>
|
|
547
|
+
{:else if connectorSnippet}
|
|
548
|
+
{@render connectorSnippet(lineType)}
|
|
549
|
+
{:else}
|
|
550
|
+
<Connector type={lineType} />
|
|
551
|
+
{/if}
|
|
552
|
+
{/each}
|
|
553
|
+
{:else}
|
|
554
|
+
<!-- No lines - just indent and toggle -->
|
|
555
|
+
<span data-tree-indent style="width: {node.level * 1.25}rem"></span>
|
|
556
|
+
<button
|
|
557
|
+
type="button"
|
|
558
|
+
data-tree-toggle-btn
|
|
559
|
+
onclick={() => toggleNodeByKey(node.path)}
|
|
560
|
+
aria-label={isLoading ? 'Loading' : isExpanded ? 'Collapse' : 'Expand'}
|
|
561
|
+
tabindex={-1}
|
|
562
|
+
>
|
|
563
|
+
{#if isLoading}
|
|
564
|
+
<span data-tree-spinner aria-hidden="true"></span>
|
|
565
|
+
{:else if toggleSnippet}
|
|
566
|
+
{@render toggleSnippet(isExpanded, node.isExpandable, icons)}
|
|
567
|
+
{:else}
|
|
568
|
+
{@render defaultToggle(isExpanded, node.isExpandable, icons)}
|
|
569
|
+
{/if}
|
|
570
|
+
</button>
|
|
571
|
+
{/if}
|
|
572
|
+
|
|
573
|
+
<!-- Node content -->
|
|
574
|
+
{#if customSnippet}
|
|
575
|
+
<svelte:boundary>
|
|
576
|
+
{@render customSnippet(
|
|
577
|
+
proxy.original as TreeItem,
|
|
578
|
+
fields,
|
|
579
|
+
handlers,
|
|
580
|
+
isActive,
|
|
581
|
+
isExpanded,
|
|
582
|
+
node.level
|
|
583
|
+
)}
|
|
584
|
+
{#snippet failed()}
|
|
585
|
+
{@render defaultItem(proxy, handlers, isActive, isExpanded, node.level, node.path)}
|
|
586
|
+
{/snippet}
|
|
587
|
+
</svelte:boundary>
|
|
588
|
+
{:else}
|
|
589
|
+
{@render defaultItem(proxy, handlers, isActive, isExpanded, node.level, node.path)}
|
|
590
|
+
{/if}
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
{/snippet}
|
|
594
|
+
|
|
595
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
596
|
+
<div
|
|
597
|
+
bind:this={treeRef}
|
|
598
|
+
data-tree
|
|
599
|
+
data-size={size}
|
|
600
|
+
data-show-lines={showLines || undefined}
|
|
601
|
+
data-multiselect={multiselect || undefined}
|
|
602
|
+
class={className || undefined}
|
|
603
|
+
role="tree"
|
|
604
|
+
aria-label="Tree"
|
|
605
|
+
aria-multiselectable={multiselect || undefined}
|
|
606
|
+
onkeydown={handleTreeKeyDown}
|
|
607
|
+
onfocusin={handleFocusIn}
|
|
608
|
+
use:navigator={{ wrapper: controller, orientation: 'vertical', nested: true, typeahead: true }}
|
|
609
|
+
>
|
|
610
|
+
{#each flatNodes as node (node.path)}
|
|
611
|
+
{@render renderNode(node)}
|
|
612
|
+
{/each}
|
|
613
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// UI Components
|
|
2
|
+
export { default as Button } from './Button.svelte'
|
|
3
|
+
export { default as ButtonGroup } from './ButtonGroup.svelte'
|
|
4
|
+
export { default as Code } from './Code.svelte'
|
|
5
|
+
export { default as Menu } from './Menu.svelte'
|
|
6
|
+
export { default as Select } from './Select.svelte'
|
|
7
|
+
export { default as MultiSelect } from './MultiSelect.svelte'
|
|
8
|
+
export { default as Toolbar } from './Toolbar.svelte'
|
|
9
|
+
export { default as ToolbarGroup } from './ToolbarGroup.svelte'
|
|
10
|
+
export { default as Tabs } from './Tabs.svelte'
|
|
11
|
+
export { default as Toggle } from './Toggle.svelte'
|
|
12
|
+
export { default as List } from './List.svelte'
|
|
13
|
+
export { default as Tree } from './Tree.svelte'
|
|
14
|
+
export { default as Connector } from './Connector.svelte'
|
|
15
|
+
export { default as ItemContent } from './ItemContent.svelte'
|
|
16
|
+
export { default as FloatingAction } from './FloatingAction.svelte'
|
|
17
|
+
export { default as PaletteManager } from './PaletteManager.svelte'
|
|
18
|
+
export { default as Tilt } from './Tilt.svelte'
|
|
19
|
+
export { default as Shine } from './Shine.svelte'
|
|
20
|
+
export { default as BreadCrumbs } from './BreadCrumbs.svelte'
|
|
21
|
+
export { default as Card } from './Card.svelte'
|
|
22
|
+
export { default as ProgressBar } from './ProgressBar.svelte'
|
|
23
|
+
export { default as Carousel } from './Carousel.svelte'
|
|
24
|
+
export { default as Pill } from './Pill.svelte'
|
|
25
|
+
export { default as Rating } from './Rating.svelte'
|
|
26
|
+
export { default as Stepper } from './Stepper.svelte'
|
|
27
|
+
export { default as Switch } from './Switch.svelte'
|
|
28
|
+
export { default as Table } from './Table.svelte'
|
|
29
|
+
export { default as SearchFilter } from './SearchFilter.svelte'
|
|
30
|
+
export { default as Range } from './Range.svelte'
|
|
31
|
+
export { default as Timeline } from './Timeline.svelte'
|
|
32
|
+
export { default as Reveal } from './Reveal.svelte'
|
|
33
|
+
export { default as FloatingNavigation } from './FloatingNavigation.svelte'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export {
|
|
3
|
+
Button,
|
|
4
|
+
ButtonGroup,
|
|
5
|
+
Code,
|
|
6
|
+
Menu,
|
|
7
|
+
Select,
|
|
8
|
+
MultiSelect,
|
|
9
|
+
Toolbar,
|
|
10
|
+
ToolbarGroup,
|
|
11
|
+
Tabs,
|
|
12
|
+
Toggle,
|
|
13
|
+
List,
|
|
14
|
+
Tree,
|
|
15
|
+
FloatingAction,
|
|
16
|
+
FloatingNavigation,
|
|
17
|
+
PaletteManager,
|
|
18
|
+
Tilt,
|
|
19
|
+
Shine,
|
|
20
|
+
BreadCrumbs,
|
|
21
|
+
Card,
|
|
22
|
+
ProgressBar,
|
|
23
|
+
Carousel,
|
|
24
|
+
Pill,
|
|
25
|
+
Rating,
|
|
26
|
+
Connector,
|
|
27
|
+
Stepper,
|
|
28
|
+
Switch,
|
|
29
|
+
Table,
|
|
30
|
+
SearchFilter,
|
|
31
|
+
Range,
|
|
32
|
+
Timeline,
|
|
33
|
+
Reveal
|
|
34
|
+
} from './components/index.js'
|
|
35
|
+
|
|
36
|
+
// Utilities
|
|
37
|
+
export { highlightCode, preloadHighlighter, getSupportedLanguages } from './utils/shiki.js'
|
|
38
|
+
export * from './utils/palette.js'
|
|
39
|
+
|
|
40
|
+
// Types
|
|
41
|
+
export * from './types/index.js'
|