claude-artifact-framework 0.6.0 → 0.7.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 +60 -9
- package/dist/index.esm.js +315 -254
- package/dist/index.esm.js.map +4 -4
- package/dist/index.global.js +23 -13
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +73 -18
- package/src/blocks.js +15 -4
- package/src/chat.js +10 -3
- package/src/records.js +2 -0
- package/src/steps.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
package/src/app.js
CHANGED
|
@@ -17,13 +17,13 @@ import { RecordsLayout } from "./records.js";
|
|
|
17
17
|
import { StepsLayout } from "./steps.js";
|
|
18
18
|
import { ChatLayout, CHAT_KEYS } from "./chat.js";
|
|
19
19
|
|
|
20
|
-
const LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
20
|
+
const LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
21
21
|
const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
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"];
|
|
26
|
+
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
|
|
27
27
|
|
|
28
28
|
// An unknown field type used to fall through silently to a text input — the
|
|
29
29
|
// exact quiet degradation the framework exists to prevent (a blind-tested
|
|
@@ -36,6 +36,32 @@ function checkFields(fields, where) {
|
|
|
36
36
|
`claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
|
+
if (f.type === "select" && f.options !== undefined && !Array.isArray(f.options) && typeof f.options !== "function") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`claude-artifact-framework: field "${f.key}" in ${where}: \`options\` must be an array or a function of data returning one.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkChatConfig(c, where) {
|
|
48
|
+
if (!c || (typeof c.system !== "string" && typeof c.system !== "function")) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`claude-artifact-framework: ${where} needs \`{ system: "..." }\` (a string or a function of data).`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
|
|
54
|
+
if (unknown.length) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`claude-artifact-framework: ${where} has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
60
|
+
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
39
65
|
}
|
|
40
66
|
}
|
|
41
67
|
|
|
@@ -84,6 +110,7 @@ function checkBlocks(blocks) {
|
|
|
84
110
|
}
|
|
85
111
|
checkFields(block.fields, `blocks[${i}]`);
|
|
86
112
|
if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
|
|
113
|
+
if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
|
|
87
114
|
});
|
|
88
115
|
}
|
|
89
116
|
|
|
@@ -133,25 +160,28 @@ function validate(spec) {
|
|
|
133
160
|
return layout;
|
|
134
161
|
}
|
|
135
162
|
if (layout === "chat") {
|
|
136
|
-
|
|
137
|
-
|
|
163
|
+
checkChatConfig(spec.chat, "chat");
|
|
164
|
+
return layout;
|
|
165
|
+
}
|
|
166
|
+
if (layout === "workspace") {
|
|
167
|
+
if (!Array.isArray(spec.main) || spec.main.length === 0) {
|
|
138
168
|
throw new Error(
|
|
139
|
-
'claude-artifact-framework: layout "
|
|
169
|
+
'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
|
|
140
170
|
);
|
|
141
171
|
}
|
|
142
|
-
|
|
143
|
-
if (
|
|
172
|
+
checkBlocks(spec.main);
|
|
173
|
+
if (spec.sidebar !== undefined) {
|
|
174
|
+
if (!Array.isArray(spec.sidebar)) {
|
|
175
|
+
throw new Error("claude-artifact-framework: workspace `sidebar` must be an array of blocks.");
|
|
176
|
+
}
|
|
177
|
+
checkBlocks(spec.sidebar);
|
|
178
|
+
}
|
|
179
|
+
const chats = [...spec.main, ...(spec.sidebar || [])].filter((b) => b && b.chat);
|
|
180
|
+
if (chats.length > 1) {
|
|
144
181
|
throw new Error(
|
|
145
|
-
|
|
182
|
+
"claude-artifact-framework: only one chat block per app — the conversation persists under data.chat and there is exactly one."
|
|
146
183
|
);
|
|
147
184
|
}
|
|
148
|
-
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
149
|
-
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
185
|
return layout;
|
|
156
186
|
}
|
|
157
187
|
if (layout === "records") {
|
|
@@ -222,10 +252,12 @@ function defaultsFrom(spec) {
|
|
|
222
252
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
223
253
|
data.records = [];
|
|
224
254
|
}
|
|
225
|
-
|
|
255
|
+
const allBlocks = [...(spec.blocks || []), ...(spec.main || []), ...(spec.sidebar || [])];
|
|
256
|
+
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
257
|
+
if (hasChat && !data.chat) {
|
|
226
258
|
data.chat = { api: [], log: [] };
|
|
227
259
|
}
|
|
228
|
-
for (const block of [...
|
|
260
|
+
for (const block of [...allBlocks, ...(spec.steps || [])]) {
|
|
229
261
|
for (const field of block.fields || []) {
|
|
230
262
|
if (field && field.key !== undefined && !(field.key in data)) {
|
|
231
263
|
data[field.key] = field.value !== undefined ? field.value : field.type === "check" ? false : "";
|
|
@@ -312,7 +344,20 @@ function RecordsWithBlocks({ spec, ctx }) {
|
|
|
312
344
|
);
|
|
313
345
|
}
|
|
314
346
|
|
|
315
|
-
|
|
347
|
+
// The "content + copilot" shape: a full-width main column and a sidebar.
|
|
348
|
+
// Stacks vertically on narrow screens (sidebar after main).
|
|
349
|
+
function WorkspaceLayout({ spec, ctx }) {
|
|
350
|
+
const render = (blocks) =>
|
|
351
|
+
(blocks || []).map((block, i) => h("section", { key: i, className: "caf-ws-slot" }, h(Block, { block, ctx })));
|
|
352
|
+
return h(
|
|
353
|
+
"div",
|
|
354
|
+
{ className: "caf-workspace" },
|
|
355
|
+
h("div", { className: "caf-ws-main" }, render(spec.main)),
|
|
356
|
+
spec.sidebar && spec.sidebar.length ? h("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
|
|
316
361
|
|
|
317
362
|
export function createApp(spec = {}) {
|
|
318
363
|
const layout = validate(spec);
|
|
@@ -505,6 +550,16 @@ const BASE_CSS = `
|
|
|
505
550
|
.caf-md-h { display: block; margin-top: 0.3rem; }
|
|
506
551
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
507
552
|
.caf-md-li::before { content: "•"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
553
|
+
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
554
|
+
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
555
|
+
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
|
|
556
|
+
.caf-ws-slot { min-width: 0; }
|
|
557
|
+
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
558
|
+
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
559
|
+
@media (max-width: 900px) {
|
|
560
|
+
.caf-workspace { flex-direction: column; align-items: stretch; }
|
|
561
|
+
.caf-ws-side { width: 100%; position: static; }
|
|
562
|
+
}
|
|
508
563
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
509
564
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
510
565
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|
package/src/blocks.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Bound lazily rather than snapshotted as `React.createElement`, because this
|
|
12
12
|
// runs at module load — before the host has had a chance to hand React over.
|
|
13
13
|
import { createElement as h, useState, useEffect } from "react";
|
|
14
|
+
import { ChatPanel } from "./chat.js";
|
|
14
15
|
|
|
15
16
|
const FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
16
17
|
|
|
@@ -49,7 +50,7 @@ export function validateField(field, value) {
|
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function Field({ field, value, onChange }) {
|
|
53
|
+
export function Field({ field, value, onChange, data }) {
|
|
53
54
|
const error = validateField(field, value);
|
|
54
55
|
const id = `caf-f-${field.key}`;
|
|
55
56
|
const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
|
|
@@ -79,11 +80,14 @@ export function Field({ field, value, onChange }) {
|
|
|
79
80
|
if (field.type === "textarea") {
|
|
80
81
|
control = h("textarea", { ...common, rows: field.rows || 3 });
|
|
81
82
|
} else if (field.type === "select") {
|
|
83
|
+
// Options may be declared as a function of the app data, for selects
|
|
84
|
+
// whose choices come from what the user has entered elsewhere.
|
|
85
|
+
const options = typeof field.options === "function" ? field.options(data) || [] : field.options || [];
|
|
82
86
|
control = h(
|
|
83
87
|
"select",
|
|
84
88
|
common,
|
|
85
89
|
h("option", { value: "" }, field.placeholder || "Select…"),
|
|
86
|
-
|
|
90
|
+
options.map((opt) => {
|
|
87
91
|
const val = typeof opt === "string" ? opt : opt.value;
|
|
88
92
|
const label = typeof opt === "string" ? opt : opt.label;
|
|
89
93
|
return h("option", { key: val, value: val }, label);
|
|
@@ -126,6 +130,7 @@ export function FieldsBlock({ spec, ctx }) {
|
|
|
126
130
|
h(Field, {
|
|
127
131
|
key: field.key,
|
|
128
132
|
field,
|
|
133
|
+
data: ctx.data,
|
|
129
134
|
value: ctx.data[field.key],
|
|
130
135
|
onChange: (v) => ctx.update((d) => { d[field.key] = v; }),
|
|
131
136
|
})
|
|
@@ -170,13 +175,18 @@ export function TextBlock({ spec, ctx }) {
|
|
|
170
175
|
|
|
171
176
|
export function ListBlock({ spec, ctx }) {
|
|
172
177
|
const rows = (typeof spec.list === "function" ? spec.list(ctx.data) : spec.list) || [];
|
|
178
|
+
// `title` follows the same convention as every other block: a string is the
|
|
179
|
+
// block heading. A function is the per-row title. Both are supported —
|
|
180
|
+
// making list the odd one out was an API inconsistency a blind test caught.
|
|
181
|
+
const heading = typeof spec.title === "string" ? spec.title : null;
|
|
182
|
+
const rowTitle = typeof spec.title === "function" ? spec.title : (r) => r.title ?? r.name ?? r.label ?? r.text ?? String(r);
|
|
173
183
|
if (!rows.length) {
|
|
174
184
|
return h("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing here yet.");
|
|
175
185
|
}
|
|
176
|
-
const title = spec.title || ((r) => r.title ?? r.name ?? r.label ?? String(r));
|
|
177
186
|
return h(
|
|
178
187
|
"div",
|
|
179
188
|
{ className: "caf-block caf-list" },
|
|
189
|
+
heading ? h("h2", { className: "caf-block-title" }, heading) : null,
|
|
180
190
|
rows.map((row, i) =>
|
|
181
191
|
h(
|
|
182
192
|
"button",
|
|
@@ -186,7 +196,7 @@ export function ListBlock({ spec, ctx }) {
|
|
|
186
196
|
type: "button",
|
|
187
197
|
onClick: spec.onSelect ? () => spec.onSelect(row, ctx) : undefined,
|
|
188
198
|
},
|
|
189
|
-
h("span", { className: "caf-row-title" },
|
|
199
|
+
h("span", { className: "caf-row-title" }, rowTitle(row)),
|
|
190
200
|
spec.subtitle ? h("span", { className: "caf-row-sub" }, spec.subtitle(row)) : null
|
|
191
201
|
)
|
|
192
202
|
)
|
|
@@ -416,6 +426,7 @@ export const BLOCKS = {
|
|
|
416
426
|
chart: ChartBlock,
|
|
417
427
|
timer: TimerBlock,
|
|
418
428
|
actions: ActionsBlock,
|
|
429
|
+
chat: ({ spec, ctx }) => h(ChatPanel, { cfg: spec.chat, ctx }),
|
|
419
430
|
custom: CustomBlock,
|
|
420
431
|
};
|
|
421
432
|
|
package/src/chat.js
CHANGED
|
@@ -168,8 +168,11 @@ function Bubble({ m }) {
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// The embeddable core: everything the chat layout does, as a single column
|
|
172
|
+
// that can live in a workspace sidebar (or any block slot) as well as fill
|
|
173
|
+
// the whole app. One conversation per app — it persists under data.chat.
|
|
174
|
+
export function ChatPanel({ cfg, ctx }) {
|
|
175
|
+
const c = cfg;
|
|
173
176
|
const [draft, setDraft] = useState("");
|
|
174
177
|
const [busy, setBusy] = useState(false);
|
|
175
178
|
const [live, setLive] = useState(null);
|
|
@@ -243,7 +246,7 @@ export function ChatLayout({ spec, ctx }) {
|
|
|
243
246
|
|
|
244
247
|
return h(
|
|
245
248
|
"div",
|
|
246
|
-
{ className: "caf-
|
|
249
|
+
{ className: "caf-chat-panel" },
|
|
247
250
|
h(
|
|
248
251
|
"div",
|
|
249
252
|
{ className: "caf-block caf-chat-log" },
|
|
@@ -287,3 +290,7 @@ export function ChatLayout({ spec, ctx }) {
|
|
|
287
290
|
)
|
|
288
291
|
);
|
|
289
292
|
}
|
|
293
|
+
|
|
294
|
+
export function ChatLayout({ spec, ctx }) {
|
|
295
|
+
return h("div", { className: "caf-panel caf-panel-single" }, h(ChatPanel, { cfg: spec.chat, ctx }));
|
|
296
|
+
}
|
package/src/records.js
CHANGED
|
@@ -199,6 +199,7 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
199
199
|
h(Field, {
|
|
200
200
|
key: field.key,
|
|
201
201
|
field,
|
|
202
|
+
data: ctx.data,
|
|
202
203
|
value: record[field.key],
|
|
203
204
|
onChange: (v) =>
|
|
204
205
|
ctx.update((d) => {
|
|
@@ -272,6 +273,7 @@ function ItemsEditor({ spec, ctx, record }) {
|
|
|
272
273
|
h(Field, {
|
|
273
274
|
key: field.key,
|
|
274
275
|
field,
|
|
276
|
+
data: ctx.data,
|
|
275
277
|
value: item[field.key],
|
|
276
278
|
onChange: (v) =>
|
|
277
279
|
mutate((rec) => {
|