claude-artifact-framework 0.5.0 → 0.5.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.5.0",
3
+ "version": "0.5.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
@@ -12,7 +12,7 @@
12
12
  import { createElement as h, useState, useEffect, hasReact, getReact } from "react";
13
13
  import { createAppState } from "./appState.js";
14
14
  import { themeCss, seriesColors } from "./theme.js";
15
- import { BLOCKS, BLOCK_NAMES } from "./blocks.js";
15
+ import { BLOCKS, BLOCK_NAMES, FIELD_TYPES } from "./blocks.js";
16
16
  import { RecordsLayout } from "./records.js";
17
17
  import { StepsLayout } from "./steps.js";
18
18
  import { ChatLayout, CHAT_KEYS } from "./chat.js";
@@ -25,6 +25,48 @@ const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
25
25
  // silently rendering nothing — a blank pane is indistinguishable from a bug.
26
26
  const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
27
27
 
28
+ // An unknown field type used to fall through silently to a text input — the
29
+ // exact quiet degradation the framework exists to prevent (a blind-tested
30
+ // Haiku wrote "checkbox" for "check" and shipped a text field).
31
+ function checkFields(fields, where) {
32
+ for (const f of fields || []) {
33
+ if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
34
+ if (f.type !== undefined && !FIELD_TYPES.includes(f.type)) {
35
+ throw new Error(
36
+ `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
37
+ );
38
+ }
39
+ }
40
+ }
41
+
42
+ function checkBlocks(blocks) {
43
+ if (!Array.isArray(blocks)) {
44
+ throw new Error("claude-artifact-framework: `blocks` must be an array.");
45
+ }
46
+ blocks.forEach((block, i) => {
47
+ if (!block || typeof block !== "object") {
48
+ throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
49
+ }
50
+ const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
51
+ const known = keys.filter((k) => BLOCK_NAMES.includes(k));
52
+ if (known.length === 0) {
53
+ throw new Error(
54
+ `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${
55
+ keys.join(", ") || "nothing"
56
+ }). Valid block types: ${BLOCK_NAMES.join(", ")}.`
57
+ );
58
+ }
59
+ if (known.length > 1) {
60
+ throw new Error(
61
+ `claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
62
+ ", "
63
+ )}). Split them into separate entries of \`blocks\`.`
64
+ );
65
+ }
66
+ checkFields(block.fields, `blocks[${i}]`);
67
+ });
68
+ }
69
+
28
70
  function validate(spec) {
29
71
  const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
30
72
  if (unknownTop.length) {
@@ -58,8 +100,8 @@ function validate(spec) {
58
100
  if (!st.fields && !st.text && !st.result) {
59
101
  throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
60
102
  }
103
+ checkFields(st.fields, `steps[${i}]`);
61
104
  for (const f of st.fields || []) {
62
- if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
63
105
  if (seen.has(f.key)) {
64
106
  throw new Error(
65
107
  `claude-artifact-framework: field key "${f.key}" appears in more than one step — keys share one data pool and must be unique.`
@@ -106,13 +148,14 @@ function validate(spec) {
106
148
  `claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
107
149
  );
108
150
  }
151
+ checkFields(r.fields, "records");
109
152
  const seen = new Set();
110
153
  for (const f of r.fields) {
111
- if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
112
154
  if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records — the framework assigns it.');
113
155
  if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
114
156
  seen.add(f.key);
115
157
  }
158
+ if (spec.blocks) checkBlocks(spec.blocks);
116
159
  for (const [key, c] of Object.entries(r.compute || {})) {
117
160
  if (seen.has(key) || key === "id") {
118
161
  throw new Error(
@@ -127,31 +170,7 @@ function validate(spec) {
127
170
  }
128
171
  return layout;
129
172
  }
130
- const blocks = spec.blocks || [];
131
- if (!Array.isArray(blocks)) {
132
- throw new Error("claude-artifact-framework: `blocks` must be an array.");
133
- }
134
- blocks.forEach((block, i) => {
135
- if (!block || typeof block !== "object") {
136
- throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
137
- }
138
- const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
139
- const known = keys.filter((k) => BLOCK_NAMES.includes(k));
140
- if (known.length === 0) {
141
- throw new Error(
142
- `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${
143
- keys.join(", ") || "nothing"
144
- }). Valid block types: ${BLOCK_NAMES.join(", ")}.`
145
- );
146
- }
147
- if (known.length > 1) {
148
- throw new Error(
149
- `claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
150
- ", "
151
- )}). Split them into separate entries of \`blocks\`.`
152
- );
153
- }
154
- });
173
+ checkBlocks(spec.blocks || []);
155
174
  return layout;
156
175
  }
157
176
 
@@ -226,7 +245,34 @@ function Panel({ spec, ctx }) {
226
245
  );
227
246
  }
228
247
 
229
- const LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout, chat: ChatLayout };
248
+ // Records + extra blocks is a natural combo (a list with a chart of itself
249
+ // below). Blocks render under the list, never under the detail screen, and
250
+ // their functions receive the same data object as everywhere else — the
251
+ // collection lives at d.records.
252
+ function RecordsWithBlocks({ spec, ctx }) {
253
+ const detailOpen =
254
+ ctx.view.screen === "record" && (ctx.data.records || []).some((r) => r.id === ctx.view.arg);
255
+ return h(
256
+ "div",
257
+ null,
258
+ h(RecordsLayout, { spec, ctx }),
259
+ !detailOpen && Array.isArray(spec.blocks) && spec.blocks.length
260
+ ? h(
261
+ "div",
262
+ { className: "caf-panel caf-records-blocks" },
263
+ spec.blocks.map((block, i) =>
264
+ h(
265
+ "section",
266
+ { key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
267
+ h(Block, { block, ctx })
268
+ )
269
+ )
270
+ )
271
+ : null
272
+ );
273
+ }
274
+
275
+ const LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
230
276
 
231
277
  export function createApp(spec = {}) {
232
278
  const layout = validate(spec);
@@ -402,6 +448,7 @@ const BASE_CSS = `
402
448
  }
403
449
  .caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
404
450
  .caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
451
+ .caf-records-blocks { margin-top: 1rem; }
405
452
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
406
453
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
407
454
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }