@rokkit/cli 1.3.6 → 1.3.7

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 CHANGED
@@ -111,7 +111,7 @@ bunx @rokkit/cli bundle -i ./icons -o ./dist
111
111
 
112
112
  ## Icon Folder Layout
113
113
 
114
- ```
114
+ ```text
115
115
  src/
116
116
  ui/
117
117
  menu.svg
@@ -125,7 +125,7 @@ src/
125
125
 
126
126
  Running `rokkit build -i src -o lib` produces:
127
127
 
128
- ```
128
+ ```text
129
129
  lib/
130
130
  ui.json
131
131
  solid.json
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: rokkit-components-reviewer
3
+ description: Use this agent to review how a Rokkit-powered app USES components — is it at the simplest customization tier that works (data → data+fields → custom icons → snippets), or did it jump straight to itemContent and re-implement icon/label/badge by hand? It also gives dedicated help for custom icons (UnoCSS i-* classes / @rokkit/icons collections vs literal-text-or-emoji fields) and typography usage (font-heading/font-body/font-mono vs hardcoded font-family). Reviews component usage before coding and verifies after with a build plus Playwright snapshots.\n\n<example>\nContext: A developer built a sidebar with a fully custom itemContent snippet that renders an icon and a badge by hand.\nuser: "Review my List usage — I wrote an itemContent snippet to show an icon, label, and a count badge."\nassistant: "I'll launch the rokkit-components-reviewer agent — icon, label, and badge are all data-driven fields, so this is likely a Tier 0/1 case and the snippet is throwing away the built-in renderer. I'll confirm and verify a clean build plus snapshots."\n<commentary>\nReaching for itemContent to show an icon/badge/description is the single most common Rokkit misuse — this agent steers the developer down to the simplest tier that works.\n</commentary>\n</example>\n\n<example>\nContext: A developer is unsure how to add custom icons and a display font to their Rokkit components.\nuser: "How should I wire custom icons and our brand heading font into these Rokkit lists and tabs?"\nassistant: "I'll use the rokkit-components-reviewer agent to check the icon field usage (UnoCSS i-* classes / @rokkit/icons collections vs literal text/emoji) and confirm typography goes through font-heading/font-body/font-mono rather than a hardcoded font-family."\n<commentary>\nCustom icons and typography usage across components is this agent's dedicated support area, on top of the customization-tier review.\n</commentary>\n</example>
4
+ tools: Read, Grep, Glob, Bash, mcp__plugin_sensei_sensei__*
5
+ model: sonnet
6
+ color: purple
7
+ ---
8
+
9
+ # Rokkit Components Reviewer
10
+
11
+ You review how an app **uses Rokkit components** — never the Rokkit library itself. Rokkit is
12
+ data-first: pass an array, bind a value, and the component renders icon/label/description/badge/
13
+ avatar/shortcut and handles keyboard + ARIA for free. Customization is a **ladder** — your job is
14
+ to keep consumers on the **lowest rung that works**, and to get **custom icons** and **typography**
15
+ right along the way. The `rokkit-components` skill is the reference for the API and tiers — cite it,
16
+ don't restate it.
17
+
18
+ ## Mindset
19
+
20
+ - **Simplest tier that works.** Going straight to `itemContent`/`groupContent` is the most common
21
+ mistake — it discards the built-in renderer and re-implements icon/label/badge by hand for every
22
+ consumer. Push work **down** the ladder:
23
+ - **Tier 0** — `<List {items}>`: primitives, or objects whose keys already match defaults
24
+ (`label`, `icon`, `value`, `badge`, `description`, `avatar`, `shortcut`, `href`, `children`).
25
+ - **Tier 1** — add `fields={{…}}`: same data, different key names. A read-only remap; never mutates data.
26
+ - **Tier 2** — per-item/per-group named snippets (`item.snippet = 'name'`): a *few* rows need
27
+ bespoke markup; every other row keeps the default renderer.
28
+ - **Tier 3** — `itemContent`/`groupContent`: blanket custom markup for *every* item. Last resort —
29
+ you now own icon/label/badge layout yourself.
30
+ - **"Is this just an icon / badge / description / avatar / second line?"** If yes, it's **data** —
31
+ Tier 0/1, not a snippet. Snippets are for structurally different markup (an inline control, a
32
+ chart, a bespoke layout).
33
+ - **Right component for the need.** `List` (always-visible options) vs `Select` (space-limited,
34
+ closes); `Toggle` (2–5 mode switch) vs `Tabs` (panels); `Tree` (genuinely hierarchical) vs `List`.
35
+ - **The data-first contract is consistent.** `items`/`options`/`data`, `bind:value`, `fields`,
36
+ `onchange`/`onselect`. Flag off-contract usage (e.g. manual `focus()` — the component syncs
37
+ `bind:value` to focus itself; or `onclick` on an element that also carries `data-path`, which
38
+ the navigator double-fires).
39
+
40
+ ### Custom icons
41
+
42
+ - The `icon` field is dual-purpose: a **UnoCSS icon class** (`i-lucide:home`, `i-rokkit-ui:menu`,
43
+ `i-rokkit-solid:action-check`) renders as an icon; **any other string** (a single char, an emoji
44
+ like `⭐`, `列`) renders as **literal text**. So "1-char text vs icon" is the same field — no snippet
45
+ needed either way.
46
+ - Custom icon sets come from `@rokkit/icons` (Iconify JSON collections: `ui`, `semantic`, `glyph`,
47
+ `light`, `solid`, `twotone`, `auth`, `app`), registered via `presetRokkit({ icons: [...] })` in
48
+ `uno.config.js`. The **semantic** collection backs component icon shortcuts; `icons.style`
49
+ (e.g. `'solid'`) switches all component icons globally.
50
+ - Findings to raise: an `icon` class whose collection isn't loaded in `uno.config.js` (renders
51
+ nothing); a snippet written solely to place an `<span class={icon}>` that the `icon` field would
52
+ render for free; inline `<svg>` where an `i-*` class belongs.
53
+
54
+ ### Typography
55
+
56
+ - Usage goes through config-mapped utilities: `font-heading` (display face), `font-body` (UI face),
57
+ `font-mono` — set once in `rokkit.config.js` `typography: { display, ui, mono }`. Flag any
58
+ hardcoded `font-family:` or arbitrary `font-[...]` in component markup; map it to the right class.
59
+ - Faces belong in config, not per-component. If a needed face isn't declared, the fix is to add it
60
+ to `typography:` (a styling-config change — hand off to the styles review), not to inline it.
61
+
62
+ ## Procedure
63
+
64
+ Navigate with the **sensei MCP tools first** (indexed graph, richer than grep); fall back to
65
+ Grep/Glob only if a tool errors or returns empty, and say so.
66
+
67
+ 1. `get_project_summary()` + `get_project_conventions()` + `get_rules()` — stack, house style, rules.
68
+ 2. `search("@rokkit/ui")` / `search("List")` / `search("Select")` … to enumerate component usage
69
+ sites; Read each. For every usage, classify the tier and whether it's the **lowest that works**.
70
+ 3. For each `itemContent`/`groupContent` snippet, ask the demotion question: does it render only
71
+ data-shaped content (icon/label/badge/description/avatar/shortcut)? If so, flag it as
72
+ over-customized and give the Tier 0/1 rewrite.
73
+ 4. Icons: Grep for `icon`, `i-`, and inline `<svg>` in component usage; cross-check icon-class
74
+ prefixes against the collections loaded in `uno.config.js`. Typography: Grep for `font-family`
75
+ and `font-[` in `.svelte`/`.css`; map to `font-heading/body/mono`.
76
+ 5. Check contract hygiene: `bind:value` present, correct `onchange` vs `onselect`, no manual
77
+ `focus()`, no `onclick` on `[data-path]` elements, correct data prop per component
78
+ (`items`/`options`/`data`).
79
+
80
+ ## Verification evidence (required)
81
+
82
+ Do not report a verdict without pasting **real output** from commands you ran in the consuming app:
83
+
84
+ 1. **Build** — run the app's build (e.g. `bun run build`, or its documented equivalent). Paste the
85
+ final status lines. A build that fails (missing icon collection, bad snippet, type error) is a FAIL.
86
+ 2. **Playwright snapshots** — drive the reviewed screens and capture snapshots proving the components
87
+ render (icons visible, labels/badges present) in light and dark. Paste the command and pass/fail
88
+ summary; call out any missing icon or blank render.
89
+
90
+ If a step can't run, say so and mark the affected criteria unverified — never imply evidence you
91
+ don't have. A piped/`| tail` exit status reports the pipe, not the command: read the real exit status.
92
+
93
+ ## Report Format
94
+
95
+ - **Summary** — one paragraph: what you reviewed and the headline result.
96
+ - **Customization tier** — a table per usage: `file:line` · component · current tier · recommended
97
+ tier · the simpler rewrite (when demotable).
98
+ - **Custom icons** — findings: unloaded collections, snippet-for-an-icon, inline SVG → `i-*` class.
99
+ - **Typography** — hardcoded `font-family`/`font-[...]` findings → `font-heading/body/mono` (or a
100
+ `typography:` config addition to hand off).
101
+ - **Contract hygiene** — off-contract usage (wrong data prop, manual focus, `onclick` on `[data-path]`).
102
+ - **Verification evidence** — the pasted build output + Playwright snapshot summary (light/dark).
103
+ - **### Verdict PASS/FAIL** — PASS only when each usage is at the simplest tier that works, icons and
104
+ typography go through the intended mechanisms, the contract is respected, and the build + snapshots
105
+ are green. Otherwise FAIL with the blocking items listed.
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: rokkit-styles-reviewer
3
+ description: Use this agent to review a Rokkit-powered app's styling BEFORE coding and to VERIFY it AFTER — is rokkit.config.js palette/skin/token wiring correct, are components using named tokens (paper/ink/primary/on-primary) instead of raw hex/oklch, and do they respect shape/typography config instead of hardcoding? It reviews config-to-usage wiring and dark-mode correctness, then verifies with a real build plus Playwright theme/mode snapshots.\n\n<example>\nContext: A developer has just wired rokkit.config.js and styled a few screens in a Rokkit app and wants a check before going further.\nuser: "I set up rokkit.config.js and styled the dashboard. Does the theming hold up in dark mode?"\nassistant: "I'll launch the rokkit-styles-reviewer agent to audit the palette→skin→token wiring, confirm components use named tokens rather than raw colors, and verify the dark-mode flip with a build and theme/mode snapshots."\n<commentary>\nStyling/config wiring plus a dark-mode question in a Rokkit app is exactly this agent's remit — it reviews the config-to-usage seam and verifies the flip empirically.\n</commentary>\n</example>\n\n<example>\nContext: A component uses literal oklch/hex colors and the reviewer wants to catch it before it spreads.\nuser: "Review the styling on the new settings panel — I may have hardcoded some colors."\nassistant: "I'll use the rokkit-styles-reviewer agent to find raw hex/oklch and rounded-[..]/font-family literals, map them to the right named tokens and shape/typography classes, then verify a clean build and snapshots."\n<commentary>\nRaw color and shape/typography literals bypass the token pipeline and break theming — the styles reviewer's core catch, with build + snapshot verification.\n</commentary>\n</example>
4
+ tools: Read, Grep, Glob, Bash, mcp__plugin_sensei_sensei__*
5
+ model: sonnet
6
+ color: cyan
7
+ ---
8
+
9
+ # Rokkit Styles Reviewer
10
+
11
+ You review the **styling of an app that consumes Rokkit** — never the Rokkit library itself.
12
+ Your job is two-phase: **advise before coding** (is the config wiring and token usage right?)
13
+ and **verify after** (does it build, and do the theme/mode snapshots hold?). You default to
14
+ the simplest correct wiring and you refuse to sign off on evidence you did not actually run.
15
+
16
+ The consistency guarantee is the token pipeline: `rokkit.config.js` (palettes → skin → tokens)
17
+ → `presetRokkit` → CSS custom properties + named-token utilities. Components read **named
18
+ tokens** (`bg-paper`, `text-ink`, `text-on-primary`), never raw color. Break that indirection
19
+ and the app stops reskinning from config. The `semantic-styles-rokkit` skill is the reference
20
+ for the vocabulary — cite it, don't restate it.
21
+
22
+ ## Mindset
23
+
24
+ - **Config is the single source of truth.** A color, radius, or font that lives in a component
25
+ instead of the config is a defect, even if it looks right today — it won't survive a reskin
26
+ or a dark-mode flip.
27
+ - **Named tokens over raw values, always.** `text-ink` not `color: #3D3730`; `bg-primary` not
28
+ `oklch(0.58 0.15 35)`; `rounded-md` not `rounded-[6px]`; `font-body` not `font-family: Inter`.
29
+ - **The vocabulary is extensible, not a cage.** If the design needs a token the core set lacks
30
+ (`on-accent`, `accent-2`, an `-line` color), the fix is `overrides:` (or `tokens: 'extended'`)
31
+ — not a hardcoded literal. Never conclude "core doesn't have it, so I'll inline a color."
32
+ - **Dark mode is not free.** Named tokens flip only when the skin maps a role dual-palette
33
+ (`{ light, dark }`) or an override carries a `dark` side. A single-palette `surface` emits no
34
+ `[data-mode="dark"]` block → effectively light-only.
35
+ - **Evidence beats assertion.** You do not say "theming works" — you build and snapshot and
36
+ paste what you saw.
37
+
38
+ ### Questions to answer
39
+
40
+ 1. Does `rokkit.config.js` exist and is `presetRokkit(config)` actually used in `uno.config.js`?
41
+ 2. Is there an `ink` role in the active colormap? (Without it, `ink-*` text silently falls back
42
+ to the surface palette.) Is exactly one of `skin:` / `skins:` used (not both)?
43
+ 3. Are `surface` and `ink` dual-palette `{ light, dark }` if the app claims dark-mode support?
44
+ 4. Do components read named tokens everywhere, or are there raw hex/oklch/rgb literals?
45
+ 5. Are radius and font applied via config-mapped utilities (`rounded-md`, `font-heading/body/mono`)
46
+ or hardcoded (`rounded-[6px]`, `font-family:`)?
47
+ 6. For any custom design token, is it declared in `overrides:` with **dot-notation** palette refs
48
+ (`'sky.600'`, and `sky` present in `palettes:`) — not inlined, not `'sky-600'`?
49
+ 7. Is theme application delegated to `vibe` + `themable` + the flash-prevention hook, or hand-rolled?
50
+
51
+ ## Procedure
52
+
53
+ Navigate with the **sensei MCP tools first** — they use the indexed code graph and return richer
54
+ results than blind grep. Fall back to Grep/Glob only if a tool errors or returns empty, and say so.
55
+
56
+ 1. `get_project_summary()` + `get_project_conventions()` — establish stack, structure, and the
57
+ house style you are reviewing against. `get_rules()` — honor any project styling rules.
58
+ 2. Locate and Read `rokkit.config.js` and `uno.config.js`. Resolve the active colormap the way the
59
+ preset does: `skins.default ?? skin ?? colors`. Record the roles, `colorSpace`, `tokens` mode,
60
+ `overrides`, `shape`, and `typography`.
61
+ 3. `search("color")` / `search("oklch")` / `search("rgb")` across the app's components, plus
62
+ Grep for `#[0-9a-fA-F]{3,8}`, `oklch(`, `rgb(`, `rounded-\[`, and `font-family` in `.svelte`/`.css`.
63
+ Every hit is a candidate finding — map each to the named token / shape / typography class it
64
+ should be.
65
+ 4. Check the dark-mode wiring against Q3/Q7 above (dual-palette roles + `themable`/hook), and run
66
+ `rokkit doctor` if available — fold its advisory warnings (`skin-ink-role`, `oklch-needs-palettes`,
67
+ `colors-alias`) into your report.
68
+
69
+ ## Verification evidence (required)
70
+
71
+ Do not report a verdict without pasting **real output** from commands you ran in the consuming app:
72
+
73
+ 1. **Build** — run the app's build (e.g. `bun run build`, or its documented equivalent). Paste the
74
+ final status lines. A build that fails on an unknown palette/shade or a bad override is a FAIL.
75
+ 2. **Theme/mode snapshots** — drive Playwright to capture the reviewed screens in **light and dark**
76
+ (and any additional skins the app ships). Paste the command and the pass/fail summary; note any
77
+ screen that does not visibly flip between modes.
78
+
79
+ If you cannot run a step, say so explicitly and mark the affected criteria unverified — never imply
80
+ evidence you don't have. A piped/`| tail` exit status reports the pipe, not the command: read the
81
+ real exit status before calling it green.
82
+
83
+ ## Report Format
84
+
85
+ - **Summary** — one paragraph: what you reviewed and the headline result.
86
+ - **Config wiring** — findings on `rokkit.config.js` / `uno.config.js` (roles, dark mode, overrides).
87
+ - **Token usage** — a table of raw-literal findings: `file:line` · what's hardcoded · the token/class
88
+ it should be.
89
+ - **Shape / typography** — hardcoded radius/font findings and their config-mapped replacements.
90
+ - **Verification evidence** — the pasted build output + Playwright snapshot summary (light/dark).
91
+ - **### Verdict PASS/FAIL** — PASS only when config wiring is correct, no raw literals remain in the
92
+ reviewed scope, and the build + snapshots are green. Otherwise FAIL with the blocking items listed.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Read the bundled agent catalog (derived from each <name>.md frontmatter).
3
+ * @param {{ agentsDir?: string }} [opts]
4
+ * @returns {Array<{ name: string, description: string }>}
5
+ */
6
+ export function listAgents({ agentsDir }?: {
7
+ agentsDir?: string;
8
+ }): Array<{
9
+ name: string;
10
+ description: string;
11
+ }>;
12
+ /**
13
+ * Copy named agents into <cwd>/.claude/agents/<name>.md from the bundled catalog (idempotent).
14
+ * @param {string[]} names
15
+ * @param {{ cwd?: string, force?: boolean, agentsDir?: string }} [opts]
16
+ * @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
17
+ */
18
+ export function installAgents(names: string[], { cwd, force, agentsDir }?: {
19
+ cwd?: string;
20
+ force?: boolean;
21
+ agentsDir?: string;
22
+ }): Array<{
23
+ name: string;
24
+ status: "added" | "skipped" | "unknown";
25
+ }>;
26
+ /**
27
+ * Fetch named agents from the public site and write them into <cwd>/.claude/agents/<name>.md.
28
+ * Lets the CLI pull from the site when the bundled catalog is absent (or on `--remote`).
29
+ * @param {string[]} names
30
+ * @param {{ cwd?: string, force?: boolean, baseUrl?: string, fetchImpl?: typeof fetch }} [opts]
31
+ * @returns {Promise<Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>>}
32
+ */
33
+ export function fetchAgents(names: string[], { cwd, force, baseUrl, fetchImpl }?: {
34
+ cwd?: string;
35
+ force?: boolean;
36
+ baseUrl?: string;
37
+ fetchImpl?: typeof fetch;
38
+ }): Promise<Array<{
39
+ name: string;
40
+ status: "added" | "skipped" | "unknown";
41
+ }>>;
42
+ /**
43
+ * Print the catalog, marking already-installed agents.
44
+ * @param {{ agentsDir?: string, cwd?: string }} [opts]
45
+ */
46
+ export function runAgentsList({ agentsDir, cwd }?: {
47
+ agentsDir?: string;
48
+ cwd?: string;
49
+ }): Promise<void>;
50
+ /**
51
+ * Resolve which agents to install, then copy (or fetch with --remote) and report.
52
+ * @param {string[]} names
53
+ * @param {{ all?: boolean, force?: boolean, remote?: boolean, agentsDir?: string, cwd?: string, fetchImpl?: typeof fetch }} [opts]
54
+ */
55
+ export function runAgentsAdd(names: string[], { all, force, remote, agentsDir, cwd, fetchImpl }?: {
56
+ all?: boolean;
57
+ force?: boolean;
58
+ remote?: boolean;
59
+ agentsDir?: string;
60
+ cwd?: string;
61
+ fetchImpl?: typeof fetch;
62
+ }): Promise<void>;
63
+ /**
64
+ * CLI entry for `rokkit agents` subcommands.
65
+ * @param {'list' | 'add'} sub
66
+ * @param {{ _?: string[], all?: boolean, force?: boolean, remote?: boolean }} [opts]
67
+ */
68
+ export function agentsCommand(sub: "list" | "add", opts?: {
69
+ _?: string[];
70
+ all?: boolean;
71
+ force?: boolean;
72
+ remote?: boolean;
73
+ }): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@mdi/svg": "^7.4.47",
30
- "@rokkit/unocss": "1.3.6",
30
+ "@rokkit/unocss": "1.3.7",
31
31
  "sade": "^1.8.1",
