claude-artifact-framework 0.12.0 → 0.13.1
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 +50 -1
- package/dist/index.esm.js +135 -49
- 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 +1 -1
- package/src/app.js +150 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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
|
@@ -25,7 +25,7 @@ const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "col
|
|
|
25
25
|
// silently rendering nothing — a blank pane is indistinguishable from a bug.
|
|
26
26
|
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents"];
|
|
27
27
|
|
|
28
|
-
const DOCUMENTS_KEYS = ["label", "title", "blocks", "empty"];
|
|
28
|
+
const DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
|
|
29
29
|
const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
|
|
30
30
|
const ITEMS_KEYS = ["label", "fields", "empty"];
|
|
31
31
|
|
|
@@ -219,9 +219,14 @@ function validate(spec) {
|
|
|
219
219
|
}
|
|
220
220
|
if (layout === "documents") {
|
|
221
221
|
const d = spec.documents;
|
|
222
|
-
if (!d ||
|
|
222
|
+
if (!d || typeof d !== "object" || (!d.blocks && !d.types)) {
|
|
223
223
|
throw new Error(
|
|
224
|
-
'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }`
|
|
224
|
+
'claude-artifact-framework: layout "documents" needs `documents: { blocks: [...] }` (every document repeats the same blocks) or `documents: { types: {...} }` (each document is one of several kinds).'
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (d.blocks && d.types) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"claude-artifact-framework: documents takes `blocks` (uniform documents) OR `types` (typed documents) — not both."
|
|
225
230
|
);
|
|
226
231
|
}
|
|
227
232
|
const unknownD = Object.keys(d).filter((k) => !DOCUMENTS_KEYS.includes(k));
|
|
@@ -233,18 +238,47 @@ function validate(spec) {
|
|
|
233
238
|
if (d.title !== undefined && typeof d.title !== "function") {
|
|
234
239
|
throw new Error("claude-artifact-framework: documents.title must be a function of the document's data returning the tab label.");
|
|
235
240
|
}
|
|
236
|
-
checkBlocks(d.blocks);
|
|
237
241
|
// Inside documents both chat and file are PER DOCUMENT: the scoped ctx
|
|
238
242
|
// gives each document its own data.chat and its own ctx.files. One chat
|
|
239
243
|
// block in the declaration = one conversation per document.
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
const checkDocBlocks = (blocks, where) => {
|
|
245
|
+
checkBlocks(blocks);
|
|
246
|
+
const flat = flattenBlocks(blocks);
|
|
247
|
+
if (flat.filter((b) => b && b.chat).length > 1) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`claude-artifact-framework: only one chat block per document (${where}) — each document's conversation persists under its own data.chat and there is exactly one.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
checkRecordsBlockKeys(flat);
|
|
253
|
+
};
|
|
254
|
+
if (d.blocks) {
|
|
255
|
+
if (!Array.isArray(d.blocks) || d.blocks.length === 0) {
|
|
256
|
+
throw new Error("claude-artifact-framework: documents.blocks must be a non-empty array of blocks.");
|
|
257
|
+
}
|
|
258
|
+
checkDocBlocks(d.blocks, "documents.blocks");
|
|
259
|
+
} else {
|
|
260
|
+
const names = Object.keys(d.types || {});
|
|
261
|
+
if (!names.length) {
|
|
262
|
+
throw new Error("claude-artifact-framework: documents.types needs at least one type, e.g. `types: { nota: { blocks: [...] } }`.");
|
|
263
|
+
}
|
|
264
|
+
for (const name of names) {
|
|
265
|
+
const t = d.types[name];
|
|
266
|
+
const TYPE_KEYS = ["label", "title", "blocks", "empty"];
|
|
267
|
+
const unknownT = Object.keys(t || {}).filter((k) => !TYPE_KEYS.includes(k));
|
|
268
|
+
if (unknownT.length) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`claude-artifact-framework: documents.types.${name} has unknown keys (${unknownT.join(", ")}). Valid keys: ${TYPE_KEYS.join(", ")}.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (!t || !Array.isArray(t.blocks) || t.blocks.length === 0) {
|
|
274
|
+
throw new Error(`claude-artifact-framework: documents.types.${name} needs a non-empty \`blocks\` array.`);
|
|
275
|
+
}
|
|
276
|
+
if (t.title !== undefined && typeof t.title !== "function") {
|
|
277
|
+
throw new Error(`claude-artifact-framework: documents.types.${name}.title must be a function of the document's data.`);
|
|
278
|
+
}
|
|
279
|
+
checkDocBlocks(t.blocks, `documents.types.${name}`);
|
|
280
|
+
}
|
|
246
281
|
}
|
|
247
|
-
checkRecordsBlockKeys(docFlat);
|
|
248
282
|
return layout;
|
|
249
283
|
}
|
|
250
284
|
if (layout === "records") {
|
|
@@ -574,7 +608,7 @@ function docId() {
|
|
|
574
608
|
|
|
575
609
|
const DOC_TAB_KEY = "caf:doc";
|
|
576
610
|
|
|
577
|
-
function docTitle(dspec, data) {
|
|
611
|
+
function docTitle(dspec, data, untitled) {
|
|
578
612
|
if (dspec.title) {
|
|
579
613
|
try {
|
|
580
614
|
const t = dspec.title(data);
|
|
@@ -583,35 +617,52 @@ function docTitle(dspec, data) {
|
|
|
583
617
|
/* fall through to the default below */
|
|
584
618
|
}
|
|
585
619
|
}
|
|
586
|
-
// Default: the first text-ish field — same convention records uses.
|
|
620
|
+
// Default: the first text-ish field — same convention records uses. Typed
|
|
621
|
+
// documents fall back to their type label instead of "(untitled)".
|
|
587
622
|
for (const block of flattenBlocks(dspec.blocks)) {
|
|
588
623
|
for (const f of block.fields || []) {
|
|
589
624
|
if (!f.type || f.type === "text" || f.type === "textarea" || f.type === "select") {
|
|
590
625
|
const v = data[f.key];
|
|
591
626
|
if (v !== "" && v !== undefined && v !== null) return String(v);
|
|
592
|
-
return "(untitled)";
|
|
627
|
+
return untitled || "(untitled)";
|
|
593
628
|
}
|
|
594
629
|
}
|
|
595
630
|
}
|
|
596
|
-
return "(untitled)";
|
|
631
|
+
return untitled || "(untitled)";
|
|
597
632
|
}
|
|
598
633
|
|
|
599
634
|
function DocumentsLayout({ spec, ctx }) {
|
|
600
635
|
const dspec = spec.documents;
|
|
601
636
|
const label = dspec.label || "document";
|
|
602
637
|
const docs = Array.isArray(ctx.data.docs) ? ctx.data.docs : [];
|
|
603
|
-
|
|
638
|
+
// Typed documents: each doc records which kind it is (`doc.type`, on the
|
|
639
|
+
// envelope — never inside the document's data) and renders ITS type's
|
|
640
|
+
// blocks. Uniform documents are the one-type degenerate case.
|
|
641
|
+
const types = dspec.types || null;
|
|
642
|
+
const typeNames = types ? Object.keys(types) : [];
|
|
643
|
+
const specFor = (doc) => (types ? types[doc.type] : dspec);
|
|
644
|
+
const typeLabel = (name) => (types[name] && types[name].label) || name;
|
|
645
|
+
const [picking, setPicking] = useState(false);
|
|
604
646
|
|
|
605
647
|
const storedId = ctx.view.tabs ? ctx.view.tabs[DOC_TAB_KEY] : undefined;
|
|
606
648
|
// A deleted or stale id falls back to the first document, never a blank pane.
|
|
607
649
|
const active = docs.find((doc) => doc.id === storedId) || docs[0];
|
|
608
650
|
|
|
609
|
-
function add() {
|
|
610
|
-
const
|
|
651
|
+
function add(typeName) {
|
|
652
|
+
const tspec = types ? types[typeName] : dspec;
|
|
653
|
+
const doc = { id: docId(), ...(types ? { type: typeName } : {}), data: docDefaults(tspec) };
|
|
611
654
|
ctx.update((d) => {
|
|
612
655
|
d.docs = [...(d.docs || []), doc];
|
|
613
656
|
});
|
|
614
657
|
ctx.setTab(DOC_TAB_KEY, doc.id);
|
|
658
|
+
setPicking(false);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// One type (or uniform) creates directly; several ask which kind first.
|
|
662
|
+
function onAdd() {
|
|
663
|
+
if (!types) return add();
|
|
664
|
+
if (typeNames.length === 1) return add(typeNames[0]);
|
|
665
|
+
setPicking((p) => !p);
|
|
615
666
|
}
|
|
616
667
|
|
|
617
668
|
function close(id) {
|
|
@@ -626,6 +677,16 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
626
677
|
}
|
|
627
678
|
}
|
|
628
679
|
|
|
680
|
+
const typePicker = types && picking
|
|
681
|
+
? h(
|
|
682
|
+
"div",
|
|
683
|
+
{ className: "caf-doc-typemenu" },
|
|
684
|
+
typeNames.map((name) =>
|
|
685
|
+
h("button", { key: name, type: "button", className: "caf-btn caf-btn-sm", onClick: () => add(name) }, typeLabel(name))
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
: null;
|
|
689
|
+
|
|
629
690
|
if (!active) {
|
|
630
691
|
return h(
|
|
631
692
|
"div",
|
|
@@ -634,10 +695,37 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
634
695
|
"div",
|
|
635
696
|
{ className: "caf-block caf-empty" },
|
|
636
697
|
h("p", null, dspec.empty || `No ${label}s yet. Create the first one.`),
|
|
637
|
-
|
|
698
|
+
types
|
|
699
|
+
? h(
|
|
700
|
+
"div",
|
|
701
|
+
{ className: "caf-doc-typemenu" },
|
|
702
|
+
typeNames.map((name) =>
|
|
703
|
+
h("button", { key: name, type: "button", className: "caf-btn caf-btn-primary", onClick: () => add(name) }, `New ${typeLabel(name)}`)
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
: h("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: () => add() }, `New ${label}`)
|
|
707
|
+
)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const aspec = specFor(active);
|
|
712
|
+
// A document whose type was removed from the spec: loud, named, contained —
|
|
713
|
+
// the tab (and its ✕) stays usable, the pane explains instead of crashing.
|
|
714
|
+
if (!aspec) {
|
|
715
|
+
return h(
|
|
716
|
+
"div",
|
|
717
|
+
{ className: "caf-documents" },
|
|
718
|
+
h(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
719
|
+
typePicker,
|
|
720
|
+
h(
|
|
721
|
+
"div",
|
|
722
|
+
{ className: "caf-block caf-block-error" },
|
|
723
|
+
h("strong", null, `This ${label}'s type "${active.type}" no longer exists`),
|
|
724
|
+
h("code", null, `valid types: ${typeNames.join(", ")}`)
|
|
638
725
|
)
|
|
639
726
|
);
|
|
640
727
|
}
|
|
728
|
+
const defaults = docDefaults(aspec);
|
|
641
729
|
|
|
642
730
|
// Stored data merges over declared defaults, so a field added in a later
|
|
643
731
|
// edit of the spec falls through to its default on older documents (B3).
|
|
@@ -660,39 +748,51 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
660
748
|
return h(
|
|
661
749
|
"div",
|
|
662
750
|
{ className: "caf-documents" },
|
|
663
|
-
h(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
751
|
+
h(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
752
|
+
typePicker,
|
|
753
|
+
h(Panel, { spec: { blocks: aspec.blocks }, ctx: scoped })
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function DocTabbar({ docs, active, dspec, types, close, onAdd, label, ctx }) {
|
|
758
|
+
const tabTitle = (doc) => {
|
|
759
|
+
const tspec = types ? types[doc.type] : dspec;
|
|
760
|
+
if (!tspec) return doc.type; // removed type: the name is the best label left
|
|
761
|
+
const merged = { ...docDefaults(tspec), ...doc.data };
|
|
762
|
+
const fallback = types ? (tspec.label || doc.type) : undefined;
|
|
763
|
+
return docTitle(tspec, merged, fallback);
|
|
764
|
+
};
|
|
765
|
+
return h(
|
|
766
|
+
"div",
|
|
767
|
+
{ className: "caf-tabbar", role: "tablist" },
|
|
768
|
+
docs.map((doc) =>
|
|
769
|
+
h(
|
|
770
|
+
"span",
|
|
771
|
+
{ key: doc.id, className: "caf-doc-tab" },
|
|
667
772
|
h(
|
|
668
|
-
"
|
|
669
|
-
{
|
|
670
|
-
|
|
671
|
-
"
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"
|
|
683
|
-
{
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
onClick: () => close(doc.id),
|
|
688
|
-
},
|
|
689
|
-
"✕"
|
|
690
|
-
)
|
|
773
|
+
"button",
|
|
774
|
+
{
|
|
775
|
+
type: "button",
|
|
776
|
+
role: "tab",
|
|
777
|
+
"aria-selected": doc.id === active.id,
|
|
778
|
+
className: doc.id === active.id ? "caf-tab caf-tab-active" : "caf-tab",
|
|
779
|
+
onClick: () => ctx.setTab(DOC_TAB_KEY, doc.id),
|
|
780
|
+
},
|
|
781
|
+
tabTitle(doc)
|
|
782
|
+
),
|
|
783
|
+
h(
|
|
784
|
+
"button",
|
|
785
|
+
{
|
|
786
|
+
type: "button",
|
|
787
|
+
className: "caf-doc-close",
|
|
788
|
+
"aria-label": `Close ${label}`,
|
|
789
|
+
onClick: () => close(doc.id),
|
|
790
|
+
},
|
|
791
|
+
"✕"
|
|
691
792
|
)
|
|
692
|
-
)
|
|
693
|
-
h("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: add }, "+")
|
|
793
|
+
)
|
|
694
794
|
),
|
|
695
|
-
h(
|
|
795
|
+
h("button", { type: "button", className: "caf-tab caf-doc-add", "aria-label": `New ${label}`, onClick: onAdd }, "+")
|
|
696
796
|
);
|
|
697
797
|
}
|
|
698
798
|
|
|
@@ -1014,6 +1114,7 @@ const BASE_CSS = `
|
|
|
1014
1114
|
.caf-doc-add { font-size: 1rem; font-weight: 650; }
|
|
1015
1115
|
.caf-empty .caf-btn { margin-top: 0.6rem; }
|
|
1016
1116
|
.caf-records-block { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
1117
|
+
.caf-doc-typemenu { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
1017
1118
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
1018
1119
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
1019
1120
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|