@westopp/windo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/chunk-5RM2VYAM.js +150 -0
  4. package/dist/chunk-5RM2VYAM.js.map +1 -0
  5. package/dist/cli.cjs +303 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +1 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +138 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.cjs +219 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +374 -0
  14. package/dist/index.d.ts +374 -0
  15. package/dist/index.js +182 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/plugin.cjs +185 -0
  18. package/dist/plugin.cjs.map +1 -0
  19. package/dist/plugin.d.cts +11 -0
  20. package/dist/plugin.d.ts +11 -0
  21. package/dist/plugin.js +11 -0
  22. package/dist/plugin.js.map +1 -0
  23. package/package.json +95 -0
  24. package/src/cli/index.ts +160 -0
  25. package/src/client/App.tsx +310 -0
  26. package/src/client/Canvas.tsx +358 -0
  27. package/src/client/Inspector.tsx +586 -0
  28. package/src/client/Sidebar.tsx +108 -0
  29. package/src/client/bridge.ts +124 -0
  30. package/src/client/chrome.css +1966 -0
  31. package/src/client/icons.tsx +110 -0
  32. package/src/client/index.ts +15 -0
  33. package/src/client/internal-types.ts +147 -0
  34. package/src/client/persist.ts +38 -0
  35. package/src/define-config.ts +54 -0
  36. package/src/descriptor.test.ts +59 -0
  37. package/src/descriptor.ts +185 -0
  38. package/src/globals.d.ts +9 -0
  39. package/src/index.ts +54 -0
  40. package/src/plugin/index.ts +181 -0
  41. package/src/preview/ctx.ts +43 -0
  42. package/src/preview/index.ts +283 -0
  43. package/src/preview/preview.css +81 -0
  44. package/src/preview/registry.ts +159 -0
  45. package/src/preview/render.tsx +90 -0
  46. package/src/preview/virtual.d.ts +8 -0
  47. package/src/protocol.ts +59 -0
  48. package/src/types.ts +319 -0
