variantkit 0.1.0 → 0.2.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 CHANGED
@@ -10,7 +10,7 @@ do two things: **scaffold** variant sets when asked for "options/variants/takes"
10
10
  Pruning must be reliable. It is mostly file deletion plus one file rename. Never move JSX
11
11
  between files by hand. Follow this exactly.
12
12
 
13
- **Vocabulary:** element, variant, control, configuration, snapshot, finalize, decision, prune
13
+ **Vocabulary:** element, variant, control, configuration, finalize, decision, prune
14
14
  — one word per concept, defined in `NAMING.md`. Use them consistently.
15
15
 
16
16
  ---
@@ -19,9 +19,10 @@ between files by hand. Follow this exactly.
19
19
 
20
20
  **Default to divergence.** When the developer asks you to build or change any user-facing
21
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.
22
+ single interpretation. Scaffold a **variant set** of **2-3 structural takes** (§1) a 4th
23
+ only when the directions genuinely diverge, since each take is more generation the developer
24
+ waits on — and let them choose live in the panel, then finalize. Generating N is the point;
25
+ one is the exception.
25
26
 
26
27
  Offer variants especially when:
27
28
  - the request is aesthetic or open-ended ("a pricing card", "make this nicer", "a hero")
@@ -199,27 +200,19 @@ and a live grid of all variants when Compare is on (clicking a cell selects it):
199
200
  return <VariantStage name="PricingCard" registry={registry} active={String(v.variant)} props={variantProps} />
200
201
  ```
201
202
 
202
- ### Hide the redundant copy button
203
+ ### Hide DialKit's top toolbar
203
204
 
204
205
  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).
206
+ - `variantkit/dialkit-clean.css` — hides DialKit's entire top toolbar row (the preset/"Version"
207
+ manager + the "Copy parameters" button) and the folder/header dividers (panel reads on
208
+ spacing, like DialKit's own UI). VariantKit has no snapshots concept: the variant selector is
209
+ the first thing in the panel — pick a variant, tweak it, Finalize.
208
210
  - `variantkit/dialkit-dark.css` — the dark palette DialKit lacks. Call `useDialkitTheme()`
209
211
  (from `variantkit/react`) once: it applies the theme, persists it, and injects a sun/moon
210
212
  toggle into the panel header so the user flips the panel's light/dark right there.
211
213
  - `variantkit/motion.css` — press feedback, theme-switch cross-fade, reduced-motion. Scoped
212
214
  strictly to the panel chrome; it never styles or animates the project's UI.
213
215
 
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
216
  ## 3. `decision.json` schema (schema 2)
224
217
 
225
218
  Finalize writes one decision per component. Values are **dot-path flattened** — folder
@@ -337,15 +330,28 @@ tabs are an intentional, system-wide tool chrome, not slop.
337
330
  ## 7. Full configuration, not three sliders — the completeness bar
338
331
 
339
332
  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).
333
+ section adds the other half: for a rich element, **a panel with 2-3 loose sliders is a
334
+ failure.** During exploration the panel must feel like the element's actual configuration
335
+ panel — covering every design decision the element really has, grouped the way a real
336
+ settings panel would group them.
337
+
338
+ But **scale the panel to the element.** Every control and every variant is generation the
339
+ developer waits onover-building a button into 20 controls is what makes VariantKit feel
340
+ slow for no payoff. Match the work to the ask (see the minimum bar below).
341
+
342
+ **Paramify rule — expose the decisions, not every number.** A control earns its place only
343
+ if a developer would plausibly reach for it while exploring THIS element. Run that test over
344
+ every design literal a variant renders — px sizes, radii, colors, font sizes, weights,
345
+ spacing, shadows, durations: if it's a real, tweakable design decision, it becomes a control
346
+ fed from the shell; if it's a fixed structural constant, a value the layout simply dictates,
347
+ or something nobody would touch, leave it inline. The bar is *"would they turn this dial?"*,
348
+ not *"is there a number here?"*. No **meaningful** design value stays outside the panel — and
349
+ no useless one clutters it. (A variant's structural identity always stays inline — see below.)
350
+
351
+ Curating is the job, not a shortcut. A panel of 8 controls that all matter beats one of 20
352
+ where half are noise — the developer scans every row, so each junk control is a small tax.
353
+ When a control feels borderline, drop it (or collapse it into a secondary folder); the
354
+ developer can always ask for more.
349
355
 
350
356
  **Archetype checklists.** `variantkit/schemas/archetypes.ts` ships per-element-family
351
357
  checklists of design axes:
@@ -367,8 +373,15 @@ They are **checklists to adapt, not sets to paste** (§2 authoring rules apply u
367
373
  `[def,min,max,step?]`, boolean, text, color (hex), select, spring, easing, action, nested
368
374
  folders (`_collapsed: true`).
369
375
 
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.
376
+ **Proportional bar** size the panel to the element, smallest sufficient first:
377
+ - **Trivial** (icon, divider, single label, tag) ⇒ a flat 3-5 control panel.
378
+ - **Small** (button, badge, single input, chip) ⇒ the ~3-8 controls that genuinely matter; one
379
+ or two folders at most. Resist padding it out.
380
+ - **Rich** (hero, pricing card, navbar, modal, form, table) ⇒ the full set: ≥4 folders,
381
+ ~12-25 controls, secondary folders collapsed.
382
+
383
+ When unsure between two tiers, build the smaller one — an under-built panel is a quick add;
384
+ an over-built one already cost the developer the wait.
372
385
 
373
386
  **Identity exception.** A control must be honest. If a value IS the variant's structural
374
387
  identity (the dark variant's background, the outlined variant's transparent surface), do
package/NAMING.md CHANGED
@@ -8,12 +8,11 @@ term here conflicts with how you named something, this file wins — rename the
8
8
  | Term | Means | Not to be called |
9
9
  |------|-------|------------------|
10
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`) |
11
+ | **Variant** | One structural take on an element — different code/layout, same role (Slab vs Ledger vs Inverse). The agent generates several. | "version", "option" (fine in plain English, but the type is `variant`) |
12
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) |
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" |
14
14
  | **Defaults** | The frozen reference values a variant ships with. The override diff is measured against these. | "initial" |
