Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions web/board/src/board/board.css
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,61 @@ details.confidential-notice > summary:hover { color: var(--ink); }
}

.gate-rail {
position: relative;
border: 1px solid var(--gate-line);
border-left: 4px solid var(--gate);
background: var(--gate-bg);
border-radius: var(--radius);
margin-bottom: 12px;
}
/* The head is the FOLD TOGGLE — a full-width button that shows the count and
opens/closes the rail. Collapsed, it is the whole rail: one fixed line. */
.gate-rail-head {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px 9px;
flex-wrap: wrap;
width: 100%;
font: inherit;
text-align: left;
background: none;
border: 0;
cursor: pointer;
}
.gate-rail-caret {
margin-left: auto;
font-size: 11.5px;
font-weight: 640;
color: var(--gate);
border: 1px solid var(--gate-line);
border-radius: 100px;
padding: 2px 10px;
flex: none;
}
.gate-rail-head:hover .gate-rail-caret { background: var(--gate); color: #fff; border-color: var(--gate); }
@media (prefers-color-scheme: dark) { .gate-rail-head:hover .gate-rail-caret { color: #1a1408; } }
:root[data-theme="dark"] .gate-rail-head:hover .gate-rail-caret { color: #1a1408; }
/* COLLAPSED: the list is out of flow entirely, so gates folding in over the
first minute never push the board or reflow the view (ready-...). */
.gate-rail:not(.open) .gate-list { display: none; }
/* EXPANDED: the list OVERLAYS the board — absolute, capped, scrollable — so even
opening it (a deliberate act) never shoves the real content down. */
.gate-rail.open { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.gate-rail.open .gate-list {
position: absolute;
top: 100%;
left: -1px;
right: -1px;
z-index: 40;
max-height: 62vh;
overflow-y: auto;
background: var(--gate-bg);
border: 1px solid var(--gate-line);
border-top: 0;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.2);
padding: 8px;
}
.gate-rail-dot {
width: 8px;
Expand Down
15 changes: 15 additions & 0 deletions web/board/src/board/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,21 @@ describe("the gate rail is a stack of self-contained cards", () => {
// and there is exactly one card per gate — not a header + a loose control
expect(container.querySelectorAll(".gate-list > .gate-item").length).toBe(1);
});

it("folds to a banner by default and expands on click, without dropping the cards", () => {
const gated = makeItem({ id: "g1", status: "waiting", waitingType: "gate", gateMsgId: "m1", gate: "design" });
ws = mountBoardWorkspace(container, [gated]);
// Collapsed by default: no .open, so the list is out of flow (CSS) and gates
// folding in never push the board. The head is the toggle.
expect(container.querySelector(".gate-rail")!.classList.contains("open")).toBe(false);
expect(container.querySelector(".gate-rail-head")!.getAttribute("aria-expanded")).toBe("false");
// The cards stay in the DOM even when folded — nothing is lost, it is display only.
expect(container.querySelector('.gate-item[data-id="g1"]')).not.toBeNull();
// Clicking the head expands it.
container.querySelector<HTMLElement>(".gate-rail-head")!.click();
expect(container.querySelector(".gate-rail")!.classList.contains("open")).toBe(true);
expect(container.querySelector(".gate-rail-head")!.getAttribute("aria-expanded")).toBe("true");
});
});

describe("board identity: names, never coordinates", () => {
Expand Down
36 changes: 26 additions & 10 deletions web/board/src/board/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ export class BoardWorkspace {
/** Whether the degraded-board status list is showing all rows or just the
* first BOARD_STATUS_CAP behind a "+N more" disclosure (ready-412). */
private boardStatusExpanded = false;
/** Whether the "Waiting on you" gate rail is folded to its one-line banner
* (default) or expanded to the overlay of gate cards (ready-...). Collapsed by
* default so gates folding in during load never push the board or reflow it. */
private gateRailExpanded = false;
private readonly onKeydown = (e: KeyboardEvent) => {
if (e.key === "Escape" && this.selectedId !== undefined) {
this.closeDetail();
Expand Down Expand Up @@ -964,17 +968,29 @@ export class BoardWorkspace {
return clear;
}

const rail = el("div", { className: "gate-rail" });
rail.append(
el("div", { className: "gate-rail-head" }, [
el("span", { className: "gate-rail-dot" }),
el("h2", { className: "gate-rail-heading", textContent: "Waiting on you" }),
el("span", {
className: "gate-rail-sub",
textContent: `${gated.length} of ${this.openCount()} — the only ones that cannot move without your decision.`,
}),
]),
// A FOLDABLE BANNER, not a growing wall (ready-...). Collapsed, the rail is
// one fixed-height line — so gates folding in over the first minute only tick
// the count, they never push the board down or reflow the view under a reader
// who is just looking. Expanding is a deliberate act, and even then the list
// OVERLAYS the board (CSS: .gate-rail.open .gate-list is absolute) rather than
// shoving it. The list is always in the DOM; only its display is folded.
const rail = el("div", { className: `gate-rail${this.gateRailExpanded ? " open" : ""}` });
const head = el("button", { className: "gate-rail-head", type: "button" });
head.setAttribute("aria-expanded", String(this.gateRailExpanded));
head.append(
el("span", { className: "gate-rail-dot" }),
el("span", { className: "gate-rail-heading", textContent: "Waiting on you" }),
el("span", {
className: "gate-rail-sub",
textContent: `${gated.length} of ${this.openCount()} — the only ones that cannot move without your decision.`,
}),
el("span", { className: "gate-rail-caret", textContent: this.gateRailExpanded ? "Hide" : "Show" }),
);
head.addEventListener("click", () => {
this.gateRailExpanded = !this.gateRailExpanded;
this.render();
});
rail.append(head);

const list = el("ul", { className: "gate-list" });
for (const item of gated) {
Expand Down
Loading