@@ -0,0 +1,159 @@
1
+ // Pure helpers for the preview runtime — no DOM, no React. Turn module paths
2
+ // into stable ids, flatten context definitions into serialisable metadata,
3
+ // reshape zod errors for the chrome, and order manifest entries by group.
4
+
5
+ import type { z } from 'zod'
6
+ import type { WindoContextControlMeta, WindoContextMap, WindoContextMeta, WindoFieldError } from '../types'
7
+
8
+ /** Derive a stable id from a windo file path: basename minus `.windo.tsx`, kebab-cased. */
9
+ export function idFromPath(path: string): string {
10
+ const base = path.split(/[\\/]/).pop() ?? path
11
+ const name = base.replace(/\.windo\.tsx$/, '').replace(/\.windo\.(jsx|ts|js)$/, '')
12
+ return kebab(name)
13
+ }
14
+
15
+ function kebab(value: string): string {
16
+ return value
17
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
18
+ .replace(/[\s_]+/g, '-')
19
+ .replace(/[^a-zA-Z0-9-]/g, '')
20
+ .replace(/-+/g, '-')
21
+ .replace(/^-|-$/g, '')
22
+ .toLowerCase()
23
+ }
24
+
25
+ /** Flatten a context map into serialisable metadata for the chrome's Context panel. */
26
+ export function buildContextMeta(contexts: WindoContextMap): WindoContextMeta[] {
27
+ return Object.keys(contexts).map(name => {
28
+ const def = contexts[name]
29
+ const controlMap = def.controls ?? {}
30
+ const controls: WindoContextControlMeta[] = Object.keys(controlMap).map(key => {
31
+ const spec = controlMap[key]
32
+ const meta: WindoContextControlMeta = {
33
+ key,
34
+ type: spec.type,
35
+ default: spec.default,
36
+ }
37
+ if (spec.label !== undefined) meta.label = spec.label
38
+ if (spec.options !== undefined) meta.options = [...spec.options]
39
+ if (spec.min !== undefined) meta.min = spec.min
40
+ if (spec.max !== undefined) meta.max = spec.max
41
+ if (spec.step !== undefined) meta.step = spec.step
42
+ return meta
43
+ })
44
+ const meta: WindoContextMeta = {
45
+ name,
46
+ ambient: controls.length > 0,
47
+ hasProvider: !!def.provider,
48
+ controls,
49
+ }
50
+ if (def.label !== undefined) meta.label = def.label
51
+ if (def.description !== undefined) meta.description = def.description
52
+ return meta
53
+ })
54
+ }
55
+
56
+ /** Map a zod v4 error's issues into the chrome's per-field error shape. */
57
+ export function flattenZodError(err: z.ZodError): WindoFieldError[] {
58
+ return err.issues.map(issue => ({
59
+ path: issue.path.join('.'),
60
+ message: issue.message,
61
+ }))
62
+ }
63
+
64
+ /**
65
+ * Deep-convert a value to something `postMessage` can structured-clone. Functions
66
+ * and symbols are dropped; React elements become a `[node]` marker; Date/Map/Set
67
+ * are reduced to plain JSON forms; cycles are broken. Used on every payload that
68
+ * crosses the iframe boundary (variant patches, defaults, log args) so a stray
69
+ * ReactNode or SyntheticEvent can't throw a DataCloneError.
70
+ */
71
+ export function toCloneable(value: unknown): unknown {
72
+ return cloneValue(value, new WeakSet())
73
+ }
74
+
75
+ function cloneValue(value: unknown, seen: WeakSet<object>): unknown {
76
+ if (value === null) return null
77
+ const t = typeof value
78
+ if (t === 'string' || t === 'number' || t === 'boolean') return value
79
+ if (t === 'bigint') return (value as bigint).toString()
80
+ if (t === 'function' || t === 'symbol' || t === 'undefined') return undefined
81
+ const obj = value as { $$typeof?: symbol }
82
+ if (typeof obj.$$typeof === 'symbol') return '[node]'
83
+ if (value instanceof Date) return value.toISOString()
84
+ if (seen.has(value as object)) return undefined
85
+ seen.add(value as object)
86
+ if (Array.isArray(value)) return value.map(item => cloneValue(item, seen))
87
+ if (value instanceof Map) return Array.from(value.entries()).map(([k, v]) => [cloneValue(k, seen), cloneValue(v, seen)])
88
+ if (value instanceof Set) return Array.from(value.values()).map(v => cloneValue(v, seen))
89
+ const out: Record<string, unknown> = {}
90
+ for (const key of Object.keys(value as object)) {
91
+ const cloned = cloneValue((value as Record<string, unknown>)[key], seen)
92
+ if (cloned !== undefined) out[key] = cloned
93
+ }
94
+ return out
95
+ }
96
+
97
+ /**
98
+ * Summarize a single log argument for the console. Events and DOM nodes — the
99
+ * common things passed to `ctx.logger.log(e)` — collapse to compact markers
100
+ * instead of dumping a 200kB SyntheticEvent across postMessage. Plain data is
101
+ * kept (bounded in depth/breadth); functions/symbols are dropped.
102
+ */
103
+ export function summarizeLogArg(value: unknown): unknown {
104
+ return summarize(value, 0, new WeakSet())
105
+ }
106
+
107
+ const MAX_DEPTH = 4
108
+ const MAX_KEYS = 40
109
+ const MAX_ITEMS = 50
110
+
111
+ function summarize(value: unknown, depth: number, seen: WeakSet<object>): unknown {
112
+ if (value === null) return null
113
+ const t = typeof value
114
+ if (t === 'string') return (value as string).length > 2000 ? `${(value as string).slice(0, 2000)}…` : value
115
+ if (t === 'number' || t === 'boolean') return value
116
+ if (t === 'bigint') return `${(value as bigint).toString()}n`
117
+ if (t === 'function') return '[Function]'
118
+ if (t === 'symbol' || t === 'undefined') return undefined
119
+
120
+ // DOM / React event
121
+ if (typeof Event !== 'undefined' && value instanceof Event) return `[Event ${value.type}]`
122
+ const ev = value as { nativeEvent?: unknown; type?: unknown }
123
+ if (ev.nativeEvent !== undefined && typeof ev.type === 'string') return `[Event ${ev.type}]`
124
+ // DOM node
125
+ if (typeof Node !== 'undefined' && value instanceof Node) {
126
+ const el = value as { tagName?: string; nodeName?: string }
127
+ return `[<${(el.tagName ?? el.nodeName ?? 'node').toLowerCase()}>]`
128
+ }
129
+ if (value instanceof Date) return value.toISOString()
130
+ if (typeof (value as { $$typeof?: symbol }).$$typeof === 'symbol') return '[ReactElement]'
131
+
132
+ if (depth >= MAX_DEPTH) return Array.isArray(value) ? '[Array]' : '[Object]'
133
+ if (seen.has(value as object)) return '[Circular]'
134
+ seen.add(value as object)
135
+
136
+ if (Array.isArray(value)) {
137
+ const out = value.slice(0, MAX_ITEMS).map(v => summarize(v, depth + 1, seen))
138
+ if (value.length > MAX_ITEMS) out.push(`…+${value.length - MAX_ITEMS}`)
139
+ return out
140
+ }
141
+ const keys = Object.keys(value as object).slice(0, MAX_KEYS)
142
+ const out: Record<string, unknown> = {}
143
+ for (const key of keys) {
144
+ const cloned = summarize((value as Record<string, unknown>)[key], depth + 1, seen)
145
+ if (cloned !== undefined) out[key] = cloned
146
+ }
147
+ return out
148
+ }
149
+
150
+ /** Order entries by their group's position in `groupOrder`, then by title. */
151
+ export function sortEntries<T extends { group: string; title: string }>(entries: T[], groupOrder: string[]): T[] {
152
+ const rank = new Map(groupOrder.map((slug, i) => [slug, i]))
153
+ return [...entries].sort((a, b) => {
154
+ const ra = rank.has(a.group) ? (rank.get(a.group) as number) : groupOrder.length
155
+ const rb = rank.has(b.group) ? (rank.get(b.group) as number) : groupOrder.length
156
+ if (ra !== rb) return ra - rb
157
+ return a.title.localeCompare(b.title)
158
+ })
159
+ }
@@ -0,0 +1,90 @@
1
+ // The component tree rendered inside the iframe: resolve default props, merge
2
+ // the editor's JSON over the serialisable subset, compose opted-in providers
3
+ // plus any local provider, and render the component under an error boundary.
4
+
5
+ import type { ErrorInfo, ReactNode } from 'react'
6
+ import { Component } from 'react'
7
+ import type { WindoContextMap, WindoDefinition, WindoPlacement, WindoRenderContext } from '../types'
8
+
9
+ interface ErrorBoundaryProps {
10
+ onError: (message: string, stack?: string) => void
11
+ children: ReactNode
12
+ }
13
+
14
+ interface ErrorBoundaryState {
15
+ failed: boolean
16
+ }
17
+
18
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
19
+ state: ErrorBoundaryState = { failed: false }
20
+
21
+ static getDerivedStateFromError(): ErrorBoundaryState {
22
+ return { failed: true }
23
+ }
24
+
25
+ componentDidCatch(error: Error, info: ErrorInfo) {
26
+ const message = error?.message ?? String(error)
27
+ const stack = error?.stack ?? info?.componentStack ?? undefined
28
+ this.props.onError(message, stack)
29
+ }
30
+
31
+ componentDidUpdate(prev: ErrorBoundaryProps) {
32
+ if (prev.children !== this.props.children && this.state.failed) {
33
+ this.setState({ failed: false })
34
+ }
35
+ }
36
+
37
+ render() {
38
+ if (this.state.failed) return null
39
+ return this.props.children
40
+ }
41
+ }
42
+
43
+ export interface PreviewRootProps {
44
+ def: WindoDefinition
45
+ ctx: WindoRenderContext
46
+ values: unknown
47
+ contexts: WindoContextMap
48
+ onError: (message: string, stack?: string) => void
49
+ /** Fires the windo's pointer-bound actions (`enter`/`exit`/`hover`) as the pointer crosses the stage. */
50
+ onStage: (phase: 'enter' | 'leave') => void
51
+ }
52
+
53
+ export function PreviewRoot(props: PreviewRootProps) {
54
+ const { def, ctx, values, contexts, onError, onStage } = props
55
+
56
+ const defaultProps = typeof def.defaultProps === 'function' ? (def.defaultProps as (c: WindoRenderContext) => unknown)(ctx) : def.defaultProps
57
+ const finalProps = { ...(defaultProps as object), ...((values as object) ?? {}) }
58
+
59
+ let tree: ReactNode = def.component(finalProps, ctx)
60
+
61
+ if (def.providers) {
62
+ const Local = def.providers
63
+ tree = <Local ctx={ctx}>{tree}</Local>
64
+ }
65
+
66
+ const uses = def.uses ?? []
67
+ for (let i = uses.length - 1; i >= 0; i--) {
68
+ const name = uses[i]
69
+ const context = contexts[name]
70
+ if (!context?.provider) continue
71
+ const Provider = context.provider
72
+ const contextValues = ctx.contexts[name]
73
+ tree = (
74
+ <Provider values={contextValues} ctx={ctx}>
75
+ {tree}
76
+ </Provider>
77
+ )
78
+ }
79
+
80
+ const placement: WindoPlacement = def.placement ?? 'center'
81
+ // Placements render flush; the `-padding` suffix opts into frame padding.
82
+ const padded = placement.endsWith('-padding')
83
+ const align = padded ? placement.slice(0, -'-padding'.length) : placement
84
+
85
+ return (
86
+ <div className={`windo-placement ${align}${padded ? ' padded' : ''}`} onPointerEnter={() => onStage('enter')} onPointerLeave={() => onStage('leave')}>
87
+ <ErrorBoundary onError={onError}>{tree}</ErrorBoundary>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,8 @@
1
+ // The Vite plugin synthesises this module at build/dev time: it globs the
2
+ // project's `*.windo.tsx` files into lazy importers and bundles the resolved
3
+ // `windo.config.ts`. The preview entry imports both from here.
4
+
5
+ declare module 'virtual:windo-registry' {
6
+ export const modules: Record<string, () => Promise<{ default: import('../types').WindoModule }>>
7
+ export const config: import('../types').WindoConfig
8
+ }
@@ -0,0 +1,59 @@
1
+ // The chrome <-> iframe postMessage contract. The schema itself never crosses
2
+ // the boundary: the iframe walks it into a serialisable descriptor, the chrome
3
+ // sends candidate JSON down, the iframe parses and reports back. Every payload
4
+ // here is plain JSON.
5
+
6
+ import type { WindoContextMeta, WindoEnvState, WindoFieldError, WindoGroup, WindoLogEntry, WindoManifestEntry, WindoPropDoc, WindoSchemaDescriptor, WindoVariantMeta } from './types'
7
+
8
+ export const WINDO_MSG = 'windo'
9
+
10
+ /** chrome -> iframe */
11
+ export type WindoHostMessage =
12
+ | { source: typeof WINDO_MSG; dir: 'host'; type: 'request-manifest' }
13
+ | { source: typeof WINDO_MSG; dir: 'host'; type: 'select'; id: string }
14
+ | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-props'; id: string; json: string }
15
+ | { source: typeof WINDO_MSG; dir: 'host'; type: 'set-env'; env: WindoEnvState }
16
+ | { source: typeof WINDO_MSG; dir: 'host'; type: 'invoke-action'; id: string; actionId: string }
17
+
18
+ /** iframe -> chrome */
19
+ export type WindoPreviewMessage =
20
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'ready' }
21
+ | {
22
+ source: typeof WINDO_MSG
23
+ dir: 'preview'
24
+ type: 'manifest'
25
+ title: string
26
+ entries: WindoManifestEntry[]
27
+ groups: WindoGroup[]
28
+ contexts: WindoContextMeta[]
29
+ }
30
+ | {
31
+ source: typeof WINDO_MSG
32
+ dir: 'preview'
33
+ type: 'describe'
34
+ id: string
35
+ descriptor: WindoSchemaDescriptor
36
+ props: WindoPropDoc[]
37
+ variants: WindoVariantMeta[]
38
+ defaults: unknown
39
+ code: string | null
40
+ }
41
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-ok'; id: string }
42
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-error'; id: string; errors: WindoFieldError[] }
43
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'log'; entry: WindoLogEntry }
44
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'state'; id: string; state: Record<string, unknown>; actions: { id: string; disabled: boolean }[] }
45
+ | { source: typeof WINDO_MSG; dir: 'preview'; type: 'render-error'; id: string; message: string; stack?: string }
46
+
47
+ export type WindoMessage = WindoHostMessage | WindoPreviewMessage
48
+
49
+ export function isWindoMessage(data: unknown): data is WindoMessage {
50
+ return typeof data === 'object' && data !== null && (data as { source?: unknown }).source === WINDO_MSG
51
+ }
52
+
53
+ export function isHostMessage(msg: WindoMessage): msg is WindoHostMessage {
54
+ return msg.dir === 'host'
55
+ }
56
+
57
+ export function isPreviewMessage(msg: WindoMessage): msg is WindoPreviewMessage {
58
+ return msg.dir === 'preview'
59
+ }
package/src/types.ts ADDED
@@ -0,0 +1,319 @@
1
+ // Core type system for windo. Everything — the authoring API, the iframe
2
+ // preview runtime, the chrome UI, and the postMessage protocol — is typed
3
+ // against the contracts in this file.
4
+
5
+ import type { ComponentType, ReactNode } from 'react'
6
+ import type { z } from 'zod'
7
+
8
+ /* ------------------------------------------------------------------ *
9
+ * Primitives
10
+ * ------------------------------------------------------------------ */
11
+
12
+ export type WindoStatus = 'stable' | 'beta' | 'deprecated'
13
+
14
+ export const WINDO_STATUSES: readonly WindoStatus[] = ['stable', 'beta', 'deprecated']
15
+
16
+ /** Anchor a component within the canvas frame. */
17
+ export type WindoPlacementBase = 'center' | 'fill' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
18
+
19
+ /**
20
+ * Where a component renders inside the canvas frame. Placements render flush by
21
+ * default; append `-padding` to inset the component from the frame edges
22
+ * (e.g. `top` sits flush at the top, `top-padding` adds breathing room).
23
+ */
24
+ export type WindoPlacement = WindoPlacementBase | `${WindoPlacementBase}-padding`
25
+
26
+ const WINDO_PLACEMENT_BASE: readonly WindoPlacementBase[] = ['center', 'fill', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right']
27
+
28
+ export const WINDO_PLACEMENTS: readonly WindoPlacement[] = [...WINDO_PLACEMENT_BASE, ...WINDO_PLACEMENT_BASE.map(p => `${p}-padding` as WindoPlacement)]
29
+
30
+ /** A configured group. Components reference a group by its `slug`. */
31
+ export interface WindoGroup {
32
+ name: string
33
+ slug: string
34
+ description?: string
35
+ }
36
+
37
+ /* ------------------------------------------------------------------ *
38
+ * Context system
39
+ * ------------------------------------------------------------------ */
40
+
41
+ export type WindoControlType = 'enum' | 'boolean' | 'string' | 'number'
42
+
43
+ /** A single ambient control: a value plus the metadata to render its toggle. */
44
+ export interface WindoControlSpec<T = unknown> {
45
+ type: WindoControlType
46
+ label?: string
47
+ default: T
48
+ options?: readonly string[]
49
+ min?: number
50
+ max?: number
51
+ step?: number
52
+ }
53
+
54
+ export type WindoControlMap = Record<string, WindoControlSpec>
55
+
56
+ /** Resolve a control map to the value object it produces. */
57
+ export type WindoControlValues<C extends WindoControlMap> = {
58
+ [K in keyof C]: C[K] extends WindoControlSpec<infer T> ? T : never
59
+ }
60
+
61
+ /**
62
+ * A named context. Two capabilities, either or both:
63
+ * - `controls` → ambient values + UI toggles (free, no opt-in needed)
64
+ * - `provider` → a React wrapper, mounted inside the iframe around components
65
+ * that opt in via `uses`
66
+ */
67
+ export interface WindoContextDefinition<Values = unknown, Provided = Values> {
68
+ label?: string
69
+ description?: string
70
+ controls?: WindoControlMap
71
+ provider?: ComponentType<{ children: ReactNode; values: Values; ctx: WindoRenderContext }>
72
+ /** Derive the value exposed on `ctx.contexts[name]`. Defaults to the control values. */
73
+ resolve?: (values: Values, ctx: WindoRenderContext) => Provided
74
+ }
75
+
76
+ // Contexts are each generic over a different control map `C`; TS has no existential
77
+ // to hold `exists C. WindoContextDefinition<WindoControlValues<C>>`. `Values` sits in
78
+ // contravariant positions (provider props, resolve arg) so no precise common supertype
79
+ // exists — `any` is the single, contained variance hatch for this heterogeneous
80
+ // registry. Mirrors WindoModule. Definition sites stay precise; this only erases at the
81
+ // point of collection.
82
+ // biome-ignore lint/suspicious/noExplicitAny: existential variance hatch for the heterogeneous context registry
83
+ export type WindoContextMap = Record<string, WindoContextDefinition<any, any>>
84
+
85
+ /* ------------------------------------------------------------------ *
86
+ * Render-time context
87
+ * ------------------------------------------------------------------ */
88
+
89
+ export interface WindoViewport {
90
+ width: number
91
+ height: number
92
+ name: 'mobile' | 'tablet' | 'desktop'
93
+ }
94
+
95
+ /** Console channel: `ctx.logger.log(…)` posts an entry to the chrome's Console tab. */
96
+ export interface WindoLogger {
97
+ log: (...args: unknown[]) => void
98
+ }
99
+
100
+ /** How an action fires. `click` renders a toolbar button; the rest bind to the stage's pointer events. */
101
+ export type WindoActionTrigger = 'click' | 'enter' | 'exit' | 'hover'
102
+
103
+ export const WINDO_ACTION_TRIGGERS: readonly WindoActionTrigger[] = ['click', 'enter', 'exit', 'hover']
104
+
105
+ /**
106
+ * An out-of-band action that drives a component's state. `click` actions render as
107
+ * toolbar buttons; `enter`/`exit`/`hover` bind to the stage's pointer events. `run`
108
+ * receives the live ctx and an `active` flag — for `hover` it is `true` on
109
+ * pointer-enter and `false` on pointer-leave; for the others it is always `true`.
110
+ */
111
+ export interface WindoAction<State = unknown> {
112
+ label: string
113
+ /** Defaults to `click`. */
114
+ on?: WindoActionTrigger
115
+ run: (ctx: WindoRenderContext<State>, active: boolean) => void
116
+ /** Greys out a `click` action's toolbar button. Evaluated against the live state. */
117
+ disabled?: (ctx: WindoRenderContext<State>) => boolean
118
+ }
119
+
120
+ /** Live environment handed to every render-time function inside the iframe. */
121
+ export interface WindoRenderContext<State = unknown> {
122
+ colorScheme: 'light' | 'dark'
123
+ viewport: WindoViewport
124
+ reducedMotion: boolean
125
+ direction: 'ltr' | 'rtl'
126
+ locale: string
127
+ logger: WindoLogger
128
+ /** Current component-local state, typed by the windo's `State` generic. */
129
+ state: State
130
+ /** Merge a patch into the component-local state and re-render. */
131
+ setState: (patch: Partial<State>) => void
132
+ /** Resolved values of opted-in contexts, keyed by context name. */
133
+ contexts: Record<string, unknown>
134
+ }
135
+
136
+ /* ------------------------------------------------------------------ *
137
+ * Authoring API
138
+ * ------------------------------------------------------------------ */
139
+
140
+ /** A variant: a label plus a partial prop patch. Renders in the gallery and is click-to-apply. */
141
+ export interface WindoVariant<Props> {
142
+ label: string
143
+ props: Partial<Props>
144
+ }
145
+
146
+ /** A row in the authored Props documentation table. */
147
+ export interface WindoPropDoc {
148
+ name: string
149
+ type: string
150
+ default?: string
151
+ desc?: string
152
+ }
153
+
154
+ export type WindoDefaultProps<Props, State = unknown> = Props | ((ctx: WindoRenderContext<State>) => Props)
155
+
156
+ /**
157
+ * The object returned by a `windo(...)` factory.
158
+ *
159
+ * Keystone rule: the surrounding factory runs ONCE (definition-time) for static
160
+ * fields (title, group, schema). Every function field below — `defaultProps`,
161
+ * `actions`, `providers`, `component` — runs at render-time with the live `ctx`.
162
+ * Never close over live values in the static factory body.
163
+ */
164
+ export interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends string = string> {
165
+ title: string
166
+ group: GroupSlug
167
+ status?: WindoStatus
168
+ description?: string
169
+ deprecation?: string
170
+ placement?: WindoPlacement
171
+ /** Initial component-local state. Its shape is the `State` generic; `ctx.state`/`ctx.setState` derive from it. */
172
+ state?: State
173
+ /** Out-of-band actions that drive state: toolbar buttons (`click`) and stage pointer triggers (`enter`/`exit`/`hover`). */
174
+ actions?: WindoAction<State>[]
175
+ /** zod schema: validator + parser for the JSON-editable prop subset. `z.output ⊆ Props`. */
176
+ configurableProps?: z.ZodType
177
+ /** Full props incl. functions/JSX. The editor's JSON overrides merge on top. */
178
+ defaultProps: WindoDefaultProps<Props, State>
179
+ /** Names of provider contexts this component opts into. */
180
+ uses?: string[]
181
+ variants?: WindoVariant<Props>[]
182
+ /** Authored documentation table (not derived from the schema). */
183
+ props?: WindoPropDoc[]
184
+ /** Optional authored code snippet for the Code tab. */
185
+ code?: (values: Props) => string
186
+ /** A local provider wrapping just this windo (in addition to `uses`). */
187
+ providers?: ComponentType<{ children: ReactNode; ctx: WindoRenderContext<State> }>
188
+ component: (props: Props, ctx: WindoRenderContext<State>) => ReactNode
189
+ }
190
+
191
+ /** Argument handed to the `windo(w => ...)` factory. */
192
+ export interface WindoFactoryArg<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap> {
193
+ /** Configured groups keyed by slug. */
194
+ groups: Record<Groups[number]['slug'], WindoGroup>
195
+ contexts: Contexts
196
+ }
197
+
198
+ /**
199
+ * The default export of a `*.windo.tsx` file. A branded, lazily-resolved
200
+ * definition — the runtime calls `resolve(w)` with the config-derived factory arg.
201
+ */
202
+ // biome-ignore lint/suspicious/noExplicitAny: variance escape hatch for the heterogeneous WindoModule registry
203
+ export interface WindoModule<Props = any, State = any> {
204
+ readonly __windo: true
205
+ resolve: (w: WindoFactoryArg<readonly WindoGroup[], WindoContextMap>) => WindoDefinition<Props, State>
206
+ }
207
+
208
+ /* ------------------------------------------------------------------ *
209
+ * Config
210
+ * ------------------------------------------------------------------ */
211
+
212
+ export interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup[], Contexts extends WindoContextMap = WindoContextMap> {
213
+ /** Configured groups. A component's `group` must be one of these slugs. */
214
+ groups: Groups
215
+ /** Named contexts available to components. */
216
+ contexts?: Contexts
217
+ /** Glob(s) for discovery, relative to project root. Default `**\/*.windo.tsx`. */
218
+ include?: string | string[]
219
+ /** Title shown in the workbench chrome. */
220
+ title?: string
221
+ }
222
+
223
+ /* ------------------------------------------------------------------ *
224
+ * Schema descriptor (crosses the iframe boundary; renders the controls)
225
+ * ------------------------------------------------------------------ */
226
+
227
+ export type WindoControlKind = 'string' | 'number' | 'boolean' | 'enum' | 'date' | 'array' | 'object' | 'unknown'
228
+
229
+ export interface WindoControlDescriptor {
230
+ key: string
231
+ kind: WindoControlKind
232
+ optional: boolean
233
+ options?: string[]
234
+ min?: number
235
+ max?: number
236
+ description?: string
237
+ }
238
+
239
+ export interface WindoSchemaDescriptor {
240
+ fields: WindoControlDescriptor[]
241
+ }
242
+
243
+ /* ------------------------------------------------------------------ *
244
+ * Runtime manifest + protocol payloads
245
+ * ------------------------------------------------------------------ */
246
+
247
+ /** Serialisable metadata for one action — drives the canvas toolbar. */
248
+ export interface WindoActionMeta {
249
+ id: string
250
+ label: string
251
+ on: WindoActionTrigger
252
+ }
253
+
254
+ /** Static, serialisable metadata for one windo — drives the sidebar. */
255
+ export interface WindoManifestEntry {
256
+ id: string
257
+ title: string
258
+ group: string
259
+ status: WindoStatus
260
+ description?: string
261
+ deprecation?: string
262
+ placement: WindoPlacement
263
+ uses: string[]
264
+ hasVariants: boolean
265
+ actions: WindoActionMeta[]
266
+ hasState: boolean
267
+ }
268
+
269
+ export interface WindoVariantMeta {
270
+ label: string
271
+ props: Record<string, unknown>
272
+ }
273
+
274
+ /** Ambient environment pushed from the chrome down into the iframe. */
275
+ export interface WindoEnvState {
276
+ colorScheme: 'light' | 'dark'
277
+ viewport: WindoViewport
278
+ reducedMotion: boolean
279
+ direction: 'ltr' | 'rtl'
280
+ locale: string
281
+ /** Per-context control values, keyed by context name then control key. */
282
+ contexts: Record<string, Record<string, unknown>>
283
+ }
284
+
285
+ export interface WindoLogEntry {
286
+ ts: number
287
+ args: unknown[]
288
+ }
289
+
290
+ export interface WindoFieldError {
291
+ path: string
292
+ message: string
293
+ }
294
+
295
+ /* ------------------------------------------------------------------ *
296
+ * Context metadata (serialisable; drives the chrome's Context panel)
297
+ * ------------------------------------------------------------------ */
298
+
299
+ export interface WindoContextControlMeta {
300
+ key: string
301
+ type: WindoControlType
302
+ label?: string
303
+ options?: string[]
304
+ default: unknown
305
+ min?: number
306
+ max?: number
307
+ step?: number
308
+ }
309
+
310
+ export interface WindoContextMeta {
311
+ name: string
312
+ label?: string
313
+ description?: string
314
+ /** True when the context contributes controls (ambient values). */
315
+ ambient: boolean
316
+ /** True when the context mounts a provider (opt-in via `uses`). */
317
+ hasProvider: boolean
318
+ controls: WindoContextControlMeta[]
319
+ }