claude-artifact-framework 0.14.1 → 0.15.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
@@ -631,6 +631,44 @@ ArtifactKit.createApp({
631
631
  })
632
632
  ```
633
633
 
634
+ ### Brand — logo, banner, images, icons, avatars
635
+
636
+ Identity without uploads. Artifacts can't persist real image files (the
637
+ ~5MB pool), so every brand element is **zero-asset-first**: it works with
638
+ nothing, and a real asset is an optional upgrade.
639
+
640
+ ```js
641
+ ArtifactKit.createApp({
642
+ title: "Napoli Pizzas",
643
+ subtitle: "Del horno a tu mesa",
644
+ theme: { seed: "#c2410c" },
645
+ brand: {
646
+ // the logo ladder: omit → automatic monogram (initials over the accent);
647
+ // an emoji; an inline <svg>...</svg> (best: you can WRITE one); or an https URL
648
+ logo: '<svg viewBox="0 0 24 24">...</svg>',
649
+ footer: "Napoli Pizzas — pedidos al 11-5555-5555",
650
+ },
651
+ blocks: [
652
+ // full-width hero; with no image it paints a gradient derived from the seed
653
+ { banner: { title: "Promo de julio", subtitle: "2x1 los martes", image: "https://..." } },
654
+ { image: { src: "https://... o un <svg> inline", caption: "El local", fit: "cover" } },
655
+ { text: "...", title: "Horarios", icon: "🕐" }, // icon prefixes any block title
656
+ ],
657
+ })
658
+ ```
659
+
660
+ - **`brand.logo`** renders beside the title; **`brand.footer`** closes the
661
+ page. Banner keys: `image`, `title`, `subtitle`. Image keys: `src`,
662
+ `caption`, `fit` (`cover`/`contain`), `height`.
663
+ - **`icon` (an emoji) goes on any block, any tab, and any document type** —
664
+ emoji are zero bytes, work in both themes, and read as a consistent icon
665
+ set. Don't mix emoji icons with image icons in one app.
666
+ - **`avatar: true` on records** draws an initials circle per row, colored
667
+ deterministically from the row title through the theme palette — a CRM or
668
+ ranking looks finished with no images at all.
669
+ - Prefer inline SVG over URLs for logos and small graphics: no network, no
670
+ broken-image state, theme-controllable. Reserve URLs for real photos.
671
+
634
672
  ### External libraries, uploaded files, full-bleed custom, viewer identity
635
673
 
636
674
  Four primitives for the custom-heavy end of the spectrum (document viewers,
@@ -922,3 +960,6 @@ The rules that decide whether a generated app works, all in one place
922
960
  `marked(...)`); pin versions; when unsure of the file path use the bare
923
961
  package URL.
924
962
  12. **Framework ids are strings** — never `Number(record.id)`.
963
+ 13. **Brand is zero-asset-first** — monogram/gradient/emoji defaults before
964
+ URLs; inline `<svg>` over remote images; one icon style per app
965
+ (emoji-first); `avatar: true` instead of hand-built initials circles.
package/dist/index.esm.js CHANGED
@@ -780,6 +780,32 @@ function OutputBlock({ spec, ctx }) {
780
780
  )
781
781
  );
782
782
  }
783
+ function BannerBlock({ spec, ctx }) {
784
+ const b = typeof spec.banner === "string" ? { title: spec.banner } : spec.banner || {};
785
+ const [c1, c2] = ctx.colors(2);
786
+ const background = b.image ? `linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)), url(${JSON.stringify(b.image)}) center / cover` : `linear-gradient(135deg, ${c1}, ${c2})`;
787
+ return createElement(
788
+ "div",
789
+ { className: "caf-banner", style: { background } },
790
+ b.title ? createElement("h2", { className: "caf-banner-title" }, b.title) : null,
791
+ b.subtitle ? createElement("p", { className: "caf-banner-sub" }, b.subtitle) : null
792
+ );
793
+ }
794
+ function ImageBlock({ spec }) {
795
+ const im = typeof spec.image === "string" ? { src: spec.image } : spec.image || {};
796
+ const isSvg = typeof im.src === "string" && im.src.trim().startsWith("<svg");
797
+ return createElement(
798
+ "figure",
799
+ { className: "caf-block caf-image" },
800
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
801
+ isSvg ? createElement("div", { className: "caf-image-svg", dangerouslySetInnerHTML: { __html: im.src } }) : createElement("img", {
802
+ src: im.src,
803
+ alt: im.caption || spec.title || "",
804
+ style: { objectFit: im.fit || "cover", maxHeight: im.height ? `${im.height}px` : void 0 }
805
+ }),
806
+ im.caption ? createElement("figcaption", { className: "caf-hint" }, im.caption) : null
807
+ );
808
+ }
783
809
  function TextBlock({ spec, ctx }) {
784
810
  const body = typeof spec.text === "function" ? spec.text(ctx.data) : spec.text;
785
811
  return createElement(
@@ -1011,6 +1037,8 @@ var BLOCKS = {
1011
1037
  chart: ChartBlock,
1012
1038
  timer: TimerBlock,
1013
1039
  actions: ActionsBlock,
1040
+ banner: BannerBlock,
1041
+ image: ImageBlock,
1014
1042
  chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx, title: spec.title }),
1015
1043
  custom: CustomBlock
1016
1044
  };
@@ -1042,6 +1070,13 @@ function recordDefaults(fields) {
1042
1070
  function makeId() {
1043
1071
  return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
1044
1072
  }
1073
+ function Avatar({ title, ctx }) {
1074
+ const initials = String(title).split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";
1075
+ let hash = 0;
1076
+ for (const ch of String(title)) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
1077
+ const palette2 = ctx.colors(8);
1078
+ return createElement("span", { className: "caf-avatar", style: { background: palette2[hash % palette2.length] } }, initials);
1079
+ }
1045
1080
  function rowTitle(spec, rec) {
1046
1081
  if (spec.title) return spec.title(rec);
1047
1082
  const f = spec.fields.find((f2) => !f2.type || f2.type === "text" || f2.type === "textarea" || f2.type === "select");
@@ -1125,11 +1160,16 @@ function ListScreen({ spec, ctx }) {
1125
1160
  "button",
1126
1161
  {
1127
1162
  type: "button",
1128
- className: "caf-row",
1163
+ className: spec.avatar ? "caf-row caf-row-avatar" : "caf-row",
1129
1164
  onClick: () => ctx.go("record", rec.id)
1130
1165
  },
1131
- createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
1132
- spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
1166
+ spec.avatar ? createElement(Avatar, { title: rowTitle(spec, rec), ctx }) : null,
1167
+ createElement(
1168
+ "span",
1169
+ { className: "caf-row-main" },
1170
+ createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
1171
+ spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
1172
+ )
1133
1173
  ),
1134
1174
  spec.actions ? createElement(
1135
1175
  "div",
@@ -1394,14 +1434,17 @@ function StepsLayout({ spec, ctx }) {
1394
1434
 
1395
1435
  // src/app.js
1396
1436
  var LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
1397
- var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill", "state"];
1398
- var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine"];
1437
+ var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill", "state", "icon"];
1438
+ var BRAND_KEYS = ["logo", "footer"];
1439
+ var BANNER_KEYS = ["image", "title", "subtitle"];
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"];
1399
1442
  var MACHINE_KEYS = ["key", "initial", "states", "events"];
1400
1443
  var MACHINE_STATE_KEYS = ["after"];
1401
1444
  var MACHINE_AFTER_KEYS = ["seconds", "then", "do"];
1402
1445
  var MACHINE_EVENT_KEYS = ["from", "do", "then"];
1403
1446
  var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
1404
- var RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
1447
+ var RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions", "avatar"];
1405
1448
  var ITEMS_KEYS = ["label", "fields", "empty"];
1406
1449
  function checkFields(fields, where) {
1407
1450
  for (const f of fields || []) {
@@ -1481,6 +1524,25 @@ function checkBlocks(blocks) {
1481
1524
  if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
1482
1525
  if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
1483
1526
  if (known[0] === "records") checkRecordsSpec(block.records, `blocks[${i}].records`, true);
1527
+ if (known[0] === "banner" && typeof block.banner === "object" && block.banner !== null) {
1528
+ const unknownBn = Object.keys(block.banner).filter((k) => !BANNER_KEYS.includes(k));
1529
+ if (unknownBn.length) {
1530
+ throw new Error(
1531
+ `claude-artifact-framework: blocks[${i}].banner has unknown keys (${unknownBn.join(", ")}). Valid keys: ${BANNER_KEYS.join(", ")}.`
1532
+ );
1533
+ }
1534
+ }
1535
+ if (known[0] === "image" && typeof block.image === "object" && block.image !== null) {
1536
+ const unknownIm = Object.keys(block.image).filter((k) => !IMAGE_KEYS.includes(k));
1537
+ if (unknownIm.length) {
1538
+ throw new Error(
1539
+ `claude-artifact-framework: blocks[${i}].image has unknown keys (${unknownIm.join(", ")}). Valid keys: ${IMAGE_KEYS.join(", ")}.`
1540
+ );
1541
+ }
1542
+ }
1543
+ if (block.icon !== void 0 && typeof block.icon !== "string") {
1544
+ throw new Error(`claude-artifact-framework: blocks[${i}].icon must be a string (an emoji works best).`);
1545
+ }
1484
1546
  if (known[0] === "tabs") {
1485
1547
  const tabs = block.tabs;
1486
1548
  if (!Array.isArray(tabs) || tabs.length === 0) {
@@ -1587,6 +1649,17 @@ function validate(spec) {
1587
1649
  );
1588
1650
  }
1589
1651
  if (spec.machine !== void 0) checkMachine(spec.machine);
1652
+ if (spec.brand !== void 0) {
1653
+ const unknownB = Object.keys(spec.brand || {}).filter((k) => !BRAND_KEYS.includes(k));
1654
+ if (unknownB.length) {
1655
+ throw new Error(
1656
+ `claude-artifact-framework: brand has unknown keys (${unknownB.join(", ")}). Valid keys: ${BRAND_KEYS.join(", ")}.`
1657
+ );
1658
+ }
1659
+ if (spec.brand.logo !== void 0 && typeof spec.brand.logo !== "string") {
1660
+ throw new Error("claude-artifact-framework: brand.logo must be a string \u2014 an emoji, an inline <svg>...</svg>, or an https URL. Omit it for an automatic monogram.");
1661
+ }
1662
+ }
1590
1663
  if (spec.libs !== void 0) {
1591
1664
  if (!Array.isArray(spec.libs) || spec.libs.some((u) => typeof u !== "string" || !/^https?:\/\//.test(u))) {
1592
1665
  throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
@@ -1700,7 +1773,7 @@ function validate(spec) {
1700
1773
  }
1701
1774
  for (const name of names) {
1702
1775
  const t = d.types[name];
1703
- const TYPE_KEYS = ["label", "title", "blocks", "empty"];
1776
+ const TYPE_KEYS = ["label", "title", "blocks", "empty", "icon"];
1704
1777
  const unknownT = Object.keys(t || {}).filter((k) => !TYPE_KEYS.includes(k));
1705
1778
  if (unknownT.length) {
1706
1779
  throw new Error(
@@ -1895,7 +1968,7 @@ function TabsBlock({ spec, ctx }) {
1895
1968
  className: i === active ? "caf-tab caf-tab-active" : "caf-tab",
1896
1969
  onClick: () => ctx.setTab(key, i)
1897
1970
  },
1898
- t.label
1971
+ t.icon ? `${t.icon} ${t.label}` : t.label
1899
1972
  )
1900
1973
  )
1901
1974
  ),
@@ -1911,6 +1984,7 @@ var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
1911
1984
  function Block({ block, ctx }) {
1912
1985
  if (typeof block.when === "function" && !block.when(ctx.data)) return null;
1913
1986
  if (block.state !== void 0 && ctx.machine && ctx.data[ctx.machine.key || "fase"] !== block.state) return null;
1987
+ if (block.icon && typeof block.title === "string") block = { ...block, title: `${block.icon} ${block.title}` };
1914
1988
  const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
1915
1989
  const Component2 = ALL_BLOCKS[name];
1916
1990
  const inner = createElement(
@@ -1945,9 +2019,9 @@ function Panel({ spec, ctx }) {
1945
2019
  (spec.blocks || []).map(
1946
2020
  (block, i) => createElement(
1947
2021
  "section",
1948
- // A collapsible is always full-width: half-width accordions leave an
1949
- // orphaned right edge on desktop and change width across breakpoints.
1950
- { key: i, className: block.wide || block.collapsible ? "caf-slot caf-slot-wide" : "caf-slot" },
2022
+ // Collapsibles and banners are always full-width: half-width versions
2023
+ // leave an orphaned right edge on desktop and change across breakpoints.
2024
+ { key: i, className: block.wide || block.collapsible || block.banner ? "caf-slot caf-slot-wide" : "caf-slot" },
1951
2025
  createElement(Block, { block, ctx })
1952
2026
  )
1953
2027
  )
@@ -1965,9 +2039,9 @@ function RecordsWithBlocks({ spec, ctx }) {
1965
2039
  spec.blocks.map(
1966
2040
  (block, i) => createElement(
1967
2041
  "section",
1968
- // A collapsible is always full-width: half-width accordions leave an
1969
- // orphaned right edge on desktop and change width across breakpoints.
1970
- { key: i, className: block.wide || block.collapsible ? "caf-slot caf-slot-wide" : "caf-slot" },
2042
+ // Collapsibles and banners are always full-width: half-width versions
2043
+ // leave an orphaned right edge on desktop and change across breakpoints.
2044
+ { key: i, className: block.wide || block.collapsible || block.banner ? "caf-slot caf-slot-wide" : "caf-slot" },
1971
2045
  createElement(Block, { block, ctx })
1972
2046
  )
1973
2047
  )
@@ -2028,7 +2102,10 @@ function DocumentsLayout({ spec, ctx }) {
2028
2102
  const types = dspec.types || null;
2029
2103
  const typeNames = types ? Object.keys(types) : [];
2030
2104
  const specFor = (doc) => types ? types[doc.type] : dspec;
2031
- const typeLabel = (name) => types[name] && types[name].label || name;
2105
+ const typeLabel = (name) => {
2106
+ const t = types[name] || {};
2107
+ return (t.icon ? t.icon + " " : "") + (t.label || name);
2108
+ };
2032
2109
  const [picking, setPicking] = useState(false);
2033
2110
  const storedId = ctx.view.tabs ? ctx.view.tabs[DOC_TAB_KEY] : void 0;
2034
2111
  const active = docs.find((doc) => doc.id === storedId) || docs[0];
@@ -2295,6 +2372,12 @@ function createApp(spec = {}) {
2295
2372
  }
2296
2373
  };
2297
2374
  const Layout = LAYOUT_COMPONENTS[layout];
2375
+ const logo = spec.brand && spec.brand.logo;
2376
+ const logoEl = !spec.brand ? null : logo && logo.trim().startsWith("<svg") ? createElement("span", { className: "caf-logo caf-logo-svg", dangerouslySetInnerHTML: { __html: logo } }) : logo && /^https?:\/\//.test(logo) ? createElement("img", { className: "caf-logo", src: logo, alt: "" }) : logo ? createElement("span", { className: "caf-logo caf-logo-emoji", "aria-hidden": true }, logo) : createElement(
2377
+ "span",
2378
+ { className: "caf-logo caf-logo-mono", "aria-hidden": true },
2379
+ (spec.title || "?").split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase()
2380
+ );
2298
2381
  return createElement(
2299
2382
  "div",
2300
2383
  // The layout class lets shared CSS follow the active container width —
@@ -2303,16 +2386,22 @@ function createApp(spec = {}) {
2303
2386
  createElement("style", { dangerouslySetInnerHTML: { __html: css } }),
2304
2387
  createElement(
2305
2388
  "header",
2306
- { className: "caf-header" },
2307
- spec.title ? createElement("h1", null, spec.title) : null,
2308
- spec.subtitle ? createElement("p", null, spec.subtitle) : null
2389
+ { className: spec.brand ? "caf-header caf-header-brand" : "caf-header" },
2390
+ logoEl,
2391
+ createElement(
2392
+ "div",
2393
+ { className: "caf-header-text" },
2394
+ spec.title ? createElement("h1", null, spec.title) : null,
2395
+ spec.subtitle ? createElement("p", null, spec.subtitle) : null
2396
+ )
2309
2397
  ),
2310
2398
  libsState === "error" ? createElement(
2311
2399
  "div",
2312
2400
  { className: "caf-block caf-block-error" },
2313
2401
  createElement("strong", null, "A library failed to load"),
2314
2402
  createElement("code", null, libsError)
2315
- ) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : createElement(Layout, { spec, ctx })
2403
+ ) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : createElement(Layout, { spec, ctx }),
2404
+ spec.brand && spec.brand.footer ? createElement("footer", { className: "caf-footer" }, spec.brand.footer) : null
2316
2405
  );
2317
2406
  };
2318
2407
  }
@@ -2525,6 +2614,35 @@ var BASE_CSS = `
2525
2614
  .caf-empty .caf-btn { margin-top: 0.6rem; }
