claude-artifact-framework 0.11.0 → 0.12.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
@@ -394,6 +394,47 @@ The active tab and each accordion's open/closed state are framework-owned
394
394
  view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
395
395
  `when` must be a function; both throw otherwise.
396
396
 
397
+ ### `records` as a block — a CRUD collection inside any screen
398
+
399
+ The full records machinery (list, add-then-edit, detail, search, summary,
400
+ compute, items, row actions) is also a **block**, embeddable wherever panel
401
+ blocks go — a workspace pane, a document, a tab. Declare it with the same
402
+ `records: {...}` spec plus one extra key: **`key`**, naming where the
403
+ collection lives in the enclosing data pool (default `"records"`; two
404
+ records blocks need distinct keys).
405
+
406
+ ```js
407
+ // A task list per project: records INSIDE documents — each document gets
408
+ // its own independent collection automatically.
409
+ ArtifactKit.createApp({
410
+ title: "Proyectos",
411
+ layout: "documents",
412
+ documents: {
413
+ label: "proyecto",
414
+ title: (d) => d.nombre || "Sin nombre",
415
+ blocks: [
416
+ { fields: [{ key: "nombre", label: "Proyecto", type: "text" }] },
417
+ { records: {
418
+ key: "tareas",
419
+ label: "tarea",
420
+ fields: [
421
+ { key: "titulo", label: "Título", type: "text", required: true },
422
+ { key: "hecha", label: "Hecha", type: "check" },
423
+ ],
424
+ summary: (all) => [{ label: "Pendientes", value: all.filter((t) => !t.hecha).length, big: true }],
425
+ }},
426
+ ],
427
+ },
428
+ })
429
+ ```
430
+
431
+ Inside a `documents` layout the collection is per document; in `panel` or
432
+ `workspace` it lives in the flat pool under `key`, where `output`/`chart`
433
+ blocks and chat tools can read and write it (`d.tareas`). Which record is
434
+ open is framework-owned view state per collection — it survives reload,
435
+ and a deleted open record falls back to the list. A records block below a
436
+ records *layout* throws (the layout already owns the collection).
437
+
397
438
  ### The `documents` layout — parallel documents, one per top tab
398
439
 
399
440
  `tabs` are views over ONE shared data pool. When the ask is "several
package/dist/index.esm.js CHANGED
@@ -1072,6 +1072,13 @@ function Summary({ spec, records }) {
1072
1072
  function ListScreen({ spec, ctx }) {
1073
1073
  const label = spec.label || "item";
1074
1074
  const [query, setQuery] = useState("");
1075
+ useEffect(() => {
1076
+ if ((ctx.data.records || []).some((r) => r && !r.id)) {
1077
+ ctx.update((d) => {
1078
+ for (const r of d.records || []) if (r && !r.id) r.id = makeId();
1079
+ });
1080
+ }
1081
+ });
1075
1082
  let records = ctx.data.records.map((r) => withComputed(spec, r));
1076
1083
  if (spec.sort) records = records.slice().sort(spec.sort);
1077
1084
  const total = records.length;
@@ -1253,6 +1260,38 @@ function ItemsEditor({ spec, ctx, record }) {
1253
1260
  )
1254
1261
  );
1255
1262
  }
