claude-artifact-framework 0.5.2 → 0.6.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 +67 -6
- package/dist/index.esm.js +200 -14
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +25 -9
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +58 -2
- package/src/blocks.js +34 -0
- package/src/chat.js +34 -3
- package/src/records.js +107 -9
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ mirrors npm packages automatically.
|
|
|
12
12
|
### As a global script (`<script>` tag)
|
|
13
13
|
|
|
14
14
|
```html
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.6.0/dist/index.global.js"></script>
|
|
16
16
|
<script>
|
|
17
17
|
const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
|
|
18
18
|
</script>
|
|
@@ -27,11 +27,11 @@ mirrors npm packages automatically.
|
|
|
27
27
|
createStore,
|
|
28
28
|
createPersistedStore,
|
|
29
29
|
createSharedStore,
|
|
30
|
-
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
30
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.6.0/dist/index.esm.js";
|
|
31
31
|
</script>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Pin a version (`@0.
|
|
34
|
+
Pin a version (`@0.6.0`) for reproducible artifacts, or drop the version
|
|
35
35
|
(`claude-artifact-framework/dist/...`) to always get the latest release.
|
|
36
36
|
|
|
37
37
|
### Inside a React artifact
|
|
@@ -254,6 +254,67 @@ ArtifactKit.createApp({
|
|
|
254
254
|
that haven't been persisted yet. View state (which screen is open) stays
|
|
255
255
|
personal per viewer.
|
|
256
256
|
|
|
257
|
+
### `items` — a child collection per record
|
|
258
|
+
|
|
259
|
+
For "a thing with parts": invoices with line items, recipes with
|
|
260
|
+
ingredients, projects with tasks. One level deep by design; children edit
|
|
261
|
+
inline on the parent's detail screen (they're for small rows, not records
|
|
262
|
+
that deserve their own screen). The collection lives at `r.items` and is
|
|
263
|
+
available to `compute` / `title` / `subtitle` / `summary`:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
records: {
|
|
267
|
+
label: "factura",
|
|
268
|
+
fields: [{ key: "cliente", type: "text", required: true }],
|
|
269
|
+
items: {
|
|
270
|
+
label: "ítem",
|
|
271
|
+
fields: [
|
|
272
|
+
{ key: "concepto", type: "text" },
|
|
273
|
+
{ key: "cantidad", type: "number", value: 1, min: 0 },
|
|
274
|
+
{ key: "precio", type: "money", value: 0, min: 0 },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
compute: {
|
|
278
|
+
total: { label: "Total c/IVA", format: "money",
|
|
279
|
+
value: (r) => (r.items || []).reduce((s, i) => s + i.cantidad * i.precio, 0) * 1.21 },
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`items` keys: `label`, `fields`, `empty`. Each child gets a framework
|
|
285
|
+
`id`; a parent field can't be named `items`.
|
|
286
|
+
|
|
287
|
+
### `actions` — one-tap buttons that mutate data
|
|
288
|
+
|
|
289
|
+
On records rows (mark done, +1, vote — without opening the record):
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
records: {
|
|
293
|
+
fields: [{ key: "nombre", type: "text" }, { key: "veces", type: "number", value: 0 }],
|
|
294
|
+
actions: [
|
|
295
|
+
{ label: "Hecho hoy", run: (r) => { r.veces = (r.veces || 0) + 1; } },
|
|
296
|
+
{ label: "Reset", kind: "danger", when: (r) => r.veces > 0, run: (r) => { r.veces = 0; } },
|
|
297
|
+
],
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
And as a block in `panel` (quick counters):
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
blocks: [
|
|
305
|
+
{ actions: [{ label: "+1 vaso de agua", kind: "primary", run: (d) => { d.agua = (d.agua || 0) + 1; } }] },
|
|
306
|
+
{ output: (d) => [{ label: "Agua hoy", value: d.agua || 0, big: true }] },
|
|
307
|
+
]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Action keys: `label`, `run`, optional `when` (hides the button when false)
|
|
311
|
+
and `kind` (`"primary"` / `"danger"`). Row actions receive the record;
|
|
312
|
+
block actions receive data. Changes persist like any other update.
|
|
313
|
+
|
|
314
|
+
Chat bubbles render the assistant's **markdown** (bold, code, lists,
|
|
315
|
+
headings) automatically — HTML is escaped first, so model output can't
|
|
316
|
+
inject markup.
|
|
317
|
+
|
|
257
318
|
## API reference
|
|
258
319
|
|
|
259
320
|
Everything an app can declare, in one place.
|
|
@@ -266,7 +327,7 @@ list), `steps` (wizard), `chat` (conversation with Claude). Each requires its
|
|
|
266
327
|
matching key (`blocks` / `records` / `steps` / `chat`).
|
|
267
328
|
|
|
268
329
|
**Block types** (entries of `blocks`): `fields`, `output`, `text`, `list`,
|
|
269
|
-
`chart`, `timer`, `custom`. One type per entry, plus optional `title`, `empty`,
|
|
330
|
+
`chart`, `timer`, `actions`, `custom`. One type per entry, plus optional `title`, `empty`,
|
|
270
331
|
`wide`. In the `records` layout, `blocks` render below the list (never on the
|
|
271
332
|
detail screen); their functions receive the same `data` object as everywhere
|
|
272
333
|
else — the collection lives at `d.records`.
|
|
@@ -278,8 +339,8 @@ else — the collection lives at `d.records`.
|
|
|
278
339
|
An unknown `type` throws naming the valid ones.
|
|
279
340
|
|
|
280
341
|
**`records` keys**: `label`, `fields`, `title(r)`, `subtitle(r)`, `sort(a,b)`,
|
|
281
|
-
`summary(all)`, `empty`, `search`, `compute`. `id` is
|
|
282
|
-
reserved.
|
|
342
|
+
`summary(all)`, `empty`, `search`, `compute`, `items`, `actions`. `id` is
|
|
343
|
+
framework-assigned and reserved on records and items alike.
|
|
283
344
|
|
|
284
345
|
**`steps` keys** (per step): `title`, `text`, `fields`, `result(d)`. Field
|
|
285
346
|
keys must be unique across steps.
|
package/dist/index.esm.js
CHANGED
|
@@ -694,6 +694,31 @@ function TimerBlock({ spec, ctx }) {
|
|
|
694
694
|
)
|
|
695
695
|
);
|
|
696
696
|
}
|
|
697
|
+
function ActionsBlock({ spec, ctx }) {
|
|
698
|
+
const actions = spec.actions || [];
|
|
699
|
+
const visible = actions.filter((a) => !a.when || a.when(ctx.data));
|
|
700
|
+
return createElement(
|
|
701
|
+
"div",
|
|
702
|
+
{ className: "caf-block caf-actions" },
|
|
703
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
704
|
+
createElement(
|
|
705
|
+
"div",
|
|
706
|
+
{ className: "caf-actions-row" },
|
|
707
|
+
visible.map(
|
|
708
|
+
(a) => createElement(
|
|
709
|
+
"button",
|
|
710
|
+
{
|
|
711
|
+
key: a.label,
|
|
712
|
+
type: "button",
|
|
713
|
+
className: a.kind === "danger" ? "caf-btn caf-btn-danger" : a.kind === "primary" ? "caf-btn caf-btn-primary" : "caf-btn",
|
|
714
|
+
onClick: () => ctx.update((d) => a.run(d))
|
|
715
|
+
},
|
|
716
|
+
a.label
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
}
|
|
697
722
|
function CustomBlock({ spec, ctx }) {
|
|
698
723
|
const rendered = spec.custom(ctx);
|
|
699
724
|
if (typeof rendered === "string") {
|
|
@@ -708,6 +733,7 @@ var BLOCKS = {
|
|
|
708
733
|
list: ListBlock,
|
|
709
734
|
chart: ChartBlock,
|
|
710
735
|
timer: TimerBlock,
|
|
736
|
+
actions: ActionsBlock,
|
|
711
737
|
custom: CustomBlock
|
|
712
738
|
};
|
|
713
739
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
@@ -779,6 +805,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
779
805
|
}
|
|
780
806
|
function add() {
|
|
781
807
|
const rec = { id: makeId(), ...recordDefaults(spec.fields) };
|
|
808
|
+
if (spec.items) rec.items = [];
|
|
782
809
|
ctx.update((d) => {
|
|
783
810
|
d.records = [...d.records, rec];
|
|
784
811
|
});
|
|
@@ -807,15 +834,37 @@ function ListScreen({ spec, ctx }) {
|
|
|
807
834
|
spec.search && q && records.length === 0 && total > 0 ? createElement("div", { className: "caf-empty" }, `Nothing matches "${query}".`) : null,
|
|
808
835
|
records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
|
|
809
836
|
(rec) => createElement(
|
|
810
|
-
"
|
|
811
|
-
{
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
837
|
+
"div",
|
|
838
|
+
{ key: rec.id, className: "caf-row-wrap" },
|
|
839
|
+
createElement(
|
|
840
|
+
"button",
|
|
841
|
+
{
|
|
842
|
+
type: "button",
|
|
843
|
+
className: "caf-row",
|
|
844
|
+
onClick: () => ctx.go("record", rec.id)
|
|
845
|
+
},
|
|
846
|
+
createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
|
|
847
|
+
spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
|
|
848
|
+
),
|
|
849
|
+
spec.actions ? createElement(
|
|
850
|
+
"div",
|
|
851
|
+
{ className: "caf-row-actions" },
|
|
852
|
+
spec.actions.filter((a) => !a.when || a.when(rec)).map(
|
|
853
|
+
(a) => createElement(
|
|
854
|
+
"button",
|
|
855
|
+
{
|
|
856
|
+
key: a.label,
|
|
857
|
+
type: "button",
|
|
858
|
+
className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
|
|
859
|
+
onClick: () => ctx.update((d) => {
|
|
860
|
+
const live = d.records.find((x) => x.id === rec.id);
|
|
861
|
+
if (live) a.run(live, d);
|
|
862
|
+
})
|
|
863
|
+
},
|
|
864
|
+
a.label
|
|
865
|
+
)
|
|
866
|
+
)
|
|
867
|
+
) : null
|
|
819
868
|
)
|
|
820
869
|
)
|
|
821
870
|
);
|
|
@@ -848,6 +897,7 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
848
897
|
})
|
|
849
898
|
})
|
|
850
899
|
),
|
|
900
|
+
spec.items ? createElement(ItemsEditor, { spec, ctx, record }) : null,
|
|
851
901
|
spec.compute ? createElement(
|
|
852
902
|
"div",
|
|
853
903
|
{ className: "caf-output-grid caf-computed" },
|
|
@@ -863,6 +913,66 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
863
913
|
) : null
|
|
864
914
|
);
|
|
865
915
|
}
|
|
916
|
+
function ItemsEditor({ spec, ctx, record }) {
|
|
917
|
+
const ispec = spec.items;
|
|
918
|
+
const label = ispec.label || "item";
|
|
919
|
+
function mutate(fn) {
|
|
920
|
+
ctx.update((d) => {
|
|
921
|
+
const rec = d.records.find((r) => r.id === record.id);
|
|
922
|
+
if (!rec) return;
|
|
923
|
+
if (!Array.isArray(rec.items)) rec.items = [];
|
|
924
|
+
fn(rec);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
const items = Array.isArray(record.items) ? record.items : [];
|
|
928
|
+
return createElement(
|
|
929
|
+
"div",
|
|
930
|
+
{ className: "caf-items" },
|
|
931
|
+
createElement(
|
|
932
|
+
"div",
|
|
933
|
+
{ className: "caf-toolbar" },
|
|
934
|
+
createElement("span", { className: "caf-count" }, `${items.length} ${label}${items.length === 1 ? "" : "s"}`),
|
|
935
|
+
createElement(
|
|
936
|
+
"button",
|
|
937
|
+
{
|
|
938
|
+
type: "button",
|
|
939
|
+
className: "caf-btn caf-btn-sm",
|
|
940
|
+
onClick: () => mutate((rec) => rec.items.push({ id: makeId(), ...recordDefaults(ispec.fields) }))
|
|
941
|
+
},
|
|
942
|
+
`Add ${label}`
|
|
943
|
+
)
|
|
944
|
+
),
|
|
945
|
+
items.length === 0 ? createElement("div", { className: "caf-empty caf-empty-sm" }, ispec.empty || `No ${label}s yet.`) : items.map(
|
|
946
|
+
(item) => createElement(
|
|
947
|
+
"div",
|
|
948
|
+
{ key: item.id, className: "caf-item-row" },
|
|
949
|
+
ispec.fields.map(
|
|
950
|
+
(field) => createElement(Field, {
|
|
951
|
+
key: field.key,
|
|
952
|
+
field,
|
|
953
|
+
value: item[field.key],
|
|
954
|
+
onChange: (v) => mutate((rec) => {
|
|
955
|
+
const it = rec.items.find((x) => x.id === item.id);
|
|
956
|
+
if (it) it[field.key] = v;
|
|
957
|
+
})
|
|
958
|
+
})
|
|
959
|
+
),
|
|
960
|
+
createElement(
|
|
961
|
+
"button",
|
|
962
|
+
{
|
|
963
|
+
type: "button",
|
|
964
|
+
className: "caf-btn caf-btn-danger caf-btn-sm caf-item-remove",
|
|
965
|
+
"aria-label": `Remove ${label}`,
|
|
966
|
+
onClick: () => mutate((rec) => {
|
|
967
|
+
rec.items = rec.items.filter((x) => x.id !== item.id);
|
|
968
|
+
})
|
|
969
|
+
},
|
|
970
|
+
"\u2715"
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
866
976
|
function RecordsLayout({ spec, ctx }) {
|
|
867
977
|
const rspec = spec.records;
|
|
868
978
|
if (ctx.view.screen === "record") {
|
|
@@ -1049,6 +1159,24 @@ function runTools(calls, tools, ctx) {
|
|
|
1049
1159
|
}
|
|
1050
1160
|
});
|
|
1051
1161
|
}
|
|
1162
|
+
var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1163
|
+
function renderMarkdown(text) {
|
|
1164
|
+
const codeBlocks = [];
|
|
1165
|
+
let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
|
|
1166
|
+
codeBlocks.push(code.replace(/\n$/, ""));
|
|
1167
|
+
return "\0" + (codeBlocks.length - 1) + "\0";
|
|
1168
|
+
});
|
|
1169
|
+
t = escHtml(t);
|
|
1170
|
+
t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
1171
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
1172
|
+
t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
1173
|
+
t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
|
|
1174
|
+
t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
|
|
1175
|
+
t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
|
|
1176
|
+
t = t.replace(/\n/g, "<br>");
|
|
1177
|
+
t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
|
|
1178
|
+
return t;
|
|
1179
|
+
}
|
|
1052
1180
|
function Bubble({ m }) {
|
|
1053
1181
|
if (m.role === "tools") {
|
|
1054
1182
|
return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
|
|
@@ -1062,7 +1190,11 @@ function Bubble({ m }) {
|
|
|
1062
1190
|
createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
|
|
1063
1191
|
);
|
|
1064
1192
|
}
|
|
1065
|
-
|
|
1193
|
+
if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
|
|
1194
|
+
return createElement("div", {
|
|
1195
|
+
className: "caf-msg caf-msg-assistant",
|
|
1196
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
|
|
1197
|
+
});
|
|
1066
1198
|
}
|
|
1067
1199
|
function ChatLayout({ spec, ctx }) {
|
|
1068
1200
|
const c = spec.chat;
|
|
@@ -1135,9 +1267,9 @@ Continue with this information. If you have everything you need, answer in natur
|
|
|
1135
1267
|
createElement(
|
|
1136
1268
|
"div",
|
|
1137
1269
|
{ className: "caf-block caf-chat-log" },
|
|
1138
|
-
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant"
|
|
1270
|
+
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
|
|
1139
1271
|
log.map((m, i) => createElement(Bubble, { key: i, m })),
|
|
1140
|
-
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live"
|
|
1272
|
+
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
|
|
1141
1273
|
busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
|
|
1142
1274
|
createElement("div", { ref: endRef })
|
|
1143
1275
|
),
|
|
@@ -1186,6 +1318,23 @@ function checkFields(fields, where) {
|
|
|
1186
1318
|
}
|
|
1187
1319
|
}
|
|
1188
1320
|
}
|
|
1321
|
+
var ACTION_KEYS = ["label", "run", "when", "kind"];
|
|
1322
|
+
function checkActions(actions, where) {
|
|
1323
|
+
if (!Array.isArray(actions)) {
|
|
1324
|
+
throw new Error(`claude-artifact-framework: ${where} must be an array of { label, run }.`);
|
|
1325
|
+
}
|
|
1326
|
+
actions.forEach((a, i) => {
|
|
1327
|
+
if (!a || typeof a.label !== "string" || typeof a.run !== "function") {
|
|
1328
|
+
throw new Error(`claude-artifact-framework: ${where}[${i}] needs a \`label\` string and a \`run\` function.`);
|
|
1329
|
+
}
|
|
1330
|
+
const unknown = Object.keys(a).filter((k) => !ACTION_KEYS.includes(k));
|
|
1331
|
+
if (unknown.length) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`claude-artifact-framework: ${where}[${i}] has unknown keys (${unknown.join(", ")}). Valid keys: ${ACTION_KEYS.join(", ")}.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1189
1338
|
function checkBlocks(blocks) {
|
|
1190
1339
|
if (!Array.isArray(blocks)) {
|
|
1191
1340
|
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
@@ -1209,6 +1358,7 @@ function checkBlocks(blocks) {
|
|
|
1209
1358
|
);
|
|
1210
1359
|
}
|
|
1211
1360
|
checkFields(block.fields, `blocks[${i}]`);
|
|
1361
|
+
if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
|
|
1212
1362
|
});
|
|
1213
1363
|
}
|
|
1214
1364
|
function validate(spec) {
|
|
@@ -1285,7 +1435,7 @@ function validate(spec) {
|
|
|
1285
1435
|
'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
|
|
1286
1436
|
);
|
|
1287
1437
|
}
|
|
1288
|
-
const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute"];
|
|
1438
|
+
const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
|
|
1289
1439
|
const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
|
|
1290
1440
|
if (unknownR.length) {
|
|
1291
1441
|
throw new Error(
|
|
@@ -1300,8 +1450,28 @@ function validate(spec) {
|
|
|
1300
1450
|
seen.add(f.key);
|
|
1301
1451
|
}
|
|
1302
1452
|
if (spec.blocks) checkBlocks(spec.blocks);
|
|
1453
|
+
if (r.actions) checkActions(r.actions, "records.actions");
|
|
1454
|
+
if (r.items) {
|
|
1455
|
+
const ITEMS_KEYS = ["label", "fields", "empty"];
|
|
1456
|
+
const unknownI = Object.keys(r.items).filter((k) => !ITEMS_KEYS.includes(k));
|
|
1457
|
+
if (unknownI.length) {
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
`claude-artifact-framework: records.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
|
|
1463
|
+
throw new Error("claude-artifact-framework: records.items needs `fields: [...]` \u2014 the same field declarations as everywhere else.");
|
|
1464
|
+
}
|
|
1465
|
+
checkFields(r.items.fields, "records.items");
|
|
1466
|
+
for (const f of r.items.fields) {
|
|
1467
|
+
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
|
|
1468
|
+
}
|
|
1469
|
+
if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
|
|
1470
|
+
throw new Error('claude-artifact-framework: a records field cannot be named "items" when `items` is declared \u2014 that key holds the child collection.');
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1303
1473
|
for (const [key, c] of Object.entries(r.compute || {})) {
|
|
1304
|
-
if (seen.has(key) || key === "id") {
|
|
1474
|
+
if (seen.has(key) || key === "id" || r.items && key === "items") {
|
|
1305
1475
|
throw new Error(
|
|
1306
1476
|
`claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
|
|
1307
1477
|
);
|
|
@@ -1561,6 +1731,22 @@ var BASE_CSS = `
|
|
|
1561
1731
|
.caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1562
1732
|
.caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
1563
1733
|
.caf-records-blocks { margin-top: 1rem; }
|
|
1734
|
+
.caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
|
|
1735
|
+
.caf-row-wrap .caf-row { flex: 1; min-width: 0; }
|
|
1736
|
+
.caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
|
|
1737
|
+
.caf-btn-sm { font-size: 0.78rem; padding: 0.3rem 0.6rem; border-radius: 6px; }
|
|
1738
|
+
.caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
1739
|
+
.caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
1740
|
+
.caf-item-row .caf-field { flex: 1; min-width: 0; }
|
|
1741
|
+
.caf-item-row .caf-field label { font-size: 0.68rem; }
|
|
1742
|
+
.caf-item-remove { flex: none; margin-bottom: 0.15rem; }
|
|
1743
|
+
.caf-empty-sm { padding: 0.6rem; font-size: 0.82rem; }
|
|
1744
|
+
.caf-actions-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }
|
|
1745
|
+
.caf-code { background: var(--caf-sunken); border-radius: 6px; padding: 0.55rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0.3rem 0; font-family: ui-monospace, monospace; }
|
|
1746
|
+
.caf-msg code { background: color-mix(in srgb, currentColor 12%, transparent); border-radius: 4px; padding: 0.05rem 0.3rem; font-size: 0.85em; font-family: ui-monospace, monospace; }
|
|
1747
|
+
.caf-md-h { display: block; margin-top: 0.3rem; }
|
|
1748
|
+
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
1749
|
+
.caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
1564
1750
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1565
1751
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1566
1752
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|