@rokkit/ui 1.0.0-next.127 → 1.0.0-next.129

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 (54) hide show
  1. package/package.json +6 -16
  2. package/src/components/BreadCrumbs.svelte +37 -17
  3. package/src/components/Button.svelte +11 -5
  4. package/src/components/Carousel.svelte +11 -6
  5. package/src/components/Code.svelte +6 -2
  6. package/src/components/FloatingAction.svelte +24 -21
  7. package/src/components/FloatingNavigation.svelte +36 -29
  8. package/src/components/Grid.svelte +128 -0
  9. package/src/components/ItemContent.svelte +21 -20
  10. package/src/components/LazyTree.svelte +165 -0
  11. package/src/components/List.svelte +146 -434
  12. package/src/components/Menu.svelte +195 -346
  13. package/src/components/MultiSelect.svelte +238 -390
  14. package/src/components/PaletteManager.svelte +15 -5
  15. package/src/components/Pill.svelte +19 -14
  16. package/src/components/Range.svelte +8 -3
  17. package/src/components/Rating.svelte +19 -9
  18. package/src/components/Reveal.svelte +1 -13
  19. package/src/components/SearchFilter.svelte +11 -3
  20. package/src/components/Select.svelte +265 -454
  21. package/src/components/Stepper.svelte +9 -6
  22. package/src/components/Switch.svelte +11 -11
  23. package/src/components/Table.svelte +0 -1
  24. package/src/components/Tabs.svelte +96 -172
  25. package/src/components/Timeline.svelte +5 -5
  26. package/src/components/Toggle.svelte +55 -119
  27. package/src/components/Toolbar.svelte +24 -23
  28. package/src/components/Tree.svelte +115 -584
  29. package/src/components/UploadFileStatus.svelte +83 -0
  30. package/src/components/UploadProgress.svelte +131 -0
  31. package/src/components/UploadTarget.svelte +124 -0
  32. package/src/components/index.ts +5 -0
  33. package/src/index.ts +6 -1
  34. package/src/types/button.ts +3 -0
  35. package/src/types/code.ts +4 -4
  36. package/src/types/floating-action.ts +13 -8
  37. package/src/types/floating-navigation.ts +14 -2
  38. package/src/types/index.ts +5 -3
  39. package/src/types/list.ts +10 -6
  40. package/src/types/menu.ts +38 -138
  41. package/src/types/palette.ts +17 -0
  42. package/src/types/select.ts +33 -63
  43. package/src/types/switch.ts +9 -5
  44. package/src/types/table.ts +6 -6
  45. package/src/types/tabs.ts +13 -34
  46. package/src/types/timeline.ts +5 -3
  47. package/src/types/toggle.ts +15 -56
  48. package/src/types/toolbar.ts +1 -1
  49. package/src/types/tree.ts +9 -18
  50. package/src/types/upload-file-status.ts +45 -0
  51. package/src/types/upload-progress.ts +111 -0
  52. package/src/types/upload-target.ts +68 -0
  53. package/src/utils/upload.js +128 -0
  54. package/src/types/item-proxy.ts +0 -358
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
4
+ import { messages } from '@rokkit/states'
3
5
 
4
6
  interface StepperStep {
5
7
  /** Step label (shown below circle) */
@@ -15,8 +17,8 @@
15
17
  }
16
18
 
17
19
  interface StepperIcons {
18
- /** Icon class for completed state (default: i-lucide:check) */
19
- completed?: string
20
+ /** Icon class for check/completed state */
21
+ check?: string
20
22
  }
21
23
 
22
24
  interface StepperProps {
@@ -41,7 +43,7 @@
41
43
  }
42
44
 
43
45
  const defaultIcons: StepperIcons = {
44
- completed: 'i-lucide:check'
46
+ check: DEFAULT_STATE_ICONS.action.check
45
47
  }
46
48
 
47
49
  let {
@@ -50,11 +52,12 @@
50
52
  currentStage = $bindable(0),
51
53
  linear = false,
52
54
  orientation = 'horizontal',
55
+ label = messages.current.stepper.label,
53
56
  icons: userIcons,
54
57
  onclick,
55
58
  content,
56
59
  class: className = ''
57
- }: StepperProps = $props()
60
+ }: StepperProps & { label?: string } = $props()
58
61
 
59
62
  const icons = $derived<StepperIcons>({ ...defaultIcons, ...userIcons })
60
63
 
@@ -105,7 +108,7 @@
105
108
  data-orientation={orientation}
106
109
  class={className || undefined}
107
110
  role="group"
108
- aria-label="Progress"
111
+ aria-label={label}
109
112
  >
