@westopp/windo 0.1.1 → 0.1.3
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/{chunk-5RM2VYAM.js → chunk-IBJ6MG77.js} +2 -1
- package/dist/chunk-IBJ6MG77.js.map +1 -0
- package/dist/cli.cjs +2 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -10
- package/dist/index.d.ts +53 -10
- package/dist/index.js.map +1 -1
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +64 -1
- package/src/client/Inspector.tsx +37 -0
- package/src/client/bridge.ts +24 -0
- package/src/client/chrome.css +72 -47
- package/src/client/internal-types.ts +14 -0
- package/src/index.ts +2 -0
- package/src/plugin/index.ts +1 -0
- package/src/preview/ctx.ts +8 -1
- package/src/preview/index.ts +153 -10
- package/src/preview/render.tsx +5 -3
- package/src/protocol.ts +7 -0
- package/src/types.ts +35 -9
- package/dist/chunk-5RM2VYAM.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -93,6 +93,20 @@ interface WindoRenderContext<State = unknown> {
|
|
|
93
93
|
setState: (patch: Partial<State>) => void;
|
|
94
94
|
/** Resolved values of opted-in contexts, keyed by context name. */
|
|
95
95
|
contexts: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Shared, cross-component state seeded from the config's `ctxState`. Unlike
|
|
98
|
+
* `state` (per-windo, reset on selection), this persists across selection and
|
|
99
|
+
* is global: any component can read it and write it via `setCtxState`. Use it
|
|
100
|
+
* to drive providers that wrap every component — e.g. a theme provider toggled
|
|
101
|
+
* from any component on the canvas.
|
|
102
|
+
*/
|
|
103
|
+
ctxState: Record<string, unknown>;
|
|
104
|
+
/** Merge a patch into the shared `ctxState` and re-render every consumer. */
|
|
105
|
+
setCtxState: (patch: Record<string, unknown>) => void;
|
|
106
|
+
/** Set the canvas colour scheme from a component or action. */
|
|
107
|
+
setColorScheme: (scheme: 'light' | 'dark') => void;
|
|
108
|
+
/** Flip the canvas colour scheme between light and dark. */
|
|
109
|
+
toggleTheme: () => void;
|
|
96
110
|
}
|
|
97
111
|
/** A variant: a label plus a partial prop patch. Renders in the gallery and is click-to-apply. */
|
|
98
112
|
interface WindoVariant<Props> {
|
|
@@ -106,7 +120,11 @@ interface WindoPropDoc {
|
|
|
106
120
|
default?: string;
|
|
107
121
|
desc?: string;
|
|
108
122
|
}
|
|
109
|
-
|
|
123
|
+
/** A value that may be authored statically or as a context-aware `ctx => value` function resolved at render-time. */
|
|
124
|
+
type Ctxual<T, State = unknown> = T | ((ctx: WindoRenderContext<State>) => T);
|
|
125
|
+
/** 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. */
|
|
126
|
+
type WindoInitContext<State = unknown> = Omit<WindoRenderContext<State>, 'state' | 'setState'>;
|
|
127
|
+
type WindoDefaultProps<Props, State = unknown> = Ctxual<Props, State>;
|
|
110
128
|
/**
|
|
111
129
|
* The object returned by a `windo(...)` factory.
|
|
112
130
|
*
|
|
@@ -123,9 +141,10 @@ interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends st
|
|
|
123
141
|
status?: WindoStatus;
|
|
124
142
|
description?: string;
|
|
125
143
|
deprecation?: string;
|
|
126
|
-
placement
|
|
127
|
-
|
|
128
|
-
state
|
|
144
|
+
/** Where the component anchors in the canvas frame. Either a static placement or a function resolved with the live `ctx`. */
|
|
145
|
+
placement?: Ctxual<WindoPlacement, State>;
|
|
146
|
+
/** 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. */
|
|
147
|
+
state?: State | ((ctx: WindoInitContext<State>) => State);
|
|
129
148
|
/** Out-of-band actions that drive state: toolbar buttons (`click`) and stage pointer triggers (`enter`/`exit`/`hover`). */
|
|
130
149
|
actions?: WindoAction<State>[];
|
|
131
150
|
/** zod schema: validator + parser for the JSON-editable prop subset. `z.output ⊆ Props`. */
|
|
@@ -134,11 +153,12 @@ interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends st
|
|
|
134
153
|
defaultProps: WindoDefaultProps<Props, State>;
|
|
135
154
|
/** Names of provider contexts this component opts into. */
|
|
136
155
|
uses?: string[];
|
|
137
|
-
variants
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
code
|
|
156
|
+
/** Gallery variants. Either a static array or a function resolved with the live `ctx` when the component is selected. */
|
|
157
|
+
variants?: Ctxual<WindoVariant<Props>[], State>;
|
|
158
|
+
/** 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. */
|
|
159
|
+
props?: Ctxual<WindoPropDoc[], State>;
|
|
160
|
+
/** Optional authored code snippet for the Code tab, resolved with the JSON-editable values and the live `ctx`. */
|
|
161
|
+
code?: (values: Props, ctx: WindoRenderContext<State>) => string;
|
|
142
162
|
/** A local provider wrapping just this windo (in addition to `uses`). */
|
|
143
163
|
providers?: ComponentType<{
|
|
144
164
|
children: ReactNode;
|
|
@@ -169,10 +189,14 @@ interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup
|
|
|
169
189
|
contexts?: Contexts;
|
|
170
190
|
/** The set of tags components may be assigned. A component's `tags` must be drawn from this list; the sidebar filters by them. */
|
|
171
191
|
tags?: Tags;
|
|
192
|
+
/** Initial shared state exposed on `ctx.ctxState`. Global across every component and persisted across selection — write it from any component via `ctx.setCtxState`. */
|
|
193
|
+
ctxState?: Record<string, unknown>;
|
|
172
194
|
/** Glob(s) for discovery, relative to project root. Default `**\/*.windo.tsx`. */
|
|
173
195
|
include?: string | string[];
|
|
174
196
|
/** Title shown in the workbench chrome. */
|
|
175
197
|
title?: string;
|
|
198
|
+
/** Icon URL for the workbench logo and the browser favicon. When set, it replaces the built-in `wn` mark in both places. Any `<img>`/favicon source — a path, URL, or data URI. */
|
|
199
|
+
faviconUrl?: string;
|
|
176
200
|
}
|
|
177
201
|
type WindoControlKind = 'string' | 'number' | 'boolean' | 'enum' | 'date' | 'array' | 'object' | 'unknown';
|
|
178
202
|
interface WindoControlDescriptor {
|
|
@@ -309,6 +333,11 @@ type WindoHostMessage = {
|
|
|
309
333
|
dir: 'host';
|
|
310
334
|
type: 'set-env';
|
|
311
335
|
env: WindoEnvState;
|
|
336
|
+
} | {
|
|
337
|
+
source: typeof WINDO_MSG;
|
|
338
|
+
dir: 'host';
|
|
339
|
+
type: 'set-ctx-state';
|
|
340
|
+
state: Record<string, unknown>;
|
|
312
341
|
} | {
|
|
313
342
|
source: typeof WINDO_MSG;
|
|
314
343
|
dir: 'host';
|
|
@@ -326,10 +355,14 @@ type WindoPreviewMessage = {
|
|
|
326
355
|
dir: 'preview';
|
|
327
356
|
type: 'manifest';
|
|
328
357
|
title: string;
|
|
358
|
+
/** Config `faviconUrl` — drives the chrome logo + browser favicon. `null` falls back to the built-in `wn` mark. */
|
|
359
|
+
faviconUrl: string | null;
|
|
329
360
|
entries: WindoManifestEntry[];
|
|
330
361
|
groups: WindoGroup[];
|
|
331
362
|
tags: string[];
|
|
332
363
|
contexts: WindoContextMeta[];
|
|
364
|
+
/** Initial shared state from the config — seeds the chrome's editable strip. */
|
|
365
|
+
ctxState: Record<string, unknown>;
|
|
333
366
|
} | {
|
|
334
367
|
source: typeof WINDO_MSG;
|
|
335
368
|
dir: 'preview';
|
|
@@ -366,6 +399,16 @@ type WindoPreviewMessage = {
|
|
|
366
399
|
id: string;
|
|
367
400
|
disabled: boolean;
|
|
368
401
|
}[];
|
|
402
|
+
} | {
|
|
403
|
+
source: typeof WINDO_MSG;
|
|
404
|
+
dir: 'preview';
|
|
405
|
+
type: 'ctx-state';
|
|
406
|
+
state: Record<string, unknown>;
|
|
407
|
+
} | {
|
|
408
|
+
source: typeof WINDO_MSG;
|
|
409
|
+
dir: 'preview';
|
|
410
|
+
type: 'color-scheme';
|
|
411
|
+
colorScheme: 'light' | 'dark';
|
|
369
412
|
} | {
|
|
370
413
|
source: typeof WINDO_MSG;
|
|
371
414
|
dir: 'preview';
|
|
@@ -379,4 +422,4 @@ declare function isWindoMessage(data: unknown): data is WindoMessage;
|
|
|
379
422
|
declare function isHostMessage(msg: WindoMessage): msg is WindoHostMessage;
|
|
380
423
|
declare function isPreviewMessage(msg: WindoMessage): msg is WindoPreviewMessage;
|
|
381
424
|
|
|
382
|
-
export { type DefineWindoConfigResult, WINDO_ACTION_TRIGGERS, WINDO_MSG, WINDO_PLACEMENTS, WINDO_STATUSES, type WindoAction, type WindoActionMeta, type WindoActionTrigger, type WindoConfig, type WindoContextControlMeta, type WindoContextDefinition, type WindoContextMap, type WindoContextMeta, type WindoControlDescriptor, type WindoControlKind, type WindoControlMap, type WindoControlSpec, type WindoControlType, type WindoControlValues, type WindoDefaultProps, type WindoDefinition, type WindoEnvState, type WindoFactoryArg, type WindoFieldError, type WindoGroup, type WindoHostMessage, type WindoLogEntry, type WindoLogger, type WindoManifestEntry, type WindoMessage, type WindoModule, type WindoPlacement, type WindoPreviewMessage, type WindoPropDoc, type WindoRenderContext, type WindoSchemaDescriptor, type WindoStatus, type WindoVariant, type WindoVariantMeta, type WindoViewport, configurableProps, defineContext, defineWindoConfig, describeSchema, isHostMessage, isPreviewMessage, isWindoMessage };
|
|
425
|
+
export { type Ctxual, type DefineWindoConfigResult, WINDO_ACTION_TRIGGERS, WINDO_MSG, WINDO_PLACEMENTS, WINDO_STATUSES, type WindoAction, type WindoActionMeta, type WindoActionTrigger, type WindoConfig, type WindoContextControlMeta, type WindoContextDefinition, type WindoContextMap, type WindoContextMeta, type WindoControlDescriptor, type WindoControlKind, type WindoControlMap, type WindoControlSpec, type WindoControlType, type WindoControlValues, type WindoDefaultProps, type WindoDefinition, type WindoEnvState, type WindoFactoryArg, type WindoFieldError, type WindoGroup, type WindoHostMessage, type WindoInitContext, type WindoLogEntry, type WindoLogger, type WindoManifestEntry, type WindoMessage, type WindoModule, type WindoPlacement, type WindoPreviewMessage, type WindoPropDoc, type WindoRenderContext, type WindoSchemaDescriptor, type WindoStatus, type WindoVariant, type WindoVariantMeta, type WindoViewport, configurableProps, defineContext, defineWindoConfig, describeSchema, isHostMessage, isPreviewMessage, isWindoMessage };
|
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 /** Config `faviconUrl` — drives the chrome logo + browser favicon. `null` falls back to the built-in `wn` mark. */\n faviconUrl: string | null\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 /** Icon URL for the workbench logo and the browser favicon. When set, it replaces the built-in `wn` mark in both places. Any `<img>`/favicon source — a path, URL, or data URI. */\n faviconUrl?: 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":[]}
|
package/dist/plugin.cjs
CHANGED
|
@@ -60,6 +60,7 @@ var CHROME_HTML = `<!doctype html>
|
|
|
60
60
|
<meta charset="utf-8" />
|
|
61
61
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
62
62
|
<title>windo</title>
|
|
63
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%229%22%20fill%3D%22%23d83a2e%22%2F%3E%3Ctext%20x%3D%2216%22%20y%3D%2222%22%20text-anchor%3D%22middle%22%20font-size%3D%2216%22%20font-weight%3D%22800%22%20font-family%3D%22system-ui%2C-apple-system%2CHelvetica%2CArial%2Csans-serif%22%20fill%3D%22%23f5c518%22%3Ewn%3C%2Ftext%3E%3C%2Fsvg%3E" />
|
|
63
64
|
</head>
|
|
64
65
|
<body>
|
|
65
66
|
<div id="root"></div>
|
package/dist/plugin.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin/index.ts"],"sourcesContent":["// Vite plugin (node side). Pairs `@vitejs/plugin-react` with the windo plugin:\n// serves the chrome + iframe HTML documents ourselves (appType custom) and\n// exposes a `virtual:windo-registry` module that re-exports the user config and\n// glob-imports every `*.windo.tsx` file.\n\nimport { existsSync } from 'node:fs'\nimport { isAbsolute, resolve as resolvePath } from 'node:path'\nimport reactPlugin from '@vitejs/plugin-react'\nimport type { Plugin, PluginOption, UserConfig, ViteDevServer } from 'vite'\n\nexport interface WindoPluginOptions {\n include?: string | string[]\n config?: string\n root?: string\n}\n\nconst VIRTUAL_ID = 'virtual:windo-registry'\nconst RESOLVED_VIRTUAL_ID = `\\0${VIRTUAL_ID}`\n\nconst CONFIG_CANDIDATES = ['windo.config.ts', 'windo.config.tsx', 'windo.config.mts', 'windo.config.js', 'windo.config.mjs'] as const\n\nfunction findConfig(root: string, configOpt?: string): string | null {\n if (configOpt) return isAbsolute(configOpt) ? configOpt : resolvePath(root, configOpt)\n for (const candidate of CONFIG_CANDIDATES) {\n const full = resolvePath(root, candidate)\n if (existsSync(full)) return full\n }\n return null\n}\n\nfunction toGlob(include?: string | string[]): string {\n const first = Array.isArray(include) ? include[0] : include\n const pattern = first ?? '**/*.windo.tsx'\n return pattern.startsWith('/') ? pattern : `/${pattern}`\n}\n\nconst CHROME_HTML = `<!doctype html>\n<html data-theme=\"light\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>windo</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">\n import('@westopp/windo/client')\n </script>\n </body>\n</html>\n`\n\nconst IFRAME_HTML = `<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body style=\"background: transparent\">\n <div id=\"root\"></div>\n <script type=\"module\">\n import('@westopp/windo/preview')\n </script>\n </body>\n</html>\n`\n\n// Absolute ids for the two HTML entry documents, relative to the project root.\n// They are virtual — the files need not exist on disk; the plugin's `load`\n// returns their contents. Keeping them rooted at `<root>/index.html` and\n// `<root>/__windo/iframe.html` makes `vite build` emit them at the matching\n// paths in the output directory (Vite derives an HTML asset's output path from\n// the input id's path relative to root).\nfunction htmlIds(root: string) {\n return {\n index: resolvePath(root, 'index.html'),\n iframe: resolvePath(root, '__windo/iframe.html'),\n }\n}\n\nexport function windo(options: WindoPluginOptions = {}): PluginOption[] {\n let resolvedRoot = process.cwd()\n let ids = htmlIds(resolvedRoot)\n\n const registryModule = (): string => {\n const root = options.root ?? resolvedRoot ?? process.cwd()\n const configPath = findConfig(root, options.config)\n const glob = toGlob(options.include)\n const importPath = configPath ? JSON.stringify(configPath) : 'null'\n const globLiteral = JSON.stringify(glob)\n return [\n `import * as __cfg from ${importPath}`,\n 'const config = __cfg.config ?? (__cfg.default && __cfg.default.config) ?? __cfg.default',\n 'export { config }',\n `export const modules = import.meta.glob(${globLiteral})`,\n '',\n ].join('\\n')\n }\n\n const plugin: Plugin = {\n name: 'windo',\n // `pre` so our resolveId/load claim the virtual registry and the two HTML\n // entries before Vite's filesystem fallback tries (and fails) to read them.\n enforce: 'pre',\n\n config(userConfig: UserConfig, { command, isPreview }) {\n const root = resolvePath(options.root ?? userConfig.root ?? '.')\n ids = htmlIds(root)\n const base: UserConfig = {\n // Dev serves the chrome + iframe HTML from `configureServer`, so it runs\n // headless (`custom`). Build and preview want the normal SPA behaviour so\n // `/` resolves to the emitted index.html.\n appType: command === 'serve' && !isPreview ? 'custom' : 'spa',\n define: {\n __WINDO_IFRAME_SRC__: JSON.stringify(command === 'build' ? './__windo/iframe.html' : './__windo/iframe'),\n },\n resolve: {\n dedupe: ['react', 'react-dom'],\n },\n optimizeDeps: {\n // Pre-bundle every React entrypoint in one pass so React's identity is\n // fixed up front. If `react-dom/client` (the preview renderer) is\n // discovered late, Vite re-optimizes and can split React across two\n // instances — fine until a component calls a hook, then \"invalid hook\n // call\". Keeping them all here prevents that re-optimize.\n include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],\n },\n }\n\n // Wire the chrome + iframe as build inputs unless the user supplied their\n // own. Both are emitted as static HTML the canvas can be hosted from.\n if (command === 'build' && !userConfig.build?.rollupOptions?.input) {\n base.build = { rollupOptions: { input: { index: ids.index, iframe: ids.iframe } } }\n }\n return base\n },\n\n configResolved(resolved) {\n resolvedRoot = resolved.root\n ids = htmlIds(resolvedRoot)\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID\n if (id === ids.index || id === ids.iframe) return id\n return null\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) return registryModule()\n if (id === ids.index) return CHROME_HTML\n if (id === ids.iframe) return IFRAME_HTML\n return null\n },\n\n configureServer(server: ViteDevServer) {\n server.middlewares.use(async (req, res, next) => {\n if (req.method !== 'GET') return next()\n const url = (req.url ?? '').split('?')[0]\n let html: string | null = null\n if (url === '/' || url === '/index.html') html = CHROME_HTML\n else if (url.startsWith('/__windo/iframe')) html = IFRAME_HTML\n if (html === null) return next()\n try {\n const transformed = await server.transformIndexHtml(url, html)\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html')\n res.end(transformed)\n } catch (err) {\n next(err)\n }\n })\n },\n }\n\n return [reactPlugin(), plugin]\n}\n\nexport const windoPlugin = windo\n\nexport default windo\n"],"mappings":"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","names":["resolvePath","reactPlugin"]}
|
|
1
|
+
{"version":3,"sources":["../src/plugin/index.ts"],"sourcesContent":["// Vite plugin (node side). Pairs `@vitejs/plugin-react` with the windo plugin:\n// serves the chrome + iframe HTML documents ourselves (appType custom) and\n// exposes a `virtual:windo-registry` module that re-exports the user config and\n// glob-imports every `*.windo.tsx` file.\n\nimport { existsSync } from 'node:fs'\nimport { isAbsolute, resolve as resolvePath } from 'node:path'\nimport reactPlugin from '@vitejs/plugin-react'\nimport type { Plugin, PluginOption, UserConfig, ViteDevServer } from 'vite'\n\nexport interface WindoPluginOptions {\n include?: string | string[]\n config?: string\n root?: string\n}\n\nconst VIRTUAL_ID = 'virtual:windo-registry'\nconst RESOLVED_VIRTUAL_ID = `\\0${VIRTUAL_ID}`\n\nconst CONFIG_CANDIDATES = ['windo.config.ts', 'windo.config.tsx', 'windo.config.mts', 'windo.config.js', 'windo.config.mjs'] as const\n\nfunction findConfig(root: string, configOpt?: string): string | null {\n if (configOpt) return isAbsolute(configOpt) ? configOpt : resolvePath(root, configOpt)\n for (const candidate of CONFIG_CANDIDATES) {\n const full = resolvePath(root, candidate)\n if (existsSync(full)) return full\n }\n return null\n}\n\nfunction toGlob(include?: string | string[]): string {\n const first = Array.isArray(include) ? include[0] : include\n const pattern = first ?? '**/*.windo.tsx'\n return pattern.startsWith('/') ? pattern : `/${pattern}`\n}\n\nconst CHROME_HTML = `<!doctype html>\n<html data-theme=\"light\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>windo</title>\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%229%22%20fill%3D%22%23d83a2e%22%2F%3E%3Ctext%20x%3D%2216%22%20y%3D%2222%22%20text-anchor%3D%22middle%22%20font-size%3D%2216%22%20font-weight%3D%22800%22%20font-family%3D%22system-ui%2C-apple-system%2CHelvetica%2CArial%2Csans-serif%22%20fill%3D%22%23f5c518%22%3Ewn%3C%2Ftext%3E%3C%2Fsvg%3E\" />\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">\n import('@westopp/windo/client')\n </script>\n </body>\n</html>\n`\n\nconst IFRAME_HTML = `<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body style=\"background: transparent\">\n <div id=\"root\"></div>\n <script type=\"module\">\n import('@westopp/windo/preview')\n </script>\n </body>\n</html>\n`\n\n// Absolute ids for the two HTML entry documents, relative to the project root.\n// They are virtual — the files need not exist on disk; the plugin's `load`\n// returns their contents. Keeping them rooted at `<root>/index.html` and\n// `<root>/__windo/iframe.html` makes `vite build` emit them at the matching\n// paths in the output directory (Vite derives an HTML asset's output path from\n// the input id's path relative to root).\nfunction htmlIds(root: string) {\n return {\n index: resolvePath(root, 'index.html'),\n iframe: resolvePath(root, '__windo/iframe.html'),\n }\n}\n\nexport function windo(options: WindoPluginOptions = {}): PluginOption[] {\n let resolvedRoot = process.cwd()\n let ids = htmlIds(resolvedRoot)\n\n const registryModule = (): string => {\n const root = options.root ?? resolvedRoot ?? process.cwd()\n const configPath = findConfig(root, options.config)\n const glob = toGlob(options.include)\n const importPath = configPath ? JSON.stringify(configPath) : 'null'\n const globLiteral = JSON.stringify(glob)\n return [\n `import * as __cfg from ${importPath}`,\n 'const config = __cfg.config ?? (__cfg.default && __cfg.default.config) ?? __cfg.default',\n 'export { config }',\n `export const modules = import.meta.glob(${globLiteral})`,\n '',\n ].join('\\n')\n }\n\n const plugin: Plugin = {\n name: 'windo',\n // `pre` so our resolveId/load claim the virtual registry and the two HTML\n // entries before Vite's filesystem fallback tries (and fails) to read them.\n enforce: 'pre',\n\n config(userConfig: UserConfig, { command, isPreview }) {\n const root = resolvePath(options.root ?? userConfig.root ?? '.')\n ids = htmlIds(root)\n const base: UserConfig = {\n // Dev serves the chrome + iframe HTML from `configureServer`, so it runs\n // headless (`custom`). Build and preview want the normal SPA behaviour so\n // `/` resolves to the emitted index.html.\n appType: command === 'serve' && !isPreview ? 'custom' : 'spa',\n define: {\n __WINDO_IFRAME_SRC__: JSON.stringify(command === 'build' ? './__windo/iframe.html' : './__windo/iframe'),\n },\n resolve: {\n dedupe: ['react', 'react-dom'],\n },\n optimizeDeps: {\n // Pre-bundle every React entrypoint in one pass so React's identity is\n // fixed up front. If `react-dom/client` (the preview renderer) is\n // discovered late, Vite re-optimizes and can split React across two\n // instances — fine until a component calls a hook, then \"invalid hook\n // call\". Keeping them all here prevents that re-optimize.\n include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],\n },\n }\n\n // Wire the chrome + iframe as build inputs unless the user supplied their\n // own. Both are emitted as static HTML the canvas can be hosted from.\n if (command === 'build' && !userConfig.build?.rollupOptions?.input) {\n base.build = { rollupOptions: { input: { index: ids.index, iframe: ids.iframe } } }\n }\n return base\n },\n\n configResolved(resolved) {\n resolvedRoot = resolved.root\n ids = htmlIds(resolvedRoot)\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID\n if (id === ids.index || id === ids.iframe) return id\n return null\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) return registryModule()\n if (id === ids.index) return CHROME_HTML\n if (id === ids.iframe) return IFRAME_HTML\n return null\n },\n\n configureServer(server: ViteDevServer) {\n server.middlewares.use(async (req, res, next) => {\n if (req.method !== 'GET') return next()\n const url = (req.url ?? '').split('?')[0]\n let html: string | null = null\n if (url === '/' || url === '/index.html') html = CHROME_HTML\n else if (url.startsWith('/__windo/iframe')) html = IFRAME_HTML\n if (html === null) return next()\n try {\n const transformed = await server.transformIndexHtml(url, html)\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html')\n res.end(transformed)\n } catch (err) {\n next(err)\n }\n })\n },\n }\n\n return [reactPlugin(), plugin]\n}\n\nexport const windoPlugin = windo\n\nexport default windo\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,qBAA2B;AAC3B,uBAAmD;AACnD,0BAAwB;AASxB,IAAM,aAAa;AACnB,IAAM,sBAAsB,KAAK,UAAU;AAE3C,IAAM,oBAAoB,CAAC,mBAAmB,oBAAoB,oBAAoB,mBAAmB,kBAAkB;AAE3H,SAAS,WAAW,MAAc,WAAmC;AACnE,MAAI,UAAW,YAAO,6BAAW,SAAS,IAAI,gBAAY,iBAAAA,SAAY,MAAM,SAAS;AACrF,aAAW,aAAa,mBAAmB;AACzC,UAAM,WAAO,iBAAAA,SAAY,MAAM,SAAS;AACxC,YAAI,2BAAW,IAAI,EAAG,QAAO;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,SAAS,OAAO,SAAqC;AACnD,QAAM,QAAQ,MAAM,QAAQ,OAAO,IAAI,QAAQ,CAAC,IAAI;AACpD,QAAM,UAAU,SAAS;AACzB,SAAO,QAAQ,WAAW,GAAG,IAAI,UAAU,IAAI,OAAO;AACxD;AAEA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBpB,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBpB,SAAS,QAAQ,MAAc;AAC7B,SAAO;AAAA,IACL,WAAO,iBAAAA,SAAY,MAAM,YAAY;AAAA,IACrC,YAAQ,iBAAAA,SAAY,MAAM,qBAAqB;AAAA,EACjD;AACF;AAEO,SAAS,MAAM,UAA8B,CAAC,GAAmB;AACtE,MAAI,eAAe,QAAQ,IAAI;AAC/B,MAAI,MAAM,QAAQ,YAAY;AAE9B,QAAM,iBAAiB,MAAc;AACnC,UAAM,OAAO,QAAQ,QAAQ,gBAAgB,QAAQ,IAAI;AACzD,UAAM,aAAa,WAAW,MAAM,QAAQ,MAAM;AAClD,UAAM,OAAO,OAAO,QAAQ,OAAO;AACnC,UAAM,aAAa,aAAa,KAAK,UAAU,UAAU,IAAI;AAC7D,UAAM,cAAc,KAAK,UAAU,IAAI;AACvC,WAAO;AAAA,MACL,0BAA0B,UAAU;AAAA,MACpC;AAAA,MACA;AAAA,MACA,2CAA2C,WAAW;AAAA,MACtD;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,EACb;AAEA,QAAM,SAAiB;AAAA,IACrB,MAAM;AAAA;AAAA;AAAA,IAGN,SAAS;AAAA,IAET,OAAO,YAAwB,EAAE,SAAS,UAAU,GAAG;AACrD,YAAM,WAAO,iBAAAA,SAAY,QAAQ,QAAQ,WAAW,QAAQ,GAAG;AAC/D,YAAM,QAAQ,IAAI;AAClB,YAAM,OAAmB;AAAA;AAAA;AAAA;AAAA,QAIvB,SAAS,YAAY,WAAW,CAAC,YAAY,WAAW;AAAA,QACxD,QAAQ;AAAA,UACN,sBAAsB,KAAK,UAAU,YAAY,UAAU,0BAA0B,kBAAkB;AAAA,QACzG;AAAA,QACA,SAAS;AAAA,UACP,QAAQ,CAAC,SAAS,WAAW;AAAA,QAC/B;AAAA,QACA,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMZ,SAAS,CAAC,SAAS,aAAa,oBAAoB,qBAAqB,uBAAuB;AAAA,QAClG;AAAA,MACF;AAIA,UAAI,YAAY,WAAW,CAAC,WAAW,OAAO,eAAe,OAAO;AAClE,aAAK,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,IAAI,OAAO,EAAE,EAAE;AAAA,MACpF;AACA,aAAO;AAAA,IACT;AAAA,IAEA,eAAe,UAAU;AACvB,qBAAe,SAAS;AACxB,YAAM,QAAQ,YAAY;AAAA,IAC5B;AAAA,IAEA,UAAU,IAAI;AACZ,UAAI,OAAO,WAAY,QAAO;AAC9B,UAAI,OAAO,IAAI,SAAS,OAAO,IAAI,OAAQ,QAAO;AAClD,aAAO;AAAA,IACT;AAAA,IAEA,KAAK,IAAI;AACP,UAAI,OAAO,oBAAqB,QAAO,eAAe;AACtD,UAAI,OAAO,IAAI,MAAO,QAAO;AAC7B,UAAI,OAAO,IAAI,OAAQ,QAAO;AAC9B,aAAO;AAAA,IACT;AAAA,IAEA,gBAAgB,QAAuB;AACrC,aAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAC/C,YAAI,IAAI,WAAW,MAAO,QAAO,KAAK;AACtC,cAAM,OAAO,IAAI,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AACxC,YAAI,OAAsB;AAC1B,YAAI,QAAQ,OAAO,QAAQ,cAAe,QAAO;AAAA,iBACxC,IAAI,WAAW,iBAAiB,EAAG,QAAO;AACnD,YAAI,SAAS,KAAM,QAAO,KAAK;AAC/B,YAAI;AACF,gBAAM,cAAc,MAAM,OAAO,mBAAmB,KAAK,IAAI;AAC7D,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,WAAW;AACzC,cAAI,IAAI,WAAW;AAAA,QACrB,SAAS,KAAK;AACZ,eAAK,GAAG;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,KAAC,oBAAAC,SAAY,GAAG,MAAM;AAC/B;AAEO,IAAM,cAAc;AAE3B,IAAO,iBAAQ;","names":["resolvePath","reactPlugin"]}
|
package/dist/plugin.js
CHANGED
package/package.json
CHANGED
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.
|
|
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
|
package/src/client/App.tsx
CHANGED
|
@@ -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
|
|
@@ -94,6 +102,23 @@ export function App() {
|
|
|
94
102
|
document.documentElement.setAttribute('data-theme', theme)
|
|
95
103
|
saveString('windo:theme', theme)
|
|
96
104
|
}, [theme])
|
|
105
|
+
// Reflect the configured title in the browser tab once the manifest arrives
|
|
106
|
+
// (the served HTML ships a static `windo` title; this updates it after hydration).
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
document.title = bridge.title || 'windo'
|
|
109
|
+
}, [bridge.title])
|
|
110
|
+
// A configured `faviconUrl` overrides the browser favicon; with none, the built-in
|
|
111
|
+
// `wn` mark baked into the served HTML stays in place.
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!bridge.faviconUrl) return
|
|
114
|
+
let link = document.querySelector<HTMLLinkElement>("link[rel='icon']")
|
|
115
|
+
if (!link) {
|
|
116
|
+
link = document.createElement('link')
|
|
117
|
+
link.rel = 'icon'
|
|
118
|
+
document.head.appendChild(link)
|
|
119
|
+
}
|
|
120
|
+
link.href = bridge.faviconUrl
|
|
121
|
+
}, [bridge.faviconUrl])
|
|
97
122
|
useEffect(() => saveJSON('windo:selected', selected), [selected])
|
|
98
123
|
useEffect(() => saveJSON('windo:tags', selectedTags), [selectedTags])
|
|
99
124
|
useEffect(() => saveJSON('windo:tag-match', tagMatch), [tagMatch])
|
|
@@ -104,6 +129,7 @@ export function App() {
|
|
|
104
129
|
useEffect(() => saveJSON('windo:zoom', zoom), [zoom])
|
|
105
130
|
useEffect(() => saveJSON('windo:grid', grid), [grid])
|
|
106
131
|
useEffect(() => saveJSON('windo:env', env), [env])
|
|
132
|
+
useEffect(() => saveJSON('windo:ctx-state', ctxState), [ctxState])
|
|
107
133
|
|
|
108
134
|
// ── selection wiring ───────────────────────────────────────────────────
|
|
109
135
|
useEffect(() => {
|
|
@@ -143,6 +169,33 @@ export function App() {
|
|
|
143
169
|
bridge.setEnv(next)
|
|
144
170
|
}, [env, width, height, theme, bridge.setEnv, bridge.readyNonce])
|
|
145
171
|
|
|
172
|
+
// ── shared state (ctxState) sync ──────────────────────────────────────────
|
|
173
|
+
// Seed the canonical from the config defaults, filling only keys the chrome
|
|
174
|
+
// hasn't already persisted (persisted edits win).
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!Object.keys(bridge.ctxStateDefaults).length) return
|
|
177
|
+
setCtxState(prev => ({ ...bridge.ctxStateDefaults, ...prev }))
|
|
178
|
+
}, [bridge.ctxStateDefaults])
|
|
179
|
+
|
|
180
|
+
// Adopt component-driven writes echoed up from the preview (preview is the live
|
|
181
|
+
// truth for those). The chrome's own pushes don't echo, so this never loops.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!Object.keys(bridge.ctxState).length) return
|
|
184
|
+
setCtxState(prev => ({ ...prev, ...bridge.ctxState }))
|
|
185
|
+
}, [bridge.ctxState])
|
|
186
|
+
|
|
187
|
+
// Re-sync the canonical down on every (re)ready — the iframe reloads on HMR and
|
|
188
|
+
// re-seeds from config defaults, so we re-assert the persisted shared state.
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (Object.keys(ctxStateRef.current).length) bridge.setCtxState(ctxStateRef.current)
|
|
191
|
+
}, [bridge.setCtxState, bridge.readyNonce])
|
|
192
|
+
|
|
193
|
+
// Follow a component's `toggleTheme`/`setColorScheme` push (depends only on the
|
|
194
|
+
// echo, never on `theme` — so a topbar toggle is never reverted by a stale echo).
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (bridge.colorScheme) setTheme(bridge.colorScheme.value)
|
|
197
|
+
}, [bridge.colorScheme])
|
|
198
|
+
|
|
146
199
|
// ── draft commit / variant ─────────────────────────────────────────────
|
|
147
200
|
function setValuesJson(json: string) {
|
|
148
201
|
if (!selected) return
|
|
@@ -196,6 +249,14 @@ export function App() {
|
|
|
196
249
|
}))
|
|
197
250
|
}
|
|
198
251
|
|
|
252
|
+
// Edit one key of the shared state from the chrome: update the canonical and
|
|
253
|
+
// push the whole object down (the preview adopts it without echoing back).
|
|
254
|
+
function setCtxStateValue(key: string, value: unknown) {
|
|
255
|
+
const next = { ...ctxStateRef.current, [key]: value }
|
|
256
|
+
setCtxState(next)
|
|
257
|
+
bridge.setCtxState(next)
|
|
258
|
+
}
|
|
259
|
+
|
|
199
260
|
function setGrid(patch: Partial<CanvasGridOpts> | null) {
|
|
200
261
|
if (patch === null) {
|
|
201
262
|
setGridState(CANVAS_GRID_DEFAULTS)
|
|
@@ -276,6 +337,8 @@ export function App() {
|
|
|
276
337
|
logs={bridge.logs}
|
|
277
338
|
clearLogs={bridge.clearLogs}
|
|
278
339
|
state={stateForEntry}
|
|
340
|
+
ctxState={ctxState}
|
|
341
|
+
setCtxStateValue={setCtxStateValue}
|
|
279
342
|
contexts={contextsForEntry}
|
|
280
343
|
env={env}
|
|
281
344
|
setEnv={setEnvPatch}
|
|
@@ -291,7 +354,7 @@ export function App() {
|
|
|
291
354
|
<div className="wb-app" style={accentVars} data-screen-label="Workbench">
|
|
292
355
|
<header className="wb-topbar">
|
|
293
356
|
<span className="wb-logo">
|
|
294
|
-
<WindoMark />
|
|
357
|
+
{bridge.faviconUrl ? <img className="wb-logo-mark" src={bridge.faviconUrl} alt="" /> : <WindoMark />}
|
|
295
358
|
{title}
|
|
296
359
|
<span className="wb-logo-sub">/ Workbench</span>
|
|
297
360
|
</span>
|
package/src/client/Inspector.tsx
CHANGED
|
@@ -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
|
package/src/client/bridge.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
15
15
|
const [ready, setReady] = useState(false)
|
|
16
16
|
const [readyNonce, setReadyNonce] = useState(0)
|
|
17
17
|
const [title, setTitle] = useState('')
|
|
18
|
+
const [faviconUrl, setFaviconUrl] = useState<string | null>(null)
|
|
18
19
|
const [groups, setGroups] = useState<WindoGroup[]>([])
|
|
19
20
|
const [tags, setTags] = useState<string[]>([])
|
|
20
21
|
const [contexts, setContexts] = useState<WindoContextMeta[]>([])
|
|
@@ -25,6 +26,14 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
25
26
|
const [logs, setLogs] = useState<WindoLogRow[]>([])
|
|
26
27
|
const [stateValues, setStateValues] = useState<Record<string, Record<string, unknown>>>({})
|
|
27
28
|
const [actionDisabled, setActionDisabled] = useState<Record<string, Record<string, boolean>>>({})
|
|
29
|
+
// Shared state: `ctxStateDefaults` is the config seed (from the manifest);
|
|
30
|
+
// `ctxState` is the latest snapshot echoed after a component writes it.
|
|
31
|
+
const [ctxStateDefaults, setCtxStateDefaults] = useState<Record<string, unknown>>({})
|
|
32
|
+
const [ctxState, setCtxStateEcho] = useState<Record<string, unknown>>({})
|
|
33
|
+
// Latest colour scheme a component pushed via `ctx.toggleTheme`/`setColorScheme`
|
|
34
|
+
// (null until one does). Wrapped in a fresh object each echo so consumers re-fire
|
|
35
|
+
// even when the same scheme is pushed twice (e.g. after a manual revert between).
|
|
36
|
+
const [colorScheme, setColorSchemeEcho] = useState<{ value: 'light' | 'dark' } | null>(null)
|
|
28
37
|
const seqRef = useRef(0)
|
|
29
38
|
|
|
30
39
|
const post = useCallback(
|
|
@@ -50,10 +59,12 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
50
59
|
break
|
|
51
60
|
case 'manifest':
|
|
52
61
|
setTitle(msg.title)
|
|
62
|
+
setFaviconUrl(msg.faviconUrl)
|
|
53
63
|
setManifest(msg.entries)
|
|
54
64
|
setGroups(msg.groups)
|
|
55
65
|
setTags(msg.tags)
|
|
56
66
|
setContexts(msg.contexts)
|
|
67
|
+
setCtxStateDefaults(msg.ctxState)
|
|
57
68
|
break
|
|
58
69
|
case 'describe':
|
|
59
70
|
setDescribe({
|
|
@@ -82,6 +93,12 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
82
93
|
setStateValues(prev => ({ ...prev, [msg.id]: msg.state }))
|
|
83
94
|
setActionDisabled(prev => ({ ...prev, [msg.id]: Object.fromEntries(msg.actions.map(a => [a.id, a.disabled])) }))
|
|
84
95
|
break
|
|
96
|
+
case 'ctx-state':
|
|
97
|
+
setCtxStateEcho(msg.state)
|
|
98
|
+
break
|
|
99
|
+
case 'color-scheme':
|
|
100
|
+
setColorSchemeEcho({ value: msg.colorScheme })
|
|
101
|
+
break
|
|
85
102
|
case 'render-error':
|
|
86
103
|
setRenderError({ message: msg.message, stack: msg.stack })
|
|
87
104
|
break
|
|
@@ -100,6 +117,8 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
100
117
|
|
|
101
118
|
const setEnv = useCallback((env: WindoEnvState) => post({ source: WINDO_MSG, dir: 'host', type: 'set-env', env }), [post])
|
|
102
119
|
|
|
120
|
+
const setCtxState = useCallback((state: Record<string, unknown>) => post({ source: WINDO_MSG, dir: 'host', type: 'set-ctx-state', state }), [post])
|
|
121
|
+
|
|
103
122
|
const invokeAction = useCallback((id: string, actionId: string) => post({ source: WINDO_MSG, dir: 'host', type: 'invoke-action', id, actionId }), [post])
|
|
104
123
|
|
|
105
124
|
const clearLogs = useCallback(() => setLogs([]), [])
|
|
@@ -108,6 +127,7 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
108
127
|
ready,
|
|
109
128
|
readyNonce,
|
|
110
129
|
title,
|
|
130
|
+
faviconUrl,
|
|
111
131
|
groups,
|
|
112
132
|
tags,
|
|
113
133
|
contexts,
|
|
@@ -119,9 +139,13 @@ export function useWindoBridge(iframeRef: RefObject<HTMLIFrameElement | null>):
|
|
|
119
139
|
clearLogs,
|
|
120
140
|
stateValues,
|
|
121
141
|
actionDisabled,
|
|
142
|
+
ctxStateDefaults,
|
|
143
|
+
ctxState,
|
|
144
|
+
colorScheme,
|
|
122
145
|
select,
|
|
123
146
|
setProps,
|
|
124
147
|
setEnv,
|
|
148
|
+
setCtxState,
|
|
125
149
|
invokeAction,
|
|
126
150
|
}
|
|
127
151
|
}
|