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/README.md CHANGED
@@ -168,6 +168,18 @@ ArtifactKit.createApp({
168
168
  Field keys must be unique across all steps (they share one data pool); a
169
169
  duplicate throws at creation, as does an unknown step key.
170
170
 
171
+ A result step can end on more than stats: give it `blocks` and any block
172
+ set (a chart of the answers, explanatory text, a list) renders under the
173
+ result grid:
174
+
175
+ ```js
176
+ { title: "Resultado",
177
+ result: (d) => [{ label: "Libre por mes", value: libre(d), format: "money", big: true }],
178
+ blocks: [{ title: "Distribución", chart: (d) => ({ type: "donut", format: "money", items: porRubro(d) }) }] }
179
+ ```
180
+
181
+ `blocks` on a non-result step throws.
182
+
171
183
  ### The `chart` block — bar, line, donut
172
184
 
173
185
  Same vocabulary as `output` (items of `{ label, value }` plus an optional
@@ -343,12 +355,13 @@ blocks: [
343
355
  ```
344
356
 
345
357
  Action keys: `label`, `run`, optional `when` (hides the button when false)
346
- and `kind` (`"primary"` / `"danger"`). Row actions receive
347
- `(record, data, viewerId)`, block actions `(data, viewerId)`. Stopping
348
- early is fine (`run: (r) => ...`), but **never skip a middle argument**:
349
- `run: (r, viewerId)` reads `data` as your viewer id and breaks silently —
350
- if you need `viewerId`, name everything before it: `run: (r, d, viewerId)`.
351
- Changes persist like any other update.
358
+ and `kind` (`"primary"` / `"danger"`). Row actions — `when` and `run`
359
+ alike — receive `(record, data, viewerId)`; block actions
360
+ `(data, viewerId)`. Stopping early is fine (`run: (r) => ...`), and the
361
+ framework matches the viewer id **by name**: a second parameter literally
362
+ named `viewerId` receives the viewer id even though `data` was skipped
363
+ `run: (r, viewerId) => ...` works as written. Still, prefer the full
364
+ order: `run: (r, d, viewerId)`. Changes persist like any other update.
352
365
  Give the constructive action `kind: "primary"` — a "Vote"/"Add"/"Done" left
353
366
  as the neutral default reads weaker than a `"danger"` next to it. And a
354
367
  smell to avoid: **a number that changes by pressing is an `action`, not an
@@ -993,8 +1006,9 @@ The rules that decide whether a generated app works, all in one place
993
1006
  or `ctx.files`.
994
1007
  5. **Phases belong to the `machine`** — never track game/process phases
995
1008
  with hand-rolled flags and `setTimeout`; `ctx.send` is the only door.
996
- 6. **Never skip a middle positional argument** — `run: (r, d, viewerId)`,
997
- never `run: (r, viewerId)`.
1009
+ 6. **Row callbacks are `(record, data, viewerId)`** — `when` and `run`
1010
+ alike. Prefer the full order; a second parameter literally named
1011
+ `viewerId` is matched by name and still gets the viewer id.
998
1012
  7. **One `big: true` per screen** — it marks THE result, not every stat.
999
1013
  8. **The constructive action takes `kind: "primary"`**; a counter changed
1000
1014
  by pressing is an `action`, not an editable `number` field.
package/dist/index.esm.js CHANGED
@@ -1070,6 +1070,19 @@ function recordDefaults(fields) {
1070
1070
  function makeId() {
1071
1071
  return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
1072
1072
  }
1073
+ var rowCallLegacy = /* @__PURE__ */ new WeakMap();
1074
+ function rowCall(fn, rec, data, viewerId) {
1075
+ let legacy = rowCallLegacy.get(fn);
1076
+ if (legacy === void 0) {
1077
+ const src = String(fn);
1078
+ const arrow = src.match(/^\s*(?:async\s+)?([\w$]+)\s*=>/);
1079
+ const parens = src.match(/^[^(]*\(([^)]*)\)/);
1080
+ const params = arrow ? [arrow[1]] : parens ? parens[1].split(",").map((s) => s.trim().split(/[=\s:]/)[0]) : [];
1081
+ legacy = /viewer|voter|usuario|user/i.test(params[1] || "");
1082
+ rowCallLegacy.set(fn, legacy);
1083
+ }
1084
+ return legacy ? fn(rec, viewerId) : fn(rec, data, viewerId);
1085
+ }
1073
1086
  function Avatar({ title, ctx }) {
1074
1087
  const initials = String(title).split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";
1075
1088
  let hash = 0;
@@ -1174,7 +1187,7 @@ function ListScreen({ spec, ctx }) {
1174
1187
  spec.actions ? createElement(
1175
1188
  "div",
1176
1189
  { className: "caf-row-actions" },
1177
- spec.actions.filter((a) => !a.when || a.when(rec, ctx.viewerId)).map(
1190
+ spec.actions.filter((a) => !a.when || rowCall(a.when, rec, ctx.data, ctx.viewerId)).map(
1178
1191
  (a) => createElement(
1179
1192
  "button",
1180
1193
  {
@@ -1183,7 +1196,7 @@ function ListScreen({ spec, ctx }) {
1183
1196
  className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
1184
1197
  onClick: () => ctx.update((d) => {
1185
1198
  const live = d.records.find((x) => x.id === rec.id);
1186
- if (live) a.run(live, d, ctx.viewerId);
1199
+ if (live) rowCall(a.run, live, d, ctx.viewerId);
1187
1200
  })
1188
1201
  },
1189
1202
  a.label
@@ -1354,23 +1367,31 @@ function stepErrors(step, data) {
1354
1367
  }
1355
1368
  function ResultScreen({ step, ctx }) {
1356
1369
  const items = step.result(ctx.data) || [];
1370
+ const Block2 = ctx._Block;
1357
1371
  return createElement(
1358
1372
  "div",
1359
- { className: "caf-block caf-output" },
1360
- step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
1373
+ { className: "caf-steps-result" },
1361
1374
  createElement(
1362
1375
  "div",
1363
- { className: "caf-output-grid" },
1364
- items.map(
1365
- (item, i) => createElement(
1366
- "div",
1367
- { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
1368
- createElement("span", { className: "caf-stat-label" }, item.label),
1369
- createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
1370
- item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
1376
+ { className: "caf-block caf-output" },
1377
+ step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
1378
+ createElement(
1379
+ "div",
1380
+ { className: "caf-output-grid" },
1381
+ items.map(
1382
+ (item, i) => createElement(
1383
+ "div",
1384
+ { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
1385
+ createElement("span", { className: "caf-stat-label" }, item.label),
1386
+ createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
1387
+ item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
1388
+ )
1371
1389
  )
1372
1390
  )
1373
1391
  ),
1392
+ // A result can end on more than stats: any declared blocks (charts,
1393
+ // text, lists) render under the grid with full block services.
1394
+ (step.blocks || []).map((b, i) => createElement(Block2, { key: "rb" + i, block: b, ctx })),
1374
1395
  createElement(
1375
1396
  "div",
1376
1397
  { className: "caf-step-nav" },
@@ -1701,7 +1722,7 @@ function validate(spec) {
1701
1722
  if (!Array.isArray(steps) || steps.length === 0) {
1702
1723
  throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
1703
1724
  }
1704
- const STEP_KEYS = ["title", "text", "fields", "result"];
1725
+ const STEP_KEYS = ["title", "text", "fields", "result", "blocks"];
1705
1726
  const seen = /* @__PURE__ */ new Set();
1706
1727
  steps.forEach((st, i) => {
1707
1728
  if (!st || typeof st !== "object") {
@@ -1716,6 +1737,12 @@ function validate(spec) {
1716
1737
  if (!st.fields && !st.text && !st.result) {
1717
1738
  throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
1718
1739
  }
1740
+ if (st.blocks !== void 0) {
1741
+ if (typeof st.result !== "function") {
1742
+ throw new Error(`claude-artifact-framework: steps[${i}].blocks only renders on a result step \u2014 add \`result\` or move the blocks to the step's fields.`);
1743
+ }
1744
+ checkBlocks(st.blocks);
1745
+ }
1719
1746
  checkFields(st.fields, `steps[${i}]`);
1720
1747
  for (const f of st.fields || []) {
1721
1748
  if (seen.has(f.key)) {
@@ -2221,7 +2248,10 @@ function DocumentsLayout({ spec, ctx }) {
2221
2248
  { className: "caf-documents" },
2222
2249
  createElement(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
2223
2250
  typePicker,
2224
- createElement(Panel, { spec: { blocks: aspec.blocks }, ctx: scoped })
2251
+ // Keyed by document id: switching tabs REMOUNTS the pane, so DOM-held
2252
+ // state can't leak across documents. Measured: the native file input
2253
+ // kept showing the previous document's filename.
2254
+ createElement(Panel, { key: active.id, spec: { blocks: aspec.blocks }, ctx: scoped })
2225
2255
  );
2226
2256
  }
2227
2257
  function DocTabbar({ docs, active, dspec, types, close, onAdd, label, ctx }) {
@@ -2404,6 +2434,8 @@ function createApp(spec = {}) {
2404
2434
  colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
2405
2435
  viewport: narrow ? "mobile" : "desktop",
2406
2436
  _navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
2437
+ _Block: Block,
2438
+ // for layouts in other modules (steps) that render blocks
2407
2439
  machine: spec.machine || null,
2408
2440
  send: spec.machine ? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload) : () => {
2409
2441
  throw new Error("claude-artifact-framework: ctx.send() needs a `machine` declared at the top level.");
@@ -2438,7 +2470,13 @@ function createApp(spec = {}) {
2438
2470
  { className: "caf-block caf-block-error" },
2439
2471
  createElement("strong", null, "A library failed to load"),
2440
2472
  createElement("code", null, libsError)
2441
- ) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : createElement(Layout, { spec, ctx }),
2473
+ ) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : (
2474
+ // The same containment blocks get, one level up: a crash inside a
2475
+ // layout (a throwing sort, a bad row callback) shows a named error
2476
+ // under the header instead of killing the whole app. Measured: an
2477
+ // unhandled row-action throw once blanked an entire blind app.
2478
+ createElement(boundary(ctx.React), null, createElement(Layout, { spec, ctx }))
2479
+ ),
2442
2480
  spec.brand && spec.brand.footer ? createElement("footer", { className: "caf-footer" }, spec.brand.footer) : null
2443
2481
  );
2444
2482
  };
@@ -2561,6 +2599,7 @@ var BASE_CSS = `
2561
2599
  .caf-timer-controls { display: flex; gap: 0.6rem; }
2562
2600
  .caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
2563
2601
  .caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
2602
+ .caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
2564
2603
  .caf-steps-fill { transition: width 0.25s ease; }
2565
2604
  .caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
2566
2605
  .caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }