@tilewise/workbench-client 0.0.0-stage → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +176 -2
  4. package/dist/blocks/ScreenScope.d.ts +11 -0
  5. package/dist/blocks/ScreenScope.js +11 -0
  6. package/dist/blocks/drawnBlocks.d.ts +11 -0
  7. package/dist/blocks/drawnBlocks.js +47 -0
  8. package/dist/blocks/screenContext.d.ts +2 -0
  9. package/dist/blocks/screenContext.js +3 -0
  10. package/dist/bridge/Bridge.d.ts +9 -0
  11. package/dist/bridge/Bridge.js +120 -0
  12. package/dist/bridge/device.d.ts +6 -0
  13. package/dist/bridge/device.js +28 -0
  14. package/dist/bridge/post.d.ts +5 -0
  15. package/dist/bridge/post.js +8 -0
  16. package/dist/bridge/readViewerMessage.d.ts +3 -0
  17. package/dist/bridge/readViewerMessage.js +44 -0
  18. package/dist/bridge/revealControl.d.ts +10 -0
  19. package/dist/bridge/revealControl.js +62 -0
  20. package/dist/bridge/toneAtTop.d.ts +3 -0
  21. package/dist/bridge/toneAtTop.js +23 -0
  22. package/dist/bridge/watchScreen.d.ts +10 -0
  23. package/dist/bridge/watchScreen.js +50 -0
  24. package/dist/bridge/wholeHeight.d.ts +8 -0
  25. package/dist/bridge/wholeHeight.js +16 -0
  26. package/dist/controls/paint.d.ts +8 -0
  27. package/dist/controls/paint.js +16 -0
  28. package/dist/controls/parseValue.d.ts +7 -0
  29. package/dist/controls/parseValue.js +34 -0
  30. package/dist/controls/resetControl.d.ts +3 -0
  31. package/dist/controls/resetControl.js +6 -0
  32. package/dist/controls/setControl.d.ts +3 -0
  33. package/dist/controls/setControl.js +6 -0
  34. package/dist/controls/store.d.ts +12 -0
  35. package/dist/controls/store.js +125 -0
  36. package/dist/controls/types.d.ts +111 -0
  37. package/dist/controls/types.js +6 -0
  38. package/dist/controls/useControl.d.ts +7 -0
  39. package/dist/controls/useControl.js +16 -0
  40. package/dist/define/buildManifest.d.ts +8 -0
  41. package/dist/define/buildManifest.js +51 -0
  42. package/dist/define/checkDefaults.d.ts +3 -0
  43. package/dist/define/checkDefaults.js +29 -0
  44. package/dist/define/defineApp.d.ts +24 -0
  45. package/dist/define/defineApp.js +92 -0
  46. package/dist/define/defineBlock.d.ts +12 -0
  47. package/dist/define/defineBlock.js +10 -0
  48. package/dist/define/defineScreen.d.ts +11 -0
  49. package/dist/define/defineScreen.js +10 -0
  50. package/dist/define/part.d.ts +23 -0
  51. package/dist/define/part.js +22 -0
  52. package/dist/define/registeredApp.d.ts +12 -0
  53. package/dist/define/registeredApp.js +5 -0
  54. package/dist/draw/boolean.d.ts +6 -0
  55. package/dist/draw/boolean.js +5 -0
  56. package/dist/draw/choose.d.ts +4 -0
  57. package/dist/draw/choose.js +4 -0
  58. package/dist/draw/color.d.ts +2 -0
  59. package/dist/draw/color.js +4 -0
  60. package/dist/draw/number.d.ts +5 -0
  61. package/dist/draw/number.js +5 -0
  62. package/dist/draw/text.d.ts +5 -0
  63. package/dist/draw/text.js +5 -0
  64. package/dist/draw/variant.d.ts +17 -0
  65. package/dist/draw/variant.js +4 -0
  66. package/dist/index.d.ts +18 -0
  67. package/dist/index.js +14 -0
  68. package/dist/protocol/protocol.d.ts +8 -0
  69. package/dist/protocol/protocol.js +8 -0
  70. package/dist/protocol/types.d.ts +133 -0
  71. package/dist/protocol/types.js +1 -0
  72. package/dist/vite/builds.d.ts +29 -0
  73. package/dist/vite/builds.js +196 -0
  74. package/dist/vite/checkout.d.ts +11 -0
  75. package/dist/vite/checkout.js +43 -0
  76. package/dist/vite/environment.d.ts +9 -0
  77. package/dist/vite/environment.js +10 -0
  78. package/dist/vite/git.d.ts +7 -0
  79. package/dist/vite/git.js +18 -0
  80. package/dist/vite/index.d.ts +1 -0
  81. package/dist/vite/index.js +1 -0
  82. package/dist/vite/serveBuild.d.ts +4 -0
  83. package/dist/vite/serveBuild.js +39 -0
  84. package/dist/vite/versions.d.ts +44 -0
  85. package/dist/vite/versions.js +122 -0
  86. package/dist/vite/workbenchLocal.d.ts +21 -0
  87. package/dist/vite/workbenchLocal.js +186 -0
  88. package/docs/protocol.md +58 -0
  89. package/package.json +60 -3