1263
+ function aliasRecords(d, key) {
1264
+ if (key === "records") return d;
1265
+ return new Proxy(d, {
1266
+ get: (t, p, r) => p === "records" ? t[key] : Reflect.get(t, p, r),
1267
+ set: (t, p, v) => Reflect.set(t, p === "records" ? key : p, v),
1268
+ has: (t, p) => p === "records" ? key in t : p in t
1269
+ });
1270
+ }
1271
+ function RecordsBlock({ spec, ctx }) {
1272
+ const rspec = spec.records;
1273
+ const key = rspec.key || "records";
1274
+ const arr = Array.isArray(ctx.data[key]) ? ctx.data[key] : [];
1275
+ const stateKey = "caf:rec:" + key;
1276
+ const openId = ctx.view.tabs ? ctx.view.tabs[stateKey] : void 0;
1277
+ const scoped = {
1278
+ ...ctx,
1279
+ data: { ...ctx.data, records: arr },
1280
+ update: (fn) => ctx.update((d) => fn(aliasRecords(d, key))),
1281
+ go: (_screen, id) => ctx.setTab(stateKey, id),
1282
+ back: () => ctx.setTab(stateKey, void 0)
1283
+ };
1284
+ const record = openId ? arr.find((r) => r.id === openId) : null;
1285
+ return createElement(
1286
+ "div",
1287
+ { className: "caf-records-block" },
1288
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
1289
+ record ? createElement(DetailScreen, { key: "d", spec: rspec, ctx: scoped, record }) : [
1290
+ createElement(Summary, { key: "s", spec: rspec, records: arr.map((r) => withComputed(rspec, r)) }),
1291
+ createElement(ListScreen, { key: "l", spec: rspec, ctx: scoped })
1292
+ ]
1293
+ );
1294
+ }
1256
1295
  function RecordsLayout({ spec, ctx }) {
1257
1296
  const rspec = spec.records;
1258
1297
  if (ctx.view.screen === "record") {
@@ -1358,6 +1397,8 @@ var LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
1358
1397
  var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
1359
1398
  var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents"];
1360
1399
  var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty"];
1400
+ var RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
1401
+ var ITEMS_KEYS = ["label", "fields", "empty"];
1361
1402
  function checkFields(fields, where) {
1362
1403
  for (const f of fields || []) {
1363
1404
  if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
@@ -1419,10 +1460,10 @@ function checkBlocks(blocks) {
1419
1460
  throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
1420
1461
  }
1421
1462
  const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
1422
- const known = keys.filter((k) => BLOCK_NAMES.includes(k) || k === "tabs");
1463
+ const known = keys.filter((k) => BLOCK_NAMES.includes(k) || k === "tabs" || k === "records");
1423
1464
  if (known.length === 0) {
1424
1465
  throw new Error(
1425
- `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
1466
+ `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${[...BLOCK_NAMES, "tabs", "records"].join(", ")}.`
1426
1467
  );
1427
1468
  }
1428
1469
  if (known.length > 1) {
@@ -1435,6 +1476,7 @@ function checkBlocks(blocks) {
1435
1476
  checkFields(block.fields, `blocks[${i}]`);
1436
1477
  if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
1437
1478
  if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
1479
+ if (known[0] === "records") checkRecordsSpec(block.records, `blocks[${i}].records`, true);
1438
1480
  if (known[0] === "tabs") {
1439
1481
  const tabs = block.tabs;
1440
1482
  if (!Array.isArray(tabs) || tabs.length === 0) {
@@ -1525,12 +1567,14 @@ function validate(spec) {
1525
1567
  }
1526
1568
  checkBlocks(spec.sidebar);
1527
1569
  }
1528
- const chats = flattenBlocks([...spec.main, ...spec.sidebar || []]).filter((b) => b && b.chat);
1570
+ const wsFlat = flattenBlocks([...spec.main, ...spec.sidebar || []]);
1571
+ const chats = wsFlat.filter((b) => b && b.chat);
1529
1572
  if (chats.length > 1) {
1530
1573
  throw new Error(
1531
1574
  "claude-artifact-framework: only one chat block per app \u2014 the conversation persists under data.chat and there is exactly one."
1532
1575
  );
1533
1576
  }
1577
+ checkRecordsBlockKeys(wsFlat);
1534
1578
  return layout;
1535
1579
  }
1536
1580
  if (layout === "documents") {
@@ -1550,77 +1594,104 @@ function validate(spec) {
1550
1594
  throw new Error("claude-artifact-framework: documents.title must be a function of the document's data returning the tab label.");
1551
1595
  }
1552
1596
  checkBlocks(d.blocks);
1553
- const chats = flattenBlocks(d.blocks).filter((b) => b && b.chat);
1597
+ const docFlat = flattenBlocks(d.blocks);
1598
+ const chats = docFlat.filter((b) => b && b.chat);
1554
1599
  if (chats.length > 1) {
1555
1600
  throw new Error(
1556
1601
  "claude-artifact-framework: only one chat block per document \u2014 each document's conversation persists under its own data.chat and there is exactly one."
1557
1602
  );
1558
1603
  }
1604
+ checkRecordsBlockKeys(docFlat);
1559
1605
  return layout;
1560
1606
  }
1561
1607
  if (layout === "records") {
1562
- const r = spec.records;
1563
- if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
1608
+ checkRecordsSpec(spec.records, "records", false);
1609
+ if (spec.blocks) {
1610
+ checkBlocks(spec.blocks);
1611
+ if (flattenBlocks(spec.blocks).some((b) => b && b.records)) {
1612
+ throw new Error(
1613
+ "claude-artifact-framework: a records BLOCK below a records LAYOUT is ambiguous \u2014 the layout already owns the collection. Use panel/workspace/documents to embed records blocks."
1614
+ );
1615
+ }
1616
+ }
1617
+ return layout;
1618
+ }
1619
+ checkBlocks(spec.blocks || []);
1620
+ checkRecordsBlockKeys(flattenBlocks(spec.blocks || []));
1621
+ return layout;
1622
+ }
1623
+ function checkRecordsSpec(r, where, allowKey) {
1624
+ if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
1625
+ throw new Error(
1626
+ `claude-artifact-framework: ${where} needs \`records: { fields: [...] }\` \u2014 the same field declarations the fields block uses.`
1627
+ );
1628
+ }
1629
+ const valid = allowKey ? [...RECORDS_KEYS, "key"] : RECORDS_KEYS;
1630
+ const unknownR = Object.keys(r).filter((k) => !valid.includes(k));
1631
+ if (unknownR.length) {
1632
+ throw new Error(
1633
+ `claude-artifact-framework: ${where} has unknown keys (${unknownR.join(", ")}). Valid keys: ${valid.join(", ")}.`
1634
+ );
1635
+ }
1636
+ if (allowKey && r.key !== void 0 && (typeof r.key !== "string" || !r.key)) {
1637
+ throw new Error(`claude-artifact-framework: ${where}.key must be a non-empty string naming the collection in data.`);
1638
+ }
1639
+ checkFields(r.fields, where);
1640
+ if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
1641
+ throw new Error(
1642
+ "claude-artifact-framework: `file` fields live in the flat data pool (panel blocks or steps) \u2014 they are not supported inside records or items."
1643
+ );
1644
+ }
1645
+ const seen = /* @__PURE__ */ new Set();
1646
+ for (const f of r.fields) {
1647
+ if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
1648
+ if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate ${where} field key "${f.key}".`);
1649
+ seen.add(f.key);
1650
+ }
1651
+ if (r.actions) checkActions(r.actions, `${where}.actions`);
1652
+ if (r.items) {
1653
+ const unknownI = Object.keys(r.items).filter((k) => !ITEMS_KEYS.includes(k));
1654
+ if (unknownI.length) {
1564
1655
  throw new Error(
1565
- 'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
1656
+ `claude-artifact-framework: ${where}.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
1566
1657
  );
1567
1658
  }
1568
- const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
1569
- const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
1570
- if (unknownR.length) {
1659
+ if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
1660
+ throw new Error(`claude-artifact-framework: ${where}.items needs \`fields: [...]\` \u2014 the same field declarations as everywhere else.`);
1661
+ }
1662
+ checkFields(r.items.fields, `${where}.items`);
1663
+ for (const f of r.items.fields) {
1664
+ if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
1665
+ }
1666
+ if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
1667
+ throw new Error(`claude-artifact-framework: a ${where} field cannot be named "items" when \`items\` is declared \u2014 that key holds the child collection.`);
1668
+ }
1669
+ }
1670
+ for (const [key, c] of Object.entries(r.compute || {})) {
1671
+ if (seen.has(key) || key === "id" || r.items && key === "items") {
1571
1672
  throw new Error(
1572
- `claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
1673
+ `claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
1573
1674
  );
1574
1675
  }
1575
- checkFields(r.fields, "records");
1576
- if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
1676
+ if (typeof c !== "function" && typeof (c && c.value) !== "function") {
1577
1677
  throw new Error(
1578
- "claude-artifact-framework: `file` fields live in the flat data pool (panel blocks or steps) \u2014 they are not supported inside records or items."
1678
+ `claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
1579
1679
  );
1580
1680
  }
1581
- const seen = /* @__PURE__ */ new Set();
1582
- for (const f of r.fields) {
1583
- if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
1584
- if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
1585
- seen.add(f.key);
1586
- }
1587
- if (spec.blocks) checkBlocks(spec.blocks);
1588
- if (r.actions) checkActions(r.actions, "records.actions");
1589
- if (r.items) {
1590
- const ITEMS_KEYS = ["label", "fields", "empty"];
1591
- const unknownI = Object.keys(r.items).filter((k) => !ITEMS_KEYS.includes(k));
1592
- if (unknownI.length) {
1593
- throw new Error(
1594
- `claude-artifact-framework: records.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
1595
- );
1596
- }
1597
- if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
1598
- throw new Error("claude-artifact-framework: records.items needs `fields: [...]` \u2014 the same field declarations as everywhere else.");
1599
- }
1600
- checkFields(r.items.fields, "records.items");
1601
- for (const f of r.items.fields) {
1602
- if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
1603
- }
1604
- if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
1605
- throw new Error('claude-artifact-framework: a records field cannot be named "items" when `items` is declared \u2014 that key holds the child collection.');
1606
- }
1607
- }
1608
- for (const [key, c] of Object.entries(r.compute || {})) {
1609
- if (seen.has(key) || key === "id" || r.items && key === "items") {
1610
- throw new Error(
1611
- `claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
1612
- );
1613
- }
1614
- if (typeof c !== "function" && typeof (c && c.value) !== "function") {
1615
- throw new Error(
1616
- `claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
1617
- );
1618
- }
1681
+ }
1682
+ }
1683
+ function checkRecordsBlockKeys(flatBlocks) {
1684
+ const seen = /* @__PURE__ */ new Set();
1685
+ for (const b of flatBlocks) {
1686
+ if (!b || !b.records) continue;
1687
+ const k = b.records.key || "records";
1688
+ if (seen.has(k)) {
1689
+ throw new Error(
1690
+ `claude-artifact-framework: two records blocks share the collection key "${k}" \u2014 give each a distinct \`key\`.`
1691
+ );
1619
1692
  }
1620
- return layout;
1693
+ seen.add(k);
1621
1694
  }
1622
- checkBlocks(spec.blocks || []);
1623
- return layout;
1624
1695
  }
