claude-artifact-framework 0.7.3 → 0.8.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 +51 -3
- package/dist/index.esm.js +104 -16
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +14 -13
- package/dist/index.global.js.map +3 -3
- package/package.json +2 -2
- package/src/app.js +58 -5
- package/src/appState.js +14 -1
- package/src/blocks.js +44 -6
- package/src/records.js +2 -2
- package/src/steps.js +2 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ mirrors npm packages automatically.
|
|
|
20
20
|
### As a global script (`<script>` tag)
|
|
21
21
|
|
|
22
22
|
```html
|
|
23
|
-
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.8.0/dist/index.global.js"></script>
|
|
24
24
|
<script>
|
|
25
25
|
ArtifactKit.useReact(React); // hand over the page's React once
|
|
26
26
|
const App = ArtifactKit.createApp({ title: "Mi app", blocks: [ /* ... */ ] });
|
|
@@ -38,11 +38,11 @@ mirrors npm packages automatically.
|
|
|
38
38
|
createStore,
|
|
39
39
|
createPersistedStore,
|
|
40
40
|
createSharedStore,
|
|
41
|
-
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
41
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.8.0/dist/index.esm.js";
|
|
42
42
|
</script>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Pin a version (`@0.
|
|
45
|
+
Pin a version (`@0.8.0`) for reproducible artifacts, or drop the version
|
|
46
46
|
(`claude-artifact-framework/dist/...`) to always get the latest release.
|
|
47
47
|
|
|
48
48
|
### Inside a React artifact
|
|
@@ -388,6 +388,54 @@ The active tab and each accordion's open/closed state are framework-owned
|
|
|
388
388
|
view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
|
|
389
389
|
`when` must be a function; both throw otherwise.
|
|
390
390
|
|
|
391
|
+
### External libraries, uploaded files, full-bleed custom, viewer identity
|
|
392
|
+
|
|
393
|
+
Four primitives for the custom-heavy end of the spectrum (document viewers,
|
|
394
|
+
canvases, analyzers):
|
|
395
|
+
|
|
396
|
+
```js
|
|
397
|
+
ArtifactKit.createApp({
|
|
398
|
+
title: "Visor de PDF",
|
|
399
|
+
layout: "workspace",
|
|
400
|
+
libs: ["https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"],
|
|
401
|
+
main: [
|
|
402
|
+
{ fields: [{ key: "doc", label: "Documento", type: "file", accept: "application/pdf" }] },
|
|
403
|
+
{ custom: (ctx) => renderPdf(ctx.files.doc), fill: true },
|
|
404
|
+
],
|
|
405
|
+
sidebar: [ { chat: { system: "Sos un asistente de lectura." } } ],
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- **`libs: ["https://..."]`** loads external scripts before the app mounts,
|
|
410
|
+
deduped by URL, with a "Loading libraries…" state and a named error block
|
|
411
|
+
if one fails. No hand-written `loadScript` in custom code.
|
|
412
|
+
- **`type: "file"`** fields hand the live `File` to your code at
|
|
413
|
+
`ctx.files[key]` (in memory only — the 5MB/key storage limit makes real
|
|
414
|
+
files unpersistable); `data[key]` gets serializable metadata
|
|
415
|
+
(`{ name, size, type }`) that survives reload, after which the file must
|
|
416
|
+
be picked again. `accept` filters the picker. File fields live in the flat
|
|
417
|
+
data pool (panel blocks or steps) — declaring one inside records/items
|
|
418
|
+
throws.
|
|
419
|
+
- **`fill: true`** on a `custom` block drops the card chrome so the block
|
|
420
|
+
owns a large scrollable surface — the workspace main-area case.
|
|
421
|
+
- **`ctx.viewerId`** is a stable anonymous id per viewer, always in the
|
|
422
|
+
personal scope. In a `shared` app it expresses "one vote per person" and
|
|
423
|
+
"added by" with no account system. It is available on the `ctx` that
|
|
424
|
+
`custom` blocks and chat tools receive; row/block actions receive it as a
|
|
425
|
+
third argument:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
records: {
|
|
429
|
+
fields: [{ key: "nombre", type: "text" }],
|
|
430
|
+
actions: [{
|
|
431
|
+
label: "Votar",
|
|
432
|
+
when: (r, viewerId) => !(r.votantes || []).includes(viewerId),
|
|
433
|
+
run: (r, d, viewerId) => { r.votantes = [...(r.votantes || []), viewerId]; },
|
|
434
|
+
}],
|
|
435
|
+
subtitle: (r) => (r.votantes || []).length + " votos",
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
391
439
|
## API reference
|
|
392
440
|
|
|
393
441
|
Everything an app can declare, in one place.
|
package/dist/index.esm.js
CHANGED
|
@@ -133,6 +133,7 @@ var Component = React && React.Component || class ReactMissing {
|
|
|
133
133
|
|
|
134
134
|
// src/appState.js
|
|
135
135
|
var DATA_KEY = "caf:data";
|
|
136
|
+
var VIEWER_KEY = "caf:viewer";
|
|
136
137
|
var VIEW_KEY = "caf:view";
|
|
137
138
|
var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null, tabs: {}, collapsed: {} };
|
|
138
139
|
function isPlainObject(v) {
|
|
@@ -160,6 +161,7 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
|
|
|
160
161
|
let data = structuredCloneish(defaults);
|
|
161
162
|
let view = { ...EMPTY_VIEW };
|
|
162
163
|
let hydrated = false;
|
|
164
|
+
let viewerId = null;
|
|
163
165
|
let writes = 0;
|
|
164
166
|
let persistedWrites = 0;
|
|
165
167
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -194,10 +196,18 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
|
|
|
194
196
|
}, pollMs);
|
|
195
197
|
}
|
|
196
198
|
const ready = (async () => {
|
|
197
|
-
const [storedData, storedView] = await Promise.all([
|
|
199
|
+
const [storedData, storedView, storedViewer] = await Promise.all([
|
|
198
200
|
dataStore.get(DATA_KEY, void 0),
|
|
199
|
-
storage.get(VIEW_KEY, void 0)
|
|
201
|
+
storage.get(VIEW_KEY, void 0),
|
|
202
|
+
storage.get(VIEWER_KEY, void 0)
|
|
200
203
|
]);
|
|
204
|
+
if (typeof storedViewer === "string" && storedViewer) {
|
|
205
|
+
viewerId = storedViewer;
|
|
206
|
+
} else {
|
|
207
|
+
viewerId = "v" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
|
|
208
|
+
storage.set(VIEWER_KEY, viewerId).catch?.(() => {
|
|
209
|
+
});
|
|
210
|
+
}
|
|
201
211
|
if (storedData !== void 0) data = mergeDefaults(defaults, storedData);
|
|
202
212
|
if (storedView !== void 0) view = { ...EMPTY_VIEW, ...storedView };
|
|
203
213
|
hydrated = true;
|
|
@@ -219,6 +229,7 @@ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2
|
|
|
219
229
|
return {
|
|
220
230
|
ready,
|
|
221
231
|
isHydrated: () => hydrated,
|
|
232
|
+
getViewerId: () => viewerId,
|
|
222
233
|
getData: () => data,
|
|
223
234
|
getView: () => view,
|
|
224
235
|
update,
|
|
@@ -608,7 +619,7 @@ function ChatLayout({ spec, ctx }) {
|
|
|
608
619
|
}
|
|
609
620
|
|
|
610
621
|
// src/blocks.js
|
|
611
|
-
var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
622
|
+
var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select", "file"];
|
|
612
623
|
function formatValue(value, format) {
|
|
613
624
|
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
614
625
|
const n = Number(value);
|
|
@@ -642,6 +653,20 @@ function Field({ field, value, onChange, data }) {
|
|
|
642
653
|
const error = validateField(field, value);
|
|
643
654
|
const id = `caf-f-${field.key}`;
|
|
644
655
|
const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
|
|
656
|
+
if (field.type === "file") {
|
|
657
|
+
return createElement(
|
|
658
|
+
"div",
|
|
659
|
+
{ className: "caf-field" },
|
|
660
|
+
createElement("label", { htmlFor: id }, field.label || field.key),
|
|
661
|
+
createElement("input", {
|
|
662
|
+
id,
|
|
663
|
+
type: "file",
|
|
664
|
+
accept: field.accept,
|
|
665
|
+
onChange: (e) => onChange(e.target.files && e.target.files[0] ? e.target.files[0] : null)
|
|
666
|
+
}),
|
|
667
|
+
value && value.name ? createElement("small", { className: "caf-hint" }, `${value.name} (${Math.round((value.size || 0) / 1024)} KB)${value.stale ? " \u2014 re-pick after reload" : ""}`) : field.hint ? createElement("small", { className: "caf-hint" }, field.hint) : null
|
|
668
|
+
);
|
|
669
|
+
}
|
|
645
670
|
if (field.type === "check") {
|
|
646
671
|
return createElement(
|
|
647
672
|
"label",
|
|
@@ -700,6 +725,15 @@ function toNumber(raw) {
|
|
|
700
725
|
const n = Number(raw);
|
|
701
726
|
return Number.isFinite(n) ? n : raw;
|
|
702
727
|
}
|
|
728
|
+
function writeField(ctx, key, field, v, setter) {
|
|
729
|
+
if (field.type === "file") {
|
|
730
|
+
ctx.files[key] = v || null;
|
|
731
|
+
const meta = v ? { name: v.name, size: v.size, type: v.type } : null;
|
|
732
|
+
ctx.update((d) => setter(d, meta));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
ctx.update((d) => setter(d, v));
|
|
736
|
+
}
|
|
703
737
|
function FieldsBlock({ spec, ctx }) {
|
|
704
738
|
const fields = spec.fields || [];
|
|
705
739
|
return createElement(
|
|
@@ -712,8 +746,8 @@ function FieldsBlock({ spec, ctx }) {
|
|
|
712
746
|
field,
|
|
713
747
|
data: ctx.data,
|
|
714
748
|
value: ctx.data[field.key],
|
|
715
|
-
onChange: (v) => ctx.
|
|
716
|
-
d[field.key] =
|
|
749
|
+
onChange: (v) => writeField(ctx, field.key, field, v, (d, val) => {
|
|
750
|
+
d[field.key] = val;
|
|
717
751
|
})
|
|
718
752
|
})
|
|
719
753
|
)
|
|
@@ -935,7 +969,7 @@ function TimerBlock({ spec, ctx }) {
|
|
|
935
969
|
}
|
|
936
970
|
function ActionsBlock({ spec, ctx }) {
|
|
937
971
|
const actions = spec.actions || [];
|
|
938
|
-
const visible = actions.filter((a) => !a.when || a.when(ctx.data));
|
|
972
|
+
const visible = actions.filter((a) => !a.when || a.when(ctx.data, ctx.viewerId));
|
|
939
973
|
return createElement(
|
|
940
974
|
"div",
|
|
941
975
|
{ className: "caf-block caf-actions" },
|
|
@@ -950,7 +984,7 @@ function ActionsBlock({ spec, ctx }) {
|
|
|
950
984
|
key: a.label,
|
|
951
985
|
type: "button",
|
|
952
986
|
className: a.kind === "danger" ? "caf-btn caf-btn-danger" : a.kind === "primary" ? "caf-btn caf-btn-primary" : "caf-btn",
|
|
953
|
-
onClick: () => ctx.update((d) => a.run(d))
|
|
987
|
+
onClick: () => ctx.update((d) => a.run(d, ctx.viewerId))
|
|
954
988
|
},
|
|
955
989
|
a.label
|
|
956
990
|
)
|
|
@@ -960,10 +994,11 @@ function ActionsBlock({ spec, ctx }) {
|
|
|
960
994
|
}
|
|
961
995
|
function CustomBlock({ spec, ctx }) {
|
|
962
996
|
const rendered = spec.custom(ctx);
|
|
997
|
+
const cls = spec.fill ? "caf-block caf-block-fill" : "caf-block";
|
|
963
998
|
if (typeof rendered === "string") {
|
|
964
|
-
return createElement("div", { className:
|
|
999
|
+
return createElement("div", { className: cls, dangerouslySetInnerHTML: { __html: rendered } });
|
|
965
1000
|
}
|
|
966
|
-
return createElement("div", { className:
|
|
1001
|
+
return createElement("div", { className: cls }, rendered);
|
|
967
1002
|
}
|
|
968
1003
|
var BLOCKS = {
|
|
969
1004
|
fields: FieldsBlock,
|
|
@@ -1089,7 +1124,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
1089
1124
|
spec.actions ? createElement(
|
|
1090
1125
|
"div",
|
|
1091
1126
|
{ className: "caf-row-actions" },
|
|
1092
|
-
spec.actions.filter((a) => !a.when || a.when(rec)).map(
|
|
1127
|
+
spec.actions.filter((a) => !a.when || a.when(rec, ctx.viewerId)).map(
|
|
1093
1128
|
(a) => createElement(
|
|
1094
1129
|
"button",
|
|
1095
1130
|
{
|
|
@@ -1098,7 +1133,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
1098
1133
|
className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
|
|
1099
1134
|
onClick: () => ctx.update((d) => {
|
|
1100
1135
|
const live = d.records.find((x) => x.id === rec.id);
|
|
1101
|
-
if (live) a.run(live, d);
|
|
1136
|
+
if (live) a.run(live, d, ctx.viewerId);
|
|
1102
1137
|
})
|
|
1103
1138
|
},
|
|
1104
1139
|
a.label
|
|
@@ -1291,8 +1326,8 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1291
1326
|
field,
|
|
1292
1327
|
data: ctx.data,
|
|
1293
1328
|
value: ctx.data[field.key],
|
|
1294
|
-
onChange: (v) => ctx.
|
|
1295
|
-
d[field.key] =
|
|
1329
|
+
onChange: (v) => writeField(ctx, field.key, field, v, (d, val) => {
|
|
1330
|
+
d[field.key] = val;
|
|
1296
1331
|
})
|
|
1297
1332
|
})
|
|
1298
1333
|
),
|
|
@@ -1317,8 +1352,8 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1317
1352
|
|
|
1318
1353
|
// src/app.js
|
|
1319
1354
|
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
1320
|
-
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible"];
|
|
1321
|
-
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
|
|
1355
|
+
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
|
|
1356
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs"];
|
|
1322
1357
|
function checkFields(fields, where) {
|
|
1323
1358
|
for (const f of fields || []) {
|
|
1324
1359
|
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
|
|
@@ -1414,6 +1449,9 @@ function checkBlocks(blocks) {
|
|
|
1414
1449
|
if (block.when !== void 0 && typeof block.when !== "function") {
|
|
1415
1450
|
throw new Error(`claude-artifact-framework: blocks[${i}].when must be a function of data returning true/false.`);
|
|
1416
1451
|
}
|
|
1452
|
+
if (block.fill !== void 0 && known[0] !== "custom") {
|
|
1453
|
+
throw new Error(`claude-artifact-framework: \`fill\` is only valid on custom blocks \u2014 blocks[${i}] is "${known[0]}".`);
|
|
1454
|
+
}
|
|
1417
1455
|
});
|
|
1418
1456
|
}
|
|
1419
1457
|
function validate(spec) {
|
|
@@ -1423,6 +1461,11 @@ function validate(spec) {
|
|
|
1423
1461
|
`claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
|
|
1424
1462
|
);
|
|
1425
1463
|
}
|
|
1464
|
+
if (spec.libs !== void 0) {
|
|
1465
|
+
if (!Array.isArray(spec.libs) || spec.libs.some((u) => typeof u !== "string" || !/^https?:\/\//.test(u))) {
|
|
1466
|
+
throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1426
1469
|
const layout = spec.layout || "panel";
|
|
1427
1470
|
if (!LAYOUTS.includes(layout)) {
|
|
1428
1471
|
throw new Error(
|
|
@@ -1501,6 +1544,11 @@ function validate(spec) {
|
|
|
1501
1544
|
);
|
|
1502
1545
|
}
|
|
1503
1546
|
checkFields(r.fields, "records");
|
|
1547
|
+
if (r.fields.some((f) => f.type === "file") || r.items && r.items.fields.some((f) => f.type === "file")) {
|
|
1548
|
+
throw new Error(
|
|
1549
|
+
"claude-artifact-framework: `file` fields live in the flat data pool (panel blocks or steps) \u2014 they are not supported inside records or items."
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1504
1552
|
const seen = /* @__PURE__ */ new Set();
|
|
1505
1553
|
for (const f of r.fields) {
|
|
1506
1554
|
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
|
|
@@ -1704,10 +1752,30 @@ function WorkspaceLayout({ spec, ctx }) {
|
|
|
1704
1752
|
);
|
|
1705
1753
|
}
|
|
1706
1754
|
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
|
|
1755
|
+
function loadScript(src) {
|
|
1756
|
+
return new Promise((resolve, reject) => {
|
|
1757
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
1758
|
+
if (existing) {
|
|
1759
|
+
if (existing.dataset.cafReady === "1") return resolve();
|
|
1760
|
+
existing.addEventListener("load", resolve);
|
|
1761
|
+
existing.addEventListener("error", () => reject(new Error(`failed to load ${src}`)));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const tag = document.createElement("script");
|
|
1765
|
+
tag.src = src;
|
|
1766
|
+
tag.onload = () => {
|
|
1767
|
+
tag.dataset.cafReady = "1";
|
|
1768
|
+
resolve();
|
|
1769
|
+
};
|
|
1770
|
+
tag.onerror = () => reject(new Error(`failed to load ${src}`));
|
|
1771
|
+
document.head.appendChild(tag);
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1707
1774
|
function createApp(spec = {}) {
|
|
1708
1775
|
const layout = validate(spec);
|
|
1709
1776
|
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
1710
1777
|
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
1778
|
+
const files = {};
|
|
1711
1779
|
return function App() {
|
|
1712
1780
|
if (!hasReact()) {
|
|
1713
1781
|
throw new Error(
|
|
@@ -1715,6 +1783,18 @@ function createApp(spec = {}) {
|
|
|
1715
1783
|
);
|
|
1716
1784
|
}
|
|
1717
1785
|
const [, force] = useState(0);
|
|
1786
|
+
const [libsState, setLibsState] = useState(spec.libs && spec.libs.length ? "loading" : "ready");
|
|
1787
|
+
const [libsError, setLibsError] = useState(null);
|
|
1788
|
+
useEffect(() => {
|
|
1789
|
+
if (!spec.libs || !spec.libs.length) return;
|
|
1790
|
+
Promise.all(spec.libs.map(loadScript)).then(
|
|
1791
|
+
() => setLibsState("ready"),
|
|
1792
|
+
(e) => {
|
|
1793
|
+
setLibsError(String(e && e.message || e));
|
|
1794
|
+
setLibsState("error");
|
|
1795
|
+
}
|
|
1796
|
+
);
|
|
1797
|
+
}, []);
|
|
1718
1798
|
useEffect(() => {
|
|
1719
1799
|
const unsubscribe = state.subscribe(() => force((n) => n + 1));
|
|
1720
1800
|
if (state.isHydrated()) force((n) => n + 1);
|
|
@@ -1728,6 +1808,8 @@ function createApp(spec = {}) {
|
|
|
1728
1808
|
go: state.go,
|
|
1729
1809
|
back: state.back,
|
|
1730
1810
|
setStep: (n) => state.setView({ step: n }),
|
|
1811
|
+
files,
|
|
1812
|
+
viewerId: state.getViewerId(),
|
|
1731
1813
|
setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
|
|
1732
1814
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
1733
1815
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
|
|
@@ -1743,7 +1825,12 @@ function createApp(spec = {}) {
|
|
|
1743
1825
|
spec.title ? createElement("h1", null, spec.title) : null,
|
|
1744
1826
|
spec.subtitle ? createElement("p", null, spec.subtitle) : null
|
|
1745
1827
|
),
|
|
1746
|
-
|
|
1828
|
+
libsState === "error" ? createElement(
|
|
1829
|
+
"div",
|
|
1830
|
+
{ className: "caf-block caf-block-error" },
|
|
1831
|
+
createElement("strong", null, "A library failed to load"),
|
|
1832
|
+
createElement("code", null, libsError)
|
|
1833
|
+
) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : createElement(Layout, { spec, ctx })
|
|
1747
1834
|
);
|
|
1748
1835
|
};
|
|
1749
1836
|
}
|
|
@@ -1916,6 +2003,7 @@ var BASE_CSS = `
|
|
|
1916
2003
|
.caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1917
2004
|
.caf-chevron { transition: transform 0.15s ease; color: var(--caf-muted); }
|
|
1918
2005
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
2006
|
+
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
1919
2007
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1920
2008
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1921
2009
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|