@lovek4d/kickflip 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +69 -37
- package/SKILL.md +19 -13
- package/bin/kickflip.mjs +12 -12
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +156 -83
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -5
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +27 -13
- package/board/public/app-header.js +6 -7
- package/board/public/app-lanes.js +15 -38
- package/board/public/app-needs.js +36 -31
- package/board/public/app-snapshot.js +43 -70
- package/board/public/app-threads.js +38 -46
- package/board/public/app.html +9 -4
- package/board/public/app.js +78 -32
- package/board/public/hub-kit.js +62 -2
- package/board/public/hub.css +2 -0
- package/board/public/new-task.js +8 -6
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +109 -4
- package/board/public/prototype-switcher.js +123 -29
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +64 -24
- package/board/public/sidebar.css +1 -0
- package/board/public/sidebar.js +16 -69
- package/board/public/style.css +108 -57
- package/board/public/ui-cards.js +61 -46
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-header.js +25 -16
- package/board/public/ui-hub.js +12 -1
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +25 -17
- package/board/public/ui-sidebar.js +25 -23
- package/board/public/ui-snapshot.js +43 -52
- package/board/public/ui-threads.js +57 -28
- package/board/public/ui.js +92 -17
- package/board/public/view-hub-settings.js +12 -58
- package/board/public/view-hub.js +2 -1
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +90 -68
- package/board/public/view-settings.js +133 -15
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +19 -19
- package/board/stop.mjs +1 -0
- package/board/store.mjs +27 -18
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +22 -21
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/board/public/style.css
CHANGED
|
@@ -82,8 +82,10 @@ button.chip { border: 0; }
|
|
|
82
82
|
.status.review { border-color: var(--color-info); background: conic-gradient(var(--color-info) 0 75%, transparent 75% 100%); }
|
|
83
83
|
.sub.agent { color: var(--color-notice); }
|
|
84
84
|
.sub.next { color: var(--color-notice); }
|
|
85
|
-
.card-hold
|
|
85
|
+
.card-hold { color: var(--color-quiet); }
|
|
86
|
+
.card-blocked, .card-action { color: var(--color-notice); }
|
|
86
87
|
.card-ready { color: var(--color-success); }
|
|
88
|
+
.card-staging { color: var(--color-notice); }
|
|
87
89
|
.sub.next.idle { color: var(--muted); }
|
|
88
90
|
.sub.refused { color: var(--color-danger); }
|
|
89
91
|
.toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 12px + env(safe-area-inset-bottom));
|
|
@@ -166,7 +168,10 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
166
168
|
.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; }
|
|
167
169
|
.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; }
|
|
168
170
|
.wb-counts:hover .wb-tip, .wb-counts.open .wb-tip { display: block; }
|
|
169
|
-
.wb-counts .wv-cur { font-weight: 700;
|
|
171
|
+
.wb-counts .wv-cur { font-weight: 700; }
|
|
172
|
+
.wb-counts .wv-review { color: var(--color-info); }
|
|
173
|
+
.wb-counts .wv-progress { color: var(--color-notice); }
|
|
174
|
+
.wb-counts .wv-todo, .wb-counts .wv-done { color: var(--color-quiet); }
|
|
170
175
|
.wb-lane + .wb-lane, .wb-lane + .wb-loose { border-top: 1px solid var(--line); }
|
|
171
176
|
.wb-loosehead:hover { background: var(--band); }
|
|
172
177
|
/* A wave card's step number, inside its id chip, and its ⋯ menu. */
|
|
@@ -186,8 +191,9 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
186
191
|
.refresh #refresh { padding: 4px; margin-left: -4px; flex: none; }
|
|
187
192
|
.refresh #status { font-size: 12px; min-width: 0; }
|
|
188
193
|
.refresh .late { color: var(--color-notice); font-weight: 600; }
|
|
189
|
-
|
|
190
|
-
.refresh .
|
|
194
|
+
/* The header's time keeps its text; a stopped or offline board turns it and the refresh arrow red, paused usage orange. */
|
|
195
|
+
.refresh .down, .refresh:has(.down) #refresh { color: var(--color-danger); }
|
|
196
|
+
.refresh .hold, .refresh:has(.hold) #refresh { color: var(--color-notice); }
|
|
191
197
|
.lane-github { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--muted); text-align: right; }
|
|
192
198
|
.lane-github .late { color: var(--color-notice); }
|
|
193
199
|
.msg.other { background: transparent; border: 1px solid var(--line); }
|
|
@@ -328,14 +334,17 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
328
334
|
}
|
|
329
335
|
.working-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--color-notice); margin-right: 5px; vertical-align: 1px; }
|
|
330
336
|
@media (prefers-reduced-motion: no-preference) { .working-dot { animation: pulse 1.4s ease-in-out infinite; } }
|
|
331
|
-
|
|
332
|
-
|
|
337
|
+
/* A thread box's row: What's left? at the left, the eye, fire and attach at the right, then Send, or a pair below
|
|
338
|
+
(docs/adr/2026-11-05-1918-done-shows-only-in-in-review-beside-reply.md). */
|
|
339
|
+
form[data-reply] .row .quick { margin: 0; }
|
|
340
|
+
form[data-reply] .icons { display: inline-flex; flex: none; margin-left: auto; }
|
|
341
|
+
.dz.bare > textarea { padding-bottom: 12px; }
|
|
333
342
|
/* A composer's send buttons (ui.js sendButton), each with its key as a small gray hint where there is a keyboard and
|
|
334
343
|
room. A pair takes a row of two halves; a Todo card's reply box wraps its pair under the row. */
|
|
335
344
|
.send-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
|
|
336
345
|
.send-actions button, [data-sends] button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
|
337
346
|
.send-actions button { min-height: 44px; }
|
|
338
|
-
form[data-reply] .row { flex-wrap: wrap; }
|
|
347
|
+
form[data-reply] .row { flex-wrap: wrap; gap: 6px; }
|
|
339
348
|
form[data-reply] .row > .send-actions { flex: 0 0 100%; margin-top: 4px; }
|
|
340
349
|
.key-hint { display: none; font-size: 11px; font-weight: 400; color: var(--muted); white-space: nowrap; }
|
|
341
350
|
button.primary .key-hint { color: color-mix(in srgb, var(--on-accent) 62%, transparent); }
|
|
@@ -355,15 +364,18 @@ button.attach:disabled { opacity: .35; }
|
|
|
355
364
|
pixel off its siblings (docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md). */
|
|
356
365
|
.card-time .phase-bars { display: inline-flex; gap: 2px; vertical-align: middle; position: relative; top: -1px; }
|
|
357
366
|
.phase-bars i { width: 9px; height: 4px; border-radius: 2px; background: var(--line); }
|
|
358
|
-
/* A strip dims
|
|
359
|
-
(docs/adr/2026-11-03-2024-every-in-progress-card-shows-its-phase-bars.md). app-cards.js
|
|
367
|
+
/* A strip dims while the card waits on something other than a builder; ui-progress-bar.js then writes its running bar
|
|
368
|
+
as done, so it stops blinking (docs/adr/2026-11-03-2024-every-in-progress-card-shows-its-phase-bars.md). app-cards.js
|
|
369
|
+
sweeps bars in --bar-fill. */
|
|
360
370
|
.phase-bars { --bar-fill: var(--color-notice); }
|
|
361
371
|
.phase-bars.dim { --bar-fill: color-mix(in srgb, var(--muted) 55%, transparent); }
|
|
362
372
|
.phase-bars i.done, .phase-bars i.now { background: var(--bar-fill); }
|
|
363
373
|
/* A bar part way, to --p: a starting board's clone (ui-sidebar.js). */
|
|
364
374
|
.phase-bars i.fill { background: linear-gradient(to right, var(--bar-fill) var(--p), var(--line) var(--p)); }
|
|
365
|
-
|
|
366
|
-
@
|
|
375
|
+
/* The pulse dims the bar's own fill, so it pulses in its lane's colour. */
|
|
376
|
+
@keyframes pulse-fill { 50% { background-color: color-mix(in srgb, var(--bar-fill) 35%, transparent); } }
|
|
377
|
+
/* Every pulse beats on one clock (ui-progress-bar.js onePulse), so a card's running bar and its status dot blink together. */
|
|
378
|
+
@media (prefers-reduced-motion: no-preference) { .phase-bars i.now { animation: pulse-fill 1.4s ease-in-out infinite; } }
|
|
367
379
|
/* A stale build's status line, dot included, is the needs-you red, and a tap on it nudges Claude (ui-cards.js builderStatus). */
|
|
368
380
|
/* A status line that is a button is drawn as the div it replaces: this one reset, then only a colour per state. The font
|
|
369
381
|
shorthand stays out so .sub's 12px wins. It sets no width: a button is its own block formatting context, so a full-width one
|
|
@@ -390,26 +402,33 @@ button.sub.agent.stalled.nudging { color: var(--color-notice); cursor: default;
|
|
|
390
402
|
.msg.agent { padding: 0 10px; margin: 4px 0; font-size: 12px; font-weight: 600; overflow-wrap: anywhere; }
|
|
391
403
|
.msg.agent.started { color: var(--color-notice); } .msg.agent.finished { color: var(--color-success); }
|
|
392
404
|
.msg.agent .muted { font-weight: 400; }
|
|
393
|
-
/* A planner's line, dot included, is the owner's blue
|
|
394
|
-
(docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). */
|
|
395
|
-
.
|
|
405
|
+
/* A planner's line in the thread, dot included, is the owner's blue; its finish stays green
|
|
406
|
+
(docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). On the card it takes the lane's colour below. */
|
|
407
|
+
.msg.builder-status.planner, .msg.agent.started.planner { color: var(--accent); }
|
|
396
408
|
.planner .working-dot { background: currentColor; }
|
|
397
|
-
/*
|
|
398
|
-
|
|
399
|
-
(docs/adr/2026-11-
|
|
409
|
+
/* A run's status line, dot included, takes its card's lane colour, a planner's too: grey in Todo, the phase-bar orange in
|
|
410
|
+
In Progress, blue in In Review; a Todo card's planner dashes are its grey, the running one pulsing
|
|
411
|
+
(docs/adr/2026-11-06-0145-every-run-takes-its-lanes-colour.md). */
|
|
412
|
+
.sub.agent.in-todo { color: var(--muted); }
|
|
400
413
|
.sub.agent.in-progress { color: var(--color-notice); }
|
|
401
|
-
.sub.agent.in-
|
|
402
|
-
.sub.agent.in-progress .working-dot, .sub.agent.in-
|
|
414
|
+
.sub.agent.in-review { color: var(--color-info); }
|
|
415
|
+
.sub.agent.in-todo .working-dot, .sub.agent.in-progress .working-dot, .sub.agent.in-review .working-dot { background: currentColor; }
|
|
403
416
|
/* docs/adr/2026-11-05-0410-a-card-status-word-comes-from-a-signal-the-board-checks.md: a quiet builder and a request no planner
|
|
404
417
|
has picked up are grey, after the lane colours so they win. */
|
|
405
418
|
.sub.agent.quiet, .sub.asked { color: var(--muted); }
|
|
406
|
-
.
|
|
407
|
-
|
|
408
|
-
|
|
419
|
+
.sub.agent.waiting-gray { color: var(--muted); }
|
|
420
|
+
/* The one class every paused status takes, gray: the card and thread line, the header, the hub banner. */
|
|
421
|
+
.paused { color: var(--muted); font-weight: 600; }
|
|
422
|
+
.phase-bars.in-todo { --bar-fill: var(--muted); }
|
|
409
423
|
.msg.calls { padding: 0 10px; margin: 4px 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
410
424
|
.msg.calls summary { cursor: pointer; color: var(--muted); list-style: none; }
|
|
411
425
|
.msg.calls summary::-webkit-details-marker { display: none; }
|
|
412
426
|
.msg.calls[open] summary .mark-icon { transform: rotate(90deg); }
|
|
427
|
+
.msg.calls.status-fold summary { color: var(--color-notice); overflow-wrap: anywhere; }
|
|
428
|
+
.msg.calls.status-fold.planner summary { color: var(--accent); }
|
|
429
|
+
/* Only a run's finished line is green: the fold's summary mirrors its last line, green once the run has finished, else the
|
|
430
|
+
started and status lines' orange or a planner's blue (docs/adr/2026-11-07-1600-only-a-runs-finished-line-is-green.md). */
|
|
431
|
+
.msg.calls.status-fold summary.finished { color: var(--color-success); }
|
|
413
432
|
.msg.calls ol { margin: 4px 0; padding-left: 23px; }
|
|
414
433
|
.agent-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 5px; vertical-align: 1px; }
|
|
415
434
|
/* Every thread status line's leading mark, icon or dot, sits centred in one 18px box, so the text after it always starts
|
|
@@ -426,7 +445,6 @@ button.sub.agent.stalled.nudging { color: var(--color-notice); cursor: default;
|
|
|
426
445
|
.msg .reply { display: flex; flex-wrap: wrap; align-items: center; column-gap: 12px; row-gap: 2px; }
|
|
427
446
|
.msg-foot .foot-status { display: inline-flex; align-items: center; }
|
|
428
447
|
/* The thread's eye and fire are toggles: pressed reads as chosen, and the hint under the box says what Send posts. */
|
|
429
|
-
form[data-reply] .tools { display: inline-flex; flex: none; }
|
|
430
448
|
button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chip); }
|
|
431
449
|
/* INT-30: card line order */
|
|
432
450
|
.line6 { margin-top: 4px; }
|
|
@@ -444,17 +462,25 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
444
462
|
.lane[open] > .list { border-radius: 0 0 10px 10px; }
|
|
445
463
|
.lane > summary .count { color: var(--muted); font-weight: 400; }
|
|
446
464
|
/* Each lane's sort icon and its menu, grey (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md) */
|
|
447
|
-
/*
|
|
448
|
-
.view
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
.shared-view #compose,
|
|
453
|
-
.shared-
|
|
465
|
+
/* A board shared with someone else (docs/adr/2026-11-05-2215-a-board-is-shared-view-only-with-a-tailscale-login.md):
|
|
466
|
+
shell.js marks the page .shared-view, which hides Board settings and the architecture review, and, unless the person
|
|
467
|
+
has Full on this board (.shared-full; docs/adr/2026-11-05-1912-a-shared-person-is-view-or-full.md), the +, every
|
|
468
|
+
composer and a card's buttons, keeping its links (Visit, Notes, Open questions). Merge shows disabled under Full. */
|
|
469
|
+
.shared-view :is(#settingsLink, #archBulb) { display: none !important; }
|
|
470
|
+
.shared-view:not(.shared-full) :is(#compose, [data-composer], .actions button, button[data-confirm]) { display: none !important; }
|
|
471
|
+
.shared-full button[data-confirm="merge"] { opacity: .45; pointer-events: none; }
|
|
454
472
|
.view-only-strip { margin: 8px 0; padding: 8px 12px; border-radius: 10px; background: var(--accent-soft); font-size: 13px; }
|
|
455
473
|
.view-only-strip b { color: var(--accent); }
|
|
456
|
-
|
|
457
|
-
|
|
474
|
+
/* 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
|
+
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-band-move], 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
|
+
/* 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
|
+
.down-landing { min-height: 60vh; display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; text-align: center; }
|
|
479
|
+
.down-landing-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
|
|
480
|
+
.down-landing-line { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
|
|
481
|
+
.down-landing-hint { margin: 0; font-size: 14px; color: var(--muted); }
|
|
482
|
+
.view-board.down main > :not(#downLanding), .view-board.down :is(#needsPane, #compose) { display: none; }
|
|
483
|
+
.view-settings.down .settings-body > :not(.down-landing) { display: none; }
|
|
458
484
|
/* INT-28: answer banner */
|
|
459
485
|
.answer-banner { display: flex; align-items: center; gap: 4px; background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
460
486
|
border-radius: 10px; padding: 2px 4px 2px 0; margin-bottom: 12px; }
|
|
@@ -500,11 +526,21 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
500
526
|
.top .nav #refresh, .top .nav a.icon { display: inline-flex; align-items: center; justify-content: center; margin: 0; padding: 4px; border: 0; line-height: 1; }
|
|
501
527
|
.top .nav #status { line-height: 1.1; white-space: nowrap; }
|
|
502
528
|
.top .nav #refresh:disabled, .top .nav a.icon[aria-disabled="true"] { opacity: .35; pointer-events: none; }
|
|
503
|
-
/*
|
|
504
|
-
|
|
505
|
-
.
|
|
506
|
-
.
|
|
507
|
-
.
|
|
529
|
+
/* The foot of the board (docs/adr/2026-11-07-1300-board-and-open-questions-are-two-pages.md): the bulb and the gear, centred under the lanes.
|
|
530
|
+
[the bulb runs an architecture review of the board's repo](docs/adr/2026-11-02-1813-the-bulb-runs-an-architecture-review-of-the-boards-repo.md): it glows orange while an architecture review runs */
|
|
531
|
+
.board-foot { display: flex; justify-content: center; gap: 8px; margin: 20px 0 0; }
|
|
532
|
+
.board-foot .arch-bulb, .board-foot a.icon { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; margin: 0; padding: 4px; border: 0; line-height: 1; color: var(--muted); }
|
|
533
|
+
.board-foot :is(.arch-bulb, a.icon):hover { color: var(--text); }
|
|
534
|
+
.board-foot .arch-bulb[hidden] { display: none; }
|
|
535
|
+
.board-foot .arch-bulb.running { color: var(--color-notice); }
|
|
536
|
+
.board-foot a.icon[aria-disabled="true"] { opacity: .35; pointer-events: none; }
|
|
537
|
+
/* The pages switch below 900px: the board header's ? with the open-question count on its corner, and Waiting on you's check back to the board. */
|
|
538
|
+
.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); }
|
|
539
|
+
.top .pages-switch:hover { color: var(--text); }
|
|
540
|
+
.top .pages-switch:active { background: none; }
|
|
541
|
+
.pages-switch .q-badge { position: absolute; top: 0; right: -2px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--color-notice); color: #1c1407;
|
|
542
|
+
font-size: 11px; font-weight: 700; line-height: 16px; text-align: center; box-shadow: 0 0 0 1.5px var(--bg); }
|
|
543
|
+
.pages-switch .q-badge[hidden] { display: none; }
|
|
508
544
|
.arch-bulb.running svg { filter: drop-shadow(0 0 4px currentColor); }
|
|
509
545
|
@media (prefers-reduced-motion: no-preference) { .arch-bulb.running svg { animation: arch-glow 1.4s ease-in-out infinite; } }
|
|
510
546
|
@keyframes arch-glow { 50% { filter: drop-shadow(0 0 1px currentColor); opacity: .55; } }
|
|
@@ -514,8 +550,6 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
514
550
|
.arch-last .muted { font-size: 12.5px; }
|
|
515
551
|
.arch-body .arch-waits { color: var(--color-notice); }
|
|
516
552
|
.arch-body .arch-run { width: 100%; min-height: 44px; font-size: 15px; }
|
|
517
|
-
/* INT-64: offline, the header's time and refresh turn red */
|
|
518
|
-
.view-board.offline .refresh #refresh, .refresh .link-state.offline { color: var(--color-danger); }
|
|
519
553
|
.back { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; color: var(--accent); text-decoration: none; }
|
|
520
554
|
/* The lead slot, logo or back, is one 44px tap box on the page's left edge, so every page's header lines up the same
|
|
521
555
|
(docs/adr/2026-10-29-1639-a-questions-takes-show-inline.md). The brand keeps that height when the rail hides the logo, so the
|
|
@@ -526,15 +560,10 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
526
560
|
.top .hub + .back { margin-left: 0; }
|
|
527
561
|
.top .back:active { background: var(--accent-soft); }
|
|
528
562
|
.top .brand :is(h1, h2) { font-size: 17px; font-weight: 600; line-height: 22px; }
|
|
529
|
-
/*
|
|
530
|
-
|
|
531
|
-
a blue tint that fades (app-snapshot.js, whose SIGNS holds each animation's length). */
|
|
563
|
+
/* Every change, a line, a moved card or an updated card, open body included, takes the one blue tint that fades
|
|
564
|
+
(app-snapshot.js highlight, whose TINT_MS is its length). */
|
|
532
565
|
main > * { transition: opacity .25s ease; }
|
|
533
|
-
.
|
|
534
|
-
.view-board[data-board="snapshot"] :is(.actions button, button[data-plan], button[data-bump], button[data-confirm="bump"], button[data-edit], button[data-withdraw], button[data-check-github], form[data-reply] button[type=submit], form[data-reply] button[data-quick], form[data-reply] button[data-proto], form[data-reply] button[data-confirm], .prompt button) { opacity: .45; pointer-events: none; }
|
|
535
|
-
.flash { animation: snapshot-flash 1.6s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
|
|
536
|
-
@keyframes snapshot-flash { from { box-shadow: inset 0 0 0 2px var(--color-info); } to { box-shadow: inset 0 0 0 2px transparent; } }
|
|
537
|
-
.item.tint { animation: card-tint 2.4s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
|
|
566
|
+
.tint { animation: card-tint 2.4s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
|
|
538
567
|
@keyframes card-tint { 0%, 25% { background: var(--accent-soft); } 100% { background: transparent; } }
|
|
539
568
|
.msg .quote { margin: 4px 0 6px; padding: 2px 0 2px 8px; border-left: 2px solid var(--line); color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
540
569
|
/* [one board registry for every hub feature](docs/adr/2026-10-29-1639-one-board-registry-for-every-hub-feature.md): the GitHub mark opens the Hub links sheet, with a red dot while a link is broken; a new board's setup checklist */
|
|
@@ -565,6 +594,9 @@ main > * { transition: opacity .25s ease; }
|
|
|
565
594
|
.view-settings .add input { flex: 1; min-width: 0; }
|
|
566
595
|
.view-settings .add.pick { flex-wrap: wrap; }
|
|
567
596
|
.view-settings .share-now { color: var(--ok); font-weight: 600; }
|
|
597
|
+
.view-settings .share-level { display: flex; gap: 4px; margin-top: 6px; }
|
|
598
|
+
.view-settings .share-level button { min-height: 32px; padding: 4px 12px; font-size: 13px; }
|
|
599
|
+
.view-settings .share-level button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
568
600
|
.view-settings .share-now::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: currentColor; vertical-align: 1px; }
|
|
569
601
|
.view-settings .share-reach { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 10px 0 4px; }
|
|
570
602
|
.view-settings .share-reach p { flex: 1 1 220px; margin: 0; }
|
|
@@ -579,6 +611,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
579
611
|
.view-settings label.who .sub { display: block; margin-top: 2px; }
|
|
580
612
|
.view-settings input.toggle { flex: none; width: 1.4rem; height: 1.4rem; margin: 0; accent-color: var(--accent); }
|
|
581
613
|
.view-settings input.count { flex: none; width: 4.5em; text-align: center; }
|
|
614
|
+
.view-settings label.who select { flex: none; max-width: 45%; min-height: 36px; }
|
|
582
615
|
/* A settings switch waiting on the server (ui.js waitOn) keeps its face, disabled, with a spinner beside it
|
|
583
616
|
(docs/adr/2026-11-03-1921-board-settings-grant-a-boards-session-permissions.md) */
|
|
584
617
|
.view-settings .toggle-slot { flex: none; display: inline-flex; align-items: center; gap: 8px; }
|
|
@@ -599,6 +632,14 @@ main > * { transition: opacity .25s ease; }
|
|
|
599
632
|
.view-settings .close-board-warning:has(li) { border: 1px solid var(--color-danger); padding: 0 12px; border-radius: 10px; margin-bottom: 14px; }
|
|
600
633
|
.view-questions .q { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px; margin: 12px 0; }
|
|
601
634
|
.view-questions .q.urgent { border-color: var(--color-notice); }
|
|
635
|
+
/* docs/adr/2026-11-05-2331-a-sent-answer-is-resolved-in-place-until-the-page-reloads.md
|
|
636
|
+
A requested question takes an accent tint and a sent or resolved one a green tint, each with its border drawn inside
|
|
637
|
+
the card as well, so the card keeps its size. */
|
|
638
|
+
.view-questions .q.pr-busy { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--panel)); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
639
|
+
.view-questions .q.pr-busy .opt { opacity: .55; cursor: default; }
|
|
640
|
+
.view-questions .q.pr-busy .opt:has(input:checked) { opacity: 1; }
|
|
641
|
+
.view-questions .q.resolved { border-color: var(--color-success); background: color-mix(in srgb, var(--color-success) 9%, var(--panel)); box-shadow: inset 0 0 0 1px var(--color-success); }
|
|
642
|
+
.view-questions .q.resolved .sent-line { margin: 6px 0; font-size: 13px; color: var(--color-success); }
|
|
602
643
|
.view-questions .q h3 { font-size: 15px; margin: 6px 0 4px; line-height: 1.3; }
|
|
603
644
|
.view-questions .ctx { color: var(--muted); font-size: 13px; margin-bottom: 8px; }
|
|
604
645
|
.view-questions .opt { display: grid; grid-template-columns: 22px 1fr; gap: 6px; padding: 8px; border: 1px solid var(--line);
|
|
@@ -620,20 +661,17 @@ main > * { transition: opacity .25s ease; }
|
|
|
620
661
|
.view-questions .topics button { min-width: 0; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
|
|
621
662
|
background: var(--panel); color: var(--text); white-space: normal; text-align: left; overflow-wrap: anywhere; }
|
|
622
663
|
.view-questions .topics button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
664
|
+
.view-questions .topics button.progress { border-color: var(--line); background: color-mix(in srgb, var(--color-notice) 12%, var(--panel)); }
|
|
665
|
+
.view-questions .topics button.progress[aria-pressed="true"] { border-color: var(--accent); background: color-mix(in srgb, var(--color-notice) 12%, var(--accent-soft)); }
|
|
623
666
|
@keyframes pill-arrive { from { opacity: 0; transform: translateX(-12px); box-shadow: 0 0 0 3px var(--accent); } to { opacity: 1; transform: none; box-shadow: 0 0 0 0 transparent; } }
|
|
624
667
|
@media (prefers-reduced-motion: no-preference) { .view-questions .topics button.arrived { animation: pill-arrive .6s ease-out; } }
|
|
625
668
|
.view-questions .topics .n { color: var(--muted); margin-left: 4px; }
|
|
626
669
|
.view-questions .topichead { margin: 2px 0 6px; }
|
|
627
670
|
.view-questions details.done > summary { cursor: pointer; color: var(--muted); font-size: 13px; margin: 16px 0 4px; }
|
|
628
|
-
.view-questions details.done .trow { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
629
|
-
.view-questions details.done button.ghost { font-size: 14px; text-align: left; padding: 6px 2px; }
|
|
630
|
-
.view-questions details.done .trow button { min-width: 0; }
|
|
631
|
-
.view-questions details.done .trow span { flex: none; }
|
|
632
671
|
.view-questions details.resolved-group > summary { list-style: none; min-height: 44px; display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 12px 0; }
|
|
633
672
|
.view-questions details.resolved-group > summary::-webkit-details-marker { display: none; }
|
|
634
673
|
.view-questions details.resolved-group > summary .caret { transition: transform .15s; }
|
|
635
674
|
.view-questions details.resolved-group[open] > summary .caret { transform: rotate(90deg); }
|
|
636
|
-
.view-questions details.resolved-group > summary .names { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
637
675
|
.view-questions .q.resolved button.chip { cursor: pointer; font: inherit; font-size: 12px; min-height: 28px; }
|
|
638
676
|
.view-questions .q.resolved button.chip:disabled { cursor: default; }
|
|
639
677
|
.view-questions .q.resolved .resolved-line { margin: 8px 0; }
|
|
@@ -647,10 +685,12 @@ main > * { transition: opacity .25s ease; }
|
|
|
647
685
|
.view-questions .bar .send-hint { margin-left: auto; }
|
|
648
686
|
/* The + rides above the sticky Send bar rather than over its Send. */
|
|
649
687
|
.view-questions .bar #qstatus { min-width: 0; }
|
|
650
|
-
/* Typing with the keyboard up: one compact row docked on the keyboard, and the + out of the way (view-questions.js dock). */
|
|
651
|
-
.view-questions.kb-up .bar { position: fixed; left: 0; right: 0; bottom: var(--kb, 0px); z-index: 7; padding: 6px 16px; box-shadow: 0 -6px 14px rgba(0,0,0,.12); }
|
|
652
|
-
.view-questions.kb-up .bar .send-hint { display: none; }
|
|
653
688
|
.view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
|
|
689
|
+
/* With no open question, the moon and its line sit at the centre of what the page shows between its top and the nav,
|
|
690
|
+
or of what the desktop pane has left under its Needs you lines. */
|
|
691
|
+
.view-questions .q-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--muted); text-align: center;
|
|
692
|
+
min-height: calc(var(--vh, 100dvh) - var(--app-bottom, 0px) - max(16px, env(safe-area-inset-top))); }
|
|
693
|
+
.view-questions .q-empty p { margin: 0; font-size: 15px; }
|
|
654
694
|
.view-questions .solo { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 4px 0; }
|
|
655
695
|
/* docs/adr/2026-11-02-1332-a-prototype-round-ends-with-a-one-take-confirm.md */
|
|
656
696
|
.view-questions .topichead { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
@@ -721,7 +761,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
721
761
|
position: fixed; top: auto; bottom: 0; left: 0; right: 0; z-index: 7;
|
|
722
762
|
border-bottom: 0; border-top: 1px solid var(--line); padding-top: 4px; padding-bottom: max(4px, env(safe-area-inset-bottom));
|
|
723
763
|
}
|
|
724
|
-
.top .nav #refresh, .top .nav a.icon, .top .
|
|
764
|
+
.top .nav #refresh, .top .nav a.icon, .top .pages-switch { min-width: 44px; min-height: 44px; }
|
|
725
765
|
body > main, body.has-sidebar > :not(.sidebar) > main, body.has-sidebar > :not(.sidebar) > .split > main { padding-top: max(16px, env(safe-area-inset-top)); padding-bottom: calc(96px + var(--app-bottom, 0px) + env(safe-area-inset-bottom)); }
|
|
726
766
|
.view-frame:not(.own-header) { padding-bottom: var(--app-bottom, 0px); }
|
|
727
767
|
.sheet { bottom: max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px)));
|
|
@@ -730,6 +770,9 @@ main > * { transition: opacity .25s ease; }
|
|
|
730
770
|
.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)))); }
|
|
731
771
|
}
|
|
732
772
|
|
|
773
|
+
/* Typing, a text field focused (shell.js sets :root.typing): the floating + is disabled and the page stays as it is. */
|
|
774
|
+
:root.typing .fab { opacity: .35; pointer-events: none; }
|
|
775
|
+
|
|
733
776
|
.settings-modal { position: fixed; inset: max(12px, env(safe-area-inset-top)) 0 calc(12px + max(var(--kb, 0px), var(--app-bottom, 0px))); margin: auto; width: min(680px, calc(100% - 24px));
|
|
734
777
|
height: fit-content; max-width: none; max-height: max(120px, calc(var(--vh, 100dvh) - max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px))) - 24px - env(safe-area-inset-top)));
|
|
735
778
|
padding: 0; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); color: var(--text);
|
|
@@ -903,15 +946,23 @@ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentCo
|
|
|
903
946
|
.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));
|
|
904
947
|
background: color-mix(in srgb, var(--color-notice) 14%, var(--panel)); color: var(--text); font-weight: 600; font-size: 14px; text-decoration: none; }
|
|
905
948
|
.deck-more::after { content: '›'; color: var(--accent); font-size: 18px; }
|
|
949
|
+
.deck-more:hover { text-decoration: none; }
|
|
906
950
|
@media (min-width: 900px) {
|
|
907
951
|
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
|
|
908
952
|
.split > main { max-width: none; margin: 0; }
|
|
909
953
|
.needs-deck { display: none; }
|
|
954
|
+
/* Waiting on you is the board's pane here, so neither page shows the switch to the other. */
|
|
955
|
+
.top .pages-switch { display: none; }
|
|
910
956
|
/* 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. */
|
|
911
957
|
.view-board .fab { right: calc(50% - var(--sidebar-w, 0px) / 2 + var(--fab-edge)); }
|
|
912
958
|
body.has-sidebar { --sidebar-w: 272px; }
|
|
913
959
|
html.sb-collapsed body.has-sidebar { --sidebar-w: 0px; }
|
|
914
|
-
.needs-pane { display: block; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; overscroll-behavior: contain; padding: 12px 16px
|
|
960
|
+
.needs-pane { display: block; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; overscroll-behavior: contain; padding: 12px 16px 0; border-left: 1px solid var(--line); }
|
|
915
961
|
.needs-pane .needs-questions main { padding: 0; max-width: none; }
|
|
962
|
+
.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; }
|
|
963
|
+
.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; }
|
|
916
964
|
.needs-pane .waiting { margin: 0 0 12px; }
|
|
917
965
|
}
|
|
966
|
+
/* 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. */
|
|
967
|
+
body .view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
968
|
+
.msg.calls.status-fold summary .fold-log { margin-left: 6px; color: var(--accent); text-decoration: underline; }
|
package/board/public/ui-cards.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Work card renderers: the lines and chips of a card in a lane, on the board page. Needs ui.js, ui-enums.js and ui-
|
|
1
|
+
// Work card renderers: the lines and chips of a card in a lane, on the board page. Needs ui.js, ui-enums.js and ui-progress-bar.js.
|
|
2
2
|
(function () {
|
|
3
|
-
const { esc, ago, lasted, clock, cardTime, etaLine, workingLine, badge, bumpButton, cardBumpButton, editButton, withdrawButton, BUTTON, ACTION_KIND, BUILDER_KIND, PR_STATE, LINK_ARROW,
|
|
3
|
+
const { esc, escText, ago, lasted, clock, cardTime, etaLine, workingLine, thinkingLine, isPaused, PAUSED, badge, bumpButton, cardBumpButton, editButton, withdrawButton, BUTTON, ACTION_KIND, BUILDER_KIND, PR_STATE, LINK_ARROW, progressBar } = globalThis.BoardUI;
|
|
4
4
|
const EXTERNAL = { target: '_blank', rel: 'noopener' };
|
|
5
5
|
// The PR badge is the one place a PR's state shows: its mark and tone, marked like the reviewer and CI chips, and its
|
|
6
6
|
// title (docs/adr/2026-11-03-1843-a-merged-card-reopens-and-its-pr-badge-shows-the-prs-state.md). A card with several PRs shows a
|
|
@@ -57,11 +57,16 @@
|
|
|
57
57
|
return `<a class="card-action" href="${esc(it.action.url)}" title="${esc(it.action.label)}" aria-label="${esc(`Action needed: ${it.action.label}`)}"${external}>Action needed</a>`;
|
|
58
58
|
}
|
|
59
59
|
// Each unfinished dep, linking to its card: "blocked by #53, #54", or "waits on #53" on a bumped card not held, which
|
|
60
|
-
// the pull takes once nothing blocks it (docs/adr/2026-11-05-0802-todo-has-no-queue.md)
|
|
60
|
+
// the pull takes once nothing blocks it (docs/adr/2026-11-05-0802-todo-has-no-queue.md); then its condition, "blocked
|
|
61
|
+
// until <condition>" ("and until <condition>" after deps), then ", <reason>"
|
|
62
|
+
// (docs/adr/2026-11-05-2358-nothing-waiting-on-the-orchestrator-is-missed.md).
|
|
61
63
|
function blockedWords(it) {
|
|
62
64
|
const deps = it.blockedBy || [];
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const waits = it.bumpedAt && !it.held;
|
|
66
|
+
const refs = deps.map((d, i) => `<a class="card-blocked" href="#item=${encodeURIComponent(d.key)}">${i ? '' : waits ? 'waits on ' : 'blocked by '}${esc(d.ref)}</a>`).join('<span class="sep">, </span>');
|
|
67
|
+
const until = it.blockedUntil ? `${deps.length ? ' and until' : waits ? 'waits until' : 'blocked until'} ${it.blockedUntil}` : '';
|
|
68
|
+
const tail = until + (it.blockReason && (refs || until) ? `, ${it.blockReason}` : '');
|
|
69
|
+
return refs + (tail ? `<span class="card-blocked">${esc(tail)}</span>` : '');
|
|
65
70
|
}
|
|
66
71
|
const tryWords = (kind) => kind === 'app' ? {
|
|
67
72
|
go: 'Serve', going: 'serving…', done: 'Install', failed: 'serve failed',
|
|
@@ -143,7 +148,10 @@
|
|
|
143
148
|
// server records it (app-cards.js), and waits (ui.js BUSY) while the session merges it, or the board server lands a
|
|
144
149
|
// card with no PR, and from the tap until a board shows the card left In Review or its merge refused (ui.js pending) (docs/adr/2026-10-29-1752-auto-merge-is-a-board-setting-off-by-default.md,
|
|
145
150
|
// docs/adr/2026-11-04-2100-a-land-waits-for-your-merge.md).
|
|
151
|
+
// A blocked card's Merge shows disabled, its tooltip naming what blocks it
|
|
152
|
+
// (docs/adr/2026-11-06-0102-a-block-past-todo-bumps-a-build-and-holds-a-land.md).
|
|
146
153
|
function mergeButton(it) {
|
|
154
|
+
if (it?.mergeBlocked) return `<button class="primary" disabled title="${esc(it.mergeBlocked)}">Merge</button>`;
|
|
147
155
|
if (it?.action?.kind !== ACTION_KIND.merge && !it?.merging) return '';
|
|
148
156
|
const busy = it.merging || BoardUI.pending.has(`${it.key}:merge`);
|
|
149
157
|
// A merging card has no Action needed left, so resolvedBy is null by then: merging keeps the colour resolvedBy gave it.
|
|
@@ -152,43 +160,49 @@
|
|
|
152
160
|
// A builder reads "starting" until its first status line, from the Implement tap or the session's dispatch on, and
|
|
153
161
|
// ends with how long ago that line was set. One whose process is gone leads with stopped; one whose live process
|
|
154
162
|
// went quiet keeps its words and only turns grey (domain.mjs builderOf; docs/adr/2026-11-05-0410-a-card-status-word-comes-from-a-signal-the-board-checks.md).
|
|
155
|
-
|
|
163
|
+
// One the orchestrator stopped reads the stop's words and how long ago it stopped
|
|
164
|
+
// (docs/adr/2026-11-05-1809-the-orchestrator-stops-the-boards-own-runs.md).
|
|
165
|
+
const builderLine = (b) => (b.stopped ? `${b.stopped} · ${lasted(b.silentSince)}`
|
|
166
|
+
: [b.alive === false && 'stopped', b.progress || (b.alive === false ? null : 'starting'), b.at && lasted(b.at)].filter(Boolean).join(' · '));
|
|
156
167
|
// Five bars at the end of an In Progress card's time row: reached phases filled, a running one marked. The phases are
|
|
157
168
|
// store.mjs PHASES (docs/adr/2026-11-03-0128-a-builder-status-line-is-a-phase-and-a-short-detail.md,
|
|
158
169
|
// docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md).
|
|
159
170
|
const BUILD_PHASES = ['reading', 'building', 'testing', 'checking', 'landing'];
|
|
160
171
|
const stepWords = (phase) => `step ${BUILD_PHASES.indexOf(phase) + 1} of ${BUILD_PHASES.length}`;
|
|
161
172
|
const ALL_DONE = `all ${BUILD_PHASES.length} steps done`;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
173
|
+
// A card's progress bar (ui-progress-bar.js), or null off In Progress and on a card only a planner works on: how many
|
|
174
|
+
// phases it reached, which of them is running, whether it dims because the card waits on something other than a builder (its
|
|
164
175
|
// questions, a blocker or queue, a reopening) or its build is stale, and its label. An install is no build: its card keeps the phase its
|
|
165
176
|
// build reached, undimmed
|
|
166
177
|
// (docs/adr/2026-11-03-2024-every-in-progress-card-shows-its-phase-bars.md).
|
|
167
|
-
// A Todo card a grill or prototype runs on draws the planner's dashes instead, in
|
|
168
|
-
// prototype's second running (docs/adr/2026-11-04-2300-each-lane-is-a-queue-for-the-orchestrator.md
|
|
178
|
+
// A Todo card a grill or prototype runs on draws the planner's dashes instead, in Todo's grey, the grill's first and the
|
|
179
|
+
// prototype's second running (docs/adr/2026-11-04-2300-each-lane-is-a-queue-for-the-orchestrator.md,
|
|
180
|
+
// docs/adr/2026-11-06-0145-every-run-takes-its-lanes-colour.md).
|
|
169
181
|
function phaseStrip(it) {
|
|
170
182
|
if (it.lane === 'todo' && it.builder?.planner) {
|
|
171
183
|
const prototype = it.builder.kind === 'prototype';
|
|
172
|
-
|
|
184
|
+
const done = prototype ? 2 : 1;
|
|
185
|
+
return { count: BUILD_PHASES.length, done, now: done - 1, tone: 'in-todo', label: prototype ? 'Prototyping' : 'Grilling' };
|
|
173
186
|
}
|
|
174
187
|
if (it.lane !== 'progress' || it.builder?.planner) return null;
|
|
175
188
|
const all = BUILD_PHASES.length;
|
|
176
189
|
// Orchestrator review is the checking phase, before landing (docs/adr/2026-11-04-2130-the-hub-launches-every-agent-from-the-card.md).
|
|
177
|
-
|
|
190
|
+
const checking = BUILD_PHASES.indexOf('checking');
|
|
191
|
+
if (it.waitsOn === 'checking') return { count: all, done: checking + 1, now: checking, label: `Orchestrator review, ${stepWords('checking')}` };
|
|
178
192
|
const asking = it.action?.kind === ACTION_KIND.questions || it.action?.kind === ACTION_KIND.pick;
|
|
179
193
|
const answers = asking ? ', waiting on your answers' : '';
|
|
180
194
|
const name = (phase) => `${phase[0].toUpperCase()}${phase.slice(1)}`;
|
|
181
195
|
if (it.builder && it.builder.kind !== BUILDER_KIND.install) {
|
|
182
|
-
const
|
|
183
|
-
return {
|
|
196
|
+
const done = BUILD_PHASES.indexOf(it.builder.phase) + 1;
|
|
197
|
+
return { count: all, done, now: done ? done - 1 : undefined, dim: asking || Boolean(it.builder.stalled || it.builder.quiet), label: `${done ? `${name(it.builder.phase)}, ${stepWords(it.builder.phase)}` : 'Starting, no step yet'}${answers}` };
|
|
184
198
|
}
|
|
185
199
|
const dim = !it.builder || asking;
|
|
186
|
-
if (it.reopened) return {
|
|
187
|
-
const
|
|
188
|
-
const label =
|
|
189
|
-
return {
|
|
200
|
+
if (it.reopened) return { count: all, done: all, dim, label: `Reopened, ${ALL_DONE}${answers}` };
|
|
201
|
+
const done = BUILD_PHASES.indexOf(it.reachedPhase) + 1;
|
|
202
|
+
const label = done ? `Stopped at ${it.reachedPhase}, ${stepWords(it.reachedPhase)}` : 'No step yet';
|
|
203
|
+
return { count: all, done, dim, label: `${label}${answers}` };
|
|
190
204
|
}
|
|
191
|
-
// What the bars do when a card's
|
|
205
|
+
// What the bars do when a card's done count goes from prev to next across redraws, null or undefined meaning no
|
|
192
206
|
// strip (docs/adr/2026-11-03-1850-the-phase-bars-animate-as-a-build-starts-advances-and-ends.md).
|
|
193
207
|
function phaseMotion(prev, next) {
|
|
194
208
|
const had = typeof prev === 'number';
|
|
@@ -196,23 +210,23 @@
|
|
|
196
210
|
if (had !== has) return has ? 'enter' : 'leave';
|
|
197
211
|
return has && next > prev ? 'fill' : null;
|
|
198
212
|
}
|
|
199
|
-
|
|
200
|
-
// docs/adr/2026-11-
|
|
201
|
-
|
|
202
|
-
// owner's blue (style.css).
|
|
203
|
-
const tone = (it) => (it.lane === 'progress' ? ' in-progress' : it.lane === 'todo' ? ' in-todo' : '');
|
|
213
|
+
// A run's status line takes its card's lane colour (style.css): grey in Todo, the phase-bar orange in In Progress, blue in
|
|
214
|
+
// In Review (docs/adr/2026-11-06-0145-every-run-takes-its-lanes-colour.md). A Done card's line keeps the plain orange.
|
|
215
|
+
const tone = (it) => (['todo', 'progress', 'review'].includes(it.lane) ? ` in-${it.lane}` : '');
|
|
204
216
|
const DOT = '<span class="working-dot" aria-hidden="true"></span>';
|
|
205
217
|
// A card idle on the orchestrator (domain.mjs orchestratorWaitOf) reads waiting for orchestrator, then orchestrator
|
|
206
218
|
// review with its dot once the session picks it up, each with how long ago.
|
|
207
219
|
function orchestratorStatus(it) {
|
|
208
220
|
const { since, picked } = it.orchestrator;
|
|
209
|
-
|
|
221
|
+
const words = isPaused() ? `${PAUSED} · ${esc(lasted(picked || since))}` : picked ? `${DOT}orchestrator review · ${esc(lasted(picked))}` : `waiting for orchestrator · ${esc(lasted(since))}`;
|
|
222
|
+
return `<div class="line3 sub agent${tone(it)}${picked && !isPaused() ? '' : ' waiting-gray'}">${words}</div>`;
|
|
210
223
|
}
|
|
211
224
|
// A stale or stopped build's status line is the needs-you red, and a tap on it nudges Claude to check the implementer.
|
|
212
|
-
// A quiet one is grey, with no dot.
|
|
225
|
+
// A quiet one is grey, with no dot, and one the orchestrator stopped plain, since the orchestrator already knows.
|
|
213
226
|
function builderStatus(it) {
|
|
214
227
|
const { quiet } = it.builder;
|
|
215
228
|
const classes = `line3 sub agent${it.builder.planner ? ' planner' : ''}${tone(it)}${quiet ? ' quiet' : ''}`;
|
|
229
|
+
if (it.builder.stopped) return `<div class="${classes}">${esc(builderLine(it.builder))}</div>`;
|
|
216
230
|
const words = `${quiet ? '' : DOT}${esc(builderLine(it.builder))}`;
|
|
217
231
|
if (!it.builder.stalled || it.lane === 'done') return `<div class="${classes}">${words}</div>`;
|
|
218
232
|
const nudging = BoardUI.pending.has(`${it.key}:nudge`) ? ' nudging' : '';
|
|
@@ -223,11 +237,11 @@
|
|
|
223
237
|
function statusWord(it) {
|
|
224
238
|
const b = it.builder;
|
|
225
239
|
if (b?.planner) return b.kind === 'prototype' ? 'prototyping' : 'grilling';
|
|
226
|
-
if (b && b.kind !== BUILDER_KIND.install) return b.alive === false ? 'stopped' : b.phase === 'landing' ? 'landing' : 'implementing';
|
|
240
|
+
if (b && b.kind !== BUILDER_KIND.install) return b.stopped || (b.alive === false ? 'stopped' : b.phase === 'landing' ? 'landing' : 'implementing');
|
|
227
241
|
if (it.waitsOn === 'checking') return 'Orchestrator review';
|
|
228
242
|
if (it.lane === 'done') return 'done';
|
|
229
243
|
if (it.grilling || it.prototyping) return 'preheating';
|
|
230
|
-
if (it.lane === 'todo') return it.ready ? 'ready' : 'to do';
|
|
244
|
+
if (it.lane === 'todo') return it.staging ? 'staging' : it.ready ? 'ready' : 'to do';
|
|
231
245
|
if (it.lane === 'progress') return it.reachedPhase ? `stopped at ${it.reachedPhase}` : 'in progress';
|
|
232
246
|
return 'in review';
|
|
233
247
|
}
|
|
@@ -249,15 +263,15 @@
|
|
|
249
263
|
: `Held until ${d.toLocaleDateString([], { weekday: 'short', month: 'numeric', day: 'numeric' })}, ${clock(d)}. ${it.lane === 'todo' ? 'It starts on its own then, once nothing else blocks it.' : 'Claude picks it up again then.'}`;
|
|
250
264
|
return `<span class="card-hold" title="${esc(title)}">${esc(d ? `held until ${heldUntilWords(it.heldUntil)}` : 'held')}</span>`;
|
|
251
265
|
}
|
|
252
|
-
// A card's last line: the time its lane sorts by (domain.mjs sortAt), then its hold, what blocks it, then Action needed
|
|
253
|
-
// or ready, then an In Progress card's phase bars, then its unread messages, new questions counting as one, which app-cards.js
|
|
266
|
+
// A card's last line: the time its lane sorts by (domain.mjs sortAt), then its hold, what blocks it, then Action needed,
|
|
267
|
+
// staging in orange or ready in green (docs/adr/2026-11-07-0700-staging-then-ready.md), then an In Progress card's phase bars, then its unread messages, new questions counting as one, which app-cards.js
|
|
254
268
|
// takes away when the card opens (docs/adr/2026-11-04-0247-a-builder-asks-the-orchestrator-mid-build.md,
|
|
255
269
|
// docs/adr/2026-11-03-0516-a-new-card-is-ready-grilling-or-prototyping.md, docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md).
|
|
256
270
|
const SEP = '<span class="sep"> · </span>';
|
|
257
271
|
const newWords = (n, word) => n && `${n} new ${word}${n === 1 ? '' : 's'}`;
|
|
258
272
|
function timeLine(it, unread, bars) {
|
|
259
273
|
const iso = it.sortAt;
|
|
260
|
-
const parts = [iso && `<time datetime="${esc(iso)}">${esc(cardTime(iso))}</time>`, holdWords(it), blockedWords(it), actionWords(it), it.ready && '<span class="card-ready">ready</span>'].filter(Boolean);
|
|
274
|
+
const parts = [iso && `<time datetime="${esc(iso)}">${esc(cardTime(iso))}</time>`, holdWords(it), blockedWords(it), actionWords(it), it.staging && '<span class="card-staging">staging</span>', it.ready && '<span class="card-ready">ready</span>'].filter(Boolean);
|
|
261
275
|
const news = newWords(unread, 'message');
|
|
262
276
|
const count = news ? `<span class="card-unread">${parts.length || bars ? SEP : ''}${news}</span>` : '';
|
|
263
277
|
const tail = bars ? `${parts.length ? SEP : ''}${bars}` : '';
|
|
@@ -284,36 +298,37 @@
|
|
|
284
298
|
const ownerDoneButton = (it) => `<button type="button" class="ghost icon owner-done" data-confirm="finish" data-key="${esc(it.key)}" aria-label="Done" title="${esc(`Done: ${it.ownerStep}`)}">${CHECK}</button>`;
|
|
285
299
|
const cardActions = (it, top) => (it.isNew ? askActions(it, top) : it.ownerStep ? `<div class="ask-actions">${ownerDoneButton(it)}</div>`
|
|
286
300
|
: it.bumpable ? `<div class="ask-actions">${cardBumpButton(it.key, Boolean(it.bumpedAt))}</div>` : '');
|
|
287
|
-
const askLine = (ask) => [ask.from && `Moved from ${esc(ask.from)}`, ask.edited && 'Edited', ask.state === 'working' &&
|
|
301
|
+
const askLine = (ask) => [ask.from && `Moved from ${esc(ask.from)}`, ask.edited && 'Edited', ask.state === 'working' && thinkingLine()].filter(Boolean).join(' · ');
|
|
288
302
|
// In a wave band (app-lanes.js), step is the number of the card's phase in its wave, shown inside its id chip, and actions replace
|
|
289
303
|
// its bump with the band's ⋯ menu.
|
|
290
304
|
function workLines(it, { unread = 0, top = false, step = 0, actions = null } = {}) {
|
|
291
305
|
const ticket = it.ticket ? ticketChip(it) : it.ticketSource === 'linear' ? linearChips(it) : it.internal ? badge('quiet', esc(it.ref), { title: 'Work on this site, not a product PR' }) : linearChips(it);
|
|
292
|
-
// A card
|
|
306
|
+
// A work card always shows its ticket; New is only on an ask (askChip).
|
|
293
307
|
const chip = step ? ticket.replace(/^<[^>]*>/, (open) => `${open}<span class="wn">${step}</span>`) : ticket;
|
|
294
|
-
const ids = it.kind === 'ask' ? askChip(it) :
|
|
308
|
+
const ids = it.kind === 'ask' ? askChip(it) : chip;
|
|
295
309
|
const chips = `${prChips(it)}${reviewChips(it)}${it.risky ? badge('notice', 'risky') : ''}${installChip(it)}`;
|
|
296
310
|
const status = it.isNew && !it.action ? (askLine(it.ask) && `<div class="line3 sub">${askLine(it.ask)}</div>`)
|
|
297
|
-
: it.orchestrator && !it.builder ? `${it.rebaseFailed ? `<div class="line3 sub">${
|
|
311
|
+
: it.orchestrator && !it.builder ? `${it.rebaseFailed ? `<div class="line3 sub">${escText(it.rebaseFailed)}</div>` : ''}${orchestratorStatus(it)}`
|
|
298
312
|
: it.waitsOn === 'checking' ? `<div class="line3 sub agent${tone(it)}">${DOT}Orchestrator review</div>`
|
|
299
313
|
: it.builder ? builderStatus(it)
|
|
300
314
|
: it.grilling || it.prototyping ? '<div class="line3 sub asked">preheating</div>'
|
|
301
|
-
: it.refusal ? `<div class="line3 sub refused">${
|
|
302
|
-
: it.
|
|
303
|
-
: it.
|
|
304
|
-
|
|
315
|
+
: it.refusal ? `<div class="line3 sub refused">${escText(it.refusal)}</div>`
|
|
316
|
+
: it.landStopped ? `<div class="line3 sub">${escText(`${it.landStopped.words} · ${lasted(it.landStopped.at)}`)}</div>`
|
|
317
|
+
: it.working ? `<div class="line3 sub agent">${thinkingLine()}</div>`
|
|
318
|
+
: it.next ? `<div class="line3 sub next${it.lane === 'todo' || it.lane === 'done' || it.held ? ' idle' : ''}">Next: ${escText(it.next)}</div>` : '';
|
|
319
|
+
const eta = it.eta && it.builder ? `<div class="line3 sub eta">${escText(etaLine(it.eta))}</div>` : '';
|
|
305
320
|
// The owner step's code check, which closes the card when it passes (docs/adr/2026-11-04-2308-an-owner-step-can-carry-a-code-check.md).
|
|
306
|
-
const check = !it.ownerCheck ? '' : it.ownerCheck.broken ? `<div class="line3 sub refused">${
|
|
307
|
-
: `<div class="line3 sub check">${
|
|
308
|
-
return `${actions ?? cardActions(it, top)}<div class="line1 title">${ids ? `${ids} ` : ''}${
|
|
321
|
+
const check = !it.ownerCheck ? '' : it.ownerCheck.broken ? `<div class="line3 sub refused">${escText(`Check can't run: ${it.ownerCheck.broken}`)}</div>`
|
|
322
|
+
: `<div class="line3 sub check">${escText(`Checks: ${it.ownerCheck.words}`)}</div>`;
|
|
323
|
+
return `${actions ?? cardActions(it, top)}<div class="line1 title">${ids ? `${ids} ` : ''}${escText(it.title)}</div>
|
|
309
324
|
${status}${check}${eta}
|
|
310
325
|
${chips ? `<div class="line2 meta">${chips}</div>` : ''}
|
|
311
|
-
${timeLine(it, unread,
|
|
326
|
+
${timeLine(it, unread, progressBar(phaseStrip(it)))}`;
|
|
312
327
|
}
|
|
313
328
|
const WAVE_ICON = '<svg class="wave-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 9c2.5-3 5-3 7.5 0s5 3 7.5 0 3.5-3 5-1.5"/><path d="M2 16c2.5-3 5-3 7.5 0s5 3 7.5 0 3.5-3 5-1.5"/></svg>';
|
|
314
329
|
const askChip = (it) => (it.isNew ? badge('notice', 'New') : '');
|
|
315
330
|
function planButton(items) {
|
|
316
|
-
const keys = items.filter((i) => i.kind === 'work' && !i.held && !i.
|
|
331
|
+
const keys = items.filter((i) => i.kind === 'work' && !i.held && !i.blocked).map((i) => i.key);
|
|
317
332
|
return keys.length > 1 ? `<button type="button" class="thread-more" data-plan="${esc(keys.join(' '))}"${BoardUI.pending.has('todo:plan') ? BoardUI.BUSY : ''}>Plan waves</button>` : '';
|
|
318
333
|
}
|
|
319
334
|
|
|
@@ -334,11 +349,11 @@
|
|
|
334
349
|
const reviewWhen = (c) => `${c.state === 'reviewing' ? 'started ' : ''}${ago(c.at)}`;
|
|
335
350
|
function reviewCard(c, open) {
|
|
336
351
|
return `<details class="rv-card" data-review="${c.number}"${open ? ' open' : ''}><summary><span class="status ${REVIEW_DOTS[c.state]}"></span><div class="title-wrap">
|
|
337
|
-
<div class="line1 title">${badge('normal', `PR #${c.number}`)} ${
|
|
352
|
+
<div class="line1 title">${badge('normal', `PR #${c.number}`)} ${escText(c.title)}</div>
|
|
338
353
|
<div class="line3 sub${c.state === 'reviewing' ? ' agent' : ''}">${c.state === 'reviewing' ? '<span class="working-dot" aria-hidden="true"></span>' : ''}<span class="rv-word s-${c.state}">${REVIEW_WORDS[c.state]}</span> · ${esc(reviewWhen(c))}</div>
|
|
339
354
|
<div class="line2 meta">${reviewChipsOf(c)}</div></div></summary>${reviewDetail(c)}</details>`;
|
|
340
355
|
}
|
|
341
|
-
const reviewLine = (c, open) => `<details class="rv-line" data-review="${c.number}"${open ? ' open' : ''}><summary><div class="title">${badge('normal', `PR #${c.number}`)} ${
|
|
356
|
+
const reviewLine = (c, open) => `<details class="rv-line" data-review="${c.number}"${open ? ' open' : ''}><summary><div class="title">${badge('normal', `PR #${c.number}`)} ${escText(c.title)}</div>
|
|
342
357
|
<div class="sub">${esc([c.author, c.state === 'posted' ? `posted ${ago(c.at)}` : 'not posted', c.open ? '' : 'closed'].filter(Boolean).join(' · '))}</div></summary>${reviewDetail(c)}</details>`;
|
|
343
358
|
// reviews: the built board's reviews; open: {lane, earlier, cards: Set of open PR numbers}.
|
|
344
359
|
function reviewLane(reviews, { open = {} } = {}) {
|