@lovek4d/kickflip 0.1.0 → 0.3.1
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 +177 -78
- package/README.md +46 -5
- package/SKILL.md +58 -37
- package/bin/doctor.mjs +4 -3
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +87 -32
- package/bin/statusline.mjs +4 -3
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +5 -0
- package/board/bin/agent.mjs +1 -1
- package/board/bin/board.mjs +241 -69
- package/board/bin/builder-stop-hook.mjs +4 -3
- package/board/domain.mjs +5 -3
- package/board/on-host.mjs +1 -0
- package/board/public/app-arch.js +75 -0
- package/board/public/app-asks.js +33 -14
- package/board/public/app-cards.js +242 -49
- package/board/public/app-core.js +39 -5
- package/board/public/app-header.js +24 -6
- package/board/public/app-lanes.js +76 -26
- package/board/public/app-needs.js +1 -0
- package/board/public/app-snapshot.js +45 -11
- package/board/public/app-threads.js +13 -11
- package/board/public/app.html +4 -1
- package/board/public/app.js +131 -20
- package/board/public/dialogs.js +114 -116
- package/board/public/hub-kit.js +19 -2
- package/board/public/hub.css +24 -18
- package/board/public/linear-key.js +141 -0
- package/board/public/router.js +6 -3
- package/board/public/rules-editor.js +1 -1
- package/board/public/shell.js +1 -1
- package/board/public/sidebar.css +31 -11
- package/board/public/sidebar.js +88 -16
- package/board/public/style.css +141 -46
- package/board/public/toasts.js +6 -4
- package/board/public/ui-cards.js +102 -37
- package/board/public/ui-header.js +17 -8
- package/board/public/ui-hub.js +30 -12
- package/board/public/ui-links.js +18 -10
- package/board/public/ui-order.js +18 -0
- package/board/public/ui-page-header.js +4 -2
- package/board/public/ui-questions.js +37 -16
- package/board/public/ui-sidebar.js +49 -11
- package/board/public/ui-snapshot.js +15 -9
- package/board/public/ui-threads.js +25 -16
- package/board/public/ui.js +23 -11
- package/board/public/view-hub-settings.js +46 -30
- package/board/public/view-hub.js +64 -13
- package/board/public/view-notes.js +7 -4
- package/board/public/view-polling.js +3 -3
- package/board/public/view-questions.js +42 -22
- package/board/public/view-settings.js +90 -35
- package/board/review.mjs +1 -0
- package/board/server.mjs +12 -7
- package/board/store.mjs +14 -6
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +14 -13
- package/docs/hub-session.md +3 -3
- 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 +4 -5
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -10
- package/hub/linear.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/pr-report.mjs +1 -0
- 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 +1 -1
- package/hub/trust.mjs +2 -0
- package/hub/try.mjs +4 -3
- package/hub/upgrade.mjs +1 -0
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/colors.css +7 -5
- package/shared/fake-gh.mjs +54 -0
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The board view's lanes and
|
|
1
|
+
// The board view's lanes and the Waves section above them: each lane's header and cards, Done's and Asks' Show more,
|
|
2
|
+
// Done's finished-wave folds, and the wave rows that filter the lanes and start a wave's triage.
|
|
2
3
|
// Needs app-core.js and app-cards.js.
|
|
3
4
|
BoardView.part((B) => {
|
|
4
5
|
const { $, store, state } = B;
|
|
@@ -8,11 +9,24 @@ BoardView.part((B) => {
|
|
|
8
9
|
{ id: 'stale', name: 'Stale', open: true, hideEmpty: true },
|
|
9
10
|
{ id: 'progress', name: 'In Progress', open: true },
|
|
10
11
|
{ id: 'todo', name: 'Todo', open: true },
|
|
11
|
-
{ id: 'done', name: 'Done', open: false },
|
|
12
|
+
{ id: 'done', name: 'Done', open: false, capped: true },
|
|
13
|
+
{ id: 'asks', name: 'Asks', open: false, capped: true },
|
|
12
14
|
];
|
|
15
|
+
// docs/adr/0144-answered-asks-have-their-own-section-below-done.md
|
|
16
|
+
// A capped lane shows STEP cards, and each Show more tap adds STEP more, kept in memory per lane. A card the owner has
|
|
17
|
+
// open stays shown past the cap. Show more says how many are left (docs/adr/0012-lists-never-cut-silently.md).
|
|
18
|
+
const STEP = 10;
|
|
19
|
+
const moreTaps = new Map();
|
|
20
|
+
function cappedRows(laneId, cards) {
|
|
21
|
+
const lastOpen = cards.findLastIndex((it) => state.openItems.has(it.key));
|
|
22
|
+
const shown = Math.max(STEP * (1 + (moreTaps.get(laneId) || 0)), lastOpen + 1);
|
|
23
|
+
const left = cards.length - shown;
|
|
24
|
+
return cards.slice(0, shown).map((it) => B.card(it)).join('')
|
|
25
|
+
+ (left > 0 ? `<button type="button" class="thread-more" data-lane-more="${laneId}">Show more <span class="n">· ${left} older card${left === 1 ? '' : 's'}</span></button>` : '');
|
|
26
|
+
}
|
|
13
27
|
|
|
14
28
|
// A fold's open state lives in storage, not in setHTML's summary-text match, because its row's count changes.
|
|
15
|
-
// Fold ids are
|
|
29
|
+
// Fold ids are wave:<slug> (board/domain.mjs waveFold), so any id maps to its own storage key.
|
|
16
30
|
const FOLDS = new Proxy({}, { get: (_, id) => ({ store: `foldOpen:${String(id)}` }) });
|
|
17
31
|
// A fold's header reads like a lane's: its name, then its count, then an optional note.
|
|
18
32
|
function fold(id, list, name, note = '') {
|
|
@@ -37,14 +51,13 @@ BoardView.part((B) => {
|
|
|
37
51
|
function lanesHtml(d, shown) {
|
|
38
52
|
const laneState = store.get('lanes', {});
|
|
39
53
|
return LANES.map((lane) => {
|
|
54
|
+
// Items arrive in each lane's fixed order (domain.mjs laneOrder).
|
|
40
55
|
const items = shown.filter((i) => i.lane === lane.id);
|
|
41
56
|
if (lane.hideEmpty && !items.length) return '';
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
+ waveFolds(d, items) + fold('older', older, 'Older');
|
|
47
|
-
return `<details class="lane" data-lane="${lane.id}" ${open ? 'open' : ''}>
|
|
57
|
+
const expanded = laneState[lane.id] ?? lane.open;
|
|
58
|
+
const open = items.filter((i) => !i.fold);
|
|
59
|
+
const rows = (lane.capped ? cappedRows(lane.id, open) : open.map((it) => B.card(it)).join('')) + waveFolds(d, items);
|
|
60
|
+
return `<details class="lane" data-lane="${lane.id}" ${expanded ? 'open' : ''}>
|
|
48
61
|
<summary><span class="lane-name">${lane.name}</span><span class="count">${items.length}</span>${lane.id === 'review' ? B.githubHeader(d) : ''}</summary>
|
|
49
62
|
<div class="list">${rows || '<div class="empty">Nothing here.</div>'}
|
|
50
63
|
${lane.id === 'todo' ? BoardUI.planButton(items) : ''}</div>
|
|
@@ -52,21 +65,38 @@ BoardView.part((B) => {
|
|
|
52
65
|
}).join('');
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
// The Waves section: a lane of one row per open wave (domain.mjs waveRowsOf). Tapping a row shows only its cards;
|
|
69
|
+
// Notes and Triage act without filtering. A lineup has them at the row's foot, and a wave file has only Triage, at
|
|
70
|
+
// the end of its name line.
|
|
71
|
+
const triaging = new Set();
|
|
72
|
+
const triageTopic = (r) => `${r.kind}:${r.slug}`;
|
|
73
|
+
function triageLink(r) {
|
|
74
|
+
if (r.triage.running || triaging.has(triageTopic(r))) return '<span class="wv-busy" role="status"><span class="try-spin" aria-hidden="true"></span>Triaging…</span>';
|
|
75
|
+
if (r.triage.questions) return `<span class="muted">Triage: ${B.plural(r.triage.questions, 'open question')}</span>`;
|
|
76
|
+
return `<button type="button" class="wv-link" data-triage="${esc(triageTopic(r))}">Triage</button>`;
|
|
77
|
+
}
|
|
78
|
+
function waveRow(r) {
|
|
79
|
+
const on = r.slug === state.waveFilter;
|
|
80
|
+
const lineup = r.kind === 'lineup';
|
|
81
|
+
const links = lineup ? `<div class="wv-links"><span class="external-link"><a href="notes.html?wave=${encodeURIComponent(r.slug)}">Notes ${BoardUI.LINK_ARROW}</a></span><span class="external-link"><span class="external-separator" aria-hidden="true"> · </span>${triageLink(r)}</span></div>` : '';
|
|
82
|
+
return `<div class="wv-row${on ? ' on' : ''}" data-wave-row="${esc(r.slug)}">
|
|
83
|
+
<div class="wv-id"><button type="button" class="wv-name" aria-pressed="${on}">${BoardUI.WAVE_ICON}<span>${esc(r.title)}</span></button><span class="muted small">${r.merged} of ${r.total} merged</span>${lineup ? '' : `<span class="wv-end">${triageLink(r)}</span>`}</div>
|
|
84
|
+
${r.goal ? `<div class="wv-note"><b>Goal</b> ${esc(r.goal)}</div>` : ''}
|
|
85
|
+
${r.order ? `<div class="wv-note"><b>Order</b> ${r.order.map((o) => esc(o.ref)).join(' → ')}</div>` : ''}
|
|
86
|
+
${links}
|
|
87
|
+
</div>`;
|
|
88
|
+
}
|
|
55
89
|
function renderWaves(d) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
$('#waves').innerHTML = rows.map(({ w, total, merged, drafts, review }) => `<button class="wave ${w.slug === state.waveFilter ? 'on' : ''}" data-wave="${esc(w.slug)}">
|
|
65
|
-
<span class="wave-name">${esc(w.title)}</span>
|
|
66
|
-
<span class="muted small">${merged} of ${total} ${w.internal ? 'done' : 'merged'}${review ? ` · ${review} in review` : ''}${drafts ? ` · ${drafts} draft${drafts === 1 ? '' : 's'}` : ''}</span>
|
|
67
|
-
<span class="bar"><span style="width:${Math.round((merged / total) * 100)}%"></span></span>
|
|
68
|
-
</button>`).join('');
|
|
90
|
+
const rows = d.waveRows || [];
|
|
91
|
+
if (state.waveFilter && !rows.some((r) => r.slug === state.waveFilter)) { state.waveFilter = null; store.set('wave', null); }
|
|
92
|
+
const open = store.get('lanes', {}).waves ?? true;
|
|
93
|
+
const shown = rows.find((r) => r.slug === state.waveFilter);
|
|
94
|
+
BoardUI.setHTML($('#waves'), rows.length ? `<details class="lane" data-lane="waves" ${open ? 'open' : ''}>
|
|
95
|
+
<summary><span class="lane-name">Waves</span><span class="count">${rows.length}</span></summary>
|
|
96
|
+
<div class="list">${rows.map(waveRow).join('')}</div>
|
|
97
|
+
</details>${shown ? `<div class="wv-showing muted small">Showing ${esc(shown.title)} only · <button type="button" class="wv-all">Show all</button></div>` : ''}` : '');
|
|
69
98
|
}
|
|
99
|
+
const showWave = (slug) => { state.waveFilter = slug; store.set('wave', slug); B.render(); };
|
|
70
100
|
|
|
71
101
|
B.on(B.root, 'toggle', (ev) => {
|
|
72
102
|
const el = ev.target;
|
|
@@ -76,16 +106,36 @@ BoardView.part((B) => {
|
|
|
76
106
|
}, true);
|
|
77
107
|
|
|
78
108
|
B.on(B.root, 'click', async (ev) => {
|
|
109
|
+
const more = ev.target.closest('[data-lane-more]');
|
|
110
|
+
if (more) { moreTaps.set(more.dataset.laneMore, (moreTaps.get(more.dataset.laneMore) || 0) + 1); B.render(); return; }
|
|
79
111
|
const pb = ev.target.closest('button[data-plan]');
|
|
80
112
|
if (pb) {
|
|
81
|
-
if (!(await B.confirm('Claude
|
|
113
|
+
if (!(await B.confirm('Claude triages these Todo cards into one or more waves and asks you about them in Needs you. Nothing is implemented until you answer. Start?'))) return;
|
|
82
114
|
pb.disabled = true;
|
|
83
|
-
try { await B.
|
|
115
|
+
try { await B.send('plan-waves', { keys: pb.dataset.plan.split(' ') }); pb.textContent = 'Planning… its questions appear in Needs you'; }
|
|
84
116
|
catch { B.notice('Could not send. Is the host awake?'); pb.disabled = false; }
|
|
85
117
|
return;
|
|
86
118
|
}
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
119
|
+
const tb = ev.target.closest('button[data-triage]');
|
|
120
|
+
if (tb) {
|
|
121
|
+
const topic = tb.dataset.triage;
|
|
122
|
+
const row = state.board.waveRows.find((r) => triageTopic(r) === topic);
|
|
123
|
+
const asks = row.kind === 'lineup'
|
|
124
|
+
? `Claude reads the code for ${row.title}'s unstarted cards and asks you in Needs you about splitting, merging, adding or closing any. Start?`
|
|
125
|
+
: `Claude checks ${row.title}'s unfinished cards, fixes what it can and asks you in Needs you about any that need you. Start?`;
|
|
126
|
+
if (!(await B.confirm(asks))) return;
|
|
127
|
+
triaging.add(topic);
|
|
128
|
+
B.render();
|
|
129
|
+
try { await B.send('wave-triage', { [row.kind]: row.slug }); }
|
|
130
|
+
catch { B.notice('Could not send. Is the host awake?'); }
|
|
131
|
+
finally { triaging.delete(topic); B.render(); }
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const go = ev.target.closest('a[data-wave-show]');
|
|
135
|
+
if (go) { ev.preventDefault(); showWave(go.dataset.waveShow); $('#waves').scrollIntoView({ block: 'start' }); return; }
|
|
136
|
+
if (ev.target.closest('.wv-all')) { showWave(null); return; }
|
|
137
|
+
const row = ev.target.closest('.wv-row');
|
|
138
|
+
if (row && !ev.target.closest('a')) showWave(row.dataset.waveRow !== state.waveFilter ? row.dataset.waveRow : null);
|
|
89
139
|
});
|
|
90
140
|
|
|
91
141
|
Object.assign(B, { FOLDS, lanesHtml, renderWaves });
|
|
@@ -23,6 +23,7 @@ BoardView.part((B) => {
|
|
|
23
23
|
w.innerHTML = needs.length ? `<h2 style="margin:0 0 6px">Needs you · ${needs.length}</h2>
|
|
24
24
|
<ul>${needs.map((line) => (line.prompt ? promptItem(line.prompt, prompts.state(line.prompt))
|
|
25
25
|
: line.key ? `<li><a href="${esc(line.url)}">${esc(line.title)}</a> <span class="muted small">· ${parts(line)}</span></li>`
|
|
26
|
+
: line.waveRow ? `<li><a href="#waves" data-wave-show="${esc(line.waveRow)}">${esc(line.title)}</a> <span class="muted small">· ${parts(line)}</span></li>`
|
|
26
27
|
: `<li><a href="${esc(line.url)}">${esc(line.title)}</a> <span class="muted small">${plural(line.questions.open, 'question')}</span></li>`)).join('')}</ul>` : '';
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The last visit's board on the board view: reading and saving it, holding the controls that send until the fresh
|
|
2
|
-
// board lands,
|
|
2
|
+
// board lands, flashing the lines the fresh board changed (docs/adr/0028-the-board-opens-on-the-last-visit.md), and
|
|
3
|
+
// the sign on a card that changed lanes or was updated (docs/adr/0135-an-updated-card-takes-a-brief-tint.md).
|
|
3
4
|
// Needs app-core.js and ui-snapshot.js; app.js paints it.
|
|
4
5
|
BoardView.part((B) => {
|
|
5
6
|
const { $$, state } = B;
|
|
6
|
-
const { packSnapshot, unpackSnapshot, preloadKeys, liveControls, changedLines, SNAPSHOT_MAX } = BoardUI;
|
|
7
|
+
const { packSnapshot, unpackSnapshot, preloadKeys, liveControls, changedLines, updateSign, SNAPSHOT_MAX } = BoardUI;
|
|
7
8
|
// The hub serves every site from one origin, so each board keeps its own key.
|
|
8
9
|
const KEY = 'boardSnapshot:' + B.site.url('');
|
|
9
10
|
// Every board's snapshots together stay within three at ui-snapshot.js's cap, the boards saved least recently
|
|
@@ -11,11 +12,12 @@ BoardView.part((B) => {
|
|
|
11
12
|
// otherwise fail to save.
|
|
12
13
|
const BUDGET = 3 * SNAPSHOT_MAX;
|
|
13
14
|
const ORDER = 'boardSnapshots';
|
|
15
|
+
const BUILD = document.documentElement.dataset.build;
|
|
14
16
|
|
|
15
17
|
function readSnapshot() {
|
|
16
18
|
let raw = null;
|
|
17
19
|
try { raw = localStorage.getItem(KEY); } catch {}
|
|
18
|
-
return raw ? unpackSnapshot(raw) : null;
|
|
20
|
+
return raw ? unpackSnapshot(raw, BUILD) : null;
|
|
19
21
|
}
|
|
20
22
|
// Makes room for this board's raw snapshot.
|
|
21
23
|
function keepRecent(raw) {
|
|
@@ -38,6 +40,7 @@ BoardView.part((B) => {
|
|
|
38
40
|
saveTimer = null;
|
|
39
41
|
if (state.view !== 'fresh' || !state.board) return;
|
|
40
42
|
const raw = packSnapshot(state.board, state.threadCache, {
|
|
43
|
+
build: BUILD,
|
|
41
44
|
savedAt: state.board.generatedAt || new Date().toISOString(),
|
|
42
45
|
keep: [...state.openItems, ...preloadKeys(state.board.items, [])],
|
|
43
46
|
});
|
|
@@ -66,13 +69,9 @@ BoardView.part((B) => {
|
|
|
66
69
|
}, true);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
// The lines a fresh board can change: each
|
|
72
|
+
// The lines a fresh board can change: each lane count, Needs you line and thread message. A card shows its own sign below.
|
|
70
73
|
function lines() {
|
|
71
74
|
const out = new Map();
|
|
72
|
-
for (const wrap of $$('details.item[data-key] > summary .title-wrap')) {
|
|
73
|
-
const key = wrap.closest('[data-key]').dataset.key;
|
|
74
|
-
[...wrap.children].forEach((el, i) => out.set(`${key}|${i}`, el));
|
|
75
|
-
}
|
|
76
75
|
for (const count of $$('.lane[data-lane] > summary .count')) out.set(`lane:${count.closest('.lane').dataset.lane}`, count);
|
|
77
76
|
$$('#waiting li').forEach((li, i) => out.set(`needs:${i}`, li));
|
|
78
77
|
for (const msgs of $$('[data-thread] > .msgs')) [...msgs.children].forEach((m, i) => out.set(`thread:${msgs.parentElement.dataset.thread}|${i}`, m));
|
|
@@ -86,12 +85,47 @@ BoardView.part((B) => {
|
|
|
86
85
|
const els = lines();
|
|
87
86
|
const after = texts(els);
|
|
88
87
|
for (const id of changedLines(before, after)) {
|
|
89
|
-
|
|
90
|
-
(el.closest('details.item') || el).classList.add('flash');
|
|
88
|
+
els.get(id).classList.add('flash');
|
|
91
89
|
before[id] = after[id];
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
92
|
const endFlash = () => { before = null; };
|
|
95
93
|
|
|
96
|
-
|
|
94
|
+
// Since the page last drew it, a last visit's board included, a card that changed lanes, or a work card that takes
|
|
95
|
+
// the place of a drawn New task (its fromAsk), flashes once; any other card whose updateSign changed takes a brief
|
|
96
|
+
// tint on its title row. Neither shows for a change the owner sent from this page: sentHere(key, send) holds the
|
|
97
|
+
// card's next change unsigned until the first board load started after send settles.
|
|
98
|
+
let drawn = new Map();
|
|
99
|
+
const ownChanges = new Map();
|
|
100
|
+
async function sentHere(key, send) {
|
|
101
|
+
ownChanges.set(key, Infinity);
|
|
102
|
+
try { return await send(); } finally { ownChanges.set(key, B.boardLoad || 0); }
|
|
103
|
+
}
|
|
104
|
+
// Each sign's style.css animation length. A lane repaint resets a card's classes, so a sign still running is put back
|
|
105
|
+
// as far into its animation as it was.
|
|
106
|
+
const SIGNS = { flash: 1600, tint: 2400 };
|
|
107
|
+
const running = new Map();
|
|
108
|
+
function signCards(load = null) {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
for (const it of state.board.items) {
|
|
111
|
+
const was = drawn.get(it.key);
|
|
112
|
+
const moved = was ? was.lane !== it.lane : Boolean(it.fromAsk) && drawn.has(`ask:${it.fromAsk}`);
|
|
113
|
+
const updated = Boolean(was) && was.sign !== updateSign(it);
|
|
114
|
+
const own = ownChanges.get(it.key);
|
|
115
|
+
if (own !== undefined) { if (moved || updated || (load !== null && load > own)) ownChanges.delete(it.key); continue; }
|
|
116
|
+
if ((moved || updated) && state.view === 'fresh') running.set(it.key, { sign: moved ? 'flash' : 'tint', at: now });
|
|
117
|
+
}
|
|
118
|
+
drawn = new Map(state.board.items.map((i) => [i.key, { lane: i.lane, sign: updateSign(i) }]));
|
|
119
|
+
for (const [key, { sign, at }] of running) {
|
|
120
|
+
const card = now - at < SIGNS[sign] && B.root.querySelector(`details.item[data-key="${CSS.escape(key)}"]`);
|
|
121
|
+
if (!card) { running.delete(key); continue; }
|
|
122
|
+
// Re-adding the class after a layout read restarts the animation, from the delay's point.
|
|
123
|
+
card.classList.remove('flash', 'tint');
|
|
124
|
+
card.style.setProperty('--sign-delay', `-${now - at}ms`);
|
|
125
|
+
void card.offsetWidth;
|
|
126
|
+
card.classList.add(sign);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Object.assign(B, { readSnapshot, saveSnapshotSoon, setView, holdForFlash, flashChanged, endFlash, sentHere, signCards });
|
|
97
131
|
});
|
|
@@ -6,11 +6,10 @@ BoardView.part((B) => {
|
|
|
6
6
|
const thread = (key) => `<div class="thread" data-thread="${esc(key)}"><div class="muted small">Loading thread…</div></div>`;
|
|
7
7
|
|
|
8
8
|
// The board, not the thread, decides the Done button, so it follows each board load even when the thread is unchanged.
|
|
9
|
-
const sendingDone = new Set();
|
|
10
9
|
function syncDone(el, key) {
|
|
11
10
|
const send = el.querySelector('form button[type=submit]');
|
|
12
11
|
if (!send) return;
|
|
13
|
-
const html = doneButton(state.board?.items.find((i) => i.key === key)
|
|
12
|
+
const html = doneButton(state.board?.items.find((i) => i.key === key));
|
|
14
13
|
const current = el.querySelector('form [data-confirm="finish"]');
|
|
15
14
|
if (current?.outerHTML === html) return;
|
|
16
15
|
current?.remove();
|
|
@@ -18,10 +17,13 @@ BoardView.part((B) => {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
// Each open card's ui-threads.js threadMessages view. Opening a card marks it seen (app-core.js markSeen) before its
|
|
21
|
-
// thread draws, so its unread messages start from its seen mark as the page loaded or as the card last closed.
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
20
|
+
// thread draws, so its unread messages start from its seen mark as the page loaded or as the card last closed. A
|
|
21
|
+
// board left and opened again in this page keeps them, so its open threads show what they showed.
|
|
22
|
+
const kept = B.place?.threads;
|
|
23
|
+
const seenBefore = new Map(kept?.seenBefore || Object.entries(B.seen));
|
|
24
|
+
const moreTaps = new Map(kept?.moreTaps);
|
|
25
|
+
const firstDrawn = new Map(kept?.firstDrawn);
|
|
26
|
+
B.keep(() => ({ threads: { seenBefore: [...seenBefore], moreTaps: [...moreTaps], firstDrawn: [...firstDrawn] } }));
|
|
25
27
|
const newestTs = (msgs) => msgs.reduce((top, m) => [String(m.ts), m.reply?.at ? String(m.reply.at) : ''].reduce((a, b) => (b > a ? b : a), top), '');
|
|
26
28
|
const viewOf = (key) => ({ seenAt: seenBefore.get(key) || null, more: moreTaps.get(key) || 0, upto: firstDrawn.get(key) || null });
|
|
27
29
|
B.on(B.root, 'toggle', (ev) => {
|
|
@@ -104,7 +106,7 @@ BoardView.part((B) => {
|
|
|
104
106
|
if (!b) return;
|
|
105
107
|
const key = b.closest('form').dataset.reply;
|
|
106
108
|
b.disabled = true;
|
|
107
|
-
try { await B.
|
|
109
|
+
try { await B.sentHere(key, () => B.send('item-message', { key, text: b.dataset.quick })); loadThread(key, { force: true }); }
|
|
108
110
|
catch { toast('Could not send'); }
|
|
109
111
|
b.disabled = false;
|
|
110
112
|
});
|
|
@@ -119,13 +121,13 @@ BoardView.part((B) => {
|
|
|
119
121
|
const n = protoNotice(pending, latest);
|
|
120
122
|
if (!n.bump) { B.notice(n.text); return; }
|
|
121
123
|
if (!(await B.confirm(n.text))) return;
|
|
122
|
-
|
|
124
|
+
await B.bumpAsk(pending);
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
125
127
|
if (!(await B.confirm('Ask Claude for prototypes of this item?'))) return;
|
|
126
128
|
b.disabled = true;
|
|
127
129
|
try {
|
|
128
|
-
await B.
|
|
130
|
+
await B.sentHere(key, () => B.send('prototype-request', { key }));
|
|
129
131
|
b.title = b.ariaLabel = 'Prototypes asked for';
|
|
130
132
|
toast('Asked Claude for prototypes');
|
|
131
133
|
loadThread(key, { force: true });
|
|
@@ -145,7 +147,7 @@ BoardView.part((B) => {
|
|
|
145
147
|
const btn = f.querySelector('button[type=submit]'); btn.disabled = true;
|
|
146
148
|
try {
|
|
147
149
|
await sendDraft('draft:' + key, ta, () => $(`form[data-reply="${CSS.escape(key)}"] textarea`),
|
|
148
|
-
() => B.
|
|
150
|
+
() => B.sentHere(key, () => B.send('item-message', { key, text, ...(files.length ? { files } : {}) })));
|
|
149
151
|
BoardAttach.clear(id, B.site.root);
|
|
150
152
|
btn.disabled = false;
|
|
151
153
|
loadThread(f.dataset.reply, { force: true });
|
|
@@ -158,7 +160,7 @@ BoardView.part((B) => {
|
|
|
158
160
|
});
|
|
159
161
|
|
|
160
162
|
Object.assign(B, {
|
|
161
|
-
thread, renderThread, loadThread,
|
|
163
|
+
thread, renderThread, loadThread,
|
|
162
164
|
repaintThread: (key) => { if (state.threadCache.has(key)) renderThread(key, state.threadCache.get(key)); },
|
|
163
165
|
syncDone: (key) => { const el = $(`[data-thread="${CSS.escape(key)}"]`); if (el) syncDone(el, key); },
|
|
164
166
|
});
|
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,7 @@ 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-order.js"></script>
|
|
44
45
|
<script src="/_hub/app/ui-threads.js"></script>
|
|
45
46
|
<script src="/_hub/app/ui-cards.js"></script>
|
|
46
47
|
<script src="/_hub/app/ui-header.js"></script>
|
|
@@ -58,6 +59,7 @@ window.BoardBoot = (function () {
|
|
|
58
59
|
<script src="/_hub/app/claude.js"></script>
|
|
59
60
|
<script src="/_hub/app/attach.js"></script>
|
|
60
61
|
<script src="/_hub/app/hub-kit.js"></script>
|
|
62
|
+
<script src="/_hub/app/linear-key.js"></script>
|
|
61
63
|
<script src="/_hub/app/sidebar.js"></script>
|
|
62
64
|
<script src="/_hub/app/app.js"></script>
|
|
63
65
|
<script src="/_hub/app/app-core.js"></script>
|
|
@@ -67,6 +69,7 @@ window.BoardBoot = (function () {
|
|
|
67
69
|
<script src="/_hub/app/app-asks.js"></script>
|
|
68
70
|
<script src="/_hub/app/app-needs.js"></script>
|
|
69
71
|
<script src="/_hub/app/app-header.js"></script>
|
|
72
|
+
<script src="/_hub/app/app-arch.js"></script>
|
|
70
73
|
<script src="/_hub/app/app-banner.js"></script>
|
|
71
74
|
<script src="/_hub/app/app-snapshot.js"></script>
|
|
72
75
|
<script src="/_hub/app/view-settings.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.
|
|
@@ -111,7 +115,12 @@ window.BoardView = (function () {
|
|
|
111
115
|
waveFilter: store.get('wave', null),
|
|
112
116
|
};
|
|
113
117
|
const signal = opts.signal ? (fn) => { if (B.alive()) B.own(opts.signal(fn)); } : null;
|
|
114
|
-
|
|
118
|
+
// B.keep(fn): a part adds fn() → {…} to what the board keeps as it unmounts, read before any part's unmount step.
|
|
119
|
+
const keepers = [];
|
|
120
|
+
const keep = (fn) => { if (B.alive()) keepers.push(fn); };
|
|
121
|
+
const place = places.get(B.route) || null;
|
|
122
|
+
places.delete(B.route);
|
|
123
|
+
Object.assign(B, { store, state, signal, live: opts.live || null, pause: opts.pause, place, keep });
|
|
115
124
|
|
|
116
125
|
function mount() {
|
|
117
126
|
if (B.alive()) return;
|
|
@@ -119,7 +128,12 @@ window.BoardView = (function () {
|
|
|
119
128
|
for (const p of parts) p(B);
|
|
120
129
|
start(B);
|
|
121
130
|
}
|
|
122
|
-
|
|
131
|
+
function unmount() {
|
|
132
|
+
if (B.alive()) places.set(B.route, Object.assign({}, ...keepers.map((fn) => fn())));
|
|
133
|
+
keepers.length = 0;
|
|
134
|
+
stop();
|
|
135
|
+
}
|
|
136
|
+
return { B, mount, unmount };
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
// A view other than the board: run(B) adds what it needs through B's recorders when the view mounts.
|
|
@@ -139,17 +153,19 @@ window.BoardView = (function () {
|
|
|
139
153
|
const { setHTML, preloadKeys } = BoardUI;
|
|
140
154
|
|
|
141
155
|
// Renders open threads from state.threadCache; loadBoard and opening a card load them.
|
|
142
|
-
function render() {
|
|
156
|
+
function render(load = null) {
|
|
143
157
|
const d = state.board;
|
|
144
158
|
const title = $(':scope > .top h1');
|
|
145
159
|
if (d.title && title && title.textContent !== d.title) title.textContent = d.title;
|
|
146
160
|
if (d.title && document.title !== d.title) document.title = d.title;
|
|
147
161
|
steady(() => {
|
|
148
162
|
B.renderHeader(d);
|
|
163
|
+
B.renderArch(d);
|
|
149
164
|
B.renderWaves(d);
|
|
150
165
|
BoardUI.setHTML($('#tryMain'), BoardUI.mainBar(d.main));
|
|
166
|
+
B.renderMainLog(d.main, load);
|
|
151
167
|
const live = d.items.filter((i) => !state.withdrawnAsks.has(i.id));
|
|
152
|
-
const shown = state.waveFilter ? live.filter((i) => i.wave === state.waveFilter) : live;
|
|
168
|
+
const shown = state.waveFilter ? live.filter((i) => i.wave === state.waveFilter || i.lineup?.slug === state.waveFilter) : live;
|
|
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.
|
|
@@ -158,7 +174,9 @@ window.BoardView = (function () {
|
|
|
158
174
|
B.renderNeeds(d);
|
|
159
175
|
B.renderBanner(d);
|
|
160
176
|
});
|
|
177
|
+
paintFades();
|
|
161
178
|
B.flashChanged();
|
|
179
|
+
B.signCards(load);
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
// #item=<wave>/<id> opens that card; push notifications link here, with &reply to land in the thread's reply box
|
|
@@ -166,6 +184,8 @@ window.BoardView = (function () {
|
|
|
166
184
|
let focusKey = null;
|
|
167
185
|
let replyKey = null;
|
|
168
186
|
let hashRead = false;
|
|
187
|
+
// A board this page left goes back to where the owner left it once it first renders, unless its URL names a card.
|
|
188
|
+
const returnToPlace = () => B.restorePlace({ linked: /^#item=/.test(location.hash) });
|
|
169
189
|
function readHash() {
|
|
170
190
|
const m = /^#item=([^&]+)(&reply)?$/.exec(location.hash);
|
|
171
191
|
if (!m) return;
|
|
@@ -214,7 +234,7 @@ window.BoardView = (function () {
|
|
|
214
234
|
const snap = B.readSnapshot();
|
|
215
235
|
if (!snap) return false;
|
|
216
236
|
const live = Boolean(B.live) && snap.board?.generatedAt === B.live.generatedAt;
|
|
217
|
-
state.board = snap.board;
|
|
237
|
+
served = state.board = snap.board;
|
|
218
238
|
for (const [key, msgs] of Object.entries(snap.threads)) state.threadCache.set(key, msgs);
|
|
219
239
|
state.fresh = live ? B.live.fresh : { at: snap.savedAt, sig: null };
|
|
220
240
|
B.setView(live ? 'fresh' : 'snapshot');
|
|
@@ -222,6 +242,7 @@ window.BoardView = (function () {
|
|
|
222
242
|
hashRead = true;
|
|
223
243
|
render();
|
|
224
244
|
scrollToFocus(false);
|
|
245
|
+
returnToPlace();
|
|
225
246
|
if (!live) B.holdForFlash();
|
|
226
247
|
return live;
|
|
227
248
|
}
|
|
@@ -235,6 +256,7 @@ window.BoardView = (function () {
|
|
|
235
256
|
if (boot?.route && boot.route !== B.route) boot = null;
|
|
236
257
|
let preloaded = false;
|
|
237
258
|
async function loadBoard({ via = 'poll' } = {}) {
|
|
259
|
+
const load = B.boardLoad = (B.boardLoad || 0) + 1;
|
|
238
260
|
const started = boot;
|
|
239
261
|
boot = null;
|
|
240
262
|
const threads = [...state.openItems].map((k) => B.loadThread(k, { from: started?.threads.get(k) || null, force: via !== 'poll' }));
|
|
@@ -242,15 +264,7 @@ window.BoardView = (function () {
|
|
|
242
264
|
try {
|
|
243
265
|
const board = await (started?.board || fetch(B.site.url('api/board'), { cache: 'no-store' })
|
|
244
266
|
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`board ${r.status}`)))));
|
|
245
|
-
|
|
246
|
-
state.board = board;
|
|
247
|
-
B.setView('fresh');
|
|
248
|
-
if (!hashRead || (first && focusKey)) { readHash(); hashRead = true; }
|
|
249
|
-
B.linkUp();
|
|
250
|
-
render();
|
|
251
|
-
scrollToFocus();
|
|
252
|
-
B.site.syncBadge();
|
|
253
|
-
B.saveSnapshotSoon();
|
|
267
|
+
land(board, load);
|
|
254
268
|
landed = true;
|
|
255
269
|
if (!preloaded) {
|
|
256
270
|
preloaded = true;
|
|
@@ -264,6 +278,79 @@ window.BoardView = (function () {
|
|
|
264
278
|
if (state.view === 'fresh') B.endFlash();
|
|
265
279
|
}
|
|
266
280
|
|
|
281
|
+
// A board from the server, a load's or an events POST's, with the taps still in flight drawn over it. One built
|
|
282
|
+
// before the board on screen leaves it as it is, so a slow load can't undo a newer one. → whether it landed
|
|
283
|
+
let served = null;
|
|
284
|
+
const drawing = new Set();
|
|
285
|
+
const drawn = () => [...drawing].reduce((board, draw) => draw(board), served);
|
|
286
|
+
function land(board, load) {
|
|
287
|
+
if (state.view === 'fresh' && served && board.generatedAt < served.generatedAt) return false;
|
|
288
|
+
const first = state.view !== 'fresh';
|
|
289
|
+
served = board;
|
|
290
|
+
state.board = drawn();
|
|
291
|
+
B.setView('fresh');
|
|
292
|
+
if (!hashRead || (first && focusKey)) { readHash(); hashRead = true; }
|
|
293
|
+
B.linkUp();
|
|
294
|
+
render(load);
|
|
295
|
+
scrollToFocus();
|
|
296
|
+
returnToPlace();
|
|
297
|
+
B.site.syncBadge();
|
|
298
|
+
B.saveSnapshotSoon();
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
function undraw(draw) {
|
|
302
|
+
drawing.delete(draw);
|
|
303
|
+
state.board = drawn();
|
|
304
|
+
render();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// An owner tap (docs/adr/0151-every-owner-tap-the-server-can-apply-draws-at-once.md): the events POST answers with
|
|
308
|
+
// the board as the tap left it, which lands as a load of its own, so the tap needs no reload. draw(board) → board is
|
|
309
|
+
// what the tap changes when its result follows from the tap alone: the page draws it before the POST and keeps it
|
|
310
|
+
// over every board that lands until the POST settles; a failed POST takes it back off and throws.
|
|
311
|
+
async function send(kind, data, draw = null) {
|
|
312
|
+
if (draw) {
|
|
313
|
+
drawing.add(draw);
|
|
314
|
+
state.board = drawn();
|
|
315
|
+
render();
|
|
316
|
+
}
|
|
317
|
+
let out;
|
|
318
|
+
try { out = await B.site.send(kind, data); }
|
|
319
|
+
catch (e) {
|
|
320
|
+
if (draw && B.alive()) undraw(draw);
|
|
321
|
+
throw e;
|
|
322
|
+
}
|
|
323
|
+
drawing.delete(draw);
|
|
324
|
+
// A board newer than the POST's already holds the tap.
|
|
325
|
+
if (B.alive() && !land(out.board, B.boardLoad = (B.boardLoad || 0) + 1) && draw) undraw(draw);
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
// A draw that changes the item with that key, the board's order kept as the server sorts it.
|
|
329
|
+
const drawItem = (key, change) => (board) => ({ ...board, items: board.items.map((i) => (i.key === key ? change({ ...i }) : i)).sort(BoardUI.laneOrder) });
|
|
330
|
+
|
|
331
|
+
// The bump icon's fill fades in as a tap bumps a card and out as one unbumps it, on a toast's timing (toasts.js), and
|
|
332
|
+
// keeps fading through any redraw meanwhile. selector names the icon.
|
|
333
|
+
const fades = new Map();
|
|
334
|
+
function paintFades() {
|
|
335
|
+
for (const [selector, { name, ms }] of fades) {
|
|
336
|
+
const icon = $(selector);
|
|
337
|
+
if (!icon) continue;
|
|
338
|
+
icon.classList.add(name);
|
|
339
|
+
icon.style.setProperty('--bump-fade', `${ms}ms`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function fadeBump(selector, on) {
|
|
343
|
+
const fade = { name: on ? 'bump-in' : 'bump-out', ms: on ? BoardToasts.enterTime() : BoardToasts.exitTime() };
|
|
344
|
+
$(selector)?.classList.remove('bump-in', 'bump-out');
|
|
345
|
+
fades.set(selector, fade);
|
|
346
|
+
paintFades();
|
|
347
|
+
B.later(fade.ms, () => {
|
|
348
|
+
if (fades.get(selector) !== fade) return;
|
|
349
|
+
fades.delete(selector);
|
|
350
|
+
$(selector)?.classList.remove(fade.name);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
267
354
|
// The server signals each change to what the board reads, through the shell, and the board reloads at once; the
|
|
268
355
|
// 30 s poll stays as the fallback and the offline check (docs/adr/0031-the-board-reloads-on-a-change-signal.md).
|
|
269
356
|
let reloading = null;
|
|
@@ -278,7 +365,9 @@ window.BoardView = (function () {
|
|
|
278
365
|
});
|
|
279
366
|
}
|
|
280
367
|
|
|
281
|
-
|
|
368
|
+
// The toast for a tap drawn at once whose POST failed, as the page takes the drawing back off.
|
|
369
|
+
const FAILED = 'That didn’t go through. Is the host awake?';
|
|
370
|
+
Object.assign(B, { render, loadBoard, send, drawItem, fadeBump, reload: signalled, FAILED });
|
|
282
371
|
const live = paintSnapshot();
|
|
283
372
|
B.headerWired();
|
|
284
373
|
if (live) B.linkUp();
|
|
@@ -294,16 +383,16 @@ window.BoardView = (function () {
|
|
|
294
383
|
}
|
|
295
384
|
|
|
296
385
|
function markup({ title = '' } = {}) {
|
|
297
|
-
return `${BoardUI.header({ lead: 'logo', title, logoSrc: '/_hub/kickflip.png' })}
|
|
386
|
+
return `${BoardUI.header({ lead: 'logo', title, logoSrc: '/_hub/kickflip.png', bulb: true })}
|
|
298
387
|
<main>
|
|
299
388
|
<section id="setup" class="setup" aria-live="polite" hidden></section>
|
|
300
389
|
<section id="answers" class="answer-banner" hidden></section>
|
|
301
390
|
<section id="waiting" class="waiting" hidden></section>
|
|
302
|
-
<div id="waves"
|
|
391
|
+
<div id="waves"></div>
|
|
303
392
|
<div id="tryMain"></div>
|
|
304
393
|
<div id="lanes"></div>
|
|
305
394
|
</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="
|
|
395
|
+
<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
396
|
<dialog id="sheet" class="sheet" aria-labelledby="sheetTitle">
|
|
308
397
|
<form id="askForm" data-composer="ask">
|
|
309
398
|
<div class="sheet-head"><h2 id="sheetTitle">New task</h2><button type="button" class="ghost" id="sheetClose" aria-label="Close">✕</button></div>
|
|
@@ -315,6 +404,28 @@ window.BoardView = (function () {
|
|
|
315
404
|
</div>
|
|
316
405
|
</form>
|
|
317
406
|
</dialog>
|
|
407
|
+
<dialog id="mainLogSheet" class="sheet" aria-labelledby="mainLogTitle">
|
|
408
|
+
<div class="sheet-body">
|
|
409
|
+
<div class="sheet-head"><h2 id="mainLogTitle"></h2><button type="button" class="ghost" id="mainLogClose" aria-label="Close">✕</button></div>
|
|
410
|
+
<p id="mainLogSource" class="main-log-source muted small"></p>
|
|
411
|
+
<pre id="mainLogLines" class="main-log-lines" tabindex="0" aria-label="Main’s build log"></pre>
|
|
412
|
+
<p id="mainLogReason" class="small" role="status" hidden></p>
|
|
413
|
+
<div class="main-log-actions"><button type="button" class="primary" id="mainLogRetry">Retry</button></div>
|
|
414
|
+
</div>
|
|
415
|
+
</dialog>
|
|
416
|
+
<dialog id="holdSheet" class="sheet hold-sheet" aria-labelledby="holdTitle">
|
|
417
|
+
<div class="sheet-body">
|
|
418
|
+
<div class="sheet-head"><h2 id="holdTitle" tabindex="-1"></h2><button type="button" class="ghost" id="holdClose" aria-label="Close">✕</button></div>
|
|
419
|
+
<p id="holdLede" class="hold-lede muted"></p>
|
|
420
|
+
<div id="holdPicker"></div>
|
|
421
|
+
</div>
|
|
422
|
+
</dialog>
|
|
423
|
+
<dialog id="archSheet" class="sheet arch-sheet" aria-labelledby="archTitle">
|
|
424
|
+
<div class="sheet-body">
|
|
425
|
+
<div class="sheet-head"><h2 id="archTitle">Architecture review</h2><button type="button" class="ghost" id="archClose" aria-label="Close sheet">✕</button></div>
|
|
426
|
+
<div id="archBody" class="arch-body"></div>
|
|
427
|
+
</div>
|
|
428
|
+
</dialog>
|
|
318
429
|
<dialog id="linksSheet" class="sheet" aria-labelledby="linksTitle">
|
|
319
430
|
<div class="sheet-body">
|
|
320
431
|
<div class="sheet-head"><h2 id="linksTitle">Hub links</h2><button type="button" class="ghost" id="linksClose" aria-label="Close">✕</button></div>
|