claude-artifact-framework 0.15.0 → 0.17.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 +52 -4
- package/dist/index.esm.js +74 -7
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +38 -9
- package/dist/index.global.js.map +3 -3
- package/llms.txt +8 -1
- package/package.json +1 -1
- package/src/app.js +87 -6
- package/src/blocks.js +4 -1
package/llms.txt
CHANGED
|
@@ -14,7 +14,10 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
|
|
|
14
14
|
- Top-level keys: title, subtitle, theme:{seed}, layout, blocks, records,
|
|
15
15
|
steps, chat, main, sidebar, documents, data, shared, libs, machine,
|
|
16
16
|
brand:{logo, footer} (logo: emoji | inline <svg> | https URL | omit for an
|
|
17
|
-
automatic monogram)
|
|
17
|
+
automatic monogram), mobile:{nav:"bottom"} (phone: pins the first
|
|
18
|
+
top-level tabs block's bar to the bottom — declare it when one tabs
|
|
19
|
+
block with 4+ sections IS the app; a top bar overflows sideways at
|
|
20
|
+
phone width and hides the last sections)
|
|
18
21
|
- Layouts: panel (default), records, steps, chat, workspace, documents
|
|
19
22
|
- Block types: fields, output, text, list, chart, timer, actions, banner
|
|
20
23
|
({image?, title, subtitle} — no image paints a seed-derived gradient),
|
|
@@ -64,6 +67,10 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
|
|
|
64
67
|
13. Brand is zero-asset-first: monogram/gradient/emoji defaults before URLs;
|
|
65
68
|
inline <svg> over remote images; one icon style per app (emoji-first);
|
|
66
69
|
avatar: true instead of hand-built initials circles.
|
|
70
|
+
14. Column grids in custom (kanban, week views) branch on ctx.viewport
|
|
71
|
+
("mobile" < 720px | "desktop", reactive) — stack columns on mobile; half
|
|
72
|
+
the viewers are on a phone. The framework never clips (wide custom
|
|
73
|
+
content scrolls), but scrolling is the fallback, not the design.
|
|
67
74
|
|
|
68
75
|
## Canonical example
|
|
69
76
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Utilities for building apps inside Claude Artifacts: platform storage, chat tool-calling loops, and other primitives verified against the real runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
package/src/app.js
CHANGED
|
@@ -26,7 +26,8 @@ const IMAGE_KEYS = ["src", "caption", "fit", "height"];
|
|
|
26
26
|
// Frontier models still invent identifiers a few percent of the time, so an
|
|
27
27
|
// unknown name has to fail loudly and name the alternatives rather than
|
|
28
28
|
// silently rendering nothing — a blank pane is indistinguishable from a bug.
|
|
29
|
-
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine", "brand"];
|
|
29
|
+
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine", "brand", "mobile"];
|
|
30
|
+
const MOBILE_KEYS = ["nav"];
|
|
30
31
|
|
|
31
32
|
const MACHINE_KEYS = ["key", "initial", "states", "events"];
|
|
32
33
|
const MACHINE_STATE_KEYS = ["after"];
|
|
@@ -248,6 +249,18 @@ function checkMachine(m) {
|
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
// The tabs block mobile.nav moves: the first one at the top level of the
|
|
253
|
+
// panel's `blocks` or the workspace's `main`. Nested tabs never qualify —
|
|
254
|
+
// a bottom bar is app-shell navigation, not in-page structure.
|
|
255
|
+
function findNavTabs(spec) {
|
|
256
|
+
for (const list of [spec.blocks, spec.main]) {
|
|
257
|
+
if (!Array.isArray(list)) continue;
|
|
258
|
+
const found = list.find((b) => b && Array.isArray(b.tabs));
|
|
259
|
+
if (found) return found;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
function validate(spec) {
|
|
252
265
|
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
253
266
|
if (unknownTop.length) {
|
|
@@ -272,6 +285,22 @@ function validate(spec) {
|
|
|
272
285
|
throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
|
|
273
286
|
}
|
|
274
287
|
}
|
|
288
|
+
if (spec.mobile !== undefined) {
|
|
289
|
+
const unknownM = Object.keys(spec.mobile || {}).filter((k) => !MOBILE_KEYS.includes(k));
|
|
290
|
+
if (unknownM.length) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`claude-artifact-framework: mobile has unknown keys (${unknownM.join(", ")}). Valid keys: ${MOBILE_KEYS.join(", ")}.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (spec.mobile.nav !== undefined && spec.mobile.nav !== "bottom") {
|
|
296
|
+
throw new Error('claude-artifact-framework: mobile.nav only accepts "bottom" (omit `mobile` for the default top tab bar).');
|
|
297
|
+
}
|
|
298
|
+
if (spec.mobile.nav === "bottom" && !findNavTabs(spec)) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'claude-artifact-framework: mobile.nav: "bottom" moves a top-level `tabs` block\'s bar to the bottom of the phone screen — this spec has no top-level tabs block (in `blocks` or `main`) to move.'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
275
304
|
const layout = spec.layout || "panel";
|
|
276
305
|
if (!LAYOUTS.includes(layout)) {
|
|
277
306
|
throw new Error(
|
|
@@ -576,17 +605,21 @@ function boundary(React) {
|
|
|
576
605
|
|
|
577
606
|
// Tabs are defined here (not blocks.js) because they render other blocks —
|
|
578
607
|
// keeping the recursion in one file avoids an import cycle.
|
|
579
|
-
function TabsBlock({ spec, ctx }) {
|
|
608
|
+
function TabsBlock({ spec, ctx, bottomNav }) {
|
|
580
609
|
const tabs = spec.tabs;
|
|
581
610
|
const key = tabs.map((t) => t.label).join("|");
|
|
582
611
|
const stored = ctx.view.tabs ? ctx.view.tabs[key] : undefined;
|
|
583
612
|
const active = Math.min(stored === undefined ? 0 : stored, tabs.length - 1);
|
|
613
|
+
// mobile.nav: "bottom" — on a phone, this block's bar becomes a fixed
|
|
614
|
+
// bottom nav: every section stays visible and inside the thumb zone
|
|
615
|
+
// instead of hiding behind the tab bar's sideways scroll.
|
|
616
|
+
const bottom = !!bottomNav && ctx.viewport === "mobile";
|
|
584
617
|
return h(
|
|
585
618
|
"div",
|
|
586
619
|
{ className: "caf-tabs" },
|
|
587
620
|
h(
|
|
588
621
|
"div",
|
|
589
|
-
{ className: "caf-tabbar", role: "tablist" },
|
|
622
|
+
{ className: bottom ? "caf-tabbar caf-tabbar-bottom" : "caf-tabbar", role: "tablist" },
|
|
590
623
|
tabs.map((t, i) =>
|
|
591
624
|
h(
|
|
592
625
|
"button",
|
|
@@ -598,7 +631,11 @@ function TabsBlock({ spec, ctx }) {
|
|
|
598
631
|
className: i === active ? "caf-tab caf-tab-active" : "caf-tab",
|
|
599
632
|
onClick: () => ctx.setTab(key, i),
|
|
600
633
|
},
|
|
601
|
-
|
|
634
|
+
bottom && t.icon
|
|
635
|
+
? [h("span", { key: "i", className: "caf-tab-ico", "aria-hidden": true }, t.icon), h("span", { key: "l", className: "caf-tab-lbl" }, t.label)]
|
|
636
|
+
: t.icon
|
|
637
|
+
? `${t.icon} ${t.label}`
|
|
638
|
+
: t.label
|
|
602
639
|
)
|
|
603
640
|
)
|
|
604
641
|
),
|
|
@@ -618,6 +655,8 @@ function Block({ block, ctx }) {
|
|
|
618
655
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
619
656
|
// `state: "x"` — machine sugar: the block exists only in that phase.
|
|
620
657
|
if (block.state !== undefined && ctx.machine && ctx.data[ctx.machine.key || "fase"] !== block.state) return null;
|
|
658
|
+
// Identity against the original spec object — before any sugar copies it.
|
|
659
|
+
const bottomNav = ctx._navTabs !== undefined && ctx._navTabs === block;
|
|
621
660
|
// `icon` is sugar: it prefixes the title everywhere titles render.
|
|
622
661
|
if (block.icon && typeof block.title === "string") block = { ...block, title: `${block.icon} ${block.title}` };
|
|
623
662
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
@@ -625,7 +664,7 @@ function Block({ block, ctx }) {
|
|
|
625
664
|
const inner = h(
|
|
626
665
|
boundary(ctx.React),
|
|
627
666
|
null,
|
|
628
|
-
h(Component, { spec: block.collapsible ? { ...block, title: undefined } : block, ctx })
|
|
667
|
+
h(Component, { spec: block.collapsible ? { ...block, title: undefined } : block, ctx, bottomNav })
|
|
629
668
|
);
|
|
630
669
|
if (!block.collapsible) return inner;
|
|
631
670
|
// Collapsed state is framework-owned view state, keyed by the block's title
|
|
@@ -1029,6 +1068,17 @@ export function createApp(spec = {}) {
|
|
|
1029
1068
|
);
|
|
1030
1069
|
}
|
|
1031
1070
|
const [, force] = useState(0);
|
|
1071
|
+
// ctx.viewport shares the 720px breakpoint the block grid already uses,
|
|
1072
|
+
// so a custom block that branches on it flips in sync with the layout.
|
|
1073
|
+
const [narrow, setNarrow] = useState(
|
|
1074
|
+
() => typeof window !== "undefined" && window.matchMedia("(max-width: 719px)").matches
|
|
1075
|
+
);
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
const mq = window.matchMedia("(max-width: 719px)");
|
|
1078
|
+
const onChange = (e) => setNarrow(e.matches);
|
|
1079
|
+
mq.addEventListener("change", onChange);
|
|
1080
|
+
return () => mq.removeEventListener("change", onChange);
|
|
1081
|
+
}, []);
|
|
1032
1082
|
const [libsState, setLibsState] = useState(spec.libs && spec.libs.length ? "loading" : "ready");
|
|
1033
1083
|
const [libsError, setLibsError] = useState(null);
|
|
1034
1084
|
useEffect(() => {
|
|
@@ -1081,6 +1131,8 @@ export function createApp(spec = {}) {
|
|
|
1081
1131
|
setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
|
|
1082
1132
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
1083
1133
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
1134
|
+
viewport: narrow ? "mobile" : "desktop",
|
|
1135
|
+
_navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
|
|
1084
1136
|
machine: spec.machine || null,
|
|
1085
1137
|
send: spec.machine
|
|
1086
1138
|
? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload)
|
|
@@ -1118,7 +1170,7 @@ export function createApp(spec = {}) {
|
|
|
1118
1170
|
"div",
|
|
1119
1171
|
// The layout class lets shared CSS follow the active container width —
|
|
1120
1172
|
// e.g. the header must share the workspace's wider axis, not panel's.
|
|
1121
|
-
{ className: `caf-root caf-layout-${layout}` },
|
|
1173
|
+
{ className: `caf-root caf-layout-${layout}${ctx._navTabs ? " caf-has-bottomnav" : ""}` },
|
|
1122
1174
|
h("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
1123
1175
|
h(
|
|
1124
1176
|
"header",
|
|
@@ -1390,4 +1442,33 @@ const BASE_CSS = `
|
|
|
1390
1442
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1391
1443
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|
|
1392
1444
|
.caf-block-error code { font-size: 0.78rem; color: var(--caf-muted); word-break: break-word; }
|
|
1445
|
+
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
1446
|
+
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
1447
|
+
.caf-custom { overflow-x: auto; }
|
|
1448
|
+
/* mobile.nav: "bottom" — the app-shell tab bar pinned to the thumb zone.
|
|
1449
|
+
Tabs share the width evenly so every section stays visible; the root
|
|
1450
|
+
reserves space so the bar never covers the end of the content. */
|
|
1451
|
+
.caf-has-bottomnav { padding-bottom: 76px; }
|
|
1452
|
+
.caf-tabbar-bottom {
|
|
1453
|
+
position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
|
|
1454
|
+
gap: 0; margin: 0; overflow-x: visible;
|
|
1455
|
+
background: var(--caf-surface); border-top: 1px solid var(--caf-border); border-bottom: none;
|
|
1456
|
+
padding: 0.25rem 0.4rem calc(0.3rem + env(safe-area-inset-bottom, 0px));
|
|
1457
|
+
box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.07);
|
|
1458
|
+
}
|
|
1459
|
+
.caf-tabbar-bottom .caf-tab {
|
|
1460
|
+
flex: 1 1 0; min-width: 0; min-height: 48px;
|
|
1461
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
1462
|
+
gap: 0.15rem; padding: 0.25rem 0.2rem; margin-bottom: 0;
|
|
1463
|
+
font-size: 0.66rem; border-bottom: none;
|
|
1464
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
1465
|
+
}
|
|
1466
|
+
.caf-tabbar-bottom .caf-tab-ico { font-size: 1.25rem; line-height: 1; }
|
|
1467
|
+
.caf-tabbar-bottom .caf-tab-lbl { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1468
|
+
.caf-tabbar-bottom .caf-tab-active, .caf-tabbar-bottom .caf-tab-active:hover { border-bottom-color: transparent; }
|
|
1469
|
+
/* Touch devices get tappable framework controls regardless of the spec. */
|
|
1470
|
+
@media (pointer: coarse) {
|
|
1471
|
+
.caf-btn, .caf-tab, .caf-field input, .caf-field select { min-height: 44px; }
|
|
1472
|
+
.caf-btn-sm, .caf-doc-close, .caf-item-remove { min-height: 40px; }
|
|
1473
|
+
}
|
|
1393
1474
|
`;
|
package/src/blocks.js
CHANGED
|
@@ -485,7 +485,10 @@ export function CustomBlock({ spec, ctx }) {
|
|
|
485
485
|
const rendered = spec.custom(ctx);
|
|
486
486
|
// `fill: true` frees the block from the card chrome so it can own a large
|
|
487
487
|
// scrollable surface (a viewer, a canvas) — the workspace main-area case.
|
|
488
|
-
|
|
488
|
+
// caf-custom guarantees a custom block can never CLIP its content on a
|
|
489
|
+
// narrow screen: wide hand-built grids become scrollable, not amputated
|
|
490
|
+
// (measured: a week grid cut off its last column at 390px with no way in).
|
|
491
|
+
const cls = spec.fill ? "caf-block caf-block-fill caf-custom" : "caf-block caf-custom";
|
|
489
492
|
if (typeof rendered === "string") {
|
|
490
493
|
return h("div", { className: cls, dangerouslySetInnerHTML: { __html: rendered } });
|
|
491
494
|
}
|