claude-artifact-framework 0.10.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 +74 -3
- package/dist/index.esm.js +148 -65
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +14 -13
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +118 -65
- 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
|
|
@@ -428,9 +469,39 @@ 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
471
|
survive reload. Documents keys: `label`, `title`, `blocks`, `empty`.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
472
|
+
|
|
473
|
+
Inside `documents`, blocks work as in `panel` — **including `chat` and
|
|
474
|
+
`file`, which become per-document**:
|
|
475
|
+
|
|
476
|
+
- A `chat` block inside `documents` gives **each document its own
|
|
477
|
+
conversation**, persisted with that document (`data.chat` is scoped). Its
|
|
478
|
+
`system` function receives the document's data, so the copilot talks
|
|
479
|
+
about THIS document. One chat block per document (a second throws).
|
|
480
|
+
- A `file` field inside `documents` scopes `ctx.files[key]` to the active
|
|
481
|
+
document — two documents never collide on the same field key. As
|
|
482
|
+
everywhere, the live `File` is memory-only: metadata survives reload,
|
|
483
|
+
the file is re-picked per document.
|
|
484
|
+
|
|
485
|
+
That combination — one file plus one conversation per tab — is the
|
|
486
|
+
multi-document annotator shape (PDFs, images, CSVs) declared in full:
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
ArtifactKit.createApp({
|
|
490
|
+
title: "Anotador de archivos",
|
|
491
|
+
layout: "documents",
|
|
492
|
+
documents: {
|
|
493
|
+
label: "archivo",
|
|
494
|
+
title: (d) => (d.archivo && d.archivo.name) || "Sin archivo",
|
|
495
|
+
blocks: [
|
|
496
|
+
{ fields: [{ key: "archivo", label: "Archivo", type: "file" }] },
|
|
497
|
+
{ custom: (ctx) => { /* render ctx.files.archivo */ }, fill: true },
|
|
498
|
+
{ title: "Copiloto", chat: {
|
|
499
|
+
system: (d) => "Ayudás con el archivo " + ((d.archivo && d.archivo.name) || "(ninguno)"),
|
|
500
|
+
}},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
```
|
|
434
505
|
|
|
435
506
|
### External libraries, uploaded files, full-bleed custom, viewer identity
|
|
436
507
|
|
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
|
|
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,82 +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
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
'claude-artifact-framework: a chat block inside "documents" is not supported \u2014 the conversation store is app-wide, not per document.'
|
|
1557
|
-
);
|
|
1558
|
-
}
|
|
1559
|
-
if (flat.some((b) => (b.fields || []).some((f) => f.type === "file"))) {
|
|
1597
|
+
const docFlat = flattenBlocks(d.blocks);
|
|
1598
|
+
const chats = docFlat.filter((b) => b && b.chat);
|
|
1599
|
+
if (chats.length > 1) {
|
|
1560
1600
|
throw new Error(
|
|
1561
|
-
"claude-artifact-framework:
|
|
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."
|
|
1562
1602
|
);
|
|
1563
1603
|
}
|
|
1604
|
+
checkRecordsBlockKeys(docFlat);
|
|
1564
1605
|
return layout;
|
|
1565
1606
|
}
|
|
1566
1607
|
if (layout === "records") {
|
|
1567
|
-
|
|
1568
|
-
if (
|
|
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) {
|
|
1569
1655
|
throw new Error(
|
|
1570
|
-
|
|
1656
|
+
`claude-artifact-framework: ${where}.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
|
|
1571
1657
|
);
|
|
1572
1658
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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") {
|
|
1576
1672
|
throw new Error(
|
|
1577
|
-
`claude-artifact-framework:
|
|
1673
|
+
`claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
|
|
1578
1674
|
);
|
|
1579
1675
|
}
|
|
1580
|
-
|
|
1581
|
-
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") {
|
|
1582
1677
|
throw new Error(
|
|
1583
|
-
|
|
1678
|
+
`claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
|
|
1584
1679
|
);
|
|
1585
1680
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
if (unknownI.length) {
|
|
1598
|
-
throw new Error(
|
|
1599
|
-
`claude-artifact-framework: records.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
|
|
1600
|
-
);
|
|
1601
|
-
}
|
|
1602
|
-
if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
|
|
1603
|
-
throw new Error("claude-artifact-framework: records.items needs `fields: [...]` \u2014 the same field declarations as everywhere else.");
|
|
1604
|
-
}
|
|
1605
|
-
checkFields(r.items.fields, "records.items");
|
|
1606
|
-
for (const f of r.items.fields) {
|
|
1607
|
-
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
|
|
1608
|
-
}
|
|
1609
|
-
if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
|
|
1610
|
-
throw new Error('claude-artifact-framework: a records field cannot be named "items" when `items` is declared \u2014 that key holds the child collection.');
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
for (const [key, c] of Object.entries(r.compute || {})) {
|
|
1614
|
-
if (seen.has(key) || key === "id" || r.items && key === "items") {
|
|
1615
|
-
throw new Error(
|
|
1616
|
-
`claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
|
|
1617
|
-
);
|
|
1618
|
-
}
|
|
1619
|
-
if (typeof c !== "function" && typeof (c && c.value) !== "function") {
|
|
1620
|
-
throw new Error(
|
|
1621
|
-
`claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
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
|
+
);
|
|
1624
1692
|
}
|
|
1625
|
-
|
|
1693
|
+
seen.add(k);
|
|
1626
1694
|
}
|
|
1627
|
-
checkBlocks(spec.blocks || []);
|
|
1628
|
-
return layout;
|
|
1629
1695
|
}
|
|
1630
1696
|
function flattenBlocks(blocks) {
|
|
1631
1697
|
const out = [];
|
|
@@ -1655,6 +1721,12 @@ function defaultsFrom(spec) {
|
|
|
1655
1721
|
}
|
|
1656
1722
|
}
|
|
1657
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
|
+
}
|
|
1658
1730
|
return data;
|
|
1659
1731
|
}
|
|
1660
1732
|
var BlockBoundary = null;
|
|
@@ -1716,8 +1788,8 @@ function TabsBlock({ spec, ctx }) {
|
|
|
1716
1788
|
)
|
|
1717
1789
|
);
|
|
1718
1790
|
}
|
|
1719
|
-
var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock };
|
|
1720
|
-
var ALL_NAMES = [...BLOCK_NAMES, "tabs"];
|
|
1791
|
+
var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock, records: RecordsBlock };
|
|
1792
|
+
var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
|
|
1721
1793
|
function Block({ block, ctx }) {
|
|
1722
1794
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
1723
1795
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
@@ -1800,6 +1872,10 @@ function docDefaults(dspec) {
|
|
|
1800
1872
|
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
1801
1873
|
}
|
|
1802
1874
|
}
|
|
1875
|
+
if (block.records) {
|
|
1876
|
+
const k = block.records.key || "records";
|
|
1877
|
+
if (!Array.isArray(data[k])) data[k] = [];
|
|
1878
|
+
}
|
|
1803
1879
|
}
|
|
1804
1880
|
return data;
|
|
1805
1881
|
}
|
|
@@ -1845,6 +1921,7 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1845
1921
|
ctx.update((d) => {
|
|
1846
1922
|
d.docs = d.docs.filter((doc) => doc.id !== id);
|
|
1847
1923
|
});
|
|
1924
|
+
delete ctx.files["doc:" + id];
|
|
1848
1925
|
if (active && active.id === id) {
|
|
1849
1926
|
const next = docs[idx + 1] || docs[idx - 1];
|
|
1850
1927
|
if (next) ctx.setTab(DOC_TAB_KEY, next.id);
|
|
@@ -1869,7 +1946,12 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1869
1946
|
update: (fn) => ctx.update((d) => {
|
|
1870
1947
|
const doc = (d.docs || []).find((x) => x.id === active.id);
|
|
1871
1948
|
if (doc) fn(doc.data);
|
|
1872
|
-
})
|
|
1949
|
+
}),
|
|
1950
|
+
// Live File objects scoped per document: each doc gets its own sub-map,
|
|
1951
|
+
// so two documents with the same file field never collide. Like the flat
|
|
1952
|
+
// pool, the File itself is memory-only — metadata survives reload, the
|
|
1953
|
+
// file is re-picked per document.
|
|
1954
|
+
files: ctx.files["doc:" + active.id] = ctx.files["doc:" + active.id] || {}
|
|
1873
1955
|
};
|
|
1874
1956
|
return createElement(
|
|
1875
1957
|
"div",
|
|
@@ -2208,6 +2290,7 @@ var BASE_CSS = `
|
|
|
2208
2290
|
.caf-doc-close:hover { color: var(--caf-danger); }
|
|
2209
2291
|
.caf-doc-add { font-size: 1rem; font-weight: 650; }
|
|
2210
2292
|
.caf-empty .caf-btn { margin-top: 0.6rem; }
|
|
2293
|
+
.caf-records-block { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
2211
2294
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
2212
2295
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
2213
2296
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|