@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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/chunk-5RM2VYAM.js +150 -0
- package/dist/chunk-5RM2VYAM.js.map +1 -0
- package/dist/cli.cjs +303 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +374 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +185 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +11 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +11 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +95 -0
- package/src/cli/index.ts +160 -0
- package/src/client/App.tsx +310 -0
- package/src/client/Canvas.tsx +358 -0
- package/src/client/Inspector.tsx +586 -0
- package/src/client/Sidebar.tsx +108 -0
- package/src/client/bridge.ts +124 -0
- package/src/client/chrome.css +1966 -0
- package/src/client/icons.tsx +110 -0
- package/src/client/index.ts +15 -0
- package/src/client/internal-types.ts +147 -0
- package/src/client/persist.ts +38 -0
- package/src/define-config.ts +54 -0
- package/src/descriptor.test.ts +59 -0
- package/src/descriptor.ts +185 -0
- package/src/globals.d.ts +9 -0
- package/src/index.ts +54 -0
- package/src/plugin/index.ts +181 -0
- package/src/preview/ctx.ts +43 -0
- package/src/preview/index.ts +283 -0
- package/src/preview/preview.css +81 -0
- package/src/preview/registry.ts +159 -0
- package/src/preview/render.tsx +90 -0
- package/src/preview/virtual.d.ts +8 -0
- package/src/protocol.ts +59 -0
- 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
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -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
|
+
}
|