claude-artifact-framework 0.9.0 → 0.10.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 +43 -1
- package/dist/index.esm.js +180 -9
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +35 -18
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +198 -7
- package/src/blocks.js +1 -1
- package/src/chat.js +4 -1
package/README.md
CHANGED
|
@@ -347,7 +347,8 @@ ArtifactKit.createApp({
|
|
|
347
347
|
{ output: (d) => [{ label: "Palabras", value: countWords(d.nota), big: true }] },
|
|
348
348
|
],
|
|
349
349
|
sidebar: [
|
|
350
|
-
{
|
|
350
|
+
{ title: "Asistente",
|
|
351
|
+
chat: {
|
|
351
352
|
system: (d) => "Sos un copiloto de escritura. Nota actual: " + (d.nota || ""),
|
|
352
353
|
tools: {
|
|
353
354
|
reescribir_nota: {
|
|
@@ -361,6 +362,9 @@ ArtifactKit.createApp({
|
|
|
361
362
|
})
|
|
362
363
|
```
|
|
363
364
|
|
|
365
|
+
Give the chat block a `title` (like every other block) — a chat card that
|
|
366
|
+
opens straight into a bubble is the one block on screen without a heading.
|
|
367
|
+
|
|
364
368
|
`main` is required; `sidebar` is optional. Both take the same blocks as
|
|
365
369
|
`panel`. The panes sit side by side and stack on narrow screens. One chat
|
|
366
370
|
block per app — the conversation persists under `data.chat` and there is
|
|
@@ -390,6 +394,44 @@ The active tab and each accordion's open/closed state are framework-owned
|
|
|
390
394
|
view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
|
|
391
395
|
`when` must be a function; both throw otherwise.
|
|
392
396
|
|
|
397
|
+
### The `documents` layout — parallel documents, one per top tab
|
|
398
|
+
|
|
399
|
+
`tabs` are views over ONE shared data pool. When the ask is "several
|
|
400
|
+
documents, each with its own state" — a multi-note editor, one budget per
|
|
401
|
+
trip, one character sheet per player — that is the `documents` layout:
|
|
402
|
+
declare the blocks once, and every document gets its own private copy of
|
|
403
|
+
their data.
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
ArtifactKit.createApp({
|
|
407
|
+
title: "Editor multi-documento",
|
|
408
|
+
layout: "documents",
|
|
409
|
+
documents: {
|
|
410
|
+
label: "documento",
|
|
411
|
+
title: (d) => d.nombre || "Sin título", // tab label, live from the doc's data
|
|
412
|
+
blocks: [
|
|
413
|
+
{ fields: [
|
|
414
|
+
{ key: "nombre", label: "Nombre", type: "text" },
|
|
415
|
+
{ key: "contenido", label: "Contenido", type: "textarea", rows: 14 },
|
|
416
|
+
]},
|
|
417
|
+
{ output: (d) => [{ label: "Palabras", value: (d.contenido || "").split(/\s+/).filter(Boolean).length }] },
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The framework owns the whole document lifecycle: the top tab bar (one tab
|
|
424
|
+
per document, live-labelled, plus a **+** tab that creates and opens a new
|
|
425
|
+
document — no draft to lose, it already exists), the **✕** on each tab to
|
|
426
|
+
close it (closing the open document falls back to a neighbour, never a
|
|
427
|
+
blank pane), the per-document data pools (`d` inside any block, `output`,
|
|
428
|
+
`custom`, or `when` is THAT document's data; `ctx.update` writes there),
|
|
429
|
+
the empty state, and persistence — all documents and which one is open
|
|
430
|
+
survive reload. Documents keys: `label`, `title`, `blocks`, `empty`.
|
|
431
|
+
Inside `documents`, blocks work as in `panel`; `chat` blocks and `file`
|
|
432
|
+
fields are not supported there (both are app-wide, not per-document) and
|
|
433
|
+
throw at creation.
|
|
434
|
+
|
|
393
435
|
### External libraries, uploaded files, full-bleed custom, viewer identity
|
|
394
436
|
|
|
395
437
|
Four primitives for the custom-heavy end of the spectrum (document viewers,
|
package/dist/index.esm.js
CHANGED
|
@@ -507,7 +507,7 @@ function Bubble({ m }) {
|
|
|
507
507
|
dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
|
|
508
508
|
});
|
|
509
509
|
}
|
|
510
|
-
function ChatPanel({ cfg, ctx }) {
|
|
510
|
+
function ChatPanel({ cfg, ctx, title }) {
|
|
511
511
|
const c = cfg;
|
|
512
512
|
const [draft, setDraft] = useState("");
|
|
513
513
|
const [busy, setBusy] = useState(false);
|
|
@@ -578,6 +578,9 @@ Continue with this information. If you have everything you need, answer in natur
|
|
|
578
578
|
createElement(
|
|
579
579
|
"div",
|
|
580
580
|
{ className: "caf-block caf-chat-log" },
|
|
581
|
+
// Every other block opens with its title label — a chat card that
|
|
582
|
+
// starts straight at a bubble was the one heading-less block on screen.
|
|
583
|
+
title ? createElement("h2", { className: "caf-block-title" }, title) : null,
|
|
581
584
|
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
|
|
582
585
|
log.map((m, i) => createElement(Bubble, { key: i, m })),
|
|
583
586
|
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
|
|
@@ -1008,7 +1011,7 @@ var BLOCKS = {
|
|
|
1008
1011
|
chart: ChartBlock,
|
|
1009
1012
|
timer: TimerBlock,
|
|
1010
1013
|
actions: ActionsBlock,
|
|
1011
|
-
chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx }),
|
|
1014
|
+
chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx, title: spec.title }),
|
|
1012
1015
|
custom: CustomBlock
|
|
1013
1016
|
};
|
|
1014
1017
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
@@ -1351,9 +1354,10 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1351
1354
|
}
|
|
1352
1355
|
|
|
1353
1356
|
// src/app.js
|
|
1354
|
-
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
1357
|
+
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
|
|
1355
1358
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
|
|
1356
|
-
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs"];
|
|
1359
|
+
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"];
|
|
1357
1361
|
function checkFields(fields, where) {
|
|
1358
1362
|
for (const f of fields || []) {
|
|
1359
1363
|
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
|
|
@@ -1529,6 +1533,36 @@ function validate(spec) {
|
|
|
1529
1533
|
}
|
|
1530
1534
|
return layout;
|
|
1531
1535
|
}
|
|
1536
|
+
if (layout === "documents") {
|
|
1537
|
+
const d = spec.documents;
|
|
1538
|
+
if (!d || !Array.isArray(d.blocks) || d.blocks.length === 0) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }` \u2014 the blocks every document repeats, each with its own data.'
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const unknownD = Object.keys(d).filter((k) => !DOCUMENTS_KEYS.includes(k));
|
|
1544
|
+
if (unknownD.length) {
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
`claude-artifact-framework: documents has unknown keys (${unknownD.join(", ")}). Valid keys: ${DOCUMENTS_KEYS.join(", ")}.`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (d.title !== void 0 && typeof d.title !== "function") {
|
|
1550
|
+
throw new Error("claude-artifact-framework: documents.title must be a function of the document's data returning the tab label.");
|
|
1551
|
+
}
|
|
1552
|
+
checkBlocks(d.blocks);
|
|
1553
|
+
const flat = flattenBlocks(d.blocks);
|
|
1554
|
+
if (flat.some((b) => b && b.chat)) {
|
|
1555
|
+
throw new Error(
|
|
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"))) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
"claude-artifact-framework: `file` fields live in the flat data pool \u2014 they are not supported inside documents (the live File cannot be scoped per document)."
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return layout;
|
|
1565
|
+
}
|
|
1532
1566
|
if (layout === "records") {
|
|
1533
1567
|
const r = spec.records;
|
|
1534
1568
|
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
@@ -1606,6 +1640,9 @@ function defaultsFrom(spec) {
|
|
|
1606
1640
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
1607
1641
|
data.records = [];
|
|
1608
1642
|
}
|
|
1643
|
+
if ((spec.layout || "panel") === "documents" && !Array.isArray(data.docs)) {
|
|
1644
|
+
data.docs = [];
|
|
1645
|
+
}
|
|
1609
1646
|
const allBlocks = flattenBlocks([...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []]);
|
|
1610
1647
|
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
1611
1648
|
if (hasChat && !data.chat) {
|
|
@@ -1755,7 +1792,124 @@ function WorkspaceLayout({ spec, ctx }) {
|
|
|
1755
1792
|
spec.sidebar && spec.sidebar.length ? createElement("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
|
|
1756
1793
|
);
|
|
1757
1794
|
}
|
|
1758
|
-
|
|
1795
|
+
function docDefaults(dspec) {
|
|
1796
|
+
const data = {};
|
|
1797
|
+
for (const block of flattenBlocks(dspec.blocks)) {
|
|
1798
|
+
for (const field of block.fields || []) {
|
|
1799
|
+
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
1800
|
+
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return data;
|
|
1805
|
+
}
|
|
1806
|
+
function docId() {
|
|
1807
|
+
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1808
|
+
}
|
|
1809
|
+
var DOC_TAB_KEY = "caf:doc";
|
|
1810
|
+
function docTitle(dspec, data) {
|
|
1811
|
+
if (dspec.title) {
|
|
1812
|
+
try {
|
|
1813
|
+
const t = dspec.title(data);
|
|
1814
|
+
if (t !== void 0 && t !== null && t !== "") return String(t);
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
for (const block of flattenBlocks(dspec.blocks)) {
|
|
1819
|
+
for (const f of block.fields || []) {
|
|
1820
|
+
if (!f.type || f.type === "text" || f.type === "textarea" || f.type === "select") {
|
|
1821
|
+
const v = data[f.key];
|
|
1822
|
+
if (v !== "" && v !== void 0 && v !== null) return String(v);
|
|
1823
|
+
return "(untitled)";
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return "(untitled)";
|
|
1828
|
+
}
|
|
1829
|
+
function DocumentsLayout({ spec, ctx }) {
|
|
1830
|
+
const dspec = spec.documents;
|
|
1831
|
+
const label = dspec.label || "document";
|
|
1832
|
+
const docs = Array.isArray(ctx.data.docs) ? ctx.data.docs : [];
|
|
1833
|
+
const defaults = docDefaults(dspec);
|
|
1834
|
+
const storedId = ctx.view.tabs ? ctx.view.tabs[DOC_TAB_KEY] : void 0;
|
|
1835
|
+
const active = docs.find((doc) => doc.id === storedId) || docs[0];
|
|
1836
|
+
function add() {
|
|
1837
|
+
const doc = { id: docId(), data: docDefaults(dspec) };
|
|
1838
|
+
ctx.update((d) => {
|
|
1839
|
+
d.docs = [...d.docs || [], doc];
|
|
1840
|
+
});
|
|
1841
|
+
ctx.setTab(DOC_TAB_KEY, doc.id);
|
|
1842
|
+
}
|
|
1843
|
+
function close(id) {
|
|
1844
|
+
const idx = docs.findIndex((doc) => doc.id === id);
|
|
1845
|
+
ctx.update((d) => {
|
|
1846
|
+
d.docs = d.docs.filter((doc) => doc.id !== id);
|
|
1847
|
+
});
|
|
1848
|
+
if (active && active.id === id) {
|
|
1849
|
+
const next = docs[idx + 1] || docs[idx - 1];
|
|
1850
|
+
if (next) ctx.setTab(DOC_TAB_KEY, next.id);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (!active) {
|
|
1854
|
+
return createElement(
|
|
1855
|
+
"div",
|
|
1856
|
+
{ className: "caf-panel caf-panel-single" },
|
|
1857
|
+
createElement(
|
|
1858
|
+
"div",
|
|
1859
|
+
{ className: "caf-block caf-empty" },
|
|
1860
|
+
createElement("p", null, dspec.empty || `No ${label}s yet. Create the first one.`),
|
|
1861
|
+
createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `New ${label}`)
|
|
1862
|
+
)
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
const activeData = { ...defaults, ...active.data };
|
|
1866
|
+
const scoped = {
|
|
1867
|
+
...ctx,
|
|
1868
|
+
data: activeData,
|
|
1869
|
+
update: (fn) => ctx.update((d) => {
|
|
1870
|
+
const doc = (d.docs || []).find((x) => x.id === active.id);
|
|
1871
|
+
if (doc) fn(doc.data);
|
|
1872
|
+
})
|
|
1873
|
+
};
|
|
1874
|
+
return createElement(
|
|
1875
|
+
"div",
|
|
1876
|
+
{ className: "caf-documents" },
|
|
1877
|
+
createElement(
|
|
1878
|
+
"div",
|
|
1879
|
+
{ className: "caf-tabbar", role: "tablist" },
|
|
1880
|
+
docs.map(
|
|
1881
|
+
(doc) => createElement(
|
|
1882
|
+
"span",
|
|
1883
|
+
{ key: doc.id, className: "caf-doc-tab" },
|
|
1884
|
+
createElement(
|
|
1885
|
+
"button",
|
|
1886
|
+
{
|
|
1887
|
+
type: "button",
|
|
1888
|
+
role: "tab",
|
|
1889
|
+
"aria-selected": doc.id === active.id,
|
|
1890
|
+
className: doc.id === active.id ? "caf-tab caf-tab-active" : "caf-tab",
|
|
1891
|
+
onClick: () => ctx.setTab(DOC_TAB_KEY, doc.id)
|
|
1892
|
+
},
|
|
1893
|
+
docTitle(dspec, { ...defaults, ...doc.data })
|
|
1894
|
+
),
|
|
1895
|
+
createElement(
|
|
1896
|
+
"button",
|
|
1897
|
+
{
|
|
1898
|
+
type: "button",
|
|
1899
|
+
className: "caf-doc-close",
|
|
1900
|
+
"aria-label": `Close ${label}`,
|
|
1901
|
+
onClick: () => close(doc.id)
|
|
1902
|
+
},
|
|
1903
|
+
"\u2715"
|
|
1904
|
+
)
|
|
1905
|
+
)
|
|
1906
|
+
),
|
|
1907
|
+
createElement("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: add }, "+")
|
|
1908
|
+
),
|
|
1909
|
+
createElement(Panel, { spec: { blocks: dspec.blocks }, ctx: scoped })
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout, documents: DocumentsLayout };
|
|
1759
1913
|
function loadScript(src) {
|
|
1760
1914
|
return new Promise((resolve, reject) => {
|
|
1761
1915
|
const existing = document.querySelector(`script[src="${src}"]`);
|
|
@@ -1884,7 +2038,9 @@ var BASE_CSS = `
|
|
|
1884
2038
|
}
|
|
1885
2039
|
.caf-block-title { margin: 0; font-size: 0.78rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em; color: var(--caf-muted); }
|
|
1886
2040
|
.caf-field { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
1887
|
-
|
|
2041
|
+
/* A full size+weight step below .caf-block-title \u2014 the uppercase alone was
|
|
2042
|
+
not enough to tell "card title" from "field label" at a glance. */
|
|
2043
|
+
.caf-field label { font-size: 0.68rem; font-weight: 500; color: var(--caf-muted); }
|
|
1888
2044
|
.caf-field input, .caf-field select, .caf-field textarea {
|
|
1889
2045
|
font: inherit; font-size: 0.95rem; padding: 0.5rem 0.6rem;
|
|
1890
2046
|
border-radius: var(--caf-control-radius); border: 1px solid var(--caf-border);
|
|
@@ -1918,7 +2074,9 @@ var BASE_CSS = `
|
|
|
1918
2074
|
.caf-row-sub { font-size: 0.78rem; color: var(--caf-muted); }
|
|
1919
2075
|
.caf-panel-single { grid-template-columns: 1fr; }
|
|
1920
2076
|
.caf-panel-single .caf-slot, .caf-panel-single > * { grid-column: span 1; }
|
|
1921
|
-
|
|
2077
|
+
/* Bottom padding matches the inter-row rhythm so the toolbar doesn't sit
|
|
2078
|
+
closer to the first row than rows sit to each other. */
|
|
2079
|
+
.caf-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.3rem 0.3rem 0.75rem; }
|
|
1922
2080
|
.caf-count { font-size: 0.8rem; color: var(--caf-muted); font-variant-numeric: tabular-nums; }
|
|
1923
2081
|
.caf-btn {
|
|
1924
2082
|
font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer;
|
|
@@ -1943,7 +2101,8 @@ var BASE_CSS = `
|
|
|
1943
2101
|
.caf-line-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
|
|
1944
2102
|
.caf-line-last { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--caf-accent); }
|
|
1945
2103
|
.caf-donut { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
|
|
1946
|
-
|
|
2104
|
+
/* -3px optical correction: a circle beside flush-left text reads indented. */
|
|
2105
|
+
.caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); margin-left: -3px; }
|
|
1947
2106
|
.caf-donut-seg { fill: none; stroke-width: 6; }
|
|
1948
2107
|
/* Capped width keeps each value NEXT TO its label in wide cards \u2014 the pair
|
|
1949
2108
|
must read as a pair regardless of container width. */
|
|
@@ -2036,7 +2195,19 @@ var BASE_CSS = `
|
|
|
2036
2195
|
.caf-collapse-head { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); }
|
|
2037
2196
|
.caf-collapse-head:hover { filter: brightness(0.97); }
|
|
2038
2197
|
.caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
2039
|
-
|
|
2198
|
+
/* Accent chevron: the one interaction cue that distinguishes a collapsible
|
|
2199
|
+
header from a static card title without breaking their shared typography. */
|
|
2200
|
+
.caf-chevron { transition: transform 0.15s ease; color: var(--caf-accent); }
|
|
2201
|
+
.caf-documents { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 1rem; }
|
|
2202
|
+
.caf-doc-tab { display: inline-flex; align-items: center; }
|
|
2203
|
+
.caf-doc-tab .caf-tab { padding-right: 0.35rem; max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
|
|
2204
|
+
.caf-doc-close {
|
|
2205
|
+
font: inherit; font-size: 0.7rem; cursor: pointer; border: none; background: none;
|
|
2206
|
+
color: var(--caf-muted); padding: 0.2rem 0.45rem 0.2rem 0.1rem; line-height: 1;
|
|
2207
|
+
}
|
|
2208
|
+
.caf-doc-close:hover { color: var(--caf-danger); }
|
|
2209
|
+
.caf-doc-add { font-size: 1rem; font-weight: 650; }
|
|
2210
|
+
.caf-empty .caf-btn { margin-top: 0.6rem; }
|
|
2040
2211
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
2041
2212
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
2042
2213
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|