15
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
16
  | **Finalize** | Committing the chosen variant + its current values. Produces a decision. | "save", "submit" |
18
17
  | **Decision** | The machine-readable result of finalize: `{ component, finalized, values, overridesFromDefault, prune, … }`. The handoff to the agent. | "result", "output" |
19
18
  | **Prune** | The agent deleting the losing variants down to the clean winner. | "cleanup", "resolve" |
@@ -37,6 +36,5 @@ term here conflicts with how you named something, this file wins — rename the
37
36
  ## One sentence, all of it
38
37
 
39
38
  > 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.
39
+ > **controls** (its **configuration**) in the **panel**; you **finalize** the winner, which
40
+ > writes a **decision**; the agent **prunes** the losers to a clean component.
package/README.md CHANGED
@@ -67,8 +67,6 @@ Flags: `--dry-run` `--skip-install` `--no-mount` `--no-skill` (init) · `--keep-
67
67
 
68
68
  - **Studio** — exploring several elements at once? One panel, a folder per element, each
69
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
70
  - **Panel polish** — dark mode with a header toggle, a delightful minimize/expand morph
73
71
  (shipped as a patch-package patch), micro-motion. All panel-side: nothing is ever drawn
74
72
  over your UI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "variantkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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
5
  "type": "module",
6
6
  "bin": {
@@ -38,8 +38,11 @@ export function panelConfig(
38
38
  opts?: { finalizeLabel?: string; component?: string },
39
39
  ): PanelConfig {
40
40
  return {
41
+ // `segmented: true` makes DialKit render the variant as clean separated selection pills
42
+ // (one per take) instead of a dropdown — the variant is the panel's hero choice, so it
43
+ // shows every option at a glance. Pills wrap when names are long / the panel is narrow.
41
44
  ...(variantKeys.length > 1
42
- ? { variant: { type: 'select', options: variantKeys, default: variantKeys[0] } }
45
+ ? { variant: { type: 'select', options: variantKeys, default: variantKeys[0], segmented: true } }
43
46
  : {}),
44
47
  ...controls,
45
48
  finalize: { type: 'action', label: opts?.finalizeLabel ?? `Finalize ${opts?.component ?? ''}`.trim() },
@@ -1,9 +1,10 @@
1
1
  /* VariantKit panel cleanup for DialKit.
2
- - Hide only the "Copy parameters" clipboard button (redundant with Finalize). The preset
3
- toolbar STAYS it's VariantKit's snapshot mechanism (save a tuned variant, switch, then
4
- finalize the active one). See AGENT.md "Snapshots".
2
+ - Hide DialKit's entire top toolbar row (the preset/"Version" manager + the "Copy
3
+ parameters" button). VariantKit has no snapshots concept: you pick a variant, tweak it,
4
+ and Finalize. The preset toolbar only confused people ("what is Version 1, why first?"),
5
+ so the whole row goes — leaving the variant selector as the first thing in the panel.
5
6
  - Remove folder/header dividers so the panel reads on spacing alone, like DialKit's own UI. */
6
- .dialkit-root [title='Copy parameters'] {
7
+ .dialkit-root .dialkit-panel-toolbar {
7
8
  display: none !important;
8
9
  }
9
10
 
@@ -15,12 +16,85 @@
15
16
  }
16
17
  .dialkit-root .dialkit-panel-header {
17
18
  border-bottom: none !important;
19
+ /* The title row carried bottom spacing meant to clear the preset toolbar. With the toolbar
20
+ hidden that became a ~38px dead gap above the first control — reclaim it so the title sits
21
+ a normal step above the panel body. */
22
+ margin-bottom: 2px !important;
23
+ }
24
+ .dialkit-root .dialkit-panel-header .dialkit-folder-header-top {
25
+ padding-bottom: 0 !important;
18
26
  }
19
27
  .dialkit-root .dialkit-folder {
20
28
  border-top: none !important;
21
29
  border-bottom: none !important;
22
30
  }
23
31
 
32
+ /* ── Variant selector: clean separated selection pills ───────────────────────────────────
33
+ DialKit renders the `variant` control (segmented:true) as a row of separated pills, one
34
+ per take, instead of a dropdown. The variant is the panel's hero choice, so every option
35
+ stays visible. Pills wrap to new lines when labels are long or the panel is narrow — they
36
+ never overflow or clip the row. */
37
+ .dialkit-root .dialkit-vk-pills {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 7px;
41
+ align-items: stretch;
42
+ }
43
+ .dialkit-root .dialkit-vk-pills .dialkit-select-label {
44
+ /* sits on its own line above the pills */
45
+ padding: 0 2px;
46
+ }
47
+ .dialkit-root .dialkit-vk-pills-track {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ gap: 5px;
51
+ }
52
+ .dialkit-root .dialkit-vk-pill {
53
+ /* DialKit absolutely-positions panel buttons; pills must opt back into normal flow so the
54
+ track lays them out in a wrapping row. Inactive pills are quiet (faint surface, no border)
55
+ so the selected one carries all the emphasis. */
56
+ position: relative;
57
+ flex: 1 1 auto;
58
+ min-width: 60px;
59
+ max-width: 100%;
60
+ padding: 7px 11px;
61
+ border-radius: 8px;
62
+ border: 1px solid transparent;
63
+ background: var(--dial-surface-subtle);
64
+ color: var(--dial-text-secondary);
65
+ font: 500 12px/1.15 system-ui, -apple-system, 'Segoe UI', sans-serif;
66
+ letter-spacing: 0.01em;
67
+ text-align: center;
68
+ white-space: nowrap;
69
+ overflow: hidden;
70
+ text-overflow: ellipsis;
71
+ cursor: pointer;
72
+ -webkit-font-smoothing: antialiased;
73
+ transition:
74
+ background-color 180ms cubic-bezier(0.23, 1, 0.32, 1),
75
+ color 180ms cubic-bezier(0.23, 1, 0.32, 1),
76
+ box-shadow 180ms cubic-bezier(0.23, 1, 0.32, 1),
77
+ transform 120ms cubic-bezier(0.23, 1, 0.32, 1);
78
+ }
79
+ .dialkit-root .dialkit-vk-pill:hover {
80
+ background: var(--dial-surface-hover);
81
+ color: var(--dial-text-primary);
82
+ }
83
+ .dialkit-root .dialkit-vk-pill:active {
84
+ transform: scale(0.96);
85
+ }
86
+ .dialkit-root .dialkit-vk-pill[data-active='true'] {
87
+ /* Solid fill + a small lift = unmistakable selection. `--dial-glass-bg` is the solid panel
88
+ colour (dark in dark mode, light in light mode), so the label always contrasts the fill. */
89
+ background: var(--dial-text-primary);
90
+ color: var(--dial-glass-bg);
91
+ font-weight: 600;
92
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.13);
93
+ }
94
+ .dialkit-root .dialkit-vk-pill[data-active='true']:hover {
95
+ color: var(--dial-glass-bg);
96
+ }
97
+
24
98
  /* The injected panel theme toggle — placed just left of DialKit's settings icon (right:12). */
25
99
  .dialkit-root .vk-theme-toggle {
26
100
  position: absolute;
@@ -1,7 +1,16 @@
1
1
  diff --git a/node_modules/dialkit/dist/index.cjs b/node_modules/dialkit/dist/index.cjs
2
- index d9f4837..4bf4e6d 100644
2
+ index d9f4837..385c7dc 100644
3
3
  --- a/node_modules/dialkit/dist/index.cjs
4
4
  +++ b/node_modules/dialkit/dist/index.cjs
5
+ @@ -337,7 +337,7 @@ var DialStoreClass = class {
6
+ } else if (this.isActionConfig(value)) {
7
+ controls.push({ type: "action", path, label: value.label || label });
8
+ } else if (this.isSelectConfig(value)) {
9
+ - controls.push({ type: "select", path, label, options: value.options });
10
+ + controls.push({ type: "select", path, label, options: value.options, segmented: value.segmented });
11
+ } else if (this.isColorConfig(value)) {
12
+ controls.push({ type: "color", path, label });
13
+ } else if (this.isTextConfig(value)) {
5
14
  @@ -1006,8 +1006,9 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
6
15
  style: panelStyle,
7
16
  onClick: !isOpen ? handleToggle : void 0,
@@ -14,10 +23,44 @@ index d9f4837..4bf4e6d 100644
14
23
  children: folderContent
15
24
  }
16
25
  );
26
+ @@ -1853,6 +1854,15 @@ function normalizeOptions(options) {
27
+ (opt) => typeof opt === "string" ? { value: opt, label: toTitleCase(opt) } : opt
28
+ );
29
+ }
30
+ +function VKSegmented({ label, value, options, onChange }) {
31
+ + const normalized = normalizeOptions(options);
32
+ + return (0, import_jsx_runtime11.jsxs)("div", { className: "dialkit-vk-pills", children: [
33
+ + (0, import_jsx_runtime11.jsx)("span", { className: "dialkit-select-label", children: label }),
34
+ + (0, import_jsx_runtime11.jsxs)("div", { className: "dialkit-vk-pills-track", children: normalized.map(function(o) {
35
+ + return (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "dialkit-vk-pill", "data-active": o.value === value ? "true" : "false", title: o.label, onClick: function() { return onChange(o.value); }, children: o.label }, o.value);
36
+ + }) })
37
+ + ] });
38
+ +}
39
+ function SelectControl({ label, value, options, onChange }) {
40
+ const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
41
+ const triggerRef = (0, import_react10.useRef)(null);
42
+ @@ -2266,7 +2276,7 @@ Apply these values as the new defaults in the useDialKit call.`;
43
+ );
44
+ case "select":
45
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
46
+ - SelectControl,
47
+ + (control.segmented ? VKSegmented : SelectControl),
48
+ {
49
+ label: control.label,
50
+ value,
17
51
  diff --git a/node_modules/dialkit/dist/index.js b/node_modules/dialkit/dist/index.js
18
- index f8d8baf..7a0e7b3 100644
52
+ index f8d8baf..a58300a 100644
19
53
  --- a/node_modules/dialkit/dist/index.js
20
54
  +++ b/node_modules/dialkit/dist/index.js
55
+ @@ -297,7 +297,7 @@ var DialStoreClass = class {
56
+ } else if (this.isActionConfig(value)) {
57
+ controls.push({ type: "action", path, label: value.label || label });
58
+ } else if (this.isSelectConfig(value)) {
59
+ - controls.push({ type: "select", path, label, options: value.options });
60
+ + controls.push({ type: "select", path, label, options: value.options, segmented: value.segmented });
61
+ } else if (this.isColorConfig(value)) {
62
+ controls.push({ type: "color", path, label });
63
+ } else if (this.isTextConfig(value)) {
21
64
  @@ -966,8 +966,9 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
22
65
  style: panelStyle,
23
66
  onClick: !isOpen ? handleToggle : void 0,
@@ -30,3 +73,28 @@ index f8d8baf..7a0e7b3 100644
30
73
  children: folderContent
31
74
  }
32
75
  );
