@rokkit/cli 1.1.12 → 1.1.14

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.
@@ -0,0 +1,415 @@
1
+ ---
2
+ name: rokkit-components
3
+ description: Use when building UI with Rokkit components in a Svelte 5 app — the data-first contract (items, bindable value, fields field-mapping, onchange/onselect), snippet-based customization, built-in keyboard navigation + ARIA, and which component to reach for (List, Tree, Select, MultiSelect, Menu, Table, Tabs, Toggle).
4
+ ---
5
+
6
+ # Rokkit Components — Usage
7
+
8
+ Rokkit components are data-first: you pass an array, bind a value, and the component
9
+ handles rendering, keyboard navigation, and accessibility out of the box. You extend
10
+ behavior through the `fields` prop (remap data keys) and named snippets (customize
11
+ rendering) — not by modifying components or writing boilerplate.
12
+
13
+ Import from `@rokkit/ui`:
14
+
15
+ ```svelte
16
+ import { List, Tree, Select, MultiSelect, Menu, Table, Tabs, Toggle } from '@rokkit/ui'
17
+ ```
18
+
19
+ ---
20
+
21
+ ## The data-first contract
22
+
23
+ Every Rokkit component follows a consistent API:
24
+
25
+ | Prop | Purpose |
26
+ |------|---------|
27
+ | `items` / `options` / `data` | The data array to render |
28
+ | `bind:value` | Bindable selected value (or values array for MultiSelect) |
29
+ | `fields` | Remap your data's key names to component semantic keys |
30
+ | `onchange` / `onselect` | Callback fired when selection changes |
31
+
32
+ Primitive arrays (strings, numbers) work without any transformation:
33
+
34
+ ```svelte
35
+ <script>
36
+ import { List } from '@rokkit/ui'
37
+
38
+ const nav = ['Dashboard', 'Reports', 'Settings']
39
+ let value = $state(null)
40
+ </script>
41
+
42
+ <List items={nav} bind:value onselect={(v) => console.log('selected:', v)} />
43
+ ```
44
+
45
+ Object arrays are equally direct — use `fields` only when your key names differ from defaults:
46
+
47
+ ```svelte
48
+ <script>
49
+ import { Select } from '@rokkit/ui'
50
+
51
+ const countries = [
52
+ { code: 'us', name: 'United States' },
53
+ { code: 'gb', name: 'United Kingdom' }
54
+ ]
55
+ let value = $state(null)
56
+ </script>
57
+
58
+ <!-- fields remaps 'name' → label slot, 'code' → value slot -->
59
+ <Select items={countries} fields={{ label: 'name', value: 'code' }} bind:value />
60
+ ```
61
+
62
+ **Prop naming note:** `List`, `Tree`, `Select`, `MultiSelect`, and `Menu` all take `items`.
63
+ `Tabs` and `Toggle` take `options`. `Table` takes `data`.
64
+
65
+ ---
66
+
67
+ ## Field mapping (fields)
68
+
69
+ The `fields` prop is a plain object that maps **semantic slot names** → **your data's key names**.
70
+ It never mutates your data — it is only a read instruction for the component.
71
+
72
+ ### Default field keys
73
+
74
+ These are the raw key names each component reads by default. Override only the ones that differ in your data.
75
+
76
+ | Semantic slot | Default key | Used by |
77
+ |---------------|-------------|---------|
78
+ | `label` / `text` | `'label'` / `'text'` | display text (see note) |
79
+ | `value` | `'value'` | selection value |
80
+ | `icon` | `'icon'` | icon CSS class |
81
+ | `children` | `'children'` | nested items array |
82
+ | `href` | `'href'` | URL (renders as `<a>`) |
83
+ | `disabled` | `'disabled'` | disabled state |
84
+ | `expanded` | `'expanded'` | initial expand state |
85
+ | `type` | `'type'` | `'separator'` / `'spacer'` markers |
86
+ | `snippet` | `'snippet'` | per-item named snippet override |
87
+ | `badge` | `'badge'` | badge / count indicator |
88
+ | `description` | `'description'` | secondary subtitle text |
89
+
90
+ > **Text field note:** `List` / `Tree` / `Menu` read `item.label` by default.
91
+ > `Select` / `MultiSelect` also read `item.label` (they use `fields.label || 'label'`
92
+ > internally). Tabs reads `item.text` or `item.label`. `Toggle` reads `item.text`
93
+ > (with `label` as fallback). When in doubt, provide both keys or use `fields`.
94
+
95
+ ### Remapping example
96
+
97
+ Your API returns `{ id, title, subitems }`. Map to Rokkit slots with `fields`:
98
+
99
+ ```svelte
100
+ <List
101
+ items={apiData}
102
+ fields={{ value: 'id', label: 'title', children: 'subitems' }}
103
+ bind:value
104
+ />
105
+ ```
106
+
107
+ ### Table columns
108
+
109
+ `Table` uses a separate `columns` array to define column shapes. Row-level identity
110
+ is set via `fields={{ value: 'id' }}` (defaults to `'id'`):
111
+
112
+ ```svelte
113
+ <script>
114
+ import { Table } from '@rokkit/ui'
115
+
116
+ const columns = [
117
+ { name: 'name', label: 'Full Name', sortable: true },
118
+ { name: 'role', label: 'Role', sortable: true },
119
+ { name: 'active', label: 'Active', sortable: false, align: 'center' }
120
+ ]
121
+ const rows = [
122
+ { id: 1, name: 'Alice', role: 'Admin', active: true },
123
+ { id: 2, name: 'Bob', role: 'Editor', active: false }
124
+ ]
125
+ let value = $state(null)
126
+ </script>
127
+
128
+ <Table data={rows} {columns} bind:value />
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Snippet customization
134
+
135
+ Snippets let you replace inner rendering without changing the component's
136
+ wrapper, ARIA structure, or keyboard handling. The component always owns the
137
+ interactive container (`<button>`, `<a>`, tab panel, etc.) — snippets control
138
+ the **inner content** only.
139
+
140
+ ### itemContent — List, Tree, Menu (and Toggle, for option labels)
141
+
142
+ The most common override. Receives a `ProxyItem` instance:
143
+
144
+ ```svelte
145
+ <List {items} bind:value onselect={(v) => (value = v)}>
146
+ {#snippet itemContent(proxy)}
147
+ {#if proxy.icon}
148
+ <span class={proxy.icon} aria-hidden="true"></span>
149
+ {/if}
150
+ <span class="flex-1">{proxy.label}</span>
151
+ {#if proxy.get('badge')}
152
+ <span class="ml-auto text-xs text-ink-mute">{proxy.get('badge')}</span>
153
+ {/if}
154
+ {/snippet}
155
+ </List>
156
+ ```
157
+
158
+ ### groupContent — List, Menu (not Tree — tree nodes use itemContent)
159
+
160
+ Overrides the group header button content (the expand/collapse header row):
161
+
162
+ ```svelte
163
+ <List {items} collapsible bind:value>
164
+ {#snippet groupContent(proxy)}
165
+ <span class={proxy.icon}></span>
166
+ <span>{proxy.label}</span>
167
+ <span class={proxy.expanded ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'}></span>
168
+ {/snippet}
169
+ </List>
170
+ ```
171
+
172
+ ### ProxyItem API (passed to all content snippets)
173
+
174
+ | Property / method | Description |
175
+ |-------------------|-------------|
176
+ | `proxy.label` | Mapped display text |
177
+ | `proxy.icon` | Mapped icon CSS class |
178
+ | `proxy.value` | The original raw item (object or primitive) |
179
+ | `proxy.href` | Mapped href |
180
+ | `proxy.disabled` | Whether this item is disabled |
181
+ | `proxy.expanded` | Expand state for groups/nodes (reactive `$state`) |
182
+ | `proxy.hasChildren` | True when the item has a `children` array |
183
+ | `proxy.get('key')` | Read any field by semantic or raw name |
184
+
185
+ ### Select and MultiSelect snippets
186
+
187
+ Select and MultiSelect do not expose a trigger or chips snippet — the trigger face
188
+ and selected chips are rendered from `fields`/`label` and are not consumer-overridable.
189
+
190
+ Both components support `itemContent` and `groupContent` for the **dropdown options**,
191
+ using the same `ProxyItem` API as `List`:
192
+
193
+ ```svelte
194
+ <Select {items} bind:value>
195
+ {#snippet itemContent(proxy)}
196
+ {#if proxy.icon}
197
+ <span class={proxy.icon} aria-hidden="true"></span>
198
+ {/if}
199
+ <span>{proxy.label}</span>
200
+ {#if proxy.get('description')}
201
+ <span class="text-xs text-ink-mute ml-auto">{proxy.get('description')}</span>
202
+ {/if}
203
+ {/snippet}
204
+ </Select>
205
+ ```
206
+
207
+ The same `itemContent` / `groupContent` patterns work identically on `MultiSelect`.
208
+
209
+ ### Tabs snippets
210
+
211
+ `Tabs` supports `tabPanel` (receives one `ProxyItem` argument) and `itemContent`
212
+ (for the tab trigger button content). Access panel data via `proxy.get('key')`:
213
+
214
+ ```svelte
215
+ <Tabs {options} bind:value>
216
+ {#snippet tabPanel(proxy)}
217
+ <div class="p-4">
218
+ <h2 class="text-ink font-medium">{proxy.label}</h2>
219
+ <div>{proxy.get('content')}</div>
220
+ </div>
221
+ {/snippet}
222
+ </Tabs>
223
+ ```
224
+
225
+ `Tabs` also supports an `empty` snippet (no arguments) rendered when `options` is empty.
226
+
227
+ ### Per-item named snippets (List)
228
+
229
+ Set `item.snippet = 'name'` on specific items to route them to a named snippet.
230
+ Other items fall back to `itemContent`:
231
+
232
+ ```svelte
233
+ <script>
234
+ const items = [
235
+ { label: 'Apple', snippet: 'fruit' },
236
+ { label: 'Carrot', snippet: 'veggie' }
237
+ ]
238
+ </script>
239
+
240
+ <List {items}>
241
+ {#snippet fruit(proxy)}
242
+ <span class="i-lucide:apple"></span>
243
+ <span>{proxy.label}</span>
244
+ {/snippet}
245
+ {#snippet veggie(proxy)}
246
+ <span class="i-lucide:carrot"></span>
247
+ <span>{proxy.label}</span>
248
+ {/snippet}
249
+ </List>
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Keyboard navigation & accessibility
255
+
256
+ Rokkit components are accessible out of the box — no extra wiring required as a consumer.
257
+ The navigator pattern handles keyboard events on the component's root or dropdown container.
258
+
259
+ ### What you get for free
260
+
261
+ | Component | Keys | ARIA |
262
+ |-----------|------|------|
263
+ | List / Tree | `↑↓` move focus, `↵`/`Space` select, `→`/`←` expand/collapse groups, `Home`/`End`, typeahead `a-z` | `role="navigation"`, `aria-current="page"` on active link |
264
+ | Select / MultiSelect | `↑↓` navigate dropdown, `↵` select/toggle, `Esc` close, `Home`/`End` | `role="combobox"`, `aria-expanded`, `aria-selected` |
265
+ | Menu | `↑↓` navigate items, `↵`/`Space` select, `Esc` close + return focus | `role="menu"`, `role="menuitem"` |
266
+ | Tree | `↑↓` navigate, `→` expand/enter first child, `←` collapse/go to parent | `role="tree"`, `role="treeitem"`, `aria-expanded`, `aria-selected` |
267
+ | Table | `↑↓` between rows, `↵`/`Space` select | `role="grid"`, `aria-sort` on sortable columns, `aria-selected` on rows |
268
+ | Tabs | `←`/`→` (horizontal) or `↑`/`↓` (vertical) between tabs, `↵`/`Space` select | `role="tablist"`, `role="tab"`, `role="tabpanel"`, `aria-selected`, `aria-controls` |
269
+ | Toggle | `←`/`→` between options, `↵`/`Space` select | `role="radiogroup"`, `role="radio"`, `aria-checked` |
270
+
271
+ ### Focus management note
272
+
273
+ The components sync `bind:value` to DOM focus automatically — when `value` changes
274
+ programmatically, the component moves keyboard focus to the matching item. You do not
275
+ need to call `focus()` yourself.
276
+
277
+ ### Interactive elements inside snippets
278
+
279
+ If your `itemContent` snippet contains its own interactive elements (inputs, buttons),
280
+ stop propagation to prevent the navigator from also triggering selection:
281
+
282
+ ```svelte
283
+ <List {items}>
284
+ {#snippet itemContent(proxy)}
285
+ <span class="flex-1">{proxy.label}</span>
286
+ <button
287
+ onclick={(e) => { e.stopPropagation(); doAction(proxy.value) }}
288
+ >
289
+ Action
290
+ </button>
291
+ {/snippet}
292
+ </List>
293
+ ```
294
+
295
+ Do NOT add an `onclick` on elements that also carry `data-path` — the navigator
296
+ intercepts those clicks and would double-fire.
297
+
298
+ ---
299
+
300
+ ## Which component when
301
+
302
+ | Need | Component | Data prop | Callback |
303
+ |------|-----------|-----------|----------|
304
+ | Vertical navigable list (sidebar nav, option list) | `List` | `items` | `onselect` |
305
+ | Hierarchical tree with expand/collapse | `Tree` | `items` | `onselect` |
306
+ | Dropdown single selection | `Select` | `items` | `onchange` |
307
+ | Dropdown multi-selection with chips | `MultiSelect` | `items` | `onchange` |
308
+ | Contextual action menu (dropdown actions) | `Menu` | `items` | `onselect` |
309
+ | Flat data table with columns + row selection | `Table` | `data` | `onselect` |
310
+ | Tabbed panels (horizontal or vertical) | `Tabs` | `options` | `onchange` |
311
+ | Compact mode/view switcher (segmented control) | `Toggle` | `options` | `onchange` |
312
+
313
+ **Use `List` not `Select`** when the options are always visible (sidebar, settings panel).
314
+ **Use `Select` not `List`** when screen space is limited and the dropdown can close.
315
+ **Use `Toggle` not `Tabs`** for 2–5 options with no panel content (just a mode switch).
316
+ **Use `Tree` not `List`** when data is genuinely hierarchical (file system, nested categories).
317
+
318
+ ---
319
+
320
+ ## Recipes
321
+
322
+ ### Select bound to a value
323
+
324
+ ```svelte
325
+ <script>
326
+ import { Select } from '@rokkit/ui'
327
+
328
+ const roles = [
329
+ { label: 'Admin', value: 'admin' },
330
+ { label: 'Editor', value: 'editor' },
331
+ { label: 'Viewer', value: 'viewer' }
332
+ ]
333
+ let role = $state('viewer')
334
+ </script>
335
+
336
+ <Select items={roles} bind:value={role} onchange={(v) => console.log('role:', v)} />
337
+ <p>Current role: {role}</p>
338
+ ```
339
+
340
+ ### Tree with nested children
341
+
342
+ ```svelte
343
+ <script>
344
+ import { Tree } from '@rokkit/ui'
345
+
346
+ const files = [
347
+ {
348
+ label: 'src',
349
+ icon: 'i-lucide:folder',
350
+ children: [
351
+ { label: 'index.ts', icon: 'i-lucide:file-code' },
352
+ { label: 'utils.ts', icon: 'i-lucide:file-code' }
353
+ ]
354
+ },
355
+ { label: 'README.md', icon: 'i-lucide:file-text' }
356
+ ]
357
+
358
+ let selected = $state(null)
359
+ </script>
360
+
361
+ <Tree items={files} bind:value={selected} onselect={(v) => console.log('file:', v)} />
362
+ ```
363
+
364
+ ### MultiSelect with chips
365
+
366
+ ```svelte
367
+ <script>
368
+ import { MultiSelect } from '@rokkit/ui'
369
+
370
+ const tags = [
371
+ { label: 'Frontend', value: 'fe' },
372
+ { label: 'Backend', value: 'be' },
373
+ { label: 'DevOps', value: 'ops' }
374
+ ]
375
+ let selected = $state([])
376
+ </script>
377
+
378
+ <MultiSelect
379
+ items={tags}
380
+ bind:value={selected}
381
+ placeholder="Select tags…"
382
+ onchange={(vals) => console.log('selected:', vals)}
383
+ />
384
+ <p>Tags: {selected.join(', ')}</p>
385
+ ```
386
+
387
+ ### Table with explicit columns and field mapping
388
+
389
+ ```svelte
390
+ <script>
391
+ import { Table } from '@rokkit/ui'
392
+
393
+ // Row identifier is 'uid' — remap via fields
394
+ const users = [
395
+ { uid: 1, fullName: 'Alice Chen', dept: 'Engineering' },
396
+ { uid: 2, fullName: 'Bob Tanaka', dept: 'Design' }
397
+ ]
398
+
399
+ const columns = [
400
+ { name: 'fullName', label: 'Name', sortable: true },
401
+ { name: 'dept', label: 'Department', sortable: true }
402
+ ]
403
+
404
+ let selectedUser = $state(null)
405
+ </script>
406
+
407
+ <Table
408
+ data={users}
409
+ {columns}
410
+ fields={{ value: 'uid' }}
411
+ bind:value={selectedUser}
412
+ caption="User list"
413
+ onselect={(v, row) => console.log('selected row:', row)}
414
+ />
415
+ ```