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 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
+ ![VariantKit demo: switching variants with keys, live compare grid, finalize](https://raw.githubusercontent.com/deepshal99/variantkit/main/docs/demo.gif)
13
+
14
+ ![VariantKit: three live variants, full contextual panel, compare grid, finalize bar](https://raw.githubusercontent.com/deepshal99/variantkit/main/docs/demo-compare.png)
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
+ }