@chalky/chalky 0.1.4 → 0.1.6

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 (137) hide show
  1. package/README.md +16 -5
  2. package/agent/SYSTEM.md +36 -0
  3. package/agent/extensions/app/README.md +72 -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/app/skills/test-your-app/SKILL.md +23 -0
  9. package/{app/extensions/boards → agent/extensions/board}/browser.js +30 -32
  10. package/{app/extensions/boards → agent/extensions/board}/guide.md +1 -1
  11. package/{app/extensions/boards → agent/extensions/board}/index.ts +32 -30
  12. package/agent/extensions/card/README.md +88 -0
  13. package/agent/extensions/card/blocks/Button.js +34 -0
  14. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  15. package/agent/extensions/card/blocks/Markdown.js +37 -0
  16. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  18. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  19. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -129
  20. package/agent/extensions/card/guide.md +13 -0
  21. package/agent/extensions/card/index.ts +370 -0
  22. package/{app/extensions/commands → agent/extensions/command}/browser.js +3 -3
  23. package/agent/extensions/command/guide.md +2 -0
  24. package/{app/extensions/commands → agent/extensions/command}/index.ts +14 -16
  25. package/agent/extensions/doodle/README.md +57 -0
  26. package/agent/extensions/doodle/browser.js +1013 -0
  27. package/agent/extensions/doodle/guide.md +22 -0
  28. package/agent/extensions/doodle/index.ts +274 -0
  29. package/agent/extensions/file/browser.js +182 -0
  30. package/agent/extensions/file/guide.md +5 -0
  31. package/{app/extensions/files → agent/extensions/file}/index.ts +37 -18
  32. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  33. package/{app/extensions/frames → agent/extensions/frame}/browser.js +20 -18
  34. package/agent/extensions/frame/guide.md +3 -0
  35. package/agent/extensions/frame/index.ts +90 -0
  36. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  37. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +34 -33
  38. package/{app → agent}/extensions/mcp/browser.js +1 -1
  39. package/agent/extensions/mcp/guide.md +3 -0
  40. package/{app → agent}/extensions/mcp/index.ts +2 -4
  41. package/{app/extensions/login → agent/extensions/model}/browser.js +67 -67
  42. package/{app → agent}/extensions/model/index.ts +8 -4
  43. package/{app/extensions/login/index.ts → agent/extensions/model/login.ts} +47 -46
  44. package/agent/extensions/note/README.md +37 -0
  45. package/agent/extensions/note/browser.js +382 -0
  46. package/agent/extensions/note/guide.md +9 -0
  47. package/agent/extensions/note/index.ts +215 -0
  48. package/agent/extensions/page/browser.js +453 -0
  49. package/{app → agent}/extensions/page/index.ts +72 -3
  50. package/{app → agent}/extensions/page/page.html +102 -49
  51. package/agent/extensions/page/skills/interact-with-page/SKILL.md +59 -0
  52. package/agent/extensions/prompt/index.ts +38 -0
  53. package/{app → agent}/extensions/reload/index.ts +3 -3
  54. package/{app → agent}/extensions/screen/browser.js +10 -23
  55. package/{app → agent}/extensions/screen/index.ts +11 -11
  56. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  57. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  58. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  59. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  60. package/{app/extensions/versions → agent/extensions/version}/browser.js +19 -10
  61. package/agent/extensions/version/index.ts +205 -0
  62. package/{app → agent}/extensions/viewer/README.md +4 -4
  63. package/{app → agent}/extensions/viewer/browser.js +8 -8
  64. package/agent/extensions/viewer/guide.md +3 -0
  65. package/{app → agent}/extensions/viewer/index.ts +22 -17
  66. package/{app → agent}/extensions/voice/browser.js +4 -3
  67. package/{app → agent}/extensions/voice/index.ts +8 -6
  68. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  69. package/agent/settings.json +8 -0
  70. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  71. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  72. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +15 -15
  73. package/agent/skills/extension-map/SKILL.md +56 -0
  74. package/agent/skills/show-me/SKILL.md +66 -0
  75. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  76. package/bin/chalky.mjs +2 -2
  77. package/package.json +9 -8
  78. package/server.ts +313 -58
  79. package/app/SYSTEM.md +0 -31
  80. package/app/extensions/appdata/README.md +0 -10
  81. package/app/extensions/appdata/guide.md +0 -4
  82. package/app/extensions/appdata/index.ts +0 -47
  83. package/app/extensions/commands/guide.md +0 -3
  84. package/app/extensions/doodle/README.md +0 -15
  85. package/app/extensions/doodle/browser.js +0 -398
  86. package/app/extensions/doodle/guide.md +0 -6
  87. package/app/extensions/doodle/index.ts +0 -54
  88. package/app/extensions/files/browser.js +0 -96
  89. package/app/extensions/files/guide.md +0 -7
  90. package/app/extensions/frames/guide.md +0 -7
  91. package/app/extensions/frames/index.ts +0 -85
  92. package/app/extensions/mcp/guide.md +0 -5
  93. package/app/extensions/point/README.md +0 -22
  94. package/app/extensions/point/browser.js +0 -247
  95. package/app/extensions/point/guide.md +0 -6
  96. package/app/extensions/point/index.ts +0 -112
  97. package/app/extensions/prompt/index.ts +0 -19
  98. package/app/extensions/ui/README.md +0 -69
  99. package/app/extensions/ui/components/App.js +0 -28
  100. package/app/extensions/ui/components/Button.js +0 -28
  101. package/app/extensions/ui/components/Markdown.js +0 -37
  102. package/app/extensions/ui/guide.md +0 -7
  103. package/app/extensions/ui/index.ts +0 -291
  104. package/app/extensions/versions/index.ts +0 -168
  105. package/app/extensions/viewer/guide.md +0 -5
  106. package/app/mcp.json +0 -3
  107. package/app/settings.json +0 -3
  108. package/app/skills/plugin-map/SKILL.md +0 -49
  109. package/app/ui/apps/.keep +0 -0
  110. package/node_modules/xlsx/CHANGELOG.md +0 -361
  111. package/node_modules/xlsx/LICENSE +0 -201
  112. package/node_modules/xlsx/README.md +0 -41
  113. package/node_modules/xlsx/bin/xlsx.njs +0 -310
  114. package/node_modules/xlsx/bower.json +0 -22
  115. package/node_modules/xlsx/dist/LICENSE +0 -201
  116. package/node_modules/xlsx/dist/cpexcel.d.ts +0 -39
  117. package/node_modules/xlsx/dist/cpexcel.full.mjs +0 -1502
  118. package/node_modules/xlsx/dist/cpexcel.js +0 -1506
  119. package/node_modules/xlsx/dist/shim.min.js +0 -2
  120. package/node_modules/xlsx/dist/xlsx.core.min.js +0 -18
  121. package/node_modules/xlsx/dist/xlsx.core.min.map +0 -1
  122. package/node_modules/xlsx/dist/xlsx.extendscript.js +0 -28259
  123. package/node_modules/xlsx/dist/xlsx.full.min.js +0 -24
  124. package/node_modules/xlsx/dist/xlsx.full.min.map +0 -1
  125. package/node_modules/xlsx/dist/xlsx.mini.min.js +0 -10
  126. package/node_modules/xlsx/dist/xlsx.mini.min.map +0 -1
  127. package/node_modules/xlsx/dist/xlsx.zahl.js +0 -4
  128. package/node_modules/xlsx/dist/xlsx.zahl.mjs +0 -2
  129. package/node_modules/xlsx/dist/zahl.d.ts +0 -4
  130. package/node_modules/xlsx/package.json +0 -183
  131. package/node_modules/xlsx/types/index.d.ts +0 -1065
  132. package/node_modules/xlsx/types/tsconfig.json +0 -15
  133. package/node_modules/xlsx/xlsx.js +0 -28103
  134. package/node_modules/xlsx/xlsx.mjs +0 -28228
  135. package/node_modules/xlsx/xlsxworker.js +0 -14
  136. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  137. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
