@tinkerlandai/indiana-iam 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -3
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +19 -5
- package/package.json +5 -13
- package/src/analyze.mjs +799 -109
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/framework-routes.mjs +3 -1
- package/src/generate.mjs +3 -1
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -0
- package/src/library-entry.mjs +4 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/node-hashes.mjs +9 -8
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-media.mjs +21 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/validate.mjs +5 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { JSDOM, VirtualConsole } from "jsdom";
|
|
2
|
+
import { documentDom } from "./dom-payload.mjs";
|
|
3
|
+
import { createStateMachine } from "./ui-state-machine.mjs";
|
|
4
|
+
import { previewUiDocument } from "./ui-preview.mjs";
|
|
5
|
+
import {graphSource} from './graph-sources.mjs';
|
|
6
|
+
|
|
7
|
+
/** Build thumbnail states and edges by dispatching the IAM's actual scalar transitions. */
|
|
8
|
+
export function tabUiScenes(manifest, tabKey) {
|
|
9
|
+
const graph = manifest.routeTree,
|
|
10
|
+
tab = graph.ui?.tabs.find((t) => t.tabKey === tabKey);
|
|
11
|
+
if (!tab || !tab.variableKey) throw Error("Unknown controlled source Tab");
|
|
12
|
+
const render = graph.renders.find((r) => r.renderKey === tab.renderKey),
|
|
13
|
+
document = manifest.documents.find(
|
|
14
|
+
(d) => d.documentKey === render.documentKey,
|
|
15
|
+
);
|
|
16
|
+
const dom = new JSDOM(documentDom(document).html, {
|
|
17
|
+
virtualConsole: new VirtualConsole(),
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const doc = dom.window.document,
|
|
21
|
+
nodes = new Map(
|
|
22
|
+
[...doc.querySelectorAll("[data-iam-node]")].map((n) => [
|
|
23
|
+
n.getAttribute("data-iam-node"),
|
|
24
|
+
n,
|
|
25
|
+
]),
|
|
26
|
+
);
|
|
27
|
+
const ancestors = [];
|
|
28
|
+
let current = nodes.get(tab.nodeId);
|
|
29
|
+
while (current && ancestors.length < 128) {
|
|
30
|
+
ancestors.push(current);
|
|
31
|
+
current = current.parentElement;
|
|
32
|
+
}
|
|
33
|
+
const conditions = {},
|
|
34
|
+
regions = {},
|
|
35
|
+
values = {},
|
|
36
|
+
ids = new Set(ancestors.map((n) => n.getAttribute("data-iam-node")));
|
|
37
|
+
for (const node of ancestors) {
|
|
38
|
+
if (node.hasAttribute("data-iam-condition")) {
|
|
39
|
+
const truthy = node.getAttribute("data-iam-alternative") === "true",
|
|
40
|
+
root = truthy ? node : node.previousElementSibling;
|
|
41
|
+
if (
|
|
42
|
+
!root ||
|
|
43
|
+
root.getAttribute("data-iam-condition") !==
|
|
44
|
+
node.getAttribute("data-iam-condition")
|
|
45
|
+
)
|
|
46
|
+
throw Error("Invalid source condition pair");
|
|
47
|
+
conditions[root.getAttribute("data-iam-node")] = truthy;
|
|
48
|
+
}
|
|
49
|
+
if (node.hasAttribute("data-iam-region-value")) {
|
|
50
|
+
const region = (graph.ui.regions ?? []).find((r) =>
|
|
51
|
+
r.options.some(
|
|
52
|
+
(o) => o.nodeId === node.getAttribute("data-iam-node"),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
if (region)
|
|
56
|
+
regions[region.regionKey] = node.getAttribute(
|
|
57
|
+
"data-iam-region-value",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const binding of graph.bindings.filter(
|
|
62
|
+
(b) =>
|
|
63
|
+
b.renderKey === tab.renderKey &&
|
|
64
|
+
b.property === "visible" &&
|
|
65
|
+
ids.has(b.nodeId),
|
|
66
|
+
)) {
|
|
67
|
+
if (binding.value.op === "var") values[binding.value.key] = true;
|
|
68
|
+
if (binding.value.op === "not" && binding.value.arg.op === "var")
|
|
69
|
+
values[binding.value.arg.key] = false;
|
|
70
|
+
if (
|
|
71
|
+
binding.value.op === "eq" &&
|
|
72
|
+
binding.value.left.op === "var" &&
|
|
73
|
+
binding.value.right.op === "const"
|
|
74
|
+
)
|
|
75
|
+
values[binding.value.left.key] = binding.value.right.value;
|
|
76
|
+
}
|
|
77
|
+
const scope = nodes.get(tab.scopeNodeId ?? tab.nodeId),
|
|
78
|
+
overlay = scope?.getAttribute("role") === "dialog",
|
|
79
|
+
width = Number(
|
|
80
|
+
/\bw-\[(\d+)px\]/.exec(scope?.getAttribute("class") ?? "")?.[1] || 0,
|
|
81
|
+
);
|
|
82
|
+
const viewport = overlay
|
|
83
|
+
? {
|
|
84
|
+
width: width ? Math.max(320, width + 48) : 640,
|
|
85
|
+
height: width ? 480 : 400,
|
|
86
|
+
}
|
|
87
|
+
: { width: 480, height: 760 };
|
|
88
|
+
const base = {
|
|
89
|
+
strategy: "content",
|
|
90
|
+
conditions,
|
|
91
|
+
regions,
|
|
92
|
+
values,
|
|
93
|
+
focusNodeId: tab.scopeNodeId ?? tab.nodeId,
|
|
94
|
+
viewport,
|
|
95
|
+
};
|
|
96
|
+
const initial = createStateMachine(
|
|
97
|
+
graph,
|
|
98
|
+
tab.routeKey,
|
|
99
|
+
tab.renderKey,
|
|
100
|
+
).snapshot(),
|
|
101
|
+
scenes = [];
|
|
102
|
+
for (const item of tab.items) {
|
|
103
|
+
const machine = createStateMachine(graph, tab.routeKey, tab.renderKey);
|
|
104
|
+
machine.preview(values);
|
|
105
|
+
const transition = graph.transitions.find(
|
|
106
|
+
(t) =>
|
|
107
|
+
t.renderKey === tab.renderKey &&
|
|
108
|
+
t.trigger.nodeId === item.nodeId &&
|
|
109
|
+
t.trigger.event === "click",
|
|
110
|
+
);
|
|
111
|
+
const result = transition
|
|
112
|
+
? machine.dispatch(item.nodeId, "click", {})
|
|
113
|
+
: null;
|
|
114
|
+
if (!result || machine.snapshot()[tab.variableKey] !== item.value)
|
|
115
|
+
continue;
|
|
116
|
+
const changed = Object.fromEntries(
|
|
117
|
+
Object.entries(machine.snapshot()).filter(
|
|
118
|
+
([key, value]) => initial[key] !== value || key === tab.variableKey,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
const options = { ...base, values: { ...values, ...changed } },
|
|
122
|
+
preview = previewUiDocument(manifest, document, options),
|
|
123
|
+
sceneDom = new JSDOM(preview.html, {
|
|
124
|
+
virtualConsole: new VirtualConsole(),
|
|
125
|
+
});
|
|
126
|
+
const focused = [
|
|
127
|
+
...sceneDom.window.document.querySelectorAll("[data-iam-node]"),
|
|
128
|
+
]
|
|
129
|
+
.find((n) => n.getAttribute("data-iam-node") === base.focusNodeId)
|
|
130
|
+
?.cloneNode(true);
|
|
131
|
+
focused
|
|
132
|
+
?.querySelectorAll("[hidden],[role=tablist]")
|
|
133
|
+
.forEach((n) => n.remove());
|
|
134
|
+
const headings = focused
|
|
135
|
+
? [...focused.querySelectorAll("h1,h2,h3,h4,label,[role=columnheader]")]
|
|
136
|
+
.map((n) => n.textContent.replace(/\s+/g, " ").trim())
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
: [];
|
|
139
|
+
const content = focused?.textContent.replace(/\s+/g, " ").trim() ?? "";
|
|
140
|
+
const fields = focused
|
|
141
|
+
? [...focused.querySelectorAll("input,textarea,select")].map((n) => ({
|
|
142
|
+
tag: n.tagName.toLowerCase(),
|
|
143
|
+
label: n.getAttribute("aria-label") ?? "",
|
|
144
|
+
placeholder: n.getAttribute("placeholder") ?? "",
|
|
145
|
+
}))
|
|
146
|
+
: [];
|
|
147
|
+
const allowedIds = new Set([
|
|
148
|
+
...ancestors.map((n) => n.getAttribute("data-iam-node")),
|
|
149
|
+
...(scope
|
|
150
|
+
? [scope, ...scope.querySelectorAll("[data-iam-node]")].map((n) =>
|
|
151
|
+
n.getAttribute("data-iam-node"),
|
|
152
|
+
)
|
|
153
|
+
: []),
|
|
154
|
+
]);
|
|
155
|
+
scenes.push({
|
|
156
|
+
assumptions: preview.assumptions.filter((a) =>
|
|
157
|
+
allowedIds.has(a.groupKey),
|
|
158
|
+
),
|
|
159
|
+
value: item.value,
|
|
160
|
+
label:
|
|
161
|
+
nodes.get(item.nodeId)?.textContent.replace(/\s+/g, " ").trim() ||
|
|
162
|
+
item.value,
|
|
163
|
+
options,
|
|
164
|
+
headings: [...new Set(headings)].slice(0, 8),
|
|
165
|
+
content: content.slice(0, 1000),
|
|
166
|
+
fields,
|
|
167
|
+
collections: preview.collectionChoices.filter((c) => {
|
|
168
|
+
const row = (graph.ui.collections ?? []).find(
|
|
169
|
+
(r) => r.collectionKey === c.collectionKey,
|
|
170
|
+
),
|
|
171
|
+
node = row && nodes.get(row.nodeId);
|
|
172
|
+
return node && (scope === node || scope?.contains(node));
|
|
173
|
+
}),
|
|
174
|
+
unobserved: preview.unobserved.filter((u) =>
|
|
175
|
+
allowedIds.has(u.groupKey),
|
|
176
|
+
),
|
|
177
|
+
transitionKey: result.transitionKey,
|
|
178
|
+
changedNodeIds: tab.contentNodeIds ?? [],
|
|
179
|
+
});
|
|
180
|
+
sceneDom.window.close();
|
|
181
|
+
}
|
|
182
|
+
const edges = [];
|
|
183
|
+
for (const from of scenes)
|
|
184
|
+
for (const to of scenes) {
|
|
185
|
+
if (from.value === to.value) continue;
|
|
186
|
+
const item = tab.items.find((i) => i.value === to.value),
|
|
187
|
+
machine = createStateMachine(graph, tab.routeKey, tab.renderKey);
|
|
188
|
+
machine.preview(from.options.values);
|
|
189
|
+
const result = machine.dispatch(item.nodeId, "click", {});
|
|
190
|
+
if (!result || machine.snapshot()[tab.variableKey] !== to.value)
|
|
191
|
+
continue;
|
|
192
|
+
const transition = graph.transitions.find(
|
|
193
|
+
(t) => t.transitionKey === result.transitionKey,
|
|
194
|
+
);
|
|
195
|
+
edges.push({
|
|
196
|
+
from: from.value,
|
|
197
|
+
to: to.value,
|
|
198
|
+
transitionKey: result.transitionKey,
|
|
199
|
+
event: transition.trigger.event,
|
|
200
|
+
variableKey: tab.variableKey,
|
|
201
|
+
writes: transition.stateWrites.map((w) => ({
|
|
202
|
+
variableKey: w.variableKey,
|
|
203
|
+
label: graph.variables.find((v) => v.variableKey === w.variableKey)
|
|
204
|
+
.label,
|
|
205
|
+
value: machine.snapshot()[w.variableKey],
|
|
206
|
+
})),
|
|
207
|
+
sourceRefs: [graphSource(graph,transition), tab.scopeSource],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return { tabKey, scenes, edges };
|
|
211
|
+
} finally {
|
|
212
|
+
dom.window.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { JSDOM, VirtualConsole } from "jsdom";
|
|
2
|
+
import { documentDom } from "./dom-payload.mjs";
|
|
3
|
+
import { createStateMachine } from "./ui-state-machine.mjs";
|
|
4
|
+
import { applySourceAttributes } from "./source-attributes.mjs";
|
|
5
|
+
import {
|
|
6
|
+
applySourceCollections,
|
|
7
|
+
markUnobservedData,
|
|
8
|
+
} from "./source-collections.mjs";
|
|
9
|
+
|
|
10
|
+
/** Inert source alternatives for UI inspection. This never changes the admitted render or SDK targets. */
|
|
11
|
+
export function previewUiDocument(manifest, document, options = {}) {
|
|
12
|
+
const graph = manifest.routeTree,
|
|
13
|
+
render = graph.renders.find((r) => r.documentKey === document.documentKey);
|
|
14
|
+
if (!render) throw Error("Unknown UI preview render");
|
|
15
|
+
const dom = new JSDOM(documentDom(document).html, {
|
|
16
|
+
virtualConsole: new VirtualConsole(),
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const doc = dom.window.document;
|
|
20
|
+
const nodes = new Map(
|
|
21
|
+
[...doc.querySelectorAll("[data-iam-node]")].map((n) => [
|
|
22
|
+
n.getAttribute("data-iam-node"),
|
|
23
|
+
n,
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
const sourceConditions = new Map(
|
|
27
|
+
(graph.ui?.conditions ?? []).map((c) => [c.conditionKey, c]),
|
|
28
|
+
);
|
|
29
|
+
const conditions = [
|
|
30
|
+
...doc.querySelectorAll(
|
|
31
|
+
'[data-iam-condition][data-iam-alternative="true"]',
|
|
32
|
+
),
|
|
33
|
+
].map((node) => {
|
|
34
|
+
const source = sourceConditions.get(
|
|
35
|
+
node.getAttribute("data-iam-condition"),
|
|
36
|
+
);
|
|
37
|
+
if (!source) throw Error("Unknown source condition");
|
|
38
|
+
const other = node.nextElementSibling;
|
|
39
|
+
return {
|
|
40
|
+
...source,
|
|
41
|
+
defaultChoice:
|
|
42
|
+
node.getAttribute("data-iam-condition-default") === "false"
|
|
43
|
+
? false
|
|
44
|
+
: undefined,
|
|
45
|
+
groupKey: node.getAttribute("data-iam-node"),
|
|
46
|
+
truthyNodeId: node.getAttribute("data-iam-node"),
|
|
47
|
+
falsyNodeId:
|
|
48
|
+
other?.getAttribute("data-iam-condition") === source.conditionKey &&
|
|
49
|
+
other.getAttribute("data-iam-alternative") === "false"
|
|
50
|
+
? other.getAttribute("data-iam-node")
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const selections = new Map();
|
|
55
|
+
const groups = new Map();
|
|
56
|
+
for (const row of conditions) {
|
|
57
|
+
if (!groups.has(row.groupKey)) groups.set(row.groupKey, []);
|
|
58
|
+
groups.get(row.groupKey).push(row);
|
|
59
|
+
}
|
|
60
|
+
if (options.strategy !== undefined && options.strategy !== "content")
|
|
61
|
+
throw Error("Unknown UI preview strategy");
|
|
62
|
+
for (const [key, rows] of groups) {
|
|
63
|
+
const supplied = options.conditions?.[key];
|
|
64
|
+
if (supplied !== undefined && typeof supplied !== "boolean")
|
|
65
|
+
throw Error("Invalid UI condition choice");
|
|
66
|
+
if (supplied !== undefined) selections.set(key, supplied);
|
|
67
|
+
else if (options.strategy === "content") {
|
|
68
|
+
if (rows[0].observation) continue;
|
|
69
|
+
if (rows[0].defaultChoice !== undefined) {
|
|
70
|
+
selections.set(key, rows[0].defaultChoice);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const score = (id) => {
|
|
74
|
+
const node = nodes.get(id);
|
|
75
|
+
return node
|
|
76
|
+
? node.textContent.trim().length +
|
|
77
|
+
node.querySelectorAll("input,button,a,textarea,select").length *
|
|
78
|
+
24
|
|
79
|
+
: 0;
|
|
80
|
+
};
|
|
81
|
+
selections.set(
|
|
82
|
+
key,
|
|
83
|
+
rows.reduce(
|
|
84
|
+
(n, r) => n + score(r.truthyNodeId) - score(r.falsyNodeId),
|
|
85
|
+
0,
|
|
86
|
+
) >= 0,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const key of Object.keys(options.conditions ?? {}))
|
|
91
|
+
if (!groups.has(key)) throw Error("Unknown UI condition");
|
|
92
|
+
for (const row of conditions)
|
|
93
|
+
if (selections.has(row.groupKey)) {
|
|
94
|
+
nodes
|
|
95
|
+
.get(row.truthyNodeId)
|
|
96
|
+
?.toggleAttribute("hidden", !selections.get(row.groupKey));
|
|
97
|
+
if (row.falsyNodeId)
|
|
98
|
+
nodes
|
|
99
|
+
.get(row.falsyNodeId)
|
|
100
|
+
?.toggleAttribute("hidden", selections.get(row.groupKey));
|
|
101
|
+
}
|
|
102
|
+
const machine = createStateMachine(
|
|
103
|
+
graph,
|
|
104
|
+
render.routeKey,
|
|
105
|
+
render.renderKey,
|
|
106
|
+
);
|
|
107
|
+
if (options.values) machine.preview(options.values);
|
|
108
|
+
for (const binding of machine.bindings()) {
|
|
109
|
+
const node = nodes.get(binding.nodeId);
|
|
110
|
+
if (!node) continue;
|
|
111
|
+
if (binding.property === "visible")
|
|
112
|
+
node.toggleAttribute("hidden", !binding.value);
|
|
113
|
+
else if (binding.property === "text")
|
|
114
|
+
node.textContent = String(binding.value);
|
|
115
|
+
else if (binding.property === "className")
|
|
116
|
+
node.setAttribute("class", String(binding.value));
|
|
117
|
+
else if (["disabled", "checked"].includes(binding.property))
|
|
118
|
+
node.toggleAttribute(binding.property, Boolean(binding.value));
|
|
119
|
+
else if (
|
|
120
|
+
binding.property === "value" &&
|
|
121
|
+
node.getAttribute("type") !== "password"
|
|
122
|
+
)
|
|
123
|
+
node.setAttribute("value", String(binding.value));
|
|
124
|
+
}
|
|
125
|
+
const values = machine.snapshot();
|
|
126
|
+
for (const tab of graph.ui?.tabs ?? [])
|
|
127
|
+
if (tab.renderKey === render.renderKey) {
|
|
128
|
+
const selected = tab.variableKey
|
|
129
|
+
? values[tab.variableKey]
|
|
130
|
+
: tab.initialValue;
|
|
131
|
+
for (const item of tab.items)
|
|
132
|
+
nodes
|
|
133
|
+
.get(item.nodeId)
|
|
134
|
+
?.setAttribute("aria-selected", String(item.value === selected));
|
|
135
|
+
}
|
|
136
|
+
const regions = (graph.ui?.regions ?? []).filter(
|
|
137
|
+
(r) => r.renderKey === render.renderKey,
|
|
138
|
+
),
|
|
139
|
+
componentChoices = [];
|
|
140
|
+
for (const key of Object.keys(options.regions ?? {}))
|
|
141
|
+
if (!regions.some((r) => r.regionKey === key))
|
|
142
|
+
throw Error("Unknown component region");
|
|
143
|
+
for (const region of regions) {
|
|
144
|
+
const requested = options.regions?.[region.regionKey],
|
|
145
|
+
stateValue = region.variableKey ? values[region.variableKey] : null;
|
|
146
|
+
const selected =
|
|
147
|
+
requested ??
|
|
148
|
+
(options.strategy === "content"
|
|
149
|
+
? region.options.some((o) => o.value === stateValue)
|
|
150
|
+
? stateValue
|
|
151
|
+
: (region.initialValue ?? region.options[0].value)
|
|
152
|
+
: null);
|
|
153
|
+
if (
|
|
154
|
+
selected !== null &&
|
|
155
|
+
!region.options.some((o) => o.value === selected)
|
|
156
|
+
)
|
|
157
|
+
throw Error("Unknown source component alternative");
|
|
158
|
+
if (selected !== null) {
|
|
159
|
+
for (const option of region.options)
|
|
160
|
+
nodes
|
|
161
|
+
.get(option.nodeId)
|
|
162
|
+
?.toggleAttribute("hidden", option.value !== selected);
|
|
163
|
+
componentChoices.push({
|
|
164
|
+
regionKey: region.regionKey,
|
|
165
|
+
value: selected,
|
|
166
|
+
source: region.source,
|
|
167
|
+
expression: region.expression,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
applySourceAttributes(doc, graph.ui, render.renderKey, values);
|
|
172
|
+
const focus = options.focusNodeId ? nodes.get(options.focusNodeId) : null;
|
|
173
|
+
const collectionUi = focus
|
|
174
|
+
? {
|
|
175
|
+
...graph.ui,
|
|
176
|
+
collections: (graph.ui?.collections ?? []).filter((row) => {
|
|
177
|
+
const node = nodes.get(row.nodeId);
|
|
178
|
+
return node && focus.contains(node);
|
|
179
|
+
}),
|
|
180
|
+
}
|
|
181
|
+
: graph.ui;
|
|
182
|
+
const collectionChoices = applySourceCollections(
|
|
183
|
+
doc,
|
|
184
|
+
collectionUi,
|
|
185
|
+
render.renderKey,
|
|
186
|
+
values,
|
|
187
|
+
options.strategy === "content" || Boolean(options.values),
|
|
188
|
+
);
|
|
189
|
+
const unobserved = markUnobservedData(
|
|
190
|
+
doc,
|
|
191
|
+
graph.ui,
|
|
192
|
+
options.strategy === "content",
|
|
193
|
+
selections,
|
|
194
|
+
);
|
|
195
|
+
const assumptions = [...groups].flatMap(([key, rows]) => {
|
|
196
|
+
if (!selections.has(key)) return [];
|
|
197
|
+
const row = rows.find((r) => {
|
|
198
|
+
const node = nodes.get(
|
|
199
|
+
selections.get(key) ? r.truthyNodeId : r.falsyNodeId,
|
|
200
|
+
);
|
|
201
|
+
return node && !node.parentElement?.closest("[hidden]");
|
|
202
|
+
});
|
|
203
|
+
return row
|
|
204
|
+
? [
|
|
205
|
+
{
|
|
206
|
+
groupKey: key,
|
|
207
|
+
expression: row.expression,
|
|
208
|
+
selected: selections.get(key),
|
|
209
|
+
source: row.source,
|
|
210
|
+
},
|
|
211
|
+
]
|
|
212
|
+
: [];
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
html: doc.documentElement.outerHTML,
|
|
216
|
+
assumptions,
|
|
217
|
+
values,
|
|
218
|
+
componentChoices,
|
|
219
|
+
collectionChoices,
|
|
220
|
+
unobserved,
|
|
221
|
+
};
|
|
222
|
+
} finally {
|
|
223
|
+
dom.window.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Generated from assets/state-machine.js by build-runtime.mjs. */
|
|
2
|
+
/* Indiana declarative UI state machine. Pure computation; no merchant callbacks. */
|
|
3
|
+
export function createStateMachine(graph, routeKey, renderKey) {
|
|
4
|
+
{
|
|
5
|
+
renderKey ??= graph.routes.find(route => route.routeKey === routeKey)?.initialRenderKey;
|
|
6
|
+
if (!graph.renders.some(render => render.renderKey === renderKey && render.routeKey === routeKey)) throw new Error("Unknown UI render");
|
|
7
|
+
}
|
|
8
|
+
const owns = row => row.routeKey === routeKey && row.renderKey === renderKey;
|
|
9
|
+
const variables = new Map(graph.variables.filter(owns).map(v => [v.variableKey, v]));
|
|
10
|
+
const transitions = graph.transitions.filter(t => owns(t) && t.simulation === "implemented");
|
|
11
|
+
let values = Object.fromEntries([...variables].map(([key, v]) => [key, v.initialValue]));
|
|
12
|
+
const read = (expression, payload, snapshot, depth = 0) => {
|
|
13
|
+
if (depth > 24) throw new Error("Expression budget exceeded");
|
|
14
|
+
const next = e => read(e, payload, snapshot, depth + 1);
|
|
15
|
+
switch (expression.op) {
|
|
16
|
+
case "const": return expression.value;
|
|
17
|
+
case "var": {
|
|
18
|
+
if (!variables.has(expression.key)) throw new Error("Unknown variable");
|
|
19
|
+
return snapshot[expression.key];
|
|
20
|
+
}
|
|
21
|
+
case "event": {
|
|
22
|
+
if (!Object.hasOwn(payload, expression.key)) throw new Error("Missing event input");
|
|
23
|
+
return payload[expression.key];
|
|
24
|
+
}
|
|
25
|
+
case "not": return !next(expression.arg);
|
|
26
|
+
case "eq": return next(expression.left) === next(expression.right);
|
|
27
|
+
case "ne": return next(expression.left) !== next(expression.right);
|
|
28
|
+
case "add":
|
|
29
|
+
case "subtract": {
|
|
30
|
+
const left = next(expression.left), right = next(expression.right);
|
|
31
|
+
if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Invalid numeric expression");
|
|
32
|
+
const value = expression.op === "add" ? left + right : left - right;
|
|
33
|
+
if (!Number.isFinite(value)) throw new Error("Invalid numeric result");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
case "all": return expression.args.every(e => next(e) === true);
|
|
37
|
+
case "any": return expression.args.some(e => next(e) === true);
|
|
38
|
+
case "classes": return expression.parts.filter(part=>next(part.when)===true).map(part=>part.value).join(' ');
|
|
39
|
+
default: throw new Error("Unsupported expression");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
snapshot: () => ({ ...values }),
|
|
44
|
+
preview(input) {
|
|
45
|
+
if (!input || Array.isArray(input) || typeof input !== "object" || Object.keys(input).length > variables.size) throw new Error("Invalid preview values");
|
|
46
|
+
const writes = Object.create(null);
|
|
47
|
+
for (const [key, value] of Object.entries(input)) {
|
|
48
|
+
const variable = variables.get(key);
|
|
49
|
+
if (!variable || typeof value !== variable.valueType || typeof value === "string" && value.length > 4000 || typeof value === "number" && !Number.isFinite(value)) throw new Error("Invalid preview value");
|
|
50
|
+
writes[key] = value;
|
|
51
|
+
}
|
|
52
|
+
values = { ...values, ...writes };
|
|
53
|
+
return { values: { ...values }, evidence: "manual-ui-preview" };
|
|
54
|
+
},
|
|
55
|
+
reset: () => { values = Object.fromEntries([...variables].map(([key, v]) => [key, v.initialValue])); },
|
|
56
|
+
bindings: () => graph.bindings.filter(owns).map(b => ({ ...b, value: read(b.value, {}, values) })),
|
|
57
|
+
dispatch(nodeId, event, payload = {}) {
|
|
58
|
+
const candidates = transitions.filter(t => t.trigger.nodeId === nodeId && t.trigger.event === event);
|
|
59
|
+
for (const t of candidates) {
|
|
60
|
+
const expected = Object.keys(t.trigger.payload);
|
|
61
|
+
if (Object.keys(payload).some(key => !expected.includes(key)) || expected.some(key => typeof payload[key] !== t.trigger.payload[key] || typeof payload[key] === "string" && payload[key].length > 4000)) throw new Error("Invalid event input");
|
|
62
|
+
}
|
|
63
|
+
const eligible = candidates.filter(t => read(t.guard, payload, values) === true);
|
|
64
|
+
if (eligible.length > 1) throw new Error("Ambiguous UI transition");
|
|
65
|
+
if (!eligible.length) return null;
|
|
66
|
+
const t = eligible[0], writes = Object.create(null);
|
|
67
|
+
for (const w of t.stateWrites) {
|
|
68
|
+
const value = read(w.value, payload, values), variable = variables.get(w.variableKey);
|
|
69
|
+
if (!variable || typeof value !== variable.valueType || typeof value === "string" && value.length > 4000 || typeof value === "number" && !Number.isFinite(value)) throw new Error("Invalid state write");
|
|
70
|
+
writes[w.variableKey] = value;
|
|
71
|
+
}
|
|
72
|
+
values = { ...values, ...writes };
|
|
73
|
+
return { transitionKey: t.transitionKey, values: { ...values } };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|