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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-artifact-framework",
3
- "version": "0.6.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
- const c = spec.chat;
137
- if (!c || (typeof c.system !== "string" && typeof c.system !== "function")) {
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 "chat" needs `chat: { system: "..." }` (a string or a function of data).'
169
+ 'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
140
170
  );
141
171
  }
142
- const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
143
- if (unknown.length) {
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
- `claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
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
- if ((spec.layout || "panel") === "chat" && !data.chat) {
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 [...(spec.blocks || []), ...(spec.steps || [])]) {
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
- const LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
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
- (field.options || []).map((opt) => {
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" }, title(row)),
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
- export function ChatLayout({ spec, ctx }) {
172
- const c = spec.chat;
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-panel caf-panel-single" },
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) => {
package/src/steps.js CHANGED
@@ -78,6 +78,7 @@ export function StepsLayout({ spec, ctx }) {
78
78
  h(Field, {
79
79
  key: field.key,
80
80
  field,
81
+ data: ctx.data,
81
82
  value: ctx.data[field.key],
82
83
  onChange: (v) => ctx.update((d) => { d[field.key] = v; }),
83
84
  })