@@ -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,50 +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.dataset.board = s.board ?? 1; // its board: boards/browser.js shows it there
129
- 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;
130
132
  s.box.toggleAttribute("data-pinned", !!s.pinned);
131
133
  s.box.toggleAttribute("data-flipped", !!s.flipped);
132
134
  s.box.toggleAttribute("data-options", !!s.options);
@@ -136,24 +138,26 @@ export default async function (page) {
136
138
  width: p.width ? `${p.width}px` : "", height: p.height ? `${p.height}px` : "" }
137
139
  : { position: "", left: "", top: "", margin: "", width: "", height: "" });
138
140
  drawOptionsBar(s, id);
139
- 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;
140
144
  try {
141
- 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}`);
142
146
  } catch (e) {
143
- s.body.textContent = `Could not load a component: ${e}`;
147
+ s.body.textContent = `Could not load a block: ${e}`;
144
148
  return;
145
149
  }
146
- const byId = Object.fromEntries(shown.components.map((c) => [c.id, c]));
147
- 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);
148
152
  const props = {};
149
- 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(
150
154
  Object.entries(propsOf(c)).map(([k, v]) => [k, resolve(v)])), s);
151
155
 
152
156
  const build = (cid) => {
153
- const c = byId[cid], tag = `ui-${c.component.toLowerCase()}`;
157
+ const c = byId[cid], tag = `block-${c.block.toLowerCase()}`;
154
158
  const node = s.nodes[cid]?.localName === tag ? s.nodes[cid] : document.createElement(tag);
155
159
  s.nodes[cid] = node;
156
- node.dataset.cid = cid;
160
+ node.dataset.block = cid;
157
161
  const children = (c.children || (c.child ? [c.child] : [])).map(build);
158
162
  node.update(props[cid], {
159
163
  children,
@@ -161,67 +165,89 @@ export default async function (page) {
161
165
  if (container.children.length !== children.length || children.some((ch, i) => container.children[i] !== ch))
162
166
  container.replaceChildren(...children);
163
167
  },
164
- set(prop, value) { // write a bound prop into the data: at once here, saved shortly after (ui_set)
165
- 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
166
170
  setPath(shown.data, c[prop].path, value);
167
171
  if (!s.options) sendSet(id, c[prop].path, value);
168
172
  draw(id);
169
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
+ },
170
182
  action(name, context = {}) {
171
183
  const ctx = Object.fromEntries(Object.entries(context).map(([k, v]) => [k, resolve(v)]));
172
- 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 });
173
185
  },
174
186
  });
187
+ node.hidden = !!c.when && get(shown.data, c.when.path) !== c.when.is; // shown only while its data matches
175
188
  return node;
176
189
  };
177
190
  try {
178
191
  const root = build("root");
179
192
  if (s.body.firstChild !== root) s.body.replaceChildren(root);
180
193
  } catch (e) {
181
- s.body.textContent = `Could not draw this surface: ${e}`;
194
+ s.body.textContent = `Could not draw this card: ${e}`;
182
195
  }
183
196
  drawBack(s, id, shown, resolve);
184
- 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);
185
211
  }
186
212
 
187
213
  function drawLabel(s, id) { // name · owner, pinned or not, ✕ (which asks first if others read from it)
188
- 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));
189
215
  s.label.classList.remove("asking");
190
216
  s.label.replaceChildren(
191
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" }),
192
218
  el("button", { text: "✕", title: "Close", onclick: () => {
193
- if (!users.length) return page.send({ type: "ui_close", surface: id });
219
+ if (!users.length) return page.send({ type: "card_close", card: id });
194
220
  s.label.classList.add("asking");
195
221
  s.label.replaceChildren(
196
222
  el("span", { class: "ask", text: `${users.join(", ")} ${users.length > 1 ? "read" : "reads"} from this. Copy it in, then close?` }),
197
- 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 }) }),
198
224
  el("button", { text: "Cancel", onclick: () => drawLabel(s, id) }));
199
225
  } }));
200
226
  }
201
227
 
202
228
  function drawOptionsBar(s, id) {
203
- s.box.querySelector(".surface-options")?.remove();
229
+ s.box.querySelector(".card-options")?.remove();
204
230
  if (!s.options) return;
205
231
  const i = s.option ?? 0, n = s.options.length;
206
232
  const flip = (d) => { s.option = (i + d + n) % n; draw(id); };
207
- const bar = el("div", { class: "surface-options" },
233
+ const bar = el("div", { class: "card-options" },
208
234
  el("button", { text: "‹", title: "Previous", "aria-label": "Previous option", onclick: () => flip(-1) }),
209
235
  el("span", { text: `${i + 1} of ${n}: ${s.options[i].label}` }),
210
236
  el("button", { text: "›", title: "Next", "aria-label": "Next option", onclick: () => flip(1) }),
211
- 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 }) }));
212
238
  bar.onwheel = (e) => { e.preventDefault(); flip(Math.sign(e.deltaY || e.deltaX)); };
213
239
  s.box.insertBefore(bar, s.body);
214
240
  }
215
241
 
216
242
  function drawBack(s, id, shown, resolve) { // the back side: about, made from, levers, pinned
217
- s.box.querySelector(".surface-back")?.remove();
243
+ s.box.querySelector(".card-back")?.remove();
218
244
  if (!s.flipped) return;
219
- 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` }),
220
246
  el("p", { class: s.about ? "" : "muted", text: s.about || "No description yet. Ask its agent to write one." }));
