@lovek4d/kickflip 0.1.0 → 0.4.0
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/CONTEXT.md +239 -104
- package/README.md +54 -4
- package/SKILL.md +63 -42
- package/bin/doctor.mjs +4 -3
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +103 -40
- package/bin/statusline.mjs +4 -3
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +5 -0
- package/board/auto-review.mjs +2 -0
- package/board/bin/agent.mjs +1 -1
- package/board/bin/board.mjs +371 -82
- package/board/bin/builder-stop-hook.mjs +4 -3
- package/board/domain.mjs +8 -3
- package/board/finish.mjs +3 -0
- package/board/headless.mjs +3 -0
- package/board/on-host.mjs +1 -0
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +75 -0
- package/board/public/app-asks.js +66 -28
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +354 -77
- package/board/public/app-core.js +40 -8
- package/board/public/app-header.js +33 -8
- package/board/public/app-lanes.js +238 -34
- package/board/public/app-needs.js +2 -0
- package/board/public/app-snapshot.js +45 -11
- package/board/public/app-threads.js +20 -21
- package/board/public/app.html +6 -2
- package/board/public/app.js +114 -33
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +161 -118
- package/board/public/hub-kit.js +27 -7
- package/board/public/hub.css +26 -33
- package/board/public/linear-key.js +143 -0
- package/board/public/log.html +48 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +14 -10
- package/board/public/rules-editor.js +4 -5
- package/board/public/shell.js +21 -7
- package/board/public/sidebar.css +51 -11
- package/board/public/sidebar.js +124 -19
- package/board/public/style.css +244 -56
- package/board/public/toasts.js +5 -3
- package/board/public/ui-cards.js +236 -66
- package/board/public/ui-enums.js +5 -4
- package/board/public/ui-header.js +23 -14
- package/board/public/ui-hub.js +34 -16
- package/board/public/ui-links.js +18 -10
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +21 -0
- package/board/public/ui-page-header.js +13 -8
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +35 -11
- package/board/public/ui-sidebar.js +83 -21
- package/board/public/ui-snapshot.js +30 -13
- package/board/public/ui-threads.js +38 -23
- package/board/public/ui.js +96 -16
- package/board/public/view-frame.js +1 -1
- package/board/public/view-hub-settings.js +67 -44
- package/board/public/view-hub.js +105 -13
- package/board/public/view-notes.js +8 -5
- package/board/public/view-questions.js +123 -52
- package/board/public/view-settings.js +235 -62
- package/board/review.mjs +1 -0
- package/board/server.mjs +18 -7
- package/board/store.mjs +15 -6
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +20 -13
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
- package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
- package/hub/awake.mjs +1 -0
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/duration.mjs +1 -0
- package/hub/github.mjs +3 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -10
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -0
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -0
- package/hub/repos.mjs +3 -2
- package/hub/restart-board.mjs +1 -0
- package/hub/routes.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/shell.mjs +2 -1
- package/hub/slack.mjs +2 -0
- package/hub/stream.mjs +12 -12
- package/hub/sw.js +2 -2
- package/hub/trust.mjs +2 -0
- package/hub/try.mjs +8 -3
- package/hub/upgrade.mjs +1 -0
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +9 -6
- package/shared/fake-gh.mjs +105 -0
- package/shared/repos.mjs +1 -1
- package/shared/service.mjs +16 -16
- package/shared/skills.mjs +2 -0
- package/shared/usage-gate.mjs +1 -1
- package/starter/public/claude.js +3 -2
- package/bin/web-ts.mjs +0 -2
- package/board/public/view-polling.js +0 -87
package/board/public/app.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en" data-build="{{build}}">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
@@ -41,6 +41,9 @@ window.BoardBoot = (function () {
|
|
|
41
41
|
<script src="/_hub/app/ui.js"></script>
|
|
42
42
|
<script src="/_hub/app/ui-links.js"></script>
|
|
43
43
|
<script src="/_hub/app/ui-enums.js"></script>
|
|
44
|
+
<script src="/_hub/app/ui-phase-bars.js"></script>
|
|
45
|
+
<script src="/_hub/app/ui-order.js"></script>
|
|
46
|
+
<script src="/_hub/app/ui-patch.js"></script>
|
|
44
47
|
<script src="/_hub/app/ui-threads.js"></script>
|
|
45
48
|
<script src="/_hub/app/ui-cards.js"></script>
|
|
46
49
|
<script src="/_hub/app/ui-header.js"></script>
|
|
@@ -58,6 +61,7 @@ window.BoardBoot = (function () {
|
|
|
58
61
|
<script src="/_hub/app/claude.js"></script>
|
|
59
62
|
<script src="/_hub/app/attach.js"></script>
|
|
60
63
|
<script src="/_hub/app/hub-kit.js"></script>
|
|
64
|
+
<script src="/_hub/app/linear-key.js"></script>
|
|
61
65
|
<script src="/_hub/app/sidebar.js"></script>
|
|
62
66
|
<script src="/_hub/app/app.js"></script>
|
|
63
67
|
<script src="/_hub/app/app-core.js"></script>
|
|
@@ -67,6 +71,7 @@ window.BoardBoot = (function () {
|
|
|
67
71
|
<script src="/_hub/app/app-asks.js"></script>
|
|
68
72
|
<script src="/_hub/app/app-needs.js"></script>
|
|
69
73
|
<script src="/_hub/app/app-header.js"></script>
|
|
74
|
+
<script src="/_hub/app/app-arch.js"></script>
|
|
70
75
|
<script src="/_hub/app/app-banner.js"></script>
|
|
71
76
|
<script src="/_hub/app/app-snapshot.js"></script>
|
|
72
77
|
<script src="/_hub/app/view-settings.js"></script>
|
|
@@ -74,7 +79,6 @@ window.BoardBoot = (function () {
|
|
|
74
79
|
<script src="/_hub/app/view-notes.js"></script>
|
|
75
80
|
<script src="/_hub/app/view-hub.js"></script>
|
|
76
81
|
<script src="/_hub/app/view-hub-settings.js"></script>
|
|
77
|
-
<script src="/_hub/app/view-polling.js"></script>
|
|
78
82
|
<script src="/_hub/app/view-frame.js"></script>
|
|
79
83
|
<script src="/_hub/app/router.js"></script>
|
|
80
84
|
<script src="/_hub/app/shell.js"></script>
|
package/board/public/app.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// and mount() runs the parts in load order, then renders the board from /api/board and per-item threads from
|
|
4
4
|
// /api/thread. Actions go out as events through B.site.send (each wakes Claude). Parts add listeners, intervals,
|
|
5
5
|
// timers, watches and unmount steps only through B.on, B.every, B.later, B.watch and B.atUnmount, so unmount() undoes
|
|
6
|
-
// them all and no board's state outlives its view
|
|
7
|
-
// HTML. BoardView.page(root, site, run) gives a board's other views
|
|
6
|
+
// them all and no board's state outlives its view but what B.keep keeps for its next mount in this page.
|
|
7
|
+
// BoardView.markup({title}) is the root's inner HTML. BoardView.page(root, site, run) gives a board's other views
|
|
8
|
+
// (view-*.js) the same site, $ and recorders.
|
|
8
9
|
// opts, both from the app shell (shell.js):
|
|
9
10
|
// signal(fn) → off calls fn on each change signal for this board, from the hub's one stream
|
|
10
11
|
// live {fresh, generatedAt}
|
|
@@ -14,6 +15,9 @@
|
|
|
14
15
|
window.BoardView = (function () {
|
|
15
16
|
const parts = [];
|
|
16
17
|
const part = (fn) => { parts.push(fn); };
|
|
18
|
+
// route → what a board's view kept as it unmounted, which its next mount in this page reads as B.place. A reload
|
|
19
|
+
// starts with none.
|
|
20
|
+
const places = new Map();
|
|
17
21
|
|
|
18
22
|
// Every board shares the hub's one origin, so each keeps its keys under site:<route>:; app.html's preload reads
|
|
19
23
|
// openItems by the same key. Thread and question drafts are already per card and keep their own keys.
|
|
@@ -105,13 +109,21 @@ window.BoardView = (function () {
|
|
|
105
109
|
fresh: null,
|
|
106
110
|
openItems: new Set(store.get('openItems', [])),
|
|
107
111
|
threadCache: new Map(),
|
|
108
|
-
withdrawnAsks: new Set(),
|
|
109
112
|
edits: new Map(),
|
|
110
113
|
githubPending: null,
|
|
111
|
-
|
|
114
|
+
// Main's refresh icon (ui-cards.js mainPull): a tap waiting on the server, and the failed pull's reason open on touch.
|
|
115
|
+
mainPull: { sending: false, open: false },
|
|
116
|
+
// The Done cards Show more paged in from the archive past the board's own (app-lanes.js), and how many older ones
|
|
117
|
+
// the archive holds past them, null before the first page.
|
|
118
|
+
archived: { cards: [], left: null },
|
|
112
119
|
};
|
|
113
120
|
const signal = opts.signal ? (fn) => { if (B.alive()) B.own(opts.signal(fn)); } : null;
|
|
114
|
-
|
|
121
|
+
// B.keep(fn): a part adds fn() → {…} to what the board keeps as it unmounts, read before any part's unmount step.
|
|
122
|
+
const keepers = [];
|
|
123
|
+
const keep = (fn) => { if (B.alive()) keepers.push(fn); };
|
|
124
|
+
const place = places.get(B.route) || null;
|
|
125
|
+
places.delete(B.route);
|
|
126
|
+
Object.assign(B, { store, state, signal, live: opts.live || null, pause: opts.pause, away: opts.away, place, keep });
|
|
115
127
|
|
|
116
128
|
function mount() {
|
|
117
129
|
if (B.alive()) return;
|
|
@@ -119,7 +131,12 @@ window.BoardView = (function () {
|
|
|
119
131
|
for (const p of parts) p(B);
|
|
120
132
|
start(B);
|
|
121
133
|
}
|
|
122
|
-
|
|
134
|
+
function unmount() {
|
|
135
|
+
if (B.alive()) places.set(B.route, Object.assign({}, ...keepers.map((fn) => fn())));
|
|
136
|
+
keepers.length = 0;
|
|
137
|
+
stop();
|
|
138
|
+
}
|
|
139
|
+
return { B, mount, unmount };
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
// A view other than the board: run(B) adds what it needs through B's recorders when the view mounts.
|
|
@@ -139,34 +156,41 @@ window.BoardView = (function () {
|
|
|
139
156
|
const { setHTML, preloadKeys } = BoardUI;
|
|
140
157
|
|
|
141
158
|
// Renders open threads from state.threadCache; loadBoard and opening a card load them.
|
|
142
|
-
function render() {
|
|
159
|
+
function render(load = null) {
|
|
143
160
|
const d = state.board;
|
|
144
161
|
const title = $(':scope > .top h1');
|
|
145
162
|
if (d.title && title && title.textContent !== d.title) title.textContent = d.title;
|
|
146
163
|
if (d.title && document.title !== d.title) document.title = d.title;
|
|
147
164
|
steady(() => {
|
|
148
165
|
B.renderHeader(d);
|
|
149
|
-
B.
|
|
150
|
-
BoardUI.setHTML($('#tryMain'), BoardUI.mainBar(d.main));
|
|
151
|
-
|
|
152
|
-
const shown = state.waveFilter ? live.filter((i) => i.wave === state.waveFilter) : live;
|
|
166
|
+
B.renderArch(d);
|
|
167
|
+
BoardUI.setHTML($('#tryMain'), BoardUI.mainBar(d.main, state.mainPull));
|
|
168
|
+
B.renderMainLog(d.main, load);
|
|
153
169
|
for (const el of $$('details.item[open]')) state.openItems.add(el.dataset.key);
|
|
154
170
|
for (const it of d.items) if (!it.isNew) markSeen(it, !state.openItems.has(it.key));
|
|
155
171
|
// An open card's thread is renderThread's (app-threads.js), so a lane repaint leaves it as it is.
|
|
156
|
-
|
|
172
|
+
const strips = B.captureStrips();
|
|
173
|
+
setHTML($('#lanes'), B.lanesHtml(d, d.items), { keep: '[data-thread]' });
|
|
157
174
|
for (const key of state.openItems) if (state.threadCache.has(key)) B.renderThread(key, state.threadCache.get(key));
|
|
175
|
+
B.moveStrips(d.items, strips);
|
|
158
176
|
B.renderNeeds(d);
|
|
159
177
|
B.renderBanner(d);
|
|
160
178
|
});
|
|
161
179
|
B.flashChanged();
|
|
180
|
+
B.signCards(load);
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
// #review=<PR number> opens that review in the Auto Review lane (app-lanes.js openReview), as a held review's push does.
|
|
164
184
|
// #item=<wave>/<id> opens that card; push notifications link here, with &reply to land in the thread's reply box
|
|
165
|
-
// (docs/adr/
|
|
185
|
+
// (docs/adr/2026-10-29-1639-a-push-lands-where-the-owner-answers.md). An ask now in a work item's thread opens that item.
|
|
166
186
|
let focusKey = null;
|
|
167
187
|
let replyKey = null;
|
|
168
188
|
let hashRead = false;
|
|
189
|
+
// A board this page left goes back to where the owner left it once it first renders, unless its URL names a card.
|
|
190
|
+
const returnToPlace = () => B.restorePlace({ linked: /^#(?:item|review)=/.test(location.hash) });
|
|
169
191
|
function readHash() {
|
|
192
|
+
const review = /^#review=(\d+)$/.exec(location.hash);
|
|
193
|
+
if (review && state.board) { B.openReview(Number(review[1])); return; }
|
|
170
194
|
const m = /^#item=([^&]+)(&reply)?$/.exec(location.hash);
|
|
171
195
|
if (!m) return;
|
|
172
196
|
focusKey = decodeURIComponent(m[1]);
|
|
@@ -174,7 +198,7 @@ window.BoardView = (function () {
|
|
|
174
198
|
replyKey = m[2] ? focusKey : null;
|
|
175
199
|
state.openItems.add(focusKey);
|
|
176
200
|
const it = state.board?.items.find((i) => i.key === focusKey);
|
|
177
|
-
if (it) { const s = store.get('lanes', {}); s[it.lane] = true; store.set('lanes', s); }
|
|
201
|
+
if (it) { const s = store.get('lanes', {}); s[it.lane] = true; store.set('lanes', s); B.openBand({ key: focusKey }); }
|
|
178
202
|
if (it?.fold) store.set(B.FOLDS[it.fold].store, true);
|
|
179
203
|
}
|
|
180
204
|
B.on(window, 'hashchange', () => { readHash(); if (state.board) { render(); scrollToFocus(); } });
|
|
@@ -208,7 +232,7 @@ window.BoardView = (function () {
|
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
// The last visit's board paints dimmed while the first load runs; it doesn't count as landed, so refresh keeps
|
|
211
|
-
// spinning, and the header's time is the snapshot's until a load lands (docs/adr/
|
|
235
|
+
// spinning, and the header's time is the snapshot's until a load lands (docs/adr/2026-10-29-1639-the-board-opens-on-the-last-visit.md).
|
|
212
236
|
// → true when it painted live (B.live), so the controls turn on at once.
|
|
213
237
|
function paintSnapshot() {
|
|
214
238
|
const snap = B.readSnapshot();
|
|
@@ -222,6 +246,7 @@ window.BoardView = (function () {
|
|
|
222
246
|
hashRead = true;
|
|
223
247
|
render();
|
|
224
248
|
scrollToFocus(false);
|
|
249
|
+
returnToPlace();
|
|
225
250
|
if (!live) B.holdForFlash();
|
|
226
251
|
return live;
|
|
227
252
|
}
|
|
@@ -235,26 +260,23 @@ window.BoardView = (function () {
|
|
|
235
260
|
if (boot?.route && boot.route !== B.route) boot = null;
|
|
236
261
|
let preloaded = false;
|
|
237
262
|
async function loadBoard({ via = 'poll' } = {}) {
|
|
263
|
+
const load = B.boardLoad = (B.boardLoad || 0) + 1;
|
|
238
264
|
const started = boot;
|
|
239
265
|
boot = null;
|
|
240
266
|
const threads = [...state.openItems].map((k) => B.loadThread(k, { from: started?.threads.get(k) || null, force: via !== 'poll' }));
|
|
241
267
|
let landed = false;
|
|
242
268
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
state.board
|
|
247
|
-
B.
|
|
248
|
-
|
|
249
|
-
B.linkUp();
|
|
250
|
-
render();
|
|
251
|
-
scrollToFocus();
|
|
252
|
-
B.site.syncBadge();
|
|
253
|
-
B.saveSnapshotSoon();
|
|
269
|
+
// A reload names the version of the board on screen: an unchanged board answers 304 with no body
|
|
270
|
+
// (docs/adr/2026-11-04-0128-the-board-download-leaves-out-resolved-questions.md), and a changed one a patch from it
|
|
271
|
+
// (docs/adr/2026-11-04-0332-the-board-sends-per-card-patches.md).
|
|
272
|
+
const tag = state.view === 'fresh' ? state.board.version : null;
|
|
273
|
+
const board = await (started?.board || fetch(B.site.url('api/board' + (tag ? '?since=' + encodeURIComponent(tag) : '')), { cache: 'no-store', ...(tag ? { headers: { 'if-none-match': `"${tag}"` } } : {}) })
|
|
274
|
+
.then((r) => (r.status === 304 ? null : r.ok ? r.json() : Promise.reject(new Error(`board ${r.status}`)))));
|
|
275
|
+
if (board) land(board, load); else B.linkUp();
|
|
254
276
|
landed = true;
|
|
255
277
|
if (!preloaded) {
|
|
256
278
|
preloaded = true;
|
|
257
|
-
for (const key of preloadKeys(board.items, state.openItems)) B.loadThread(key);
|
|
279
|
+
for (const key of preloadKeys(state.board.items, state.openItems)) B.loadThread(key);
|
|
258
280
|
}
|
|
259
281
|
} catch (e) {
|
|
260
282
|
B.linkDown();
|
|
@@ -264,8 +286,42 @@ window.BoardView = (function () {
|
|
|
264
286
|
if (state.view === 'fresh') B.endFlash();
|
|
265
287
|
}
|
|
266
288
|
|
|
289
|
+
// A board from the server, a load's or an events POST's, or a patch from the board on screen (ui-patch.js). One built
|
|
290
|
+
// before the board on screen leaves it as it is, so a slow load can't undo a newer one; a newer patch from a board
|
|
291
|
+
// no longer on screen, which another answer replaced meanwhile, reloads instead. → whether it landed
|
|
292
|
+
function land(answer, load) {
|
|
293
|
+
if (state.view === 'fresh' && state.board && answer.generatedAt < state.board.generatedAt) return false;
|
|
294
|
+
const board = answer.since === undefined ? answer : BoardUI.applyPatch(state.view === 'fresh' ? state.board : null, answer);
|
|
295
|
+
if (!board) { signalled(); return false; }
|
|
296
|
+
const first = state.view !== 'fresh';
|
|
297
|
+
state.board = board;
|
|
298
|
+
B.setView('fresh');
|
|
299
|
+
if (!hashRead || (first && focusKey)) { readHash(); hashRead = true; }
|
|
300
|
+
B.linkUp();
|
|
301
|
+
render(load);
|
|
302
|
+
scrollToFocus();
|
|
303
|
+
returnToPlace();
|
|
304
|
+
B.site.syncBadge();
|
|
305
|
+
B.saveSnapshotSoon();
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// An owner tap that saves something (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md): the events POST
|
|
310
|
+
// answers once the server has recorded the tap, with the board as the tap left it, which lands as a load of its own,
|
|
311
|
+
// so the tap needs no reload (docs/adr/2026-11-03-0500-every-owner-tap-the-server-can-apply-draws-at-once.md); it names
|
|
312
|
+
// the version on screen, so the answer is a patch from it. Nothing draws
|
|
313
|
+
// before that answer. answered() runs once it comes, before its board lands. A refusal throws, carrying the
|
|
314
|
+
// server's reason for failed.
|
|
315
|
+
async function send(kind, data, answered = null) {
|
|
316
|
+
const out = await B.site.send(kind, { ...data, ...(state.view === 'fresh' ? { since: state.board.version } : {}) });
|
|
317
|
+
if (!B.alive()) return out;
|
|
318
|
+
answered?.();
|
|
319
|
+
land(out.board, B.boardLoad = (B.boardLoad || 0) + 1);
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
267
323
|
// The server signals each change to what the board reads, through the shell, and the board reloads at once; the
|
|
268
|
-
// 30 s poll stays as the fallback and the offline check (docs/adr/
|
|
324
|
+
// 30 s poll stays as the fallback and the offline check (docs/adr/2026-10-29-1639-the-board-reloads-on-a-change-signal.md).
|
|
269
325
|
let reloading = null;
|
|
270
326
|
let reloadAgain = false;
|
|
271
327
|
let signalMissed = false;
|
|
@@ -278,7 +334,9 @@ window.BoardView = (function () {
|
|
|
278
334
|
});
|
|
279
335
|
}
|
|
280
336
|
|
|
281
|
-
|
|
337
|
+
// The toast for a tap the server refused, with its reason, or that never reached it.
|
|
338
|
+
const failed = (e) => B.toast(e?.reason || 'That didn’t go through. Is the host awake?');
|
|
339
|
+
Object.assign(B, { render, loadBoard, send, failed, reload: signalled });
|
|
282
340
|
const live = paintSnapshot();
|
|
283
341
|
B.headerWired();
|
|
284
342
|
if (live) B.linkUp();
|
|
@@ -294,27 +352,50 @@ window.BoardView = (function () {
|
|
|
294
352
|
}
|
|
295
353
|
|
|
296
354
|
function markup({ title = '' } = {}) {
|
|
297
|
-
return `${BoardUI.header({ lead: 'logo', title, logoSrc: '/_hub/kickflip.png' })}
|
|
355
|
+
return `${BoardUI.header({ lead: 'logo', title, logoSrc: '/_hub/kickflip.png', bulb: true, moon: true })}
|
|
298
356
|
<main>
|
|
299
357
|
<section id="setup" class="setup" aria-live="polite" hidden></section>
|
|
300
358
|
<section id="answers" class="answer-banner" hidden></section>
|
|
301
359
|
<section id="waiting" class="waiting" hidden></section>
|
|
302
|
-
<div id="waves" class="waves"></div>
|
|
303
360
|
<div id="tryMain"></div>
|
|
304
361
|
<div id="lanes"></div>
|
|
305
362
|
</main>
|
|
306
|
-
<button id="compose" class="fab" type="button" aria-label="New task" title="New task (press n twice)" aria-haspopup="dialog" aria-expanded="false"><svg width="
|
|
363
|
+
<button id="compose" class="fab" type="button" aria-label="New task" title="New task (press n twice)" aria-haspopup="dialog" aria-expanded="false"><svg width="35" height="35" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
|
|
307
364
|
<dialog id="sheet" class="sheet" aria-labelledby="sheetTitle">
|
|
308
365
|
<form id="askForm" data-composer="ask">
|
|
309
366
|
<div class="sheet-head"><h2 id="sheetTitle">New task</h2><button type="button" class="ghost" id="sheetClose" aria-label="Close">✕</button></div>
|
|
310
|
-
<textarea id="askText" rows="4" placeholder="Ask a question or give me a task…" autofocus></textarea>
|
|
367
|
+
${BoardUI.dropZone('<textarea id="askText" rows="4" placeholder="Ask a question or give me a task…" autofocus></textarea>')}
|
|
311
368
|
<div class="pending-files" data-files></div>
|
|
369
|
+
<div class="tk-seg ask-seg" role="group" aria-label="Before building"><button type="button" id="askDiscuss" aria-pressed="true">Discuss</button><button type="button" id="askQuick" aria-pressed="false">Quick build</button></div>
|
|
370
|
+
<p class="muted small ask-choice-help" id="askChoiceHelp" aria-live="polite"></p>
|
|
312
371
|
<div class="row">
|
|
313
372
|
<span class="muted small" id="askStatus"></span>
|
|
314
373
|
<button type="submit" class="primary">Send</button>
|
|
315
374
|
</div>
|
|
316
375
|
</form>
|
|
317
376
|
</dialog>
|
|
377
|
+
<dialog id="mainLogSheet" class="sheet" aria-labelledby="mainLogTitle">
|
|
378
|
+
<div class="sheet-body">
|
|
379
|
+
<div class="sheet-head"><h2 id="mainLogTitle"></h2><button type="button" class="ghost" id="mainLogClose" aria-label="Close">✕</button></div>
|
|
380
|
+
<p id="mainLogSource" class="main-log-source muted small"></p>
|
|
381
|
+
<pre id="mainLogLines" class="main-log-lines" tabindex="0" aria-label="Main’s build log"></pre>
|
|
382
|
+
<p id="mainLogReason" class="small" role="status" hidden></p>
|
|
383
|
+
<div class="main-log-actions"><button type="button" class="primary" id="mainLogRetry">Retry</button></div>
|
|
384
|
+
</div>
|
|
385
|
+
</dialog>
|
|
386
|
+
<dialog id="holdSheet" class="sheet hold-sheet" aria-labelledby="holdTitle">
|
|
387
|
+
<div class="sheet-body">
|
|
388
|
+
<div class="sheet-head"><h2 id="holdTitle" tabindex="-1"></h2><button type="button" class="ghost" id="holdClose" aria-label="Close">✕</button></div>
|
|
389
|
+
<p id="holdLede" class="hold-lede muted"></p>
|
|
390
|
+
<div id="holdPicker"></div>
|
|
391
|
+
</div>
|
|
392
|
+
</dialog>
|
|
393
|
+
<dialog id="archSheet" class="sheet arch-sheet" aria-labelledby="archTitle">
|
|
394
|
+
<div class="sheet-body">
|
|
395
|
+
<div class="sheet-head"><h2 id="archTitle">Architecture review</h2><button type="button" class="ghost" id="archClose" aria-label="Close sheet">✕</button></div>
|
|
396
|
+
<div id="archBody" class="arch-body"></div>
|
|
397
|
+
</div>
|
|
398
|
+
</dialog>
|
|
318
399
|
<dialog id="linksSheet" class="sheet" aria-labelledby="linksTitle">
|
|
319
400
|
<div class="sheet-body">
|
|
320
401
|
<div class="sheet-head"><h2 id="linksTitle">Hub links</h2><button type="button" class="ghost" id="linksClose" aria-label="Close">✕</button></div>
|
package/board/public/attach.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// docs/adr/2026-11-03-1722-every-text-box-attaches-through-attach-js.md
|
|
2
|
+
// Files attached to a composer: any element with data-composer="<id>" holding BoardUI.attachButton(), its text box in
|
|
3
|
+
// BoardUI.dropZone(), and a [data-files] list. Picking files, dropping them on the composer, or pasting them into it
|
|
4
|
+
// uploads them at once to the api/upload of the site named by the nearest
|
|
3
5
|
// [data-site] around the composer (a board view's root, app.js), else of the page's own site (ClaudeSite). Pending
|
|
4
6
|
// files live in memory by site and composer id, so a page that rebuilds its markup calls paintAll() to show them
|
|
5
7
|
// again, and a board the app shell switches away from keeps its own. site defaults to the page's own below.
|
|
@@ -65,9 +67,43 @@ window.BoardAttach = (function () {
|
|
|
65
67
|
const input = ev.target.closest && ev.target.closest('input[data-attach]');
|
|
66
68
|
const c = input && input.closest('[data-composer]');
|
|
67
69
|
if (!c) return;
|
|
68
|
-
|
|
70
|
+
addFiles(c, input.files);
|
|
69
71
|
input.value = '';
|
|
70
72
|
});
|
|
73
|
+
// A drag of files over a composer marks it .dragging, which outlines its drop zone. dragenter and dragleave fire on
|
|
74
|
+
// each child it crosses, so the composer counts them, and a drop ends the drag.
|
|
75
|
+
const depth = new WeakMap();
|
|
76
|
+
const composerOf = (ev) => (ev.target.closest && ev.target.closest('[data-composer]')) || null;
|
|
77
|
+
const carriesFiles = (ev) => [...(ev.dataTransfer?.types || [])].includes('Files');
|
|
78
|
+
const setDepth = (c, n) => { depth.set(c, n); c.classList.toggle('dragging', n > 0); };
|
|
79
|
+
const addFiles = (c, files) => { for (const f of files) upload(siteOf(c), c.dataset.composer, f); };
|
|
80
|
+
document.addEventListener('dragenter', (ev) => {
|
|
81
|
+
const c = composerOf(ev);
|
|
82
|
+
if (c && carriesFiles(ev)) setDepth(c, (depth.get(c) || 0) + 1);
|
|
83
|
+
});
|
|
84
|
+
document.addEventListener('dragleave', (ev) => {
|
|
85
|
+
const c = composerOf(ev);
|
|
86
|
+
if (c && depth.get(c)) setDepth(c, depth.get(c) - 1);
|
|
87
|
+
});
|
|
88
|
+
// A file drag is taken anywhere on the page, so a drop that misses every composer does nothing in place of the
|
|
89
|
+
// browser opening the file over the board.
|
|
90
|
+
document.addEventListener('dragover', (ev) => { if (carriesFiles(ev)) ev.preventDefault(); });
|
|
91
|
+
document.addEventListener('drop', (ev) => {
|
|
92
|
+
if (!carriesFiles(ev)) return;
|
|
93
|
+
ev.preventDefault();
|
|
94
|
+
const c = composerOf(ev);
|
|
95
|
+
if (!c) return;
|
|
96
|
+
setDepth(c, 0);
|
|
97
|
+
addFiles(c, ev.dataTransfer.files);
|
|
98
|
+
});
|
|
99
|
+
// A paste that carries files attaches them in place of pasting; a paste of text alone pastes as usual.
|
|
100
|
+
document.addEventListener('paste', (ev) => {
|
|
101
|
+
const c = composerOf(ev);
|
|
102
|
+
const files = [...(ev.clipboardData?.files || [])];
|
|
103
|
+
if (!c || !files.length) return;
|
|
104
|
+
ev.preventDefault();
|
|
105
|
+
addFiles(c, files);
|
|
106
|
+
});
|
|
71
107
|
document.addEventListener('click', (ev) => {
|
|
72
108
|
const b = ev.target.closest && ev.target.closest('[data-unattach]');
|
|
73
109
|
const c = b && b.closest('[data-composer]');
|