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 CHANGED
@@ -1,8 +1,16 @@
1
1
  # claude-artifact-framework
2
2
 
3
- Utilities for building apps inside Claude Artifacts: platform storage, reactive
4
- state, and other primitives verified against the real runtime. Zero
5
- dependencies, no build step drop it into an artifact straight from a CDN.
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 copilotand 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.1/dist/index.global.js"></script>
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.1/dist/index.esm.js";
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.1`) for reproducible artifacts, or drop the version
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`, `data`, `shared`. Anything else throws.
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`, `custom`. One type per entry, plus optional `title`, `empty`,
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
- const name = Object.keys(block).find((k) => BLOCK_NAMES.includes(k));
1577
- const Component2 = BLOCKS[name];
1578
- return createElement(boundary(ctx.React), null, createElement(Component2, { spec: block, ctx }));
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); }