@chalky/chalky 0.1.3 → 0.1.5

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.
Files changed (103) hide show
  1. package/README.md +10 -6
  2. package/{app → agent}/SYSTEM.md +16 -11
  3. package/agent/extensions/app/README.md +68 -0
  4. package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
  5. package/agent/extensions/app/guide.md +5 -0
  6. package/agent/extensions/app/index.ts +66 -0
  7. package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
  8. package/agent/extensions/board/browser.js +359 -0
  9. package/agent/extensions/board/guide.md +4 -0
  10. package/agent/extensions/board/index.ts +100 -0
  11. package/agent/extensions/card/README.md +88 -0
  12. package/agent/extensions/card/blocks/Button.js +34 -0
  13. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  14. package/agent/extensions/card/blocks/Markdown.js +37 -0
  15. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  16. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  18. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -128
  19. package/agent/extensions/card/guide.md +13 -0
  20. package/agent/extensions/card/index.ts +370 -0
  21. package/agent/extensions/command/browser.js +13 -0
  22. package/agent/extensions/command/guide.md +2 -0
  23. package/agent/extensions/command/index.ts +94 -0
  24. package/agent/extensions/doodle/README.md +57 -0
  25. package/agent/extensions/doodle/browser.js +1013 -0
  26. package/agent/extensions/doodle/guide.md +22 -0
  27. package/agent/extensions/doodle/index.ts +274 -0
  28. package/agent/extensions/file/browser.js +143 -0
  29. package/agent/extensions/file/guide.md +5 -0
  30. package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
  31. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  32. package/{app/extensions/frames → agent/extensions/frame}/browser.js +24 -19
  33. package/agent/extensions/frame/guide.md +3 -0
  34. package/agent/extensions/frame/index.ts +81 -0
  35. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  36. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -33
  37. package/{app → agent}/extensions/mcp/browser.js +1 -1
  38. package/agent/extensions/mcp/guide.md +3 -0
  39. package/{app → agent}/extensions/mcp/index.ts +2 -4
  40. package/agent/extensions/model/browser.js +363 -0
  41. package/{app → agent}/extensions/model/index.ts +8 -4
  42. package/agent/extensions/model/login.ts +215 -0
  43. package/agent/extensions/note/README.md +35 -0
  44. package/agent/extensions/note/browser.js +356 -0
  45. package/agent/extensions/note/guide.md +8 -0
  46. package/agent/extensions/note/index.ts +192 -0
  47. package/{app → agent}/extensions/page/index.ts +2 -2
  48. package/{app → agent}/extensions/page/page.html +55 -49
  49. package/agent/extensions/prompt/index.ts +38 -0
  50. package/{app → agent}/extensions/reload/index.ts +3 -3
  51. package/{app → agent}/extensions/screen/browser.js +4 -4
  52. package/{app → agent}/extensions/screen/index.ts +11 -11
  53. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  54. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  55. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  56. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  57. package/{app/extensions/versions → agent/extensions/version}/browser.js +10 -10
  58. package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
  59. package/{app → agent}/extensions/viewer/README.md +4 -4
  60. package/{app → agent}/extensions/viewer/browser.js +8 -8
  61. package/agent/extensions/viewer/guide.md +3 -0
  62. package/{app → agent}/extensions/viewer/index.ts +22 -17
  63. package/agent/extensions/voice/browser.js +72 -0
  64. package/agent/extensions/voice/index.ts +39 -0
  65. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  66. package/agent/settings.json +8 -0
  67. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  68. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  69. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +14 -14
  70. package/agent/skills/extension-map/SKILL.md +56 -0
  71. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  72. package/bin/chalky.mjs +3 -3
  73. package/package.json +8 -8
  74. package/server.ts +162 -63
  75. package/app/extensions/appdata/README.md +0 -10
  76. package/app/extensions/appdata/guide.md +0 -4
  77. package/app/extensions/appdata/index.ts +0 -47
  78. package/app/extensions/doodle/README.md +0 -15
  79. package/app/extensions/doodle/browser.js +0 -388
  80. package/app/extensions/doodle/guide.md +0 -6
  81. package/app/extensions/doodle/index.ts +0 -54
  82. package/app/extensions/files/browser.js +0 -96
  83. package/app/extensions/files/guide.md +0 -7
  84. package/app/extensions/frames/guide.md +0 -7
  85. package/app/extensions/frames/index.ts +0 -76
  86. package/app/extensions/mcp/guide.md +0 -5
  87. package/app/extensions/point/README.md +0 -22
  88. package/app/extensions/point/browser.js +0 -245
  89. package/app/extensions/point/guide.md +0 -6
  90. package/app/extensions/point/index.ts +0 -97
  91. package/app/extensions/prompt/index.ts +0 -19
  92. package/app/extensions/ui/README.md +0 -69
  93. package/app/extensions/ui/components/App.js +0 -28
  94. package/app/extensions/ui/components/Button.js +0 -28
  95. package/app/extensions/ui/components/Markdown.js +0 -37
  96. package/app/extensions/ui/guide.md +0 -7
  97. package/app/extensions/ui/index.ts +0 -271
  98. package/app/extensions/viewer/guide.md +0 -5
  99. package/app/mcp.json +0 -3
  100. package/app/skills/plugin-map/SKILL.md +0 -49
  101. package/app/ui/apps/.keep +0 -0
  102. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  103. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