2526
2615
  .caf-records-block { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
2527
2616
  .caf-doc-typemenu { display: flex; gap: 0.5rem; flex-wrap: wrap; }
2617
+ .caf-header-brand { display: flex; align-items: center; gap: 0.8rem; }
2618
+ .caf-header-text { min-width: 0; }
2619
+ .caf-logo { flex: none; width: 40px; height: 40px; border-radius: 9px; object-fit: contain; }
2620
+ .caf-logo-emoji { display: flex; align-items: center; justify-content: center; font-size: 1.7rem; background: var(--caf-surface); border: 1px solid var(--caf-border); }
2621
+ .caf-logo-mono {
2622
+ display: flex; align-items: center; justify-content: center;
2623
+ background: var(--caf-accent); color: var(--caf-accentText);
2624
+ font-size: 0.95rem; font-weight: 700; letter-spacing: 0.02em;
2625
+ }
2626
+ .caf-logo-svg { display: flex; align-items: center; justify-content: center; overflow: hidden; }
2627
+ .caf-logo-svg svg { width: 100%; height: 100%; }
2628
+ .caf-banner {
2629
+ border-radius: var(--caf-radius); padding: 1.6rem 1.4rem; min-height: 110px;
2630
+ display: flex; flex-direction: column; justify-content: flex-end; gap: 0.2rem;
2631
+ }
2632
+ .caf-banner-title { margin: 0; color: #fff; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.01em; text-shadow: 0 1px 3px rgba(0,0,0,0.35); }
2633
+ .caf-banner-sub { margin: 0; color: rgba(255,255,255,0.92); font-size: 0.9rem; text-shadow: 0 1px 2px rgba(0,0,0,0.35); }
2634
+ .caf-image { margin: 0; }
2635
+ .caf-image img { width: 100%; border-radius: 7px; display: block; }
2636
+ .caf-image-svg svg { width: 100%; height: auto; display: block; }
2637
+ .caf-footer { max-width: 760px; margin: 2rem auto 0; padding-top: 0.8rem; border-top: 1px solid var(--caf-border); color: var(--caf-muted); font-size: 0.78rem; text-align: center; }
2638
+ .caf-layout-workspace .caf-footer { max-width: 1200px; }
2639
+ .caf-avatar {
2640
+ flex: none; width: 30px; height: 30px; border-radius: 50%;
2641
+ display: flex; align-items: center; justify-content: center;
2642
+ color: #fff; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em;
2643
+ }
2644
+ .caf-row-avatar { flex-direction: row; align-items: center; gap: 0.6rem; }
2645
+ .caf-row-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
2528
2646
  .caf-chevron-closed { transform: rotate(-90deg); }
2529
2647
  .caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
2530
2648
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }