@rokkit/ui 1.0.0-next.99 → 1.0.1

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 (81) 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/PaletteManager.svelte +360 -0
  26. package/src/components/Pill.svelte +83 -0
  27. package/src/components/ProgressBar.svelte +31 -0
  28. package/src/components/Range.svelte +346 -0
  29. package/src/components/Rating.svelte +115 -0
  30. package/src/components/Reveal.svelte +46 -0
  31. package/src/components/SearchFilter.svelte +88 -0
  32. package/src/components/Select.svelte +424 -0
  33. package/src/components/Shine.svelte +87 -0
  34. package/src/components/Stack.svelte +38 -0
  35. package/src/components/StatusList.svelte +18 -0
  36. package/src/components/Stepper.svelte +174 -0
  37. package/src/components/Swatch.svelte +102 -0
  38. package/src/components/Switch.svelte +71 -0
  39. package/src/components/Table.svelte +245 -0
  40. package/src/components/Tabs.svelte +202 -0
  41. package/src/components/Tilt.svelte +68 -0
  42. package/src/components/Timeline.svelte +61 -0
  43. package/src/components/Toggle.svelte +93 -0
  44. package/src/components/Toolbar.svelte +306 -0
  45. package/src/components/ToolbarGroup.svelte +17 -0
  46. package/src/components/Tooltip.svelte +121 -0
  47. package/src/components/Tree.svelte +143 -0
  48. package/src/components/UploadFileStatus.svelte +102 -0
  49. package/src/components/UploadProgress.svelte +127 -0
  50. package/src/components/UploadTarget.svelte +126 -0
  51. package/src/components/index.ts +48 -0
  52. package/src/index.ts +59 -0
  53. package/src/markdown-plugin.ts +12 -0
  54. package/src/types/button.ts +86 -0
  55. package/src/types/code.ts +46 -0
  56. package/src/types/floating-action.ts +123 -0
  57. package/src/types/floating-navigation.ts +80 -0
  58. package/src/types/index.ts +58 -0
  59. package/src/types/list.ts +200 -0
  60. package/src/types/menu.ts +95 -0
  61. package/src/types/palette.ts +160 -0
  62. package/src/types/range.ts +51 -0
  63. package/src/types/search-filter.ts +67 -0
  64. package/src/types/select.ts +176 -0
  65. package/src/types/switch.ts +68 -0
  66. package/src/types/table.ts +219 -0
  67. package/src/types/tabs.ts +103 -0
  68. package/src/types/timeline.ts +53 -0
  69. package/src/types/toggle.ts +68 -0
  70. package/src/types/toolbar.ts +167 -0
  71. package/src/types/tooltip.ts +21 -0
  72. package/src/types/tree.ts +250 -0
  73. package/src/types/upload-file-status.ts +45 -0
  74. package/src/types/upload-progress.ts +111 -0
  75. package/src/types/upload-target.ts +71 -0
  76. package/src/utils/palette.ts +618 -0
  77. package/src/utils/shiki.ts +114 -0
  78. package/src/utils/upload.js +145 -0
  79. package/LICENSE +0 -21
  80. package/src/index.js +0 -8
  81. package/src/wrappers.js +0 -2
