fix(projects): close the design gaps in the git panel
An audit of the project-detail panel against docs/mockups/project-detail-git.html turned up 59 differences. The ones that changed what you see: Controls that had class names but no rules, so they rendered as raw OS widgets: - .proj-wt-form had no layout at all — the branch input and Create Worktree button sat flush together, and the button jumped to its own line the moment a validation error appeared between them. - .proj-wt-error / .proj-wt-actions-error had no rule, so a validation failure rendered as 16px near-white body text, indistinguishable from content. - .proj-wt-remove (the worktree ✕) and .proj-wt-prune had no rule either. - .proj-fanout-mode never reset `appearance`, so WebKit kept its own light dropdown — the one control in the panel that stayed a native grey widget. - --danger was never defined, so every validation message fell back to an off-palette red. Cascade bugs — a later rule at equal specificity was silently winning: - .proj-wt-row-current's amber wash and border were completely dead. "You are here" rendered identically to every other row. - .proj-wt-branch inherited the 16px user-agent default in the UI sans, making a branch name the largest text on the page. Now 13.5px mono. Layout: - .proj-syncband dropped flex-wrap. Wrapping produced ragged half-empty rows between 721px and 1000px — at 900px the Fetch button dropped alone onto a second row. One cell now absorbs the shrink so the band stays a single row down to the 720px column break. - Commit rows are a 4-column grid, not flex, and the ↑ mark cell is rendered on every row instead of only unpushed ones. Previously no two lines agreed on where the sha started, so the unpushed group did not read as a group. - Pushed commit subjects step down to --text-dim, which is what makes the upstream boundary mean anything. Honesty of the sync band — the design's two hard rules: - Green "✓ in sync" was reachable when ahead/behind were UNKNOWN, because `?? 0` folded undefined into zero. Unknown counts now render "↑ —" / "↓ —" and are marked unverified. Green requires real zeros and a fresh fetch. - A stale ↓ painted the digit warning-red. Since a missing FETCH_HEAD is the default state, the largest glyph in the band was almost always red and the panel read as "this repo is broken". The doubt now sits on the `unverified` chip and the footnote, never on the number. Also: semantic tokens (--sunk/--warn/--ok/--dirty/--mono-font) replacing hex literals, one chip language across the panel, section headings as tracked caps with the count as a separate lighter element, and a ≤720px rule that drops the age column — placed after the base rule, since a media query adds no specificity. Verified in a real browser at 1280/1000/900/780/721/600/420px: the sync band holds one row down to 721px and stacks below it, commit columns align on every row, and nothing overflows or scrolls the page horizontally.
This commit is contained in:
@@ -87,12 +87,16 @@ function relTime(ms: number): string {
|
||||
* individual rows. */
|
||||
function renderCommitRow(c: CommitLogEntry): HTMLElement {
|
||||
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) {
|
||||
row.classList.add('proj-commit-unpushed')
|
||||
const mark = el('span', 'proj-commit-mark', '↑')
|
||||
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-time', `${relTime(c.at)} ago`))
|
||||
row.append(el('span', 'proj-commit-subject', c.subject)) // attacker-influenced → textContent
|
||||
|
||||
Binary file not shown.
330
public/style.css
330
public/style.css
@@ -2,6 +2,7 @@
|
||||
|
||||
:root {
|
||||
/* 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 */
|
||||
--surface-1: #181613; /* tab bar, key bar */
|
||||
--surface-2: #1f1c17; /* tabs, buttons, cards */
|
||||
@@ -18,6 +19,16 @@
|
||||
--green: #46d07f; /* connection-status: connected */
|
||||
--amber: #f5b14c; /* connection-status: connecting */
|
||||
--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);
|
||||
--radius: 9px;
|
||||
--radius-lg: 14px;
|
||||
@@ -1568,23 +1579,33 @@ body {
|
||||
.proj-syncband {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin: 10px 0 4px;
|
||||
background: var(--surface-1);
|
||||
margin: 16px 0 4px;
|
||||
background: var(--sunk);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
border-radius: var(--radius);
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 11px 18px;
|
||||
padding: 12px 18px;
|
||||
min-width: 0;
|
||||
flex: none;
|
||||
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 {
|
||||
border-right: 0;
|
||||
}
|
||||
@@ -1623,7 +1644,7 @@ body {
|
||||
}
|
||||
|
||||
.proj-sync-sub-bad {
|
||||
color: #cf6b4f;
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.proj-sync-upstream {
|
||||
@@ -1631,26 +1652,40 @@ body {
|
||||
}
|
||||
|
||||
.proj-sync-chip {
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11.5px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
border-radius: 5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
padding: 3px 9px;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
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 {
|
||||
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 {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.proj-sync-zero {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* The one reassuring state. Reachable only with an upstream AND a fresh fetch. */
|
||||
.proj-sync-ok {
|
||||
color: #7d9b76;
|
||||
color: var(--ok);
|
||||
background: rgba(125, 155, 118, 0.13);
|
||||
border-color: rgba(125, 155, 118, 0.3);
|
||||
}
|
||||
@@ -1659,7 +1694,7 @@ body {
|
||||
.proj-sync-stale,
|
||||
.proj-sync-noupstream,
|
||||
.proj-sync-detached {
|
||||
color: #cf6b4f;
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.proj-sync-chip.proj-sync-stale,
|
||||
@@ -1670,11 +1705,12 @@ body {
|
||||
}
|
||||
|
||||
.proj-sync-dirty {
|
||||
color: var(--amber);
|
||||
font-family: var(--mono-font);
|
||||
color: var(--dirty);
|
||||
background: rgba(224, 151, 90, 0.13);
|
||||
border: 1px solid rgba(224, 151, 90, 0.3);
|
||||
border-radius: 5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
padding: 3px 9px;
|
||||
font-size: 11.5px;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
@@ -1721,14 +1757,17 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── w6: unpushed count beside the commit-list heading ───────────────────── */
|
||||
.proj-commitlog-count {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
/* ── w6: counts sitting on a section heading ─────────────────────────────────
|
||||
The heading is tracked-out uppercase; a count is data and opts out of both,
|
||||
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-variant-numeric: tabular-nums;
|
||||
color: var(--accent);
|
||||
color: var(--text-dim);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
letter-spacing: 0.04em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1736,9 +1775,14 @@ body {
|
||||
/* 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
|
||||
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);
|
||||
background: linear-gradient(90deg, var(--accent-soft), transparent 46%);
|
||||
background: linear-gradient(90deg, var(--accent-soft), var(--surface-2) 46%);
|
||||
}
|
||||
|
||||
.proj-wt-main {
|
||||
@@ -1762,7 +1806,13 @@ body {
|
||||
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-size: 11.5px;
|
||||
cursor: pointer;
|
||||
@@ -1770,24 +1820,48 @@ body {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-strong);
|
||||
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);
|
||||
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-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 ───────────────────────── */
|
||||
.proj-commit-unpushed {
|
||||
position: relative;
|
||||
background: linear-gradient(90deg, var(--accent-soft), transparent 55%);
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.proj-commit-unpushed::before {
|
||||
@@ -1801,11 +1875,12 @@ body {
|
||||
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 {
|
||||
font-family: var(--mono-font);
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
flex: none;
|
||||
width: 12px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1813,16 +1888,20 @@ body {
|
||||
.proj-commit-boundary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 7px 0 5px;
|
||||
gap: 12px;
|
||||
margin: 9px 0 7px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.proj-commit-boundary-label {
|
||||
font-size: 10.5px;
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-dim);
|
||||
background: var(--sunk);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
padding: 3px 11px;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
@@ -1833,18 +1912,30 @@ body {
|
||||
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 {
|
||||
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;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 5px;
|
||||
padding: 2px 7px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
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. */
|
||||
.tg-cost-warn {
|
||||
color: var(--red);
|
||||
@@ -1862,36 +1953,63 @@ body {
|
||||
.proj-commitlog-list {
|
||||
display: flex;
|
||||
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 {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 22px 74px 56px 1fr;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.proj-commit-hash {
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
color: var(--accent);
|
||||
flex: none;
|
||||
font-family: var(--mono-font);
|
||||
color: var(--accent-2);
|
||||
}
|
||||
.proj-commit-time {
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11.5px;
|
||||
color: var(--text-faint);
|
||||
flex: none;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.proj-commit-subject {
|
||||
font-size: 13.5px;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
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 {
|
||||
font-size: 11px;
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11.5px;
|
||||
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). */
|
||||
@@ -2236,14 +2354,30 @@ body {
|
||||
color: var(--text-faint);
|
||||
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 {
|
||||
margin: 22px 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 22px 0 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
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. */
|
||||
.proj-fanout-form {
|
||||
display: flex;
|
||||
@@ -2270,17 +2404,19 @@ body {
|
||||
flex: 1 1 160px;
|
||||
min-width: 120px;
|
||||
}
|
||||
.proj-fanout-error {
|
||||
color: var(--danger, #e5534b);
|
||||
/* --danger was never defined, so every validation message fell back to an
|
||||
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;
|
||||
}
|
||||
.proj-fanout-submit {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.proj-fanout-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* ── Projects panel controls ──────────────────────────────────────────────────
|
||||
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
|
||||
@@ -2323,7 +2459,7 @@ body {
|
||||
/* A branch name is an identifier, so it reads as one. */
|
||||
.proj-wt-branch-input,
|
||||
.proj-fanout-branch {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-family: var(--mono-font);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
@@ -2332,19 +2468,36 @@ body {
|
||||
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 {
|
||||
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-fanout-submit {
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 560;
|
||||
cursor: pointer;
|
||||
padding: 8px 15px;
|
||||
padding: 7px 15px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: 7px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
@@ -2354,15 +2507,22 @@ body {
|
||||
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 {
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
padding: 7px 14px;
|
||||
border-radius: 8px;
|
||||
padding: 7px 15px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.proj-diff-toggle:hover {
|
||||
@@ -2441,7 +2601,7 @@ body {
|
||||
.proj-wt-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
.proj-wt-row {
|
||||
/* w6: two-line card — content column, then the actions rail pinned right. */
|
||||
@@ -2449,37 +2609,49 @@ body {
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 11px 14px;
|
||||
padding: 12px 15px;
|
||||
background: var(--surface-2);
|
||||
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 {
|
||||
font-family: var(--mono-font);
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
/* One chip language across the panel: recessed fill, hairline border, mono. */
|
||||
.proj-wt-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-3);
|
||||
color: var(--text-dim);
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--sunk);
|
||||
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 {
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
background: var(--accent-soft);
|
||||
border-color: rgba(227, 166, 74, 0.36);
|
||||
color: var(--accent);
|
||||
}
|
||||
.proj-wt-path {
|
||||
/* 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 branch it belongs to, so auto margin would strand it across the card. */
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
the branch it belongs to, so auto margin would strand it across the card.
|
||||
One line with an ellipsis: a wrapped absolute path re-flows the whole list
|
||||
and is no more readable for it. */
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11.5px;
|
||||
color: var(--text-faint);
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Detailed session rows */
|
||||
.proj-detail-sessions {
|
||||
|
||||
@@ -752,7 +752,34 @@ describe('makeSyncBand (w6 G3)', () => {
|
||||
const ahead = b.querySelector('.proj-sync-ahead')!
|
||||
expect(ahead.textContent).toContain('9')
|
||||
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', () => {
|
||||
@@ -812,7 +839,9 @@ describe('makeSyncBand (w6 G3)', () => {
|
||||
it('renders the counts at display size', () => {
|
||||
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-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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user