76
+ @@ -1813,6 +1814,15 @@ function normalizeOptions(options) {
77
+ (opt) => typeof opt === "string" ? { value: opt, label: toTitleCase(opt) } : opt
78
+ );
79
+ }
80
+ +function VKSegmented({ label, value, options, onChange }) {
81
+ + const normalized = normalizeOptions(options);
82
+ + return jsxs10("div", { className: "dialkit-vk-pills", children: [
83
+ + jsx11("span", { className: "dialkit-select-label", children: label }),
84
+ + jsxs10("div", { className: "dialkit-vk-pills-track", children: normalized.map(function(o) {
85
+ + return jsx11("button", { type: "button", className: "dialkit-vk-pill", "data-active": o.value === value ? "true" : "false", title: o.label, onClick: function() { return onChange(o.value); }, children: o.label }, o.value);
86
+ + }) })
87
+ + ] });
88
+ +}
89
+ function SelectControl({ label, value, options, onChange }) {
90
+ const [isOpen, setIsOpen] = useState6(false);
91
+ const triggerRef = useRef8(null);
92
+ @@ -2226,7 +2236,7 @@ Apply these values as the new defaults in the useDialKit call.`;
93
+ );
94
+ case "select":
95
+ return /* @__PURE__ */ jsx14(
96
+ - SelectControl,
97
+ + (control.segmented ? VKSegmented : SelectControl),
98
+ {
99
+ label: control.label,
100
+ value,
@@ -77,23 +77,39 @@ export function Studio({ elements, name = 'VariantKit', focusOnHover, onFinalize
77
77
  const elsRef = useRef(elements)
78
78
  elsRef.current = elements
79
79
 
80
- // One combined config: a folder per element, first open and the rest collapsed. The
81
- // finalize button's "✓ Copied" feedback is handled inside copyDecision (panel-side, no
82
- // overlay), so the label stays a plain "Finalize <name>" here.
80
+ // Build the combined config. With ONE element, a per-element folder is pure redundancy
81
+ // the panel title already names it, so a "Button" folder under "Button Lab" just adds a
82
+ // confusing second hierarchy level. So flatten: the lone element's controls sit at the panel
83
+ // root. With 2+ elements, give each its own folder (first open, rest collapsed) — that's
84
+ // where the grouping earns its keep. The finalize button's "✓ Copied" feedback is handled
85
+ // inside copyDecision (panel-side), so the label stays a plain "Finalize <name>".
86
+ const single = elements.length === 1
83
87
  const combined: Record<string, unknown> = {}
84
- elements.forEach((e, i) => {
88
+ if (single) {
89
+ const e = elements[0]
85
90
  const base = cfgFor(e)
86
- const finalize = { ...(base.finalize as object), label: `Finalize ${e.name}` }
87
- combined[e.name] = { ...base, finalize, _collapsed: i !== 0 }
88
- })
91
+ Object.assign(combined, base, {
92
+ finalize: { ...(base.finalize as object), label: `Finalize ${e.name}` },
93
+ })
94
+ } else {
95
+ elements.forEach((e, i) => {
96
+ const base = cfgFor(e)
97
+ const finalize = { ...(base.finalize as object), label: `Finalize ${e.name}` }
98
+ combined[e.name] = { ...base, finalize, _collapsed: i !== 0 }
99
+ })
100
+ }
89
101
 
90
102
  const all = useDialKit(name, combined as never, {
91
103
  onAction: (path: string) => {
92
- const elName = path.split('.')[0]
93
- const e = elsRef.current.find((x) => x.name === elName)
104
+ // Single element → its finalize lives at the root (path === 'finalize'), and its values
105
+ // ARE `all`. Multiple the element name prefixes the path and `all[name]` is its slice.
106
+ const e = single ? elsRef.current[0] : elsRef.current.find((x) => x.name === path.split('.')[0])
94
107
  if (!e) return
95
- const slice = (all as Record<string, Record<string, ParamValue>>)[elName]
96
- const decision = buildDecision(elName, slice, defaultsOf(cfgFor(e)), regOf(e.keys))
108
+ const slice = (single ? all : (all as Record<string, Record<string, ParamValue>>)[e.name]) as Record<
109
+ string,
110
+ ParamValue
111
+ >
112
+ const decision = buildDecision(e.name, slice, defaultsOf(cfgFor(e)), regOf(e.keys))
97
113
  // Dev transport first ("✓ Saved" -> .variantkit/decisions/), clipboard fallback ("✓ Copied").
98
114
  submitDecision(decision)
99
115
  onFinalize?.(decision)
@@ -101,15 +117,17 @@ export function Studio({ elements, name = 'VariantKit', focusOnHover, onFinalize
101
117
  }) as Record<string, Record<string, ParamValue>>
102
118
 
103
119
  useEffect(() => {
104
- if (focusOnHover) focusFolder(focused)
105
- }, [focused, focusOnHover])
120
+ // Focus-on-hover only applies to the multi-element layout (it toggles per-element folders).
121
+ // With a single flattened element there are no element folders to focus.
122
+ if (focusOnHover && !single) focusFolder(focused)
123
+ }, [focused, focusOnHover, single])
106
124
 
107
125
  // Render the elements as-is — VariantKit adds no layout, spacing, alignment, rings, or
108
126
  // badges around the project's UI. The host page owns presentation entirely.
109
127
  return (
110
128
  <MotionConfig reducedMotion="user">
111
129
  {elements.map((e) => {
112
- const slice = all[e.name]
130
+ const slice = single ? (all as unknown as Record<string, ParamValue>) : all[e.name]
113
131
  if (!slice) return null
114
132
  const variant = e.keys.length > 1 ? String(slice.variant) : e.keys[0]
115
133
  return (
@@ -13,8 +13,10 @@ it and prefer it over this summary.
13
13
  ## When to use (proactively)
14
14
 
15
15
  Trigger whenever the user asks to build or change user-facing UI and the result is open-ended
16
- (aesthetic, layout, tone, structure, density). Default to offering 2-4 variants. Skip only for
17
- mechanical, exactly-specified changes, or when the user says "just one".
16
+ (aesthetic, layout, tone, structure, density). **Default to 2-3 variants** reach for a 4th
17
+ only when the directions genuinely diverge, since each variant is more generation the developer
18
+ waits on. Skip variants entirely for mechanical, exactly-specified changes, or when the user
19
+ says "just one".
18
20
 
19
21
  ## Step 1 — make sure the project is set up
20
22
 
@@ -33,7 +35,7 @@ not a wrapper):
33
35
  ```tsx
34
36
  import { DialRoot } from 'dialkit'
35
37
  import 'dialkit/styles.css'
36
- import './variantkit/dialkit-clean.css' // hide redundant copy button + dividers (keeps snapshots)
38
+ import './variantkit/dialkit-clean.css' // hide DialKit's preset toolbar + redundant copy button + dividers
37
39
  import './variantkit/dialkit-dark.css' // optional dark palette; set data-theme="dark" on .dialkit-root
38
40
  import './variantkit/motion.css' // stagger, press feedback, easings, reduced-motion
39
41
  // render <App /> and <DialRoot /> as siblings
@@ -45,8 +47,7 @@ stylesheets automatically. Check or undo anytime: `npx variantkit doctor`
45
47
  (15 checks with fix-its) / `npx variantkit remove` (zero-residue).
46
48
 
47
49
  Finalize feedback is in-button (the button morphs to "✓ Saved" via the transport, or
48
- "✓ Copied" on the clipboard fallback), so no toast is needed. **Snapshots:** the panel's preset toolbar (≡+ / Version) saves a tuned
49
- variant — use it to keep two tunings and switch between them; Finalize acts on the active one.
50
+ "✓ Copied" on the clipboard fallback), so no toast is needed.
50
51
 
