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 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 || !Array.isArray(d.blocks) || d.blocks.length === 0) {
1582
+ if (!d || typeof d !== "object" || !d.blocks && !d.types) {
1583
1583
  throw new Error(
1584
- 'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }` \u2014 the blocks every document repeats, each with its own data.'
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
- checkBlocks(d.blocks);
1597
- const docFlat = flattenBlocks(d.blocks);
1598
- const chats = docFlat.filter((b) => b && b.chat);
1599
- if (chats.length > 1) {
1600
- throw new Error(
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."
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 defaults = docDefaults(dspec);
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 doc = { id: docId(), data: docDefaults(dspec) };
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("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `New ${label}`)
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
- "div",
1961
- { className: "caf-tabbar", role: "tablist" },
1962
- docs.map(
1963
- (doc) => createElement(
1964
- "span",
1965
- { key: doc.id, className: "caf-doc-tab" },
1966
- createElement(
1967
- "button",
1968
- {
1969
- type: "button",
1970
- role: "tab",
1971
- "aria-selected": doc.id === active.id,
1972
- className: doc.id === active.id ? "caf-tab caf-tab-active" : "caf-tab",
1973
- onClick: () => ctx.setTab(DOC_TAB_KEY, doc.id)
1974
- },
1975
- docTitle(dspec, { ...defaults, ...doc.data })
1976
- ),
1977
- createElement(
1978
- "button",
1979
- {
1980
- type: "button",
1981
- className: "caf-doc-close",
1982
- "aria-label": `Close ${label}`,
1983
- onClick: () => close(doc.id)
1984
- },
1985
- "\u2715"
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(Panel, { spec: { blocks: dspec.blocks }, ctx: scoped })
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; }