@lovek4d/kickflip 0.7.7 → 0.8.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 +7 -7
- package/SKILL.md +4 -4
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +1 -1
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/public/app-needs.js +1 -1
- package/board/public/app.html +2 -0
- package/board/public/app.js +1 -0
- package/board/public/col-resize.js +73 -0
- package/board/public/hub.css +1 -0
- package/board/public/prototype.html +1 -0
- package/board/public/sidebar.css +8 -2
- package/board/public/style.css +32 -10
- package/board/public/ui-cards.js +3 -3
- package/board/public/ui-sidebar.js +11 -3
- package/board/public/ui-threads.js +2 -1
- package/board/public/ui.js +1 -1
- package/board/public/view-hub.js +25 -2
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +18 -18
- package/board/store.mjs +29 -29
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-messages.mjs +53 -0
- package/hooks-plugin/hooks/hooks.json +10 -0
- package/hub/boards.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/schedule.mjs +1 -1
- package/hub/try.mjs +10 -10
- package/hub/upgrade.mjs +1 -1
- package/package.json +1 -1
package/board/public/app.html
CHANGED
|
@@ -42,6 +42,7 @@ window.BoardBoot = (function () {
|
|
|
42
42
|
<body class="has-sidebar">
|
|
43
43
|
<aside id="sidebar" class="sidebar" aria-label="Boards">{{sidebar}}</aside>
|
|
44
44
|
<div id="sidebarScrim" class="sidebar-scrim"></div>
|
|
45
|
+
<div class="col-handle" data-col="sidebar" role="separator" aria-orientation="vertical" aria-label="Resize the boards sidebar" tabindex="0"></div>
|
|
45
46
|
<script id="sidebarData" type="application/json">{{sidebarData}}</script>
|
|
46
47
|
<script src="/_hub/app/ui.js"></script>
|
|
47
48
|
<script src="/_hub/app/ui-links.js"></script>
|
|
@@ -67,6 +68,7 @@ window.BoardBoot = (function () {
|
|
|
67
68
|
<script src="/_hub/app/attach.js"></script>
|
|
68
69
|
<script src="/_hub/app/hub-kit.js"></script>
|
|
69
70
|
<script src="/_hub/app/linear-key.js"></script>
|
|
71
|
+
<script src="/_hub/app/col-resize.js"></script>
|
|
70
72
|
<script src="/_hub/app/sidebar.js"></script>
|
|
71
73
|
<script src="/_hub/app/app.js"></script>
|
|
72
74
|
<script src="/_hub/app/new-task.js"></script>
|
package/board/public/app.js
CHANGED
|
@@ -464,6 +464,7 @@ ${strip} <section id="setup" class="setup" aria-live="polite" hidden></section>
|
|
|
464
464
|
<div id="downLanding" hidden></div>
|
|
465
465
|
${BoardUI.boardFoot()}
|
|
466
466
|
</main>
|
|
467
|
+
<div class="col-handle" data-col="pane" role="separator" aria-orientation="vertical" aria-label="Resize the Waiting on you pane" tabindex="0"></div>
|
|
467
468
|
<aside id="needsPane" class="needs-pane" aria-label="Needs you"><section id="waiting" class="waiting" hidden></section></aside>
|
|
468
469
|
</div>
|
|
469
470
|
${BoardNewTask.markup()}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Drag-resizing of the desktop columns (docs/adr/2026-11-10-1600-the-desktop-columns-drag-and-this-device-keeps-their-widths.md).
|
|
2
|
+
// A `.col-handle[data-col="sidebar"|"pane"]` drags its column's edge: the sidebar rail's, or the seam between the lanes
|
|
3
|
+
// and the Waiting on you pane. Widths are clamped (sidebar 220–420px, the lanes and the pane each 360px or more), kept
|
|
4
|
+
// per device for every board, and set on <html> as --sb-w and --pane-w, which style.css and sidebar.css read. A double-click
|
|
5
|
+
// on a handle forgets the width; ←/→ on a focused handle moves it 16px. Loaded after the body's first elements so the
|
|
6
|
+
// stored widths land before the first paint of the board.
|
|
7
|
+
(function () {
|
|
8
|
+
const KEYS = { sidebar: 'hub:sidebarWidth', pane: 'hub:paneWidth' };
|
|
9
|
+
const VARS = { sidebar: '--sb-w', pane: '--pane-w' };
|
|
10
|
+
const SIDEBAR_MIN = 220, SIDEBAR_MAX = 420, LANE_MIN = 360, STEP = 16, DEFAULT_SIDEBAR = 272;
|
|
11
|
+
const html = document.documentElement;
|
|
12
|
+
const wide = () => window.matchMedia('(min-width: 900px)').matches;
|
|
13
|
+
const read = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
14
|
+
const write = (k, v) => { try { localStorage.setItem(k, v); } catch { /* private mode or full: kept for this page only */ } };
|
|
15
|
+
const forget = (k) => { try { localStorage.removeItem(k); } catch { /* nothing kept */ } };
|
|
16
|
+
const stored = (col) => { const n = Number(read(KEYS[col])); return Number.isFinite(n) && n > 0 ? n : null; };
|
|
17
|
+
// The sidebar's width now, which the pane's room depends on.
|
|
18
|
+
const sidebarNow = (viewport) => (html.classList.contains('sb-collapsed') ? 0 : clamp('sidebar', stored('sidebar') ?? DEFAULT_SIDEBAR, viewport, 0));
|
|
19
|
+
|
|
20
|
+
// → px clamped for the column: the pane's ceiling leaves the lanes their minimum beside the sidebar, and its floor
|
|
21
|
+
// wins where the window is too narrow for both.
|
|
22
|
+
function clamp(col, px, viewport, sidebarW) {
|
|
23
|
+
if (col === 'sidebar') return Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, px));
|
|
24
|
+
return Math.max(LANE_MIN, Math.min(px, viewport - sidebarW - LANE_MIN));
|
|
25
|
+
}
|
|
26
|
+
function paint(col) {
|
|
27
|
+
const px = stored(col);
|
|
28
|
+
if (px === null) { html.style.removeProperty(VARS[col]); return; }
|
|
29
|
+
html.style.setProperty(VARS[col], `${col === 'pane' ? clamp('pane', px, window.innerWidth, sidebarNow(window.innerWidth)) : clamp('sidebar', px)}px`);
|
|
30
|
+
}
|
|
31
|
+
function paintAll() { paint('sidebar'); paint('pane'); }
|
|
32
|
+
function set(col, px) { write(KEYS[col], String(Math.round(px))); paintAll(); }
|
|
33
|
+
function reset(col) { forget(KEYS[col]); paintAll(); }
|
|
34
|
+
paintAll();
|
|
35
|
+
window.addEventListener('resize', paintAll);
|
|
36
|
+
|
|
37
|
+
// The width a handle's drag edge is at now, and the width the pointer at x asks for.
|
|
38
|
+
const current = (col, el) => {
|
|
39
|
+
const edge = el.getBoundingClientRect();
|
|
40
|
+
return col === 'sidebar' ? edge.left + edge.width / 2 : window.innerWidth - (edge.left + edge.width / 2);
|
|
41
|
+
};
|
|
42
|
+
const wanted = (col, x) => (col === 'sidebar' ? x : window.innerWidth - x);
|
|
43
|
+
|
|
44
|
+
document.addEventListener('pointerdown', (e) => {
|
|
45
|
+
const el = e.target.closest?.('.col-handle');
|
|
46
|
+
if (!el || !wide() || e.button > 0) return;
|
|
47
|
+
const col = el.dataset.col;
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
el.setPointerCapture?.(e.pointerId);
|
|
50
|
+
el.classList.add('dragging');
|
|
51
|
+
html.classList.add('col-dragging');
|
|
52
|
+
const move = (m) => set(col, wanted(col, m.clientX));
|
|
53
|
+
const end = () => { el.classList.remove('dragging'); html.classList.remove('col-dragging'); el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', end); el.removeEventListener('pointercancel', end); };
|
|
54
|
+
el.addEventListener('pointermove', move);
|
|
55
|
+
el.addEventListener('pointerup', end);
|
|
56
|
+
el.addEventListener('pointercancel', end);
|
|
57
|
+
});
|
|
58
|
+
document.addEventListener('dblclick', (e) => {
|
|
59
|
+
const el = e.target.closest?.('.col-handle');
|
|
60
|
+
if (el && wide()) reset(el.dataset.col);
|
|
61
|
+
});
|
|
62
|
+
document.addEventListener('keydown', (e) => {
|
|
63
|
+
const el = e.target.closest?.('.col-handle');
|
|
64
|
+
if (!el || !wide() || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return;
|
|
65
|
+
const col = el.dataset.col;
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
// → grows the sidebar and shrinks the pane, as the edge moves right.
|
|
68
|
+
const dx = (e.key === 'ArrowRight' ? STEP : -STEP) * (col === 'sidebar' ? 1 : -1);
|
|
69
|
+
set(col, (stored(col) ?? current(col, el)) + dx);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
window.ColResize = { clamp, set, reset };
|
|
73
|
+
})();
|
package/board/public/hub.css
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
.hub-view .dur-on .title { font-weight:600; white-space:normal; }
|
|
79
79
|
.hub-view code { font-family:var(--mono); font-size:.85em; white-space:nowrap; }
|
|
80
80
|
.hub-view footer { color:var(--muted); font-size:12px; margin:1.8rem 2px 0; }
|
|
81
|
+
.hub-view .version { font:inherit; color:inherit; background:none; border:0; padding:0; text-decoration:underline dotted; cursor:pointer; }
|
|
81
82
|
.hub-view .host { font-family:var(--mono); font-size:11px; overflow-wrap:anywhere; }
|
|
82
83
|
.hub-view .st.ok { color:var(--color-success); }
|
|
83
84
|
.hub-view .links { display:block; margin-top:2px; }
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<script src="attach.js"></script>
|
|
85
85
|
<script src="hub-kit.js"></script>
|
|
86
86
|
<script src="linear-key.js"></script>
|
|
87
|
+
<script src="col-resize.js"></script>
|
|
87
88
|
<script src="sidebar.js"></script>
|
|
88
89
|
<script src="app.js"></script>
|
|
89
90
|
<script src="new-task.js"></script>
|
package/board/public/sidebar.css
CHANGED
|
@@ -188,12 +188,14 @@ html.sb-hub-down body.has-sidebar > .top .hub::after, html.sb-hub-down body.has-
|
|
|
188
188
|
.sidebar .t-paused { --tone: var(--muted); }
|
|
189
189
|
|
|
190
190
|
@media (min-width: 900px) {
|
|
191
|
-
body.has-sidebar { padding-left: 272px; }
|
|
192
|
-
body.has-sidebar .sidebar { width: 272px; transform: none; visibility: visible; transition: none; z-index: 4; }
|
|
191
|
+
body.has-sidebar { padding-left: var(--sidebar-w, 272px); }
|
|
192
|
+
body.has-sidebar .sidebar { width: var(--sb-w, 272px); transform: none; visibility: visible; transition: none; z-index: 4; }
|
|
193
193
|
body.has-sidebar .sidebar .sb-close { display: none; }
|
|
194
194
|
body.has-sidebar .sidebar-scrim { display: none; }
|
|
195
195
|
html:not(.sb-collapsed) body.has-sidebar > .top .hub, html:not(.sb-collapsed) body.has-sidebar > :not(.sidebar) > .top .hub { display: none; }
|
|
196
196
|
html.sb-collapsed body.has-sidebar { padding-left: 0; }
|
|
197
|
+
body.has-sidebar > .col-handle { position: fixed; left: calc(var(--sidebar-w, 272px) - 4px); }
|
|
198
|
+
html.sb-collapsed body.has-sidebar > .col-handle { display: none; }
|
|
197
199
|
html.sb-collapsed body.has-sidebar .sidebar { transform: translateX(-102%); visibility: hidden; }
|
|
198
200
|
/* Switch mode shows a collapsed rail over the page until it ends, and leaves it collapsed. */
|
|
199
201
|
html.sb-collapsed body.has-sidebar .sidebar.ks-on { transform: none; visibility: visible; z-index: 20; }
|
|
@@ -244,3 +246,7 @@ html.sb-hub-down body.has-sidebar > .top .hub::after, html.sb-hub-down body.has-
|
|
|
244
246
|
font: 600 12.5px/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif; }
|
|
245
247
|
.shortcuts-sheet .sc-foot { margin: 14px 0 0; padding-top: 10px; border-top: 1px solid var(--line); font-size: 12.5px; color: var(--muted); }
|
|
246
248
|
@media (prefers-reduced-motion: reduce) { .sheet.shortcuts-sheet[open] { animation: none; } }
|
|
249
|
+
/* A held row (hub/home.mjs holdNeeds): its last good line, flat gray with (stale) after the word. */
|
|
250
|
+
.sidebar .held .dot, .sidebar .held .word, .sidebar .held .lc { --tone: var(--muted); }
|
|
251
|
+
.sidebar .held .dot.pulse { animation: none; }
|
|
252
|
+
.sidebar .held .stale-word { flex: none; color: var(--muted); font-size: 11px; }
|
package/board/public/style.css
CHANGED
|
@@ -240,7 +240,7 @@ code { overflow-wrap: anywhere; }
|
|
|
240
240
|
/* Compose button and sheet */
|
|
241
241
|
main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
242
242
|
/* The + sits two thirds of (16px + the safe area) from the corner, and at least 32px where there is no safe area. */
|
|
243
|
-
:root { --fab-edge: max(32px, calc((16px + env(safe-area-inset-bottom)) * 2 / 3)); }
|
|
243
|
+
:root { --sel-bg: var(--accent-soft); --sel-line: var(--accent); --fab-edge: max(32px, calc((16px + env(safe-area-inset-bottom)) * 2 / 3)); }
|
|
244
244
|
.fab { position: fixed; z-index: 6; right: max(32px, calc((16px + max(env(safe-area-inset-right), env(safe-area-inset-bottom))) * 2 / 3)); bottom: calc(var(--app-bottom, 0px) + var(--fab-edge));
|
|
245
245
|
width: 75px; height: 75px; padding: 0; border-radius: 50%; border: 0; background: var(--accent); color: var(--on-accent);
|
|
246
246
|
display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
|
|
@@ -289,13 +289,15 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
289
289
|
@keyframes sheet-in { from { transform: translateY(100%); } }
|
|
290
290
|
@keyframes sheet-out { to { transform: translateY(100%); } }
|
|
291
291
|
@keyframes fade-in { from { opacity: 0; } }
|
|
292
|
+
@keyframes reader-in { from { transform: translateX(100%); } }
|
|
293
|
+
@keyframes reader-out { to { transform: translateX(100%); } }
|
|
292
294
|
@media (min-width: 700px) {
|
|
293
295
|
.sheet { bottom: calc(var(--kb, 0px) + 24px); max-height: calc(var(--vh, 100dvh) - env(safe-area-inset-top) - 12px); padding-bottom: 0;
|
|
294
296
|
border-bottom: 1px solid var(--line); border-radius: 16px; }
|
|
295
297
|
.sheet form { padding-bottom: 14px; }
|
|
296
298
|
}
|
|
297
299
|
@media (prefers-reduced-motion: reduce) {
|
|
298
|
-
.sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.closing::backdrop { animation: none; }
|
|
300
|
+
.sheet[open], .sheet.reader-sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.reader-sheet.closing, .sheet.closing::backdrop { animation: none; }
|
|
299
301
|
.fab:active { transform: none; }
|
|
300
302
|
}
|
|
301
303
|
/* The hub lives at the tailnet root, outside this site, so its link is root-absolute. */
|
|
@@ -417,6 +419,11 @@ div.status-line { padding-left: 33px; text-indent: -23px; }
|
|
|
417
419
|
/* Only a run's finished line is green: the fold's summary mirrors its last line, green once the run has finished, else the
|
|
418
420
|
started and status lines' orange or a planner's blue (docs/adr/2026-11-07-1600-only-a-runs-finished-line-is-green.md). */
|
|
419
421
|
.msg.calls.status-fold summary.finished { color: var(--color-success); }
|
|
422
|
+
.msg.fault-detail summary { cursor: pointer; list-style: none; overflow-wrap: anywhere; }
|
|
423
|
+
.msg.fault-detail summary::-webkit-details-marker { display: none; }
|
|
424
|
+
.msg.fault-detail[open] summary .mark-icon { transform: rotate(90deg); }
|
|
425
|
+
/* The whole error of a fault line, monospace, wrapping at any width. */
|
|
426
|
+
.fault-error { margin: 4px 0 4px 23px; padding: 8px; font: 12px/1.4 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--bg-soft, rgba(127,127,127,.12)); border-radius: 6px; color: var(--fg, inherit); }
|
|
420
427
|
.msg.calls ol { margin: 4px 0; padding-left: 23px; }
|
|
421
428
|
.agent-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 5px; vertical-align: 1px; }
|
|
422
429
|
/* Every thread status line's leading mark, icon or dot, sits centred in one 18px box, so the text after it always starts
|
|
@@ -636,7 +643,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
636
643
|
.view-questions .ctx { color: var(--muted); font-size: 13px; margin-bottom: 8px; }
|
|
637
644
|
.view-questions .opt { display: grid; grid-template-columns: 22px 1fr; gap: 6px; padding: 8px; border: 1px solid var(--line);
|
|
638
645
|
border-radius: 8px; margin: 6px 0; cursor: pointer; }
|
|
639
|
-
.view-questions .opt:has(input:checked) { border-color: var(--
|
|
646
|
+
.view-questions .opt:has(input:checked) { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
640
647
|
.view-questions .opt input { margin: 3px 0 0; width: 16px; height: 16px; accent-color: var(--accent); }
|
|
641
648
|
.view-questions .opt .lbl { font-weight: 600; }
|
|
642
649
|
.view-questions .opt .rec { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
@@ -664,9 +671,9 @@ main > * { transition: opacity .25s ease; }
|
|
|
664
671
|
.view-questions .topics button { min-width: 0; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
|
|
665
672
|
background: var(--panel); color: var(--text); display: inline-flex; max-width: 100%; text-align: left; }
|
|
666
673
|
.view-questions .topics .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
667
|
-
.view-questions .topics button[aria-pressed="true"] { border-color: var(--
|
|
674
|
+
.view-questions .topics button[aria-pressed="true"] { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
668
675
|
.view-questions .topics button.progress { border-color: var(--line); background: color-mix(in srgb, var(--color-notice) 12%, var(--panel)); }
|
|
669
|
-
.view-questions .topics button.progress[aria-pressed="true"] { border-color: var(--
|
|
676
|
+
.view-questions .topics button.progress[aria-pressed="true"] { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
670
677
|
@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; } }
|
|
671
678
|
@media (prefers-reduced-motion: no-preference) { .view-questions .topics button.arrived { animation: pill-arrive .6s ease-out; } }
|
|
672
679
|
.view-questions .topics .n { color: var(--muted); margin-left: 4px; }
|
|
@@ -934,6 +941,7 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
934
941
|
/* Needs you (app-needs.js): a deck at the top of the board on a phone; from 900px a pane beside the lanes, its lines over
|
|
935
942
|
Waiting on you (docs/adr/2026-11-05-0408-needs-you-is-a-deck-on-a-phone-and-a-pane-on-a-desktop.md). */
|
|
936
943
|
.needs-pane { display: none; }
|
|
944
|
+
.col-handle { display: none; }
|
|
937
945
|
.needs-deck { margin: 0 0 12px; border: 1px solid color-mix(in srgb, var(--color-notice) 45%, var(--line)); border-radius: 14px;
|
|
938
946
|
background: color-mix(in srgb, var(--color-notice) 7%, var(--panel)); overflow: hidden; }
|
|
939
947
|
.deck-head { display: flex; align-items: center; gap: 8px; padding: 10px 14px 4px; }
|
|
@@ -948,7 +956,7 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
948
956
|
.deck-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
949
957
|
.deck-opts button { text-align: left; border-radius: 10px; padding: 10px 12px; font-size: 14px; }
|
|
950
958
|
.deck-opts button.rec { border-color: var(--accent); }
|
|
951
|
-
.deck-opts button.armed {
|
|
959
|
+
.deck-opts button.armed { border-color: var(--sel-line); background: var(--sel-bg); font-weight: 600; }
|
|
952
960
|
.deck-opts .rec-word { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
953
961
|
.deck-opts .det { color: var(--muted); font-size: 12.5px; font-weight: 400; }
|
|
954
962
|
.deck-prompt { margin: 0; padding: 0; list-style: none; }
|
|
@@ -964,16 +972,30 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
964
972
|
.deck-more::after { content: '›'; color: var(--accent); font-size: 18px; }
|
|
965
973
|
.deck-more:hover { text-decoration: none; }
|
|
966
974
|
@media (min-width: 900px) {
|
|
967
|
-
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
|
|
975
|
+
.split { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) var(--pane-w, minmax(0, 1fr)); align-items: start; }
|
|
968
976
|
.split > main { max-width: none; margin: 0; }
|
|
969
977
|
.needs-deck { display: none; }
|
|
970
978
|
/* Waiting on you is the board's pane here, so neither page shows the switch to the other. */
|
|
971
979
|
.top .pages-switch { display: none; }
|
|
972
980
|
.top .nav a.pages-switch { display: none; }
|
|
973
981
|
/* The board's + sits at the bottom right of the left pane: the pane ends at the middle of the space right of the sidebar. */
|
|
974
|
-
.view-board .fab { right: calc(50% - var(--sidebar-w, 0px) / 2 + var(--fab-edge)); }
|
|
975
|
-
body.has-sidebar { --sidebar-w: 272px; }
|
|
982
|
+
.view-board .fab { right: calc(var(--pane-w, calc(50% - var(--sidebar-w, 0px) / 2)) + var(--fab-edge)); }
|
|
983
|
+
body.has-sidebar { --sidebar-w: var(--sb-w, 272px); }
|
|
976
984
|
html.sb-collapsed body.has-sidebar { --sidebar-w: 0px; }
|
|
985
|
+
/* The drag edges (col-resize.js): a hairline that thickens on hover, on the pane's left edge and the sidebar's right. */
|
|
986
|
+
.col-handle { display: block; position: absolute; z-index: 5; top: 0; bottom: 0; width: 9px; cursor: col-resize; touch-action: none; }
|
|
987
|
+
.col-handle::after { content: ''; position: absolute; top: 0; bottom: 0; left: 4px; width: 1px; background: transparent; transition: background .12s, width .12s, left .12s; }
|
|
988
|
+
.col-handle:is(:hover, :focus-visible, .dragging)::after { left: 3px; width: 3px; background: var(--accent); }
|
|
989
|
+
.col-handle:focus-visible { outline: none; }
|
|
990
|
+
html.col-dragging, html.col-dragging * { cursor: col-resize !important; user-select: none; }
|
|
991
|
+
.split > .col-handle { right: calc(var(--pane-w, 50%) - 4px); }
|
|
992
|
+
.split:has(.needs-pane.widened) > .col-handle { display: none; }
|
|
993
|
+
/* The reader sheet (app-reader.js) is a panel over the pane's half, at the width the seam gives it, while it is open. */
|
|
994
|
+
.sheet.reader-sheet { inset: 56px 0 0 auto; margin: 0; width: var(--pane-w, calc((100vw - var(--sidebar-w, 0px)) / 2)); max-width: none;
|
|
995
|
+
height: auto; max-height: none; border-width: 0 0 0 1px; border-radius: 0; box-shadow: none; }
|
|
996
|
+
.sheet.reader-sheet[open] { animation: reader-in .22s ease-out; }
|
|
997
|
+
.sheet.reader-sheet.closing { animation: reader-out .18s ease-in forwards; }
|
|
998
|
+
.reader-sheet::backdrop { background: transparent; }
|
|
977
999
|
.needs-pane { display: block; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 12px 16px 0; border-left: 1px solid var(--line); }
|
|
978
1000
|
.needs-pane .needs-questions main { padding: 0; max-width: none; }
|
|
979
1001
|
.needs-pane:has(.q-empty), .needs-pane .needs-questions:has(.q-empty), .needs-pane .needs-questions:has(.q-empty) main, .needs-pane #page:has(.q-empty) { display: flex; flex-direction: column; }
|
|
@@ -1003,5 +1025,5 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
1003
1025
|
.pane-viewer .ps-sheet button span + span { color: var(--muted); }
|
|
1004
1026
|
}
|
|
1005
1027
|
/* 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. */
|
|
1006
|
-
body .view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1028
|
+
body .view-questions.view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1007
1029
|
.pfile.retry { cursor: pointer; }
|
package/board/public/ui-cards.js
CHANGED
|
@@ -333,9 +333,9 @@
|
|
|
333
333
|
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>`;
|
|
334
334
|
const cardActions = (it) => (it.isNew ? askActions(it) : it.ownerStep ? `<div class="ask-actions">${ownerDoneButton(it)}</div>`
|
|
335
335
|
: it.bumpable ? `<div class="ask-actions">${cardBumpButton(it.key, Boolean(it.bumpedAt))}</div>` : '');
|
|
336
|
-
// docs/adr/2026-11-07-0115-the-orchestrator-sorts-new-tasks-itself.md: a New card reads a grey "waiting", its dot
|
|
337
|
-
// still, until the orchestrator picks it up (the watcher's working mark), then "thinking".
|
|
338
|
-
const PICKUP_WAIT = '<span class="pickup-wait"><span class="working-dot" aria-hidden="true"></span>waiting
|
|
336
|
+
// docs/adr/2026-11-07-0115-the-orchestrator-sorts-new-tasks-itself.md: a New card reads a grey "waiting…", its dot
|
|
337
|
+
// still, until the orchestrator picks it up (the watcher's or the PostToolUse hook's working mark), then "thinking…".
|
|
338
|
+
const PICKUP_WAIT = '<span class="pickup-wait"><span class="working-dot" aria-hidden="true"></span>waiting…</span>';
|
|
339
339
|
const askLine = (ask) => [ask.from && `Moved from ${esc(ask.from)}`, ask.edited && 'Edited', ask.state === 'working' ? thinkingLine() : ask.state === 'waiting' && PICKUP_WAIT].filter(Boolean).join(' · ');
|
|
340
340
|
function workLines(it, { unread = 0 } = {}) {
|
|
341
341
|
const ticket = it.ticket ? ticketChip(it) : it.internal ? badge('quiet', esc(it.ref), { title: 'Work on this site, not a product PR' }) : it.linear?.length ? linearChips(it) : taskChip(it);
|
|
@@ -44,7 +44,15 @@
|
|
|
44
44
|
if (w.state === 'reset') return `Reset ${esc(usageClock(w.resets_at))}`;
|
|
45
45
|
return `<span class="u-num">${Math.round(w.used_percentage)}%</span> · resets ${esc(usageClock(w.resets_at))}`;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// Claude's week shows as a second bar once it is live within the near band under the pause line, which starts twice the
|
|
48
|
+
// buffer below 100 (shared/usage-gate.mjs nearFrom): orange under the line, red at it. Off (100) has no band.
|
|
49
|
+
function weekBar(reading, pauseAt) {
|
|
50
|
+
const w = reading.seven_day;
|
|
51
|
+
if (!(pauseAt < 100) || w?.state !== 'live' || !(w.used_percentage >= 2 * pauseAt - 100)) return '';
|
|
52
|
+
const tone = w.used_percentage >= pauseAt ? 'full' : 'high';
|
|
53
|
+
return `<span class="u-one u-tone-${tone}"><span class="u-line"><span class="u-name">Claude week</span> ${usageWords(reading, 'seven_day')}</span>${usageBar(w)}</span>`;
|
|
54
|
+
}
|
|
55
|
+
function sidebarUsage(data, pauseAt) {
|
|
48
56
|
const products = usageProducts(data);
|
|
49
57
|
return products.length ? `<span class="sb-usage">${products.map((p) => {
|
|
50
58
|
const r = data[p];
|
|
@@ -54,7 +62,7 @@
|
|
|
54
62
|
: w.state === 'stale' ? `${name} · last seen ${esc(usageClock(r.at))}`
|
|
55
63
|
: w.state === 'reset' ? `${name} · reset ${esc(usageClock(w.resets_at))}`
|
|
56
64
|
: `${name} ${usageWords(r, 'five_hour')}`;
|
|
57
|
-
return `<span class="u-one u-tone-${usageTone(w)}"><span class="u-line">${words}</span>${usageBar(w)}</span
|
|
65
|
+
return `<span class="u-one u-tone-${usageTone(w)}"><span class="u-line">${words}</span>${usageBar(w)}</span>${p === 'claude' ? weekBar(r, pauseAt) : ''}`;
|
|
58
66
|
}).join('')}</span>` : '';
|
|
59
67
|
}
|
|
60
68
|
// The utility rows, in the order they show.
|
|
@@ -227,7 +235,7 @@
|
|
|
227
235
|
const turnOn = device === 'off' ? row({ href: '/_hub/#notifications', icon: tile('bell', 'var(--accent)'), title: 'Turn on notifications', sub: 'Off on this device', cls: 'sb-turnon' })
|
|
228
236
|
: device === 'install' ? row({ href: '/_hub/#notifications', icon: tile('bell', 'var(--accent)'), title: 'Add Claude to your Home Screen', sub: 'Notifications come only to the Home Screen app', cls: 'sb-turnon' })
|
|
229
237
|
: '';
|
|
230
|
-
const usage = sidebarUsage(data?.usage);
|
|
238
|
+
const usage = sidebarUsage(data?.usage, data?.pause?.pause_at);
|
|
231
239
|
const hub = data?.hub?.tone === 'down' || data?.hub?.tone === 'wait' ? data.hub : paused ? { tone: 'paused', word: 'paused', detail: pauseUntilWords(data.pause.resets_at) } : busy ? { tone: 'wait', word: 'busy', detail: 'the host is busy' } : data?.hub;
|
|
232
240
|
const n = data?.notifications || {};
|
|
233
241
|
const lower = (text) => String(text || '').replace(/^\w/, (c) => c.toLowerCase());
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const stateOf = (m) => m.state || (m.reply ? 'replied' : 'waiting');
|
|
21
21
|
// A message's footer: the tap it came from (m.status, such as "answers submitted"), then, to its right, Claude's
|
|
22
22
|
// pick-up while it waits (docs/adr/2026-11-04-2130-a-button-tap-is-a-status-line-or-a-message-footer.md).
|
|
23
|
-
const PICK_UP = { working: thinkingLine, waiting: () => '
|
|
23
|
+
const PICK_UP = { working: thinkingLine, waiting: () => 'waiting…' };
|
|
24
24
|
const msgFooter = (m) => {
|
|
25
25
|
const parts = [m.status && `<span class="foot-status">${statusMark(m.status)}${esc(m.status)}</span>`, PICK_UP[stateOf(m)]?.()].filter(Boolean);
|
|
26
26
|
return parts.length ? `<div class="reply muted small">${parts.join('')}</div>` : '';
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
: m.request ? `<div class="msg status-line request${m.command === 'readied' ? ' readied' : ''}">${statusMark(m.mark || m.text)}${escText(m.text)} <span class="muted">· ${m.by ? `${esc(m.by)} · ` : ''}${esc(time(m.ts))}</span></div>`
|
|
50
50
|
// docs/adr/2026-11-09-1200-the-board-writes-status-lines-and-only-the-orchestrator-writes-messages.md: the board's
|
|
51
51
|
// own line, its kind's mark and words, red for a fault.
|
|
52
|
+
: m.kind === 'hub' && m.detail ? `<details class="msg status-line request hub fault-detail${m.fault ? ' fault' : ''}"><summary>${statusMark(m.mark)}${escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></summary><pre class="fault-error">${esc(m.detail)}</pre></details>`
|
|
52
53
|
: m.kind === 'hub' ? `<div class="msg status-line request hub${m.fault ? ' fault' : ''}">${statusMark(m.mark)}${m.link ? `<a href="${esc(m.link)}">${escText(m.text)}</a>` : escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></div>`
|
|
53
54
|
// docs/adr/2026-11-07-1400-a-card-stores-its-lane-and-facts-replace-statuses.md: a card's move between lanes.
|
|
54
55
|
: m.kind === 'move' ? `<div class="msg status-line request move">${statusMark('move')}${escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></div>`
|
package/board/public/ui.js
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
const cardBumpButton = (key, bumped) => (bumped
|
|
137
137
|
? `<button class="ghost icon bumped" data-card-bump="${esc(key)}" data-bumped aria-pressed="true" aria-label="Unbump" title="Bumped. Tap to unbump">${BUMP}</button>`
|
|
138
138
|
: `<button class="ghost icon" data-card-bump="${esc(key)}" aria-pressed="false" aria-label="Bump to top" title="Bump to top">${BUMP}</button>`);
|
|
139
|
-
const workingLine = '<span class="thinking"><span class="working-dot" aria-hidden="true"></span>thinking
|
|
139
|
+
const workingLine = '<span class="thinking"><span class="working-dot" aria-hidden="true"></span>thinking…</span>';
|
|
140
140
|
// Every paused status, whatever its place, is this one class (style.css .paused); BoardUI.usagePause is set by the
|
|
141
141
|
// board view (app.js) to read the hub's usage pause.
|
|
142
142
|
const PAUSED = '<span class="paused">paused</span>';
|
package/board/public/view-hub.js
CHANGED
|
@@ -91,7 +91,15 @@ ${d.empty ? row({ title: 'No sites yet', sub: 'In a Claude session, run <code>/k
|
|
|
91
91
|
${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembered?.length ? `<h2 class="label">Sessions without a board</h2><div class="group">${rememberedHtml(d.remembered)}</div>` : ''}<p class="note">The hub runs every site and restarts one that stops. A board's links are what the hub owes it: GitHub polling, the fetch of its repo, events (its session and a watcher run, or the hub can wake it), daily wakes and reviewer links. One that just broke reads settling first.</p>`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// The version is a button: a tap checks npm now (POST /_hub/check-update) and the answer shows beside it (updateNote).
|
|
95
|
+
const footer = (d, note = '') => `<footer>${esc(d.sites)}. Each belongs to one Claude session, which adds it with <code>/kickflip <route></code>. This page refreshes every 30 seconds.<br><span class="host">${esc(d.host)}</span><br><span class="brand"><a href="https://kickflip.sh">kickflip.sh</a> / <button type="button" class="version" data-check-update>v${esc(d.version)}</button>${note ? ` <span class="update-note" role="status">${esc(note)}</span>` : ''}</span></footer>`;
|
|
96
|
+
// What the hub's answer to a version tap says (hub/upgrade.mjs check()).
|
|
97
|
+
const updateNote = (out) => ({
|
|
98
|
+
clone: `${out.commit} · pulls with main`,
|
|
99
|
+
same: 'up to date',
|
|
100
|
+
pulling: `pulling ${out.latest}…`,
|
|
101
|
+
failed: `check failed: ${out.error}`,
|
|
102
|
+
})[out.state];
|
|
95
103
|
|
|
96
104
|
// iOS shows an app badge only in a Home Screen app with notifications allowed; elsewhere this does nothing.
|
|
97
105
|
function badge(n) {
|
|
@@ -166,6 +174,20 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
166
174
|
flash = { text: `The hub hasn’t come back. On the host, run ${out.command}`, error: true };
|
|
167
175
|
if (B.alive()) paint();
|
|
168
176
|
}
|
|
177
|
+
// A tap on the version asks the hub to check npm now and shows the answer beside it until the next paint of new data.
|
|
178
|
+
let updateText = '';
|
|
179
|
+
async function checkUpdate() {
|
|
180
|
+
if (updateText === 'checking…') return;
|
|
181
|
+
updateText = 'checking…';
|
|
182
|
+
paint();
|
|
183
|
+
try {
|
|
184
|
+
const r = await fetch('/_hub/check-update', { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: '{}' });
|
|
185
|
+
const out = await r.json();
|
|
186
|
+
if (!r.ok) throw new Error();
|
|
187
|
+
updateText = updateNote(out);
|
|
188
|
+
} catch { updateText = 'could not check. Is the host awake?'; }
|
|
189
|
+
if (B.alive()) paint();
|
|
190
|
+
}
|
|
169
191
|
async function forgetDevice(id) {
|
|
170
192
|
const out = await fetch('/_hub/push/forget', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) }).then((r) => r.ok, () => false);
|
|
171
193
|
if (!out) { flash = { text: 'Could not remove that device. Is the host awake?', error: true }; paint(); return; }
|
|
@@ -181,12 +203,13 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
181
203
|
if (restarter) restart(restarter.dataset.restart, restarter.dataset.title);
|
|
182
204
|
});
|
|
183
205
|
const foot = B.$('.hub-foot');
|
|
206
|
+
B.on(foot, 'click', (e) => { if (e.target.closest?.('[data-check-update]')) checkUpdate(); });
|
|
184
207
|
// A #keep-awake link scrolls to Health's Keep awake row once it is drawn.
|
|
185
208
|
let scrollToAwake = ctx.view?.hash === '#keep-awake';
|
|
186
209
|
const paint = () => {
|
|
187
210
|
if (!last) return;
|
|
188
211
|
main.innerHTML = html(last, { flash, prompts, restarting, restartingHub });
|
|
189
|
-
foot.innerHTML = footer(last);
|
|
212
|
+
foot.innerHTML = footer(last, updateText);
|
|
190
213
|
if (scrollToAwake) { scrollToAwake = false; B.$('#keep-awake')?.scrollIntoView(); }
|
|
191
214
|
};
|
|
192
215
|
const prompts = PromptAnswers.wire(B, {
|
package/board/render-check.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0];let
|
|
3
|
-
`)[0]||t.text,"described");async function
|
|
4
|
-
`)[0]}`):
|
|
1
|
+
var A=Object.defineProperty;var l=(t,e)=>A(t,"name",{value:e,configurable:!0});import g from"node:fs";import k from"node:fs/promises";import B from"node:http";import P from"node:os";import m from"node:path";import{spawn as R}from"node:child_process";import{setTimeout as b}from"node:timers/promises";import{fileURLToPath as D}from"node:url";import{createInterface as O}from"node:readline";import{BoardError as w,readPrototypes as F}from"./store.mjs";import{prototypePage as G}from"./prototype-page.mjs";import{BASE_KEY as U,baseWorktreeOf as K}from"./prototype-base.mjs";import{buildScriptError as W,buildScriptOf as C}from"./build-script.mjs";import{boardAt as q}from"../hub/boards.mjs";const S=m.dirname(D(import.meta.url)),_=[390,1280],j=process.env.CHROME||"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",T={".js":"text/javascript",".css":"text/css",".html":"text/html",".json":"application/json",".png":"image/png",".svg":"image/svg+xml"},N=3e3,J=300,Y=12e4,M="(document.getElementById('stage')?.innerHTML || '').trim()",V="(document.body?.innerText || '').trim()";function z(t,e){const n=[m.join(t,"public"),m.join(S,"public"),m.join(S,"..","starter","public"),m.join(S,"..","shared")];return B.createServer(async(u,r)=>{let o=decodeURIComponent(new URL(u.url,"http://x").pathname);const d=/^\/_hub\/(icon-(?:180|192|512)\.png)$/.exec(o);if(d){r.writeHead(200,{"content-type":T[".png"]}).end(await k.readFile(m.join(S,"..","hub",d[1])));return}if(["/_hub/kickflip.png","/_hub/hub.css","/_hub/colors.css"].includes(o)&&(o=o.slice(5)),o===`/prototype-${e.topic}.html`){r.writeHead(200,{"content-type":T[".html"]}).end(await G(t,e));return}for(const c of n){const i=m.resolve(c,`.${o}`);if(i.startsWith(c+m.sep)&&g.existsSync(i)&&g.statSync(i).isFile()){r.writeHead(200,{"content-type":T[m.extname(i)]||"application/octet-stream"}).end(await k.readFile(i));return}}r.writeHead(o==="/favicon.ico"?204:404).end()})}l(z,"serve");async function H(t){if(!g.existsSync(j))throw new w(`the render check needs Chrome; none at ${j}, set CHROME to its path`);const e=R(j,["--headless=new","--remote-debugging-port=0",`--user-data-dir=${t}`,"--no-first-run","about:blank"],{stdio:"ignore",env:{...process.env,HOME:P.userInfo().homedir}}),n=m.join(t,"DevToolsActivePort");for(let a=0;a<100&&!g.existsSync(n);a++)await b(100);if(!g.existsSync(n))throw e.kill(),new w("the render check could not start Chrome");const u=g.readFileSync(n,"utf8").split(`
|
|
2
|
+
`)[0];let r;for(let a=0;a<100&&!r;a++)r=(await fetch(`http://127.0.0.1:${u}/json`).then(p=>p.json(),()=>[])).find(p=>p.type==="page"),r||await b(100);if(!r)throw e.kill(),new w("the render check could not start Chrome");const o=new WebSocket(r.webSocketDebuggerUrl);try{await new Promise((a,p)=>{o.onopen=a,o.onerror=p})}catch{throw e.kill(),new w("the render check could not start Chrome")}let d=0;const c=new Map,i=[];return o.onmessage=({data:a})=>{const p=JSON.parse(a);p.id?c.get(p.id)?.(p):i.forEach(f=>f(p))},{cdp:l((a,p={})=>new Promise((f,$)=>{const y=++d;c.set(y,v=>v.error?$(new Error(v.error.message)):f(v.result)),o.send(JSON.stringify({id:y,method:a,params:p}))}),"cdp"),on:l(a=>(i.push(a),()=>i.splice(i.indexOf(a),1)),"on"),close:l(async()=>{o.close();const a=new Promise(p=>e.once("exit",p));e.kill(),await a},"close")}}l(H,"launchOnce");async function Q(t){try{return await H(t)}catch(e){if(!(e instanceof w)||!/could not start Chrome/.test(e.message))throw e;return await k.rm(t,{recursive:!0,force:!0}),await k.mkdir(t),H(t)}}l(Q,"launch");const X=l(t=>t.exception?.description?.split(`
|
|
3
|
+
`)[0]||t.text,"described");async function I({cdp:t,on:e},n,u,r=M){const o=[],d=e(s=>{s.method==="Runtime.exceptionThrown"?o.push(X(s.params.exceptionDetails)):s.method==="Runtime.consoleAPICalled"&&s.params.type==="error"?o.push(`console error: ${s.params.args.map(h=>h.value??h.description).join(" ").split(`
|
|
4
|
+
`)[0]}`):s.method==="Log.entryAdded"&&s.params.entry.level==="error"&&s.params.entry.source!=="javascript"&&!/\/favicon\.ico$/.test(s.params.entry.url||"")&&o.push(`${s.params.entry.text}${s.params.entry.url?` ${s.params.entry.url}`:""}`)});await t("Emulation.setDeviceMetricsOverride",{width:u,height:900,deviceScaleFactor:1,mobile:u<600}),await t("Page.navigate",{url:n});const c=l(async()=>(await t("Runtime.evaluate",{expression:r,returnByValue:!0})).result.value,"stage");let i="";for(let s=0;s<N&&!i;s+=100)await b(100),i=await c();return await b(J),d(),i||o.push(r===M?"renders an empty stage":"renders an empty page"),[...new Set(o)]}l(I,"checkTake");async function Z(t){const e=q(t);if(!e)throw new w(`${t} is not a board in the hub's manifest, so it has no base prototype worktree to check`);const n=K(e.route),u=m.join(n,C(e.build_target)),r=W(u);if(r)throw new w(`the base prototype worktree can't serve the round: ${r} (${u})`);const o=R(u,[U,n],{cwd:n,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:e.route,HOST:"127.0.0.1"}}),d=[],c=l(f=>{d.push(f),d.length>5&&d.shift()},"keep");O({input:o.stderr}).on("line",c);const i=new Promise(f=>o.once("exit",f)),s=l(async()=>{try{process.kill(-o.pid,"SIGTERM")}catch{return}if(!await Promise.race([i.then(()=>!0),b(4e3).then(()=>!1)])){try{process.kill(-o.pid,"SIGKILL")}catch{}await i}},"stop"),h=await new Promise(f=>{const $=O({input:o.stdout}),y=l(x=>{clearTimeout(v),$.close(),f(x)},"done"),v=setTimeout(()=>y(null),Y);$.on("line",x=>{const E=/^port (\d{1,5})$/.exec(x);E?y(Number(E[1])):c(x)}),i.then(()=>y(null))}),a=l(()=>fetch(`http://127.0.0.1:${h}/`,{signal:AbortSignal.timeout(1e3)}).then(()=>!0,()=>!1),"answers");let p=!!h;for(let f=0;p&&!await a();f+=250)f>=6e4?p=!1:await b(250);if(!p)throw await s(),new w(`the base prototype worktree's ${C(e.build_target)} didn't serve on a port, so the round's question is not published:
|
|
5
5
|
${d.join(`
|
|
6
|
-
`)}`);return{origin:`http://127.0.0.1:${
|
|
6
|
+
`)}`);return{origin:`http://127.0.0.1:${h}`,dir:n,stop:s}}l(Z,"serveBase");async function L(t){const e=await k.mkdtemp(m.join(P.tmpdir(),"render-check-"));let n;try{return n=await Q(e),await n.cdp("Runtime.enable"),await n.cdp("Log.enable"),await t(n)}finally{await n?.close(),await k.rm(e,{recursive:!0,force:!0,maxRetries:30,retryDelay:200})}}l(L,"withChrome");async function ee(t,e){const n=await Z(t);try{const u=e.takes[0].key,r=(e.states||[]).map(c=>c.key),o=[...e.takes.map(c=>[c.key,r[0]??null]),...r.slice(1).map(c=>[u,c])],d=await L(async c=>{const i=[];for(const[s,h]of o){const a=new URL(e.served,n.origin);a.searchParams.set("variant",s),h&&a.searchParams.set("state",h);for(const p of _)for(const f of await I(c,a.href,p,V))i.push(`take ${s}${h?`, state ${h}`:""} at ${p}px: ${f}`)}return i});if(d.length)throw new w(`prototype ${e.topic} does not render, so its question is not published; fix its page in ${n.dir} and ask again:
|
|
7
7
|
${d.join(`
|
|
8
|
-
`)}`)}finally{await
|
|
9
|
-
${
|
|
10
|
-
`)}`)}finally{u.close()}}l(we,"checkTakesRender");export{
|
|
8
|
+
`)}`)}finally{await n.stop()}}l(ee,"checkServedRender");async function we(t,e){const n=(await F(t)).find(r=>r.topic===e);if(n.served)return ee(t,n);const u=z(t,n);await new Promise(r=>u.listen(0,"127.0.0.1",r));try{const r=`http://127.0.0.1:${u.address().port}/prototype-${e}.html`,o=await L(async d=>{const c=[];for(const i of n.takes)for(const s of _)for(const h of await I(d,`${r}?variant=${i.key}`,s))c.push(`take ${i.key} at ${s}px: ${h}`);return c});if(o.length)throw new w(`prototype ${e} does not render, so its question is not published; fix the take files and ask again:
|
|
9
|
+
${o.join(`
|
|
10
|
+
`)}`)}finally{u.close()}}l(we,"checkTakesRender");export{_ as WIDTHS,we as checkTakesRender};
|