claude-artifact-framework 0.5.0 → 0.5.2
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 +40 -3
- package/dist/index.esm.js +80 -35
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +10 -9
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +76 -29
- package/src/chat.js +19 -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.5.
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.5.2/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.
|
|
30
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.5.2/dist/index.esm.js";
|
|
31
31
|
</script>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Pin a version (`@0.5.
|
|
34
|
+
Pin a version (`@0.5.2`) 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,43 @@ 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
|
+
## API reference
|
|
258
|
+
|
|
259
|
+
Everything an app can declare, in one place.
|
|
260
|
+
|
|
261
|
+
**Top-level keys**: `title`, `subtitle`, `theme: { seed }`, `layout`, `blocks`,
|
|
262
|
+
`records`, `steps`, `chat`, `data`, `shared`. Anything else throws.
|
|
263
|
+
|
|
264
|
+
**Layouts** (`layout:`): `panel` (default — grid of blocks), `records` (CRUD
|
|
265
|
+
list), `steps` (wizard), `chat` (conversation with Claude). Each requires its
|
|
266
|
+
matching key (`blocks` / `records` / `steps` / `chat`).
|
|
267
|
+
|
|
268
|
+
**Block types** (entries of `blocks`): `fields`, `output`, `text`, `list`,
|
|
269
|
+
`chart`, `timer`, `custom`. One type per entry, plus optional `title`, `empty`,
|
|
270
|
+
`wide`. In the `records` layout, `blocks` render below the list (never on the
|
|
271
|
+
detail screen); their functions receive the same `data` object as everywhere
|
|
272
|
+
else — the collection lives at `d.records`.
|
|
273
|
+
|
|
274
|
+
**Field types** (`type:` on any field): `text` (default), `textarea`,
|
|
275
|
+
`number`, `money`, `percent`, `check` (checkbox), `date`, `select` (needs
|
|
276
|
+
`options`). Other field keys: `key` (required), `label`, `value` (initial),
|
|
277
|
+
`required`, `min`, `max`, `step`, `placeholder`, `hint`, `rows`, `options`.
|
|
278
|
+
An unknown `type` throws naming the valid ones.
|
|
279
|
+
|
|
280
|
+
**`records` keys**: `label`, `fields`, `title(r)`, `subtitle(r)`, `sort(a,b)`,
|
|
281
|
+
`summary(all)`, `empty`, `search`, `compute`. `id` is framework-assigned and
|
|
282
|
+
reserved.
|
|
283
|
+
|
|
284
|
+
**`steps` keys** (per step): `title`, `text`, `fields`, `result(d)`. Field
|
|
285
|
+
keys must be unique across steps.
|
|
286
|
+
|
|
287
|
+
**`chat` keys**: `system` (string or `(d) => string`), `greeting`,
|
|
288
|
+
`placeholder`, `tools`, `model`, `maxTokens`. Each tool:
|
|
289
|
+
`{ description, input, run(input, ctx) }`.
|
|
290
|
+
|
|
291
|
+
**Formats** (`format:` on output/chart/compute items): `money`, `percent`,
|
|
292
|
+
`number`, `date`.
|
|
293
|
+
|
|
257
294
|
## `storage`
|
|
258
295
|
|
|
259
296
|
Thin wrapper over `window.storage`, the persistence API Claude injects into a
|
package/dist/index.esm.js
CHANGED
|
@@ -373,6 +373,7 @@ function seriesColors(seed, n) {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
// src/blocks.js
|
|
376
|
+
var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
376
377
|
function formatValue(value, format) {
|
|
377
378
|
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
378
379
|
const n = Number(value);
|
|
@@ -1140,23 +1141,33 @@ Continue with this information. If you have everything you need, answer in natur
|
|
|
1140
1141
|
busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
|
|
1141
1142
|
createElement("div", { ref: endRef })
|
|
1142
1143
|
),
|
|
1144
|
+
// Deliberately NOT a <form>: the artifact iframe's sandbox lacks
|
|
1145
|
+
// allow-forms, so native form submission is blocked there — and on mobile
|
|
1146
|
+
// the keyboard's send key triggers exactly that implicit submission. A
|
|
1147
|
+
// plain div with onClick + Enter via onKeyDown never touches the native
|
|
1148
|
+
// form machinery, so it can't be sandboxed away.
|
|
1143
1149
|
createElement(
|
|
1144
|
-
"
|
|
1145
|
-
{
|
|
1146
|
-
className: "caf-chat-input",
|
|
1147
|
-
onSubmit: (e) => {
|
|
1148
|
-
e.preventDefault();
|
|
1149
|
-
send();
|
|
1150
|
-
}
|
|
1151
|
-
},
|
|
1150
|
+
"div",
|
|
1151
|
+
{ className: "caf-chat-input" },
|
|
1152
1152
|
createElement("input", {
|
|
1153
1153
|
value: draft,
|
|
1154
1154
|
placeholder: c.placeholder || "Message\u2026",
|
|
1155
1155
|
disabled: busy,
|
|
1156
|
+
enterKeyHint: "send",
|
|
1156
1157
|
onChange: (e) => setDraft(e.target.value),
|
|
1158
|
+
onKeyDown: (e) => {
|
|
1159
|
+
if (e.key === "Enter") {
|
|
1160
|
+
e.preventDefault();
|
|
1161
|
+
send();
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1157
1164
|
"aria-label": "Message"
|
|
1158
1165
|
}),
|
|
1159
|
-
createElement(
|
|
1166
|
+
createElement(
|
|
1167
|
+
"button",
|
|
1168
|
+
{ type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
|
|
1169
|
+
"Send"
|
|
1170
|
+
)
|
|
1160
1171
|
)
|
|
1161
1172
|
);
|
|
1162
1173
|
}
|
|
@@ -1165,6 +1176,41 @@ Continue with this information. If you have everything you need, answer in natur
|
|
|
1165
1176
|
var LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
1166
1177
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
1167
1178
|
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
|
|
1179
|
+
function checkFields(fields, where) {
|
|
1180
|
+
for (const f of fields || []) {
|
|
1181
|
+
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
|
|
1182
|
+
if (f.type !== void 0 && !FIELD_TYPES.includes(f.type)) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function checkBlocks(blocks) {
|
|
1190
|
+
if (!Array.isArray(blocks)) {
|
|
1191
|
+
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
1192
|
+
}
|
|
1193
|
+
blocks.forEach((block, i) => {
|
|
1194
|
+
if (!block || typeof block !== "object") {
|
|
1195
|
+
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
1196
|
+
}
|
|
1197
|
+
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
1198
|
+
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
1199
|
+
if (known.length === 0) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
if (known.length > 1) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
|
|
1207
|
+
", "
|
|
1208
|
+
)}). Split them into separate entries of \`blocks\`.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
checkFields(block.fields, `blocks[${i}]`);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1168
1214
|
function validate(spec) {
|
|
1169
1215
|
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
1170
1216
|
if (unknownTop.length) {
|
|
@@ -1198,8 +1244,8 @@ function validate(spec) {
|
|
|
1198
1244
|
if (!st.fields && !st.text && !st.result) {
|
|
1199
1245
|
throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
|
|
1200
1246
|
}
|
|
1247
|
+
checkFields(st.fields, `steps[${i}]`);
|
|
1201
1248
|
for (const f of st.fields || []) {
|
|
1202
|
-
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
|
|
1203
1249
|
if (seen.has(f.key)) {
|
|
1204
1250
|
throw new Error(
|
|
1205
1251
|
`claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
|
|
@@ -1246,13 +1292,14 @@ function validate(spec) {
|
|
|
1246
1292
|
`claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
|
|
1247
1293
|
);
|
|
1248
1294
|
}
|
|
1295
|
+
checkFields(r.fields, "records");
|
|
1249
1296
|
const seen = /* @__PURE__ */ new Set();
|
|
1250
1297
|
for (const f of r.fields) {
|
|
1251
|
-
if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
|
|
1252
1298
|
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
|
|
1253
1299
|
if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
|
|
1254
1300
|
seen.add(f.key);
|
|
1255
1301
|
}
|
|
1302
|
+
if (spec.blocks) checkBlocks(spec.blocks);
|
|
1256
1303
|
for (const [key, c] of Object.entries(r.compute || {})) {
|
|
1257
1304
|
if (seen.has(key) || key === "id") {
|
|
1258
1305
|
throw new Error(
|
|
@@ -1267,29 +1314,7 @@ function validate(spec) {
|
|
|
1267
1314
|
}
|
|
1268
1315
|
return layout;
|
|
1269
1316
|
}
|
|
1270
|
-
|
|
1271
|
-
if (!Array.isArray(blocks)) {
|
|
1272
|
-
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
1273
|
-
}
|
|
1274
|
-
blocks.forEach((block, i) => {
|
|
1275
|
-
if (!block || typeof block !== "object") {
|
|
1276
|
-
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
1277
|
-
}
|
|
1278
|
-
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
1279
|
-
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
1280
|
-
if (known.length === 0) {
|
|
1281
|
-
throw new Error(
|
|
1282
|
-
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
if (known.length > 1) {
|
|
1286
|
-
throw new Error(
|
|
1287
|
-
`claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
|
|
1288
|
-
", "
|
|
1289
|
-
)}). Split them into separate entries of \`blocks\`.`
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1317
|
+
checkBlocks(spec.blocks || []);
|
|
1293
1318
|
return layout;
|
|
1294
1319
|
}
|
|
1295
1320
|
function defaultsFrom(spec) {
|
|
@@ -1353,7 +1378,26 @@ function Panel({ spec, ctx }) {
|
|
|
1353
1378
|
)
|
|
1354
1379
|
);
|
|
1355
1380
|
}
|
|
1356
|
-
|
|
1381
|
+
function RecordsWithBlocks({ spec, ctx }) {
|
|
1382
|
+
const detailOpen = ctx.view.screen === "record" && (ctx.data.records || []).some((r) => r.id === ctx.view.arg);
|
|
1383
|
+
return createElement(
|
|
1384
|
+
"div",
|
|
1385
|
+
null,
|
|
1386
|
+
createElement(RecordsLayout, { spec, ctx }),
|
|
1387
|
+
!detailOpen && Array.isArray(spec.blocks) && spec.blocks.length ? createElement(
|
|
1388
|
+
"div",
|
|
1389
|
+
{ className: "caf-panel caf-records-blocks" },
|
|
1390
|
+
spec.blocks.map(
|
|
1391
|
+
(block, i) => createElement(
|
|
1392
|
+
"section",
|
|
1393
|
+
{ key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
|
|
1394
|
+
createElement(Block, { block, ctx })
|
|
1395
|
+
)
|
|
1396
|
+
)
|
|
1397
|
+
) : null
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
|
|
1357
1401
|
function createApp(spec = {}) {
|
|
1358
1402
|
const layout = validate(spec);
|
|
1359
1403
|
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
@@ -1516,6 +1560,7 @@ var BASE_CSS = `
|
|
|
1516
1560
|
}
|
|
1517
1561
|
.caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1518
1562
|
.caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
1563
|
+
.caf-records-blocks { margin-top: 1rem; }
|
|
1519
1564
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1520
1565
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1521
1566
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|