@@ -0,0 +1,202 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Dropdown — Trigger that shows the selected value + dropdown panel with options.
4
+ *
5
+ * Similar to Menu but the trigger label reflects the currently selected value
6
+ * instead of a static label prop.
7
+ *
8
+ * Data attributes:
9
+ * data-dropdown — root container
10
+ * data-dropdown-trigger — trigger button
11
+ * data-dropdown-icon — optional icon in trigger
12
+ * data-dropdown-label — selected value text in trigger
13
+ * data-dropdown-arrow — expand/collapse chevron
14
+ * data-dropdown-panel — dropdown options panel
15
+ * data-dropdown-option — each option item
16
+ * data-dropdown-separator — separator between options
17
+ * data-active — marks the currently selected option
18
+ * data-open — present on root when panel is open
19
+ * data-size — size variant (sm | md | lg)
20
+ * data-align — panel alignment (start | end)
21
+ * data-direction — panel direction (down | up)
22
+ */
23
+ // @ts-nocheck
24
+ import { ProxyTree, Wrapper, messages } from '@rokkit/states'
25
+ import { Navigator, Trigger } from '@rokkit/actions'
26
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
27
+
28
+ interface DropdownIcons {
29
+ opened?: string
30
+ closed?: string
31
+ }
32
+
33
+ let {
34
+ items = [],
35
+ fields = {},
36
+ value = $bindable(),
37
+ placeholder = messages.select,
38
+ icon,
39
+ size = 'md',
40
+ disabled = false,
41
+ showArrow = true,
42
+ align = 'start',
43
+ direction = 'down',
44
+ icons: userIcons = {} as DropdownIcons,
45
+ onchange,
46
+ class: className = ''
47
+ }: {
48
+ items?: unknown[]
49
+ fields?: Record<string, string>
50
+ value?: unknown
51
+ placeholder?: string
52
+ icon?: string
53
+ size?: string
54
+ disabled?: boolean
55
+ showArrow?: boolean
56
+ align?: 'start' | 'end'
57
+ direction?: 'up' | 'down'
58
+ icons?: DropdownIcons
59
+ onchange?: (value: unknown, item: unknown) => void
60
+ class?: string
61
+ } = $props()
62
+
63
+ const icons = $derived({ ...DEFAULT_STATE_ICONS.selector, ...userIcons })
64
+
65
+ // ─── State ────────────────────────────────────────────────────────────────
66
+
67
+ let isOpen = $state(false)
68
+ let rootRef = $state<HTMLElement | null>(null)
69
+ let triggerRef = $state<HTMLElement | null>(null)
70
+ let panelRef = $state<HTMLElement | null>(null)
71
+
72
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
73
+
74
+ function handleSelect(v: unknown, proxy: unknown) {
75
+ if ((proxy as { disabled?: boolean }).disabled) return
76
+ value = v
77
+ onchange?.(v, (proxy as { original: unknown }).original)
78
+ isOpen = false
79
+ triggerRef?.focus()
80
+ }
81
+
82
+ const proxyTree = $derived(new ProxyTree(items, fields))
83
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
84
+
85
+ $effect(() => {
86
+ const w = wrapper
87
+ w.cancel = () => {
88
+ isOpen = false
89
+ triggerRef?.focus()
90
+ }
91
+ w.blur = () => {
92
+ isOpen = false
93
+ }
94
+ })
95
+
96
+ $effect(() => {
97
+ const _w = wrapper
98
+ if (isOpen) _w.first(null)
99
+ })
100
+
101
+ // ─── Trigger action ───────────────────────────────────────────────────────
102
+
103
+ $effect(() => {
104
+ if (!triggerRef || !rootRef || disabled) return
105
+ const t = new Trigger(triggerRef, rootRef, {
106
+ isOpen: () => isOpen,
107
+ onopen: () => {
108
+ isOpen = true
109
+ requestAnimationFrame(() => wrapper.first(null))
110
+ },
111
+ onclose: () => {
112
+ isOpen = false
113
+ },
114
+ onlast: () => requestAnimationFrame(() => wrapper.last(null))
115
+ })
116
+ return () => t.destroy()
117
+ })
118
+
119
+ // ─── Navigator on panel ───────────────────────────────────────────────────
120
+
121
+ $effect(() => {
122
+ if (!isOpen || !panelRef) return
123
+ const nav = new Navigator(panelRef, wrapper, { containScroll: true })
124
+ return () => nav.destroy()
125
+ })
126
+
127
+ // Focus sync — move DOM focus to focusedKey in panel
128
+ $effect(() => {
129
+ const key = wrapper.focusedKey
130
+ if (!isOpen || !panelRef || !key) return
131
+ requestAnimationFrame(() => {
132
+ const target = panelRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
133
+ if (target && target !== document.activeElement) target.focus()
134
+ })
135
+ })
136
+
137
+ // ─── Selected label ───────────────────────────────────────────────────────
138
+
139
+ const selectedLabel = $derived.by(() => {
140
+ if (value === undefined || value === null) return null
141
+ for (const node of wrapper.flatView) {
142
+ if (node.proxy.value === value) return node.proxy.label
143
+ }
144
+ return String(value)
145
+ })
146
+ </script>
147
+
148
+ <div
149
+ bind:this={rootRef}
150
+ data-dropdown
151
+ data-open={isOpen || undefined}
152
+ data-size={size}
153
+ data-disabled={disabled || undefined}
154
+ data-align={align}
155
+ data-direction={direction}
156
+ class={className || undefined}
157
+ >
158
+ <button
159
+ bind:this={triggerRef}
160
+ type="button"
161
+ data-dropdown-trigger
162
+ {disabled}
163
+ aria-haspopup="listbox"
164
+ aria-expanded={isOpen}
165
+ >
166
+ {#if icon}
167
+ <span data-dropdown-icon class={icon} aria-hidden="true"></span>
168
+ {/if}
169
+ <span data-dropdown-label>{selectedLabel ?? placeholder}</span>
170
+ {#if showArrow}
171
+ <span data-dropdown-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"
172
+ ></span>
173
+ {/if}
174
+ </button>
175
+
176
+ {#if isOpen}
177
+ <div bind:this={panelRef} data-dropdown-panel role="listbox">
178
+ {#each wrapper.flatView as node (node.key)}
179
+ {@const proxy = node.proxy}
180
+ {@const isActive = proxy.value === value}
181
+
182
+ {#if node.type === 'separator'}
183
+ <hr data-dropdown-separator />
184
+ {:else}
185
+ <button
186
+ type="button"
187
+ data-dropdown-option
188
+ data-path={node.key}
189
+ data-active={isActive || undefined}
190
+ data-disabled={proxy.disabled || undefined}
191
+ disabled={proxy.disabled || disabled}
192
+ role="option"
193
+ aria-selected={isActive}
194
+ tabindex="-1"
195
+ >
196
+ <span data-dropdown-option-label>{proxy.label}</span>
197
+ </button>
198
+ {/if}
199
+ {/each}
200
+ </div>
201
+ {/if}
202
+ </div>
@@ -0,0 +1,367 @@
1
+ <script lang="ts">
2
+ import type {
3
+ FloatingActionProps,
4
+ FloatingActionItem,
5
+ FloatingActionItemSnippet,
6
+ FloatingActionItemHandlers,
7
+ FloatingActionIcons
8
+ } from '../types/floating-action.js'
9
+ import { getSnippet } from '../types/floating-action.js'
10
+ import { ProxyItem } from '@rokkit/states'
11
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
12
+
13
+ let {
14
+ items = [],
15
+ fields: userFields,
16
+ icons: userIcons = {} as FloatingActionIcons,
17
+ label = 'Actions',
18
+ size = 'md',
19
+ position = 'bottom-right',
20
+ expand = 'vertical',
21
+ itemAlign = 'center',
22
+ disabled = false,
23
+ open = $bindable(false),
24
+ backdrop = true,
25
+ contained = false,
26
+ onselect,
27
+ onopen,
28
+ onclose,
29
+ class: className = '',
30
+ item: itemSnippet,
31
+ ...snippets
32
+ }: FloatingActionProps & { [key: string]: FloatingActionItemSnippet | unknown } = $props()
33
+
34
+ const icons = $derived({
35
+ add: DEFAULT_STATE_ICONS.action.add,
36
+ close: DEFAULT_STATE_ICONS.action.close,
37
+ ...userIcons
38
+ })
39
+
40
+ /**
41
+ * Create a ProxyItem for the given item
42
+ */
43
+ function createProxy(item: FloatingActionItem): ProxyItem {
44
+ return new ProxyItem(item, userFields)
45
+ }
46
+
47
+ let fabRef = $state<HTMLDivElement | null>(null)
48
+ let focusedIndex = $state(-1)
49
+
50
+ // Flatten items for keyboard navigation (excluding disabled)
51
+ const flatItems = $derived.by(() => {
52
+ return items
53
+ .map((item) => ({ proxy: createProxy(item), original: item }))
54
+ .filter((item) => !item.proxy.disabled)
55
+ })
56
+
57
+ /**
58
+ * Toggle the FAB open/closed
59
+ */
60
+ function toggle() {
61
+ if (disabled) return
62
+ if (open) {
63
+ close()
64
+ } else {
65
+ openMenu()
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Open the FAB menu
71
+ */
72
+ function openMenu() {
73
+ if (disabled || open) return
74
+ open = true
75
+ focusedIndex = 0
76
+ onopen?.()
77
+ }
78
+
79
+ /**
80
+ * Close the FAB menu
81
+ */
82
+ function close() {
83
+ if (!open) return
84
+ open = false
85
+ focusedIndex = -1
86
+ onclose?.()
87
+ }
88
+
89
+ /**
90
+ * Handle item selection
91
+ */
92
+ function handleItemClick(item: { proxy: ProxyItem; original: FloatingActionItem }) {
93
+ if (item.proxy.disabled) return
94
+ onselect?.(item.proxy.value, item.original)
95
+ close()
96
+ // Return focus to trigger
97
+ const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
98
+ trigger?.focus()
99
+ }
100
+
101
+ /**
102
+ * Move DOM focus to the menu item at the given index
103
+ */
104
+ function focusDomItem(index: number) {
105
+ const menu = fabRef?.querySelector('[data-fab-menu]')
106
+ if (!menu) return
107
+ const menuItems = menu.querySelectorAll('[data-fab-item]:not([data-disabled])')
108
+ const menuItem = menuItems[index] as HTMLElement | undefined
109
+ menuItem?.focus()
110
+ }
111
+
112
+ /**
113
+ * Focus an item by index
114
+ */
115
+ function focusItem(index: number) {
116
+ if (index < 0 || index >= flatItems.length) return
117
+ focusedIndex = index
118
+ focusDomItem(index)
119
+ }
120
+
121
+ /**
122
+ * Handle keyboard navigation on trigger
123
+ */
124
+ function handleTriggerKeyDown(event: KeyboardEvent) {
125
+ if (event.key === 'Enter' || event.key === ' ') {
126
+ event.preventDefault()
127
+ toggle()
128
+ } else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
129
+ event.preventDefault()
130
+ openMenu()
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Close menu and return focus to trigger
136
+ */
137
+ function closeAndFocusTrigger() {
138
+ close()
139
+ const trigger = fabRef?.querySelector('[data-fab-trigger]') as HTMLElement | undefined
140
+ trigger?.focus()
141
+ }
142
+
143
+ /**
144
+ * Activate the currently focused item
145
+ */
146
+ function activateFocusedItem() {
147
+ if (focusedIndex >= 0 && focusedIndex < flatItems.length) {
148
+ handleItemClick(flatItems[focusedIndex])
149
+ }
150
+ }
151
+
152
+ function wrapNext(current: number, last: number): number {
153
+ return current < last ? current + 1 : 0
154
+ }
155
+
156
+ function wrapPrev(current: number, last: number): number {
157
+ return current > 0 ? current - 1 : last
158
+ }
159
+
160
+ function nextFabIndex(key: string, current: number, last: number): number {
161
+ if (key === 'ArrowDown') return wrapNext(current, last)
162
+ if (key === 'ArrowUp') return wrapPrev(current, last)
163
+ if (key === 'Home') return 0
164
+ if (key === 'End') return last
165
+ return -1
166
+ }
167
+
168
+ function handleFabNavMove(event: KeyboardEvent): boolean {
169
+ const last = flatItems.length - 1
170
+ const next = nextFabIndex(event.key, focusedIndex, last)
171
+ if (next === -1) return false
172
+ focusItem(next)
173
+ return true
174
+ }
175
+
176
+ function isActivateKey(key: string): boolean {
177
+ return key === 'Enter' || key === ' '
178
+ }
179
+
180
+ /**
181
+ * Handle keyboard navigation when menu is open
182
+ */
183
+ function handleKeyDown(event: KeyboardEvent) {
184
+ if (!open) return
185
+ if (event.key === 'Escape') {
186
+ event.preventDefault()
187
+ closeAndFocusTrigger()
188
+ } else if (isActivateKey(event.key)) {
189
+ event.preventDefault()
190
+ activateFocusedItem()
191
+ } else if (handleFabNavMove(event)) {
192
+ event.preventDefault()
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Handle item-specific keyboard events
198
+ */
199
+ function handleItemKeyDown(
200
+ event: KeyboardEvent,
201
+ item: { proxy: ProxyItem; original: FloatingActionItem }
202
+ ) {
203
+ if (event.key === 'Enter' || event.key === ' ') {
204
+ event.preventDefault()
205
+ handleItemClick(item)
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Handle click outside to close
211
+ */
212
+ function handleClickOutside(event: MouseEvent) {
213
+ if (fabRef && !fabRef.contains(event.target as Node)) {
214
+ close()
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Handle backdrop click
220
+ */
221
+ function handleBackdropClick() {
222
+ close()
223
+ }
224
+
225
+ /**
226
+ * Create handlers object for custom snippets
227
+ */
228
+ function createHandlers(item: {
229
+ proxy: ProxyItem
230
+ original: FloatingActionItem
231
+ }): FloatingActionItemHandlers {
232
+ return {
233
+ onclick: () => handleItemClick(item),
234
+ onkeydown: (event: KeyboardEvent) => handleItemKeyDown(event, item)
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Resolve which snippet to use for an item
240
+ */
241
+ function resolveItemSnippet(proxy: ProxyItem): FloatingActionItemSnippet | null {
242
+ const snippetName = proxy.get('snippet')
243
+ if (snippetName) {
244
+ const namedSnippet = getSnippet(snippets, snippetName)
245
+ if (namedSnippet) {
246
+ return namedSnippet as FloatingActionItemSnippet
247
+ }
248
+ }
249
+ return itemSnippet ?? null
250
+ }
251
+
252
+ /**
253
+ * Calculate item animation delay for stagger effect
254
+ */
255
+ function getItemDelay(index: number): string {
256
+ return `${index * 50}ms`
257
+ }
258
+
259
+ // Set up click outside listener when open
260
+ $effect(() => {
261
+ if (open) {
262
+ document.addEventListener('click', handleClickOutside, true)
263
+ document.addEventListener('keydown', handleKeyDown)
264
+ // Focus first item after animation starts
265
+ requestAnimationFrame(() => {
266
+ if (flatItems.length > 0) {
267
+ focusItem(0)
268
+ }
269
+ })
270
+ }
271
+ return () => {
272
+ document.removeEventListener('click', handleClickOutside, true)
273
+ document.removeEventListener('keydown', handleKeyDown)
274
+ }
275
+ })
276
+ </script>
277
+
278
+ {#snippet defaultItem(
279
+ proxy: ProxyItem,
280
+ handlers: FloatingActionItemHandlers,
281
+ index: number,
282
+ total: number
283
+ )}
284
+ <button
285
+ type="button"
286
+ data-fab-item
287
+ data-fab-index={index}
288
+ data-disabled={proxy.disabled || undefined}
289
+ disabled={proxy.disabled || disabled}
290
+ aria-label={proxy.label}
291
+ style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
292
+ onclick={handlers.onclick}
293
+ onkeydown={handlers.onkeydown}
294
+ >
295
+ {#if proxy.get('icon')}
296
+ <span data-fab-item-icon class={proxy.get('icon')} aria-hidden="true"></span>
297
+ {/if}
298
+ {#if proxy.label}
299
+ <span data-fab-item-label>{proxy.label}</span>
300
+ {/if}
301
+ </button>
302
+ {/snippet}
303
+
304
+ {#snippet renderItem(
305
+ item: { proxy: ProxyItem; original: FloatingActionItem },
306
+ index: number,
307
+ total: number
308
+ )}
309
+ {@const customSnippet = resolveItemSnippet(item.proxy)}
310
+ {@const handlers = createHandlers(item)}
311
+ {#if customSnippet}
312
+ <div
313
+ data-fab-item
314
+ data-fab-item-custom
315
+ data-fab-index={index}
316
+ data-disabled={item.proxy.disabled || undefined}
317
+ style="--fab-index: {index}; --fab-total: {total}; --fab-delay: {getItemDelay(index)}"
318
+ >
319
+ <svelte:boundary>
320
+ {@render customSnippet(item.original, item.proxy.fields, handlers)}
321
+ {#snippet failed()}
322
+ {@render defaultItem(item.proxy, handlers, index, total)}
323
+ {/snippet}
324
+ </svelte:boundary>
325
+ </div>
326
+ {:else}
327
+ {@render defaultItem(item.proxy, handlers, index, total)}
328
+ {/if}
329
+ {/snippet}
330
+
331
+ <div
332
+ bind:this={fabRef}
333
+ data-fab
334
+ data-open={open || undefined}
335
+ data-size={size}
336
+ data-position={position}
337
+ data-expand={expand}
338
+ data-item-align={itemAlign}
339
+ data-disabled={disabled || undefined}
340
+ data-contained={contained || undefined}
341
+ class={className || undefined}
342
+ >
343
+ {#if backdrop && open}
344
+ <div data-fab-backdrop role="presentation" onclick={handleBackdropClick}></div>
345
+ {/if}
346
+
347
+ {#if open}
348
+ <div data-fab-menu role="menu" aria-label={label}>
349
+ {#each flatItems as item, index (index)}
350
+ {@render renderItem(item, index, flatItems.length)}
351
+ {/each}
352
+ </div>
353
+ {/if}
354
+
355
+ <button
356
+ type="button"
357
+ data-fab-trigger
358
+ {disabled}
359
+ aria-label={label}
360
+ aria-haspopup="menu"
361
+ aria-expanded={open}
362
+ onclick={toggle}
363
+ onkeydown={handleTriggerKeyDown}
364
+ >
365
+ <span data-fab-icon class={open ? icons.close : icons.add} aria-hidden="true"></span>
366
+ </button>
367
+ </div>