221
247
  if (s.made_from) back.append(el("p", { text: `Made from ${s.made_from.join(" and ")}` + (s.instruction ? `: “${s.instruction}”` : ".") }));
222
248
  const list = el("ul");
223
- for (const c of shown.components) {
224
- const spec = abouts[c.component];
249
+ for (const c of shown.blocks ?? []) {
250
+ const spec = abouts[c.block];
225
251
  for (const sentence of spec?.levers || []) {
226
252
  const li = el("li", {}, el("span", { class: "muted", text: `${c.id}: ` }));
227
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);
@@ -230,7 +256,7 @@ export default async function (page) {
230
256
  }
231
257
  if (list.children.length) back.append(list);
232
258
  back.append(el("p", {}, "It is ", el("button", { class: "lever", text: s.pinned ? "pinned" : "not pinned",
233
- onclick: () => page.send({ type: "ui_pin", surface: id, pinned: !s.pinned }) }),
259
+ onclick: () => page.send({ type: "card_pin", card: id, pinned: !s.pinned }) }),
234
260
  `: agents ${s.pinned ? "can't" : "may"} change it.`));
235
261
  back.append(el("button", { text: "Turn back", onclick: () => { s.flipped = false; draw(id); } }));
236
262
  s.box.append(back);
@@ -240,8 +266,8 @@ export default async function (page) {
240
266
  const raw = c[prop], value = resolve(raw) ?? spec.default ?? "";
241
267
  if (isRef(raw)) return el("span", { class: "lever ref", text: String(value), title: `read from ${raw.ref}` });
242
268
  const commit = (v) => {
243
- if (isBinding(raw)) { setPath(shown.data, raw.path, v); page.send({ type: "ui_set", surface: id, path: raw.path, value: v }); }
244
- 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 }); }
245
271
  draw(id);