@@ -0,0 +1,50 @@
1
+ import { isWhole, onDeviceChange } from './device.js';
2
+ import { toneAtTop } from './toneAtTop.js';
3
+ import { wholeHeight } from './wholeHeight.js';
4
+ /**
5
+ * Tells the viewer the tone at the top of the screen and, on Full page, the
6
+ * screen's height, whenever either changes. Returns how to stop.
7
+ */
8
+ export function watchScreen(report) {
9
+ let tone = null;
10
+ let height = 0;
11
+ // A timer, not a frame: a frame behind another window never draws, but the viewer still listens.
12
+ let timer;
13
+ const check = () => {
14
+ timer = undefined;
15
+ const now = toneAtTop();
16
+ if (now !== tone) {
17
+ tone = now;
18
+ report({ type: 'tone', top: now });
19
+ }
20
+ if (!isWhole()) {
21
+ height = 0;
22
+ return;
23
+ }
24
+ const whole = Math.ceil(wholeHeight());
25
+ if (whole !== height) {
26
+ height = whole;
27
+ report({ type: 'height', height: whole });
28
+ }
29
+ };
30
+ const soon = () => {
31
+ timer ??= setTimeout(check, 50);
32
+ };
33
+ onDeviceChange(soon);
34
+ const mutations = new MutationObserver(soon);
35
+ mutations.observe(document.body, {
36
+ subtree: true,
37
+ childList: true,
38
+ attributes: true,
39
+ attributeFilter: ['class', 'style', 'data-workbench-body'],
40
+ });
41
+ const resizes = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(soon);
42
+ resizes?.observe(document.documentElement);
43
+ soon();
44
+ return () => {
45
+ mutations.disconnect();
46
+ resizes?.disconnect();
47
+ clearTimeout(timer);
48
+ onDeviceChange(null);
49
+ };
50
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How tall the window must be for the open screen to show all of it: its
3
+ * height plus what the open screen's scrolling body hides. The app marks each
4
+ * screen's scrolling body `data-workbench-body`, and takes the mark off a
5
+ * screen covered by another; a body with no height, as in a hidden tab, is
6
+ * skipped.
7
+ */
8
+ export declare function wholeHeight(): number;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How tall the window must be for the open screen to show all of it: its
3
+ * height plus what the open screen's scrolling body hides. The app marks each
4
+ * screen's scrolling body `data-workbench-body`, and takes the mark off a
5
+ * screen covered by another; a body with no height, as in a hidden tab, is
6
+ * skipped.
7
+ */
8
+ export function wholeHeight() {
9
+ let more = 0;
10
+ for (const body of document.querySelectorAll('[data-workbench-body]')) {
11
+ if (body.clientHeight > 0) {
12
+ more = Math.max(more, body.scrollHeight - body.clientHeight);
13
+ }
14
+ }
15
+ return window.innerHeight + more;
16
+ }
@@ -0,0 +1,8 @@
1
+ import type { Control, Value } from './types.js';
2
+ /**
3
+ * Writes a global control's value to the root, as `html[data-<id>]`, and a
4
+ * number or a color also as `--tweak-<id>`, so a pure-CSS tweak needs no code.
5
+ * Screen and block values are never written here: a component that styles by
6
+ * one puts it on its own root.
7
+ */
8
+ export declare function paint(control: Control, value: Value): void;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Writes a global control's value to the root, as `html[data-<id>]`, and a
3
+ * number or a color also as `--tweak-<id>`, so a pure-CSS tweak needs no code.
4
+ * Screen and block values are never written here: a component that styles by
5
+ * one puts it on its own root.
6
+ */
7
+ export function paint(control, value) {
8
+ const root = document.documentElement;
9
+ root.setAttribute(`data-${control.id}`, String(value));
10
+ if (control.type === 'number') {
11
+ root.style.setProperty(`--tweak-${control.id}`, String(value));
12
+ }
13
+ else if (control.type === 'color') {
14
+ root.style.setProperty(`--tweak-${control.id}`, `var(--${String(value)})`);
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import type { Control, Value } from './types.js';
2
+ /**
3
+ * A value as it arrives, as text, checked against its control. Anything that
4
+ * doesn't fit falls back to the default; a number outside its range is
5
+ * clamped, and text over `maxLength` is cut.
6
+ */
7
+ export declare function parseValue(control: Control, raw: string | null | undefined): Value;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A value as it arrives, as text, checked against its control. Anything that
3
+ * doesn't fit falls back to the default; a number outside its range is
4
+ * clamped, and text over `maxLength` is cut.
5
+ */
6
+ export function parseValue(control, raw) {
7
+ if (raw === null || raw === undefined) {
8
+ return control.default;
9
+ }
10
+ switch (control.type) {
11
+ case 'boolean': {
12
+ if (raw === 'true') {
13
+ return true;
14
+ }
15
+ return raw === 'false' ? false : control.default;
16
+ }
17
+ case 'number': {
18
+ const number = Number(raw);
19
+ if (raw.trim() === '' || !Number.isFinite(number)) {
20
+ return control.default;
21
+ }
22
+ return Math.min(control.max, Math.max(control.min, number));
23
+ }
24
+ case 'variant': {
25
+ return control.options.some((option) => option.value === raw) ? raw : control.default;
26
+ }
27
+ case 'color': {
28
+ return control.options.some((option) => option.token === raw) ? raw : control.default;
29
+ }
30
+ case 'text': {
31
+ return control.maxLength === undefined ? raw : raw.slice(0, control.maxLength);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { Control, Handle } from './types.js';
2
+ /** Puts a control back to its default, and forgets the value picked. */
3
+ export declare function resetControl<C extends Control>(handle: Handle<C>): void;
@@ -0,0 +1,6 @@
1
+ import { declare, resetValue } from './store.js';
2
+ /** Puts a control back to its default, and forgets the value picked. */
3
+ export function resetControl(handle) {
4
+ declare(handle);
5
+ resetValue(handle.id);
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { Control, Handle, ValueOf } from './types.js';
2
+ /** Sets a control, as the viewer's `set` does: checked against its declaration first. */
3
+ export declare function setControl<C extends Control>(handle: Handle<C>, value: ValueOf<C>): void;
@@ -0,0 +1,6 @@
1
+ import { declare, setValue } from './store.js';
2
+ /** Sets a control, as the viewer's `set` does: checked against its declaration first. */
3
+ export function setControl(handle, value) {
4
+ declare(handle);
5
+ setValue(handle.id, value);
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { Handle, Value } from './types.js';
2
+ /** Makes a control known, with its starting value, and returns its value now. */
3
+ export declare function declare(handle: Handle): Value;
4
+ /** Sets a control by its id, checked against its declaration. False for an id nobody declared. */
5
+ export declare function setValue(id: string, raw: Value): boolean;
6
+ /** Puts a control back to its default, by its id. False for an id nobody declared. */
7
+ export declare function resetValue(id: string): boolean;
8
+ /** Puts every control back. */
9
+ export declare function resetAll(): void;
10
+ /** Every control's value, now. The same object until one changes. */
11
+ export declare const valuesNow: () => Readonly<Record<string, Value>>;
12
+ export declare function subscribeValues(listener: () => void): () => void;
@@ -0,0 +1,125 @@
1
+ import { paint } from './paint.js';
2
+ import { parseValue } from './parseValue.js';
3
+ const entries = new Map();
4
+ const listeners = new Set();
5
+ let values = {};
6
+ let listening = false;
7
+ const storageKey = (id) => `control:${id}`;
8
+ const inPage = () => typeof window !== 'undefined';
9
+ function emit() {
10
+ for (const listener of listeners) {
11
+ listener();
12
+ }
13
+ }
14
+ function store(id, value) {
15
+ try {
16
+ if (value === null) {
17
+ localStorage.removeItem(storageKey(id));
18
+ }
19
+ else {
20
+ localStorage.setItem(storageKey(id), String(value));
21
+ }
22
+ }
23
+ catch {
24
+ // Storage is off, as in some private windows: the value lasts this page only.
25
+ }
26
+ }
27
+ function startingValue(control) {
28
+ if (!inPage()) {
29
+ return control.default;
30
+ }
31
+ const linked = new URLSearchParams(window.location.search).get(control.id);
32
+ if (linked !== null) {
33
+ const value = parseValue(control, linked);
34
+ store(control.id, value);
35
+ return value;
36
+ }
37
+ try {
38
+ return parseValue(control, localStorage.getItem(storageKey(control.id)));
39
+ }
40
+ catch {
41
+ return control.default;
42
+ }
43
+ }
44
+ function put(id, entry, value) {
45
+ values = { ...values, [id]: value };
46
+ if (entry.global && inPage()) {
47
+ paint(entry.control, value);
48
+ }
49
+ }
50
+ function followOtherTabs() {
51
+ if (listening || !inPage()) {
52
+ return;
53
+ }
54
+ listening = true;
55
+ window.addEventListener('storage', (event) => {
56
+ if (!event.key?.startsWith('control:')) {
57
+ return;
58
+ }
59
+ const id = event.key.slice('control:'.length);
60
+ const entry = entries.get(id);
61
+ if (!entry) {
62
+ return;
63
+ }
64
+ const value = parseValue(entry.control, event.newValue);
65
+ if (values[id] !== value) {
66
+ put(id, entry, value);
67
+ emit();
68
+ }
69
+ });
70
+ }
71
+ /** Makes a control known, with its starting value, and returns its value now. */
72
+ export function declare(handle) {
73
+ const known = values[handle.id];
74
+ if (known !== undefined) {
75
+ return known;
76
+ }
77
+ const entry = { control: handle.control, global: handle.place.level === 'global' };
78
+ entries.set(handle.id, entry);
79
+ put(handle.id, entry, startingValue(handle.control));
80
+ followOtherTabs();
81
+ // Known while drawing: everyone who reads every value hears of it next.
82
+ queueMicrotask(emit);
83
+ return values[handle.id];
84
+ }
85
+ /** Sets a control by its id, checked against its declaration. False for an id nobody declared. */
86
+ export function setValue(id, raw) {
87
+ const entry = entries.get(id);
88
+ if (!entry) {
89
+ return false;
90
+ }
91
+ const value = parseValue(entry.control, String(raw));
92
+ if (values[id] !== value) {
93
+ store(id, value);
94
+ put(id, entry, value);
95
+ emit();
96
+ }
97
+ return true;
98
+ }
99
+ /** Puts a control back to its default, by its id. False for an id nobody declared. */
100
+ export function resetValue(id) {
101
+ const entry = entries.get(id);
102
+ if (!entry) {
103
+ return false;
104
+ }
105
+ store(id, null);
106
+ if (values[id] !== entry.control.default) {
107
+ put(id, entry, entry.control.default);
108
+ emit();
109
+ }
110
+ return true;
111
+ }
112
+ /** Puts every control back. */
113
+ export function resetAll() {
114
+ for (const id of entries.keys()) {
115
+ resetValue(id);
116
+ }
117
+ }
118
+ /** Every control's value, now. The same object until one changes. */
119
+ export const valuesNow = () => values;
120
+ export function subscribeValues(listener) {
121
+ listeners.add(listener);
122
+ return () => {
123
+ listeners.delete(listener);
124
+ };
125
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A control: one value a reviewer can change from the viewer. There are five
3
+ * types and no others; the viewer draws one fixed row for each, so a new
4
+ * control is one more declaration, never new UI.
5
+ */
6
+ export interface Option {
7
+ value: string;
8
+ label: string;
9
+ }
10
+ export interface ColorOption {
11
+ /** A name from the app's tokens, such as `lime` for `--lime`. */
12
+ token: string;
13
+ label: string;
14
+ }
15
+ interface Base {
16
+ /** Its name in links, storage and messages. Unique across the app, and never renamed. */
17
+ id: string;
18
+ /** Its name in the form. */
19
+ label: string;
20
+ /** A line the form shows quietly under its row, for a rule its label can't hold. */
21
+ note?: string;
22
+ }
23
+ export type VariantControl = Base & {
24
+ type: 'variant';
25
+ options: readonly Option[];
26
+ /** One of the options' values. */
27
+ default: string;
28
+ };
29
+ export type BooleanControl = Base & {
30
+ type: 'boolean';
31
+ default: boolean;
32
+ };
33
+ export type NumberControl = Base & {
34
+ type: 'number';
35
+ min: number;
36
+ max: number;
37
+ step?: number;
38
+ default: number;
39
+ };
40
+ export type ColorControl = Base & {
41
+ type: 'color';
42
+ options: readonly ColorOption[];
43
+ /** One of the options' tokens. */
44
+ default: string;
45
+ };
46
+ export type TextControl = Base & {
47
+ type: 'text';
48
+ maxLength?: number;
49
+ default: string;
50
+ };
51
+ export type Control = VariantControl | BooleanControl | NumberControl | ColorControl | TextControl;
52
+ /** A control's value: a string, a number or a boolean. */
53
+ export type Value = string | number | boolean;
54
+ /** The value a control holds, from its declaration: a variant's values as a union. */
55
+ export type ValueOf<C> = C extends {
56
+ type: 'boolean';
57
+ } ? boolean : C extends {
58
+ type: 'number';
59
+ } ? number : C extends {
60
+ type: 'variant';
61
+ options: readonly (infer O)[];
62
+ } ? O extends {
63
+ value: infer V extends string;
64
+ } ? V : never : C extends {
65
+ type: 'color';
66
+ options: readonly (infer O)[];
67
+ } ? O extends {
68
+ token: infer T extends string;
69
+ } ? T : never : string;
70
+ /** Where a control shows in the form. */
71
+ export type Level = 'global' | 'screen' | 'block';
72
+ /** Where a control sits: the whole app, one screen, or one block. */
73
+ export type Place = {
74
+ level: 'global';
75
+ } | {
76
+ level: 'screen';
77
+ screen: string;
78
+ } | {
79
+ level: 'block';
80
+ block: string;
81
+ };
82
+ /** What a part hands its code for one control: read it with `useControl(handle)`. */
83
+ export interface Handle<C extends Control = Control> {
84
+ readonly id: string;
85
+ readonly control: C;
86
+ readonly place: Place;
87
+ }
88
+ /** The declared shape of a control of the same type as `C`. */
89
+ type ShapeOf<C> = C extends {
90
+ type: infer T;
91
+ } ? Extract<Control, {
92
+ type: T;
93
+ }> : Control;
94
+ /** Refuses keys a control's type doesn't have, as `screens`. */
95
+ type NoExtraKeys<C> = Record<Exclude<keyof C, keyof ShapeOf<C>>, never>;
96
+ type ExactOptions<C> = C extends {
97
+ options: readonly (infer O)[];
98
+ } ? ShapeOf<C> extends {
99
+ options: readonly (infer S)[];
100
+ } ? {
101
+ options: readonly (O & Record<Exclude<keyof O, keyof S>, never>)[];
102
+ } : unknown : unknown;
103
+ /**
104
+ * Each control as declared, held to its type: no keys it doesn't have, and no
105
+ * option's `short`. A default that isn't one of its values is caught as the
106
+ * app starts.
107
+ */
108
+ export type Declared<C extends readonly Control[]> = {
109
+ [K in keyof C]: C[K] & NoExtraKeys<C[K]> & ExactOptions<C[K]>;
110
+ };
111
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A control: one value a reviewer can change from the viewer. There are five
3
+ * types and no others; the viewer draws one fixed row for each, so a new
4
+ * control is one more declaration, never new UI.
5
+ */
6
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { Control, Handle, ValueOf } from './types.js';
2
+ /**
3
+ * A control's value, typed by its declaration, drawn again whenever it
4
+ * changes. A block's control also reports the block as drawn on the screen
5
+ * of the nearest `<ScreenScope>`, until it unmounts.
6
+ */
7
+ export declare function useControl<C extends Control>(handle: Handle<C>): ValueOf<C>;
@@ -0,0 +1,16 @@
1
+ import { useContext, useEffect, useSyncExternalStore } from 'react';
2
+ import { reportBlock } from '../blocks/drawnBlocks.js';
3
+ import { ScreenContext } from '../blocks/screenContext.js';
4
+ import { declare, subscribeValues } from './store.js';
5
+ /**
6
+ * A control's value, typed by its declaration, drawn again whenever it
7
+ * changes. A block's control also reports the block as drawn on the screen
8
+ * of the nearest `<ScreenScope>`, until it unmounts.
9
+ */
10
+ export function useControl(handle) {
11
+ const value = useSyncExternalStore(subscribeValues, () => declare(handle), () => handle.control.default);
12
+ const screen = useContext(ScreenContext);
13
+ const block = handle.place.level === 'block' ? handle.place.block : null;
14
+ useEffect(() => (screen && block ? reportBlock(screen, block) : undefined), [screen, block]);
15
+ return value;
16
+ }
@@ -0,0 +1,8 @@
1
+ import type { Manifest, Screen } from '../protocol/types.js';
2
+ import { type Part } from './part.js';
3
+ /**
4
+ * Everything the app has, for the viewer: its screens and states, every
5
+ * control with where it sits, and every block. A state goes without its jump,
6
+ * so only the fields the protocol names are copied.
7
+ */
8
+ export declare function buildManifest(app: string, screens: readonly Screen[], parts: readonly Part[]): Manifest;
@@ -0,0 +1,51 @@
1
+ import { PART } from './part.js';
2
+ /**
3
+ * Everything the app has, for the viewer: its screens and states, every
4
+ * control with where it sits, and every block. A state goes without its jump,
5
+ * so only the fields the protocol names are copied.
6
+ */
7
+ export function buildManifest(app, screens, parts) {
8
+ const controls = parts.flatMap((part) => {
9
+ const { place, controls: declared } = part[PART];
10
+ return declared.map((control) => ({ ...control, ...place }));
11
+ });
12
+ const blocks = parts.flatMap((part) => {
13
+ const { place, label, controls: declared } = part[PART];
14
+ return place.level === 'block'
15
+ ? [
16
+ {
17
+ id: place.block,
18
+ label: label ?? place.block,
19
+ controls: declared.map((control) => control.id),
20
+ },
21
+ ]
22
+ : [];
23
+ });
24
+ return { app, screens: screens.map(screenOf), controls, blocks };
25
+ }
26
+ function screenOf(screen) {
27
+ const { id, label, short, parent, tab, group, page, where, statesBy, files } = screen;
28
+ return {
29
+ id,
30
+ label,
31
+ short,
32
+ ...(parent !== undefined && { parent }),
33
+ ...(tab !== undefined && { tab }),
34
+ ...(group !== undefined && { group }),
35
+ ...(page !== undefined && { page }),
36
+ ...(where !== undefined && { where }),
37
+ ...(statesBy !== undefined && { statesBy }),
38
+ ...(files !== undefined && { files: [...files] }),
39
+ states: screen.states.map(stateOf),
40
+ };
41
+ }
42
+ function stateOf(state) {
43
+ const { id, label, kind, note, when } = state;
44
+ return {
45
+ id,
46
+ label,
47
+ ...(kind !== undefined && { kind }),
48
+ ...(note !== undefined && { note }),
49
+ ...(when !== undefined && { when: [...when] }),
50
+ };
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { Control } from '../controls/types.js';
2
+ /** Throws on a `default` that isn't one of the options, or a number's outside `min` and `max`. */
3
+ export declare function checkDefaults(controls: readonly Control[], where: string): void;
@@ -0,0 +1,29 @@
1
+ /** Throws on a `default` that isn't one of the options, or a number's outside `min` and `max`. */
2
+ export function checkDefaults(controls, where) {
3
+ for (const control of controls) {
4
+ const fault = faultOf(control);
5
+ if (fault) {
6
+ throw new Error(`workbench-client: in ${where}, the default of "${control.id}" ${fault}.`);
7
+ }
8
+ }
9
+ }
10
+ function faultOf(control) {
11
+ switch (control.type) {
12
+ case 'variant':
13
+ return control.options.some((option) => option.value === control.default)
14
+ ? null
15
+ : `("${control.default}") isn't one of its options`;
16
+ case 'color':
17
+ return control.options.some((option) => option.token === control.default)
18
+ ? null
19
+ : `("${control.default}") isn't one of its tokens`;
20
+ case 'number':
21
+ return control.default >= control.min && control.default <= control.max
22
+ ? null
23
+ : `(${control.default}) is outside ${control.min} to ${control.max}`;
24
+ case 'boolean':
25
+ return typeof control.default === 'boolean' ? null : "isn't true or false";
26
+ case 'text':
27
+ return typeof control.default === 'string' ? null : "isn't text";
28
+ }
29
+ }
@@ -0,0 +1,24 @@
1
+ import type { Control, Declared } from '../controls/types.js';
2
+ import type { Navigation, Screen, State } from '../protocol/types.js';
3
+ import { type Part } from './part.js';
4
+ /**
5
+ * Declares the app: its name, its screens and how to move between them,
6
+ * every other part, and the global controls, which show on every page. An
7
+ * app has exactly one, beside its root component.
8
+ *
9
+ * `parts` is every module that holds a part, as `import.meta.glob` gives
10
+ * them, so each loads as the app starts and the manifest and the first
11
+ * `values` hold every control. As it starts, the app throws on two controls
12
+ * with one id, two blocks with one name, a screen id that isn't in `screens`,
13
+ * a bad default, or a second `defineApp`.
14
+ */
15
+ export declare function defineApp<S extends State, const C extends readonly Control[]>(declaration: {
16
+ /** The app's name in the manifest, such as `aitems`. */
17
+ app: string;
18
+ /** Every screen and its states, each state with the jump that gets there. */
19
+ screens: readonly Screen<S>[];
20
+ /** A hook: how to go to a state, and where the app is now. */
21
+ useNavigation: () => Navigation<S>;
22
+ parts: Readonly<Record<string, unknown>>;
23
+ controls: C & NoInfer<Declared<C>>;
24
+ }): Part<C>;
@@ -0,0 +1,92 @@
1
+ import { declare } from '../controls/store.js';
2
+ import { buildManifest } from './buildManifest.js';
3
+ import { checkDefaults } from './checkDefaults.js';
4
+ import { isPart, makePart, nameOf, PART } from './part.js';
5
+ import { registerApp, registeredApp } from './registeredApp.js';
6
+ /**
7
+ * Declares the app: its name, its screens and how to move between them,
8
+ * every other part, and the global controls, which show on every page. An
9
+ * app has exactly one, beside its root component.
10
+ *
11
+ * `parts` is every module that holds a part, as `import.meta.glob` gives
12
+ * them, so each loads as the app starts and the manifest and the first
13
+ * `values` hold every control. As it starts, the app throws on two controls
14
+ * with one id, two blocks with one name, a screen id that isn't in `screens`,
15
+ * a bad default, or a second `defineApp`.
16
+ */
17
+ export function defineApp(declaration) {
18
+ if (registeredApp()) {
19
+ throw new Error('workbench-client: defineApp was called a second time; an app has exactly one.');
20
+ }
21
+ checkDefaults(declaration.controls, 'the app');
22
+ const global = makePart({ level: 'global' }, declaration.controls);
23
+ const parts = [global, ...partsIn(declaration.parts)];
24
+ checkParts(parts, new Set(declaration.screens.map((screen) => screen.id)));
25
+ for (const part of parts) {
26
+ for (const handle of part[PART].handles) {
27
+ declare(handle);
28
+ }
29
+ }
30
+ const states = new Map();
31
+ for (const screen of declaration.screens) {
32
+ for (const state of screen.states) {
33
+ states.set(state.id, state);
34
+ }
35
+ }
36
+ registerApp({
37
+ manifest: buildManifest(declaration.app, declaration.screens, parts),
38
+ useNavigation: () => {
39
+ const navigation = declaration.useNavigation();
40
+ return {
41
+ at: navigation.at,
42
+ go: (id) => {
43
+ const state = states.get(id);
44
+ if (state) {
45
+ navigation.go(state);
46
+ }
47
+ return state !== undefined;
48
+ },
49
+ };
50
+ },
51
+ });
52
+ return global;
53
+ }
54
+ /** Every part among the modules' exports, each once. */
55
+ function partsIn(modules) {
56
+ const found = new Set();
57
+ for (const module of Object.values(modules)) {
58
+ if (typeof module !== 'object' || module === null) {
59
+ continue;
60
+ }
61
+ for (const value of Object.values(module)) {
62
+ if (isPart(value)) {
63
+ found.add(value);
64
+ }
65
+ }
66
+ }
67
+ return [...found];
68
+ }
69
+ function checkParts(parts, screens) {
70
+ const ids = new Map();
71
+ const blocks = new Set();
72
+ for (const part of parts) {
73
+ const info = part[PART];
74
+ const where = nameOf(info);
75
+ if (info.place.level === 'screen' && !screens.has(info.place.screen)) {
76
+ throw new Error(`workbench-client: ${where} isn't one of the app's screens.`);
77
+ }
78
+ if (info.place.level === 'block') {
79
+ if (blocks.has(info.place.block)) {
80
+ throw new Error(`workbench-client: two blocks are named "${info.place.block}".`);
81
+ }
82
+ blocks.add(info.place.block);
83
+ }
84
+ for (const control of info.controls) {
85
+ const first = ids.get(control.id);
86
+ if (first !== undefined) {
87
+ throw new Error(`workbench-client: two controls have the id "${control.id}", in ${first} and in ${where}.`);
88
+ }
89
+ ids.set(control.id, where);
90
+ }
91
+ }
92
+ }