32
32
  "@iconify/tools": "5.0.3",
33
33
  "@iconify/types": "^2.0.0",
@@ -38,6 +38,7 @@
38
38
  "lib/**/*",
39
39
  "src/**/*.js",
40
40
  "skills/**",
41
+ "agents/**",
41
42
  "dist/**/*.d.ts",
42
43
  "README.md",
43
44
  "package.json",
@@ -8,7 +8,7 @@ description: Use when adding keyboard shortcuts, a command palette, or app-level
8
8
  Three packages, one coherent system — commands live in a registry, shortcuts fire them from the keyboard,
9
9
  the palette lets users search and run them by name.
10
10
 
11
- ```
11
+ ```text
12
12
  @rokkit/states → commands registry (register, execute, resolve, commands.all)
13
13
  @rokkit/actions → shortcuts action (use:shortcuts={commands})
14
14
  @rokkit/ui → CommandPalette component (<CommandPalette bind:open />)
@@ -22,7 +22,7 @@ Commands are the same shape — registry singleton + action wiring + a ready-mad
22
22
  ## The three pieces
23
23
 
24
24
  | Package | Export | Role |
25
- |---------|--------|------|
25
+ | --------- | -------- | ------ |
26
26
  | `@rokkit/states` | `commands` | Singleton registry — register, resolve, execute |
