@rokkit/ui 1.0.0-next.99 → 1.0.2

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 (83) 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/NavContent.svelte +42 -0
  26. package/src/components/PaletteManager.svelte +360 -0
  27. package/src/components/Pill.svelte +83 -0
  28. package/src/components/ProgressBar.svelte +31 -0
  29. package/src/components/Range.svelte +346 -0
  30. package/src/components/Rating.svelte +115 -0
  31. package/src/components/ResponsiveGrid.svelte +34 -0
  32. package/src/components/Reveal.svelte +46 -0
  33. package/src/components/SearchFilter.svelte +88 -0
  34. package/src/components/Select.svelte +424 -0
  35. package/src/components/Shine.svelte +87 -0
  36. package/src/components/Stack.svelte +38 -0
  37. package/src/components/StatusList.svelte +18 -0
  38. package/src/components/Stepper.svelte +174 -0
  39. package/src/components/Swatch.svelte +102 -0
  40. package/src/components/Switch.svelte +71 -0
  41. package/src/components/Table.svelte +245 -0
  42. package/src/components/Tabs.svelte +202 -0
  43. package/src/components/Tilt.svelte +68 -0
  44. package/src/components/Timeline.svelte +61 -0
  45. package/src/components/Toggle.svelte +93 -0
  46. package/src/components/Toolbar.svelte +306 -0
  47. package/src/components/ToolbarGroup.svelte +17 -0
  48. package/src/components/Tooltip.svelte +121 -0
  49. package/src/components/Tree.svelte +143 -0
  50. package/src/components/UploadFileStatus.svelte +102 -0
  51. package/src/components/UploadProgress.svelte +127 -0
  52. package/src/components/UploadTarget.svelte +126 -0
  53. package/src/components/index.ts +50 -0
  54. package/src/index.ts +61 -0
  55. package/src/markdown-plugin.ts +12 -0
  56. package/src/types/button.ts +86 -0
  57. package/src/types/code.ts +46 -0
  58. package/src/types/floating-action.ts +123 -0
  59. package/src/types/floating-navigation.ts +80 -0
  60. package/src/types/index.ts +58 -0
  61. package/src/types/list.ts +200 -0
  62. package/src/types/menu.ts +95 -0
  63. package/src/types/palette.ts +160 -0
  64. package/src/types/range.ts +51 -0
  65. package/src/types/search-filter.ts +67 -0
  66. package/src/types/select.ts +176 -0
  67. package/src/types/switch.ts +68 -0
  68. package/src/types/table.ts +219 -0
  69. package/src/types/tabs.ts +103 -0
  70. package/src/types/timeline.ts +53 -0
  71. package/src/types/toggle.ts +68 -0
  72. package/src/types/toolbar.ts +167 -0
  73. package/src/types/tooltip.ts +21 -0
  74. package/src/types/tree.ts +250 -0
  75. package/src/types/upload-file-status.ts +45 -0
  76. package/src/types/upload-progress.ts +111 -0
  77. package/src/types/upload-target.ts +71 -0
  78. package/src/utils/palette.ts +618 -0
  79. package/src/utils/shiki.ts +114 -0
  80. package/src/utils/upload.js +145 -0
  81. package/LICENSE +0 -21
  82. package/src/index.js +0 -8
  83. package/src/wrappers.js +0 -2
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
4
+ import { messages } from '@rokkit/states'
5
+
6
+ interface StepperStep {
7
+ /** Step label (shown below circle) */
8
+ text: string
9
+ /** Short text inside circle (default: step number) */
10
+ label?: string
11
+ /** Step is finished (shows checkmark) */
12
+ completed?: boolean
13
+ /** Step cannot be navigated to */
14
+ disabled?: boolean
15
+ /** Number of sub-stages within this step (default: 1) */
16
+ stages?: number
17
+ }
18
+
19
+ interface StepperIcons {
20
+ /** Icon class for check/completed state */
21
+ check?: string
22
+ }
23
+
24
+ interface StepperProps {
25
+ /** Array of step definitions */
26
+ steps?: StepperStep[]
27
+ /** Current step index (bindable) */
28
+ current?: number
29
+ /** Current sub-stage within step (bindable, 0-based) */
30
+ currentStage?: number
31
+ /** Only allow clicking completed steps + first incomplete */
32
+ linear?: boolean
33
+ /** Layout orientation */
34
+ orientation?: 'horizontal' | 'vertical'
35
+ /** Custom icons */
36
+ icons?: StepperIcons
37
+ /** Callback when a step or dot is clicked */
38
+ onclick?: (step: number, stage?: number) => void
39
+ /** Content snippet rendered below the stepper */
40
+ content?: Snippet<[StepperStep, number]>
41
+ /** Additional CSS class */
42
+ class?: string
43
+ }
44
+
45
+ const defaultIcons: StepperIcons = {
46
+ check: DEFAULT_STATE_ICONS.action.check
47
+ }
48
+
49
+ let {
50
+ steps = [],
51
+ current = $bindable(0),
52
+ currentStage = $bindable(0),
53
+ linear = false,
54
+ orientation = 'horizontal',
55
+ label = messages.stepper.label,
56
+ icons: userIcons,
57
+ onclick,
58
+ content,
59
+ class: className = ''
60
+ }: StepperProps & { label?: string } = $props()
61
+
62
+ const icons = $derived<StepperIcons>({ ...defaultIcons, ...userIcons })
63
+
64
+ /**
65
+ * Whether a step can be clicked
66
+ */
67
+ function isClickable(index: number): boolean {
68
+ const step = steps[index]
69
+ if (step.disabled) return false
70
+ if (!linear) return true
71
+ // Linear mode: allow completed steps and the first incomplete step
72
+ if (step.completed) return true
73
+ // First incomplete step = first step where index >= first non-completed
74
+ const firstIncomplete = steps.findIndex((s) => !s.completed)
75
+ return index === firstIncomplete
76
+ }
77
+
78
+ /**
79
+ * Whether the connector line before step N should look "completed"
80
+ */
81
+ function isConnectorCompleted(index: number): boolean {
82
+ return index > 0 && Boolean(steps[index - 1]?.completed)
83
+ }
84
+
85
+ /**
86
+ * Handle step circle click
87
+ */
88
+ function handleStepClick(index: number) {
89
+ if (!isClickable(index)) return
90
+ current = index
91
+ currentStage = 0
92
+ onclick?.(index)
93
+ }
94
+
95
+ /**
96
+ * Handle sub-stage dot click
97
+ */
98
+ function handleDotClick(stepIndex: number, stageIndex: number) {
99
+ if (!isClickable(stepIndex)) return
100
+ current = stepIndex
101
+ currentStage = stageIndex
102
+ onclick?.(stepIndex, stageIndex)
103
+ }
104
+ </script>
105
+
106
+ <div
107
+ data-stepper
108
+ data-orientation={orientation}
109
+ class={className || undefined}
110
+ role="group"
111
+ aria-label={label}
112
+ >
113
+ {#each steps as step, index (index)}
114
+ <!-- Connector line before step (except first) -->
115
+ {#if index > 0}
116
+ <div
117
+ data-stepper-connector
118
+ data-completed={isConnectorCompleted(index) || undefined}
119
+ aria-hidden="true"
120
+ ></div>
121
+ {/if}
122
+
123
+ <!-- Step -->
124
+ <div
125
+ data-stepper-step
126
+ data-completed={step.completed || undefined}
127
+ data-active={index === current || undefined}
128
+ data-disabled={step.disabled || undefined}
129
+ >
130
+ <button
131
+ type="button"
132
+ data-stepper-circle
133
+ disabled={!isClickable(index)}
134
+ aria-label="{step.text}{step.completed ? ' (completed)' : ''}"
135
+ aria-current={index === current ? 'step' : undefined}
136
+ onclick={() => handleStepClick(index)}
137
+ >
138
+ {#if step.completed}
139
+ <span data-stepper-check-icon class={icons.check} aria-hidden="true"></span>
140
+ {:else}
141
+ {step.label ?? index + 1}
142
+ {/if}
143
+ </button>
144
+
145
+ <span data-stepper-label>{step.text}</span>
146
+
147
+ <!-- Sub-stage dots -->
148
+ {#if step.stages && step.stages > 1}
149
+ <div data-stepper-dots aria-label="Sub-stages for {step.text}">
150
+ {#each Array(step.stages) as _, stageIndex (stageIndex)}
151
+ <button
152
+ type="button"
153
+ data-stepper-dot
154
+ data-active={(index === current && stageIndex === currentStage) || undefined}
155
+ data-completed={step.completed ||
156
+ (index === current && stageIndex < currentStage) ||
157
+ undefined}
158
+ disabled={!isClickable(index)}
159
+ aria-label="Stage {stageIndex + 1}"
160
+ onclick={() => handleDotClick(index, stageIndex)}
161
+ ></button>
162
+ {/each}
163
+ </div>
164
+ {/if}
165
+ </div>
166
+ {/each}
167
+
168
+ <!-- Content area -->
169
+ {#if content && steps[current]}
170
+ <div data-stepper-content>
171
+ {@render content(steps[current], current)}
172
+ </div>
173
+ {/if}
174
+ </div>
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ // @ts-nocheck
3
+ import { Wrapper, ProxyTree, messages } from '@rokkit/states'
4
+ import { Navigator } from '@rokkit/actions'
5
+
6
+ let {
7
+ options = [],
8
+ fields: userFields = {},
9
+ value = $bindable(),
10
+ multiple = false,
11
+ shape = 'square',
12
+ size = 'md',
13
+ disabled = false,
14
+ label = messages.swatch?.label ?? 'Select',
15
+ class: className = '',
16
+ onchange,
17
+ item: itemSnippet,
18
+ ..._rest
19
+ } = $props()
20
+
21
+ const proxyTree = $derived(new ProxyTree(options, userFields))
22
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
23
+
24
+ let containerRef = $state<HTMLElement | null>(null)
25
+
26
+ $effect(() => {
27
+ if (!containerRef) return
28
+ const nav = new Navigator(containerRef, wrapper, { orientation: 'horizontal' })
29
+ return () => nav.destroy()
30
+ })
31
+
32
+ $effect(() => {
33
+ wrapper.moveToValue(value)
34
+ })
35
+
36
+ function isSelected(proxy) {
37
+ if (multiple && Array.isArray(value)) return value.includes(proxy.value)
38
+ return proxy.value === value
39
+ }
40
+
41
+ function toggleMultiValue(extracted, original) {
42
+ const arr = Array.isArray(value) ? [...value] : []
43
+ const idx = arr.indexOf(extracted)
44
+ if (idx >= 0) arr.splice(idx, 1)
45
+ else arr.push(extracted)
46
+ value = arr
47
+ onchange?.(value, original)
48
+ }
49
+
50
+ function selectSingleValue(extracted, original) {
51
+ if (extracted === value) return
52
+ value = extracted
53
+ onchange?.(extracted, original)
54
+ }
55
+
56
+ function handleSelect(extracted, proxy) {
57
+ if (proxy.disabled || disabled) return
58
+ if (multiple) toggleMultiValue(extracted, proxy.original)
59
+ else selectSingleValue(extracted, proxy.original)
60
+ }
61
+ </script>
62
+
63
+ <div
64
+ bind:this={containerRef}
65
+ data-swatch
66
+ data-swatch-size={size}
67
+ data-swatch-shape={shape}
68
+ data-swatch-disabled={disabled || undefined}
69
+ data-swatch-multiple={multiple || undefined}
70
+ role={multiple ? 'group' : 'radiogroup'}
71
+ aria-label={label}
72
+ aria-disabled={disabled || undefined}
73
+ class={className || undefined}
74
+ >
75
+ {#each wrapper.flatView as node (node.key)}
76
+ {@const proxy = node.proxy}
77
+ {@const sel = isSelected(proxy)}
78
+ {@const fill = proxy.get(userFields.fill ?? 'fill')}
79
+ {@const stroke = proxy.get(userFields.stroke ?? 'stroke')}
80
+ <button
81
+ type="button"
82
+ data-swatch-item
83
+ data-path={node.key}
84
+ data-selected={sel || undefined}
85
+ data-disabled={proxy.disabled || undefined}
86
+ role={multiple ? 'checkbox' : 'radio'}
87
+ aria-checked={sel}
88
+ aria-label={proxy.label}
89
+ title={proxy.label}
90
+ disabled={proxy.disabled || disabled}
91
+ style={fill ? `--swatch-fill:${fill};--swatch-stroke:${stroke ?? fill}` : undefined}
92
+ >
93
+ {#if itemSnippet}
94
+ {@render itemSnippet(proxy, sel)}
95
+ {:else if shape === 'circle'}
96
+ <span data-swatch-circle></span>
97
+ {:else}
98
+ <span data-swatch-square></span>
99
+ {/if}
100
+ </button>
101
+ {/each}
102
+ </div>
@@ -0,0 +1,71 @@
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 shouldToggle(key: string): boolean {
32
+ if (key === ' ' || key === 'Enter') return true
33
+ if (key === 'ArrowRight') return !isChecked
34
+ if (key === 'ArrowLeft') return isChecked
35
+ return false
36
+ }
37
+
38
+ function handleKeyDown(event: KeyboardEvent) {
39
+ if (disabled) return
40
+ if (shouldToggle(event.key)) {
41
+ event.preventDefault()
42
+ toggle()
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <button
48
+ type="button"
49
+ role="switch"
50
+ data-switch
51
+ data-switch-size={size}
52
+ data-switch-disabled={disabled || undefined}
53
+ aria-checked={isChecked}
54
+ aria-label={currentProxy.label || undefined}
55
+ title={currentProxy.get('subtext') ?? currentProxy.label ?? undefined}
56
+ {disabled}
57
+ class={className || undefined}
58
+ onclick={toggle}
59
+ onkeydown={handleKeyDown}
60
+ >
61
+ <span data-switch-track>
62
+ <span data-switch-thumb>
63
+ {#if currentProxy.get('icon')}
64
+ <span data-switch-icon class={currentProxy.get('icon')} aria-hidden="true"></span>
65
+ {/if}
66
+ </span>
67
+ </span>
68
+ {#if showLabels && currentProxy.label}
69
+ <span data-switch-label>{currentProxy.label}</span>
70
+ {/if}
71
+ </button>
@@ -0,0 +1,245 @@
1
+ <script lang="ts">
2
+ import type { TableProps, TableColumn, TableSortIcons } from '../types/table.js'
3
+ import { defaultTableSortIcons } from '../types/table.js'
4
+ import { TableController, messages } 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
+ values = $bindable<unknown[]>([]),
13
+ selectable = 'single' as 'single' | 'multi' | false,
14
+ caption,
15
+ size = 'md',
16
+ striped = false,
17
+ responsive = false,
18
+ disabled = false,
19
+ fields: userFields,
20
+ onselect,
21
+ onsort,
22
+ class: className = '',
23
+ icons: userIcons,
24
+ header: headerSnippet,
25
+ row: rowSnippet,
26
+ cell: cellSnippet,
27
+ empty: emptySnippet
28
+ }: TableProps = $props()
29
+
30
+ const icons = $derived<TableSortIcons>({ ...defaultTableSortIcons, ...userIcons })
31
+
32
+ // ─── Controller ─────────────────────────────────────────────────
33
+
34
+ let controller = untrack(
35
+ () =>
36
+ new TableController(data, {
37
+ columns: userColumns,
38
+ fields: userFields,
39
+ value,
40
+ multiselect: selectable === 'multi'
41
+ })
42
+ )
43
+
44
+ // Sync values binding from controller selection in multi-select mode
45
+ $effect(() => {
46
+ if (selectable === 'multi') {
47
+ values = controller.selected.slice()
48
+ }
49
+ })
50
+ let tableRef = $state<HTMLElement | null>(null)
51
+
52
+ // Sync data changes to controller
53
+ $effect(() => {
54
+ controller.update(data)
55
+ })
56
+
57
+ // Sync columns changes
58
+ $effect(() => {
59
+ if (userColumns) {
60
+ controller.columns = userColumns.map((c) => ({
61
+ sortable: true,
62
+ sorted: 'none',
63
+ ...c
64
+ }))
65
+ }
66
+ })
67
+
68
+ // ─── Focus management ───────────────────────────────────────────
69
+
70
+ function focusByKey(el: HTMLElement, key: string) {
71
+ const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
72
+ if (target && target !== document.activeElement) {
73
+ target.focus()
74
+ target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })
75
+ }
76
+ }
77
+
78
+ $effect(() => {
79
+ if (!tableRef) return
80
+ const el = tableRef
81
+
82
+ function onAction(event: Event) {
83
+ const detail = (event as CustomEvent).detail
84
+ if (detail.name === 'move') {
85
+ const key = controller.focusedKey
86
+ if (key) focusByKey(el, key)
87
+ }
88
+ if (detail.name === 'select') handleSelectAction()
89
+ }
90
+
91
+ el.addEventListener('action', onAction)
92
+ return () => el.removeEventListener('action', onAction)
93
+ })
94
+
95
+ function handleFocusIn(event: FocusEvent) {
96
+ const target = event.target as HTMLElement
97
+ if (!target) return
98
+ const path = target.dataset.path
99
+ if (path !== undefined) {
100
+ controller.moveTo(path)
101
+ }
102
+ }
103
+
104
+ function handleSelectAction() {
105
+ if (selectable === false || disabled) return
106
+
107
+ const key = controller.focusedKey
108
+ if (!key) return
109
+
110
+ const proxy = controller.lookup.get(key)
111
+ if (!proxy) return
112
+
113
+ onselect?.(proxy.value, proxy.value as Record<string, unknown>)
114
+ }
115
+
116
+ // ─── Sort ───────────────────────────────────────────────────────
117
+
118
+ function handleSort(event: MouseEvent, column: TableColumn) {
119
+ if (column.sortable === false || disabled) return
120
+ controller.sortBy(column.name, event.shiftKey)
121
+ onsort?.(controller.sortState)
122
+ }
123
+
124
+ // ─── Cell rendering helpers ─────────────────────────────────────
125
+
126
+ function getCellValue(row: Record<string, unknown>, column: TableColumn): unknown {
127
+ const fieldName = column.fields?.text ?? column.name
128
+ return row[fieldName]
129
+ }
130
+
131
+ function formatCellValue(row: Record<string, unknown>, column: TableColumn): string {
132
+ const value = getCellValue(row, column)
133
+ if (column.formatter) return column.formatter(value, row)
134
+ return value !== null && value !== undefined ? String(value) : ''
135
+ }
136
+
137
+ function getCellIcon(row: Record<string, unknown>, column: TableColumn): string | null {
138
+ if (!column.fields?.icon) return null
139
+ const iconValue = row[column.fields.icon]
140
+ if (!iconValue) return null
141
+ if (column.iconFormatter) return column.iconFormatter(iconValue)
142
+ return String(iconValue)
143
+ }
144
+ </script>
145
+
146
+ <div
147
+ bind:this={tableRef}
148
+ data-table
149
+ data-size={size}
150
+ data-selectable={selectable || undefined}
151
+ data-disabled={disabled || undefined}
152
+ data-table-responsive={responsive || undefined}
153
+ class={className || undefined}
154
+ onfocusin={handleFocusIn}
155
+ use:navigator={{ wrapper: controller, orientation: 'vertical' }}
156
+ >
157
+ <table role="grid" aria-label={caption} data-table-striped={striped || undefined}>
158
+ {#if caption}
159
+ <caption data-table-caption>{caption}</caption>
160
+ {/if}
161
+
162
+ <thead data-table-header>
163
+ {#if headerSnippet}
164
+ {@render headerSnippet(controller.columns, controller.sortState)}
165
+ {:else}
166
+ <tr>
167
+ {#each controller.columns as column (column.name)}
168
+ <th
169
+ data-table-header-cell
170
+ data-column={column.name}
171
+ data-sortable={column.sortable !== false || undefined}
172
+ data-sort-order={column.sorted ?? 'none'}
173
+ scope="col"
174
+ aria-sort={column.sorted === 'ascending'
175
+ ? 'ascending'
176
+ : column.sorted === 'descending'
177
+ ? 'descending'
178
+ : 'none'}
179
+ style:width={column.width}
180
+ style:text-align={column.align}
181
+ onclick={(e) => handleSort(e, column)}
182
+ >
183
+ <span data-table-header-text>{column.label ?? column.name}</span>
184
+ {#if column.sortable !== false}
185
+ {@const sortIcon = icons[column.sorted ?? 'none'] ?? icons.none}
186
+ <span data-table-sort-icon class={sortIcon} aria-hidden="true"></span>
187
+ {/if}
188
+ </th>
189
+ {/each}
190
+ </tr>
191
+ {/if}
192
+ </thead>
193
+
194
+ <tbody data-table-body>
195
+ {#if controller.data.length === 0}
196
+ {#if emptySnippet}
197
+ <tr data-table-empty-row>
198
+ <td colspan={controller.columns.length}>
199
+ {@render emptySnippet()}
200
+ </td>
201
+ </tr>
202
+ {:else}
203
+ <tr data-table-empty-row>
204
+ <td data-table-empty colspan={controller.columns.length}>{messages.table.empty}</td>
205
+ </tr>
206
+ {/if}
207
+ {:else}
208
+ {#each controller.data as entry, rowIndex (entry.key)}
209
+ {@const row = entry.value as Record<string, unknown>}
210
+ {@const isSelected = controller.selectedKeys.has(entry.key)}
211
+ {@const isFocused = controller.focusedKey === entry.key}
212
+ {#if rowSnippet}
213
+ {@render rowSnippet(row, controller.columns, rowIndex, isSelected)}
214
+ {:else}
215
+ <tr
216
+ data-table-row
217
+ data-path={entry.key}
218
+ data-selected={isSelected || undefined}
219
+ data-focused={isFocused || undefined}
220
+ aria-selected={isSelected}
221
+ aria-rowindex={rowIndex + 1}
222
+ tabindex={isFocused ? 0 : -1}
223
+ >
224
+ {#each controller.columns as column (column.name)}
225
+ {#if cellSnippet}
226
+ <td data-table-cell data-column={column.name} data-label={column.label ?? column.name} style:text-align={column.align}>
227
+ {@render cellSnippet(getCellValue(row, column), column, row)}
228
+ </td>
229
+ {:else}
230
+ {@const cellIcon = getCellIcon(row, column)}
231
+ <td data-table-cell data-column={column.name} data-label={column.label ?? column.name} style:text-align={column.align}>
232
+ {#if cellIcon}
233
+ <span data-cell-icon class={cellIcon} aria-hidden="true"></span>
234
+ {/if}
235
+ <span data-cell-value>{formatCellValue(row, column)}</span>
236
+ </td>
237
+ {/if}
238
+ {/each}
239
+ </tr>
240
+ {/if}
241
+ {/each}
242
+ {/if}
243
+ </tbody>
244
+ </table>
245
+ </div>