variantkit 0.1.0 → 0.3.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 +41 -28
- package/NAMING.md +4 -6
- package/README.md +0 -2
- package/package.json +1 -1
- package/variantkit/configs.ts +30 -1
- package/variantkit/dialkit-clean.css +191 -9
- package/variantkit/motion.css +33 -0
- package/variantkit/patches/dialkit+1.2.0.patch +118 -2
- package/variantkit/react.tsx +237 -28
- package/variantkit/skill/SKILL.md +34 -20
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,
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
206
|
-
folder/header dividers (panel reads on
|
|
207
|
-
|
|
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
|
|
341
|
-
exploration the panel must feel like the element's actual configuration
|
|
342
|
-
every design decision the element really has, grouped the way a real
|
|
343
|
-
group them.
|
|
344
|
-
|
|
345
|
-
**
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 on — over-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
|
-
**
|
|
371
|
-
divider, single label) ⇒ a flat 3-5 control panel
|
|
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"
|
|
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"
|
|
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
|
|
41
|
-
>
|
|
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.
|
|
3
|
+
"version": "0.3.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": {
|
package/variantkit/configs.ts
CHANGED
|
@@ -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() },
|
|
@@ -68,3 +71,29 @@ export function defaultsOf(cfg: PanelConfig): Record<string, number | string | b
|
|
|
68
71
|
export function regOf(variantKeys: string[]): Record<string, true> {
|
|
69
72
|
return Object.fromEntries(variantKeys.map((k) => [k, true]))
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
// Flatten a (possibly nested) config into the path→default map DialKit addresses values by:
|
|
76
|
+
// dot-joined raw keys (`headingSize`, `Pricing Card.accent`). Used by the header Reset button
|
|
77
|
+
// to push every control — INCLUDING the variant — back to where it started. Folders are plain
|
|
78
|
+
// objects without a `type`; `_`-prefixed keys (e.g. `_collapsed`) and actions are skipped.
|
|
79
|
+
export function flatDefaults(cfg: PanelConfig, prefix = ''): Record<string, number | string | boolean> {
|
|
80
|
+
const out: Record<string, number | string | boolean> = {}
|
|
81
|
+
for (const [key, c] of Object.entries(cfg)) {
|
|
82
|
+
if (key.startsWith('_') || c == null) continue
|
|
83
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
84
|
+
if (typeof c === 'number' || typeof c === 'string' || typeof c === 'boolean') {
|
|
85
|
+
out[path] = c
|
|
86
|
+
} else if (Array.isArray(c)) {
|
|
87
|
+
out[path] = c[0] as number
|
|
88
|
+
} else if (typeof c === 'object') {
|
|
89
|
+
const o = c as { type?: string; default?: unknown }
|
|
90
|
+
if (o.type === 'action') continue
|
|
91
|
+
if (o.type) {
|
|
92
|
+
if (o.default !== undefined) out[path] = o.default as number | string | boolean
|
|
93
|
+
} else {
|
|
94
|
+
Object.assign(out, flatDefaults(c as PanelConfig, path)) // nested folder of controls
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* VariantKit panel cleanup for DialKit.
|
|
2
|
-
- Hide
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
7
|
+
.dialkit-root .dialkit-panel-toolbar {
|
|
7
8
|
display: none !important;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -12,20 +13,199 @@
|
|
|
12
13
|
bubble sets its own uniform 12px, so this only affects the open panel. */
|
|
13
14
|
.dialkit-root .dialkit-panel-inner:not([data-collapsed='true']) {
|
|
14
15
|
padding-bottom: 12px;
|
|
16
|
+
/* DialKit sets an inline pixel `height` from its own content measurement — but it under-measures
|
|
17
|
+
by ~14px (our bottom padding + the wrapped variant pills aren't counted), so the last control
|
|
18
|
+
(Finalize) gets clipped on EVERY window size, not just short ones. Force `height: auto` so the
|
|
19
|
+
panel hugs its real content, then cap to the viewport and scroll if a short window demands it.
|
|
20
|
+
Scoped to the expanded panel so DialKit's collapse animation (which drives `data-collapsed`)
|
|
21
|
+
is left untouched. */
|
|
22
|
+
height: auto !important;
|
|
23
|
+
max-height: calc(100vh - 28px);
|
|
24
|
+
overflow-y: auto;
|
|
25
|
+
overscroll-behavior: contain;
|
|
26
|
+
scrollbar-width: thin;
|
|
27
|
+
scrollbar-color: var(--dial-surface-hover) transparent;
|
|
28
|
+
}
|
|
29
|
+
.dialkit-root .dialkit-panel-inner:not([data-collapsed='true'])::-webkit-scrollbar {
|
|
30
|
+
width: 8px;
|
|
31
|
+
}
|
|
32
|
+
.dialkit-root .dialkit-panel-inner:not([data-collapsed='true'])::-webkit-scrollbar-thumb {
|
|
33
|
+
background: var(--dial-surface-hover);
|
|
34
|
+
border-radius: 999px;
|
|
35
|
+
border: 2px solid transparent;
|
|
36
|
+
background-clip: padding-box;
|
|
15
37
|
}
|
|
16
38
|
.dialkit-root .dialkit-panel-header {
|
|
17
|
-
|
|
39
|
+
/* A quiet divider + breathing room separates the header (title + chrome) from the controls,
|
|
40
|
+
so the panel reads as "identity up top, then the body". */
|
|
41
|
+
border-bottom: 1px solid var(--dial-border) !important;
|
|
42
|
+
padding-bottom: 12px !important;
|
|
43
|
+
margin-bottom: 12px !important;
|
|
44
|
+
}
|
|
45
|
+
.dialkit-root .dialkit-panel-header .dialkit-folder-header-top {
|
|
46
|
+
padding-bottom: 0 !important;
|
|
18
47
|
}
|
|
19
48
|
.dialkit-root .dialkit-folder {
|
|
20
49
|
border-top: none !important;
|
|
21
50
|
border-bottom: none !important;
|
|
22
51
|
}
|
|
23
52
|
|
|
24
|
-
/*
|
|
53
|
+
/* ── Variant selector: clean separated selection pills ───────────────────────────────────
|
|
54
|
+
DialKit renders the `variant` control (segmented:true) as a row of separated pills, one
|
|
55
|
+
per take, instead of a dropdown. The variant is the panel's hero choice, so every option
|
|
56
|
+
stays visible. Pills wrap to new lines when labels are long or the panel is narrow — they
|
|
57
|
+
never overflow or clip the row. */
|
|
58
|
+
.dialkit-root .dialkit-vk-pills {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 7px;
|
|
62
|
+
align-items: stretch;
|
|
63
|
+
}
|
|
64
|
+
.dialkit-root .dialkit-vk-pills .dialkit-select-label {
|
|
65
|
+
/* Drop the "Variant" caption — the segmented pills are the first thing under the title and are
|
|
66
|
+
self-evidently the take selector. A lone label above them just read as an orphaned control
|
|
67
|
+
row. The panel now opens straight into the pills, like a tab bar. */
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
.dialkit-root .dialkit-vk-pills-track {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
gap: 5px;
|
|
74
|
+
}
|
|
75
|
+
.dialkit-root .dialkit-vk-pill {
|
|
76
|
+
/* DialKit absolutely-positions panel buttons; pills must opt back into normal flow so the
|
|
77
|
+
track lays them out in a wrapping row. Inactive pills are quiet (faint surface, no border)
|
|
78
|
+
so the selected one carries all the emphasis. */
|
|
79
|
+
position: relative;
|
|
80
|
+
flex: 1 1 auto;
|
|
81
|
+
min-width: 60px;
|
|
82
|
+
max-width: 100%;
|
|
83
|
+
padding: 7px 11px;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
border: 1px solid transparent;
|
|
86
|
+
background: var(--dial-surface-subtle);
|
|
87
|
+
color: var(--dial-text-secondary);
|
|
88
|
+
font: 500 12px/1.15 system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
89
|
+
letter-spacing: 0.01em;
|
|
90
|
+
text-align: center;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
-webkit-font-smoothing: antialiased;
|
|
96
|
+
transition:
|
|
97
|
+
background-color 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
98
|
+
color 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
99
|
+
box-shadow 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
100
|
+
transform 120ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
101
|
+
}
|
|
102
|
+
.dialkit-root .dialkit-vk-pill:hover {
|
|
103
|
+
background: var(--dial-surface-hover);
|
|
104
|
+
color: var(--dial-text-primary);
|
|
105
|
+
}
|
|
106
|
+
.dialkit-root .dialkit-vk-pill:active {
|
|
107
|
+
transform: scale(0.96);
|
|
108
|
+
}
|
|
109
|
+
.dialkit-root .dialkit-vk-pill[data-active='true'] {
|
|
110
|
+
/* Solid fill + a small lift = unmistakable selection. `--dial-glass-bg` is the solid panel
|
|
111
|
+
colour (dark in dark mode, light in light mode), so the label always contrasts the fill. */
|
|
112
|
+
background: var(--dial-text-primary);
|
|
113
|
+
color: var(--dial-glass-bg);
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.13);
|
|
116
|
+
}
|
|
117
|
+
.dialkit-root .dialkit-vk-pill[data-active='true']:hover {
|
|
118
|
+
color: var(--dial-glass-bg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── Panel icon: collapse glyph (expanded) + brand mark (collapsed) ───────────────────────
|
|
122
|
+
DialKit's `.dialkit-panel-icon` sits top-right; clicking it (via the header's toggle) collapses
|
|
123
|
+
the panel. We paint it with a CSS mask in `currentColor` (themes itself, survives React
|
|
124
|
+
re-renders): a contract/minimize glyph while EXPANDED — that's its real job, the panel's
|
|
125
|
+
collapse affordance — and VariantKit's brand mark on the COLLAPSED bubble, where the panel
|
|
126
|
+
needs a face (two rounded cards, one solid, echoing the variant pills). */
|
|
127
|
+
.dialkit-root .dialkit-panel-icon > * {
|
|
128
|
+
display: none; /* hide DialKit's own dial paths */
|
|
129
|
+
}
|
|
130
|
+
.dialkit-root .dialkit-panel-icon {
|
|
131
|
+
background-color: currentColor;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
-webkit-mask-repeat: no-repeat;
|
|
134
|
+
mask-repeat: no-repeat;
|
|
135
|
+
-webkit-mask-position: center;
|
|
136
|
+
mask-position: center;
|
|
137
|
+
-webkit-mask-size: contain;
|
|
138
|
+
mask-size: contain;
|
|
139
|
+
}
|
|
140
|
+
.dialkit-root .dialkit-panel-inner:not([data-collapsed='true']) .dialkit-panel-icon {
|
|
141
|
+
-webkit-mask-image: var(--vk-collapse-mark);
|
|
142
|
+
mask-image: var(--vk-collapse-mark);
|
|
143
|
+
}
|
|
144
|
+
.dialkit-root .dialkit-panel-inner[data-collapsed='true'] .dialkit-panel-icon {
|
|
145
|
+
-webkit-mask-image: var(--vk-brand-mark);
|
|
146
|
+
mask-image: var(--vk-brand-mark);
|
|
147
|
+
}
|
|
148
|
+
.dialkit-root {
|
|
149
|
+
--vk-brand-mark: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Crect%20x='2.2'%20y='4.4'%20width='5.2'%20height='7.2'%20rx='1.7'%20fill='black'/%3E%3Crect%20x='8.6'%20y='4.4'%20width='5.2'%20height='7.2'%20rx='1.7'%20fill='none'%20stroke='black'%20stroke-width='1.3'%20opacity='.5'/%3E%3C/svg%3E");
|
|
150
|
+
--vk-collapse-mark: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20d='M12.8%203.2%20L9%207%20M9%204.7%20L9%207%20L11.3%207'%20fill='none'%20stroke='black'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3Cpath%20d='M3.2%2012.8%20L7%209%20M7%2011.3%20L7%209%20L4.7%209'%20fill='none'%20stroke='black'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── Element actions: Shuffle + Reset, beside the element name ────────────────────────────
|
|
154
|
+
Two quiet icon buttons sit right after the title, in the same row — they act on the ELEMENT,
|
|
155
|
+
so they read as the element's own controls. Panel chrome (theme + collapse) stays on the far
|
|
156
|
+
right; the title row reserves space for it so the two groups never collide. */
|
|
157
|
+
.dialkit-root .dialkit-panel-header .dialkit-folder-title-row {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 3px;
|
|
161
|
+
padding-right: 76px; /* clear the theme toggle (right 40) + collapse icon (right 12) */
|
|
162
|
+
}
|
|
163
|
+
.dialkit-root .dialkit-panel-header .dialkit-folder-title-row .dialkit-folder-title {
|
|
164
|
+
flex: 0 1 auto;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
text-overflow: ellipsis;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
.dialkit-root .vk-actions {
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 1px;
|
|
174
|
+
margin-left: 4px;
|
|
175
|
+
flex: 0 0 auto;
|
|
176
|
+
}
|
|
177
|
+
.dialkit-root .vk-action-btn {
|
|
178
|
+
width: 24px;
|
|
179
|
+
height: 24px;
|
|
180
|
+
display: inline-grid;
|
|
181
|
+
place-items: center;
|
|
182
|
+
border: none;
|
|
183
|
+
background: transparent;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
color: var(--dial-text-secondary);
|
|
187
|
+
transition:
|
|
188
|
+
background-color 140ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
189
|
+
color 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
190
|
+
}
|
|
191
|
+
.dialkit-root .vk-action-btn:hover {
|
|
192
|
+
background: var(--dial-surface-hover);
|
|
193
|
+
color: var(--dial-text-primary);
|
|
194
|
+
}
|
|
195
|
+
.dialkit-root .vk-action-btn:active {
|
|
196
|
+
transform: scale(0.9);
|
|
197
|
+
}
|
|
198
|
+
.dialkit-root .vk-action-btn svg {
|
|
199
|
+
display: block;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ── Header chrome: theme toggle ─────────────────────────────────────────────────────────
|
|
203
|
+
Panel chrome on the right — the theme toggle, just left of the collapse/brand icon. */
|
|
25
204
|
.dialkit-root .vk-theme-toggle {
|
|
26
205
|
position: absolute;
|
|
27
206
|
top: 6px;
|
|
28
|
-
right: 40px;
|
|
207
|
+
right: 40px; /* just left of the collapse icon (DialKit's panel-icon at right ~12) */
|
|
208
|
+
z-index: 2;
|
|
29
209
|
width: 26px;
|
|
30
210
|
height: 26px;
|
|
31
211
|
display: inline-grid;
|
|
@@ -35,11 +215,13 @@
|
|
|
35
215
|
border-radius: 7px;
|
|
36
216
|
cursor: pointer;
|
|
37
217
|
color: var(--dial-text-secondary);
|
|
38
|
-
|
|
39
|
-
|
|
218
|
+
transition:
|
|
219
|
+
background-color 140ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
220
|
+
color 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
40
221
|
}
|
|
41
222
|
.dialkit-root .vk-theme-toggle:hover {
|
|
42
223
|
background: var(--dial-surface-hover);
|
|
224
|
+
color: var(--dial-text-primary);
|
|
43
225
|
}
|
|
44
226
|
.dialkit-root .vk-theme-toggle:active {
|
|
45
227
|
transform: scale(0.92);
|
package/variantkit/motion.css
CHANGED
|
@@ -60,6 +60,35 @@
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/* ── Header actions feedback ─────────────────────────────────────────────────────────────
|
|
64
|
+
Reset spins its arrow once counter-clockwise; Shuffle gives its icon a quick tumble. Both
|
|
65
|
+
are keyed off a data attribute the JS sets for ~480ms, so each click animates fresh. */
|
|
66
|
+
.dialkit-root .vk-reset[data-spinning] svg {
|
|
67
|
+
animation: vk-reset-spin 480ms var(--vk-ease-out);
|
|
68
|
+
}
|
|
69
|
+
@keyframes vk-reset-spin {
|
|
70
|
+
from {
|
|
71
|
+
transform: rotate(0);
|
|
72
|
+
}
|
|
73
|
+
to {
|
|
74
|
+
transform: rotate(-360deg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
.dialkit-root .vk-shuffle[data-shuffling] svg {
|
|
78
|
+
animation: vk-shuffle-tumble 420ms var(--vk-ease-out);
|
|
79
|
+
}
|
|
80
|
+
@keyframes vk-shuffle-tumble {
|
|
81
|
+
0% {
|
|
82
|
+
transform: rotate(0) scale(1);
|
|
83
|
+
}
|
|
84
|
+
45% {
|
|
85
|
+
transform: rotate(16deg) scale(0.86);
|
|
86
|
+
}
|
|
87
|
+
100% {
|
|
88
|
+
transform: rotate(0) scale(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
@media (prefers-reduced-motion: reduce) {
|
|
64
93
|
.dialkit-root.vk-theming,
|
|
65
94
|
.dialkit-root.vk-theming * {
|
|
@@ -68,6 +97,10 @@
|
|
|
68
97
|
.vk-theme-toggle .vk-swap {
|
|
69
98
|
animation: none;
|
|
70
99
|
}
|
|
100
|
+
.dialkit-root .vk-reset[data-spinning] svg,
|
|
101
|
+
.dialkit-root .vk-shuffle[data-shuffling] svg {
|
|
102
|
+
animation: none;
|
|
103
|
+
}
|
|
71
104
|
.dialkit-root button:active {
|
|
72
105
|
transform: none;
|
|
73
106
|
}
|
|
@@ -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..
|
|
2
|
+
index d9f4837..ce0432f 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,68 @@ index d9f4837..4bf4e6d 100644
|
|
|
14
23
|
children: folderContent
|
|
15
24
|
}
|
|
16
25
|
);
|
|
26
|
+
@@ -1034,6 +1035,7 @@ function Slider({
|
|
27
|
+
shortcut,
|
|
28
|
+
shortcutActive
|
|
29
|
+
}) {
|
|
30
|
+
+ if (typeof value !== "number") value = typeof min === "number" ? min : 0;
|
|
31
|
+
const wrapperRef = (0, import_react5.useRef)(null);
|
|
32
|
+
const trackRef = (0, import_react5.useRef)(null);
|
|
33
|
+
const inputRef = (0, import_react5.useRef)(null);
|
|
34
|
+
@@ -1825,6 +1827,7 @@ function EaseTextInput({ ease, onChange }) {
|
|
35
|
+
// src/components/TextControl.tsx
|
|
36
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
37
|
+
function TextControl({ label, value, onChange, placeholder }) {
|
|
38
|
+
+ if (typeof value !== "string") value = "";
|
|
39
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "dialkit-text-control", children: [
|
|
40
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: "dialkit-text-label", children: label }),
|
|
41
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
42
|
+
@@ -1853,6 +1856,15 @@ function normalizeOptions(options) {
|
|
43
|
+
(opt) => typeof opt === "string" ? { value: opt, label: toTitleCase(opt) } : opt
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
+function VKSegmented({ label, value, options, onChange }) {
|
|
47
|
+
+ const normalized = normalizeOptions(options);
|
|
48
|
+
+ return (0, import_jsx_runtime11.jsxs)("div", { className: "dialkit-vk-pills", children: [
|
|
49
|
+
+ (0, import_jsx_runtime11.jsx)("span", { className: "dialkit-select-label", children: label }),
|
|
50
|
+
+ (0, import_jsx_runtime11.jsxs)("div", { className: "dialkit-vk-pills-track", children: normalized.map(function(o) {
|
|
51
|
+
+ 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);
|
|
52
|
+
+ }) })
|
|
53
|
+
+ ] });
|
|
54
|
+
+}
|
|
55
|
+
function SelectControl({ label, value, options, onChange }) {
|
|
56
|
+
const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
|
|
57
|
+
const triggerRef = (0, import_react10.useRef)(null);
|
|
58
|
+
@@ -1966,6 +1978,7 @@ var import_react12 = require("react");
|
|
59
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
60
|
+
var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
61
|
+
function ColorControl({ label, value, onChange }) {
|
|
62
|
+
+ if (typeof value !== "string") value = "#000000";
|
|
63
|
+
const [isEditing, setIsEditing] = (0, import_react12.useState)(false);
|
|
64
|
+
const [editValue, setEditValue] = (0, import_react12.useState)(value);
|
|
65
|
+
const colorInputRef = (0, import_react12.useRef)(null);
|
|
66
|
+
@@ -2266,7 +2279,7 @@ Apply these values as the new defaults in the useDialKit call.`;
|
|
67
|
+
);
|
|
68
|
+
case "select":
|
|
69
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
70
|
+
- SelectControl,
|
|
71
|
+
+ (control.segmented ? VKSegmented : SelectControl),
|
|
72
|
+
{
|
|
73
|
+
label: control.label,
|
|
74
|
+
value,
|
|
17
75
|
diff --git a/node_modules/dialkit/dist/index.js b/node_modules/dialkit/dist/index.js
|
|
18
|
-
index f8d8baf..
|
|
76
|
+
index f8d8baf..47a7e4d 100644
|
|
19
77
|
--- a/node_modules/dialkit/dist/index.js
|
|
20
78
|
+++ b/node_modules/dialkit/dist/index.js
|
|
79
|
+
@@ -297,7 +297,7 @@ var DialStoreClass = class {
|
|
80
|
+
} else if (this.isActionConfig(value)) {
|
|
81
|
+
controls.push({ type: "action", path, label: value.label || label });
|
|
82
|
+
} else if (this.isSelectConfig(value)) {
|
|
83
|
+
- controls.push({ type: "select", path, label, options: value.options });
|
|
84
|
+
+ controls.push({ type: "select", path, label, options: value.options, segmented: value.segmented });
|
|
85
|
+
} else if (this.isColorConfig(value)) {
|
|
86
|
+
controls.push({ type: "color", path, label });
|
|
87
|
+
} else if (this.isTextConfig(value)) {
|
|
21
88
|
@@ -966,8 +966,9 @@ function Folder({ title, children, defaultOpen = true, isRoot = false, inline =
|
|
22
89
|
style: panelStyle,
|
|
23
90
|
onClick: !isOpen ? handleToggle : void 0,
|
|
@@ -30,3 +97,52 @@ index f8d8baf..7a0e7b3 100644
|
|
|
30
97
|
children: folderContent
|
|
31
98
|
}
|
|
32
99
|
);
|
|
100
|
+
@@ -994,6 +995,7 @@ function Slider({
|
|
101
|
+
shortcut,
|
|
102
|
+
shortcutActive
|
|
103
|
+
}) {
|
|
104
|
+
+ if (typeof value !== "number") value = typeof min === "number" ? min : 0;
|
|
105
|
+
const wrapperRef = useRef4(null);
|
|
106
|
+
const trackRef = useRef4(null);
|
|
107
|
+
const inputRef = useRef4(null);
|
|
108
|
+
@@ -1785,6 +1787,7 @@ function EaseTextInput({ ease, onChange }) {
|
|
109
|
+
// src/components/TextControl.tsx
|
|
110
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
111
|
+
function TextControl({ label, value, onChange, placeholder }) {
|
|
112
|
+
+ if (typeof value !== "string") value = "";
|
|
113
|
+
return /* @__PURE__ */ jsxs9("div", { className: "dialkit-text-control", children: [
|
|
114
|
+
/* @__PURE__ */ jsx10("label", { className: "dialkit-text-label", children: label }),
|
|
115
|
+
/* @__PURE__ */ jsx10(
|
|
116
|
+
@@ -1813,6 +1816,15 @@ function normalizeOptions(options) {
|
|
117
|
+
(opt) => typeof opt === "string" ? { value: opt, label: toTitleCase(opt) } : opt
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
+function VKSegmented({ label, value, options, onChange }) {
|
|
121
|
+
+ const normalized = normalizeOptions(options);
|
|
122
|
+
+ return jsxs10("div", { className: "dialkit-vk-pills", children: [
|
|
123
|
+
+ jsx11("span", { className: "dialkit-select-label", children: label }),
|
|
124
|
+
+ jsxs10("div", { className: "dialkit-vk-pills-track", children: normalized.map(function(o) {
|
|
125
|
+
+ 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);
|
|
126
|
+
+ }) })
|
|
127
|
+
+ ] });
|
|
128
|
+
+}
|
|
129
|
+
function SelectControl({ label, value, options, onChange }) {
|
|
130
|
+
const [isOpen, setIsOpen] = useState6(false);
|
|
131
|
+
const triggerRef = useRef8(null);
|
|
132
|
+
@@ -1926,6 +1938,7 @@ import { useState as useState7, useRef as useRef9, useEffect as useEffect6 } fro
|
|
133
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
134
|
+
var HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
135
|
+
function ColorControl({ label, value, onChange }) {
|
|
136
|
+
+ if (typeof value !== "string") value = "#000000";
|
|
137
|
+
const [isEditing, setIsEditing] = useState7(false);
|
|
138
|
+
const [editValue, setEditValue] = useState7(value);
|
|
139
|
+
const colorInputRef = useRef9(null);
|
|
140
|
+
@@ -2226,7 +2239,7 @@ Apply these values as the new defaults in the useDialKit call.`;
|
|
141
|
+
);
|
|
142
|
+
case "select":
|
|
143
|
+
return /* @__PURE__ */ jsx14(
|
|
144
|
+
- SelectControl,
|
|
145
|
+
+ (control.segmented ? VKSegmented : SelectControl),
|
|
146
|
+
{
|
|
147
|
+
label: control.label,
|
|
148
|
+
value,
|
package/variantkit/react.tsx
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// (recommended) ./dialkit-clean.css.
|
|
20
20
|
import { useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'
|
|
21
21
|
import { motion, MotionConfig } from 'motion/react'
|
|
22
|
-
import { useDialKit } from 'dialkit'
|
|
23
|
-
import { panelConfig, defaultsOf, regOf, type PanelConfig } from './configs'
|
|
22
|
+
import { useDialKit, DialStore } from 'dialkit'
|
|
23
|
+
import { panelConfig, defaultsOf, regOf, flatDefaults, type PanelConfig } from './configs'
|
|
24
24
|
import { buildDecision, submitDecision, type ParamValue } from './buildDecision'
|
|
25
25
|
|
|
26
26
|
export interface ElementDef {
|
|
@@ -32,8 +32,13 @@ export interface ElementDef {
|
|
|
32
32
|
* The element's own controls — contextual, authored for THIS element (any DialKit control:
|
|
33
33
|
* slider, select, boolean toggle, color, text, spring, nested folder groups…). VariantKit
|
|
34
34
|
* adds `variant` + `finalize` around them; it never decides what these are.
|
|
35
|
+
*
|
|
36
|
+
* Pass a plain object for controls shared by every variant, OR a function of the active variant
|
|
37
|
+
* to make the control set DEPEND on the variant — different takes often need different knobs (a
|
|
38
|
+
* "split" hero has a media control a "minimal" one doesn't; a "vinyl" player has spin speed).
|
|
39
|
+
* Switch the variant and the panel swaps its controls; shared keys keep their values.
|
|
35
40
|
*/
|
|
36
|
-
controls?: PanelConfig
|
|
41
|
+
controls?: PanelConfig | ((variant: string) => PanelConfig)
|
|
37
42
|
/** Render the active variant from its resolved values. */
|
|
38
43
|
render: (variant: string, values: Record<string, ParamValue>) => ReactNode
|
|
39
44
|
/** Optional full config override (replaces the assembled variant+controls+finalize). */
|
|
@@ -50,9 +55,6 @@ export interface StudioProps {
|
|
|
50
55
|
onFinalize?: (decision: ReturnType<typeof buildDecision>) => void
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
const cfgFor = (e: ElementDef): PanelConfig =>
|
|
54
|
-
e.config ?? panelConfig(e.controls ?? {}, e.keys, { component: e.name })
|
|
55
|
-
|
|
56
58
|
// Match DialKit's humanized folder title ("Pricing Card") back to an element name.
|
|
57
59
|
const norm = (s: string) => s.replace(/\s+/g, '').toLowerCase()
|
|
58
60
|
|
|
@@ -77,39 +79,99 @@ export function Studio({ elements, name = 'VariantKit', focusOnHover, onFinalize
|
|
|
77
79
|
const elsRef = useRef(elements)
|
|
78
80
|
elsRef.current = elements
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
82
|
+
const single = elements.length === 1
|
|
83
|
+
|
|
84
|
+
// Each element's active variant — seeds the variant-specific control resolution below. Seeded
|
|
85
|
+
// with the first key, then synced from the live panel after each render (see the effect). When
|
|
86
|
+
// a variant changes, this updates → the config rebuilds → the panel swaps that variant's
|
|
87
|
+
// controls. (Studio is remounted per element set, so this reseeds correctly.)
|
|
88
|
+
const [variants, setVariants] = useState<Record<string, string>>(() =>
|
|
89
|
+
Object.fromEntries(elements.map((e) => [e.name, e.keys[0]])),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Resolve an element's controls for its active variant: a function `controls` is called with the
|
|
93
|
+
// variant, a plain object is shared across all variants.
|
|
94
|
+
const controlsOf = (e: ElementDef): PanelConfig => {
|
|
95
|
+
const v = variants[e.name] ?? e.keys[0]
|
|
96
|
+
return typeof e.controls === 'function' ? e.controls(v) : e.controls ?? {}
|
|
97
|
+
}
|
|
98
|
+
const cfgForE = (e: ElementDef): PanelConfig =>
|
|
99
|
+
e.config ?? panelConfig(controlsOf(e), e.keys, { component: e.name })
|
|
100
|
+
|
|
101
|
+
// Build the combined config. With ONE element, a per-element folder is pure redundancy —
|
|
102
|
+
// the panel title already names it, so a "Button" folder under "Button Lab" just adds a
|
|
103
|
+
// confusing second hierarchy level. So flatten: the lone element's controls sit at the panel
|
|
104
|
+
// root. With 2+ elements, give each its own folder (first open, rest collapsed) — that's
|
|
105
|
+
// where the grouping earns its keep. The finalize button's "✓ Copied" feedback is handled
|
|
106
|
+
// inside copyDecision (panel-side), so the label stays a plain "Finalize <name>".
|
|
83
107
|
const combined: Record<string, unknown> = {}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
combined
|
|
88
|
-
|
|
108
|
+
if (single) {
|
|
109
|
+
const e = elements[0]
|
|
110
|
+
const base = cfgForE(e)
|
|
111
|
+
Object.assign(combined, base, {
|
|
112
|
+
finalize: { ...(base.finalize as object), label: `Finalize ${e.name}` },
|
|
113
|
+
})
|
|
114
|
+
} else {
|
|
115
|
+
elements.forEach((e, i) => {
|
|
116
|
+
const base = cfgForE(e)
|
|
117
|
+
const finalize = { ...(base.finalize as object), label: `Finalize ${e.name}` }
|
|
118
|
+
combined[e.name] = { ...base, finalize, _collapsed: i !== 0 }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
89
121
|
|
|
90
122
|
const all = useDialKit(name, combined as never, {
|
|
91
123
|
onAction: (path: string) => {
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
// Single element → its finalize lives at the root (path === 'finalize'), and its values
|
|
125
|
+
// ARE `all`. Multiple → the element name prefixes the path and `all[name]` is its slice.
|
|
126
|
+
const e = single ? elsRef.current[0] : elsRef.current.find((x) => x.name === path.split('.')[0])
|
|
94
127
|
if (!e) return
|
|
95
|
-
const slice = (all as Record<string, Record<string, ParamValue>>)[
|
|
96
|
-
|
|
128
|
+
const slice = (single ? all : (all as Record<string, Record<string, ParamValue>>)[e.name]) as Record<
|
|
129
|
+
string,
|
|
130
|
+
ParamValue
|
|
131
|
+
>
|
|
132
|
+
const decision = buildDecision(e.name, slice, defaultsOf(cfgForE(e)), regOf(e.keys))
|
|
97
133
|
// Dev transport first ("✓ Saved" -> .variantkit/decisions/), clipboard fallback ("✓ Copied").
|
|
98
134
|
submitDecision(decision)
|
|
99
135
|
onFinalize?.(decision)
|
|
100
136
|
},
|
|
101
137
|
}) as Record<string, Record<string, ParamValue>>
|
|
102
138
|
|
|
139
|
+
// Keep `variants` in step with the live panel so variant-specific controls swap in when you
|
|
140
|
+
// pick a different take. Runs after every commit; setVariants no-ops when nothing changed, so
|
|
141
|
+
// it settles in one extra render and never loops.
|
|
103
142
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
setVariants((prev) => {
|
|
144
|
+
let changed = false
|
|
145
|
+
const next = { ...prev }
|
|
146
|
+
for (const e of elsRef.current) {
|
|
147
|
+
if (e.keys.length < 2) continue
|
|
148
|
+
const live = single
|
|
149
|
+
? (all as unknown as Record<string, ParamValue>).variant
|
|
150
|
+
: (all as Record<string, Record<string, ParamValue>>)[e.name]?.variant
|
|
151
|
+
if (live != null && String(live) !== prev[e.name]) {
|
|
152
|
+
next[e.name] = String(live)
|
|
153
|
+
changed = true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return changed ? next : prev
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
// Focus-on-hover only applies to the multi-element layout (it toggles per-element folders).
|
|
162
|
+
// With a single flattened element there are no element folders to focus.
|
|
163
|
+
if (focusOnHover && !single) focusFolder(focused)
|
|
164
|
+
}, [focused, focusOnHover, single])
|
|
165
|
+
|
|
166
|
+
// Shuffle (randomize every knob — and the take) + Reset (back to defaults), in the header.
|
|
167
|
+
usePanelActions(name, combined as PanelConfig)
|
|
106
168
|
|
|
107
169
|
// Render the elements as-is — VariantKit adds no layout, spacing, alignment, rings, or
|
|
108
170
|
// badges around the project's UI. The host page owns presentation entirely.
|
|
109
171
|
return (
|
|
110
172
|
<MotionConfig reducedMotion="user">
|
|
111
173
|
{elements.map((e) => {
|
|
112
|
-
const slice = all[e.name]
|
|
174
|
+
const slice = single ? (all as unknown as Record<string, ParamValue>) : all[e.name]
|
|
113
175
|
if (!slice) return null
|
|
114
176
|
const variant = e.keys.length > 1 ? String(slice.variant) : e.keys[0]
|
|
115
177
|
return (
|
|
@@ -170,6 +232,8 @@ export function useDialkitTheme(initial: 'light' | 'dark' = 'light') {
|
|
|
170
232
|
if (el.getAttribute('data-theme') !== themeRef.current) el.setAttribute('data-theme', themeRef.current)
|
|
171
233
|
})
|
|
172
234
|
document.querySelectorAll<HTMLElement>('.dialkit-panel-header').forEach((hdr) => {
|
|
235
|
+
// Theme is PANEL chrome, so it lives in the header on the right (CSS pins it just left of
|
|
236
|
+
// the collapse icon) — deliberately apart from the element actions (shuffle/reset, left).
|
|
173
237
|
const titleRow = hdr.querySelector<HTMLElement>('.dialkit-folder-header-top') ?? hdr
|
|
174
238
|
// Keep exactly one toggle (hot-reload can leave a stale node behind).
|
|
175
239
|
const existing = titleRow.querySelectorAll<HTMLButtonElement>('.vk-theme-toggle')
|
|
@@ -177,7 +241,6 @@ export function useDialkitTheme(initial: 'light' | 'dark' = 'light') {
|
|
|
177
241
|
let btn = existing[0] ?? null
|
|
178
242
|
if (!btn) {
|
|
179
243
|
btn = document.createElement('button')
|
|
180
|
-
// Absolute, just left of DialKit's settings icon (which is absolute at right:12).
|
|
181
244
|
btn.className = 'vk-theme-toggle'
|
|
182
245
|
btn.type = 'button'
|
|
183
246
|
btn.setAttribute('aria-label', 'Toggle panel theme')
|
|
@@ -197,18 +260,20 @@ export function useDialkitTheme(initial: 'light' | 'dark' = 'light') {
|
|
|
197
260
|
})
|
|
198
261
|
}
|
|
199
262
|
|
|
200
|
-
// Coalesce mutation bursts into one sync per frame
|
|
201
|
-
//
|
|
263
|
+
// Coalesce mutation bursts into one sync per frame. sync() is idempotent (it only writes when
|
|
264
|
+
// a value actually differs and only appends the toggle when it's missing), so we stay
|
|
265
|
+
// connected rather than disconnecting around our own writes — the same reason as the action
|
|
266
|
+
// cluster: disconnecting opened a window where the panel-swap mutation landed unobserved and
|
|
267
|
+
// the toggle never got (re)appended into the freshly-built cluster.
|
|
202
268
|
let frame = 0
|
|
203
|
-
const
|
|
269
|
+
const schedule = () => {
|
|
204
270
|
if (frame) return
|
|
205
271
|
frame = requestAnimationFrame(() => {
|
|
206
272
|
frame = 0
|
|
207
|
-
mo.disconnect()
|
|
208
273
|
sync()
|
|
209
|
-
mo.observe(document.body, { childList: true, subtree: true })
|
|
210
274
|
})
|
|
211
|
-
}
|
|
275
|
+
}
|
|
276
|
+
const mo = new MutationObserver(schedule)
|
|
212
277
|
|
|
213
278
|
// Delightful theme switch: add `.vk-theming` so the panel cross-fades its colors (the
|
|
214
279
|
// class scopes a transition that only exists during the switch — see motion.css), then
|
|
@@ -216,6 +281,7 @@ export function useDialkitTheme(initial: 'light' | 'dark' = 'light') {
|
|
|
216
281
|
const panels = document.querySelectorAll('.dialkit-root')
|
|
217
282
|
panels.forEach((p) => p.classList.add('vk-theming'))
|
|
218
283
|
sync()
|
|
284
|
+
schedule() // catch the action cluster if it commits a frame after this effect
|
|
219
285
|
const settle = setTimeout(() => panels.forEach((p) => p.classList.remove('vk-theming')), 420)
|
|
220
286
|
mo.observe(document.body, { childList: true, subtree: true })
|
|
221
287
|
try {
|
|
@@ -232,3 +298,146 @@ export function useDialkitTheme(initial: 'light' | 'dark' = 'light') {
|
|
|
232
298
|
|
|
233
299
|
return { theme, setTheme }
|
|
234
300
|
}
|
|
301
|
+
|
|
302
|
+
// ── Element actions: Shuffle + Reset ──────────────────────────────────────────────────────
|
|
303
|
+
// Two icon buttons injected right after the element name in the header title row — they act on
|
|
304
|
+
// the ELEMENT (randomize / restore its controls), so they read as the element's own controls
|
|
305
|
+
// (panel chrome — theme + collapse — stays on the far right). They drive the live panel through
|
|
306
|
+
// DialKit's store, so every control and the variant pills update in place, as if you'd dragged
|
|
307
|
+
// them.
|
|
308
|
+
// Shuffle → a fresh random valid value for every slider/select/toggle (and the variant), so
|
|
309
|
+
// "surprise me" lands a whole new combination. Color/text/spring are left alone —
|
|
310
|
+
// random hex or copy is noise, not exploration.
|
|
311
|
+
// Reset → every control (incl. the variant) back to its authored default.
|
|
312
|
+
|
|
313
|
+
const SHUFFLE =
|
|
314
|
+
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>'
|
|
315
|
+
const RESET =
|
|
316
|
+
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>'
|
|
317
|
+
|
|
318
|
+
// A DialKit control's metadata (the shape we read from the live store). Loosely typed — we only
|
|
319
|
+
// touch the fields we need and ignore the rest of DialKit's union.
|
|
320
|
+
interface ControlMeta {
|
|
321
|
+
type: string
|
|
322
|
+
path: string
|
|
323
|
+
min?: number
|
|
324
|
+
max?: number
|
|
325
|
+
step?: number
|
|
326
|
+
options?: (string | { value: string; label: string })[]
|
|
327
|
+
children?: ControlMeta[]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A random *valid* value for a single control, or undefined to leave it untouched. Sliders snap
|
|
331
|
+
// to their step; selects/variant pick an option; toggles flip a coin. Anything else is skipped.
|
|
332
|
+
function randomValue(meta: ControlMeta): number | string | boolean | undefined {
|
|
333
|
+
if (meta.type === 'slider') {
|
|
334
|
+
const min = meta.min ?? 0
|
|
335
|
+
const max = meta.max ?? 1
|
|
336
|
+
const step = meta.step ?? (Number.isInteger(min) && Number.isInteger(max) ? 1 : (max - min) / 100 || 1)
|
|
337
|
+
const steps = Math.max(1, Math.round((max - min) / step))
|
|
338
|
+
return +(min + Math.floor(Math.random() * (steps + 1)) * step).toFixed(4)
|
|
339
|
+
}
|
|
340
|
+
if (meta.type === 'toggle') return Math.random() < 0.5
|
|
341
|
+
if (meta.type === 'select') {
|
|
342
|
+
const opts = (meta.options ?? []).map((o) => (typeof o === 'string' ? o : o.value))
|
|
343
|
+
return opts.length ? opts[Math.floor(Math.random() * opts.length)] : undefined
|
|
344
|
+
}
|
|
345
|
+
return undefined // color / text / spring / transition / action / folder — left as-is
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function usePanelActions(panelName: string, config: PanelConfig) {
|
|
349
|
+
const cfgRef = useRef(config)
|
|
350
|
+
cfgRef.current = config
|
|
351
|
+
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
const panelId = () => DialStore.getPanels().find((p) => p.name === panelName)?.id
|
|
354
|
+
|
|
355
|
+
const reset = () => {
|
|
356
|
+
const id = panelId()
|
|
357
|
+
if (!id) return
|
|
358
|
+
for (const [path, value] of Object.entries(flatDefaults(cfgRef.current))) {
|
|
359
|
+
DialStore.updateValue(id, path, value as never)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const shuffle = () => {
|
|
364
|
+
const id = panelId()
|
|
365
|
+
if (!id) return
|
|
366
|
+
const panel = DialStore.getPanel(id)
|
|
367
|
+
if (!panel) return
|
|
368
|
+
const walk = (controls: ControlMeta[]) => {
|
|
369
|
+
for (const c of controls) {
|
|
370
|
+
if (c.children?.length) walk(c.children)
|
|
371
|
+
const v = randomValue(c)
|
|
372
|
+
if (v !== undefined) DialStore.updateValue(id, c.path, v as never)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
walk(panel.controls as unknown as ControlMeta[])
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Build an icon button once; the spin/pop feedback comes from a data attribute the CSS keys
|
|
379
|
+
// off. `label` is the accessible/tooltip description (the button itself is icon-only).
|
|
380
|
+
const make = (cls: string, label: string, svg: string, onClick: () => void, flash: string) => {
|
|
381
|
+
const btn = document.createElement('button')
|
|
382
|
+
btn.type = 'button'
|
|
383
|
+
btn.className = `vk-action-btn ${cls}`
|
|
384
|
+
btn.setAttribute('aria-label', label)
|
|
385
|
+
btn.title = label
|
|
386
|
+
btn.innerHTML = svg
|
|
387
|
+
const stop = (e: Event) => e.stopPropagation()
|
|
388
|
+
btn.addEventListener('pointerdown', stop)
|
|
389
|
+
btn.addEventListener('mousedown', stop)
|
|
390
|
+
btn.addEventListener('click', (e) => {
|
|
391
|
+
e.stopPropagation()
|
|
392
|
+
onClick()
|
|
393
|
+
btn.setAttribute(flash, '')
|
|
394
|
+
setTimeout(() => btn.removeAttribute(flash), 480)
|
|
395
|
+
})
|
|
396
|
+
return btn
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const sync = () => {
|
|
400
|
+
// Ensure the panel header's title row (the element name, only present when expanded) carries
|
|
401
|
+
// exactly one shuffle/reset pair, appended after the title — robust against React re-renders.
|
|
402
|
+
// Then sweep any orphaned pairs (panel collapsed, element swapped, hot-reload).
|
|
403
|
+
const valid = new Set<Element>()
|
|
404
|
+
document.querySelectorAll<HTMLElement>('.dialkit-panel-header .dialkit-folder-title-row').forEach((row) => {
|
|
405
|
+
let bar = row.querySelector<HTMLElement>(':scope > .vk-actions')
|
|
406
|
+
if (!bar) {
|
|
407
|
+
bar = document.createElement('div')
|
|
408
|
+
bar.className = 'vk-actions'
|
|
409
|
+
bar.appendChild(make('vk-shuffle', 'Shuffle all controls', SHUFFLE, shuffle, 'data-shuffling'))
|
|
410
|
+
bar.appendChild(make('vk-reset', 'Reset to defaults', RESET, reset, 'data-spinning'))
|
|
411
|
+
row.appendChild(bar)
|
|
412
|
+
}
|
|
413
|
+
valid.add(bar)
|
|
414
|
+
})
|
|
415
|
+
document.querySelectorAll('.vk-actions').forEach((b) => {
|
|
416
|
+
if (!valid.has(b)) b.remove()
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Frame-coalesced sync on any DOM change. sync() is idempotent — it injects the cluster only
|
|
421
|
+
// when it's missing and bails when it's already there — so we DON'T disconnect the observer
|
|
422
|
+
// around our own writes: our injection fires one more (no-op) sync and then goes quiet. The
|
|
423
|
+
// old disconnect/reconnect dance opened a window where the panel-swap mutation (on project
|
|
424
|
+
// switch) landed unobserved, leaving the cluster un-injected. Staying connected closes it.
|
|
425
|
+
let frame = 0
|
|
426
|
+
const schedule = () => {
|
|
427
|
+
if (frame) return
|
|
428
|
+
frame = requestAnimationFrame(() => {
|
|
429
|
+
frame = 0
|
|
430
|
+
sync()
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
const mo = new MutationObserver(schedule)
|
|
434
|
+
sync()
|
|
435
|
+
schedule() // catch panel DOM that commits the frame after mount
|
|
436
|
+
mo.observe(document.body, { childList: true, subtree: true })
|
|
437
|
+
return () => {
|
|
438
|
+
if (frame) cancelAnimationFrame(frame)
|
|
439
|
+
mo.disconnect()
|
|
440
|
+
document.querySelectorAll('.vk-actions').forEach((el) => el.remove())
|
|
441
|
+
}
|
|
442
|
+
}, [panelName])
|
|
443
|
+
}
|
|
@@ -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
|
|
17
|
-
|
|
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
|
|
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.
|
|
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. **
|
|
73
|
-
|
|
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
|
-
|
|
113
|
-
the
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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' },
|