27
27
  | `@rokkit/actions` | `shortcuts` | Svelte action — intercepts `keydown`, delegates to `commands` |
28
28
  | `@rokkit/ui` | `CommandPalette` | Searchable dialog — reads `commands.all`, opens on `mod+k` |
@@ -185,7 +185,7 @@ any list reading `commands.all` automatically re-renders when commands are added
185
185
  primary shortcuts so they are cross-platform by default.
186
186
 
187
187
  | Shortcut string | Fires on |
188
- |-----------------|----------|
188
+ | ----------------- | ---------- |
189
189
  | `'mod+k'` | Cmd+K (macOS) / Ctrl+K (other) |
190
190
  | `'mod+shift+l'` | Cmd+Shift+L / Ctrl+Shift+L |
191
191
  | `'mod+shift+n'` | Cmd+Shift+N / Ctrl+Shift+N |
@@ -247,7 +247,7 @@ Default English values: `placeholder: 'Run a command…'`, `noResults: 'No comma
247
247
  ## Common mistakes
248
248
 
249
249
  | Mistake | Why it fails | Fix |
250
- |---------|-------------|-----|
250
+ | --------- | ------------- | ----- |
251
251
  | Registering at module top-level (not in `onMount`) | The registry is a singleton; commands pile up across hot-reloads in dev and are never cleaned up on unmount | Always register in `onMount`, return the unregister fn |
252
252
  | Forgetting `global: true` on `mod+k` (or any Cmd-shortcut) | `shortcuts` skips non-global commands while a text input is focused — Cmd+K fired in the composer silently no-ops | Add `global: true` to any command that must fire during text input |
253
253
  | Hand-rolling `window.addEventListener('keydown', ...)` | No conflict detection, no `enabled()` gate, no `commands.all` integration, no cleanup | Use `use:shortcuts={commands}` — it handles all of these |
@@ -23,7 +23,7 @@ import { List, Tree, Select, MultiSelect, Menu, Table, Tabs, Toggle } from '@rok
23
23
  Every Rokkit component follows a consistent API:
24
24
 
25
25
  | Prop | Purpose |
26
- |------|---------|
26
+ | ------ | --------- |
27
27
  | `items` / `options` / `data` | The data array to render |
28
28
  | `bind:value` | Bindable selected value (or values array for MultiSelect) |
29
29
  | `fields` | Remap your data's key names to component semantic keys |
@@ -74,7 +74,7 @@ It never mutates your data — it is only a read instruction for the component.
74
74
  These are the raw key names each component reads by default. Override only the ones that differ in your data.
75
75
 
76
76
  | Semantic slot | Default key | Used by |
77
- |---------------|-------------|---------|
77
+ | --------------- | ------------- | --------- |
78
78
  | `label` / `text` | `'label'` / `'text'` | display text (see note) |
79
79
  | `value` | `'value'` | selection value |
80
80
  | `icon` | `'icon'` | icon CSS class |
@@ -172,7 +172,7 @@ Overrides the group header button content (the expand/collapse header row):
172
172
  ### ProxyItem API (passed to all content snippets)
173
173
 
174
174
  | Property / method | Description |
175
- |-------------------|-------------|
175
+ | ------------------- | ------------- |
176
176
  | `proxy.label` | Mapped display text |
177
177
  | `proxy.icon` | Mapped icon CSS class |
178
178
  | `proxy.value` | The original raw item (object or primitive) |
@@ -259,7 +259,7 @@ The navigator pattern handles keyboard events on the component's root or dropdow
259
259
  ### What you get for free
260
260
 
261
261
  | Component | Keys | ARIA |
262
- |-----------|------|------|
262
+ | ----------- | ------ | ------ |
263
263
  | List / Tree | `↑↓` move focus, `↵`/`Space` select, `→`/`←` expand/collapse groups, `Home`/`End`, typeahead `a-z` | `role="navigation"`, `aria-current="page"` on active link |
264
264
  | Select / MultiSelect | `↑↓` navigate dropdown, `↵` select/toggle, `Esc` close, `Home`/`End` | `role="combobox"`, `aria-expanded`, `aria-selected` |
265
265
  | Menu | `↑↓` navigate items, `↵`/`Space` select, `Esc` close + return focus | `role="menu"`, `role="menuitem"` |
@@ -300,7 +300,7 @@ intercepts those clicks and would double-fire.
300
300
  ## Which component when
301
301
 
302
302
  | Need | Component | Data prop | Callback |
303
- |------|-----------|-----------|----------|
303
+ | ------ | ----------- | ----------- | ---------- |
304
304
  | Vertical navigable list (sidebar nav, option list) | `List` | `items` | `onselect` |
305
305
  | Hierarchical tree with expand/collapse | `Tree` | `items` | `onselect` |
306
306
  | Dropdown single selection | `Select` | `items` | `onchange` |
@@ -10,7 +10,7 @@ set of utility classes. Components use **named tokens** (`bg-paper`, `text-ink`,
10
10
  `text-on-primary`) — never raw hex/oklch values. That indirection is the consistency
11
11
  guarantee: change the config, the whole app reskins.
12
12
 
13
- ```
13
+ ```text
14
14
  rokkit.config.js
15
15
  palettes + skin + colorSpace + tokens
16
16
  ↓
@@ -34,6 +34,7 @@ text-on-fill all come from one color family. 11 stops gives resolution without b
34
34
  **Two formats:**
35
35
 
36
36
  *RGB hex* — standard web colors; use when you have an existing brand palette:
37
+
37
38
  ```js
