claude-artifact-framework 0.13.1 → 0.14.1
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 +148 -13
- package/dist/index.esm.js +151 -3
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +13 -13
- package/dist/index.global.js.map +3 -3
- package/llms.txt +90 -0
- package/package.json +9 -4
- package/src/app.js +175 -2
package/README.md
CHANGED
|
@@ -107,6 +107,12 @@ already on `globalThis`, it is picked up automatically and the call is a no-op.
|
|
|
107
107
|
> extra closing brace at the tail. Indented, multi-line specs have never
|
|
108
108
|
> produced one. Format like every example in this document.
|
|
109
109
|
|
|
110
|
+
> **Don't hand-build empty, loading, or error states.** Every block that can
|
|
111
|
+
> be empty (`output`, `chart`, `list`, `records`) shows a named empty state
|
|
112
|
+
> by itself; hydration shows a loading screen; a block that throws shows a
|
|
113
|
+
> named error in its own slot. Code like `if (!d.items.length) return "No
|
|
114
|
+
> hay datos"` duplicates what the framework already guarantees.
|
|
115
|
+
|
|
110
116
|
### The `records` layout — a full CRUD app from a declaration
|
|
111
117
|
|
|
112
118
|
For "list of things" apps (expenses, habits, notes, todos), declare what a
|
|
@@ -272,7 +278,13 @@ ArtifactKit.createApp({
|
|
|
272
278
|
viewer of the published artifact converges on the same data via polling.
|
|
273
279
|
Conflict handling is last-write-wins; a poll never overwrites local edits
|
|
274
280
|
that haven't been persisted yet. View state (which screen is open) stays
|
|
275
|
-
personal per viewer.
|
|
281
|
+
personal per viewer. **Design for last-write-wins**: two viewers writing
|
|
282
|
+
within the same couple of seconds means one write can be lost — prefer
|
|
283
|
+
per-viewer marks in arrays (`votantes: [...viewerIds]`, like the voting
|
|
284
|
+
pattern) over several people editing the same field, and don't build
|
|
285
|
+
anything where a lost write really hurts (money, bookings). Measured in
|
|
286
|
+
practice: appending to your own mark converges; racing on one value
|
|
287
|
+
doesn't.
|
|
276
288
|
|
|
277
289
|
### `items` — a child collection per record
|
|
278
290
|
|
|
@@ -302,7 +314,10 @@ records: {
|
|
|
302
314
|
```
|
|
303
315
|
|
|
304
316
|
`items` keys: `label`, `fields`, `empty`. Each child gets a framework
|
|
305
|
-
`id`; a parent field can't be named `items`.
|
|
317
|
+
`id`; a parent field can't be named `items`. The smell this replaces:
|
|
318
|
+
**never ask the user to type JSON (or comma-separated rows) into a
|
|
319
|
+
`textarea`** — if a record has parts, declare `items` and each part gets
|
|
320
|
+
real fields, validation, and its own remove button.
|
|
306
321
|
|
|
307
322
|
### `actions` — one-tap buttons that mutate data
|
|
308
323
|
|
|
@@ -328,10 +343,17 @@ blocks: [
|
|
|
328
343
|
```
|
|
329
344
|
|
|
330
345
|
Action keys: `label`, `run`, optional `when` (hides the button when false)
|
|
331
|
-
and `kind` (`"primary"` / `"danger"`). Row actions receive
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
346
|
+
and `kind` (`"primary"` / `"danger"`). Row actions receive
|
|
347
|
+
`(record, data, viewerId)`, block actions `(data, viewerId)`. Stopping
|
|
348
|
+
early is fine (`run: (r) => ...`), but **never skip a middle argument**:
|
|
349
|
+
`run: (r, viewerId)` reads `data` as your viewer id and breaks silently —
|
|
350
|
+
if you need `viewerId`, name everything before it: `run: (r, d, viewerId)`.
|
|
351
|
+
Changes persist like any other update.
|
|
352
|
+
Give the constructive action `kind: "primary"` — a "Vote"/"Add"/"Done" left
|
|
353
|
+
as the neutral default reads weaker than a `"danger"` next to it. And a
|
|
354
|
+
smell to avoid: **a number that changes by pressing is an `action`, not an
|
|
355
|
+
editable `number` field** — votes, counters, and streaks as editable inputs
|
|
356
|
+
invite typos and lie about intent.
|
|
335
357
|
|
|
336
358
|
Chat bubbles render the assistant's **markdown** (bold, code, lists,
|
|
337
359
|
headings) automatically — HTML is escaped first, so model output can't
|
|
@@ -400,6 +422,63 @@ The active tab and each accordion's open/closed state are framework-owned
|
|
|
400
422
|
view state — they survive reload, per viewer. Tabs cannot nest inside tabs;
|
|
401
423
|
`when` must be a function; both throw otherwise.
|
|
402
424
|
|
|
425
|
+
### The `machine` — declared phases instead of hand-rolled state logic
|
|
426
|
+
|
|
427
|
+
If your app has **phases** — a game (choosing → comparing → won), a turn
|
|
428
|
+
cycle, a multi-stage process — never track them by hand with flags and
|
|
429
|
+
`setTimeout`s: that is the single most measured source of broken apps
|
|
430
|
+
(a memory game died because its compare logic fired in the wrong phase).
|
|
431
|
+
Declare the machine instead:
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
ArtifactKit.createApp({
|
|
435
|
+
title: "Memoria",
|
|
436
|
+
machine: {
|
|
437
|
+
key: "fase", // d.fase holds the current state — persisted
|
|
438
|
+
initial: "eligiendo",
|
|
439
|
+
states: {
|
|
440
|
+
eligiendo: {},
|
|
441
|
+
comparando: { // timed transition, framework-owned timer
|
|
442
|
+
after: { seconds: 1, then: "eligiendo", do: (d) => { d.reveladas = []; } },
|
|
443
|
+
},
|
|
444
|
+
ganado: {},
|
|
445
|
+
},
|
|
446
|
+
events: {
|
|
447
|
+
elegir: {
|
|
448
|
+
from: ["eligiendo"], // ONLY legal here — elsewhere it's a no-op
|
|
449
|
+
do: (d, carta) => { d.reveladas = [...d.reveladas, carta]; },
|
|
450
|
+
then: (d) => d.pares === d.total ? "ganado" : d.reveladas.length === 2 ? "comparando" : "eligiendo",
|
|
451
|
+
},
|
|
452
|
+
reiniciar: { from: "*", do: (d) => { d.reveladas = []; d.pares = 0; }, then: "eligiendo" },
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
blocks: [
|
|
456
|
+
{ custom: (ctx) => /* cards call ctx.send("elegir", i) */ null },
|
|
457
|
+
{ text: "¡Ganaste!", state: "ganado" }, // block exists only in that phase
|
|
458
|
+
],
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The contract:
|
|
463
|
+
|
|
464
|
+
- **`ctx.send(event, payload)` is the only door.** An unknown event throws
|
|
465
|
+
naming the valid ones; a legal event sent from the wrong phase is a
|
|
466
|
+
silent no-op — a user mashing buttons is normal, not a bug. Never write
|
|
467
|
+
`d.fase = ...` yourself.
|
|
468
|
+
- `do` mutates data (it receives `(d, payload)`), `then` names the next
|
|
469
|
+
state — a string, or a function of data returning one. Omit `then` to
|
|
470
|
+
stay in the same state.
|
|
471
|
+
- `after: { seconds, then, do }` on a state is a **framework-owned timer**:
|
|
472
|
+
armed on entry, cancelled on leaving the state, cleaned up on unmount.
|
|
473
|
+
No `setTimeout` to forget.
|
|
474
|
+
- The current state lives in `data` under `machine.key` (default `"fase"`),
|
|
475
|
+
so it **persists and resumes** — reloading mid-game continues the phase.
|
|
476
|
+
- Blocks take `state: "nombre"` as sugar for "visible only in that phase";
|
|
477
|
+
`when:` still works for anything more complex.
|
|
478
|
+
- Machine keys: `key`, `initial`, `states`, `events`. Event keys: `from`
|
|
479
|
+
(state, array, or `"*"`), `do`, `then`. One machine per app, app-wide
|
|
480
|
+
(inside `documents` it is NOT per-document).
|
|
481
|
+
|
|
403
482
|
### `records` as a block — a CRUD collection inside any screen
|
|
404
483
|
|
|
405
484
|
The full records machinery (list, add-then-edit, detail, search, summary,
|
|
@@ -661,6 +740,29 @@ custom: (ctx) => {
|
|
|
661
740
|
}
|
|
662
741
|
```
|
|
663
742
|
|
|
743
|
+
- **Never render a `<form>`.** The published-artifact iframe sandbox lacks
|
|
744
|
+
`allow-forms`: a form that works in preview dies on the phone. Inputs +
|
|
745
|
+
a button with `onClick` (plus Enter via `onKeyDown` if you want it) do
|
|
746
|
+
the same job — that is why every framework block is formless.
|
|
747
|
+
- **Use the theme, not hardcoded colors.** The app ships a light AND dark
|
|
748
|
+
palette; a custom block painted with `#fff` backgrounds or `#dc2626`
|
|
749
|
+
reds looks broken the moment the viewer is in dark mode. Use the CSS
|
|
750
|
+
variables — `var(--caf-text)`, `var(--caf-muted)`, `var(--caf-accent)`,
|
|
751
|
+
`var(--caf-surface)`, `var(--caf-sunken)`, `var(--caf-border)`,
|
|
752
|
+
`var(--caf-danger)` — and `ctx.colors(n)` for chart-like series:
|
|
753
|
+
|
|
754
|
+
```js
|
|
755
|
+
custom: (ctx) => React.createElement("div", {
|
|
756
|
+
style: { background: "var(--caf-sunken)", color: "var(--caf-text)",
|
|
757
|
+
border: "1px solid var(--caf-border)", borderRadius: "7px", padding: "0.6rem" }
|
|
758
|
+
}, "se ve bien en ambos temas")
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- **Never write big strings into `data`.** Storage allows ~5MB per key and
|
|
762
|
+
the whole pool is one key: a data-URL preview of a photo written to
|
|
763
|
+
`d.preview` can kill persistence for the entire app. Big things (data
|
|
764
|
+
URLs, file contents, parse results) live in `React.useState` or come from
|
|
765
|
+
`ctx.files`; `data` holds what is small and serializable.
|
|
664
766
|
- Custom re-renders whenever `data` or `view` change; it receives the full
|
|
665
767
|
`ctx` (`data`, `update`, `view`, `files`, `viewerId`, `colors`, `go`,
|
|
666
768
|
`back`) and lives inside the block error boundary — a bug in your custom
|
|
@@ -671,17 +773,18 @@ custom: (ctx) => {
|
|
|
671
773
|
Everything an app can declare, in one place.
|
|
672
774
|
|
|
673
775
|
**Top-level keys**: `title`, `subtitle`, `theme: { seed }`, `layout`, `blocks`,
|
|
674
|
-
`records`, `steps`, `chat`, `main`, `sidebar` (workspace panes), `
|
|
675
|
-
`shared`. Anything else throws.
|
|
776
|
+
`records`, `steps`, `chat`, `main`, `sidebar` (workspace panes), `documents`,
|
|
777
|
+
`data`, `shared`, `libs`. Anything else throws.
|
|
676
778
|
|
|
677
779
|
**Layouts** (`layout:`): `panel` (default — grid of blocks), `records` (CRUD
|
|
678
780
|
list), `steps` (wizard), `chat` (conversation with Claude), `workspace`
|
|
679
|
-
(main column + sidebar)
|
|
680
|
-
`records` / `steps` / `chat` /
|
|
781
|
+
(main column + sidebar), `documents` (parallel documents in top tabs).
|
|
782
|
+
Each requires its matching key (`blocks` / `records` / `steps` / `chat` /
|
|
783
|
+
`main`+`sidebar` / `documents`).
|
|
681
784
|
|
|
682
|
-
**Block types** (entries of `blocks`, `main`,
|
|
683
|
-
`output`, `text`, `list`, `chart`, `timer`, `actions`,
|
|
684
|
-
`custom`. Any block also accepts `when: (d) => bool` (conditional
|
|
785
|
+
**Block types** (entries of `blocks`, `main`, `sidebar`, or a document's
|
|
786
|
+
`blocks`): `fields`, `output`, `text`, `list`, `chart`, `timer`, `actions`,
|
|
787
|
+
`chat`, `tabs`, `records`, `custom`. Any block also accepts `when: (d) => bool` (conditional
|
|
685
788
|
visibility) and `collapsible: true | "collapsed"` (accordion; needs `title`).
|
|
686
789
|
On every block `title` is the heading string; `list` additionally accepts a
|
|
687
790
|
function as `title`, used as the per-row title (rows fall back to
|
|
@@ -787,3 +890,35 @@ board.stop(); // stop polling, e.g. on teardown
|
|
|
787
890
|
Conflict handling is last-write-wins: if two viewers change the same key
|
|
788
891
|
within one poll window, whichever write reaches storage last overwrites the
|
|
789
892
|
other. There is no merge/CRDT logic.
|
|
893
|
+
|
|
894
|
+
## Hard rules — the short list
|
|
895
|
+
|
|
896
|
+
The rules that decide whether a generated app works, all in one place
|
|
897
|
+
(each is explained in its section above):
|
|
898
|
+
|
|
899
|
+
1. **Indented, never minified** — the only measured syntax errors came from
|
|
900
|
+
single-line output.
|
|
901
|
+
2. **Theme variables, never hardcoded hex** — `var(--caf-text)`,
|
|
902
|
+
`var(--caf-surface)`, `var(--caf-accent)`, `var(--caf-border)`,
|
|
903
|
+
`var(--caf-sunken)`, `var(--caf-muted)`, `var(--caf-danger)`, and
|
|
904
|
+
`ctx.colors(n)` for series. A custom block painted with `#fff` or
|
|
905
|
+
`#ef4444` breaks in dark mode.
|
|
906
|
+
3. **Never render a `<form>`** — the published sandbox lacks `allow-forms`;
|
|
907
|
+
use inputs plus a button with `onClick`.
|
|
908
|
+
4. **Never write big strings into `data`** (data URLs, file contents) —
|
|
909
|
+
the whole pool shares one ~5MB key; big things live in `React.useState`
|
|
910
|
+
or `ctx.files`.
|
|
911
|
+
5. **Phases belong to the `machine`** — never track game/process phases
|
|
912
|
+
with hand-rolled flags and `setTimeout`; `ctx.send` is the only door.
|
|
913
|
+
6. **Never skip a middle positional argument** — `run: (r, d, viewerId)`,
|
|
914
|
+
never `run: (r, viewerId)`.
|
|
915
|
+
7. **One `big: true` per screen** — it marks THE result, not every stat.
|
|
916
|
+
8. **The constructive action takes `kind: "primary"`**; a counter changed
|
|
917
|
+
by pressing is an `action`, not an editable `number` field.
|
|
918
|
+
9. **Parts of a record are `items`** — never JSON in a `textarea`.
|
|
919
|
+
10. **Don't hand-build empty/loading/error states** — the framework owns
|
|
920
|
+
them.
|
|
921
|
+
11. **CDN libs: call the method, not the global** (`marked.parse`, not
|
|
922
|
+
`marked(...)`); pin versions; when unsure of the file path use the bare
|
|
923
|
+
package URL.
|
|
924
|
+
12. **Framework ids are strings** — never `Number(record.id)`.
|
package/dist/index.esm.js
CHANGED
|
@@ -1394,8 +1394,12 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1394
1394
|
|
|
1395
1395
|
// src/app.js
|
|
1396
1396
|
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace", "documents"];
|
|
1397
|
-
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill"];
|
|
1398
|
-
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents"];
|
|
1397
|
+
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible", "fill", "state"];
|
|
1398
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar", "libs", "documents", "machine"];
|
|
1399
|
+
var MACHINE_KEYS = ["key", "initial", "states", "events"];
|
|
1400
|
+
var MACHINE_STATE_KEYS = ["after"];
|
|
1401
|
+
var MACHINE_AFTER_KEYS = ["seconds", "then", "do"];
|
|
1402
|
+
var MACHINE_EVENT_KEYS = ["from", "do", "then"];
|
|
1399
1403
|
var DOCUMENTS_KEYS = ["label", "title", "blocks", "empty", "types"];
|
|
1400
1404
|
var RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute", "items", "actions"];
|
|
1401
1405
|
var ITEMS_KEYS = ["label", "fields", "empty"];
|
|
@@ -1500,6 +1504,81 @@ function checkBlocks(blocks) {
|
|
|
1500
1504
|
}
|
|
1501
1505
|
});
|
|
1502
1506
|
}
|
|
1507
|
+
function checkMachine(m) {
|
|
1508
|
+
const unknown = Object.keys(m).filter((k) => !MACHINE_KEYS.includes(k));
|
|
1509
|
+
if (unknown.length) {
|
|
1510
|
+
throw new Error(
|
|
1511
|
+
`claude-artifact-framework: machine has unknown keys (${unknown.join(", ")}). Valid keys: ${MACHINE_KEYS.join(", ")}.`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
if (m.key !== void 0 && (typeof m.key !== "string" || !m.key)) {
|
|
1515
|
+
throw new Error("claude-artifact-framework: machine.key must be a non-empty string (where the current state lives in data).");
|
|
1516
|
+
}
|
|
1517
|
+
const stateNames = Object.keys(m.states || {});
|
|
1518
|
+
if (!stateNames.length) {
|
|
1519
|
+
throw new Error("claude-artifact-framework: machine needs `states: { nombre: {...} }` with at least one state.");
|
|
1520
|
+
}
|
|
1521
|
+
const validState = (name, where) => {
|
|
1522
|
+
if (!stateNames.includes(name)) {
|
|
1523
|
+
throw new Error(
|
|
1524
|
+
`claude-artifact-framework: ${where} names unknown state "${name}". Valid states: ${stateNames.join(", ")}.`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
if (typeof m.initial !== "string") {
|
|
1529
|
+
throw new Error('claude-artifact-framework: machine needs `initial: "..."` naming the starting state.');
|
|
1530
|
+
}
|
|
1531
|
+
validState(m.initial, "machine.initial");
|
|
1532
|
+
for (const [name, st] of Object.entries(m.states)) {
|
|
1533
|
+
const unknownS = Object.keys(st || {}).filter((k) => !MACHINE_STATE_KEYS.includes(k));
|
|
1534
|
+
if (unknownS.length) {
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`claude-artifact-framework: machine.states.${name} has unknown keys (${unknownS.join(", ")}). Valid keys: ${MACHINE_STATE_KEYS.join(", ")}.`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
if (st && st.after !== void 0) {
|
|
1540
|
+
const a = st.after;
|
|
1541
|
+
const unknownA = Object.keys(a || {}).filter((k) => !MACHINE_AFTER_KEYS.includes(k));
|
|
1542
|
+
if (unknownA.length) {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
`claude-artifact-framework: machine.states.${name}.after has unknown keys (${unknownA.join(", ")}). Valid keys: ${MACHINE_AFTER_KEYS.join(", ")}.`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
if (typeof a.seconds !== "number" || a.seconds <= 0) {
|
|
1548
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after needs \`seconds\` > 0.`);
|
|
1549
|
+
}
|
|
1550
|
+
if (typeof a.then !== "string") {
|
|
1551
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after needs \`then: "estado"\`.`);
|
|
1552
|
+
}
|
|
1553
|
+
validState(a.then, `machine.states.${name}.after.then`);
|
|
1554
|
+
if (a.do !== void 0 && typeof a.do !== "function") {
|
|
1555
|
+
throw new Error(`claude-artifact-framework: machine.states.${name}.after.do must be a function of data.`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const eventNames = Object.keys(m.events || {});
|
|
1560
|
+
if (!eventNames.length) {
|
|
1561
|
+
throw new Error("claude-artifact-framework: machine needs `events: { nombre: {...} }` \u2014 ctx.send() is the only way to move it.");
|
|
1562
|
+
}
|
|
1563
|
+
for (const [name, ev] of Object.entries(m.events)) {
|
|
1564
|
+
const unknownE = Object.keys(ev || {}).filter((k) => !MACHINE_EVENT_KEYS.includes(k));
|
|
1565
|
+
if (unknownE.length) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
`claude-artifact-framework: machine.events.${name} has unknown keys (${unknownE.join(", ")}). Valid keys: ${MACHINE_EVENT_KEYS.join(", ")}.`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
if (ev.from !== void 0 && ev.from !== "*") {
|
|
1571
|
+
for (const f of [].concat(ev.from)) validState(f, `machine.events.${name}.from`);
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof ev.then === "string") validState(ev.then, `machine.events.${name}.then`);
|
|
1574
|
+
else if (ev.then !== void 0 && typeof ev.then !== "function") {
|
|
1575
|
+
throw new Error(`claude-artifact-framework: machine.events.${name}.then must be a state name or a function of data returning one.`);
|
|
1576
|
+
}
|
|
1577
|
+
if (ev.do !== void 0 && typeof ev.do !== "function") {
|
|
1578
|
+
throw new Error(`claude-artifact-framework: machine.events.${name}.do must be a function (data, payload).`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1503
1582
|
function validate(spec) {
|
|
1504
1583
|
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
1505
1584
|
if (unknownTop.length) {
|
|
@@ -1507,6 +1586,7 @@ function validate(spec) {
|
|
|
1507
1586
|
`claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
|
|
1508
1587
|
);
|
|
1509
1588
|
}
|
|
1589
|
+
if (spec.machine !== void 0) checkMachine(spec.machine);
|
|
1510
1590
|
if (spec.libs !== void 0) {
|
|
1511
1591
|
if (!Array.isArray(spec.libs) || spec.libs.some((u) => typeof u !== "string" || !/^https?:\/\//.test(u))) {
|
|
1512
1592
|
throw new Error("claude-artifact-framework: `libs` must be an array of https:// script URLs.");
|
|
@@ -1743,6 +1823,10 @@ function defaultsFrom(spec) {
|
|
|
1743
1823
|
if ((spec.layout || "panel") === "documents" && !Array.isArray(data.docs)) {
|
|
1744
1824
|
data.docs = [];
|
|
1745
1825
|
}
|
|
1826
|
+
if (spec.machine) {
|
|
1827
|
+
const k = spec.machine.key || "fase";
|
|
1828
|
+
if (!(k in data)) data[k] = spec.machine.initial;
|
|
1829
|
+
}
|
|
1746
1830
|
const allBlocks = flattenBlocks([...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []]);
|
|
1747
1831
|
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
1748
1832
|
if (hasChat && !data.chat) {
|
|
@@ -1826,6 +1910,7 @@ var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock, records: RecordsBlock };
|
|
|
1826
1910
|
var ALL_NAMES = [...BLOCK_NAMES, "tabs", "records"];
|
|
1827
1911
|
function Block({ block, ctx }) {
|
|
1828
1912
|
if (typeof block.when === "function" && !block.when(ctx.data)) return null;
|
|
1913
|
+
if (block.state !== void 0 && ctx.machine && ctx.data[ctx.machine.key || "fase"] !== block.state) return null;
|
|
1829
1914
|
const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
|
|
1830
1915
|
const Component2 = ALL_BLOCKS[name];
|
|
1831
1916
|
const inner = createElement(
|
|
@@ -2103,8 +2188,52 @@ function loadScript(src) {
|
|
|
2103
2188
|
document.head.appendChild(tag);
|
|
2104
2189
|
});
|
|
2105
2190
|
}
|
|
2191
|
+
function everyBlock(spec) {
|
|
2192
|
+
const docs = spec.documents ? spec.documents.blocks || Object.values(spec.documents.types || {}).flatMap((t) => t.blocks || []) : [];
|
|
2193
|
+
return flattenBlocks([...spec.blocks || [], ...spec.main || [], ...spec.sidebar || [], ...docs]);
|
|
2194
|
+
}
|
|
2195
|
+
function runMachineEvent(machine, update, eventName, payload) {
|
|
2196
|
+
const ev = machine.events[eventName];
|
|
2197
|
+
if (!ev) {
|
|
2198
|
+
throw new Error(
|
|
2199
|
+
`claude-artifact-framework: unknown machine event "${eventName}". Valid events: ${Object.keys(machine.events).join(", ")}.`
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
const key = machine.key || "fase";
|
|
2203
|
+
update((d) => {
|
|
2204
|
+
const current = d[key];
|
|
2205
|
+
const from = ev.from === void 0 ? "*" : ev.from;
|
|
2206
|
+
if (from !== "*" && ![].concat(from).includes(current)) return;
|
|
2207
|
+
if (ev.do) ev.do(d, payload);
|
|
2208
|
+
const next = typeof ev.then === "function" ? ev.then(d) : ev.then;
|
|
2209
|
+
if (next !== void 0 && next !== null) {
|
|
2210
|
+
if (!machine.states[next]) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`claude-artifact-framework: machine event "${eventName}" resolved to unknown state "${next}". Valid states: ${Object.keys(machine.states).join(", ")}.`
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
d[key] = next;
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2106
2219
|
function createApp(spec = {}) {
|
|
2107
2220
|
const layout = validate(spec);
|
|
2221
|
+
if (spec.machine) {
|
|
2222
|
+
const names = Object.keys(spec.machine.states);
|
|
2223
|
+
for (const b of everyBlock(spec)) {
|
|
2224
|
+
if (b && b.state !== void 0 && !names.includes(b.state)) {
|
|
2225
|
+
throw new Error(
|
|
2226
|
+
`claude-artifact-framework: a block declares state: "${b.state}" but the machine's states are: ${names.join(", ")}.`
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
} else {
|
|
2231
|
+
for (const b of everyBlock(spec)) {
|
|
2232
|
+
if (b && b.state !== void 0) {
|
|
2233
|
+
throw new Error("claude-artifact-framework: `state:` on a block needs a `machine` declared at the top level.");
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2108
2237
|
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
2109
2238
|
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
2110
2239
|
const files = {};
|
|
@@ -2132,6 +2261,21 @@ function createApp(spec = {}) {
|
|
|
2132
2261
|
if (state.isHydrated()) force((n) => n + 1);
|
|
2133
2262
|
return unsubscribe;
|
|
2134
2263
|
}, []);
|
|
2264
|
+
const machineState = spec.machine ? state.getData()[spec.machine.key || "fase"] : null;
|
|
2265
|
+
useEffect(() => {
|
|
2266
|
+
if (!spec.machine || !state.isHydrated()) return;
|
|
2267
|
+
const st = spec.machine.states[machineState];
|
|
2268
|
+
if (!st || !st.after) return;
|
|
2269
|
+
const timer = setTimeout(() => {
|
|
2270
|
+
state.update((d) => {
|
|
2271
|
+
const key = spec.machine.key || "fase";
|
|
2272
|
+
if (d[key] !== machineState) return;
|
|
2273
|
+
if (st.after.do) st.after.do(d);
|
|
2274
|
+
d[key] = st.after.then;
|
|
2275
|
+
});
|
|
2276
|
+
}, st.after.seconds * 1e3);
|
|
2277
|
+
return () => clearTimeout(timer);
|
|
2278
|
+
}, [machineState, state.isHydrated()]);
|
|
2135
2279
|
const ctx = {
|
|
2136
2280
|
React: getReact(),
|
|
2137
2281
|
data: state.getData(),
|
|
@@ -2144,7 +2288,11 @@ function createApp(spec = {}) {
|
|
|
2144
2288
|
viewerId: state.getViewerId(),
|
|
2145
2289
|
setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
|
|
2146
2290
|
toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
|
|
2147
|
-
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
|
|
2291
|
+
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
2292
|
+
machine: spec.machine || null,
|
|
2293
|
+
send: spec.machine ? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload) : () => {
|
|
2294
|
+
throw new Error("claude-artifact-framework: ctx.send() needs a `machine` declared at the top level.");
|
|
2295
|
+
}
|
|
2148
2296
|
};
|
|
2149
2297
|
const Layout = LAYOUT_COMPONENTS[layout];
|
|
2150
2298
|
return createElement(
|