51
52
  ## The rules that make this work (never violate)
52
53
 
@@ -69,9 +70,8 @@ If you cannot run the installer (offline), you can still scaffold using the reci
69
70
  ## Vocabulary (use these exactly)
70
71
 
71
72
  **Element** = the thing being designed. **Variant** = one structural take on it. **Control** =
72
- one setting; **Configuration** = the contextual set of controls for an element. **Snapshot** =
73
- a saved variant+values state. **Finalize** → writes a **Decision** → agent **prunes** losers.
74
- Full glossary: `NAMING.md`.
73
+ one setting; **Configuration** = the contextual set of controls for an element. **Finalize**
74
+ writes a **Decision** → agent **prunes** losers. Full glossary: `NAMING.md`.
75
75
 
76
76
  ## Step 2 — scaffold a variant set
77
77
 
@@ -107,16 +107,28 @@ is shown. `focusOnHover` expands the hovered element's folder — panel-side onl
107
107
  drawn over the rendered element. Mount `<DialRoot/>` once in the app root. Still author the
108
108
  variant components file-per-variant (recipe below) so the prune stays a clean delete.
109
109
 
110
- ### The completeness bar (AGENT.md §7)
111
-
112
- A panel with 2-3 loose sliders is a failure: during exploration the panel must feel like
113
- the element's ACTUAL configuration panel. Every design literal a variant renders becomes a
114
- control (paramify rule). Non-trivial element ⇒ ≥4 folders, 12-25 controls; collapse the
115
- secondary ones. Use the archetype checklists in `variantkit/schemas/archetypes.ts`
116
- (button, card, hero, navbar, modal, form, table, list, badge, pricing, section) they are
117
- checklists to ADAPT and seed from the project's real values, never sets to paste. Drop a
118
- control that is a variant's structural identity by destructuring; resolve token selects
119
- (shadow, font family) to CSS in the shell via `SHADOWS` / `FONT_STACKS`.
110
+ ### The completeness bar — scale it to the element (AGENT.md §7)
111
+
112
+ The panel must feel like the element's ACTUAL configuration panel, not 2-3 loose sliders
113
+ but **match the work to the element**, because every control and every variant is generation
114
+ the developer waits on. Scale it:
115
+ - **Small/simple element** (button, badge, single input, tag): the few controls that genuinely
116
+ matter usually ~3-8. Do NOT manufacture 20 controls for a button; that just makes
117
+ VariantKit feel slow with no payoff.
118
+ - **Rich element** (hero, pricing card, navbar, modal, form): earn the full set — ≥4 folders,
119
+ ~12-25 controls collapsing the secondary ones.
120
+
121
+ **Expose the decisions, not every number (paramify rule).** A control earns its place only if
122
+ the developer would plausibly reach for it on THIS element. A design literal that's a real,
123
+ tweakable decision becomes a control; a fixed structural constant, a value the layout dictates,
124
+ or anything nobody would touch stays inline. The bar is "would they turn this dial?", not "is
125
+ there a number here?" — 8 controls that all matter beat 20 where half are noise (every junk row
126
+ is a small tax the developer pays scanning the panel). A literal that IS a variant's structural
127
+ identity is dropped by destructuring, not exposed. Use the archetype checklists in
128
+ `variantkit/schemas/archetypes.ts` (button, card, hero, navbar, modal, form, table, list, badge,
129
+ pricing, section) — checklists to ADAPT and seed from the project's real values, never sets to
130
+ paste. Resolve token selects (shadow, font family) to CSS in the
131
+ shell via `SHADOWS` / `FONT_STACKS`.
120
132
 
