@westopp/windo 0.1.1 → 0.1.2

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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/define-config.ts","../src/descriptor.ts","../src/protocol.ts","../src/types.ts"],"sourcesContent":["// The authoring API. `defineWindoConfig` is the single entry point: it captures\n// the project's groups + contexts and hands back a `windo` factory whose `group`\n// field is type-checked against the configured slugs.\n\nimport type { ComponentType, ReactNode } from 'react'\nimport type { z } from 'zod'\nimport type { WindoConfig, WindoContextDefinition, WindoContextMap, WindoControlMap, WindoControlValues, WindoDefinition, WindoFactoryArg, WindoGroup, WindoModule, WindoRenderContext } from './types'\n\nexport interface DefineWindoConfigResult<Groups extends readonly WindoGroup[], Tags extends readonly string[], Contexts extends WindoContextMap> {\n config: WindoConfig<Groups, Contexts, Tags>\n windo: <Props, State = Record<string, never>>(\n factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>\n ) => WindoModule<Props, State>\n}\n\nexport function defineWindoConfig<const Groups extends readonly WindoGroup[], const Tags extends readonly string[] = readonly [], Contexts extends WindoContextMap = Record<string, never>>(\n config: WindoConfig<Groups, Contexts, Tags>\n): DefineWindoConfigResult<Groups, Tags, Contexts> {\n function windo<Props, State = Record<string, never>>(\n factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>\n ): WindoModule<Props, State> {\n return {\n __windo: true,\n resolve: w => factory(w as unknown as WindoFactoryArg<Groups, Contexts, Tags>),\n }\n }\n return { config, windo }\n}\n\n/**\n * Define a named context. A context can contribute ambient `controls` (values +\n * UI), a `provider` (mounted inside the iframe for components that opt in via\n * `uses`), or both.\n *\n * No cast: `def` is already structurally a `WindoContextDefinition<WindoControlValues<C>,\n * Provided>` (only `controls` widens, covariantly). The unavoidable variance — TS has\n * no existential type to hold contexts heterogeneous in `C` — is absorbed once, in\n * `WindoContextMap`, not here and not at call sites.\n */\nexport function defineContext<C extends WindoControlMap, Provided = WindoControlValues<C>>(def: {\n label?: string\n description?: string\n controls?: C\n provider?: ComponentType<{ children: ReactNode; values: WindoControlValues<C>; ctx: WindoRenderContext }>\n resolve?: (values: WindoControlValues<C>, ctx: WindoRenderContext) => Provided\n}): WindoContextDefinition<WindoControlValues<C>, Provided> {\n return def\n}\n\n/**\n * Bind a zod schema to a component's props. The generic carries the component's\n * prop type so the schema's output stays a subset of it; the returned schema is\n * the runtime validator + parser (`z.input` is the JSON edit surface, `z.output`\n * is what the component receives).\n */\nexport function configurableProps<P>() {\n return <S extends z.ZodType<Partial<P>>>(schema: S): S => schema\n}\n","// Walk a zod schema into a serialisable descriptor that can cross the iframe\n// boundary and render the Controls/Schema UI. We lean on `z.toJSONSchema` (zod\n// v4) for the input shape (enum options, min/max, optionality), then enrich the\n// kind from the zod node's own type tag — some types (Date, Map, Set) are\n// unrepresentable in JSON Schema and come back as `{}`, but their zod `def.type`\n// is exact. The live schema (with transforms/coerce/refine) stays in the iframe\n// and does the actual parsing — only this descriptor travels.\n\nimport { z } from 'zod'\nimport type { WindoControlDescriptor, WindoControlKind, WindoSchemaDescriptor } from './types'\n\ninterface JsonNode {\n type?: string | string[]\n enum?: unknown[]\n const?: unknown\n format?: string\n properties?: Record<string, JsonNode>\n required?: string[]\n items?: JsonNode\n anyOf?: JsonNode[]\n oneOf?: JsonNode[]\n allOf?: JsonNode[]\n minimum?: number\n maximum?: number\n description?: string\n}\n\nexport function describeSchema(schema: z.ZodType | undefined | null): WindoSchemaDescriptor {\n if (!schema) return { fields: [] }\n let json: JsonNode\n try {\n json = z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' }) as JsonNode\n } catch {\n return { fields: [] }\n }\n const root = unwrap(json)\n const properties = root.properties ?? {}\n const required = new Set(root.required ?? [])\n const zodKinds = shapeKinds(schema)\n const fields: WindoControlDescriptor[] = Object.keys(properties).map(key => fieldFrom(key, properties[key], required.has(key), zodKinds[key]))\n return { fields }\n}\n\n// JSON Schema often wraps the object in anyOf (for optional/nullable). Find the\n// node that actually carries the object's properties.\nfunction unwrap(node: JsonNode): JsonNode {\n if (node.properties) return node\n const branches = node.anyOf ?? node.oneOf ?? node.allOf\n if (branches) {\n const withProps = branches.find(b => b.properties)\n if (withProps) return withProps\n }\n return node\n}\n\nfunction fieldFrom(key: string, node: JsonNode, isRequired: boolean, zodKind?: WindoControlKind): WindoControlDescriptor {\n const inner = collapseNullable(node)\n let kind = kindOf(inner)\n // The zod tag wins for types JSON Schema can't express (date/array/object that\n // came back as {}), and as a fallback whenever the JSON kind is unknown.\n if (zodKind && (kind === 'unknown' || zodKind === 'date')) kind = zodKind\n const descriptor: WindoControlDescriptor = {\n key,\n kind,\n optional: !isRequired || isNullable(node),\n }\n if (inner.description) descriptor.description = inner.description\n const options = enumOptions(inner)\n if (options) descriptor.options = options\n if (typeof inner.minimum === 'number') descriptor.min = inner.minimum\n if (typeof inner.maximum === 'number') descriptor.max = inner.maximum\n return descriptor\n}\n\nfunction isNullable(node: JsonNode): boolean {\n const branches = node.anyOf ?? node.oneOf\n if (!branches) return false\n return branches.some(b => b.type === 'null')\n}\n\n// Strip a `{ anyOf: [T, null] }` wrapper down to T.\nfunction collapseNullable(node: JsonNode): JsonNode {\n const branches = node.anyOf ?? node.oneOf\n if (!branches) return node\n const nonNull = branches.filter(b => b.type !== 'null')\n if (nonNull.length === 1) return nonNull[0]\n return node\n}\n\nfunction enumOptions(node: JsonNode): string[] | undefined {\n if (Array.isArray(node.enum)) return node.enum.map(v => String(v))\n const branches = node.anyOf ?? node.oneOf\n if (branches?.every(b => b.const !== undefined)) {\n return branches.map(b => String(b.const))\n }\n return undefined\n}\n\nfunction kindOf(node: JsonNode): WindoControlKind {\n if (Array.isArray(node.enum) || (node.anyOf?.every(b => b.const !== undefined) ?? false)) return 'enum'\n if (node.format === 'date-time' || node.format === 'date') return 'date'\n const type = Array.isArray(node.type) ? node.type.find(t => t !== 'null') : node.type\n switch (type) {\n case 'string':\n return 'string'\n case 'number':\n case 'integer':\n return 'number'\n case 'boolean':\n return 'boolean'\n case 'array':\n return 'array'\n case 'object':\n return 'object'\n default:\n return 'unknown'\n }\n}\n\n/* ------------------------------------------------------------------ *\n * zod node introspection (the kind source of truth for unrepresentable types)\n * ------------------------------------------------------------------ */\n\ninterface ZodDefLike {\n type?: string\n innerType?: unknown\n}\n\nfunction zodDef(node: unknown): ZodDefLike | undefined {\n const n = node as { def?: ZodDefLike; _zod?: { def?: ZodDefLike } } | null\n if (!n || typeof n !== 'object') return undefined\n return n.def ?? n._zod?.def\n}\n\nconst ZOD_WRAPPERS = new Set(['optional', 'nullable', 'default', 'prefault', 'catch', 'readonly', 'nonoptional', 'lazy'])\n\n// Unwrap optional/nullable/default/... down to the meaningful inner node.\nfunction unwrapZodDef(node: unknown): ZodDefLike | undefined {\n let def = zodDef(node)\n let guard = 0\n while (def && def.innerType && ZOD_WRAPPERS.has(def.type ?? '') && guard++ < 16) {\n def = zodDef(def.innerType)\n }\n return def\n}\n\nfunction mapZodType(type: string | undefined): WindoControlKind | undefined {\n switch (type) {\n case 'date':\n return 'date'\n case 'string':\n return 'string'\n case 'number':\n case 'int':\n case 'bigint':\n return 'number'\n case 'boolean':\n return 'boolean'\n case 'array':\n case 'set':\n case 'tuple':\n return 'array'\n case 'object':\n case 'record':\n case 'map':\n return 'object'\n case 'enum':\n case 'literal':\n return 'enum'\n default:\n return undefined\n }\n}\n\n// Per-field kind read straight from the zod object's shape.\nfunction shapeKinds(schema: z.ZodType): Record<string, WindoControlKind> {\n const shape = (schema as { shape?: Record<string, unknown> }).shape\n if (!shape || typeof shape !== 'object') return {}\n const out: Record<string, WindoControlKind> = {}\n for (const key of Object.keys(shape)) {\n const kind = mapZodType(unwrapZodDef(shape[key])?.type)\n if (kind) out[key] = kind\n }\n return out\n}\n","// The chrome <-> iframe postMessage contract. The schema itself never crosses\n// the boundary: the iframe walks it into a serialisable descriptor, the chrome\n// sends candidate JSON down, the iframe parses and reports back. Every payload\n// here is plain JSON.\n\nimport type { WindoContextMeta, WindoEnvState, WindoFieldError, WindoGroup, WindoLogEntry, WindoManifestEntry, WindoPropDoc, WindoSchemaDescriptor, WindoVariantMeta } from './types'\n\nexport const WINDO_MSG = 'windo'\n\n/** chrome -> iframe */\nexport type WindoHostMessage =\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'request-manifest' }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'select'; id: string }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-props'; id: string; json: string }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-env'; env: WindoEnvState }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'invoke-action'; id: string; actionId: string }\n\n/** iframe -> chrome */\nexport type WindoPreviewMessage =\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'ready' }\n | {\n source: typeof WINDO_MSG\n dir: 'preview'\n type: 'manifest'\n title: string\n entries: WindoManifestEntry[]\n groups: WindoGroup[]\n tags: string[]\n contexts: WindoContextMeta[]\n }\n | {\n source: typeof WINDO_MSG\n dir: 'preview'\n type: 'describe'\n id: string\n descriptor: WindoSchemaDescriptor\n props: WindoPropDoc[]\n variants: WindoVariantMeta[]\n defaults: unknown\n code: string | null\n }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-ok'; id: string }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-error'; id: string; errors: WindoFieldError[] }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'log'; entry: WindoLogEntry }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'state'; id: string; state: Record<string, unknown>; actions: { id: string; disabled: boolean }[] }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'render-error'; id: string; message: string; stack?: string }\n\nexport type WindoMessage = WindoHostMessage | WindoPreviewMessage\n\nexport function isWindoMessage(data: unknown): data is WindoMessage {\n return typeof data === 'object' && data !== null && (data as { source?: unknown }).source === WINDO_MSG\n}\n\nexport function isHostMessage(msg: WindoMessage): msg is WindoHostMessage {\n return msg.dir === 'host'\n}\n\nexport function isPreviewMessage(msg: WindoMessage): msg is WindoPreviewMessage {\n return msg.dir === 'preview'\n}\n","// Core type system for windo. Everything — the authoring API, the iframe\n// preview runtime, the chrome UI, and the postMessage protocol — is typed\n// against the contracts in this file.\n\nimport type { ComponentType, ReactNode } from 'react'\nimport type { z } from 'zod'\n\n/* ------------------------------------------------------------------ *\n * Primitives\n * ------------------------------------------------------------------ */\n\nexport type WindoStatus = 'stable' | 'beta' | 'deprecated'\n\nexport const WINDO_STATUSES: readonly WindoStatus[] = ['stable', 'beta', 'deprecated']\n\n/** Anchor a component within the canvas frame. */\nexport type WindoPlacementBase = 'center' | 'fill' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\n/**\n * Where a component renders inside the canvas frame. Placements render flush by\n * default; append `-padding` to inset the component from the frame edges\n * (e.g. `top` sits flush at the top, `top-padding` adds breathing room).\n */\nexport type WindoPlacement = WindoPlacementBase | `${WindoPlacementBase}-padding`\n\nconst WINDO_PLACEMENT_BASE: readonly WindoPlacementBase[] = ['center', 'fill', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right']\n\nexport const WINDO_PLACEMENTS: readonly WindoPlacement[] = [...WINDO_PLACEMENT_BASE, ...WINDO_PLACEMENT_BASE.map(p => `${p}-padding` as WindoPlacement)]\n\n/** A configured group. Components reference a group by its `slug`. */\nexport interface WindoGroup {\n name: string\n slug: string\n description?: string\n}\n\n/* ------------------------------------------------------------------ *\n * Context system\n * ------------------------------------------------------------------ */\n\nexport type WindoControlType = 'enum' | 'boolean' | 'string' | 'number'\n\n/** A single ambient control: a value plus the metadata to render its toggle. */\nexport interface WindoControlSpec<T = unknown> {\n type: WindoControlType\n label?: string\n default: T\n options?: readonly string[]\n min?: number\n max?: number\n step?: number\n}\n\nexport type WindoControlMap = Record<string, WindoControlSpec>\n\n/** Resolve a control map to the value object it produces. */\nexport type WindoControlValues<C extends WindoControlMap> = {\n [K in keyof C]: C[K] extends WindoControlSpec<infer T> ? T : never\n}\n\n/**\n * A named context. Two capabilities, either or both:\n * - `controls` → ambient values + UI toggles (free, no opt-in needed)\n * - `provider` → a React wrapper, mounted inside the iframe around components\n * that opt in via `uses`\n */\nexport interface WindoContextDefinition<Values = unknown, Provided = Values> {\n label?: string\n description?: string\n controls?: WindoControlMap\n provider?: ComponentType<{ children: ReactNode; values: Values; ctx: WindoRenderContext }>\n /** Derive the value exposed on `ctx.contexts[name]`. Defaults to the control values. */\n resolve?: (values: Values, ctx: WindoRenderContext) => Provided\n}\n\n// Contexts are each generic over a different control map `C`; TS has no existential\n// to hold `exists C. WindoContextDefinition<WindoControlValues<C>>`. `Values` sits in\n// contravariant positions (provider props, resolve arg) so no precise common supertype\n// exists — `any` is the single, contained variance hatch for this heterogeneous\n// registry. Mirrors WindoModule. Definition sites stay precise; this only erases at the\n// point of collection.\n// biome-ignore lint/suspicious/noExplicitAny: existential variance hatch for the heterogeneous context registry\nexport type WindoContextMap = Record<string, WindoContextDefinition<any, any>>\n\n/* ------------------------------------------------------------------ *\n * Render-time context\n * ------------------------------------------------------------------ */\n\nexport interface WindoViewport {\n width: number\n height: number\n name: 'mobile' | 'tablet' | 'desktop'\n}\n\n/** Console channel: `ctx.logger.log(…)` posts an entry to the chrome's Console tab. */\nexport interface WindoLogger {\n log: (...args: unknown[]) => void\n}\n\n/** How an action fires. `click` renders a toolbar button; the rest bind to the stage's pointer events. */\nexport type WindoActionTrigger = 'click' | 'enter' | 'exit' | 'hover'\n\nexport const WINDO_ACTION_TRIGGERS: readonly WindoActionTrigger[] = ['click', 'enter', 'exit', 'hover']\n\n/**\n * An out-of-band action that drives a component's state. `click` actions render as\n * toolbar buttons; `enter`/`exit`/`hover` bind to the stage's pointer events. `run`\n * receives the live ctx and an `active` flag — for `hover` it is `true` on\n * pointer-enter and `false` on pointer-leave; for the others it is always `true`.\n */\nexport interface WindoAction<State = unknown> {\n label: string\n /** Defaults to `click`. */\n on?: WindoActionTrigger\n run: (ctx: WindoRenderContext<State>, active: boolean) => void\n /** Greys out a `click` action's toolbar button. Evaluated against the live state. */\n disabled?: (ctx: WindoRenderContext<State>) => boolean\n}\n\n/** Live environment handed to every render-time function inside the iframe. */\nexport interface WindoRenderContext<State = unknown> {\n colorScheme: 'light' | 'dark'\n viewport: WindoViewport\n reducedMotion: boolean\n direction: 'ltr' | 'rtl'\n locale: string\n logger: WindoLogger\n /** Current component-local state, typed by the windo's `State` generic. */\n state: State\n /** Merge a patch into the component-local state and re-render. */\n setState: (patch: Partial<State>) => void\n /** Resolved values of opted-in contexts, keyed by context name. */\n contexts: Record<string, unknown>\n}\n\n/* ------------------------------------------------------------------ *\n * Authoring API\n * ------------------------------------------------------------------ */\n\n/** A variant: a label plus a partial prop patch. Renders in the gallery and is click-to-apply. */\nexport interface WindoVariant<Props> {\n label: string\n props: Partial<Props>\n}\n\n/** A row in the authored Props documentation table. */\nexport interface WindoPropDoc {\n name: string\n type: string\n default?: string\n desc?: string\n}\n\nexport type WindoDefaultProps<Props, State = unknown> = Props | ((ctx: WindoRenderContext<State>) => Props)\n\n/**\n * The object returned by a `windo(...)` factory.\n *\n * Keystone rule: the surrounding factory runs ONCE (definition-time) for static\n * fields (title, group, schema). Every function field below — `defaultProps`,\n * `actions`, `providers`, `component` — runs at render-time with the live `ctx`.\n * Never close over live values in the static factory body.\n */\nexport interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends string = string, Tag extends string = string> {\n title: string\n group: GroupSlug\n /** Tags this component carries. Each must be one of the config's declared `tags`. Drives the sidebar's tag filter. */\n tags?: Tag[]\n status?: WindoStatus\n description?: string\n deprecation?: string\n placement?: WindoPlacement\n /** Initial component-local state. Its shape is the `State` generic; `ctx.state`/`ctx.setState` derive from it. */\n state?: State\n /** Out-of-band actions that drive state: toolbar buttons (`click`) and stage pointer triggers (`enter`/`exit`/`hover`). */\n actions?: WindoAction<State>[]\n /** zod schema: validator + parser for the JSON-editable prop subset. `z.output ⊆ Props`. */\n configurableProps?: z.ZodType\n /** Full props incl. functions/JSX. The editor's JSON overrides merge on top. */\n defaultProps: WindoDefaultProps<Props, State>\n /** Names of provider contexts this component opts into. */\n uses?: string[]\n variants?: WindoVariant<Props>[]\n /** Authored documentation table (not derived from the schema). */\n props?: WindoPropDoc[]\n /** Optional authored code snippet for the Code tab. */\n code?: (values: Props) => string\n /** A local provider wrapping just this windo (in addition to `uses`). */\n providers?: ComponentType<{ children: ReactNode; ctx: WindoRenderContext<State> }>\n component: (props: Props, ctx: WindoRenderContext<State>) => ReactNode\n}\n\n/** Argument handed to the `windo(w => ...)` factory. */\nexport interface WindoFactoryArg<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap, Tags extends readonly string[] = readonly string[]> {\n /** Configured groups keyed by slug. */\n groups: Record<Groups[number]['slug'], WindoGroup>\n contexts: Contexts\n /** The config's declared tags, in declaration order. */\n tags: Tags\n}\n\n/**\n * The default export of a `*.windo.tsx` file. A branded, lazily-resolved\n * definition — the runtime calls `resolve(w)` with the config-derived factory arg.\n */\n// biome-ignore lint/suspicious/noExplicitAny: variance escape hatch for the heterogeneous WindoModule registry\nexport interface WindoModule<Props = any, State = any> {\n readonly __windo: true\n resolve: (w: WindoFactoryArg<readonly WindoGroup[], WindoContextMap>) => WindoDefinition<Props, State>\n}\n\n/* ------------------------------------------------------------------ *\n * Config\n * ------------------------------------------------------------------ */\n\nexport interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup[], Contexts extends WindoContextMap = WindoContextMap, Tags extends readonly string[] = readonly string[]> {\n /** Configured groups. A component's `group` must be one of these slugs. */\n groups: Groups\n /** Named contexts available to components. */\n contexts?: Contexts\n /** The set of tags components may be assigned. A component's `tags` must be drawn from this list; the sidebar filters by them. */\n tags?: Tags\n /** Glob(s) for discovery, relative to project root. Default `**\\/*.windo.tsx`. */\n include?: string | string[]\n /** Title shown in the workbench chrome. */\n title?: string\n}\n\n/* ------------------------------------------------------------------ *\n * Schema descriptor (crosses the iframe boundary; renders the controls)\n * ------------------------------------------------------------------ */\n\nexport type WindoControlKind = 'string' | 'number' | 'boolean' | 'enum' | 'date' | 'array' | 'object' | 'unknown'\n\nexport interface WindoControlDescriptor {\n key: string\n kind: WindoControlKind\n optional: boolean\n options?: string[]\n min?: number\n max?: number\n description?: string\n}\n\nexport interface WindoSchemaDescriptor {\n fields: WindoControlDescriptor[]\n}\n\n/* ------------------------------------------------------------------ *\n * Runtime manifest + protocol payloads\n * ------------------------------------------------------------------ */\n\n/** Serialisable metadata for one action — drives the canvas toolbar. */\nexport interface WindoActionMeta {\n id: string\n label: string\n on: WindoActionTrigger\n}\n\n/** Static, serialisable metadata for one windo — drives the sidebar. */\nexport interface WindoManifestEntry {\n id: string\n title: string\n group: string\n tags: string[]\n status: WindoStatus\n description?: string\n deprecation?: string\n placement: WindoPlacement\n uses: string[]\n hasVariants: boolean\n actions: WindoActionMeta[]\n hasState: boolean\n}\n\nexport interface WindoVariantMeta {\n label: string\n props: Record<string, unknown>\n}\n\n/** Ambient environment pushed from the chrome down into the iframe. */\nexport interface WindoEnvState {\n colorScheme: 'light' | 'dark'\n viewport: WindoViewport\n reducedMotion: boolean\n direction: 'ltr' | 'rtl'\n locale: string\n /** Per-context control values, keyed by context name then control key. */\n contexts: Record<string, Record<string, unknown>>\n}\n\nexport interface WindoLogEntry {\n ts: number\n args: unknown[]\n}\n\nexport interface WindoFieldError {\n path: string\n message: string\n}\n\n/* ------------------------------------------------------------------ *\n * Context metadata (serialisable; drives the chrome's Context panel)\n * ------------------------------------------------------------------ */\n\nexport interface WindoContextControlMeta {\n key: string\n type: WindoControlType\n label?: string\n options?: string[]\n default: unknown\n min?: number\n max?: number\n step?: number\n}\n\nexport interface WindoContextMeta {\n name: string\n label?: string\n description?: string\n /** True when the context contributes controls (ambient values). */\n ambient: boolean\n /** True when the context mounts a provider (opt-in via `uses`). */\n hasProvider: boolean\n controls: WindoContextControlMeta[]\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/define-config.ts","../src/descriptor.ts","../src/protocol.ts","../src/types.ts"],"sourcesContent":["// The authoring API. `defineWindoConfig` is the single entry point: it captures\n// the project's groups + contexts and hands back a `windo` factory whose `group`\n// field is type-checked against the configured slugs.\n\nimport type { ComponentType, ReactNode } from 'react'\nimport type { z } from 'zod'\nimport type { WindoConfig, WindoContextDefinition, WindoContextMap, WindoControlMap, WindoControlValues, WindoDefinition, WindoFactoryArg, WindoGroup, WindoModule, WindoRenderContext } from './types'\n\nexport interface DefineWindoConfigResult<Groups extends readonly WindoGroup[], Tags extends readonly string[], Contexts extends WindoContextMap> {\n config: WindoConfig<Groups, Contexts, Tags>\n windo: <Props, State = Record<string, never>>(\n factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>\n ) => WindoModule<Props, State>\n}\n\nexport function defineWindoConfig<const Groups extends readonly WindoGroup[], const Tags extends readonly string[] = readonly [], Contexts extends WindoContextMap = Record<string, never>>(\n config: WindoConfig<Groups, Contexts, Tags>\n): DefineWindoConfigResult<Groups, Tags, Contexts> {\n function windo<Props, State = Record<string, never>>(\n factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>\n ): WindoModule<Props, State> {\n return {\n __windo: true,\n resolve: w => factory(w as unknown as WindoFactoryArg<Groups, Contexts, Tags>),\n }\n }\n return { config, windo }\n}\n\n/**\n * Define a named context. A context can contribute ambient `controls` (values +\n * UI), a `provider` (mounted inside the iframe for components that opt in via\n * `uses`), or both.\n *\n * No cast: `def` is already structurally a `WindoContextDefinition<WindoControlValues<C>,\n * Provided>` (only `controls` widens, covariantly). The unavoidable variance — TS has\n * no existential type to hold contexts heterogeneous in `C` — is absorbed once, in\n * `WindoContextMap`, not here and not at call sites.\n */\nexport function defineContext<C extends WindoControlMap, Provided = WindoControlValues<C>>(def: {\n label?: string\n description?: string\n controls?: C\n provider?: ComponentType<{ children: ReactNode; values: WindoControlValues<C>; ctx: WindoRenderContext }>\n resolve?: (values: WindoControlValues<C>, ctx: WindoRenderContext) => Provided\n}): WindoContextDefinition<WindoControlValues<C>, Provided> {\n return def\n}\n\n/**\n * Bind a zod schema to a component's props. The generic carries the component's\n * prop type so the schema's output stays a subset of it; the returned schema is\n * the runtime validator + parser (`z.input` is the JSON edit surface, `z.output`\n * is what the component receives).\n */\nexport function configurableProps<P>() {\n return <S extends z.ZodType<Partial<P>>>(schema: S): S => schema\n}\n","// Walk a zod schema into a serialisable descriptor that can cross the iframe\n// boundary and render the Controls/Schema UI. We lean on `z.toJSONSchema` (zod\n// v4) for the input shape (enum options, min/max, optionality), then enrich the\n// kind from the zod node's own type tag — some types (Date, Map, Set) are\n// unrepresentable in JSON Schema and come back as `{}`, but their zod `def.type`\n// is exact. The live schema (with transforms/coerce/refine) stays in the iframe\n// and does the actual parsing — only this descriptor travels.\n\nimport { z } from 'zod'\nimport type { WindoControlDescriptor, WindoControlKind, WindoSchemaDescriptor } from './types'\n\ninterface JsonNode {\n type?: string | string[]\n enum?: unknown[]\n const?: unknown\n format?: string\n properties?: Record<string, JsonNode>\n required?: string[]\n items?: JsonNode\n anyOf?: JsonNode[]\n oneOf?: JsonNode[]\n allOf?: JsonNode[]\n minimum?: number\n maximum?: number\n description?: string\n}\n\nexport function describeSchema(schema: z.ZodType | undefined | null): WindoSchemaDescriptor {\n if (!schema) return { fields: [] }\n let json: JsonNode\n try {\n json = z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' }) as JsonNode\n } catch {\n return { fields: [] }\n }\n const root = unwrap(json)\n const properties = root.properties ?? {}\n const required = new Set(root.required ?? [])\n const zodKinds = shapeKinds(schema)\n const fields: WindoControlDescriptor[] = Object.keys(properties).map(key => fieldFrom(key, properties[key], required.has(key), zodKinds[key]))\n return { fields }\n}\n\n// JSON Schema often wraps the object in anyOf (for optional/nullable). Find the\n// node that actually carries the object's properties.\nfunction unwrap(node: JsonNode): JsonNode {\n if (node.properties) return node\n const branches = node.anyOf ?? node.oneOf ?? node.allOf\n if (branches) {\n const withProps = branches.find(b => b.properties)\n if (withProps) return withProps\n }\n return node\n}\n\nfunction fieldFrom(key: string, node: JsonNode, isRequired: boolean, zodKind?: WindoControlKind): WindoControlDescriptor {\n const inner = collapseNullable(node)\n let kind = kindOf(inner)\n // The zod tag wins for types JSON Schema can't express (date/array/object that\n // came back as {}), and as a fallback whenever the JSON kind is unknown.\n if (zodKind && (kind === 'unknown' || zodKind === 'date')) kind = zodKind\n const descriptor: WindoControlDescriptor = {\n key,\n kind,\n optional: !isRequired || isNullable(node),\n }\n if (inner.description) descriptor.description = inner.description\n const options = enumOptions(inner)\n if (options) descriptor.options = options\n if (typeof inner.minimum === 'number') descriptor.min = inner.minimum\n if (typeof inner.maximum === 'number') descriptor.max = inner.maximum\n return descriptor\n}\n\nfunction isNullable(node: JsonNode): boolean {\n const branches = node.anyOf ?? node.oneOf\n if (!branches) return false\n return branches.some(b => b.type === 'null')\n}\n\n// Strip a `{ anyOf: [T, null] }` wrapper down to T.\nfunction collapseNullable(node: JsonNode): JsonNode {\n const branches = node.anyOf ?? node.oneOf\n if (!branches) return node\n const nonNull = branches.filter(b => b.type !== 'null')\n if (nonNull.length === 1) return nonNull[0]\n return node\n}\n\nfunction enumOptions(node: JsonNode): string[] | undefined {\n if (Array.isArray(node.enum)) return node.enum.map(v => String(v))\n const branches = node.anyOf ?? node.oneOf\n if (branches?.every(b => b.const !== undefined)) {\n return branches.map(b => String(b.const))\n }\n return undefined\n}\n\nfunction kindOf(node: JsonNode): WindoControlKind {\n if (Array.isArray(node.enum) || (node.anyOf?.every(b => b.const !== undefined) ?? false)) return 'enum'\n if (node.format === 'date-time' || node.format === 'date') return 'date'\n const type = Array.isArray(node.type) ? node.type.find(t => t !== 'null') : node.type\n switch (type) {\n case 'string':\n return 'string'\n case 'number':\n case 'integer':\n return 'number'\n case 'boolean':\n return 'boolean'\n case 'array':\n return 'array'\n case 'object':\n return 'object'\n default:\n return 'unknown'\n }\n}\n\n/* ------------------------------------------------------------------ *\n * zod node introspection (the kind source of truth for unrepresentable types)\n * ------------------------------------------------------------------ */\n\ninterface ZodDefLike {\n type?: string\n innerType?: unknown\n}\n\nfunction zodDef(node: unknown): ZodDefLike | undefined {\n const n = node as { def?: ZodDefLike; _zod?: { def?: ZodDefLike } } | null\n if (!n || typeof n !== 'object') return undefined\n return n.def ?? n._zod?.def\n}\n\nconst ZOD_WRAPPERS = new Set(['optional', 'nullable', 'default', 'prefault', 'catch', 'readonly', 'nonoptional', 'lazy'])\n\n// Unwrap optional/nullable/default/... down to the meaningful inner node.\nfunction unwrapZodDef(node: unknown): ZodDefLike | undefined {\n let def = zodDef(node)\n let guard = 0\n while (def && def.innerType && ZOD_WRAPPERS.has(def.type ?? '') && guard++ < 16) {\n def = zodDef(def.innerType)\n }\n return def\n}\n\nfunction mapZodType(type: string | undefined): WindoControlKind | undefined {\n switch (type) {\n case 'date':\n return 'date'\n case 'string':\n return 'string'\n case 'number':\n case 'int':\n case 'bigint':\n return 'number'\n case 'boolean':\n return 'boolean'\n case 'array':\n case 'set':\n case 'tuple':\n return 'array'\n case 'object':\n case 'record':\n case 'map':\n return 'object'\n case 'enum':\n case 'literal':\n return 'enum'\n default:\n return undefined\n }\n}\n\n// Per-field kind read straight from the zod object's shape.\nfunction shapeKinds(schema: z.ZodType): Record<string, WindoControlKind> {\n const shape = (schema as { shape?: Record<string, unknown> }).shape\n if (!shape || typeof shape !== 'object') return {}\n const out: Record<string, WindoControlKind> = {}\n for (const key of Object.keys(shape)) {\n const kind = mapZodType(unwrapZodDef(shape[key])?.type)\n if (kind) out[key] = kind\n }\n return out\n}\n","// The chrome <-> iframe postMessage contract. The schema itself never crosses\n// the boundary: the iframe walks it into a serialisable descriptor, the chrome\n// sends candidate JSON down, the iframe parses and reports back. Every payload\n// here is plain JSON.\n\nimport type { WindoContextMeta, WindoEnvState, WindoFieldError, WindoGroup, WindoLogEntry, WindoManifestEntry, WindoPropDoc, WindoSchemaDescriptor, WindoVariantMeta } from './types'\n\nexport const WINDO_MSG = 'windo'\n\n/** chrome -> iframe */\nexport type WindoHostMessage =\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'request-manifest' }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'select'; id: string }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-props'; id: string; json: string }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-env'; env: WindoEnvState }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-ctx-state'; state: Record<string, unknown> }\n | { source: typeof WINDO_MSG; dir: 'host'; type: 'invoke-action'; id: string; actionId: string }\n\n/** iframe -> chrome */\nexport type WindoPreviewMessage =\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'ready' }\n | {\n source: typeof WINDO_MSG\n dir: 'preview'\n type: 'manifest'\n title: string\n entries: WindoManifestEntry[]\n groups: WindoGroup[]\n tags: string[]\n contexts: WindoContextMeta[]\n /** Initial shared state from the config — seeds the chrome's editable strip. */\n ctxState: Record<string, unknown>\n }\n | {\n source: typeof WINDO_MSG\n dir: 'preview'\n type: 'describe'\n id: string\n descriptor: WindoSchemaDescriptor\n props: WindoPropDoc[]\n variants: WindoVariantMeta[]\n defaults: unknown\n code: string | null\n }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-ok'; id: string }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-error'; id: string; errors: WindoFieldError[] }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'log'; entry: WindoLogEntry }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'state'; id: string; state: Record<string, unknown>; actions: { id: string; disabled: boolean }[] }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'ctx-state'; state: Record<string, unknown> }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'color-scheme'; colorScheme: 'light' | 'dark' }\n | { source: typeof WINDO_MSG; dir: 'preview'; type: 'render-error'; id: string; message: string; stack?: string }\n\nexport type WindoMessage = WindoHostMessage | WindoPreviewMessage\n\nexport function isWindoMessage(data: unknown): data is WindoMessage {\n return typeof data === 'object' && data !== null && (data as { source?: unknown }).source === WINDO_MSG\n}\n\nexport function isHostMessage(msg: WindoMessage): msg is WindoHostMessage {\n return msg.dir === 'host'\n}\n\nexport function isPreviewMessage(msg: WindoMessage): msg is WindoPreviewMessage {\n return msg.dir === 'preview'\n}\n","// Core type system for windo. Everything — the authoring API, the iframe\n// preview runtime, the chrome UI, and the postMessage protocol — is typed\n// against the contracts in this file.\n\nimport type { ComponentType, ReactNode } from 'react'\nimport type { z } from 'zod'\n\n/* ------------------------------------------------------------------ *\n * Primitives\n * ------------------------------------------------------------------ */\n\nexport type WindoStatus = 'stable' | 'beta' | 'deprecated'\n\nexport const WINDO_STATUSES: readonly WindoStatus[] = ['stable', 'beta', 'deprecated']\n\n/** Anchor a component within the canvas frame. */\nexport type WindoPlacementBase = 'center' | 'fill' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\n/**\n * Where a component renders inside the canvas frame. Placements render flush by\n * default; append `-padding` to inset the component from the frame edges\n * (e.g. `top` sits flush at the top, `top-padding` adds breathing room).\n */\nexport type WindoPlacement = WindoPlacementBase | `${WindoPlacementBase}-padding`\n\nconst WINDO_PLACEMENT_BASE: readonly WindoPlacementBase[] = ['center', 'fill', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right']\n\nexport const WINDO_PLACEMENTS: readonly WindoPlacement[] = [...WINDO_PLACEMENT_BASE, ...WINDO_PLACEMENT_BASE.map(p => `${p}-padding` as WindoPlacement)]\n\n/** A configured group. Components reference a group by its `slug`. */\nexport interface WindoGroup {\n name: string\n slug: string\n description?: string\n}\n\n/* ------------------------------------------------------------------ *\n * Context system\n * ------------------------------------------------------------------ */\n\nexport type WindoControlType = 'enum' | 'boolean' | 'string' | 'number'\n\n/** A single ambient control: a value plus the metadata to render its toggle. */\nexport interface WindoControlSpec<T = unknown> {\n type: WindoControlType\n label?: string\n default: T\n options?: readonly string[]\n min?: number\n max?: number\n step?: number\n}\n\nexport type WindoControlMap = Record<string, WindoControlSpec>\n\n/** Resolve a control map to the value object it produces. */\nexport type WindoControlValues<C extends WindoControlMap> = {\n [K in keyof C]: C[K] extends WindoControlSpec<infer T> ? T : never\n}\n\n/**\n * A named context. Two capabilities, either or both:\n * - `controls` → ambient values + UI toggles (free, no opt-in needed)\n * - `provider` → a React wrapper, mounted inside the iframe around components\n * that opt in via `uses`\n */\nexport interface WindoContextDefinition<Values = unknown, Provided = Values> {\n label?: string\n description?: string\n controls?: WindoControlMap\n provider?: ComponentType<{ children: ReactNode; values: Values; ctx: WindoRenderContext }>\n /** Derive the value exposed on `ctx.contexts[name]`. Defaults to the control values. */\n resolve?: (values: Values, ctx: WindoRenderContext) => Provided\n}\n\n// Contexts are each generic over a different control map `C`; TS has no existential\n// to hold `exists C. WindoContextDefinition<WindoControlValues<C>>`. `Values` sits in\n// contravariant positions (provider props, resolve arg) so no precise common supertype\n// exists — `any` is the single, contained variance hatch for this heterogeneous\n// registry. Mirrors WindoModule. Definition sites stay precise; this only erases at the\n// point of collection.\n// biome-ignore lint/suspicious/noExplicitAny: existential variance hatch for the heterogeneous context registry\nexport type WindoContextMap = Record<string, WindoContextDefinition<any, any>>\n\n/* ------------------------------------------------------------------ *\n * Render-time context\n * ------------------------------------------------------------------ */\n\nexport interface WindoViewport {\n width: number\n height: number\n name: 'mobile' | 'tablet' | 'desktop'\n}\n\n/** Console channel: `ctx.logger.log(…)` posts an entry to the chrome's Console tab. */\nexport interface WindoLogger {\n log: (...args: unknown[]) => void\n}\n\n/** How an action fires. `click` renders a toolbar button; the rest bind to the stage's pointer events. */\nexport type WindoActionTrigger = 'click' | 'enter' | 'exit' | 'hover'\n\nexport const WINDO_ACTION_TRIGGERS: readonly WindoActionTrigger[] = ['click', 'enter', 'exit', 'hover']\n\n/**\n * An out-of-band action that drives a component's state. `click` actions render as\n * toolbar buttons; `enter`/`exit`/`hover` bind to the stage's pointer events. `run`\n * receives the live ctx and an `active` flag — for `hover` it is `true` on\n * pointer-enter and `false` on pointer-leave; for the others it is always `true`.\n */\nexport interface WindoAction<State = unknown> {\n label: string\n /** Defaults to `click`. */\n on?: WindoActionTrigger\n run: (ctx: WindoRenderContext<State>, active: boolean) => void\n /** Greys out a `click` action's toolbar button. Evaluated against the live state. */\n disabled?: (ctx: WindoRenderContext<State>) => boolean\n}\n\n/** Live environment handed to every render-time function inside the iframe. */\nexport interface WindoRenderContext<State = unknown> {\n colorScheme: 'light' | 'dark'\n viewport: WindoViewport\n reducedMotion: boolean\n direction: 'ltr' | 'rtl'\n locale: string\n logger: WindoLogger\n /** Current component-local state, typed by the windo's `State` generic. */\n state: State\n /** Merge a patch into the component-local state and re-render. */\n setState: (patch: Partial<State>) => void\n /** Resolved values of opted-in contexts, keyed by context name. */\n contexts: Record<string, unknown>\n /**\n * Shared, cross-component state seeded from the config's `ctxState`. Unlike\n * `state` (per-windo, reset on selection), this persists across selection and\n * is global: any component can read it and write it via `setCtxState`. Use it\n * to drive providers that wrap every component — e.g. a theme provider toggled\n * from any component on the canvas.\n */\n ctxState: Record<string, unknown>\n /** Merge a patch into the shared `ctxState` and re-render every consumer. */\n setCtxState: (patch: Record<string, unknown>) => void\n /** Set the canvas colour scheme from a component or action. */\n setColorScheme: (scheme: 'light' | 'dark') => void\n /** Flip the canvas colour scheme between light and dark. */\n toggleTheme: () => void\n}\n\n/* ------------------------------------------------------------------ *\n * Authoring API\n * ------------------------------------------------------------------ */\n\n/** A variant: a label plus a partial prop patch. Renders in the gallery and is click-to-apply. */\nexport interface WindoVariant<Props> {\n label: string\n props: Partial<Props>\n}\n\n/** A row in the authored Props documentation table. */\nexport interface WindoPropDoc {\n name: string\n type: string\n default?: string\n desc?: string\n}\n\n/** A value that may be authored statically or as a context-aware `ctx => value` function resolved at render-time. */\nexport type Ctxual<T, State = unknown> = T | ((ctx: WindoRenderContext<State>) => T)\n\n/** The ctx surface available while resolving a windo's initial state — the render-time `state`/`setState` pair is excluded because it does not exist yet. */\nexport type WindoInitContext<State = unknown> = Omit<WindoRenderContext<State>, 'state' | 'setState'>\n\nexport type WindoDefaultProps<Props, State = unknown> = Ctxual<Props, State>\n\n/**\n * The object returned by a `windo(...)` factory.\n *\n * Keystone rule: the surrounding factory runs ONCE (definition-time) for static\n * fields (title, group, schema). Every function field below — `defaultProps`,\n * `actions`, `providers`, `component` — runs at render-time with the live `ctx`.\n * Never close over live values in the static factory body.\n */\nexport interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends string = string, Tag extends string = string> {\n title: string\n group: GroupSlug\n /** Tags this component carries. Each must be one of the config's declared `tags`. Drives the sidebar's tag filter. */\n tags?: Tag[]\n status?: WindoStatus\n description?: string\n deprecation?: string\n /** Where the component anchors in the canvas frame. Either a static placement or a function resolved with the live `ctx`. */\n placement?: Ctxual<WindoPlacement, State>\n /** Initial component-local state. Its shape is the `State` generic; `ctx.state`/`ctx.setState` derive from it. Either a static value or a function resolved with the init `ctx` (no `state`/`setState`) when the component is selected. */\n state?: State | ((ctx: WindoInitContext<State>) => State)\n /** Out-of-band actions that drive state: toolbar buttons (`click`) and stage pointer triggers (`enter`/`exit`/`hover`). */\n actions?: WindoAction<State>[]\n /** zod schema: validator + parser for the JSON-editable prop subset. `z.output ⊆ Props`. */\n configurableProps?: z.ZodType\n /** Full props incl. functions/JSX. The editor's JSON overrides merge on top. */\n defaultProps: WindoDefaultProps<Props, State>\n /** Names of provider contexts this component opts into. */\n uses?: string[]\n /** Gallery variants. Either a static array or a function resolved with the live `ctx` when the component is selected. */\n variants?: Ctxual<WindoVariant<Props>[], State>\n /** Authored documentation table (not derived from the schema). Either a static array or a function resolved with the live `ctx` when the component is selected. */\n props?: Ctxual<WindoPropDoc[], State>\n /** Optional authored code snippet for the Code tab, resolved with the JSON-editable values and the live `ctx`. */\n code?: (values: Props, ctx: WindoRenderContext<State>) => string\n /** A local provider wrapping just this windo (in addition to `uses`). */\n providers?: ComponentType<{ children: ReactNode; ctx: WindoRenderContext<State> }>\n component: (props: Props, ctx: WindoRenderContext<State>) => ReactNode\n}\n\n/** Argument handed to the `windo(w => ...)` factory. */\nexport interface WindoFactoryArg<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap, Tags extends readonly string[] = readonly string[]> {\n /** Configured groups keyed by slug. */\n groups: Record<Groups[number]['slug'], WindoGroup>\n contexts: Contexts\n /** The config's declared tags, in declaration order. */\n tags: Tags\n}\n\n/**\n * The default export of a `*.windo.tsx` file. A branded, lazily-resolved\n * definition — the runtime calls `resolve(w)` with the config-derived factory arg.\n */\n// biome-ignore lint/suspicious/noExplicitAny: variance escape hatch for the heterogeneous WindoModule registry\nexport interface WindoModule<Props = any, State = any> {\n readonly __windo: true\n resolve: (w: WindoFactoryArg<readonly WindoGroup[], WindoContextMap>) => WindoDefinition<Props, State>\n}\n\n/* ------------------------------------------------------------------ *\n * Config\n * ------------------------------------------------------------------ */\n\nexport interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup[], Contexts extends WindoContextMap = WindoContextMap, Tags extends readonly string[] = readonly string[]> {\n /** Configured groups. A component's `group` must be one of these slugs. */\n groups: Groups\n /** Named contexts available to components. */\n contexts?: Contexts\n /** The set of tags components may be assigned. A component's `tags` must be drawn from this list; the sidebar filters by them. */\n tags?: Tags\n /** Initial shared state exposed on `ctx.ctxState`. Global across every component and persisted across selection — write it from any component via `ctx.setCtxState`. */\n ctxState?: Record<string, unknown>\n /** Glob(s) for discovery, relative to project root. Default `**\\/*.windo.tsx`. */\n include?: string | string[]\n /** Title shown in the workbench chrome. */\n title?: string\n}\n\n/* ------------------------------------------------------------------ *\n * Schema descriptor (crosses the iframe boundary; renders the controls)\n * ------------------------------------------------------------------ */\n\nexport type WindoControlKind = 'string' | 'number' | 'boolean' | 'enum' | 'date' | 'array' | 'object' | 'unknown'\n\nexport interface WindoControlDescriptor {\n key: string\n kind: WindoControlKind\n optional: boolean\n options?: string[]\n min?: number\n max?: number\n description?: string\n}\n\nexport interface WindoSchemaDescriptor {\n fields: WindoControlDescriptor[]\n}\n\n/* ------------------------------------------------------------------ *\n * Runtime manifest + protocol payloads\n * ------------------------------------------------------------------ */\n\n/** Serialisable metadata for one action — drives the canvas toolbar. */\nexport interface WindoActionMeta {\n id: string\n label: string\n on: WindoActionTrigger\n}\n\n/** Static, serialisable metadata for one windo — drives the sidebar. */\nexport interface WindoManifestEntry {\n id: string\n title: string\n group: string\n tags: string[]\n status: WindoStatus\n description?: string\n deprecation?: string\n placement: WindoPlacement\n uses: string[]\n hasVariants: boolean\n actions: WindoActionMeta[]\n hasState: boolean\n}\n\nexport interface WindoVariantMeta {\n label: string\n props: Record<string, unknown>\n}\n\n/** Ambient environment pushed from the chrome down into the iframe. */\nexport interface WindoEnvState {\n colorScheme: 'light' | 'dark'\n viewport: WindoViewport\n reducedMotion: boolean\n direction: 'ltr' | 'rtl'\n locale: string\n /** Per-context control values, keyed by context name then control key. */\n contexts: Record<string, Record<string, unknown>>\n}\n\nexport interface WindoLogEntry {\n ts: number\n args: unknown[]\n}\n\nexport interface WindoFieldError {\n path: string\n message: string\n}\n\n/* ------------------------------------------------------------------ *\n * Context metadata (serialisable; drives the chrome's Context panel)\n * ------------------------------------------------------------------ */\n\nexport interface WindoContextControlMeta {\n key: string\n type: WindoControlType\n label?: string\n options?: string[]\n default: unknown\n min?: number\n max?: number\n step?: number\n}\n\nexport interface WindoContextMeta {\n name: string\n label?: string\n description?: string\n /** True when the context contributes controls (ambient values). */\n ambient: boolean\n /** True when the context mounts a provider (opt-in via `uses`). */\n hasProvider: boolean\n controls: WindoContextControlMeta[]\n}\n"],"mappings":";AAeO,SAAS,kBACd,QACiD;AACjD,WAAS,MACP,SAC2B;AAC3B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,OAAK,QAAQ,CAAuD;AAAA,IAC/E;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,MAAM;AACzB;AAYO,SAAS,cAA2E,KAM/B;AAC1D,SAAO;AACT;AAQO,SAAS,oBAAuB;AACrC,SAAO,CAAkC,WAAiB;AAC5D;;;ACjDA,SAAS,SAAS;AAmBX,SAAS,eAAe,QAA6D;AAC1F,MAAI,CAAC,OAAQ,QAAO,EAAE,QAAQ,CAAC,EAAE;AACjC,MAAI;AACJ,MAAI;AACF,WAAO,EAAE,aAAa,QAAQ,EAAE,IAAI,SAAS,iBAAiB,MAAM,CAAC;AAAA,EACvE,QAAQ;AACN,WAAO,EAAE,QAAQ,CAAC,EAAE;AAAA,EACtB;AACA,QAAM,OAAO,OAAO,IAAI;AACxB,QAAM,aAAa,KAAK,cAAc,CAAC;AACvC,QAAM,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC,CAAC;AAC5C,QAAM,WAAW,WAAW,MAAM;AAClC,QAAM,SAAmC,OAAO,KAAK,UAAU,EAAE,IAAI,SAAO,UAAU,KAAK,WAAW,GAAG,GAAG,SAAS,IAAI,GAAG,GAAG,SAAS,GAAG,CAAC,CAAC;AAC7I,SAAO,EAAE,OAAO;AAClB;AAIA,SAAS,OAAO,MAA0B;AACxC,MAAI,KAAK,WAAY,QAAO;AAC5B,QAAM,WAAW,KAAK,SAAS,KAAK,SAAS,KAAK;AAClD,MAAI,UAAU;AACZ,UAAM,YAAY,SAAS,KAAK,OAAK,EAAE,UAAU;AACjD,QAAI,UAAW,QAAO;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,UAAU,KAAa,MAAgB,YAAqB,SAAoD;AACvH,QAAM,QAAQ,iBAAiB,IAAI;AACnC,MAAI,OAAO,OAAO,KAAK;AAGvB,MAAI,YAAY,SAAS,aAAa,YAAY,QAAS,QAAO;AAClE,QAAM,aAAqC;AAAA,IACzC;AAAA,IACA;AAAA,IACA,UAAU,CAAC,cAAc,WAAW,IAAI;AAAA,EAC1C;AACA,MAAI,MAAM,YAAa,YAAW,cAAc,MAAM;AACtD,QAAM,UAAU,YAAY,KAAK;AACjC,MAAI,QAAS,YAAW,UAAU;AAClC,MAAI,OAAO,MAAM,YAAY,SAAU,YAAW,MAAM,MAAM;AAC9D,MAAI,OAAO,MAAM,YAAY,SAAU,YAAW,MAAM,MAAM;AAC9D,SAAO;AACT;AAEA,SAAS,WAAW,MAAyB;AAC3C,QAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,KAAK,OAAK,EAAE,SAAS,MAAM;AAC7C;AAGA,SAAS,iBAAiB,MAA0B;AAClD,QAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,UAAU,SAAS,OAAO,OAAK,EAAE,SAAS,MAAM;AACtD,MAAI,QAAQ,WAAW,EAAG,QAAO,QAAQ,CAAC;AAC1C,SAAO;AACT;AAEA,SAAS,YAAY,MAAsC;AACzD,MAAI,MAAM,QAAQ,KAAK,IAAI,EAAG,QAAO,KAAK,KAAK,IAAI,OAAK,OAAO,CAAC,CAAC;AACjE,QAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,UAAU,MAAM,OAAK,EAAE,UAAU,MAAS,GAAG;AAC/C,WAAO,SAAS,IAAI,OAAK,OAAO,EAAE,KAAK,CAAC;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,OAAO,MAAkC;AAChD,MAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,OAAK,EAAE,UAAU,MAAS,KAAK,OAAQ,QAAO;AACjG,MAAI,KAAK,WAAW,eAAe,KAAK,WAAW,OAAQ,QAAO;AAClE,QAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,OAAK,MAAM,MAAM,IAAI,KAAK;AACjF,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAWA,SAAS,OAAO,MAAuC;AACrD,QAAM,IAAI;AACV,MAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO;AACxC,SAAO,EAAE,OAAO,EAAE,MAAM;AAC1B;AAEA,IAAM,eAAe,oBAAI,IAAI,CAAC,YAAY,YAAY,WAAW,YAAY,SAAS,YAAY,eAAe,MAAM,CAAC;AAGxH,SAAS,aAAa,MAAuC;AAC3D,MAAI,MAAM,OAAO,IAAI;AACrB,MAAI,QAAQ;AACZ,SAAO,OAAO,IAAI,aAAa,aAAa,IAAI,IAAI,QAAQ,EAAE,KAAK,UAAU,IAAI;AAC/E,UAAM,OAAO,IAAI,SAAS;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,WAAW,MAAwD;AAC1E,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAGA,SAAS,WAAW,QAAqD;AACvE,QAAM,QAAS,OAA+C;AAC9D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO,CAAC;AACjD,QAAM,MAAwC,CAAC;AAC/C,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAM,OAAO,WAAW,aAAa,MAAM,GAAG,CAAC,GAAG,IAAI;AACtD,QAAI,KAAM,KAAI,GAAG,IAAI;AAAA,EACvB;AACA,SAAO;AACT;;;ACjLO,IAAM,YAAY;AA+ClB,SAAS,eAAe,MAAqC;AAClE,SAAO,OAAO,SAAS,YAAY,SAAS,QAAS,KAA8B,WAAW;AAChG;AAEO,SAAS,cAAc,KAA4C;AACxE,SAAO,IAAI,QAAQ;AACrB;AAEO,SAAS,iBAAiB,KAA+C;AAC9E,SAAO,IAAI,QAAQ;AACrB;;;ACnDO,IAAM,iBAAyC,CAAC,UAAU,QAAQ,YAAY;AAYrF,IAAM,uBAAsD,CAAC,UAAU,QAAQ,OAAO,UAAU,QAAQ,SAAS,YAAY,aAAa,eAAe,cAAc;AAEhK,IAAM,mBAA8C,CAAC,GAAG,sBAAsB,GAAG,qBAAqB,IAAI,OAAK,GAAG,CAAC,UAA4B,CAAC;AA2EhJ,IAAM,wBAAuD,CAAC,SAAS,SAAS,QAAQ,OAAO;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westopp/windo",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Zero-infra component canvas — point it at *.windo.tsx files and get a resizable preview with live props, variants, and context",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli/index.ts CHANGED
@@ -10,7 +10,7 @@ import pc from 'picocolors'
10
10
  import { build, createServer, preview } from 'vite'