1625
1696
  function flattenBlocks(blocks) {
1626
1697
  const out = [];
@@ -1650,6 +1721,12 @@ function defaultsFrom(spec) {
1650
1721
  }
1651
1722
  }
1652
1723
  }
1724
+ for (const b of allBlocks) {
1725
+ if (b && b.records) {
1726
+ const k = b.records.key || "records";
1727
+ if (!Array.isArray(data[k])) data[k] = [];
1728
+ }
1729
+ }
1653
1730
  return data;
1654
1731
  }
1655
1732
  var BlockBoundary = null;
@@ -1711,8 +1788,8 @@ function TabsBlock({ spec, ctx }) {
1711
1788
  )
1712
1789
  );
1713
1790
  }
1714
- var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock };
1715
- var ALL_NAMES = [...BLOCK_NAMES, "tabs"];
1791
+ var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock, records: RecordsBlock };
1792
+ var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
1716
1793
  function Block({ block, ctx }) {
1717
1794
  if (typeof block.when === "function" && !block.when(ctx.data)) return null;
1718
1795
  const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
@@ -1795,6 +1872,10 @@ function docDefaults(dspec) {
1795
1872
  data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
1796
1873
  }
1797
1874
  }
1875
+ if (block.records) {
1876
+ const k = block.records.key || "records";
1877
+ if (!Array.isArray(data[k])) data[k] = [];
1878
+ }
1798
1879
  }
1799
1880
  return data;
1800
1881
  }
@@ -2209,6 +2290,7 @@ var BASE_CSS = `
2209
2290
  .caf-doc-close:hover { color: var(--caf-danger); }
2210
2291
  .caf-doc-add { font-size: 1rem; font-weight: 650; }
2211
2292
  .caf-empty .caf-btn { margin-top: 0.6rem; }
2293
+ .caf-records-block { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
2212
2294
  .caf-chevron-closed { transform: rotate(-90deg); }
2213
2295
  .caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
2214
2296
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }