@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,181 @@
|
|
|
1
|
+
// Vite plugin (node side). Pairs `@vitejs/plugin-react` with the windo plugin:
|
|
2
|
+
// serves the chrome + iframe HTML documents ourselves (appType custom) and
|
|
3
|
+
// exposes a `virtual:windo-registry` module that re-exports the user config and
|
|
4
|
+
// glob-imports every `*.windo.tsx` file.
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { isAbsolute, resolve as resolvePath } from 'node:path'
|
|
8
|
+
import reactPlugin from '@vitejs/plugin-react'
|
|
9
|
+
import type { Plugin, PluginOption, UserConfig, ViteDevServer } from 'vite'
|
|
10
|
+
|
|
11
|
+
export interface WindoPluginOptions {
|
|
12
|
+
include?: string | string[]
|
|
13
|
+
config?: string
|
|
14
|
+
root?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const VIRTUAL_ID = 'virtual:windo-registry'
|
|
18
|
+
const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}`
|
|
19
|
+
|
|
20
|
+
const CONFIG_CANDIDATES = ['windo.config.ts', 'windo.config.tsx', 'windo.config.mts', 'windo.config.js', 'windo.config.mjs'] as const
|
|
21
|
+
|
|
22
|
+
function findConfig(root: string, configOpt?: string): string | null {
|
|
23
|
+
if (configOpt) return isAbsolute(configOpt) ? configOpt : resolvePath(root, configOpt)
|
|
24
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
25
|
+
const full = resolvePath(root, candidate)
|
|
26
|
+
if (existsSync(full)) return full
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toGlob(include?: string | string[]): string {
|
|
32
|
+
const first = Array.isArray(include) ? include[0] : include
|
|
33
|
+
const pattern = first ?? '**/*.windo.tsx'
|
|
34
|
+
return pattern.startsWith('/') ? pattern : `/${pattern}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CHROME_HTML = `<!doctype html>
|
|
38
|
+
<html data-theme="light">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="utf-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
+
<title>windo</title>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<div id="root"></div>
|
|
46
|
+
<script type="module">
|
|
47
|
+
import('@westopp/windo/client')
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
const IFRAME_HTML = `<!doctype html>
|
|
54
|
+
<html>
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="utf-8" />
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
58
|
+
</head>
|
|
59
|
+
<body style="background: transparent">
|
|
60
|
+
<div id="root"></div>
|
|
61
|
+
<script type="module">
|
|
62
|
+
import('@westopp/windo/preview')
|
|
63
|
+
</script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
// Absolute ids for the two HTML entry documents, relative to the project root.
|
|
69
|
+
// They are virtual — the files need not exist on disk; the plugin's `load`
|
|
70
|
+
// returns their contents. Keeping them rooted at `<root>/index.html` and
|
|
71
|
+
// `<root>/__windo/iframe.html` makes `vite build` emit them at the matching
|
|
72
|
+
// paths in the output directory (Vite derives an HTML asset's output path from
|
|
73
|
+
// the input id's path relative to root).
|
|
74
|
+
function htmlIds(root: string) {
|
|
75
|
+
return {
|
|
76
|
+
index: resolvePath(root, 'index.html'),
|
|
77
|
+
iframe: resolvePath(root, '__windo/iframe.html'),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function windo(options: WindoPluginOptions = {}): PluginOption[] {
|
|
82
|
+
let resolvedRoot = process.cwd()
|
|
83
|
+
let ids = htmlIds(resolvedRoot)
|
|
84
|
+
|
|
85
|
+
const registryModule = (): string => {
|
|
86
|
+
const root = options.root ?? resolvedRoot ?? process.cwd()
|
|
87
|
+
const configPath = findConfig(root, options.config)
|
|
88
|
+
const glob = toGlob(options.include)
|
|
89
|
+
const importPath = configPath ? JSON.stringify(configPath) : 'null'
|
|
90
|
+
const globLiteral = JSON.stringify(glob)
|
|
91
|
+
return [
|
|
92
|
+
`import * as __cfg from ${importPath}`,
|
|
93
|
+
'const config = __cfg.config ?? (__cfg.default && __cfg.default.config) ?? __cfg.default',
|
|
94
|
+
'export { config }',
|
|
95
|
+
`export const modules = import.meta.glob(${globLiteral})`,
|
|
96
|
+
'',
|
|
97
|
+
].join('\n')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const plugin: Plugin = {
|
|
101
|
+
name: 'windo',
|
|
102
|
+
// `pre` so our resolveId/load claim the virtual registry and the two HTML
|
|
103
|
+
// entries before Vite's filesystem fallback tries (and fails) to read them.
|
|
104
|
+
enforce: 'pre',
|
|
105
|
+
|
|
106
|
+
config(userConfig: UserConfig, { command, isPreview }) {
|
|
107
|
+
const root = resolvePath(options.root ?? userConfig.root ?? '.')
|
|
108
|
+
ids = htmlIds(root)
|
|
109
|
+
const base: UserConfig = {
|
|
110
|
+
// Dev serves the chrome + iframe HTML from `configureServer`, so it runs
|
|
111
|
+
// headless (`custom`). Build and preview want the normal SPA behaviour so
|
|
112
|
+
// `/` resolves to the emitted index.html.
|
|
113
|
+
appType: command === 'serve' && !isPreview ? 'custom' : 'spa',
|
|
114
|
+
define: {
|
|
115
|
+
__WINDO_IFRAME_SRC__: JSON.stringify(command === 'build' ? './__windo/iframe.html' : './__windo/iframe'),
|
|
116
|
+
},
|
|
117
|
+
resolve: {
|
|
118
|
+
dedupe: ['react', 'react-dom'],
|
|
119
|
+
},
|
|
120
|
+
optimizeDeps: {
|
|
121
|
+
// Pre-bundle every React entrypoint in one pass so React's identity is
|
|
122
|
+
// fixed up front. If `react-dom/client` (the preview renderer) is
|
|
123
|
+
// discovered late, Vite re-optimizes and can split React across two
|
|
124
|
+
// instances — fine until a component calls a hook, then "invalid hook
|
|
125
|
+
// call". Keeping them all here prevents that re-optimize.
|
|
126
|
+
include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Wire the chrome + iframe as build inputs unless the user supplied their
|
|
131
|
+
// own. Both are emitted as static HTML the canvas can be hosted from.
|
|
132
|
+
if (command === 'build' && !userConfig.build?.rollupOptions?.input) {
|
|
133
|
+
base.build = { rollupOptions: { input: { index: ids.index, iframe: ids.iframe } } }
|
|
134
|
+
}
|
|
135
|
+
return base
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
configResolved(resolved) {
|
|
139
|
+
resolvedRoot = resolved.root
|
|
140
|
+
ids = htmlIds(resolvedRoot)
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
resolveId(id) {
|
|
144
|
+
if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID
|
|
145
|
+
if (id === ids.index || id === ids.iframe) return id
|
|
146
|
+
return null
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
load(id) {
|
|
150
|
+
if (id === RESOLVED_VIRTUAL_ID) return registryModule()
|
|
151
|
+
if (id === ids.index) return CHROME_HTML
|
|
152
|
+
if (id === ids.iframe) return IFRAME_HTML
|
|
153
|
+
return null
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
configureServer(server: ViteDevServer) {
|
|
157
|
+
server.middlewares.use(async (req, res, next) => {
|
|
158
|
+
if (req.method !== 'GET') return next()
|
|
159
|
+
const url = (req.url ?? '').split('?')[0]
|
|
160
|
+
let html: string | null = null
|
|
161
|
+
if (url === '/' || url === '/index.html') html = CHROME_HTML
|
|
162
|
+
else if (url.startsWith('/__windo/iframe')) html = IFRAME_HTML
|
|
163
|
+
if (html === null) return next()
|
|
164
|
+
try {
|
|
165
|
+
const transformed = await server.transformIndexHtml(url, html)
|
|
166
|
+
res.statusCode = 200
|
|
167
|
+
res.setHeader('Content-Type', 'text/html')
|
|
168
|
+
res.end(transformed)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
next(err)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [reactPlugin(), plugin]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const windoPlugin = windo
|
|
180
|
+
|
|
181
|
+
export default windo
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Build the live `WindoRenderContext` handed to every render-time function
|
|
2
|
+
// inside the iframe. `ctx.logger.log` posts WindoLogEntry events up to the
|
|
3
|
+
// chrome; `ctx.state`/`ctx.setState` are the component-local state surface;
|
|
4
|
+
// opted-in contexts are resolved against the current env values.
|
|
5
|
+
|
|
6
|
+
import type { WindoContextMap, WindoEnvState, WindoLogEntry, WindoRenderContext } from '../types'
|
|
7
|
+
|
|
8
|
+
export function buildRenderContext<State>(
|
|
9
|
+
env: WindoEnvState,
|
|
10
|
+
contexts: WindoContextMap,
|
|
11
|
+
postLog: (entry: WindoLogEntry) => void,
|
|
12
|
+
state: State,
|
|
13
|
+
setState: (patch: Partial<State>) => void
|
|
14
|
+
): WindoRenderContext<State> {
|
|
15
|
+
const logger = {
|
|
16
|
+
log: (...args: unknown[]) => postLog({ ts: Date.now(), args }),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ctx: WindoRenderContext<State> = {
|
|
20
|
+
colorScheme: env.colorScheme,
|
|
21
|
+
viewport: env.viewport,
|
|
22
|
+
reducedMotion: env.reducedMotion,
|
|
23
|
+
direction: env.direction,
|
|
24
|
+
locale: env.locale,
|
|
25
|
+
logger,
|
|
26
|
+
state,
|
|
27
|
+
setState,
|
|
28
|
+
contexts: {},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const name of Object.keys(contexts)) {
|
|
32
|
+
const def = contexts[name]
|
|
33
|
+
const defaults: Record<string, unknown> = {}
|
|
34
|
+
const controlMap = def.controls ?? {}
|
|
35
|
+
for (const key of Object.keys(controlMap)) {
|
|
36
|
+
defaults[key] = controlMap[key].default
|
|
37
|
+
}
|
|
38
|
+
const values = { ...defaults, ...(env.contexts[name] ?? {}) }
|
|
39
|
+
ctx.contexts[name] = def.resolve ? def.resolve(values, ctx) : values
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ctx
|
|
43
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// The iframe preview entry. Resolves the virtual registry into a manifest,
|
|
2
|
+
// owns the runtime state (selected windo, parsed values, env), renders the
|
|
3
|
+
// selected component, and runs the postMessage conversation with the chrome.
|
|
4
|
+
|
|
5
|
+
import { config, modules } from 'virtual:windo-registry'
|
|
6
|
+
import { createElement } from 'react'
|
|
7
|
+
import type { Root } from 'react-dom/client'
|
|
8
|
+
import { createRoot } from 'react-dom/client'
|
|
9
|
+
import type { z } from 'zod'
|
|
10
|
+
import { describeSchema } from '../descriptor'
|
|
11
|
+
import type { WindoHostMessage, WindoPreviewMessage } from '../protocol'
|
|
12
|
+
import { isHostMessage, isWindoMessage, WINDO_MSG } from '../protocol'
|
|
13
|
+
import type { WindoActionMeta, WindoContextMap, WindoDefinition, WindoEnvState, WindoFactoryArg, WindoGroup, WindoLogEntry, WindoManifestEntry, WindoRenderContext, WindoVariantMeta } from '../types'
|
|
14
|
+
import { buildRenderContext } from './ctx'
|
|
15
|
+
import { buildContextMeta, flattenZodError, idFromPath, sortEntries, summarizeLogArg, toCloneable } from './registry'
|
|
16
|
+
import { PreviewRoot } from './render'
|
|
17
|
+
import './preview.css'
|
|
18
|
+
|
|
19
|
+
const contexts = config.contexts ?? {}
|
|
20
|
+
|
|
21
|
+
const w: WindoFactoryArg<readonly WindoGroup[], WindoContextMap> = {
|
|
22
|
+
groups: Object.fromEntries(config.groups.map(g => [g.slug, g])) as Record<string, WindoGroup>,
|
|
23
|
+
contexts,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ResolvedWindo {
|
|
27
|
+
id: string
|
|
28
|
+
def: WindoDefinition
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_ENV: WindoEnvState = {
|
|
32
|
+
colorScheme: 'light',
|
|
33
|
+
viewport: { width: 1160, height: 720, name: 'desktop' },
|
|
34
|
+
reducedMotion: false,
|
|
35
|
+
direction: 'ltr',
|
|
36
|
+
locale: 'en-US',
|
|
37
|
+
contexts: {},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let resolved: ResolvedWindo[] = []
|
|
41
|
+
let entries: WindoManifestEntry[] = []
|
|
42
|
+
let currentId: string | null = null
|
|
43
|
+
let currentDef: WindoDefinition | null = null
|
|
44
|
+
let currentSchema: z.ZodType | undefined
|
|
45
|
+
let currentValues: unknown = {}
|
|
46
|
+
let env: WindoEnvState = DEFAULT_ENV
|
|
47
|
+
let currentState: Record<string, unknown> = {}
|
|
48
|
+
let root: Root | null = null
|
|
49
|
+
|
|
50
|
+
function postToParent(msg: WindoPreviewMessage) {
|
|
51
|
+
window.parent.postMessage(msg, '*')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function postLog(entry: WindoLogEntry) {
|
|
55
|
+
// Summarize args (events/DOM nodes → compact markers) so the console stays
|
|
56
|
+
// readable and a SyntheticEvent doesn't balloon the postMessage payload.
|
|
57
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'log', entry: { ...entry, args: entry.args.map(summarizeLogArg) } })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The live ctx, rebuilt on demand so render-time, toolbar, and stage events all share the current state. */
|
|
61
|
+
function makeCtx(): WindoRenderContext {
|
|
62
|
+
return buildRenderContext(env, contexts, postLog, currentState, setState)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setState(patch: Partial<Record<string, unknown>>) {
|
|
66
|
+
currentState = { ...currentState, ...patch }
|
|
67
|
+
render()
|
|
68
|
+
postState()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Post the current state snapshot + each action's live `disabled` flag up to the chrome. */
|
|
72
|
+
function postState() {
|
|
73
|
+
if (!currentId || !currentDef) return
|
|
74
|
+
// `disabled` predicates are pure reads of the same state — one ctx covers them all.
|
|
75
|
+
const ctx = makeCtx()
|
|
76
|
+
const actions = (currentDef.actions ?? []).map((a, i) => ({
|
|
77
|
+
id: String(i),
|
|
78
|
+
disabled: a.disabled ? !!a.disabled(ctx) : false,
|
|
79
|
+
}))
|
|
80
|
+
// Preserve declared keys (incl. explicit `undefined`) so the read-only strip mirrors state faithfully.
|
|
81
|
+
const state: Record<string, unknown> = {}
|
|
82
|
+
for (const key of Object.keys(currentState)) state[key] = toCloneable(currentState[key])
|
|
83
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'state', id: currentId, state, actions })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Run a toolbar (`click`) action by its index. */
|
|
87
|
+
function invokeAction(actionId: string) {
|
|
88
|
+
const action = currentDef?.actions?.[Number(actionId)]
|
|
89
|
+
if (!action) return
|
|
90
|
+
action.run(makeCtx(), true)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Dispatch pointer-bound actions as the pointer crosses the stage. */
|
|
94
|
+
function dispatchStage(phase: 'enter' | 'leave') {
|
|
95
|
+
if (!currentDef?.actions) return
|
|
96
|
+
for (const action of currentDef.actions) {
|
|
97
|
+
const on = action.on ?? 'click'
|
|
98
|
+
if (on === 'click') continue
|
|
99
|
+
// Rebuild ctx per action so a later action sees an earlier one's setState.
|
|
100
|
+
const ctx = makeCtx()
|
|
101
|
+
if (on === 'hover') action.run(ctx, phase === 'enter')
|
|
102
|
+
else if (on === 'enter' && phase === 'enter') action.run(ctx, true)
|
|
103
|
+
else if (on === 'exit' && phase === 'leave') action.run(ctx, true)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function manifestEntry(id: string, def: WindoDefinition): WindoManifestEntry {
|
|
108
|
+
const actions: WindoActionMeta[] = (def.actions ?? []).map((a, i) => ({ id: String(i), label: a.label, on: a.on ?? 'click' }))
|
|
109
|
+
const entry: WindoManifestEntry = {
|
|
110
|
+
id,
|
|
111
|
+
title: def.title,
|
|
112
|
+
group: def.group,
|
|
113
|
+
status: def.status ?? 'stable',
|
|
114
|
+
placement: def.placement ?? 'center',
|
|
115
|
+
uses: def.uses ?? [],
|
|
116
|
+
hasVariants: (def.variants?.length ?? 0) > 0,
|
|
117
|
+
actions,
|
|
118
|
+
hasState: !!def.state && Object.keys(def.state as object).length > 0,
|
|
119
|
+
}
|
|
120
|
+
if (def.description !== undefined) entry.description = def.description
|
|
121
|
+
if (def.deprecation !== undefined) entry.deprecation = def.deprecation
|
|
122
|
+
return entry
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function computeDefaults(schema: z.ZodType | undefined): unknown {
|
|
126
|
+
if (!schema) return {}
|
|
127
|
+
const result = schema.safeParse({})
|
|
128
|
+
return result.success ? result.data : {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function safeCode(def: WindoDefinition, defaults: unknown): string | null {
|
|
132
|
+
if (!def.code) return null
|
|
133
|
+
try {
|
|
134
|
+
return def.code(defaults as never)
|
|
135
|
+
} catch {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function postManifest() {
|
|
141
|
+
postToParent({
|
|
142
|
+
source: WINDO_MSG,
|
|
143
|
+
dir: 'preview',
|
|
144
|
+
type: 'manifest',
|
|
145
|
+
title: config.title ?? 'windo',
|
|
146
|
+
entries,
|
|
147
|
+
groups: [...config.groups],
|
|
148
|
+
contexts: buildContextMeta(contexts),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function postDescribe(id: string, def: WindoDefinition, schema: z.ZodType | undefined) {
|
|
153
|
+
const defaults = computeDefaults(schema)
|
|
154
|
+
// variant patches and defaults may carry ReactNodes/functions that postMessage
|
|
155
|
+
// cannot clone — reduce them to JSON-safe forms before they cross the boundary.
|
|
156
|
+
const variants: WindoVariantMeta[] = (def.variants ?? []).map(v => ({ label: v.label, props: toCloneable(v.props) as Record<string, unknown> }))
|
|
157
|
+
postToParent({
|
|
158
|
+
source: WINDO_MSG,
|
|
159
|
+
dir: 'preview',
|
|
160
|
+
type: 'describe',
|
|
161
|
+
id,
|
|
162
|
+
descriptor: describeSchema(schema),
|
|
163
|
+
props: def.props ?? [],
|
|
164
|
+
variants,
|
|
165
|
+
defaults: toCloneable(defaults),
|
|
166
|
+
code: safeCode(def, defaults),
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function render() {
|
|
171
|
+
if (!currentDef || !root) return
|
|
172
|
+
const ctx = makeCtx()
|
|
173
|
+
const id = currentId ?? ''
|
|
174
|
+
root.render(
|
|
175
|
+
createElement(PreviewRoot, {
|
|
176
|
+
def: currentDef,
|
|
177
|
+
ctx,
|
|
178
|
+
values: currentValues,
|
|
179
|
+
contexts,
|
|
180
|
+
onStage: dispatchStage,
|
|
181
|
+
onError: (message: string, stack?: string) => {
|
|
182
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'render-error', id, message, ...(stack === undefined ? {} : { stack }) })
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleSelect(id: string) {
|
|
189
|
+
const match = resolved.find(r => r.id === id)
|
|
190
|
+
if (!match) return
|
|
191
|
+
currentId = id
|
|
192
|
+
currentDef = match.def
|
|
193
|
+
currentSchema = match.def.configurableProps
|
|
194
|
+
currentValues = computeDefaults(currentSchema)
|
|
195
|
+
// Reset component-local state to the windo's declared initial state.
|
|
196
|
+
currentState = { ...((match.def.state as Record<string, unknown>) ?? {}) }
|
|
197
|
+
render()
|
|
198
|
+
postState()
|
|
199
|
+
postDescribe(id, currentDef, currentSchema)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleSetProps(id: string, json: string) {
|
|
203
|
+
let parsed: unknown
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(json)
|
|
206
|
+
} catch (err) {
|
|
207
|
+
postToParent({
|
|
208
|
+
source: WINDO_MSG,
|
|
209
|
+
dir: 'preview',
|
|
210
|
+
type: 'parse-error',
|
|
211
|
+
id,
|
|
212
|
+
errors: [{ path: '', message: err instanceof Error ? err.message : 'Invalid JSON' }],
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (currentSchema) {
|
|
218
|
+
const result = currentSchema.safeParse(parsed)
|
|
219
|
+
if (result.success) {
|
|
220
|
+
currentValues = result.data
|
|
221
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'parse-ok', id })
|
|
222
|
+
render()
|
|
223
|
+
} else {
|
|
224
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'parse-error', id, errors: flattenZodError(result.error) })
|
|
225
|
+
}
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
currentValues = parsed
|
|
230
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'parse-ok', id })
|
|
231
|
+
render()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleMessage(msg: WindoHostMessage) {
|
|
235
|
+
switch (msg.type) {
|
|
236
|
+
case 'request-manifest':
|
|
237
|
+
postManifest()
|
|
238
|
+
break
|
|
239
|
+
case 'select':
|
|
240
|
+
handleSelect(msg.id)
|
|
241
|
+
break
|
|
242
|
+
case 'set-props':
|
|
243
|
+
handleSetProps(msg.id, msg.json)
|
|
244
|
+
break
|
|
245
|
+
case 'set-env':
|
|
246
|
+
env = msg.env
|
|
247
|
+
render()
|
|
248
|
+
break
|
|
249
|
+
case 'invoke-action':
|
|
250
|
+
invokeAction(msg.actionId)
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function init() {
|
|
256
|
+
const rootEl = document.getElementById('root')
|
|
257
|
+
if (!rootEl) return
|
|
258
|
+
root = createRoot(rootEl)
|
|
259
|
+
|
|
260
|
+
const paths = Object.keys(modules)
|
|
261
|
+
const loaded = await Promise.all(
|
|
262
|
+
paths.map(async path => {
|
|
263
|
+
const mod = await modules[path]()
|
|
264
|
+
return { path, def: mod.default.resolve(w) }
|
|
265
|
+
})
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
resolved = loaded.map(({ path, def }) => ({ id: idFromPath(path), def }))
|
|
269
|
+
entries = sortEntries(
|
|
270
|
+
resolved.map(r => manifestEntry(r.id, r.def)),
|
|
271
|
+
config.groups.map(g => g.slug)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
window.addEventListener('message', event => {
|
|
275
|
+
if (!isWindoMessage(event.data)) return
|
|
276
|
+
if (!isHostMessage(event.data)) return
|
|
277
|
+
handleMessage(event.data)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'ready' })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void init()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* The iframe document is transparent — the chrome paints the canvas grid
|
|
2
|
+
behind it. The root fills the frame; placement classes position the
|
|
3
|
+
rendered component within it. */
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
background: transparent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#root {
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
display: flex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.windo-placement {
|
|
17
|
+
flex: 1;
|
|
18
|
+
display: flex;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Opt-in padding via the `-padding` placement suffix. */
|
|
23
|
+
.windo-placement.padded {
|
|
24
|
+
padding: 24px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.windo-placement.center {
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.windo-placement.top {
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: flex-start;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.windo-placement.bottom {
|
|
38
|
+
justify-content: center;
|
|
39
|
+
align-items: flex-end;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.windo-placement.left {
|
|
43
|
+
justify-content: flex-start;
|
|
44
|
+
align-items: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.windo-placement.right {
|
|
48
|
+
justify-content: flex-end;
|
|
49
|
+
align-items: center;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.windo-placement.top-left {
|
|
53
|
+
justify-content: flex-start;
|
|
54
|
+
align-items: flex-start;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.windo-placement.top-right {
|
|
58
|
+
justify-content: flex-end;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.windo-placement.bottom-left {
|
|
63
|
+
justify-content: flex-start;
|
|
64
|
+
align-items: flex-end;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.windo-placement.bottom-right {
|
|
68
|
+
justify-content: flex-end;
|
|
69
|
+
align-items: flex-end;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.windo-placement.fill {
|
|
73
|
+
align-items: stretch;
|
|
74
|
+
justify-content: stretch;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.windo-placement.fill > * {
|
|
78
|
+
flex: 1;
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
}
|