@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.
@@ -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
  }
@@ -138,13 +145,8 @@ button {
138
145
  .wb-logo-mark {
139
146
  width: 22px;
140
147
  height: 22px;
141
- border-radius: 6px;
142
- background: var(--text);
143
- color: var(--panel);
144
- display: grid;
145
- place-items: center;
146
- font-size: 10.5px;
147
- font-weight: 700;
148
+ flex-shrink: 0;
149
+ display: block;
148
150
  }
149
151
 
150
152
  .wb-logo-sub {
@@ -289,6 +291,251 @@ button {
289
291
  box-shadow: 0 0 0 3px var(--accent-soft);
290
292
  }
291
293
 
294
+ /* ---------- Tag filter ---------- */
295
+
296
+ .wb-tagfilter {
297
+ position: relative;
298
+ margin: 0 12px 2px 12px;
299
+ }
300
+
301
+ .wb-tagfilter-control {
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 5px;
305
+ width: 100%;
306
+ min-height: 32px;
307
+ padding: 4px 8px 4px 10px;
308
+ border: 1px solid var(--border);
309
+ border-radius: var(--r);
310
+ background: var(--panel2);
311
+ transition:
312
+ border-color 0.12s,
313
+ box-shadow 0.12s;
314
+ }
315
+
316
+ .wb-tagfilter-control:hover {
317
+ border-color: var(--border-strong);
318
+ }
319
+
320
+ .wb-tagfilter-control.open {
321
+ border-color: var(--border-strong);
322
+ box-shadow: 0 0 0 3px var(--accent-soft);
323
+ }
324
+
325
+ .wb-tagfilter-toggle {
326
+ flex: 1;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: space-between;
330
+ gap: 6px;
331
+ min-width: 0;
332
+ align-self: stretch;
333
+ border: none;
334
+ background: transparent;
335
+ color: var(--text);
336
+ font: inherit;
337
+ font-size: 12.5px;
338
+ text-align: left;
339
+ cursor: pointer;
340
+ }
341
+
342
+ .wb-tagfilter-placeholder {
343
+ flex: 1;
344
+ color: var(--faint);
345
+ }
346
+
347
+ .wb-tagfilter-pills {
348
+ flex: 1 1 auto;
349
+ min-width: 0;
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ gap: 5px;
353
+ }
354
+
355
+ .wb-tagfilter-chev {
356
+ flex-shrink: 0;
357
+ display: inline-flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ align-self: stretch;
361
+ padding: 0 2px;
362
+ border: none;
363
+ background: transparent;
364
+ color: var(--faint);
365
+ cursor: pointer;
366
+ }
367
+
368
+ .wb-tag-pill {
369
+ display: inline-flex;
370
+ align-items: center;
371
+ gap: 4px;
372
+ padding: 3px 4px 3px 9px;
373
+ border-radius: 999px;
374
+ background: var(--accent);
375
+ color: var(--accent-text);
376
+ font-size: 11.5px;
377
+ font-weight: 500;
378
+ line-height: 1;
379
+ }
380
+
381
+ [data-theme="dark"] .wb-tag-pill {
382
+ background: var(--accent-ui);
383
+ color: var(--accent-contrast);
384
+ }
385
+
386
+ .wb-tag-pill .x {
387
+ display: inline-flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ width: 15px;
391
+ height: 15px;
392
+ padding: 0;
393
+ border: none;
394
+ border-radius: 50%;
395
+ background: transparent;
396
+ color: inherit;
397
+ font: inherit;
398
+ font-size: 13px;
399
+ line-height: 1;
400
+ opacity: 0.6;
401
+ cursor: pointer;
402
+ transition:
403
+ opacity 0.1s,
404
+ background 0.1s;
405
+ }
406
+
407
+ .wb-tag-pill .x:hover {
408
+ opacity: 1;
409
+ background: color-mix(in srgb, currentColor 22%, transparent);
410
+ }
411
+
412
+ .wb-tagfilter-control .chev {
413
+ flex-shrink: 0;
414
+ color: var(--faint);
415
+ transition: transform 0.15s;
416
+ }
417
+
418
+ .wb-tagfilter-control.open .chev {
419
+ transform: rotate(180deg);
420
+ }
421
+
422
+ .wb-tagfilter-menu {
423
+ position: absolute;
424
+ z-index: 20;
425
+ top: calc(100% + 4px);
426
+ left: 0;
427
+ right: 0;
428
+ padding: 5px;
429
+ border: 1px solid var(--border);
430
+ border-radius: var(--r);
431
+ background: var(--panel);
432
+ box-shadow: var(--shadow);
433
+ max-height: 280px;
434
+ overflow-y: auto;
435
+ }
436
+
437
+ .wb-tag-option {
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 9px;
441
+ width: 100%;
442
+ padding: 7px 8px;
443
+ border: none;
444
+ border-radius: var(--r);
445
+ background: transparent;
446
+ color: var(--text);
447
+ font: inherit;
448
+ font-size: 13px;
449
+ text-align: left;
450
+ cursor: pointer;
451
+ transition: background 0.1s;
452
+ }
453
+
454
+ .wb-tag-option:hover {
455
+ background: var(--inset);
456
+ }
457
+
458
+ .wb-tag-option .lbl {
459
+ flex: 1;
460
+ overflow: hidden;
461
+ text-overflow: ellipsis;
462
+ white-space: nowrap;
463
+ }
464
+
465
+ .wb-check {
466
+ display: inline-flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ width: 17px;
470
+ height: 17px;
471
+ flex-shrink: 0;
472
+ border: 1.5px solid var(--border-strong);
473
+ border-radius: 5px;
474
+ background: var(--panel2);
475
+ color: var(--accent-text);
476
+ }
477
+
478
+ .wb-check.checked {
479
+ background: var(--accent);
480
+ border-color: var(--accent);
481
+ }
482
+
483
+ [data-theme="dark"] .wb-check.checked {
484
+ background: var(--accent-ui);
485
+ border-color: var(--accent-ui);
486
+ color: var(--accent-contrast);
487
+ }
488
+
489
+ .wb-tag-divider {
490
+ height: 1px;
491
+ margin: 5px 4px;
492
+ background: var(--border);
493
+ }
494
+
495
+ .wb-tagmatch {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 10px;
499
+ margin: 8px 2px 0 2px;
500
+ }
501
+
502
+ .wb-tagmatch-label {
503
+ font-size: 10.5px;
504
+ font-weight: 600;
505
+ letter-spacing: 0.07em;
506
+ text-transform: uppercase;
507
+ color: var(--faint);
508
+ }
509
+
510
+ .wb-tagmatch-seg {
511
+ display: inline-flex;
512
+ padding: 2px;
513
+ border-radius: 999px;
514
+ background: var(--inset);
515
+ }
516
+
517
+ .wb-tagmatch-seg button {
518
+ border: none;
519
+ background: transparent;
520
+ color: var(--muted);
521
+ font: inherit;
522
+ font-size: 12px;
523
+ font-weight: 500;
524
+ padding: 4px 14px;
525
+ border-radius: 999px;
526
+ cursor: pointer;
527
+ transition:
528
+ background 0.12s,
529
+ color 0.12s,
530
+ box-shadow 0.12s;
531
+ }
532
+
533
+ .wb-tagmatch-seg button.on {
534
+ background: var(--panel);
535
+ color: var(--text);
536
+ box-shadow: 0 1px 2px rgba(16, 16, 18, 0.08);
537
+ }
538
+
292
539
  .wb-nav {
293
540
  flex: 1;
294
541
  overflow-y: auto;
@@ -402,29 +649,16 @@ button {
402
649
  }
403
650
 
404
651
  .wb-status.stable {
405
- color: #3c7a4a;
406
- background: rgba(60, 122, 74, 0.1);
652
+ color: var(--success);
653
+ background: color-mix(in srgb, var(--success) 10%, transparent);
407
654
  }
408
655
  .wb-status.beta {
409
- color: #99661c;
410
- background: rgba(153, 102, 28, 0.1);
656
+ color: var(--warn);
657
+ background: color-mix(in srgb, var(--warn) 10%, transparent);
411
658
  }
412
659
  .wb-status.deprecated {
413
- color: #a8433a;
414
- background: rgba(168, 67, 58, 0.1);
415
- }
416
-
417
- [data-theme="dark"] .wb-status.stable {
418
- color: #84c896;
419
- background: rgba(132, 200, 150, 0.12);
420
- }
421
- [data-theme="dark"] .wb-status.beta {
422
- color: #dcb064;
423
- background: rgba(220, 176, 100, 0.12);
424
- }
425
- [data-theme="dark"] .wb-status.deprecated {
426
- color: #e6918a;
427
- background: rgba(230, 145, 138, 0.12);
660
+ color: var(--deprecated);
661
+ background: color-mix(in srgb, var(--deprecated) 10%, transparent);
428
662
  }
429
663
 
430
664
  .wb-nav-empty {
@@ -528,7 +762,7 @@ button {
528
762
  [data-theme="dark"] .wb-trigger.on {
529
763
  background: var(--accent-ui);
530
764
  border-color: var(--accent-ui);
531
- color: #111113;
765
+ color: var(--accent-contrast);
532
766
  }
533
767
 
534
768
  .wb-trigger .tdot {
@@ -1002,6 +1236,45 @@ button {
1002
1236
  font-weight: 600;
1003
1237
  }
1004
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
+
1005
1278
  .wb-tab {
1006
1279
  height: 100%;
1007
1280
  padding: 0 1px;
@@ -1119,7 +1392,7 @@ button {
1119
1392
  [data-theme="dark"] .wb-save {
1120
1393
  background: var(--accent-ui);
1121
1394
  border-color: var(--accent-ui);
1122
- color: #111113;
1395
+ color: var(--accent-contrast);
1123
1396
  }
1124
1397
 
1125
1398
  .wb-save:disabled {
@@ -1161,8 +1434,8 @@ button {
1161
1434
  font-size: 10.5px;
1162
1435
  line-height: 1.6;
1163
1436
  color: var(--danger);
1164
- background: rgba(192, 68, 60, 0.07);
1165
- border: 1px solid rgba(192, 68, 60, 0.25);
1437
+ background: color-mix(in srgb, var(--danger) 7%, transparent);
1438
+ border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
1166
1439
  border-radius: var(--r);
1167
1440
  padding: 8px 10px;
1168
1441
  white-space: pre-wrap;
@@ -1171,11 +1444,7 @@ button {
1171
1444
  .wb-saved-flash {
1172
1445
  font-size: 11.5px;
1173
1446
  font-weight: 550;
1174
- color: #3c7a4a;
1175
- }
1176
-
1177
- [data-theme="dark"] .wb-saved-flash {
1178
- color: #84c896;
1447
+ color: var(--success);
1179
1448
  }
1180
1449
 
1181
1450
  .wb-schema {
@@ -1392,11 +1661,7 @@ button {
1392
1661
  color: var(--text);
1393
1662
  }
1394
1663
  .wb-copy.copied {
1395
- color: #3c7a4a;
1396
- }
1397
-
1398
- [data-theme="dark"] .wb-copy.copied {
1399
- color: #84c896;
1664
+ color: var(--success);
1400
1665
  }
1401
1666
 
1402
1667
  /* ---------- Variants gallery ---------- */
@@ -1645,6 +1910,10 @@ select.wb-ctrl-input {
1645
1910
  background-position: right 8px center;
1646
1911
  }
1647
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
+
1648
1917
  .wb-ctrl-row input[type="range"] {
1649
1918
  flex: 1;
1650
1919
  accent-color: var(--accent-ui);
@@ -1695,7 +1964,7 @@ select.wb-ctrl-input {
1695
1964
  }
1696
1965
 
1697
1966
  [data-theme="dark"] .wb-switch.on::after {
1698
- background: #111113;
1967
+ background: var(--accent-contrast);
1699
1968
  }
1700
1969
 
1701
1970
  .wb-context-empty {
@@ -1810,21 +2079,17 @@ select.wb-ctrl-input {
1810
2079
  color: var(--accent-ui);
1811
2080
  }
1812
2081
  .wb-log.warn .llevel {
1813
- color: #99661c;
2082
+ color: var(--warn);
1814
2083
  }
1815
2084
  .wb-log.error .llevel {
1816
2085
  color: var(--danger);
1817
2086
  }
1818
2087
 
1819
- [data-theme="dark"] .wb-log.warn .llevel {
1820
- color: #dcb064;
1821
- }
1822
-
1823
2088
  .wb-log.warn {
1824
- background: rgba(153, 102, 28, 0.05);
2089
+ background: color-mix(in srgb, var(--warn) 5%, transparent);
1825
2090
  }
1826
2091
  .wb-log.error {
1827
- background: rgba(192, 68, 60, 0.05);
2092
+ background: color-mix(in srgb, var(--danger) 5%, transparent);
1828
2093
  }
1829
2094
 
1830
2095
  .wb-log .lmsg {
@@ -1858,8 +2123,8 @@ select.wb-ctrl-input {
1858
2123
  margin: 14px 16px;
1859
2124
  padding: 12px 14px;
1860
2125
  border-radius: 10px;
1861
- background: rgba(192, 68, 60, 0.07);
1862
- border: 1px solid rgba(192, 68, 60, 0.25);
2126
+ background: color-mix(in srgb, var(--danger) 7%, transparent);
2127
+ border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
1863
2128
  }
1864
2129
 
1865
2130
  .wb-render-error .rtitle {
@@ -1907,8 +2172,8 @@ select.wb-ctrl-input {
1907
2172
  margin: 12px 16px 0 16px;
1908
2173
  padding: 8px 11px;
1909
2174
  border-radius: var(--r);
1910
- background: rgba(168, 67, 58, 0.06);
1911
- border: 1px solid rgba(168, 67, 58, 0.22);
2175
+ background: color-mix(in srgb, var(--deprecated) 6%, transparent);
2176
+ border: 1px solid color-mix(in srgb, var(--deprecated) 22%, transparent);
1912
2177
  color: var(--danger);
1913
2178
  font-size: 12px;
1914
2179
  line-height: 1.45;
@@ -31,6 +31,8 @@ export interface BridgeApi {
31
31
  readyNonce: number
32
32
  title: string
33
33
  groups: WindoGroup[]
34
+ /** The config's declared tag set (filter options for the sidebar). */
35
+ tags: string[]
34
36
  contexts: WindoContextMeta[]
35
37
  manifest: WindoManifestEntry[]
36
38
  /** describe payload for the most recently selected id (null until it arrives). */
@@ -43,15 +45,26 @@ export interface BridgeApi {
43
45
  stateValues: Record<string, Record<string, unknown>>
44
46
  /** Live `disabled` flag per action id (keyed windo id → action id), echoed from the preview. */
45
47
  actionDisabled: Record<string, Record<string, boolean>>
48
+ /** Config seed for the shared `ctxState` (from the manifest) — fills keys the chrome hasn't persisted. */
49
+ ctxStateDefaults: Record<string, unknown>
50
+ /** Latest shared-state snapshot echoed after a component wrote it via `ctx.setCtxState`. */
51
+ ctxState: Record<string, unknown>
52
+ /** 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. */
53
+ colorScheme: { value: 'light' | 'dark' } | null
46
54
  // outbound
47
55
  select: (id: string) => void
48
56
  setProps: (id: string, json: string) => void
49
57
  setEnv: (env: WindoEnvState) => void
58
+ /** Push the shared state down to the preview (editor edit or reload re-sync). */
59
+ setCtxState: (state: Record<string, unknown>) => void
50
60
  invokeAction: (id: string, actionId: string) => void
51
61
  }
52
62
 
53
63
  export type InspectorPosition = 'right' | 'bottom'
54
64
 
65
+ /** How the sidebar's tag filter combines multiple selected tags. */
66
+ export type TagMatch = 'any' | 'all'
67
+
55
68
  export type ThemeMode = 'light' | 'dark'
56
69
 
57
70
  /** Cosmetic canvas chrome (drawn behind a transparent iframe), persisted to localStorage. */
@@ -85,11 +98,18 @@ export interface ChromeEnv {
85
98
 
86
99
  export interface SidebarProps {
87
100
  groups: WindoGroup[]
101
+ /** Declared tag set — the filter's options. */
102
+ tags: string[]
88
103
  manifest: WindoManifestEntry[]
89
104
  selected: string | null
90
105
  onSelect: (id: string) => void
91
106
  query: string
92
107
  setQuery: (q: string) => void
108
+ /** Currently-checked tag filters. Empty = no filter. */
109
+ selectedTags: string[]
110
+ setSelectedTags: (next: string[]) => void
111
+ tagMatch: TagMatch
112
+ setTagMatch: (m: TagMatch) => void
93
113
  collapsed: boolean
94
114
  onToggle: () => void
95
115
  }
@@ -134,6 +154,10 @@ export interface InspectorProps {
134
154
  clearLogs: () => void
135
155
  /** Live component-local state snapshot for the selected windo (drives the read-only State strip). */
136
156
  state: Record<string, unknown>
157
+ /** Shared, cross-component state — drives the editable Shared strip. */
158
+ ctxState: Record<string, unknown>
159
+ /** Edit one key of the shared state (pushes down to the preview). */
160
+ setCtxStateValue: (key: string, value: unknown) => void
137
161
  /** Context metadata filtered to this windo (its `uses` providers + all ambient). */
138
162
  contexts: WindoContextMeta[]
139
163
  env: ChromeEnv
@@ -6,18 +6,22 @@ import type { ComponentType, ReactNode } from 'react'
6
6
  import type { z } from 'zod'
7
7
  import type { WindoConfig, WindoContextDefinition, WindoContextMap, WindoControlMap, WindoControlValues, WindoDefinition, WindoFactoryArg, WindoGroup, WindoModule, WindoRenderContext } from './types'
8
8
 
9
- export interface DefineWindoConfigResult<Groups extends readonly WindoGroup[], Contexts extends WindoContextMap> {
10
- config: WindoConfig<Groups, Contexts>
11
- windo: <Props, State = Record<string, never>>(factory: (w: WindoFactoryArg<Groups, Contexts>) => WindoDefinition<Props, State, Groups[number]['slug']>) => WindoModule<Props, State>
9
+ export interface DefineWindoConfigResult<Groups extends readonly WindoGroup[], Tags extends readonly string[], Contexts extends WindoContextMap> {
10
+ config: WindoConfig<Groups, Contexts, Tags>
11
+ windo: <Props, State = Record<string, never>>(
12
+ factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>
13
+ ) => WindoModule<Props, State>
12
14
  }
13
15
 
14
- export function defineWindoConfig<const Groups extends readonly WindoGroup[], Contexts extends WindoContextMap = Record<string, never>>(
15
- config: WindoConfig<Groups, Contexts>
16
- ): DefineWindoConfigResult<Groups, Contexts> {
17
- function windo<Props, State = Record<string, never>>(factory: (w: WindoFactoryArg<Groups, Contexts>) => WindoDefinition<Props, State, Groups[number]['slug']>): WindoModule<Props, State> {
16
+ export function defineWindoConfig<const Groups extends readonly WindoGroup[], const Tags extends readonly string[] = readonly [], Contexts extends WindoContextMap = Record<string, never>>(
17
+ config: WindoConfig<Groups, Contexts, Tags>
18
+ ): DefineWindoConfigResult<Groups, Tags, Contexts> {
19
+ function windo<Props, State = Record<string, never>>(
20
+ factory: (w: WindoFactoryArg<Groups, Contexts, Tags>) => WindoDefinition<Props, State, Groups[number]['slug'], Tags[number]>
21
+ ): WindoModule<Props, State> {
18
22
  return {
19
23
  __windo: true,
20
- resolve: w => factory(w as unknown as WindoFactoryArg<Groups, Contexts>),
24
+ resolve: w => factory(w as unknown as WindoFactoryArg<Groups, Contexts, Tags>),
21
25
  }
22
26
  }
23
27
  return { config, windo }
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,
@@ -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)) {