@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.
- package/README.md +18 -0
- package/dist/src/init.d.ts +11 -3
- package/dist/src/skills.d.ts +63 -0
- package/package.json +2 -1
- package/skills/command-system-rokkit/SKILL.md +256 -0
- package/skills/rokkit-components/SKILL.md +415 -0
- package/skills/semantic-styles-rokkit/SKILL.md +390 -0
- package/src/index.js +18 -0
- package/src/init.js +24 -4
- package/src/skills.js +141 -0
|
@@ -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
|
+
```
|