@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.
Files changed (154) hide show
  1. package/README.md +198 -101
  2. package/package.json +42 -34
  3. package/src/components/BreadCrumbs.svelte +90 -0
  4. package/src/components/Button.svelte +93 -0
  5. package/src/components/ButtonGroup.svelte +18 -0
  6. package/src/components/Card.svelte +61 -0
  7. package/src/components/Carousel.svelte +174 -0
  8. package/src/components/Code.svelte +189 -0
  9. package/src/components/Connector.svelte +46 -0
  10. package/src/components/FloatingAction.svelte +334 -0
  11. package/src/components/FloatingNavigation.svelte +235 -0
  12. package/src/components/Grid.svelte +128 -0
  13. package/src/components/ItemContent.svelte +25 -0
  14. package/src/components/LazyTree.svelte +165 -0
  15. package/src/components/List.svelte +188 -0
  16. package/src/components/Menu.svelte +270 -0
  17. package/src/components/MultiSelect.svelte +369 -0
  18. package/src/components/PaletteManager.svelte +364 -0
  19. package/src/components/Pill.svelte +83 -0
  20. package/src/components/ProgressBar.svelte +31 -0
  21. package/src/components/Range.svelte +330 -0
  22. package/src/components/Rating.svelte +101 -0
  23. package/src/components/Reveal.svelte +58 -0
  24. package/src/components/SearchFilter.svelte +88 -0
  25. package/src/components/Select.svelte +396 -0
  26. package/src/{Shine.svelte → components/Shine.svelte} +29 -21
  27. package/src/components/Stepper.svelte +172 -0
  28. package/src/components/Switch.svelte +75 -0
  29. package/src/components/Table.svelte +242 -0
  30. package/src/components/Tabs.svelte +192 -0
  31. package/src/components/Tilt.svelte +68 -0
  32. package/src/components/Timeline.svelte +61 -0
  33. package/src/components/Toggle.svelte +93 -0
  34. package/src/components/Toolbar.svelte +308 -0
  35. package/src/components/ToolbarGroup.svelte +17 -0
  36. package/src/components/Tree.svelte +144 -0
  37. package/src/components/UploadFileStatus.svelte +83 -0
  38. package/src/components/UploadProgress.svelte +131 -0
  39. package/src/components/UploadTarget.svelte +124 -0
  40. package/src/components/index.ts +38 -0
  41. package/src/index.ts +46 -0
  42. package/src/types/button.ts +86 -0
  43. package/src/types/code.ts +46 -0
  44. package/src/types/floating-action.ts +123 -0
  45. package/src/types/floating-navigation.ts +80 -0
  46. package/src/types/index.ts +55 -0
  47. package/src/types/list.ts +200 -0
  48. package/src/types/menu.ts +95 -0
  49. package/src/types/palette.ts +160 -0
  50. package/src/types/range.ts +51 -0
  51. package/src/types/search-filter.ts +67 -0
  52. package/src/types/select.ts +176 -0
  53. package/src/types/switch.ts +68 -0
  54. package/src/types/table.ts +210 -0
  55. package/src/types/tabs.ts +103 -0
  56. package/src/types/timeline.ts +53 -0
  57. package/src/types/toggle.ts +68 -0
  58. package/src/types/toolbar.ts +164 -0
  59. package/src/types/tree.ts +250 -0
  60. package/src/types/upload-file-status.ts +45 -0
  61. package/src/types/upload-progress.ts +111 -0
  62. package/src/types/upload-target.ts +68 -0
  63. package/src/utils/palette.ts +582 -0
  64. package/src/utils/shiki.ts +122 -0
  65. package/src/utils/upload.js +128 -0
  66. package/dist/constants.d.ts +0 -2
  67. package/dist/index.d.ts +0 -41
  68. package/dist/lib/fields.d.ts +0 -16
  69. package/dist/lib/form.d.ts +0 -95
  70. package/dist/lib/index.d.ts +0 -6
  71. package/dist/lib/layout.d.ts +0 -7
  72. package/dist/lib/nested.d.ts +0 -48
  73. package/dist/lib/schema.d.ts +0 -7
  74. package/dist/lib/select.d.ts +0 -8
  75. package/dist/lib/tree.d.ts +0 -9
  76. package/dist/tree/List.spec.svelte.d.ts +0 -1
  77. package/dist/tree/Node.spec.svelte.d.ts +0 -1
  78. package/dist/tree/Root.spec.svelte.d.ts +0 -1
  79. package/dist/types.d.ts +0 -5
  80. package/dist/wrappers/index.d.ts +0 -3
  81. package/src/Accordion.svelte +0 -118
  82. package/src/BreadCrumbs.svelte +0 -32
  83. package/src/Button.svelte +0 -57
  84. package/src/Calendar.svelte +0 -93
  85. package/src/Card.svelte +0 -45
  86. package/src/Carousel.svelte +0 -49
  87. package/src/CheckBox.svelte +0 -56
  88. package/src/Connector.svelte +0 -40
  89. package/src/DropDown.svelte +0 -68
  90. package/src/DropSearch.svelte +0 -37
  91. package/src/Fillable.svelte +0 -19
  92. package/src/GraphPaper.svelte +0 -43
  93. package/src/Icon.svelte +0 -81
  94. package/src/Item.svelte +0 -25
  95. package/src/Link.svelte +0 -21
  96. package/src/List.svelte +0 -89
  97. package/src/ListBody.svelte +0 -43
  98. package/src/Message.svelte +0 -11
  99. package/src/MultiSelect.svelte +0 -48
  100. package/src/NestedList.svelte +0 -78
  101. package/src/NestedPaginator.svelte +0 -63
  102. package/src/Node.svelte +0 -76
  103. package/src/Overlay.svelte +0 -21
  104. package/src/PageNavigator.svelte +0 -94
  105. package/src/PickOne.svelte +0 -60
  106. package/src/Pill.svelte +0 -41
  107. package/src/ProgressBar.svelte +0 -21
  108. package/src/ProgressDots.svelte +0 -53
  109. package/src/RadioGroup.svelte +0 -52
  110. package/src/Range.svelte +0 -45
  111. package/src/RangeMinMax.svelte +0 -124
  112. package/src/RangeSlider.svelte +0 -79
  113. package/src/RangeTick.svelte +0 -28
  114. package/src/Rating.svelte +0 -95
  115. package/src/ResponsiveGrid.svelte +0 -88
  116. package/src/Scrollable.svelte +0 -7
  117. package/src/Select.svelte +0 -114
  118. package/src/Separator.svelte +0 -1
  119. package/src/Slider.svelte +0 -14
  120. package/src/SlidingColumns.svelte +0 -50
  121. package/src/Stage.svelte +0 -41
  122. package/src/Stepper.svelte +0 -66
  123. package/src/Summary.svelte +0 -22
  124. package/src/Switch.svelte +0 -106
  125. package/src/TableCell.svelte +0 -51
  126. package/src/TableHeaderCell.svelte +0 -54
  127. package/src/Tabs.svelte +0 -176
  128. package/src/Tilt.svelte +0 -66
  129. package/src/Toggle.svelte +0 -58
  130. package/src/ToggleThemeMode.svelte +0 -23
  131. package/src/Tree.svelte +0 -80
  132. package/src/TreeTable.svelte +0 -171
  133. package/src/ValidationReport.svelte +0 -23
  134. package/src/constants.js +0 -4
  135. package/src/index.js +0 -48
  136. package/src/lib/fields.js +0 -118
  137. package/src/lib/form.js +0 -72
  138. package/src/lib/index.js +0 -13
  139. package/src/lib/layout.js +0 -63
  140. package/src/lib/nested.js +0 -192
  141. package/src/lib/schema.js +0 -32
  142. package/src/lib/select.js +0 -38
  143. package/src/lib/tree.js +0 -22
  144. package/src/tree/List.spec.svelte.js +0 -84
  145. package/src/tree/List.svelte +0 -78
  146. package/src/tree/Node.spec.svelte.js +0 -104
  147. package/src/tree/Node.svelte +0 -80
  148. package/src/tree/Root.spec.svelte.js +0 -63
  149. package/src/tree/Root.svelte +0 -81
  150. package/src/types.js +0 -9
  151. package/src/wrappers/Category.svelte +0 -27
  152. package/src/wrappers/Section.svelte +0 -16
  153. package/src/wrappers/Wrapper.svelte +0 -12
  154. 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>