claude-artifact-framework 0.11.0 → 0.13.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 +85 -1
- package/dist/index.esm.js +271 -103
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +15 -13
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +254 -104
- package/src/records.js +61 -1
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
|
|
@@ -427,7 +468,50 @@ close it (closing the open document falls back to a neighbour, never a
|
|
|
427
468
|
blank pane), the per-document data pools (`d` inside any block, `output`,
|
|
428
469
|
`custom`, or `when` is THAT document's data; `ctx.update` writes there),
|
|
429
470
|
the empty state, and persistence — all documents and which one is open
|
|
430
|
-
survive reload. Documents keys: `label`, `title`, `blocks`, `empty
|
|
471
|
+
survive reload. Documents keys: `label`, `title`, `blocks`, `empty`,
|
|
472
|
+
`types`.
|
|
473
|
+
|
|
474
|
+
**Typed documents** — when tabs are not all the same kind of thing, declare
|
|
475
|
+
`types` instead of `blocks` (one or the other, not both). Each type has its
|
|
476
|
+
own `label`, optional `title` function, and its own `blocks`; the **+** tab
|
|
477
|
+
asks which kind to create when there is more than one. Each document
|
|
478
|
+
remembers its type forever; an untitled document shows its type label in
|
|
479
|
+
the tab.
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
ArtifactKit.createApp({
|
|
483
|
+
title: "Cuaderno",
|
|
484
|
+
layout: "documents",
|
|
485
|
+
documents: {
|
|
486
|
+
label: "página",
|
|
487
|
+
types: {
|
|
488
|
+
nota: {
|
|
489
|
+
label: "Nota",
|
|
490
|
+
title: (d) => d.titulo || "",
|
|
491
|
+
blocks: [{ fields: [
|
|
492
|
+
{ key: "titulo", label: "Título", type: "text" },
|
|
493
|
+
{ key: "texto", label: "Texto", type: "textarea", rows: 12 },
|
|
494
|
+
]}],
|
|
495
|
+
},
|
|
496
|
+
compras: {
|
|
497
|
+
label: "Lista de compras",
|
|
498
|
+
blocks: [
|
|
499
|
+
{ records: { key: "items", label: "producto", fields: [
|
|
500
|
+
{ key: "producto", label: "Producto", type: "text", required: true },
|
|
501
|
+
{ key: "comprado", label: "Comprado", type: "check" },
|
|
502
|
+
]}},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
When the variation is smaller than "a different kind of document" — the
|
|
511
|
+
same document showing or hiding sections by its own state — prefer one
|
|
512
|
+
`blocks` set with `when:` conditions (`when: (d) => d.modo === "avanzado"`)
|
|
513
|
+
over `types`: one schema with conditional blocks keeps every document
|
|
514
|
+
migratable and searchable as the same shape.
|
|
431
515
|
|
|
432
516
|
Inside `documents`, blocks work as in `panel` — **including `chat` and
|
|
433
517
|
`file`, which become per-document**:
|
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") {
|
|
@@ -1357,7 +1396,9 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1357
1396
|
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
|
-
var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty"];
|
|
1399
|
+
var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
|
|
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,19 +1567,26 @@ function validate(spec) {
|
|
|
1525
1567
|
}
|
|
1526
1568
|
checkBlocks(spec.sidebar);
|
|
1527
1569
|
}
|
|
1528
|
-
const
|
|
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") {
|
|
1537
1581
|
const d = spec.documents;
|
|
1538
|
-
if (!d ||
|
|
1582
|
+
if (!d || typeof d !== "object" || !d.blocks && !d.types) {
|
|
1539
1583
|
throw new Error(
|
|
1540
|
-
'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }`
|
|
1584
|
+
'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }` (every document repeats the same blocks) or `documents: { types: {...} }` (each document is one of several kinds).'
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
if (d.blocks && d.types) {
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
"claude-artifact-framework: documents takes `blocks` (uniform documents) OR `types` (typed documents) \u2014 not both."
|
|
1541
1590
|
);
|
|
1542
1591
|
}
|
|
1543
1592
|
const unknownD = Object.keys(d).filter((k) => !DOCUMENTS_KEYS.includes(k));
|
|
@@ -1549,79 +1598,135 @@ function validate(spec) {
|
|
|
1549
1598
|
if (d.title !== void 0 && typeof d.title !== "function") {
|
|
1550
1599
|
throw new Error("claude-artifact-framework: documents.title must be a function of the document's data returning the tab label.");
|
|
1551
1600
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
"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
|
-
);
|
|
1558
|
-
}
|
|
1559
|
-
return layout;
|
|
1560
|
-
}
|
|
1561
|
-
if (layout === "records") {
|
|
1562
|
-
const r = spec.records;
|
|
1563
|
-
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
|
|
1566
|
-
);
|
|
1567
|
-
}
|
|
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) {
|
|
1571
|
-
throw new Error(
|
|
1572
|
-
`claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
|
|
1573
|
-
);
|
|
1574
|
-
}
|
|
1575
|
-
checkFields(r.fields, "records");
|
|
1576
|
-
if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
|
|
1577
|
-
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."
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
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) {
|
|
1601
|
+
const checkDocBlocks = (blocks, where) => {
|
|
1602
|
+
checkBlocks(blocks);
|
|
1603
|
+
const flat = flattenBlocks(blocks);
|
|
1604
|
+
if (flat.filter((b) => b && b.chat).length > 1) {
|
|
1593
1605
|
throw new Error(
|
|
1594
|
-
`claude-artifact-framework:
|
|
1606
|
+
`claude-artifact-framework: only one chat block per document (${where}) \u2014 each document's conversation persists under its own data.chat and there is exactly one.`
|
|
1595
1607
|
);
|
|
1596
1608
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1609
|
+
checkRecordsBlockKeys(flat);
|
|
1610
|
+
};
|
|
1611
|
+
if (d.blocks) {
|
|
1612
|
+
if (!Array.isArray(d.blocks) || d.blocks.length === 0) {
|
|
1613
|
+
throw new Error("claude-artifact-framework: documents.blocks must be a non-empty array of blocks.");
|
|
1599
1614
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1615
|
+
checkDocBlocks(d.blocks, "documents.blocks");
|
|
1616
|
+
} else {
|
|
1617
|
+
const names = Object.keys(d.types || {});
|
|
1618
|
+
if (!names.length) {
|
|
1619
|
+
throw new Error("claude-artifact-framework: documents.types needs at least one type, e.g. `types: { nota: { blocks: [...] } }`.");
|
|
1603
1620
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1621
|
+
for (const name of names) {
|
|
1622
|
+
const t = d.types[name];
|
|
1623
|
+
const TYPE_KEYS = ["label", "title", "blocks", "empty"];
|
|
1624
|
+
const unknownT = Object.keys(t || {}).filter((k) => !TYPE_KEYS.includes(k));
|
|
1625
|
+
if (unknownT.length) {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
`claude-artifact-framework: documents.types.${name} has unknown keys (${unknownT.join(", ")}). Valid keys: ${TYPE_KEYS.join(", ")}.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
if (!t || !Array.isArray(t.blocks) || t.blocks.length === 0) {
|
|
1631
|
+
throw new Error(`claude-artifact-framework: documents.types.${name} needs a non-empty \`blocks\` array.`);
|
|
1632
|
+
}
|
|
1633
|
+
if (t.title !== void 0 && typeof t.title !== "function") {
|
|
1634
|
+
throw new Error(`claude-artifact-framework: documents.types.${name}.title must be a function of the document's data.`);
|
|
1635
|
+
}
|
|
1636
|
+
checkDocBlocks(t.blocks, `documents.types.${name}`);
|
|
1606
1637
|
}
|
|
1607
1638
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
if (
|
|
1639
|
+
return layout;
|
|
1640
|
+
}
|
|
1641
|
+
if (layout === "records") {
|
|
1642
|
+
checkRecordsSpec(spec.records, "records", false);
|
|
1643
|
+
if (spec.blocks) {
|
|
1644
|
+
checkBlocks(spec.blocks);
|
|
1645
|
+
if (flattenBlocks(spec.blocks).some((b) => b && b.records)) {
|
|
1615
1646
|
throw new Error(
|
|
1616
|
-
|
|
1647
|
+
"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."
|
|
1617
1648
|
);
|
|
1618
1649
|
}
|
|
1619
1650
|
}
|
|
1620
1651
|
return layout;
|
|
1621
1652
|
}
|
|
1622
1653
|
checkBlocks(spec.blocks || []);
|
|
1654
|
+
checkRecordsBlockKeys(flattenBlocks(spec.blocks || []));
|
|
1623
1655
|
return layout;
|
|
1624
1656
|
}
|
|
1657
|
+
function checkRecordsSpec(r, where, allowKey) {
|
|
1658
|
+
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
1659
|
+
throw new Error(
|
|
1660
|
+
`claude-artifact-framework: ${where} needs \`records: { fields: [...] }\` \u2014 the same field declarations the fields block uses.`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
const valid = allowKey ? [...RECORDS_KEYS, "key"] : RECORDS_KEYS;
|
|
1664
|
+
const unknownR = Object.keys(r).filter((k) => !valid.includes(k));
|
|
1665
|
+
if (unknownR.length) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`claude-artifact-framework: ${where} has unknown keys (${unknownR.join(", ")}). Valid keys: ${valid.join(", ")}.`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
if (allowKey && r.key !== void 0 && (typeof r.key !== "string" || !r.key)) {
|
|
1671
|
+
throw new Error(`claude-artifact-framework: ${where}.key must be a non-empty string naming the collection in data.`);
|
|
1672
|
+
}
|
|
1673
|
+
checkFields(r.fields, where);
|
|
1674
|
+
if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
|
|
1675
|
+
throw new Error(
|
|
1676
|
+
"claude-artifact-framework: `file` fields live in the flat data pool (panel blocks or steps) \u2014 they are not supported inside records or items."
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1680
|
+
for (const f of r.fields) {
|
|
1681
|
+
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
|
|
1682
|
+
if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate ${where} field key "${f.key}".`);
|
|
1683
|
+
seen.add(f.key);
|
|
1684
|
+
}
|
|
1685
|
+
if (r.actions) checkActions(r.actions, `${where}.actions`);
|
|
1686
|
+
if (r.items) {
|
|
1687
|
+
const unknownI = Object.keys(r.items).filter((k) => !ITEMS_KEYS.includes(k));
|
|
1688
|
+
if (unknownI.length) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
`claude-artifact-framework: ${where}.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
|
|
1694
|
+
throw new Error(`claude-artifact-framework: ${where}.items needs \`fields: [...]\` \u2014 the same field declarations as everywhere else.`);
|
|
1695
|
+
}
|
|
1696
|
+
checkFields(r.items.fields, `${where}.items`);
|
|
1697
|
+
for (const f of r.items.fields) {
|
|
1698
|
+
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
|
|
1699
|
+
}
|
|
1700
|
+
if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
|
|
1701
|
+
throw new Error(`claude-artifact-framework: a ${where} field cannot be named "items" when \`items\` is declared \u2014 that key holds the child collection.`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
for (const [key, c] of Object.entries(r.compute || {})) {
|
|
1705
|
+
if (seen.has(key) || key === "id" || r.items && key === "items") {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
if (typeof c !== "function" && typeof (c && c.value) !== "function") {
|
|
1711
|
+
throw new Error(
|
|
1712
|
+
`claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function checkRecordsBlockKeys(flatBlocks) {
|
|
1718
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1719
|
+
for (const b of flatBlocks) {
|
|
1720
|
+
if (!b || !b.records) continue;
|
|
1721
|
+
const k = b.records.key || "records";
|
|
1722
|
+
if (seen.has(k)) {
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
`claude-artifact-framework: two records blocks share the collection key "${k}" \u2014 give each a distinct \`key\`.`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
seen.add(k);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1625
1730
|
function flattenBlocks(blocks) {
|
|
1626
1731
|
const out = [];
|
|
1627
1732
|
for (const b of blocks || []) {
|
|
@@ -1650,6 +1755,12 @@ function defaultsFrom(spec) {
|
|
|
1650
1755
|
}
|
|
1651
1756
|
}
|
|
1652
1757
|
}
|
|
1758
|
+
for (const b of allBlocks) {
|
|
1759
|
+
if (b && b.records) {
|
|
1760
|
+
const k = b.records.key || "records";
|
|
1761
|
+
if (!Array.isArray(data[k])) data[k] = [];
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1653
1764
|
return data;
|
|
1654
1765
|
}
|
|
1655
1766
|
var BlockBoundary = null;
|
|
@@ -1711,8 +1822,8 @@ function TabsBlock({ spec, ctx }) {
|
|
|
1711
1822
|
)
|
|
1712
1823
|
);
|
|
1713
1824
|
}
|
|
1714
|
-
var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock };
|
|
1715
|
-
var ALL_NAMES = [...BLOCK_NAMES, "tabs"];
|
|
1825
|
+
var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock, records: RecordsBlock };
|
|
1826
|
+
var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
|
|
1716
1827
|
function Block({ block, ctx }) {
|
|
1717
1828
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
1718
1829
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
@@ -1795,6 +1906,10 @@ function docDefaults(dspec) {
|
|
|
1795
1906
|
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
1796
1907
|
}
|
|
1797
1908
|
}
|
|
1909
|
+
if (block.records) {
|
|
1910
|
+
const k = block.records.key || "records";
|
|
1911
|
+
if (!Array.isArray(data[k])) data[k] = [];
|
|
1912
|
+
}
|
|
1798
1913
|
}
|
|
1799
1914
|
return data;
|
|
1800
1915
|
}
|
|
@@ -1802,7 +1917,7 @@ function docId() {
|
|
|
1802
1917
|
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1803
1918
|
}
|
|
1804
1919
|
var DOC_TAB_KEY = "caf:doc";
|
|
1805
|
-
function docTitle(dspec, data) {
|
|
1920
|
+
function docTitle(dspec, data, untitled) {
|
|
1806
1921
|
if (dspec.title) {
|
|
1807
1922
|
try {
|
|
1808
1923
|
const t = dspec.title(data);
|
|
@@ -1815,25 +1930,36 @@ function docTitle(dspec, data) {
|
|
|
1815
1930
|
if (!f.type || f.type === "text" || f.type === "textarea" || f.type === "select") {
|
|
1816
1931
|
const v = data[f.key];
|
|
1817
1932
|
if (v !== "" && v !== void 0 && v !== null) return String(v);
|
|
1818
|
-
return "(untitled)";
|
|
1933
|
+
return untitled || "(untitled)";
|
|
1819
1934
|
}
|
|
1820
1935
|
}
|
|
1821
1936
|
}
|
|
1822
|
-
return "(untitled)";
|
|
1937
|
+
return untitled || "(untitled)";
|
|
1823
1938
|
}
|
|
1824
1939
|
function DocumentsLayout({ spec, ctx }) {
|
|
1825
1940
|
const dspec = spec.documents;
|
|
1826
1941
|
const label = dspec.label || "document";
|
|
1827
1942
|
const docs = Array.isArray(ctx.data.docs) ? ctx.data.docs : [];
|
|
1828
|
-
const
|
|
1943
|
+
const types = dspec.types || null;
|
|
1944
|
+
const typeNames = types ? Object.keys(types) : [];
|
|
1945
|
+
const specFor = (doc) => types ? types[doc.type] : dspec;
|
|
1946
|
+
const typeLabel = (name) => types[name] && types[name].label || name;
|
|
1947
|
+
const [picking, setPicking] = useState(false);
|
|
1829
1948
|
const storedId = ctx.view.tabs ? ctx.view.tabs[DOC_TAB_KEY] : void 0;
|
|
1830
1949
|
const active = docs.find((doc) => doc.id === storedId) || docs[0];
|
|
1831
|
-
function add() {
|
|
1832
|
-
const
|
|
1950
|
+
function add(typeName) {
|
|
1951
|
+
const tspec = types ? types[typeName] : dspec;
|
|
1952
|
+
const doc = { id: docId(), ...types ? { type: typeName } : {}, data: docDefaults(tspec) };
|
|
1833
1953
|
ctx.update((d) => {
|
|
1834
1954
|
d.docs = [...d.docs || [], doc];
|
|
1835
1955
|
});
|
|
1836
1956
|
ctx.setTab(DOC_TAB_KEY, doc.id);
|
|
1957
|
+
setPicking(false);
|
|
1958
|
+
}
|
|
1959
|
+
function onAdd() {
|
|
1960
|
+
if (!types) return add();
|
|
1961
|
+
if (typeNames.length === 1) return add(typeNames[0]);
|
|
1962
|
+
setPicking((p) => !p);
|
|
1837
1963
|
}
|
|
1838
1964
|
function close(id) {
|
|
1839
1965
|
const idx = docs.findIndex((doc) => doc.id === id);
|
|
@@ -1846,6 +1972,13 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1846
1972
|
if (next) ctx.setTab(DOC_TAB_KEY, next.id);
|
|
1847
1973
|
}
|
|
1848
1974
|
}
|
|
1975
|
+
const typePicker = types && picking ? createElement(
|
|
1976
|
+
"div",
|
|
1977
|
+
{ className: "caf-doc-typemenu" },
|
|
1978
|
+
typeNames.map(
|
|
1979
|
+
(name) => createElement("button", { key: name, type: "button", className: "caf-btn caf-btn-sm", onClick: () => add(name) }, typeLabel(name))
|
|
1980
|
+
)
|
|
1981
|
+
) : null;
|
|
1849
1982
|
if (!active) {
|
|
1850
1983
|
return createElement(
|
|
1851
1984
|
"div",
|
|
@@ -1854,10 +1987,32 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1854
1987
|
"div",
|
|
1855
1988
|
{ className: "caf-block caf-empty" },
|
|
1856
1989
|
createElement("p", null, dspec.empty || `No ${label}s yet. Create the first one.`),
|
|
1857
|
-
createElement(
|
|
1990
|
+
types ? createElement(
|
|
1991
|
+
"div",
|
|
1992
|
+
{ className: "caf-doc-typemenu" },
|
|
1993
|
+
typeNames.map(
|
|
1994
|
+
(name) => createElement("button", { key: name, type: "button", className: "caf-btn caf-btn-primary", onClick: () => add(name) }, `New ${typeLabel(name)}`)
|
|
1995
|
+
)
|
|
1996
|
+
) : createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: () => add() }, `New ${label}`)
|
|
1858
1997
|
)
|
|
1859
1998
|
);
|
|
1860
1999
|
}
|
|
2000
|
+
const aspec = specFor(active);
|
|
2001
|
+
if (!aspec) {
|
|
2002
|
+
return createElement(
|
|
2003
|
+
"div",
|
|
2004
|
+
{ className: "caf-documents" },
|
|
2005
|
+
createElement(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
2006
|
+
typePicker,
|
|
2007
|
+
createElement(
|
|
2008
|
+
"div",
|
|
2009
|
+
{ className: "caf-block caf-block-error" },
|
|
2010
|
+
createElement("strong", null, `This ${label}'s type "${active.type}" no longer exists`),
|
|
2011
|
+
createElement("code", null, `valid types: ${typeNames.join(", ")}`)
|
|
2012
|
+
)
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
const defaults = docDefaults(aspec);
|
|
1861
2016
|
const activeData = { ...defaults, ...active.data };
|
|
1862
2017
|
const scoped = {
|
|
1863
2018
|
...ctx,
|
|
@@ -1875,39 +2030,50 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1875
2030
|
return createElement(
|
|
1876
2031
|
"div",
|
|
1877
2032
|
{ className: "caf-documents" },
|
|
1878
|
-
createElement(
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
"
|
|
1905
|
-
|
|
2033
|
+
createElement(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
2034
|
+
typePicker,
|
|
2035
|
+
createElement(Panel, { spec: { blocks: aspec.blocks }, ctx: scoped })
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
function DocTabbar({ docs, active, dspec, types, close, onAdd, label, ctx }) {
|
|
2039
|
+
const tabTitle = (doc) => {
|
|
2040
|
+
const tspec = types ? types[doc.type] : dspec;
|
|
2041
|
+
if (!tspec) return doc.type;
|
|
2042
|
+
const merged = { ...docDefaults(tspec), ...doc.data };
|
|
2043
|
+
const fallback = types ? tspec.label || doc.type : void 0;
|
|
2044
|
+
return docTitle(tspec, merged, fallback);
|
|
2045
|
+
};
|
|
2046
|
+
return createElement(
|
|
2047
|
+
"div",
|
|
2048
|
+
{ className: "caf-tabbar", role: "tablist" },
|
|
2049
|
+
docs.map(
|
|
2050
|
+
(doc) => createElement(
|
|
2051
|
+
"span",
|
|
2052
|
+
{ key: doc.id, className: "caf-doc-tab" },
|
|
2053
|
+
createElement(
|
|
2054
|
+
"button",
|
|
2055
|
+
{
|
|
2056
|
+
type: "button",
|
|
2057
|
+
role: "tab",
|
|
2058
|
+
"aria-selected": doc.id === active.id,
|
|
2059
|
+
className: doc.id === active.id ? "caf-tab caf-tab-active" : "caf-tab",
|
|
2060
|
+
onClick: () => ctx.setTab(DOC_TAB_KEY, doc.id)
|
|
2061
|
+
},
|
|
2062
|
+
tabTitle(doc)
|
|
2063
|
+
),
|
|
2064
|
+
createElement(
|
|
2065
|
+
"button",
|
|
2066
|
+
{
|
|
2067
|
+
type: "button",
|
|
2068
|
+
className: "caf-doc-close",
|
|
2069
|
+
"aria-label": `Close ${label}`,
|
|
2070
|
+
onClick: () => close(doc.id)
|
|
2071
|
+
},
|
|
2072
|
+
"\u2715"
|
|
1906
2073
|
)
|
|
1907
|
-
)
|
|
1908
|
-
createElement("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: add }, "+")
|
|
2074
|
+
)
|
|
1909
2075
|
),
|
|
1910
|
-
createElement(
|
|
2076
|
+
createElement("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: onAdd }, "+")
|
|
1911
2077
|
);
|
|
1912
2078
|
}
|
|
1913
2079
|
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout, documents: DocumentsLayout };
|
|
@@ -2209,6 +2375,8 @@ var BASE_CSS = `
|
|
|
2209
2375
|
.caf-doc-close:hover { color: var(--caf-danger); }
|
|
2210
2376
|
.caf-doc-add { font-size: 1rem; font-weight: 650; }
|
|
2211
2377
|
.caf-empty .caf-btn { margin-top: 0.6rem; }
|
|
2378
|
+
.caf-records-block { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
2379
|
+
.caf-doc-typemenu { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
2212
2380
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
2213
2381
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
2214
2382
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|