@westopp/windo 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -17
- package/dist/index.d.ts +64 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +82 -3
- package/src/client/Canvas.tsx +8 -3
- package/src/client/Inspector.tsx +37 -0
- package/src/client/Sidebar.tsx +13 -3
- package/src/client/TagFilter.tsx +125 -0
- package/src/client/bridge.ts +24 -0
- package/src/client/chrome.css +317 -52
- package/src/client/internal-types.ts +24 -0
- package/src/define-config.ts +12 -8
- package/src/index.ts +2 -0
- package/src/preview/ctx.ts +8 -1
- package/src/preview/index.ts +155 -10
- package/src/preview/render.tsx +5 -3
- package/src/protocol.ts +6 -0
- package/src/types.ts +43 -12
- package/src/descriptor.test.ts +0 -59
package/src/preview/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { z } from 'zod'
|
|
|
10
10
|
import { describeSchema } from '../descriptor'
|
|
11
11
|
import type { WindoHostMessage, WindoPreviewMessage } from '../protocol'
|
|
12
12
|
import { isHostMessage, isWindoMessage, WINDO_MSG } from '../protocol'
|
|
13
|
-
import type { WindoActionMeta, WindoContextMap, WindoDefinition, WindoEnvState, WindoFactoryArg, WindoGroup, WindoLogEntry, WindoManifestEntry, WindoRenderContext, WindoVariantMeta } from '../types'
|
|
13
|
+
import type { WindoActionMeta, WindoContextMap, WindoDefinition, WindoEnvState, WindoFactoryArg, WindoGroup, WindoInitContext, WindoLogEntry, WindoManifestEntry, WindoPlacement, WindoPropDoc, WindoRenderContext, WindoVariant, WindoVariantMeta } from '../types'
|
|
14
14
|
import { buildRenderContext } from './ctx'
|
|
15
15
|
import { buildContextMeta, flattenZodError, idFromPath, sortEntries, summarizeLogArg, toCloneable } from './registry'
|
|
16
16
|
import { PreviewRoot } from './render'
|
|
@@ -21,6 +21,7 @@ const contexts = config.contexts ?? {}
|
|
|
21
21
|
const w: WindoFactoryArg<readonly WindoGroup[], WindoContextMap> = {
|
|
22
22
|
groups: Object.fromEntries(config.groups.map(g => [g.slug, g])) as Record<string, WindoGroup>,
|
|
23
23
|
contexts,
|
|
24
|
+
tags: config.tags ? [...config.tags] : [],
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface ResolvedWindo {
|
|
@@ -45,7 +46,14 @@ let currentSchema: z.ZodType | undefined
|
|
|
45
46
|
let currentValues: unknown = {}
|
|
46
47
|
let env: WindoEnvState = DEFAULT_ENV
|
|
47
48
|
let currentState: Record<string, unknown> = {}
|
|
49
|
+
// Shared, cross-component state. Seeded once from the config and — unlike
|
|
50
|
+
// `currentState` — never reset on selection, so it survives switching windos.
|
|
51
|
+
let ctxState: Record<string, unknown> = { ...((config.ctxState as Record<string, unknown>) ?? {}) }
|
|
48
52
|
let root: Root | null = null
|
|
53
|
+
// Raised while a windo's full-ctx fields are being resolved (init state, defaultProps,
|
|
54
|
+
// placement). Resolution must be a pure read of the env/ctxState surface — so the
|
|
55
|
+
// mutating callbacks no-op while it's set, closing every re-entrant render path.
|
|
56
|
+
let resolving = false
|
|
49
57
|
|
|
50
58
|
function postToParent(msg: WindoPreviewMessage) {
|
|
51
59
|
window.parent.postMessage(msg, '*')
|
|
@@ -59,15 +67,57 @@ function postLog(entry: WindoLogEntry) {
|
|
|
59
67
|
|
|
60
68
|
/** The live ctx, rebuilt on demand so render-time, toolbar, and stage events all share the current state. */
|
|
61
69
|
function makeCtx(): WindoRenderContext {
|
|
62
|
-
return buildRenderContext(env, contexts, postLog, currentState, setState)
|
|
70
|
+
return buildRenderContext(env, contexts, postLog, currentState, setState, ctxState, setCtxState, setColorScheme)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The init-scoped ctx handed to a `state` resolver. State is resolved exactly once,
|
|
75
|
+
* here, before any component-local state exists — so it exposes the env surface only
|
|
76
|
+
* with a no-op `setState`. The shared `setCtxState`/`setColorScheme` are still wired
|
|
77
|
+
* through but no-op while `resolving` is set, preventing an author from re-triggering
|
|
78
|
+
* resolution mid-init. Typed as `WindoInitContext` to drop the absent `state`/`setState`.
|
|
79
|
+
*/
|
|
80
|
+
function makeInitCtx(): WindoInitContext {
|
|
81
|
+
return buildRenderContext(env, contexts, postLog, {}, () => {}, ctxState, setCtxState, setColorScheme)
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
function setState(patch: Partial<Record<string, unknown>>) {
|
|
85
|
+
// No-op while resolving a windo's full-ctx fields — those are pure reads and must
|
|
86
|
+
// never drive a render, which would re-enter resolution and loop.
|
|
87
|
+
if (resolving) return
|
|
66
88
|
currentState = { ...currentState, ...patch }
|
|
67
89
|
render()
|
|
68
90
|
postState()
|
|
69
91
|
}
|
|
70
92
|
|
|
93
|
+
/** Merge a patch into the shared state, re-render, and echo it up to the chrome. */
|
|
94
|
+
function setCtxState(patch: Record<string, unknown>) {
|
|
95
|
+
if (resolving) return
|
|
96
|
+
ctxState = { ...ctxState, ...patch }
|
|
97
|
+
render()
|
|
98
|
+
postCtxState()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Cloneable snapshot of the shared state (drops functions/JSX before postMessage). */
|
|
102
|
+
function cloneableCtxState(): Record<string, unknown> {
|
|
103
|
+
const out: Record<string, unknown> = {}
|
|
104
|
+
for (const key of Object.keys(ctxState)) out[key] = toCloneable(ctxState[key])
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function postCtxState() {
|
|
109
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'ctx-state', state: cloneableCtxState() })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Set the canvas colour scheme from a component/action and echo it so the chrome's toggle stays in sync. */
|
|
113
|
+
function setColorScheme(scheme: 'light' | 'dark') {
|
|
114
|
+
if (resolving) return
|
|
115
|
+
if (env.colorScheme === scheme) return
|
|
116
|
+
env = { ...env, colorScheme: scheme }
|
|
117
|
+
render()
|
|
118
|
+
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'color-scheme', colorScheme: scheme })
|
|
119
|
+
}
|
|
120
|
+
|
|
71
121
|
/** Post the current state snapshot + each action's live `disabled` flag up to the chrome. */
|
|
72
122
|
function postState() {
|
|
73
123
|
if (!currentId || !currentDef) return
|
|
@@ -110,12 +160,15 @@ function manifestEntry(id: string, def: WindoDefinition): WindoManifestEntry {
|
|
|
110
160
|
id,
|
|
111
161
|
title: def.title,
|
|
112
162
|
group: def.group,
|
|
163
|
+
tags: def.tags ?? [],
|
|
113
164
|
status: def.status ?? 'stable',
|
|
114
|
-
|
|
165
|
+
// The now-functionable fields can't be resolved here (no ctx at init): fall back
|
|
166
|
+
// to a safe static value, and assume the dynamic forms contribute a variant/state.
|
|
167
|
+
placement: typeof def.placement === 'function' ? 'center' : (def.placement ?? 'center'),
|
|
115
168
|
uses: def.uses ?? [],
|
|
116
|
-
hasVariants: (def.variants?.length ?? 0) > 0,
|
|
169
|
+
hasVariants: typeof def.variants === 'function' ? true : (def.variants?.length ?? 0) > 0,
|
|
117
170
|
actions,
|
|
118
|
-
hasState: !!def.state && Object.keys(def.state as object).length > 0,
|
|
171
|
+
hasState: typeof def.state === 'function' ? true : !!def.state && Object.keys(def.state as object).length > 0,
|
|
119
172
|
}
|
|
120
173
|
if (def.description !== undefined) entry.description = def.description
|
|
121
174
|
if (def.deprecation !== undefined) entry.deprecation = def.deprecation
|
|
@@ -130,10 +183,85 @@ function computeDefaults(schema: z.ZodType | undefined): unknown {
|
|
|
130
183
|
|
|
131
184
|
function safeCode(def: WindoDefinition, defaults: unknown): string | null {
|
|
132
185
|
if (!def.code) return null
|
|
186
|
+
// `code` is a pure string producer — resolve it under `resolving` so it can't drive a
|
|
187
|
+
// render and post an inconsistent describe payload by mutating state mid-resolution.
|
|
188
|
+
resolving = true
|
|
133
189
|
try {
|
|
134
|
-
return def.code(defaults as never)
|
|
190
|
+
return def.code(defaults as never, makeCtx())
|
|
135
191
|
} catch {
|
|
136
192
|
return null
|
|
193
|
+
} finally {
|
|
194
|
+
resolving = false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Resolve the authored Props table — a static array or a `ctx => array` function — under `resolving` so it stays a pure read. */
|
|
199
|
+
function safeProps(def: WindoDefinition): WindoPropDoc[] {
|
|
200
|
+
const p = def.props
|
|
201
|
+
if (typeof p !== 'function') return p ?? []
|
|
202
|
+
resolving = true
|
|
203
|
+
try {
|
|
204
|
+
return p(makeCtx()) ?? []
|
|
205
|
+
} catch {
|
|
206
|
+
return []
|
|
207
|
+
} finally {
|
|
208
|
+
resolving = false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Resolve a windo's initial state — a static value or an init-ctx function — under `resolving`, degrading to `{}` if it throws. */
|
|
213
|
+
function safeState(def: WindoDefinition): Record<string, unknown> {
|
|
214
|
+
const state = def.state
|
|
215
|
+
if (typeof state !== 'function') return { ...((state as Record<string, unknown> | undefined) ?? {}) }
|
|
216
|
+
resolving = true
|
|
217
|
+
try {
|
|
218
|
+
return { ...((state as (c: WindoInitContext) => unknown)(makeInitCtx()) as Record<string, unknown>) }
|
|
219
|
+
} catch {
|
|
220
|
+
return {}
|
|
221
|
+
} finally {
|
|
222
|
+
resolving = false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Resolve `defaultProps` — a static value or a `ctx => props` function — under `resolving` so it can't drive a render. */
|
|
227
|
+
function safeDefaultProps(def: WindoDefinition, ctx: WindoRenderContext): unknown {
|
|
228
|
+
const dp = def.defaultProps
|
|
229
|
+
if (typeof dp !== 'function') return dp
|
|
230
|
+
resolving = true
|
|
231
|
+
try {
|
|
232
|
+
return (dp as (c: WindoRenderContext) => unknown)(ctx)
|
|
233
|
+
} catch {
|
|
234
|
+
return {}
|
|
235
|
+
} finally {
|
|
236
|
+
resolving = false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Resolve `placement` — a static value or a `ctx => placement` function — under `resolving`, degrading to `center` if it throws. */
|
|
241
|
+
function safePlacement(def: WindoDefinition, ctx: WindoRenderContext): WindoPlacement {
|
|
242
|
+
const p = def.placement
|
|
243
|
+
if (typeof p !== 'function') return p ?? 'center'
|
|
244
|
+
resolving = true
|
|
245
|
+
try {
|
|
246
|
+
return p(ctx)
|
|
247
|
+
} catch {
|
|
248
|
+
return 'center'
|
|
249
|
+
} finally {
|
|
250
|
+
resolving = false
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Resolve the gallery variants — a static array or a `ctx => array` function — under `resolving` so it stays a pure read. */
|
|
255
|
+
function safeVariants(def: WindoDefinition): WindoVariant<unknown>[] {
|
|
256
|
+
const v = def.variants
|
|
257
|
+
if (typeof v !== 'function') return v ?? []
|
|
258
|
+
resolving = true
|
|
259
|
+
try {
|
|
260
|
+
return v(makeCtx()) ?? []
|
|
261
|
+
} catch {
|
|
262
|
+
return []
|
|
263
|
+
} finally {
|
|
264
|
+
resolving = false
|
|
137
265
|
}
|
|
138
266
|
}
|
|
139
267
|
|
|
@@ -145,7 +273,9 @@ function postManifest() {
|
|
|
145
273
|
title: config.title ?? 'windo',
|
|
146
274
|
entries,
|
|
147
275
|
groups: [...config.groups],
|
|
276
|
+
tags: config.tags ? [...config.tags] : [],
|
|
148
277
|
contexts: buildContextMeta(contexts),
|
|
278
|
+
ctxState: cloneableCtxState(),
|
|
149
279
|
})
|
|
150
280
|
}
|
|
151
281
|
|
|
@@ -153,14 +283,14 @@ function postDescribe(id: string, def: WindoDefinition, schema: z.ZodType | unde
|
|
|
153
283
|
const defaults = computeDefaults(schema)
|
|
154
284
|
// variant patches and defaults may carry ReactNodes/functions that postMessage
|
|
155
285
|
// cannot clone — reduce them to JSON-safe forms before they cross the boundary.
|
|
156
|
-
const variants: WindoVariantMeta[] = (def
|
|
286
|
+
const variants: WindoVariantMeta[] = safeVariants(def).map(v => ({ label: v.label, props: toCloneable(v.props) as Record<string, unknown> }))
|
|
157
287
|
postToParent({
|
|
158
288
|
source: WINDO_MSG,
|
|
159
289
|
dir: 'preview',
|
|
160
290
|
type: 'describe',
|
|
161
291
|
id,
|
|
162
292
|
descriptor: describeSchema(schema),
|
|
163
|
-
props: def
|
|
293
|
+
props: safeProps(def),
|
|
164
294
|
variants,
|
|
165
295
|
defaults: toCloneable(defaults),
|
|
166
296
|
code: safeCode(def, defaults),
|
|
@@ -171,12 +301,18 @@ function render() {
|
|
|
171
301
|
if (!currentDef || !root) return
|
|
172
302
|
const ctx = makeCtx()
|
|
173
303
|
const id = currentId ?? ''
|
|
304
|
+
// Resolve the full-ctx fields here, OUTSIDE the React render cycle, so a function
|
|
305
|
+
// field that touches a mutating callback can't re-enter render() synchronously.
|
|
306
|
+
const defaultProps = safeDefaultProps(currentDef, ctx)
|
|
307
|
+
const placement = safePlacement(currentDef, ctx)
|
|
174
308
|
root.render(
|
|
175
309
|
createElement(PreviewRoot, {
|
|
176
310
|
def: currentDef,
|
|
177
311
|
ctx,
|
|
178
312
|
values: currentValues,
|
|
179
313
|
contexts,
|
|
314
|
+
defaultProps,
|
|
315
|
+
placement,
|
|
180
316
|
onStage: dispatchStage,
|
|
181
317
|
onError: (message: string, stack?: string) => {
|
|
182
318
|
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'render-error', id, message, ...(stack === undefined ? {} : { stack }) })
|
|
@@ -192,8 +328,11 @@ function handleSelect(id: string) {
|
|
|
192
328
|
currentDef = match.def
|
|
193
329
|
currentSchema = match.def.configurableProps
|
|
194
330
|
currentValues = computeDefaults(currentSchema)
|
|
195
|
-
// Reset component-local state to the windo's declared initial state.
|
|
196
|
-
|
|
331
|
+
// Reset component-local state to the windo's declared initial state. Resolve it
|
|
332
|
+
// ONCE here — a `state` function runs against the init ctx (no live state/setState)
|
|
333
|
+
// under `resolving`, degrading to `{}` on throw. It is never re-resolved in
|
|
334
|
+
// render/makeCtx/set-env/set-ctx-state, so the author cannot drive a resolution loop.
|
|
335
|
+
currentState = safeState(currentDef)
|
|
197
336
|
render()
|
|
198
337
|
postState()
|
|
199
338
|
postDescribe(id, currentDef, currentSchema)
|
|
@@ -246,6 +385,12 @@ function handleMessage(msg: WindoHostMessage) {
|
|
|
246
385
|
env = msg.env
|
|
247
386
|
render()
|
|
248
387
|
break
|
|
388
|
+
case 'set-ctx-state':
|
|
389
|
+
// Chrome is the source of this update (an editor edit or a reload re-sync),
|
|
390
|
+
// so adopt it and re-render — but don't echo it back, to avoid a ping-pong.
|
|
391
|
+
ctxState = { ...msg.state }
|
|
392
|
+
render()
|
|
393
|
+
break
|
|
249
394
|
case 'invoke-action':
|
|
250
395
|
invokeAction(msg.actionId)
|
|
251
396
|
break
|
package/src/preview/render.tsx
CHANGED
|
@@ -45,15 +45,18 @@ export interface PreviewRootProps {
|
|
|
45
45
|
ctx: WindoRenderContext
|
|
46
46
|
values: unknown
|
|
47
47
|
contexts: WindoContextMap
|
|
48
|
+
/** `defaultProps` resolved outside the render cycle by `index.ts`, so a function field can't re-enter render here. */
|
|
49
|
+
defaultProps: unknown
|
|
50
|
+
/** `placement` resolved outside the render cycle by `index.ts`, for the same reason. */
|
|
51
|
+
placement: WindoPlacement
|
|
48
52
|
onError: (message: string, stack?: string) => void
|
|
49
53
|
/** Fires the windo's pointer-bound actions (`enter`/`exit`/`hover`) as the pointer crosses the stage. */
|
|
50
54
|
onStage: (phase: 'enter' | 'leave') => void
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export function PreviewRoot(props: PreviewRootProps) {
|
|
54
|
-
const { def, ctx, values, contexts, onError, onStage } = props
|
|
58
|
+
const { def, ctx, values, contexts, defaultProps, placement, onError, onStage } = props
|
|
55
59
|
|
|
56
|
-
const defaultProps = typeof def.defaultProps === 'function' ? (def.defaultProps as (c: WindoRenderContext) => unknown)(ctx) : def.defaultProps
|
|
57
60
|
const finalProps = { ...(defaultProps as object), ...((values as object) ?? {}) }
|
|
58
61
|
|
|
59
62
|
let tree: ReactNode = def.component(finalProps, ctx)
|
|
@@ -77,7 +80,6 @@ export function PreviewRoot(props: PreviewRootProps) {
|
|
|
77
80
|
)
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
const placement: WindoPlacement = def.placement ?? 'center'
|
|
81
83
|
// Placements render flush; the `-padding` suffix opts into frame padding.
|
|
82
84
|
const padded = placement.endsWith('-padding')
|
|
83
85
|
const align = padded ? placement.slice(0, -'-padding'.length) : placement
|
package/src/protocol.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type WindoHostMessage =
|
|
|
13
13
|
| { source: typeof WINDO_MSG; dir: 'host'; type: 'select'; id: string }
|
|
14
14
|
| { source: typeof WINDO_MSG; dir: 'host'; type: 'set-props'; id: string; json: string }
|
|
15
15
|
| { source: typeof WINDO_MSG; dir: 'host'; type: 'set-env'; env: WindoEnvState }
|
|
16
|
+
| { source: typeof WINDO_MSG; dir: 'host'; type: 'set-ctx-state'; state: Record<string, unknown> }
|
|
16
17
|
| { source: typeof WINDO_MSG; dir: 'host'; type: 'invoke-action'; id: string; actionId: string }
|
|
17
18
|
|
|
18
19
|
/** iframe -> chrome */
|
|
@@ -25,7 +26,10 @@ export type WindoPreviewMessage =
|
|
|
25
26
|
title: string
|
|
26
27
|
entries: WindoManifestEntry[]
|
|
27
28
|
groups: WindoGroup[]
|
|
29
|
+
tags: string[]
|
|
28
30
|
contexts: WindoContextMeta[]
|
|
31
|
+
/** Initial shared state from the config — seeds the chrome's editable strip. */
|
|
32
|
+
ctxState: Record<string, unknown>
|
|
29
33
|
}
|
|
30
34
|
| {
|
|
31
35
|
source: typeof WINDO_MSG
|
|
@@ -42,6 +46,8 @@ export type WindoPreviewMessage =
|
|
|
42
46
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-error'; id: string; errors: WindoFieldError[] }
|
|
43
47
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'log'; entry: WindoLogEntry }
|
|
44
48
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'state'; id: string; state: Record<string, unknown>; actions: { id: string; disabled: boolean }[] }
|
|
49
|
+
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'ctx-state'; state: Record<string, unknown> }
|
|
50
|
+
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'color-scheme'; colorScheme: 'light' | 'dark' }
|
|
45
51
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'render-error'; id: string; message: string; stack?: string }
|
|
46
52
|
|
|
47
53
|
export type WindoMessage = WindoHostMessage | WindoPreviewMessage
|
package/src/types.ts
CHANGED
|
@@ -131,6 +131,20 @@ export interface WindoRenderContext<State = unknown> {
|
|
|
131
131
|
setState: (patch: Partial<State>) => void
|
|
132
132
|
/** Resolved values of opted-in contexts, keyed by context name. */
|
|
133
133
|
contexts: Record<string, unknown>
|
|
134
|
+
/**
|
|
135
|
+
* Shared, cross-component state seeded from the config's `ctxState`. Unlike
|
|
136
|
+
* `state` (per-windo, reset on selection), this persists across selection and
|
|
137
|
+
* is global: any component can read it and write it via `setCtxState`. Use it
|
|
138
|
+
* to drive providers that wrap every component — e.g. a theme provider toggled
|
|
139
|
+
* from any component on the canvas.
|
|
140
|
+
*/
|
|
141
|
+
ctxState: Record<string, unknown>
|
|
142
|
+
/** Merge a patch into the shared `ctxState` and re-render every consumer. */
|
|
143
|
+
setCtxState: (patch: Record<string, unknown>) => void
|
|
144
|
+
/** Set the canvas colour scheme from a component or action. */
|
|
145
|
+
setColorScheme: (scheme: 'light' | 'dark') => void
|
|
146
|
+
/** Flip the canvas colour scheme between light and dark. */
|
|
147
|
+
toggleTheme: () => void
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
/* ------------------------------------------------------------------ *
|
|
@@ -151,7 +165,13 @@ export interface WindoPropDoc {
|
|
|
151
165
|
desc?: string
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
|
|
168
|
+
/** A value that may be authored statically or as a context-aware `ctx => value` function resolved at render-time. */
|
|
169
|
+
export type Ctxual<T, State = unknown> = T | ((ctx: WindoRenderContext<State>) => T)
|
|
170
|
+
|
|
171
|
+
/** 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. */
|
|
172
|
+
export type WindoInitContext<State = unknown> = Omit<WindoRenderContext<State>, 'state' | 'setState'>
|
|
173
|
+
|
|
174
|
+
export type WindoDefaultProps<Props, State = unknown> = Ctxual<Props, State>
|
|
155
175
|
|
|
156
176
|
/**
|
|
157
177
|
* The object returned by a `windo(...)` factory.
|
|
@@ -161,15 +181,18 @@ export type WindoDefaultProps<Props, State = unknown> = Props | ((ctx: WindoRend
|
|
|
161
181
|
* `actions`, `providers`, `component` — runs at render-time with the live `ctx`.
|
|
162
182
|
* Never close over live values in the static factory body.
|
|
163
183
|
*/
|
|
164
|
-
export interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends string = string> {
|
|
184
|
+
export interface WindoDefinition<Props = unknown, State = unknown, GroupSlug extends string = string, Tag extends string = string> {
|
|
165
185
|
title: string
|
|
166
186
|
group: GroupSlug
|
|
187
|
+
/** Tags this component carries. Each must be one of the config's declared `tags`. Drives the sidebar's tag filter. */
|
|
188
|
+
tags?: Tag[]
|
|
167
189
|
status?: WindoStatus
|
|
168
190
|
description?: string
|
|
169
191
|
deprecation?: string
|
|
170
|
-
placement
|
|
171
|
-
|
|
172
|
-
state
|
|
192
|
+
/** Where the component anchors in the canvas frame. Either a static placement or a function resolved with the live `ctx`. */
|
|
193
|
+
placement?: Ctxual<WindoPlacement, State>
|
|
194
|
+
/** 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. */
|
|
195
|
+
state?: State | ((ctx: WindoInitContext<State>) => State)
|
|
173
196
|
/** Out-of-band actions that drive state: toolbar buttons (`click`) and stage pointer triggers (`enter`/`exit`/`hover`). */
|
|
174
197
|
actions?: WindoAction<State>[]
|
|
175
198
|
/** zod schema: validator + parser for the JSON-editable prop subset. `z.output ⊆ Props`. */
|
|
@@ -178,21 +201,24 @@ export interface WindoDefinition<Props = unknown, State = unknown, GroupSlug ext
|
|
|
178
201
|
defaultProps: WindoDefaultProps<Props, State>
|
|
179
202
|
/** Names of provider contexts this component opts into. */
|
|
180
203
|
uses?: string[]
|
|
181
|
-
variants
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
code
|
|
204
|
+
/** Gallery variants. Either a static array or a function resolved with the live `ctx` when the component is selected. */
|
|
205
|
+
variants?: Ctxual<WindoVariant<Props>[], State>
|
|
206
|
+
/** 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. */
|
|
207
|
+
props?: Ctxual<WindoPropDoc[], State>
|
|
208
|
+
/** Optional authored code snippet for the Code tab, resolved with the JSON-editable values and the live `ctx`. */
|
|
209
|
+
code?: (values: Props, ctx: WindoRenderContext<State>) => string
|
|
186
210
|
/** A local provider wrapping just this windo (in addition to `uses`). */
|
|
187
211
|
providers?: ComponentType<{ children: ReactNode; ctx: WindoRenderContext<State> }>
|
|
188
212
|
component: (props: Props, ctx: WindoRenderContext<State>) => ReactNode
|
|
189
213
|
}
|
|
190
214
|
|
|
191
215
|
/** Argument handed to the `windo(w => ...)` factory. */
|
|
192
|
-
export interface WindoFactoryArg<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap> {
|
|
216
|
+
export interface WindoFactoryArg<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap, Tags extends readonly string[] = readonly string[]> {
|
|
193
217
|
/** Configured groups keyed by slug. */
|
|
194
218
|
groups: Record<Groups[number]['slug'], WindoGroup>
|
|
195
219
|
contexts: Contexts
|
|
220
|
+
/** The config's declared tags, in declaration order. */
|
|
221
|
+
tags: Tags
|
|
196
222
|
}
|
|
197
223
|
|
|
198
224
|
/**
|
|
@@ -209,11 +235,15 @@ export interface WindoModule<Props = any, State = any> {
|
|
|
209
235
|
* Config
|
|
210
236
|
* ------------------------------------------------------------------ */
|
|
211
237
|
|
|
212
|
-
export interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup[], Contexts extends WindoContextMap = WindoContextMap> {
|
|
238
|
+
export interface WindoConfig<Groups extends readonly WindoGroup[] = readonly WindoGroup[], Contexts extends WindoContextMap = WindoContextMap, Tags extends readonly string[] = readonly string[]> {
|
|
213
239
|
/** Configured groups. A component's `group` must be one of these slugs. */
|
|
214
240
|
groups: Groups
|
|
215
241
|
/** Named contexts available to components. */
|
|
216
242
|
contexts?: Contexts
|
|
243
|
+
/** The set of tags components may be assigned. A component's `tags` must be drawn from this list; the sidebar filters by them. */
|
|
244
|
+
tags?: Tags
|
|
245
|
+
/** Initial shared state exposed on `ctx.ctxState`. Global across every component and persisted across selection — write it from any component via `ctx.setCtxState`. */
|
|
246
|
+
ctxState?: Record<string, unknown>
|
|
217
247
|
/** Glob(s) for discovery, relative to project root. Default `**\/*.windo.tsx`. */
|
|
218
248
|
include?: string | string[]
|
|
219
249
|
/** Title shown in the workbench chrome. */
|
|
@@ -256,6 +286,7 @@ export interface WindoManifestEntry {
|
|
|
256
286
|
id: string
|
|
257
287
|
title: string
|
|
258
288
|
group: string
|
|
289
|
+
tags: string[]
|
|
259
290
|
status: WindoStatus
|
|
260
291
|
description?: string
|
|
261
292
|
deprecation?: string
|
package/src/descriptor.test.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { describeSchema } from './descriptor'
|
|
4
|
-
import type { WindoControlDescriptor } from './types'
|
|
5
|
-
|
|
6
|
-
function field(fields: WindoControlDescriptor[], key: string): WindoControlDescriptor {
|
|
7
|
-
const found = fields.find(f => f.key === key)
|
|
8
|
-
if (!found) throw new Error(`no field "${key}"`)
|
|
9
|
-
return found
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('describeSchema', () => {
|
|
13
|
-
it('returns no fields for a missing schema', () => {
|
|
14
|
-
expect(describeSchema(undefined).fields).toEqual([])
|
|
15
|
-
expect(describeSchema(null).fields).toEqual([])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('describes a z.enum as kind enum with the right options', () => {
|
|
19
|
-
const { fields } = describeSchema(z.object({ variant: z.enum(['primary', 'secondary', 'ghost']) }))
|
|
20
|
-
const variant = field(fields, 'variant')
|
|
21
|
-
expect(variant.kind).toBe('enum')
|
|
22
|
-
expect(variant.options).toEqual(['primary', 'secondary', 'ghost'])
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('describes a z.boolean as kind boolean', () => {
|
|
26
|
-
const { fields } = describeSchema(z.object({ disabled: z.boolean() }))
|
|
27
|
-
expect(field(fields, 'disabled').kind).toBe('boolean')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('describes a bounded z.number with min and max', () => {
|
|
31
|
-
const { fields } = describeSchema(z.object({ count: z.number().min(0).max(10) }))
|
|
32
|
-
const count = field(fields, 'count')
|
|
33
|
-
expect(count.kind).toBe('number')
|
|
34
|
-
expect(count.min).toBe(0)
|
|
35
|
-
expect(count.max).toBe(10)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('marks an optional field as optional', () => {
|
|
39
|
-
const { fields } = describeSchema(z.object({ label: z.string().optional() }))
|
|
40
|
-
const label = field(fields, 'label')
|
|
41
|
-
expect(label.kind).toBe('string')
|
|
42
|
-
expect(label.optional).toBe(true)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('marks a required field as not optional', () => {
|
|
46
|
-
const { fields } = describeSchema(z.object({ label: z.string() }))
|
|
47
|
-
expect(field(fields, 'label').optional).toBe(false)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('describes a z.coerce.date as kind date', () => {
|
|
51
|
-
const { fields } = describeSchema(z.object({ when: z.coerce.date() }))
|
|
52
|
-
expect(field(fields, 'when').kind).toBe('date')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('describes a z.array as kind array', () => {
|
|
56
|
-
const { fields } = describeSchema(z.object({ tags: z.array(z.string()) }))
|
|
57
|
-
expect(field(fields, 'tags').kind).toBe('array')
|
|
58
|
-
})
|
|
59
|
-
})
|