@lovek4d/kickflip 0.5.0 → 0.5.3
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 +47 -42
- package/README.md +1 -1
- package/SKILL.md +16 -16
- package/bin/kickflip.mjs +26 -17
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -0
- package/board/bin/board.mjs +65 -18
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +5 -4
- package/board/on-host.mjs +1 -1
- package/board/public/app-asks.js +6 -6
- package/board/public/app-banner.js +7 -13
- package/board/public/app-cards.js +51 -27
- package/board/public/app-core.js +26 -8
- package/board/public/app-header.js +2 -1
- package/board/public/app-lanes.js +55 -23
- package/board/public/app-needs.js +129 -4
- package/board/public/app-reader.js +1 -1
- package/board/public/app-snapshot.js +6 -5
- package/board/public/app-threads.js +70 -59
- package/board/public/app.js +38 -8
- package/board/public/attach.js +2 -2
- package/board/public/dialogs.js +9 -39
- package/board/public/hub-kit.js +4 -2
- package/board/public/linear-key.js +46 -35
- package/board/public/new-task.js +17 -38
- package/board/public/prototype-kit.js +28 -3
- package/board/public/prototype-switcher.js +42 -107
- package/board/public/shell.js +7 -5
- package/board/public/sidebar.js +44 -5
- package/board/public/style.css +108 -37
- package/board/public/ui-cards.js +41 -30
- package/board/public/ui-header.js +2 -2
- package/board/public/ui-order.js +12 -10
- package/board/public/ui-questions.js +25 -38
- package/board/public/ui-sidebar.js +18 -12
- package/board/public/ui-threads.js +3 -3
- package/board/public/ui.js +40 -8
- package/board/public/view-hub-settings.js +11 -10
- package/board/public/view-hub.js +17 -3
- package/board/public/view-questions.js +142 -199
- package/board/public/view-settings.js +191 -72
- package/board/server.mjs +19 -20
- package/board/store.mjs +20 -15
- package/board/transcript.mjs +2 -0
- package/board/worktree.mjs +24 -20
- package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
- package/hooks-plugin/hooks/hooks.json +9 -0
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +5 -5
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/linear.mjs +1 -2
- package/hub/live.mjs +2 -2
- package/hub/locks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/presence.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +1 -2
- package/hub/review-send.mjs +1 -0
- package/hub/secrets.mjs +2 -0
- package/hub/self-restart.mjs +2 -0
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -0
- package/hub/slack.mjs +2 -2
- package/hub/stream.mjs +10 -13
- package/hub/try.mjs +6 -6
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +2 -1
- package/shared/audit.mjs +4 -0
- package/shared/fake-gh.mjs +17 -4
- package/shared/service.mjs +4 -4
- package/shared/usage-gate.mjs +1 -1
package/board/public/style.css
CHANGED
|
@@ -163,6 +163,11 @@ button.link { background: none; border: 0; color: var(--accent); padding: 8px 12
|
|
|
163
163
|
.wb-done { padding: 8px 12px; font-size: 12.5px; color: var(--muted); }
|
|
164
164
|
.wb-done a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
165
165
|
.wb-loosehead { cursor: default; }
|
|
166
|
+
.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
|
+
.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
|
+
.wb-counts:hover .wb-tip, .wb-counts.open .wb-tip { display: block; }
|
|
169
|
+
.wb-counts .wv-cur { font-weight: 700; color: var(--accent); }
|
|
170
|
+
.wb-lane + .wb-lane, .wb-lane + .wb-loose { border-top: 1px solid var(--line); }
|
|
166
171
|
.wb-loosehead:hover { background: var(--band); }
|
|
167
172
|
/* A wave card's step number, inside its id chip, and its ⋯ menu. */
|
|
168
173
|
.wn { color: var(--muted); font-variant-numeric: tabular-nums; padding-right: 5px; margin-right: 4px; border-right: 1px solid var(--line); }
|
|
@@ -202,10 +207,12 @@ code { overflow-wrap: anywhere; }
|
|
|
202
207
|
/* The ⌘↵ / Ctrl+↵ hint left of Send: only where there is a keyboard to press it on. */
|
|
203
208
|
.send-hint { display: none; color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
204
209
|
@media (hover: hover) and (pointer: fine) { .send-hint { display: inline; } }
|
|
205
|
-
/* A composer's text box (BoardUI.dropZone)
|
|
206
|
-
the
|
|
210
|
+
/* A composer's text box (BoardUI.dropZone), its attach button inside at the bottom right with the text kept clear of it:
|
|
211
|
+
while files are dragged over the composer, attach.js marks it .dragging and the box gets a dashed accent outline
|
|
212
|
+
with Drop to attach over it. */
|
|
207
213
|
.dz { position: relative; clear: both; border-radius: 8px; }
|
|
208
|
-
.dz > textarea { display: block; }
|
|
214
|
+
.dz > textarea { display: block; padding-bottom: 40px; }
|
|
215
|
+
.dz > .attach { position: absolute; right: 6px; bottom: 6px; width: 32px; height: 32px; z-index: 1; }
|
|
209
216
|
.dz-hint { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; border-radius: 8px; pointer-events: none;
|
|
210
217
|
background: color-mix(in srgb, var(--panel) 80%, transparent); color: var(--accent); font-weight: 600; font-size: 15px; }
|
|
211
218
|
[data-composer].dragging .dz { outline: 2px dashed var(--accent); outline-offset: 3px; }
|
|
@@ -232,7 +239,6 @@ code { overflow-wrap: anywhere; }
|
|
|
232
239
|
.msg .body:empty { display: none; }
|
|
233
240
|
.note-tools { display: flex; align-items: flex-start; gap: 6px; }
|
|
234
241
|
.note-tools .pending-files { flex: 1; margin-top: 4px; }
|
|
235
|
-
.note-tools .attach { margin-top: 2px; }
|
|
236
242
|
|
|
237
243
|
/* Compose button and sheet */
|
|
238
244
|
main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
@@ -324,7 +330,19 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
324
330
|
@media (prefers-reduced-motion: no-preference) { .working-dot { animation: pulse 1.4s ease-in-out infinite; } }
|
|
325
331
|
form[data-reply] .row .quick { margin-left: auto; margin-right: 6px; }
|
|
326
332
|
form[data-reply] .row [data-confirm="finish"] { margin-right: 6px; }
|
|
327
|
-
|
|
333
|
+
/* A composer's send buttons (ui.js sendButton), each with its key as a small gray hint where there is a keyboard and
|
|
334
|
+
room. A pair takes a row of two halves; a Todo card's reply box wraps its pair under the row. */
|
|
335
|
+
.send-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
|
|
336
|
+
.send-actions button, [data-sends] button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
|
|
337
|
+
.send-actions button { min-height: 44px; }
|
|
338
|
+
form[data-reply] .row { flex-wrap: wrap; }
|
|
339
|
+
form[data-reply] .row > .send-actions { flex: 0 0 100%; margin-top: 4px; }
|
|
340
|
+
.key-hint { display: none; font-size: 11px; font-weight: 400; color: var(--muted); white-space: nowrap; }
|
|
341
|
+
button.primary .key-hint { color: color-mix(in srgb, var(--on-accent) 62%, transparent); }
|
|
342
|
+
@media (hover: hover) and (pointer: fine) and (min-width: 600px) { .key-hint { display: inline; } }
|
|
343
|
+
#askStatus { margin: 6px 2px 0; }
|
|
344
|
+
#askStatus:empty { display: none; }
|
|
345
|
+
.q .proto-view { display: inline-block; margin: 0 0 10px; color: var(--accent); }
|
|
328
346
|
.msg .ask-actions { margin: -4px -6px 0 8px; }
|
|
329
347
|
button.attach { border: 0; background: transparent; padding: 0; }
|
|
330
348
|
button.attach:disabled { opacity: .35; }
|
|
@@ -348,10 +366,17 @@ button.attach:disabled { opacity: .35; }
|
|
|
348
366
|
@media (prefers-reduced-motion: no-preference) { .phase-bars:not(.dim) i.now { animation: pulse-fill 1.4s ease-in-out infinite; } }
|
|
349
367
|
/* A stale build's status line, dot included, is the needs-you red, and a tap on it nudges Claude (ui-cards.js builderStatus). */
|
|
350
368
|
/* 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
|
|
351
|
-
shorthand stays out so .sub's 12px wins.
|
|
352
|
-
|
|
369
|
+
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
|
|
370
|
+
drops below the bump icons' float instead of sitting beside it as the div's line box does (board/layout.test.mjs). */
|
|
371
|
+
button.sub { display: block; margin-inline: 0; margin-bottom: 0; padding: 0; min-height: 0; border: 0; border-radius: 0; background: none;
|
|
353
372
|
font-family: inherit; font-weight: inherit; letter-spacing: inherit; line-height: inherit; text-align: left; appearance: none; -webkit-appearance: none; }
|
|
354
373
|
button.sub.agent.stalled { color: var(--color-danger); }
|
|
374
|
+
/* A tap on it calms the line to the in-progress orange, its dot sending a ripple, until the card's next status line (app-cards.js nudge). */
|
|
375
|
+
button.sub.agent.stalled.nudging { color: var(--color-notice); cursor: default; }
|
|
376
|
+
.nudging .working-dot { position: relative; }
|
|
377
|
+
.nudging .working-dot::after { content: ''; position: absolute; inset: -1px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0; }
|
|
378
|
+
@keyframes nudge-ping { from { transform: scale(1); opacity: .9; } to { transform: scale(2.6); opacity: 0; } }
|
|
379
|
+
@media (prefers-reduced-motion: no-preference) { .nudging .working-dot::after { animation: nudge-ping 1.1s ease-out infinite; } }
|
|
355
380
|
.stalled .working-dot { background: currentColor; }
|
|
356
381
|
.brand { display: grid; grid-template-columns: auto auto minmax(0, 1fr); column-gap: 10px; row-gap: 2px; }
|
|
357
382
|
.brand .hub { grid-row: 1 / span 2; align-self: center; }
|
|
@@ -375,6 +400,9 @@ button.sub.agent.stalled { color: var(--color-danger); }
|
|
|
375
400
|
.sub.agent.in-progress { color: var(--color-notice); }
|
|
376
401
|
.sub.agent.in-todo { color: var(--accent); }
|
|
377
402
|
.sub.agent.in-progress .working-dot, .sub.agent.in-todo .working-dot { background: currentColor; }
|
|
403
|
+
/* 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
|
+
has picked up are grey, after the lane colours so they win. */
|
|
405
|
+
.sub.agent.quiet, .sub.asked { color: var(--muted); }
|
|
378
406
|
.phase-bars.plan { --bar-fill: var(--accent); }
|
|
379
407
|
@keyframes pulse-fill-plan { 50% { background-color: color-mix(in srgb, var(--accent) 35%, transparent); } }
|
|
380
408
|
@media (prefers-reduced-motion: no-preference) { .phase-bars.plan:not(.dim) i.now { animation-name: pulse-fill-plan; } }
|
|
@@ -400,8 +428,6 @@ button.sub.agent.stalled { color: var(--color-danger); }
|
|
|
400
428
|
/* The thread's eye and fire are toggles: pressed reads as chosen, and the hint under the box says what Send posts. */
|
|
401
429
|
form[data-reply] .tools { display: inline-flex; flex: none; }
|
|
402
430
|
button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chip); }
|
|
403
|
-
.flag-hint { margin: 4px 2px 0; min-height: 1.2em; color: var(--muted); font-size: 12px; }
|
|
404
|
-
.flag-hint.on { color: var(--accent); }
|
|
405
431
|
/* INT-30: card line order */
|
|
406
432
|
.line6 { margin-top: 4px; }
|
|
407
433
|
.card-time time { font-variant-numeric: tabular-nums; }
|
|
@@ -420,6 +446,13 @@ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chi
|
|
|
420
446
|
/* Each lane's sort icon and its menu, grey (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md) */
|
|
421
447
|
/* INT-35: offline header */
|
|
422
448
|
.view-board.offline #compose { display: none; }
|
|
449
|
+
/* A board shared with someone else, view only (docs/adr/2026-11-05-2215-a-board-is-shared-view-only-with-a-tailscale-login.md):
|
|
450
|
+
shell.js marks the page .shared-view, which hides the +, every composer, Board settings, the architecture review and
|
|
451
|
+
a card's buttons, keeping its links (Visit, Notes, Open questions). */
|
|
452
|
+
.shared-view #compose, .shared-view [data-composer], .shared-view #settingsLink, .shared-view #archBulb,
|
|
453
|
+
.shared-view .actions button, .shared-view button[data-confirm] { display: none !important; }
|
|
454
|
+
.view-only-strip { margin: 8px 0; padding: 8px 12px; border-radius: 10px; background: var(--accent-soft); font-size: 13px; }
|
|
455
|
+
.view-only-strip b { color: var(--accent); }
|
|
423
456
|
.view-board.offline main > * { opacity: .72; }
|
|
424
457
|
.view-board.offline main, .view-board.offline > .top > :not(.nav), .view-board.offline > .top .nav > :not(.refresh) { pointer-events: none; }
|
|
425
458
|
/* INT-28: answer banner */
|
|
@@ -530,6 +563,17 @@ main > * { transition: opacity .25s ease; }
|
|
|
530
563
|
.view-settings .who.new { border-color: var(--accent); }
|
|
531
564
|
.view-settings .add { display: flex; gap: 6px; margin-top: 8px; }
|
|
532
565
|
.view-settings .add input { flex: 1; min-width: 0; }
|
|
566
|
+
.view-settings .add.pick { flex-wrap: wrap; }
|
|
567
|
+
.view-settings .share-now { color: var(--ok); font-weight: 600; }
|
|
568
|
+
.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
|
+
.view-settings .share-reach { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 10px 0 4px; }
|
|
570
|
+
.view-settings .share-reach p { flex: 1 1 220px; margin: 0; }
|
|
571
|
+
.view-settings .share-reach button { flex: none; }
|
|
572
|
+
.view-settings .pick-col { flex: 1 1 140px; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
573
|
+
.view-settings .pick-list { max-height: 160px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
|
|
574
|
+
.view-settings .pick-list button { text-align: left; }
|
|
575
|
+
.view-settings .pick-list button[aria-pressed="true"] { border-color: var(--accent); }
|
|
576
|
+
.view-settings .who .sub { display: block; margin-top: 2px; }
|
|
533
577
|
.view-settings .replies .reply { margin-top: 8px; }
|
|
534
578
|
.view-settings label.who { gap: 12px; cursor: pointer; }
|
|
535
579
|
.view-settings label.who .sub { display: block; margin-top: 2px; }
|
|
@@ -576,6 +620,8 @@ main > * { transition: opacity .25s ease; }
|
|
|
576
620
|
.view-questions .topics button { min-width: 0; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
|
|
577
621
|
background: var(--panel); color: var(--text); white-space: normal; text-align: left; overflow-wrap: anywhere; }
|
|
578
622
|
.view-questions .topics button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
623
|
+
@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
|
+
@media (prefers-reduced-motion: no-preference) { .view-questions .topics button.arrived { animation: pill-arrive .6s ease-out; } }
|
|
579
625
|
.view-questions .topics .n { color: var(--muted); margin-left: 4px; }
|
|
580
626
|
.view-questions .topichead { margin: 2px 0 6px; }
|
|
581
627
|
.view-questions details.done > summary { cursor: pointer; color: var(--muted); font-size: 13px; margin: 16px 0 4px; }
|
|
@@ -600,39 +646,24 @@ main > * { transition: opacity .25s ease; }
|
|
|
600
646
|
display: flex; justify-content: space-between; align-items: center; gap: 8px; border-top: 1px solid var(--line); }
|
|
601
647
|
.view-questions .bar .send-hint { margin-left: auto; }
|
|
602
648
|
/* The + rides above the sticky Send bar rather than over its Send. */
|
|
603
|
-
.view-questions:has(.bar) .fab { bottom: calc(var(--app-bottom, 0px) + var(--fab-edge) + 72px); }
|
|
604
649
|
.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; }
|
|
605
653
|
.view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
|
|
606
654
|
.view-questions .solo { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 4px 0; }
|
|
607
655
|
/* docs/adr/2026-11-02-1332-a-prototype-round-ends-with-a-one-take-confirm.md */
|
|
608
656
|
.view-questions .topichead { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
609
657
|
.view-questions .topichead .try-spin { width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px; }
|
|
610
|
-
.view-questions .q .pr-open { display: inline-block; margin: 0 0 8px; }
|
|
611
658
|
.view-questions .pr-rule { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
|
|
612
659
|
.view-questions .pr-rule svg { flex: none; }
|
|
613
660
|
|
|
614
|
-
/* A question's takes sit in as many columns as keep each at least 380px wide, one on a phone; each its option above
|
|
615
|
-
|
|
661
|
+
/* A question's drawn takes sit in as many columns as keep each at least 380px wide, one on a phone; each its option above
|
|
662
|
+
its diagram, and each row's takes top-aligned. */
|
|
616
663
|
.view-questions .takes { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr)); gap: 10px; align-items: start; margin: 6px 0; }
|
|
617
664
|
.view-questions .take { min-width: 0; border: 1px solid var(--line); border-radius: 12px; background: var(--bg); padding: 10px; }
|
|
618
665
|
.view-questions .take:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
619
666
|
.view-questions .take .opt, .view-questions .take .opt:has(input:checked) { border: 0; padding: 0; margin: 0; background: none; }
|
|
620
|
-
/* view-questions.js sets a frame's height to its page's once measured; until then it shows nothing, so a take that turns
|
|
621
|
-
its question into the full-width layout never flashes cut off. */
|
|
622
|
-
.view-questions .take-shot { position: relative; display: block; margin-top: 8px; height: 360px; border: 1px solid var(--line); border-radius: 10px;
|
|
623
|
-
overflow: hidden; transition: height .15s ease; }
|
|
624
|
-
.view-questions .take-shot iframe { width: 100%; height: 100%; border: 0; background: var(--bg); }
|
|
625
|
-
.view-questions .take-shot.measuring { transition: none; }
|
|
626
|
-
.view-questions .take-shot.measuring iframe { visibility: hidden; }
|
|
627
|
-
.view-questions .take-shot.measuring::before { content: 'Loading take…'; position: absolute; inset: 0; display: grid; place-items: center;
|
|
628
|
-
color: var(--muted); font-size: 12.5px; }
|
|
629
|
-
.view-questions:has(.takes-full) main { max-width: none; }
|
|
630
|
-
.view-questions .takes-full { grid-template-columns: minmax(0, 1fr); }
|
|
631
|
-
.view-questions .take-choices { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
632
|
-
.view-questions .take-choices .opt { flex: 1 1 160px; margin: 0; }
|
|
633
|
-
.view-questions .take-choices .opt[aria-current="true"] { border-color: var(--accent); }
|
|
634
|
-
.view-questions .takes-full .take { padding: 0; border: 0; }
|
|
635
|
-
.view-questions .takes-full .take-shot { margin-top: 0; transition: none; }
|
|
636
667
|
|
|
637
668
|
/* A take with nothing to see: boxes and arrows, then what it means in practice. */
|
|
638
669
|
.view-questions .flow { margin: 10px 0 4px; }
|
|
@@ -683,7 +714,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
683
714
|
.view-frame .alone { white-space: nowrap; font-size: 13px; }
|
|
684
715
|
|
|
685
716
|
@media (max-width: 899px) {
|
|
686
|
-
:root:has(body > .top, body.has-sidebar > :not(.sidebar) > .top)
|
|
717
|
+
:root:has(body > .top, body.has-sidebar > :not(.sidebar) > .top) {
|
|
687
718
|
--app-bottom: calc(49px + max(4px, env(safe-area-inset-bottom)));
|
|
688
719
|
}
|
|
689
720
|
body > .top, body.has-sidebar > :not(.sidebar) > .top {
|
|
@@ -691,7 +722,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
691
722
|
border-bottom: 0; border-top: 1px solid var(--line); padding-top: 4px; padding-bottom: max(4px, env(safe-area-inset-bottom));
|
|
692
723
|
}
|
|
693
724
|
.top .nav #refresh, .top .nav a.icon, .top .nav .arch-bulb { min-width: 44px; min-height: 44px; }
|
|
694
|
-
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)); }
|
|
725
|
+
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)); }
|
|
695
726
|
.view-frame:not(.own-header) { padding-bottom: var(--app-bottom, 0px); }
|
|
696
727
|
.sheet { bottom: max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px)));
|
|
697
728
|
max-height: calc(var(--vh, 100dvh) + var(--kb, 0px) - max(var(--app-bottom, 0px), var(--kb, 0px)) - env(safe-area-inset-top) - 12px); }
|
|
@@ -716,21 +747,18 @@ main > * { transition: opacity .25s ease; }
|
|
|
716
747
|
.lk-over, .lk-over > main { display: contents; }
|
|
717
748
|
|
|
718
749
|
.tk-section { margin: 10px 0; }
|
|
719
|
-
/* The Tickets source row's live state
|
|
750
|
+
/* The Tickets source row's live state (dialogs.js ticketsControl keyState). */
|
|
720
751
|
.tk-status { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; font-size: 13px; color: var(--muted); }
|
|
721
752
|
.tk-status b { font-weight: 600; color: var(--text); }
|
|
722
753
|
.tk-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: none; }
|
|
723
754
|
.tk-status.on .dot { background: var(--accent); }
|
|
724
755
|
.tk-status.on b { color: var(--accent); }
|
|
725
|
-
.tk-inline { margin: 6px 0 10px; border: 1px solid var(--accent); border-radius: 10px; padding: 8px 10px; background: var(--accent-soft); }
|
|
726
|
-
.tk-inline .tk-ask { margin: 2px 0 6px; font-size: 13px; }
|
|
727
|
-
.tk-inline .hub-view main { padding: 0; }
|
|
728
756
|
.tk-seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; padding: 4px; margin: 8px 0; border: 1px solid var(--line); border-radius: 9px; background: var(--chip); }
|
|
729
757
|
.tk-seg button { min-width: 0; min-height: 44px; padding: 6px 4px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); font: inherit; font-size: 13px; cursor: pointer; }
|
|
730
758
|
.tk-seg button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
731
|
-
.ask-seg { grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 10px 0 0; }
|
|
732
|
-
.ask-choice-help { margin: 4px 2px 0; }
|
|
733
759
|
.tk-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
|
|
760
|
+
.landing-seg button { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 6px; text-align: center; }
|
|
761
|
+
.landing-seg .sub { font-weight: 400; opacity: .8; line-height: 1.25; }
|
|
734
762
|
.tk-grid { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr); }
|
|
735
763
|
@media (min-width: 560px) { .tk-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); } }
|
|
736
764
|
.tk-field { display: block; min-width: 0; }
|
|
@@ -844,3 +872,46 @@ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentCo
|
|
|
844
872
|
.rv-links a { color: var(--accent); text-decoration: none; }
|
|
845
873
|
.rv-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
846
874
|
.view-questions .bar #qstatus .try-spin { color: var(--accent); margin-right: 6px; }
|
|
875
|
+
|
|
876
|
+
/* 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
|
|
877
|
+
Waiting on you (docs/adr/2026-11-05-0408-needs-you-is-a-deck-on-a-phone-and-a-pane-on-a-desktop.md). */
|
|
878
|
+
.needs-pane { display: none; }
|
|
879
|
+
.needs-deck { margin: 0 0 12px; border: 1px solid color-mix(in srgb, var(--color-notice) 45%, var(--line)); border-radius: 14px;
|
|
880
|
+
background: color-mix(in srgb, var(--color-notice) 7%, var(--panel)); overflow: hidden; }
|
|
881
|
+
.deck-head { display: flex; align-items: center; gap: 8px; padding: 10px 14px 4px; }
|
|
882
|
+
.deck-head h2 { margin: 0; flex: 1; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--color-notice); }
|
|
883
|
+
.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; }
|
|
884
|
+
.deck-card { padding: 2px 14px 12px; min-width: 0; overflow-wrap: anywhere; }
|
|
885
|
+
.deck-what { margin: 0 0 2px; font-size: 12px; color: var(--muted); }
|
|
886
|
+
.deck-card h3 { margin: 0 0 4px; font-size: 15px; line-height: 1.3; }
|
|
887
|
+
.deck-card h3 a { color: inherit; text-decoration: none; }
|
|
888
|
+
.deck-card .ctx { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
|
889
|
+
.deck-proto { margin: 0 0 10px; color: var(--accent); }
|
|
890
|
+
.deck-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
891
|
+
.deck-opts button { text-align: left; border-radius: 10px; padding: 10px 12px; font-size: 14px; }
|
|
892
|
+
.deck-opts button.rec { border-color: var(--accent); }
|
|
893
|
+
.deck-opts .rec-word { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
894
|
+
.deck-opts small { display: block; color: var(--muted); font-size: 12.5px; }
|
|
895
|
+
.deck-prompt { margin: 0; padding: 0; list-style: none; }
|
|
896
|
+
.deck-prompt li.prompt { padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
897
|
+
.deck-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
898
|
+
.deck-bar { display: flex; align-items: center; gap: 10px; padding: 0 14px 10px; }
|
|
899
|
+
.deck-dots { flex: 1; display: flex; flex-wrap: wrap; gap: 5px; }
|
|
900
|
+
.deck-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--line); }
|
|
901
|
+
.deck-dots i.on { background: var(--color-notice); }
|
|
902
|
+
.deck-bar button { border-radius: 999px; padding: 5px 12px; font-size: 13px; }
|
|
903
|
+
.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
|
+
background: color-mix(in srgb, var(--color-notice) 14%, var(--panel)); color: var(--text); font-weight: 600; font-size: 14px; text-decoration: none; }
|
|
905
|
+
.deck-more::after { content: '›'; color: var(--accent); font-size: 18px; }
|
|
906
|
+
@media (min-width: 900px) {
|
|
907
|
+
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
|
|
908
|
+
.split > main { max-width: none; margin: 0; }
|
|
909
|
+
.needs-deck { display: none; }
|
|
910
|
+
/* 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
|
+
.view-board .fab { right: calc(50% - var(--sidebar-w, 0px) / 2 + var(--fab-edge)); }
|
|
912
|
+
body.has-sidebar { --sidebar-w: 272px; }
|
|
913
|
+
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 24px; border-left: 1px solid var(--line); }
|
|
915
|
+
.needs-pane .needs-questions main { padding: 0; max-width: none; }
|
|
916
|
+
.needs-pane .waiting { margin: 0 0 12px; }
|
|
917
|
+
}
|
package/board/public/ui-cards.js
CHANGED
|
@@ -56,10 +56,12 @@
|
|
|
56
56
|
const external = it.action.kind === ACTION_KIND.ready ? Object.entries(EXTERNAL).map(([k, v]) => ` ${k}="${esc(v)}"`).join('') : '';
|
|
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
|
-
// Each unfinished dep, linking to its card: "blocked by #53, #54"
|
|
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
61
|
function blockedWords(it) {
|
|
61
62
|
const deps = it.blockedBy || [];
|
|
62
|
-
|
|
63
|
+
const lead = it.bumpedAt && !it.held ? 'waits on ' : 'blocked by ';
|
|
64
|
+
return deps.map((d, i) => `<a class="card-blocked" href="#item=${encodeURIComponent(d.key)}">${i ? '' : lead}${esc(d.ref)}</a>`).join('<span class="sep">, </span>');
|
|
63
65
|
}
|
|
64
66
|
const tryWords = (kind) => kind === 'app' ? {
|
|
65
67
|
go: 'Serve', going: 'serving…', done: 'Install', failed: 'serve failed',
|
|
@@ -112,16 +114,17 @@
|
|
|
112
114
|
if (it.kind !== 'work' || !t?.script || t.autoBuild || !t.changes) return '';
|
|
113
115
|
const words = tryWords(t.kind);
|
|
114
116
|
const reason = t.state === 'running' ? words.busy : t.state === 'ready' && !t.stale ? words.current : '';
|
|
115
|
-
|
|
117
|
+
const attrs = BoardUI.pending.has(`${it.key}:try`) ? BoardUI.BUSY : reason ? ` aria-disabled="true" title="${esc(reason)}" data-tip="${esc(reason)}"` : '';
|
|
118
|
+
return `<button type="button" data-try="${esc(it.key)}"${attrs}>${t.state === 'failed' && !reason ? 'Retry' : words.go}</button>`;
|
|
116
119
|
}
|
|
117
120
|
const BRANCH = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="8" r="2.5"/><path d="M6 8.5v7"/><path d="M18 10.5c0 4-6 3-11 6"/></svg>';
|
|
118
121
|
// Main's refresh icon on a local board (docs/adr/2026-11-03-1914-a-local-boards-main-pulls-from-origin-on-its-own.md),
|
|
119
122
|
// the header's refresh icon as a ghost button right of the timestamp: it spins while a pull runs or a tap waits on the
|
|
120
123
|
// server, and turns red when the last pull failed, with the reason as a tooltip on hover or a popover a tap opens
|
|
121
124
|
// (`open`). Visit is left alone: it changes only once Main rebuilds.
|
|
122
|
-
function mainPull(pull, {
|
|
125
|
+
function mainPull(pull, { open = false } = {}) {
|
|
123
126
|
if (!pull) return '';
|
|
124
|
-
const busy = pull.running ||
|
|
127
|
+
const busy = pull.running || BoardUI.pending.has('main:pull');
|
|
125
128
|
const failed = !busy && pull.error;
|
|
126
129
|
const label = busy ? 'Pulling main' : failed ? pull.error : 'Pull main now';
|
|
127
130
|
const why = failed ? `<span class="main-pull-why${open ? ' open' : ''}" role="tooltip">${esc(pull.error)}</span>` : '';
|
|
@@ -143,21 +146,13 @@
|
|
|
143
146
|
function mergeButton(it) {
|
|
144
147
|
if (it?.action?.kind !== ACTION_KIND.merge && !it?.merging) return '';
|
|
145
148
|
const busy = it.merging || BoardUI.pending.has(`${it.key}:merge`);
|
|
146
|
-
|
|
149
|
+
// A merging card has no Action needed left, so resolvedBy is null by then: merging keeps the colour resolvedBy gave it.
|
|
150
|
+
return `<button class="${it.merging || it.resolvedBy === BUTTON.merge ? 'resolves' : 'primary'}" data-confirm="merge" data-key="${esc(it.key)}"${busy ? BoardUI.BUSY : ''}>Merge</button>`;
|
|
147
151
|
}
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
// docs/adr/2026-11-03-0219-a-build-tap-starts-the-build-in-the-board-server.md, docs/adr/2026-11-03-1549-a-todo-cards-button-reads-implement.md).
|
|
153
|
-
// Implement is plain until the card is ready, then builder orange (docs/adr/2026-11-04-0131-every-new-task-is-grilled-or-prototyped-at-once.md).
|
|
154
|
-
function buildButton(it) {
|
|
155
|
-
if (it.kind !== 'work' || it.lane !== 'todo' || it.queued) return '';
|
|
156
|
-
return `<button class="build${it.ready ? ' ready' : ''}" data-confirm="card-build" data-key="${esc(it.key)}">Implement</button>`;
|
|
157
|
-
}
|
|
158
|
-
// A builder reads "preheating" until its first status line, from the Implement tap or the session's dispatch on, and
|
|
159
|
-
// ends with how long ago that line was set.
|
|
160
|
-
const builderLine = (b) => [b.progress || 'preheating', b.at && lasted(b.at)].filter(Boolean).join(' · ');
|
|
152
|
+
// A builder reads "starting" until its first status line, from the Implement tap or the session's dispatch on, and
|
|
153
|
+
// ends with how long ago that line was set. One whose process is gone leads with stopped; one whose live process
|
|
154
|
+
// 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
|
+
const builderLine = (b) => [b.alive === false && 'stopped', b.progress || (b.alive === false ? null : 'starting'), b.at && lasted(b.at)].filter(Boolean).join(' · ');
|
|
161
156
|
// Five bars at the end of an In Progress card's time row: reached phases filled, a running one marked. The phases are
|
|
162
157
|
// store.mjs PHASES (docs/adr/2026-11-03-0128-a-builder-status-line-is-a-phase-and-a-short-detail.md,
|
|
163
158
|
// docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md).
|
|
@@ -185,12 +180,12 @@
|
|
|
185
180
|
const name = (phase) => `${phase[0].toUpperCase()}${phase.slice(1)}`;
|
|
186
181
|
if (it.builder && it.builder.kind !== BUILDER_KIND.install) {
|
|
187
182
|
const reached = BUILD_PHASES.indexOf(it.builder.phase) + 1;
|
|
188
|
-
return { reached, running: reached > 0, dim: asking || Boolean(it.builder.stalled), label: `${reached ? `${name(it.builder.phase)}, ${stepWords(it.builder.phase)}` : '
|
|
183
|
+
return { reached, running: reached > 0, dim: asking || Boolean(it.builder.stalled || it.builder.quiet), label: `${reached ? `${name(it.builder.phase)}, ${stepWords(it.builder.phase)}` : 'Starting, no step yet'}${answers}` };
|
|
189
184
|
}
|
|
190
185
|
const dim = !it.builder || asking;
|
|
191
186
|
if (it.reopened) return { reached: all, running: false, dim, label: `Reopened, ${ALL_DONE}${answers}` };
|
|
192
187
|
const reached = BUILD_PHASES.indexOf(it.reachedPhase) + 1;
|
|
193
|
-
const label = reached ? `Stopped at ${it.reachedPhase}, ${stepWords(it.reachedPhase)}` :
|
|
188
|
+
const label = reached ? `Stopped at ${it.reachedPhase}, ${stepWords(it.reachedPhase)}` : 'No step yet';
|
|
194
189
|
return { reached, running: false, dim, label: `${label}${answers}` };
|
|
195
190
|
}
|
|
196
191
|
// What the bars do when a card's reached count goes from prev to next across redraws, null or undefined meaning no
|
|
@@ -213,12 +208,28 @@
|
|
|
213
208
|
const { since, picked } = it.orchestrator;
|
|
214
209
|
return `<div class="line3 sub agent${tone(it)}">${picked ? `${DOT}orchestrator review · ${esc(lasted(picked))}` : `waiting for orchestrator · ${esc(lasted(since))}`}</div>`;
|
|
215
210
|
}
|
|
216
|
-
// A stale build's status line is the needs-you red, and a tap on it nudges Claude to check the implementer.
|
|
211
|
+
// 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.
|
|
217
213
|
function builderStatus(it) {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
214
|
+
const { quiet } = it.builder;
|
|
215
|
+
const classes = `line3 sub agent${it.builder.planner ? ' planner' : ''}${tone(it)}${quiet ? ' quiet' : ''}`;
|
|
216
|
+
const words = `${quiet ? '' : DOT}${esc(builderLine(it.builder))}`;
|
|
220
217
|
if (!it.builder.stalled || it.lane === 'done') return `<div class="${classes}">${words}</div>`;
|
|
221
|
-
|
|
218
|
+
const nudging = BoardUI.pending.has(`${it.key}:nudge`) ? ' nudging' : '';
|
|
219
|
+
return `<button type="button" class="${classes} stalled${nudging}" data-confirm="nudge" data-key="${esc(it.key)}" title="No recent progress. Nudge Claude to check this implementer">${words}</button>`;
|
|
220
|
+
}
|
|
221
|
+
// The open card's Status row: what runs on the card, else where its lane leaves it, never the wave file's raw status
|
|
222
|
+
// (docs/adr/2026-11-05-0410-a-card-status-word-comes-from-a-signal-the-board-checks.md).
|
|
223
|
+
function statusWord(it) {
|
|
224
|
+
const b = it.builder;
|
|
225
|
+
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';
|
|
227
|
+
if (it.waitsOn === 'checking') return 'Orchestrator review';
|
|
228
|
+
if (it.lane === 'done') return 'done';
|
|
229
|
+
if (it.grilling || it.prototyping) return 'preheating';
|
|
230
|
+
if (it.lane === 'todo') return it.ready ? 'ready' : 'to do';
|
|
231
|
+
if (it.lane === 'progress') return it.reachedPhase ? `stopped at ${it.reachedPhase}` : 'in progress';
|
|
232
|
+
return 'in review';
|
|
222
233
|
}
|
|
223
234
|
const unreadCount = (othersAt, seenAt) => (seenAt ? (othersAt || []).filter((t) => String(t) > seenAt).length : 0);
|
|
224
235
|
// When a hold ends, as a card reads it: the time today, the weekday within six days, else the date
|
|
@@ -283,13 +294,13 @@
|
|
|
283
294
|
const ids = it.kind === 'ask' ? askChip(it) : it.unsorted ? `${badge('notice', 'New')} ${chip}` : chip;
|
|
284
295
|
const chips = `${prChips(it)}${reviewChips(it)}${it.risky ? badge('notice', 'risky') : ''}${installChip(it)}`;
|
|
285
296
|
const status = it.isNew && !it.action ? (askLine(it.ask) && `<div class="line3 sub">${askLine(it.ask)}</div>`)
|
|
286
|
-
: it.orchestrator && !it.builder ? orchestratorStatus(it)
|
|
297
|
+
: it.orchestrator && !it.builder ? `${it.rebaseFailed ? `<div class="line3 sub">${esc(it.rebaseFailed)}</div>` : ''}${orchestratorStatus(it)}`
|
|
287
298
|
: it.waitsOn === 'checking' ? `<div class="line3 sub agent${tone(it)}">${DOT}Orchestrator review</div>`
|
|
288
299
|
: it.builder ? builderStatus(it)
|
|
289
|
-
: it.grilling || it.prototyping ?
|
|
300
|
+
: it.grilling || it.prototyping ? '<div class="line3 sub asked">preheating</div>'
|
|
290
301
|
: it.refusal ? `<div class="line3 sub refused">${esc(it.refusal)}</div>`
|
|
291
302
|
: it.working ? `<div class="line3 sub agent">${workingLine}</div>`
|
|
292
|
-
: it.next ? `<div class="line3 sub next${it.lane === 'todo' || it.lane === 'done' || it.held
|
|
303
|
+
: it.next ? `<div class="line3 sub next${it.lane === 'todo' || it.lane === 'done' || it.held ? ' idle' : ''}">Next: ${esc(it.next)}</div>` : '';
|
|
293
304
|
const eta = it.eta && it.builder ? `<div class="line3 sub eta">${esc(etaLine(it.eta))}</div>` : '';
|
|
294
305
|
// 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).
|
|
295
306
|
const check = !it.ownerCheck ? '' : it.ownerCheck.broken ? `<div class="line3 sub refused">${esc(`Check can't run: ${it.ownerCheck.broken}`)}</div>`
|
|
@@ -303,7 +314,7 @@
|
|
|
303
314
|
const askChip = (it) => (it.isNew ? badge('notice', 'New') : '');
|
|
304
315
|
function planButton(items) {
|
|
305
316
|
const keys = items.filter((i) => i.kind === 'work' && !i.held && !i.blockedBy?.length).map((i) => i.key);
|
|
306
|
-
return keys.length > 1 ? `<button type="button" class="thread-more" data-plan="${esc(keys.join(' '))}">Plan waves</button>` : '';
|
|
317
|
+
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>` : '';
|
|
307
318
|
}
|
|
308
319
|
|
|
309
320
|
// docs/adr/2026-11-03-1731-each-board-reviews-incoming-prs-that-touch-its-work.md
|
|
@@ -344,5 +355,5 @@
|
|
|
344
355
|
<div class="list">${list}${fold}</div></details>`;
|
|
345
356
|
}
|
|
346
357
|
|
|
347
|
-
Object.assign(globalThis.BoardUI, { prChips, prName, linearChips, ticketChip, REVIEW, reviewAge, reviewChips, pendingQuestions, actionWords, blockedWords, tryWords, tryButton, mainBar, installChip, installButton, mergeButton,
|
|
358
|
+
Object.assign(globalThis.BoardUI, { prChips, prName, linearChips, ticketChip, REVIEW, reviewAge, reviewChips, pendingQuestions, actionWords, blockedWords, tryWords, tryButton, mainBar, installChip, installButton, mergeButton, builderLine, statusWord, BUILD_PHASES, phaseStrip, phaseMotion, unreadCount, heldUntilWords, holdPicker, workLines, askChip, askActions, askLine, planButton, WAVE_ICON, reviewLane });
|
|
348
359
|
})();
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
? `<ul class="hub-links">${links.map(linkRow).join('')}</ul>`
|
|
66
66
|
: '<p class="muted">The hub doesn\'t list this board yet. It picks a board up from the manifest on its next check.</p>');
|
|
67
67
|
|
|
68
|
-
// The checklist a new board shows at the top until its session answered the
|
|
68
|
+
// The checklist a new board shows at the top until its session answered the ping and GitHub checks reach it; setup
|
|
69
69
|
// is /api/board's, null once both pass. Cards can start meanwhile.
|
|
70
70
|
const setupCard = (setup) => {
|
|
71
71
|
if (!setup) return '';
|
|
72
72
|
const step = (ok, done, waiting) => `<li><span class="dot ${ok ? 'pass' : 'pending'}" aria-hidden="true"></span>${ok ? done : waiting}</li>`;
|
|
73
73
|
return `<h2>Connecting this board</h2><ul class="setup-steps">
|
|
74
|
-
${step(setup.events, 'Claude answered the
|
|
74
|
+
${step(setup.events, 'Claude answered the ping', 'Waiting for Claude to answer the ping')}
|
|
75
75
|
${setup.github === undefined ? '' : step(setup.github, 'GitHub checks reach this board', 'Waiting for the hub\'s next GitHub check')}
|
|
76
76
|
</ul>`;
|
|
77
77
|
};
|
package/board/public/ui-order.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
// The board's order of its items, each lane together, which board/domain.mjs sorts the board by and the page sorts a
|
|
2
2
|
// tap it draws at once by, so the card lands where the server will put it
|
|
3
|
-
// (docs/adr/2026-11-03-0223-each-lane-has-one-fixed-order.md). Each item carries its lane, isNew,
|
|
3
|
+
// (docs/adr/2026-11-03-0223-each-lane-has-one-fixed-order.md). Each item carries its lane, isNew, bumpedAt, sortAt, order and key.
|
|
4
4
|
// Needs nothing else: board/domain.mjs runs this file in a vm context.
|
|
5
5
|
(function () {
|
|
6
6
|
const LANE_ORDER = { review: 0, stale: 1, progress: 2, todo: 3, done: 4, asks: 5 };
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// Todo lists bumped cards first, the latest bump on top, a bumped New ask included
|
|
8
|
+
// (docs/adr/2026-10-29-1639-a-card-that-hasnt-started-can-be-bumped.md); then the rest oldest first by sortAt, a card with no
|
|
9
|
+
// time last, the first added to its wave file on top, so a New ask and every new card join the bottom and the pull takes the
|
|
10
|
+
// top ready card (docs/adr/2026-11-05-2345-new-todo-cards-join-the-bottom.md,
|
|
11
|
+
// docs/adr/2026-11-06-0300-a-new-task-lands-at-the-bottom-of-todo-and-flashes-blue.md). Every other
|
|
12
|
+
// lane lists its cards newest first by sortAt, a card with no time last, the last added to its wave file on top.
|
|
12
13
|
function laneOrder(a, b) {
|
|
13
14
|
const bumped = (i) => i.bumpedAt || '';
|
|
14
|
-
const unsorted = (i) => (i.isNew ? 0 : 1);
|
|
15
|
-
const queued = (i) => (i.queued ? 1 : 0);
|
|
16
15
|
const at = (i) => String(i.sortAt || '');
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
const oldestFirst = a.lane === 'todo';
|
|
17
|
+
const timeless = (i) => (at(i) ? 0 : 1);
|
|
18
|
+
const age = oldestFirst ? timeless(a) - timeless(b) || at(a).localeCompare(at(b)) : at(b).localeCompare(at(a));
|
|
19
|
+
const added = a.lane !== 'todo' ? b.order - a.order : a.order - b.order;
|
|
20
|
+
return LANE_ORDER[a.lane] - LANE_ORDER[b.lane] || bumped(b).localeCompare(bumped(a)) || age || added || a.key.localeCompare(b.key);
|
|
19
21
|
}
|
|
20
22
|
Object.assign(globalThis.BoardUI ||= {}, { laneOrder });
|
|
21
23
|
})();
|