246
272
  };
247
273
  if (spec.type === "choice" && spec.options?.length)
@@ -259,48 +285,48 @@ export default async function (page) {
259
285
  return el("span", { class: "lever ref", text: JSON.stringify(value) }); // lists, objects: shown, changed by the agent
260
286
  }
261
287
 
262
- page.on("ui", (ev) => {
288
+ page.on("card", (ev) => {
263
289
  if (ev.op === "render") {
264
- const old = surfaces[ev.surface] || {};
265
- const { op, type, surface, ...record } = ev;
266
- 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,
267
293
  flipped: old.flipped, option: record.options ? old.option : 0 };
268
- draw(ev.surface);
269
- } else if (ev.op === "delete" && surfaces[ev.surface]) {
270
- surfaces[ev.surface].box?.remove();
271
- delete surfaces[ev.surface];
272
- 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);
273
299
  } else if (ev.op === "reset") { // a whole saved state (undo, redo, rewind)
274
- for (const id of Object.keys(surfaces)) if (!(id in ev.surfaces)) { surfaces[id].box?.remove(); delete surfaces[id]; }
275
- for (const [id, record] of Object.entries(ev.surfaces)) {
276
- const old = surfaces[id] || {};
277
- 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 };
278
304
  }
279
- for (const id of Object.keys(surfaces)) draw(id);
305
+ for (const id of Object.keys(cards)) draw(id);
280
306
  }
281
307
  });
