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 +84 -8
- package/dist/index.esm.js +216 -18
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +25 -9
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +63 -2
- package/src/blocks.js +47 -4
- package/src/chat.js +34 -3
- package/src/records.js +109 -9
- package/src/steps.js +1 -0
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.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.
|
|
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.
|
|
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`
|
|
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
|
|
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
|
-
|
|
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" },
|
|
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
|
-
"
|
|
811
|
-
{
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
-
|
|
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"
|
|
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"
|
|
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); }
|