121
133
  ### Manual wiring (if not using the helper)
122
134
 
@@ -133,7 +145,9 @@ ComponentName/
133
145
  <c>.tsx
134
146
  ```
135
147
 
136
- `index.tsx` — variant = a DialKit `select`, your authored controls, finalize = an `action`.
148
+ `index.tsx` — variant = a DialKit `select` with `segmented: true` (renders as clean
149
+ separated selection pills, one per take, instead of a dropdown), your authored controls,
150
+ finalize = an `action`.
137
151
  (Control names/defaults below are placeholders — derive yours from the element + the
138
152
  project's tokens.)
139
153
 
@@ -147,7 +161,7 @@ const DEFAULTS: Record<string, ParamValue> = { density: 'comfortable', accent: t
147
161
 
148
162
  export default function ComponentName(props: { /* real props */ }) {
149
163
  const v = useDialKit('ComponentName', {
150
- variant: { type: 'select', options: ['a', 'b', 'c'], default: 'a' }, // omit when only one variant
164
+ variant: { type: 'select', options: ['a', 'b', 'c'], default: 'a', segmented: true }, // pills, not a dropdown; omit when only one variant
151
165
  density: { type: 'select', options: ['compact', 'comfortable'], default: DEFAULTS.density },
152
166
  accent: DEFAULTS.accent as string,
153
167
  finalize: { type: 'action', label: 'Finalize' },