@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,390 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: semantic-styles-rokkit
|
|
3
|
+
description: Use when building, styling, or auditing a Rokkit-powered app (Svelte 5 + UnoCSS presetRokkit) — covers the named-token vocabulary (paper / ink / primary / on-primary / *-soft), the rokkit.config.js palette→skin→tokens pipeline, dual-palette dark mode, migrating legacy z-scale utilities (bg-surface-z0, text-primary-z5) to named tokens, and the rokkit init / rokkit doctor CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Semantic Styles — Rokkit
|
|
7
|
+
|
|
8
|
+
Rokkit's token pipeline turns a small config into a consistent, themeable, dark-mode-ready
|
|
9
|
+
set of utility classes. Components use **named tokens** (`bg-paper`, `text-ink`,
|
|
10
|
+
`text-on-primary`) — never raw hex/oklch values. That indirection is the consistency
|
|
11
|
+
guarantee: change the config, the whole app reskins.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
rokkit.config.js
|
|
15
|
+
palettes + skin + colorSpace + tokens
|
|
16
|
+
↓
|
|
17
|
+
UnoCSS preset (presetRokkit) → CSS custom properties + utility classes
|
|
18
|
+
↓
|
|
19
|
+
Components use named-token utilities: bg-paper · text-ink · bg-primary · text-on-primary
|
|
20
|
+
(legacy z-scale utilities — bg-surface-z1 — still resolve via back-compat aliases)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The pipeline has three layers. Most authoring happens in **Layer 3 (named tokens)**.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Layer 1 — Palettes
|
|
28
|
+
|
|
29
|
+
**What:** A palette is a named 11-stop color scale from shade `50` (lightest) to `950`
|
|
30
|
+
(darkest), declared in `rokkit.config.js` under `palettes:`.
|
|
31
|
+
|
|
32
|
+
**Why:** Each role needs a range of shades so hover, active, tint-on-surface, and
|
|
33
|
+
text-on-fill all come from one color family. 11 stops gives resolution without banding.
|
|
34
|
+
|
|
35
|
+
**Two formats:**
|
|
36
|
+
|
|
37
|
+
*RGB hex* — standard web colors; use when you have an existing brand palette:
|
|
38
|
+
```js
|
|
39
|
+
palettes: { shu: { 50: '#FEF3EE', 500: '#E8552B', 950: '#3C100A' } }
|
|
40
|
+
// colorSpace: 'rgb' (default — may be omitted)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
*OKLCH bare components* — perceptual, uniform lightness steps; best for palettes designed
|
|
44
|
+
from scratch (increment L by equal steps for balanced results):
|
|
45
|
+
```js
|
|
46
|
+
palettes: { shu: { 50: '0.970 0.020 35', 500: '0.580 0.150 35', 950: '0.220 0.060 35' } },
|
|
47
|
+
colorSpace: 'oklch' // REQUIRED when palette values are bare "L C H" components
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The preset wraps values into complete colors when it emits CSS vars (see Layer 3).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Layer 2 — Skin and skins
|
|
55
|
+
|
|
56
|
+
**What:** A skin maps semantic role names to palette names. The core roles that back the
|
|
57
|
+
named-token vocabulary are: `surface`, `ink`, `primary`, `accent`, `success`, `warning`,
|
|
58
|
+
`danger`, `error`, `info`. (`secondary`/`tertiary` are accepted but no named token reads
|
|
59
|
+
them — they only matter if you also use `extended` z-scale utilities for those roles.)
|
|
60
|
+
|
|
61
|
+
**Why:** Decoupling role from palette lets you swap the whole scheme in config, not code.
|
|
62
|
+
`bg-paper` doesn't know whether `surface` is `kami`, `slate`, or `zinc`.
|
|
63
|
+
|
|
64
|
+
### Single skin (`skin:`) — one fixed colormap
|
|
65
|
+
```js
|
|
66
|
+
skin: {
|
|
67
|
+
surface: 'slate', ink: 'slate', // ink backs the ink-* text tokens
|
|
68
|
+
primary: 'orange', accent: 'sky',
|
|
69
|
+
success: 'green', warning: 'yellow', danger: 'red', error: 'red', info: 'cyan',
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
`colors:` is accepted as a back-compat alias but **prefer `skin:`**. Always include an
|
|
73
|
+
`ink` role — without it the `ink-*` text tokens silently fall back to the surface palette.
|
|
74
|
+
|
|
75
|
+
### Multi-skin (`skins:`) — named colormaps for runtime switching
|
|
76
|
+
```js
|
|
77
|
+
skins: {
|
|
78
|
+
default: { surface: { light: 'kami', dark: 'sumi' }, ink: { light: 'kami', dark: 'sumi' }, primary: 'shu' },
|
|
79
|
+
ocean: { surface: 'slate', primary: 'sky', accent: 'teal' },
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
`skins.default` is active on first load. **Use `skin:` OR `skins:`, not both** — providing
|
|
83
|
+
`skins:` makes `skin:` ignored.
|
|
84
|
+
|
|
85
|
+
### `tokens:` — which CSS the preset emits
|
|
86
|
+
```js
|
|
87
|
+
tokens: 'core' // default — emit the 24 named tokens (palette values inlined) + z-aliases
|
|
88
|
+
tokens: 'extended' // also emit the full 11-shade ladder (--color-{role}-{shade}) per role
|
|
89
|
+
```
|
|
90
|
+
`'core'` is the default and what apps should use. Reach for `'extended'` only when you need
|
|
91
|
+
the full ladder (charts / data-viz). Per-role is allowed: `tokens: { surface: 'extended' }`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Layer 2b — Dual-palette dark mode (how the flip actually works)
|
|
96
|
+
|
|
97
|
+
**Named tokens flip in dark mode ONLY when the skin uses a dual-palette `{ light, dark }`
|
|
98
|
+
mapping for a role (or an override carries a `dark` value).** The preset emits a
|
|
99
|
+
`[data-mode="dark"]` block redefining the tokens **only then** — there is no automatic
|
|
100
|
+
z-flip in the themes CSS anymore.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
skin: {
|
|
104
|
+
surface: { light: 'kami', dark: 'sumi' }, // ← emits the dark block
|
|
105
|
+
ink: { light: 'kami', dark: 'sumi' },
|
|
106
|
+
primary: 'shu', // single palette — constant across modes (correct for an accent)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The trick is that the **dark palette is authored inverted** relative to the light one, so
|
|
111
|
+
the same shade index reads correctly per mode:
|
|
112
|
+
|
|
113
|
+
| token (shade) | light (`kami`) | dark (`sumi`) |
|
|
114
|
+
|---------------|-----------------------|-----------------------|
|
|
115
|
+
| `--paper` (50) | `0.985…` (light bg) | `0.170…` (dark bg) |
|
|
116
|
+
| `--ink` (900) | `0.220…` (dark text) | `0.940…` (light text) |
|
|
117
|
+
|
|
118
|
+
> **Gotcha:** a **single-palette** surface (e.g. `surface: 'slate'`) emits **no dark
|
|
119
|
+
> block → no flip** → effectively light-only. For dark mode, make `surface` and `ink`
|
|
120
|
+
> dual-palette (the canonical `kami`/`sumi` OKLCH pair, where `sumi` is authored inverted).
|
|
121
|
+
> The `rokkit init` "Zen-Sumi" starter ships this; the plain Tailwind starter does not.
|
|
122
|
+
|
|
123
|
+
Dark mode flips when `data-mode="dark"` is on `<html>`/`<body>` — see the wiring section
|
|
124
|
+
below for how to apply and persist that correctly.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Applying & persisting the theme (don't hand-roll this)
|
|
129
|
+
|
|
130
|
+
**Never** hand-roll `data-mode` writes, `localStorage` save/load, or your own theme store.
|
|
131
|
+
The pattern is: the **`vibe` store** (`@rokkit/states`) holds reactive state; the
|
|
132
|
+
**`themable` action** (`@rokkit/actions`) syncs it to the DOM + storage; **`ThemeSwitcherToggle`**
|
|
133
|
+
(`@rokkit/app`) is the ready-made control.
|
|
134
|
+
|
|
135
|
+
**1. Wire the root once** (e.g. `+layout.svelte`). `themable` loads from storage on mount,
|
|
136
|
+
writes `data-style`/`data-mode`/`data-density` (mirrored to `<html>` to match the
|
|
137
|
+
flash-prevention init script), **saves on every change**, and syncs across tabs via the
|
|
138
|
+
`storage` event:
|
|
139
|
+
|
|
140
|
+
```svelte
|
|
141
|
+
<script>
|
|
142
|
+
import { vibe } from '@rokkit/states'
|
|
143
|
+
import { themable } from '@rokkit/actions'
|
|
144
|
+
</script>
|
|
145
|
+
<svelte:body use:themable={{ theme: vibe, storageKey: 'app-theme' }} />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**2. Add the mode control** anywhere — it drives `vibe.mode`, which `themable` then writes:
|
|
149
|
+
|
|
150
|
+
```svelte
|
|
151
|
+
<script>import { ThemeSwitcherToggle } from '@rokkit/app'</script>
|
|
152
|
+
<ThemeSwitcherToggle variant="single" /> <!-- one button, cycles light ↔ dark -->
|
|
153
|
+
<ThemeSwitcherToggle variant="triad" /> <!-- group: system | light | dark (default) -->
|
|
154
|
+
<ThemeSwitcherToggle variant="triad" includeSystem={false} /> <!-- group: light | dark -->
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**3. Switch the visual style** by setting state — no DOM writes:
|
|
158
|
+
```svelte
|
|
159
|
+
<button onclick={() => (vibe.style = 'minimal')}>Minimal</button>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**4. Prevent the flash on first paint.** Inject the pre-paint script so the persisted
|
|
163
|
+
theme applies before first render. SvelteKit apps use the SSR `themeHook`; static apps use
|
|
164
|
+
the script `rokkit init` writes into `app.html`:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// src/hooks.server.ts
|
|
168
|
+
import { themeHook } from '@rokkit/unocss/hooks'
|
|
169
|
+
export const handle = themeHook({ storageKey: 'app-theme', defaultMode: 'system' })
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`defaultMode: 'system'` (also the default when omitted) resolves `prefers-color-scheme`
|
|
173
|
+
to a concrete `light`/`dark` pre-paint — never an invalid `data-mode="system"`. `'auto'`
|
|
174
|
+
is accepted as a legacy alias. (Requires `@rokkit/unocss` ≥ 1.1.12.)
|
|
175
|
+
|
|
176
|
+
`vibe` (singleton) exposes reactive `mode` (`light`|`dark`), `style`
|
|
177
|
+
(`rokkit`|`minimal`|`material`), `density` (`compact`|`comfortable`|`cozy`), `direction`,
|
|
178
|
+
plus `load(key)` / `save(key)` / `update(partial)`. `ThemeSwitcherToggle` lives in
|
|
179
|
+
`@rokkit/app` (the store-wired shell package), built on `@rokkit/ui`'s `Toggle`; it adds
|
|
180
|
+
`system` on top of `vibe.mode` via `ColorModeManager` (resolving `system` through
|
|
181
|
+
`prefers-color-scheme`). The `storageKey` must be the same everywhere — `themable`,
|
|
182
|
+
`ThemeSwitcherToggle`'s store, and the flash-prevention script (step 4).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Layer 3 — Named tokens (what components use)
|
|
187
|
+
|
|
188
|
+
The 24-token core vocabulary, all flipping automatically under `[data-mode="dark"]`:
|
|
189
|
+
|
|
190
|
+
| Group | Tokens | Use |
|
|
191
|
+
|---------|---------------------------------------------------------|--------------------------------------------------|
|
|
192
|
+
| Surface | `paper` · `paper-soft` · `paper-mute` · `paper-edge` | canvas · card · subdued panel · hairline border |
|
|
193
|
+
| Text | `ink` · `ink-mute` · `ink-soft` · `ink-faint` | body · secondary · placeholder · disabled |
|
|
194
|
+
| Primary | `primary` · `on-primary` | CTA fill · text/icon on a primary fill |
|
|
195
|
+
| Accent | `accent` · `accent-soft` | accent fill · tinted accent background |
|
|
196
|
+
| Status | `success`/`-soft`, `warning`/`-soft`, `danger`/`-soft`, `error`/`-soft`, `info`/`-soft` | solid status · tinted callout bg |
|
|
197
|
+
| Misc | `focus-ring` · `shadow-tint` | focus ring color · shadow color |
|
|
198
|
+
|
|
199
|
+
**Prefixes:** `bg-`, `text-`, `border-` (+ `border-t/-b/-l/-r`), `fill-`, `stroke-`,
|
|
200
|
+
`ring-`, `outline-`, `divide-`. Two constraints: `on-primary` emits **text only**
|
|
201
|
+
(`text-on-primary`); `focus-ring` emits ring/outline/border/divide only; `shadow-tint`
|
|
202
|
+
emits no utility (used inside `box-shadow` expressions).
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<!-- card -->
|
|
206
|
+
<div class="bg-paper-soft border border-paper-edge rounded-md p-4">
|
|
207
|
+
<h3 class="text-ink text-sm font-medium">Title</h3>
|
|
208
|
+
<p class="text-ink-mute text-xs mt-1">Description</p>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- primary button + status badge -->
|
|
212
|
+
<button class="bg-primary text-on-primary rounded-md px-3 py-2">Save</button>
|
|
213
|
+
<span class="bg-success-soft text-success text-xs px-2 py-0.5 rounded-full">Active</span>
|
|
214
|
+
|
|
215
|
+
<!-- input -->
|
|
216
|
+
<input class="bg-paper border border-paper-edge text-ink rounded-md px-3 py-2
|
|
217
|
+
focus:border-primary focus:outline-none text-[13px]" />
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### CSS custom properties
|
|
221
|
+
|
|
222
|
+
Named tokens emit `--{token}` vars holding **complete color values** —
|
|
223
|
+
`--paper: oklch(0.985 0.005 85)` (oklch) or `--primary: rgb(232, 85, 43)` (rgb). Use them
|
|
224
|
+
directly in CSS-only contexts; for alpha, use `color-mix` (the form the preset itself uses):
|
|
225
|
+
```css
|
|
226
|
+
.thing { background: var(--paper); border-color: var(--paper-edge); }
|
|
227
|
+
.thing-50pc { background: color-mix(in oklch, var(--primary) 50%, transparent); }
|
|
228
|
+
```
|
|
229
|
+
Prefer utility classes over raw vars where you can. (In `core` mode the legacy
|
|
230
|
+
`--color-{role}-z{n}` vars are aliases pointing at named tokens, so they too resolve to
|
|
231
|
+
complete colors — the old "wrap bare components in `rgb()`/`oklch()`" rule no longer
|
|
232
|
+
applies in core mode.)
|
|
233
|
+
|
|
234
|
+
### Legacy z-scale utilities (back-compat)
|
|
235
|
+
|
|
236
|
+
The older z-scale utilities (`bg-surface-z0`, `text-surface-z9`, `text-primary-z5`, …)
|
|
237
|
+
**still resolve** as a back-compat layer that collapses onto the named tokens above —
|
|
238
|
+
existing code keeps working. New code should use named tokens. See the migration section.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Migrating from z-scale to named tokens
|
|
243
|
+
|
|
244
|
+
`rokkit doctor` scans `src/**` and prints the suggested replacement per occurrence
|
|
245
|
+
(advisory — it never rewrites your code). The mapping it uses:
|
|
246
|
+
|
|
247
|
+
**Surface role** (`{prefix}-surface-z{n}` → `{prefix}-{token}`):
|
|
248
|
+
|
|
249
|
+
| z | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|
|
250
|
+
|---|---|---|---|---|---|---|---|---|---|---|----|
|
|
251
|
+
| token | `paper` | `paper-soft` | `paper-mute` | `paper-mute` | `paper-edge` | `ink-soft` | `ink-soft` | `ink-mute` | `ink-mute` | `ink` | `ink` |
|
|
252
|
+
|
|
253
|
+
**Ink role** (inverted scale):
|
|
254
|
+
|
|
255
|
+
| z | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|
|
256
|
+
|---|---|---|---|---|---|---|---|---|---|---|----|
|
|
257
|
+
| token | `ink` | `ink-mute` | `ink-mute` | `ink-soft` | `ink-soft` | `paper-edge` | `paper-edge` | `paper-mute` | `paper-mute` | `paper-soft` | `paper` |
|
|
258
|
+
|
|
259
|
+
**Accent / status roles** (`primary`, `accent`, `success`, `warning`, `danger`, `error`,
|
|
260
|
+
`info`): `z0–z2 → {role}-soft` (tinted background), `z3+ → {role}` (solid). Exception:
|
|
261
|
+
`primary` has **no** `-soft` companion — `primary-z*` all map to `primary`; for a tinted
|
|
262
|
+
primary background use `accent-soft` or a custom token.
|
|
263
|
+
|
|
264
|
+
Common rewrites:
|
|
265
|
+
```
|
|
266
|
+
bg-surface-z0 → bg-paper text-surface-z9 → text-ink
|
|
267
|
+
bg-surface-z1 → bg-paper-soft text-surface-z7 → text-ink-mute
|
|
268
|
+
border-surface-z3 → border-paper-edge text-surface-z5 → text-ink-soft
|
|
269
|
+
text-primary-z5 → text-primary bg-success-z1 → bg-success-soft
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Rokkit CLI — `init` and `doctor`
|
|
275
|
+
|
|
276
|
+
**`rokkit init`** scaffolds `rokkit.config.js` in the named-token shape (a header comment
|
|
277
|
+
documents the vocabulary). Two starters via the palette prompt:
|
|
278
|
+
- **Tailwind (rgb)** — `skin` of Tailwind palette names, `colorSpace: 'rgb'`, `tokens: 'core'`. Single-palette → light-leaning (see Layer 2b).
|
|
279
|
+
- **Zen-Sumi (OKLCH)** — ships `palettes` (kami/sumi/shu/…), `colorSpace: 'oklch'`, dual-palette `surface`/`ink` → full dark mode out of the box.
|
|
280
|
+
|
|
281
|
+
**`rokkit doctor`** validates an existing setup. Beyond file checks (config exists,
|
|
282
|
+
`uno.config.js` uses `presetRokkit(config)`, `app.css` imports a theme, `app.html` has the
|
|
283
|
+
mode init script), it runs **advisory** config-shape warnings and migration hints — none
|
|
284
|
+
fail the command or block `--fix`:
|
|
285
|
+
|
|
286
|
+
| Check | Warns when |
|
|
287
|
+
|-------|-----------|
|
|
288
|
+
| `skin-ink-role` | the active colormap has no `ink` role (text tokens fall back) |
|
|
289
|
+
| `oklch-needs-palettes` | `colorSpace: 'oklch'` but no `palettes` block |
|
|
290
|
+
| `colors-alias` | config uses the legacy `colors:` instead of `skin:` |
|
|
291
|
+
| legacy z-utilities | `src/**` contains `bg-surface-z*` / `text-primary-z*` etc. (prints named-token suggestions) |
|
|
292
|
+
|
|
293
|
+
The "active colormap" is resolved the same way the preset does: `skins.default ?? skin ??
|
|
294
|
+
colors`. Run `rokkit doctor --fix` to auto-write a real starter config when one is missing.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Typography configuration
|
|
299
|
+
|
|
300
|
+
Set faces in config; the preset emits CSS vars and Tailwind `fontFamily` keys.
|
|
301
|
+
```js
|
|
302
|
+
typography: {
|
|
303
|
+
display: "'Fraunces', 'Iowan Old Style', Georgia, serif", // heading/display face
|
|
304
|
+
sans: "'Inter', system-ui, sans-serif", // UI/body face (alias: `ui`)
|
|
305
|
+
mono: "'JetBrains Mono', 'SF Mono', Menlo, monospace",
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
Canonical config keys are `display` / `ui` / `mono`; `heading` / `sans` are accepted
|
|
309
|
+
aliases. Usage classes: `font-heading` (display face), `font-body` (UI face), `font-mono`.
|
|
310
|
+
```html
|
|
311
|
+
<h1 class="font-heading text-2xl text-ink">Page Title</h1>
|
|
312
|
+
<p class="font-body text-[13px] text-ink-mute">Description</p>
|
|
313
|
+
<code class="font-mono text-xs text-ink-soft">path/to/file.ts</code>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Shape / radius configuration
|
|
319
|
+
|
|
320
|
+
`shape: { radius: 'soft' }` selects a preset emitting `--radius-{sm,md,lg,xl,full}`.
|
|
321
|
+
|
|
322
|
+
| Preset | sm | md | lg | xl | full | Personality |
|
|
323
|
+
|--------|----|----|----|----|------|-------------|
|
|
324
|
+
| `sharp` | 0 | 0 | 0 | 0 | 9999px | Minimal, editorial |
|
|
325
|
+
| `soft` | 2px | 6px | 10px | 12px | 9999px | Professional, calm |
|
|
326
|
+
| `rounded` | 4px | 8px | 12px | 16px | 9999px | Friendly, modern |
|
|
327
|
+
| `pill` | 9999px | 9999px | 9999px | 9999px | 9999px | Playful, pill-native |
|
|
328
|
+
|
|
329
|
+
Custom: `shape: { radius: { sm: '2px', md: '6px', lg: '10px', xl: '14px', full: '9999px' } }`.
|
|
330
|
+
Use the Tailwind radius utilities that map to the vars (`rounded-md`, `rounded-lg`, …) —
|
|
331
|
+
never `rounded-[6px]`, which bypasses the config.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Spacing
|
|
336
|
+
|
|
337
|
+
Rokkit inherits Tailwind's 4px-grid spacing. **Stay on the 4px grid, use Tailwind names.**
|
|
338
|
+
|
|
339
|
+
| Tailwind | px | When |
|
|
340
|
+
|----------|----|------|
|
|
341
|
+
| `1` / `2` / `3` | 4 / 8 / 12 | tight gap · component gap · compact padding |
|
|
342
|
+
| `4` / `6` | 16 / 24 | default padding · panel padding |
|
|
343
|
+
| `8` / `12` / `16` | 32 / 48 / 64 | section separation · page padding · hero whitespace |
|
|
344
|
+
|
|
345
|
+
Density: **compact** (tables/sidebars) `gap-1`/`gap-2`, `p-2`/`p-3`; **comfortable**
|
|
346
|
+
(cards/forms) `gap-3`/`gap-4`, `p-4`/`p-6`; **relaxed** (marketing) `16–24px+`.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Full config example
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
// rokkit.config.js
|
|
354
|
+
export default {
|
|
355
|
+
palettes: {
|
|
356
|
+
kami: { 50: '0.985 0.005 85', /* … */ 950: '0.170 0.010 50' }, // light surface (warm paper)
|
|
357
|
+
sumi: { 50: '0.170 0.010 50', /* … */ 950: '0.975 0.008 85' }, // dark surface (inverted ink)
|
|
358
|
+
shu: { 50: '0.970 0.020 35', 500: '0.580 0.150 35', 950: '0.220 0.060 35' }, // accent
|
|
359
|
+
},
|
|
360
|
+
colorSpace: 'oklch', // required for bare "L C H" palette values
|
|
361
|
+
tokens: 'core', // emit the named-token vocabulary (default)
|
|
362
|
+
skin: {
|
|
363
|
+
surface: { light: 'kami', dark: 'sumi' }, // dual-palette → dark mode
|
|
364
|
+
ink: { light: 'kami', dark: 'sumi' },
|
|
365
|
+
primary: 'shu', accent: 'shu',
|
|
366
|
+
success: 'hisui', warning: 'kohaku', danger: 'shu', error: 'shu', info: 'kohaku',
|
|
367
|
+
},
|
|
368
|
+
typography: { display: "'Fraunces', Georgia, serif", sans: "'Inter', sans-serif", mono: "'JetBrains Mono', monospace" },
|
|
369
|
+
shape: { radius: 'soft' },
|
|
370
|
+
switcher: 'full', // 'system' | 'manual' | 'full'
|
|
371
|
+
storageKey: 'app-theme',
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Common mistakes
|
|
378
|
+
|
|
379
|
+
| Mistake | Why it fails | Fix |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| `color: #3D3730` in a component | breaks on theme change | `text-ink` (or the right named token) |
|
|
382
|
+
| Using `bg-surface-z5` / `text-primary-z5` in new code | legacy back-compat layer | named tokens — see migration table (or run `rokkit doctor`) |
|
|
383
|
+
| Single-palette `surface` but expecting dark mode | no `[data-mode]` block is emitted | make `surface`/`ink` dual-palette `{ light, dark }` |
|
|
384
|
+
| Skin with no `ink` role | `ink-*` text tokens fall back to surface | add `ink: '<palette>'` (reuse surface is fine) |
|
|
385
|
+
| Using `skin:` and `skins:` together | `skins:` silently wins | pick one |
|
|
386
|
+
| `oklch(0.58 0.15 35)` literal in a component | loses theming + composability | `bg-primary` / `var(--primary)` |
|
|
387
|
+
| Double-wrapping in core mode: `oklch(var(--paper))` | `--paper` is already a complete color | use `var(--paper)` directly |
|
|
388
|
+
| `colorSpace: 'oklch'` with hex palettes (or vice-versa) | wrap form won't match values | match `colorSpace` to the palette format |
|
|
389
|
+
| `border-radius: 6px` / `font-family: 'Inter'` hardcoded | bypasses shape/typography config | `rounded-md` / `font-body` |
|
|
390
|
+
| `@apply bg-paper` on `body {}` | descendant-combinator shortcut can't match `body` when `data-mode` is on it | `body { background: var(--paper); }` |
|
package/src/index.js
CHANGED
|
@@ -121,4 +121,22 @@ prog
|
|
|
121
121
|
await theme('create', opts)
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
prog
|
|
125
|
+
.command('skills list')
|
|
126
|
+
.describe('List available Rokkit AI skills')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
const { skillsCommand } = await import('./skills.js')
|
|
129
|
+
await skillsCommand('list')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
prog
|
|
133
|
+
.command('skills add')
|
|
134
|
+
.describe('Add Rokkit AI skills to .claude/skills/')
|
|
135
|
+
.option('--all', 'Install all available skills')
|
|
136
|
+
.option('--force', 'Overwrite an existing skill')
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const { skillsCommand } = await import('./skills.js')
|
|
139
|
+
await skillsCommand('add', opts)
|
|
140
|
+
})
|
|
141
|
+
|
|
124
142
|
prog.parse(process.argv)
|
package/src/init.js
CHANGED
|
@@ -314,13 +314,30 @@ export function generateAppCssImports(themes) {
|
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
316
|
* Generate the flash-prevention script for app.html.
|
|
317
|
-
*
|
|
317
|
+
*
|
|
318
|
+
* Themes flip on `[data-mode="dark"]` only — there is no `@media
|
|
319
|
+
* (prefers-color-scheme)` fallback — so every switcher needs a script that
|
|
320
|
+
* resolves the mode pre-paint (mirrors `@rokkit/unocss`'s `themeHook`):
|
|
321
|
+
* - `system` tracks the OS preference directly (no persisted toggle).
|
|
322
|
+
* - `manual` / `full` read the persisted mode and resolve a stored-or-default
|
|
323
|
+
* `'system'`/`'auto'` against the OS preference (default: follow the OS).
|
|
324
|
+
*
|
|
318
325
|
* @param {string} switcher — 'system' | 'manual' | 'full'
|
|
319
326
|
* @param {string} [storageKey='rokkit-theme']
|
|
320
|
-
* @
|
|
327
|
+
* @param {string} [defaultStyle='rokkit']
|
|
328
|
+
* @returns {string} the `<script>` tag to inject after `<body>`
|
|
321
329
|
*/
|
|
322
330
|
export function generateInitScript(switcher, storageKey = 'rokkit-theme', defaultStyle = 'rokkit') {
|
|
323
|
-
if (switcher === 'system')
|
|
331
|
+
if (switcher === 'system') {
|
|
332
|
+
return ` <script>
|
|
333
|
+
(function () {
|
|
334
|
+
try {
|
|
335
|
+
document.body.dataset.mode =
|
|
336
|
+
matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
337
|
+
} catch (e) {}
|
|
338
|
+
})()
|
|
339
|
+
</script>`
|
|
340
|
+
}
|
|
324
341
|
|
|
325
342
|
const setStyle =
|
|
326
343
|
switcher === 'full' ? `b.dataset.style = t.style || '${defaultStyle}'\n ` : ''
|
|
@@ -330,7 +347,10 @@ export function generateInitScript(switcher, storageKey = 'rokkit-theme', defaul
|
|
|
330
347
|
try {
|
|
331
348
|
var t = JSON.parse(localStorage.getItem('${storageKey}') || '{}')
|
|
332
349
|
var b = document.body
|
|
333
|
-
${setStyle}
|
|
350
|
+
${setStyle}var m = t.mode || 'system'
|
|
351
|
+
if (m === 'system' || m === 'auto')
|
|
352
|
+
m = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
353
|
+
b.dataset.mode = m
|
|
334
354
|
} catch (e) {}
|
|
335
355
|
})()
|
|
336
356
|
</script>`
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { readFileSync, readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import prompts from 'prompts'
|
|
6
|
+
|
|
7
|
+
// The catalog ships inside the package: packages/cli/skills/ in dev/test,
|
|
8
|
+
// node_modules/@rokkit/cli/skills/ once installed. One relative path, both worlds.
|
|
9
|
+
// Guard against non-file: URLs (e.g. jsdom sets import.meta.url to http://localhost/...).
|
|
10
|
+
const DEFAULT_SKILLS_DIR = (() => {
|
|
11
|
+
try {
|
|
12
|
+
return fileURLToPath(new URL('../skills/', import.meta.url))
|
|
13
|
+
} catch {
|
|
14
|
+
return resolve(process.cwd(), 'packages/cli/skills')
|
|
15
|
+
}
|
|
16
|
+
})()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse `name` and `description` from a SKILL.md YAML frontmatter block.
|
|
20
|
+
* @param {string} md
|
|
21
|
+
* @returns {{ name: string, description: string }}
|
|
22
|
+
*/
|
|
23
|
+
export function parseFrontmatter(md) {
|
|
24
|
+
// Keep these as literal regexes — do NOT collapse into a dynamic
|
|
25
|
+
// `new RegExp(`^${key}:...`)` helper: the keys are fixed, so the dynamic form
|
|
26
|
+
// adds nothing but trips the no-dynamic-regex lint, and the literals are
|
|
27
|
+
// plainly linear-time. The `?.[1]?.trim() ?? ''` chain returns '' for a
|
|
28
|
+
// missing block, a missing field, or a bare `key:` with no value.
|
|
29
|
+
const block = md.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? ''
|
|
30
|
+
const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? ''
|
|
31
|
+
const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? ''
|
|
32
|
+
return { name, description }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the bundled skill catalog (derived from each SKILL.md's frontmatter).
|
|
37
|
+
* @param {{ skillsDir?: string }} [opts]
|
|
38
|
+
* @returns {Array<{ name: string, description: string }>}
|
|
39
|
+
*/
|
|
40
|
+
export function listSkills({ skillsDir = DEFAULT_SKILLS_DIR } = {}) {
|
|
41
|
+
if (!existsSync(skillsDir)) return []
|
|
42
|
+
return readdirSync(skillsDir)
|
|
43
|
+
.filter((entry) => existsSync(join(skillsDir, entry, 'SKILL.md')))
|
|
44
|
+
.map((entry) => parseFrontmatter(readFileSync(join(skillsDir, entry, 'SKILL.md'), 'utf-8')))
|
|
45
|
+
.filter((s) => s.name)
|
|
46
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Copy named skills into <cwd>/.claude/skills/<name>/ (idempotent).
|
|
51
|
+
* @param {string[]} names
|
|
52
|
+
* @param {{ cwd?: string, force?: boolean, skillsDir?: string }} [opts]
|
|
53
|
+
* @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
|
|
54
|
+
*/
|
|
55
|
+
export function installSkills(
|
|
56
|
+
names,
|
|
57
|
+
{ cwd = process.cwd(), force = false, skillsDir = DEFAULT_SKILLS_DIR } = {}
|
|
58
|
+
) {
|
|
59
|
+
const targetRoot = resolve(cwd, '.claude', 'skills')
|
|
60
|
+
return names.map((name) => {
|
|
61
|
+
const src = join(skillsDir, name)
|
|
62
|
+
if (!existsSync(join(src, 'SKILL.md'))) return { name, status: 'unknown' }
|
|
63
|
+
const dest = join(targetRoot, name)
|
|
64
|
+
if (existsSync(dest) && !force) return { name, status: 'skipped' }
|
|
65
|
+
mkdirSync(targetRoot, { recursive: true })
|
|
66
|
+
cpSync(src, dest, { recursive: true })
|
|
67
|
+
return { name, status: 'added' }
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Print the catalog, marking already-installed skills.
|
|
73
|
+
* @param {{ skillsDir?: string, cwd?: string }} [opts]
|
|
74
|
+
*/
|
|
75
|
+
export async function runSkillsList({ skillsDir, cwd = process.cwd() } = {}) {
|
|
76
|
+
const catalog = listSkills({ skillsDir })
|
|
77
|
+
if (catalog.length === 0) {
|
|
78
|
+
console.info('No skills available.')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const installedRoot = resolve(cwd, '.claude', 'skills')
|
|
82
|
+
console.info('Available Rokkit skills:\n')
|
|
83
|
+
for (const s of catalog) {
|
|
84
|
+
const mark = existsSync(join(installedRoot, s.name, 'SKILL.md')) ? '✓ ' : ' '
|
|
85
|
+
console.info(`${mark}${s.name}`)
|
|
86
|
+
console.info(` ${s.description}`)
|
|
87
|
+
}
|
|
88
|
+
console.info('\nInstall with: rokkit skills add <name> [...] (or --all)')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve which skills to install, then copy and report.
|
|
93
|
+
* @param {string[]} names
|
|
94
|
+
* @param {{ all?: boolean, force?: boolean, skillsDir?: string, cwd?: string }} [opts]
|
|
95
|
+
*/
|
|
96
|
+
export async function runSkillsAdd(
|
|
97
|
+
names,
|
|
98
|
+
{ all = false, force = false, skillsDir, cwd = process.cwd() } = {}
|
|
99
|
+
) {
|
|
100
|
+
const catalog = listSkills({ skillsDir })
|
|
101
|
+
let selected = names
|
|
102
|
+
if (all) {
|
|
103
|
+
selected = catalog.map((s) => s.name)
|
|
104
|
+
} else if (selected.length === 0) {
|
|
105
|
+
const res = await prompts({
|
|
106
|
+
type: 'multiselect',
|
|
107
|
+
name: 'picked',
|
|
108
|
+
message: 'Select skills to add',
|
|
109
|
+
choices: catalog.map((s) => ({ title: s.name, value: s.name, description: s.description }))
|
|
110
|
+
})
|
|
111
|
+
selected = res.picked ?? []
|
|
112
|
+
}
|
|
113
|
+
if (selected.length === 0) {
|
|
114
|
+
console.info('No skills selected.')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const results = installSkills(selected, { cwd, force, skillsDir })
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
if (r.status === 'added') console.info(` added .claude/skills/${r.name}`)
|
|
120
|
+
else if (r.status === 'skipped')
|
|
121
|
+
console.info(` skipped ${r.name} (exists — use --force to overwrite)`)
|
|
122
|
+
else console.error(` unknown skill: ${r.name}`)
|
|
123
|
+
}
|
|
124
|
+
if (results.some((r) => r.status === 'unknown')) {
|
|
125
|
+
console.error(`\nValid skills: ${catalog.map((s) => s.name).join(', ')}`)
|
|
126
|
+
process.exitCode = 1
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* CLI entry for `rokkit skills` subcommands.
|
|
132
|
+
* @param {'list' | 'add'} sub
|
|
133
|
+
* @param {{ _?: string[], all?: boolean, force?: boolean }} [opts]
|
|
134
|
+
*/
|
|
135
|
+
export async function skillsCommand(sub, opts = {}) {
|
|
136
|
+
if (sub === 'add') {
|
|
137
|
+
await runSkillsAdd(opts._ ?? [], { all: opts.all, force: opts.force })
|
|
138
|
+
} else {
|
|
139
|
+
await runSkillsList()
|
|
140
|
+
}
|
|
141
|
+
}
|