110
113
  {#each steps as step, index (index)}
111
114
  <!-- Connector line before step (except first) -->
@@ -133,7 +136,7 @@
133
136
  onclick={() => handleStepClick(index)}
134
137
  >
135
138
  {#if step.completed}
136
- <span class={icons.completed} aria-hidden="true"></span>
139
+ <span data-stepper-check-icon class={icons.check} aria-hidden="true"></span>
137
140
  {:else}
138
141
  {step.label ?? index + 1}
139
142
  {/if}
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SwitchProps, SwitchItem } from '../types/switch.js'
3
- import { ItemProxy } from '../types/item-proxy.js'
3
+ import { ProxyItem } from '@rokkit/states'
4
4
 
5
5
  const DEFAULT_OPTIONS: [SwitchItem, SwitchItem] = [false, true]
6
6
 
@@ -15,15 +15,15 @@
15
15
  class: className = ''
16
16
  }: SwitchProps = $props()
17
17
 
18
- let offProxy = $derived(new ItemProxy(options[0] as Record<string, unknown>, userFields))
19
- let onProxy = $derived(new ItemProxy(options[1] as Record<string, unknown>, userFields))
20
- let isChecked = $derived(value === onProxy.itemValue)
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
21
  let currentProxy = $derived(isChecked ? onProxy : offProxy)
22
22
 
23
23
  function toggle() {
24
24
  if (disabled) return
25
25
  const next = isChecked ? offProxy : onProxy
26
- const nextValue = next.itemValue
26
+ const nextValue = next.value
27
27
  value = nextValue
28
28
  onchange?.(nextValue, next.original as SwitchItem)
29
29
  }
@@ -55,8 +55,8 @@
55
55
  data-switch-size={size}
56
56
  data-switch-disabled={disabled || undefined}
57
57
  aria-checked={isChecked}
58
- aria-label={currentProxy.text || undefined}
59
- title={currentProxy.description ?? currentProxy.text ?? undefined}
58
+ aria-label={currentProxy.label || undefined}
59
+ title={currentProxy.get('subtext') ?? currentProxy.label ?? undefined}
60
60
  {disabled}
61
61
  class={className || undefined}
62
62
  onclick={toggle}
@@ -64,12 +64,12 @@
64
64
  >
65
65
  <span data-switch-track>
66
66
  <span data-switch-thumb>
67
- {#if currentProxy.icon}
68
- <span data-switch-icon class={currentProxy.icon} aria-hidden="true"></span>
67
+ {#if currentProxy.get('icon')}
68
+ <span data-switch-icon class={currentProxy.get('icon')} aria-hidden="true"></span>
69
69
  {/if}
70
70
  </span>
71
71
  </span>
72
- {#if showLabels && currentProxy.text}
73
- <span data-switch-label>{currentProxy.text}</span>
72
+ {#if showLabels && currentProxy.label}
73
+ <span data-switch-label>{currentProxy.label}</span>
74
74
  {/if}
75
75
  </button>
@@ -206,7 +206,6 @@
206
206
  data-path={entry.key}
207
207
  data-selected={isSelected || undefined}
208
208
  data-focused={isFocused || undefined}
209
- role="row"
210
209
  aria-selected={isSelected}
211
210
  aria-rowindex={rowIndex + 1}
212
211
  tabindex={isFocused ? 0 : -1}
@@ -1,13 +1,32 @@
1
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'
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'
7
26
 
8
27
  let {
9
28
  options = [],
10
- fields: userFields,
29
+ fields: userFields = {},
11
30
  value = $bindable(),
12
31
  orientation = 'horizontal',
13
32
  position = 'before',
@@ -16,177 +35,72 @@
16
35
  editable = false,
17
36
  placeholder = 'Select a tab to view its content.',
18
37
  disabled = false,
38
+ labels: userLabels = {},
19
39
  class: className = '',
20
40
  onchange,
21
41
  onselect,
22
42
  onadd,
23
43
  onremove,
24
- tabItem: tabItemSnippet,
25
- tabPanel: tabPanelSnippet,
26
- empty: emptySnippet
27
- }: TabsProps = $props()
44
+ ...snippets
45
+ }: TabsProps & { labels?: Record<string, string>; [key: string]: unknown } = $props()
28
46
 
29
- /** Content field name from user fields or default */
30
- const contentField = $derived((userFields as Record<string, string> | undefined)?.content ?? 'content')
47
+ const labels = $derived({ ...messages.current.tabs, ...userLabels })
31
48
 
32
- let controller = untrack(() => new ListController(options, value, userFields))
33
- let containerRef: HTMLElement | null = $state(null)
34
- let lastSyncedValue: unknown = value
49
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
35
50
 
36
- $effect(() => {
37
- controller.update(options)
38
- })
51
+ const proxyTree = $derived(new ProxyTree(options, userFields))
52
+ const wrapper = $derived(new Wrapper(proxyTree, { onchange, onselect }))
39
53
 
40
- // Sync controller focus when value changes externally
41
- $effect(() => {
42
- if (value !== lastSyncedValue) {
43
- lastSyncedValue = value
44
- controller.moveToValue(value)
45
- }
46
- })
54
+ // ─── Navigator ────────────────────────────────────────────────────────────
55
+
56
+ let containerRef = $state<HTMLElement | null>(null)
47
57
 
48
- // Focus the tab matching controller.focusedKey on navigator move events
49
58
  $effect(() => {
50
59
  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)
60
+ const nav = new Navigator(containerRef, wrapper, { orientation })
61
+ return () => nav.destroy()
73
62
  })
74
63
 
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
- }
64
+ // ─── Sync external value → focused key ────────────────────────────────────
88
65
 
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
- }
66
+ $effect(() => {
67
+ wrapper.moveToValue(value)
68
+ })
117
69
 
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
- }
70
+ // ─── Editable handlers ────────────────────────────────────────────────────
137
71
 