282
308
 
283
- function pinStacked() { // pin each stacked surface where it is now, so taking one out of the stack moves none
284
- 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);
285
311
  for (const [, s] of stacked) s.place = { x: s.box.offsetLeft, y: s.box.offsetTop }; // read all, then move
286
312
  for (const [id, s] of stacked) {
287
313
  s.box.toggleAttribute("data-placed", true);
288
314
  Object.assign(s.box.style, { position: "absolute", margin: "0", left: `${s.place.x}px`, top: `${s.place.y}px` });
289
- page.send({ type: "ui_move", surface: id, place: s.place });
315
+ page.send({ type: "card_move", card: id, place: s.place });
290
316
  }
291
317
  }
292
318
 
293
- function surfaceAt(x, y, except) { // the surface under a window point, other than `except`
294
- 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;
295
321
  }
296
322
 
297
- 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
298
324
  if (e.target.closest("button")) return;
299
325
  const handle = e.currentTarget, box = s.box;
300
- 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
301
327
  let place = null, held = false, x0 = 0, y0 = 0;
302
- let under = null, armed = null, wait = null, sx = 0, sy = 0; // the surface under the pointer; armed: lit, ready to combine
303
- 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);
304
330
  handle.onpointermove = (ev) => {
305
331
  if (held || Math.hypot(ev.clientX - e.clientX, ev.clientY - e.clientY) < 5) return;
306
332
  clearTimeout(hold);
@@ -309,7 +335,7 @@ export default async function (page) {
309
335
  place = { ...s.place, x: Math.round(x0 + ev.clientX - e.clientX), y: Math.round(y0 + ev.clientY - e.clientY) };
310
336
  box.toggleAttribute("data-placed", true);
311
337
  Object.assign(box.style, { position: "absolute", margin: "0", left: `${place.x}px`, top: `${place.y}px` });
312
- const now = surfaceAt(ev.clientX, ev.clientY, box);
338
+ const now = cardAt(ev.clientX, ev.clientY, box);
313
339
  if (now !== under || Math.hypot(ev.clientX - sx, ev.clientY - sy) > 6) { // moved: start waiting again
314
340
  clearTimeout(wait);
315
341
  armed?.removeAttribute("data-drop");
@@ -324,19 +350,19 @@ export default async function (page) {
324
350
  armed?.removeAttribute("data-drop");
325
351
  if (!place) return; // a click or a hold, not a drag
326
352
  box.style.zIndex = "";
327
- 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
328
354
  Object.assign(box.style, { left: `${s.place.x}px`, top: `${s.place.y}px` });
329
- const b = armed.dataset.surface;
330
- if (surfaces[b]?.pinned) return page.note(`${b} is pinned, so it can't take ${id} in. Hold its label to unpin it first.`);
331
- 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 } }));
332
358
  return;
333
359
  }
334
360
  s.place = place;
335
- page.send({ type: "ui_move", surface: id, place });
361
+ page.send({ type: "card_move", card: id, place });
336
362
  };
337
363
  }
338
364
 
339
- 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
340
366
  if (s.pinned) return;
341
367
  pinStacked();
342
368
  const handle = e.currentTarget, box = s.box;
@@ -354,11 +380,11 @@ export default async function (page) {
354
380
  handle.onpointermove = handle.onpointerup = null;
355
381
  if (!place) return;
356
382
  s.place = place;
357
- page.send({ type: "ui_move", surface: id, place });
383
+ page.send({ type: "card_move", card: id, place });
358
384
  };
359
385
  }
360
386
 
361
387
  await loadAbouts();
362
- for (const [id, s] of Object.entries(await (await fetch("/ui-api/surfaces")).json())) surfaces[id] = { ...s, option: 0 };
363
- 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);
364
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.