claude-artifact-framework 0.5.2 → 0.7.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
@@ -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.5.2/dist/index.global.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.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.5.2/dist/index.esm.js";
30
+ } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.7.0/dist/index.esm.js";
31
31
  </script>
32
32
  ```
33
33
 
34
- Pin a version (`@0.5.2`) for reproducible artifacts, or drop the version
34
+ Pin a version (`@0.7.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,20 +327,21 @@ 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`.
273
334
 
274
335
  **Field types** (`type:` on any field): `text` (default), `textarea`,
275
336
  `number`, `money`, `percent`, `check` (checkbox), `date`, `select` (needs
276
- `options`). Other field keys: `key` (required), `label`, `value` (initial),
337
+ `options` an array, or `(d) => [...]` for choices that come from the
338
+ app's data, e.g. a "who paid" select fed by a user-entered group). Other field keys: `key` (required), `label`, `value` (initial),
277
339
  `required`, `min`, `max`, `step`, `placeholder`, `hint`, `rows`, `options`.
278
340
  An unknown `type` throws naming the valid ones.
279
341
 
280
342
  **`records` keys**: `label`, `fields`, `title(r)`, `subtitle(r)`, `sort(a,b)`,
281
- `summary(all)`, `empty`, `search`, `compute`. `id` is framework-assigned and
282
- reserved.
343
+ `summary(all)`, `empty`, `search`, `compute`, `items`, `actions`. `id` is
344
+ framework-assigned and reserved on records and items alike.
283
345
 
284
346
  **`steps` keys** (per step): `title`, `text`, `fields`, `result(d)`. Field
285
347
  keys must be unique across steps.
@@ -289,7 +351,21 @@ keys must be unique across steps.
289
351
  `{ description, input, run(input, ctx) }`.
290
352
 
291
353
  **Formats** (`format:` on output/chart/compute items): `money`, `percent`,
292
- `number`, `date`.
354
+ `number`, `date`. `percent` expects 0–100 (pass `ratio * 100`, not the raw
355
+ ratio).
356
+
357
+ **Quick-add pattern** — a lightweight alternative to `records` when you just
358
+ need "type something, press a button, it lands in a list":
359
+
360
+ ```js
361
+ blocks: [
362
+ { fields: [{ key: "nueva", label: "Persona", type: "text" }] },
363
+ { actions: [{ label: "Agregar", kind: "primary", run: (d) => {
364
+ if (d.nueva && d.nueva.trim()) { d.personas = [...(d.personas || []), d.nueva.trim()]; d.nueva = ""; }
365
+ }}]},
366
+ { text: (d) => (d.personas || []).join(", ") || "Sin personas todavía" },
367
+ ]
368
+ ```
293
369
 
294
370
  ## `storage`
295
371
 
package/dist/index.esm.js CHANGED
@@ -403,7 +403,7 @@ function validateField(field, value) {
403
403
  }
404
404
  return null;
405
405
  }
406
- function Field({ field, value, onChange }) {
406
+ function Field({ field, value, onChange, data }) {
407
407
  const error = validateField(field, value);
408
408
  const id = `caf-f-${field.key}`;
409
409
  const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
@@ -430,11 +430,12 @@ function Field({ field, value, onChange }) {
430
430
  if (field.type === "textarea") {
431
431
  control = createElement("textarea", { ...common, rows: field.rows || 3 });
432
432
  } else if (field.type === "select") {
433
+ const options = typeof field.options === "function" ? field.options(data) || [] : field.options || [];
433
434
  control = createElement(
434
435
  "select",
435
436
  common,
436
437
  createElement("option", { value: "" }, field.placeholder || "Select\u2026"),
437
- (field.options || []).map((opt) => {
438
+ options.map((opt) => {
438
439
  const val = typeof opt === "string" ? opt : opt.value;
439
440
  const label = typeof opt === "string" ? opt : opt.label;
440
441
  return createElement("option", { key: val, value: val }, label);
@@ -474,6 +475,7 @@ function FieldsBlock({ spec, ctx }) {
474
475
  (field) => createElement(Field, {
475
476
  key: field.key,
476
477
  field,
478
+ data: ctx.data,
477
479
  value: ctx.data[field.key],
478
480
  onChange: (v) => ctx.update((d) => {
479
481
  d[field.key] = v;
@@ -517,13 +519,15 @@ function TextBlock({ spec, ctx }) {
517
519
  }
518
520
  function ListBlock({ spec, ctx }) {
519
521
  const rows = (typeof spec.list === "function" ? spec.list(ctx.data) : spec.list) || [];
522
+ const heading = typeof spec.title === "string" ? spec.title : null;
523
+ const rowTitle2 = typeof spec.title === "function" ? spec.title : (r) => r.title ?? r.name ?? r.label ?? r.text ?? String(r);
520
524
  if (!rows.length) {
521
525
  return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing here yet.");
522
526
  }
523
- const title = spec.title || ((r) => r.title ?? r.name ?? r.label ?? String(r));
524
527
  return createElement(
525
528
  "div",
526
529
  { className: "caf-block caf-list" },
530
+ heading ? createElement("h2", { className: "caf-block-title" }, heading) : null,
527
531
  rows.map(
528
532
  (row, i) => createElement(
529
533
  "button",
@@ -533,7 +537,7 @@ function ListBlock({ spec, ctx }) {
533
537
  type: "button",
534
538
  onClick: spec.onSelect ? () => spec.onSelect(row, ctx) : void 0
535
539
  },
536
- createElement("span", { className: "caf-row-title" }, title(row)),
540
+ createElement("span", { className: "caf-row-title" }, rowTitle2(row)),
537
541
  spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(row)) : null
538
542
  )
539
543
  )
@@ -694,6 +698,31 @@ function TimerBlock({ spec, ctx }) {
694
698
  )
695
699
  );
696
700
  }
701
+ function ActionsBlock({ spec, ctx }) {
702
+ const actions = spec.actions || [];
703
+ const visible = actions.filter((a) => !a.when || a.when(ctx.data));
704
+ return createElement(
705
+ "div",
706
+ { className: "caf-block caf-actions" },
707
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
708
+ createElement(
709
+ "div",
710
+ { className: "caf-actions-row" },
711
+ visible.map(
712
+ (a) => createElement(
713
+ "button",
714
+ {
715
+ key: a.label,
716
+ type: "button",
717
+ className: a.kind === "danger" ? "caf-btn caf-btn-danger" : a.kind === "primary" ? "caf-btn caf-btn-primary" : "caf-btn",
718
+ onClick: () => ctx.update((d) => a.run(d))
719
+ },
720
+ a.label
721
+ )
722
+ )
723
+ )
724
+ );
725
+ }
697
726
  function CustomBlock({ spec, ctx }) {
698
727
  const rendered = spec.custom(ctx);
699
728
  if (typeof rendered === "string") {
@@ -708,6 +737,7 @@ var BLOCKS = {
708
737
  list: ListBlock,
709
738
  chart: ChartBlock,
710
739
  timer: TimerBlock,
740
+ actions: ActionsBlock,
711
741
  custom: CustomBlock
712
742
  };
713
743
  var BLOCK_NAMES = Object.keys(BLOCKS);
@@ -779,6 +809,7 @@ function ListScreen({ spec, ctx }) {
779
809
  }
780
810
  function add() {
781
811
  const rec = { id: makeId(), ...recordDefaults(spec.fields) };
812
+ if (spec.items) rec.items = [];
782
813
  ctx.update((d) => {
783
814
  d.records = [...d.records, rec];
784
815
  });
@@ -807,15 +838,37 @@ function ListScreen({ spec, ctx }) {
807
838
  spec.search && q && records.length === 0 && total > 0 ? createElement("div", { className: "caf-empty" }, `Nothing matches "${query}".`) : null,
808
839
  records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
809
840
  (rec) => createElement(
810
- "button",
811
- {
812
- key: rec.id,
813
- type: "button",
814
- className: "caf-row",
815
- onClick: () => ctx.go("record", rec.id)
816
- },
817
- createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
818
- spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
841
+ "div",
842
+ { key: rec.id, className: "caf-row-wrap" },
843
+ createElement(
844
+ "button",
845
+ {
846
+ type: "button",
847
+ className: "caf-row",
848
+ onClick: () => ctx.go("record", rec.id)
849
+ },
850
+ createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
851
+ spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
852
+ ),
853
+ spec.actions ? createElement(
854
+ "div",
855
+ { className: "caf-row-actions" },
856
+ spec.actions.filter((a) => !a.when || a.when(rec)).map(
857
+ (a) => createElement(
858
+ "button",
859
+ {
860
+ key: a.label,
861
+ type: "button",
862
+ className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
863
+ onClick: () => ctx.update((d) => {
864
+ const live = d.records.find((x) => x.id === rec.id);
865
+ if (live) a.run(live, d);
866
+ })
867
+ },
868
+ a.label
869
+ )
870
+ )
871
+ ) : null
819
872
  )
820
873
  )
821
874
  );
@@ -841,6 +894,7 @@ function DetailScreen({ spec, ctx, record }) {
841
894
  (field) => createElement(Field, {
842
895
  key: field.key,
843
896
  field,
897
+ data: ctx.data,
844
898
  value: record[field.key],
845
899
  onChange: (v) => ctx.update((d) => {
846
900
  const rec = d.records.find((r) => r.id === record.id);
@@ -848,6 +902,7 @@ function DetailScreen({ spec, ctx, record }) {
848
902
  })
849
903
  })
850
904
  ),
905
+ spec.items ? createElement(ItemsEditor, { spec, ctx, record }) : null,
851
906
  spec.compute ? createElement(
852
907
  "div",
853
908
  { className: "caf-output-grid caf-computed" },
@@ -863,6 +918,67 @@ function DetailScreen({ spec, ctx, record }) {
863
918
  ) : null
864
919
  );
865
920
  }
921
+ function ItemsEditor({ spec, ctx, record }) {
922
+ const ispec = spec.items;
923
+ const label = ispec.label || "item";
924
+ function mutate(fn) {
925
+ ctx.update((d) => {
926
+ const rec = d.records.find((r) => r.id === record.id);
927
+ if (!rec) return;
928
+ if (!Array.isArray(rec.items)) rec.items = [];
929
+ fn(rec);
930
+ });
931
+ }
932
+ const items = Array.isArray(record.items) ? record.items : [];
933
+ return createElement(
934
+ "div",
935
+ { className: "caf-items" },
936
+ createElement(
937
+ "div",
938
+ { className: "caf-toolbar" },
939
+ createElement("span", { className: "caf-count" }, `${items.length} ${label}${items.length === 1 ? "" : "s"}`),
940
+ createElement(
941
+ "button",
942
+ {
943
+ type: "button",
944
+ className: "caf-btn caf-btn-sm",
945
+ onClick: () => mutate((rec) => rec.items.push({ id: makeId(), ...recordDefaults(ispec.fields) }))
946
+ },
947
+ `Add ${label}`
948
+ )
949
+ ),
950
+ items.length === 0 ? createElement("div", { className: "caf-empty caf-empty-sm" }, ispec.empty || `No ${label}s yet.`) : items.map(
951
+ (item) => createElement(
952
+ "div",
953
+ { key: item.id, className: "caf-item-row" },
954
+ ispec.fields.map(
955
+ (field) => createElement(Field, {
956
+ key: field.key,
957
+ field,
958
+ data: ctx.data,
959
+ value: item[field.key],
960
+ onChange: (v) => mutate((rec) => {
961
+ const it = rec.items.find((x) => x.id === item.id);
962
+ if (it) it[field.key] = v;
963
+ })
964
+ })
965
+ ),
966
+ createElement(
967
+ "button",
968
+ {
969
+ type: "button",
970
+ className: "caf-btn caf-btn-danger caf-btn-sm caf-item-remove",
971
+ "aria-label": `Remove ${label}`,
972
+ onClick: () => mutate((rec) => {
973
+ rec.items = rec.items.filter((x) => x.id !== item.id);
974
+ })
975
+ },
976
+ "\u2715"
977
+ )
978
+ )
979
+ )
980
+ );
981
+ }
866
982
  function RecordsLayout({ spec, ctx }) {
867
983
  const rspec = spec.records;
868
984
  if (ctx.view.screen === "record") {
@@ -937,6 +1053,7 @@ function StepsLayout({ spec, ctx }) {
937
1053
  (field) => createElement(Field, {
938
1054
  key: field.key,
939
1055
  field,
1056
+ data: ctx.data,
940
1057
  value: ctx.data[field.key],
941
1058
  onChange: (v) => ctx.update((d) => {
942
1059
  d[field.key] = v;
@@ -1049,6 +1166,24 @@ function runTools(calls, tools, ctx) {
1049
1166
  }
1050
1167
  });
1051
1168
  }
1169
+ var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1170
+ function renderMarkdown(text) {
1171
+ const codeBlocks = [];
1172
+ let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
1173
+ codeBlocks.push(code.replace(/\n$/, ""));
1174
+ return "\0" + (codeBlocks.length - 1) + "\0";
1175
+ });
1176
+ t = escHtml(t);
1177
+ t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
1178
+ t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
1179
+ t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
1180
+ t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
1181
+ t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
1182
+ t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
1183
+ t = t.replace(/\n/g, "<br>");
1184
+ t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
1185
+ return t;
1186
+ }
1052
1187
  function Bubble({ m }) {
1053
1188
  if (m.role === "tools") {
1054
1189
  return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
@@ -1062,7 +1197,11 @@ function Bubble({ m }) {
1062
1197
  createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
1063
1198
  );
1064
1199
  }
1065
- return createElement("div", { className: m.role === "user" ? "caf-msg caf-msg-user" : "caf-msg caf-msg-assistant" }, m.content);
1200
+ if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
1201
+ return createElement("div", {
1202
+ className: "caf-msg caf-msg-assistant",
1203
+ dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
1204
+ });
1066
1205
  }
1067
1206
  function ChatLayout({ spec, ctx }) {
1068
1207
  const c = spec.chat;
@@ -1135,9 +1274,9 @@ Continue with this information. If you have everything you need, answer in natur
1135
1274
  createElement(
1136
1275
  "div",
1137
1276
  { className: "caf-block caf-chat-log" },
1138
- c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant" }, c.greeting) : null,
1277
+ c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
1139
1278
  log.map((m, i) => createElement(Bubble, { key: i, m })),
1140
- live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live" }, live || "\u2026") : null,
1279
+ live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
1141
1280
  busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
1142
1281
  createElement("div", { ref: endRef })
1143
1282
  ),
@@ -1184,8 +1323,30 @@ function checkFields(fields, where) {
1184
1323
  `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
