claude-artifact-framework 0.7.3 → 0.8.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
@@ -20,7 +20,7 @@ mirrors npm packages automatically.
20
20
  ### As a global script (`<script>` tag)
21
21
 
22
22
  ```html
23
- <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.3/dist/index.global.js"></script>
23
+ <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.8.0/dist/index.global.js"></script>
24
24
  <script>
25
25
  ArtifactKit.useReact(React); // hand over the page's React once
26
26
  const App = ArtifactKit.createApp({ title: "Mi app", blocks: [ /* ... */ ] });
@@ -38,11 +38,11 @@ mirrors npm packages automatically.
38
38
  createStore,
39
39
  createPersistedStore,
40
40
  createSharedStore,
41
- } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.3/dist/index.esm.js";
41
+ } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.8.0/dist/index.esm.js";
42
42
  </script>
43
43
  ```
44
44
 
45
- Pin a version (`@0.7.3`) for reproducible artifacts, or drop the version
45
+ Pin a version (`@0.8.0`) for reproducible artifacts, or drop the version
46
46
  (`claude-artifact-framework/dist/...`) to always get the latest release.
47
47
 
48
48
  ### Inside a React artifact
@@ -388,6 +388,54 @@ The active tab and each accordion's open/closed state are framework-owned
388
388
  view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
389
389
  `when` must be a function; both throw otherwise.
390
390
 
391
+ ### External libraries, uploaded files, full-bleed custom, viewer identity
392
+
393
+ Four primitives for the custom-heavy end of the spectrum (document viewers,
394
+ canvases, analyzers):
395
+
396
+ ```js
397
+ ArtifactKit.createApp({
398
+ title: "Visor de PDF",
399
+ layout: "workspace",
400
+ libs: ["https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"],
401
+ main: [
402
+ { fields: [{ key: "doc", label: "Documento", type: "file", accept: "application/pdf" }] },
403
+ { custom: (ctx) => renderPdf(ctx.files.doc), fill: true },
404
+ ],
405
+ sidebar: [ { chat: { system: "Sos un asistente de lectura." } } ],
406
+ })
407
+ ```
408
+
409
+ - **`libs: ["https://..."]`** loads external scripts before the app mounts,
410
+ deduped by URL, with a "Loading libraries…" state and a named error block
411
+ if one fails. No hand-written `loadScript` in custom code.
412
+ - **`type: "file"`** fields hand the live `File` to your code at
413
+ `ctx.files[key]` (in memory only — the 5MB/key storage limit makes real
414
+ files unpersistable); `data[key]` gets serializable metadata
415
+ (`{ name, size, type }`) that survives reload, after which the file must
416
+ be picked again. `accept` filters the picker. File fields live in the flat
417
+ data pool (panel blocks or steps) — declaring one inside records/items
418
+ throws.
419
+ - **`fill: true`** on a `custom` block drops the card chrome so the block
420
+ owns a large scrollable surface — the workspace main-area case.
421
+ - **`ctx.viewerId`** is a stable anonymous id per viewer, always in the
422
+ personal scope. In a `shared` app it expresses "one vote per person" and
423
+ "added by" with no account system. It is available on the `ctx` that
424
+ `custom` blocks and chat tools receive; row/block actions receive it as a
425
+ third argument:
426
+
427
+ ```js
428
+ records: {
429
+ fields: [{ key: "nombre", type: "text" }],
430
+ actions: [{
431
+ label: "Votar",
432
+ when: (r, viewerId) => !(r.votantes || []).includes(viewerId),
433
+ run: (r, d, viewerId) => { r.votantes = [...(r.votantes || []), viewerId]; },
434
+ }],
435
+ subtitle: (r) => (r.votantes || []).length + " votos",
436
+ }
437
+ ```
438
+
391
439
  ## API reference
392
440
 
393
441
  Everything an app can declare, in one place.
package/dist/index.esm.js CHANGED
@@ -133,6 +133,7 @@ var Component = React && React.Component || class ReactMissing {
133
133
 
134
134
  // src/appState.js
135
135
  var DATA_KEY = "caf:data";
136
+ var VIEWER_KEY = "caf:viewer";
136
137
  var VIEW_KEY = "caf:view";
137
138
  var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null, tabs: {}, collapsed: {} };
