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 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.0/dist/index.global.js"></script>
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.0/dist/index.esm.js";
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.0`) for reproducible artifacts, or drop the version
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
- "form",
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("button", { type: "submit", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim() }, "Send")
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
- const blocks = spec.blocks || [];
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
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout, chat: ChatLayout };
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); }