1185
1324
  );
1186
1325
  }
1326
+ if (f.type === "select" && f.options !== void 0 && !Array.isArray(f.options) && typeof f.options !== "function") {
1327
+ throw new Error(
1328
+ `claude-artifact-framework: field "${f.key}" in ${where}: \`options\` must be an array or a function of data returning one.`
1329
+ );
1330
+ }
1187
1331
  }
1188
1332
  }
1333
+ var ACTION_KEYS = ["label", "run", "when", "kind"];
1334
+ function checkActions(actions, where) {
1335
+ if (!Array.isArray(actions)) {
1336
+ throw new Error(`claude-artifact-framework: ${where} must be an array of { label, run }.`);
1337
+ }
1338
+ actions.forEach((a, i) => {
1339
+ if (!a || typeof a.label !== "string" || typeof a.run !== "function") {
1340
+ throw new Error(`claude-artifact-framework: ${where}[${i}] needs a \`label\` string and a \`run\` function.`);
1341
+ }
1342
+ const unknown = Object.keys(a).filter((k) => !ACTION_KEYS.includes(k));
1343
+ if (unknown.length) {
1344
+ throw new Error(
1345
+ `claude-artifact-framework: ${where}[${i}] has unknown keys (${unknown.join(", ")}). Valid keys: ${ACTION_KEYS.join(", ")}.`
1346
+ );
1347
+ }
1348
+ });
1349
+ }
1189
1350
  function checkBlocks(blocks) {
1190
1351
  if (!Array.isArray(blocks)) {
1191
1352
  throw new Error("claude-artifact-framework: `blocks` must be an array.");
@@ -1209,6 +1370,7 @@ function checkBlocks(blocks) {
1209
1370
  );
1210
1371
  }
1211
1372
  checkFields(block.fields, `blocks[${i}]`);
1373
+ if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
1212
1374
  });
