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 +41 -0
- package/dist/index.esm.js +137 -19
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +40 -11
- package/dist/index.global.js.map +3 -3
- package/llms.txt +13 -5
- package/package.json +1 -1
- package/src/app.js +112 -16
- package/src/blocks.js +38 -0
- package/src/records.js +24 -3
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(
|
|
1132
|
-
|
|
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
|
|
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
|
-
//
|
|
1949
|
-
// orphaned right edge on desktop and change
|
|
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
|
-
//
|
|
1969
|
-
// orphaned right edge on desktop and change
|
|
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) =>
|
|
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
|
-
|
|
2308
|
-
|
|
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; }
|