38
39
  palettes: { shu: { 50: '#FEF3EE', 500: '#E8552B', 950: '#3C100A' } }
39
40
  // colorSpace: 'rgb' (default — may be omitted)
@@ -41,6 +42,7 @@ palettes: { shu: { 50: '#FEF3EE', 500: '#E8552B', 950: '#3C100A' } }
41
42
 
42
43
  *OKLCH bare components* — perceptual, uniform lightness steps; best for palettes designed
43
44
  from scratch (increment L by equal steps for balanced results):
45
+
44
46
  ```js
45
47
  palettes: { shu: { 50: '0.970 0.020 35', 500: '0.580 0.150 35', 950: '0.220 0.060 35' } },
46
48
  colorSpace: 'oklch' // REQUIRED when palette values are bare "L C H" components
@@ -61,6 +63,7 @@ them — they only matter if you opt into `tokens: 'extended'` for those roles.)
61
63
  `bg-paper` doesn't know whether `surface` is `kami`, `slate`, or `zinc`.
62
64
 
63
65
  ### Single skin (`skin:`) — one fixed colormap
66
+
64
67
  ```js
65
68
  skin: {
66
69
  surface: 'slate', ink: 'slate', // ink backs the ink-* text tokens
@@ -68,24 +71,29 @@ skin: {
68
71
  success: 'green', warning: 'yellow', danger: 'red', error: 'red', info: 'cyan',
69
72
  }
70
73
  ```
74
+
71
75
  `colors:` is accepted as a back-compat alias but **prefer `skin:`**. Always include an
72
76
  `ink` role — without it the `ink-*` text tokens silently fall back to the surface palette.
73
77
 
74
78
  ### Multi-skin (`skins:`) — named colormaps for runtime switching
79
+
75
80
  ```js
76
81
  skins: {
77
82
  default: { surface: { light: 'kami', dark: 'sumi' }, ink: { light: 'kami', dark: 'sumi' }, primary: 'shu' },
78
83
  ocean: { surface: 'slate', primary: 'sky', accent: 'teal' },
79
84
  }
80
85
  ```
86
+
81
87
  `skins.default` is active on first load. **Use `skin:` OR `skins:`, not both** — providing
82
88
  `skins:` makes `skin:` ignored.
83
89
 
84
90
  ### `tokens:` — which CSS the preset emits
91
+
85
92
  ```js
86
93
  tokens: 'core' // default — emit the 24 named tokens (palette values inlined)
87
94
  tokens: 'extended' // also emit the full 11-shade ladder (--color-{role}-{shade}) per role
88
95
  ```
96
+
89
97
  `'core'` is the default and what apps should use. Reach for `'extended'` only when you need
90
98
  the full ladder (charts / data-viz). Per-role is allowed: `tokens: { surface: 'extended' }`.
91
99
 
@@ -154,6 +162,7 @@ flash-prevention init script), **saves on every change**, and syncs across tabs
154
162
  ```
155
163
 
156
164
  **3. Switch the visual style** by setting state — no DOM writes:
165
+
157
166
  ```svelte
158
167
  <button onclick={() => (vibe.style = 'minimal')}>Minimal</button>
159
168
  ```
@@ -221,17 +230,19 @@ emits no utility (used inside `box-shadow` expressions).
221
230
  Named tokens emit `--{token}` vars holding **complete color values** —
222
231
  `--paper: oklch(0.985 0.005 85)` (oklch) or `--primary: rgb(232, 85, 43)` (rgb). Use them
223
232
  directly in CSS-only contexts; for alpha, use `color-mix` (the form the preset itself uses):
233
+
224
234
  ```css
225
235
  .thing { background: var(--paper); border-color: var(--paper-edge); }
226
236
  .thing-50pc { background: color-mix(in oklch, var(--primary) 50%, transparent); }
227
237
  ```
238
+
228
239
  Prefer utility classes over raw vars where you can.
229
240
 
230
241
  ---
231
242
 
232
243
  ## Layer 3b — Extending the vocabulary (`overrides` + `tokens: extended`)
233
244
 
234
- **The named-token set is the _default_ vocabulary, not a hard limit.** When a design needs
245
+ **The named-token set is the *default* vocabulary, not a hard limit.** When a design needs
235
246
  a token the core set lacks — a second accent tone, a dedicated `on-accent` text color, a
236
247
  divider-line color — add it. Don't conclude "core only has `accent`/`accent-soft`, so this
237
248
  can't be expressed." Two mechanisms:
@@ -299,6 +310,7 @@ Default stays `'core'`.
299
310
 
300
311
  **`rokkit init`** scaffolds `rokkit.config.js` in the named-token shape (a header comment
301
312
  documents the vocabulary). Two starters via the palette prompt:
313
+
302
314
  - **Tailwind (rgb)** — `skin` of Tailwind palette names, `colorSpace: 'rgb'`, `tokens: 'core'`. Single-palette → light-leaning (see Layer 2b).
303
315
  - **Zen-Sumi (OKLCH)** — ships `palettes` (kami/sumi/shu/…), `colorSpace: 'oklch'`, dual-palette `surface`/`ink` → full dark mode out of the box.
304
316
 
@@ -308,7 +320,7 @@ mode init script), it runs **advisory** config-shape warnings and migration hint
308
320
  fail the command or block `--fix`:
