@rokkit/ui 1.0.0-next.98 → 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,367 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MultiSelect — Trigger + dropdown with List-style flatView content.
4
+ *
5
+ * Same architecture as Select but with toggle selection (dropdown stays open),
6
+ * checkbox indicators, and tags display in trigger.
7
+ *
8
+ * Data attributes:
9
+ * data-select / data-multiselect — root container (both for theme compat)
10
+ * data-select-trigger — trigger button
11
+ * data-select-value — selected value display area
12
+ * data-select-tags — tags container
13
+ * data-select-tag — individual tag
14
+ * data-select-tag-text — tag text
15
+ * data-select-tag-remove — tag remove button
16
+ * data-select-count — count indicator when exceeding maxDisplay
17
+ * data-select-placeholder — placeholder text
18
+ * data-select-arrow — dropdown arrow icon
19
+ * data-select-dropdown — dropdown container
20
+ * data-select-option — option items
21
+ * data-select-checkbox — checkbox indicator
22
+ * data-select-group-label — group header (non-interactive)
23
+ * data-select-group-icon — icon inside group label
24
+ * data-select-divider — divider between groups
25
+ * data-path, data-selected, data-checked, data-disabled, data-open, data-size
26
+ */
27
+ // @ts-nocheck
28
+ import type { ProxyItem } from '@rokkit/states'
29
+ import { Wrapper, ProxyTree, messages } from '@rokkit/states'
30
+ import { SvelteSet } from 'svelte/reactivity'
31
+ import { Navigator, Trigger } from '@rokkit/actions'
32
+ import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
33
+ import ItemContent from './ItemContent.svelte'
34
+
35
+ interface MultiSelectIcons {
36
+ opened?: string
37
+ closed?: string
38
+ checked?: string
39
+ remove?: string
40
+ }
41
+
42
+ let {
43
+ items = [],
44
+ fields = {},
45
+ value = $bindable<unknown[]>([]),
46
+ selected = $bindable<unknown[]>([]),
47
+ placeholder = messages.select,
48
+ size = 'md',
49
+ disabled = false,
50
+ maxDisplay = 3,
51
+ align = 'start',
52
+ direction = 'down',
53
+ icons: userIcons = {} as MultiSelectIcons,
54
+ onchange,
55
+ class: className = '',
56
+ ...snippets
57
+ }: {
58
+ items?: unknown[]
59
+ fields?: Record<string, string>
60
+ value?: unknown[]
61
+ selected?: unknown[]
62
+ placeholder?: string
63
+ size?: string
64
+ disabled?: boolean
65
+ maxDisplay?: number
66
+ align?: 'start' | 'end'
67
+ direction?: 'up' | 'down'
68
+ icons?: MultiSelectIcons
69
+ onchange?: (values: unknown[], items: unknown[]) => void
70
+ class?: string
71
+ [key: string]: unknown
72
+ } = $props()
73
+
74
+ const icons = $derived({
75
+ ...DEFAULT_STATE_ICONS.selector,
76
+ ...DEFAULT_STATE_ICONS.checkbox,
77
+ ...DEFAULT_STATE_ICONS.action,
78
+ ...userIcons
79
+ })
80
+
81
+ // ─── Dropdown state ───────────────────────────────────────────────────────
82
+
83
+ let isOpen = $state(false)
84
+ let selectRef = $state<HTMLElement | null>(null)
85
+ let triggerRef = $state<HTMLElement | null>(null)
86
+ let dropdownRef = $state<HTMLElement | null>(null)
87
+
88
+ // ─── Pre-process items ────────────────────────────────────────────────────
89
+
90
+ const childrenField = $derived(fields?.children || 'children')
91
+
92
+ // Force groups expanded + disabled (non-navigable labels)
93
+ const processedItems = $derived(
94
+ items.map((item) => {
95
+ const children = item[childrenField]
96
+ if (Array.isArray(children) && children.length > 0) {
97
+ return { ...item, expanded: true, disabled: true }
98
+ }
99
+ return item
100
+ })
101
+ )
102
+
103
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
104
+
105
+ function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
106
+ if (proxy.disabled) return
107
+ toggleItemSelection(extractedValue)
108
+ }
109
+
110
+ const proxyTree = $derived(new ProxyTree(processedItems, fields))
111
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
112
+
113
+ // Override cancel/blur to close dropdown
114
+ $effect(() => {
115
+ const w = wrapper
116
+ w.cancel = () => {
117
+ isOpen = false
118
+ triggerRef?.focus()
119
+ }
120
+ w.blur = () => {
121
+ isOpen = false
122
+ }
123
+ })
124
+
125
+ // When wrapper recreates while open, focus first item
126
+ $effect(() => {
127
+ const _w = wrapper
128
+ if (isOpen) _w.first(null)
129
+ })
130
+
131
+ // ─── Selection logic ──────────────────────────────────────────────────────
132
+
133
+ function isItemSelected(extractedValue: unknown): boolean {
134
+ return (value ?? []).some((v) => v === extractedValue)
135
+ }
136
+
137
+ function buildSelectedItems(newValues: unknown[]): unknown[] {
138
+ const result: unknown[] = []
139
+ for (const [, proxy] of wrapper.lookup) {
140
+ if (isLeafWithValue(proxy, newValues)) result.push(proxy.original)
141
+ }
142
+ return result
143
+ }
144
+
145
+ function applySelection(newValues: unknown[]) {
146
+ const newItems = buildSelectedItems(newValues)
147
+ value = newValues
148
+ selected = newItems
149
+ onchange?.(newValues, newItems)
150
+ }
151
+
152
+ function toggleItemSelection(extractedValue: unknown) {
153
+ const currentValues = value ?? []
154
+ const alreadySelected = currentValues.some((v) => v === extractedValue)
155
+ const newValues = alreadySelected
156
+ ? currentValues.filter((v) => v !== extractedValue)
157
+ : [...currentValues, extractedValue]
158
+ applySelection(newValues)
159
+ }
160
+
161
+ function removeTag(extractedValue: unknown) {
162
+ const newValues = (value ?? []).filter((v) => v !== extractedValue)
163
+ applySelection(newValues)
164
+ }
165
+
166
+ // ─── Selected items for tags display ──────────────────────────────────────
167
+
168
+ function isLeafWithValue(proxy: ProxyItem, vals: unknown[]): boolean {
169
+ return !proxy.hasChildren && vals.some((v) => v === proxy.value)
170
+ }
171
+
172
+ function computeSelectedProxies(): ProxyItem[] {
173
+ const vals = value ?? []
174
+ if (vals.length === 0) return []
175
+ const result: ProxyItem[] = []
176
+ for (const [, proxy] of wrapper.lookup) {
177
+ if (isLeafWithValue(proxy, vals)) result.push(proxy)
178
+ }
179
+ return result
180
+ }
181
+
182
+ const selectedProxies = $derived.by(computeSelectedProxies)
183
+
184
+ // ─── Trigger action ───────────────────────────────────────────────────────
185
+
186
+ $effect(() => {
187
+ if (!triggerRef || !selectRef || disabled) return
188
+ const t = new Trigger(triggerRef, selectRef, {
189
+ isOpen: () => isOpen,
190
+ onopen: () => {
191
+ isOpen = true
192
+ requestAnimationFrame(() => wrapper.first(null))
193
+ },
194
+ onclose: () => {
195
+ isOpen = false
196
+ },
197
+ onlast: () => requestAnimationFrame(() => wrapper.last(null))
198
+ })
199
+ return () => t.destroy()
200
+ })
201
+
202
+ // ─── Navigator on dropdown ────────────────────────────────────────────────
203
+
204
+ $effect(() => {
205
+ if (!isOpen || !dropdownRef) return
206
+ const dir = getComputedStyle(dropdownRef).direction || 'ltr'
207
+ const nav = new Navigator(dropdownRef, wrapper, { dir, containScroll: true })
208
+ return () => nav.destroy()
209
+ })
210
+
211
+ // DOM focus sync
212
+ $effect(() => {
213
+ const key = wrapper.focusedKey
214
+ if (!isOpen || !dropdownRef || !key) return
215
+ requestAnimationFrame(() => {
216
+ const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
217
+ if (target && target !== document.activeElement) {
218
+ target.focus()
219
+ target.scrollIntoView?.({ block: 'nearest' })
220
+ }
221
+ })
222
+ })
223
+
224
+ // ─── Helpers ──────────────────────────────────────────────────────────────
225
+
226
+ const groupDividers = $derived.by(() => {
227
+ const set = new SvelteSet<string>()
228
+ let foundFirst = false
229
+ for (const node of wrapper.flatView) {
230
+ if (node.hasChildren) {
231
+ if (foundFirst) set.add(node.key)
232
+ foundFirst = true
233
+ }
234
+ }
235
+ return set
236
+ })
237
+ </script>
238
+
239
+ {#snippet defaultOptionContent(proxy: ProxyItem)}
240
+ <ItemContent {proxy} />
241
+ {/snippet}
242
+
243
+ {#snippet defaultGroupContent(proxy: ProxyItem)}
244
+ {#if proxy.get('icon')}
245
+ <span data-select-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
246
+ {/if}
247
+ <span>{proxy.label}</span>
248
+ {/snippet}
249
+
250
+ <div
251
+ bind:this={selectRef}
252
+ data-select
253
+ data-multiselect
254
+ data-open={isOpen || undefined}
255
+ data-size={size}
256
+ data-disabled={disabled || undefined}
257
+ data-align={align}
258
+ data-direction={direction}
259
+ class={className || undefined}
260
+ >
261
+ <button
262
+ bind:this={triggerRef}
263
+ type="button"
264
+ data-select-trigger
265
+ {disabled}
266
+ aria-haspopup="listbox"
267
+ aria-expanded={isOpen}
268
+ >
269
+ <span data-select-value>
270
+ {#if selectedProxies.length > 0}
271
+ {#if selectedProxies.length <= maxDisplay}
272
+ <span data-select-tags>
273
+ {#each selectedProxies as proxy (proxy.value)}
274
+ <span data-select-tag>
275
+ <span data-select-tag-text>{proxy.label}</span>
276
+ <span
277
+ role="button"
278
+ tabindex="0"
279
+ data-select-tag-remove
280
+ aria-label="Remove {proxy.label}"
281
+ onclick={(e) => {
282
+ e.stopPropagation()
283
+ removeTag(proxy.value)
284
+ }}
285
+ onkeydown={(e) => {
286
+ if (e.key === 'Enter' || e.key === ' ') {
287
+ e.preventDefault()
288
+ e.stopPropagation()
289
+ removeTag(proxy.value)
290
+ }
291
+ }}
292
+ >
293
+ <span class={icons.remove} aria-hidden="true"></span>
294
+ </span>
295
+ </span>
296
+ {/each}
297
+ </span>
298
+ {:else}
299
+ <span data-select-count>{selectedProxies.length} selected</span>
300
+ {/if}
301
+ {:else}
302
+ <span data-select-placeholder>{placeholder}</span>
303
+ {/if}
304
+ </span>
305
+ <span data-select-arrow class={icons.opened} aria-hidden="true"></span>
306
+ </button>
307
+
308
+ {#if isOpen}
309
+ <div
310
+ bind:this={dropdownRef}
311
+ data-select-dropdown
312
+ role="listbox"
313
+ aria-multiselectable="true"
314
+ aria-orientation="vertical"
315
+ >
316
+ {#each wrapper.flatView as node (node.key)}
317
+ {@const proxy = node.proxy}
318
+ {@const sel = !node.hasChildren && isItemSelected(proxy.value)}
319
+ {@const content = resolveSnippet(
320
+ snippets as Record<string, unknown>,
321
+ proxy,
322
+ node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET
323
+ )}
324
+
325
+ {#if node.type === 'separator'}
326
+ <hr data-select-separator />
327
+ {:else if node.hasChildren}
328
+ {#if groupDividers.has(node.key)}
329
+ <div data-select-divider></div>
330
+ {/if}
331
+ <div data-select-group-label role="presentation">
332
+ {#if content}
333
+ {@render content(proxy)}
334
+ {:else}
335
+ {@render defaultGroupContent(proxy)}
336
+ {/if}
337
+ </div>
338
+ {:else}
339
+ <button
340
+ type="button"
341
+ data-select-option
342
+ data-path={node.key}
343
+ data-level={node.level}
344
+ data-selected={sel || undefined}
345
+ data-disabled={proxy.disabled || undefined}
346
+ role="option"
347
+ aria-selected={sel}
348
+ aria-label={proxy.label}
349
+ disabled={proxy.disabled || disabled}
350
+ tabindex="-1"
351
+ >
352
+ <span data-select-checkbox data-checked={sel || undefined}>
353
+ {#if sel}
354
+ <span class={icons.checked} aria-hidden="true"></span>
355
+ {/if}
356
+ </span>
357
+ {#if content}
358
+ {@render content(proxy)}
359
+ {:else}
360
+ {@render defaultOptionContent(proxy)}
361
+ {/if}
362
+ </button>
363
+ {/if}
364
+ {/each}
365
+ </div>
366
+ {/if}
367
+ </div>