@rokkit/ui 1.0.0-next.98 → 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.
Files changed (81) hide show
  1. package/README.md +159 -1
  2. package/package.json +52 -53
  3. package/src/MarkdownRenderer.svelte +106 -0
  4. package/src/components/AlertList.svelte +59 -0
  5. package/src/components/Avatar.svelte +68 -0
  6. package/src/components/Badge.svelte +57 -0
  7. package/src/components/BreadCrumbs.svelte +94 -0
  8. package/src/components/Button.svelte +91 -0
  9. package/src/components/ButtonGroup.svelte +9 -0
  10. package/src/components/Card.svelte +71 -0
  11. package/src/components/Carousel.svelte +169 -0
  12. package/src/components/Code.svelte +189 -0
  13. package/src/components/Connector.svelte +46 -0
  14. package/src/components/Divider.svelte +24 -0
  15. package/src/components/Dropdown.svelte +202 -0
  16. package/src/components/FloatingAction.svelte +367 -0
  17. package/src/components/FloatingNavigation.svelte +256 -0
  18. package/src/components/Grid.svelte +128 -0
  19. package/src/components/ItemContent.svelte +25 -0
  20. package/src/components/LazyTree.svelte +171 -0
  21. package/src/components/List.svelte +224 -0
  22. package/src/components/Menu.svelte +270 -0
  23. package/src/components/Message.svelte +75 -0
  24. package/src/components/MultiSelect.svelte +367 -0
  25. package/src/components/PaletteManager.svelte +360 -0
  26. package/src/components/Pill.svelte +83 -0
  27. package/src/components/ProgressBar.svelte +31 -0
  28. package/src/components/Range.svelte +346 -0
  29. package/src/components/Rating.svelte +115 -0
  30. package/src/components/Reveal.svelte +46 -0
  31. package/src/components/SearchFilter.svelte +88 -0
  32. package/src/components/Select.svelte +424 -0
  33. package/src/components/Shine.svelte +87 -0
  34. package/src/components/Stack.svelte +38 -0
  35. package/src/components/StatusList.svelte +18 -0
  36. package/src/components/Stepper.svelte +174 -0
  37. package/src/components/Swatch.svelte +102 -0
  38. package/src/components/Switch.svelte +71 -0
  39. package/src/components/Table.svelte +245 -0
  40. package/src/components/Tabs.svelte +202 -0
  41. package/src/components/Tilt.svelte +68 -0
  42. package/src/components/Timeline.svelte +61 -0
  43. package/src/components/Toggle.svelte +93 -0
  44. package/src/components/Toolbar.svelte +306 -0
  45. package/src/components/ToolbarGroup.svelte +17 -0
  46. package/src/components/Tooltip.svelte +121 -0
  47. package/src/components/Tree.svelte +143 -0
  48. package/src/components/UploadFileStatus.svelte +102 -0
  49. package/src/components/UploadProgress.svelte +127 -0
  50. package/src/components/UploadTarget.svelte +126 -0
  51. package/src/components/index.ts +48 -0
  52. package/src/index.ts +59 -0
  53. package/src/markdown-plugin.ts +12 -0
  54. package/src/types/button.ts +86 -0
  55. package/src/types/code.ts +46 -0
  56. package/src/types/floating-action.ts +123 -0
  57. package/src/types/floating-navigation.ts +80 -0
  58. package/src/types/index.ts +58 -0
  59. package/src/types/list.ts +200 -0
  60. package/src/types/menu.ts +95 -0
  61. package/src/types/palette.ts +160 -0
  62. package/src/types/range.ts +51 -0
  63. package/src/types/search-filter.ts +67 -0
  64. package/src/types/select.ts +176 -0
  65. package/src/types/switch.ts +68 -0
  66. package/src/types/table.ts +219 -0
  67. package/src/types/tabs.ts +103 -0
  68. package/src/types/timeline.ts +53 -0
  69. package/src/types/toggle.ts +68 -0
  70. package/src/types/toolbar.ts +167 -0
  71. package/src/types/tooltip.ts +21 -0
  72. package/src/types/tree.ts +250 -0
  73. package/src/types/upload-file-status.ts +45 -0
  74. package/src/types/upload-progress.ts +111 -0
  75. package/src/types/upload-target.ts +71 -0
  76. package/src/utils/palette.ts +618 -0
  77. package/src/utils/shiki.ts +114 -0
  78. package/src/utils/upload.js +145 -0
  79. package/LICENSE +0 -21
  80. package/src/index.js +0 -8
  81. 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>