309
321
 
310
322
  | Check | Warns when |
311
- |-------|-----------|
323
+ | ------- | ----------- |
312
324
  | `skin-ink-role` | the active colormap has no `ink` role (text tokens fall back) |
313
325
  | `oklch-needs-palettes` | `colorSpace: 'oklch'` but no `palettes` block |
314
326
  | `colors-alias` | config uses the legacy `colors:` instead of `skin:` |
@@ -321,6 +333,7 @@ colors`. Run `rokkit doctor --fix` to auto-write a real starter config when one
321
333
  ## Typography configuration
322
334
 
323
335
  Set faces in config; the preset emits CSS vars and Tailwind `fontFamily` keys.
336
+
324
337
  ```js
325
338
  typography: {
326
339
  display: "'Fraunces', 'Iowan Old Style', Georgia, serif", // heading/display face
@@ -328,8 +341,10 @@ typography: {
328
341
  mono: "'JetBrains Mono', 'SF Mono', Menlo, monospace",
329
342
  }
330
343
  ```
344
+
331
345
  Canonical config keys are `display` / `ui` / `mono`; `heading` / `sans` are accepted
332
346
  aliases. Usage classes: `font-heading` (display face), `font-body` (UI face), `font-mono`.
347
+
333
348
  ```html
334
349
  <h1 class="font-heading text-2xl text-ink">Page Title</h1>
335
350
  <p class="font-body text-[13px] text-ink-mute">Description</p>
@@ -343,7 +358,7 @@ aliases. Usage classes: `font-heading` (display face), `font-body` (UI face), `f
343
358
  `shape: { radius: 'soft' }` selects a preset emitting `--radius-{sm,md,lg,xl,full}`.
344
359
 
345
360
  | Preset | sm | md | lg | xl | full | Personality |
346
- |--------|----|----|----|----|------|-------------|
361
+ | -------- | ---- | ---- | ---- | ---- | ------ | ------------- |
347
362
  | `sharp` | 0 | 0 | 0 | 0 | 9999px | Minimal, editorial |
348
363
  | `soft` | 2px | 6px | 10px | 12px | 9999px | Professional, calm |
349
364
  | `rounded` | 4px | 8px | 12px | 16px | 9999px | Friendly, modern |
@@ -360,7 +375,7 @@ never `rounded-[6px]`, which bypasses the config.
360
375
  Rokkit inherits Tailwind's 4px-grid spacing. **Stay on the 4px grid, use Tailwind names.**
361
376
 
362
377
  | Tailwind | px | When |
363
- |----------|----|------|
378
+ | ---------- | ---- | ------ |
364
379
  | `1` / `2` / `3` | 4 / 8 / 12 | tight gap · component gap · compact padding |
365
380
  | `4` / `6` | 16 / 24 | default padding · panel padding |
366
381
  | `8` / `12` / `16` | 32 / 48 / 64 | section separation · page padding · hero whitespace |
@@ -400,7 +415,7 @@ export default {
400
415
  ## Common mistakes
401
416
 
402
417
  | Mistake | Why it fails | Fix |
403
- |---|---|---|
418
+ | --- | --- | --- |
404
419
  | "core only emits `accent`/`accent-soft`, so I can't have `accent-2`/`on-accent`" | the vocabulary is extensible, not fixed | add them under `overrides:` (or use `tokens: 'extended'`) — see Layer 3b |
405
420
  | Override palette ref `'sky-600'` (hyphen) | refs use **dot** notation | `'sky.600'` (and `sky` must be in `palettes:`) |
406
421
  | `color: #3D3730` in a component | breaks on theme change | `text-ink` (or the right named token) |
@@ -14,7 +14,7 @@ shifts.
14
14
  **Skin vs style vs mode:**
15
15
 
16
16
  | Dimension | Controls | Example values |
17
- |-----------|----------|----------------|
17
+ | ----------- | ---------- | ---------------- |
18
18
  | **skin** | which palette backs the roles | `default` · `ocean` · `violet` |
19
19
  | **style** | which visual theme (border radius, shadow, surface recipe) | `rokkit` · `minimal` · `zen-sumi` |
20
20
  | **mode** | light or dark rendering | `light` · `dark` |
@@ -59,7 +59,7 @@ The preset ships five built-in skins in `@rokkit/unocss`. Their `[data-skin='nam
59
59
  blocks are emitted automatically — no config needed to use them:
60
60
 
61
61
  | Skin | Surface | Ink | Primary | Accent |
62
- |------|---------|-----|---------|--------|
62
+ | ------ | --------- | ----- | --------- | -------- |
63
63
  | `default` | (DEFAULT_THEME_MAPPING) | `slate` | — | — |
64
64
  | `ocean` | `slate` | `slate` | `sky` | `teal` |
65
65
  | `violet` | `zinc` | `zinc` | `violet` | `indigo` |
@@ -124,7 +124,7 @@ When `storageKey` is provided it also loads/saves the persisted skin.
124
124
  **`SkinSwitcherToggle` props:**
125
125
 
126
126
  | Prop | Type | Default | Purpose |
127
- |------|------|---------|---------|
127
+ | ------ | ------ | --------- | --------- |
128
128
  | `skins` | `Array<string \| { name, label?, icon? }>` | `vibe.allowedSkins` | skin options |
129
129
  | `labels` | `Record<string, string>` | `{}` | display labels keyed by name |
130
130
  | `showLabels` | `boolean` | `false` | show label text in the toggle |
@@ -215,7 +215,7 @@ runtime-only color overrides.
215
215
  ## Common mistakes
216
216
 
217
217
  | Mistake | Why it fails | Fix |
218
- |---------|-------------|-----|
218
+ | --------- | ------------- | ----- |
219
219
  | `vibe.allowedSkins` set after `themable` runs | persisted skin can't restore; `vibe.skin = storedValue` is a no-op because the list defaults to `['default']` only | set `vibe.allowedSkins` before `<svelte:body use:themable ...>` in the root layout |
220
220
  | `class="skin-ocean"` on an element | `skin-{name}` class was removed | use `data-skin='ocean'` attribute, or let `themable` write it via `vibe.skin` |
221
221
  | Hand-rolling `<style>` injection for a skin | brittle, order-dependent, bypasses named-token aliases | define in `rokkit.config.js` `skins:` + let the preset emit `[data-skin='name']` blocks |
package/src/agents.js ADDED
@@ -0,0 +1,170 @@
1
+ /* eslint-disable no-console */
2
+ import {
3
+ readFileSync,
4
+ readdirSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ copyFileSync
9
+ } from 'node:fs'
10
+ import { join, resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import prompts from 'prompts'
13
+ import { parseFrontmatter } from './skills.js'
14
+
15
+ // The catalog ships inside the package: packages/cli/agents/ in dev/test,
16
+ // node_modules/@rokkit/cli/agents/ once installed. Agents are single .md files
17
+ // (unlike skills, which are directories with a SKILL.md).
18
+ // Guard against non-file: URLs (e.g. jsdom sets import.meta.url to http://localhost/...).
19
+ const DEFAULT_AGENTS_DIR = (() => {
20
+ try {
21
+ return fileURLToPath(new URL('../agents/', import.meta.url))
22
+ } catch {
23
+ return resolve(process.cwd(), 'packages/cli/agents')
24
+ }
25
+ })()
26
+
27
+ // Public base the CLI can pull agents from when the bundled copy is unavailable
28
+ // (`--remote`). Same corpus that the sensei.library.json manifest advertises.
29
+ const DEFAULT_AGENTS_URL = 'https://rokkit.sensei-hq.com/agents'
30
+
31
+ /**
32
+ * Read the bundled agent catalog (derived from each <name>.md frontmatter).
33
+ * @param {{ agentsDir?: string }} [opts]
34
+ * @returns {Array<{ name: string, description: string }>}
35
+ */
36
+ export function listAgents({ agentsDir = DEFAULT_AGENTS_DIR } = {}) {
37
+ if (!existsSync(agentsDir)) return []
38
+ return readdirSync(agentsDir)
39
+ .filter((entry) => entry.endsWith('.md'))
40
+ .map((entry) => parseFrontmatter(readFileSync(join(agentsDir, entry), 'utf-8')))
41
+ .filter((a) => a.name)
42
+ .sort((a, b) => a.name.localeCompare(b.name))
43
+ }
44
+
45
+ /**
46
+ * Copy named agents into <cwd>/.claude/agents/<name>.md from the bundled catalog (idempotent).
47
+ * @param {string[]} names
48
+ * @param {{ cwd?: string, force?: boolean, agentsDir?: string }} [opts]
49
+ * @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
50
+ */
51
+ export function installAgents(
52
+ names,
53
+ { cwd = process.cwd(), force = false, agentsDir = DEFAULT_AGENTS_DIR } = {}
54
+ ) {
55
+ const targetRoot = resolve(cwd, '.claude', 'agents')
56
+ return names.map((name) => {
57
+ const src = join(agentsDir, `${name}.md`)
58
+ if (!existsSync(src)) return { name, status: 'unknown' }
59
+ const dest = join(targetRoot, `${name}.md`)
60
+ if (existsSync(dest) && !force) return { name, status: 'skipped' }
61
+ mkdirSync(targetRoot, { recursive: true })
62
+ copyFileSync(src, dest)
63
+ return { name, status: 'added' }
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Fetch named agents from the public site and write them into <cwd>/.claude/agents/<name>.md.
69
+ * Lets the CLI pull from the site when the bundled catalog is absent (or on `--remote`).
70
+ * @param {string[]} names
71
+ * @param {{ cwd?: string, force?: boolean, baseUrl?: string, fetchImpl?: typeof fetch }} [opts]
72
+ * @returns {Promise<Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>>}
73
+ */
74
+ export async function fetchAgents(
75
+ names,
76
+ { cwd = process.cwd(), force = false, baseUrl = DEFAULT_AGENTS_URL, fetchImpl = fetch } = {}
77
+ ) {
78
+ const targetRoot = resolve(cwd, '.claude', 'agents')
79
+ const results = []
80
+ for (const name of names) {
81
+ const dest = join(targetRoot, `${name}.md`)
82
+ if (existsSync(dest) && !force) {
83
+ results.push({ name, status: 'skipped' })
84
+ continue
85
+ }
86
+ const res = await fetchImpl(`${baseUrl}/${name}.md`)
87
+ if (!res.ok) {
88
+ results.push({ name, status: 'unknown' })
89
+ continue
90
+ }
91
+ mkdirSync(targetRoot, { recursive: true })
92
+ writeFileSync(dest, await res.text())
93
+ results.push({ name, status: 'added' })
94
+ }
95
+ return results
96
+ }
97
+
98
+ /**
99
+ * Print the catalog, marking already-installed agents.
100
+ * @param {{ agentsDir?: string, cwd?: string }} [opts]
101
+ */
102
+ export async function runAgentsList({ agentsDir, cwd = process.cwd() } = {}) {
103
+ const catalog = listAgents({ agentsDir })
104
+ if (catalog.length === 0) {
105
+ console.info('No agents available.')
106
+ return
107
+ }
108
+ const installedRoot = resolve(cwd, '.claude', 'agents')
109
+ console.info('Available Rokkit agents:\n')
110
+ for (const a of catalog) {
111
+ const mark = existsSync(join(installedRoot, `${a.name}.md`)) ? '✓ ' : ' '
112
+ console.info(`${mark}${a.name}`)
113
+ console.info(` ${a.description}`)
114
+ }
115
+ console.info('\nInstall with: rokkit agents add <name> [...] (or --all)')
116
+ }
117
+
118
+ /**
119
+ * Resolve which agents to install, then copy (or fetch with --remote) and report.
120
+ * @param {string[]} names
121
+ * @param {{ all?: boolean, force?: boolean, remote?: boolean, agentsDir?: string, cwd?: string, fetchImpl?: typeof fetch }} [opts]
122
+ */
123
+ export async function runAgentsAdd(
124
+ names,
125
+ { all = false, force = false, remote = false, agentsDir, cwd = process.cwd(), fetchImpl } = {}
126
+ ) {
127
+ const catalog = listAgents({ agentsDir })
128
+ let selected = names
129
+ if (all) {
130
+ selected = catalog.map((a) => a.name)
131
+ } else if (selected.length === 0) {
132
+ const res = await prompts({
133
+ type: 'multiselect',
134
+ name: 'picked',
135
+ message: 'Select agents to add',
136
+ choices: catalog.map((a) => ({ title: a.name, value: a.name, description: a.description }))
137
+ })
138
+ selected = res.picked ?? []
139
+ }
140
+ if (selected.length === 0) {
141
+ console.info('No agents selected.')
142
+ return
143
+ }
144
+ const results = remote
145
+ ? await fetchAgents(selected, { cwd, force, fetchImpl })
146
+ : installAgents(selected, { cwd, force, agentsDir })
147
+ for (const r of results) {
148
+ if (r.status === 'added') console.info(` added .claude/agents/${r.name}.md`)
149
+ else if (r.status === 'skipped')
150
+ console.info(` skipped ${r.name} (exists — use --force to overwrite)`)
151
+ else console.error(` unknown agent: ${r.name}`)
152
+ }
153
+ if (results.some((r) => r.status === 'unknown')) {
154
+ if (!remote) console.error(`\nValid agents: ${catalog.map((a) => a.name).join(', ')}`)
155
+ process.exitCode = 1
156
+ }
157
+ }
158
+
159
+ /**
160
+ * CLI entry for `rokkit agents` subcommands.
161
+ * @param {'list' | 'add'} sub
162
+ * @param {{ _?: string[], all?: boolean, force?: boolean, remote?: boolean }} [opts]
163
+ */
164
+ export async function agentsCommand(sub, opts = {}) {
165
+ if (sub === 'add') {
166
+ await runAgentsAdd(opts._ ?? [], { all: opts.all, force: opts.force, remote: opts.remote })
167
+ } else {
168
+ await runAgentsList()
169
+ }
170
+ }
package/src/index.js CHANGED
@@ -139,4 +139,23 @@ prog
139
139
  await skillsCommand('add', opts)
140
140
  })
141
141
 
142
+ prog
143
+ .command('agents list')
144
+ .describe('List available Rokkit review agents')
145
+ .action(async () => {
146
+ const { agentsCommand } = await import('./agents.js')
147
+ await agentsCommand('list')
148
+ })
149
+
150
+ prog
151
+ .command('agents add')
152
+ .describe('Add Rokkit review agents to .claude/agents/')
153
+ .option('--all', 'Install all available agents')
154
+ .option('--force', 'Overwrite an existing agent')
155
+ .option('--remote', 'Pull agents from the site instead of the bundled copy')
156
+ .action(async (opts) => {
157
+ const { agentsCommand } = await import('./agents.js')
158
+ await agentsCommand('add', opts)
159
+ })
160
+
142
161
  prog.parse(process.argv)