Merge panel-design-fidelity: close the git-panel design gaps

This commit is contained in:
Yaojia Wang
2026-07-29 20:47:38 +02:00
4 changed files with 288 additions and 83 deletions

View File

@@ -87,12 +87,16 @@ function relTime(ms: number): string {
* individual rows. */ * individual rows. */
function renderCommitRow(c: CommitLogEntry): HTMLElement { function renderCommitRow(c: CommitLogEntry): HTMLElement {
const row = el('div', 'proj-commit-row') const row = el('div', 'proj-commit-row')
// The mark cell is rendered on EVERY row, empty when the commit is pushed.
// Appending it only for unpushed rows left the grid's first track unoccupied on
// pushed rows, so the sha, age and subject columns shifted between lines and
// the unpushed group stopped reading as a block.
const mark = el('span', 'proj-commit-mark', c.unpushed === true ? '↑' : '')
if (c.unpushed === true) { if (c.unpushed === true) {
row.classList.add('proj-commit-unpushed') row.classList.add('proj-commit-unpushed')
const mark = el('span', 'proj-commit-mark', '↑')
mark.title = 'Not pushed yet' mark.title = 'Not pushed yet'
row.append(mark)
} }
row.append(mark)
row.append(el('span', 'proj-commit-hash', c.hash)) row.append(el('span', 'proj-commit-hash', c.hash))
row.append(el('span', 'proj-commit-time', `${relTime(c.at)} ago`)) row.append(el('span', 'proj-commit-time', `${relTime(c.at)} ago`))
row.append(el('span', 'proj-commit-subject', c.subject)) // attacker-influenced → textContent row.append(el('span', 'proj-commit-subject', c.subject)) // attacker-influenced → textContent

Binary file not shown.

View File

@@ -2,6 +2,7 @@
:root { :root {
/* palette (design tokens) — Amber 琥珀金 theme (warm-neutral base + gold accent) */ /* palette (design tokens) — Amber 琥珀金 theme (warm-neutral base + gold accent) */
--sunk: #0c0b09; /* recessed well — BELOW --bg, for data sunk into the page */
--bg: #100f0d; /* terminal / deepest surface — warm near-neutral */ --bg: #100f0d; /* terminal / deepest surface — warm near-neutral */
--surface-1: #181613; /* tab bar, key bar */ --surface-1: #181613; /* tab bar, key bar */
--surface-2: #1f1c17; /* tabs, buttons, cards */ --surface-2: #1f1c17; /* tabs, buttons, cards */
@@ -18,6 +19,16 @@
--green: #46d07f; /* connection-status: connected */ --green: #46d07f; /* connection-status: connected */
--amber: #f5b14c; /* connection-status: connecting */ --amber: #f5b14c; /* connection-status: connecting */
--red: #ff6b6b; /* connection-status: disconnected / error */ --red: #ff6b6b; /* connection-status: disconnected / error */
/* Git-panel semantics (docs/mockups/project-detail-git.html). Deliberately NOT
* --accent: the accent means "work sitting on this machine", so a state colour
* must never borrow it. --ok is the only green and is reachable from exactly
* one state (ahead 0, behind 0, freshly fetched) — see the sync band. */
--warn: #cf6b4f; /* stale / no upstream / detached / validation error */
--ok: #7d9b76; /* verified in sync — the ONLY green */
--dirty: #e0975a; /* uncommitted changes */
--mono-font: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--shadow: 0 10px 40px rgba(0, 0, 0, 0.55); --shadow: 0 10px 40px rgba(0, 0, 0, 0.55);
--radius: 9px; --radius: 9px;
--radius-lg: 14px; --radius-lg: 14px;
@@ -1568,23 +1579,33 @@ body {
.proj-syncband { .proj-syncband {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
margin: 10px 0 4px; margin: 16px 0 4px;
background: var(--surface-1); background: var(--sunk);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 9px; border-radius: var(--radius);
overflow: hidden; overflow: hidden;
flex-wrap: wrap;
} }
/* No flex-wrap. Wrapping produced ragged half-empty rows between 721px and
1000px — at 900px the Fetch cell dropped alone onto a second row. The cells
hold their natural width and only the footnote cell gives ground, so the band
stays one row all the way down to the 720px column break. */
.proj-sync-cell { .proj-sync-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
padding: 11px 18px; padding: 12px 18px;
min-width: 0; min-width: 0;
flex: none;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
} }
/* The one cell carrying a long sentence — it absorbs the shrink so the upstream
name and the Fetch button are never squeezed or clipped. */
.proj-sync-cell-flex {
flex: 0 1 auto;
}
.proj-sync-cell:last-child { .proj-sync-cell:last-child {
border-right: 0; border-right: 0;
} }
@@ -1623,7 +1644,7 @@ body {
} }
.proj-sync-sub-bad { .proj-sync-sub-bad {
color: #cf6b4f; color: var(--warn);
} }
.proj-sync-upstream { .proj-sync-upstream {
@@ -1631,26 +1652,40 @@ body {
} }
.proj-sync-chip { .proj-sync-chip {
font-family: var(--mono-font);
font-size: 11.5px; font-size: 11.5px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
border-radius: 5px; border-radius: 6px;
padding: 2px 8px; padding: 3px 9px;
white-space: nowrap; white-space: nowrap;
flex: none; flex: none;
border: 1px solid transparent; border: 1px solid transparent;
} }
/* Accent means "there is work sitting only on this machine". A zero is not that,
so it drops to the faint role rather than glowing gold at you. */
.proj-sync-ahead { .proj-sync-ahead {
color: var(--accent); color: var(--accent);
} }
/* Same class, two jobs: on the band's big number it is bare coloured text, but
in a worktree row it is a chip and needs the fill every other chip has. */
.proj-sync-chip.proj-sync-ahead {
background: var(--accent-soft);
border-color: rgba(227, 166, 74, 0.36);
}
.proj-sync-behind { .proj-sync-behind {
color: var(--text-dim);
}
.proj-sync-zero {
color: var(--text-faint); color: var(--text-faint);
} }
/* The one reassuring state. Reachable only with an upstream AND a fresh fetch. */ /* The one reassuring state. Reachable only with an upstream AND a fresh fetch. */
.proj-sync-ok { .proj-sync-ok {
color: #7d9b76; color: var(--ok);
background: rgba(125, 155, 118, 0.13); background: rgba(125, 155, 118, 0.13);
border-color: rgba(125, 155, 118, 0.3); border-color: rgba(125, 155, 118, 0.3);
} }
@@ -1659,7 +1694,7 @@ body {
.proj-sync-stale, .proj-sync-stale,
.proj-sync-noupstream, .proj-sync-noupstream,
.proj-sync-detached { .proj-sync-detached {
color: #cf6b4f; color: var(--warn);
} }
.proj-sync-chip.proj-sync-stale, .proj-sync-chip.proj-sync-stale,
@@ -1670,11 +1705,12 @@ body {
} }
.proj-sync-dirty { .proj-sync-dirty {
color: var(--amber); font-family: var(--mono-font);
color: var(--dirty);
background: rgba(224, 151, 90, 0.13); background: rgba(224, 151, 90, 0.13);
border: 1px solid rgba(224, 151, 90, 0.3); border: 1px solid rgba(224, 151, 90, 0.3);
border-radius: 5px; border-radius: 6px;
padding: 2px 8px; padding: 3px 9px;
font-size: 11.5px; font-size: 11.5px;
white-space: nowrap; white-space: nowrap;
flex: none; flex: none;
@@ -1721,14 +1757,17 @@ body {
} }
} }
/* ── w6: unpushed count beside the commit-list heading ───────────────────── */ /* ── w6: counts sitting on a section heading ─────────────────────────────────
.proj-commitlog-count { The heading is tracked-out uppercase; a count is data and opts out of both,
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; so it reads as a qualifier rather than more heading. */
.proj-commitlog-count,
.proj-section-count {
font-family: var(--mono-font);
font-size: 11.5px; font-size: 11.5px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--accent); color: var(--text-dim);
text-transform: none; text-transform: none;
letter-spacing: 0; letter-spacing: 0.04em;
margin-left: 10px; margin-left: 10px;
} }
@@ -1736,9 +1775,14 @@ body {
/* NOTE: the row's own display/grid lives with the ORIGINAL .proj-wt-row rule /* NOTE: the row's own display/grid lives with the ORIGINAL .proj-wt-row rule
further down. Declaring it twice let the later copy silently win and the further down. Declaring it twice let the later copy silently win and the
Open button never reached the right edge. One rule per selector. */ Open button never reached the right edge. One rule per selector. */
.proj-wt-row-current { /* Two classes, not one: the plain `.proj-wt-row` rule lives further down the file
and its `background`/`border` SHORTHANDS were silently winning at equal
specificity, so "you are here" rendered identically to every other row. The
gradient must land on --surface-2 (the card colour), not `transparent`, or the
panel background shows through the right 54% of the card. */
.proj-wt-row.proj-wt-row-current {
border-color: rgba(227, 166, 74, 0.36); border-color: rgba(227, 166, 74, 0.36);
background: linear-gradient(90deg, var(--accent-soft), transparent 46%); background: linear-gradient(90deg, var(--accent-soft), var(--surface-2) 46%);
} }
.proj-wt-main { .proj-wt-main {
@@ -1762,7 +1806,13 @@ body {
flex: none; flex: none;
} }
.proj-wt-open { /* The design has exactly two button roles: filled accent for the action that
commits a form (Fetch, Create Worktree), and this ghost for anything that only
opens or reveals. Everything in a worktree's action rail is a ghost — including
the ✕, which previously had no rule at all and rendered as a raw grey OS button. */
.proj-wt-open,
.proj-wt-remove,
.proj-wt-prune {
font: inherit; font: inherit;
font-size: 11.5px; font-size: 11.5px;
cursor: pointer; cursor: pointer;
@@ -1770,24 +1820,48 @@ body {
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--border-strong); border: 1px solid var(--border-strong);
background: transparent; background: transparent;
color: var(--text); color: var(--text-dim);
} }
.proj-wt-open:hover { .proj-wt-open:hover,
.proj-wt-remove:hover,
.proj-wt-prune:hover {
border-color: var(--accent); border-color: var(--accent);
color: var(--accent); color: var(--accent);
} }
.proj-wt-open:focus-visible { .proj-wt-open:focus-visible,
.proj-wt-remove:focus-visible,
.proj-wt-prune:focus-visible {
outline: 2px solid var(--text); outline: 2px solid var(--text);
outline-offset: 2px; outline-offset: 2px;
} }
/* Square icon variant — same role, just not a word. Height matches Open so the
two sit flush in the rail. */
.proj-wt-remove {
width: 26px;
padding: 0;
line-height: 24px;
text-align: center;
flex: none;
}
.proj-wt-remove:hover {
border-color: var(--red);
color: var(--red);
}
/* Destructive and rarely wanted — it sits under the list, not in a row. */
.proj-wt-prune {
margin-top: 8px;
}
/* ── w6/G4: unpushed commits + the upstream boundary ───────────────────────── */ /* ── w6/G4: unpushed commits + the upstream boundary ───────────────────────── */
.proj-commit-unpushed { .proj-commit-unpushed {
position: relative; position: relative;
background: linear-gradient(90deg, var(--accent-soft), transparent 55%); background: linear-gradient(90deg, var(--accent-soft), transparent 55%);
border-radius: 4px; border-radius: 5px;
} }
.proj-commit-unpushed::before { .proj-commit-unpushed::before {
@@ -1801,11 +1875,12 @@ body {
background: var(--accent); background: var(--accent);
} }
/* Fills its own 22px grid track — sizing it here would knock the columns out of
alignment again. Pushed rows render an empty mark so the track still exists. */
.proj-commit-mark { .proj-commit-mark {
font-family: var(--mono-font);
color: var(--accent); color: var(--accent);
font-size: 11px; font-size: 12px;
flex: none;
width: 12px;
text-align: center; text-align: center;
} }
@@ -1813,16 +1888,20 @@ body {
.proj-commit-boundary { .proj-commit-boundary {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
margin: 7px 0 5px; margin: 9px 0 7px;
padding-left: 8px;
} }
.proj-commit-boundary-label { .proj-commit-boundary-label {
font-size: 10.5px; font-family: var(--mono-font);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--text-dim); color: var(--text-dim);
background: var(--sunk);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 999px; border-radius: 999px;
padding: 2px 10px; padding: 3px 11px;
white-space: nowrap; white-space: nowrap;
flex: none; flex: none;
} }
@@ -1833,18 +1912,30 @@ body {
background: var(--border-strong); background: var(--border-strong);
} }
/* w6/G7: running sessions attributed to one worktree row. */ /* w6/G7: running sessions attributed to one worktree row. Muted mono text with a
live dot, not a filled pill — a session count is ambient status, and an accent
fill here competed with the ↑ backlog chip beside it for the same attention. */
.proj-wt-sessions { .proj-wt-sessions {
font-size: 11px; display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--mono-font);
font-size: 11.5px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--accent); color: var(--text-dim);
background: var(--accent-soft);
border-radius: 5px;
padding: 2px 7px;
white-space: nowrap; white-space: nowrap;
flex: none; flex: none;
} }
.proj-wt-sessions::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
flex: none;
}
/* W3(b): cost chip in the per-tab telemetry gauge, warn-styled over budget. */ /* W3(b): cost chip in the per-tab telemetry gauge, warn-styled over budget. */
.tg-cost-warn { .tg-cost-warn {
color: var(--red); color: var(--red);
@@ -1862,36 +1953,63 @@ body {
.proj-commitlog-list { .proj-commitlog-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px;
} }
/* A grid, not a flex row: every column has to land in the same place on every
line, or the unpushed block above the boundary does not read as a block. Flex
sized each row to its own content, and the ↑ mark only existed on unpushed
rows, so no two lines agreed on where the sha started. */
.proj-commit-row { .proj-commit-row {
display: flex; display: grid;
grid-template-columns: 22px 74px 56px 1fr;
align-items: baseline; align-items: baseline;
gap: 8px; gap: 10px;
padding: 4px 8px;
font-size: 12px; font-size: 12px;
min-width: 0; min-width: 0;
} }
.proj-commit-hash { .proj-commit-hash {
font-family: Menlo, Consolas, monospace; font-family: var(--mono-font);
color: var(--accent); color: var(--accent-2);
flex: none;
} }
.proj-commit-time { .proj-commit-time {
font-family: var(--mono-font);
font-size: 11.5px;
color: var(--text-faint); color: var(--text-faint);
flex: none; text-align: right;
white-space: nowrap; white-space: nowrap;
} }
.proj-commit-subject { .proj-commit-subject {
font-size: 13.5px;
color: var(--text); color: var(--text);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
min-width: 0; min-width: 0;
} }
/* Already pushed — history, not your working set. Stepping it down is what makes
the boundary line mean something. */
.proj-commit-row:not(.proj-commit-unpushed) .proj-commit-subject {
color: var(--text-dim);
}
.proj-commit-more { .proj-commit-more {
font-size: 11px; font-family: var(--mono-font);
font-size: 11.5px;
color: var(--text-faint); color: var(--text-faint);
margin-top: 4px; padding: 9px 8px 0;
}
/* Must sit AFTER .proj-commit-row: a media query adds no specificity, so an
earlier block at the same specificity simply loses. Dropping the age column
while the row still declared four tracks pushed the subject into the 56px age
track and squeezed it to nothing. */
@media (max-width: 720px) {
.proj-commit-row {
grid-template-columns: 18px 66px 1fr;
}
/* On a phone the message is the only column worth the width. */
.proj-commit-time {
display: none;
}
} }
/* W3(c): "while you were away" reconnect banner (compact, dismissible top bar). */ /* W3(c): "while you were away" reconnect banner (compact, dismissible top bar). */
@@ -2236,14 +2354,30 @@ body {
color: var(--text-faint); color: var(--text-faint);
word-break: break-all; word-break: break-all;
} }
/* Section headings are wayfinding, not emphasis — wide tracking at low weight,
the way the design draws them. Bold caps competed with the content below. */
.proj-section-title { .proj-section-title {
margin: 22px 0 10px; margin: 22px 0 9px;
font-size: 12px; font-size: 11px;
font-weight: 700; font-weight: 400;
letter-spacing: 0.06em; letter-spacing: 0.15em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-faint); color: var(--text-faint);
} }
/* New Worktree form — had no container rule at all, so the input and its submit
button sat flush against each other and the button jumped to its own line the
moment a validation error appeared between them. Mirrors the fan-out form. */
.proj-wt-form {
display: flex;
flex-direction: column;
gap: 8px;
}
.proj-wt-branch-input {
width: 100%;
}
.proj-wt-submit {
align-self: flex-start;
}
/* W5 fan-out form — sibling of the New Worktree form. */ /* W5 fan-out form — sibling of the New Worktree form. */
.proj-fanout-form { .proj-fanout-form {
display: flex; display: flex;
@@ -2270,17 +2404,19 @@ body {
flex: 1 1 160px; flex: 1 1 160px;
min-width: 120px; min-width: 120px;
} }
.proj-fanout-error { /* --danger was never defined, so every validation message fell back to an
color: var(--danger, #e5534b); off-palette red. These three are the same message in three places and now say
so — previously the two worktree ones had no rule at all and rendered as 16px
near-white body text, indistinguishable from content. */
.proj-fanout-error,
.proj-wt-error,
.proj-wt-actions-error {
color: var(--warn);
font-size: 12px; font-size: 12px;
} }
.proj-fanout-submit { .proj-fanout-submit {
align-self: flex-start; align-self: flex-start;
} }
.proj-fanout-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── Projects panel controls ────────────────────────────────────────────────── /* ── Projects panel controls ──────────────────────────────────────────────────
These eight controls shipped with class names but no visual rules, so they These eight controls shipped with class names but no visual rules, so they
rendered as raw user-agent widgets: white boxes and grey buttons in a dark rendered as raw user-agent widgets: white boxes and grey buttons in a dark
@@ -2323,7 +2459,7 @@ body {
/* A branch name is an identifier, so it reads as one. */ /* A branch name is an identifier, so it reads as one. */
.proj-wt-branch-input, .proj-wt-branch-input,
.proj-fanout-branch { .proj-fanout-branch {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-family: var(--mono-font);
font-size: 12.5px; font-size: 12.5px;
} }
@@ -2332,19 +2468,36 @@ body {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
/* Without appearance:none WebKit keeps its own light control, so this was the one
widget in the panel that stayed a native grey dropdown. The arrow has to be
redrawn by hand once the native one is gone. */
.proj-fanout-mode { .proj-fanout-mode {
cursor: pointer; cursor: pointer;
appearance: none;
-webkit-appearance: none;
padding-right: 30px;
background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
background-position:
right 14px top 50%,
right 9px top 50%;
background-size:
5px 5px,
5px 5px;
background-repeat: no-repeat;
} }
/* Role 1 — filled accent, for the action that commits a form. Matches the Fetch
button exactly (padding 7/15, radius 7), which already implements the spec. */
.proj-wt-submit, .proj-wt-submit,
.proj-fanout-submit { .proj-fanout-submit {
font: inherit; font: inherit;
font-size: 12.5px; font-size: 12.5px;
font-weight: 560; font-weight: 560;
cursor: pointer; cursor: pointer;
padding: 8px 15px; padding: 7px 15px;
border: 0; border: 0;
border-radius: 8px; border-radius: 7px;
background: var(--accent); background: var(--accent);
color: var(--on-accent); color: var(--on-accent);
} }
@@ -2354,15 +2507,22 @@ body {
filter: brightness(1.08); filter: brightness(1.08);
} }
.proj-wt-submit:disabled,
.proj-fanout-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Role 2 — ghost, for anything that only reveals or toggles. */
.proj-diff-toggle { .proj-diff-toggle {
font: inherit; font: inherit;
font-size: 12.5px; font-size: 12.5px;
cursor: pointer; cursor: pointer;
padding: 7px 14px; padding: 7px 15px;
border-radius: 8px; border-radius: 7px;
border: 1px solid var(--border-strong); border: 1px solid var(--border-strong);
background: transparent; background: transparent;
color: var(--text); color: var(--text-dim);
} }
.proj-diff-toggle:hover { .proj-diff-toggle:hover {
@@ -2441,7 +2601,7 @@ body {
.proj-wt-list { .proj-wt-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 8px;
} }
.proj-wt-row { .proj-wt-row {
/* w6: two-line card — content column, then the actions rail pinned right. */ /* w6: two-line card — content column, then the actions rail pinned right. */
@@ -2449,37 +2609,49 @@ body {
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
padding: 11px 14px; padding: 12px 15px;
background: var(--surface-2); background: var(--surface-2);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: var(--radius);
} }
/* A branch name is an identifier, so it reads as one. Without these it inherited
the 16px user-agent default in the UI sans — the largest text on the page. */
.proj-wt-branch { .proj-wt-branch {
font-family: var(--mono-font);
font-size: 13.5px;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
} }
/* One chip language across the panel: recessed fill, hairline border, mono. */
.proj-wt-tag { .proj-wt-tag {
font-size: 10px; font-family: var(--mono-font);
font-weight: 700; font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.04em;
padding: 2px 7px; padding: 2px 8px;
border-radius: 999px; border-radius: 6px;
background: var(--surface-3); background: var(--sunk);
color: var(--text-dim); border: 1px solid var(--border);
color: var(--text-faint);
} }
/* "Current" is orientation, not an alarm — muted, the same as every other tag.
The row's amber wash is what marks where you are. */
.proj-wt-tag-current { .proj-wt-tag-current {
background: var(--accent); background: var(--accent-soft);
color: var(--on-accent); border-color: rgba(227, 166, 74, 0.36);
color: var(--accent);
} }
.proj-wt-path { .proj-wt-path {
/* No margin-left:auto — that was for the old single-line row, where it pushed /* No margin-left:auto — that was for the old single-line row, where it pushed
the path to the far right. Inside the two-line card it sits directly under the path to the far right. Inside the two-line card it sits directly under
the branch it belongs to, so auto margin would strand it across the card. */ the branch it belongs to, so auto margin would strand it across the card.
font-family: Menlo, Consolas, monospace; One line with an ellipsis: a wrapped absolute path re-flows the whole list
font-size: 11px; and is no more readable for it. */
font-family: var(--mono-font);
font-size: 11.5px;
color: var(--text-faint); color: var(--text-faint);
word-break: break-all; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
/* Detailed session rows */ /* Detailed session rows */
.proj-detail-sessions { .proj-detail-sessions {

View File

@@ -752,7 +752,34 @@ describe('makeSyncBand (w6 G3)', () => {
const ahead = b.querySelector('.proj-sync-ahead')! const ahead = b.querySelector('.proj-sync-ahead')!
expect(ahead.textContent).toContain('9') expect(ahead.textContent).toContain('9')
expect(ahead.classList.contains('proj-sync-stale')).toBe(false) expect(ahead.classList.contains('proj-sync-stale')).toBe(false)
expect(b.querySelector('.proj-sync-behind')!.classList.contains('proj-sync-stale')).toBe(true) })
it('carries staleness on the chip, never by recolouring the count', () => {
const b = band({ upstream: 'origin/main', ahead: 9, behind: 2, lastFetchMs: STALE })!
// Being behind is information, not an error. Painting the largest glyph in
// the band warn-red made every un-fetched repo look broken.
for (const big of b.querySelectorAll('.proj-sync-big')) {
expect(big.classList.contains('proj-sync-stale')).toBe(false)
}
expect(b.querySelector('.proj-sync-chip.proj-sync-stale')!.textContent).toBe('unverified')
})
it('does not paint a zero in the accent — accent means work is waiting', () => {
const b = band({ upstream: 'origin/main', ahead: 0, behind: 0, lastFetchMs: FRESH })!
expect(b.querySelector('.proj-sync-ahead')).toBeNull()
expect(b.querySelector('.proj-sync-behind')).toBeNull()
expect(b.querySelectorAll('.proj-sync-zero')).toHaveLength(2)
})
it('refuses green when a count is unknown rather than treating it as zero', () => {
// `undefined` is not 0 — it means the count could not be computed. Folding
// the two together let a repo whose counts failed to resolve claim it was
// in sync, which is the one thing the design forbids.
const b = band({ upstream: 'origin/main', ahead: undefined, behind: undefined, lastFetchMs: FRESH })!
expect(b.querySelector('.proj-sync-ok')).toBeNull()
expect(b.querySelector('.proj-sync-chip.proj-sync-stale')).not.toBeNull()
expect(b.textContent).toContain('↑ —')
expect(b.textContent).toContain('↓ —')
}) })
it('says "no upstream" instead of inventing counts, and is not green', () => { it('says "no upstream" instead of inventing counts, and is not green', () => {
@@ -812,7 +839,9 @@ describe('makeSyncBand (w6 G3)', () => {
it('renders the counts at display size', () => { it('renders the counts at display size', () => {
const b = band({ upstream: 'origin/develop', ahead: 3, behind: 0, lastFetchMs: FRESH })! const b = band({ upstream: 'origin/develop', ahead: 3, behind: 0, lastFetchMs: FRESH })!
expect(b.querySelector('.proj-sync-ahead')!.classList.contains('proj-sync-big')).toBe(true) expect(b.querySelector('.proj-sync-ahead')!.classList.contains('proj-sync-big')).toBe(true)
expect(b.querySelector('.proj-sync-behind')!.classList.contains('proj-sync-big')).toBe(true) // behind is 0, so it takes the neutral zero role — still at display size.
expect(b.querySelector('.proj-sync-zero')!.classList.contains('proj-sync-big')).toBe(true)
expect(b.querySelectorAll('.proj-sync-big')).toHaveLength(2)
}) })
}) })