claude-artifact-framework 0.7.1 → 0.7.2

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
@@ -12,7 +12,7 @@ mirrors npm packages automatically.
12
12
  ### As a global script (`<script>` tag)
13
13
 
14
14
  ```html
15
- <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.1/dist/index.global.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.2/dist/index.global.js"></script>
16
16
  <script>
17
17
  const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
18
18
  </script>
@@ -27,11 +27,11 @@ mirrors npm packages automatically.
27
27
  createStore,
28
28
  createPersistedStore,
29
29
  createSharedStore,
30
- } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.1/dist/index.esm.js";
30
+ } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.2/dist/index.esm.js";
31
31
  </script>
32
32
  ```
33
33
 
34
- Pin a version (`@0.7.1`) for reproducible artifacts, or drop the version
34
+ Pin a version (`@0.7.2`) for reproducible artifacts, or drop the version
35
35
  (`claude-artifact-framework/dist/...`) to always get the latest release.
36
36
 
37
37
  ### Inside a React artifact
@@ -350,6 +350,30 @@ ArtifactKit.createApp({
350
350
  block per app — the conversation persists under `data.chat` and there is
351
351
  exactly one.
352
352
 
353
+ ### Tabs, conditional blocks, and accordions
354
+
355
+ Three in-screen navigation primitives, usable in `panel`, `workspace`, and
356
+ below a `records` list alike:
357
+
358
+ ```js
359
+ blocks: [
360
+ { tabs: [
361
+ { label: "Registrar", blocks: [{ fields: [...] }] },
362
+ { label: "Resumen", blocks: [{ output: (d) => [...] }, { chart: (d) => ({...}) }] },
363
+ ], wide: true },
364
+
365
+ // `when` hides any block until the condition holds
366
+ { output: (d) => [...], when: (d) => d.records.length > 0 },
367
+
368
+ // `collapsible` needs a title; `"collapsed"` starts closed
369
+ { text: "Instrucciones…", title: "Ayuda", collapsible: "collapsed" },
370
+ ]
371
+ ```
372
+
373
+ The active tab and each accordion's open/closed state are framework-owned
374
+ view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
375
+ `when` must be a function; both throw otherwise.
376
+
353
377
  ## API reference
354
378
 
355
379
  Everything an app can declare, in one place.
@@ -363,7 +387,9 @@ list), `steps` (wizard), `chat` (conversation with Claude), `workspace`
363
387
  `records` / `steps` / `chat` / `main`+`sidebar`).
364
388
 
365
389
  **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`,
390
+ `output`, `text`, `list`, `chart`, `timer`, `actions`, `chat`, `tabs`,
391
+ `custom`. Any block also accepts `when: (d) => bool` (conditional
392
+ visibility) and `collapsible: true | "collapsed"` (accordion; needs `title`). One type per entry, plus optional `title`, `empty`,
367
393
  `wide`. In the `records` layout, `blocks` render below the list (never on the
368
394
  detail screen); their functions receive the same `data` object as everywhere
369
395
  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); }