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 +22 -8
- package/dist/index.esm.js +54 -15
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +13 -12
- package/dist/index.global.js.map +3 -3
- package/llms.txt +5 -3
- package/package.json +1 -1
- package/src/app.js +21 -3
- package/src/records.js +23 -2
- package/src/steps.js +18 -10
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
|
|
347
|
-
`(record, data, viewerId)
|
|
348
|
-
early is fine (`run: (r) => ...`),
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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. **
|
|
997
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
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") :
|
|
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; }
|