@lovek4d/kickflip 0.7.0 → 0.7.2
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 +53 -49
- package/SKILL.md +17 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +3 -3
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +113 -105
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +3 -3
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +6 -7
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -62
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +9 -5
- package/board/public/app.js +3 -3
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +11 -13
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +0 -1
- package/board/public/linear-key.js +2 -2
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +3 -3
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +43 -43
- package/board/public/ui-cards.js +25 -20
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +6 -7
- package/board/public/ui-questions.js +17 -13
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +29 -21
- package/board/public/ui.js +23 -6
- package/board/public/view-hub-settings.js +11 -19
- package/board/public/view-hub.js +25 -11
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +7 -7
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -26
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -1
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/starter/server.mjs +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The reader sheet (docs/adr/2026-11-04-2345-a-builders-log-and-notes-open-in-a-sheet.md): a thread's "View transcript"
|
|
2
|
-
// (ui-threads.js) and a card's or a
|
|
2
|
+
// (ui-threads.js) and a card's or a wave's Notes (app-cards.js, app-lanes.js) open over the board in one sheet in
|
|
3
3
|
// place of log.html and notes.html, which still open on their own. It closes on its ✕, Escape, a tap on the dim area
|
|
4
4
|
// and Back, through a history entry of its own that carries the board's URL, so the router's pop keeps the board.
|
|
5
5
|
// A link in it closes it first, then opens through the shell.
|
|
@@ -21,9 +21,12 @@ BoardView.part((B) => {
|
|
|
21
21
|
let log = null;
|
|
22
22
|
let stopReading = null;
|
|
23
23
|
|
|
24
|
-
// A row of the transcript (transcript.mjs); a call with its output opens it on a tap of its line.
|
|
25
|
-
|
|
24
|
+
// A row of the transcript (transcript.mjs); a call with its output opens it on a tap of its line. What Claude says,
|
|
25
|
+
// its text rows and its result, is markdown in compact styles, linking the board's refs
|
|
26
|
+
// (docs/adr/2026-11-09-1453-orchestrator-messages-replies-and-transcript-text-are-markdown.md).
|
|
27
|
+
function rowHtml(r, i, links) {
|
|
26
28
|
const tag = r.label ? `<span class="reader-tag">${esc(r.label)}</span>` : '';
|
|
29
|
+
if (r.kind.split(' ')[0] === 'text') return `<div class="reader-row ${r.kind} md">${tag}${markdown(r.body, links)}</div>`;
|
|
27
30
|
if (r.out === undefined) return `<div class="reader-row ${r.kind}">${tag}${esc(r.body)}</div>`;
|
|
28
31
|
const on = log.opened.has(i);
|
|
29
32
|
return `<div class="reader-row tool call" data-call="${i}"><span class="reader-caret">${on ? '▾' : '▸'}</span>${tag}${esc(r.body)}</div>${on ? `<div class="reader-out">${esc(r.out)}</div>` : ''}`;
|
|
@@ -42,10 +45,11 @@ BoardView.part((B) => {
|
|
|
42
45
|
}
|
|
43
46
|
function drawLog() {
|
|
44
47
|
const { rows, stream, raw, text } = log;
|
|
48
|
+
const links = linkContext(state.board?.items || [], null);
|
|
45
49
|
body.innerHTML = !rows.length ? '<p class="muted reader-empty">The log is empty so far.</p>'
|
|
46
50
|
: !stream ? `<pre class="reader-text">${esc(rows.map((r) => `${r.body}\n`).join(''))}</pre>`
|
|
47
51
|
: raw ? (text === null ? '<p class="muted">Loading…</p>' : `<pre class="reader-raw">${esc(text)}</pre>`)
|
|
48
|
-
: rows.map(rowHtml).join('');
|
|
52
|
+
: rows.map((r, i) => rowHtml(r, i, links)).join('');
|
|
49
53
|
drawTools();
|
|
50
54
|
drawMore();
|
|
51
55
|
}
|
|
@@ -148,7 +152,7 @@ BoardView.part((B) => {
|
|
|
148
152
|
else openNotes(href.slice(href.indexOf('?')));
|
|
149
153
|
});
|
|
150
154
|
B.on(sheet, 'click', (ev) => {
|
|
151
|
-
if (ev
|
|
155
|
+
if (BoardUI.onBackdrop(ev, sheet)) { close(); return; }
|
|
152
156
|
const act = ev.target.closest('[data-reader]')?.dataset.reader;
|
|
153
157
|
if (act === 'close') close();
|
|
154
158
|
else if (act === 'raw') { log.raw = !log.raw; log.text = null; drawLog(); follow(); if (log.raw) readRaw(opened); }
|
package/board/public/app.js
CHANGED
|
@@ -128,7 +128,7 @@ window.BoardView = (function () {
|
|
|
128
128
|
board: null,
|
|
129
129
|
fresh: null,
|
|
130
130
|
openItems: new Set(store.get('openItems', [])),
|
|
131
|
-
// The cards open when the page loaded: Done
|
|
131
|
+
// The cards open when the page loaded: Done hides those past their cap until the owner opens one this visit.
|
|
132
132
|
restored: new Set(store.get('openItems', [])),
|
|
133
133
|
threadCache: new Map(),
|
|
134
134
|
edits: new Map(),
|
|
@@ -206,7 +206,7 @@ window.BoardView = (function () {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// #review=<PR number> opens that review in the Auto Review lane (app-lanes.js openReview), as a held review's push does.
|
|
209
|
-
// #selectedCard=<
|
|
209
|
+
// #selectedCard=<file>/<id> (or the older #item=) opens that card, and &openItemsCard=<topic id> opens that pill in the pane; push notifications link here, with &reply to land in the thread's reply box
|
|
210
210
|
// (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.
|
|
211
211
|
let focusKey = null;
|
|
212
212
|
let replyKey = null;
|
|
@@ -251,7 +251,7 @@ window.BoardView = (function () {
|
|
|
251
251
|
if (!band && !state.board?.items.some((i) => i.key === key)) return false;
|
|
252
252
|
if (key) { location.hash = AppRouter.hashWith(location.hash, { selectedCard: key }); readHash(); } else B.openBand({ slug });
|
|
253
253
|
render();
|
|
254
|
-
if (key) scrollToFocus(); else $(`section.wb[data-band="${CSS.escape(slug)}"]`)?.scrollIntoView({ block: 'start' });
|
|
254
|
+
if (key) scrollToFocus(); else $(`section.wb[data-lane="todo"][data-band="${CSS.escape(slug)}"]`)?.scrollIntoView({ block: 'start' });
|
|
255
255
|
return true;
|
|
256
256
|
}
|
|
257
257
|
// A card missing from the last visit's board waits for the fresh one, which is final.
|
package/board/public/attach.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// painted also gets a bubbling attach-change event with detail {id}
|
|
13
13
|
window.BoardAttach = (function () {
|
|
14
14
|
const LIMIT = 20 * 1024 * 1024;
|
|
15
|
+
const OVER = 'Over 20 MB';
|
|
15
16
|
const state = new Map();
|
|
16
17
|
const listeners = [];
|
|
17
18
|
let seq = 0;
|
|
@@ -32,10 +33,17 @@ window.BoardAttach = (function () {
|
|
|
32
33
|
const paintAll = () => { for (const c of document.querySelectorAll('[data-composer]')) paint(c.dataset.composer, siteOf(c)); };
|
|
33
34
|
|
|
34
35
|
function upload(site, id, file) {
|
|
35
|
-
const p = { id: 'f' + ++seq, name: file.name || 'file', type: file.type || 'application/octet-stream', status: 'uploading', progress: 0 };
|
|
36
|
+
const p = { id: 'f' + ++seq, name: file.name || 'file', type: file.type || 'application/octet-stream', status: 'uploading', progress: 0, source: file };
|
|
36
37
|
if (/^image\//.test(p.type)) p.preview = URL.createObjectURL(file);
|
|
37
38
|
state.set(keyOf(site, id), [...list(site, id), p]);
|
|
38
39
|
if (file.size > LIMIT) { p.status = 'error'; p.error = 'Over 20 MB'; paint(id, site); return; }
|
|
40
|
+
send(site, id, p);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Sends p's File and paints it uploading; a retry from an error chip runs it again.
|
|
44
|
+
function send(site, id, p) {
|
|
45
|
+
const file = p.source;
|
|
46
|
+
p.status = 'uploading'; p.progress = 0; delete p.error;
|
|
39
47
|
paint(id, site);
|
|
40
48
|
const xhr = new XMLHttpRequest();
|
|
41
49
|
p.xhr = xhr;
|
|
@@ -54,6 +62,11 @@ window.BoardAttach = (function () {
|
|
|
54
62
|
xhr.send(file);
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
function retry(site, id, fileId) {
|
|
66
|
+
const p = list(site, id).find((x) => x.id === fileId);
|
|
67
|
+
if (p && p.status === 'error' && p.error !== OVER) send(site, id, p);
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function remove(site, id, fileId) {
|
|
58
71
|
const p = list(site, id).find((x) => x.id === fileId);
|
|
59
72
|
if (!p) return;
|
|
@@ -107,7 +120,10 @@ window.BoardAttach = (function () {
|
|
|
107
120
|
document.addEventListener('click', (ev) => {
|
|
108
121
|
const b = ev.target.closest && ev.target.closest('[data-unattach]');
|
|
109
122
|
const c = b && b.closest('[data-composer]');
|
|
110
|
-
if (c) { ev.preventDefault(); remove(siteOf(c), c.dataset.composer, b.dataset.unattach); }
|
|
123
|
+
if (c) { ev.preventDefault(); remove(siteOf(c), c.dataset.composer, b.dataset.unattach); return; }
|
|
124
|
+
const r = ev.target.closest && ev.target.closest('[data-retry]');
|
|
125
|
+
const rc = r && r.closest('[data-composer]');
|
|
126
|
+
if (rc) { ev.preventDefault(); retry(siteOf(rc), rc.dataset.composer, r.dataset.retry); }
|
|
111
127
|
});
|
|
112
128
|
|
|
113
129
|
return {
|
package/board/public/dialogs.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// remote, it can't be picked and says why (docs/adr/2026-11-02-1850-a-board-can-work-on-any-git-remote.md).
|
|
11
11
|
// With keyState(), Hub's /_hub/api/settings `linear`, as Board Settings passes it, the source row reads its live state
|
|
12
12
|
// as a status word (docs/adr/2026-11-04-0227-every-new-card-gets-a-ticket-in-the-boards-current-source.md).
|
|
13
|
-
function ticketsControl(root, { value = { source: '
|
|
13
|
+
function ticketsControl(root, { value = { source: 'none' }, repo = () => '', title = () => '', linear = null, loadLinear, isNew = false, change = () => {}, createProject, keyState } = {}) {
|
|
14
14
|
const { esc } = BoardUI;
|
|
15
|
-
const draft = {
|
|
15
|
+
const draft = { ...value };
|
|
16
16
|
let creating = false, createError = '', destroyed = false, asking = false, noGithub = false, key = null;
|
|
17
17
|
const works = () => Boolean(linear?.set && !linear.refused);
|
|
18
18
|
const keyWorks = () => Boolean(key?.set && !key.refused);
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
const workspace = () => linear?.url_key ?? linear?.workspace ?? draft.workspace;
|
|
32
32
|
const newName = () => (draft.newName ?? title()).trim();
|
|
33
33
|
const ready = () => works() && !linear.error;
|
|
34
|
-
const read = () => draft.source === '
|
|
34
|
+
const read = () => draft.source === 'none' ? { source: 'none' }
|
|
35
35
|
: draft.source === 'github' ? { source: 'github', ...(draft.account ? { account: draft.account } : {}) }
|
|
36
36
|
: !ready() ? { source: 'linear', workspace: draft.workspace, team: draft.team, project: draft.project }
|
|
37
37
|
: draft.project === '__new' && isNew ? { source: 'linear', workspace: workspace(), team: draft.team, new_project: newName() }
|
|
38
38
|
: { source: 'linear', workspace: workspace(), team: draft.team, project: draft.project };
|
|
39
|
-
const valid = () => draft.source === '
|
|
39
|
+
const valid = () => draft.source === 'none' ? true
|
|
40
40
|
: draft.source === 'github' ? Boolean(repo())
|
|
41
41
|
: Boolean(ready() && draft.team && draft.project && (draft.project !== '__new' || (isNew && newName())));
|
|
42
42
|
const pickDefaults = () => {
|
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
const team = esc(draft.team || '');
|
|
59
59
|
const note = draft.project === '__new'
|
|
60
60
|
? (isNew ? `<p class="muted small">Start creates the project in the ${team} team on Linear.</p>` : `<p class="muted small">Creates the project in the ${team} team on Linear.</p>`)
|
|
61
|
-
: `<p class="muted small">A
|
|
61
|
+
: `<p class="muted small">A task gets a ${team} issue in ${esc(project?.name || draft.project || '')}, in the ${esc(linear.workspace || workspace())} workspace from Hub’s key, when you answer yes to its ticket question. Its badge opens it in Linear.</p>`;
|
|
62
62
|
return `<div class="tk-grid"><label class="tk-field"><span>Team</span><select data-tk="team">${(linear.teams || []).map((t) => `<option value="${esc(t.key)}"${t.key === draft.team ? ' selected' : ''}>${esc(t.key)} · ${esc(t.name)}</option>`).join('')}</select></label><label class="tk-field"><span>Project</span><select data-tk="project">${(linear.projects || []).filter((p) => p.team === draft.team).map((p) => `<option value="${esc(p.id)}"${p.id === draft.project ? ' selected' : ''}>${esc(p.name)}</option>`).join('')}<option value="__new"${draft.project === '__new' ? ' selected' : ''}>New project…</option></select></label></div>${draft.project === '__new' ? `<div class="tk-new"><input data-tk="newName" value="${esc(draft.newName ?? title())}" aria-label="New Linear project name" placeholder="Project name" maxlength="80" autocomplete="off">${isNew ? '' : `<button type="button" data-tk-create${creating ? BoardUI.BUSY : ''}>Create</button>`}</div>` : ''}${createError ? `<p class="muted small" role="alert">${esc(createError)}</p>` : ''}${note}`;
|
|
63
63
|
}
|
|
64
64
|
function render() {
|
|
65
65
|
const off = !repo();
|
|
66
|
-
const segment = `<div class="tk-seg" role="group" aria-label="Ticket source">${[['linear', 'Linear'], ['github', 'GitHub issues'], ['
|
|
66
|
+
const segment = `<div class="tk-seg" role="group" aria-label="Ticket source">${[['linear', 'Linear'], ['github', 'GitHub issues'], ['none', 'None']].map(([source, name]) => `<button type="button" data-tk-source="${source}" aria-pressed="${draft.source === source}"${source === 'github' && off ? ' aria-disabled="true"' : ''}>${name}</button>`).join('')}</div>`;
|
|
67
67
|
const why = `<p class="muted small" role="status">GitHub issues need a repo on GitHub, and ${isNew ? 'the repo chosen above' : 'this board’s repo'} isn’t on GitHub.</p>`;
|
|
68
68
|
let detail;
|
|
69
69
|
if (draft.source === 'github' && off) detail = why;
|
|
70
|
-
else if (draft.source === '
|
|
71
|
-
else if (draft.source === 'github') detail = `<div class="who tk-static"><span>Issues in ${esc(repo())}<span class="muted small sub">${isNew ? 'The repo chosen above' : 'The board’s repo, set above'}. A
|
|
70
|
+
else if (draft.source === 'none') detail = '<p class="muted small">Every card is a task, numbered T-1, T-2… on this board, with no ticket. Its badge reads Task.</p>';
|
|
71
|
+
else if (draft.source === 'github') detail = `<div class="who tk-static"><span>Issues in ${esc(repo())}<span class="muted small sub">${isNew ? 'The repo chosen above' : 'The board’s repo, set above'}. A task opens an issue there${draft.account ? ` as gh account ${esc(draft.account)}` : ''} when you answer yes to its ticket question; its badge then reads #41.</span></span></div>`;
|
|
72
72
|
else detail = linearDetail();
|
|
73
73
|
root.innerHTML = `${status()}${segment}<div class="tk-detail">${noGithub && off && draft.source !== 'github' ? why : ''}${detail}</div>`;
|
|
74
74
|
if (draft.source === 'linear' && ready()) {
|
|
@@ -126,11 +126,9 @@
|
|
|
126
126
|
const input = (e) => {
|
|
127
127
|
const field = e.target.dataset.tk;
|
|
128
128
|
if (!field || (e.type === 'input' && e.target.tagName === 'SELECT')) return;
|
|
129
|
-
draft[field] =
|
|
130
|
-
if (field === 'prefix') e.target.value = draft.prefix;
|
|
129
|
+
draft[field] = e.target.value;
|
|
131
130
|
if (field === 'team') draft.project = isNew ? '__new' : linear.projects?.find((p) => p.team === draft.team)?.id;
|
|
132
131
|
if (field === 'team' || field === 'project') { createError = ''; render(); }
|
|
133
|
-
if (field === 'prefix') root.querySelector('[data-tk-example]').textContent = `Cards number ${draft.prefix || 'INT'}-1, ${draft.prefix || 'INT'}-2… on this board. The badge opens the card.`;
|
|
134
132
|
change(read(), valid(), { typing: e.type === 'input' });
|
|
135
133
|
};
|
|
136
134
|
root.addEventListener('click', click);
|
|
@@ -190,7 +188,7 @@
|
|
|
190
188
|
el.setAttribute('class', 'sheet');
|
|
191
189
|
el.setAttribute('aria-labelledby', 'newBoardTitle');
|
|
192
190
|
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
193
|
-
const draft = { name: '', purpose: '', github: '', remote: '', branch: '', tickets: { source: '
|
|
191
|
+
const draft = { name: '', purpose: '', github: '', remote: '', branch: '', tickets: { source: 'none' } };
|
|
194
192
|
let tickets;
|
|
195
193
|
let repos = [], signedIn = false, pasted, selected, edited = false, sending = false, closed = false, generation = 0, lookupTimer, linear;
|
|
196
194
|
const controller = new AbortController();
|
|
@@ -348,7 +346,7 @@
|
|
|
348
346
|
} catch (e) { sending = false; fail(e); sync(); }
|
|
349
347
|
});
|
|
350
348
|
el.addEventListener('cancel', (e) => { e.preventDefault(); done(); });
|
|
351
|
-
el.addEventListener('click', (e) => { if (e
|
|
349
|
+
el.addEventListener('click', (e) => { if (BoardUI.onBackdrop(e, el)) done(); });
|
|
352
350
|
el.addEventListener('close', () => {
|
|
353
351
|
if (closed) return;
|
|
354
352
|
closed = true;
|
package/board/public/hub-kit.js
CHANGED
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
if (reset) input.value = input.defaultValue = localInput(new Date(now + 3_600_000));
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
// The one duration picker Snooze,
|
|
184
|
+
// The one duration picker Snooze, Pause hub and Pause board share (hub/duration.mjs): chips that post
|
|
185
185
|
// `name` to `action` at once, then Custom, whose date and time posts as `name` too. While its setting is on, `on`
|
|
186
186
|
// replaces it with when it ends and a button that posts `name`=off.
|
|
187
187
|
function durationPicker({ id, title, icon, action, name, choices, on, button }) {
|
package/board/public/hub.css
CHANGED
|
@@ -76,7 +76,6 @@
|
|
|
76
76
|
.hub-view .dur-custom input::-webkit-calendar-picker-indicator { position:absolute; inset:0; width:auto; height:auto; opacity:0; cursor:pointer; }
|
|
77
77
|
.hub-view .dur-custom:focus-within { outline:2px solid var(--accent); outline-offset:2px; border-radius:7px; }
|
|
78
78
|
.hub-view .dur-on .title { font-weight:600; white-space:normal; }
|
|
79
|
-
.hub-view .awake-on { color:var(--color-notice); font-weight:600; }
|
|
80
79
|
.hub-view code { font-family:var(--mono); font-size:.85em; white-space:nowrap; }
|
|
81
80
|
.hub-view footer { color:var(--muted); font-size:12px; margin:1.8rem 2px 0; }
|
|
82
81
|
.hub-view .host { font-family:var(--mono); font-size:11px; overflow-wrap:anywhere; }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Hub's token slots: Linear's key and Slack's user token. Hub settings
|
|
1
|
+
// Hub's token slots: Linear's key and Slack's user token. The Hub settings modals for Linear and Slack mount them, and the Tickets
|
|
2
2
|
// control (dialogs.js) opens Linear's over Board settings or New board when Linear is tapped without a working key
|
|
3
3
|
// (docs/adr/2026-11-02-1712-a-board-picks-linear-only-while-hubs-key-works.md). A token saves on paste, change or Enter
|
|
4
4
|
// through POST /_hub/settings/<kind>, which checks it with the service; state is hub/views.mjs settingsView's `<kind>`.
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
}
|
|
137
137
|
dialog.addEventListener('click', (e) => {
|
|
138
138
|
if (e.target.closest?.('[data-lk-done]')) { close(); return; }
|
|
139
|
-
if (e
|
|
139
|
+
if (!BoardUI.onBackdrop(e, dialog)) return;
|
|
140
140
|
const r = dialog.getBoundingClientRect();
|
|
141
141
|
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) close();
|
|
142
142
|
});
|
package/board/public/new-task.js
CHANGED
|
@@ -75,7 +75,7 @@ globalThis.BoardNewTask = (function () {
|
|
|
75
75
|
B.newTaskKey = newTaskKey;
|
|
76
76
|
B.on(document, 'keydown', newTaskKey);
|
|
77
77
|
B.on($('#sheetClose'), 'click', closeSheet);
|
|
78
|
-
B.on(sheet, 'click', (ev) => { if (ev
|
|
78
|
+
B.on(sheet, 'click', (ev) => { if (BoardUI.onBackdrop(ev, sheet)) closeSheet(); });
|
|
79
79
|
B.on(sheet, 'cancel', (ev) => { ev.preventDefault(); closeSheet(); });
|
|
80
80
|
// Discuss has the session grill or prototype the task at once; Implement sends the ask with quick: true, and the
|
|
81
81
|
// session marks the card ready (docs/adr/2026-11-04-0131-every-new-task-is-grilled-or-prototyped-at-once.md).
|
|
@@ -87,7 +87,6 @@
|
|
|
87
87
|
usage: fake.usage(),
|
|
88
88
|
hub: { tone: 'ok', word: 'healthy', detail: '3 of 3 sites up' },
|
|
89
89
|
sites: fake.sites(),
|
|
90
|
-
polling: { title: 'Checks every minute while you’re here', lines: ['Next check 12:00pm · last 11:45am, 8 PRs, no change', 'No pushes queued.'], count: 0 },
|
|
91
90
|
settings: [['notifications', 'Notifications', 'Pushes on · Off on weekends · This device on'], ['quiet', 'Quiet hours', '10:00pm – 8:00am · pushes held'],
|
|
92
91
|
['codex', 'Codex share', 'Auto'], ['rules', 'Rules for every board', '1 rule'], ['closed', 'Closed boards', 'No closed boards']],
|
|
93
92
|
}),
|
|
@@ -105,7 +104,7 @@
|
|
|
105
104
|
|
|
106
105
|
function hub(data = {}) {
|
|
107
106
|
const d = { ...fake.hub(), header: true, ...data };
|
|
108
|
-
const { row, tile,
|
|
107
|
+
const { row, tile, CHEVRON } = HubUI;
|
|
109
108
|
const site = (s) => row({ icon: s.icon, title: esc(s.title), sub: s.status ?? '<span class="st ok">up · listening · every link passes</span>', wrap: true, trail: CHEVRON, href: `/${s.route}/` });
|
|
110
109
|
const settings = d.settings.map(([id, title, sub]) => `<button type="button" class="hub-setting" data-setting="${esc(id)}" data-kit-inert aria-haspopup="dialog"><span class="text"><span class="title">${esc(title)}</span><span class="sub">${esc(sub)}</span></span>${CHEVRON}</button>`).join('');
|
|
111
110
|
const usage = d.usageCard ?? HubUI.usageCard(d.usage);
|
|
@@ -114,9 +113,7 @@
|
|
|
114
113
|
<div class="group">${row({ icon: tile('power', { ok: '#22c55e', wait: '#f59e0b' }[d.hub.tone] || '#ef4444'), title: 'Hub', sub: `<span class="st ${esc(d.hub.tone)}">${esc(d.hub.word)}</span> · ${esc(d.hub.detail)}`, wrap: true })}</div>
|
|
115
114
|
<h2 class="label">Boards</h2>
|
|
116
115
|
<div class="group">${d.sites.map(site).join('')}</div>
|
|
117
|
-
<p class="note">The hub runs every site and restarts one that stops. A board's links are what the hub owes it: GitHub polling, events (its session and a watcher run, or the hub can wake it), daily wakes, pushes and reviewer links. One that just broke reads settling first.</p>
|
|
118
|
-
<h2 class="label">Polling status</h2>
|
|
119
|
-
<div class="group">${nowRow(d.polling, tile('clock', '#3b82f6'))}</div></div>
|
|
116
|
+
<p class="note">The hub runs every site and restarts one that stops. A board's links are what the hub owes it: GitHub polling, events (its session and a watcher run, or the hub can wake it), daily wakes, pushes and reviewer links. One that just broke reads settling first.</p></div>
|
|
120
117
|
<section class="hub-settings"><h2 class="label">Hub settings</h2>${settings}</section>
|
|
121
118
|
<div class="hub-foot"><footer>${d.sites.length} sites. Each belongs to one Claude session, which adds it with <code>/kickflip <route></code>. This page refreshes every 30 seconds.<br><span class="host">mac.example.ts.net</span><br><span class="brand"><a href="https://kickflip.sh">kickflip.sh</a> / v0.4.2</span></footer></div></main></div>`;
|
|
122
119
|
}
|
package/board/public/router.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// board {route, hash} /<route>/, hash such as #selectedCard=<key>&openItemsCard=<topic id>, or the older #item=<key>&reply
|
|
7
7
|
// settings {route} /<route>/settings.html, a request to open the modal over that board
|
|
8
8
|
// questions {route, id} /<route>/questions.html#<id>
|
|
9
|
-
// notes {route, item | wave} /<route>/notes.html?item=<key> or ?wave=<
|
|
9
|
+
// notes {route, item | wave} /<route>/notes.html?item=<key> or ?wave=<wave slug>, and plan.html, which the board
|
|
10
10
|
// redirects there
|
|
11
11
|
// hub {hash} /_hub/ with a settings modal named by hash; legacy /_hub/settings opens the same modals
|
|
12
12
|
// ([hub settings open as modals](docs/adr/2026-11-01-1221-hub-settings-open-as-modals.md)),
|
package/board/public/sidebar.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
@media (hover: hover) { .sidebar .sb-close:hover { background: var(--chip); color: var(--text); } }
|
|
22
22
|
.sidebar .sb-label { margin: 12px 8px 4px; font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
23
23
|
.sidebar .sb-group { display: flex; flex-direction: column; gap: 2px; }
|
|
24
|
-
.sidebar .sb-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: max(12px, env(safe-area-inset-bottom)); display: flex; flex-direction: column; }
|
|
24
|
+
.sidebar .sb-scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; overflow-wrap: anywhere; overscroll-behavior: contain; padding-bottom: max(12px, env(safe-area-inset-bottom)); display: flex; flex-direction: column; }
|
|
25
25
|
.sidebar .sb-scroll > section { flex: none; }
|
|
26
26
|
.sidebar .sb-utility { padding-top: 6px; }
|
|
27
27
|
.sidebar .sb-boards { margin-top: auto; }
|
|
@@ -150,11 +150,11 @@ html.sb-hub-down body.has-sidebar > .top .hub::after, html.sb-hub-down body.has-
|
|
|
150
150
|
@starting-style { .sidebar.ks-on .ks-label-hint { opacity: 0; } }
|
|
151
151
|
/* The label is the bar's containing block: left 0 is the BOARDS text's edge, and right -8px crosses the label's 8px
|
|
152
152
|
side margin (.sb-label) to the rows' right edge. The bar sits in the label's 4px bottom margin. */
|
|
153
|
-
.sidebar .ks-bar { position: absolute; left: 0; right: -8px; top: 100%; height: 2px; margin-top: 1px; background: var(--
|
|
153
|
+
.sidebar .ks-bar { position: absolute; left: 0; right: -8px; top: 100%; height: 2px; margin-top: 1px; background: var(--color-notice);
|
|
154
154
|
transform-origin: 0 50%; animation: ks-drain linear forwards; }
|
|
155
155
|
@keyframes ks-drain { to { transform: scaleX(0); } }
|
|
156
156
|
.sidebar .ks-u { transition: color 200ms linear; }
|
|
157
|
-
.sidebar.ks-on .ks-u { color: var(--
|
|
157
|
+
.sidebar.ks-on .ks-u { color: var(--color-notice); transition: color 150ms linear; }
|
|
158
158
|
@starting-style { .sidebar.ks-on .ks-u { color: inherit; } }
|
|
159
159
|
@media (prefers-reduced-motion: reduce) {
|
|
160
160
|
.sidebar .ks-bar { animation: none; }
|
package/board/public/sidebar.js
CHANGED
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
}
|
|
237
237
|
el.addEventListener('click', (e) => {
|
|
238
238
|
if (sending) return;
|
|
239
|
-
if (e
|
|
239
|
+
if (BoardUI.onBackdrop(e, el) || e.target.closest('[data-failed-close]')) { done(); return; }
|
|
240
240
|
const dismiss = e.target.closest('[data-failed-dismiss]');
|
|
241
241
|
const retry = e.target.closest('[data-failed-retry]');
|
|
242
242
|
if (dismiss) {
|
|
@@ -309,7 +309,7 @@
|
|
|
309
309
|
el.innerHTML = shortcuts();
|
|
310
310
|
el.addEventListener('cancel', (e) => { e.preventDefault(); closeHelp(); });
|
|
311
311
|
el.addEventListener('close', closeHelp);
|
|
312
|
-
el.addEventListener('click', (e) => { if (e
|
|
312
|
+
el.addEventListener('click', (e) => { if (BoardUI.onBackdrop(e, el)) closeHelp(); });
|
|
313
313
|
help = { el, previous: document.activeElement };
|
|
314
314
|
document.body.appendChild(el);
|
|
315
315
|
el.showModal();
|
package/board/public/style.css
CHANGED
|
@@ -93,8 +93,10 @@ button.chip { border: 0; }
|
|
|
93
93
|
/* The footer status of a stale card, and of one the orchestrator stalled (ui-cards.js timeLine). */
|
|
94
94
|
.card-stale { color: var(--muted); }
|
|
95
95
|
.card-stalled { color: var(--color-danger); }
|
|
96
|
-
/* A bumped
|
|
97
|
-
|
|
96
|
+
/* A bumped ready card's footer status while the host holds new work, and a paused build's (ui-cards.js timeLine);
|
|
97
|
+
a build still pausing reads orange. */
|
|
98
|
+
.card-host-held, .card-paused { color: var(--muted); }
|
|
99
|
+
.card-pausing { color: var(--color-notice); }
|
|
98
100
|
.sub.next.idle { color: var(--muted); }
|
|
99
101
|
.sub.refused { color: var(--color-danger); }
|
|
100
102
|
.toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 12px + env(safe-area-inset-bottom));
|
|
@@ -139,7 +141,7 @@ button.chip { border: 0; }
|
|
|
139
141
|
.thread-more:active { background: var(--accent-soft); }
|
|
140
142
|
.thread-more:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
141
143
|
.thread-more .n { color: var(--muted); font-weight: 400; }
|
|
142
|
-
/* [answered asks
|
|
144
|
+
/* [answered asks fold into the one Done list](docs/adr/2026-11-09-1430-answered-asks-fold-into-the-one-done-list.md): Done's Show more, the thread's look as a row of the lane's list */
|
|
143
145
|
.list > .thread-more { width: 100%; margin: 0; border-top: 1px solid var(--line); border-radius: 0; }
|
|
144
146
|
|
|
145
147
|
@media (max-width: 560px) {
|
|
@@ -155,7 +157,7 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
155
157
|
.wv-link:disabled { color: var(--muted); cursor: default; }
|
|
156
158
|
.wv-busy { display: inline-flex; align-items: center; gap: 6px; color: var(--color-notice); }
|
|
157
159
|
/* docs/adr/2026-11-03-1840-waves-are-bands-in-todo.md: each wave is a lane-style band inside Todo's list. */
|
|
158
|
-
.wb + .wb
|
|
160
|
+
.wb + .wb { border-top: 1px solid var(--line); }
|
|
159
161
|
.wb-head { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-height: 34px; padding: 5px 12px;
|
|
160
162
|
background: var(--band); font-size: 13px; font-weight: 500; cursor: pointer; user-select: none; }
|
|
161
163
|
.wb-head:hover { background: color-mix(in srgb, var(--band), var(--text) 5%); }
|
|
@@ -173,7 +175,6 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
173
175
|
background: var(--panel); box-shadow: 0 6px 18px rgba(0,0,0,.18); color: var(--text); font-weight: 400; line-height: 1.35; overflow-wrap: anywhere; cursor: default; }
|
|
174
176
|
.wb-done { padding: 8px 12px; font-size: 12.5px; color: var(--muted); }
|
|
175
177
|
.wb-done a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
176
|
-
.wb-loosehead { cursor: default; }
|
|
177
178
|
.wb-counts { position: relative; border: 0; background: none; padding: 4px 0; font: inherit; cursor: help; color: var(--muted); font-weight: 400; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
178
179
|
.wb-tip { display: none; position: absolute; right: 0; top: 100%; z-index: 6; margin-top: 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: 0 6px 18px rgba(0,0,0,.18); color: var(--text); font-weight: 400; text-align: left; }
|
|
179
180
|
.wb-counts:hover .wb-tip, .wb-counts.open .wb-tip { display: block; }
|
|
@@ -181,18 +182,6 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
181
182
|
.wb-counts .wv-review { color: var(--color-info); }
|
|
182
183
|
.wb-counts .wv-progress { color: var(--color-notice); }
|
|
183
184
|
.wb-counts .wv-todo, .wb-counts .wv-done { color: var(--color-quiet); }
|
|
184
|
-
.wb-lane + .wb-lane, .wb-lane + .wb-loose { border-top: 1px solid var(--line); }
|
|
185
|
-
.wb-loosehead:hover { background: var(--band); }
|
|
186
|
-
/* A wave card's step number, inside its id chip, and its ⋯ menu. */
|
|
187
|
-
.wn { color: var(--muted); font-variant-numeric: tabular-nums; padding-right: 5px; margin-right: 4px; border-right: 1px solid var(--line); }
|
|
188
|
-
.wb .item, .wb .item > summary .title-wrap { position: relative; }
|
|
189
|
-
.wb-dots[aria-expanded="true"] { color: var(--text); background: var(--chip); }
|
|
190
|
-
.wb-dots svg { display: block; }
|
|
191
|
-
.wb-menu { position: absolute; right: 0; top: calc(100% + 4px); z-index: 5; display: grid; min-width: 160px; padding: 4px;
|
|
192
|
-
border: 1px solid var(--line); border-radius: 10px; background: var(--panel); box-shadow: 0 6px 18px rgba(0,0,0,.18); }
|
|
193
|
-
.wb-menu button { border: 0; background: none; text-align: left; padding: 8px 10px; min-height: 0; border-radius: 6px; font: inherit; font-size: 13px; color: var(--text); cursor: pointer; }
|
|
194
|
-
.wb-menu button:hover:not(:disabled) { background: var(--chip); }
|
|
195
|
-
.wb-menu button:disabled { color: var(--muted); cursor: default; }
|
|
196
185
|
.lane[data-lane="todo"] > .list:has(.wb) { overflow: visible; }
|
|
197
186
|
.nav a.icon { display: inline-flex; align-items: center; padding: 4px; color: var(--muted); }
|
|
198
187
|
.nav a.icon:hover { color: var(--text); }
|
|
@@ -218,9 +207,6 @@ code { overflow-wrap: anywhere; }
|
|
|
218
207
|
/* A file input draws its "Choose File" text at the 16px coarse-pointer size, wider than the button; iOS Safari widens the page to fit it. */
|
|
219
208
|
.attach input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; padding: 0; border: 0; font-size: 0; }
|
|
220
209
|
.row > .attach + .muted { flex: 1; min-width: 0; }
|
|
221
|
-
/* The ⌘↵ / Ctrl+↵ hint left of Send: only where there is a keyboard to press it on. */
|
|
222
|
-
.send-hint { display: none; color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
223
|
-
@media (hover: hover) and (pointer: fine) { .send-hint { display: inline; } }
|
|
224
210
|
/* A composer's text box (BoardUI.dropZone), its attach button inside at the bottom right with the text kept clear of it:
|
|
225
211
|
while files are dragged over the composer, attach.js marks it .dragging and the box gets a dashed accent outline
|
|
226
212
|
with Drop to attach over it. */
|
|
@@ -317,7 +303,7 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
317
303
|
}
|
|
318
304
|
/* The hub lives at the tailnet root, outside this site, so its link is root-absolute. */
|
|
319
305
|
.brand { align-items: center; }
|
|
320
|
-
.hub { display: inline-flex; flex: none; }
|
|
306
|
+
a.hub { display: inline-flex; flex: none; }
|
|
321
307
|
.hub img { width: 32px; height: auto; display: block; image-rendering: pixelated; }
|
|
322
308
|
@keyframes pulse { 50% { opacity: .35; } }
|
|
323
309
|
/* A lane's folds are inset boxes in its list, so they read as part of that lane, not as more lanes. */
|
|
@@ -350,7 +336,7 @@ form[data-reply] .icons { display: inline-flex; flex: none; margin-left: auto; }
|
|
|
350
336
|
/* A composer's send buttons (ui.js sendButton), each with its key as a small gray hint where there is a keyboard and
|
|
351
337
|
room. A pair takes a row of two halves; a Todo card's reply box wraps its pair under the row. */
|
|
352
338
|
.send-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
|
|
353
|
-
.send-actions button, [data-sends] button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
|
339
|
+
.send-actions button, [data-sends] button, .view-questions .bar button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
|
354
340
|
.send-actions button { min-height: 44px; }
|
|
355
341
|
form[data-reply] .row { flex-wrap: wrap; gap: 6px; }
|
|
356
342
|
form[data-reply] .row > .send-actions { flex: 0 0 100%; margin-top: 4px; }
|
|
@@ -392,12 +378,18 @@ button.attach:disabled { opacity: .35; }
|
|
|
392
378
|
/* A New card's "waiting", until the orchestrator picks it up: grey, its dot still. */
|
|
393
379
|
.pickup-wait { color: var(--muted); font-size: 12px; font-weight: 600; }
|
|
394
380
|
.pickup-wait .working-dot { background: currentColor; animation: none; }
|
|
395
|
-
|
|
396
|
-
.
|
|
381
|
+
/* Every status line shares this spacing and gutter: the mark sits in the first 23px, and wrapped text hangs under the
|
|
382
|
+
first line's text, never under the mark. A fold's summary hangs the same way inside its box. Each kind keeps only its colour. */
|
|
383
|
+
.status-line { padding: 0 10px; margin: 4px 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
384
|
+
div.status-line { padding-left: 33px; text-indent: -23px; }
|
|
385
|
+
.status-line > summary { padding-left: 23px; text-indent: -23px; }
|
|
386
|
+
.status-line > ol { text-indent: 0; }
|
|
387
|
+
.msg.builder-status { color: var(--color-notice); }
|
|
397
388
|
.msg.builder-status .working-dot { animation: none; }
|
|
398
|
-
.msg.request {
|
|
389
|
+
.msg.request { color: var(--muted); }
|
|
399
390
|
.msg.request.readied { color: var(--color-success); }
|
|
400
|
-
.msg.
|
|
391
|
+
.msg.request.hub.fault { color: var(--color-danger); }
|
|
392
|
+
.msg.agent { font-weight: 600; }
|
|
401
393
|
.msg.agent.started { color: var(--color-notice); } .msg.agent.finished { color: var(--color-success); }
|
|
402
394
|
.msg.agent .muted { font-weight: 400; }
|
|
403
395
|
/* A planner's line in the thread, dot included, is the owner's blue; its finish stays green
|
|
@@ -420,7 +412,6 @@ button.attach:disabled { opacity: .35; }
|
|
|
420
412
|
.sub.agent.stale .working-dot { animation: none; }
|
|
421
413
|
/* The one class every paused status takes, gray: the card and thread line, the header, the hub banner. */
|
|
422
414
|
.paused { color: var(--muted); font-weight: 600; }
|
|
423
|
-
.msg.calls { padding: 0 10px; margin: 4px 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
424
415
|
.msg.calls summary { cursor: pointer; color: var(--muted); list-style: none; }
|
|
425
416
|
.msg.calls summary::-webkit-details-marker { display: none; }
|
|
426
417
|
.msg.calls[open] summary .mark-icon { transform: rotate(90deg); }
|
|
@@ -473,7 +464,7 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
473
464
|
.view-only-strip b { color: var(--accent); }
|
|
474
465
|
/* A board out of reach (docs/adr/2026-11-06-0135-a-board-out-of-reach-keeps-its-copy-or-shows-a-landing.md): it reads as before,
|
|
475
466
|
undimmed, and only the buttons that send, ui.js SENDS, are held, the labelled ones saying why; app-header.js stops their taps. */
|
|
476
|
-
.offline :is(form button[type="submit"], .actions button, button[data-confirm], button[data-deck-pick], button[data-review-answer], button[data-prompt-send], button[data-prompt-choice], button[data-quick], button[data-card-bump], button[data-bump], button[data-withdraw], button[data-
|
|
467
|
+
.offline :is(form button[type="submit"], .actions button, button[data-confirm], button[data-deck-pick], button[data-review-answer], button[data-prompt-send], button[data-prompt-choice], button[data-quick], button[data-card-bump], button[data-bump], button[data-withdraw], button[data-grill], button[data-proto], button[data-visual], button[data-plan], button[data-main-pull], button[data-arch-run], button[data-try], button[data-reopen]) { opacity: .55; cursor: not-allowed; }
|
|
477
468
|
/* With no copy to show, the landing takes the board's place: the kickflip mark and two lines, as hub/front-door.mjs's page has them. */
|
|
478
469
|
.down-landing { min-height: 60vh; display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; text-align: center; }
|
|
479
470
|
.down-landing-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
|
|
@@ -653,9 +644,20 @@ main > * { transition: opacity .25s ease; }
|
|
|
653
644
|
.view-questions .opt .lbl { font-weight: 600; }
|
|
654
645
|
.view-questions .opt .rec { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
655
646
|
.view-questions .opt .det { color: var(--muted); font-size: 13px; }
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
647
|
+
/* A question's context and an option's detail take markdown at body size: headings drawn as bold lines, blocks tight
|
|
648
|
+
(docs/adr/2026-11-09-1500-a-questions-descriptions-are-markdown-and-its-text-is-one-brief-line.md). So does any .md
|
|
649
|
+
box: the orchestrator's thread messages, markdown replies and View transcript's text rows
|
|
650
|
+
(docs/adr/2026-11-09-1453-orchestrator-messages-replies-and-transcript-text-are-markdown.md), whose blocks break the
|
|
651
|
+
lines, not the text's own newlines. */
|
|
652
|
+
:is(.msg .body, .reader-row).md { white-space: normal; }
|
|
653
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) { overflow-wrap: anywhere; }
|
|
654
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) :is(p, ul, ol, blockquote, pre, table) { margin: 0 0 6px; }
|
|
655
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) :is(h1, h2, h3, h4, h5, h6) { font-size: inherit; font-weight: 600; margin: 6px 0 2px; }
|
|
656
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) :is(ul, ol) { padding-left: 18px; }
|
|
657
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) :is(pre, code) { white-space: pre-wrap; font-size: 12px; }
|
|
658
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) table { display: block; max-width: 100%; overflow-x: hidden; }
|
|
659
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .deck-card .ctx, .deck-opts .det, .md) > :last-child { margin-bottom: 0; }
|
|
660
|
+
.view-questions .opt .suggested-why { margin-top: 4px; color: var(--color-info); font-size: 13px; line-height: 1.35; overflow-wrap: anywhere; }
|
|
659
661
|
.view-questions .note { margin-top: 4px; }
|
|
660
662
|
.view-questions .ans { background: var(--accent-soft); border-radius: 9px; padding: 7px 10px; margin-top: 8px; font-size: 13px; }
|
|
661
663
|
.view-questions .rep { background: var(--claude); border-radius: 9px; padding: 7px 10px; margin-top: 6px; font-size: 13px; }
|
|
@@ -687,7 +689,6 @@ main > * { transition: opacity .25s ease; }
|
|
|
687
689
|
.view-questions .q.resolved .reask-line { margin-top: 10px; }
|
|
688
690
|
.view-questions .bar { position: sticky; bottom: var(--app-bottom, 0px); padding: 10px 0 max(10px, env(safe-area-inset-bottom)); background: var(--bg); z-index: 2;
|
|
689
691
|
display: flex; justify-content: space-between; align-items: center; gap: 8px; border-top: 1px solid var(--line); }
|
|
690
|
-
.view-questions .bar .send-hint { margin-left: auto; }
|
|
691
692
|
/* The + rides above the sticky Send bar rather than over its Send. */
|
|
692
693
|
.view-questions .bar #qstatus { min-width: 0; }
|
|
693
694
|
.view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
|
|
@@ -739,18 +740,18 @@ main > * { transition: opacity .25s ease; }
|
|
|
739
740
|
.view-notes .plan li { margin: 3px 0; }
|
|
740
741
|
.view-notes .plan li.nested { margin-left: 20px; }
|
|
741
742
|
.view-notes .plan code { font-size: 12.5px; background: var(--chip); border-radius: 4px; padding: 0 3px; }
|
|
742
|
-
.view-notes .plan pre { overflow-x:
|
|
743
|
-
.view-notes .plan pre code { background: none; padding: 0;
|
|
743
|
+
.view-notes .plan pre { white-space: pre-wrap; overflow-x: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; }
|
|
744
|
+
.view-notes .plan pre code { background: none; padding: 0; }
|
|
744
745
|
.view-notes .plan blockquote { border-left: 3px solid var(--line); padding-left: 10px; color: var(--muted); }
|
|
745
746
|
.view-notes .plan hr { border: 0; border-top: 1px solid var(--line); margin: 16px 0; }
|
|
746
747
|
.view-notes .plan a { color: var(--accent); }
|
|
747
|
-
.view-notes .md-table { overflow-x:
|
|
748
|
-
.view-notes .md-table table { border-collapse: collapse; font-size: 13px; line-height: 1.4; }
|
|
749
|
-
.view-notes .md-table th, .view-notes .md-table td { text-align: left; vertical-align: top; padding: 6px 8px; border-bottom: 1px solid var(--line);
|
|
748
|
+
.view-notes .md-table { overflow-x: hidden; margin: 10px 0; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
|
|
749
|
+
.view-notes .md-table table { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 1.4; }
|
|
750
|
+
.view-notes .md-table th, .view-notes .md-table td { text-align: left; vertical-align: top; padding: 6px 8px; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
|
|
750
751
|
.view-notes .md-table th { background: var(--bg); font-weight: 600; }
|
|
751
752
|
.view-notes .md-table tr:last-child td { border-bottom: 0; }
|
|
752
|
-
|
|
753
|
-
.view-notes .
|
|
753
|
+
/* The sidebar and every pane never scroll sideways: content wraps at the pane's width, overflow-x hidden is the last guard. */
|
|
754
|
+
.view-questions, .view-notes, .reader-sheet, .pane-viewer { overflow-x: hidden; }
|
|
754
755
|
/* A framed page (view-frame.js) fills the space beside the header, and scrolls inside its frame. */
|
|
755
756
|
.view-frame { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
|
|
756
757
|
.view-frame > .top { flex: none; }
|
|
@@ -777,9 +778,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
777
778
|
.sheet.keyboard-sheet { --sheet-limit: max(80px, calc(var(--vh, 100dvh) + var(--kb, 0px) - max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px))) - max(12px, env(safe-area-inset-top)))); }
|
|
778
779
|
}
|
|
779
780
|
|
|
780
|
-
/* Typing, a text field focused (shell.js sets :root.typing): the
|
|
781
|
-
:root.typing .fab { opacity: .35; pointer-events: none; }
|
|
782
|
-
/* On a phone the + hides while typing, so it never sits over the keyboard's field. */
|
|
781
|
+
/* Typing, a text field focused (shell.js sets :root.typing). Only a phone changes: the + hides, so it never sits over the keyboard's field; wider screens keep it as it is. */
|
|
783
782
|
@media (max-width: 699px) {
|
|
784
783
|
:root.typing .fab { display: none; }
|
|
785
784
|
/* The board page bottom bar hides too; --app-bottom stays, so the page does not move. */
|
|
@@ -951,7 +950,7 @@ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentCo
|
|
|
951
950
|
.deck-opts button.rec { border-color: var(--accent); }
|
|
952
951
|
.deck-opts button.armed { background: var(--color-notice); border-color: var(--color-notice); color: #000; font-weight: 600; }
|
|
953
952
|
.deck-opts .rec-word { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
954
|
-
.deck-opts
|
|
953
|
+
.deck-opts .det { color: var(--muted); font-size: 12.5px; font-weight: 400; }
|
|
955
954
|
.deck-prompt { margin: 0; padding: 0; list-style: none; }
|
|
956
955
|
.deck-prompt li.prompt { padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
957
956
|
.deck-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
@@ -1005,3 +1004,4 @@ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentCo
|
|
|
1005
1004
|
}
|
|
1006
1005
|
/* The Send bar is the last thing on its page: only the phone nav under it is left clear, so its background runs to the edge. */
|
|
1007
1006
|
body .view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1007
|
+
.pfile.retry { cursor: pointer; }
|