variantkit 0.1.0
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/AGENT.md +428 -0
- package/LICENSE +21 -0
- package/NAMING.md +42 -0
- package/README.md +111 -0
- package/package.json +36 -0
- package/variantkit/README.md +32 -0
- package/variantkit/buildDecision.ts +264 -0
- package/variantkit/configs.ts +70 -0
- package/variantkit/dialkit-clean.css +46 -0
- package/variantkit/dialkit-dark.css +27 -0
- package/variantkit/init.mjs +587 -0
- package/variantkit/motion.css +77 -0
- package/variantkit/patches/dialkit+1.2.0.patch +32 -0
- package/variantkit/react/VariantBar.tsx +208 -0
- package/variantkit/react/VariantStage.tsx +92 -0
- package/variantkit/react/vkStore.ts +38 -0
- package/variantkit/react.tsx +234 -0
- package/variantkit/schemas/archetypes.ts +216 -0
- package/variantkit/schemas/sections.ts +151 -0
- package/variantkit/skill/SKILL.md +223 -0
- package/variantkit/templates/next-pages-api.ts +33 -0
- package/variantkit/templates/next-route.ts +33 -0
- package/variantkit/vite-plugin.d.mts +8 -0
- package/variantkit/vite-plugin.mjs +55 -0
package/AGENT.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# VariantKit — Agent Contract
|
|
2
|
+
|
|
3
|
+
`contractVersion: 0.3`
|
|
4
|
+
|
|
5
|
+
VariantKit lets a developer generate N structural variants of a component, choose one
|
|
6
|
+
live in the running app, tweak its params, and finalize a winner. You (the coding agent)
|
|
7
|
+
do two things: **scaffold** variant sets when asked for "options/variants/takes", and
|
|
8
|
+
**prune** a set down to the chosen winner when a decision is finalized.
|
|
9
|
+
|
|
10
|
+
Pruning must be reliable. It is mostly file deletion plus one file rename. Never move JSX
|
|
11
|
+
between files by hand. Follow this exactly.
|
|
12
|
+
|
|
13
|
+
**Vocabulary:** element, variant, control, configuration, snapshot, finalize, decision, prune
|
|
14
|
+
— one word per concept, defined in `NAMING.md`. Use them consistently.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 0. When to offer variants (read this first)
|
|
19
|
+
|
|
20
|
+
**Default to divergence.** When the developer asks you to build or change any user-facing
|
|
21
|
+
UI — a component, screen, section, state, layout, or visual treatment — do NOT commit to a
|
|
22
|
+
single interpretation. Scaffold a **variant set** of 2–4 structural takes (§1) and let the
|
|
23
|
+
developer choose live in the panel, then finalize. Generating N is the point; one is the
|
|
24
|
+
exception.
|
|
25
|
+
|
|
26
|
+
Offer variants especially when:
|
|
27
|
+
- the request is aesthetic or open-ended ("a pricing card", "make this nicer", "a hero")
|
|
28
|
+
- there are real directions to explore (layout, density, tone, emphasis, structure)
|
|
29
|
+
- the developer has not pinned exact specs
|
|
30
|
+
|
|
31
|
+
Skip variants only when the change is mechanical and unambiguous (a copy fix, a wired bug,
|
|
32
|
+
an exact pixel spec) or the developer explicitly says "just one". When unsure, offer options.
|
|
33
|
+
|
|
34
|
+
Each variant you generate must pass the deslop rules in §6 — generated UI should not look
|
|
35
|
+
AI-generated.
|
|
36
|
+
|
|
37
|
+
**VariantKit presents; the project decides.** VariantKit is the panel, the wiring, the
|
|
38
|
+
finalize → prune loop — nothing else. It has no opinion on what the variants look like or
|
|
39
|
+
what their controls are:
|
|
40
|
+
|
|
41
|
+
- **Design comes from the project.** You already know this project's design system, tokens,
|
|
42
|
+
and guidelines — every variant follows them, exactly as a hand-built component would.
|
|
43
|
+
VariantKit ships no colors, radii, fonts, spacings, or "house style". Never introduce a
|
|
44
|
+
VariantKit default into project UI.
|
|
45
|
+
- **Controls come from the element.** For each element, author the controls that genuinely
|
|
46
|
+
matter for tweaking THAT element — its real design axes. Any number, any kind. There is no
|
|
47
|
+
standard control set, and no control is required besides finalize.
|
|
48
|
+
- **Defaults come from the code.** Every control's default is the element's current/intended
|
|
49
|
+
value from the project (its tokens, its existing CSS) — never an invented value.
|
|
50
|
+
- **The rendered element stays untouched.** No rings, badges, overlays, entrance animations,
|
|
51
|
+
or layout imposed on project UI. The user must be able to judge the element exactly as it
|
|
52
|
+
will ship.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 1. Scaffolding a variant set
|
|
57
|
+
|
|
58
|
+
When the developer asks for "options" / "N takes" / "variants" of a component, create a
|
|
59
|
+
folder, file-per-variant:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
ComponentName/
|
|
63
|
+
index.tsx # thin shell: drives selection via DialKit, renders the active variant.
|
|
64
|
+
# THE ONLY FILE THAT IMPORTS dialkit / variantkit / registry / buildDecision.
|
|
65
|
+
registry.ts # maps variant key -> { component, label }
|
|
66
|
+
variants/
|
|
67
|
+
<keyA>.tsx # one self-contained component per variant
|
|
68
|
+
<keyB>.tsx
|
|
69
|
+
<keyC>.tsx
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- Each `variants/<key>.tsx` **default-exports** a component taking the SAME props.
|
|
74
|
+
- Each variant file is **self-contained**: it imports nothing from VariantKit/DialKit and
|
|
75
|
+
nothing from a shared variant helper. Whatever it needs (e.g. the transition) is a local
|
|
76
|
+
const in that file.
|
|
77
|
+
- **Shared transition (morph rule):** every variant declares the *identical* transition
|
|
78
|
+
local const on the morph-able properties (radius, padding, shadow, color). Duplicated on
|
|
79
|
+
purpose so the winner is self-contained after prune and switching reads as a morph, not a
|
|
80
|
+
snap.
|
|
81
|
+
- Only `index.tsx` wires the tool. That single-point-of-wiring is what makes prune a
|
|
82
|
+
deletion, not surgery.
|
|
83
|
+
|
|
84
|
+
## 2. Build on DialKit (v0)
|
|
85
|
+
|
|
86
|
+
`index.tsx` drives selection through DialKit — variant choice is a `select`, params are
|
|
87
|
+
controls, finalize is an `action`. **You author the controls per element** — there is no
|
|
88
|
+
preset menu. `panelConfig(controls, variantKeys, opts)` (from `variantkit/configs`) wraps
|
|
89
|
+
your controls with the structural parts: a `variant` select (only when there are 2+ keys) and
|
|
90
|
+
a `finalize` action. Derive `defaults` with `defaultsOf` so you never hand-maintain a separate
|
|
91
|
+
defaults object.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useDialKit } from 'dialkit'
|
|
95
|
+
import { registry } from './registry'
|
|
96
|
+
import { buildDecision, submitDecision } from '../../variantkit/buildDecision'
|
|
97
|
+
import { panelConfig, defaultsOf, regOf } from '../../variantkit/configs'
|
|
98
|
+
|
|
99
|
+
const KEYS = ['ledger', 'slab', 'inverse']
|
|
100
|
+
// These controls are EXAMPLES — derive yours from this element + this project's tokens.
|
|
101
|
+
const cfg = panelConfig(
|
|
102
|
+
{
|
|
103
|
+
density: { type: 'select', options: ['compact', 'comfortable'], default: 'comfortable' },
|
|
104
|
+
accent: tokens.brand, // default = the project's real token, not an invented hex
|
|
105
|
+
showAnnualToggle: true,
|
|
106
|
+
},
|
|
107
|
+
KEYS,
|
|
108
|
+
{ component: 'PricingCard' },
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
export default function PricingCard(props: { plan?: string }) {
|
|
112
|
+
const v = useDialKit('PricingCard', cfg, {
|
|
113
|
+
onAction: () => submitDecision(buildDecision('PricingCard', v, defaultsOf(cfg), regOf(KEYS))),
|
|
114
|
+
})
|
|
115
|
+
const Active = registry[v.variant].component
|
|
116
|
+
return <Active {...props} density={v.density} accent={v.accent} showAnnualToggle={v.showAnnualToggle} />
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Authoring controls — contextual, unrestricted
|
|
121
|
+
|
|
122
|
+
Ask: *which axes of this element would the developer actually want to tweak before
|
|
123
|
+
committing?* Those are the controls. Nothing else.
|
|
124
|
+
|
|
125
|
+
- **Any DialKit control is fair game:** number `[default, min, max]` → slider; string →
|
|
126
|
+
text input; `#hex` string → color picker; boolean → segmented Off|On toggle; `select` →
|
|
127
|
+
dropdown; `{ type: 'spring' | 'transition' }` → motion editor (only for elements that
|
|
128
|
+
actually move); a nested object → a folder (group related controls of a complex element).
|
|
129
|
+
- **Any count.** A button might need two controls; a data table might need ten in two
|
|
130
|
+
folders. More is not better — show what this element needs, nothing it doesn't.
|
|
131
|
+
- **Never copy a control set** from this file, another element, or a previous project. The
|
|
132
|
+
set that repeats across unrelated elements is by definition not contextual. (The archetype
|
|
133
|
+
schemas in §7 are checklists of design axes to ADAPT and seed from the code — not sets to
|
|
134
|
+
paste.)
|
|
135
|
+
- **Cover the element fully.** Contextual does not mean minimal — §7 sets the completeness
|
|
136
|
+
bar: the panel should feel like the element's actual configuration panel.
|
|
137
|
+
- **Defaults are the project's values.** Pull them from the design tokens or the element's
|
|
138
|
+
current styles. If you typed a literal that exists nowhere in the project, it's wrong.
|
|
139
|
+
|
|
140
|
+
### Multiple elements → ONE panel (folders), never many
|
|
141
|
+
|
|
142
|
+
DialKit renders one panel per `useDialKit` call, so calling it from each component clutters
|
|
143
|
+
the screen with stacked panels. For 2+ elements in play, make a **single** `useDialKit` call
|
|
144
|
+
whose config has **one folder per element** (a nested object becomes a folder). Route each
|
|
145
|
+
folder's finalize via `onAction(path)` — `path` is dot-notation, e.g. `PricingCard.finalize`.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const ELEMENTS = [
|
|
149
|
+
{ name: 'Hero', keys: ['centered','split','minimal'], controls: heroControls },
|
|
150
|
+
{ name: 'PricingCard', keys: ['slab','ledger','inverse'], controls: cardControls },
|
|
151
|
+
]
|
|
152
|
+
const combined = Object.fromEntries(
|
|
153
|
+
ELEMENTS.map((e, i) => [e.name, { ...panelConfig(e.controls, e.keys, { component: e.name }), _collapsed: i !== 0 }]),
|
|
154
|
+
)
|
|
155
|
+
const all = useDialKit('VariantKit', combined, {
|
|
156
|
+
onAction: (path) => {
|
|
157
|
+
const e = ELEMENTS.find((x) => x.name === path.split('.')[0])!
|
|
158
|
+
submitDecision(buildDecision(e.name, all[e.name], defaultsOf(panelConfig(e.controls, e.keys)), regOf(e.keys)))
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
// values are nested: all.Hero.headingSize, all.PricingCard.density, ...
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`_collapsed: true` starts a folder closed (first one open). Result: one panel, a section per
|
|
165
|
+
element, each with its contextual controls and its own Finalize. See `examples/contextual`.
|
|
166
|
+
|
|
167
|
+
**Easiest path — the `Studio` helper.** Don't hand-write the wiring above; use it:
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Studio, type ElementDef } from './variantkit/react'
|
|
171
|
+
|
|
172
|
+
const ELEMENTS: ElementDef[] = [
|
|
173
|
+
{ name: 'PricingCard', keys: ['slab','ledger','inverse'], controls: cardControls, render: (variant, v) => <Card variant={variant} {...v} /> },
|
|
174
|
+
// ...more elements, each with ITS OWN authored controls
|
|
175
|
+
]
|
|
176
|
+
<Studio elements={ELEMENTS} focusOnHover /> // one panel, folders, finalize routing, focus
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`focusOnHover` expands the panel folder of the element you hover — so the panel always shows
|
|
180
|
+
"the element you're editing." It is panel-side only: **nothing is ever drawn on or around the
|
|
181
|
+
rendered element** (no rings, badges, or overlays — the user must see the element exactly as
|
|
182
|
+
it ships). For a SINGLE element, just pass one entry (one folder, nothing extra); with one
|
|
183
|
+
variant key, no variant dropdown is shown. DialKit's `<DialRoot/>` must be mounted once in
|
|
184
|
+
the app root.
|
|
185
|
+
|
|
186
|
+
### On-canvas chrome — VariantBar and VariantStage
|
|
187
|
+
|
|
188
|
+
`<VariantBar/>` (`variantkit/react/VariantBar`) mounts once next to `<DialRoot/>`: a slim
|
|
189
|
+
bottom bar with variant tabs, keys 1..9, a live side-by-side **Compare** toggle, and
|
|
190
|
+
Finalize. It auto-discovers every variant set from DialKit's store — both the classic
|
|
191
|
+
shell layout and the Studio's folder-per-element layout — no per-component wiring. It is
|
|
192
|
+
tool chrome at the screen edge; it never decorates the rendered element itself.
|
|
193
|
+
|
|
194
|
+
For the classic single-set shell, render through `<VariantStage/>`
|
|
195
|
+
(`variantkit/react/VariantStage`) instead of a bare `<Active/>` — same render normally,
|
|
196
|
+
and a live grid of all variants when Compare is on (clicking a cell selects it):
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
return <VariantStage name="PricingCard" registry={registry} active={String(v.variant)} props={variantProps} />
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Hide the redundant copy button
|
|
203
|
+
|
|
204
|
+
Import these three stylesheets once (the `Studio` helper assumes them):
|
|
205
|
+
- `variantkit/dialkit-clean.css` — hides the redundant "Copy parameters" button and the
|
|
206
|
+
folder/header dividers (panel reads on spacing, like DialKit's own UI). **Keeps the preset
|
|
207
|
+
toolbar** — that's the snapshot mechanism (below).
|
|
208
|
+
- `variantkit/dialkit-dark.css` — the dark palette DialKit lacks. Call `useDialkitTheme()`
|
|
209
|
+
(from `variantkit/react`) once: it applies the theme, persists it, and injects a sun/moon
|
|
210
|
+
toggle into the panel header so the user flips the panel's light/dark right there.
|
|
211
|
+
- `variantkit/motion.css` — press feedback, theme-switch cross-fade, reduced-motion. Scoped
|
|
212
|
+
strictly to the panel chrome; it never styles or animates the project's UI.
|
|
213
|
+
|
|
214
|
+
### Snapshots — keep two tunings and compare
|
|
215
|
+
|
|
216
|
+
A **Snapshot** = a saved (variant + control values) state, so you can keep two tunings of one
|
|
217
|
+
element (Slab/12/green vs Inverse/4/amber) and pick one. This is DialKit's **preset toolbar**
|
|
218
|
+
(the ≡+ "add" and the Version dropdown), reused: because the variant is itself a control, a
|
|
219
|
+
preset captures variant+values, and DialKit restores them atomically on switch. Finalize acts
|
|
220
|
+
on the **active** snapshot. So: tune → ≡+ to snapshot → tune differently → switch between them
|
|
221
|
+
→ Finalize the winner. (We hide only the redundant Copy button, not the preset toolbar.)
|
|
222
|
+
|
|
223
|
+
## 3. `decision.json` schema (schema 2)
|
|
224
|
+
|
|
225
|
+
Finalize writes one decision per component. Values are **dot-path flattened** — folder
|
|
226
|
+
groups become `"surface.radius"` — so grouped configurations stay flat and inlineable.
|
|
227
|
+
|
|
228
|
+
```jsonc
|
|
229
|
+
{
|
|
230
|
+
"schema": 2,
|
|
231
|
+
"component": "PricingCard",
|
|
232
|
+
"finalized": "slab", // the winning variant key
|
|
233
|
+
"values": { // final live values to inline
|
|
234
|
+
"density": "compact",
|
|
235
|
+
"surface.radius": 12,
|
|
236
|
+
"accent": "#175048"
|
|
237
|
+
},
|
|
238
|
+
"overridesFromDefault": { // only changed keys; the taste signal
|
|
239
|
+
"density": { "from": "comfortable", "to": "compact" },
|
|
240
|
+
"accent": { "from": "#1F5E54", "to": "#175048" } // from = the project's own default
|
|
241
|
+
},
|
|
242
|
+
"prune": ["ledger", "inverse"], // loser keys to delete
|
|
243
|
+
"note": "",
|
|
244
|
+
"status": "pending",
|
|
245
|
+
"timestamp": "2026-06-09T00:00:00Z"
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
When inlining (§4), a dot-path maps to the prop the shell derived from it: the prop fed by
|
|
250
|
+
`v.surface.radius` gets the literal at `"surface.radius"`. Token values (`"shadow": "lg"`)
|
|
251
|
+
inline as the **resolved CSS** the shell produced, not the token string.
|
|
252
|
+
|
|
253
|
+
## 4. Prune algorithm (the reliable part)
|
|
254
|
+
|
|
255
|
+
**Where decisions come from.** Finalize ships the decision through the dev transport
|
|
256
|
+
(vite plugin / Next route) into `.variantkit/decisions/<Component>.json`. When no
|
|
257
|
+
transport is running it falls back to the clipboard and the developer pastes it to you.
|
|
258
|
+
|
|
259
|
+
**When to apply.** On "apply decision" / "apply the decision" — and at the start of any
|
|
260
|
+
session in a VariantKit project — scan `.variantkit/decisions/*.json` for
|
|
261
|
+
`status: "pending"` and prune each one. A pasted decision JSON is applied the same way.
|
|
262
|
+
|
|
263
|
+
Given a decision with `status: "pending"`:
|
|
264
|
+
|
|
265
|
+
1. **INLINE** the `values` into the winner file `variants/<finalized>.tsx` — replace the
|
|
266
|
+
prop-driven values with the literals (so the component needs no incoming params).
|
|
267
|
+
2. **PROMOTE by rename** — `mv variants/<finalized>.tsx index.tsx`, overwriting the old
|
|
268
|
+
shell. **Do NOT copy code into the old index by hand.** (Rule 1A: rename, never move JSX.)
|
|
269
|
+
3. **DELETE** every loser in `prune` plus any leftover `variants/*.tsx`; remove the now-empty
|
|
270
|
+
`variants/` folder.
|
|
271
|
+
4. **DELETE** `registry.ts`.
|
|
272
|
+
5. Mark the decision `resolved`: append the decision (with `"status": "resolved"`) as one
|
|
273
|
+
line to `.variantkit/history/log.jsonl`, then delete
|
|
274
|
+
`.variantkit/decisions/<Component>.json`. For pasted decisions, append the same way.
|
|
275
|
+
|
|
276
|
+
Net diff: deleted files + one renamed file + inlined literals. No JSX moved between files.
|
|
277
|
+
|
|
278
|
+
## 5. Self-check after pruning (ALL must be true)
|
|
279
|
+
|
|
280
|
+
- [ ] No file imports `dialkit`, `variantkit`, `./registry`, or `buildDecision`.
|
|
281
|
+
- [ ] `variants/` is gone; `registry.ts` is gone.
|
|
282
|
+
- [ ] `index.tsx` renders the winner with the finalized `values` inlined as literals.
|
|
283
|
+
- [ ] The component's visible output matches the winning variant before prune.
|
|
284
|
+
|
|
285
|
+
If any box is unchecked, the prune is wrong. Re-read this file; do not hand-patch around it.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 6. Deslop — generated variants must not look AI-generated
|
|
290
|
+
|
|
291
|
+
Apply this to every variant you generate. Also run it as a pass on request ("deslop",
|
|
292
|
+
"remove the AI slop", "this looks AI-generated", "strip the AI tells").
|
|
293
|
+
|
|
294
|
+
**Core principle:** slop is *decoration without a system*. A tell is slop when it appears as
|
|
295
|
+
a one-off for "flavor"; it is fine when it is a consistent, intentional, repeated system. The
|
|
296
|
+
test is never "does this pattern appear" — it is "does it earn its place in a repeated
|
|
297
|
+
system." Default to **keep** when meaning is unclear; never delete load-bearing meaning.
|
|
298
|
+
**Subtract, never add** — deslop introduces no new color, font, or decoration.
|
|
299
|
+
|
|
300
|
+
Catalog (signature → slop when → fix), adapted from the design-deslop skill:
|
|
301
|
+
|
|
302
|
+
1. **Random italics** — `italic`, `<em>/<i>` on headings/labels/captions → one-off "flavor"
|
|
303
|
+
→ remove italic; use weight/size for emphasis. Keep true inline prose emphasis.
|
|
304
|
+
2. **Random mono font** — `font-mono`, `*mono*` family on labels/eyebrows/body → "tech feel"
|
|
305
|
+
with no data reason → revert to UI font; for numbers use `tabular-nums`, not a mono face.
|
|
306
|
+
3. **All-caps + wide tracking "eyebrows"** — `uppercase` + `tracking-wide*` kicker above every
|
|
307
|
+
heading (the single most common AI tell) → delete it, or keep at most one system-wide,
|
|
308
|
+
normal-case at real hierarchy.
|
|
309
|
+
4. **Decorative accent / divider lines** — `w-8 h-px`, `h-1 w-10 bg-{color}`, `::before` bars
|
|
310
|
+
under headings → remove. Keep only full-width structural rules doing layout work.
|
|
311
|
+
5. **Ornamental colored dots** — `w-1.5 h-1.5 rounded-full bg-{color}`, `•`, `animate-pulse`
|
|
312
|
+
dots that convey no status → remove. Keep dots that encode real state (online, unread).
|
|
313
|
+
6. **Unmotivated warm accents** — amber/orange/rose (`#f59e0b #f97316 #fb923c #f43f5e`, warm
|
|
314
|
+
gradients) injected for "energy" when warm is not the brand → map back to the design's
|
|
315
|
+
real tokens. Reserve warm for true semantic meaning (a real warning).
|
|
316
|
+
7. **Decorative single letters / monograms** — lone styled `A`/`01`/drop-cap/letter-in-a-box
|
|
317
|
+
standing for nothing → remove. Keep real initial avatars for named entities.
|
|
318
|
+
8. **Oversized rounded corners** — `rounded-3xl`, radius ≥ 20px on cards/buttons/inputs →
|
|
319
|
+
bring to ~8–12px, consistent and concentric (inner = outer − padding). Pills/avatars stay
|
|
320
|
+
full-round on purpose.
|
|
321
|
+
9. **One-sided / gradient highlight borders** — `border-t-2 border-{accent}` top stripes,
|
|
322
|
+
gradient/`mask` borders, one-side glows for flair → use a uniform 1px border or none.
|
|
323
|
+
Reserve edge accents for real selected/semantic states.
|
|
324
|
+
10. **Em dashes in copy** — replace `—` with commas, colons, or separate sentences.
|
|
325
|
+
11. **Emoji in product UI** — remove decorative emoji from interface copy and labels.
|
|
326
|
+
|
|
327
|
+
**Deslop-on-request workflow:** scope (the named files or the current diff) → scan each
|
|
328
|
+
signature → judge each hit with the Intentional Test above → remove only the slop → verify
|
|
329
|
+
in the browser (before/after) → report a table (`# | tell | file:line | removed/kept | why`).
|
|
330
|
+
|
|
331
|
+
**Scope note:** this applies to the **generated app components** (the variants). It does NOT
|
|
332
|
+
apply to the VariantKit/DialKit dev panel — the panel's mono labels, amber accent, and pill
|
|
333
|
+
tabs are an intentional, system-wide tool chrome, not slop.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 7. Full configuration, not three sliders — the completeness bar
|
|
338
|
+
|
|
339
|
+
The §0 rules stand: controls come from the element, defaults come from the code. This
|
|
340
|
+
section adds the other half: **a panel with 2-3 loose sliders is a failure.** During
|
|
341
|
+
exploration the panel must feel like the element's actual configuration panel — covering
|
|
342
|
+
every design decision the element really has, grouped the way a real settings panel would
|
|
343
|
+
group them.
|
|
344
|
+
|
|
345
|
+
**Paramify rule.** Every design literal a variant renders — px sizes, radii, colors, font
|
|
346
|
+
sizes, weights, spacing, shadows, durations — becomes a control, fed through props from the
|
|
347
|
+
shell. No hardcoded design value stays outside the panel during exploration, except a
|
|
348
|
+
variant's structural identity (below).
|
|
349
|
+
|
|
350
|
+
**Archetype checklists.** `variantkit/schemas/archetypes.ts` ships per-element-family
|
|
351
|
+
checklists of design axes:
|
|
352
|
+
|
|
353
|
+
`button · card · hero · navbar · modal · form · table · list · badge · pricing · section`
|
|
354
|
+
|
|
355
|
+
built from section builders in `variantkit/schemas/sections.ts` (`layoutSection`,
|
|
356
|
+
`surfaceSection`, `typographySection`, `colorSection`, `motionSection`, `statesSection`).
|
|
357
|
+
They are **checklists to adapt, not sets to paste** (§2 authoring rules apply unchanged):
|
|
358
|
+
|
|
359
|
+
- **Seed every default from the code.** Pass the element's rendered values as overrides —
|
|
360
|
+
`pricingArchetype({ surface: { radius: 18 }, color: { accent: tokens.brand } })` — so the
|
|
361
|
+
panel opens matching what's on screen. The builders' fallback values are scaffolding for
|
|
362
|
+
brand-new elements only; on an existing element, an unseeded default is a §0 violation.
|
|
363
|
+
- **Adapt the set.** Drop axes this element doesn't have, add the ones it does (`extra`),
|
|
364
|
+
rename folders if the element thinks in different terms. Two unrelated elements ending up
|
|
365
|
+
with identical panels means you pasted, not adapted.
|
|
366
|
+
- **Copy control shapes, never invent control types.** DialKit supports: slider
|
|
367
|
+
`[def,min,max,step?]`, boolean, text, color (hex), select, spring, easing, action, nested
|
|
368
|
+
folders (`_collapsed: true`).
|
|
369
|
+
|
|
370
|
+
**Minimum bar.** Non-trivial element ⇒ ≥4 folders, 12-25 controls. Trivial element (icon,
|
|
371
|
+
divider, single label) ⇒ a flat 3-5 control panel is fine. Collapse secondary folders.
|
|
372
|
+
|
|
373
|
+
**Identity exception.** A control must be honest. If a value IS the variant's structural
|
|
374
|
+
identity (the dark variant's background, the outlined variant's transparent surface), do
|
|
375
|
+
not expose it — drop that control by destructuring and leave the literal in the variant:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const { bg: _bg, ...surface } = sections.surface // variants own their surface bg
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Token resolution.** Selects may return tokens (`shadow: 'lg'`, `family: 'mono'`). The
|
|
382
|
+
SHELL resolves tokens to CSS (`SHADOWS` / `FONT_STACKS` from the schemas) and passes plain
|
|
383
|
+
CSS values as props. Variants never import from variantkit/schemas — they stay
|
|
384
|
+
self-contained (§1).
|
|
385
|
+
|
|
386
|
+
**Standalone paramify (no variants).** On "paramify this" / "let me tweak this" / "give me
|
|
387
|
+
controls for this" for an EXISTING component: wrap it with a full configuration — no
|
|
388
|
+
registry, no variants/ folder, no variant select. Just `useDialKit` (or a single-entry
|
|
389
|
+
`Studio`) with the adapted archetype + a finalize action, props fed from panel values. On
|
|
390
|
+
finalize: inline the final values back as literals and strip the wiring completely (§5
|
|
391
|
+
self-check applies, minus the registry/variants boxes).
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 8. Taste memory — decisions compound
|
|
396
|
+
|
|
397
|
+
Every resolved decision is one line in `.variantkit/history/log.jsonl`. The
|
|
398
|
+
`overridesFromDefault` fields are the developer telling you, with numbers, where your
|
|
399
|
+
defaults were wrong. Use them.
|
|
400
|
+
|
|
401
|
+
**Distill (after resolving, when history has ≥3 entries).** Write or update
|
|
402
|
+
`.variantkit/TASTE.md` with observed preferences. Rules:
|
|
403
|
+
|
|
404
|
+
- **Grounded only.** Every claim cites ≥2 decisions with the actual values. No claim from
|
|
405
|
+
a single data point; no speculation ("seems to like minimal" is banned — "finalized
|
|
406
|
+
radius 8-12 in 4/4 decisions, always below my default" is the format).
|
|
407
|
+
- **Track the dimensions that repeat:** radius range, accent hue temperature, spacing
|
|
408
|
+
density (padding/gap direction vs default), shadow level, type scale, variant character
|
|
409
|
+
(which structural take keeps winning: dense/outlined/dark...).
|
|
410
|
+
- **Keep it short** — a scannable bullet list, ≤15 lines, newest evidence first.
|
|
411
|
+
- **Update, don't append forever:** revise existing bullets as new decisions land; drop
|
|
412
|
+
bullets the data stops supporting.
|
|
413
|
+
|
|
414
|
+
Format:
|
|
415
|
+
|
|
416
|
+
```markdown
|
|
417
|
+
# Taste — observed from finalized decisions
|
|
418
|
+
<!-- distilled by the agent from .variantkit/history/log.jsonl — grounded claims only -->
|
|
419
|
+
|
|
420
|
+
- Radius lands 8-12 (18→12, 16→8, 12→10 across PricingCard, Hero, Modal). Default to 10.
|
|
421
|
+
- Accents shift cooler + darker (#1F5E54→#175048, #3B82F6→#1D4ED8). Avoid warm accents.
|
|
422
|
+
- Picks the densest structural take (slab 2x, compact-table 1x). Offer one dense variant first.
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Read back (before generating).** Before scaffolding any variant set or paramify panel,
|
|
426
|
+
read `.variantkit/TASTE.md` if it exists. Seed defaults toward the observed preferences —
|
|
427
|
+
and still include ONE variant that deliberately breaks the pattern, so taste keeps getting
|
|
428
|
+
tested rather than ossified.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deepak Maurya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NAMING.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# VariantKit — Vocabulary
|
|
2
|
+
|
|
3
|
+
One word per concept, used the same way everywhere (code, UI, docs, agent contract). If a
|
|
4
|
+
term here conflicts with how you named something, this file wins — rename the code.
|
|
5
|
+
|
|
6
|
+
## The core nouns
|
|
7
|
+
|
|
8
|
+
| Term | Means | Not to be called |
|
|
9
|
+
|------|-------|------------------|
|
|
10
|
+
| **Element** | The UI thing being designed: a pricing card, a button, a hero. The subject of a session. | "component" (too generic), "widget" |
|
|
11
|
+
| **Variant** | One structural take on an element — different code/layout, same role (Slab vs Ledger vs Inverse). The agent generates several. | "version" (means snapshot), "option" (fine in plain English, but the type is `variant`) |
|
|
12
|
+
| **Control** | One tunable setting of an element (radius, accent, padding). | "param" (ok internally), "knob" |
|
|
13
|
+
| **Configuration** | The full set of controls exposed for an element — the settings panel for it. Authored per element by the agent, contextual; there is no predefined menu. | "config object" (that's the DialKit wiring, see below), "preset" (that word is reserved for DialKit's snapshot toolbar) |
|
|
14
|
+
| **Defaults** | The frozen reference values a variant ships with. The override diff is measured against these. | "initial" |
|
|
15
|
+
| **Override** | A control whose value differs from its default — the "taste signal". The set of them = the **override diff**. | "change", "delta" |
|
|
16
|
+
| **Snapshot** | A saved (variant + control values) state kept so you can compare two tunings of the same element. Implemented via DialKit's preset toolbar (≡+ / Version dropdown), reused. | — |
|
|
17
|
+
| **Finalize** | Committing the chosen variant + its current values. Produces a decision. | "save", "submit" |
|
|
18
|
+
| **Decision** | The machine-readable result of finalize: `{ component, finalized, values, overridesFromDefault, prune, … }`. The handoff to the agent. | "result", "output" |
|
|
19
|
+
| **Prune** | The agent deleting the losing variants down to the clean winner. | "cleanup", "resolve" |
|
|
20
|
+
|
|
21
|
+
## Supporting terms
|
|
22
|
+
|
|
23
|
+
| Term | Means |
|
|
24
|
+
|------|-------|
|
|
25
|
+
| **Panel** | The floating dev-time UI that hosts the controls (DialKit). One panel per session. |
|
|
26
|
+
| **Folder** | One element's collapsible section inside the single panel. One folder = one element. |
|
|
27
|
+
| **Registry** | Map of variant key → component, per element. Used to render the active variant and to compute the prune list. |
|
|
28
|
+
| **Set** (variant set) | An element plus its variants + registry + the shell that wires them. What the agent scaffolds. |
|
|
29
|
+
| **DialKit config object** | The technical config you pass to `useDialKit` (controls + the `variant` select + the `finalize` action). The *implementation* of a Configuration. Don't say "config" when you mean the user-facing Configuration. |
|
|
30
|
+
| **Archetype** | A per-element-family CHECKLIST of design axes (`variantkit/schemas/archetypes.ts`) used to assemble a complete Configuration — adapted per element and seeded from the project's values, never pasted as-is. |
|
|
31
|
+
| **Section** | One design dimension's worth of controls (layout, surface, typography, color, motion, states) — the building blocks archetypes compose. |
|
|
32
|
+
| **Paramify** | Wrapping an EXISTING component in its full Configuration (no variants) so the user can tune it live, then inline the result. |
|
|
33
|
+
| **Transport** | The dev-only channel (vite plugin / Next route) that carries a Decision from Finalize into `.variantkit/decisions/`. Clipboard is the fallback. |
|
|
34
|
+
| **Compare** | VariantBar's side-by-side mode: every variant rendered live in a grid (VariantStage). |
|
|
35
|
+
| **Taste** | `.variantkit/TASTE.md` — grounded preferences distilled from resolved Decisions, read back before generating. |
|
|
36
|
+
|
|
37
|
+
## One sentence, all of it
|
|
38
|
+
|
|
39
|
+
> The agent scaffolds a **set** of **variants** for an **element**; you tune each variant's
|
|
40
|
+
> **controls** (its **configuration**) in the **panel**, optionally keeping **snapshots** to
|
|
41
|
+
> compare; you **finalize** the winner, which writes a **decision**; the agent **prunes** the
|
|
42
|
+
> losers to a clean component.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# VariantKit
|
|
2
|
+
|
|
3
|
+
**The configuration panel for AI-built UI.**
|
|
4
|
+
|
|
5
|
+
You: *"three takes on the pricing card."*
|
|
6
|
+
Your agent builds three real variants in your running app — wired to a full control panel
|
|
7
|
+
with every knob that matters: layout, surface, typography, color, motion. You switch with
|
|
8
|
+
keys 1-2-3, compare them side by side, drag sliders until it's right, hit **Finalize** —
|
|
9
|
+
and the losers are pruned from your codebase. What ships is a plain component with zero
|
|
10
|
+
tool residue.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
Stop describing tweaks in chat. Tweak the thing, then hand your agent the decision.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
From inside your project (Vite or Next.js, React 18+):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx variantkit
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
(or straight from GitHub: `npx github:deepshal99/variantkit`)
|
|
27
|
+
|
|
28
|
+
One command does everything:
|
|
29
|
+
|
|
30
|
+
- installs `dialkit` + `motion`
|
|
31
|
+
- copies the runtime (`buildDecision`, archetype schemas, `VariantBar`/`VariantStage`)
|
|
32
|
+
- wires the decision transport (Vite plugin or Next API route)
|
|
33
|
+
- mounts `<DialRoot/>` + `<VariantBar/>` in your app entry
|
|
34
|
+
- teaches your AI the contract (`AGENT.md` + a pointer in `CLAUDE.md`/`AGENTS.md`/Cursor rules)
|
|
35
|
+
- installs the global Claude Code skill (so every project gets variants proactively)
|
|
36
|
+
|
|
37
|
+
Check or undo anytime:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx variantkit doctor # 15 checks with fix-its
|
|
41
|
+
npx variantkit remove # zero-residue uninstall (git-clean verified)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Flags: `--dry-run` `--skip-install` `--no-mount` `--no-skill` (init) · `--keep-deps` (remove)
|
|
45
|
+
|
|
46
|
+
## The loop
|
|
47
|
+
|
|
48
|
+
1. **Ask for options.** "Give me three takes on the hero." The agent scaffolds a variant
|
|
49
|
+
set: 2-4 self-contained components + a thin shell wiring the panel.
|
|
50
|
+
2. **Get a real panel, not 3 sliders.** The agent authors the controls for YOUR element
|
|
51
|
+
(VariantKit presents; the project decides) using **archetype checklists** per element
|
|
52
|
+
type (`button · card · hero · navbar · modal · form · table · list · badge · pricing ·
|
|
53
|
+
section`) — adapted, grouped in folders, every default seeded from your code.
|
|
54
|
+
3. **Explore.** Switch variants with the bottom bar or keys 1-9. **Compare** renders all
|
|
55
|
+
variants in a live grid — drag a slider, every variant reacts.
|
|
56
|
+
4. **Finalize.** The decision (winner + your overrides — the taste signal) lands in
|
|
57
|
+
`.variantkit/decisions/` via the dev server. No copy-paste. (Clipboard fallback when no
|
|
58
|
+
transport is running.)
|
|
59
|
+
5. **"Apply decision."** The agent inlines your final values into the winner, renames it to
|
|
60
|
+
`index.tsx`, deletes the losers and all tool wiring. Deterministic: deletion + one
|
|
61
|
+
rename + literal inlining — no JSX surgery.
|
|
62
|
+
6. **It compounds.** Resolved decisions append to `.variantkit/history/`. After 3+, the
|
|
63
|
+
agent distills `TASTE.md` — grounded observations ("radius lands 8-12, 3/3 decisions")
|
|
64
|
+
that seed better defaults next time.
|
|
65
|
+
|
|
66
|
+
## Also in the box
|
|
67
|
+
|
|
68
|
+
- **Studio** — exploring several elements at once? One panel, a folder per element, each
|
|
69
|
+
with its own controls and Finalize; hovering an element focuses its folder.
|
|
70
|
+
- **Snapshots** — keep two tunings of the same element (DialKit's preset toolbar) and
|
|
71
|
+
switch between them; Finalize acts on the active one.
|
|
72
|
+
- **Panel polish** — dark mode with a header toggle, a delightful minimize/expand morph
|
|
73
|
+
(shipped as a patch-package patch), micro-motion. All panel-side: nothing is ever drawn
|
|
74
|
+
over your UI.
|
|
75
|
+
|
|
76
|
+
## Not just variants: paramify anything
|
|
77
|
+
|
|
78
|
+
*"Let me tweak this"* on any existing component wraps it in its archetype panel — no
|
|
79
|
+
variants, no registry. Tune it live, finalize, and the values inline back as literals with
|
|
80
|
+
the wiring stripped.
|
|
81
|
+
|
|
82
|
+
## Why not just ask the AI to build a switcher?
|
|
83
|
+
|
|
84
|
+
You can — every time, ad hoc, with 2-3 controls it happens to think of, and then ask it to
|
|
85
|
+
rip its own scaffolding back out and hope. VariantKit makes that loop a **contract**:
|
|
86
|
+
archetype panels with the controls the element actually needs, one keyboard-driven bar for
|
|
87
|
+
every set, a schema'd decision file, a prune algorithm with a self-check, and an uninstall
|
|
88
|
+
that leaves `git status` clean. Compared to Storybook knobs: this runs **in your real app**,
|
|
89
|
+
against real data and layout — and removes itself when you decide.
|
|
90
|
+
|
|
91
|
+
## How it relates to DialKit
|
|
92
|
+
|
|
93
|
+
[DialKit](https://github.com/joshpuckett/dialkit) answers *"what number should this be?"*
|
|
94
|
+
VariantKit answers *"which direction should this go?"* — and uses DialKit as its control
|
|
95
|
+
panel through its documented store API (no fork; the one patch-package patch is cosmetic —
|
|
96
|
+
the panel's minimize/expand morph — and entirely optional).
|
|
97
|
+
|
|
98
|
+
## Repo map
|
|
99
|
+
|
|
100
|
+
- `variantkit/` — what gets installed: `buildDecision.ts` (pure core), `configs.ts`
|
|
101
|
+
(panel assembly), `schemas/` (sections + 11 archetype checklists), `react.tsx` (Studio +
|
|
102
|
+
panel theme), `react/` (VariantBar, VariantStage), panel css, `patches/` (panel morph),
|
|
103
|
+
`vite-plugin.mjs`, `templates/` (Next routes), `init.mjs` (init/doctor/remove), `skill/`
|
|
104
|
+
- `AGENT.md` — the agent contract: scaffold convention, authoring rules, decision schema
|
|
105
|
+
(v2, dot-paths), prune + self-check, §7 completeness bar, deslop, taste memory
|
|
106
|
+
- `NAMING.md` — the vocabulary (one word per concept)
|
|
107
|
+
- `examples/sandbox/` — wired Vite example (PricingCard, 3 variants, 19-control panel);
|
|
108
|
+
`examples/panel-studio/` — the Studio dogfood
|
|
109
|
+
- `fixture/` — prune before/after proof + taste distillation fixture
|
|
110
|
+
|
|
111
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "variantkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The configuration panel for AI-built UI: your agent generates variants with a full contextual control panel, you tweak and finalize, the losers get pruned from the codebase. Built on DialKit.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"variantkit": "variantkit/init.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"variantkit",
|
|
11
|
+
"AGENT.md",
|
|
12
|
+
"NAMING.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"init": "node variantkit/init.mjs"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/deepshal99/variantkit.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ai",
|
|
23
|
+
"ui",
|
|
24
|
+
"design",
|
|
25
|
+
"variants",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"cursor",
|
|
28
|
+
"dialkit",
|
|
29
|
+
"dev-tools",
|
|
30
|
+
"react"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|