claude-artifact-framework 0.17.0 → 0.18.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/llms.txt CHANGED
@@ -32,7 +32,8 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
32
32
  (+ key when used as a block)
33
33
  - documents: label, title, blocks OR types, empty; machine: key, initial,
34
34
  states, events (event: from, do, then); chat: system, greeting, placeholder,
35
- tools, model, maxTokens; steps step: title, text, fields, result
35
+ tools, model, maxTokens; steps step: title, text, fields, result,
36
+ blocks (result steps only — end a wizard on charts/text under the stats)
36
37
  - Formats: money, percent (0-100), number, date. Output/chart/compute items:
37
38
  { label, value, format, big }
38
39
 
@@ -50,8 +51,9 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
50
51
  5. Phases (games, turns, processes) belong to `machine` — ctx.send(event,
51
52
  payload) is the only door; events are no-ops outside their `from` states;
52
53
  `after: {seconds, then, do}` timers are framework-owned.
53
- 6. Row actions receive (record, data, viewerId); block actions (data,
54
- viewerId). Stop early if you like, but never skip a middle argument.
54
+ 6. Row callbacks (when AND run) receive (record, data, viewerId); block
55
+ actions (data, viewerId). Stop early if you like; a 2nd param literally
56
+ named viewerId is matched by name and gets the viewer id.
55
57
  7. One big:true per screen; the constructive action takes kind:"primary"; a
56
58
  counter changed by pressing is an action, not an editable number field.
57
59
  8. Parts of a record are `items` — never JSON in a textarea.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-artifact-framework",
