@lovek4d/kickflip 0.10.1 → 0.11.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 +19 -16
- package/SKILL.md +7 -7
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +30 -17
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/public/app-cards.js +2 -0
- package/board/public/app-header.js +8 -0
- package/board/public/app-lanes.js +43 -19
- package/board/public/app-snapshot.js +1 -2
- package/board/public/app-threads.js +2 -2
- package/board/public/app.html +0 -1
- package/board/public/app.js +25 -27
- package/board/public/col-resize.js +25 -44
- package/board/public/dialogs.js +2 -2
- package/board/public/new-task.js +2 -2
- package/board/public/prompts.js +1 -1
- package/board/public/prototype-kit.js +1 -1
- package/board/public/prototype-switcher.js +3 -8
- package/board/public/prototype.html +2 -6
- package/board/public/router.js +16 -22
- package/board/public/shell.js +3 -39
- package/board/public/style.css +47 -95
- package/board/public/ui-cards.js +29 -22
- package/board/public/ui-enums.js +3 -3
- package/board/public/ui-page-header.js +2 -2
- package/board/public/ui-prompts.js +1 -1
- package/board/public/ui-questions.js +2 -3
- package/board/public/ui.js +10 -2
- package/board/public/view-hub.js +1 -1
- package/board/public/view-questions.js +7 -26
- package/board/server.mjs +18 -18
- package/board/store.mjs +32 -32
- package/hub/hub.mjs +8 -8
- package/hub/prompts.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +4 -2
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/board/public/app-needs.js +0 -248
package/board/public/shell.js
CHANGED
|
@@ -242,35 +242,10 @@ window.AppShell = (function () {
|
|
|
242
242
|
// go(view, href, how, from): router.js's, with how 'replace' for a framed page's hand-off and 'reload' for a view's
|
|
243
243
|
// own refresh. → true, or a promise of whether the view switched once the owner answers about an unsaved edit.
|
|
244
244
|
let router = null;
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
// from 900px the board shows Waiting on you in its pane, so every view of the questions page, a link, a push, a
|
|
248
|
-
// hand-off, a history entry or a cold load, shows the board at its own URL and opens the question's topic there (id,
|
|
249
|
-
// or null for none). A board's prototype page does the same and opens in the pane
|
|
250
|
-
// (docs/adr/2026-11-07-2030-on-desktop-the-prototype-viewer-opens-in-the-right-pane.md).
|
|
251
|
-
const inPane = (view) => wide.matches && (view?.kind === 'questions' || AppRouter.isPrototype(view, boards()));
|
|
252
|
-
// ask(B) → true once the board's pane did what the view asks. A board just shown takes a moment to fill its pane, so
|
|
253
|
-
// the pane is asked again until it has.
|
|
254
|
-
const paneAsk = (view, href) => (view.kind === 'questions' ? (B) => B.focusQuestion?.(view.id) : (B) => B.openViewer?.(href));
|
|
255
|
-
function askPane(ask, tries = 50) {
|
|
256
|
-
if (current?.view.kind !== 'board' || ask(current.v.B) || tries <= 0) return;
|
|
257
|
-
setTimeout(() => askPane(ask, tries - 1), 100);
|
|
258
|
-
}
|
|
245
|
+
// Board and Waiting on you are a board's two pages at every width, and a prototype is a page of its own
|
|
246
|
+
// (docs/adr/2026-11-07-1300-board-and-open-questions-are-two-pages.md, docs/adr/2026-11-12-0700-the-board-has-no-needs-you-deck-or-pane.md).
|
|
259
247
|
function go(view, href, how, from) {
|
|
260
248
|
if (!AppRouter.mounted(view)) return false;
|
|
261
|
-
if (inPane(view)) {
|
|
262
|
-
const ask = paneAsk(view, href);
|
|
263
|
-
const board = { kind: 'board', route: view.route, hash: '' };
|
|
264
|
-
const boardHref = new URL(AppRouter.urlOf(board), location.href).href;
|
|
265
|
-
if (how === 'pop') history.replaceState(history.state, '', boardHref);
|
|
266
|
-
// On its own board it only asks the pane, so the URL, history and an open card stay as they are.
|
|
267
|
-
else if (current?.view.kind === 'board' && current.route === view.route && !settings) {
|
|
268
|
-
askPane(ask);
|
|
269
|
-
sidebar.close();
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
return Promise.resolve(go(board, boardHref, how, from)).then((shown) => { if (shown) askPane(ask); return shown; });
|
|
273
|
-
}
|
|
274
249
|
if (view.kind === 'settings') {
|
|
275
250
|
if (current?.view.kind === 'board' && current.route === view.route) {
|
|
276
251
|
const here = AppRouter.viewOf(location.href, { boards: boards() });
|
|
@@ -322,17 +297,7 @@ window.AppShell = (function () {
|
|
|
322
297
|
}
|
|
323
298
|
if (!AppRouter.mounted(first)) { location.replace('/_hub/'); return; }
|
|
324
299
|
|
|
325
|
-
|
|
326
|
-
if (inPane(first)) {
|
|
327
|
-
pane = paneAsk(first, location.href);
|
|
328
|
-
first = { kind: 'board', route: first.route, hash: '' };
|
|
329
|
-
history.replaceState(history.state, '', AppRouter.urlOf(first));
|
|
330
|
-
}
|
|
331
|
-
router = AppRouter.listen({ boards, go, wide: () => wide.matches });
|
|
332
|
-
// Widening past 900px on Waiting on you or a prototype swaps to the board with it open in the pane.
|
|
333
|
-
wide.addEventListener('change', () => {
|
|
334
|
-
if (current && inPane(current.view)) go(AppRouter.viewOf(location.href, { boards: boards() }), location.href, 'replace');
|
|
335
|
-
});
|
|
300
|
+
router = AppRouter.listen({ boards, go });
|
|
336
301
|
opener = (url, { replace = false, frames = true } = {}) => {
|
|
337
302
|
const view = AppRouter.viewOf(url, { boards: boards() });
|
|
338
303
|
if (!AppRouter.mounted(view) || (!frames && view.kind === 'frame')) return false;
|
|
@@ -348,7 +313,6 @@ window.AppShell = (function () {
|
|
|
348
313
|
navigator.serviceWorker?.startMessages?.();
|
|
349
314
|
|
|
350
315
|
show(first);
|
|
351
|
-
if (pane) askPane(pane);
|
|
352
316
|
openStream();
|
|
353
317
|
addEventListener('visibilitychange', () => { if (!document.hidden) openStream(); });
|
|
354
318
|
addEventListener('pageshow', openStream);
|
package/board/public/style.css
CHANGED
|
@@ -85,7 +85,7 @@ button.chip { border: 0; }
|
|
|
85
85
|
.sub.agent { color: var(--color-notice); }
|
|
86
86
|
.sub.next { color: var(--color-notice); }
|
|
87
87
|
.card-hold { color: var(--color-quiet); }
|
|
88
|
-
.card-blocked
|
|
88
|
+
.card-blocked { color: var(--color-notice); }
|
|
89
89
|
/* docs/adr/2026-11-11-1200-every-lane-has-phases.md: a card's phase word, toned by who acts next (ui-cards.js PHASE_LOOK). */
|
|
90
90
|
.card-phase.ph-quiet { color: var(--color-quiet); }
|
|
91
91
|
.card-phase.ph-info { color: var(--color-info); }
|
|
@@ -285,6 +285,9 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
285
285
|
border-radius: 15px 15px 0 0; padding-bottom: calc(max(12px, calc(env(safe-area-inset-bottom) - var(--app-bottom, 0px))) + var(--kb, 0px)); }
|
|
286
286
|
.sheet.keyboard-sheet.kb form { padding-bottom: calc(10px + var(--kb, 0px)); }
|
|
287
287
|
#askText { font-size: 16px; }
|
|
288
|
+
/* The thread box is as tall as the New task box: 4 rows, its 8px top padding and the 40px under the text for the
|
|
289
|
+
attach row (the borders sit inside the box). It grows past that as it does elsewhere. */
|
|
290
|
+
form[data-reply] .dz.bare > textarea { font-size: 16px; min-height: calc(4lh + 48px); }
|
|
288
291
|
}
|
|
289
292
|
@keyframes sheet-in { from { transform: translateY(100%); } }
|
|
290
293
|
@keyframes sheet-out { to { transform: translateY(100%); } }
|
|
@@ -392,19 +395,22 @@ div.status-line { padding-left: 33px; text-indent: -23px; }
|
|
|
392
395
|
.msg.agent.started { color: var(--color-notice); } .msg.agent.finished { color: var(--color-success); }
|
|
393
396
|
.msg.agent .muted { font-weight: 400; }
|
|
394
397
|
/* A planner's line in the thread, dot included, is the owner's blue; its finish stays green
|
|
395
|
-
(docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). On the card it
|
|
398
|
+
(docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). On the card it is blue too (.sub.agent.planner below). */
|
|
396
399
|
.msg.builder-status.planner, .msg.agent.started.planner { color: var(--accent); }
|
|
397
400
|
.planner .working-dot { background: currentColor; }
|
|
398
|
-
/* A run's status line, dot included, takes its card's lane colour
|
|
401
|
+
/* A builder run's status line, dot included, takes its card's lane colour: grey in Todo, the phase-bar orange in
|
|
399
402
|
In Progress, blue in In Review; a Todo card's planner dashes are its grey, the running one pulsing
|
|
400
403
|
(docs/adr/2026-11-06-0145-every-run-takes-its-lanes-colour.md). */
|
|
401
404
|
.sub.agent.in-todo { color: var(--muted); }
|
|
402
405
|
.sub.agent.in-progress { color: var(--color-notice); }
|
|
403
406
|
.sub.agent.in-review { color: var(--color-info); }
|
|
404
407
|
.sub.agent.in-todo .working-dot, .sub.agent.in-progress .working-dot, .sub.agent.in-review .working-dot { background: currentColor; }
|
|
405
|
-
/* docs/adr/2026-11-
|
|
406
|
-
|
|
407
|
-
.sub.agent.
|
|
408
|
+
/* docs/adr/2026-11-12-0600-every-planner-launches-from-the-orchestrators-brief.md: a grill, prototype or visual's status
|
|
409
|
+
line, its brief's and its agent's, is blue in every lane, after the lane colours so it wins. */
|
|
410
|
+
.sub.agent.planner { color: var(--color-info); }
|
|
411
|
+
/* docs/adr/2026-11-05-0410-a-card-status-word-comes-from-a-signal-the-board-checks.md: a quiet builder is grey, after the
|
|
412
|
+
lane colours so it wins. */
|
|
413
|
+
.sub.agent.quiet { color: var(--muted); }
|
|
408
414
|
.sub.agent.waiting-gray { color: var(--muted); }
|
|
409
415
|
/* A stale build's status line is grey, its dot still (ui-cards.js builderStatus). */
|
|
410
416
|
.sub.agent.stale { color: var(--muted); }
|
|
@@ -444,8 +450,11 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
444
450
|
/* INT-30: card line order */
|
|
445
451
|
.line6 { margin-top: 4px; }
|
|
446
452
|
.card-time time { font-variant-numeric: tabular-nums; }
|
|
447
|
-
.card-
|
|
448
|
-
.card-
|
|
453
|
+
.card-alert { color: var(--color-notice); font-weight: 600; }
|
|
454
|
+
.card-alert .sep { color: var(--muted); font-weight: 400; }
|
|
455
|
+
.card-alert .card-action { color: var(--color-notice); }
|
|
456
|
+
.card-alert .card-stalled { color: var(--color-danger); }
|
|
457
|
+
.lane[data-lane="done"] .card-alert .card-unread { color: var(--color-info); }
|
|
449
458
|
/* Lane headers: Linear-style group bands */
|
|
450
459
|
.lane > summary { display: flex; align-items: center; gap: 8px; margin: 0; min-height: 34px; padding: 5px 12px;
|
|
451
460
|
background: var(--band); border: 1px solid var(--line); border-radius: 10px; font-size: 13px; font-weight: 500; color: var(--text); }
|
|
@@ -468,13 +477,15 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
468
477
|
.view-only-strip b { color: var(--accent); }
|
|
469
478
|
/* 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,
|
|
470
479
|
undimmed, and only the buttons that send, ui.js SENDS, are held, the labelled ones saying why; app-header.js stops their taps. */
|
|
471
|
-
.offline :is(form button[type="submit"], .actions button, button[data-confirm], button[data-
|
|
480
|
+
.offline :is(form button[type="submit"], .actions button, button[data-confirm], 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-try-kill], button[data-reopen]) { opacity: .55; cursor: not-allowed; }
|
|
472
481
|
/* 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. */
|
|
473
482
|
.down-landing { min-height: 60vh; display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; text-align: center; }
|
|
474
483
|
.down-landing-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
|
|
475
484
|
.down-landing-line { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
|
|
476
485
|
.down-landing-hint { margin: 0; font-size: 14px; color: var(--muted); }
|
|
477
|
-
.
|
|
486
|
+
.down-landing-stack { margin: 0; max-width: 100%; overflow: auto; text-align: left; font-size: 12px; padding: 10px; border-radius: 8px; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
487
|
+
.down-landing-stack[hidden] { display: none; }
|
|
488
|
+
.view-board.down main > :not(#downLanding), .view-board.down #compose { display: none; }
|
|
478
489
|
.view-settings.down .settings-body > :not(.down-landing) { display: none; }
|
|
479
490
|
/* INT-28: answer banner */
|
|
480
491
|
.answer-banner { display: flex; align-items: center; gap: 4px; background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
@@ -529,7 +540,7 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
529
540
|
.board-foot .arch-bulb[hidden] { display: none; }
|
|
530
541
|
.board-foot .arch-bulb.running { color: var(--color-notice); }
|
|
531
542
|
.board-foot a.icon[aria-disabled="true"] { opacity: .35; pointer-events: none; }
|
|
532
|
-
/* The pages switch
|
|
543
|
+
/* The pages switch: the board header's ? with the open-question count on its corner, and Waiting on you's check back to the board. */
|
|
533
544
|
.top .pages-switch { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; width: auto; height: auto; margin: 0; padding: 4px; border: 0; border-radius: 0; line-height: 1; color: var(--muted); }
|
|
534
545
|
.top .pages-switch:hover { color: var(--text); }
|
|
535
546
|
.top .pages-switch:active { background: none; }
|
|
@@ -654,13 +665,13 @@ main > * { transition: opacity .25s ease; }
|
|
|
654
665
|
(docs/adr/2026-11-09-1453-orchestrator-messages-replies-and-transcript-text-are-markdown.md), whose blocks break the
|
|
655
666
|
lines, not the text's own newlines. */
|
|
656
667
|
:is(.msg .body, .reader-row).md { white-space: normal; }
|
|
657
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
658
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
659
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
660
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
661
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
662
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
663
|
-
:is(.view-questions .ctx, .view-questions .opt .det, .
|
|
668
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) { overflow-wrap: anywhere; }
|
|
669
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) :is(p, ul, ol, blockquote, pre, table) { margin: 0 0 6px; }
|
|
670
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) :is(h1, h2, h3, h4, h5, h6) { font-size: inherit; font-weight: 600; margin: 6px 0 2px; }
|
|
671
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) :is(ul, ol) { padding-left: 18px; }
|
|
672
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) :is(pre, code) { white-space: pre-wrap; font-size: 12px; }
|
|
673
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) table { display: block; max-width: 100%; overflow-x: hidden; }
|
|
674
|
+
:is(.view-questions .ctx, .view-questions .opt .det, .md) > :last-child { margin-bottom: 0; }
|
|
664
675
|
.view-questions .opt .suggested-why { margin-top: 4px; color: var(--color-info); font-size: 13px; line-height: 1.35; overflow-wrap: anywhere; }
|
|
665
676
|
.view-questions .note { margin-top: 4px; }
|
|
666
677
|
.view-questions .ans { background: var(--accent-soft); border-radius: 9px; padding: 7px 10px; margin-top: 8px; font-size: 13px; }
|
|
@@ -698,8 +709,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
698
709
|
/* The + rides above the sticky Send bar rather than over its Send. */
|
|
699
710
|
.view-questions .bar #qstatus { min-width: 0; }
|
|
700
711
|
.view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
|
|
701
|
-
/* With no open question, the moon and its line sit at the centre of what the page shows between its top and the nav
|
|
702
|
-
or of what the desktop pane has left under its Needs you lines. */
|
|
712
|
+
/* With no open question, the moon and its line sit at the centre of what the page shows between its top and the nav. */
|
|
703
713
|
.view-questions .q-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--muted); text-align: center;
|
|
704
714
|
min-height: calc(var(--vh, 100dvh) - var(--app-bottom, 0px) - max(16px, env(safe-area-inset-top))); }
|
|
705
715
|
.view-questions .q-empty p { margin: 0; font-size: 15px; }
|
|
@@ -756,8 +766,8 @@ main > * { transition: opacity .25s ease; }
|
|
|
756
766
|
.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; }
|
|
757
767
|
.view-notes .md-table th { background: var(--bg); font-weight: 600; }
|
|
758
768
|
.view-notes .md-table tr:last-child td { border-bottom: 0; }
|
|
759
|
-
/* The sidebar and every
|
|
760
|
-
.view-questions, .view-notes, .reader-sheet
|
|
769
|
+
/* The sidebar and every page never scroll sideways: content wraps at its width, overflow-x hidden is the last guard. */
|
|
770
|
+
.view-questions, .view-notes, .reader-sheet { overflow-x: hidden; }
|
|
761
771
|
/* A framed page (view-frame.js) fills the space beside the header, and scrolls inside its frame. */
|
|
762
772
|
.view-frame { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
|
|
763
773
|
.view-frame > .top { flex: none; }
|
|
@@ -776,7 +786,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
776
786
|
border-bottom: 0; border-top: 1px solid var(--line); padding-top: 4px; padding-bottom: max(4px, env(safe-area-inset-bottom));
|
|
777
787
|
}
|
|
778
788
|
.top .nav #refresh, .top .nav a.icon, .top .pages-switch { min-width: 44px; min-height: 44px; }
|
|
779
|
-
body > main, body.has-sidebar > :not(.sidebar) > main
|
|
789
|
+
body > main, body.has-sidebar > :not(.sidebar) > main { padding-top: max(16px, env(safe-area-inset-top)); padding-bottom: calc(96px + var(--app-bottom, 0px) + env(safe-area-inset-bottom)); }
|
|
780
790
|
.view-frame:not(.own-header) { padding-bottom: var(--app-bottom, 0px); }
|
|
781
791
|
.sheet { bottom: max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px)));
|
|
782
792
|
max-height: calc(var(--vh, 100dvh) + var(--kb, 0px) - max(var(--app-bottom, 0px), var(--kb, 0px)) - env(safe-area-inset-top) - 12px); }
|
|
@@ -938,92 +948,34 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
938
948
|
.rv-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
939
949
|
.view-questions .bar #qstatus .try-spin { color: var(--accent); margin-right: 6px; }
|
|
940
950
|
|
|
941
|
-
/* Needs you (app-needs.js): a deck at the top of the board on a phone; from 900px a pane beside the lanes, its lines over
|
|
942
|
-
Waiting on you (docs/adr/2026-11-05-0408-needs-you-is-a-deck-on-a-phone-and-a-pane-on-a-desktop.md). */
|
|
943
|
-
.needs-pane { display: none; }
|
|
944
951
|
.col-handle { display: none; }
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
.deck-head { display: flex; align-items: center; gap: 8px; padding: 10px 14px 4px; }
|
|
948
|
-
.deck-head h2 { margin: 0; flex: 1; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--color-notice); }
|
|
949
|
-
.deck-n { min-width: 20px; padding: 1px 7px; border-radius: 999px; background: var(--color-notice); color: #1c1407; font-size: 12px; font-weight: 700; text-align: center; }
|
|
950
|
-
.deck-card { padding: 2px 14px 12px; min-width: 0; overflow-wrap: anywhere; }
|
|
951
|
-
.deck-what { margin: 0 0 2px; font-size: 12px; color: var(--muted); }
|
|
952
|
-
.deck-card h3 { margin: 0 0 4px; font-size: 15px; line-height: 1.3; }
|
|
953
|
-
.deck-card h3 a { color: inherit; text-decoration: none; }
|
|
954
|
-
.deck-card .ctx { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
|
955
|
-
.deck-proto { margin: 0 0 10px; color: var(--accent); }
|
|
956
|
-
.deck-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
957
|
-
.deck-opts button { text-align: left; border-radius: 10px; padding: 10px 12px; font-size: 14px; }
|
|
958
|
-
.deck-opts button.rec { border-color: var(--accent); }
|
|
959
|
-
.deck-opts button.armed { border-color: var(--sel-line); background: var(--sel-bg); font-weight: 600; }
|
|
960
|
-
.deck-opts .rec-word { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
961
|
-
.deck-opts .det { color: var(--muted); font-size: 12.5px; font-weight: 400; }
|
|
962
|
-
.deck-prompt { margin: 0; padding: 0; list-style: none; }
|
|
963
|
-
.deck-prompt li.prompt { padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
964
|
-
.deck-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
965
|
-
.deck-bar { display: flex; align-items: center; gap: 10px; padding: 0 14px 10px; }
|
|
966
|
-
.deck-dots { flex: 1; display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; }
|
|
967
|
-
.deck-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--line); }
|
|
968
|
-
.deck-dots i.on { background: var(--color-notice); }
|
|
969
|
-
.deck-bar button { border-radius: 999px; padding: 5px 12px; font-size: 13px; }
|
|
970
|
-
.deck-more { display: flex; align-items: center; justify-content: space-between; min-height: 46px; padding: 10px 14px; border-top: 1px solid color-mix(in srgb, var(--color-notice) 28%, var(--line));
|
|
971
|
-
background: color-mix(in srgb, var(--color-notice) 14%, var(--panel)); color: var(--text); font-weight: 600; font-size: 14px; text-decoration: none; }
|
|
972
|
-
.deck-more::after { content: '›'; color: var(--accent); font-size: 18px; }
|
|
973
|
-
.deck-more:hover { text-decoration: none; }
|
|
952
|
+
/* From 900px the lanes take the board's full width; Waiting on you is the header's ?
|
|
953
|
+
(docs/adr/2026-11-12-0700-the-board-has-no-needs-you-deck-or-pane.md). */
|
|
974
954
|
@media (min-width: 900px) {
|
|
975
|
-
.
|
|
976
|
-
.split > main { max-width: none; margin: 0; }
|
|
977
|
-
.needs-deck { display: none; }
|
|
978
|
-
/* Waiting on you is the board's pane here, so neither page shows the switch to the other. */
|
|
979
|
-
.top .pages-switch { display: none; }
|
|
980
|
-
.top .nav a.pages-switch { display: none; }
|
|
981
|
-
/* The board's + sits at the bottom right of the left pane: the pane ends at the middle of the space right of the sidebar. */
|
|
982
|
-
.view-board .fab { right: calc(var(--pane-w, calc(50% - var(--sidebar-w, 0px) / 2)) + var(--fab-edge)); }
|
|
955
|
+
.view-board > main { max-width: none; margin: 0; }
|
|
983
956
|
body.has-sidebar { --sidebar-w: var(--sb-w, 272px); }
|
|
984
957
|
html.sb-collapsed body.has-sidebar { --sidebar-w: 0px; }
|
|
985
|
-
/* The drag
|
|
958
|
+
/* The sidebar's drag edge (col-resize.js): a hairline that thickens on hover. */
|
|
986
959
|
.col-handle { display: block; position: absolute; z-index: 5; top: 0; bottom: 0; width: 9px; cursor: col-resize; touch-action: none; }
|
|
987
960
|
.col-handle::after { content: ''; position: absolute; top: 0; bottom: 0; left: 4px; width: 1px; background: transparent; transition: background .12s, width .12s, left .12s; }
|
|
988
961
|
.col-handle:is(:hover, :focus-visible, .dragging)::after { left: 3px; width: 3px; background: var(--accent); }
|
|
989
962
|
.col-handle:focus-visible { outline: none; }
|
|
990
963
|
html.col-dragging, html.col-dragging * { cursor: col-resize !important; user-select: none; }
|
|
991
|
-
|
|
992
|
-
.
|
|
993
|
-
/* The reader sheet (app-reader.js) is a panel over the pane's half, at the width the seam gives it, while it is open. */
|
|
994
|
-
.sheet.reader-sheet { position: fixed; z-index: 30; inset: 56px 0 0 auto; margin: 0; width: var(--pane-w, calc((100vw - var(--sidebar-w, 0px)) / 2)); max-width: none;
|
|
964
|
+
/* The reader sheet (app-reader.js) is a panel over the right half of the space beside the sidebar while it is open. */
|
|
965
|
+
.sheet.reader-sheet { position: fixed; z-index: 30; inset: 56px 0 0 auto; margin: 0; width: calc((100vw - var(--sidebar-w, 0px)) / 2); max-width: none;
|
|
995
966
|
height: auto; max-height: none; border-width: 0 0 0 1px; border-radius: 0; box-shadow: none; }
|
|
996
967
|
.sheet.reader-sheet[open] { animation: reader-in .22s ease-out; }
|
|
997
968
|
.sheet.reader-sheet.closing { animation: reader-out .18s ease-in forwards; }
|
|
998
969
|
.reader-sheet::backdrop { background: transparent; }
|
|
999
|
-
.needs-pane { display: block; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 12px 16px 0; border-left: 1px solid var(--line); }
|
|
1000
|
-
.needs-pane .needs-questions main { padding: 0; max-width: none; }
|
|
1001
|
-
.needs-pane:has(.q-empty), .needs-pane .needs-questions:has(.q-empty), .needs-pane .needs-questions:has(.q-empty) main, .needs-pane #page:has(.q-empty) { display: flex; flex-direction: column; }
|
|
1002
|
-
.needs-pane .needs-questions:has(.q-empty), .needs-pane .needs-questions:has(.q-empty) main, .needs-pane #page:has(.q-empty), .needs-pane .q-empty { flex: 1; min-height: 0; }
|
|
1003
|
-
.needs-pane .waiting { margin: 0 0 12px; }
|
|
1004
|
-
.needs-pane * { min-width: 0; overflow-wrap: anywhere; }
|
|
1005
|
-
.needs-pane pre, .needs-pane code { white-space: pre-wrap; }
|
|
1006
|
-
.needs-pane :is(.deck-opts, .row, .actions, .btns) { flex-wrap: wrap; }
|
|
1007
|
-
/* A prototype in the pane (app-needs.js openViewer): a top bar over the framed page, whose footer ends the pane; ⤢
|
|
1008
|
-
(.widened) lays the pane over the lanes, from the sidebar's edge. */
|
|
1009
|
-
.needs-pane.viewing { display: flex; flex-direction: column; padding: 0; overflow: hidden; }
|
|
1010
|
-
.needs-pane.viewing > :not(.pane-viewer) { display: none; }
|
|
1011
|
-
.needs-pane.viewing.widened { position: fixed; top: 56px; left: var(--sidebar-w, 0px); right: 0; bottom: 0; height: auto; z-index: 7; border-left: 0; background: var(--bg); }
|
|
1012
|
-
.pane-viewer { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
1013
|
-
.pv-top { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
1014
|
-
.pv-top .pv-grow { flex: 1; }
|
|
1015
|
-
.pv-top button { min-height: 36px; border-radius: 8px; font-weight: 600; }
|
|
1016
|
-
.pv-top .pv-back { border: 0; background: transparent; color: var(--text); padding: 6px 8px; }
|
|
1017
|
-
.pv-top .pv-back:hover { background: var(--line); }
|
|
1018
|
-
.pv-top .pv-widen { width: 36px; padding: 0; font-size: 17px; }
|
|
1019
|
-
.pv-top .pv-widen[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
|
1020
|
-
.pv-top .ps-chip { border-radius: 999px; padding: 0 14px; }
|
|
1021
|
-
.pv-frame { flex: 1; min-height: 0; width: 100%; border: 0; display: block; background: var(--panel); }
|
|
1022
|
-
.pane-viewer .ps-sheet { position: absolute; left: 8px; right: 8px; top: 56px; z-index: 2; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: 0 8px 30px rgba(0,0,0,.25); }
|
|
1023
|
-
.pane-viewer .ps-sheet h3 { margin: 0 0 6px; font-size: 14px; }
|
|
1024
|
-
.pane-viewer .ps-sheet button { display: flex; justify-content: space-between; gap: 12px; width: 100%; border: 0; border-top: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--text); padding: 10px 2px; text-align: left; }
|
|
1025
|
-
.pane-viewer .ps-sheet button span + span { color: var(--muted); }
|
|
1026
970
|
}
|
|
1027
971
|
/* 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. */
|
|
1028
972
|
body .view-questions.view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1029
973
|
.pfile.retry { cursor: pointer; }
|
|
974
|
+
/* The Todo header's filter (docs/adr/2026-11-12-0600-the-todo-header-has-a-filter.md): a funnel at the right, orange while a box is unchecked, and its checklist under the header. */
|
|
975
|
+
.lane { position: relative; }
|
|
976
|
+
.lane-filter { margin-left: auto; display: inline-flex; align-items: center; padding: 4px; border: 0; background: none; color: var(--muted); cursor: pointer; }
|
|
977
|
+
.lane-filter.on { color: var(--color-notice); }
|
|
978
|
+
.lane-filter-menu { position: absolute; z-index: 5; top: 36px; right: 8px; display: flex; flex-direction: column; min-width: 160px; padding: 4px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); box-shadow: 0 6px 18px rgba(0, 0, 0, .25); }
|
|
979
|
+
.lane-filter-box { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border: 0; background: none; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
|
|
980
|
+
.lane-filter-box .box { width: 14px; height: 14px; border: 1.5px solid var(--muted); border-radius: 4px; }
|
|
981
|
+
.lane-filter-box[aria-checked="true"] .box { border-color: var(--accent); background: var(--accent); }
|
package/board/public/ui-cards.js
CHANGED
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
if (b && b.kind !== BUILDER_KIND.install) return b.stopped || (b.pause ? (b.pause.paused ? 'paused' : 'pausing') : b.alive === false ? 'stopped' : b.phase === 'landing' ? 'landing' : 'implementing');
|
|
255
255
|
if (it.waitsOn === 'checking') return 'Orchestrator review';
|
|
256
256
|
if (it.lane === 'done') return 'done';
|
|
257
|
-
if (it.
|
|
257
|
+
if (it.planning) return it.planning;
|
|
258
258
|
if (it.lane === 'todo') return it.phase && (it.ready || it.phase !== 'ready') ? PHASE_LOOK[it.phase][0] : 'to do';
|
|
259
259
|
if (it.lane === 'progress') return it.reachedPhase ? `stopped at ${it.reachedPhase}` : 'in progress';
|
|
260
260
|
return 'in review';
|
|
@@ -280,16 +280,19 @@
|
|
|
280
280
|
// docs/adr/2026-11-11-1200-every-lane-has-phases.md
|
|
281
281
|
// A Todo, In Review or Done card's phase as a word, toned by who acts next: grey while a machine waits, blue while
|
|
282
282
|
// Claude works, orange while it waits on the owner, green once it can go and purple once merged. In Progress shows its
|
|
283
|
-
// phase only as its progress bar, and a card In Review for an owner wait other than the merge has none. Ready reads
|
|
283
|
+
// phase only as its progress bar, a New card has none (its footer line says it), and a card In Review for an owner wait other than the merge has none. Ready reads
|
|
284
284
|
// only on a card that can start (domain.mjs ready), so a blocked one shows what blocks it instead.
|
|
285
285
|
const PHASE_LOOK = {
|
|
286
|
-
new: ['new', 'quiet'], grilling: ['grilling', 'info'], prototyping: ['prototyping', 'info'], staging: ['staging', 'notice'], ready: ['ready', 'success'],
|
|
286
|
+
new: ['new', 'quiet'], discussing: ['discussing', 'info'], grilling: ['grilling', 'info'], prototyping: ['prototyping', 'info'], staging: ['staging', 'notice'], ready: ['ready', 'success'],
|
|
287
287
|
draft: ['draft', 'quiet'], reviewers: ['pending review', 'notice'], merge: ['pending merge', 'notice'], stale: ['stale', 'quiet'],
|
|
288
288
|
merged: ['merged', 'merged'], closed: ['closed', 'quiet'],
|
|
289
289
|
};
|
|
290
|
+
// docs/adr/2026-11-12-0500-a-new-task-is-a-conversation.md: a discussing card reads just "discussing", blue on the
|
|
291
|
+
// owner's turn and grey while the orchestrator owes the reply.
|
|
292
|
+
const TURN_LOOK = { owner: ['discussing', 'info'], claude: ['discussing', 'quiet'] };
|
|
290
293
|
function phaseWord(it) {
|
|
291
|
-
if (it.kind !== 'work' || it.lane === 'progress' || !it.phase || (it.phase === 'ready' && !it.ready)) return '';
|
|
292
|
-
const [word, tone] = PHASE_LOOK[it.phase];
|
|
294
|
+
if (it.kind !== 'work' || it.lane === 'progress' || !it.phase || it.phase === 'new' || (it.phase === 'ready' && !it.ready)) return '';
|
|
295
|
+
const [word, tone] = it.phase === 'discussing' ? TURN_LOOK[it.turn] : PHASE_LOOK[it.phase];
|
|
293
296
|
return `<span class="card-phase ph-${tone}">${word}</span>`;
|
|
294
297
|
}
|
|
295
298
|
// docs/adr/2026-11-09-1300-a-card-pause-stops-its-builder-at-its-next-stopping-point.md
|
|
@@ -297,26 +300,29 @@
|
|
|
297
300
|
// orange while its builder runs on to its stopping point, then "paused · blocked by #61" in grey.
|
|
298
301
|
const pauseOf = (it) => (it.lane === 'progress' ? it.builder?.pause ?? null : null);
|
|
299
302
|
const pauseWords = (pause) => pause && `<span class="card-${pause.paused ? 'paused' : 'pausing'}">${pause.paused ? 'paused' : 'pausing'} · ${esc(pause.words)}</span>`;
|
|
300
|
-
// A card's last
|
|
301
|
-
//
|
|
302
|
-
// (docs/adr/2026-11-06-1700-a-bump-marks-the-priority-through-every-lane.md),
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
303
|
+
// A card's last two rows. The footer: the time its lane sorts by (domain.mjs sortAt), then its hold and what blocks it,
|
|
304
|
+
// or its phase word (phaseWord) when neither shows, then a build's stale in grey, held while host recovers on a bumped
|
|
305
|
+
// ready card a busy host holds, in grey (docs/adr/2026-11-06-1700-a-bump-marks-the-priority-through-every-lane.md),
|
|
306
|
+
// a paused build's words (pauseWords), then an In Progress card's phase bars
|
|
307
|
+
// (docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md). The row under it, drawn only when it has
|
|
308
|
+
// something: Action needed and the unread messages in bold orange, new questions counting as one (app-cards.js takes
|
|
309
|
+
// the count away when the card opens), then stalled in red, rightmost
|
|
310
|
+
// (docs/adr/2026-11-06-1252-stale-is-automatic-stalled-is-the-orchestrators-flag.md,
|
|
311
|
+
// docs/adr/2026-11-04-0247-a-builder-asks-the-orchestrator-mid-build.md,
|
|
312
|
+
// docs/adr/2026-11-12-0600-action-needed-and-new-messages-get-their-own-row.md).
|
|
307
313
|
const SEP = '<span class="sep"> · </span>';
|
|
308
314
|
const newWords = (n, word) => n && `${n} new ${word}${n === 1 ? '' : 's'}`;
|
|
309
315
|
function timeLine(it, unread, bars) {
|
|
310
316
|
const iso = it.sortAt;
|
|
311
317
|
const pause = pauseOf(it);
|
|
312
|
-
const
|
|
318
|
+
const holdBlock = [pause?.cause !== 'held' && holdWords(it), pause?.cause !== 'blocked' && blockedWords(it)].filter(Boolean);
|
|
319
|
+
const footer = [iso && `<time datetime="${esc(iso)}">${esc(cardTime(iso))}</time>`, ...(holdBlock.length ? holdBlock : [phaseWord(it)]),
|
|
313
320
|
it.hostHeld && '<span class="card-host-held">held while host recovers</span>',
|
|
314
|
-
it.stalled
|
|
321
|
+
!it.stalled && it.stale && it.phase !== 'stale' && '<span class="card-stale">stale</span>', pauseWords(pause), bars].filter(Boolean);
|
|
315
322
|
const news = newWords(unread, 'message');
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return `<div class="line6 sub card-time">${parts.join(SEP)}${tail}${count}</div>`;
|
|
323
|
+
const alert = [actionWords(it), news && `<span class="card-unread">${news}</span>`, it.stalled && '<span class="card-stalled">stalled</span>'].filter(Boolean);
|
|
324
|
+
return [footer.length && `<div class="line6 sub card-time">${footer.join(SEP)}</div>`,
|
|
325
|
+
alert.length && `<div class="line6 sub card-alert">${alert.join(SEP)}</div>`].filter(Boolean).join('');
|
|
320
326
|
}
|
|
321
327
|
// The Hold sheet's picker (app-cards.js): /api/board's holdChoices, the chips hub/duration.mjs shares with Snooze and
|
|
322
328
|
// Pause hub, then Custom. Its .hub-view root takes hub.css's duration picker rules, which view-hub-settings.js
|
|
@@ -343,15 +349,16 @@
|
|
|
343
349
|
const PICKUP_WAIT = '<span class="pickup-wait"><span class="working-dot" aria-hidden="true"></span>waiting…</span>';
|
|
344
350
|
const askLine = (ask) => [ask.from && `Moved from ${esc(ask.from)}`, ask.edited && 'Edited', ask.state === 'working' ? thinkingLine() : ask.state === 'waiting' && PICKUP_WAIT].filter(Boolean).join(' · ');
|
|
345
351
|
function workLines(it, { unread = 0 } = {}) {
|
|
346
|
-
const
|
|
347
|
-
// A card shows its ticket, or Task when it has none; the card an ask became shows
|
|
348
|
-
const ids = it.isNew ? badge('notice', 'New') : ticket;
|
|
352
|
+
const ids = it.ticket ? ticketChip(it) : it.internal ? badge('quiet', esc(it.ref), { title: 'Work on this site, not a product PR' }) : it.linear?.length ? linearChips(it) : taskChip(it);
|
|
353
|
+
// A card shows its ticket, or Task when it has none; the card an ask became shows its ticket from the moment it lands.
|
|
349
354
|
const chips = `${prChips(it)}${reviewChips(it)}${it.risky ? badge('notice', 'risky') : ''}${installChip(it)}`;
|
|
350
355
|
const status = it.isNew && !it.action ? (askLine(it.ask) && `<div class="line3 sub">${askLine(it.ask)}</div>`)
|
|
356
|
+
// docs/adr/2026-11-12-0600-every-planner-launches-from-the-orchestrators-brief.md: preheating, planning prototype
|
|
357
|
+
// or planning visual while the orchestrator writes the brief, generating visual while a visual agent draws, in blue.
|
|
358
|
+
: it.planning && !it.builder ? `<div class="line3 sub agent planner">${DOT}${esc(it.planning)}</div>`
|
|
351
359
|
: it.orchestrator && !it.builder ? `${it.rebaseFailed ? `<div class="line3 sub">${escText(it.rebaseFailed)}</div>` : ''}${orchestratorStatus(it)}`
|
|
352
360
|
: it.waitsOn === 'checking' ? `<div class="line3 sub agent${tone(it)}">${DOT}Orchestrator review</div>`
|
|
353
361
|
: it.builder ? builderStatus(it)
|
|
354
|
-
: it.grilling || it.prototyping ? '<div class="line3 sub asked">preheating</div>'
|
|
355
362
|
: it.refusal ? `<div class="line3 sub refused">${escText(it.refusal)}</div>`
|
|
356
363
|
: it.landStopped ? `<div class="line3 sub">${escText(`${it.landStopped.words} · ${lasted(it.landStopped.at)}`)}</div>`
|
|
357
364
|
: it.working ? `<div class="line3 sub agent">${thinkingLine()}</div>`
|
package/board/public/ui-enums.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// badge shows (docs/adr/2026-11-03-1843-a-merged-card-reopens-and-its-pr-badge-shows-the-prs-state.md). Each value is its name,
|
|
4
4
|
// since cards carry it in the board's JSON. Needs nothing else: board/domain.mjs runs this file in a vm context (docs/adr/2026-10-30-1309-an-unstarted-card-waits-on-its-questions-in-todo.md).
|
|
5
5
|
(function () {
|
|
6
|
-
const BUTTON = Object.freeze({ openQuestions: 'openQuestions', github: 'github', merge: 'merge', reply: 'reply' });
|
|
7
|
-
const ACTION_KIND = Object.freeze({ pick: 'pick', questions: 'questions', ready: 'ready', steps: 'steps', merge: 'merge', stalled: 'stalled' });
|
|
6
|
+
const BUTTON = Object.freeze({ openQuestions: 'openQuestions', github: 'github', merge: 'merge', reply: 'reply', implement: 'implement' });
|
|
7
|
+
const ACTION_KIND = Object.freeze({ pick: 'pick', questions: 'questions', ready: 'ready', steps: 'steps', merge: 'merge', stalled: 'stalled', implement: 'implement' });
|
|
8
8
|
const BUILDER_KIND = Object.freeze({ install: 'install' });
|
|
9
9
|
const PR_STATE = Object.freeze({ open: 'open', draft: 'draft', merged: 'merged', closed: 'closed' });
|
|
10
10
|
// Each lane's phases, in order, which a card stores beside its lane (docs/adr/2026-11-11-1200-every-lane-has-phases.md).
|
|
11
11
|
// In Progress's are the five build steps its progress bar draws; a review's "reviewers" and "merge" read "pending review"
|
|
12
12
|
// and "pending merge" on the card (ui-cards.js PHASE_LOOK).
|
|
13
13
|
const PHASES = Object.freeze({
|
|
14
|
-
todo: Object.freeze(['new', 'grilling', 'prototyping', 'staging', 'ready']),
|
|
14
|
+
todo: Object.freeze(['new', 'discussing', 'grilling', 'prototyping', 'staging', 'ready']),
|
|
15
15
|
progress: Object.freeze(['reading', 'building', 'testing', 'checking', 'landing']),
|
|
16
16
|
review: Object.freeze(['draft', 'reviewers', 'merge', 'stale']),
|
|
17
17
|
done: Object.freeze(['merged', 'closed']),
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
// lead is 'logo' (the kickflip, which toggles the sidebar, sidebar.js; logoHref, the hub's page, without it)
|
|
48
48
|
// or 'back' (the arrow, which follows backStep, with backHref the work item or the board, after the logo). The controls (time and refresh) paint disabled with refresh spinning until the
|
|
49
49
|
// page's script turns them on (docs/adr/2026-10-29-1639-the-board-shows-github-checks-and-watcher-health.md). With controls
|
|
50
|
-
// false, trail (HTML) takes their place. pages is the switch between a board's two pages
|
|
50
|
+
// false, trail (HTML) takes their place. pages is the switch between a board's two pages
|
|
51
51
|
// (docs/adr/2026-11-07-1300-board-and-open-questions-are-two-pages.md): 'questions' puts the ? after the controls, with
|
|
52
|
-
// the open-question count in its badge (app-
|
|
52
|
+
// the open-question count in its badge (app-header.js), and 'board' puts the check, back up to the board, after trail. leadAttrs adds to or replaces the lead link's attributes, and level is the
|
|
53
53
|
// title's heading level, for a header that isn't the page's own, such as the sidebar's (ui-sidebar.js). moon puts the
|
|
54
54
|
// away moon right of the title, hidden until app-header.js renderStatus shows it (docs/adr/2026-11-04-0226-the-hub-checks-every-minute-while-youre-here-and-every-5-when-away.md).
|
|
55
55
|
function header({ lead = 'logo', title = '', titleId, backHref = './', backLabel = 'Back to the board', backId, logoSrc = 'kickflip.png',
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// '<question index>:<label>'. It waits (ui.js BUSY) and the line's other buttons are disabled.
|
|
23
23
|
function promptItem(p, { picks = {}, sending = null } = {}) {
|
|
24
24
|
const off = (tapped) => (sending === null ? '' : sending === tapped ? BoardUI.BUSY : ' disabled');
|
|
25
|
-
const where = [ago(p.at), p.cwd].filter(Boolean).map(esc).join(' · ');
|
|
25
|
+
const where = [p.board, ago(p.at), p.cwd].filter(Boolean).map(esc).join(' · ');
|
|
26
26
|
const head = `<div class="prompt-head"><b>${esc(p.questions ? 'Question' : p.tool)}</b><span class="muted small">${where}</span></div>`;
|
|
27
27
|
if (p.questions) {
|
|
28
28
|
const questions = p.questions.map((q, i) => `<div class="prompt-q"><p>${q.header ? `<span class="chip">${esc(q.header)}</span> ` : ''}${esc(q.question)}</p>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// resolved it. A sent question draws as resolvedCard, locked, until its badge reopens it.
|
|
22
22
|
const isSent = (q) => q.state === 'answered' || q.state === 'replied' || q.state === 'resolved';
|
|
23
23
|
// A question's unsent {choice, note} on this device, in localStorage under qdraft:<id>. view-questions.js and the
|
|
24
|
-
//
|
|
24
|
+
// prototype viewer's Choose this (prototype-switcher.js) write it.
|
|
25
25
|
const getDraft = (id) => { try { return JSON.parse(localStorage.getItem('qdraft:' + id) || '{}'); } catch { return {}; } };
|
|
26
26
|
const setDraft = (id, v) => { try { localStorage.setItem('qdraft:' + id, JSON.stringify(v)); } catch {} };
|
|
27
27
|
const suggestedLine = (q, o, links) => q.suggested?.option === o.id ? linkText(q.suggested.reason ?? `you answered this on ${q.suggested.card}: "${q.suggested.answer}"`, links) : '';
|
|
@@ -178,8 +178,7 @@
|
|
|
178
178
|
},
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
-
// A prototype viewer's Round pill and its sheet
|
|
182
|
-
// (app-needs.js). rounds: the card's prototype questions, oldest round first, as /api/board's card prototypes.rounds
|
|
181
|
+
// A prototype viewer's Round pill and its sheet (prototype-switcher.js). rounds: the card's prototype questions, oldest round first, as /api/board's card prototypes.rounds
|
|
183
182
|
// lists them ({n, q, url, confirm, state}). The pill names the round qid is in, out of the newest round's number; a
|
|
184
183
|
// card with a single round gets none.
|
|
185
184
|
function roundPill(rounds, qid) {
|
package/board/public/ui.js
CHANGED
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
'ask-link': mini('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
|
|
107
107
|
move: mini('<path d="M3 7h14"/><path d="m13 3 4 4-4 4"/><path d="M21 17H7"/><path d="m11 13-4 4 4 4"/>'),
|
|
108
108
|
'Implementation requested': mini('<path d="m8 6-6 6 6 6"/><path d="m16 6 6 6-6 6"/>'),
|
|
109
|
+
'Marked as discussing': mini('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
|
|
109
110
|
'Marked as staging': mini('<path d="m12 2 10 5-10 5L2 7z"/><path d="m2 12 10 5 10-5"/><path d="m2 17 10 5 10-5"/>'),
|
|
110
111
|
'Marked as ready': mini('<path d="M4 22V4"/><path d="M4 4h13l-2 4 2 4H4"/>'),
|
|
111
112
|
Held: mini('<path d="M9 5v14"/><path d="M15 5v14"/>'),
|
|
@@ -406,11 +407,18 @@
|
|
|
406
407
|
// A view's place when it has no copy to show: the kickflip mark and down's two lines, as the no-access page has them.
|
|
407
408
|
const downLanding = (down) => `<div class="down-landing" data-down="${down.state}"><img class="down-landing-mark" src="/_hub/kickflip.png" width="70" height="60" alt="">
|
|
408
409
|
<p class="down-landing-line">${esc(down.line)}</p><p class="down-landing-hint">${esc(down.hint)}</p></div>`;
|
|
410
|
+
// The landing for a board that loaded but threw while drawing (docs/adr/2026-11-12-0400-a-board-that-throws-while-drawing-shows-a-landing-with-the-error.md):
|
|
411
|
+
// the error's message, its stack behind Show details, Reload and Copy error (app.js wires them).
|
|
412
|
+
const drawLanding = (err) => `<div class="down-landing" data-down="draw"><img class="down-landing-mark" src="/_hub/kickflip.png" width="70" height="60" alt="">
|
|
413
|
+
<p class="down-landing-line">This board couldn't draw</p><p class="down-landing-hint">The board loaded, but it threw while drawing. Your data is fine.</p>
|
|
414
|
+
<p class="down-landing-hint"><b>${esc(err?.name || 'Error')}:</b> ${esc(err?.message ?? String(err))}</p>
|
|
415
|
+
<div class="down-landing-buttons"><button type="button" data-draw="reload">Reload</button> <button type="button" data-draw="copy">Copy error</button> <button type="button" data-draw="more">Show details</button></div>
|
|
416
|
+
<pre class="down-landing-stack" hidden>${esc(err?.stack || '')}</pre></div>`;
|
|
409
417
|
// The line over a side page painted from its copy while the board can't be reached.
|
|
410
418
|
const copyNote = (what, savedAt) => `<p class="note copy-note">Copy of ${esc(what)} as of ${since(savedAt)}. Read only.</p>`;
|
|
411
419
|
// The buttons that send, which a board out of reach holds (style.css .offline): every one greyed and stopped, keeping its
|
|
412
420
|
// label and size. Reading controls, folds, links, Edit and drafts stay live.
|
|
413
|
-
const SENDS = `form button[type="submit"], .actions button, button[data-confirm], button[data-
|
|
421
|
+
const SENDS = `form button[type="submit"], .actions button, button[data-confirm], 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-try-kill], button[data-reopen]`;
|
|
414
422
|
// While held(), a tap on one of root's sending buttons, and any form's submit there, Enter's included, goes nowhere, and onHeld
|
|
415
423
|
// tells the tap it is waiting. on is the view's B.on.
|
|
416
424
|
function holdSends(on, root, held, onHeld) {
|
|
@@ -464,7 +472,7 @@
|
|
|
464
472
|
|
|
465
473
|
globalThis.BoardUI = {
|
|
466
474
|
LINK_ARROW, escText, onBackdrop,
|
|
467
|
-
esc, record, time, ago, lasted, since, clock, etaLine, cardTime, replyBody, setHTML, closeDetails, BUSY, waitOn, pending, fetchJSON, WAITING, RESTARTING, restartNotice, downOf, downLanding, copyNote, SENDS, holdSends,
|
|
475
|
+
esc, record, time, ago, lasted, since, clock, etaLine, cardTime, replyBody, setHTML, closeDetails, BUSY, waitOn, pending, fetchJSON, WAITING, RESTARTING, restartNotice, downOf, downLanding, drawLanding, copyNote, SENDS, holdSends,
|
|
468
476
|
attachments, attachButton, dropZone, sendButton, keyedSend, pendingFiles, protoButton, grillButton, visualButton, mark, statusMark, MARK_ICONS, withdrawButton, editButton, cardBumpButton, workingLine, thinkingLine, isPaused, PAUSED, BADGE_TONES, badge,
|
|
469
477
|
};
|
|
470
478
|
})();
|
package/board/public/view-hub.js
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
? HubUI.row({ icon: HubUI.tile('coffee', 'var(--color-quiet)'), title: 'Keep awake', sub: esc(a.unsupported), wrap: true, attrs: ' id="keep-awake"' })
|
|
66
66
|
: `<form class="row field" id="keep-awake" method="post" action="/_hub/settings/keep-awake">${HubUI.tile('coffee', a.on ? 'var(--color-notice)' : 'var(--color-quiet)')}<label class="body"><span class="text"><span class="title">Keep awake</span><span class="sub wrap">${AWAKE_NOTE}</span></span><input type="checkbox" id="awake" name="keep_awake" value="on" class="toggle" data-submit${a.on ? ' checked' : ''}></label></form>`;
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Every pending prompt waits here, a board session's naming its board (prompts.js).
|
|
69
69
|
function promptsHtml(list, prompts) {
|
|
70
70
|
const shown = list.filter(prompts.shows);
|
|
71
71
|
return shown.length ? `<section class="waiting"><h2>Needs you · ${shown.length}</h2><ul>${shown.map((p) => BoardUI.promptItem(p, prompts.state(p))).join('')}</ul></section>` : '';
|