@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,359 @@
|
|
|
1
|
+
// The page side of the board extension: the main space as numbered boards, and the shelf to move between them (above the input).
|
|
2
|
+
// Each board is a container in #top. The one you're on is display: contents (its things sit in the main space as
|
|
3
|
+
// before); the others are laid out but hidden, so apps in them keep running and the shelf can draw them.
|
|
4
|
+
// Any element with data-board="n" that lands in #top (a card, a frame) is moved into board n's container, and
|
|
5
|
+
// moved again when its data-board changes (a MutationObserver): no extension needs this one. One on the body (a
|
|
6
|
+
// note's pin, fixed to the window) shows only while its board is shown.
|
|
7
|
+
// Other scripts read the board shown from body[data-board] (new things go there), and get a DOM event "board"
|
|
8
|
+
// {from, to} just before it changes (doodle: the drawing so far becomes a step; frames: count this board's).
|
|
9
|
+
// The shelf: a dot per board (yours is bigger). Hover it and the dots open into thumbnails, with + for a new board;
|
|
10
|
+
// click one to go there. It stays clear of the chat panel: more thumbnails than fit scroll between ‹ and › (grey at
|
|
11
|
+
// the ends; a new board scrolls to the end). Drag a card by its label onto a thumbnail to move it to that board, where it was ({type: "card_board"}).
|
|
12
|
+
// page messages board_view {board}: the board this page shows; board_add
|
|
13
|
+
// GET /board {boards, current} event "board" {boards, added?, by?}
|
|
14
|
+
const CSS = `
|
|
15
|
+
#top > .board:not(.on) { position: absolute; top: 0; left: 0; right: 0; height: 0; overflow: hidden; padding: inherit;
|
|
16
|
+
box-sizing: border-box; visibility: hidden; pointer-events: none; }
|
|
17
|
+
#top > .board.on { display: contents; }
|
|
18
|
+
#top > .board.on:empty::before { content: "What should we build?"; position: absolute; inset: 0; display: grid; place-items: center;
|
|
19
|
+
font-size: 30px; font-weight: 300; letter-spacing: -0.02em; color: rgba(0, 0, 0, 0.2); pointer-events: none; }
|
|
20
|
+
.boards-shelf { position: fixed; left: 50%; bottom: 72px; transform: translateX(-50%); z-index: 25; display: flex; align-items: flex-end;
|
|
21
|
+
gap: 8px; padding: 5px 8px; border-radius: 14px; border: 1px solid transparent; transition: background 0.15s; }
|
|
22
|
+
.boards-shelf.open { background: var(--glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
23
|
+
border-color: var(--edge); box-shadow: var(--shadow); }
|
|
24
|
+
.boards-shelf .slot { position: relative; display: grid; justify-items: center; gap: 4px; padding: 0; }
|
|
25
|
+
.boards-shelf .slot .x { display: none; position: absolute; top: 3px; right: 3px; width: 16px; height: 16px; border-radius: 50%;
|
|
26
|
+
font-size: 9px; line-height: 16px; text-align: center; color: var(--danger); background: var(--glass-strong); box-shadow: var(--shadow); }
|
|
27
|
+
.boards-shelf.open .slot:hover .x { display: block; }
|
|
28
|
+
.boards-toast { position: fixed; left: 50%; top: 76px; transform: translateX(-50%); /* under the palette: clear of the shelf */ z-index: 27; display: flex; gap: 12px;
|
|
29
|
+
align-items: center; padding: 7px 14px; border-radius: 10px; background: rgba(29, 29, 31, 0.88); color: #fff; font-size: 13px; }
|
|
30
|
+
.boards-toast button { color: #9cc3f5; font-weight: 600; }
|
|
31
|
+
.boards-shelf .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(0, 0, 0, 0.22); transition: all 0.15s; }
|
|
32
|
+
.boards-shelf .slot.on .dot { width: 11px; height: 11px; background: var(--text); }
|
|
33
|
+
/* more boards than fit (fit(): clear of the chat panel): the thumbnails scroll inside .view, between ‹ and › */
|
|
34
|
+
.boards-shelf .view { overflow: hidden; scroll-behavior: smooth; padding: 3px; }
|
|
35
|
+
.boards-shelf .track { position: relative; display: flex; align-items: flex-end; gap: 8px; width: max-content; }
|
|
36
|
+
.boards-shelf .arrow { display: none; width: 24px; height: 70px; margin-top: 3px; align-self: flex-start; border-radius: 6px;
|
|
37
|
+
font-size: 20px; color: var(--text); }
|
|
38
|
+
.boards-shelf.open.more .arrow { display: block; }
|
|
39
|
+
.boards-shelf .arrow:disabled { opacity: 0.25; cursor: default; }
|
|
40
|
+
.boards-shelf .arrow:not(:disabled):hover { background: var(--hover); }
|
|
41
|
+
.boards-shelf .mini { display: none; position: relative; width: 120px; overflow: hidden; border-radius: 6px;
|
|
42
|
+
background: rgba(255, 255, 255, 0.6); border: 1px solid var(--line); }
|
|
43
|
+
.boards-shelf .mini i, .boards-peek .mini i { position: absolute; overflow: hidden; border-radius: 3px; background: rgba(0, 0, 0, 0.08); }
|
|
44
|
+
/* a card: a tile in its own main colour (light grey when it has none) */
|
|
45
|
+
.boards-shelf .mini i.s, .boards-peek .mini i.s { --accent-of: rgb(200, 203, 210);
|
|
46
|
+
background: color-mix(in srgb, var(--accent-of) 30%, #fff);
|
|
47
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-of) 70%, transparent), 0 1px 3px rgba(0, 0, 0, 0.08); }
|
|
48
|
+
.boards-shelf .mini i.s::before, .boards-peek .mini i.s::before { content: ""; position: absolute; left: 0; right: 0; top: 0;
|
|
49
|
+
height: max(2px, 12%); background: var(--accent-of); }
|
|
50
|
+
.boards-peek .mini i.s.detail { background: #fff; } /* the sketch of its insides shows its colours */
|
|
51
|
+
.boards-peek .mini i.s.detail::before { display: none; }
|
|
52
|
+
.boards-peek .mini i u, .boards-peek .mini i img { display: block; }
|
|
53
|
+
.boards-peek { position: fixed; z-index: 26; transform: translateX(-50%); padding: 6px; border-radius: 12px; pointer-events: none;
|
|
54
|
+
background: var(--glass-strong); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
55
|
+
border: 1px solid var(--edge); box-shadow: var(--shadow); }
|
|
56
|
+
.boards-peek .n { font-size: 11px; color: var(--muted); margin: 0 2px 4px; }
|
|
57
|
+
.boards-peek .mini { position: relative; width: 320px; overflow: hidden; border-radius: 8px; background: rgba(255, 255, 255, 0.6);
|
|
58
|
+
border: 1px solid var(--line); }
|
|
59
|
+
.boards-peek .empty { position: absolute; inset: 0; display: grid; place-items: center; font-size: 12px; color: var(--muted); }
|
|
60
|
+
.boards-shelf .n { display: none; font-size: 10.5px; color: var(--muted); }
|
|
61
|
+
.boards-shelf.open .mini, .boards-shelf.open .n { display: block; }
|
|
62
|
+
.boards-shelf.open .dot { display: none; }
|
|
63
|
+
.boards-shelf .slot.on .mini { outline: 2px solid var(--text); outline-offset: 1px; }
|
|
64
|
+
.boards-shelf .slot.drop .mini { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
65
|
+
.boards-shelf .add { display: none; width: 122px; height: 70px; border-radius: 6px; /* a thumbnail's size, border included */ border: 1px dashed rgba(0, 0, 0, 0.25);
|
|
66
|
+
color: var(--muted); font-size: 13px; align-self: flex-start; margin-top: 3px; }
|
|
67
|
+
.boards-shelf.open .add { display: block; } /* only in the open shelf: at rest it's just the dots */`;
|
|
68
|
+
|
|
69
|
+
export default async function (page) {
|
|
70
|
+
document.head.insertAdjacentHTML("beforeend", `<style>${CSS}</style>`);
|
|
71
|
+
const top = page.slot("top");
|
|
72
|
+
const containers = {};
|
|
73
|
+
let boards = [1], current = 1, drag = null, over = null, adding = false; // adding: this page clicked +
|
|
74
|
+
|
|
75
|
+
const container = (n) => {
|
|
76
|
+
if (!containers[n]) {
|
|
77
|
+
containers[n] = Object.assign(document.createElement("div"), { className: "board" });
|
|
78
|
+
containers[n].dataset.n = n;
|
|
79
|
+
top.append(containers[n]);
|
|
80
|
+
}
|
|
81
|
+
return containers[n];
|
|
82
|
+
};
|
|
83
|
+
const home = (el) => { // an element with data-board: into its board's container
|
|
84
|
+
const c = container(Number(el.dataset.board) || 1);
|
|
85
|
+
if (el.parentElement !== c) c.append(el);
|
|
86
|
+
};
|
|
87
|
+
new MutationObserver((records) => {
|
|
88
|
+
for (const r of records) {
|
|
89
|
+
if (r.type === "attributes") home(r.target);
|
|
90
|
+
else for (const el of r.addedNodes) if (el.parentElement === top && el.dataset?.board) home(el);
|
|
91
|
+
}
|
|
92
|
+
}).observe(top, { childList: true, subtree: true, attributeFilter: ["data-board"] });
|
|
93
|
+
|
|
94
|
+
// the shelf: ‹ [the thumbnails, scrolling] › + (+ outside the scrolling part, so it is always there)
|
|
95
|
+
const shelf = Object.assign(document.createElement("div"), { className: "boards-shelf" });
|
|
96
|
+
shelf.dataset.name = "boards";
|
|
97
|
+
const button = (className, textContent, title, onclick) => Object.assign(document.createElement("button"), { className, textContent, title, onclick });
|
|
98
|
+
const view = Object.assign(document.createElement("div"), { className: "view" });
|
|
99
|
+
const track = Object.assign(document.createElement("div"), { className: "track" });
|
|
100
|
+
const step = (d) => view.scrollBy({ left: d * Math.max(130, view.clientWidth - 130) }); // about a view's width
|
|
101
|
+
const prev = button("arrow", "‹", "Earlier boards", () => step(-1));
|
|
102
|
+
const next = button("arrow", "›", "Later boards", () => step(1));
|
|
103
|
+
const add = button("add", "+", "New board", () => { adding = true; page.send({ type: "board_add" }); });
|
|
104
|
+
add.dataset.name = "new-board";
|
|
105
|
+
view.append(track);
|
|
106
|
+
shelf.append(prev, view, next, add);
|
|
107
|
+
document.body.append(shelf);
|
|
108
|
+
shelf.onmouseenter = () => open(true);
|
|
109
|
+
shelf.onmouseleave = () => { if (!drag) open(false); };
|
|
110
|
+
view.onscroll = arrows;
|
|
111
|
+
// the wheel's left/right (or up/down): each click moves the strip as an arrow does; the page doesn't scroll. At
|
|
112
|
+
// most one step per 250 ms, as a trackpad swipe sends many small wheel events
|
|
113
|
+
let lastWheel = 0;
|
|
114
|
+
shelf.addEventListener("wheel", (e) => {
|
|
115
|
+
const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
116
|
+
if (!shelf.classList.contains("more") || !d) return;
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
if (Date.now() - lastWheel < 250) return;
|
|
119
|
+
lastWheel = Date.now();
|
|
120
|
+
step(Math.sign(d));
|
|
121
|
+
}, { passive: false });
|
|
122
|
+
addEventListener("resize", fit);
|
|
123
|
+
new MutationObserver(fit).observe(document.body, { attributeFilter: ["data-panel"] }); // the chat panel opened or closed
|
|
124
|
+
|
|
125
|
+
function fit() { // as wide as fits between the chat panel (or the rail) and the same gap on the right, centred
|
|
126
|
+
const side = ["messages", "versions"].includes(document.body.dataset.panel) ? "panel" : "rail";
|
|
127
|
+
const left = document.getElementById(side)?.getBoundingClientRect().right ?? 0;
|
|
128
|
+
view.style.maxWidth = `${Math.max(130, innerWidth - 2 * (left + 16) - 2 * 32 - 140)}px`; // the arrows and + beside it
|
|
129
|
+
shelf.classList.toggle("more", track.scrollWidth > view.clientWidth + 1);
|
|
130
|
+
arrows();
|
|
131
|
+
}
|
|
132
|
+
function arrows() { // grey at the ends
|
|
133
|
+
prev.disabled = view.scrollLeft <= 0;
|
|
134
|
+
next.disabled = view.scrollLeft + view.clientWidth >= view.scrollWidth - 1;
|
|
135
|
+
}
|
|
136
|
+
function reveal() { // the board shown, in view (a new one: the strip goes to the end)
|
|
137
|
+
const s = track.querySelector(".slot.on");
|
|
138
|
+
if (s && (s.offsetLeft < view.scrollLeft || s.offsetLeft + s.offsetWidth > view.scrollLeft + view.clientWidth))
|
|
139
|
+
view.scrollLeft = s.offsetLeft - (view.clientWidth - s.offsetWidth) / 2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const others = document.createElement("style"); // hides what is on the body for other boards (a note's pin)
|
|
143
|
+
document.head.append(others);
|
|
144
|
+
function show(n, tell = true) { // go to board n (tell: the server, which the first show got it from)
|
|
145
|
+
// before the switch, so a doodle in progress becomes a step with a picture of this board
|
|
146
|
+
if (n !== current) document.dispatchEvent(new CustomEvent("board", { detail: { from: current, to: n } }));
|
|
147
|
+
current = n;
|
|
148
|
+
document.body.dataset.board = n;
|
|
149
|
+
others.textContent = `body > [data-board]:not([data-board="${n}"]) { display: none !important; }`;
|
|
150
|
+
for (const b of boards) container(b).classList.toggle("on", b === n);
|
|
151
|
+
if (tell) page.send({ type: "board_view", board: n });
|
|
152
|
+
render();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function render() {
|
|
156
|
+
const shown = boards.filter((n) => n !== pending?.n);
|
|
157
|
+
track.replaceChildren(...shown.map((n) => {
|
|
158
|
+
const slot = document.createElement("button");
|
|
159
|
+
slot.className = "slot" + (n === current ? " on" : "");
|
|
160
|
+
slot.dataset.n = n;
|
|
161
|
+
slot.title = `Board ${n}`;
|
|
162
|
+
slot.innerHTML = `<span class="mini"></span><span class="dot"></span><span class="n">${n}</span>` +
|
|
163
|
+
(shown.length > 1 ? `<span class="x" role="button" title="Remove board ${n} and everything on it">✕</span>` : ""); // the last board stays
|
|
164
|
+
slot.onclick = (e) => (e.target.closest(".x") ? removeBoard(n) : show(n));
|
|
165
|
+
slot.onmouseenter = () => showPeek(slot);
|
|
166
|
+
slot.onmouseleave = () => { peek.hidden = true; }; // off it (onto +, a gap): no preview
|
|
167
|
+
return slot;
|
|
168
|
+
}));
|
|
169
|
+
if (shelf.classList.contains("open")) { thumbnails(); fit(); reveal(); }
|
|
170
|
+
else fit();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ✕ on a thumbnail: the board leaves the shelf with a few seconds of Undo, and only then goes from the server
|
|
174
|
+
// (board_remove: its things go with it). Leaving the page in those seconds removes it at once.
|
|
175
|
+
let pending = null; // {n, was: the board shown when ✕ was clicked, timer}
|
|
176
|
+
const toast = Object.assign(document.createElement("div"), { className: "boards-toast", hidden: true });
|
|
177
|
+
document.body.append(toast);
|
|
178
|
+
function removeBoard(n) {
|
|
179
|
+
if (pending) commit();
|
|
180
|
+
const i = boards.indexOf(n);
|
|
181
|
+
pending = { n, was: current, timer: setTimeout(commit, 5000) };
|
|
182
|
+
peek.hidden = true;
|
|
183
|
+
if (current === n) show(boards.filter((b) => b !== n)[Math.max(0, i - 1)]); // the one before it (else after)
|
|
184
|
+
else render();
|
|
185
|
+
toast.replaceChildren(`Board ${n} removed`, button("", "Undo", "Bring it back", undo));
|
|
186
|
+
toast.hidden = false;
|
|
187
|
+
}
|
|
188
|
+
function commit() {
|
|
189
|
+
clearTimeout(pending.timer);
|
|
190
|
+
page.send({ type: "board_remove", board: pending.n });
|
|
191
|
+
pending = null;
|
|
192
|
+
toast.hidden = true;
|
|
193
|
+
}
|
|
194
|
+
function undo() {
|
|
195
|
+
clearTimeout(pending.timer);
|
|
196
|
+
const { n, was } = pending;
|
|
197
|
+
pending = null;
|
|
198
|
+
toast.hidden = true;
|
|
199
|
+
if (was === n) show(n);
|
|
200
|
+
else render();
|
|
201
|
+
}
|
|
202
|
+
addEventListener("pagehide", () => { if (pending) commit(); });
|
|
203
|
+
|
|
204
|
+
function thumbnails() {
|
|
205
|
+
for (const slot of shelf.querySelectorAll(".slot")) drawBoard(slot.querySelector(".mini"), Number(slot.dataset.n), 120, 68);
|
|
206
|
+
}
|
|
207
|
+
// board n's things where they are in the main space, scaled down into mini (w x h): a card is a small tile in
|
|
208
|
+
// its main colour (accentOf), no words. Zoomed to fit what's on the board (not the whole window), so the tiles fill it.
|
|
209
|
+
function drawBoard(mini, n, W, H) {
|
|
210
|
+
const PAD = 12, els = [...container(n).children];
|
|
211
|
+
const rects = els.map((el) => el.getBoundingClientRect());
|
|
212
|
+
const x0 = Math.min(...rects.map((r) => r.left)) - PAD, y0 = Math.min(...rects.map((r) => r.top)) - PAD;
|
|
213
|
+
const k = Math.min(W / (Math.max(...rects.map((r) => r.right)) + PAD - x0), H / (Math.max(...rects.map((r) => r.bottom)) + PAD - y0), 0.5);
|
|
214
|
+
mini.style.height = `${H}px`;
|
|
215
|
+
mini.replaceChildren(...els.map((el, n) => {
|
|
216
|
+
const r = rects[n], i = document.createElement("i");
|
|
217
|
+
if (el.dataset.card) {
|
|
218
|
+
const accent = accentOf(el.querySelector(".card-body") ?? el);
|
|
219
|
+
i.className = "s";
|
|
220
|
+
i.title = el.dataset.card;
|
|
221
|
+
if (accent) i.style.setProperty("--accent-of", accent);
|
|
222
|
+
if (W > 200) { i.classList.add("detail"); sketch(el, i, k); } // the big preview: what's inside it
|
|
223
|
+
}
|
|
224
|
+
Object.assign(i.style, { left: `${(r.left - x0) * k}px`, top: `${(r.top - y0) * k}px`,
|
|
225
|
+
width: `${Math.max(2, r.width * k)}px`, height: `${Math.max(2, r.height * k)}px` });
|
|
226
|
+
return i;
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
// hover one thumbnail: a bigger picture of that board above the shelf, to read it
|
|
230
|
+
const peek = Object.assign(document.createElement("div"), { className: "boards-peek", hidden: true });
|
|
231
|
+
document.body.append(peek);
|
|
232
|
+
function showPeek(slot) {
|
|
233
|
+
if (!slot || drag) return (peek.hidden = true);
|
|
234
|
+
const n = Number(slot.dataset.n), mini = Object.assign(document.createElement("div"), { className: "mini" });
|
|
235
|
+
peek.replaceChildren(Object.assign(document.createElement("div"), { className: "n", textContent: `Board ${n}` }), mini);
|
|
236
|
+
peek.hidden = false;
|
|
237
|
+
drawBoard(mini, n, 320, 180);
|
|
238
|
+
if (!mini.children.length) mini.append(Object.assign(document.createElement("span"), { className: "empty", textContent: "Empty" }));
|
|
239
|
+
const s = slot.getBoundingClientRect(), half = peek.offsetWidth / 2;
|
|
240
|
+
peek.style.left = `${Math.min(innerWidth - half - 8, Math.max(half + 8, s.left + s.width / 2))}px`;
|
|
241
|
+
peek.style.bottom = `${innerHeight - shelf.getBoundingClientRect().top + 8}px`;
|
|
242
|
+
}
|
|
243
|
+
// a card's insides, scaled by k into its tile (the big preview): each line of text a bar in its colour, coloured
|
|
244
|
+
// areas (buttons, filled boxes) in theirs, pictures and canvases (a game) as small images. Apps' pages too (same
|
|
245
|
+
// server). No words. At most 400 pieces a card.
|
|
246
|
+
function sketch(card, tile, k) {
|
|
247
|
+
const base = card.getBoundingClientRect();
|
|
248
|
+
const put = (tag, r, dx, dy, style) => {
|
|
249
|
+
if (r.width * k < 0.5 || r.height * k < 0.5) return null;
|
|
250
|
+
const e = tile.appendChild(document.createElement(tag));
|
|
251
|
+
Object.assign(e.style, { position: "absolute", left: `${(r.left + dx - base.left) * k}px`, top: `${(r.top + dy - base.top) * k}px`,
|
|
252
|
+
width: `${r.width * k}px`, height: `${r.height * k}px` }, style);
|
|
253
|
+
return e;
|
|
254
|
+
};
|
|
255
|
+
const roots = [[card.querySelector(".card-body") ?? card, 0, 0]];
|
|
256
|
+
for (const f of card.querySelectorAll("iframe")) {
|
|
257
|
+
try { const r = f.getBoundingClientRect(); if (f.contentDocument?.body) roots.push([f.contentDocument.body, r.left + f.clientLeft, r.top + f.clientTop]); } catch {}
|
|
258
|
+
}
|
|
259
|
+
let n = 0;
|
|
260
|
+
for (const [root, dx, dy] of roots) {
|
|
261
|
+
const win = root.ownerDocument.defaultView, walk = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
262
|
+
for (let node = walk.currentNode; node && n < 400; node = walk.nextNode()) {
|
|
263
|
+
if (node.nodeType === 3) { // text: a bar per line, in its colour
|
|
264
|
+
if (!node.data.trim() || node.parentElement.closest("style, script")) continue;
|
|
265
|
+
const range = root.ownerDocument.createRange();
|
|
266
|
+
range.selectNodeContents(node);
|
|
267
|
+
const color = win.getComputedStyle(node.parentElement).color;
|
|
268
|
+
for (const r of range.getClientRects())
|
|
269
|
+
if (put("u", { left: r.left, top: r.top + r.height * 0.3, width: r.width, height: r.height * 0.4 }, dx, dy,
|
|
270
|
+
{ background: color, opacity: "0.45", borderRadius: "1px" })) n++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const cs = win.getComputedStyle(node), r = node.getBoundingClientRect();
|
|
274
|
+
if (node.localName === "img" && node.currentSrc) { const e = put("img", r, dx, dy, { objectFit: cs.objectFit }); if (e) { e.src = node.currentSrc; n++; } }
|
|
275
|
+
else if (node.localName === "canvas") {
|
|
276
|
+
let src = null;
|
|
277
|
+
try { src = node.toDataURL("image/png"); } catch {} // a canvas painted from another site can't be read
|
|
278
|
+
const e = put(src ? "img" : "u", r, dx, dy, src ? {} : { background: "rgba(0, 0, 0, 0.12)" });
|
|
279
|
+
if (e && src) e.src = src;
|
|
280
|
+
if (e) n++;
|
|
281
|
+
} else if ((cs.backgroundColor.match(/[\d.]+/g)?.[3] ?? 1) > 0.05 && !/^rgba?\(255, 255, 255/.test(cs.backgroundColor) && cs.backgroundColor !== "transparent") {
|
|
282
|
+
if (put("u", r, dx, dy, { background: cs.backgroundColor, borderRadius: `${parseFloat(cs.borderTopLeftRadius) * k}px` })) n++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// the colour a thing shows most: each coloured background counts by its area, a border or text a little; whites,
|
|
288
|
+
// greys and blacks don't count. Apps are pages from this server, so their frame's insides count too. None: null
|
|
289
|
+
function accentOf(node) {
|
|
290
|
+
const roots = [node, ...[...node.querySelectorAll("iframe")].map((f) => { try { return f.contentDocument?.body; } catch { return null; } })];
|
|
291
|
+
const score = {};
|
|
292
|
+
const add = (color, weight) => {
|
|
293
|
+
const m = color.match(/[\d.]+/g)?.map(Number);
|
|
294
|
+
if (!m || (m[3] ?? 1) < 0.3) return;
|
|
295
|
+
const [r, g, b] = m, max = Math.max(r, g, b), min = Math.min(r, g, b), l = (max + min) / 510;
|
|
296
|
+
const sat = max === min ? 0 : (max - min) / 255 / (1 - Math.abs(2 * l - 1));
|
|
297
|
+
if (sat < 0.25 || l < 0.12 || l > 0.92) return; // white, grey, black
|
|
298
|
+
const key = `rgb(${r & 0xf0},${g & 0xf0},${b & 0xf0})`; // near colours count together
|
|
299
|
+
score[key] = { color: `rgb(${r},${g},${b})`, n: (score[key]?.n ?? 0) + weight };
|
|
300
|
+
};
|
|
301
|
+
for (const root of roots.filter(Boolean)) {
|
|
302
|
+
for (const e of [root, ...root.querySelectorAll("*")].slice(0, 400)) {
|
|
303
|
+
const cs = e.ownerDocument.defaultView.getComputedStyle(e);
|
|
304
|
+
add(cs.backgroundColor, Math.min(e.offsetWidth * e.offsetHeight, 60000));
|
|
305
|
+
if (parseFloat(cs.borderTopWidth) > 0) add(cs.borderTopColor, 400);
|
|
306
|
+
if ([...e.childNodes].some((c) => c.nodeType === 3 && c.data.trim())) add(cs.color, 300);
|
|
307
|
+
if (e.localName === "svg" || e.closest("svg")) add(cs.fill, 600);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return Object.values(score).sort((a, b) => b.n - a.n)[0]?.color ?? null;
|
|
311
|
+
}
|
|
312
|
+
function open(yes) {
|
|
313
|
+
shelf.classList.toggle("open", yes);
|
|
314
|
+
if (yes) { thumbnails(); fit(); reveal(); }
|
|
315
|
+
else { peek.hidden = true; fit(); }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// drag a card (by its label: card/browser.js moves it) onto a thumbnail: it goes to that board, at the place it had
|
|
319
|
+
document.addEventListener("pointerdown", (e) => {
|
|
320
|
+
const box = e.target.closest?.("[data-card] > .card-label")?.parentElement;
|
|
321
|
+
if (!box || e.target.closest("button")) return;
|
|
322
|
+
const px = (v) => (v ? parseFloat(v) : undefined);
|
|
323
|
+
drag = { id: box.dataset.card, board: Number(box.dataset.board) || 1,
|
|
324
|
+
place: box.hasAttribute("data-placed") ? { x: box.offsetLeft, y: box.offsetTop, width: px(box.style.width), height: px(box.style.height) } : null };
|
|
325
|
+
});
|
|
326
|
+
document.addEventListener("pointermove", (e) => {
|
|
327
|
+
if (!drag) return;
|
|
328
|
+
const r = shelf.getBoundingClientRect();
|
|
329
|
+
if (!shelf.classList.contains("open") && e.clientY > r.top - 80 && e.clientX > r.left - 80 && e.clientX < r.right + 80) open(true);
|
|
330
|
+
const under = document.elementsFromPoint(e.clientX, e.clientY);
|
|
331
|
+
const now = under.find((el) => el.matches(".boards-shelf .slot")) ?? null;
|
|
332
|
+
if (now !== over) { over?.classList.remove("drop"); over = now; over?.classList.add("drop"); }
|
|
333
|
+
const arrow = under.find((el) => el.matches(".boards-shelf .arrow:not(:disabled)")); // held over ‹ or ›: the strip moves on
|
|
334
|
+
if (arrow && Date.now() - lastStep > 600) { lastStep = Date.now(); step(arrow === prev ? -1 : 1); }
|
|
335
|
+
});
|
|
336
|
+
let lastStep = 0;
|
|
337
|
+
document.addEventListener("pointerup", () => {
|
|
338
|
+
if (!drag) return;
|
|
339
|
+
const to = over && Number(over.dataset.n);
|
|
340
|
+
if (to && to !== drag.board) page.send({ type: "card_board", card: drag.id, board: to, place: drag.place });
|
|
341
|
+
over?.classList.remove("drop");
|
|
342
|
+
drag = over = null;
|
|
343
|
+
if (!shelf.matches(":hover")) open(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
page.on("board", (ev) => {
|
|
347
|
+
boards = ev.boards;
|
|
348
|
+
if (ev.removed) { // its things went with it (their own events); its container goes now
|
|
349
|
+
containers[ev.removed]?.remove();
|
|
350
|
+
delete containers[ev.removed];
|
|
351
|
+
if (!boards.includes(current)) return show(boards[0]); // another page removed the board this one shows
|
|
352
|
+
}
|
|
353
|
+
if (ev.added && ev.by === "user" && adding) { adding = false; return show(ev.added); } // the + clicked here: go there
|
|
354
|
+
render();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
try { ({ boards, current } = await (await fetch("/board")).json()); } catch {}
|
|
358
|
+
show(current, false); // nothing to tell: the server said it (and the page's WebSocket may not be open yet)
|
|
359
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Boards
|
|
2
|
+
The user's work is on numbered boards; the page shows one at a time. A plain message doesn't say which board the
|
|
3
|
+
user means: when it names one, use that; when it is about something already built, use that thing's board; else
|
|
4
|
+
leave board out (card_show puts a new card on the board the user is on). board_list() lists them all.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Boards: the main space is several numbered boards, and everything on it (cards, frames, notes; a doodle's
|
|
2
|
+
// steps when sent) has a board number. Numbers are never reused: remove board 2 and the rest stay 1, 3, 4, so "board 3" keeps
|
|
3
|
+
// meaning the same board to the user and to every agent. A thing with no number is on board 1.
|
|
4
|
+
// built/board.json {last: <last number>, boards: [numbers]}: next to card.json, so a rollback
|
|
5
|
+
// brings back both together (read each time: never stale after one)
|
|
6
|
+
// The board the user is looking at is the page's to say (in memory; board 1 until it does). browser.js: the shelf.
|
|
7
|
+
// Tool boards(board?): the boards and what is on each, and the board the user is on. Others use host.api.board:
|
|
8
|
+
// list() -> [numbers] has(n) add(by?) -> the new number current() -> the user's board
|
|
9
|
+
// onRemove(name, fn) remove(n): see below
|
|
10
|
+
// GET /board {boards, current} page messages board_view {board}, board_add, board_remove {board}
|
|
11
|
+
// event "board" {boards, added, by}: a board was added (by "user": that page goes there); {boards, removed}
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
|
|
17
|
+
const host = (globalThis as any).chalky;
|
|
18
|
+
const FILE = path.join(host.built, "board.json");
|
|
19
|
+
const read = () => (fs.existsSync(FILE) ? JSON.parse(fs.readFileSync(FILE, "utf8")) : { last: 1, boards: [1] });
|
|
20
|
+
if (!host.shared.has("board-view")) host.shared.set("board-view", { current: 1 });
|
|
21
|
+
const view = host.shared.get("board-view");
|
|
22
|
+
|
|
23
|
+
host.api.board = {
|
|
24
|
+
list: () => read().boards as number[],
|
|
25
|
+
has: (n: number) => read().boards.includes(n),
|
|
26
|
+
add(by = "agent") { // by "user": the page that asked goes there
|
|
27
|
+
const saved = read();
|
|
28
|
+
saved.boards.push(++saved.last);
|
|
29
|
+
fs.writeFileSync(FILE, JSON.stringify(saved));
|
|
30
|
+
host.broadcast("board", { boards: saved.boards, added: saved.last, by });
|
|
31
|
+
return saved.last;
|
|
32
|
+
},
|
|
33
|
+
current: () => (read().boards.includes(view.current) ? view.current : read().boards[0]),
|
|
34
|
+
// Removing a board (the user's ✕, after its few seconds of Undo on the page): each extension takes its own things
|
|
35
|
+
// off it (onRemove(name, fn): fn(n) deletes them and returns [{owner, what}], e.g. {owner: "agent2", what:
|
|
36
|
+
// "card prices"}; name: the extension's, so its reload replaces its own). Each agent that owned something gets
|
|
37
|
+
// one fyi (no turn of its own: it reads it on its next one). The last board stays.
|
|
38
|
+
onRemove: (name: string, fn: (n: number) => { owner?: string; what: string }[]) => removers.set(name, fn),
|
|
39
|
+
remove(n: number) {
|
|
40
|
+
const saved = read();
|
|
41
|
+
if (!saved.boards.includes(n) || saved.boards.length === 1) return;
|
|
42
|
+
const gone: { owner?: string; what: string }[] = [];
|
|
43
|
+
for (const [name, fn] of removers) {
|
|
44
|
+
try { gone.push(...fn(n)); }
|
|
45
|
+
catch (e) { host.broadcast("error", { agent: "server", text: `board ${n} removal, ${name}: ${e}` }); }
|
|
46
|
+
}
|
|
47
|
+
saved.boards = saved.boards.filter((b: number) => b !== n);
|
|
48
|
+
fs.writeFileSync(FILE, JSON.stringify(saved));
|
|
49
|
+
host.broadcast("board", { boards: saved.boards, removed: n });
|
|
50
|
+
const byOwner = new Map<string, string[]>();
|
|
51
|
+
for (const g of gone) if (g.owner && host.agents.has(g.owner)) byOwner.set(g.owner, [...(byOwner.get(g.owner) ?? []), g.what]);
|
|
52
|
+
for (const [owner, things] of byOwner)
|
|
53
|
+
host.api.team?.fyi("user", owner, `[board removed] The user removed board ${n}; your ${things.join(", ")} went with it.`);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// on the host, so they outlive this file's reloads (as ui's watchers)
|
|
57
|
+
const removers: Map<string, (n: number) => { owner?: string; what: string }[]> = host.shared.get("board-removers") ?? new Map();
|
|
58
|
+
host.shared.set("board-removers", removers);
|
|
59
|
+
|
|
60
|
+
const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: undefined });
|
|
61
|
+
|
|
62
|
+
export default function (pi: ExtensionAPI) {
|
|
63
|
+
|
|
64
|
+
pi.registerTool({
|
|
65
|
+
name: "board_list", label: "Board list",
|
|
66
|
+
description: "List the boards, what is on each, and the board the user is on. With a board: that one in detail.",
|
|
67
|
+
parameters: Type.Object({ board: Type.Optional(Type.Number()) }),
|
|
68
|
+
async execute(_id, a) {
|
|
69
|
+
// what has a board: cards (card/), frames not filled yet (frame/), open notes (note/), marks not cleared (doodle/)
|
|
70
|
+
const of = (m: any) => Object.entries(m ?? {}) as [string, any][];
|
|
71
|
+
const kinds = (n: number) => [of(host.shared.get("card")?.cards), of(host.shared.get("frame")?.frames),
|
|
72
|
+
of(host.shared.get("note")?.notes).filter(([, r]) => r.state === "open"),
|
|
73
|
+
of(host.shared.get("doodle")?.groups).filter(([, r]) => r.state !== "cleared")]
|
|
74
|
+
.map((list) => list.filter(([, r]) => (r.board ?? 1) === n));
|
|
75
|
+
const head = `the user is on board ${host.api.board.current()}`;
|
|
76
|
+
if (a.board === undefined)
|
|
77
|
+
return result([head, ...host.api.board.list().map((n: number) => {
|
|
78
|
+
const [s, f, notes, marks] = kinds(n);
|
|
79
|
+
return `${n} ${[s.map(([id]) => id).join(", "), f.map(([id]) => id).join(", "), notes.map(([id]) => `note ${id}`).join(", "),
|
|
80
|
+
marks.map(([id]) => `marks ${id}`).join(", ")]
|
|
81
|
+
.filter(Boolean).join(" · ") || "(empty)"}`;
|
|
82
|
+
})].join("\n"));
|
|
83
|
+
if (!host.api.board.has(a.board)) return result(`error: no board ${a.board}. Boards: ${host.api.board.list().join(", ")}`);
|
|
84
|
+
const [s, f, notes, marks] = kinds(a.board);
|
|
85
|
+
return result([head, `board ${a.board}:`,
|
|
86
|
+
...s.map(([id, r]) => ` card ${id} · by ${r.owner} · place ${JSON.stringify(r.place)}${r.pinned ? " · pinned" : ""}`),
|
|
87
|
+
...f.map(([id, r]) => ` frame ${id} "${r.text}" · ${r.sent ? `sent to ${r.to}` : "not sent"} · place ${JSON.stringify({ x: r.x, y: r.y, width: r.width, height: r.height })}`),
|
|
88
|
+
...notes.map(([id, r]) => ` note ${id} "${r.thread[0]?.text}" · to ${r.to}`),
|
|
89
|
+
...marks.map(([id, r]) => ` marks ${id} (${r.state}) · ${r.label}`)].join("\n") + (s.length + f.length + notes.length + marks.length ? "" : "\n (empty)"));
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
host.routes.set("/board", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" })
|
|
94
|
+
.end(JSON.stringify({ boards: host.api.board.list(), current: host.api.board.current() })));
|
|
95
|
+
host.pageHandlers.set("board", (msg: any) => {
|
|
96
|
+
if (msg.type === "board_view" && host.api.board.has(Number(msg.board))) view.current = Number(msg.board);
|
|
97
|
+
else if (msg.type === "board_add") host.api.board.add("user");
|
|
98
|
+
else if (msg.type === "board_remove") host.api.board.remove(Number(msg.board));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Cards (the card extension)
|
|
2
|
+
A card is one thing you show the user on a board in the page's main space: a form, a table, a dashboard, a game.
|
|
3
|
+
It shows either blocks (pieces described as JSON) or an app (a page you write). Prefer a card over editing the page
|
|
4
|
+
({agent}/extensions/page/page.html). Aim for one screen; make it scroll only if the user asks.
|
|
5
|
+
A card shows only what you build, with no frame: when asked for a thing (a smiley, a chart), show just that thing.
|
|
6
|
+
Add a background, border or Panel only if the thing itself needs one. An app's page should keep a transparent
|
|
7
|
+
background, so it sits on the board.
|
|
8
|
+
Name what you build (a clear card id, block ids, ids or data-name on elements): the user points at things by name.
|
|
9
|
+
Blocks or an app? What the user clicks never wakes you, so the page must do everything a click should do:
|
|
10
|
+
- Blocks (card_show with blocks), when clicks and typing only store a value: the layout is fixed, the user types,
|
|
11
|
+
picks, ticks or switches tabs (Button set, when, bindings), and you read the data when they next write. Status,
|
|
12
|
+
tables, summaries, forms, settings, a fixed checklist, a dashboard you fill in.
|
|
13
|
+
- An app, when the page must work something out or change its own shape while the user uses it: compute a value
|
|
14
|
+
(a total, a streak, a score), add or remove items, follow rules or time (a game, a timer), draw (a chart).
|
|
15
|
+
Trackers, calculators, games, editors, charts.
|
|
16
|
+
The test: if a click or keystroke should change anything other than the value it sets, it is an app. Blocks
|
|
17
|
+
can't append to a list or calculate; a Button only writes the fixed value it was given.
|
|
18
|
+
A new block is for one part blocks can't do yet that many cards will reuse; then use it from JSON.
|
|
19
|
+
Before building or restyling what the user sees, load the design skill (how it should look and read).
|
|
20
|
+
- card_blocks (load it with tool_search): lists the blocks and their props. Read it first.
|
|
21
|
+
- card_show(card, blocks, data): show a card, or replace it. blocks is a flat list; each has "id", "block" (its
|
|
22
|
+
kind, a name from card_blocks), its props, and "children": [ids] or "child": id. One has id "root".
|
|
23
|
+
A prop can be {"path": "/x"} to bind it to this card's data (inputs write to it), or
|
|
24
|
+
{"ref": "other-card", "path": "/x"} to read another card's data live (no copy: it follows changes).
|
|
25
|
+
Any block can have "when": {"path": "/x", "is": value}: hidden while the data there is something else.
|
|
26
|
+
Tabs: Buttons with "set": {"/tab": "today"} etc., and one block per tab with "when": {"path": "/tab", "is": ...};
|
|
27
|
+
start the data with the first tab ({"tab": "today"}). Errors come back to you; fix and retry.
|
|
28
|
+
- card_show(card, app=<name>): the card shows that app, then says what it found on the board (errors, and whether
|
|
29
|
+
it fits its card): fix what it reports. Write the page first, in
|
|
30
|
+
{built}/apps/<name>/index.html (its other files next to it); the app extension's README says how an app keeps
|
|
31
|
+
its data and talks to the agents. height: its height in px when nothing else sizes it (default 400).
|
|
32
|
+
- about="...": one or two sentences on what the card is for and why it is built this way. The user reads it on
|
|
33
|
+
the card's back side; you read it in {built}/card.json. Keep it true when you change things.
|
|
34
|
+
- options=[{"label", "blocks" or "app", "data"}, ...] instead of blocks or app: when you are not sure what the user
|
|
35
|
+
meant, offer 2-3 versions; they flip through them and keep one.
|
|
36
|
+
- Pinned cards (the user held their label): your card_show, card_update and card_remove on them fail. Don't work
|
|
37
|
+
around it: make a new card (made_from=[the pinned one]) or ask the user.
|
|
38
|
+
- Cards stack in the main space and are as wide as their content. place={"x", "y", "width"?, "height"?}
|
|
39
|
+
(px from the main space's top-left) pins one at a spot instead: use it when the user points at a spot ("here")
|
|
40
|
+
or draws an area (then fill that area: its x, y, width and height).
|
|
41
|
+
- Everything you show is draggable by default: the user moves a card by its label (shown on hover), and it stays
|
|
42
|
+
where they put it (card_show without place keeps it there; where things are now: the "place" of each card in
|
|
43
|
+
{built}/card.json). Pass draggable=false only when the user wants something fixed. In an app you build, make
|
|
44
|
+
separate pieces draggable too, unless told otherwise.
|
|
45
|
+
- Everything you show is resizable by default too: the user drags its bottom-right corner (shown on hover), and
|
|
46
|
+
the card's root block (or its app) fills the new size. So build things that fill their space (sizes in %, flex,
|
|
47
|
+
or scaled to their box, e.g. an SVG with a viewBox), not fixed px. Pass resizable=false only when the user wants
|
|
48
|
+
a fixed size.
|
|
49
|
+
- card_update(card, path, value): change data only (fast; blocks keep their state). With block=<its id>, path is a
|
|
50
|
+
prop of that block (e.g. a label's text): a small fix without card_show. card_show without data keeps the card's
|
|
51
|
+
data. card_remove(card) takes it off its board.
|
|
52
|
+
- Data (what users typed, rows, scores) is kept in {data}/card.json (not versioned); the rest of each card in
|
|
53
|
+
{built}/card.json. The user can undo any change (theirs or yours), so don't keep backups.
|
|
54
|
+
- A Button's click writes its set ({path: value}) into the card's data on the page; you are not woken, and you
|
|
55
|
+
see the data in {data}/card.json. A row of Buttons setting the same path works as tabs.
|
|
56
|
+
- Voice input (dictation, meeting notes): import /voice/voice.js in the app (read its header), never the browser's
|
|
57
|
+
speech API or a provider directly: it uses the sandbox's speech-to-text API, or the browser's.
|
|
58
|
+
- A new block: {agent}/extensions/card/blocks/<Name>.js. Start with a comment line "// Name: what it does" (more
|
|
59
|
+
comment lines if needed), then its about block, then
|
|
60
|
+
`export default class extends HTMLElement { update(props, card) {...} }`.
|
|
61
|
+
The about block of a block (strict JSON after `export const about = `):
|
|
62
|
+
"version": 1 the shape of its props; raise it when they change, and export
|
|
63
|
+
upgrade(props, fromVersion) -> props, so cards saved earlier still draw
|
|
64
|
+
"props": {"name": {"type": "text" | "number" | "boolean" | "choice" | "list" | "rows" | "object" | "action",
|
|
65
|
+
"options": [...] (for choice), "default": ..., "doc": "what it is"}}
|
|
66
|
+
"children": "none" | "one" | "many"
|
|
67
|
+
"levers": ["Shown as {variant}."] the back side: plain sentences; each {prop} becomes a control the user
|
|
68
|
+
can change there (choice and boolean cycle, text and number are typed)
|
|
69
|
+
"accepts": "rows with a number column" optional: what it can take when something is dropped on it
|
|
70
|
+
"example": {...props} one sample, for the catalog and for checks
|
|
71
|
+
update() runs on every change, on the same element: keep internal state there (e.g. a game loop).
|
|
72
|
+
card.children: child elements, placed with card.mount(container); card.set(prop, value) writes a bound prop;
|
|
73
|
+
card.data is the card's data and card.write({path: value}) changes it (saved, you are not woken).
|
|
74
|
+
card.action(name, context) sends an action to you and wakes you: only for what truly needs you (e.g. "ask the
|
|
75
|
+
agents"), never for clicks the page can handle. Make it general so it can be reused. New blocks work
|
|
76
|
+
immediately; edited blocks and app files show when your turn ends (the page refreshes itself).
|
|
77
|
+
Pointing inside a canvas: give the element (or an app page's window) a partAt(x, y) function returning
|
|
78
|
+
{"name", ...} for what is at that spot, so the user can point at parts (an enemy, a cell).
|
|
79
|
+
- Agents inside what you build (a simulation's members, a debate, characters): load the app-agents skill first.
|
|
80
|
+
They are real agents that belong to the app, not workers helping you build it.
|
|
81
|
+
- Libraries: blocks import marked and dompurify by name (import { marked } from "marked"), as mapped in the import
|
|
82
|
+
map at the top of {agent}/extensions/page/page.html. To add one, put its ES-module build in
|
|
83
|
+
{agent}/extensions/card/lib/<pkg>/ and import it by path (/card/lib/<pkg>/<file>.js).
|
|
84
|
+
- Block pitfalls (you can't see the browser, so avoid them up front):
|
|
85
|
+
keep your own state in one object, this.state = {...}: fields like this.dir, this.title, this.hidden,
|
|
86
|
+
this.id, this.style, this.lang are built-in HTMLElement properties and silently break;
|
|
87
|
+
set a canvas's width/height BEFORE drawing (resizing clears it); don't create things twice in update();
|
|
88
|
+
remove document-level listeners/timers when the element is no longer connected.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Button: a click writes {set} into the card's data, on the page (no agent is woken). It looks primary
|
|
2
|
+
// while the data already holds those values, so a row of them works as tabs.
|
|
3
|
+
export const about = {
|
|
4
|
+
"version": 2,
|
|
5
|
+
"props": {
|
|
6
|
+
"label": {"type": "text", "default": "OK"},
|
|
7
|
+
"set": {"type": "object", "doc": "{path: value, ...} written into this card's data on click, e.g. {\"/tab\": \"training\"}"},
|
|
8
|
+
"variant": {"type": "choice", "options": ["primary", "secondary"], "default": "primary", "doc": "its look when not selected"}
|
|
9
|
+
},
|
|
10
|
+
"children": "none",
|
|
11
|
+
"levers": ["Says {label}.", "Sets {set}.", "Looks {variant}."],
|
|
12
|
+
"example": {"label": "Training", "set": {"/tab": "training"}, "variant": "secondary"}
|
|
13
|
+
};
|
|
14
|
+
export function upgrade(props, from) { // version 1 sent an action to the agent instead
|
|
15
|
+
const { action, context, ...rest } = props;
|
|
16
|
+
return rest;
|
|
17
|
+
}
|
|
18
|
+
const get = (data, path) => path.split("/").filter(Boolean).reduce((o, k) => o?.[k], data);
|
|
19
|
+
export default class extends HTMLElement {
|
|
20
|
+
update(props, card) {
|
|
21
|
+
if (!this.button) {
|
|
22
|
+
this.button = document.createElement("button");
|
|
23
|
+
this.button.onclick = () => this.props.set && this.card.write(this.props.set);
|
|
24
|
+
this.append(this.button);
|
|
25
|
+
}
|
|
26
|
+
this.props = props;
|
|
27
|
+
this.card = card;
|
|
28
|
+
const selected = props.set && Object.entries(props.set).every(([p, v]) => get(card.data, p) === v);
|
|
29
|
+
this.button.textContent = props.label || "OK";
|
|
30
|
+
this.button.style.cssText = "border-radius:980px;padding:7px 16px;font-weight:500;" + (!selected && props.variant === "secondary"
|
|
31
|
+
? "background:var(--glass-strong);color:var(--accent);border:1px solid var(--line)"
|
|
32
|
+
: "background:var(--accent);color:#fff;border:0");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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 = "stretch" },
|
|
13
|
+
update({ gap = 8, align = "stretch" }, card) {
|
|
14
14
|
this.style.cssText = `display:flex;flex-direction:column;gap:${gap}px;align-items:${align}`;
|
|
15
|
-
|
|
15
|
+
card.mount(this);
|
|
16
16
|
}
|
|
17
17
|
}
|