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/README.md
CHANGED
|
@@ -422,6 +422,30 @@ The active tab and each accordion's open/closed state are framework-owned
|
|
|
422
422
|
view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
|
|
423
423
|
`when` must be a function; both throw otherwise.
|
|
424
424
|
|
|
425
|
+
**When tabs ARE the app** — one tabs block as the whole UI, 4-5 sections
|
|
426
|
+
the user hops between on a phone — declare bottom navigation:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
ArtifactKit.createApp({
|
|
430
|
+
title: "Mi Gym",
|
|
431
|
+
mobile: { nav: "bottom" },
|
|
432
|
+
blocks: [
|
|
433
|
+
{ tabs: [
|
|
434
|
+
{ label: "Hoy", icon: "💪", blocks: [...] },
|
|
435
|
+
{ label: "Progreso", icon: "📈", blocks: [...] },
|
|
436
|
+
// ...up to ~5 sections
|
|
437
|
+
], wide: true },
|
|
438
|
+
],
|
|
439
|
+
})
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
On a phone the first top-level tabs block's bar pins to the bottom of the
|
|
443
|
+
screen (icons above labels, every section visible and thumb-reachable);
|
|
444
|
+
on desktop nothing changes. Measured reason to bother: a top tab bar with
|
|
445
|
+
5 sections overflows sideways at phone width and silently hides the last
|
|
446
|
+
sections behind a scroll nobody sees. Declaring `mobile: { nav: "bottom" }`
|
|
447
|
+
without a top-level tabs block throws.
|
|
448
|
+
|
|
425
449
|
### The `machine` — declared phases instead of hand-rolled state logic
|
|
426
450
|
|
|
427
451
|
If your app has **phases** — a game (choosing → comparing → won), a turn
|
|
@@ -832,7 +856,8 @@ Everything an app can declare, in one place.
|
|
|
832
856
|
|
|
833
857
|
**Top-level keys**: `title`, `subtitle`, `theme: { seed }`, `layout`, `blocks`,
|
|
834
858
|
`records`, `steps`, `chat`, `main`, `sidebar` (workspace panes), `documents`,
|
|
835
|
-
`data`, `shared`, `libs
|
|
859
|
+
`data`, `shared`, `libs`, `machine`, `brand`, `mobile: { nav: "bottom" }`.
|
|
860
|
+
Anything else throws.
|
|
836
861
|
|
|
837
862
|
**Layouts** (`layout:`): `panel` (default — grid of blocks), `records` (CRUD
|
|
838
863
|
list), `steps` (wizard), `chat` (conversation with Claude), `workspace`
|
package/dist/index.esm.js
CHANGED
|
@@ -1438,7 +1438,8 @@ var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "colla
|
|
|
1438
1438
|
var BRAND_KEYS = ["logo", "footer"];
|
|
1439
1439
|
var BANNER_KEYS = ["image", "title", "subtitle"];
|
|
1440
1440
|
var IMAGE_KEYS = ["src", "caption", "fit", "height"];
|
|
1441
|
-
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine", "brand"];
|
|
1441
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine", "brand", "mobile"];
|
|
1442
|
+
var MOBILE_KEYS = ["nav"];
|
|
1442
1443
|
var MACHINE_KEYS = ["key", "initial", "states", "events"];
|
|
1443
1444
|
var MACHINE_STATE_KEYS = ["after"];
|
|
1444
1445
|
var MACHINE_AFTER_KEYS = ["seconds", "then", "do"];
|
|
@@ -1641,6 +1642,14 @@ function checkMachine(m) {
|
|
|
1641
1642
|
}
|
|
1642
1643
|
}
|
|
1643
1644
|
}
|
|
1645
|
+
function findNavTabs(spec) {
|
|
1646
|
+
for (const list of [spec.blocks, spec.main]) {
|
|
1647
|
+
if (!Array.isArray(list)) continue;
|
|
1648
|
+
const found = list.find((b) => b && Array.isArray(b.tabs));
|
|
1649
|
+
if (found) return found;
|
|
1650
|
+
}
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1644
1653
|
function validate(spec) {
|
|
1645
1654
|
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
1646
1655
|
if (unknownTop.length) {
|
|
@@ -1665,6 +1674,22 @@ function validate(spec) {
|
|
|
1665
1674
|
throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
|
|
1666
1675
|
}
|
|
1667
1676
|
}
|
|
1677
|
+
if (spec.mobile !== void 0) {
|
|
1678
|
+
const unknownM = Object.keys(spec.mobile || {}).filter((k) => !MOBILE_KEYS.includes(k));
|
|
1679
|
+
if (unknownM.length) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`claude-artifact-framework: mobile has unknown keys (${unknownM.join(", ")}). Valid keys: ${MOBILE_KEYS.join(", ")}.`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
if (spec.mobile.nav !== void 0 && spec.mobile.nav !== "bottom") {
|
|
1685
|
+
throw new Error('claude-artifact-framework: mobile.nav only accepts "bottom" (omit `mobile` for the default top tab bar).');
|
|
1686
|
+
}
|
|
1687
|
+
if (spec.mobile.nav === "bottom" && !findNavTabs(spec)) {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
'claude-artifact-framework: mobile.nav: "bottom" moves a top-level `tabs` block\'s bar to the bottom of the phone screen \u2014 this spec has no top-level tabs block (in `blocks` or `main`) to move.'
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1668
1693
|
const layout = spec.layout || "panel";
|
|
1669
1694
|
if (!LAYOUTS.includes(layout)) {
|
|
1670
1695
|
throw new Error(
|
|
@@ -1946,17 +1971,18 @@ function boundary(React2) {
|
|
|
1946
1971
|
}
|
|
1947
1972
|
return BlockBoundary;
|
|
1948
1973
|
}
|
|
1949
|
-
function TabsBlock({ spec, ctx }) {
|
|
1974
|
+
function TabsBlock({ spec, ctx, bottomNav }) {
|
|
1950
1975
|
const tabs = spec.tabs;
|
|
1951
1976
|
const key = tabs.map((t) => t.label).join("|");
|
|
1952
1977
|
const stored = ctx.view.tabs ? ctx.view.tabs[key] : void 0;
|
|
1953
1978
|
const active = Math.min(stored === void 0 ? 0 : stored, tabs.length - 1);
|
|
1979
|
+
const bottom = !!bottomNav && ctx.viewport === "mobile";
|
|
1954
1980
|
return createElement(
|
|
1955
1981
|
"div",
|
|
1956
1982
|
{ className: "caf-tabs" },
|
|
1957
1983
|
createElement(
|
|
1958
1984
|
"div",
|
|
1959
|
-
{ className: "caf-tabbar", role: "tablist" },
|
|
1985
|
+
{ className: bottom ? "caf-tabbar caf-tabbar-bottom" : "caf-tabbar", role: "tablist" },
|
|
1960
1986
|
tabs.map(
|
|
1961
1987
|
(t, i) => createElement(
|
|
1962
1988
|
"button",
|
|
@@ -1968,7 +1994,7 @@ function TabsBlock({ spec, ctx }) {
|
|
|
1968
1994
|
className: i === active ? "caf-tab caf-tab-active" : "caf-tab",
|
|
1969
1995
|
onClick: () => ctx.setTab(key, i)
|
|
1970
1996
|
},
|
|
1971
|
-
t.icon ? `${t.icon} ${t.label}` : t.label
|
|
1997
|
+
bottom && t.icon ? [createElement("span", { key: "i", className: "caf-tab-ico", "aria-hidden": true }, t.icon), createElement("span", { key: "l", className: "caf-tab-lbl" }, t.label)] : t.icon ? `${t.icon} ${t.label}` : t.label
|
|
1972
1998
|
)
|
|
1973
1999
|
)
|
|
1974
2000
|
),
|
|
@@ -1984,13 +2010,14 @@ var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
|
|
|
1984
2010
|
function Block({ block, ctx }) {
|
|
1985
2011
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
1986
2012
|
if (block.state !== void 0 && ctx.machine && ctx.data[ctx.machine.key || "fase"] !== block.state) return null;
|
|
2013
|
+
const bottomNav = ctx._navTabs !== void 0 && ctx._navTabs === block;
|
|
1987
2014
|
if (block.icon && typeof block.title === "string") block = { ...block, title: `${block.icon} ${block.title}` };
|
|
1988
2015
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
1989
2016
|
const Component2 = ALL_BLOCKS[name];
|
|
1990
2017
|
const inner = createElement(
|
|
1991
2018
|
boundary(ctx.React),
|
|
1992
2019
|
null,
|
|
1993
|
-
createElement(Component2, { spec: block.collapsible ? { ...block, title: void 0 } : block, ctx })
|
|
2020
|
+
createElement(Component2, { spec: block.collapsible ? { ...block, title: void 0 } : block, ctx, bottomNav })
|
|
1994
2021
|
);
|
|
1995
2022
|
if (!block.collapsible) return inner;
|
|
1996
2023
|
const key = typeof block.title === "string" && block.title ? block.title : name;
|
|
@@ -2376,6 +2403,7 @@ function createApp(spec = {}) {
|
|
|
2376
2403
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
2377
2404
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
2378
2405
|
viewport: narrow ? "mobile" : "desktop",
|
|
2406
|
+
_navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
|
|
2379
2407
|
machine: spec.machine || null,
|
|
2380
2408
|
send: spec.machine ? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload) : () => {
|
|
2381
2409
|
throw new Error("claude-artifact-framework: ctx.send() needs a `machine` declared at the top level.");
|
|
@@ -2392,7 +2420,7 @@ function createApp(spec = {}) {
|
|
|
2392
2420
|
"div",
|
|
2393
2421
|
// The layout class lets shared CSS follow the active container width —
|
|
2394
2422
|
// e.g. the header must share the workspace's wider axis, not panel's.
|
|
2395
|
-
{ className: `caf-root caf-layout-${layout}` },
|
|
2423
|
+
{ className: `caf-root caf-layout-${layout}${ctx._navTabs ? " caf-has-bottomnav" : ""}` },
|
|
2396
2424
|
createElement("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
2397
2425
|
createElement(
|
|
2398
2426
|
"header",
|
|
@@ -2662,6 +2690,27 @@ var BASE_CSS = `
|
|
|
2662
2690
|
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
2663
2691
|
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
2664
2692
|
.caf-custom { overflow-x: auto; }
|
|
2693
|
+
/* mobile.nav: "bottom" \u2014 the app-shell tab bar pinned to the thumb zone.
|
|
2694
|
+
Tabs share the width evenly so every section stays visible; the root
|
|
2695
|
+
reserves space so the bar never covers the end of the content. */
|
|
2696
|
+
.caf-has-bottomnav { padding-bottom: 76px; }
|
|
2697
|
+
.caf-tabbar-bottom {
|
|
2698
|
+
position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
|
|
2699
|
+
gap: 0; margin: 0; overflow-x: visible;
|
|
2700
|
+
background: var(--caf-surface); border-top: 1px solid var(--caf-border); border-bottom: none;
|
|
2701
|
+
padding: 0.25rem 0.4rem calc(0.3rem + env(safe-area-inset-bottom, 0px));
|
|
2702
|
+
box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.07);
|
|
2703
|
+
}
|
|
2704
|
+
.caf-tabbar-bottom .caf-tab {
|
|
2705
|
+
flex: 1 1 0; min-width: 0; min-height: 48px;
|
|
2706
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
2707
|
+
gap: 0.15rem; padding: 0.25rem 0.2rem; margin-bottom: 0;
|
|
2708
|
+
font-size: 0.66rem; border-bottom: none;
|
|
2709
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
2710
|
+
}
|
|
2711
|
+
.caf-tabbar-bottom .caf-tab-ico { font-size: 1.25rem; line-height: 1; }
|
|
2712
|
+
.caf-tabbar-bottom .caf-tab-lbl { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2713
|
+
.caf-tabbar-bottom .caf-tab-active, .caf-tabbar-bottom .caf-tab-active:hover { border-bottom-color: transparent; }
|
|
2665
2714
|
/* Touch devices get tappable framework controls regardless of the spec. */
|
|
2666
2715
|
@media (pointer: coarse) {
|
|
2667
2716
|
.caf-btn, .caf-tab, .caf-field input, .caf-field select { min-height: 44px; }
|