3
- "version": "0.17.0",
3
+ "version": "0.18.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
@@ -312,7 +312,7 @@ function validate(spec) {
312
312
  if (!Array.isArray(steps) || steps.length === 0) {
313
313
  throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
314
314
  }
315
- const STEP_KEYS = ["title", "text", "fields", "result"];
315
+ const STEP_KEYS = ["title", "text", "fields", "result", "blocks"];
316
316
  const seen = new Set();
317
317
  steps.forEach((st, i) => {
318
318
  if (!st || typeof st !== "object") {
@@ -327,6 +327,15 @@ function validate(spec) {
327
327
  if (!st.fields && !st.text && !st.result) {
328
328
  throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
329
329
  }
330
+ // A result step may close on more than stats: `blocks` renders any
331
+ // block set (charts, text, lists) under the result grid. Measured
332
+ // miss: a wizard that wanted to END on a chart had nowhere to put it.
333
+ if (st.blocks !== undefined) {
334
+ if (typeof st.result !== "function") {
335
+ throw new Error(`claude-artifact-framework: steps[${i}].blocks only renders on a result step — add \`result\` or move the blocks to the step's fields.`);
336
+ }
337
+ checkBlocks(st.blocks);
338
+ }
330
339
  checkFields(st.fields, `steps[${i}]`);
331
340
  for (const f of st.fields || []) {
332
341
  if (seen.has(f.key)) {
@@ -920,7 +929,10 @@ function DocumentsLayout({ spec, ctx }) {
920
929
  { className: "caf-documents" },
921
930
  h(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
922
931
  typePicker,
923
- h(Panel, { spec: { blocks: aspec.blocks }, ctx: scoped })
932
+ // Keyed by document id: switching tabs REMOUNTS the pane, so DOM-held
933
+ // state can't leak across documents. Measured: the native file input
934
+ // kept showing the previous document's filename.
935
+ h(Panel, { key: active.id, spec: { blocks: aspec.blocks }, ctx: scoped })
924
936
  );
925
937
  }
926
938
 
@@ -1133,6 +1145,7 @@ export function createApp(spec = {}) {
1133
1145
  colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
1134
1146
  viewport: narrow ? "mobile" : "desktop",
1135
1147
  _navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
1148
+ _Block: Block, // for layouts in other modules (steps) that render blocks
1136
1149
  machine: spec.machine || null,
1137
1150
  send: spec.machine
1138
1151
  ? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload)
@@ -1192,7 +1205,11 @@ export function createApp(spec = {}) {
1192
1205
  )
1193
1206
  : !state.isHydrated() || libsState === "loading"
1194
1207
  ? h("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries…" : "Loading…")
1195
- : h(Layout, { spec, ctx }),
1208
+ : // The same containment blocks get, one level up: a crash inside a
1209
+ // layout (a throwing sort, a bad row callback) shows a named error
1210
+ // under the header instead of killing the whole app. Measured: an
1211
+ // unhandled row-action throw once blanked an entire blind app.
1212
+ h(boundary(ctx.React), null, h(Layout, { spec, ctx })),
1196
1213
  spec.brand && spec.brand.footer ? h("footer", { className: "caf-footer" }, spec.brand.footer) : null
1197
1214
  );
1198
1215
  };
@@ -1316,6 +1333,7 @@ const BASE_CSS = `
1316
1333
  .caf-timer-controls { display: flex; gap: 0.6rem; }
1317
1334
  .caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1318
1335
  .caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
1336
+ .caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
1319
1337
  .caf-steps-fill { transition: width 0.25s ease; }
1320
1338
  .caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
1321
1339
  .caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
package/src/records.js CHANGED
@@ -54,6 +54,27 @@ function makeId() {
54
54
  return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
55
55
  }
56
56
 
57
+ // Call-by-name for row callbacks. The measured 50/50 trap: both
58
+ // `when: (r, viewerId)` (the old documented form) and `run: (r, viewerId)`
59
+ // (the mistake that killed an entire blind app — `data` skipped, so `d`
60
+ // held a viewer-id string) exist in the wild. Positions lie; names don't:
61
+ // if the second declared parameter is named like the viewer id, hand it
62
+ // the viewer id. Every callback gets what its own names ask for, and the
63
+ // uniform (record, data, viewerId) order in the docs stays the one truth.
64
+ const rowCallLegacy = new WeakMap();
65
+ function rowCall(fn, rec, data, viewerId) {
66
+ let legacy = rowCallLegacy.get(fn);
67
+ if (legacy === undefined) {
68
+ const src = String(fn);
69
+ const arrow = src.match(/^\s*(?:async\s+)?([\w$]+)\s*=>/); // one bare param
70
+ const parens = src.match(/^[^(]*\(([^)]*)\)/);
71
+ const params = arrow ? [arrow[1]] : parens ? parens[1].split(",").map((s) => s.trim().split(/[=\s:]/)[0]) : [];
72
+ legacy = /viewer|voter|usuario|user/i.test(params[1] || "");
73
+ rowCallLegacy.set(fn, legacy);
74
+ }
75
+ return legacy ? fn(rec, viewerId) : fn(rec, data, viewerId);
76
+ }
77
+
57
78
  // avatar: true — an initials circle per row, colored deterministically from
58
79
  // the row title through the theme's series palette. Identity without assets.
59
80
  function Avatar({ title, ctx }) {
@@ -184,7 +205,7 @@ function ListScreen({ spec, ctx }) {
184
205
  "div",
185
206
  { className: "caf-row-actions" },
186
207
  spec.actions
187
- .filter((a) => !a.when || a.when(rec, ctx.viewerId))
208
+ .filter((a) => !a.when || rowCall(a.when, rec, ctx.data, ctx.viewerId))
188
209
  .map((a) =>
189
210
  h(
190
211
  "button",
@@ -195,7 +216,7 @@ function ListScreen({ spec, ctx }) {
195
216
  onClick: () =>
196
217
  ctx.update((d) => {
197
218
  const live = d.records.find((x) => x.id === rec.id);
198
- if (live) a.run(live, d, ctx.viewerId);
219
+ if (live) rowCall(a.run, live, d, ctx.viewerId);
199
220
  }),
200
221
  },
201
222
  a.label
package/src/steps.js CHANGED
@@ -22,23 +22,31 @@ function stepErrors(step, data) {
22
22
 
23
23
  function ResultScreen({ step, ctx }) {
24
24
  const items = step.result(ctx.data) || [];
25
+ const Block = ctx._Block;
25
26
  return h(
26
27
  "div",
27
- { className: "caf-block caf-output" },
28
- step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
28
+ { className: "caf-steps-result" },
29
29
  h(
30
30
  "div",
31
- { className: "caf-output-grid" },
32
- items.map((item, i) =>
33
- h(
34
- "div",
35
- { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
36
- h("span", { className: "caf-stat-label" }, item.label),
37
- h("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
38
- item.hint ? h("small", { className: "caf-hint" }, item.hint) : null
31
+ { className: "caf-block caf-output" },
32
+ step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
33
+ h(
34
+ "div",
35
+ { className: "caf-output-grid" },
36
+ items.map((item, i) =>
37
+ h(
38
+ "div",
39
+ { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
40
+ h("span", { className: "caf-stat-label" }, item.label),
41
+ h("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
42
+ item.hint ? h("small", { className: "caf-hint" }, item.hint) : null
43
+ )
39
44
  )
40
45
  )
41
46
  ),
47
+ // A result can end on more than stats: any declared blocks (charts,
48
+ // text, lists) render under the grid with full block services.
49
+ (step.blocks || []).map((b, i) => h(Block, { key: "rb" + i, block: b, ctx })),
42
50
  h(
43
51
  "div",
44
52
  { className: "caf-step-nav" },