claude-artifact-framework 0.13.1 → 0.14.1
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 +148 -13
- package/dist/index.esm.js +151 -3
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +13 -13
- package/dist/index.global.js.map +3 -3
- package/llms.txt +90 -0
- package/package.json +9 -4
- package/src/app.js +175 -2
package/llms.txt
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# claude-artifact-framework
|
|
2
|
+
|
|
3
|
+
> Declarative framework for building apps inside Claude Artifacts. Declare what
|
|
4
|
+
> the app IS (fields, records, steps, chat with tools, workspace, documents);
|
|
5
|
+
> the framework owns persistence, hydration, validation, empty/loading/error
|
|
6
|
+
> states, light+dark theming, navigation, and the Claude tool loop.
|
|
7
|
+
> Full documentation: README.md (this file is the compact digest).
|
|
8
|
+
|
|
9
|
+
Load (classic script, global `ArtifactKit`; call `ArtifactKit.useReact(React)` once):
|
|
10
|
+
https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
|
|
11
|
+
|
|
12
|
+
## Registries (any other name throws, naming the valid set)
|
|
13
|
+
|
|
14
|
+
- Top-level keys: title, subtitle, theme:{seed}, layout, blocks, records,
|
|
15
|
+
steps, chat, main, sidebar, documents, data, shared, libs, machine
|
|
16
|
+
- Layouts: panel (default), records, steps, chat, workspace, documents
|
|
17
|
+
- Block types: fields, output, text, list, chart, timer, actions, chat, tabs,
|
|
18
|
+
records, custom — plus per-block title, when:(d)=>bool, state:"fase",
|
|
19
|
+
collapsible, wide, fill (custom only)
|
|
20
|
+
- Field types: text, textarea, number, money, percent, check, date, select,
|
|
21
|
+
file — attrs: key, label, value, required, min, max, step, placeholder,
|
|
22
|
+
hint, rows, options (array or (d)=>array), accept
|
|
23
|
+
- records spec: label, fields, title, subtitle, sort, summary, empty, search,
|
|
24
|
+
compute, items, actions (+ key when used as a block)
|
|
25
|
+
- documents: label, title, blocks OR types, empty; machine: key, initial,
|
|
26
|
+
states, events (event: from, do, then); chat: system, greeting, placeholder,
|
|
27
|
+
tools, model, maxTokens; steps step: title, text, fields, result
|
|
28
|
+
- Formats: money, percent (0-100), number, date. Output/chart/compute items:
|
|
29
|
+
{ label, value, format, big }
|
|
30
|
+
|
|
31
|
+
## Hard rules
|
|
32
|
+
|
|
33
|
+
1. Write the spec INDENTED, never minified (measured: minified output drops
|
|
34
|
+
closing braces).
|
|
35
|
+
2. Theme variables, never hardcoded hex: var(--caf-text), var(--caf-surface),
|
|
36
|
+
var(--caf-accent), var(--caf-border), var(--caf-sunken), var(--caf-muted),
|
|
37
|
+
var(--caf-danger); ctx.colors(n) for series. Hardcoded #fff breaks dark mode.
|
|
38
|
+
3. Never render a <form> (sandbox lacks allow-forms) — inputs + button onClick.
|
|
39
|
+
4. Never write big strings (data URLs, file contents) into data — one ~5MB
|
|
40
|
+
storage key holds the whole pool; big things live in React.useState or
|
|
41
|
+
ctx.files.
|
|
42
|
+
5. Phases (games, turns, processes) belong to `machine` — ctx.send(event,
|
|
43
|
+
payload) is the only door; events are no-ops outside their `from` states;
|
|
44
|
+
`after: {seconds, then, do}` timers are framework-owned.
|
|
45
|
+
6. Row actions receive (record, data, viewerId); block actions (data,
|
|
46
|
+
viewerId). Stop early if you like, but never skip a middle argument.
|
|
47
|
+
7. One big:true per screen; the constructive action takes kind:"primary"; a
|
|
48
|
+
counter changed by pressing is an action, not an editable number field.
|
|
49
|
+
8. Parts of a record are `items` — never JSON in a textarea.
|
|
50
|
+
9. Don't hand-build empty/loading/error states — the framework owns them.
|
|
51
|
+
10. CDN libs via libs:[urls]: call the method, not the global (marked.parse);
|
|
52
|
+
pin versions; unsure of the file path → bare package URL
|
|
53
|
+
(https://cdn.jsdelivr.net/npm/<pkg>@<version>).
|
|
54
|
+
11. Framework record ids are strings — never Number(record.id).
|
|
55
|
+
12. In custom blocks: return strings/React elements (never DOM elements — use
|
|
56
|
+
ref + React.useEffect for libs that write into an element); hooks are
|
|
57
|
+
allowed; persistent state via ctx.update, ephemeral via React.useState,
|
|
58
|
+
never createStore for UI state.
|
|
59
|
+
|
|
60
|
+
## Canonical example
|
|
61
|
+
|
|
62
|
+
ArtifactKit.createApp({
|
|
63
|
+
title: "Gastos",
|
|
64
|
+
theme: { seed: "#c2410c" },
|
|
65
|
+
layout: "records",
|
|
66
|
+
shared: false,
|
|
67
|
+
records: {
|
|
68
|
+
label: "gasto",
|
|
69
|
+
fields: [
|
|
70
|
+
{ key: "concepto", label: "Concepto", type: "text", required: true },
|
|
71
|
+
{ key: "monto", label: "Monto", type: "money", value: 0, min: 0 },
|
|
72
|
+
{ key: "categoria", label: "Categoría", type: "select", options: ["comida", "transporte", "otros"] },
|
|
73
|
+
],
|
|
74
|
+
subtitle: (r) => r.categoria,
|
|
75
|
+
summary: (all) => [
|
|
76
|
+
{ label: "Total", value: all.reduce((s, r) => s + (Number(r.monto) || 0), 0), format: "money", big: true },
|
|
77
|
+
],
|
|
78
|
+
actions: [{ label: "Duplicar", run: (r, d) => { d.records = [...d.records, { ...r, id: undefined }]; } }],
|
|
79
|
+
},
|
|
80
|
+
blocks: [
|
|
81
|
+
{ chart: (d) => ({ type: "donut", format: "money", items: porCategoria(d.records) }), title: "Por categoría" },
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
Chat with tools (workspace copilot): sidebar block
|
|
86
|
+
{ title: "Asistente", chat: { system: (d) => "...", tools: { nombre: {
|
|
87
|
+
description: "...", input: { campo: "string" }, run: (input, ctx) => {
|
|
88
|
+
ctx.update((d) => { /* mutate */ }); return { ok: true }; } } } } }
|
|
89
|
+
— the artifact runtime proxies api.anthropic.com with no API key; default
|
|
90
|
+
model claude-sonnet-4-6; conversation persists under data.chat.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Utilities for building apps inside Claude Artifacts: platform storage, chat tool-calling loops, and other primitives verified against the real runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
|
@@ -12,16 +12,21 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
-
"src"
|
|
15
|
+
"src",
|
|
16
|
+
"llms.txt"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"build": "node scripts/build.mjs",
|
|
19
20
|
"build:watch": "node scripts/build.mjs --watch",
|
|
21
|
+
"test": "npm run build && node test/e2e.mjs",
|
|
20
22
|
"prepublishOnly": "npm run build",
|
|
21
|
-
"release": "node scripts/check-docs.mjs && bash scripts/publish.sh"
|
|
23
|
+
"release": "node scripts/check-docs.mjs && npm test && bash scripts/publish.sh"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
|
-
"esbuild": "^0.25.0"
|
|
26
|
+
"esbuild": "^0.25.0",
|
|
27
|
+
"playwright-core": "^1.45.0",
|
|
28
|
+
"react": "^18.3.0",
|
|
29
|
+
"react-dom": "^18.3.0"
|
|
25
30
|
},
|
|
26
31
|
"license": "MIT",
|
|
27
32
|
"author": "Alejandro Cuartas",
|
package/src/app.js
CHANGED
|
@@ -18,12 +18,17 @@ import { StepsLayout } from "./steps.js";
|
|
|
18
18
|
import { ChatLayout, CHAT_KEYS } from "./chat.js";
|
|
19
19
|
|
|
20
20
|
const LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
|
|
21
|
-
const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
|
|
21
|
+
const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill", "state"];
|
|
22
22
|
|
|
23
23
|
// Frontier models still invent identifiers a few percent of the time, so an
|
|
24
24
|
// unknown name has to fail loudly and name the alternatives rather than
|
|
25
25
|
// silently rendering nothing — a blank pane is indistinguishable from a bug.
|
|
26
|
-
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents"];
|
|
26
|
+
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine"];
|
|
27
|
+
|
|
28
|
+
const MACHINE_KEYS = ["key", "initial", "states", "events"];
|
|
29
|
+
const MACHINE_STATE_KEYS = ["after"];
|
|
30
|
+
const MACHINE_AFTER_KEYS = ["seconds", "then", "do"];
|
|
31
|
+
const MACHINE_EVENT_KEYS = ["from", "do", "then"];
|
|
27
32
|
|
|
28
33
|
const DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
|
|
29
34
|
const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
|
|
@@ -140,6 +145,87 @@ function checkBlocks(blocks) {
|
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
// The measured failure class behind this: hand-rolled phase logic (a memory
|
|
149
|
+
// game fired its comparison logic in the wrong phase and died). The machine
|
|
150
|
+
// makes illegal transitions inexpressible: ctx.send() is the only door, an
|
|
151
|
+
// event is a no-op outside its `from` states, and `after` timers are
|
|
152
|
+
// framework-owned.
|
|
153
|
+
function checkMachine(m) {
|
|
154
|
+
const unknown = Object.keys(m).filter((k) => !MACHINE_KEYS.includes(k));
|
|
155
|
+
if (unknown.length) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`claude-artifact-framework: machine has unknown keys (${unknown.join(", ")}). Valid keys: ${MACHINE_KEYS.join(", ")}.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (m.key !== undefined && (typeof m.key !== "string" || !m.key)) {
|
|
161
|
+
throw new Error("claude-artifact-framework: machine.key must be a non-empty string (where the current state lives in data).");
|
|
162
|
+
}
|
|
163
|
+
const stateNames = Object.keys(m.states || {});
|
|
164
|
+
if (!stateNames.length) {
|
|
165
|
+
throw new Error("claude-artifact-framework: machine needs `states: { nombre: {...} }` with at least one state.");
|
|
166
|
+
}
|
|
167
|
+
const validState = (name, where) => {
|
|
168
|
+
if (!stateNames.includes(name)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`claude-artifact-framework: ${where} names unknown state "${name}". Valid states: ${stateNames.join(", ")}.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
if (typeof m.initial !== "string") {
|
|
175
|
+
throw new Error("claude-artifact-framework: machine needs `initial: \"...\"` naming the starting state.");
|
|
176
|
+
}
|
|
177
|
+
validState(m.initial, "machine.initial");
|
|
178
|
+
for (const [name, st] of Object.entries(m.states)) {
|
|
179
|
+
const unknownS = Object.keys(st || {}).filter((k) => !MACHINE_STATE_KEYS.includes(k));
|
|
180
|
+
if (unknownS.length) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`claude-artifact-framework: machine.states.${name} has unknown keys (${unknownS.join(", ")}). Valid keys: ${MACHINE_STATE_KEYS.join(", ")}.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (st && st.after !== undefined) {
|
|
186
|
+
const a = st.after;
|
|
187
|
+
const unknownA = Object.keys(a || {}).filter((k) => !MACHINE_AFTER_KEYS.includes(k));
|
|
188
|
+
if (unknownA.length) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`claude-artifact-framework: machine.states.${name}.after has unknown keys (${unknownA.join(", ")}). Valid keys: ${MACHINE_AFTER_KEYS.join(", ")}.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (typeof a.seconds !== "number" || a.seconds <= 0) {
|
|
194
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after needs \`seconds\` > 0.`);
|
|
195
|
+
}
|
|
196
|
+
if (typeof a.then !== "string") {
|
|
197
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after needs \`then: "estado"\`.`);
|
|
198
|
+
}
|
|
199
|
+
validState(a.then, `machine.states.${name}.after.then`);
|
|
200
|
+
if (a.do !== undefined && typeof a.do !== "function") {
|
|
201
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after.do must be a function of data.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const eventNames = Object.keys(m.events || {});
|
|
206
|
+
if (!eventNames.length) {
|
|
207
|
+
throw new Error("claude-artifact-framework: machine needs `events: { nombre: {...} }` — ctx.send() is the only way to move it.");
|
|
208
|
+
}
|
|
209
|
+
for (const [name, ev] of Object.entries(m.events)) {
|
|
210
|
+
const unknownE = Object.keys(ev || {}).filter((k) => !MACHINE_EVENT_KEYS.includes(k));
|
|
211
|
+
if (unknownE.length) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`claude-artifact-framework: machine.events.${name} has unknown keys (${unknownE.join(", ")}). Valid keys: ${MACHINE_EVENT_KEYS.join(", ")}.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (ev.from !== undefined && ev.from !== "*") {
|
|
217
|
+
for (const f of [].concat(ev.from)) validState(f, `machine.events.${name}.from`);
|
|
218
|
+
}
|
|
219
|
+
if (typeof ev.then === "string") validState(ev.then, `machine.events.${name}.then`);
|
|
220
|
+
else if (ev.then !== undefined && typeof ev.then !== "function") {
|
|
221
|
+
throw new Error(`claude-artifact-framework: machine.events.${name}.then must be a state name or a function of data returning one.`);
|
|
222
|
+
}
|
|
223
|
+
if (ev.do !== undefined && typeof ev.do !== "function") {
|
|
224
|
+
throw new Error(`claude-artifact-framework: machine.events.${name}.do must be a function (data, payload).`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
143
229
|
function validate(spec) {
|
|
144
230
|
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
145
231
|
if (unknownTop.length) {
|
|
@@ -147,6 +233,7 @@ function validate(spec) {
|
|
|
147
233
|
`claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
|
|
148
234
|
);
|
|
149
235
|
}
|
|
236
|
+
if (spec.machine !== undefined) checkMachine(spec.machine);
|
|
150
237
|
if (spec.libs !== undefined) {
|
|
151
238
|
if (!Array.isArray(spec.libs) || spec.libs.some((u) => typeof u !== "string" || !/^https?:\/\//.test(u))) {
|
|
152
239
|
throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
|
|
@@ -399,6 +486,10 @@ function defaultsFrom(spec) {
|
|
|
399
486
|
if ((spec.layout || "panel") === "documents" && !Array.isArray(data.docs)) {
|
|
400
487
|
data.docs = [];
|
|
401
488
|
}
|
|
489
|
+
if (spec.machine) {
|
|
490
|
+
const k = spec.machine.key || "fase";
|
|
491
|
+
if (!(k in data)) data[k] = spec.machine.initial;
|
|
492
|
+
}
|
|
402
493
|
const allBlocks = flattenBlocks([...(spec.blocks || []), ...(spec.main || []), ...(spec.sidebar || [])]);
|
|
403
494
|
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
404
495
|
if (hasChat && !data.chat) {
|
|
@@ -492,6 +583,8 @@ const ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
|
|
|
492
583
|
function Block({ block, ctx }) {
|
|
493
584
|
// `when` hides a block entirely — the primitive under conditional UIs.
|
|
494
585
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
586
|
+
// `state: "x"` — machine sugar: the block exists only in that phase.
|
|
587
|
+
if (block.state !== undefined && ctx.machine && ctx.data[ctx.machine.key || "fase"] !== block.state) return null;
|
|
495
588
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
496
589
|
const Component = ALL_BLOCKS[name];
|
|
497
590
|
const inner = h(
|
|
@@ -829,8 +922,61 @@ function loadScript(src) {
|
|
|
829
922
|
});
|
|
830
923
|
}
|
|
831
924
|
|
|
925
|
+
// Every block anywhere in the spec, including inside documents types — for
|
|
926
|
+
// cross-cutting checks like `state:` against the machine's states.
|
|
927
|
+
function everyBlock(spec) {
|
|
928
|
+
const docs = spec.documents
|
|
929
|
+
? spec.documents.blocks || Object.values(spec.documents.types || {}).flatMap((t) => t.blocks || [])
|
|
930
|
+
: [];
|
|
931
|
+
return flattenBlocks([...(spec.blocks || []), ...(spec.main || []), ...(spec.sidebar || []), ...docs]);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ctx.send(event, payload) — the machine's only door. Unknown event throws
|
|
935
|
+
// (naming the valid ones); a legal event fired from the wrong phase is a
|
|
936
|
+
// silent no-op (a user mashing buttons is normal, not a bug).
|
|
937
|
+
function runMachineEvent(machine, update, eventName, payload) {
|
|
938
|
+
const ev = machine.events[eventName];
|
|
939
|
+
if (!ev) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`claude-artifact-framework: unknown machine event "${eventName}". Valid events: ${Object.keys(machine.events).join(", ")}.`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const key = machine.key || "fase";
|
|
945
|
+
update((d) => {
|
|
946
|
+
const current = d[key];
|
|
947
|
+
const from = ev.from === undefined ? "*" : ev.from;
|
|
948
|
+
if (from !== "*" && ![].concat(from).includes(current)) return;
|
|
949
|
+
if (ev.do) ev.do(d, payload);
|
|
950
|
+
const next = typeof ev.then === "function" ? ev.then(d) : ev.then;
|
|
951
|
+
if (next !== undefined && next !== null) {
|
|
952
|
+
if (!machine.states[next]) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`claude-artifact-framework: machine event "${eventName}" resolved to unknown state "${next}". Valid states: ${Object.keys(machine.states).join(", ")}.`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
d[key] = next;
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
832
962
|
export function createApp(spec = {}) {
|
|
833
963
|
const layout = validate(spec);
|
|
964
|
+
if (spec.machine) {
|
|
965
|
+
const names = Object.keys(spec.machine.states);
|
|
966
|
+
for (const b of everyBlock(spec)) {
|
|
967
|
+
if (b && b.state !== undefined && !names.includes(b.state)) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
`claude-artifact-framework: a block declares state: "${b.state}" but the machine's states are: ${names.join(", ")}.`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
for (const b of everyBlock(spec)) {
|
|
975
|
+
if (b && b.state !== undefined) {
|
|
976
|
+
throw new Error('claude-artifact-framework: `state:` on a block needs a `machine` declared at the top level.');
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
834
980
|
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
835
981
|
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
836
982
|
// Live File objects picked by `file` fields — in memory only, never persisted.
|
|
@@ -863,6 +1009,27 @@ export function createApp(spec = {}) {
|
|
|
863
1009
|
return unsubscribe;
|
|
864
1010
|
}, []);
|
|
865
1011
|
|
|
1012
|
+
// Machine `after` timers are framework-owned: armed on entering a state,
|
|
1013
|
+
// cancelled on leaving it (the dep is the current state), cleaned up on
|
|
1014
|
+
// unmount. A reload mid-state re-arms the full duration. The timeout
|
|
1015
|
+
// re-checks the state before firing — a transition that happened while
|
|
1016
|
+
// it was pending wins.
|
|
1017
|
+
const machineState = spec.machine ? state.getData()[spec.machine.key || "fase"] : null;
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
if (!spec.machine || !state.isHydrated()) return;
|
|
1020
|
+
const st = spec.machine.states[machineState];
|
|
1021
|
+
if (!st || !st.after) return;
|
|
1022
|
+
const timer = setTimeout(() => {
|
|
1023
|
+
state.update((d) => {
|
|
1024
|
+
const key = spec.machine.key || "fase";
|
|
1025
|
+
if (d[key] !== machineState) return;
|
|
1026
|
+
if (st.after.do) st.after.do(d);
|
|
1027
|
+
d[key] = st.after.then;
|
|
1028
|
+
});
|
|
1029
|
+
}, st.after.seconds * 1000);
|
|
1030
|
+
return () => clearTimeout(timer);
|
|
1031
|
+
}, [machineState, state.isHydrated()]);
|
|
1032
|
+
|
|
866
1033
|
const ctx = {
|
|
867
1034
|
React: getReact(),
|
|
868
1035
|
data: state.getData(),
|
|
@@ -876,6 +1043,12 @@ export function createApp(spec = {}) {
|
|
|
876
1043
|
setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
|
|
877
1044
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
878
1045
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
1046
|
+
machine: spec.machine || null,
|
|
1047
|
+
send: spec.machine
|
|
1048
|
+
? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload)
|
|
1049
|
+
: () => {
|
|
1050
|
+
throw new Error("claude-artifact-framework: ctx.send() needs a `machine` declared at the top level.");
|
|
1051
|
+
},
|
|
879
1052
|
};
|
|
880
1053
|
|
|
881
1054
|
const Layout = LAYOUT_COMPONENTS[layout];
|