11
11
  import { windoPlugin } from '../plugin'
12
12
 
13
- const VERSION = '0.1.0'
13
+ const VERSION = '0.1.2'
14
14
 
15
15
  const USAGE = `
16
16
  ${pc.bold('windo')} ${pc.dim(`v${VERSION}`)} — zero-infra component canvas
@@ -67,6 +67,14 @@ export function App() {
67
67
  const [grid, setGridState] = useState<CanvasGridOpts>(() => loadJSON<CanvasGridOpts>('windo:grid', CANVAS_GRID_DEFAULTS))
68
68
  const [env, setEnv] = useState<ChromeEnv>(() => ({ ...loadJSON<ChromeEnv>('windo:env', DEFAULT_ENV), colorScheme: loadString('windo:theme', 'light') as ThemeMode }))
69
69
 
70
+ // Shared, cross-component state. The chrome holds the canonical, persisted copy;
71
+ // the preview owns a working copy it can write from any component. They stay in
72
+ // sync via the bridge: component writes echo up (adopted below), chrome edits and
73
+ // reload re-syncs push down.
74
+ const [ctxState, setCtxState] = useState<Record<string, unknown>>(() => loadJSON<Record<string, unknown>>('windo:ctx-state', {}))
75
+ const ctxStateRef = useRef(ctxState)
76
+ ctxStateRef.current = ctxState
77
+
70
78
  const [draftById, setDraftById] = useState<Record<string, string>>({})
71
79
  // Last JSON pushed to the preview, per id. `dirty` = draft differs from this.
72
80
  // Every push (seed, save, reset, variant, reload re-sync) updates it, so the
@@ -104,6 +112,7 @@ export function App() {
104
112
  useEffect(() => saveJSON('windo:zoom', zoom), [zoom])
105
113
  useEffect(() => saveJSON('windo:grid', grid), [grid])
106
114
  useEffect(() => saveJSON('windo:env', env), [env])
115
+ useEffect(() => saveJSON('windo:ctx-state', ctxState), [ctxState])
107
116
 
108
117
  // ── selection wiring ───────────────────────────────────────────────────
109
118
  useEffect(() => {
@@ -143,6 +152,33 @@ export function App() {
143
152
  bridge.setEnv(next)
144
153
  }, [env, width, height, theme, bridge.setEnv, bridge.readyNonce])
145
154
 
155
+ // ── shared state (ctxState) sync ──────────────────────────────────────────
156
+ // Seed the canonical from the config defaults, filling only keys the chrome
157
+ // hasn't already persisted (persisted edits win).
158
+ useEffect(() => {
159
+ if (!Object.keys(bridge.ctxStateDefaults).length) return
160
+ setCtxState(prev => ({ ...bridge.ctxStateDefaults, ...prev }))
161
+ }, [bridge.ctxStateDefaults])
162
+
163
+ // Adopt component-driven writes echoed up from the preview (preview is the live
164
+ // truth for those). The chrome's own pushes don't echo, so this never loops.
165
+ useEffect(() => {
166
+ if (!Object.keys(bridge.ctxState).length) return
167
+ setCtxState(prev => ({ ...prev, ...bridge.ctxState }))
168
+ }, [bridge.ctxState])
169
+
170
+ // Re-sync the canonical down on every (re)ready — the iframe reloads on HMR and
171
+ // re-seeds from config defaults, so we re-assert the persisted shared state.
172
+ useEffect(() => {
173
+ if (Object.keys(ctxStateRef.current).length) bridge.setCtxState(ctxStateRef.current)
174
+ }, [bridge.setCtxState, bridge.readyNonce])
175
+
176
+ // Follow a component's `toggleTheme`/`setColorScheme` push (depends only on the
177
+ // echo, never on `theme` — so a topbar toggle is never reverted by a stale echo).
178
+ useEffect(() => {
179
+ if (bridge.colorScheme) setTheme(bridge.colorScheme.value)
180
+ }, [bridge.colorScheme])
181
+
146
182
  // ── draft commit / variant ─────────────────────────────────────────────
147
183
  function setValuesJson(json: string) {
148
184
  if (!selected) return
@@ -196,6 +232,14 @@ export function App() {
196
232
  }))
197
233
  }
198
234
 
235
+ // Edit one key of the shared state from the chrome: update the canonical and
236
+ // push the whole object down (the preview adopts it without echoing back).
237
+ function setCtxStateValue(key: string, value: unknown) {
238
+ const next = { ...ctxStateRef.current, [key]: value }
239
+ setCtxState(next)
240
+ bridge.setCtxState(next)
241
+ }
242
+
199
243
  function setGrid(patch: Partial<CanvasGridOpts> | null) {
200
244
  if (patch === null) {
201
245
  setGridState(CANVAS_GRID_DEFAULTS)
@@ -276,6 +320,8 @@ export function App() {
276
320
  logs={bridge.logs}
277
321
  clearLogs={bridge.clearLogs}
278
322
  state={stateForEntry}
323
+ ctxState={ctxState}
324
+ setCtxStateValue={setCtxStateValue}
279
325
  contexts={contextsForEntry}
280
326
  env={env}
281
327
  setEnv={setEnvPatch}
@@ -432,6 +432,40 @@ function formatStateValue(value: unknown): string {
432
432
  return out.length > 80 ? `${out.slice(0, 80)}…` : out
433
433
  }
434
434
 
435
+ /* ---------- Shared (ctxState) strip: editable, global across components ---------- */
436
+
437
+ // Infer the editor from the live value's type — ctxState carries no control
438
+ // metadata (it's a plain object, like the `state` declaration), so primitives
439
+ // get a fitting inline editor and anything richer falls back to a read-only chip.
440
+ function CtxStateEditor({ value, onChange }: { value: unknown; onChange: (next: unknown) => void }) {
441
+ if (typeof value === 'boolean') {
442
+ return <button type="button" role="switch" aria-checked={value} className={`wb-switch${value ? ' on' : ''}`} onClick={() => onChange(!value)} />
443
+ }
444
+ if (typeof value === 'number') {
445
+ return <input type="number" className="wb-ctxstate-input" value={Number.isFinite(value) ? value : ''} onChange={e => onChange(e.target.valueAsNumber)} />
446
+ }
447
+ if (typeof value === 'string') {
448
+ return <input type="text" className="wb-ctxstate-input" value={value} onChange={e => onChange(e.target.value)} />
449
+ }
450
+ return <span className="v">{formatStateValue(value)}</span>
451
+ }
452
+
453
+ function CtxStateStrip({ ctxState, setCtxStateValue }: { ctxState: Record<string, unknown>; setCtxStateValue: InspectorProps['setCtxStateValue'] }) {
454
+ const keys = Object.keys(ctxState)
455
+ if (!keys.length) return null
456
+ return (
457
+ <div className="wb-ctxstate" title="Shared state (ctx.ctxState) — global across components, editable here or from any component">
458
+ <span className="wb-state-label">shared</span>
459
+ {keys.map(k => (
460
+ <span className="wb-state-item" key={k}>
461
+ <span className="k">{k}</span>
462
+ <CtxStateEditor value={ctxState[k]} onChange={next => setCtxStateValue(k, next)} />
463
+ </span>
464
+ ))}
465
+ </div>
466
+ )
467
+ }
468
+
435
469
  function LogsStrip({ logs, clearLogs }: { logs: InspectorProps['logs']; clearLogs: InspectorProps['clearLogs'] }) {
436
470
  const recent = logs.slice(-50)
437
471
  return (
@@ -478,6 +512,8 @@ export function Inspector(props: InspectorProps) {
478
512
  logs,
479
513
  clearLogs,
480
514
  state,
515
+ ctxState,
516
+ setCtxStateValue,
481
517
  contexts,
482
518
  env,
483
519
  setEnv,
@@ -562,6 +598,7 @@ export function Inspector(props: InspectorProps) {
562
598
  ))}
563
599
  </div>
564
600
  )}
601
+ <CtxStateStrip ctxState={ctxState} setCtxStateValue={setCtxStateValue} />
565
602
  <div className="wb-inspector-body">
566
603
  {tab === 'Controls' && (
567
604
  <ControlsTab
@@ -25,6 +25,14 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
25
25
  const [logs, setLogs] = useState<WindoLogRow[]>([])
26
26
  const [stateValues, setStateValues] = useState<Record<string, Record<string, unknown>>>({})
27
27
  const [actionDisabled, setActionDisabled] = useState<Record<string, Record<string, boolean>>>({})
28
+ // Shared state: `ctxStateDefaults` is the config seed (from the manifest);
29
+ // `ctxState` is the latest snapshot echoed after a component writes it.
30
+ const [ctxStateDefaults, setCtxStateDefaults] = useState<Record<string, unknown>>({})
31
+ const [ctxState, setCtxStateEcho] = useState<Record<string, unknown>>({})
32
+ // Latest colour scheme a component pushed via `ctx.toggleTheme`/`setColorScheme`
33
+ // (null until one does). Wrapped in a fresh object each echo so consumers re-fire
34
+ // even when the same scheme is pushed twice (e.g. after a manual revert between).
35
+ const [colorScheme, setColorSchemeEcho] = useState<{ value: 'light' | 'dark' } | null>(null)
28
36
  const seqRef = useRef(0)
29
37
 
30
38
  const post = useCallback(
@@ -54,6 +62,7 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
54
62
  setGroups(msg.groups)
55
63
  setTags(msg.tags)
56
64
  setContexts(msg.contexts)
65
+ setCtxStateDefaults(msg.ctxState)
57
66
  break
58
67
  case 'describe':
59
68
  setDescribe({
@@ -82,6 +91,12 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
82
91
  setStateValues(prev => ({ ...prev, [msg.id]: msg.state }))
83
92
  setActionDisabled(prev => ({ ...prev, [msg.id]: Object.fromEntries(msg.actions.map(a => [a.id, a.disabled])) }))
84
93
  break
94
+ case 'ctx-state':
95
+ setCtxStateEcho(msg.state)
96
+ break
97
+ case 'color-scheme':
98
+ setColorSchemeEcho({ value: msg.colorScheme })
99
+ break
85
100
  case 'render-error':
86
101
  setRenderError({ message: msg.message, stack: msg.stack })
87
102
  break
@@ -100,6 +115,8 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
100
115
 
101
116
  const setEnv = useCallback((env: WindoEnvState) => post({ source: WINDO_MSG, dir: 'host', type: 'set-env', env }), [post])
102
117
 
118
+ const setCtxState = useCallback((state: Record<string, unknown>) => post({ source: WINDO_MSG, dir: 'host', type: 'set-ctx-state', state }), [post])
119
+
103
120
  const invokeAction = useCallback((id: string, actionId: string) => post({ source: WINDO_MSG, dir: 'host', type: 'invoke-action', id, actionId }), [post])
104
121
 
105
122
  const clearLogs = useCallback(() => setLogs([]), [])
@@ -119,9 +136,13 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
119
136
  clearLogs,
120
137
  stateValues,
121
138
  actionDisabled,
139
+ ctxStateDefaults,
140
+ ctxState,
141
+ colorScheme,
122
142
  select,
123
143
  setProps,
124
144
  setEnv,
145
+ setCtxState,
125
146
  invokeAction,
126
147
  }
127
148
  }
@@ -21,6 +21,9 @@
21
21
  --accent-text: #ffffff;
22
22
  --accent-soft: color-mix(in srgb, var(--accent) 8%, transparent);
23
23
  --danger: #c0443c;
24
+ --success: #3c7a4a;
25
+ --warn: #99661c;
26
+ --deprecated: #a8433a;
24
27
  --surface: #ffffff;
25
28
  --shadow: 0 1px 2px rgba(16, 16, 18, 0.04), 0 8px 24px rgba(16, 16, 18, 0.06);
26
29
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
@@ -41,6 +44,10 @@
41
44
  --accent-ui: color-mix(in oklab, var(--accent) 35%, white);
42
45
  --accent-soft: color-mix(in srgb, var(--accent-ui) 14%, transparent);
43
46
  --danger: #e0837b;
47
+ --success: #84c896;
48
+ --warn: #dcb064;
49
+ --deprecated: #e6918a;
50
+ --accent-contrast: #111113;
44
51
  --surface: #161618;
45
52
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 10px 32px rgba(0, 0, 0, 0.45);
46
53
  }
@@ -373,7 +380,7 @@ button {
373
380
 
374
381
  [data-theme="dark"] .wb-tag-pill {
375
382
  background: var(--accent-ui);
376
- color: #111113;
383
+ color: var(--accent-contrast);
377
384
  }
378
385
 
379
386
  .wb-tag-pill .x {
@@ -476,7 +483,7 @@ button {
476
483
  [data-theme="dark"] .wb-check.checked {
477
484
  background: var(--accent-ui);
478
485
  border-color: var(--accent-ui);
479
- color: #111113;
486
+ color: var(--accent-contrast);
480
487
  }
481
488
 
482
489
  .wb-tag-divider {
@@ -642,29 +649,16 @@ button {
642
649
  }
643
650
 
644
651
  .wb-status.stable {
645
- color: #3c7a4a;
646
- background: rgba(60, 122, 74, 0.1);
652
+ color: var(--success);
653
+ background: color-mix(in srgb, var(--success) 10%, transparent);
647
654
  }
648
655
  .wb-status.beta {
649
- color: #99661c;
650
- background: rgba(153, 102, 28, 0.1);
656
+ color: var(--warn);
657
+ background: color-mix(in srgb, var(--warn) 10%, transparent);
651
658
  }
652
659
  .wb-status.deprecated {
653
- color: #a8433a;
654
- background: rgba(168, 67, 58, 0.1);
655
- }
656
-
657
- [data-theme="dark"] .wb-status.stable {
658
- color: #84c896;
659
- background: rgba(132, 200, 150, 0.12);
660
- }
661
- [data-theme="dark"] .wb-status.beta {
662
- color: #dcb064;
663
- background: rgba(220, 176, 100, 0.12);
664
- }
665
- [data-theme="dark"] .wb-status.deprecated {
666
- color: #e6918a;
667
- background: rgba(230, 145, 138, 0.12);
660
+ color: var(--deprecated);
661
+ background: color-mix(in srgb, var(--deprecated) 10%, transparent);
668
662
  }
669
663
 
670
664
  .wb-nav-empty {
@@ -768,7 +762,7 @@ button {
768
762
  [data-theme="dark"] .wb-trigger.on {
769
763
  background: var(--accent-ui);
770
764
  border-color: var(--accent-ui);
771
- color: #111113;
765
+ color: var(--accent-contrast);
772
766
  }
773
767
 
774
768
  .wb-trigger .tdot {
@@ -1242,6 +1236,45 @@ button {
1242
1236
  font-weight: 600;
1243
1237
  }
1244
1238
 
1239
+ /* Editable shared-state strip (ctx.ctxState) — same look as .wb-state, with inline editors */
1240
+
1241
+ .wb-ctxstate {
1242
+ display: flex;
1243
+ align-items: center;
1244
+ gap: 12px;
1245
+ flex-wrap: wrap;
1246
+ padding: 7px 16px;
1247
+ border-bottom: 1px solid var(--border);
1248
+ background: var(--inset);
1249
+ font-size: 11.5px;
1250
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1251
+ flex-shrink: 0;
1252
+ }
1253
+
1254
+ .wb-ctxstate .wb-state-item {
1255
+ gap: 6px;
1256
+ }
1257
+
1258
+ .wb-ctxstate-input {
1259
+ width: 90px;
1260
+ padding: 1px 6px;
1261
+ border: 1px solid var(--border);
1262
+ border-radius: 5px;
1263
+ background: var(--bg);
1264
+ color: var(--text);
1265
+ font: inherit;
1266
+ font-weight: 600;
1267
+ }
1268
+
1269
+ .wb-ctxstate-input[type='number'] {
1270
+ width: 60px;
1271
+ }
1272
+
1273
+ .wb-ctxstate-input:focus {
1274
+ outline: none;
1275
+ border-color: var(--accent-ui);
1276
+ }
1277
+
1245
1278
  .wb-tab {
1246
1279
  height: 100%;
1247
1280
  padding: 0 1px;
@@ -1359,7 +1392,7 @@ button {
1359
1392
  [data-theme="dark"] .wb-save {
1360
1393
  background: var(--accent-ui);
1361
1394
  border-color: var(--accent-ui);
1362
- color: #111113;
1395
+ color: var(--accent-contrast);
1363
1396
  }
1364
1397
 
1365
1398
  .wb-save:disabled {
@@ -1401,8 +1434,8 @@ button {
1401
1434
  font-size: 10.5px;
1402
1435
  line-height: 1.6;
1403
1436
  color: var(--danger);
1404
- background: rgba(192, 68, 60, 0.07);
1405
- border: 1px solid rgba(192, 68, 60, 0.25);
1437
+ background: color-mix(in srgb, var(--danger) 7%, transparent);
1438
+ border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
1406
1439
  border-radius: var(--r);
1407
1440
  padding: 8px 10px;
1408
1441
  white-space: pre-wrap;
@@ -1411,11 +1444,7 @@ button {
1411
1444
  .wb-saved-flash {
1412
1445
  font-size: 11.5px;
1413
1446
  font-weight: 550;
1414
- color: #3c7a4a;
1415
- }
1416
-
1417
- [data-theme="dark"] .wb-saved-flash {
1418
- color: #84c896;
1447
+ color: var(--success);
1419
1448
  }
1420
1449
 
1421
1450
  .wb-schema {
@@ -1632,11 +1661,7 @@ button {
1632
1661
  color: var(--text);
1633
1662
  }
1634
1663
  .wb-copy.copied {
1635
- color: #3c7a4a;
1636
- }
1637
-
1638
- [data-theme="dark"] .wb-copy.copied {
1639
- color: #84c896;
1664
+ color: var(--success);
1640
1665
  }
1641
1666
 
1642
1667
  /* ---------- Variants gallery ---------- */
@@ -1885,6 +1910,10 @@ select.wb-ctrl-input {
1885
1910
  background-position: right 8px center;
1886
1911
  }
1887
1912
 
1913
+ [data-theme="dark"] select.wb-ctrl-input {
1914
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239b9ba2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
1915
+ }
1916
+
1888
1917
  .wb-ctrl-row input[type="range"] {
1889
1918
  flex: 1;
1890
1919
  accent-color: var(--accent-ui);
@@ -1935,7 +1964,7 @@ select.wb-ctrl-input {
1935
1964
  }
1936
1965
 
1937
1966
  [data-theme="dark"] .wb-switch.on::after {
1938
- background: #111113;
1967
+ background: var(--accent-contrast);
1939
1968
  }
1940
1969
 
1941
1970
  .wb-context-empty {
@@ -2050,21 +2079,17 @@ select.wb-ctrl-input {
2050
2079
  color: var(--accent-ui);
2051
2080
  }
2052
2081
  .wb-log.warn .llevel {
2053
- color: #99661c;
2082
+ color: var(--warn);
2054
2083
  }
2055
2084
  .wb-log.error .llevel {
2056
2085
  color: var(--danger);
2057
2086
  }
2058
2087
 
2059
- [data-theme="dark"] .wb-log.warn .llevel {
2060
- color: #dcb064;
2061
- }
2062
-
2063
2088
  .wb-log.warn {
2064
- background: rgba(153, 102, 28, 0.05);
2089
+ background: color-mix(in srgb, var(--warn) 5%, transparent);
2065
2090
  }
2066
2091
  .wb-log.error {
2067
- background: rgba(192, 68, 60, 0.05);
2092
+ background: color-mix(in srgb, var(--danger) 5%, transparent);
2068
2093
  }
2069
2094
 
2070
2095
  .wb-log .lmsg {
@@ -2098,8 +2123,8 @@ select.wb-ctrl-input {
2098
2123
  margin: 14px 16px;
2099
2124
  padding: 12px 14px;
2100
2125
  border-radius: 10px;
2101
- background: rgba(192, 68, 60, 0.07);
2102
- border: 1px solid rgba(192, 68, 60, 0.25);
2126
+ background: color-mix(in srgb, var(--danger) 7%, transparent);
2127
+ border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
2103
2128
  }
2104
2129
 
2105
2130
  .wb-render-error .rtitle {
@@ -2147,8 +2172,8 @@ select.wb-ctrl-input {
2147
2172
  margin: 12px 16px 0 16px;
2148
2173
  padding: 8px 11px;
2149
2174
  border-radius: var(--r);
2150
- background: rgba(168, 67, 58, 0.06);
2151
- border: 1px solid rgba(168, 67, 58, 0.22);
2175
+ background: color-mix(in srgb, var(--deprecated) 6%, transparent);
2176
+ border: 1px solid color-mix(in srgb, var(--deprecated) 22%, transparent);
2152
2177
  color: var(--danger);
2153
2178
  font-size: 12px;
2154
2179
  line-height: 1.45;
@@ -45,10 +45,18 @@ export interface BridgeApi {
45
45
  stateValues: Record<string, Record<string, unknown>>
46
46
  /** Live `disabled` flag per action id (keyed windo id → action id), echoed from the preview. */
47
47
  actionDisabled: Record<string, Record<string, boolean>>
48
+ /** Config seed for the shared `ctxState` (from the manifest) — fills keys the chrome hasn't persisted. */
49
+ ctxStateDefaults: Record<string, unknown>
50
+ /** Latest shared-state snapshot echoed after a component wrote it via `ctx.setCtxState`. */
51
+ ctxState: Record<string, unknown>
52
+ /** Latest colour scheme a component pushed via `ctx.toggleTheme`/`setColorScheme` (null until one does). Re-wrapped each echo so repeated same-value pushes still propagate. */
53
+ colorScheme: { value: 'light' | 'dark' } | null
48
54
  // outbound
49
55
  select: (id: string) => void
50
56
  setProps: (id: string, json: string) => void
51
57
  setEnv: (env: WindoEnvState) => void
58
+ /** Push the shared state down to the preview (editor edit or reload re-sync). */
59
+ setCtxState: (state: Record<string, unknown>) => void
52
60
  invokeAction: (id: string, actionId: string) => void
53
61
  }
54
62
 
@@ -146,6 +154,10 @@ export interface InspectorProps {
146
154
  clearLogs: () => void
147
155
  /** Live component-local state snapshot for the selected windo (drives the read-only State strip). */
148
156
  state: Record<string, unknown>
157
+ /** Shared, cross-component state — drives the editable Shared strip. */
158
+ ctxState: Record<string, unknown>
159
+ /** Edit one key of the shared state (pushes down to the preview). */
160
+ setCtxStateValue: (key: string, value: unknown) => void
149
161
  /** Context metadata filtered to this windo (its `uses` providers + all ambient). */
150
162
  contexts: WindoContextMeta[]
151
163
  env: ChromeEnv
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  WINDO_MSG,
15
15
  } from './protocol'
16
16
  export type {
17
+ Ctxual,
17
18
  WindoAction,
18
19
  WindoActionMeta,
19
20
  WindoActionTrigger,
@@ -34,6 +35,7 @@ export type {
34
35
  WindoFactoryArg,
35
36
  WindoFieldError,
36
37
  WindoGroup,
38
+ WindoInitContext,
37
39
  WindoLogEntry,
38
40
  WindoLogger,
39
41
  WindoManifestEntry,