claude-artifact-framework 0.16.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 +26 -1
- package/dist/index.esm.js +55 -6
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +29 -8
- package/dist/index.global.js.map +3 -3
- package/llms.txt +4 -1
- package/package.json +1 -1
- package/src/app.js +67 -6
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),
|
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
|
|
@@ -1093,6 +1132,7 @@ export function createApp(spec = {}) {
|
|
|
1093
1132
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
1094
1133
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
1095
1134
|
viewport: narrow ? "mobile" : "desktop",
|
|
1135
|
+
_navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
|
|
1096
1136
|
machine: spec.machine || null,
|
|
1097
1137
|
send: spec.machine
|
|
1098
1138
|
? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload)
|
|
@@ -1130,7 +1170,7 @@ export function createApp(spec = {}) {
|
|
|
1130
1170
|
"div",
|
|
1131
1171
|
// The layout class lets shared CSS follow the active container width —
|
|
1132
1172
|
// e.g. the header must share the workspace's wider axis, not panel's.
|
|
1133
|
-
{ className: `caf-root caf-layout-${layout}` },
|
|
1173
|
+
{ className: `caf-root caf-layout-${layout}${ctx._navTabs ? " caf-has-bottomnav" : ""}` },
|
|
1134
1174
|
h("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
1135
1175
|
h(
|
|
1136
1176
|
"header",
|
|
@@ -1405,6 +1445,27 @@ const BASE_CSS = `
|
|
|
1405
1445
|
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
1406
1446
|
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
1407
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; }
|
|
1408
1469
|
/* Touch devices get tappable framework controls regardless of the spec. */
|
|
1409
1470
|
@media (pointer: coarse) {
|
|
1410
1471
|
.caf-btn, .caf-tab, .caf-field input, .caf-field select { min-height: 44px; }
|