claude-artifact-framework 0.12.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 +44 -1
- package/dist/index.esm.js +135 -49
- 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 +150 -49
package/README.md
CHANGED
|
@@ -468,7 +468,50 @@ close it (closing the open document falls back to a neighbour, never a
|
|
|
468
468
|
blank pane), the per-document data pools (`d` inside any block, `output`,
|
|
469
469
|
`custom`, or `when` is THAT document's data; `ctx.update` writes there),
|
|
470
470
|
the empty state, and persistence — all documents and which one is open
|
|
471
|
-
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.
|
|
472
515
|
|
|
473
516
|
Inside `documents`, blocks work as in `panel` — **including `chat` and
|
|
474
517
|
`file`, which become per-document**:
|
package/dist/index.esm.js
CHANGED
|
@@ -1396,7 +1396,7 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1396
1396
|
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
|
|
1397
1397
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
|
|
1398
1398
|
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents"];
|
|
1399
|
-
var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty"];
|
|
1399
|
+
var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
|
|
1400
1400
|
var RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
|
|
1401
1401
|
var ITEMS_KEYS = ["label", "fields", "empty"];
|
|
1402
1402
|
function checkFields(fields, where) {
|
|
@@ -1579,9 +1579,14 @@ function validate(spec) {
|
|
|
1579
1579
|
}
|
|
1580
1580
|
if (layout === "documents") {
|
|
1581
1581
|
const d = spec.documents;
|
|
1582
|
-
if (!d ||
|
|
1582
|
+
if (!d || typeof d !== "object" || !d.blocks && !d.types) {
|
|
1583
1583
|
throw new Error(
|
|
1584
|
-
'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."
|
|
1585
1590
|
);
|
|
1586
1591
|
}
|
|
1587
1592
|
const unknownD = Object.keys(d).filter((k) => !DOCUMENTS_KEYS.includes(k));
|
|
@@ -1593,15 +1598,44 @@ function validate(spec) {
|
|
|
1593
1598
|
if (d.title !== void 0 && typeof d.title !== "function") {
|
|
1594
1599
|
throw new Error("claude-artifact-framework: documents.title must be a function of the document's data returning the tab label.");
|
|
1595
1600
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1601
|
+
const checkDocBlocks = (blocks, where) => {
|
|
1602
|
+
checkBlocks(blocks);
|
|
1603
|
+
const flat = flattenBlocks(blocks);
|
|
1604
|
+
if (flat.filter((b) => b && b.chat).length > 1) {
|
|
1605
|
+
throw new Error(
|
|
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.`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
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.");
|
|
1614
|
+
}
|
|
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: [...] } }`.");
|
|
1620
|
+
}
|
|
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}`);
|
|
1637
|
+
}
|
|
1603
1638
|
}
|
|
1604
|
-
checkRecordsBlockKeys(docFlat);
|
|
1605
1639
|
return layout;
|
|
1606
1640
|
}
|
|
1607
1641
|
if (layout === "records") {
|
|
@@ -1883,7 +1917,7 @@ function docId() {
|
|
|
1883
1917
|
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1884
1918
|
}
|
|
1885
1919
|
var DOC_TAB_KEY = "caf:doc";
|
|
1886
|
-
function docTitle(dspec, data) {
|
|
1920
|
+
function docTitle(dspec, data, untitled) {
|
|
1887
1921
|
if (dspec.title) {
|
|
1888
1922
|
try {
|
|
1889
1923
|
const t = dspec.title(data);
|
|
@@ -1896,25 +1930,36 @@ function docTitle(dspec, data) {
|
|
|
1896
1930
|
if (!f.type || f.type === "text" || f.type === "textarea" || f.type === "select") {
|
|
1897
1931
|
const v = data[f.key];
|
|
1898
1932
|
if (v !== "" && v !== void 0 && v !== null) return String(v);
|
|
1899
|
-
return "(untitled)";
|
|
1933
|
+
return untitled || "(untitled)";
|
|
1900
1934
|
}
|
|
1901
1935
|
}
|
|
1902
1936
|
}
|
|
1903
|
-
return "(untitled)";
|
|
1937
|
+
return untitled || "(untitled)";
|
|
1904
1938
|
}
|
|
1905
1939
|
function DocumentsLayout({ spec, ctx }) {
|
|
1906
1940
|
const dspec = spec.documents;
|
|
1907
1941
|
const label = dspec.label || "document";
|
|
1908
1942
|
const docs = Array.isArray(ctx.data.docs) ? ctx.data.docs : [];
|
|
1909
|
-
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);
|
|
1910
1948
|
const storedId = ctx.view.tabs ? ctx.view.tabs[DOC_TAB_KEY] : void 0;
|
|
1911
1949
|
const active = docs.find((doc) => doc.id === storedId) || docs[0];
|
|
1912
|
-
function add() {
|
|
1913
|
-
const
|
|
1950
|
+
function add(typeName) {
|
|
1951
|
+
const tspec = types ? types[typeName] : dspec;
|
|
1952
|
+
const doc = { id: docId(), ...types ? { type: typeName } : {}, data: docDefaults(tspec) };
|
|
1914
1953
|
ctx.update((d) => {
|
|
1915
1954
|
d.docs = [...d.docs || [], doc];
|
|
1916
1955
|
});
|
|
1917
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);
|
|
1918
1963
|
}
|
|
1919
1964
|
function close(id) {
|
|
1920
1965
|
const idx = docs.findIndex((doc) => doc.id === id);
|
|
@@ -1927,6 +1972,13 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1927
1972
|
if (next) ctx.setTab(DOC_TAB_KEY, next.id);
|
|
1928
1973
|
}
|
|
1929
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;
|
|
1930
1982
|
if (!active) {
|
|
1931
1983
|
return createElement(
|
|
1932
1984
|
"div",
|
|
@@ -1935,10 +1987,32 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1935
1987
|
"div",
|
|
1936
1988
|
{ className: "caf-block caf-empty" },
|
|
1937
1989
|
createElement("p", null, dspec.empty || `No ${label}s yet. Create the first one.`),
|
|
1938
|
-
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}`)
|
|
1997
|
+
)
|
|
1998
|
+
);
|
|
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(", ")}`)
|
|
1939
2012
|
)
|
|
1940
2013
|
);
|
|
1941
2014
|
}
|
|
2015
|
+
const defaults = docDefaults(aspec);
|
|
1942
2016
|
const activeData = { ...defaults, ...active.data };
|
|
1943
2017
|
const scoped = {
|
|
1944
2018
|
...ctx,
|
|
@@ -1956,39 +2030,50 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
1956
2030
|
return createElement(
|
|
1957
2031
|
"div",
|
|
1958
2032
|
{ className: "caf-documents" },
|
|
1959
|
-
createElement(
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
"
|
|
1986
|
-
|
|
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"
|
|
1987
2073
|
)
|
|
1988
|
-
)
|
|
1989
|
-
createElement("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: add }, "+")
|
|
2074
|
+
)
|
|
1990
2075
|
),
|
|
1991
|
-
createElement(
|
|
2076
|
+
createElement("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: onAdd }, "+")
|
|
1992
2077
|
);
|
|
1993
2078
|
}
|
|
1994
2079
|
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout, documents: DocumentsLayout };
|
|
@@ -2291,6 +2376,7 @@ var BASE_CSS = `
|
|
|
2291
2376
|
.caf-doc-add { font-size: 1rem; font-weight: 650; }
|
|
2292
2377
|
.caf-empty .caf-btn { margin-top: 0.6rem; }
|
|
2293
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; }
|
|
2294
2380
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
2295
2381
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
2296
2382
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|