claude-artifact-framework 0.7.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.7.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
@@ -44,6 +44,27 @@ function checkFields(fields, where) {
44
44
  }
45
45
  }
46
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
+ }
65
+ }
66
+ }
67
+
47
68
  const ACTION_KEYS = ["label", "run", "when", "kind"];
48
69
 
49
70
  function checkActions(actions, where) {
@@ -89,6 +110,7 @@ function checkBlocks(blocks) {
89
110
  }
90
111
  checkFields(block.fields, `blocks[${i}]`);
91
112
  if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
113
+ if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
92
114
  });
93
115
  }
94
116
 
@@ -138,25 +160,28 @@ function validate(spec) {
138
160
  return layout;
139
161
  }
140
162
  if (layout === "chat") {
141
- const c = spec.chat;
142
- 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) {
143
168
  throw new Error(
144
- '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.'
145
170
  );
146
171
  }
147
- const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
148
- 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) {
149
181
  throw new Error(
150
- `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."
151
183
  );
152
184
  }
153
- for (const [name, tool] of Object.entries(c.tools || {})) {
154
- if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
155
- throw new Error(
156
- `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
157
- );
158
- }
159
- }
160
185
  return layout;
161
186
  }
162
187
  if (layout === "records") {
@@ -227,10 +252,12 @@ function defaultsFrom(spec) {
227
252
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
228
253
  data.records = [];
229
254
  }
230
- 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) {
231
258
  data.chat = { api: [], log: [] };
232
259
  }
233
- for (const block of [...(spec.blocks || []), ...(spec.steps || [])]) {
260
+ for (const block of [...allBlocks, ...(spec.steps || [])]) {
234
261
  for (const field of block.fields || []) {
235
262
  if (field && field.key !== undefined && !(field.key in data)) {
236
263
  data[field.key] = field.value !== undefined ? field.value : field.type === "check" ? false : "";
@@ -317,7 +344,20 @@ function RecordsWithBlocks({ spec, ctx }) {
317
344
  );
318
345
  }
319
346
 
320
- 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 };
321
361
 
322
362
  export function createApp(spec = {}) {
323
363
  const layout = validate(spec);
@@ -510,6 +550,16 @@ const BASE_CSS = `
510
550
  .caf-md-h { display: block; margin-top: 0.3rem; }
511
551
  .caf-md-li { display: block; padding-left: 1rem; position: relative; }
512
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
+ }
513
563
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
514
564
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
515
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
 
@@ -425,6 +426,7 @@ export const BLOCKS = {
425
426
  chart: ChartBlock,
426
427
  timer: TimerBlock,
427
428
  actions: ActionsBlock,
429
+ chat: ({ spec, ctx }) => h(ChatPanel, { cfg: spec.chat, ctx }),
428
430
  custom: CustomBlock,
429
431
  };
430
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
+ }