claude-artifact-framework 0.13.0 → 0.14.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 +122 -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/package.json +1 -1
- package/src/app.js +175 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|
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];
|