@westopp/windo 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-5RM2VYAM.js → chunk-IBJ6MG77.js} +2 -1
- package/dist/chunk-IBJ6MG77.js.map +1 -0
- package/dist/cli.cjs +2 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -10
- package/dist/index.d.ts +53 -10
- package/dist/index.js.map +1 -1
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +64 -1
- package/src/client/Inspector.tsx +37 -0
- package/src/client/bridge.ts +24 -0
- package/src/client/chrome.css +72 -47
- package/src/client/internal-types.ts +14 -0
- package/src/index.ts +2 -0
- package/src/plugin/index.ts +1 -0
- package/src/preview/ctx.ts +8 -1
- package/src/preview/index.ts +153 -10
- package/src/preview/render.tsx +5 -3
- package/src/protocol.ts +7 -0
- package/src/types.ts +35 -9
- package/dist/chunk-5RM2VYAM.js.map +0 -1
package/src/client/chrome.css
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
--accent-text: #ffffff;
|
|
22
22
|
--accent-soft: color-mix(in srgb, var(--accent) 8%, transparent);
|
|
23
23
|
--danger: #c0443c;
|
|
24
|
+
--success: #3c7a4a;
|
|
25
|
+
--warn: #99661c;
|
|
26
|
+
--deprecated: #a8433a;
|
|
24
27
|
--surface: #ffffff;
|
|
25
28
|
--shadow: 0 1px 2px rgba(16, 16, 18, 0.04), 0 8px 24px rgba(16, 16, 18, 0.06);
|
|
26
29
|
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
@@ -41,6 +44,10 @@
|
|
|
41
44
|
--accent-ui: color-mix(in oklab, var(--accent) 35%, white);
|
|
42
45
|
--accent-soft: color-mix(in srgb, var(--accent-ui) 14%, transparent);
|
|
43
46
|
--danger: #e0837b;
|
|
47
|
+
--success: #84c896;
|
|
48
|
+
--warn: #dcb064;
|
|
49
|
+
--deprecated: #e6918a;
|
|
50
|
+
--accent-contrast: #111113;
|
|
44
51
|
--surface: #161618;
|
|
45
52
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 10px 32px rgba(0, 0, 0, 0.45);
|
|
46
53
|
}
|
|
@@ -373,7 +380,7 @@ button {
|
|
|
373
380
|
|
|
374
381
|
[data-theme="dark"] .wb-tag-pill {
|
|
375
382
|
background: var(--accent-ui);
|
|
376
|
-
color:
|
|
383
|
+
color: var(--accent-contrast);
|
|
377
384
|
}
|
|
378
385
|
|
|
379
386
|
.wb-tag-pill .x {
|
|
@@ -476,7 +483,7 @@ button {
|
|
|
476
483
|
[data-theme="dark"] .wb-check.checked {
|
|
477
484
|
background: var(--accent-ui);
|
|
478
485
|
border-color: var(--accent-ui);
|
|
479
|
-
color:
|
|
486
|
+
color: var(--accent-contrast);
|
|
480
487
|
}
|
|
481
488
|
|
|
482
489
|
.wb-tag-divider {
|
|
@@ -642,29 +649,16 @@ button {
|
|
|
642
649
|
}
|
|
643
650
|
|
|
644
651
|
.wb-status.stable {
|
|
645
|
-
color:
|
|
646
|
-
background:
|
|
652
|
+
color: var(--success);
|
|
653
|
+
background: color-mix(in srgb, var(--success) 10%, transparent);
|
|
647
654
|
}
|
|
648
655
|
.wb-status.beta {
|
|
649
|
-
color:
|
|
650
|
-
background:
|
|
656
|
+
color: var(--warn);
|
|
657
|
+
background: color-mix(in srgb, var(--warn) 10%, transparent);
|
|
651
658
|
}
|
|
652
659
|
.wb-status.deprecated {
|
|
653
|
-
color:
|
|
654
|
-
background:
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
[data-theme="dark"] .wb-status.stable {
|
|
658
|
-
color: #84c896;
|
|
659
|
-
background: rgba(132, 200, 150, 0.12);
|
|
660
|
-
}
|
|
661
|
-
[data-theme="dark"] .wb-status.beta {
|
|
662
|
-
color: #dcb064;
|
|
663
|
-
background: rgba(220, 176, 100, 0.12);
|
|
664
|
-
}
|
|
665
|
-
[data-theme="dark"] .wb-status.deprecated {
|
|
666
|
-
color: #e6918a;
|
|
667
|
-
background: rgba(230, 145, 138, 0.12);
|
|
660
|
+
color: var(--deprecated);
|
|
661
|
+
background: color-mix(in srgb, var(--deprecated) 10%, transparent);
|
|
668
662
|
}
|
|
669
663
|
|
|
670
664
|
.wb-nav-empty {
|
|
@@ -768,7 +762,7 @@ button {
|
|
|
768
762
|
[data-theme="dark"] .wb-trigger.on {
|
|
769
763
|
background: var(--accent-ui);
|
|
770
764
|
border-color: var(--accent-ui);
|
|
771
|
-
color:
|
|
765
|
+
color: var(--accent-contrast);
|
|
772
766
|
}
|
|
773
767
|
|
|
774
768
|
.wb-trigger .tdot {
|
|
@@ -1242,6 +1236,45 @@ button {
|
|
|
1242
1236
|
font-weight: 600;
|
|
1243
1237
|
}
|
|
1244
1238
|
|
|
1239
|
+
/* Editable shared-state strip (ctx.ctxState) — same look as .wb-state, with inline editors */
|
|
1240
|
+
|
|
1241
|
+
.wb-ctxstate {
|
|
1242
|
+
display: flex;
|
|
1243
|
+
align-items: center;
|
|
1244
|
+
gap: 12px;
|
|
1245
|
+
flex-wrap: wrap;
|
|
1246
|
+
padding: 7px 16px;
|
|
1247
|
+
border-bottom: 1px solid var(--border);
|
|
1248
|
+
background: var(--inset);
|
|
1249
|
+
font-size: 11.5px;
|
|
1250
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1251
|
+
flex-shrink: 0;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.wb-ctxstate .wb-state-item {
|
|
1255
|
+
gap: 6px;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
.wb-ctxstate-input {
|
|
1259
|
+
width: 90px;
|
|
1260
|
+
padding: 1px 6px;
|
|
1261
|
+
border: 1px solid var(--border);
|
|
1262
|
+
border-radius: 5px;
|
|
1263
|
+
background: var(--bg);
|
|
1264
|
+
color: var(--text);
|
|
1265
|
+
font: inherit;
|
|
1266
|
+
font-weight: 600;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.wb-ctxstate-input[type='number'] {
|
|
1270
|
+
width: 60px;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.wb-ctxstate-input:focus {
|
|
1274
|
+
outline: none;
|
|
1275
|
+
border-color: var(--accent-ui);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1245
1278
|
.wb-tab {
|
|
1246
1279
|
height: 100%;
|
|
1247
1280
|
padding: 0 1px;
|
|
@@ -1359,7 +1392,7 @@ button {
|
|
|
1359
1392
|
[data-theme="dark"] .wb-save {
|
|
1360
1393
|
background: var(--accent-ui);
|
|
1361
1394
|
border-color: var(--accent-ui);
|
|
1362
|
-
color:
|
|
1395
|
+
color: var(--accent-contrast);
|
|
1363
1396
|
}
|
|
1364
1397
|
|
|
1365
1398
|
.wb-save:disabled {
|
|
@@ -1401,8 +1434,8 @@ button {
|
|
|
1401
1434
|
font-size: 10.5px;
|
|
1402
1435
|
line-height: 1.6;
|
|
1403
1436
|
color: var(--danger);
|
|
1404
|
-
background:
|
|
1405
|
-
border: 1px solid
|
|
1437
|
+
background: color-mix(in srgb, var(--danger) 7%, transparent);
|
|
1438
|
+
border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
|
|
1406
1439
|
border-radius: var(--r);
|
|
1407
1440
|
padding: 8px 10px;
|
|
1408
1441
|
white-space: pre-wrap;
|
|
@@ -1411,11 +1444,7 @@ button {
|
|
|
1411
1444
|
.wb-saved-flash {
|
|
1412
1445
|
font-size: 11.5px;
|
|
1413
1446
|
font-weight: 550;
|
|
1414
|
-
color:
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
[data-theme="dark"] .wb-saved-flash {
|
|
1418
|
-
color: #84c896;
|
|
1447
|
+
color: var(--success);
|
|
1419
1448
|
}
|
|
1420
1449
|
|
|
1421
1450
|
.wb-schema {
|
|
@@ -1632,11 +1661,7 @@ button {
|
|
|
1632
1661
|
color: var(--text);
|
|
1633
1662
|
}
|
|
1634
1663
|
.wb-copy.copied {
|
|
1635
|
-
color:
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
[data-theme="dark"] .wb-copy.copied {
|
|
1639
|
-
color: #84c896;
|
|
1664
|
+
color: var(--success);
|
|
1640
1665
|
}
|
|
1641
1666
|
|
|
1642
1667
|
/* ---------- Variants gallery ---------- */
|
|
@@ -1885,6 +1910,10 @@ select.wb-ctrl-input {
|
|
|
1885
1910
|
background-position: right 8px center;
|
|
1886
1911
|
}
|
|
1887
1912
|
|
|
1913
|
+
[data-theme="dark"] select.wb-ctrl-input {
|
|
1914
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239b9ba2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1888
1917
|
.wb-ctrl-row input[type="range"] {
|
|
1889
1918
|
flex: 1;
|
|
1890
1919
|
accent-color: var(--accent-ui);
|
|
@@ -1935,7 +1964,7 @@ select.wb-ctrl-input {
|
|
|
1935
1964
|
}
|
|
1936
1965
|
|
|
1937
1966
|
[data-theme="dark"] .wb-switch.on::after {
|
|
1938
|
-
background:
|
|
1967
|
+
background: var(--accent-contrast);
|
|
1939
1968
|
}
|
|
1940
1969
|
|
|
1941
1970
|
.wb-context-empty {
|
|
@@ -2050,21 +2079,17 @@ select.wb-ctrl-input {
|
|
|
2050
2079
|
color: var(--accent-ui);
|
|
2051
2080
|
}
|
|
2052
2081
|
.wb-log.warn .llevel {
|
|
2053
|
-
color:
|
|
2082
|
+
color: var(--warn);
|
|
2054
2083
|
}
|
|
2055
2084
|
.wb-log.error .llevel {
|
|
2056
2085
|
color: var(--danger);
|
|
2057
2086
|
}
|
|
2058
2087
|
|
|
2059
|
-
[data-theme="dark"] .wb-log.warn .llevel {
|
|
2060
|
-
color: #dcb064;
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
2088
|
.wb-log.warn {
|
|
2064
|
-
background:
|
|
2089
|
+
background: color-mix(in srgb, var(--warn) 5%, transparent);
|
|
2065
2090
|
}
|
|
2066
2091
|
.wb-log.error {
|
|
2067
|
-
background:
|
|
2092
|
+
background: color-mix(in srgb, var(--danger) 5%, transparent);
|
|
2068
2093
|
}
|
|
2069
2094
|
|
|
2070
2095
|
.wb-log .lmsg {
|
|
@@ -2098,8 +2123,8 @@ select.wb-ctrl-input {
|
|
|
2098
2123
|
margin: 14px 16px;
|
|
2099
2124
|
padding: 12px 14px;
|
|
2100
2125
|
border-radius: 10px;
|
|
2101
|
-
background:
|
|
2102
|
-
border: 1px solid
|
|
2126
|
+
background: color-mix(in srgb, var(--danger) 7%, transparent);
|
|
2127
|
+
border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
|
|
2103
2128
|
}
|
|
2104
2129
|
|
|
2105
2130
|
.wb-render-error .rtitle {
|
|
@@ -2147,8 +2172,8 @@ select.wb-ctrl-input {
|
|
|
2147
2172
|
margin: 12px 16px 0 16px;
|
|
2148
2173
|
padding: 8px 11px;
|
|
2149
2174
|
border-radius: var(--r);
|
|
2150
|
-
background:
|
|
2151
|
-
border: 1px solid
|
|
2175
|
+
background: color-mix(in srgb, var(--deprecated) 6%, transparent);
|
|
2176
|
+
border: 1px solid color-mix(in srgb, var(--deprecated) 22%, transparent);
|
|
2152
2177
|
color: var(--danger);
|
|
2153
2178
|
font-size: 12px;
|
|
2154
2179
|
line-height: 1.45;
|
|
@@ -30,6 +30,8 @@ export interface BridgeApi {
|
|
|
30
30
|
/** Increments each time the preview (re)signals ready — e.g. after an HMR or dep-optimize reload. The chrome re-syncs selection/props/env when it changes. */
|
|
31
31
|
readyNonce: number
|
|
32
32
|
title: string
|
|
33
|
+
/** Config `faviconUrl` — drives the chrome logo + browser favicon. `null` falls back to the built-in `wn` mark. */
|
|
34
|
+
faviconUrl: string | null
|
|
33
35
|
groups: WindoGroup[]
|
|
34
36
|
/** The config's declared tag set (filter options for the sidebar). */
|
|
35
37
|
tags: string[]
|
|
@@ -45,10 +47,18 @@ export interface BridgeApi {
|
|
|
45
47
|
stateValues: Record<string, Record<string, unknown>>
|
|
46
48
|
/** Live `disabled` flag per action id (keyed windo id → action id), echoed from the preview. */
|
|
47
49
|
actionDisabled: Record<string, Record<string, boolean>>
|
|
50
|
+
/** Config seed for the shared `ctxState` (from the manifest) — fills keys the chrome hasn't persisted. */
|
|
51
|
+
ctxStateDefaults: Record<string, unknown>
|
|
52
|
+
/** Latest shared-state snapshot echoed after a component wrote it via `ctx.setCtxState`. */
|
|
53
|
+
ctxState: Record<string, unknown>
|
|
54
|
+
/** Latest colour scheme a component pushed via `ctx.toggleTheme`/`setColorScheme` (null until one does). Re-wrapped each echo so repeated same-value pushes still propagate. */
|
|
55
|
+
colorScheme: { value: 'light' | 'dark' } | null
|
|
48
56
|
// outbound
|
|
49
57
|
select: (id: string) => void
|
|
50
58
|
setProps: (id: string, json: string) => void
|
|
51
59
|
setEnv: (env: WindoEnvState) => void
|
|
60
|
+
/** Push the shared state down to the preview (editor edit or reload re-sync). */
|
|
61
|
+
setCtxState: (state: Record<string, unknown>) => void
|
|
52
62
|
invokeAction: (id: string, actionId: string) => void
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -146,6 +156,10 @@ export interface InspectorProps {
|
|
|
146
156
|
clearLogs: () => void
|
|
147
157
|
/** Live component-local state snapshot for the selected windo (drives the read-only State strip). */
|
|
148
158
|
state: Record<string, unknown>
|
|
159
|
+
/** Shared, cross-component state — drives the editable Shared strip. */
|
|
160
|
+
ctxState: Record<string, unknown>
|
|
161
|
+
/** Edit one key of the shared state (pushes down to the preview). */
|
|
162
|
+
setCtxStateValue: (key: string, value: unknown) => void
|
|
149
163
|
/** Context metadata filtered to this windo (its `uses` providers + all ambient). */
|
|
150
164
|
contexts: WindoContextMeta[]
|
|
151
165
|
env: ChromeEnv
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
WINDO_MSG,
|
|
15
15
|
} from './protocol'
|
|
16
16
|
export type {
|
|
17
|
+
Ctxual,
|
|
17
18
|
WindoAction,
|
|
18
19
|
WindoActionMeta,
|
|
19
20
|
WindoActionTrigger,
|
|
@@ -34,6 +35,7 @@ export type {
|
|
|
34
35
|
WindoFactoryArg,
|
|
35
36
|
WindoFieldError,
|
|
36
37
|
WindoGroup,
|
|
38
|
+
WindoInitContext,
|
|
37
39
|
WindoLogEntry,
|
|
38
40
|
WindoLogger,
|
|
39
41
|
WindoManifestEntry,
|
package/src/plugin/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ const CHROME_HTML = `<!doctype html>
|
|
|
40
40
|
<meta charset="utf-8" />
|
|
41
41
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
42
|
<title>windo</title>
|
|
43
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%229%22%20fill%3D%22%23d83a2e%22%2F%3E%3Ctext%20x%3D%2216%22%20y%3D%2222%22%20text-anchor%3D%22middle%22%20font-size%3D%2216%22%20font-weight%3D%22800%22%20font-family%3D%22system-ui%2C-apple-system%2CHelvetica%2CArial%2Csans-serif%22%20fill%3D%22%23f5c518%22%3Ewn%3C%2Ftext%3E%3C%2Fsvg%3E" />
|
|
43
44
|
</head>
|
|
44
45
|
<body>
|
|
45
46
|
<div id="root"></div>
|
package/src/preview/ctx.ts
CHANGED
|
@@ -10,7 +10,10 @@ export function buildRenderContext<State>(
|
|
|
10
10
|
contexts: WindoContextMap,
|
|
11
11
|
postLog: (entry: WindoLogEntry) => void,
|
|
12
12
|
state: State,
|
|
13
|
-
setState: (patch: Partial<State>) => void
|
|
13
|
+
setState: (patch: Partial<State>) => void,
|
|
14
|
+
ctxState: Record<string, unknown>,
|
|
15
|
+
setCtxState: (patch: Record<string, unknown>) => void,
|
|
16
|
+
setColorScheme: (scheme: 'light' | 'dark') => void
|
|
14
17
|
): WindoRenderContext<State> {
|
|
15
18
|
const logger = {
|
|
16
19
|
log: (...args: unknown[]) => postLog({ ts: Date.now(), args }),
|
|
@@ -26,6 +29,10 @@ export function buildRenderContext<State>(
|
|
|
26
29
|
state,
|
|
27
30
|
setState,
|
|
28
31
|
contexts: {},
|
|
32
|
+
ctxState,
|
|
33
|
+
setCtxState,
|
|
34
|
+
setColorScheme,
|
|
35
|
+
toggleTheme: () => setColorScheme(env.colorScheme === 'light' ? 'dark' : 'light'),
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
for (const name of Object.keys(contexts)) {
|
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'
|
|
@@ -46,7 +46,14 @@ let currentSchema: z.ZodType | undefined
|
|
|
46
46
|
let currentValues: unknown = {}
|
|
47
47
|
let env: WindoEnvState = DEFAULT_ENV
|
|
48
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>) ?? {}) }
|
|
49
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
|
|
50
57
|
|
|
51
58
|
function postToParent(msg: WindoPreviewMessage) {
|
|
52
59
|
window.parent.postMessage(msg, '*')
|
|
@@ -60,15 +67,57 @@ function postLog(entry: WindoLogEntry) {
|
|
|
60
67
|
|
|
61
68
|
/** The live ctx, rebuilt on demand so render-time, toolbar, and stage events all share the current state. */
|
|
62
69
|
function makeCtx(): WindoRenderContext {
|
|
63
|
-
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)
|
|
64
82
|
}
|
|
65
83
|
|
|
66
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
|
|
67
88
|
currentState = { ...currentState, ...patch }
|
|
68
89
|
render()
|
|
69
90
|
postState()
|
|
70
91
|
}
|
|
71
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
|
+
|
|
72
121
|
/** Post the current state snapshot + each action's live `disabled` flag up to the chrome. */
|
|
73
122
|
function postState() {
|
|
74
123
|
if (!currentId || !currentDef) return
|
|
@@ -113,11 +162,13 @@ function manifestEntry(id: string, def: WindoDefinition): WindoManifestEntry {
|
|
|
113
162
|
group: def.group,
|
|
114
163
|
tags: def.tags ?? [],
|
|
115
164
|
status: def.status ?? 'stable',
|
|
116
|
-
|
|
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'),
|
|
117
168
|
uses: def.uses ?? [],
|
|
118
|
-
hasVariants: (def.variants?.length ?? 0) > 0,
|
|
169
|
+
hasVariants: typeof def.variants === 'function' ? true : (def.variants?.length ?? 0) > 0,
|
|
119
170
|
actions,
|
|
120
|
-
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,
|
|
121
172
|
}
|
|
122
173
|
if (def.description !== undefined) entry.description = def.description
|
|
123
174
|
if (def.deprecation !== undefined) entry.deprecation = def.deprecation
|
|
@@ -132,10 +183,85 @@ function computeDefaults(schema: z.ZodType | undefined): unknown {
|
|
|
132
183
|
|
|
133
184
|
function safeCode(def: WindoDefinition, defaults: unknown): string | null {
|
|
134
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
|
|
135
189
|
try {
|
|
136
|
-
return def.code(defaults as never)
|
|
190
|
+
return def.code(defaults as never, makeCtx())
|
|
137
191
|
} catch {
|
|
138
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
|
|
139
265
|
}
|
|
140
266
|
}
|
|
141
267
|
|
|
@@ -145,10 +271,12 @@ function postManifest() {
|
|
|
145
271
|
dir: 'preview',
|
|
146
272
|
type: 'manifest',
|
|
147
273
|
title: config.title ?? 'windo',
|
|
274
|
+
faviconUrl: config.faviconUrl ?? null,
|
|
148
275
|
entries,
|
|
149
276
|
groups: [...config.groups],
|
|
150
277
|
tags: config.tags ? [...config.tags] : [],
|
|
151
278
|
contexts: buildContextMeta(contexts),
|
|
279
|
+
ctxState: cloneableCtxState(),
|
|
152
280
|
})
|
|
153
281
|
}
|
|
154
282
|
|
|
@@ -156,14 +284,14 @@ function postDescribe(id: string, def: WindoDefinition, schema: z.ZodType | unde
|
|
|
156
284
|
const defaults = computeDefaults(schema)
|
|
157
285
|
// variant patches and defaults may carry ReactNodes/functions that postMessage
|
|
158
286
|
// cannot clone — reduce them to JSON-safe forms before they cross the boundary.
|
|
159
|
-
const variants: WindoVariantMeta[] = (def
|
|
287
|
+
const variants: WindoVariantMeta[] = safeVariants(def).map(v => ({ label: v.label, props: toCloneable(v.props) as Record<string, unknown> }))
|
|
160
288
|
postToParent({
|
|
161
289
|
source: WINDO_MSG,
|
|
162
290
|
dir: 'preview',
|
|
163
291
|
type: 'describe',
|
|
164
292
|
id,
|
|
165
293
|
descriptor: describeSchema(schema),
|
|
166
|
-
props: def
|
|
294
|
+
props: safeProps(def),
|
|
167
295
|
variants,
|
|
168
296
|
defaults: toCloneable(defaults),
|
|
169
297
|
code: safeCode(def, defaults),
|
|
@@ -174,12 +302,18 @@ function render() {
|
|
|
174
302
|
if (!currentDef || !root) return
|
|
175
303
|
const ctx = makeCtx()
|
|
176
304
|
const id = currentId ?? ''
|
|
305
|
+
// Resolve the full-ctx fields here, OUTSIDE the React render cycle, so a function
|
|
306
|
+
// field that touches a mutating callback can't re-enter render() synchronously.
|
|
307
|
+
const defaultProps = safeDefaultProps(currentDef, ctx)
|
|
308
|
+
const placement = safePlacement(currentDef, ctx)
|
|
177
309
|
root.render(
|
|
178
310
|
createElement(PreviewRoot, {
|
|
179
311
|
def: currentDef,
|
|
180
312
|
ctx,
|
|
181
313
|
values: currentValues,
|
|
182
314
|
contexts,
|
|
315
|
+
defaultProps,
|
|
316
|
+
placement,
|
|
183
317
|
onStage: dispatchStage,
|
|
184
318
|
onError: (message: string, stack?: string) => {
|
|
185
319
|
postToParent({ source: WINDO_MSG, dir: 'preview', type: 'render-error', id, message, ...(stack === undefined ? {} : { stack }) })
|
|
@@ -195,8 +329,11 @@ function handleSelect(id: string) {
|
|
|
195
329
|
currentDef = match.def
|
|
196
330
|
currentSchema = match.def.configurableProps
|
|
197
331
|
currentValues = computeDefaults(currentSchema)
|
|
198
|
-
// Reset component-local state to the windo's declared initial state.
|
|
199
|
-
|
|
332
|
+
// Reset component-local state to the windo's declared initial state. Resolve it
|
|
333
|
+
// ONCE here — a `state` function runs against the init ctx (no live state/setState)
|
|
334
|
+
// under `resolving`, degrading to `{}` on throw. It is never re-resolved in
|
|
335
|
+
// render/makeCtx/set-env/set-ctx-state, so the author cannot drive a resolution loop.
|
|
336
|
+
currentState = safeState(currentDef)
|
|
200
337
|
render()
|
|
201
338
|
postState()
|
|
202
339
|
postDescribe(id, currentDef, currentSchema)
|
|
@@ -249,6 +386,12 @@ function handleMessage(msg: WindoHostMessage) {
|
|
|
249
386
|
env = msg.env
|
|
250
387
|
render()
|
|
251
388
|
break
|
|
389
|
+
case 'set-ctx-state':
|
|
390
|
+
// Chrome is the source of this update (an editor edit or a reload re-sync),
|
|
391
|
+
// so adopt it and re-render — but don't echo it back, to avoid a ping-pong.
|
|
392
|
+
ctxState = { ...msg.state }
|
|
393
|
+
render()
|
|
394
|
+
break
|
|
252
395
|
case 'invoke-action':
|
|
253
396
|
invokeAction(msg.actionId)
|
|
254
397
|
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 */
|
|
@@ -23,10 +24,14 @@ export type WindoPreviewMessage =
|
|
|
23
24
|
dir: 'preview'
|
|
24
25
|
type: 'manifest'
|
|
25
26
|
title: string
|
|
27
|
+
/** Config `faviconUrl` — drives the chrome logo + browser favicon. `null` falls back to the built-in `wn` mark. */
|
|
28
|
+
faviconUrl: string | null
|
|
26
29
|
entries: WindoManifestEntry[]
|
|
27
30
|
groups: WindoGroup[]
|
|
28
31
|
tags: string[]
|
|
29
32
|
contexts: WindoContextMeta[]
|
|
33
|
+
/** Initial shared state from the config — seeds the chrome's editable strip. */
|
|
34
|
+
ctxState: Record<string, unknown>
|
|
30
35
|
}
|
|
31
36
|
| {
|
|
32
37
|
source: typeof WINDO_MSG
|
|
@@ -43,6 +48,8 @@ export type WindoPreviewMessage =
|
|
|
43
48
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'parse-error'; id: string; errors: WindoFieldError[] }
|
|
44
49
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'log'; entry: WindoLogEntry }
|
|
45
50
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'state'; id: string; state: Record<string, unknown>; actions: { id: string; disabled: boolean }[] }
|
|
51
|
+
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'ctx-state'; state: Record<string, unknown> }
|
|
52
|
+
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'color-scheme'; colorScheme: 'light' | 'dark' }
|
|
46
53
|
| { source: typeof WINDO_MSG; dir: 'preview'; type: 'render-error'; id: string; message: string; stack?: string }
|
|
47
54
|
|
|
48
55
|
export type WindoMessage = WindoHostMessage | WindoPreviewMessage
|