138
139
  function isPlainObject(v) {
@@ -160,6 +161,7 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
160
161
  let data = structuredCloneish(defaults);
161
162
  let view = { ...EMPTY_VIEW };
162
163
  let hydrated = false;
164
+ let viewerId = null;
163
165
  let writes = 0;
164
166
  let persistedWrites = 0;
165
167
  const listeners = /* @__PURE__ */ new Set();
@@ -194,10 +196,18 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
194
196
  }, pollMs);
195
197
  }
196
198
  const ready = (async () => {
197
- const [storedData, storedView] = await Promise.all([
199
+ const [storedData, storedView, storedViewer] = await Promise.all([
198
200
  dataStore.get(DATA_KEY, void 0),
199
- storage.get(VIEW_KEY, void 0)
201
+ storage.get(VIEW_KEY, void 0),
202
+ storage.get(VIEWER_KEY, void 0)
200
203
  ]);
204
+ if (typeof storedViewer === "string" && storedViewer) {
205
+ viewerId = storedViewer;
206
+ } else {
207
+ viewerId = "v" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
208
+ storage.set(VIEWER_KEY, viewerId).catch?.(() => {
209
+ });
210
+ }
201
211
  if (storedData !== void 0) data = mergeDefaults(defaults, storedData);
202
212
  if (storedView !== void 0) view = { ...EMPTY_VIEW, ...storedView };
203
213
  hydrated = true;
@@ -219,6 +229,7 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
219
229
  return {
220
230
  ready,
221
231
  isHydrated: () => hydrated,
232
+ getViewerId: () => viewerId,
222
233
  getData: () => data,
223
234
  getView: () => view,
224
235
  update,
@@ -608,7 +619,7 @@ function ChatLayout({ spec, ctx }) {
608
619
  }
609
620
 
610
621
  // src/blocks.js
611
- var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
622
+ var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select", "file"];
612
623
  function formatValue(value, format) {
613
624
  if (value === null || value === void 0 || value === "") return "\u2014";
614
625
  const n = Number(value);
@@ -642,6 +653,20 @@ function Field({ field, value, onChange, data }) {
642
653
  const error = validateField(field, value);
643
654
  const id = `caf-f-${field.key}`;
644
655
  const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
656
+ if (field.type === "file") {
657
+ return createElement(
658
+ "div",
659
+ { className: "caf-field" },
660
+ createElement("label", { htmlFor: id }, field.label || field.key),
661
+ createElement("input", {
662
+ id,
663
+ type: "file",
664
+ accept: field.accept,
665
+ onChange: (e) => onChange(e.target.files && e.target.files[0] ? e.target.files[0] : null)
666
+ }),
667
+ value && value.name ? createElement("small", { className: "caf-hint" }, `${value.name} (${Math.round((value.size || 0) / 1024)} KB)${value.stale ? " \u2014 re-pick after reload" : ""}`) : field.hint ? createElement("small", { className: "caf-hint" }, field.hint) : null
668
+ );
669
+ }
645
670
  if (field.type === "check") {
646
671
  return createElement(
647
672
  "label",
@@ -700,6 +725,15 @@ function toNumber(raw) {
700
725
  const n = Number(raw);
701
726
  return Number.isFinite(n) ? n : raw;
702
727
  }
728
+ function writeField(ctx, key, field, v, setter) {
729
+ if (field.type === "file") {
730
+ ctx.files[key] = v || null;
731
+ const meta = v ? { name: v.name, size: v.size, type: v.type } : null;
732
+ ctx.update((d) => setter(d, meta));
733
+ return;
734
+ }
735
+ ctx.update((d) => setter(d, v));
736
+ }
703
737
  function FieldsBlock({ spec, ctx }) {
704
738
  const fields = spec.fields || [];
705
739
  return createElement(
@@ -712,8 +746,8 @@ function FieldsBlock({ spec, ctx }) {
712
746
  field,
713
747
  data: ctx.data,
714
748
  value: ctx.data[field.key],
715
- onChange: (v) => ctx.update((d) => {
716
- d[field.key] = v;
749
+ onChange: (v) => writeField(ctx, field.key, field, v, (d, val) => {
750
+ d[field.key] = val;
717
751
  })
718
752
  })
719
753
  )
@@ -935,7 +969,7 @@ function TimerBlock({ spec, ctx }) {
935
969
  }
936
970
  function ActionsBlock({ spec, ctx }) {
937
971
  const actions = spec.actions || [];
938
- const visible = actions.filter((a) => !a.when || a.when(ctx.data));
972
+ const visible = actions.filter((a) => !a.when || a.when(ctx.data, ctx.viewerId));
939
973
  return createElement(
940
974
  "div",
941
975
  { className: "caf-block caf-actions" },
@@ -950,7 +984,7 @@ function ActionsBlock({ spec, ctx }) {
950
984
  key: a.label,
951
985
  type: "button",
952
986
  className: a.kind === "danger" ? "caf-btn caf-btn-danger" : a.kind === "primary" ? "caf-btn caf-btn-primary" : "caf-btn",
953
- onClick: () => ctx.update((d) => a.run(d))
987
+ onClick: () => ctx.update((d) => a.run(d, ctx.viewerId))
954
988
  },
955
989
  a.label
956
990
  )
@@ -960,10 +994,11 @@ function ActionsBlock({ spec, ctx }) {
960
994
  }
961
995
  function CustomBlock({ spec, ctx }) {
962
996
  const rendered = spec.custom(ctx);
997
+ const cls = spec.fill ? "caf-block caf-block-fill" : "caf-block";
963
998
  if (typeof rendered === "string") {
964
- return createElement("div", { className: "caf-block", dangerouslySetInnerHTML: { __html: rendered } });
999
+ return createElement("div", { className: cls, dangerouslySetInnerHTML: { __html: rendered } });
965
1000
  }
966
- return createElement("div", { className: "caf-block" }, rendered);
1001
+ return createElement("div", { className: cls }, rendered);
967
1002
  }
968
1003
  var BLOCKS = {
969
1004
  fields: FieldsBlock,
@@ -1089,7 +1124,7 @@ function ListScreen({ spec, ctx }) {
1089
1124
  spec.actions ? createElement(
1090
1125
  "div",
1091
1126
  { className: "caf-row-actions" },
1092
- spec.actions.filter((a) => !a.when || a.when(rec)).map(
1127
+ spec.actions.filter((a) => !a.when || a.when(rec, ctx.viewerId)).map(
1093
1128
  (a) => createElement(
1094
1129
  "button",
1095
1130
  {
@@ -1098,7 +1133,7 @@ function ListScreen({ spec, ctx }) {
1098
1133
  className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
1099
1134
  onClick: () => ctx.update((d) => {
1100
1135
  const live = d.records.find((x) => x.id === rec.id);
1101
- if (live) a.run(live, d);
1136
+ if (live) a.run(live, d, ctx.viewerId);
1102
1137
  })
1103
1138
  },
1104
1139
  a.label
@@ -1291,8 +1326,8 @@ function StepsLayout({ spec, ctx }) {
1291
1326
  field,
1292
1327
  data: ctx.data,
1293
1328
  value: ctx.data[field.key],
1294
- onChange: (v) => ctx.update((d) => {
1295
- d[field.key] = v;
1329
+ onChange: (v) => writeField(ctx, field.key, field, v, (d, val) => {
1330
+ d[field.key] = val;
1296
1331
  })
1297
1332
  })
1298
1333
  ),
@@ -1317,8 +1352,8 @@ function StepsLayout({ spec, ctx }) {
1317
1352
 
1318
1353
  // src/app.js
1319
1354
  var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
1320
- var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible"];
1321
- var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
1355
+ 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"];
1322
1357
  function checkFields(fields, where) {
1323
1358
  for (const f of fields || []) {
1324
1359
  if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
@@ -1414,6 +1449,9 @@ function checkBlocks(blocks) {
1414
1449
  if (block.when !== void 0 && typeof block.when !== "function") {
1415
1450
  throw new Error(`claude-artifact-framework: blocks[${i}].when must be a function of data returning true/false.`);
1416
1451
  }
1452
+ if (block.fill !== void 0 && known[0] !== "custom") {
1453
+ throw new Error(`claude-artifact-framework: \`fill\` is only valid on custom blocks \u2014 blocks[${i}] is "${known[0]}".`);
1454
+ }
1417
1455
  });
1418
1456
  }
1419
1457
  function validate(spec) {
@@ -1423,6 +1461,11 @@ function validate(spec) {
1423
1461
  `claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
1424
1462
  );
1425
1463
  }
1464
+ if (spec.libs !== void 0) {
1465
+ if (!Array.isArray(spec.libs) || spec.libs.some((u) => typeof u !== "string" || !/^https?:\/\//.test(u))) {
1466
+ throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
1467
+ }
1468
+ }
1426
1469
  const layout = spec.layout || "panel";
1427
1470
  if (!LAYOUTS.includes(layout)) {
1428
1471
  throw new Error(
@@ -1501,6 +1544,11 @@ function validate(spec) {
1501
1544
  );
1502
1545
  }
1503
1546
  checkFields(r.fields, "records");
1547
+ if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
1548
+ throw new Error(
1549
+ "claude-artifact-framework: `file` fields live in the flat data pool (panel blocks or steps) \u2014 they are not supported inside records or items."
1550
+ );
1551
+ }
1504
1552
  const seen = /* @__PURE__ */ new Set();
1505
1553
  for (const f of r.fields) {
1506
1554
  if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
@@ -1704,10 +1752,30 @@ function WorkspaceLayout({ spec, ctx }) {
1704
1752
  );
1705
1753
  }
1706
1754
  var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
1755
+ function loadScript(src) {
1756
+ return new Promise((resolve, reject) => {
1757
+ const existing = document.querySelector(`script[src="${src}"]`);
1758
+ if (existing) {
1759
+ if (existing.dataset.cafReady === "1") return resolve();
1760
+ existing.addEventListener("load", resolve);
1761
+ existing.addEventListener("error", () => reject(new Error(`failed to load ${src}`)));
1762
+ return;
1763
+ }
1764
+ const tag = document.createElement("script");
1765
+ tag.src = src;
1766
+ tag.onload = () => {
1767
+ tag.dataset.cafReady = "1";
1768
+ resolve();
1769
+ };
1770
+ tag.onerror = () => reject(new Error(`failed to load ${src}`));
1771
+ document.head.appendChild(tag);
1772
+ });
1773
+ }
1707
1774
  function createApp(spec = {}) {
1708
1775
  const layout = validate(spec);
1709
1776
  const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
1710
1777
  const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
1778
+ const files = {};
1711
1779
  return function App() {
1712
1780
  if (!hasReact()) {
1713
1781
  throw new Error(
@@ -1715,6 +1783,18 @@ function createApp(spec = {}) {
1715
1783
  );
1716
1784
  }
1717
1785
  const [, force] = useState(0);
1786
+ const [libsState, setLibsState] = useState(spec.libs && spec.libs.length ? "loading" : "ready");
1787
+ const [libsError, setLibsError] = useState(null);
1788
+ useEffect(() => {
1789
+ if (!spec.libs || !spec.libs.length) return;
1790
+ Promise.all(spec.libs.map(loadScript)).then(
1791
+ () => setLibsState("ready"),
1792
+ (e) => {
1793
+ setLibsError(String(e && e.message || e));
1794
+ setLibsState("error");
1795
+ }
1796
+ );
1797
+ }, []);
1718
1798
  useEffect(() => {
1719
1799
  const unsubscribe = state.subscribe(() => force((n) => n + 1));
1720
1800
  if (state.isHydrated()) force((n) => n + 1);
@@ -1728,6 +1808,8 @@ function createApp(spec = {}) {
1728
1808
  go: state.go,
1729
1809
  back: state.back,
1730
1810
  setStep: (n) => state.setView({ step: n }),
1811
+ files,
1812
+ viewerId: state.getViewerId(),
1731
1813
  setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
1732
1814
  toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
1733
1815
  colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
@@ -1743,7 +1825,12 @@ function createApp(spec = {}) {
1743
1825
  spec.title ? createElement("h1", null, spec.title) : null,
1744
1826
  spec.subtitle ? createElement("p", null, spec.subtitle) : null
1745
1827
  ),
1746
- state.isHydrated() ? createElement(Layout, { spec, ctx }) : createElement("div", { className: "caf-block caf-loading" }, "Loading\u2026")
1828
+ libsState === "error" ? createElement(
1829
+ "div",
1830
+ { className: "caf-block caf-block-error" },
1831
+ createElement("strong", null, "A library failed to load"),
1832
+ createElement("code", null, libsError)
1833
+ ) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : createElement(Layout, { spec, ctx })
1747
1834
  );
1748
1835
  };
1749
1836
  }
@@ -1916,6 +2003,7 @@ var BASE_CSS = `
1916
2003
  .caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1917
2004
  .caf-chevron { transition: transform 0.15s ease; color: var(--caf-muted); }
1918
2005
  .caf-chevron-closed { transform: rotate(-90deg); }
2006
+ .caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
1919
2007
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
1920
2008
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
1921
2009
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }