configurePanel
Multi-instance init, the PanelConfig shape with tabs-based manifest, PanelInstanceHandle, applySink, and the lifecycle helpers (show / hide / toggle / reapply).
The package’s entire portable contract enters through a single setup function.
configurePanel({...}) supports multiple independent panel instances — call it with distinct storagePrefix values to register separate instances.
Each call returns a PanelInstanceHandle.
This page pins the public shape of PanelConfig, the configurePanel call, PanelInstanceHandle, the applySink contract, and the runtime lifecycle helpers.
configurePanel(config)
import { configurePanel } from '@takazudo/zdtp';
const handle = configurePanel({
storagePrefix: 'myapp-design-token-panel',
consoleNamespace: 'myapp',
modalClassPrefix: 'myapp-design-token-panel-modal',
schemaId: 'zudo-design-tokens/v2',
exportFilenameBase: 'myapp-design-tokens',
tabs: [
{
id: 'spacing',
label: 'Spacing',
tiers: [
{
id: 'base',
label: 'Base',
items: [
{ id: 'sp-md', cssVar: '--myapp-spacing-md', label: 'MD', default: '1rem',
type: { kind: 'length', step: 0.25, unit: 'rem' } },
],
},
],
},
// additional tabs...
],
applyEndpoint: 'http://localhost:4321/_dev/apply-tokens',
applyRouting: {
'myapp-spacing': 'src/styles/spacing.css',
},
});
// handle.instanceId === 'myapp-design-token-panel'
// handle.open() / close() / toggle() / destroy()
Required behaviours
- Multi-instance. Calling
configurePanelwith a distinctstoragePrefixregisters a new independent instance — independent storage keys, DOM root, toggle event, and apply target. No throw. - Idempotent for same prefix+config. Calling
configurePanelagain with the samestoragePrefixand structurally-equal values is a silent no-op that returns the same handle. This covers Astro view-transition reruns that re-parse the inline JSON config. - Same-prefix-different-config THROWS. If an instance for the prefix already exists but the config differs,
configurePanelthrows immediately. Callhandle.destroy()first, thenconfigurePanelagain to re-configure. - Synchronous. No I/O, no awaits. Safe to call inline at module-init time.
- Pure data only (except
applySink). Every field onPanelConfigother thanapplySinkMUST be JSON-serializable. The Astro frontmatter → island prop handoff stringifies the config; function fields silently disappear under that round-trip.applySinkcarries function references and must not be included in the Astro inline JSON config.
⚠️ No defaults — explicit configure required
Importing the package without calling configurePanel(...) first leaves the panel in a minimal stub state with an empty tabs array. The panel will render “no tokens configured”. Hosts MUST call configurePanel(...) to see useful behaviour.
PanelInstanceHandle
export interface PanelInstanceHandle {
/** Stable instance id — equal to the instance's storagePrefix. */
readonly instanceId: string;
/** Show this instance's panel. */
open(): void;
/** Hide this instance's panel. */
close(): void;
/** Toggle this instance's panel open/closed. */
toggle(): void;
/**
* Deregister this instance. Unmounts the Preact tree, removes the DOM
* root, and unbinds the instance's toggle-event listener. After
* destroy() the prefix can be re-configured via configurePanel.
*/
destroy(): void;
}
Two configurePanel calls with the same storagePrefix and equal config return the same handle (referential identity is preserved across idempotent re-calls). Distinct prefixes return distinct handles.
PanelConfig interface
export interface PanelConfig {
/** Base for every derived storage key. Also the instance id. */
storagePrefix: string;
/** Console API namespace — installed as `window[consoleNamespace].showDesignPanel`, etc. */
consoleNamespace: string;
/** BEM-style prefix used by every modal in the panel (export / import / apply). */
modalClassPrefix: string;
/** `$schema` value emitted into export JSON and required on import. */
schemaId: string;
/** Default filename base — exports save as `${exportFilenameBase}.json`. */
exportFilenameBase: string;
/**
* Optional window-event name that toggles THIS instance's panel.
* The default (single-panel) instance keeps `toggle-design-token-panel`.
* Any other instance defaults to `toggle-${storagePrefix}` when this field
* is omitted, giving each instance its own independent toggle channel.
*/
toggleEvent?: string;
/**
* Host-supplied tab configuration. Required.
* The panel renders its tab strip from this array.
* - Reserved ids ('color', 'color-secondary') dispatch to built-in color tab
* components. Every other id dispatches to GenericTab, which renders the
* tab's tiers using kind-appropriate editors.
*/
tabs: readonly TabConfig[];
/**
* Optional host-supplied color-scheme presets. Surfaces additional named
* ColorScheme entries in the Color tab "Scheme..." dropdown. Defaults to {}.
*/
colorPresets?: Record<string, ColorScheme>;
/**
* Optional dev-API endpoint URL. When set, the Apply button POSTs its diff payload to it.
* When undefined, the Apply button stays disabled with a tooltip.
*/
applyEndpoint?: string;
/**
* Optional CSS-var prefix → repo-relative source-file routing map.
* Apply is gated on applyEndpoint AND a non-empty routing map.
*/
applyRouting?: Record<string, string>;
/**
* Optional apply sink. Routes this instance's CSS-var writes off :root.
* Not JSON-serializable — supply via a custom adapter, not inline Astro config.
* See the applySink section below.
*/
applySink?: ApplySink;
/**
* Optional id rename map applied during loadPersistedState migration.
* Keys are old ids found in persisted state; values are:
* - string — new canonical id; the value moves to that key.
* - null — drop the id entirely.
* Defaults to an empty map (no renaming, no drops).
*/
legacyIdRenameMap?: Record<string, string | null>;
}
Field reference
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| storagePrefix | string | yes | Drives every persisted localStorage key. Also the instance id. |
| consoleNamespace | string | yes | Globals installed under window[consoleNamespace]. |
| modalClassPrefix | string | yes | BEM root for every modal the panel owns. |
| schemaId | string | yes | Emitted as $schema on exports; required on import. |
| exportFilenameBase | string | yes | Saves as ${exportFilenameBase}.json. |
| toggleEvent | string | no | Toggle-event name for this instance. Default: toggle-${storagePrefix} (non-default instances); the default instance uses toggle-design-token-panel. |
| tabs | <code>TabConfig[]</code> | yes | Tab strip definition. The Color tab reads its data from the tier model via colorExtras + TierItem[]. |
| colorPresets | Record<string, ColorScheme> | no | Host-supplied scheme presets — see Color cluster. |
| applyEndpoint | string | no | Apply POST target — see Apply pipeline. |
| applyRouting | Record<string, string> | no | Apply routing map — see Apply pipeline. |
| applySink | ApplySink | no | Optional CSS-var write target. Not JSON-serializable. See below. |
| legacyIdRenameMap | Record<string, string \| null> | no | Migration map for persisted id renames and drops. |
ℹ️ JSON-serializable invariant
Every field on PanelConfig (including nested tabs content) other than applySink must round-trip through JSON.stringify without loss. The Astro host-adapter parses the inline <script type="application/json"> payload on every page; function fields, class instances, and Symbol keys silently disappear and surface later as cryptic runtime errors.
applySink — optional CSS-var write target
When PanelConfig.applySink is supplied, all CSS-var writes and clears for that instance route through the sink rather than document.documentElement. This enables embedding the panel in a shadow root, iframe document, or test spy without touching :root.
export interface ApplySink {
/** Upsert the given var name→value pairs on the sink target. */
apply(pairs: ReadonlyArray<readonly [string, string]>): void;
/** Remove the given var names from the sink target. */
clear(names: readonly string[]): void;
}
Contract:
apply(pairs)— upsert: set each[name, value]CSS var on the sink target.clear(names)— remove: remove each named CSS var from the sink target.- Reset sends the full token set. When the user clicks Reset,
sink.clearreceives every var the instance can own (all palette, base-role, semantic, and non-color tab vars) — not just dirty vars — so the sink target is completely cleaned. - Default (no sink): writes go to
document.documentElement(unchanged behaviour). - Sink errors are non-fatal. The pipeline swallows them with
console.warnand continues. - The host owns the sink target’s lifecycle. Keep the target alive as long as the instance is alive.
- Not JSON-serializable. Supply it by constructing the config object with the sink already attached, or via a custom adapter that calls
configurePaneldirectly.
// Example: routing a panel instance to a shadow root
const shadow = shadowHost.attachShadow({ mode: 'open' });
const handle = configurePanel({
storagePrefix: 'myapp-shadow-panel',
// ...other required fields...
applySink: {
apply(pairs) {
for (const [name, value] of pairs) {
(shadow.host as HTMLElement).style.setProperty(name, value);
}
},
clear(names) {
for (const name of names) {
(shadow.host as HTMLElement).style.removeProperty(name);
}
},
},
});
Per-instance toggle events
Each panel instance has its own toggle-event channel so multiple panels on one page do not cross-talk.
| Instance | toggleEvent field | Effective toggle-event name |
| --- | --- | --- |
| Default (single-panel path) | (ignored) | toggle-design-token-panel |
| Any non-default, field omitted | — | toggle-${storagePrefix} |
| Any non-default, field supplied | a custom string | the supplied string |
The default instance is whichever instance’s storagePrefix equals the historical package default ('zudo-design-token-panel'). All other instances get per-prefix channels.
// Dispatch the toggle event for a non-default instance:
window.dispatchEvent(new CustomEvent('toggle-myapp-preview-panel'));
// or, if you supplied toggleEvent: 'my-custom-event':
window.dispatchEvent(new CustomEvent('my-custom-event'));
Storage-key derivation
storagePrefix is the only knob that controls every persisted key. The panel derives the keys at runtime from this single base.
| Logical key | Derivation | Purpose |
| --- | --- | --- |
| state-v3 | ${storagePrefix}-state-v3 | Unified envelope: color + spacing + typography + size + tabs (for generic host tabs) + optional secondary cluster slice. |
| state-v2 | ${storagePrefix}-state-v2 | v2 envelope — migrated to v3 on first load, then deleted. |
| state-v1 | ${storagePrefix}-state | Pre-v2 flat-state format (Color-only). Migrated to v2 on first load, then deleted. |
| open | ${storagePrefix}-open | Mirror of the panel’s open boolean state. |
| position | ${storagePrefix}-position | Drag position { top, right } so the panel reappears where the user left it. |
| visible | ${storagePrefix}:visible | Adapter-level visibility-intent flag. |
📝 Colon, not dash, for `visible`
The visible key uses a : separator; every other derived key uses -. This is a historical artifact preserved for storage-key continuity.
legacyIdRenameMap migration
loadPersistedState applies legacyIdRenameMap during the migration pass before the panel renders. Old item ids are moved to their new canonical ids; ids mapped to null are dropped entirely so stale localStorage entries don’t accumulate.
Hosts whose item ids are stable (no historical renames) can omit this field — the default is an empty map (no renaming, no drops).
ZDTP_LEGACY_TYPOGRAPHY_RENAME_MAP
import { ZDTP_LEGACY_TYPOGRAPHY_RENAME_MAP } from '@takazudo/zdtp';
configurePanel({
// ...
legacyIdRenameMap: ZDTP_LEGACY_TYPOGRAPHY_RENAME_MAP,
});
ZDTP_LEGACY_TYPOGRAPHY_RENAME_MAP is the historical internal rename map that was applied automatically in versions prior to the legacyIdRenameMap field. It is now exported for opt-in callers whose persisted state predates that version. Hosts that never relied on the historical internal renames should leave legacyIdRenameMap undefined (or pass an empty object).
Persisted state / external SerDe
The panel exports two primitives for hosts that need to read or write panel state outside the panel itself — for example, synchronising token overrides across tabs or building a custom import/export flow.
TweakState
import type { TweakState } from '@takazudo/zdtp';
TweakState is the TypeScript type of the serialisable override state that the panel persists in localStorage and embeds in JSON exports. Import it when building typed SerDe utilities that consume or produce panel state.
emptyOverrides
import { emptyOverrides } from '@takazudo/zdtp';
emptyOverrides is a ready-made TweakState value with all override maps empty. Use it as a starting point when constructing a fresh state object or when resetting overrides programmatically.
Complete example
import { configurePanel } from '@takazudo/zdtp';
import type { TabConfig } from '@takazudo/zdtp';
const colorTab: TabConfig = {
id: 'color',
label: 'Color',
tiers: [
{
id: 'palette',
label: 'Palette',
items: [
{ id: 'p0', cssVar: '--myapp-p0', label: 'P0', default: '#0f172a', type: { kind: 'color' } },
{ id: 'p1', cssVar: '--myapp-p1', label: 'P1', default: '#38bdf8', type: { kind: 'color' } },
],
},
{
id: 'semantic',
label: 'Semantic',
referencesTier: 'palette',
items: [
{ id: 'bg', cssVar: '--myapp-color-bg', label: 'Background', default: 'p0', type: { kind: 'color' } },
{ id: 'accent', cssVar: '--myapp-color-accent', label: 'Accent', default: 'p1', type: { kind: 'color' } },
],
},
],
colorExtras: {
id: 'myapp',
baseRoles: { background: '--myapp-p0', foreground: '--myapp-p1' },
baseDefaults: { background: 0, foreground: 1 },
defaultShikiTheme: 'github-dark',
colorSchemes: {
'Default Dark': {
background: 0, foreground: 1, cursor: 1, selectionBg: 0, selectionFg: 1,
palette: ['#0f172a', '#38bdf8'],
shikiTheme: 'github-dark',
},
},
panelSettings: { colorScheme: 'Default Dark', colorMode: false },
},
};
const spacingTab: TabConfig = {
id: 'spacing',
label: 'Spacing',
tiers: [
{
id: 'base',
label: 'Base spacing',
items: [
{ id: 'sp-sm', cssVar: '--myapp-spacing-sm', label: 'SM', default: '0.5rem', type: { kind: 'length', step: 0.125, unit: 'rem' } },
{ id: 'sp-md', cssVar: '--myapp-spacing-md', label: 'MD', default: '1rem', type: { kind: 'length', step: 0.25, unit: 'rem' } },
],
},
],
};
const handle = configurePanel({
storagePrefix: 'myapp-design-token-panel',
consoleNamespace: 'myapp',
modalClassPrefix: 'myapp-design-token-panel-modal',
schemaId: 'zudo-design-tokens/v2',
exportFilenameBase: 'myapp-design-tokens',
tabs: [colorTab, spacingTab],
applyEndpoint: 'http://localhost:4321/_dev/apply-tokens',
applyRouting: {
'myapp-p': 'src/styles/color.css',
'myapp-color': 'src/styles/color.css',
'myapp-spacing': 'src/styles/spacing.css',
},
});
// handle.instanceId === 'myapp-design-token-panel'
Lifecycle helpers
The package exposes four runtime helpers from its root entry. They are normally invoked via the console namespace (the host adapter installs window[consoleNamespace].showDesignPanel etc.), but a Vite-only host can import them directly.
import {
showDesignTokenPanel,
hideDesignTokenPanel,
toggleDesignPanel,
reapplyPersistedOverrides,
} from '@takazudo/zdtp';
showDesignTokenPanel(): void
Opens the panel. Idempotent — calling it while the panel is already open is a no-op. Mounts the Preact shell into a body-appended <div> whose id is derived from storagePrefix.
hideDesignTokenPanel(): void
Closes the panel. The Preact shell stays mounted (CSS hides it); only the open flag flips.
toggleDesignPanel(): void
Flips the panel between open and closed. The exported function name is toggleDesignPanel (not toggleDesignTokenPanel); the console-API helper name matches.
reapplyPersistedOverrides(): void
Applies persisted token overrides directly to :root BEFORE any Preact render. Called at adapter module init (and again on every astro:page-load) so the bundle’s arrival eliminates the FOUT on hard navigation. This is a no-op when nothing is persisted; it swallows errors so corrupt state never blocks the UI thread.
💡 Console API
The host adapter installs lazy-import wrappers under window[consoleNamespace]:
window[consoleNamespace].showDesignPanel = () => Promise<void>;
window[consoleNamespace].hideDesignPanel = () => Promise<void>;
window[consoleNamespace].toggleDesignPanel = () => Promise<void>;Each helper lazy-imports the adapter module and forwards to its corresponding synchronous public function.
setLifecycleAdapter(adapter)
import { setLifecycleAdapter } from '@takazudo/zdtp';
setLifecycleAdapter({
onBeforeSwap: (cb) => { /* register a before-swap hook */ },
onPageLoad: (cb) => { /* register a page-load hook */ },
});
Registers lifecycle hooks for non-Astro hosts (Vite, plain HTML, Next.js, etc.). The Astro host-adapter wires these automatically via astro:before-swap and astro:page-load events; non-Astro hosts must call setLifecycleAdapter before the panel dynamically loads to ensure reapplyPersistedOverrides fires on each navigation.
Call setLifecycleAdapter after configurePanel and before the panel is first shown. Calling it more than once replaces the previous adapter.
setPanelColorPresets(presets)
import { setPanelColorPresets } from '@takazudo/zdtp';
setPanelColorPresets({
Dracula: { /* ColorScheme */ },
Solarized: { /* ColorScheme */ },
});
Lazy preset attachment. Hosts that don’t want to ship the preset library inline in the SSR config blob can call this AFTER configurePanel from a deferred dynamic import. Same merge rules as PanelConfig.colorPresets — see Color cluster for the full merge contract.
The trailing call wins on conflict (no throw, unlike configurePanel); a host that pre-calls setPanelColorPresets before configurePanel is serviced via a holding slot inside the panel-config module.