1213
1375
  }
1214
1376
  function validate(spec) {
@@ -1285,7 +1447,7 @@ function validate(spec) {
1285
1447
  'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
1286
1448
  );
1287
1449
  }
1288
- const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute"];
1450
+ const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
1289
1451
  const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
1290
1452
  if (unknownR.length) {
1291
1453
  throw new Error(
@@ -1300,8 +1462,28 @@ function validate(spec) {
1300
1462
  seen.add(f.key);
1301
1463
  }
1302
1464
  if (spec.blocks) checkBlocks(spec.blocks);
1465
+ if (r.actions) checkActions(r.actions, "records.actions");
1466
+ if (r.items) {
1467
+ const ITEMS_KEYS = ["label", "fields", "empty"];
1468
+ const unknownI = Object.keys(r.items).filter((k) => !ITEMS_KEYS.includes(k));
1469
+ if (unknownI.length) {
1470
+ throw new Error(
1471
+ `claude-artifact-framework: records.items has unknown keys (${unknownI.join(", ")}). Valid keys: ${ITEMS_KEYS.join(", ")}.`
1472
+ );
1473
+ }
1474
+ if (!Array.isArray(r.items.fields) || r.items.fields.length === 0) {
1475
+ throw new Error("claude-artifact-framework: records.items needs `fields: [...]` \u2014 the same field declarations as everywhere else.");
1476
+ }
1477
+ checkFields(r.items.fields, "records.items");
1478
+ for (const f of r.items.fields) {
1479
+ if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on items \u2014 the framework assigns it.');
1480
+ }
1481
+ if (seen.has("items") || r.fields.some((f) => f.key === "items")) {
1482
+ throw new Error('claude-artifact-framework: a records field cannot be named "items" when `items` is declared \u2014 that key holds the child collection.');
1483
+ }
1484
+ }
1303
1485
  for (const [key, c] of Object.entries(r.compute || {})) {
1304
- if (seen.has(key) || key === "id") {
1486
+ if (seen.has(key) || key === "id" || r.items && key === "items") {
1305
1487
  throw new Error(
1306
1488
  `claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
1307
1489
  );
@@ -1561,6 +1743,22 @@ var BASE_CSS = `
1561
1743
  .caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1562
1744
  .caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
1563
1745
  .caf-records-blocks { margin-top: 1rem; }
1746
+ .caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
1747
+ .caf-row-wrap .caf-row { flex: 1; min-width: 0; }
1748
+ .caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
1749
+ .caf-btn-sm { font-size: 0.78rem; padding: 0.3rem 0.6rem; border-radius: 6px; }
1750
+ .caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
1751
+ .caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
1752
+ .caf-item-row .caf-field { flex: 1; min-width: 0; }
1753
+ .caf-item-row .caf-field label { font-size: 0.68rem; }
1754
+ .caf-item-remove { flex: none; margin-bottom: 0.15rem; }
1755
+ .caf-empty-sm { padding: 0.6rem; font-size: 0.82rem; }
1756
+ .caf-actions-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }
1757
+ .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; }
1758
+ .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; }
1759
+ .caf-md-h { display: block; margin-top: 0.3rem; }
1760
+ .caf-md-li { display: block; padding-left: 1rem; position: relative; }
1761
+ .caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
1564
1762
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
1565
1763
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
1566
1764
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }