@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,268 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TabsProps, TabsItem, TabsItemHandlers } from '../types/tabs.js'
|
|
3
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
4
|
+
import { ListController } from '@rokkit/states'
|
|
5
|
+
import { navigator } from '@rokkit/actions'
|
|
6
|
+
import { untrack } from 'svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
options = [],
|
|
10
|
+
fields: userFields,
|
|
11
|
+
value = $bindable(),
|
|
12
|
+
orientation = 'horizontal',
|
|
13
|
+
position = 'before',
|
|
14
|
+
align = 'start',
|
|
15
|
+
name = 'tabs',
|
|
16
|
+
editable = false,
|
|
17
|
+
placeholder = 'Select a tab to view its content.',
|
|
18
|
+
disabled = false,
|
|
19
|
+
class: className = '',
|
|
20
|
+
onchange,
|
|
21
|
+
onselect,
|
|
22
|
+
onadd,
|
|
23
|
+
onremove,
|
|
24
|
+
tabItem: tabItemSnippet,
|
|
25
|
+
tabPanel: tabPanelSnippet,
|
|
26
|
+
empty: emptySnippet
|
|
27
|
+
}: TabsProps = $props()
|
|
28
|
+
|
|
29
|
+
/** Content field name from user fields or default */
|
|
30
|
+
const contentField = $derived((userFields as Record<string, string> | undefined)?.content ?? 'content')
|
|
31
|
+
|
|
32
|
+
let controller = untrack(() => new ListController(options, value, userFields))
|
|
33
|
+
let containerRef: HTMLElement | null = $state(null)
|
|
34
|
+
let lastSyncedValue: unknown = value
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
controller.update(options)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Sync controller focus when value changes externally
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (value !== lastSyncedValue) {
|
|
43
|
+
lastSyncedValue = value
|
|
44
|
+
controller.moveToValue(value)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Focus the tab matching controller.focusedKey on navigator move events
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (!containerRef) return
|
|
51
|
+
const el = containerRef
|
|
52
|
+
|
|
53
|
+
function handleAction(event: Event) {
|
|
54
|
+
const detail = (event as CustomEvent).detail
|
|
55
|
+
|
|
56
|
+
if (detail.name === 'move') {
|
|
57
|
+
const key = controller.focusedKey
|
|
58
|
+
if (key) {
|
|
59
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
60
|
+
if (target && target !== document.activeElement) {
|
|
61
|
+
target.focus()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (detail.name === 'select') {
|
|
67
|
+
handleSelectAction()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
el.addEventListener('action', handleAction)
|
|
72
|
+
return () => el.removeEventListener('action', handleAction)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create an ItemProxy for the given item
|
|
77
|
+
*/
|
|
78
|
+
function createProxy(item: TabsItem): ItemProxy {
|
|
79
|
+
return new ItemProxy(item, userFields)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if an item is currently selected
|
|
84
|
+
*/
|
|
85
|
+
function isSelected(proxy: ItemProxy): boolean {
|
|
86
|
+
return proxy.itemValue === value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle tab selection via navigator select action
|
|
91
|
+
*/
|
|
92
|
+
function handleSelectAction() {
|
|
93
|
+
const key = controller.focusedKey
|
|
94
|
+
if (!key) return
|
|
95
|
+
|
|
96
|
+
const proxy = controller.lookup.get(key)
|
|
97
|
+
if (!proxy) return
|
|
98
|
+
|
|
99
|
+
const itemProxy = createProxy(proxy.value)
|
|
100
|
+
selectTab(itemProxy)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Select a tab by its proxy
|
|
105
|
+
*/
|
|
106
|
+
function selectTab(proxy: ItemProxy) {
|
|
107
|
+
if (proxy.disabled || disabled) return
|
|
108
|
+
const itemValue = proxy.itemValue
|
|
109
|
+
if (itemValue !== value) {
|
|
110
|
+
value = itemValue
|
|
111
|
+
lastSyncedValue = itemValue
|
|
112
|
+
controller.moveToValue(itemValue)
|
|
113
|
+
onchange?.(itemValue, proxy.original as TabsItem)
|
|
114
|
+
}
|
|
115
|
+
onselect?.(itemValue, proxy.original as TabsItem)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Handle keyboard events on individual tabs (Enter/Space)
|
|
120
|
+
*/
|
|
121
|
+
function handleKeyDown(event: KeyboardEvent, proxy: ItemProxy) {
|
|
122
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
selectTab(proxy)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create handlers object for custom snippets
|
|
130
|
+
*/
|
|
131
|
+
function createHandlers(proxy: ItemProxy): TabsItemHandlers {
|
|
132
|
+
return {
|
|
133
|
+
onclick: () => selectTab(proxy),
|
|
134
|
+
onkeydown: (event: KeyboardEvent) => handleKeyDown(event, proxy)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleAdd() {
|
|
139
|
+
onadd?.()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function handleRemove(proxy: ItemProxy) {
|
|
143
|
+
onremove?.(proxy.itemValue)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the panel content for a tab item
|
|
148
|
+
*/
|
|
149
|
+
function getContent(item: TabsItem): unknown {
|
|
150
|
+
return item[contentField]
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
{#snippet defaultTabItem(proxy: ItemProxy, handlers: TabsItemHandlers, selected: boolean, key: string)}
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
data-tabs-trigger
|
|
158
|
+
data-path={key}
|
|
159
|
+
data-selected={selected || undefined}
|
|
160
|
+
data-disabled={proxy.disabled || undefined}
|
|
161
|
+
role="tab"
|
|
162
|
+
aria-selected={selected}
|
|
163
|
+
aria-label={proxy.label}
|
|
164
|
+
disabled={proxy.disabled || disabled}
|
|
165
|
+
onkeydown={handlers.onkeydown}
|
|
166
|
+
>
|
|
167
|
+
{#if proxy.icon}
|
|
168
|
+
<span data-tabs-icon class={proxy.icon} aria-hidden="true"></span>
|
|
169
|
+
{/if}
|
|
170
|
+
<span data-tabs-label>{proxy.text}</span>
|
|
171
|
+
{#if editable}
|
|
172
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
173
|
+
<span
|
|
174
|
+
data-tabs-remove
|
|
175
|
+
role="button"
|
|
176
|
+
tabindex="-1"
|
|
177
|
+
aria-label="Remove tab"
|
|
178
|
+
onclick={(e) => { e.stopPropagation(); handleRemove(proxy) }}
|
|
179
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); e.preventDefault(); handleRemove(proxy) } }}
|
|
180
|
+
>
|
|
181
|
+
<span class="i-lucide:x" aria-hidden="true"></span>
|
|
182
|
+
</span>
|
|
183
|
+
{/if}
|
|
184
|
+
</button>
|
|
185
|
+
{/snippet}
|
|
186
|
+
|
|
187
|
+
{#snippet defaultPanel(item: TabsItem)}
|
|
188
|
+
<div data-tabs-content>
|
|
189
|
+
{getContent(item)}
|
|
190
|
+
</div>
|
|
191
|
+
{/snippet}
|
|
192
|
+
|
|
193
|
+
{#snippet defaultEmpty()}
|
|
194
|
+
No tabs available.
|
|
195
|
+
{/snippet}
|
|
196
|
+
|
|
197
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
198
|
+
<div
|
|
199
|
+
bind:this={containerRef}
|
|
200
|
+
data-tabs
|
|
201
|
+
data-orientation={orientation}
|
|
202
|
+
data-position={position}
|
|
203
|
+
data-align={align}
|
|
204
|
+
data-disabled={disabled || undefined}
|
|
205
|
+
class={className || undefined}
|
|
206
|
+
aria-label={name}
|
|
207
|
+
use:navigator={{ wrapper: controller, orientation }}
|
|
208
|
+
>
|
|
209
|
+
{#if options.length === 0}
|
|
210
|
+
<div data-tabs-empty>
|
|
211
|
+
{#if emptySnippet}
|
|
212
|
+
{@render emptySnippet()}
|
|
213
|
+
{:else}
|
|
214
|
+
{@render defaultEmpty()}
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
{:else}
|
|
218
|
+
<div data-tabs-list role="tablist" aria-orientation={orientation}>
|
|
219
|
+
{#each options as option, index (index)}
|
|
220
|
+
{@const proxy = createProxy(option)}
|
|
221
|
+
{@const selected = isSelected(proxy)}
|
|
222
|
+
{@const handlers = createHandlers(proxy)}
|
|
223
|
+
{@const key = String(index)}
|
|
224
|
+
|
|
225
|
+
{#if tabItemSnippet}
|
|
226
|
+
{@render tabItemSnippet(option, userFields ?? {}, handlers, selected)}
|
|
227
|
+
{:else}
|
|
228
|
+
{@render defaultTabItem(proxy, handlers, selected, key)}
|
|
229
|
+
{/if}
|
|
230
|
+
{/each}
|
|
231
|
+
{#if editable}
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
data-tabs-add
|
|
235
|
+
aria-label="Add tab"
|
|
236
|
+
onclick={handleAdd}
|
|
237
|
+
>
|
|
238
|
+
<span class="i-lucide:plus" aria-hidden="true"></span>
|
|
239
|
+
</button>
|
|
240
|
+
{/if}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{#each options as option, index (index)}
|
|
244
|
+
{@const proxy = createProxy(option)}
|
|
245
|
+
{@const active = isSelected(proxy)}
|
|
246
|
+
|
|
247
|
+
<div
|
|
248
|
+
data-tabs-panel
|
|
249
|
+
data-panel-active={active || undefined}
|
|
250
|
+
role="tabpanel"
|
|
251
|
+
id="tab-panel-{index}"
|
|
252
|
+
aria-labelledby="tab-{index}"
|
|
253
|
+
>
|
|
254
|
+
{#if tabPanelSnippet}
|
|
255
|
+
{@render tabPanelSnippet(option, userFields ?? {})}
|
|
256
|
+
{:else}
|
|
257
|
+
{@render defaultPanel(option)}
|
|
258
|
+
{/if}
|
|
259
|
+
</div>
|
|
260
|
+
{/each}
|
|
261
|
+
|
|
262
|
+
{#if value === undefined}
|
|
263
|
+
<div data-tabs-placeholder>
|
|
264
|
+
{placeholder}
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface TiltProps {
|
|
5
|
+
/** Maximum rotation angle in degrees (default: 10) */
|
|
6
|
+
maxRotation?: number
|
|
7
|
+
/** Whether to adjust brightness based on mouse Y position */
|
|
8
|
+
setBrightness?: boolean
|
|
9
|
+
/** CSS perspective value in pixels (default: 600) */
|
|
10
|
+
perspective?: number
|
|
11
|
+
/** Additional CSS class */
|
|
12
|
+
class?: string
|
|
13
|
+
children?: Snippet
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
maxRotation = 10,
|
|
18
|
+
setBrightness = false,
|
|
19
|
+
perspective = 600,
|
|
20
|
+
class: className = '',
|
|
21
|
+
children
|
|
22
|
+
}: TiltProps = $props()
|
|
23
|
+
|
|
24
|
+
let width = $state(0)
|
|
25
|
+
let height = $state(0)
|
|
26
|
+
|
|
27
|
+
let rotateX = $state(0)
|
|
28
|
+
let rotateY = $state(0)
|
|
29
|
+
let brightness = $state(1)
|
|
30
|
+
|
|
31
|
+
/** Linear interpolation: maps value from [0, max] to [rangeMin, rangeMax] */
|
|
32
|
+
function lerp(value: number, max: number, rangeMin: number, rangeMax: number): number {
|
|
33
|
+
if (max === 0) return rangeMin
|
|
34
|
+
return rangeMin + (value / max) * (rangeMax - rangeMin)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onMouseMove(e: MouseEvent) {
|
|
38
|
+
rotateY = lerp(e.offsetX, width, maxRotation, -maxRotation)
|
|
39
|
+
rotateX = lerp(e.offsetY, height, -maxRotation, maxRotation)
|
|
40
|
+
|
|
41
|
+
if (setBrightness) {
|
|
42
|
+
brightness = lerp(e.offsetY, height, 2.0, 1.0)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onMouseLeave() {
|
|
47
|
+
rotateX = 0
|
|
48
|
+
rotateY = 0
|
|
49
|
+
brightness = 1
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
54
|
+
<div
|
|
55
|
+
data-tilt
|
|
56
|
+
data-tilt-brightness={setBrightness || undefined}
|
|
57
|
+
class={className || undefined}
|
|
58
|
+
style:--tilt-perspective="{perspective}px"
|
|
59
|
+
style:--tilt-rotate-x="{rotateX}deg"
|
|
60
|
+
style:--tilt-rotate-y="{rotateY}deg"
|
|
61
|
+
style:--tilt-brightness={brightness}
|
|
62
|
+
bind:clientWidth={width}
|
|
63
|
+
bind:clientHeight={height}
|
|
64
|
+
onmousemove={onMouseMove}
|
|
65
|
+
onmouseleave={onMouseLeave}
|
|
66
|
+
>
|
|
67
|
+
{@render children?.()}
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TimelineProps } from '../types/timeline.js'
|
|
3
|
+
import { defaultTimelineFields, defaultTimelineIcons } from '../types/timeline.js'
|
|
4
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
items = [],
|
|
8
|
+
fields: userFields,
|
|
9
|
+
icons: userIcons,
|
|
10
|
+
class: className = '',
|
|
11
|
+
content
|
|
12
|
+
}: TimelineProps = $props()
|
|
13
|
+
|
|
14
|
+
const fields = $derived({ ...defaultTimelineFields, ...userFields })
|
|
15
|
+
const icons = $derived({ ...defaultTimelineIcons, ...userIcons })
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div data-timeline class={className || undefined} role="list">
|
|
19
|
+
{#each items as item, index (index)}
|
|
20
|
+
{@const proxy = new ItemProxy(item, fields)}
|
|
21
|
+
{@const text = proxy.text}
|
|
22
|
+
{@const icon = proxy.icon}
|
|
23
|
+
{@const description = proxy.description}
|
|
24
|
+
{@const completed = Boolean(item.completed)}
|
|
25
|
+
{@const active = Boolean(item.active)}
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
data-timeline-item
|
|
29
|
+
data-completed={completed || undefined}
|
|
30
|
+
data-active={active || undefined}
|
|
31
|
+
role="listitem"
|
|
32
|
+
>
|
|
33
|
+
<div data-timeline-marker aria-hidden="true">
|
|
34
|
+
<div data-timeline-circle>
|
|
35
|
+
{#if completed}
|
|
36
|
+
<span class={icons.completed}></span>
|
|
37
|
+
{:else if icon}
|
|
38
|
+
<span class={icon}></span>
|
|
39
|
+
{:else}
|
|
40
|
+
{index + 1}
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
{#if index < items.length - 1}
|
|
44
|
+
<div data-timeline-connector></div>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div data-timeline-body>
|
|
49
|
+
{#if text}
|
|
50
|
+
<div data-timeline-title>{text}</div>
|
|
51
|
+
{/if}
|
|
52
|
+
{#if description}
|
|
53
|
+
<div data-timeline-description>{description}</div>
|
|
54
|
+
{/if}
|
|
55
|
+
{#if content}
|
|
56
|
+
{@render content(item, index)}
|
|
57
|
+
{/if}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
{/each}
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ToggleProps, ToggleItem, ToggleItemHandlers } from '../types/toggle.js'
|
|
3
|
+
import { ItemProxy } from '../types/item-proxy.js'
|
|
4
|
+
import { ListController } from '@rokkit/states'
|
|
5
|
+
import { navigator } from '@rokkit/actions'
|
|
6
|
+
import { untrack } from 'svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
options = [],
|
|
10
|
+
fields: userFields,
|
|
11
|
+
value = $bindable(),
|
|
12
|
+
onchange,
|
|
13
|
+
showLabels = true,
|
|
14
|
+
size = 'md',
|
|
15
|
+
disabled = false,
|
|
16
|
+
class: className = '',
|
|
17
|
+
item: itemSnippet
|
|
18
|
+
}: ToggleProps = $props()
|
|
19
|
+
|
|
20
|
+
let controller = untrack(() => new ListController(options, value, userFields))
|
|
21
|
+
let containerRef: HTMLElement | null = $state(null)
|
|
22
|
+
let lastSyncedValue: unknown = value
|
|
23
|
+
|
|
24
|
+
$effect(() => {
|
|
25
|
+
controller.update(options)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Only sync controller focus when value prop changes externally,
|
|
29
|
+
// not after every navigator move (focus !== selection in radiogroups)
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (value !== lastSyncedValue) {
|
|
32
|
+
lastSyncedValue = value
|
|
33
|
+
controller.moveToValue(value)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Focus the button matching controller.focusedKey on navigator move events
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (!containerRef) return
|
|
40
|
+
const el = containerRef
|
|
41
|
+
|
|
42
|
+
function handleAction(event: Event) {
|
|
43
|
+
const detail = (event as CustomEvent).detail
|
|
44
|
+
if (detail.name === 'move') {
|
|
45
|
+
const key = controller.focusedKey
|
|
46
|
+
if (key) {
|
|
47
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
48
|
+
if (target && target !== document.activeElement) {
|
|
49
|
+
target.focus()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
el.addEventListener('action', handleAction)
|
|
56
|
+
return () => el.removeEventListener('action', handleAction)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create an ItemProxy for the given item
|
|
61
|
+
*/
|
|
62
|
+
function createProxy(item: ToggleItem): ItemProxy {
|
|
63
|
+
return new ItemProxy(item, userFields)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if an item is currently selected
|
|
68
|
+
*/
|
|
69
|
+
function isSelected(proxy: ItemProxy): boolean {
|
|
70
|
+
return proxy.itemValue === value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle item selection
|
|
75
|
+
*/
|
|
76
|
+
function handleSelect(proxy: ItemProxy) {
|
|
77
|
+
if (proxy.disabled || disabled) return
|
|
78
|
+
const itemValue = proxy.itemValue
|
|
79
|
+
if (itemValue !== value) {
|
|
80
|
+
value = itemValue
|
|
81
|
+
lastSyncedValue = itemValue
|
|
82
|
+
controller.moveToValue(itemValue)
|
|
83
|
+
onchange?.(itemValue, proxy.original as ToggleItem)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handle keyboard events on individual options (Enter/Space)
|
|
89
|
+
*/
|
|
90
|
+
function handleKeyDown(event: KeyboardEvent, proxy: ItemProxy) {
|
|
91
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
handleSelect(proxy)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create handlers object for custom snippets
|
|
99
|
+
*/
|
|
100
|
+
function createHandlers(proxy: ItemProxy): ToggleItemHandlers {
|
|
101
|
+
return {
|
|
102
|
+
onclick: () => handleSelect(proxy),
|
|
103
|
+
onkeydown: (event: KeyboardEvent) => handleKeyDown(event, proxy)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
{#snippet defaultOption(proxy: ItemProxy, handlers: ToggleItemHandlers, selected: boolean, key: string)}
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
data-toggle-option
|
|
112
|
+
data-path={key}
|
|
113
|
+
data-selected={selected || undefined}
|
|
114
|
+
data-disabled={proxy.disabled || undefined}
|
|
115
|
+
role="radio"
|
|
116
|
+
aria-checked={selected}
|
|
117
|
+
aria-label={proxy.text}
|
|
118
|
+
title={proxy.description || proxy.text}
|
|
119
|
+
disabled={proxy.disabled || disabled}
|
|
120
|
+
onclick={handlers.onclick}
|
|
121
|
+
onkeydown={handlers.onkeydown}
|
|
122
|
+
>
|
|
123
|
+
{#if proxy.icon}
|
|
124
|
+
<span data-toggle-icon class={proxy.icon} aria-hidden="true"></span>
|
|
125
|
+
{/if}
|
|
126
|
+
{#if showLabels && proxy.text}
|
|
127
|
+
<span data-toggle-label>{proxy.text}</span>
|
|
128
|
+
{/if}
|
|
129
|
+
</button>
|
|
130
|
+
{/snippet}
|
|
131
|
+
|
|
132
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
133
|
+
<div
|
|
134
|
+
bind:this={containerRef}
|
|
135
|
+
data-toggle
|
|
136
|
+
data-toggle-size={size}
|
|
137
|
+
data-toggle-disabled={disabled || undefined}
|
|
138
|
+
data-toggle-labels={showLabels || undefined}
|
|
139
|
+
class={className || undefined}
|
|
140
|
+
role="radiogroup"
|
|
141
|
+
aria-label="Selection"
|
|
142
|
+
aria-disabled={disabled || undefined}
|
|
143
|
+
use:navigator={{ wrapper: controller, orientation: 'horizontal' }}
|
|
144
|
+
>
|
|
145
|
+
{#each options as option, index (index)}
|
|
146
|
+
{@const proxy = createProxy(option)}
|
|
147
|
+
{@const selected = isSelected(proxy)}
|
|
148
|
+
{@const handlers = createHandlers(proxy)}
|
|
149
|
+
{@const key = String(index)}
|
|
150
|
+
|
|
151
|
+
{#if itemSnippet}
|
|
152
|
+
{@render itemSnippet(option, userFields ?? {}, handlers, selected)}
|
|
153
|
+
{:else}
|
|
154
|
+
{@render defaultOption(proxy, handlers, selected, key)}
|
|
155
|
+
{/if}
|
|
156
|
+
{/each}
|
|
157
|
+
</div>
|