claude-artifact-framework 0.7.1 → 0.7.3
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 +52 -8
- package/dist/index.esm.js +116 -8
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +34 -13
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +126 -7
- package/src/appState.js +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# claude-artifact-framework
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A declarative framework for building apps inside Claude Artifacts: declare
|
|
4
|
+
what the app **is** — fields, records, steps, a chat with tools, a workspace
|
|
5
|
+
with a copilot — and the framework owns how it works. Persistence, hydration,
|
|
6
|
+
validation, empty/loading/error states, light+dark theming, navigation, and
|
|
7
|
+
the Claude tool loop are construction guarantees, not authoring tasks.
|
|
8
|
+
|
|
9
|
+
Five layouts (`panel`, `records`, `steps`, `chat`, `workspace`), ten block
|
|
10
|
+
types, tabs/conditional/collapsible composition, shared multi-viewer data,
|
|
11
|
+
and a `custom` escape hatch that keeps every framework service. Zero
|
|
12
|
+
dependencies, no build step — load it straight from a CDN. The decisions and
|
|
13
|
+
the evidence behind them live in [DESIGN.md](./DESIGN.md).
|
|
6
14
|
|
|
7
15
|
## Usage via CDN
|
|
8
16
|
|
|
@@ -12,8 +20,11 @@ mirrors npm packages automatically.
|
|
|
12
20
|
### As a global script (`<script>` tag)
|
|
13
21
|
|
|
14
22
|
```html
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.3/dist/index.global.js"></script>
|
|
16
24
|
<script>
|
|
25
|
+
ArtifactKit.useReact(React); // hand over the page's React once
|
|
26
|
+
const App = ArtifactKit.createApp({ title: "Mi app", blocks: [ /* ... */ ] });
|
|
27
|
+
// lower-level utilities are also exported:
|
|
17
28
|
const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
|
|
18
29
|
</script>
|
|
19
30
|
```
|
|
@@ -27,11 +38,11 @@ mirrors npm packages automatically.
|
|
|
27
38
|
createStore,
|
|
28
39
|
createPersistedStore,
|
|
29
40
|
createSharedStore,
|
|
30
|
-
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.
|
|
41
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.3/dist/index.esm.js";
|
|
31
42
|
</script>
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
Pin a version (`@0.7.
|
|
45
|
+
Pin a version (`@0.7.3`) for reproducible artifacts, or drop the version
|
|
35
46
|
(`claude-artifact-framework/dist/...`) to always get the latest release.
|
|
36
47
|
|
|
37
48
|
### Inside a React artifact
|
|
@@ -172,6 +183,9 @@ blocks: [
|
|
|
172
183
|
]
|
|
173
184
|
```
|
|
174
185
|
|
|
186
|
+
`timer` takes `minutes` and/or `seconds` (`{ seconds: 90 }` for a rest timer),
|
|
187
|
+
plus optional `label` and `doneText`.
|
|
188
|
+
|
|
175
189
|
`text` blocks also accept a function of data — `{ text: (d) => "Hola " + d.nombre }` —
|
|
176
190
|
with the same contract as `output`.
|
|
177
191
|
|
|
@@ -350,12 +364,37 @@ ArtifactKit.createApp({
|
|
|
350
364
|
block per app — the conversation persists under `data.chat` and there is
|
|
351
365
|
exactly one.
|
|
352
366
|
|
|
367
|
+
### Tabs, conditional blocks, and accordions
|
|
368
|
+
|
|
369
|
+
Three in-screen navigation primitives, usable in `panel`, `workspace`, and
|
|
370
|
+
below a `records` list alike:
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
blocks: [
|
|
374
|
+
{ tabs: [
|
|
375
|
+
{ label: "Registrar", blocks: [{ fields: [...] }] },
|
|
376
|
+
{ label: "Resumen", blocks: [{ output: (d) => [...] }, { chart: (d) => ({...}) }] },
|
|
377
|
+
], wide: true },
|
|
378
|
+
|
|
379
|
+
// `when` hides any block until the condition holds
|
|
380
|
+
{ output: (d) => [...], when: (d) => d.records.length > 0 },
|
|
381
|
+
|
|
382
|
+
// `collapsible` needs a title; `"collapsed"` starts closed
|
|
383
|
+
{ text: "Instrucciones…", title: "Ayuda", collapsible: "collapsed" },
|
|
384
|
+
]
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The active tab and each accordion's open/closed state are framework-owned
|
|
388
|
+
view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
|
|
389
|
+
`when` must be a function; both throw otherwise.
|
|
390
|
+
|
|
353
391
|
## API reference
|
|
354
392
|
|
|
355
393
|
Everything an app can declare, in one place.
|
|
356
394
|
|
|
357
395
|
**Top-level keys**: `title`, `subtitle`, `theme: { seed }`, `layout`, `blocks`,
|
|
358
|
-
`records`, `steps`, `chat`, `
|
|
396
|
+
`records`, `steps`, `chat`, `main`, `sidebar` (workspace panes), `data`,
|
|
397
|
+
`shared`. Anything else throws.
|
|
359
398
|
|
|
360
399
|
**Layouts** (`layout:`): `panel` (default — grid of blocks), `records` (CRUD
|
|
361
400
|
list), `steps` (wizard), `chat` (conversation with Claude), `workspace`
|
|
@@ -363,7 +402,12 @@ list), `steps` (wizard), `chat` (conversation with Claude), `workspace`
|
|
|
363
402
|
`records` / `steps` / `chat` / `main`+`sidebar`).
|
|
364
403
|
|
|
365
404
|
**Block types** (entries of `blocks`, `main`, or `sidebar`): `fields`,
|
|
366
|
-
`output`, `text`, `list`, `chart`, `timer`, `actions`, `chat`, `
|
|
405
|
+
`output`, `text`, `list`, `chart`, `timer`, `actions`, `chat`, `tabs`,
|
|
406
|
+
`custom`. Any block also accepts `when: (d) => bool` (conditional
|
|
407
|
+
visibility) and `collapsible: true | "collapsed"` (accordion; needs `title`).
|
|
408
|
+
On every block `title` is the heading string; `list` additionally accepts a
|
|
409
|
+
function as `title`, used as the per-row title (rows fall back to
|
|
410
|
+
`r.title ?? r.name ?? r.label ?? r.text`). One type per entry, plus optional `title`, `empty`,
|
|
367
411
|
`wide`. In the `records` layout, `blocks` render below the list (never on the
|
|
368
412
|
detail screen); their functions receive the same `data` object as everywhere
|
|
369
413
|
else — the collection lives at `d.records`.
|
package/dist/index.esm.js
CHANGED
|
@@ -134,7 +134,7 @@ var Component = React && React.Component || class ReactMissing {
|
|
|
134
134
|
// src/appState.js
|
|
135
135
|
var DATA_KEY = "caf:data";
|
|
136
136
|
var VIEW_KEY = "caf:view";
|
|
137
|
-
var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null };
|
|
137
|
+
var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null, tabs: {}, collapsed: {} };
|
|
138
138
|
function isPlainObject(v) {
|
|
139
139
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
140
140
|
}
|
|
@@ -1317,7 +1317,7 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1317
1317
|
|
|
1318
1318
|
// src/app.js
|
|
1319
1319
|
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
1320
|
-
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
1320
|
+
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible"];
|
|
1321
1321
|
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
|
|
1322
1322
|
function checkFields(fields, where) {
|
|
1323
1323
|
for (const f of fields || []) {
|
|
@@ -1380,7 +1380,7 @@ function checkBlocks(blocks) {
|
|
|
1380
1380
|
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
1381
1381
|
}
|
|
1382
1382
|
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
1383
|
-
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
1383
|
+
const known = keys.filter((k) => BLOCK_NAMES.includes(k) || k === "tabs");
|
|
1384
1384
|
if (known.length === 0) {
|
|
1385
1385
|
throw new Error(
|
|
1386
1386
|
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
@@ -1396,6 +1396,24 @@ function checkBlocks(blocks) {
|
|
|
1396
1396
|
checkFields(block.fields, `blocks[${i}]`);
|
|
1397
1397
|
if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
|
|
1398
1398
|
if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
|
|
1399
|
+
if (known[0] === "tabs") {
|
|
1400
|
+
const tabs = block.tabs;
|
|
1401
|
+
if (!Array.isArray(tabs) || tabs.length === 0) {
|
|
1402
|
+
throw new Error(`claude-artifact-framework: blocks[${i}].tabs needs an array of { label, blocks: [...] }.`);
|
|
1403
|
+
}
|
|
1404
|
+
tabs.forEach((t, j) => {
|
|
1405
|
+
if (!t || typeof t.label !== "string" || !Array.isArray(t.blocks) || t.blocks.length === 0) {
|
|
1406
|
+
throw new Error(`claude-artifact-framework: tabs[${j}] needs a \`label\` string and a non-empty \`blocks\` array.`);
|
|
1407
|
+
}
|
|
1408
|
+
if (t.blocks.some((b) => b && b.tabs)) {
|
|
1409
|
+
throw new Error("claude-artifact-framework: tabs cannot nest inside tabs.");
|
|
1410
|
+
}
|
|
1411
|
+
checkBlocks(t.blocks);
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
if (block.when !== void 0 && typeof block.when !== "function") {
|
|
1415
|
+
throw new Error(`claude-artifact-framework: blocks[${i}].when must be a function of data returning true/false.`);
|
|
1416
|
+
}
|
|
1399
1417
|
});
|
|
1400
1418
|
}
|
|
1401
1419
|
function validate(spec) {
|
|
@@ -1460,7 +1478,7 @@ function validate(spec) {
|
|
|
1460
1478
|
}
|
|
1461
1479
|
checkBlocks(spec.sidebar);
|
|
1462
1480
|
}
|
|
1463
|
-
const chats = [...spec.main, ...spec.sidebar || []].filter((b) => b && b.chat);
|
|
1481
|
+
const chats = flattenBlocks([...spec.main, ...spec.sidebar || []]).filter((b) => b && b.chat);
|
|
1464
1482
|
if (chats.length > 1) {
|
|
1465
1483
|
throw new Error(
|
|
1466
1484
|
"claude-artifact-framework: only one chat block per app \u2014 the conversation persists under data.chat and there is exactly one."
|
|
@@ -1527,12 +1545,20 @@ function validate(spec) {
|
|
|
1527
1545
|
checkBlocks(spec.blocks || []);
|
|
1528
1546
|
return layout;
|
|
1529
1547
|
}
|
|
1548
|
+
function flattenBlocks(blocks) {
|
|
1549
|
+
const out = [];
|
|
1550
|
+
for (const b of blocks || []) {
|
|
1551
|
+
out.push(b);
|
|
1552
|
+
if (b && Array.isArray(b.tabs)) for (const t of b.tabs) out.push(...flattenBlocks(t.blocks));
|
|
1553
|
+
}
|
|
1554
|
+
return out;
|
|
1555
|
+
}
|
|
1530
1556
|
function defaultsFrom(spec) {
|
|
1531
1557
|
const data = { ...spec.data || {} };
|
|
1532
1558
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
1533
1559
|
data.records = [];
|
|
1534
1560
|
}
|
|
1535
|
-
const allBlocks = [...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []];
|
|
1561
|
+
const allBlocks = flattenBlocks([...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []]);
|
|
1536
1562
|
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
1537
1563
|
if (hasChat && !data.chat) {
|
|
1538
1564
|
data.chat = { api: [], log: [] };
|
|
@@ -1572,10 +1598,69 @@ function boundary(React2) {
|
|
|
1572
1598
|
}
|
|
1573
1599
|
return BlockBoundary;
|
|
1574
1600
|
}
|
|
1601
|
+
function TabsBlock({ spec, ctx }) {
|
|
1602
|
+
const tabs = spec.tabs;
|
|
1603
|
+
const key = tabs.map((t) => t.label).join("|");
|
|
1604
|
+
const stored = ctx.view.tabs ? ctx.view.tabs[key] : void 0;
|
|
1605
|
+
const active = Math.min(stored === void 0 ? 0 : stored, tabs.length - 1);
|
|
1606
|
+
return createElement(
|
|
1607
|
+
"div",
|
|
1608
|
+
{ className: "caf-tabs" },
|
|
1609
|
+
createElement(
|
|
1610
|
+
"div",
|
|
1611
|
+
{ className: "caf-tabbar", role: "tablist" },
|
|
1612
|
+
tabs.map(
|
|
1613
|
+
(t, i) => createElement(
|
|
1614
|
+
"button",
|
|
1615
|
+
{
|
|
1616
|
+
key: t.label,
|
|
1617
|
+
type: "button",
|
|
1618
|
+
role: "tab",
|
|
1619
|
+
"aria-selected": i === active,
|
|
1620
|
+
className: i === active ? "caf-tab caf-tab-active" : "caf-tab",
|
|
1621
|
+
onClick: () => ctx.setTab(key, i)
|
|
1622
|
+
},
|
|
1623
|
+
t.label
|
|
1624
|
+
)
|
|
1625
|
+
)
|
|
1626
|
+
),
|
|
1627
|
+
createElement(
|
|
1628
|
+
"div",
|
|
1629
|
+
{ className: "caf-tabs-body" },
|
|
1630
|
+
tabs[active].blocks.map((b, i) => createElement(Block, { key: active + ":" + i, block: b, ctx }))
|
|
1631
|
+
)
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock };
|
|
1635
|
+
var ALL_NAMES = [...BLOCK_NAMES, "tabs"];
|
|
1575
1636
|
function Block({ block, ctx }) {
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1637
|
+
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
1638
|
+
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
1639
|
+
const Component2 = ALL_BLOCKS[name];
|
|
1640
|
+
const inner = createElement(
|
|
1641
|
+
boundary(ctx.React),
|
|
1642
|
+
null,
|
|
1643
|
+
createElement(Component2, { spec: block.collapsible ? { ...block, title: void 0 } : block, ctx })
|
|
1644
|
+
);
|
|
1645
|
+
if (!block.collapsible) return inner;
|
|
1646
|
+
const key = typeof block.title === "string" && block.title ? block.title : name;
|
|
1647
|
+
const collapsed = ctx.view.collapsed && key in ctx.view.collapsed ? ctx.view.collapsed[key] : block.collapsible === "collapsed";
|
|
1648
|
+
return createElement(
|
|
1649
|
+
"div",
|
|
1650
|
+
{ className: "caf-collapse" },
|
|
1651
|
+
createElement(
|
|
1652
|
+
"button",
|
|
1653
|
+
{
|
|
1654
|
+
type: "button",
|
|
1655
|
+
className: "caf-collapse-head",
|
|
1656
|
+
"aria-expanded": !collapsed,
|
|
1657
|
+
onClick: () => ctx.toggleCollapsed(key, !collapsed)
|
|
1658
|
+
},
|
|
1659
|
+
createElement("span", null, block.title || name),
|
|
1660
|
+
createElement("span", { className: collapsed ? "caf-chevron caf-chevron-closed" : "caf-chevron" }, "\u25BE")
|
|
1661
|
+
),
|
|
1662
|
+
collapsed ? null : createElement("div", { className: "caf-collapse-body" }, inner)
|
|
1663
|
+
);
|
|
1579
1664
|
}
|
|
1580
1665
|
function Panel({ spec, ctx }) {
|
|
1581
1666
|
return createElement(
|
|
@@ -1643,6 +1728,8 @@ function createApp(spec = {}) {
|
|
|
1643
1728
|
go: state.go,
|
|
1644
1729
|
back: state.back,
|
|
1645
1730
|
setStep: (n) => state.setView({ step: n }),
|
|
1731
|
+
setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
|
|
1732
|
+
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
1646
1733
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
|
|
1647
1734
|
};
|
|
1648
1735
|
const Layout = LAYOUT_COMPONENTS[layout];
|
|
@@ -1808,6 +1895,27 @@ var BASE_CSS = `
|
|
|
1808
1895
|
.caf-workspace { flex-direction: column; align-items: stretch; }
|
|
1809
1896
|
.caf-ws-side { width: 100%; position: static; }
|
|
1810
1897
|
}
|
|
1898
|
+
.caf-tabs { display: flex; flex-direction: column; gap: 0.8rem; min-width: 0; }
|
|
1899
|
+
.caf-tabbar { display: flex; gap: 0.2rem; border-bottom: 1px solid var(--caf-border); overflow-x: auto; }
|
|
1900
|
+
.caf-tab {
|
|
1901
|
+
font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer;
|
|
1902
|
+
padding: 0.45rem 0.8rem; border: none; background: none; color: var(--caf-muted);
|
|
1903
|
+
border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
|
|
1904
|
+
}
|
|
1905
|
+
.caf-tab:hover { color: var(--caf-text); }
|
|
1906
|
+
.caf-tab:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: -2px; }
|
|
1907
|
+
.caf-tab-active { color: var(--caf-accent); border-bottom-color: var(--caf-accent); }
|
|
1908
|
+
.caf-tabs-body { display: flex; flex-direction: column; gap: 0.8rem; }
|
|
1909
|
+
.caf-collapse { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
|
|
1910
|
+
.caf-collapse-head {
|
|
1911
|
+
font: inherit; font-size: 0.95rem; font-weight: 650; cursor: pointer;
|
|
1912
|
+
display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
|
|
1913
|
+
padding: 0.7rem 1rem; border: 1px solid var(--caf-border); border-radius: var(--caf-radius);
|
|
1914
|
+
background: var(--caf-surface); color: var(--caf-text); width: 100%; text-align: left;
|
|
1915
|
+
}
|
|
1916
|
+
.caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1917
|
+
.caf-chevron { transition: transform 0.15s ease; color: var(--caf-muted); }
|
|
1918
|
+
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
1811
1919
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1812
1920
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1813
1921
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|