variantkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +428 -0
- package/LICENSE +21 -0
- package/NAMING.md +42 -0
- package/README.md +111 -0
- package/package.json +36 -0
- package/variantkit/README.md +32 -0
- package/variantkit/buildDecision.ts +264 -0
- package/variantkit/configs.ts +70 -0
- package/variantkit/dialkit-clean.css +46 -0
- package/variantkit/dialkit-dark.css +27 -0
- package/variantkit/init.mjs +587 -0
- package/variantkit/motion.css +77 -0
- package/variantkit/patches/dialkit+1.2.0.patch +32 -0
- package/variantkit/react/VariantBar.tsx +208 -0
- package/variantkit/react/VariantStage.tsx +92 -0
- package/variantkit/react/vkStore.ts +38 -0
- package/variantkit/react.tsx +234 -0
- package/variantkit/schemas/archetypes.ts +216 -0
- package/variantkit/schemas/sections.ts +151 -0
- package/variantkit/skill/SKILL.md +223 -0
- package/variantkit/templates/next-pages-api.ts +33 -0
- package/variantkit/templates/next-route.ts +33 -0
- package/variantkit/vite-plugin.d.mts +8 -0
- package/variantkit/vite-plugin.mjs +55 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# variantkit/ — what gets installed
|
|
2
|
+
|
|
3
|
+
This directory is the distributable. `init.mjs` copies the runtime into a target project
|
|
4
|
+
and wires everything; see the [repo README](../README.md) for positioning and
|
|
5
|
+
[docs/quickstart.md](../docs/quickstart.md) for usage.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx variantkit [init|doctor|remove] [targetDir] [flags]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `buildDecision.ts` — pure core: dot-path flatten (schema 2), override diff (taste
|
|
12
|
+
signal), prune list, `submitDecision` (dev transport, "✓ Saved") with clipboard fallback
|
|
13
|
+
("✓ Copied"), `defaultsFromConfig`
|
|
14
|
+
- `configs.ts` — panel assembly helpers (`panelConfig`, `defaultsOf`, `regOf`); VariantKit
|
|
15
|
+
adds only the variant select + finalize around YOUR controls
|
|
16
|
+
- `schemas/sections.ts` — composable control sections + token resolvers (SHADOWS, FONT_STACKS)
|
|
17
|
+
- `schemas/archetypes.ts` — 11 per-element checklists of design axes (see
|
|
18
|
+
[docs/archetypes.md](../docs/archetypes.md)) — adapt + seed, never paste
|
|
19
|
+
- `react.tsx` — `Studio` (N elements → one panel, folder each, finalize routing,
|
|
20
|
+
focus-on-hover) + `useDialkitTheme` (panel dark mode with header toggle)
|
|
21
|
+
- `react/VariantBar.tsx` — bottom bar: variant tabs, keys 1-9, Compare, Finalize;
|
|
22
|
+
auto-discovers sets (shell or Studio layout) from DialKit's documented store
|
|
23
|
+
- `react/VariantStage.tsx` — live side-by-side compare grid for the classic shell
|
|
24
|
+
- `dialkit-clean.css` / `dialkit-dark.css` / `motion.css` — panel chrome polish (hide
|
|
25
|
+
redundant copy button, dark palette, micro-motion; never touches project UI)
|
|
26
|
+
- `patches/dialkit+1.2.0.patch` — delightful panel minimize/expand morph (patch-package)
|
|
27
|
+
- `vite-plugin.mjs` (+ `.d.mts`) — dev-only decision transport for Vite
|
|
28
|
+
- `templates/` — Next.js App/Pages Router transport routes
|
|
29
|
+
- `skill/SKILL.md` — global Claude Code skill (installed to `~/.claude/skills/variantkit`)
|
|
30
|
+
- `../AGENT.md` — the agent contract: scaffold convention, authoring rules, decision
|
|
31
|
+
schema 2, prune + self-check, §7 completeness bar, §6 deslop, §8 taste memory
|
|
32
|
+
- `../NAMING.md` — the vocabulary (one word per concept)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// VariantKit core — pure, no React imports. Builds the finalize decision (winner +
|
|
2
|
+
// override diff + prune list) and ships it to your agent so it can prune the variant
|
|
3
|
+
// set down to the winner. See AGENT.md for the contract.
|
|
4
|
+
//
|
|
5
|
+
// live values (nested folders ok) + defaults
|
|
6
|
+
// │
|
|
7
|
+
// ▼
|
|
8
|
+
// buildDecision ── finalized (winner key)
|
|
9
|
+
// ├── values (dot-path flattened final params to inline)
|
|
10
|
+
// ├── overridesFromDefault (only-changed; the taste signal)
|
|
11
|
+
// └── prune (every registry key except the winner)
|
|
12
|
+
//
|
|
13
|
+
// Decision schema 2: values/overrides are dot-path flattened ("surface.radius": 12)
|
|
14
|
+
// so folder-grouped configurations produce flat, inlineable decisions.
|
|
15
|
+
|
|
16
|
+
export type ParamValue = number | string | boolean
|
|
17
|
+
|
|
18
|
+
// A control-output object (spring, easing) — carries a string `type` and is treated
|
|
19
|
+
// as a single leaf value. Plain objects WITHOUT `type` are folders and get flattened.
|
|
20
|
+
export interface ControlObject {
|
|
21
|
+
type: string
|
|
22
|
+
[key: string]: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type LeafValue = ParamValue | ControlObject
|
|
26
|
+
// Loose on purpose: accepts both resolved DialKit values and raw config objects
|
|
27
|
+
// (defaultsFromConfig walks configs, which contain slider tuples, selects, actions).
|
|
28
|
+
export type NestedValues = Record<string, unknown>
|
|
29
|
+
|
|
30
|
+
export interface Override {
|
|
31
|
+
from: LeafValue
|
|
32
|
+
to: LeafValue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Decision {
|
|
36
|
+
schema: 2
|
|
37
|
+
component: string
|
|
38
|
+
finalized: string
|
|
39
|
+
values: Record<string, LeafValue>
|
|
40
|
+
overridesFromDefault: Record<string, Override>
|
|
41
|
+
prune: string[]
|
|
42
|
+
note: string
|
|
43
|
+
status: 'pending'
|
|
44
|
+
timestamp: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Keys present in the DialKit values object that are NOT design params.
|
|
48
|
+
const RESERVED = new Set(['variant', 'finalize'])
|
|
49
|
+
|
|
50
|
+
const HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
|
51
|
+
|
|
52
|
+
function normalizeHex(s: string): string {
|
|
53
|
+
let h = s.toLowerCase()
|
|
54
|
+
if (h.length === 4) h = '#' + h[1] + h[1] + h[2] + h[2] + h[3] + h[3]
|
|
55
|
+
return h
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isControlObject(v: unknown): v is ControlObject {
|
|
59
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && typeof (v as ControlObject).type === 'string'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isFolder(v: unknown): v is NestedValues {
|
|
63
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && !isControlObject(v)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Equality that treats #FFF and #ffffff as the same color. Control objects (spring,
|
|
67
|
+
// easing) compare on the keys present in `a` (the default) so library-filled extras
|
|
68
|
+
// in the live value don't read as user overrides. Everything else strict.
|
|
69
|
+
export function valuesEqual(a: LeafValue, b: LeafValue): boolean {
|
|
70
|
+
if (typeof a === 'string' && typeof b === 'string' && HEX.test(a) && HEX.test(b)) {
|
|
71
|
+
return normalizeHex(a) === normalizeHex(b)
|
|
72
|
+
}
|
|
73
|
+
if (isControlObject(a) && isControlObject(b)) {
|
|
74
|
+
for (const k of Object.keys(a)) {
|
|
75
|
+
if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) return false
|
|
76
|
+
}
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
return a === b
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Flatten nested folder values to dot-paths. Skips reserved keys (variant/finalize at
|
|
83
|
+
// any depth — folder-per-element configs nest them), config-only keys (_collapsed),
|
|
84
|
+
// and DialKit internals (__mode).
|
|
85
|
+
export function flattenValues(nested: NestedValues, prefix = ''): Record<string, LeafValue> {
|
|
86
|
+
const out: Record<string, LeafValue> = {}
|
|
87
|
+
for (const k of Object.keys(nested)) {
|
|
88
|
+
if (k.startsWith('_')) continue
|
|
89
|
+
if (k.endsWith('__mode')) continue
|
|
90
|
+
if (RESERVED.has(k)) continue
|
|
91
|
+
const v = nested[k]
|
|
92
|
+
if (v === undefined) continue
|
|
93
|
+
const path = prefix ? `${prefix}.${k}` : k
|
|
94
|
+
if (isFolder(v)) {
|
|
95
|
+
Object.assign(out, flattenValues(v as NestedValues, path))
|
|
96
|
+
} else {
|
|
97
|
+
out[path] = v as LeafValue
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Derive the frozen defaults from a DialKit config object — the reference the
|
|
104
|
+
// override diff is measured against. Walks folders; one default per control:
|
|
105
|
+
// number/string/boolean -> itself [def, min, max, step?] -> def
|
|
106
|
+
// select -> default ?? first option color/text -> default
|
|
107
|
+
// spring/easing -> the config object action -> skipped (no value)
|
|
108
|
+
export function defaultsFromConfig(config: NestedValues): NestedValues {
|
|
109
|
+
const out: NestedValues = {}
|
|
110
|
+
for (const k of Object.keys(config)) {
|
|
111
|
+
if (k.startsWith('_')) continue
|
|
112
|
+
if (RESERVED.has(k)) continue
|
|
113
|
+
const v = config[k]
|
|
114
|
+
if (v === undefined) continue
|
|
115
|
+
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
|
|
116
|
+
out[k] = v
|
|
117
|
+
} else if (Array.isArray(v)) {
|
|
118
|
+
out[k] = v[0] as ParamValue
|
|
119
|
+
} else if (isControlObject(v)) {
|
|
120
|
+
if (v.type === 'action') continue
|
|
121
|
+
if (v.type === 'select') {
|
|
122
|
+
const opts = (v as { options?: unknown[] }).options ?? []
|
|
123
|
+
const first = opts[0]
|
|
124
|
+
const firstValue = typeof first === 'object' && first !== null ? (first as { value: string }).value : (first as string)
|
|
125
|
+
out[k] = ((v as { default?: string }).default ?? firstValue) as ParamValue
|
|
126
|
+
} else if (v.type === 'color' || v.type === 'text') {
|
|
127
|
+
out[k] = ((v as { default?: string }).default ?? '') as ParamValue
|
|
128
|
+
} else {
|
|
129
|
+
out[k] = v // spring / easing / unknown control: the object IS the default
|
|
130
|
+
}
|
|
131
|
+
} else if (isFolder(v)) {
|
|
132
|
+
out[k] = defaultsFromConfig(v as NestedValues)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildDecision(
|
|
139
|
+
component: string,
|
|
140
|
+
liveValues: NestedValues,
|
|
141
|
+
defaults: NestedValues,
|
|
142
|
+
registry: Record<string, unknown>,
|
|
143
|
+
opts?: { now?: string; note?: string },
|
|
144
|
+
): Decision {
|
|
145
|
+
// No `variant` in the live values means a single-variant set (no dropdown was rendered):
|
|
146
|
+
// the winner is the registry's only key.
|
|
147
|
+
const finalized = liveValues.variant !== undefined ? String(liveValues.variant) : (Object.keys(registry)[0] ?? '')
|
|
148
|
+
|
|
149
|
+
const values = flattenValues(liveValues)
|
|
150
|
+
const flatDefaults = flattenValues(defaults)
|
|
151
|
+
|
|
152
|
+
const overridesFromDefault: Record<string, Override> = {}
|
|
153
|
+
for (const k of Object.keys(values)) {
|
|
154
|
+
if (!(k in flatDefaults)) continue
|
|
155
|
+
if (!valuesEqual(flatDefaults[k], values[k])) {
|
|
156
|
+
overridesFromDefault[k] = { from: flatDefaults[k], to: values[k] }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const prune = Object.keys(registry).filter((k) => k !== finalized)
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
schema: 2,
|
|
164
|
+
component,
|
|
165
|
+
finalized,
|
|
166
|
+
values,
|
|
167
|
+
overridesFromDefault,
|
|
168
|
+
prune,
|
|
169
|
+
note: opts?.note ?? '',
|
|
170
|
+
status: 'pending',
|
|
171
|
+
timestamp: opts?.now ?? new Date().toISOString(),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Briefly morph the element's Finalize button (e.g. to "✓ Copied"), in the PANEL only —
|
|
176
|
+
// never an overlay on the app UI. Runs from copyDecision/submitDecision so EVERY finalize
|
|
177
|
+
// gets feedback, however it was wired. DialKit renders an action as
|
|
178
|
+
// `<button class="dialkit-button">Finalize X</button>` and does not re-render on an action
|
|
179
|
+
// (no value changed), so a direct text swap sticks until we revert it.
|
|
180
|
+
function flashFinalized(component: string, text: string): void {
|
|
181
|
+
if (typeof document === 'undefined') return
|
|
182
|
+
const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>('.dialkit-root .dialkit-button'))
|
|
183
|
+
// Prefer the exact "Finalize <component>" button; fall back to the sole action button.
|
|
184
|
+
let btn = buttons.find((b) => b.textContent?.trim() === `Finalize ${component}`)
|
|
185
|
+
if (!btn && buttons.length === 1) btn = buttons[0]
|
|
186
|
+
if (!btn || btn.dataset.vkFlashing) return
|
|
187
|
+
const original = btn.textContent ?? ''
|
|
188
|
+
btn.dataset.vkFlashing = '1'
|
|
189
|
+
btn.textContent = text
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
if (btn!.dataset.vkFlashing) {
|
|
192
|
+
btn!.textContent = original
|
|
193
|
+
delete btn!.dataset.vkFlashing
|
|
194
|
+
}
|
|
195
|
+
}, 1500)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Ship the decision to the dev server (vite plugin / Next route) so it lands in
|
|
199
|
+
// .variantkit/decisions/ and the agent can apply it with zero copy-paste. Falls back to
|
|
200
|
+
// the clipboard when no transport is running. NEVER fails silently.
|
|
201
|
+
const TRANSPORT_ENDPOINTS = ['/__variantkit/decision', '/api/__variantkit/decision']
|
|
202
|
+
|
|
203
|
+
export async function submitDecision(decision: Decision): Promise<void> {
|
|
204
|
+
if (typeof fetch !== 'undefined') {
|
|
205
|
+
for (const endpoint of TRANSPORT_ENDPOINTS) {
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch(endpoint, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'content-type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(decision),
|
|
211
|
+
})
|
|
212
|
+
if (res.ok) {
|
|
213
|
+
flashFinalized(decision.component, '✓ Saved')
|
|
214
|
+
console.info(`[variantkit] decision saved to .variantkit/decisions/${decision.component}.json — tell your agent: "apply decision".`)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// endpoint not running — try the next, then the clipboard
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await copyDecision(decision)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Copy the decision JSON to the clipboard. NEVER fail silently: if the Clipboard API is
|
|
226
|
+
// unavailable (insecure context, old webview), fall back to execCommand, then to a log.
|
|
227
|
+
// Also flashes the Finalize button to "✓ Copied" (panel-side feedback, no toast/overlay).
|
|
228
|
+
export async function copyDecision(decision: Decision): Promise<void> {
|
|
229
|
+
flashFinalized(decision.component, '✓ Copied')
|
|
230
|
+
const json = JSON.stringify(decision, null, 2)
|
|
231
|
+
try {
|
|
232
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
233
|
+
await navigator.clipboard.writeText(json)
|
|
234
|
+
console.info('[variantkit] decision copied. Paste it to your agent to prune.')
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
throw new Error('clipboard API unavailable')
|
|
238
|
+
} catch {
|
|
239
|
+
fallbackCopy(json)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function fallbackCopy(json: string): void {
|
|
244
|
+
if (typeof document === 'undefined') {
|
|
245
|
+
console.warn('[variantkit] no clipboard + no DOM. Decision:\n' + json)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const ta = document.createElement('textarea')
|
|
249
|
+
ta.value = json
|
|
250
|
+
ta.style.position = 'fixed'
|
|
251
|
+
ta.style.opacity = '0'
|
|
252
|
+
document.body.appendChild(ta)
|
|
253
|
+
ta.focus()
|
|
254
|
+
ta.select()
|
|
255
|
+
let ok = false
|
|
256
|
+
try {
|
|
257
|
+
ok = document.execCommand('copy')
|
|
258
|
+
} catch {
|
|
259
|
+
ok = false
|
|
260
|
+
}
|
|
261
|
+
document.body.removeChild(ta)
|
|
262
|
+
if (ok) console.info('[variantkit] decision copied via fallback. Paste it to your agent.')
|
|
263
|
+
else console.warn('[variantkit] could not copy automatically. Decision:\n' + json)
|
|
264
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// VariantKit — panel assembly helpers. VariantKit does NOT decide what the controls are.
|
|
2
|
+
//
|
|
3
|
+
// The agent building the project authors the controls per element — contextual, specific,
|
|
4
|
+
// derived from that element's actual design axes and the project's design system. Any control
|
|
5
|
+
// DialKit supports is fair game: slider, select, toggle (boolean), color, text, spring /
|
|
6
|
+
// transition, nested folder groups. There is no preset menu and no VariantKit default value:
|
|
7
|
+
// every default comes from the project (its tokens, or the element's current values).
|
|
8
|
+
//
|
|
9
|
+
// VariantKit's only additions are structural:
|
|
10
|
+
// - a `variant` select (only when there are 2+ variants)
|
|
11
|
+
// - a `finalize` action
|
|
12
|
+
//
|
|
13
|
+
// Usage in a component's index.tsx (built on DialKit):
|
|
14
|
+
// const cfg = panelConfig(
|
|
15
|
+
// { tone: { type: 'select', options: ['quiet','bold'], default: 'quiet' }, // yours,
|
|
16
|
+
// accent: tokens.brand, compact: false }, // per element
|
|
17
|
+
// ['solid', 'outline', 'ghost'],
|
|
18
|
+
// { component: 'Button' },
|
|
19
|
+
// )
|
|
20
|
+
// const v = useDialKit('Button', cfg, { onAction: () =>
|
|
21
|
+
// copyDecision(buildDecision('Button', v, defaultsOf(cfg), regOf(['solid','outline','ghost']))) })
|
|
22
|
+
|
|
23
|
+
// Loose config shape — these are DialKit config objects; we don't import DialKit's types here
|
|
24
|
+
// so the helpers stay framework-light. `any` keeps the result assignable to DialKit's own
|
|
25
|
+
// config type at the useDialKit call site without coupling this file to DialKit.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
export type Control = any
|
|
28
|
+
export type PanelConfig = Record<string, Control>
|
|
29
|
+
|
|
30
|
+
const RESERVED = new Set(['variant', 'finalize'])
|
|
31
|
+
|
|
32
|
+
// Assemble the full DialKit config for a variant set: [variant select +] the element's own
|
|
33
|
+
// controls + a finalize action. With a single variant key, no dropdown is added — the panel
|
|
34
|
+
// is just the element's controls + finalize.
|
|
35
|
+
export function panelConfig(
|
|
36
|
+
controls: PanelConfig,
|
|
37
|
+
variantKeys: string[],
|
|
38
|
+
opts?: { finalizeLabel?: string; component?: string },
|
|
39
|
+
): PanelConfig {
|
|
40
|
+
return {
|
|
41
|
+
...(variantKeys.length > 1
|
|
42
|
+
? { variant: { type: 'select', options: variantKeys, default: variantKeys[0] } }
|
|
43
|
+
: {}),
|
|
44
|
+
...controls,
|
|
45
|
+
finalize: { type: 'action', label: opts?.finalizeLabel ?? `Finalize ${opts?.component ?? ''}`.trim() },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve the frozen default value of each control — feeds buildDecision's `defaults`.
|
|
50
|
+
export function defaultsOf(cfg: PanelConfig): Record<string, number | string | boolean> {
|
|
51
|
+
const out: Record<string, number | string | boolean> = {}
|
|
52
|
+
for (const [key, c] of Object.entries(cfg)) {
|
|
53
|
+
if (RESERVED.has(key)) continue
|
|
54
|
+
if (typeof c === 'number' || typeof c === 'string' || typeof c === 'boolean') {
|
|
55
|
+
out[key] = c
|
|
56
|
+
} else if (Array.isArray(c)) {
|
|
57
|
+
out[key] = c[0] as number
|
|
58
|
+
} else if (c && typeof c === 'object') {
|
|
59
|
+
const o = c as { type?: string; default?: unknown }
|
|
60
|
+
if (o.type === 'action') continue
|
|
61
|
+
if (o.default !== undefined) out[key] = o.default as number | string | boolean
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Convenience: registry object from variant keys, for buildDecision's prune computation.
|
|
68
|
+
export function regOf(variantKeys: string[]): Record<string, true> {
|
|
69
|
+
return Object.fromEntries(variantKeys.map((k) => [k, true]))
|
|
70
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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".
|
|
5
|
+
- Remove folder/header dividers so the panel reads on spacing alone, like DialKit's own UI. */
|
|
6
|
+
.dialkit-root [title='Copy parameters'] {
|
|
7
|
+
display: none !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* DialKit ships the expanded panel with zero bottom padding (10px 12px 0 12px), so the last
|
|
11
|
+
control — usually Finalize — sits flush against the edge. Match the sides. The collapsed
|
|
12
|
+
bubble sets its own uniform 12px, so this only affects the open panel. */
|
|
13
|
+
.dialkit-root .dialkit-panel-inner:not([data-collapsed='true']) {
|
|
14
|
+
padding-bottom: 12px;
|
|
15
|
+
}
|
|
16
|
+
.dialkit-root .dialkit-panel-header {
|
|
17
|
+
border-bottom: none !important;
|
|
18
|
+
}
|
|
19
|
+
.dialkit-root .dialkit-folder {
|
|
20
|
+
border-top: none !important;
|
|
21
|
+
border-bottom: none !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The injected panel theme toggle — placed just left of DialKit's settings icon (right:12). */
|
|
25
|
+
.dialkit-root .vk-theme-toggle {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 6px;
|
|
28
|
+
right: 40px;
|
|
29
|
+
width: 26px;
|
|
30
|
+
height: 26px;
|
|
31
|
+
display: inline-grid;
|
|
32
|
+
place-items: center;
|
|
33
|
+
border: none;
|
|
34
|
+
background: transparent;
|
|
35
|
+
border-radius: 7px;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
color: var(--dial-text-secondary);
|
|
38
|
+
z-index: 2;
|
|
39
|
+
transition: background-color 140ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
40
|
+
}
|
|
41
|
+
.dialkit-root .vk-theme-toggle:hover {
|
|
42
|
+
background: var(--dial-surface-hover);
|
|
43
|
+
}
|
|
44
|
+
.dialkit-root .vk-theme-toggle:active {
|
|
45
|
+
transform: scale(0.92);
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Dark theme for DialKit's panel — cool, near-black, crisp (matches DialKit's own dark mode).
|
|
2
|
+
DialKit ships a light palette only and reads CSS vars on .dialkit-root[data-theme]; this
|
|
3
|
+
supplies the dark set. Set data-theme="dark" on .dialkit-root to activate. */
|
|
4
|
+
.dialkit-root[data-theme='dark'] {
|
|
5
|
+
--dial-glass-bg: #0c0c0d;
|
|
6
|
+
--dial-dropdown-bg: #1b1b1e;
|
|
7
|
+
|
|
8
|
+
--dial-surface: rgba(255, 255, 255, 0.055);
|
|
9
|
+
--dial-surface-hover: rgba(255, 255, 255, 0.09);
|
|
10
|
+
--dial-surface-active: rgba(255, 255, 255, 0.14);
|
|
11
|
+
--dial-surface-subtle: rgba(255, 255, 255, 0.04);
|
|
12
|
+
|
|
13
|
+
--dial-text-root: #fafafa;
|
|
14
|
+
--dial-text-section: rgba(255, 255, 255, 0.92);
|
|
15
|
+
--dial-text-label: rgba(255, 255, 255, 0.62);
|
|
16
|
+
--dial-text-focus: #ffffff;
|
|
17
|
+
--dial-text-primary: rgba(255, 255, 255, 0.95);
|
|
18
|
+
--dial-text-secondary: rgba(255, 255, 255, 0.55);
|
|
19
|
+
--dial-text-tertiary: rgba(255, 255, 255, 0.34);
|
|
20
|
+
|
|
21
|
+
--dial-border: rgba(255, 255, 255, 0.08);
|
|
22
|
+
--dial-border-hover: rgba(255, 255, 255, 0.16);
|
|
23
|
+
|
|
24
|
+
--dial-shadow: 0 16px 50px rgba(0, 0, 0, 0.6);
|
|
25
|
+
--dial-shadow-collapsed: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
26
|
+
--dial-shadow-dropdown: 0 12px 32px rgba(0, 0, 0, 0.55);
|
|
27
|
+
}
|