claude-artifact-framework 0.19.1 → 0.19.2

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
@@ -34,7 +34,8 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
34
34
  states, events (event: from, do, then); chat: system, greeting, placeholder,
35
35
  tools, model, maxTokens; steps step: title, text, fields, result,
36
36
  blocks (result steps only — end a wizard on charts/text under the stats)
37
- - Formats: money, percent (0-100), number, date. Output/chart/compute items:
37
+ - Formats: money, percent (0-100), number, date, text (no-op; anything else
38
+ throws). NaN/Infinity render as "—". Output/chart/compute items:
38
39
  { label, value, format, big }
39
40
 
40
41
  ## Hard rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-artifact-framework",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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/blocks.js CHANGED
@@ -17,6 +17,9 @@ const FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check",
17
17
 
18
18
  export function formatValue(value, format) {
19
19
  if (value === null || value === undefined || value === "") return "—";
20
+ // A computed NaN/Infinity (0/0 on an empty list is the measured case)
21
+ // must read as "no data yet", never as the string "NaN" on screen.
22
+ if (typeof value === "number" && !Number.isFinite(value)) return "—";
20
23
  const n = Number(value);
21
24
  switch (format) {
22
25
  case "money":
@@ -29,8 +32,17 @@ export function formatValue(value, format) {
29
32
  return Number.isFinite(n) ? n.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(value);
30
33
  case "date":
31
34
  return value instanceof Date ? value.toLocaleDateString() : String(value);
32
- default:
35
+ case "text": // observed natural usage (round 21) — an explicit no-op
36
+ case undefined:
37
+ case null:
33
38
  return String(value);
39
+ default:
40
+ // Same loud-failure rule as every other registry: a typo must name
41
+ // the valid set, not silently un-format the number. The block
42
+ // boundary contains the throw to the offending card.
43
+ throw new Error(
44
+ `claude-artifact-framework: unknown format "${format}". Valid formats: money, percent, number, date, text.`
45
+ );
34
46
  }
35
47
  }
36
48
 
@@ -173,20 +185,17 @@ export function FieldsBlock({ spec, ctx }) {
173
185
  );
174
186
  }
175
187
 
176
- export function OutputBlock({ spec, ctx }) {
177
- const items = typeof spec.output === "function" ? spec.output(ctx.data) : spec.output || [];
178
- if (!items.length) {
179
- return h("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
180
- }
181
- // The block holding the screen's hero number (a `big` stat) gets the one
182
- // differentiated surface — an accent-tinted feature card. Audited miss:
183
- // with a single card chrome, the most important number on screen weighed
184
- // exactly the same as everything else.
188
+ // THE stat grid the single render path for output blocks, records
189
+ // summaries, and steps results. It exists because the feature-card rule was
190
+ // once applied to one of three hand-copied versions of this markup and an
191
+ // audit caught the other two bare (the re-audit CRÍTICO of 0.19.0). The
192
+ // block holding a `big` stat gets the screen's one differentiated surface.
193
+ export function StatGrid({ items, title }) {
185
194
  const hero = items.some((it) => it && it.big);
186
195
  return h(
187
196
  "div",
188
197
  { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
189
- spec.title ? h("h2", { className: "caf-block-title" }, spec.title) : null,
198
+ title ? h("h2", { className: "caf-block-title" }, title) : null,
190
199
  h(
191
200
  "div",
192
201
  { className: "caf-output-grid" },
@@ -203,6 +212,14 @@ export function OutputBlock({ spec, ctx }) {
203
212
  );
204
213
  }
205
214
 
215
+ export function OutputBlock({ spec, ctx }) {
216
+ const items = typeof spec.output === "function" ? spec.output(ctx.data) : spec.output || [];
217
+ if (!items.length) {
218
+ return h("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
219
+ }
220
+ return h(StatGrid, { items, title: spec.title });
221
+ }
222
+
206
223
  // Brand elements follow the zero-asset-first principle: every form works
207
224
  // with nothing uploaded (artifacts can't persist real images — the ~5MB
208
225
  // pool). banner's default face is a gradient derived from the theme seed;
package/src/records.js CHANGED
@@ -19,7 +19,7 @@
19
19
  // simply falls back to the list.
20
20
 
21
21
  import { createElement as h, useState, useEffect } from "react";
22
- import { Field, formatValue } from "./blocks.js";
22
+ import { Field, formatValue, StatGrid } from "./blocks.js";
23
23
 
24
24
  // Computed fields are derived at render time and never stored, so they can't
25
25
  // go stale. Accepts `{ subtotal: (r) => ... }` or
@@ -61,6 +61,8 @@ function makeId() {
61
61
  // if the second declared parameter is named like the viewer id, hand it
62
62
  // the viewer id. Every callback gets what its own names ask for, and the
63
63
  // uniform (record, data, viewerId) order in the docs stays the one truth.
64
+ // KNOWN LIMIT: minified specs rename parameters, silently defeating this
65
+ // detection — one more reason hard rule 1 bans minified output.
64
66
  const rowCallLegacy = new WeakMap();
65
67
  function rowCall(fn, rec, data, viewerId) {
66
68
  let legacy = rowCallLegacy.get(fn);
@@ -103,26 +105,7 @@ function Summary({ spec, records }) {
103
105
  if (!spec.summary) return null;
104
106
  const items = spec.summary(records) || [];
105
107
  if (!items.length) return null;
106
- // Same rule as OutputBlock: the hero (big) stat promotes its card to the
107
- // screen's feature surface. Re-audit caught this second render path
108
- // missing the treatment — the rule must hold wherever big stats render.
109
- const hero = items.some((it) => it && it.big);
110
- return h(
111
- "div",
112
- { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
113
- h(
114
- "div",
115
- { className: "caf-output-grid" },
116
- items.map((item, i) =>
117
- h(
118
- "div",
119
- { key: item.label || i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
120
- h("span", { className: "caf-stat-label" }, item.label),
121
- h("span", { className: "caf-stat-value" }, formatValue(item.value, item.format))
122
- )
123
- )
124
- )
125
- );
108
+ return h(StatGrid, { items });
126
109
  }
127
110
 
128
111
  function ListScreen({ spec, ctx }) {
package/src/steps.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // on the step the user was on, holding their answers.
15
15
 
16
16
  import { createElement as h } from "react";
17
- import { Field, validateField, formatValue, writeField } from "./blocks.js";
17
+ import { Field, validateField, writeField, StatGrid } from "./blocks.js";
18
18
 
19
19
  function stepErrors(step, data) {
20
20
  return (step.fields || []).some((f) => validateField(f, data[f.key]) !== null);
@@ -26,25 +26,7 @@ function ResultScreen({ step, ctx }) {
26
26
  return h(
27
27
  "div",
28
28
  { className: "caf-steps-result" },
29
- h(
30
- "div",
31
- // Same rule as OutputBlock: the hero (big) result gets the feature card.
32
- { className: items.some((it) => it && it.big) ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
33
- step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
34
- h(
35
- "div",
36
- { className: "caf-output-grid" },
37
- items.map((item, i) =>
38
- h(
39
- "div",
40
- { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
41
- h("span", { className: "caf-stat-label" }, item.label),
42
- h("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
43
- item.hint ? h("small", { className: "caf-hint" }, item.hint) : null
44
- )
45
- )
46
- )
47
- ),
29
+ h(StatGrid, { items, title: step.title }),
48
30
  // A result can end on more than stats: any declared blocks (charts,
49
31
  // text, lists) render under the grid with full block services.
50
32
  (step.blocks || []).map((b, i) => h(Block, { key: "rb" + i, block: b, ctx })),