138
72
  function handleAdd() {
139
73
  onadd?.()
140
74
  }
141
75
 
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]
76
+ function handleRemove(proxy: ProxyItem) {
77
+ onremove?.(proxy.value)
151
78
  }
152
79
  </script>
153
80
 
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>
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}
185
99
  {/snippet}
186
100
 
187
- {#snippet defaultPanel(item: TabsItem)}
101
+ {#snippet defaultPanel(proxy: ProxyItem)}
188
102
  <div data-tabs-content>
189
- {getContent(item)}
103
+ {proxy.get('content')}
190
104
  </div>
191
105
  {/snippet}
192
106
 
@@ -204,35 +118,45 @@
204
118
  data-disabled={disabled || undefined}
205
119
  class={className || undefined}
206
120
  aria-label={name}
207
- use:navigator={{ wrapper: controller, orientation }}
208
121
  >
209
122
  {#if options.length === 0}
210
123
  <div data-tabs-empty>
211
- {#if emptySnippet}
212
- {@render emptySnippet()}
124
+ {#if snippets.empty}
125
+ {@render snippets.empty()}
213
126
  {:else}
214
127
  {@render defaultEmpty()}
215
128
  {/if}
216
129
  </div>
217
130
  {:else}
218
131
  <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)}
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)}
224
136
 
225
- {#if tabItemSnippet}
226
- {@render tabItemSnippet(option, userFields ?? {}, handlers, selected)}
227
- {:else}
228
- {@render defaultTabItem(proxy, handlers, selected, key)}
229
- {/if}
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>
230
154
  {/each}
231
155
  {#if editable}
232
156
  <button
233
157
  type="button"
234
158
  data-tabs-add
235
- aria-label="Add tab"
159
+ aria-label={labels.add}
236
160
  onclick={handleAdd}
237
161
  >
238
162
  <span class="i-lucide:plus" aria-hidden="true"></span>
@@ -240,21 +164,21 @@
240
164
  {/if}
241
165
  </div>
242
166
 
243
- {#each options as option, index (index)}
244
- {@const proxy = createProxy(option)}
245
- {@const active = isSelected(proxy)}
167
+ {#each wrapper.flatView as node (node.key)}
168
+ {@const proxy = node.proxy}
169
+ {@const active = proxy.value === value}
246
170
 
247
171
  <div
248
172
  data-tabs-panel
249
173
  data-panel-active={active || undefined}
250
174
  role="tabpanel"
251
- id="tab-panel-{index}"
252
- aria-labelledby="tab-{index}"
175
+ id="tab-panel-{node.key}"
176
+ aria-labelledby="tab-{node.key}"
253
177
  >
254
- {#if tabPanelSnippet}
255
- {@render tabPanelSnippet(option, userFields ?? {})}
178
+ {#if snippets.tabPanel}
179
+ {@render snippets.tabPanel(proxy)}
256
180
  {:else}
257
- {@render defaultPanel(option)}
181
+ {@render defaultPanel(proxy)}
258
182
  {/if}
259
183
  </div>
260
184
  {/each}
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { TimelineProps } from '../types/timeline.js'
3
3
  import { defaultTimelineFields, defaultTimelineIcons } from '../types/timeline.js'
4
- import { ItemProxy } from '../types/item-proxy.js'
4
+ import { ProxyItem } from '@rokkit/states'
5
5
 
6
6
  let {
7
7
  items = [],
@@ -17,10 +17,10 @@
17
17
 
18
18
  <div data-timeline class={className || undefined} role="list">
19
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}
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
24
  {@const completed = Boolean(item.completed)}
25
25
  {@const active = Boolean(item.active)}
26
26