@@ -0,0 +1,37 @@
1
+ // Markdown: formatted text from a markdown string (headings, lists, tables, code, links, quotes).
2
+ // HTML in the text is sanitised; links open in a new tab. The chat draws message bodies with it too.
3
+ export const about = {
4
+ "version": 1,
5
+ "props": {
6
+ "text": {"type": "text", "doc": "markdown"}
7
+ },
8
+ "children": "none",
9
+ "levers": [],
10
+ "example": {"text": "**Lunch** closes *Friday 11:00*"}
11
+ };
12
+ import { marked } from "marked"; // bare names: resolved by the import map in page.html
13
+ import DOMPurify from "dompurify";
14
+
15
+ const CSS = `
16
+ block-markdown { display: block; white-space: normal; }
17
+ block-markdown p, block-markdown ul, block-markdown ol, block-markdown pre, block-markdown table, block-markdown blockquote { margin: 6px 0; }
18
+ block-markdown ul, block-markdown ol { padding-left: 22px; }
19
+ block-markdown h1, block-markdown h2, block-markdown h3, block-markdown h4 { font-size: 1.05em; margin: 8px 0 4px; }
20
+ block-markdown code { font: 12.5px monospace; background: rgba(0, 0, 0, .06); padding: 1px 4px; border-radius: 3px; }
21
+ block-markdown pre { background: var(--hover); padding: 8px; border-radius: 4px; overflow-x: auto; }
22
+ block-markdown pre code { background: none; padding: 0; }
23
+ block-markdown table { border-collapse: collapse; }
24
+ block-markdown th, block-markdown td { border: 1px solid var(--line); padding: 3px 8px; }
25
+ block-markdown blockquote { border-left: 3px solid var(--line); padding-left: 8px; color: var(--soft); }`;
26
+
27
+ export default class extends HTMLElement {
28
+ update({ text = "" }) {
29
+ if (!document.getElementById("block-markdown-css")) {
30
+ document.head.insertAdjacentHTML("beforeend", `<style id="block-markdown-css">${CSS}</style>`);
31
+ }
32
+ if (this.state?.text === text) return; // unchanged: keep the DOM (and any selection)
33
+ this.state = { text };
34
+ this.innerHTML = DOMPurify.sanitize(marked.parse(text, { gfm: true, breaks: true }));
35
+ for (const a of this.querySelectorAll("a")) { a.target = "_blank"; a.rel = "noopener"; }
36
+ }
37
+ }
@@ -1,4 +1,4 @@
1
- // Card: a glass panel with an optional title; its children are stacked inside.
1
+ // Panel: a glass panel with an optional title; its children are stacked inside.
2
2
  export const about = {
3
3
  "version": 1,
4
4
  "props": {
@@ -9,7 +9,7 @@ export const about = {
9
9
  "example": {"title": "Lunch orders"}
10
10
  };
11
11
  export default class extends HTMLElement {
12
- update({ title = "" }, ui) {
12
+ update({ title = "" }, card) {
13
13
  if (!this.body) {
14
14
  this.style.cssText = "display:block;padding:14px 16px;border-radius:var(--radius);background:var(--glass-strong);" +
15
15
  "border:1px solid var(--edge);box-shadow:var(--shadow);backdrop-filter:var(--blur)";
@@ -18,6 +18,6 @@ export default class extends HTMLElement {
18
18
  }
19
19
  this.firstChild.textContent = title;
20
20
  this.firstChild.hidden = !title;
21
- ui.mount(this.body);
21
+ card.mount(this.body);
22
22
  }
23
23
  }
@@ -10,8 +10,8 @@ export const about = {
10
10
  "example": {"gap": 8}
11
11
  };
12
12
  export default class extends HTMLElement {
13
- update({ gap = 8, align = "center" }, ui) {
13
+ update({ gap = 8, align = "center" }, card) {
14
14
  this.style.cssText = `display:flex;flex-wrap:wrap;gap:${gap}px;align-items:${align}`;
15
- ui.mount(this);
15
+ card.mount(this);
16
16
  }
17
17
  }
@@ -12,17 +12,17 @@ export const about = {
12
12
  "example": {"label": "Your name", "value": "", "placeholder": "e.g. S. Tan"}
13
13
  };
14
14
  export default class extends HTMLElement {
15
- update({ label = "", value = "", placeholder = "", multiline = false }, ui) {
15
+ update({ label = "", value = "", placeholder = "", multiline = false }, card) {
16
16
  if (!this.input) {
17
17
  this.style.cssText = "display:flex;flex-direction:column;gap:4px";
18
18
  this.innerHTML = `<label style="font-size:12px;color:var(--muted)"></label>`;
19
19
  this.input = document.createElement(multiline ? "textarea" : "input");
20
20
  this.input.style.cssText = "padding:8px 10px;font:inherit;color:inherit;border-radius:10px;" +
21
21
  "background:var(--glass-strong);border:1px solid var(--line)";
22
- this.input.oninput = () => this.ui.set("value", this.input.value);
22
+ this.input.oninput = () => this.card.set("value", this.input.value);
23
23
  this.append(this.input);
24
24
  }
25
- this.ui = ui;
25
+ this.card = card;
26
26
  this.firstChild.textContent = label;
27
27
  this.input.placeholder = placeholder;
28
28
  if (document.activeElement !== this.input) this.input.value = value ?? ""; // don't fight the user's typing
@@ -1,73 +1,75 @@
1
- // Browser half of the ui plugin (a page script, see chat_ui.py). Draws UI surfaces in the "top" slot
2
- // (the page's main space). A surface is the record described at the top of ui.py.
3
- // A surface is bare: only what the agent built, no frame (its label shows on hover: name, owner, ✕). It is
1
+ // The page side of cards (index.ts here: the record, the tools, the messages). Draws the cards of the main space (the
2
+ // "top" slot): each shows its blocks, or its app (built/apps/..., served at /built/...) in a frame.
3
+ // A card is bare: only what the agent built, no frame (its label shows on hover: name, owner, ✕). It is
4
4
  // stacked in the main space, its content keeping its own size; or pinned at place {x, y, width?, height?}
5
- // (px from the main space's top-left, so it scrolls with the page), its root component then filling it.
5
+ // (px from the main space's top-left, so it scrolls with the page), its root block then filling it.
6
6
  // What the user can do with the label (the handle, so clicks inside still work):
7
- // drag move it; a drop sends {type: "ui_move", surface, place} (unless draggable: false)
8
- // drag and hold still over another surface (it lights up), release combine: it slides back and the page
9
- // fires a "ui-combine" DOM event. A plain drop over another surface just moves it (they overlap)
10
- // {a, b, x, y} (point.js opens a note box; the agent then changes b, and a stays).
11
- // Onto a pinned surface: refused (it would change), with a note in the chat
12
- // hold pin or unpin it ({type: "ui_pin"}): agents can't change a pinned surface, and it stays put
7
+ // drag move it; a drop sends {type: "card_move", card, place} (unless draggable: false)
8
+ // drag and hold still over another card (it lights up), release combine: it slides back and the page
9
+ // fires a "card-combine" DOM event. A plain drop over another card just moves it (they overlap)
10
+ // {a, b, x, y} (note/browser.js opens a note box; the agent then changes b, and a stays).
11
+ // Onto a pinned card: refused (it would change), with a note in the chat
12
+ // hold pin or unpin it ({type: "card_pin"}): agents can't change a pinned card, and it stays put
13
13
  // double-click turn it over: the back side shows what it is for (about), what it was made from, and
14
- // each component's levers (sentences from its about block; the words in them change it)
15
- // ✕ close it; if other surfaces read from it, the label asks first (they get a copy)
16
- // The corner resizes it (unless resizable: false). Moving or resizing one first pins every stacked surface
14
+ // each block's levers (sentences from its about block; the words in them change it)
15
+ // ✕ close it; if other cards read from it, the label asks first (they get a copy)
16
+ // The corner resizes it (unless resizable: false). Moving or resizing one first pins every stacked card
17
17
  // where it is (else the ones below would jump up).
18
- // Typing into a bound field changes the data at once and sends {type: "ui_set"} shortly after: saved, and
19
- // no agent is woken. Props can read other surfaces' data ({"ref", "path"}); those redraw when it changes.
20
- // Alternatives (options): drawn as a stack with a bar to flip through them; "Keep" sends {type: "ui_keep"}.
21
- // Components come from the page's catalog (page.load defines <ui-name>; see chat_ui.html); one saved with
22
- // an older version of its props goes through the component's upgrade(props, fromVersion) first.
23
- // Elements are kept per id across redraws, so a component keeps its state (focus, a running game).
18
+ // Typing into a bound field changes the data at once and sends {type: "card_set"} shortly after: saved, and
19
+ // no agent is woken; so does a click on a Button with set (card.write). A block with
20
+ // when: {"path", "is"} is hidden while that data is something else (tabs, steps, "show more"). Props can read other cards' data ({"ref", "path"}); those redraw when it changes.
21
+ // Alternatives (options): drawn as a stack with a bar to flip through them; "Keep" sends {type: "card_keep"}.
22
+ // Blocks come from the page's catalog (page.load defines <block-name>; see page.html); one saved with
23
+ // an older version of its props goes through the block's upgrade(props, fromVersion) first.
24
+ // Elements are kept per id across redraws, so a block keeps its state (focus, a running game).
24
25
  const get = (data, path) => path.split("/").filter(Boolean).reduce((o, k) => o?.[k], data);
25
26
  const isBinding = (v) => v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).join() === "path";
26
27
  const isRef = (v) => v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === "path,ref";
27
- const propsOf = (c) => Object.fromEntries(Object.entries(c).filter(([k]) => !["id", "component", "children", "child"].includes(k)));
28
- const refsIn = (s) => new Set(s.components.flatMap((c) => Object.values(propsOf(c)).filter(isRef).map((v) => v.ref)));
28
+ const propsOf = (c) => Object.fromEntries(Object.entries(c).filter(([k]) => !["id", "block", "children", "child", "when"].includes(k)));
29
+ const refsIn = (s) => new Set((s.blocks ?? []).flatMap((c) => Object.values(propsOf(c)).filter(isRef).map((v) => v.ref)));
29
30
  const HOLD_MS = 550; // hold the label this long, without moving, to pin or unpin
30
- const COMBINE_MS = 600; // hold a dragged surface still over another this long to combine with it
31
+ const COMBINE_MS = 600; // hold a dragged card still over another this long to combine with it
31
32
 
32
33
  const CSS = `
33
- [data-surface] { position: relative; display: flex; flex-direction: column; width: fit-content; max-width: 100%; margin: 0 0 20px; }
34
- [data-surface] > .surface-body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: flex-start; }
35
- [data-surface] > .surface-body > * { max-width: 100%; }
36
- [data-surface][data-placed] > .surface-body { align-items: stretch; }
37
- [data-surface][data-placed] > .surface-body > * { flex: 1; min-height: 0; }
38
- [data-surface] > .surface-label { position: absolute; bottom: 100%; left: 0; z-index: 2; display: flex; gap: 8px; align-items: center;
34
+ [data-card] { position: relative; display: flex; flex-direction: column; width: fit-content; max-width: 100%; margin: 0 0 20px; }
35
+ [data-card] > .card-body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: flex-start; }
36
+ [data-card] > .card-body > * { max-width: 100%; }
37
+ [data-card] > .card-body [hidden] { display: none !important; }
38
+ [data-card][data-placed] > .card-body { align-items: stretch; }
39
+ [data-card][data-placed] > .card-body > * { flex: 1; min-height: 0; }
40
+ [data-card] > .card-label { position: absolute; bottom: 100%; left: 0; z-index: 2; display: flex; gap: 8px; align-items: center;
39
41
  padding: 2px 8px; border-radius: 8px; font-size: 11px; white-space: nowrap; color: var(--muted, #888); cursor: grab;
40
42
  background: var(--glass-strong, #fff); opacity: 0; pointer-events: none; transition: opacity 0.15s; user-select: none; }
41
- [data-surface]:hover > .surface-label, [data-surface] > .surface-label.asking { opacity: 1; pointer-events: auto; }
42
- [data-surface] > .surface-label button { color: inherit; font: inherit; background: none; border: 0; cursor: pointer; padding: 0 2px; }
43
- [data-surface] > .surface-label .ask { color: var(--ink, #1d1d1f); }
44
- [data-surface] > .surface-resize { position: absolute; right: 0; bottom: 0; z-index: 1; width: 10px; height: 10px;
43
+ [data-card]:hover > .card-label, [data-card] > .card-label.asking { opacity: 1; pointer-events: auto; }
44
+ [data-card] > .card-label button { color: inherit; font: inherit; background: none; border: 0; cursor: pointer; padding: 0 2px; }
45
+ [data-card] > .card-label .ask { color: var(--ink, #1d1d1f); }
46
+ [data-card] > .card-resize { position: absolute; right: 0; bottom: 0; z-index: 1; width: 10px; height: 10px;
45
47
  border: solid var(--muted, #888); border-width: 0 2px 2px 0; border-radius: 0 0 4px 0; cursor: nwse-resize;
46
48
  opacity: 0; pointer-events: none; transition: opacity 0.15s; }
47
- [data-surface]:hover > .surface-resize { opacity: 1; pointer-events: auto; }
48
- [data-surface][data-pinned] { outline: 2px solid #d4691e; outline-offset: 4px; border-radius: 6px; }
49
- [data-surface][data-pinned] > .surface-label { cursor: default; }
50
- [data-surface][data-pinned] > .surface-resize { display: none; }
51
- [data-surface][data-drop] { outline: 3px solid var(--accent, #0071e3); outline-offset: 4px; border-radius: 6px; }
52
- [data-surface][data-flipped] { z-index: 5; } /* the back side stays above its neighbours */
53
- [data-surface][data-flipped] > .surface-body { visibility: hidden; }
54
- [data-surface] > .surface-back { position: absolute; left: 0; top: 0; z-index: 1; box-sizing: border-box; min-width: 260px; min-height: 100%;
49
+ [data-card]:hover > .card-resize { opacity: 1; pointer-events: auto; }
50
+ [data-card][data-pinned] { outline: 2px solid #d4691e; outline-offset: 4px; border-radius: 6px; }
51
+ [data-card][data-pinned] > .card-label { cursor: default; }
52
+ [data-card][data-pinned] > .card-resize { display: none; }
53
+ [data-card][data-drop] { outline: 3px solid var(--accent, #0071e3); outline-offset: 4px; border-radius: 6px; }
54
+ [data-card][data-flipped] { z-index: 5; } /* the back side stays above its neighbours */
55
+ [data-card][data-flipped] > .card-body { visibility: hidden; }
56
+ [data-card] > .card-back { position: absolute; left: 0; top: 0; z-index: 1; box-sizing: border-box; min-width: 260px; min-height: 100%;
55
57
  max-width: 420px; padding: 12px 14px; border-radius: 12px; font-size: 13px; line-height: 1.5; color: #e8eeea; background: #1f2a25;
56
58
  box-shadow: 0 8px 30px rgba(0,0,0,.18); display: flex; flex-direction: column; gap: 8px; }
57
- [data-surface] > .surface-back p { margin: 0; }
58
- [data-surface] > .surface-back ul { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
59
- [data-surface] > .surface-back .lever { font: inherit; color: #1f2a25; background: #f4d35e; border: 0; border-radius: 4px;
59
+ [data-card] > .card-back p { margin: 0; }
60
+ [data-card] > .card-back ul { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
61
+ [data-card] > .card-back .lever { font: inherit; color: #1f2a25; background: #f4d35e; border: 0; border-radius: 4px;
60
62
  padding: 0 5px; cursor: pointer; outline-offset: 1px; }
61
- [data-surface] > .surface-back span.lever { cursor: text; }
62
- [data-surface] > .surface-back .lever.ref { background: #c7d3cd; cursor: default; }
63
- [data-surface] > .surface-back .muted { opacity: .7; }
64
- [data-surface] > .surface-back > button { align-self: flex-start; font: inherit; color: inherit; background: none;
63
+ [data-card] > .card-back span.lever { cursor: text; }
64
+ [data-card] > .card-back .lever.ref { background: #c7d3cd; cursor: default; }
65
+ [data-card] > .card-back .muted { opacity: .7; }
66
+ [data-card] > .card-back > button { align-self: flex-start; font: inherit; color: inherit; background: none;
65
67
  border: 1px solid currentColor; border-radius: 8px; padding: 3px 10px; cursor: pointer; }
66
- [data-surface][data-options] > .surface-body { box-shadow: 8px 8px 0 -2px rgba(0,0,0,.07), 16px 16px 0 -4px rgba(0,0,0,.05); border-radius: 10px; }
67
- [data-surface] > .surface-options { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; font-size: 12px; color: var(--muted, #888); }
68
- [data-surface] > .surface-options button { font: inherit; color: inherit; background: var(--glass-strong, #fff);
68
+ [data-card][data-options] > .card-body { box-shadow: 8px 8px 0 -2px rgba(0,0,0,.07), 16px 16px 0 -4px rgba(0,0,0,.05); border-radius: 10px; }
69
+ [data-card] > .card-options { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; font-size: 12px; color: var(--muted, #888); }
70
+ [data-card] > .card-options button { font: inherit; color: inherit; background: var(--glass-strong, #fff);
69
71
  border: 1px solid var(--line, #ddd); border-radius: 8px; padding: 1px 8px; cursor: pointer; }
70
- [data-surface] > .surface-options button.keep { color: #fff; background: var(--accent, #0071e3); border-color: transparent; }`;
72
+ [data-card] > .card-options button.keep { color: #fff; background: var(--accent, #0071e3); border-color: transparent; }`;
71
73
 
72
74
  function setPath(data, path, value) {
73
75
  const keys = path.split("/").filter(Boolean), last = keys.pop();
@@ -83,49 +85,50 @@ function el(tag, attrs = {}, ...children) { // a small element builder (textCon
83
85
  }
84
86
 
85
87
  export default async function (page) {
86
- if (!page.load) return; // an older page (e.g. a Versions preview) without the component runtime
87
- document.head.insertAdjacentHTML("beforeend", `<style id="ui-surface-css">${CSS}</style>`);
88
+ if (!page.load) return; // an older page (e.g. a Versions preview) without the block runtime
89
+ document.head.insertAdjacentHTML("beforeend", `<style id="card-css">${CSS}</style>`);
88
90
  const area = page.slot("top");
89
- const surfaces = {}; // id -> the record from ui.py, plus what only this page has: box, body, nodes, flipped, option
91
+ const cards = {}; // id -> the record (index.ts), plus what only this page has: box, body, nodes, frame, flipped, option
90
92
  let abouts = {}, mtimes = {};
91
- const modules = {}; // component name -> its module (for upgrade())
93
+ const modules = {}; // block name -> its module (for upgrade())
92
94
  const loadAbouts = async () => {
93
- try { [abouts, mtimes] = await Promise.all(["/ui-api/about", "/ui-api/catalog"].map(async (u) => (await fetch(u)).json())); } catch {}
95
+ try { [abouts, mtimes] = await Promise.all(["/card/about", "/card/blocks"].map(async (u) => (await fetch(u)).json())); } catch {}
94
96
  };
95
- page.on("version", loadAbouts); // an agent may have changed a component
96
- const pending = {}; // "surface path" -> timer: ui_set waits until the typing pauses
97
+ page.on("version", loadAbouts); // an agent may have changed a block
98
+ const pending = {}; // "card path" -> timer: card_set waits until the typing pauses
97
99
  const sendSet = (id, path, value) => {
98
100
  clearTimeout(pending[`${id} ${path}`]);
99
- pending[`${id} ${path}`] = setTimeout(() => page.send({ type: "ui_set", surface: id, path, value }), 500);
101
+ pending[`${id} ${path}`] = setTimeout(() => page.send({ type: "card_set", card: id, path, value }), 500);
100
102
  };
101
103
 
102
104
  async function upgraded(name, props, s) { // props saved with an older about.version: through upgrade() first
103
105
  const from = s.kinds?.[name], now = abouts[name]?.version;
104
106
  if (!from || !now || from >= now) return props;
105
107
  try {
106
- const mod = await (modules[name] ??= import(`/ui/components/${name}.js?v=${mtimes[name]}`));
108
+ const mod = await (modules[name] ??= import(`/card/blocks/${name}.js?v=${mtimes[name]}`));
107
109
  return mod.upgrade ? mod.upgrade(props, from) : props;
108
110
  } catch (e) { console.warn(`upgrade of ${name}:`, e); return props; }
109
111
  }
110
112
 
111
113
  async function draw(id, chain = new Set()) {
112
- const s = surfaces[id];
113
- if (!s || chain.has(id)) return; // chain: surfaces reading from each other are drawn once each
114
+ const s = cards[id];
115
+ if (!s || chain.has(id)) return; // chain: cards reading from each other are drawn once each
114
116
  chain.add(id);
115
- if (!s.box) { // the surface, with a label (name, pin, ✕) that shows on hover
117
+ if (!s.box) { // the card, with a label (name, pin, ✕) that shows on hover
116
118
  s.box = el("div");
117
- s.box.dataset.surface = id; // lets the page work out what a click points at (point.js)
118
- s.label = el("div", { class: "surface-label" });
119
- s.body = el("div", { class: "surface-body" });
120
- s.box.append(s.label, s.body, el("div", { class: "surface-resize", title: "Drag to resize", onpointerdown: (e) => resize(s, id, e) }));
119
+ s.box.dataset.card = id; // lets the page work out what a click points at (note/browser.js)
120
+ s.label = el("div", { class: "card-label" });
121
+ s.body = el("div", { class: "card-body" });
122
+ s.box.append(s.label, s.body, el("div", { class: "card-resize", title: "Drag to resize", onpointerdown: (e) => resize(s, id, e) }));
121
123
  s.label.onpointerdown = (e) => drag(s, id, e);
122
124
  s.label.ondblclick = () => { s.flipped = !s.flipped; draw(id); };
123
125
  s.nodes = {};
124
126
  area.append(s.box);
125
127
  }
126
128
  drawLabel(s, id);
127
- s.box.dataset.owner = s.owner; // who a note on this surface goes to (point.js)
128
- s.box.querySelector(".surface-resize").hidden = s.resizable === false;
129
+ s.box.dataset.owner = s.owner; // who a note on this card goes to (note/browser.js)
130
+ s.box.dataset.board = s.board ?? 1; // its board: board/browser.js shows it there
131
+ s.box.querySelector(".card-resize").hidden = s.resizable === false;
129
132
  s.box.toggleAttribute("data-pinned", !!s.pinned);
130
133
  s.box.toggleAttribute("data-flipped", !!s.flipped);
131
134
  s.box.toggleAttribute("data-options", !!s.options);
@@ -135,24 +138,26 @@ export default async function (page) {
135
138
  width: p.width ? `${p.width}px` : "", height: p.height ? `${p.height}px` : "" }
136
139
  : { position: "", left: "", top: "", margin: "", width: "", height: "" });
137
140
  drawOptionsBar(s, id);
138
- const shown = s.options ? s.options[s.option ?? 0] : s; // an alternative, or the surface itself
141
+ const shown = s.options ? s.options[s.option ?? 0] : s; // an alternative, or the card itself
142
+ if (shown.app) return drawApp(s, id, shown);
143
+ s.frame = null;
139
144
  try {
140
- for (const c of shown.components) if (!(await page.load(c.component))) throw new Error(`no component ${c.component}`);
145
+ for (const c of shown.blocks) if (!(await page.load(c.block))) throw new Error(`no block ${c.block}`);
141
146
  } catch (e) {
142
- s.body.textContent = `Could not load a component: ${e}`;
147
+ s.body.textContent = `Could not load a block: ${e}`;
143
148
  return;
144
149
  }
145
- const byId = Object.fromEntries(shown.components.map((c) => [c.id, c]));
146
- const resolve = (v) => (isBinding(v) ? get(shown.data, v.path) : isRef(v) ? get(surfaces[v.ref]?.data ?? {}, v.path) : v);
150
+ const byId = Object.fromEntries(shown.blocks.map((c) => [c.id, c]));
151
+ const resolve = (v) => (isBinding(v) ? get(shown.data, v.path) : isRef(v) ? get(cards[v.ref]?.data ?? {}, v.path) : v);
147
152
  const props = {};
148
- for (const c of shown.components) props[c.id] = await upgraded(c.component, Object.fromEntries(
153
+ for (const c of shown.blocks) props[c.id] = await upgraded(c.block, Object.fromEntries(
149
154
  Object.entries(propsOf(c)).map(([k, v]) => [k, resolve(v)])), s);
150
155
 
151
156
  const build = (cid) => {
152
- const c = byId[cid], tag = `ui-${c.component.toLowerCase()}`;
157
+ const c = byId[cid], tag = `block-${c.block.toLowerCase()}`;
153
158
  const node = s.nodes[cid]?.localName === tag ? s.nodes[cid] : document.createElement(tag);
154
159
  s.nodes[cid] = node;
155
- node.dataset.cid = cid;
160
+ node.dataset.block = cid;
156
161
  const children = (c.children || (c.child ? [c.child] : [])).map(build);
157
162
  node.update(props[cid], {
158
163
  children,
@@ -160,67 +165,89 @@ export default async function (page) {
160
165
  if (container.children.length !== children.length || children.some((ch, i) => container.children[i] !== ch))
161
166
  container.replaceChildren(...children);
162
167
  },
163
- set(prop, value) { // write a bound prop into the data: at once here, saved shortly after (ui_set)
164
- if (!isBinding(c[prop])) return; // a plain value or another surface's data: not this field's to write
168
+ set(prop, value) { // write a bound prop into the data: at once here, saved shortly after (card_set)
169
+ if (!isBinding(c[prop])) return; // a plain value or another card's data: not this field's to write
165
170
  setPath(shown.data, c[prop].path, value);
166
171
  if (!s.options) sendSet(id, c[prop].path, value);
167
172
  draw(id);
168
173
  },
174
+ data: shown.data, // this card's data, to read (write through set or write)
175
+ write(values) { // {path: value, ...} into this card's data: at once here, saved shortly after (card_set)
176
+ for (const [path, value] of Object.entries(values)) {
177
+ setPath(shown.data, path, value);
178
+ if (!s.options) sendSet(id, path, value);
179
+ }
180
+ draw(id);
181
+ },
169
182
  action(name, context = {}) {
170
183
  const ctx = Object.fromEntries(Object.entries(context).map(([k, v]) => [k, resolve(v)]));
171
- page.send({ type: "ui_action", surface: id, component: cid, name, context: ctx, data: s.data });
184
+ page.send({ type: "card_action", card: id, block: cid, name, context: ctx, data: s.data });
172
185
  },
173
186
  });
187
+ node.hidden = !!c.when && get(shown.data, c.when.path) !== c.when.is; // shown only while its data matches
174
188
  return node;
175
189
  };
176
190
  try {
177
191
  const root = build("root");
178
192
  if (s.body.firstChild !== root) s.body.replaceChildren(root);
179
193
  } catch (e) {
180
- s.body.textContent = `Could not draw this surface: ${e}`;
194
+ s.body.textContent = `Could not draw this card: ${e}`;
181
195
  }
182
196
  drawBack(s, id, shown, resolve);
183
- for (const [other, o] of Object.entries(surfaces)) if (o.components && refsIn(o).has(id)) draw(other, chain);
197
+ for (const [other, o] of Object.entries(cards)) if (o.blocks && refsIn(o).has(id)) draw(other, chain);
198
+ }
199
+
200
+ function drawApp(s, id, shown) { // an app: its page in a frame, 640px wide (at most the space there); it fills a
201
+ if (!s.frame) { // placed card, else it is height px tall. The same frame across redraws: a running app isn't restarted
202
+ s.frame = el("iframe");
203
+ s.frame.style.cssText = "display:block;width:100%;min-height:0;border:0;background:transparent;color-scheme:normal";
204
+ }
205
+ s.frame.style.flex = `1 1 ${s.height ?? 400}px`;
206
+ s.frame.style.width = s.place?.width ? "100%" : "640px"; // a frame has no width of its own (else 300px)
207
+ if (s.body.firstChild !== s.frame) s.body.replaceChildren(s.frame);
208
+ const url = `/built/${shown.app}`;
209
+ if (s.frame.getAttribute("src") !== url) s.frame.setAttribute("src", url);
210
+ drawBack(s, id, shown, (v) => v);
184
211
  }
185
212
 
186
213
  function drawLabel(s, id) { // name · owner, pinned or not, ✕ (which asks first if others read from it)
187
- const users = Object.keys(surfaces).filter((o) => o !== id && surfaces[o].components && refsIn(surfaces[o]).has(id));
214
+ const users = Object.keys(cards).filter((o) => o !== id && cards[o].blocks && refsIn(cards[o]).has(id));
188
215
  s.label.classList.remove("asking");
189
216
  s.label.replaceChildren(
190
217
  el("span", { text: `${id} · ${s.owner}${s.pinned ? " · pinned" : ""}`, title: "Drag to move; hold still over another to combine; hold to pin; double-click to turn over" }),
191
218
  el("button", { text: "✕", title: "Close", onclick: () => {
192
- if (!users.length) return page.send({ type: "ui_close", surface: id });
219
+ if (!users.length) return page.send({ type: "card_close", card: id });
193
220
  s.label.classList.add("asking");
194
221
  s.label.replaceChildren(
195
222
  el("span", { class: "ask", text: `${users.join(", ")} ${users.length > 1 ? "read" : "reads"} from this. Copy it in, then close?` }),
196
- el("button", { text: "Copy & close", onclick: () => page.send({ type: "ui_close", surface: id }) }),
223
+ el("button", { text: "Copy & close", onclick: () => page.send({ type: "card_close", card: id }) }),
197
224
  el("button", { text: "Cancel", onclick: () => drawLabel(s, id) }));
198
225
  } }));
199
226
  }
200
227
 
201
228
  function drawOptionsBar(s, id) {
202
- s.box.querySelector(".surface-options")?.remove();
229
+ s.box.querySelector(".card-options")?.remove();
203
230
  if (!s.options) return;
204
231
  const i = s.option ?? 0, n = s.options.length;
205
232
  const flip = (d) => { s.option = (i + d + n) % n; draw(id); };
206
- const bar = el("div", { class: "surface-options" },
233
+ const bar = el("div", { class: "card-options" },
207
234
  el("button", { text: "‹", title: "Previous", "aria-label": "Previous option", onclick: () => flip(-1) }),
208
235
  el("span", { text: `${i + 1} of ${n}: ${s.options[i].label}` }),
209
236
  el("button", { text: "›", title: "Next", "aria-label": "Next option", onclick: () => flip(1) }),
210
- el("button", { class: "keep", text: "Keep this", onclick: () => page.send({ type: "ui_keep", surface: id, index: i }) }));
237
+ el("button", { class: "keep", text: "Keep this", onclick: () => page.send({ type: "card_keep", card: id, index: i }) }));
211
238
  bar.onwheel = (e) => { e.preventDefault(); flip(Math.sign(e.deltaY || e.deltaX)); };
212
239
  s.box.insertBefore(bar, s.body);
213
240
  }
214
241
 
215
242
  function drawBack(s, id, shown, resolve) { // the back side: about, made from, levers, pinned
216
- s.box.querySelector(".surface-back")?.remove();
243
+ s.box.querySelector(".card-back")?.remove();
217
244
  if (!s.flipped) return;
218
- const back = el("div", { class: "surface-back" }, el("b", { text: `How ${id} works` }),
245
+ const back = el("div", { class: "card-back" }, el("b", { text: `How ${id} works` }),
219
246
  el("p", { class: s.about ? "" : "muted", text: s.about || "No description yet. Ask its agent to write one." }));
220
247
  if (s.made_from) back.append(el("p", { text: `Made from ${s.made_from.join(" and ")}` + (s.instruction ? `: “${s.instruction}”` : ".") }));
221
248
  const list = el("ul");
222
- for (const c of shown.components) {
223
- const spec = abouts[c.component];
249
+ for (const c of shown.blocks ?? []) {
250
+ const spec = abouts[c.block];
224
251
  for (const sentence of spec?.levers || []) {
225
252
  const li = el("li", {}, el("span", { class: "muted", text: `${c.id}: ` }));
226
253
  for (const [i, part] of sentence.split(/\{(\w+)\}/).entries()) li.append(i % 2 ? lever(s, id, shown, c, part, spec.props?.[part] || {}, resolve) : part);
@@ -229,7 +256,7 @@ export default async function (page) {
229
256
  }
230
257
  if (list.children.length) back.append(list);
231
258
  back.append(el("p", {}, "It is ", el("button", { class: "lever", text: s.pinned ? "pinned" : "not pinned",
232
- onclick: () => page.send({ type: "ui_pin", surface: id, pinned: !s.pinned }) }),
259
+ onclick: () => page.send({ type: "card_pin", card: id, pinned: !s.pinned }) }),
233
260
  `: agents ${s.pinned ? "can't" : "may"} change it.`));
234
261
  back.append(el("button", { text: "Turn back", onclick: () => { s.flipped = false; draw(id); } }));
235
262
  s.box.append(back);
@@ -239,8 +266,8 @@ export default async function (page) {
239
266
  const raw = c[prop], value = resolve(raw) ?? spec.default ?? "";
240
267
  if (isRef(raw)) return el("span", { class: "lever ref", text: String(value), title: `read from ${raw.ref}` });
241
268
  const commit = (v) => {
242
- if (isBinding(raw)) { setPath(shown.data, raw.path, v); page.send({ type: "ui_set", surface: id, path: raw.path, value: v }); }
243
- else { c[prop] = v; page.send({ type: "ui_prop", surface: id, component: c.id, prop, value: v }); }
269
+ if (isBinding(raw)) { setPath(shown.data, raw.path, v); page.send({ type: "card_set", card: id, path: raw.path, value: v }); }
270
+ else { c[prop] = v; page.send({ type: "card_prop", card: id, block: c.id, prop, value: v }); }
244
271
  draw(id);
245
272
  };
246
273
  if (spec.type === "choice" && spec.options?.length)
@@ -258,48 +285,48 @@ export default async function (page) {
258
285
  return el("span", { class: "lever ref", text: JSON.stringify(value) }); // lists, objects: shown, changed by the agent
259
286
  }
260
287
 
261
- page.on("ui", (ev) => {
288
+ page.on("card", (ev) => {
262
289
  if (ev.op === "render") {
263
- const old = surfaces[ev.surface] || {};
264
- const { op, type, surface, ...record } = ev;
265
- surfaces[ev.surface] = { ...record, box: old.box, label: old.label, body: old.body, nodes: old.nodes,
290
+ const old = cards[ev.card] || {};
291
+ const { op, type, card, ...record } = ev;
292
+ cards[ev.card] = { ...record, box: old.box, label: old.label, body: old.body, nodes: old.nodes, frame: old.frame,
266
293
  flipped: old.flipped, option: record.options ? old.option : 0 };
267
- draw(ev.surface);
268
- } else if (ev.op === "delete" && surfaces[ev.surface]) {
269
- surfaces[ev.surface].box?.remove();
270
- delete surfaces[ev.surface];
271
- for (const id of Object.keys(surfaces)) drawLabel(surfaces[id], id);
294
+ draw(ev.card);
295
+ } else if (ev.op === "delete" && cards[ev.card]) {
296
+ cards[ev.card].box?.remove();
297
+ delete cards[ev.card];
298
+ for (const id of Object.keys(cards)) drawLabel(cards[id], id);
272
299
  } else if (ev.op === "reset") { // a whole saved state (undo, redo, rewind)
273
- for (const id of Object.keys(surfaces)) if (!(id in ev.surfaces)) { surfaces[id].box?.remove(); delete surfaces[id]; }
274
- for (const [id, record] of Object.entries(ev.surfaces)) {
275
- const old = surfaces[id] || {};
276
- surfaces[id] = { ...record, box: old.box, label: old.label, body: old.body, nodes: old.nodes, flipped: old.flipped, option: 0 };
300
+ for (const id of Object.keys(cards)) if (!(id in ev.cards)) { cards[id].box?.remove(); delete cards[id]; }
301
+ for (const [id, record] of Object.entries(ev.cards)) {
302
+ const old = cards[id] || {};
303
+ cards[id] = { ...record, box: old.box, label: old.label, body: old.body, nodes: old.nodes, frame: old.frame, flipped: old.flipped, option: 0 };
277
304
  }
278
- for (const id of Object.keys(surfaces)) draw(id);
305
+ for (const id of Object.keys(cards)) draw(id);
279
306
  }
280
307
  });
281
308
 
282
- function pinStacked() { // pin each stacked surface where it is now, so taking one out of the stack moves none
283
- const stacked = Object.entries(surfaces).filter(([, s]) => !s.place && s.box);
309
+ function pinStacked() { // pin each stacked card where it is now, so taking one out of the stack moves none
310
+ const stacked = Object.entries(cards).filter(([, s]) => !s.place && s.box);
284
311
  for (const [, s] of stacked) s.place = { x: s.box.offsetLeft, y: s.box.offsetTop }; // read all, then move
285
312
  for (const [id, s] of stacked) {
286
313
  s.box.toggleAttribute("data-placed", true);
287
314
  Object.assign(s.box.style, { position: "absolute", margin: "0", left: `${s.place.x}px`, top: `${s.place.y}px` });
288
- page.send({ type: "ui_move", surface: id, place: s.place });
315
+ page.send({ type: "card_move", card: id, place: s.place });
289
316
  }
290
317
  }
291
318
 
292
- function surfaceAt(x, y, except) { // the surface under a window point, other than `except`
293
- return document.elementsFromPoint(x, y).map((e) => e.closest?.("[data-surface]")).find((b) => b && b !== except) || null;
319
+ function cardAt(x, y, except) { // the card under a window point, other than `except`
320
+ return document.elementsFromPoint(x, y).map((e) => e.closest?.("[data-card]")).find((b) => b && b !== except) || null;
294
321
  }
295
322
 
296
- function drag(s, id, e) { // hold: pin; move: drag it; hold still over another surface, then release: combine
323
+ function drag(s, id, e) { // hold: pin; move: drag it; hold still over another card, then release: combine
297
324
  if (e.target.closest("button")) return;
298
325
  const handle = e.currentTarget, box = s.box;
299
- handle.setPointerCapture(e.pointerId); // keep getting moves over iframes and other surfaces
326
+ handle.setPointerCapture(e.pointerId); // keep getting moves over iframes and other cards
300
327
  let place = null, held = false, x0 = 0, y0 = 0;
301
- let under = null, armed = null, wait = null, sx = 0, sy = 0; // the surface under the pointer; armed: lit, ready to combine
302
- const hold = setTimeout(() => { held = true; page.send({ type: "ui_pin", surface: id, pinned: !s.pinned }); }, HOLD_MS);
328
+ let under = null, armed = null, wait = null, sx = 0, sy = 0; // the card under the pointer; armed: lit, ready to combine
329
+ const hold = setTimeout(() => { held = true; page.send({ type: "card_pin", card: id, pinned: !s.pinned }); }, HOLD_MS);
303
330
  handle.onpointermove = (ev) => {
304
331
  if (held || Math.hypot(ev.clientX - e.clientX, ev.clientY - e.clientY) < 5) return;
305
332
  clearTimeout(hold);
@@ -308,7 +335,7 @@ export default async function (page) {
308
335
  place = { ...s.place, x: Math.round(x0 + ev.clientX - e.clientX), y: Math.round(y0 + ev.clientY - e.clientY) };
309
336
  box.toggleAttribute("data-placed", true);
310
337
  Object.assign(box.style, { position: "absolute", margin: "0", left: `${place.x}px`, top: `${place.y}px` });
311
- const now = surfaceAt(ev.clientX, ev.clientY, box);
338
+ const now = cardAt(ev.clientX, ev.clientY, box);
312
339
  if (now !== under || Math.hypot(ev.clientX - sx, ev.clientY - sy) > 6) { // moved: start waiting again
313
340
  clearTimeout(wait);
314
341
  armed?.removeAttribute("data-drop");
@@ -323,19 +350,19 @@ export default async function (page) {
323
350
  armed?.removeAttribute("data-drop");
324
351
  if (!place) return; // a click or a hold, not a drag
325
352
  box.style.zIndex = "";
326
- if (armed) { // combine: put this one back, and ask how (point.js); b is the one that changes
353
+ if (armed) { // combine: put this one back, and ask how (note/browser.js); b is the one that changes
327
354
  Object.assign(box.style, { left: `${s.place.x}px`, top: `${s.place.y}px` });
328
- const b = armed.dataset.surface;
329
- if (surfaces[b]?.pinned) return page.note(`${b} is pinned, so it can't take ${id} in. Hold its label to unpin it first.`);
330
- document.dispatchEvent(new CustomEvent("ui-combine", { detail: { a: id, b, x: ev.clientX, y: ev.clientY } }));
355
+ const b = armed.dataset.card;
356
+ if (cards[b]?.pinned) return page.note(`${b} is pinned, so it can't take ${id} in. Hold its label to unpin it first.`);
357
+ document.dispatchEvent(new CustomEvent("card-combine", { detail: { a: id, b, x: ev.clientX, y: ev.clientY } }));
331
358
  return;
332
359
  }
333
360
  s.place = place;
334
- page.send({ type: "ui_move", surface: id, place });
361
+ page.send({ type: "card_move", card: id, place });
335
362
  };
336
363
  }
337
364
 
338
- function resize(s, id, e) { // drag the corner: the surface's new width and height; on release, pin it so
365
+ function resize(s, id, e) { // drag the corner: the card's new width and height; on release, pin it so
339
366
  if (s.pinned) return;
340
367
  pinStacked();
341
368
  const handle = e.currentTarget, box = s.box;
@@ -353,11 +380,11 @@ export default async function (page) {
353
380
  handle.onpointermove = handle.onpointerup = null;
354
381
  if (!place) return;
355
382
  s.place = place;
356
- page.send({ type: "ui_move", surface: id, place });
383
+ page.send({ type: "card_move", card: id, place });
357
384
  };
358
385
  }
359
386
 
360
387
  await loadAbouts();
361
- for (const [id, s] of Object.entries(await (await fetch("/ui-api/surfaces")).json())) surfaces[id] = { ...s, option: 0 };
362
- for (const id of Object.keys(surfaces)) draw(id);
388
+ for (const [id, s] of Object.entries(await (await fetch("/card/all")).json())) cards[id] = { ...s, option: 0 };
389
+ for (const id of Object.keys(cards)) draw(id);
363
390
  }
@@ -0,0 +1,13 @@
1
+ # Cards
2
+ Show what you build as cards on the boards in the page's main space: each shows blocks (JSON) or an app (a page you
3
+ write in {built}/apps/), with card_show, card_update and card_remove, rather than by editing the page.
4
+ For a chart, a diagram, a game's art or a restyle, load the design skill first.
5
+ - Name what you build (card ids, block ids, element ids): the user points at things by name.
6
+ - The user moves and resizes cards: build things that fill their box (%, flex, a viewBox), not fixed px.
7
+ - Blocks when clicks and typing only store values; an app when the page must calculate, add or remove items, follow
8
+ rules or draw.
9
+ - card_show(card, app=<name>) checks an app on the board and reports errors and whether it fits: fix those. Before
10
+ telling the user what a card does, try its main action (the app README says how) and describe only what worked.
11
+ - A card the user pinned: don't work around it (make a new one, made_from=[it], or ask).
12
+ Before writing an app, read {agent}/extensions/app/README.md: it is all an app needs. For blocks, load card_blocks
13
+ (tool_search): it lists them and says what to read.