@rokkit/ui 1.0.0-next.125 → 1.0.0-next.128
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 +42 -34
- package/src/components/BreadCrumbs.svelte +90 -0
- package/src/components/Button.svelte +93 -0
- package/src/components/ButtonGroup.svelte +18 -0
- package/src/components/Card.svelte +61 -0
- package/src/components/Carousel.svelte +174 -0
- package/src/components/Code.svelte +189 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/FloatingAction.svelte +334 -0
- package/src/components/FloatingNavigation.svelte +235 -0
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +25 -0
- package/src/components/LazyTree.svelte +165 -0
- package/src/components/List.svelte +188 -0
- package/src/components/Menu.svelte +270 -0
- package/src/components/MultiSelect.svelte +369 -0
- package/src/components/PaletteManager.svelte +364 -0
- package/src/components/Pill.svelte +83 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +330 -0
- package/src/components/Rating.svelte +101 -0
- package/src/components/Reveal.svelte +58 -0
- package/src/components/SearchFilter.svelte +88 -0
- package/src/components/Select.svelte +396 -0
- package/src/{Shine.svelte → components/Shine.svelte} +29 -21
- package/src/components/Stepper.svelte +172 -0
- package/src/components/Switch.svelte +75 -0
- package/src/components/Table.svelte +242 -0
- package/src/components/Tabs.svelte +192 -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 +308 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tree.svelte +144 -0
- package/src/components/UploadFileStatus.svelte +83 -0
- package/src/components/UploadProgress.svelte +131 -0
- package/src/components/UploadTarget.svelte +124 -0
- package/src/components/index.ts +38 -0
- package/src/index.ts +46 -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 +55 -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 +210 -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 +164 -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 +68 -0
- package/src/utils/palette.ts +582 -0
- package/src/utils/shiki.ts +122 -0
- package/src/utils/upload.js +128 -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,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SwitchProps, SwitchItem } from '../types/switch.js'
|
|
3
|
+
import { ProxyItem } from '@rokkit/states'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OPTIONS: [SwitchItem, SwitchItem] = [false, true]
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
options = DEFAULT_OPTIONS,
|
|
9
|
+
fields: userFields,
|
|
10
|
+
value = $bindable(),
|
|
11
|
+
onchange,
|
|
12
|
+
showLabels = false,
|
|
13
|
+
size = 'md',
|
|
14
|
+
disabled = false,
|
|
15
|
+
class: className = ''
|
|
16
|
+
}: SwitchProps = $props()
|
|
17
|
+
|
|
18
|
+
let offProxy = $derived(new ProxyItem(options[0], userFields))
|
|
19
|
+
let onProxy = $derived(new ProxyItem(options[1], userFields))
|
|
20
|
+
let isChecked = $derived(value === onProxy.value)
|
|
21
|
+
let currentProxy = $derived(isChecked ? onProxy : offProxy)
|
|
22
|
+
|
|
23
|
+
function toggle() {
|
|
24
|
+
if (disabled) return
|
|
25
|
+
const next = isChecked ? offProxy : onProxy
|
|
26
|
+
const nextValue = next.value
|
|
27
|
+
value = nextValue
|
|
28
|
+
onchange?.(nextValue, next.original as SwitchItem)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
32
|
+
if (disabled) return
|
|
33
|
+
switch (event.key) {
|
|
34
|
+
case ' ':
|
|
35
|
+
case 'Enter':
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
toggle()
|
|
38
|
+
break
|
|
39
|
+
case 'ArrowRight':
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
if (!isChecked) toggle()
|
|
42
|
+
break
|
|
43
|
+
case 'ArrowLeft':
|
|
44
|
+
event.preventDefault()
|
|
45
|
+
if (isChecked) toggle()
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
role="switch"
|
|
54
|
+
data-switch
|
|
55
|
+
data-switch-size={size}
|
|
56
|
+
data-switch-disabled={disabled || undefined}
|
|
57
|
+
aria-checked={isChecked}
|
|
58
|
+
aria-label={currentProxy.label || undefined}
|
|
59
|
+
title={currentProxy.get('subtext') ?? currentProxy.label ?? undefined}
|
|
60
|
+
{disabled}
|
|
61
|
+
class={className || undefined}
|
|
62
|
+
onclick={toggle}
|
|
63
|
+
onkeydown={handleKeyDown}
|
|
64
|
+
>
|
|
65
|
+
<span data-switch-track>
|
|
66
|
+
<span data-switch-thumb>
|
|
67
|
+
{#if currentProxy.get('icon')}
|
|
68
|
+
<span data-switch-icon class={currentProxy.get('icon')} aria-hidden="true"></span>
|
|
69
|
+
{/if}
|
|
70
|
+
</span>
|
|
71
|
+
</span>
|
|
72
|
+
{#if showLabels && currentProxy.label}
|
|
73
|
+
<span data-switch-label>{currentProxy.label}</span>
|
|
74
|
+
{/if}
|
|
75
|
+
</button>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TableProps, TableColumn, TableSortIcons } from '../types/table.js'
|
|
3
|
+
import { defaultTableSortIcons } from '../types/table.js'
|
|
4
|
+
import { TableController } from '@rokkit/states'
|
|
5
|
+
import { navigator } from '@rokkit/actions'
|
|
6
|
+
import { untrack } from 'svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
data = [],
|
|
10
|
+
columns: userColumns,
|
|
11
|
+
value,
|
|
12
|
+
caption,
|
|
13
|
+
size = 'md',
|
|
14
|
+
striped = false,
|
|
15
|
+
disabled = false,
|
|
16
|
+
fields: userFields,
|
|
17
|
+
onselect,
|
|
18
|
+
onsort,
|
|
19
|
+
class: className = '',
|
|
20
|
+
icons: userIcons,
|
|
21
|
+
header: headerSnippet,
|
|
22
|
+
row: rowSnippet,
|
|
23
|
+
cell: cellSnippet,
|
|
24
|
+
empty: emptySnippet
|
|
25
|
+
}: TableProps = $props()
|
|
26
|
+
|
|
27
|
+
const icons = $derived<TableSortIcons>({ ...defaultTableSortIcons, ...userIcons })
|
|
28
|
+
|
|
29
|
+
// ─── Controller ─────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
let controller = untrack(() => new TableController(data, {
|
|
32
|
+
columns: userColumns,
|
|
33
|
+
fields: userFields,
|
|
34
|
+
value
|
|
35
|
+
}))
|
|
36
|
+
let tableRef = $state<HTMLElement | null>(null)
|
|
37
|
+
|
|
38
|
+
// Sync data changes to controller
|
|
39
|
+
$effect(() => {
|
|
40
|
+
controller.update(data)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Sync columns changes
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (userColumns) {
|
|
46
|
+
controller.columns = userColumns.map((c) => ({
|
|
47
|
+
sortable: true,
|
|
48
|
+
sorted: 'none',
|
|
49
|
+
...c
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ─── Focus management ───────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (!tableRef) return
|
|
58
|
+
const el = tableRef
|
|
59
|
+
|
|
60
|
+
function onAction(event: Event) {
|
|
61
|
+
const detail = (event as CustomEvent).detail
|
|
62
|
+
|
|
63
|
+
if (detail.name === 'move') {
|
|
64
|
+
const key = controller.focusedKey
|
|
65
|
+
if (key) {
|
|
66
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
67
|
+
if (target && target !== document.activeElement) {
|
|
68
|
+
target.focus()
|
|
69
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (detail.name === 'select') {
|
|
75
|
+
handleSelectAction()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
el.addEventListener('action', onAction)
|
|
80
|
+
return () => el.removeEventListener('action', onAction)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function handleFocusIn(event: FocusEvent) {
|
|
84
|
+
const target = event.target as HTMLElement
|
|
85
|
+
if (!target) return
|
|
86
|
+
const path = target.dataset.path
|
|
87
|
+
if (path !== undefined) {
|
|
88
|
+
controller.moveTo(path)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
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
|
+
if (!disabled) {
|
|
100
|
+
onselect?.(proxy.value, proxy.value as Record<string, unknown>)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Sort ───────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function handleSort(event: MouseEvent, column: TableColumn) {
|
|
107
|
+
if (column.sortable === false || disabled) return
|
|
108
|
+
controller.sortBy(column.name, event.shiftKey)
|
|
109
|
+
onsort?.(controller.sortState)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Cell rendering helpers ─────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function getCellValue(row: Record<string, unknown>, column: TableColumn): unknown {
|
|
115
|
+
const fieldName = column.fields?.text ?? column.name
|
|
116
|
+
return row[fieldName]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatCellValue(row: Record<string, unknown>, column: TableColumn): string {
|
|
120
|
+
const value = getCellValue(row, column)
|
|
121
|
+
if (column.formatter) return column.formatter(value, row)
|
|
122
|
+
return value !== null && value !== undefined ? String(value) : ''
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCellIcon(row: Record<string, unknown>, column: TableColumn): string | null {
|
|
126
|
+
if (!column.fields?.icon) return null
|
|
127
|
+
const iconValue = row[column.fields.icon]
|
|
128
|
+
if (!iconValue) return null
|
|
129
|
+
if (column.iconFormatter) return column.iconFormatter(iconValue)
|
|
130
|
+
return String(iconValue)
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
135
|
+
<div
|
|
136
|
+
bind:this={tableRef}
|
|
137
|
+
data-table
|
|
138
|
+
data-size={size}
|
|
139
|
+
data-disabled={disabled || undefined}
|
|
140
|
+
class={className || undefined}
|
|
141
|
+
onfocusin={handleFocusIn}
|
|
142
|
+
use:navigator={{ wrapper: controller, orientation: 'vertical' }}
|
|
143
|
+
>
|
|
144
|
+
<table
|
|
145
|
+
role="grid"
|
|
146
|
+
aria-label={caption}
|
|
147
|
+
data-striped={striped || undefined}
|
|
148
|
+
>
|
|
149
|
+
{#if caption}
|
|
150
|
+
<caption data-table-caption>{caption}</caption>
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
153
|
+
<thead data-table-header>
|
|
154
|
+
{#if headerSnippet}
|
|
155
|
+
{@render headerSnippet(controller.columns, controller.sortState)}
|
|
156
|
+
{:else}
|
|
157
|
+
<tr>
|
|
158
|
+
{#each controller.columns as column (column.name)}
|
|
159
|
+
<th
|
|
160
|
+
data-table-header-cell
|
|
161
|
+
data-column={column.name}
|
|
162
|
+
data-sortable={column.sortable !== false || undefined}
|
|
163
|
+
data-sort-order={column.sorted ?? 'none'}
|
|
164
|
+
scope="col"
|
|
165
|
+
aria-sort={column.sorted === 'ascending' ? 'ascending' : column.sorted === 'descending' ? 'descending' : 'none'}
|
|
166
|
+
style:width={column.width}
|
|
167
|
+
style:text-align={column.align}
|
|
168
|
+
onclick={(e) => handleSort(e, column)}
|
|
169
|
+
>
|
|
170
|
+
<span data-table-header-text>{column.label ?? column.name}</span>
|
|
171
|
+
{#if column.sortable !== false}
|
|
172
|
+
{@const sortIcon = icons[column.sorted ?? 'none'] ?? icons.none}
|
|
173
|
+
<span data-table-sort-icon class={sortIcon} aria-hidden="true"></span>
|
|
174
|
+
{/if}
|
|
175
|
+
</th>
|
|
176
|
+
{/each}
|
|
177
|
+
</tr>
|
|
178
|
+
{/if}
|
|
179
|
+
</thead>
|
|
180
|
+
|
|
181
|
+
<tbody data-table-body>
|
|
182
|
+
{#if controller.data.length === 0}
|
|
183
|
+
{#if emptySnippet}
|
|
184
|
+
<tr data-table-empty-row>
|
|
185
|
+
<td colspan={controller.columns.length}>
|
|
186
|
+
{@render emptySnippet()}
|
|
187
|
+
</td>
|
|
188
|
+
</tr>
|
|
189
|
+
{:else}
|
|
190
|
+
<tr data-table-empty-row>
|
|
191
|
+
<td data-table-empty colspan={controller.columns.length}>
|
|
192
|
+
No data
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
{/if}
|
|
196
|
+
{:else}
|
|
197
|
+
{#each controller.data as entry, rowIndex (entry.key)}
|
|
198
|
+
{@const row = entry.value as Record<string, unknown>}
|
|
199
|
+
{@const isSelected = controller.selectedKeys.has(entry.key)}
|
|
200
|
+
{@const isFocused = controller.focusedKey === entry.key}
|
|
201
|
+
{#if rowSnippet}
|
|
202
|
+
{@render rowSnippet(row, controller.columns, rowIndex, isSelected)}
|
|
203
|
+
{:else}
|
|
204
|
+
<tr
|
|
205
|
+
data-table-row
|
|
206
|
+
data-path={entry.key}
|
|
207
|
+
data-selected={isSelected || undefined}
|
|
208
|
+
data-focused={isFocused || undefined}
|
|
209
|
+
aria-selected={isSelected}
|
|
210
|
+
aria-rowindex={rowIndex + 1}
|
|
211
|
+
tabindex={isFocused ? 0 : -1}
|
|
212
|
+
>
|
|
213
|
+
{#each controller.columns as column (column.name)}
|
|
214
|
+
{#if cellSnippet}
|
|
215
|
+
<td
|
|
216
|
+
data-table-cell
|
|
217
|
+
data-column={column.name}
|
|
218
|
+
style:text-align={column.align}
|
|
219
|
+
>
|
|
220
|
+
{@render cellSnippet(getCellValue(row, column), column, row)}
|
|
221
|
+
</td>
|
|
222
|
+
{:else}
|
|
223
|
+
{@const cellIcon = getCellIcon(row, column)}
|
|
224
|
+
<td
|
|
225
|
+
data-table-cell
|
|
226
|
+
data-column={column.name}
|
|
227
|
+
style:text-align={column.align}
|
|
228
|
+
>
|
|
229
|
+
{#if cellIcon}
|
|
230
|
+
<span data-cell-icon class={cellIcon} aria-hidden="true"></span>
|
|
231
|
+
{/if}
|
|
232
|
+
<span data-cell-value>{formatCellValue(row, column)}</span>
|
|
233
|
+
</td>
|
|
234
|
+
{/if}
|
|
235
|
+
{/each}
|
|
236
|
+
</tr>
|
|
237
|
+
{/if}
|
|
238
|
+
{/each}
|
|
239
|
+
{/if}
|
|
240
|
+
</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Tabs — Wrapper + Navigator + ProxyItem implementation.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Wrapper — owns focusedKey $state + flatView $derived
|
|
7
|
+
* Navigator — attaches DOM event handlers, calls wrapper[action](path)
|
|
8
|
+
* owns focus + scrollIntoView after every keyboard action
|
|
9
|
+
* flatView loop — single flat {#each} for tab triggers
|
|
10
|
+
*
|
|
11
|
+
* Snippet customization:
|
|
12
|
+
* itemContent — replaces inner content of <button> for tab triggers
|
|
13
|
+
* tabPanel — replaces panel content
|
|
14
|
+
* [named] — per-item override via item.snippet = 'name'; falls back to itemContent
|
|
15
|
+
* empty — rendered when no options
|
|
16
|
+
*
|
|
17
|
+
* Tab panels are rendered separately from triggers. Only the active panel
|
|
18
|
+
* receives data-panel-active. Navigator ignores panels (no data-path on them).
|
|
19
|
+
*/
|
|
20
|
+
// @ts-nocheck
|
|
21
|
+
import type { TabsProps } from '../types/tabs.js'
|
|
22
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
23
|
+
import { Wrapper, ProxyTree, messages } from '@rokkit/states'
|
|
24
|
+
import { Navigator } from '@rokkit/actions'
|
|
25
|
+
import { resolveSnippet, ITEM_SNIPPET, DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
options = [],
|
|
29
|
+
fields: userFields = {},
|
|
30
|
+
value = $bindable(),
|
|
31
|
+
orientation = 'horizontal',
|
|
32
|
+
position = 'before',
|
|
33
|
+
align = 'start',
|
|
34
|
+
name = 'tabs',
|
|
35
|
+
editable = false,
|
|
36
|
+
placeholder = 'Select a tab to view its content.',
|
|
37
|
+
disabled = false,
|
|
38
|
+
labels: userLabels = {},
|
|
39
|
+
class: className = '',
|
|
40
|
+
onchange,
|
|
41
|
+
onselect,
|
|
42
|
+
onadd,
|
|
43
|
+
onremove,
|
|
44
|
+
...snippets
|
|
45
|
+
}: TabsProps & { labels?: Record<string, string>; [key: string]: unknown } = $props()
|
|
46
|
+
|
|
47
|
+
const labels = $derived({ ...messages.current.tabs, ...userLabels })
|
|
48
|
+
|
|
49
|
+
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const proxyTree = $derived(new ProxyTree(options, userFields))
|
|
52
|
+
const wrapper = $derived(new Wrapper(proxyTree, { onchange, onselect }))
|
|
53
|
+
|
|
54
|
+
// ─── Navigator ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
let containerRef = $state<HTMLElement | null>(null)
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (!containerRef) return
|
|
60
|
+
const nav = new Navigator(containerRef, wrapper, { orientation })
|
|
61
|
+
return () => nav.destroy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── Sync external value → focused key ────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
$effect(() => {
|
|
67
|
+
wrapper.moveToValue(value)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// ─── Editable handlers ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function handleAdd() {
|
|
73
|
+
onadd?.()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleRemove(proxy: ProxyItem) {
|
|
77
|
+
onremove?.(proxy.value)
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{#snippet defaultTabContent(proxy: ProxyItem)}
|
|
82
|
+
{#if proxy.get('icon')}
|
|
83
|
+
<span data-tabs-icon class={proxy.get('icon')} aria-hidden="true"></span>
|
|
84
|
+
{/if}
|
|
85
|
+
<span data-tabs-label>{proxy.label}</span>
|
|
86
|
+
{#if editable}
|
|
87
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
88
|
+
<span
|
|
89
|
+
data-tabs-remove
|
|
90
|
+
role="button"
|
|
91
|
+
tabindex="-1"
|
|
92
|
+
aria-label={labels.remove}
|
|
93
|
+
onclick={(e) => { e.stopPropagation(); handleRemove(proxy) }}
|
|
94
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); e.preventDefault(); handleRemove(proxy) } }}
|
|
95
|
+
>
|
|
96
|
+
<span class={DEFAULT_STATE_ICONS.action.close} aria-hidden="true"></span>
|
|
97
|
+
</span>
|
|
98
|
+
{/if}
|
|
99
|
+
{/snippet}
|
|
100
|
+
|
|
101
|
+
{#snippet defaultPanel(proxy: ProxyItem)}
|
|
102
|
+
<div data-tabs-content>
|
|
103
|
+
{proxy.get('content')}
|
|
104
|
+
</div>
|
|
105
|
+
{/snippet}
|
|
106
|
+
|
|
107
|
+
{#snippet defaultEmpty()}
|
|
108
|
+
No tabs available.
|
|
109
|
+
{/snippet}
|
|
110
|
+
|
|
111
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
112
|
+
<div
|
|
113
|
+
bind:this={containerRef}
|
|
114
|
+
data-tabs
|
|
115
|
+
data-orientation={orientation}
|
|
116
|
+
data-position={position}
|
|
117
|
+
data-align={align}
|
|
118
|
+
data-disabled={disabled || undefined}
|
|
119
|
+
class={className || undefined}
|
|
120
|
+
aria-label={name}
|
|
121
|
+
>
|
|
122
|
+
{#if options.length === 0}
|
|
123
|
+
<div data-tabs-empty>
|
|
124
|
+
{#if snippets.empty}
|
|
125
|
+
{@render snippets.empty()}
|
|
126
|
+
{:else}
|
|
127
|
+
{@render defaultEmpty()}
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
130
|
+
{:else}
|
|
131
|
+
<div data-tabs-list role="tablist" aria-orientation={orientation}>
|
|
132
|
+
{#each wrapper.flatView as node (node.key)}
|
|
133
|
+
{@const proxy = node.proxy}
|
|
134
|
+
{@const sel = proxy.value === value}
|
|
135
|
+
{@const content = resolveSnippet(snippets, proxy, ITEM_SNIPPET)}
|
|
136
|
+
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
data-tabs-trigger
|
|
140
|
+
data-path={node.key}
|
|
141
|
+
data-selected={sel || undefined}
|
|
142
|
+
data-disabled={proxy.disabled || undefined}
|
|
143
|
+
role="tab"
|
|
144
|
+
aria-selected={sel}
|
|
145
|
+
aria-label={proxy.get('label') || proxy.label}
|
|
146
|
+
disabled={proxy.disabled || disabled}
|
|
147
|
+
>
|
|
148
|
+
{#if content}
|
|
149
|
+
{@render content(proxy, sel)}
|
|
150
|
+
{:else}
|
|
151
|
+
{@render defaultTabContent(proxy)}
|
|
152
|
+
{/if}
|
|
153
|
+
</button>
|
|
154
|
+
{/each}
|
|
155
|
+
{#if editable}
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
data-tabs-add
|
|
159
|
+
aria-label={labels.add}
|
|
160
|
+
onclick={handleAdd}
|
|
161
|
+
>
|
|
162
|
+
<span class="i-lucide:plus" aria-hidden="true"></span>
|
|
163
|
+
</button>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{#each wrapper.flatView as node (node.key)}
|
|
168
|
+
{@const proxy = node.proxy}
|
|
169
|
+
{@const active = proxy.value === value}
|
|
170
|
+
|
|
171
|
+
<div
|
|
172
|
+
data-tabs-panel
|
|
173
|
+
data-panel-active={active || undefined}
|
|
174
|
+
role="tabpanel"
|
|
175
|
+
id="tab-panel-{node.key}"
|
|
176
|
+
aria-labelledby="tab-{node.key}"
|
|
177
|
+
>
|
|
178
|
+
{#if snippets.tabPanel}
|
|
179
|
+
{@render snippets.tabPanel(proxy)}
|
|
180
|
+
{:else}
|
|
181
|
+
{@render defaultPanel(proxy)}
|
|
182
|
+
{/if}
|
|
183
|
+
</div>
|
|
184
|
+
{/each}
|
|
185
|
+
|
|
186
|
+
{#if value === undefined}
|
|
187
|
+
<div data-tabs-placeholder>
|
|
188
|
+
{placeholder}
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
{/if}
|
|
192
|
+
</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 { ProxyItem } from '@rokkit/states'
|
|
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 ProxyItem(item, fields)}
|
|
21
|
+
{@const text = proxy.label}
|
|
22
|
+
{@const icon = proxy.get('icon')}
|
|
23
|
+
{@const description = proxy.get('subtext')}
|
|
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>
|