@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.
- package/README.md +10 -6
- package/{app → agent}/SYSTEM.md +16 -11
- package/agent/extensions/app/README.md +68 -0
- package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
- package/agent/extensions/app/guide.md +5 -0
- package/agent/extensions/app/index.ts +66 -0
- package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
- package/agent/extensions/board/browser.js +359 -0
- package/agent/extensions/board/guide.md +4 -0
- package/agent/extensions/board/index.ts +100 -0
- package/agent/extensions/card/README.md +88 -0
- package/agent/extensions/card/blocks/Button.js +34 -0
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
- package/agent/extensions/card/blocks/Markdown.js +37 -0
- package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
- package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -128
- package/agent/extensions/card/guide.md +13 -0
- package/agent/extensions/card/index.ts +370 -0
- package/agent/extensions/command/browser.js +13 -0
- package/agent/extensions/command/guide.md +2 -0
- package/agent/extensions/command/index.ts +94 -0
- package/agent/extensions/doodle/README.md +57 -0
- package/agent/extensions/doodle/browser.js +1013 -0
- package/agent/extensions/doodle/guide.md +22 -0
- package/agent/extensions/doodle/index.ts +274 -0
- package/agent/extensions/file/browser.js +143 -0
- package/agent/extensions/file/guide.md +5 -0
- package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
- package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
- package/{app/extensions/frames → agent/extensions/frame}/browser.js +24 -19
- package/agent/extensions/frame/guide.md +3 -0
- package/agent/extensions/frame/index.ts +81 -0
- package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
- package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -33
- package/{app → agent}/extensions/mcp/browser.js +1 -1
- package/agent/extensions/mcp/guide.md +3 -0
- package/{app → agent}/extensions/mcp/index.ts +2 -4
- package/agent/extensions/model/browser.js +363 -0
- package/{app → agent}/extensions/model/index.ts +8 -4
- package/agent/extensions/model/login.ts +215 -0
- package/agent/extensions/note/README.md +35 -0
- package/agent/extensions/note/browser.js +356 -0
- package/agent/extensions/note/guide.md +8 -0
- package/agent/extensions/note/index.ts +192 -0
- package/{app → agent}/extensions/page/index.ts +2 -2
- package/{app → agent}/extensions/page/page.html +55 -49
- package/agent/extensions/prompt/index.ts +38 -0
- package/{app → agent}/extensions/reload/index.ts +3 -3
- package/{app → agent}/extensions/screen/browser.js +4 -4
- package/{app → agent}/extensions/screen/index.ts +11 -11
- package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
- package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
- package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
- package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
- package/{app/extensions/versions → agent/extensions/version}/browser.js +10 -10
- package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
- package/{app → agent}/extensions/viewer/README.md +4 -4
- package/{app → agent}/extensions/viewer/browser.js +8 -8
- package/agent/extensions/viewer/guide.md +3 -0
- package/{app → agent}/extensions/viewer/index.ts +22 -17
- package/agent/extensions/voice/browser.js +72 -0
- package/agent/extensions/voice/index.ts +39 -0
- package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
- package/agent/settings.json +8 -0
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
- package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +14 -14
- package/agent/skills/extension-map/SKILL.md +56 -0
- package/{app → agent}/skills/write-skill/SKILL.md +1 -1
- package/bin/chalky.mjs +3 -3
- package/package.json +8 -8
- package/server.ts +162 -63
- package/app/extensions/appdata/README.md +0 -10
- package/app/extensions/appdata/guide.md +0 -4
- package/app/extensions/appdata/index.ts +0 -47
- package/app/extensions/doodle/README.md +0 -15
- package/app/extensions/doodle/browser.js +0 -388
- package/app/extensions/doodle/guide.md +0 -6
- package/app/extensions/doodle/index.ts +0 -54
- package/app/extensions/files/browser.js +0 -96
- package/app/extensions/files/guide.md +0 -7
- package/app/extensions/frames/guide.md +0 -7
- package/app/extensions/frames/index.ts +0 -76
- package/app/extensions/mcp/guide.md +0 -5
- package/app/extensions/point/README.md +0 -22
- package/app/extensions/point/browser.js +0 -245
- package/app/extensions/point/guide.md +0 -6
- package/app/extensions/point/index.ts +0 -97
- package/app/extensions/prompt/index.ts +0 -19
- package/app/extensions/ui/README.md +0 -69
- package/app/extensions/ui/components/App.js +0 -28
- package/app/extensions/ui/components/Button.js +0 -28
- package/app/extensions/ui/components/Markdown.js +0 -37
- package/app/extensions/ui/guide.md +0 -7
- package/app/extensions/ui/index.ts +0 -271
- package/app/extensions/viewer/guide.md +0 -5
- package/app/mcp.json +0 -3
- package/app/skills/plugin-map/SKILL.md +0 -49
- package/app/ui/apps/.keep +0 -0
- /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
- /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
|
-
//
|
|
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 = "" },
|
|
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
|
-
|
|
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" },
|
|
13
|
+
update({ gap = 8, align = "center" }, card) {
|
|
14
14
|
this.style.cssText = `display:flex;flex-wrap:wrap;gap:${gap}px;align-items:${align}`;
|
|
15
|
-
|
|
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 },
|
|
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.
|
|
22
|
+
this.input.oninput = () => this.card.set("value", this.input.value);
|
|
23
23
|
this.append(this.input);
|
|
24
24
|
}
|
|
25
|
-
this.
|
|
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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// A
|
|
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
|
|
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: "
|
|
8
|
-
// drag and hold still over another
|
|
9
|
-
// fires a "
|
|
10
|
-
// {a, b, x, y} (
|
|
11
|
-
// Onto a pinned
|
|
12
|
-
// hold pin or unpin it ({type: "
|
|
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
|
|
15
|
-
// ✕ close it; if other
|
|
16
|
-
// The corner resizes it (unless resizable: false). Moving or resizing one first pins every stacked
|
|
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: "
|
|
19
|
-
// no agent is woken
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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", "
|
|
28
|
-
const refsIn = (s) => new Set(s.
|
|
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
|
|
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-
|
|
34
|
-
[data-
|
|
35
|
-
[data-
|
|
36
|
-
[data-
|
|
37
|
-
[data-
|
|
38
|
-
[data-
|
|
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-
|
|
42
|
-
[data-
|
|
43
|
-
[data-
|
|
44
|
-
[data-
|
|
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-
|
|
48
|
-
[data-
|
|
49
|
-
[data-
|
|
50
|
-
[data-
|
|
51
|
-
[data-
|
|
52
|
-
[data-
|
|
53
|
-
[data-
|
|
54
|
-
[data-
|
|
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-
|
|
58
|
-
[data-
|
|
59
|
-
[data-
|
|
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-
|
|
62
|
-
[data-
|
|
63
|
-
[data-
|
|
64
|
-
[data-
|
|
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-
|
|
67
|
-
[data-
|
|
68
|
-
[data-
|
|
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-
|
|
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
|
|
87
|
-
document.head.insertAdjacentHTML("beforeend", `<style id="
|
|
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
|
|
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 = {}; //
|
|
93
|
+
const modules = {}; // block name -> its module (for upgrade())
|
|
92
94
|
const loadAbouts = async () => {
|
|
93
|
-
try { [abouts, mtimes] = await Promise.all(["/
|
|
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
|
|
96
|
-
const pending = {}; // "
|
|
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: "
|
|
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(`/
|
|
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 =
|
|
113
|
-
if (!s || chain.has(id)) return; // chain:
|
|
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
|
|
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.
|
|
118
|
-
s.label = el("div", { class: "
|
|
119
|
-
s.body = el("div", { class: "
|
|
120
|
-
s.box.append(s.label, s.body, el("div", { class: "
|
|
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
|
|
128
|
-
s.box.
|
|
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
|
|
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.
|
|
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
|
|
147
|
+
s.body.textContent = `Could not load a block: ${e}`;
|
|
143
148
|
return;
|
|
144
149
|
}
|
|
145
|
-
const byId = Object.fromEntries(shown.
|
|
146
|
-
const resolve = (v) => (isBinding(v) ? get(shown.data, v.path) : isRef(v) ? get(
|
|
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.
|
|
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 = `
|
|
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.
|
|
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 (
|
|
164
|
-
if (!isBinding(c[prop])) return; // a plain value or another
|
|
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: "
|
|
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
|
|
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(
|
|
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(
|
|
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: "
|
|
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: "
|
|
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(".
|
|
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: "
|
|
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: "
|
|
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(".
|
|
243
|
+
s.box.querySelector(".card-back")?.remove();
|
|
217
244
|
if (!s.flipped) return;
|
|
218
|
-
const back = el("div", { class: "
|
|
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.
|
|
223
|
-
const spec = abouts[c.
|
|
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: "
|
|
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: "
|
|
243
|
-
else { c[prop] = v; page.send({ type: "
|
|
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("
|
|
288
|
+
page.on("card", (ev) => {
|
|
262
289
|
if (ev.op === "render") {
|
|
263
|
-
const old =
|
|
264
|
-
const { op, type,
|
|
265
|
-
|
|
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.
|
|
268
|
-
} else if (ev.op === "delete" &&
|
|
269
|
-
|
|
270
|
-
delete
|
|
271
|
-
for (const id of Object.keys(
|
|
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(
|
|
274
|
-
for (const [id, record] of Object.entries(ev.
|
|
275
|
-
const old =
|
|
276
|
-
|
|
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(
|
|
305
|
+
for (const id of Object.keys(cards)) draw(id);
|
|
279
306
|
}
|
|
280
307
|
});
|
|
281
308
|
|
|
282
|
-
function pinStacked() { // pin each stacked
|
|
283
|
-
const stacked = Object.entries(
|
|
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: "
|
|
315
|
+
page.send({ type: "card_move", card: id, place: s.place });
|
|
289
316
|
}
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
function
|
|
293
|
-
return document.elementsFromPoint(x, y).map((e) => e.closest?.("[data-
|
|
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
|
|
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
|
|
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
|
|
302
|
-
const hold = setTimeout(() => { held = true; page.send({ type: "
|
|
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 =
|
|
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 (
|
|
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.
|
|
329
|
-
if (
|
|
330
|
-
document.dispatchEvent(new CustomEvent("
|
|
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: "
|
|
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
|
|
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: "
|
|
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("/
|
|
362
|
-
for (const id of Object.keys(
|
|
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.
|