The project-detail Git panel's diff viewer emitted 52 class names that style.css had no rule for, so it rendered as browser defaults: Working / Staged / the base picker / ✕ Close as native OS buttons on a dark warm panel, and a diff body with no monospace font and no +/- colouring at all. The user's screenshot showed the toolbar; the audit (literal AND template-literal class names) found the rest. Adds the B1 diff-viewer block and the proj-*/tl-* rules, all on :root tokens, following the file's existing two-button-role doctrine. Also fixes, in passing: - .df-commit-btn hardcoded #1a1712 where THEME.md mandates var(--on-accent) for text on a gold background - .proj-pr-hidden — a "hidden" state class with no rule, so it never hid - .proj-dot-stuck — ClaudeStatus has 'stuck' but only four of the five dots were styled, so a stuck session was the same colour as "no signal" - --warn-wash: a derived token for the deletion wash, documented in THEME.md, so a re-skin moves the wash with --warn Notable decisions, recorded in the CSS comments: - .proj-tl-container gets NO height cap. Every capping mechanism tried (overflow-y:auto, mask-image, an ::after fade, a :has() row gate) cost more than the height it saved — the 5s wipe-and-re-append render loop cannot host a scroll position, and mask-clip:border-box ate the well's own border. It grew freely before this CSS existed; maxEvents=50 bounds it. - The horizontal scroller stays per-hunk on .df-lines. Hoisting it to .df-file would drag the file path out of view when you scroll right. - .df-file > .df-file-header keeps a smaller unscoped padding: the terminal page's .approval-preview also calls renderDiffFile(). Verified against the real DOM in a browser at 900px and 390x844 via a throwaway harness (removed before commit), not from the CSS text alone. typecheck exit 0; 2216 unit + 27 e2e tests pass.
4075 lines
112 KiB
CSS
4075 lines
112 KiB
CSS
/* Web Terminal — modern dark UI */
|
||
|
||
: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 */
|
||
--surface-3: #2a2620; /* hover / active */
|
||
--border: rgba(255, 255, 255, 0.07);
|
||
--border-strong: rgba(255, 255, 255, 0.14);
|
||
--text: #ece9e3;
|
||
--text-dim: #a8a299;
|
||
--text-faint: #6f6a61;
|
||
--accent: #e3a64a; /* amber gold */
|
||
--accent-2: #c9892f; /* deeper gold (gradients / hover) */
|
||
--accent-soft: rgba(227, 166, 74, 0.15);
|
||
--on-accent: #1a1305; /* text/icon color on a gold accent background (contrast) */
|
||
--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 */
|
||
/* Diff deletion wash. DERIVED from --warn, never a hand-copy of its channels: as a
|
||
* literal it froze when --warn was re-skinned, leaving the wash one hue and the sign
|
||
* bar and glyph beside it (both var(--warn)) another — a two-hue deletion row inside
|
||
* one rule. No rgba fallback either: a fallback pair is that same drift by another
|
||
* name, and this app already requires an engine with color-mix(). */
|
||
--warn-wash: color-mix(in srgb, var(--warn) 24%, transparent);
|
||
--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;
|
||
|
||
--tabbar-h: 40px;
|
||
--keybar-h: 46px;
|
||
|
||
/* iOS notch / home-indicator clearance. index.html sets viewport-fit=cover,
|
||
* so the layout spans the physical screen edges; without these the top bar
|
||
* hides under the status bar and the bottom key-bar covers terminal output.
|
||
* Falls back to 0px on platforms without safe areas. */
|
||
--safe-t: env(safe-area-inset-top, 0px);
|
||
--safe-b: env(safe-area-inset-bottom, 0px);
|
||
|
||
--ui-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||
}
|
||
|
||
@media (pointer: coarse) {
|
||
:root {
|
||
--tabbar-h: 48px;
|
||
--keybar-h: 60px;
|
||
}
|
||
}
|
||
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: var(--bg);
|
||
color: var(--text);
|
||
font-family: var(--ui-font);
|
||
overflow: hidden;
|
||
}
|
||
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--surface-3) transparent;
|
||
}
|
||
|
||
/* ── Tab bar ─────────────────────────────────────────────────────── */
|
||
#tabbar {
|
||
position: fixed;
|
||
inset: 0 0 auto 0;
|
||
box-sizing: border-box;
|
||
height: calc(var(--tabbar-h) + var(--safe-t));
|
||
padding-top: var(--safe-t);
|
||
background: linear-gradient(180deg, #181a22, var(--surface-1));
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: stretch;
|
||
z-index: 1001;
|
||
}
|
||
|
||
#tabs {
|
||
flex: 1 1 auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 0 4px;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
#tabs::-webkit-scrollbar {
|
||
height: 0;
|
||
}
|
||
|
||
/* tabs as floating rounded chips */
|
||
.tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
height: calc(var(--tabbar-h) - 12px);
|
||
padding: 0 4px 0 10px;
|
||
border-radius: 8px;
|
||
color: var(--text-dim);
|
||
background: transparent;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
font-size: 13px;
|
||
user-select: none;
|
||
max-width: 220px;
|
||
transition:
|
||
background 0.12s ease,
|
||
color 0.12s ease;
|
||
}
|
||
.tab:hover {
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
}
|
||
.tab.active {
|
||
background: var(--surface-3);
|
||
color: #fff;
|
||
box-shadow: inset 0 0 0 1px var(--border-strong);
|
||
}
|
||
|
||
.tab-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
flex: none;
|
||
background: var(--text-faint);
|
||
}
|
||
.dot-connected {
|
||
background: var(--green);
|
||
box-shadow: 0 0 6px rgba(70, 208, 127, 0.5);
|
||
}
|
||
.dot-connecting,
|
||
.dot-reconnecting {
|
||
background: var(--amber);
|
||
}
|
||
.dot-exited {
|
||
background: var(--red);
|
||
}
|
||
|
||
.tab-label {
|
||
padding-right: 4px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-rename {
|
||
width: 120px;
|
||
margin: 0 4px;
|
||
padding: 3px 6px;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
color: #fff;
|
||
background: var(--bg);
|
||
border: 1px solid var(--accent);
|
||
border-radius: 6px;
|
||
outline: none;
|
||
}
|
||
|
||
/* unread / Claude-waiting accents */
|
||
.tab.unread .tab-dot {
|
||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||
}
|
||
.tab.unread .tab-label {
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
.tab-claude {
|
||
margin-right: 4px;
|
||
font-size: 12px;
|
||
flex: none;
|
||
}
|
||
.tab.claude-waiting {
|
||
background: rgba(245, 177, 76, 0.14);
|
||
box-shadow: inset 0 0 0 1px rgba(245, 177, 76, 0.5);
|
||
}
|
||
|
||
.tab.dragging {
|
||
opacity: 0.4;
|
||
}
|
||
.tab.drag-over {
|
||
box-shadow: inset 3px 0 0 var(--accent);
|
||
}
|
||
|
||
.tab-close {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
padding: 3px 6px;
|
||
border-radius: 6px;
|
||
flex: none;
|
||
}
|
||
.tab-close:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: #fff;
|
||
}
|
||
@media (pointer: fine) {
|
||
.tab:not(.active):not(:hover) .tab-close {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.tab-add {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
padding: 0 10px;
|
||
flex: none;
|
||
border-radius: 7px;
|
||
}
|
||
.tab-add:hover {
|
||
color: #fff;
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
/* v0.6: Home (⌂) button — opens the Sessions/Projects chooser over open tabs. */
|
||
.tab-home {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
font-size: 17px;
|
||
padding: 0 10px;
|
||
flex: none;
|
||
border-radius: 7px;
|
||
}
|
||
.tab-home:hover {
|
||
color: #fff;
|
||
background: var(--surface-2);
|
||
}
|
||
.tab-home.active {
|
||
color: var(--on-accent, #1a1408);
|
||
background: var(--accent);
|
||
}
|
||
|
||
/* toolbar */
|
||
#toolbar {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 0 6px;
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
.toolbtn {
|
||
position: relative; /* for the hover tooltip */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
padding: 7px 9px;
|
||
border-radius: 8px;
|
||
line-height: 1;
|
||
transition: background 0.12s ease, color 0.12s ease;
|
||
}
|
||
.toolbtn svg {
|
||
display: block;
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
.toolbtn:hover {
|
||
color: var(--accent);
|
||
background: var(--surface-2);
|
||
}
|
||
/* Hover tooltip (right-anchored so it never clips the screen edge). */
|
||
.toolbtn[data-tip]::after {
|
||
content: attr(data-tip);
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
right: 0;
|
||
white-space: nowrap;
|
||
background: var(--surface-3);
|
||
color: var(--text);
|
||
border: 1px solid var(--border-strong);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.12s ease;
|
||
z-index: 60;
|
||
}
|
||
.toolbtn[data-tip]:hover::after {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* ── Terminal area ───────────────────────────────────────────────── */
|
||
#term {
|
||
position: absolute;
|
||
inset: calc(var(--tabbar-h) + var(--safe-t)) 0 calc(var(--keybar-h) + var(--safe-b)) 0;
|
||
background: var(--bg);
|
||
overflow: hidden;
|
||
}
|
||
/* Each pane lives in a .term-cell wrapper (header + terminal + optional inline
|
||
* approve). In single mode the cell fills #term absolutely (only the focused one
|
||
* is shown) so the classic one-pane look is unchanged; split layouts turn #term
|
||
* into a CSS grid and the cells flow into tracks. */
|
||
.term-cell {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
.term-pane {
|
||
position: relative;
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
padding: 6px 8px 0;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Cell header + inline-approve footer are hidden in single mode. */
|
||
.cell-head,
|
||
.cell-approve {
|
||
display: none;
|
||
}
|
||
|
||
/* ── Split-grid layouts (desktop watch board) ────────────────────────── */
|
||
/* Shared cell chrome keys off the single `grid` marker so it is independent of
|
||
* how many concrete layouts exist; each `lay-*` only sets the grid template. */
|
||
#term.grid {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding: 6px;
|
||
}
|
||
#term.lay-split-2 {
|
||
grid-template-columns: 1fr 1fr;
|
||
grid-template-rows: 1fr;
|
||
}
|
||
#term.lay-row-3 {
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
grid-template-rows: 1fr;
|
||
}
|
||
#term.lay-grid-4 {
|
||
grid-template-columns: 1fr 1fr;
|
||
grid-template-rows: 1fr 1fr;
|
||
}
|
||
#term.lay-grid-6 {
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
grid-template-rows: 1fr 1fr;
|
||
}
|
||
#term.grid .term-cell {
|
||
position: relative;
|
||
inset: auto;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
}
|
||
/* Keep the last terminal row clear of the cell border/focus ring in a grid.
|
||
* Root cause (measured): the base .term-pane is box-sizing:border-box, so
|
||
* getComputedStyle(pane).height — which xterm's FitAddon reads to compute rows —
|
||
* returns the PADDING-box height and double-counts the pane's own padding, packing
|
||
* one extra row that overruns into the border (overflow +6px). content-box makes
|
||
* that height report the CONTENT box, so FitAddon drops the phantom row and the 6px
|
||
* padding becomes real clearance. (Single mode keeps padding 0 so the terminal
|
||
* still fills to the key-bar.) */
|
||
#term.grid .term-pane {
|
||
box-sizing: content-box;
|
||
padding-bottom: 6px;
|
||
}
|
||
/* Maximized quadrant fills the whole grid as an ABSOLUTE overlay (taken out of
|
||
* grid flow so the siblings keep their placement — spanning grid tracks instead
|
||
* would shove auto-placed siblings into implicit rows, resizing their live PTYs).
|
||
* #term is a positioned containing block, so inset:0 covers the whole area. */
|
||
#term.grid .term-cell.maximized {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 4;
|
||
}
|
||
/* A tab dragged from the tab bar is hovering this quadrant. */
|
||
#term.grid .term-cell.drag-target {
|
||
outline: 2px dashed var(--accent);
|
||
outline-offset: -3px;
|
||
}
|
||
|
||
/* Draggable splitter handles overlaying the track boundaries (v3). */
|
||
.grid-gutter {
|
||
position: absolute;
|
||
z-index: 3;
|
||
touch-action: none;
|
||
}
|
||
.grid-gutter::after {
|
||
content: '';
|
||
position: absolute;
|
||
background: var(--border-strong);
|
||
border-radius: 2px;
|
||
opacity: 0;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.grid-gutter:hover::after,
|
||
.grid-gutter:active::after {
|
||
opacity: 1;
|
||
background: var(--accent);
|
||
}
|
||
.grid-gutter-col {
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 12px;
|
||
transform: translateX(-50%);
|
||
cursor: col-resize;
|
||
}
|
||
.grid-gutter-col::after {
|
||
top: 15%;
|
||
bottom: 15%;
|
||
left: 50%;
|
||
width: 3px;
|
||
transform: translateX(-50%);
|
||
}
|
||
.grid-gutter-row {
|
||
left: 0;
|
||
right: 0;
|
||
height: 12px;
|
||
transform: translateY(-50%);
|
||
cursor: row-resize;
|
||
}
|
||
.grid-gutter-row::after {
|
||
left: 15%;
|
||
right: 15%;
|
||
top: 50%;
|
||
height: 3px;
|
||
transform: translateY(-50%);
|
||
}
|
||
#term.grid .cell-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 5px 9px;
|
||
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.cell-max {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
border-radius: 5px;
|
||
}
|
||
.cell-max:hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
}
|
||
.cell-keep {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
border-radius: 5px;
|
||
}
|
||
.cell-keep:hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
}
|
||
.cell-monitor-btn {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
border-radius: 5px;
|
||
opacity: 0.7;
|
||
}
|
||
.cell-monitor-btn:hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
opacity: 1;
|
||
}
|
||
.cell-monitor-btn.active {
|
||
color: var(--accent);
|
||
opacity: 1;
|
||
}
|
||
/* Read-only monitor preview fills the cell below the header. */
|
||
.cell-monitor {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
position: relative;
|
||
padding: 6px 8px 0;
|
||
box-sizing: border-box;
|
||
}
|
||
.cell-name {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.cell-status {
|
||
margin-left: auto;
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
white-space: nowrap;
|
||
}
|
||
.cell-status.cs-working {
|
||
color: var(--amber);
|
||
}
|
||
.cell-status.cs-waiting {
|
||
color: var(--accent);
|
||
}
|
||
.cell-status.cs-idle {
|
||
color: var(--green);
|
||
}
|
||
.cell-status.cs-stuck {
|
||
color: var(--red);
|
||
}
|
||
|
||
/* Focus ring — the quadrant that owns keyboard / keybar / voice / approvals. */
|
||
#term.grid .term-cell.focused {
|
||
border-color: var(--accent);
|
||
box-shadow:
|
||
0 0 0 1px var(--accent),
|
||
var(--shadow);
|
||
}
|
||
/* Pending glow — a non-focused quadrant is waiting for approval. */
|
||
#term.grid .term-cell.pending:not(.focused) {
|
||
border-color: var(--amber);
|
||
animation: cell-pending 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes cell-pending {
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 0 0 1px rgba(245, 177, 76, 0.35);
|
||
}
|
||
50% {
|
||
box-shadow:
|
||
0 0 0 1px rgba(245, 177, 76, 0.7),
|
||
0 0 26px -8px rgba(245, 177, 76, 0.6);
|
||
}
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
#term.grid .term-cell.pending {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* Inline per-quadrant approve footer. */
|
||
#term.grid .cell-approve {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 9px;
|
||
background: rgba(245, 177, 76, 0.08);
|
||
border-top: 1px solid rgba(245, 177, 76, 0.35);
|
||
font-size: 11.5px;
|
||
}
|
||
.cell-approve-label {
|
||
color: var(--amber);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.cell-approve-btns {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.cell-approve-yes,
|
||
.cell-approve-no {
|
||
border: 0;
|
||
border-radius: 6px;
|
||
padding: 3px 11px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.cell-approve-yes {
|
||
background: var(--green);
|
||
color: #06231a;
|
||
}
|
||
.cell-approve-no {
|
||
background: var(--surface-3);
|
||
color: var(--text-dim);
|
||
}
|
||
.cell-approve-yes:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
.cell-approve-no:hover {
|
||
color: var(--text);
|
||
}
|
||
|
||
/* Empty-slot placeholder (fewer sessions than the layout holds). */
|
||
.term-cell.slot-empty {
|
||
border: 1px dashed var(--border-strong);
|
||
background: repeating-linear-gradient(
|
||
-45deg,
|
||
transparent,
|
||
transparent 10px,
|
||
rgba(255, 255, 255, 0.02) 10px,
|
||
rgba(255, 255, 255, 0.02) 20px
|
||
);
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
.term-cell.slot-empty:hover {
|
||
border-color: var(--accent);
|
||
}
|
||
.slot-new {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: none;
|
||
border: 0;
|
||
color: var(--text-faint);
|
||
font-size: 12px;
|
||
font-family: var(--ui-font);
|
||
cursor: pointer;
|
||
}
|
||
.slot-new b {
|
||
font-size: 26px;
|
||
font-weight: 300;
|
||
line-height: 1;
|
||
color: var(--text-dim);
|
||
}
|
||
.term-cell.slot-empty:hover .slot-new {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* ── Grid-toggle segmented control (toolbar) ─────────────────────────── */
|
||
.grid-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
margin-left: 4px;
|
||
padding: 2px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
}
|
||
.grid-toggle-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 26px;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
}
|
||
.grid-toggle-btn:hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
}
|
||
.grid-toggle-btn[aria-pressed='true'] {
|
||
color: var(--on-accent);
|
||
background: var(--accent);
|
||
}
|
||
.grid-glyph {
|
||
display: grid;
|
||
gap: 1.5px;
|
||
width: 14px;
|
||
height: 12px;
|
||
}
|
||
.grid-glyph.gl-single {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.grid-glyph.gl-split-2 {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
.grid-glyph.gl-row-3 {
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
}
|
||
.grid-glyph.gl-grid-4 {
|
||
grid-template-columns: 1fr 1fr;
|
||
grid-template-rows: 1fr 1fr;
|
||
}
|
||
.grid-glyph.gl-grid-6 {
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
grid-template-rows: 1fr 1fr;
|
||
}
|
||
.grid-glyph i {
|
||
background: currentColor;
|
||
border-radius: 1px;
|
||
}
|
||
/* Touch tablets clear the 1024px width gate but need bigger tap targets — mirror
|
||
* the project's existing coarse-pointer bump for the tab/key bars. */
|
||
@media (pointer: coarse) {
|
||
.grid-toggle {
|
||
gap: 3px;
|
||
}
|
||
.grid-toggle-btn {
|
||
width: 36px;
|
||
height: 34px;
|
||
}
|
||
.cell-max,
|
||
.cell-monitor-btn {
|
||
padding: 6px 9px;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
/* ── Key bar (rounded chips) ─────────────────────────────────────── */
|
||
#keybar {
|
||
position: fixed;
|
||
inset: auto 0 0 0;
|
||
box-sizing: border-box;
|
||
height: calc(var(--keybar-h) + var(--safe-b));
|
||
background: var(--surface-1);
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 0 6px var(--safe-b);
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
z-index: 1000;
|
||
}
|
||
#keybar::-webkit-scrollbar {
|
||
height: 0;
|
||
}
|
||
#keybar button {
|
||
flex: 1 0 auto;
|
||
min-width: 50px;
|
||
height: calc(var(--keybar-h) - 12px);
|
||
padding: 0 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1px;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
font-family: var(--ui-font);
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
transition:
|
||
background 0.1s ease,
|
||
transform 0.05s ease;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-touch-callout: none;
|
||
}
|
||
#keybar .kb-key {
|
||
font-size: 13px;
|
||
line-height: 1.1;
|
||
}
|
||
#keybar .kb-cap {
|
||
font-size: 9px;
|
||
line-height: 1;
|
||
color: var(--text-faint);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
#keybar button:hover {
|
||
background: var(--surface-3);
|
||
}
|
||
#keybar button:active {
|
||
transform: translateY(1px);
|
||
background: var(--surface-3);
|
||
}
|
||
#keybar button.keybar-btn-primary {
|
||
background: var(--accent-soft);
|
||
color: #f0d49a;
|
||
border-color: rgba(227, 166, 74, 0.4);
|
||
font-weight: 600;
|
||
min-width: 54px;
|
||
}
|
||
#keybar button.keybar-btn-primary:hover {
|
||
background: rgba(227, 166, 74, 0.26);
|
||
}
|
||
#keybar button.keybar-btn-primary .kb-cap {
|
||
color: rgba(240, 212, 154, 0.7);
|
||
}
|
||
|
||
/* ── Shared: overlays, cards, inputs, buttons ────────────────────── */
|
||
.overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1200;
|
||
display: flex;
|
||
background: rgba(8, 9, 12, 0.6);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
#searchbox {
|
||
position: fixed;
|
||
top: calc(var(--tabbar-h) + var(--safe-t) + 8px);
|
||
right: 10px;
|
||
z-index: 1100;
|
||
display: flex;
|
||
gap: 5px;
|
||
padding: 7px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 10px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.search-input {
|
||
width: 190px;
|
||
padding: 5px 8px;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
color: #fff;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
outline: none;
|
||
}
|
||
.search-input:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
#searchbox button {
|
||
background: var(--surface-3);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
border-radius: 7px;
|
||
padding: 0 10px;
|
||
}
|
||
#searchbox button:hover {
|
||
background: var(--accent-soft);
|
||
}
|
||
|
||
/* QR + share modals */
|
||
#qrmodal,
|
||
#sharemodal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1200;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(8, 9, 12, 0.6);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
.qr-card {
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: var(--radius-lg);
|
||
padding: 22px;
|
||
text-align: center;
|
||
max-width: 90vw;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.qr-card canvas {
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
padding: 8px;
|
||
}
|
||
.qr-title {
|
||
font-weight: 600;
|
||
margin-bottom: 14px;
|
||
}
|
||
.qr-url {
|
||
margin-top: 12px;
|
||
font-size: 13px;
|
||
color: var(--accent);
|
||
word-break: break-all;
|
||
}
|
||
.qr-note {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
max-width: 240px;
|
||
}
|
||
.qr-close {
|
||
margin-top: 16px;
|
||
padding: 8px 20px;
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 8px;
|
||
background: var(--surface-3);
|
||
color: #fff;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
}
|
||
.qr-close:hover {
|
||
background: var(--accent-soft);
|
||
}
|
||
|
||
/* Settings panel */
|
||
#settingspanel {
|
||
position: fixed;
|
||
top: calc(var(--tabbar-h) + var(--safe-t) + 8px);
|
||
right: 10px;
|
||
z-index: 1100;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
min-width: 220px;
|
||
box-shadow: var(--shadow);
|
||
color: var(--text);
|
||
}
|
||
.settings-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin: 8px 0;
|
||
}
|
||
.settings-label {
|
||
width: 48px;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
}
|
||
.settings-opt {
|
||
background: var(--surface-3);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
border-radius: 7px;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
transition: background 0.1s ease;
|
||
}
|
||
.settings-opt:hover {
|
||
background: var(--surface-1);
|
||
}
|
||
.settings-opt.active {
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border-color: var(--accent);
|
||
}
|
||
.settings-size {
|
||
min-width: 22px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Dashboard + history + shortcuts share the centered-card look */
|
||
#dashboard,
|
||
#historymodal,
|
||
#shortcutsmodal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1200;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding-top: 60px;
|
||
background: rgba(8, 9, 12, 0.6);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
.dash-card,
|
||
.hist-card,
|
||
.sc-card {
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: var(--radius-lg);
|
||
min-width: 340px;
|
||
max-width: 92vw;
|
||
max-height: 72vh;
|
||
overflow-y: auto;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.sc-card {
|
||
width: 540px;
|
||
}
|
||
.dash-title,
|
||
.hist-title,
|
||
.sc-title {
|
||
padding: 14px 18px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
/* Shortcut cheat-sheet */
|
||
.sc-group {
|
||
padding: 12px 18px 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
}
|
||
.sc-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 6px 18px;
|
||
}
|
||
.sc-key {
|
||
flex: none;
|
||
min-width: 110px;
|
||
font-family: Menlo, Consolas, monospace;
|
||
font-size: 12px;
|
||
color: #fff;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 6px;
|
||
padding: 3px 8px;
|
||
text-align: center;
|
||
}
|
||
.sc-desc {
|
||
flex: 1 1 auto;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
}
|
||
.sc-tag {
|
||
flex: none;
|
||
font-size: 10px;
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
border-radius: 5px;
|
||
padding: 2px 6px;
|
||
}
|
||
.dash-row,
|
||
.hist-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 11px 18px;
|
||
cursor: pointer;
|
||
color: var(--text);
|
||
border-bottom: 1px solid var(--border);
|
||
transition: background 0.1s ease;
|
||
}
|
||
.dash-row:hover,
|
||
.hist-row:hover {
|
||
background: var(--surface-3);
|
||
}
|
||
.dash-row.active {
|
||
box-shadow: inset 3px 0 0 var(--accent);
|
||
}
|
||
.dash-name {
|
||
flex: 1;
|
||
}
|
||
.dash-claude {
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
}
|
||
.dash-claude.pending {
|
||
color: var(--amber);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.hist-empty {
|
||
padding: 18px;
|
||
color: var(--text-dim);
|
||
}
|
||
.hist-row {
|
||
cursor: default;
|
||
}
|
||
.hist-main {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
.hist-proj {
|
||
color: #fff;
|
||
font-size: 13px;
|
||
}
|
||
.hist-prev {
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.hist-resume {
|
||
flex: none;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 7px 15px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-weight: 500;
|
||
transition: background 0.1s ease;
|
||
}
|
||
.hist-resume:hover {
|
||
background: var(--accent-2);
|
||
}
|
||
|
||
/* Approval banner (H3) */
|
||
#approvalbar {
|
||
position: fixed;
|
||
inset: auto 0 calc(var(--keybar-h) + var(--safe-b)) 0;
|
||
z-index: 1050;
|
||
display: flex;
|
||
flex-wrap: wrap; /* W1: a preview row wraps below the label + buttons */
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 14px;
|
||
background: linear-gradient(180deg, rgba(245, 177, 76, 0.16), rgba(245, 177, 76, 0.08));
|
||
border-top: 1px solid rgba(245, 177, 76, 0.5);
|
||
color: #ffe0ad;
|
||
font-size: 14px;
|
||
}
|
||
.approval-label {
|
||
flex: 1 1 auto;
|
||
}
|
||
/* W1: approval preview (command/diff) — its own full-width row, scrollable. */
|
||
.approval-preview {
|
||
flex: 1 1 100%;
|
||
order: 3; /* below the label + buttons regardless of insertion order */
|
||
max-height: 30vh;
|
||
overflow: auto;
|
||
overflow-x: auto;
|
||
border: 1px solid rgba(245, 177, 76, 0.35);
|
||
border-radius: 8px;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
padding: 8px 10px;
|
||
}
|
||
.approval-cmd {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-family: Menlo, Consolas, monospace;
|
||
font-size: 13px;
|
||
color: #f4f5f7;
|
||
}
|
||
.approval-truncated {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
}
|
||
#approvalbar button {
|
||
flex: none;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-weight: 600;
|
||
}
|
||
.approval-yes {
|
||
background: var(--green);
|
||
color: #06210f;
|
||
}
|
||
.approval-no {
|
||
background: var(--red);
|
||
color: #2a0808;
|
||
}
|
||
|
||
/* ── Launcher (home session chooser, shown when no tab is open) ──── */
|
||
.launcher {
|
||
position: absolute;
|
||
inset: 52px 0 0 0; /* mobile: reserve a row for the stacked .home-seg (desktop overrides to 0) */
|
||
overflow-y: auto;
|
||
padding: 22px 18px 40px;
|
||
box-sizing: border-box;
|
||
background: var(--bg);
|
||
}
|
||
.launcher-head {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px 16px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.launcher-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
.launcher-sub {
|
||
flex: 1 1 auto;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
}
|
||
/* A: recoverable (orphan) tmux sessions — a secondary section under the live grid,
|
||
separated by a rule so it never competes with what you are actually working on.
|
||
Hidden entirely when the list is empty. */
|
||
.launcher-orphans {
|
||
margin-top: 30px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.launcher-orphans-head {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.launcher-orphans-title {
|
||
font-size: 11px;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
}
|
||
.launcher-orphans-sub {
|
||
flex: 1 1 auto;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
}
|
||
/* Destructive, so it is the ghost role rather than a filled button — the design
|
||
reserves the accent fill for the action you actually came to perform. */
|
||
.launcher-cleanup {
|
||
font: inherit;
|
||
font-size: 12.5px;
|
||
cursor: pointer;
|
||
padding: 7px 15px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--border-strong);
|
||
background: transparent;
|
||
color: var(--text-dim);
|
||
flex: none;
|
||
}
|
||
.launcher-cleanup:hover {
|
||
border-color: var(--red);
|
||
color: var(--red);
|
||
}
|
||
.launcher-cleanup:focus-visible {
|
||
outline: 2px solid var(--text);
|
||
outline-offset: 2px;
|
||
}
|
||
/* The grid is capped, so say so rather than let the tail vanish silently. */
|
||
.launcher-orphans-more {
|
||
margin-top: 14px;
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
color: var(--text-faint);
|
||
}
|
||
.launcher-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
/* "New session" tile — leads the session grid, matching the session cards. */
|
||
.mg-new-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
min-height: 200px;
|
||
background: none;
|
||
border: 2px dashed var(--border-strong);
|
||
border-radius: 12px;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
font: inherit;
|
||
transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
|
||
}
|
||
.mg-new-card:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
}
|
||
.mg-new-plus {
|
||
font-size: 34px;
|
||
font-weight: 300;
|
||
line-height: 1;
|
||
}
|
||
.mg-new-label {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ── Session Manager page (manage.html) ──────────────────────────── */
|
||
#manage-root {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 24px 16px 60px;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
box-sizing: border-box;
|
||
}
|
||
.mg-header {
|
||
margin-bottom: 18px;
|
||
}
|
||
.mg-title {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
.mg-sub {
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
margin-top: 2px;
|
||
}
|
||
.mg-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
.mg-btn {
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
color: var(--text);
|
||
border-radius: 8px;
|
||
padding: 8px 14px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
text-decoration: none;
|
||
transition: background 0.1s ease;
|
||
}
|
||
.mg-btn:hover {
|
||
background: var(--surface-3);
|
||
}
|
||
.mg-btn.warn {
|
||
border-color: rgba(245, 177, 76, 0.5);
|
||
color: var(--amber);
|
||
}
|
||
.mg-btn.danger {
|
||
border-color: rgba(255, 107, 107, 0.5);
|
||
color: var(--red);
|
||
}
|
||
.mg-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.mg-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
}
|
||
.mg-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.mg-name {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
/* Live preview thumbnail (scaled read-only xterm) */
|
||
.mg-thumb {
|
||
position: relative;
|
||
width: 100%;
|
||
min-height: 90px;
|
||
background: #0e0f13;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
}
|
||
.mg-thumb:hover {
|
||
border-color: var(--accent);
|
||
}
|
||
.mg-thumb-inner {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
transform-origin: top left;
|
||
padding: 4px;
|
||
}
|
||
.mg-watch {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
background: var(--surface-3);
|
||
border-radius: 5px;
|
||
padding: 1px 6px;
|
||
}
|
||
.mg-watch.live {
|
||
color: var(--green);
|
||
background: rgba(70, 208, 127, 0.14);
|
||
}
|
||
.mg-status {
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
}
|
||
.mg-status.mg-waiting {
|
||
color: var(--amber);
|
||
font-weight: 600;
|
||
}
|
||
.mg-status.mg-working {
|
||
color: var(--accent);
|
||
}
|
||
.mg-meta {
|
||
color: var(--text-faint);
|
||
font-size: 11px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-family: Menlo, Consolas, monospace;
|
||
}
|
||
.mg-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.mg-actions .mg-open {
|
||
flex: 1 1 auto;
|
||
text-align: center;
|
||
}
|
||
.mg-open {
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border-radius: 8px;
|
||
padding: 7px 13px;
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
}
|
||
.mg-open:hover {
|
||
background: var(--accent-2);
|
||
}
|
||
.mg-kill {
|
||
background: transparent;
|
||
border: 1px solid rgba(255, 107, 107, 0.4);
|
||
color: var(--red);
|
||
border-radius: 8px;
|
||
padding: 7px 13px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
}
|
||
.mg-kill:hover {
|
||
background: rgba(255, 107, 107, 0.14);
|
||
}
|
||
.mg-empty {
|
||
color: var(--text-dim);
|
||
padding: 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ── v0.6 Projects panel ─────────────────────────────────────────────────── */
|
||
|
||
/* Panel wrapper (same host as .launcher; toggled via display) */
|
||
.proj-panel {
|
||
position: absolute;
|
||
inset: 52px 0 0 0; /* mobile: reserve a row for the stacked .home-seg (desktop overrides to 0) */
|
||
overflow-y: auto;
|
||
padding: 22px 18px 40px;
|
||
box-sizing: border-box;
|
||
background: var(--bg);
|
||
}
|
||
|
||
/* Search box */
|
||
.proj-search-wrap {
|
||
margin-bottom: 16px;
|
||
}
|
||
.proj-search {
|
||
width: 100%;
|
||
max-width: 320px;
|
||
padding: 8px 12px;
|
||
font: inherit;
|
||
font-size: 14px;
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 8px;
|
||
outline: none;
|
||
box-sizing: border-box;
|
||
}
|
||
.proj-search:focus {
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||
}
|
||
|
||
/* Outer container is now a vertical stack of collapsible namespace sections. */
|
||
.proj-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 22px;
|
||
}
|
||
|
||
/* The actual card grid — one per group (and the flat fallback). Mirrors .mg-grid. */
|
||
.proj-grid-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
/* ── Namespace group section ─────────────────────────────────────────────── */
|
||
.proj-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.proj-group-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 4px 2px;
|
||
position: sticky; /* keep the namespace visible while scrolling a long group */
|
||
top: 0;
|
||
z-index: 1;
|
||
background: var(--bg);
|
||
user-select: none;
|
||
}
|
||
.proj-group-head[role='button'] {
|
||
cursor: pointer;
|
||
border-radius: 7px;
|
||
}
|
||
.proj-group-head[role='button']:hover {
|
||
background: var(--surface-1);
|
||
}
|
||
.proj-group-head[role='button']:focus-visible {
|
||
outline: 2px solid var(--accent);
|
||
outline-offset: 1px;
|
||
}
|
||
.proj-group-caret {
|
||
color: var(--text-faint);
|
||
font-size: 11px;
|
||
width: 12px;
|
||
flex: none;
|
||
text-align: center;
|
||
}
|
||
.proj-group-label {
|
||
color: var(--text);
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.proj-group-count {
|
||
color: var(--text-faint);
|
||
font-size: 12px;
|
||
font-family: Menlo, Consolas, monospace;
|
||
}
|
||
/* "N active" badge on a namespace/other header — never lose running work behind a caret. */
|
||
.proj-group-active {
|
||
color: var(--green);
|
||
font-size: 11px;
|
||
margin-left: 2px;
|
||
}
|
||
/* "Active now" header — pinned, accent-tinted, non-collapsible. */
|
||
.proj-group-head.proj-group-active .proj-group-caret {
|
||
color: var(--green);
|
||
font-size: 9px;
|
||
}
|
||
.proj-group-head.proj-group-active .proj-group-label {
|
||
color: var(--accent);
|
||
}
|
||
.proj-group.collapsed {
|
||
gap: 0;
|
||
}
|
||
|
||
/* Project card */
|
||
.proj-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 14px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
transition: border-color 0.12s ease;
|
||
}
|
||
.proj-card:hover {
|
||
border-color: var(--border-strong);
|
||
}
|
||
|
||
/* Card header row */
|
||
.proj-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Repo name */
|
||
.proj-name {
|
||
flex: 1 1 auto;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Branch chip — same geometry and border as every other chip in the panel. It was
|
||
the odd one out: no border, no mono face, and 1px tighter all round, so the
|
||
detail header's branch did not match the branch chips in the worktree list. */
|
||
.proj-branch {
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
border: 1px solid rgba(227, 166, 74, 0.36);
|
||
border-radius: 6px;
|
||
padding: 3px 9px;
|
||
white-space: nowrap;
|
||
flex: none;
|
||
}
|
||
|
||
/* Dirty indicator (● dot) */
|
||
.proj-dirty {
|
||
font-size: 10px;
|
||
color: var(--amber);
|
||
flex: none;
|
||
}
|
||
|
||
/* W3(a): ahead/behind sync chip (mirrors the .proj-branch chip look). */
|
||
.proj-sync {
|
||
font-size: 11px;
|
||
color: var(--amber);
|
||
background: var(--accent-soft);
|
||
border-radius: 5px;
|
||
padding: 2px 7px;
|
||
white-space: nowrap;
|
||
flex: none;
|
||
}
|
||
|
||
/* ── w6/G3: the project detail sync band ──────────────────────────────────────
|
||
Four labelled cells, per docs/mockups/project-detail-git.html. The captions
|
||
and footnotes are load-bearing, not decoration: they are what tells a reader
|
||
WHY a zero cannot be trusted, which is the whole reason this band exists.
|
||
Semantic colours are deliberately NOT the accent; --ok reads "verified,
|
||
ignore this" and must stay unreachable from a stale or unknowable state. */
|
||
.proj-syncband {
|
||
display: flex;
|
||
align-items: stretch;
|
||
margin: 16px 0 4px;
|
||
background: var(--sunk);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 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: 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;
|
||
}
|
||
|
||
.proj-sync-actions {
|
||
margin-left: auto;
|
||
justify-content: center;
|
||
}
|
||
|
||
.proj-sync-k {
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.13em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
.proj-sync-v {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 15px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.proj-sync-big {
|
||
font-size: 19px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.proj-sync-sub {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
.proj-sync-sub-bad {
|
||
color: var(--warn);
|
||
}
|
||
|
||
.proj-sync-upstream {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.proj-sync-chip {
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
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: var(--ok);
|
||
background: rgba(125, 155, 118, 0.13);
|
||
border-color: rgba(125, 155, 118, 0.3);
|
||
}
|
||
|
||
/* "This number may be lying" — never applied to ahead, which needs no fetch. */
|
||
.proj-sync-stale,
|
||
.proj-sync-noupstream,
|
||
.proj-sync-detached {
|
||
color: var(--warn);
|
||
}
|
||
|
||
.proj-sync-chip.proj-sync-stale,
|
||
.proj-sync-noupstream,
|
||
.proj-sync-detached {
|
||
background: rgba(207, 107, 79, 0.14);
|
||
border-color: rgba(207, 107, 79, 0.36);
|
||
}
|
||
|
||
.proj-sync-dirty {
|
||
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: 6px;
|
||
padding: 3px 9px;
|
||
font-size: 11.5px;
|
||
white-space: nowrap;
|
||
flex: none;
|
||
}
|
||
|
||
.proj-sync-fetch {
|
||
font: inherit;
|
||
font-size: 12.5px;
|
||
font-weight: 560;
|
||
cursor: pointer;
|
||
padding: 7px 15px;
|
||
border-radius: 7px;
|
||
border: 0;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
}
|
||
|
||
.proj-sync-fetch:hover:not(:disabled) {
|
||
filter: brightness(1.08);
|
||
}
|
||
|
||
.proj-sync-fetch:disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.proj-sync-fetch:focus-visible {
|
||
outline: 2px solid var(--text);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.proj-syncband {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.proj-sync-cell {
|
||
border-right: 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.proj-sync-actions {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
/* ── 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(--text-dim);
|
||
text-transform: none;
|
||
letter-spacing: 0.04em;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
/* ── w6/G5: worktree rows as two-line cards ──────────────────────────────── */
|
||
/* 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. */
|
||
/* 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), var(--surface-2) 46%);
|
||
}
|
||
|
||
.proj-wt-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.proj-wt-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.proj-wt-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: none;
|
||
}
|
||
|
||
/* Role 2 of 2 — the ghost. The design has exactly two button roles: filled accent
|
||
for the action that commits a form (Fetch, Create Worktree), and this outline for
|
||
anything that only opens, reveals or toggles. ONE geometry for every one of them:
|
||
the role had drifted to three different sizes across the panel, all at
|
||
full-strength --text instead of the muted label the design asks for.
|
||
|
||
SCOPE — read this before adding a selector. The geometry below covers exactly two
|
||
surfaces: the worktree section (its Prune header button and each row's Open / ✕
|
||
rail) and the diff viewer's toggle and toolbar (View Diff, Working/Staged, ✕ Close).
|
||
Those are the buttons that only open, reveal, switch or dismiss, and every one of
|
||
them is in the list, so none of them can quietly pick its own size again. It is NOT
|
||
a claim about the whole projects panel: at least the following are outline buttons
|
||
by this file's own definition and are deliberately deferred, not overlooked —
|
||
|
||
.proj-back background:none, 1px --border, --text-dim, radius 8,
|
||
6px 12px, 13px
|
||
.proj-dsession-kill background:none, 1px --border, --text-dim, radius 7, 30×30
|
||
.proj-claudemd-btn --surface-2 fill, 1px --border-strong, radius 7, 6px 12px, 12px
|
||
.proj-launch --surface-2 fill, 1px --border, --text-dim, radius 9, 9px 4px
|
||
.df-push-btn --surface-2 fill, 1px --border-strong, --text, radius 6,
|
||
8px 16px, `font: inherit` → 13px from .df-viewer (W4)
|
||
.df-file-stage --surface-2 fill, 1px --border-strong, --text, radius 6,
|
||
3px 10px, 12px (W4)
|
||
|
||
Folding those in touches W4 and the project-detail header, which is a wider change
|
||
than this one. Until someone does it, do not add a further size in the meantime — a
|
||
new outline button either joins the selector list below or matches one of the shapes
|
||
above exactly.
|
||
|
||
One exception, and it is in the list not outside it: `.df-tab` can be disabled
|
||
(base-compare mode turns both tabs inert), so it joins :hover as
|
||
`.df-tab:not(:disabled)` — otherwise an inert tab still lit up gold under the
|
||
cursor. Nothing about the geometry is restated anywhere. */
|
||
.proj-wt-open,
|
||
.proj-wt-remove,
|
||
.proj-wt-prune,
|
||
.proj-diff-toggle,
|
||
.df-tab,
|
||
.df-close {
|
||
font: inherit;
|
||
font-size: 12.5px;
|
||
cursor: pointer;
|
||
padding: 7px 15px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--border-strong);
|
||
background: transparent;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.proj-wt-open:hover,
|
||
.proj-wt-remove:hover,
|
||
.proj-wt-prune:hover,
|
||
.proj-diff-toggle:hover,
|
||
.df-tab:not(:disabled):hover,
|
||
.df-close:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* .df-base-select is not this role — it is a form control, skinned with
|
||
.proj-fanout-mode — but it is keyboard-focusable and sits between .df-tab and
|
||
.df-close in the same rail, and it declared `outline: none` with only a 1px border
|
||
recolour to show focus. Two focus languages in three adjacent controls is one too
|
||
many, so it borrows the ring here. This rule declares outline only, so it restates
|
||
none of that control's geometry. */
|
||
.proj-wt-open:focus-visible,
|
||
.proj-wt-remove:focus-visible,
|
||
.proj-wt-prune:focus-visible,
|
||
.proj-diff-toggle:focus-visible,
|
||
.df-tab:focus-visible,
|
||
.df-base-select:focus-visible,
|
||
.df-close:focus-visible {
|
||
outline: 2px solid var(--text);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* Square icon variant — same role, just not a word. Keeps the shared height
|
||
(12.5px line + 7px padding twice) so it sits flush beside Open in the rail
|
||
rather than stretching the row. */
|
||
.proj-wt-remove {
|
||
width: 33px;
|
||
padding: 7px 0;
|
||
line-height: 1;
|
||
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: 5px;
|
||
}
|
||
|
||
.proj-commit-unpushed::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 2px;
|
||
border-radius: 2px;
|
||
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: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Where the upstream actually sits in this history — never drawn without one. */
|
||
.proj-commit-boundary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin: 9px 0 7px;
|
||
padding-left: 8px;
|
||
}
|
||
|
||
.proj-commit-boundary-label {
|
||
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: 3px 11px;
|
||
white-space: nowrap;
|
||
flex: none;
|
||
}
|
||
|
||
.proj-commit-boundary-rule {
|
||
flex: 1;
|
||
height: 1px;
|
||
background: var(--border-strong);
|
||
}
|
||
|
||
/* 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 {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
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);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* W3(d): recent-commit list in the project detail. */
|
||
.proj-commitlog {
|
||
margin: 4px 0 10px;
|
||
}
|
||
.proj-commitlog-loading {
|
||
font-size: 12px;
|
||
color: var(--text-faint);
|
||
}
|
||
.proj-commitlog-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
/* 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: grid;
|
||
grid-template-columns: 22px 74px 56px 1fr;
|
||
align-items: baseline;
|
||
gap: 10px;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
min-width: 0;
|
||
}
|
||
.proj-commit-hash {
|
||
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);
|
||
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-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
color: var(--text-faint);
|
||
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). */
|
||
.wya-banner {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 14px;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
background: var(--accent-soft);
|
||
border-bottom: 1px solid var(--accent);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||
}
|
||
.wya-title {
|
||
font-weight: 600;
|
||
color: var(--accent);
|
||
flex: none;
|
||
}
|
||
.wya-summary {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.wya-cost {
|
||
color: var(--text-faint);
|
||
flex: none;
|
||
}
|
||
.wya-dismiss {
|
||
flex: none;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
padding: 2px 6px;
|
||
}
|
||
.wya-dismiss:hover {
|
||
color: var(--text);
|
||
}
|
||
|
||
/* W3: PR + CI status chip (mirrors the .proj-branch chip look). */
|
||
.proj-pr-host {
|
||
display: inline-flex;
|
||
flex: none;
|
||
}
|
||
.proj-pr-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
border-radius: 5px;
|
||
padding: 2px 7px;
|
||
white-space: nowrap;
|
||
max-width: 260px;
|
||
color: var(--text-faint);
|
||
background: var(--accent-soft);
|
||
}
|
||
.proj-pr-chip .proj-pr-title {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: var(--text-faint);
|
||
}
|
||
.proj-pr-loading {
|
||
opacity: 0.6;
|
||
}
|
||
/* PR-state modifiers */
|
||
.proj-pr-open .proj-pr-label {
|
||
color: var(--green);
|
||
}
|
||
.proj-pr-draft .proj-pr-label {
|
||
color: var(--text-faint);
|
||
}
|
||
.proj-pr-merged .proj-pr-label {
|
||
color: var(--accent);
|
||
}
|
||
.proj-pr-closed .proj-pr-label {
|
||
color: var(--red);
|
||
}
|
||
.proj-pr-none,
|
||
.proj-pr-unavailable {
|
||
color: var(--text-faint);
|
||
background: transparent;
|
||
}
|
||
/* Checks-rollup modifiers (colour the whole chip label) */
|
||
.proj-pr-checks-ok .proj-pr-label {
|
||
color: var(--green);
|
||
}
|
||
.proj-pr-checks-fail .proj-pr-label {
|
||
color: var(--red);
|
||
}
|
||
.proj-pr-checks-pending .proj-pr-label {
|
||
color: var(--amber);
|
||
}
|
||
/* Mergeable = conflicting */
|
||
.proj-pr-conflict {
|
||
box-shadow: inset 0 0 0 1px var(--red);
|
||
}
|
||
|
||
/* Meta line (last-active time) */
|
||
.proj-meta {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
font-family: Menlo, Consolas, monospace;
|
||
}
|
||
|
||
/* Favourite star toggle */
|
||
.proj-fav {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
padding: 0 2px;
|
||
line-height: 1;
|
||
flex: none;
|
||
transition: color 0.12s ease;
|
||
}
|
||
.proj-fav:hover {
|
||
color: var(--accent);
|
||
}
|
||
.proj-fav.on {
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Sessions list (1:N) */
|
||
.proj-sessions-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
/* One running-session row */
|
||
.proj-session-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
border-radius: 8px;
|
||
background: var(--surface-3);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
transition: background 0.1s ease;
|
||
}
|
||
.proj-session-row:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
/* Status dot on a session row — colours from THEME.md v0.6 note */
|
||
.proj-session-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex: none;
|
||
background: var(--text-faint);
|
||
}
|
||
.proj-dot-working {
|
||
background: var(--accent);
|
||
box-shadow: 0 0 5px var(--accent-soft);
|
||
}
|
||
.proj-dot-waiting {
|
||
background: var(--amber);
|
||
}
|
||
/* sessionDotClass emits one class per ClaudeStatus member (src/types.ts) and 'stuck'
|
||
is one of them — without this rule a stuck session's dot rendered uncoloured.
|
||
--warn is the panel's stuck/attention colour: .tl-icon-stuck already uses it, so the
|
||
two surfaces say the same thing about the same state. */
|
||
.proj-dot-stuck {
|
||
background: var(--warn);
|
||
}
|
||
.proj-dot-idle {
|
||
background: #8b8a86;
|
||
}
|
||
.proj-dot-unknown {
|
||
background: var(--text-faint);
|
||
}
|
||
|
||
/* Session title */
|
||
.proj-session-title {
|
||
flex: 1 1 auto;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Kill (✕) on a session row — close the session straight from the card. */
|
||
.proj-session-kill {
|
||
flex: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
line-height: 1;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 5px;
|
||
background: none;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
font-size: 11px;
|
||
transition: background 0.1s ease, color 0.1s ease;
|
||
}
|
||
.proj-session-kill:hover {
|
||
background: var(--red, #e5484d);
|
||
color: #fff;
|
||
}
|
||
|
||
/* Client-count badge (👁 N) */
|
||
.proj-session-badge {
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
background: var(--surface-2);
|
||
border-radius: 4px;
|
||
padding: 1px 5px;
|
||
flex: none;
|
||
}
|
||
|
||
/* "+ New" / "+ start claude here" action button */
|
||
/* Launcher row — Claude · Codex · VS Code, each a brand-logo icon button. */
|
||
.proj-launchers {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 2px;
|
||
}
|
||
.proj-launch {
|
||
position: relative; /* for the active count badge */
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 9px 4px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 9px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
color: var(--text-dim); /* drives the logo colour (currentColor) by default */
|
||
transition: background 0.12s ease, border-color 0.12s ease, transform 0.08s ease;
|
||
}
|
||
.proj-launch svg {
|
||
display: block;
|
||
}
|
||
.proj-launch-cap {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-dim);
|
||
}
|
||
.proj-launch:hover {
|
||
background: var(--surface-3);
|
||
border-color: var(--border-strong);
|
||
transform: translateY(-1px);
|
||
}
|
||
.proj-launch:active {
|
||
transform: translateY(0);
|
||
}
|
||
.proj-launch:hover .proj-launch-cap {
|
||
color: var(--text);
|
||
}
|
||
/* Brand colours on the logos (currentColor) */
|
||
.proj-launch-claude {
|
||
color: #d97757; /* Claude coral */
|
||
}
|
||
.proj-launch-codex {
|
||
color: #e7e8ec; /* OpenAI mark — near-white on dark */
|
||
}
|
||
.proj-launch-vscode {
|
||
color: #3aa0e3; /* VS Code blue */
|
||
}
|
||
|
||
/* Active state — a matching session is running (Claude logo when a Claude
|
||
* session is live). Coral ring + faint tint draw the eye without shouting. */
|
||
.proj-launch-active {
|
||
background: rgba(217, 119, 87, 0.14);
|
||
border-color: #d97757;
|
||
box-shadow: 0 0 0 1px rgba(217, 119, 87, 0.45) inset;
|
||
}
|
||
.proj-launch-active .proj-launch-cap {
|
||
color: #e9b9a6;
|
||
}
|
||
.proj-launch-badge {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 6px;
|
||
min-width: 15px;
|
||
height: 15px;
|
||
padding: 0 3px;
|
||
box-sizing: border-box;
|
||
border-radius: 8px;
|
||
background: #d97757;
|
||
color: #1a1408;
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
line-height: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ── Project detail view ─────────────────────────────────────────── */
|
||
.proj-name-link {
|
||
cursor: pointer;
|
||
}
|
||
.proj-name-link:hover {
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
}
|
||
.proj-detail-inner {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
}
|
||
.proj-back {
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
border-radius: 8px;
|
||
padding: 6px 12px;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
margin-bottom: 16px;
|
||
transition: background 0.12s ease, color 0.12s ease;
|
||
}
|
||
.proj-back:hover {
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
}
|
||
.proj-detail-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.proj-detail-name {
|
||
margin: 0;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
.proj-detail-path {
|
||
margin: 4px 0 8px;
|
||
font-family: Menlo, Consolas, monospace;
|
||
font-size: 12px;
|
||
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 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;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.proj-fanout-prompt {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
resize: vertical;
|
||
min-height: 44px;
|
||
font: inherit;
|
||
}
|
||
.proj-fanout-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.proj-fanout-lanes {
|
||
width: 72px;
|
||
}
|
||
.proj-fanout-branch {
|
||
flex: 1 1 160px;
|
||
min-width: 120px;
|
||
}
|
||
/* --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;
|
||
}
|
||
/* ── 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
|
||
panel. Layout stays with each element's own rule above; this block only sets
|
||
the shared skin, so there is no duplicate declaration of the same property.
|
||
|
||
Two button roles, matching what the panel already established: filled accent
|
||
for the action that commits the form (like Fetch), outline for anything that
|
||
only reveals or toggles (like Open). */
|
||
.proj-wt-branch-input,
|
||
.proj-fanout-prompt,
|
||
.proj-fanout-lanes,
|
||
.proj-fanout-branch,
|
||
.proj-fanout-mode {
|
||
font: inherit;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 8px;
|
||
padding: 8px 11px;
|
||
outline: none;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.proj-wt-branch-input::placeholder,
|
||
.proj-fanout-prompt::placeholder,
|
||
.proj-fanout-branch::placeholder {
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
.proj-wt-branch-input:focus,
|
||
.proj-fanout-prompt:focus,
|
||
.proj-fanout-lanes:focus,
|
||
.proj-fanout-branch:focus,
|
||
.proj-fanout-mode:focus,
|
||
.df-base-select:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
/* A branch name is an identifier, so it reads as one. */
|
||
.proj-wt-branch-input,
|
||
.proj-fanout-branch {
|
||
font-family: var(--mono-font);
|
||
font-size: 12.5px;
|
||
}
|
||
|
||
.proj-fanout-lanes {
|
||
text-align: center;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* Without appearance:none WebKit keeps its own light control, so these were the
|
||
two widgets in the panel that stayed a native grey dropdown. The arrow has to be
|
||
redrawn by hand once the native one is gone. The diff viewer's base picker joins
|
||
the list rather than getting its own copy of the caret — it differs only in size,
|
||
which its own rule sets with padding longhands so `padding-right` stays here. */
|
||
.proj-fanout-mode,
|
||
.df-base-select {
|
||
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: 7px 15px;
|
||
border: 0;
|
||
border-radius: 7px;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
}
|
||
|
||
.proj-wt-submit:hover:not(:disabled),
|
||
.proj-fanout-submit:hover:not(:disabled) {
|
||
filter: brightness(1.08);
|
||
}
|
||
|
||
.proj-wt-submit:disabled,
|
||
.proj-fanout-submit:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Role 2 (the ghost, incl. .proj-diff-toggle) is defined once, with the worktree
|
||
action rail — see "Role 2 of 2" above. Nothing for it here on purpose: a second
|
||
block at equal specificity is how the role drifted to three sizes before. */
|
||
|
||
.proj-wt-submit:focus-visible,
|
||
.proj-fanout-submit:focus-visible {
|
||
outline: 2px solid var(--text);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* W5 fan-out status/error banner — fixed, dismissible, textContent only. */
|
||
.fanout-banner {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 16px;
|
||
transform: translateX(-50%);
|
||
max-width: min(680px, 92vw);
|
||
z-index: 60;
|
||
padding: 10px 14px;
|
||
border-radius: 8px;
|
||
background: var(--surface-3, #2a2c33);
|
||
color: var(--text, #e7e8ec);
|
||
border: 1px solid var(--border, #3a3d46);
|
||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
/* Section header with an inline action (CLAUDE.md generate/update). */
|
||
.proj-section-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin: 22px 0 10px;
|
||
}
|
||
.proj-section-row .proj-section-title {
|
||
margin: 0;
|
||
}
|
||
.proj-claudemd-btn {
|
||
flex: none;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
color: var(--accent);
|
||
border-radius: 7px;
|
||
padding: 6px 12px;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.12s ease, border-color 0.12s ease;
|
||
}
|
||
.proj-claudemd-btn:hover {
|
||
background: var(--accent-soft);
|
||
border-color: var(--accent);
|
||
}
|
||
.proj-claudemd {
|
||
margin: 0;
|
||
max-height: 320px;
|
||
overflow: auto;
|
||
padding: 12px 14px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
font-family: Menlo, Consolas, monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
/* Worktrees */
|
||
.proj-wt-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.proj-wt-row {
|
||
/* w6: two-line card — content column, then the actions rail pinned right. */
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 12px 15px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
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-family: var(--mono-font);
|
||
font-size: 11px;
|
||
letter-spacing: 0.04em;
|
||
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-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.
|
||
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);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
/* Detailed session rows */
|
||
.proj-detail-sessions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.proj-dsession {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
}
|
||
.proj-dsession-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.proj-dsession-title {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.proj-dsession-meta {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
.proj-dsession-open {
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border: none;
|
||
border-radius: 7px;
|
||
padding: 6px 14px;
|
||
font: inherit;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.proj-dsession-open:hover {
|
||
background: var(--accent-2);
|
||
}
|
||
.proj-dsession-kill {
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
border-radius: 7px;
|
||
width: 30px;
|
||
height: 30px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
.proj-dsession-kill:hover {
|
||
background: var(--red, #e5484d);
|
||
border-color: var(--red, #e5484d);
|
||
color: #fff;
|
||
}
|
||
|
||
/* Empty state */
|
||
.proj-empty {
|
||
color: var(--text-dim);
|
||
padding: 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ── Project detail: the diff section and the activity timeline ───────────────
|
||
These shipped as class names with no rules at all, so the "View Diff" panel and
|
||
the per-session activity logs rendered as raw user-agent boxes. The viewer's own
|
||
.df-* rules live with the W4 git-write block at the end of the file, so all diff
|
||
styling stays in one place; only the project-detail containers are here, beside
|
||
their .proj-* siblings. */
|
||
|
||
/* The detail column. Nothing visual belongs here — .proj-detail-inner owns the
|
||
width, and `display` is driven inline ('none' | 'block', projects.ts:1528/1569),
|
||
so a display declaration here would only ever fight it. min-width:0 is the guard
|
||
that keeps a wide descendant from widening the flex/grid column instead of
|
||
scrolling inside itself. */
|
||
.proj-detail {
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Keeps the ghost "View Diff" toggle and the panel it reveals together as one
|
||
block, so the viewer does not sit flush against the next section heading. */
|
||
.proj-diff-section {
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
/* Open-state only: the closed state is an inline display:none that wins anyway,
|
||
and the open state clears it to ''. A display declaration here would close the
|
||
panel permanently. */
|
||
.proj-diff-panel {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
/* One block per running session, so this stack compounds — three sessions is three
|
||
of these. */
|
||
.proj-activity-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.proj-activity-session {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* A per-item caption one level below .proj-section-title, and a session title is
|
||
an identifier, so it reads as one rather than as more heading. */
|
||
.proj-activity-label {
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
color: var(--text-dim);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* The same sunk well the panel uses everywhere for read-only data — fill, hairline,
|
||
radius, padding. That is all this rule is.
|
||
|
||
NO HEIGHT CAP, deliberately, and please do not add one back. Three separate capping
|
||
mechanisms were tried on this element and each cost more than the height it saved:
|
||
|
||
- `max-height` + `overflow-y: auto` gave a scrollbar nobody can use. mountTimeline
|
||
polls every 5s and render() wipes and re-appends every child (timeline.ts:174-189),
|
||
so scrollTop returns to 0 on every tick — a scroll position cannot survive a
|
||
wipe-and-re-append render loop.
|
||
- `mask-image` softened the cut edge and dissolved the well with it: mask-clip
|
||
defaults to border-box, so the gradient multiplied the --sunk fill, the bottom
|
||
hairline and both bottom corners, and did it on every well including ones that
|
||
clipped nothing.
|
||
- a `::after` gradient overlay does not touch the chrome, but a pseudo-element
|
||
cannot know whether anything is actually clipped, so it fired unconditionally.
|
||
Measured: a 6-event well is 142px tall against a 220px cap and clips nothing,
|
||
yet the 44px overlay covered its last two rows at up to .84 alpha; and in the
|
||
56px empty well it swallowed "No activity yet" outright: --text-dim reads 7.77:1
|
||
on --sunk, and under the overlay that line ran from 5.2:1 at the top of the
|
||
glyphs to 2.6:1 at their baseline — for most of its height BELOW the 3.66:1
|
||
--text-faint value that promoting .tl-empty to --text-dim had just escaped.
|
||
|
||
Gating the fade on a row count (`:has(> .tl-row:nth-child(10))`) is refused for a
|
||
different reason, and NOT because it cannot measure: .tl-label is nowrap +
|
||
ellipsis, so every row is exactly one line high and a row count does track the
|
||
well's height. It is refused because it is still a cap. The gate only decides when
|
||
to cut; the cutting is the part every attempt above paid more for than the height
|
||
it saved.
|
||
|
||
Un-capped is not unbounded: render() slices to maxEvents, default 50
|
||
(timeline.ts:20), so the well's ceiling is 50 rows and it grows to whatever it
|
||
holds — which is what this element did before any of this CSS existed. What is not
|
||
bounded is the number of wells: buildActivitySection mounts one per running session,
|
||
so the Activity section's height scales with running sessions. That is the price of
|
||
no cap, stated here so the next reader does not have to re-derive it. */
|
||
.proj-tl-container {
|
||
padding: 6px 0;
|
||
background: var(--sunk);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
/* Every .tl-* rule is scoped to .proj-tl-container on purpose. timeline.ts also
|
||
mounts into #timeline-panel on the terminal page (tabs.ts:695) — a bare div on
|
||
document.body with no CSS, no background and no positioning, and out of scope
|
||
here. Unscoped .tl-* rules would drop finished-looking rows straight onto the
|
||
terminal and make an unfinished panel look done.
|
||
A grid, not a flex row, for the same reason .proj-commit-row is one: every column
|
||
has to land in the same place on every line or the log stops scanning as a column
|
||
of times. The icon track is fixed because emoji advance widths differ per glyph. */
|
||
.proj-tl-container .tl-row {
|
||
display: grid;
|
||
grid-template-columns: 40px 20px 1fr;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
padding: 3px 8px;
|
||
font-size: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* --text-dim, not --text-faint: the timestamp is one of the log's three columns and
|
||
the only one that answers "when", and --text-faint at 11.5px is 3.67:1 on --sunk. */
|
||
.proj-tl-container .tl-time {
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--text-dim);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.proj-tl-container .tl-icon {
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
text-align: center;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* Variants after the base — same element, same specificity, source order decides.
|
||
Only ✓ (done) and ⚠ (stuck) are monochrome text glyphs that actually take a
|
||
colour; 🔧 ⏳ 💬 are colour emoji and ignore `color` entirely. The `color` on all
|
||
of them is the fallback for a platform that renders them as text.
|
||
The two high-frequency ones used to be stepped back with opacity as well, since
|
||
that does bite on emoji — dropped, because it also bites on the fallback: 🔧 as
|
||
text at --text-faint × .7 is 2.36:1, and the whole point of the icon track is to
|
||
let you tell a tool call from a message without reading. The row's grid already
|
||
ranks the icon below the label; it does not also need to be faded.
|
||
💬 (user) has no variant rule at all and takes .tl-icon's --text-dim: it was the one
|
||
icon that stepped DOWN from the base, which rendered a message quieter than the tool
|
||
call it answered — the inverse of the ranking this track exists to give. Glyph, not
|
||
weight, is what separates the two. */
|
||
.proj-tl-container .tl-icon-tool {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* waiting = --amber is already the session-dot semantic (THEME.md). */
|
||
.proj-tl-container .tl-icon-waiting {
|
||
color: var(--amber);
|
||
}
|
||
|
||
/* Not --ok. "Done" here means one tool call finished, which is nothing like
|
||
"I checked your upstream, ignore this" — the one state green is allowed to
|
||
mean (:root L23-29). The ✓ is its own signal. */
|
||
.proj-tl-container .tl-icon-done {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.proj-tl-container .tl-icon-stuck {
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* The only elastic column, and frequently a 500-character absolute path. One line
|
||
with an ellipsis, for the reason .proj-wt-path gives: a wrapped path re-flows
|
||
every row below it and is no more readable for it. */
|
||
.proj-tl-container .tl-label {
|
||
min-width: 0;
|
||
color: var(--text-dim);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Centred like .proj-empty and .df-empty, smaller because this well is one of
|
||
several on the page. --text-dim for the .tl-time reason: --text-faint is 3.66:1 on
|
||
--sunk, and "No activity yet" is the only thing the well says when a session has no
|
||
events — the one place that text appears is not the place to spend legibility. */
|
||
.proj-tl-container .tl-empty {
|
||
padding: 14px 8px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* gh-chip.ts:215-217 already sets display:none inline on this element, so this rule
|
||
changes nothing today and can never conflict. It is here so the class states its
|
||
own intent instead of depending on one line of TypeScript to stay put. */
|
||
.proj-pr-hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* Must sit AFTER the rules above — a media query adds no specificity, so an earlier
|
||
block at equal specificity simply loses. 720px is the panel's established phone
|
||
breakpoint (.proj-commit-row, .proj-syncband), not a new one. */
|
||
@media (max-width: 720px) {
|
||
/* Take back .proj-panel's 18px of side padding for the diff viewer: 36px is about
|
||
five columns of mono text, which on a 380px screen is the difference between
|
||
reading a line and scrolling it. The negative margins exactly cancel that
|
||
padding (.proj-panel is `padding: 22px 18px 40px` with box-sizing:border-box and
|
||
.proj-detail-inner adds none), so the viewer lands on the panel's padding-box
|
||
edge and adds no horizontal overflow. */
|
||
.proj-diff-panel {
|
||
margin-left: -18px;
|
||
margin-right: -18px;
|
||
}
|
||
|
||
/* Edge to edge means the side borders and corners have nothing to sit against. */
|
||
.proj-diff-panel .df-viewer {
|
||
border-left: 0;
|
||
border-right: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* All three timeline columns carry distinct information — unlike the commit row,
|
||
where the subject dominates and the age can go — so this tightens the tracks
|
||
instead of dropping one. */
|
||
.proj-tl-container .tl-row {
|
||
grid-template-columns: 38px 18px 1fr;
|
||
gap: 6px;
|
||
}
|
||
}
|
||
|
||
/* ── Home segmented control (P6 builds the DOM; styles defined here per spec) */
|
||
|
||
/* Horizontal pill container, centered above the home grid */
|
||
/* Segmented control: a single rounded "track" holding two pill buttons, with
|
||
* the active one filled (iOS-style). Centred with fit-content + margin auto. */
|
||
.home-seg {
|
||
width: fit-content;
|
||
margin: 10px auto 12px;
|
||
display: flex;
|
||
gap: 3px;
|
||
padding: 3px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
/* Inactive segment */
|
||
.home-seg-btn {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
padding: 6px 22px;
|
||
border-radius: 999px;
|
||
transition: background 0.12s ease, color 0.12s ease;
|
||
}
|
||
.home-seg-btn:not(.active):hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
}
|
||
|
||
/* Active segment — Amber accent; text uses --on-accent for contrast */
|
||
.home-seg-btn.active {
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
font-weight: 600;
|
||
}
|
||
.home-seg-btn.active:hover {
|
||
background: var(--accent-2);
|
||
}
|
||
|
||
/* On the home chooser (Sessions/Projects), there is no active terminal, so the
|
||
* bottom key-bar is just noise — hide it. Toggled by TabApp.updateHomeView(). */
|
||
body.home-open #keybar {
|
||
display: none;
|
||
}
|
||
/* Home chooser hides the key-bar, so reclaim its reserved space at the bottom
|
||
* (keeping the home-indicator clearance so launcher content stays reachable). */
|
||
body.home-open #term {
|
||
bottom: var(--safe-b);
|
||
}
|
||
|
||
/* Desktop: float the Sessions/Projects toggle top-right, on the SAME row as the
|
||
* panel's filter/header — instead of a dedicated row that left a big empty band.
|
||
* The panels fill from the top; the launcher header reserves room for the pill.
|
||
* (Mobile keeps the stacked, centred toggle — see .home-seg above — to avoid
|
||
* overlapping the full-width filter box on narrow screens.) */
|
||
@media (min-width: 769px) {
|
||
.home-seg {
|
||
position: absolute;
|
||
top: 16px;
|
||
right: 20px;
|
||
margin: 0;
|
||
z-index: 6;
|
||
}
|
||
.launcher,
|
||
.proj-panel {
|
||
inset: 0;
|
||
}
|
||
.launcher-head {
|
||
padding-right: 240px; /* clear the absolutely-positioned .home-seg */
|
||
}
|
||
}
|
||
|
||
/* v0.7: timeline + push toggle reuse the minimal .toolbtn look. */
|
||
.toolbtn.active {
|
||
color: var(--accent);
|
||
}
|
||
.toolbtn.push-bell-disabled {
|
||
opacity: 0.45;
|
||
cursor: default;
|
||
}
|
||
.toolbtn.push-bell-disabled:hover {
|
||
background: none;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* v0.7 fix: timeline + push toggle render as minimal line-icon buttons that
|
||
match the .toolbtn toolbar style (transparent, monochrome, accent on
|
||
hover/active). !important defeats the earlier boxed default for these classes. */
|
||
.tab-timeline,
|
||
.push-toggle-btn,
|
||
.push-toggle-disabled {
|
||
background: none !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 7px 9px;
|
||
border-radius: 8px;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
transition: background 0.12s ease, color 0.12s ease;
|
||
}
|
||
.tab-timeline svg,
|
||
.push-toggle-btn svg,
|
||
.push-bell svg {
|
||
width: 18px;
|
||
height: 18px;
|
||
display: block;
|
||
}
|
||
.tab-timeline:hover,
|
||
.push-toggle-btn:hover {
|
||
color: var(--accent);
|
||
background: var(--surface-2) !important;
|
||
}
|
||
.tab-timeline.active,
|
||
.push-toggle-btn.push-toggle-on {
|
||
color: var(--accent);
|
||
}
|
||
.push-toggle-disabled {
|
||
color: var(--text-faint);
|
||
opacity: 0.6;
|
||
cursor: default;
|
||
}
|
||
.push-hint {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
/* v0.7: styled hover tooltip on the timeline + push buttons (like .toolbtn).
|
||
Left-anchored — these sit on the left side of the tab bar. */
|
||
.tab-timeline,
|
||
.push-toggle-btn {
|
||
position: relative;
|
||
}
|
||
.tab-timeline[data-tip]::after,
|
||
.push-toggle-btn[data-tip]::after {
|
||
content: attr(data-tip);
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
left: 0;
|
||
white-space: nowrap;
|
||
background: var(--surface-3);
|
||
color: var(--text);
|
||
border: 1px solid var(--border-strong);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.12s ease;
|
||
z-index: 60;
|
||
}
|
||
.tab-timeline[data-tip]:hover::after,
|
||
.push-toggle-btn[data-tip]:hover::after {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* ── Saved layout presets dropdown (v3) ──────────────────────────────── */
|
||
.grid-presets {
|
||
position: relative;
|
||
display: inline-flex;
|
||
margin-left: 2px;
|
||
}
|
||
.grid-presets-btn {
|
||
border: 1px solid var(--border);
|
||
background: var(--surface-2);
|
||
color: var(--text-dim);
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
.grid-presets-btn:hover {
|
||
color: var(--text);
|
||
background: var(--surface-3);
|
||
}
|
||
.grid-presets-menu {
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
right: 0;
|
||
min-width: 220px;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 10px;
|
||
box-shadow: var(--shadow);
|
||
padding: 6px;
|
||
z-index: 40;
|
||
}
|
||
.grid-presets-menu[hidden] {
|
||
display: none;
|
||
}
|
||
.gp-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
max-height: 240px;
|
||
overflow-y: auto;
|
||
}
|
||
.gp-empty {
|
||
color: var(--text-faint);
|
||
font-size: 12px;
|
||
padding: 8px;
|
||
text-align: center;
|
||
}
|
||
.gp-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.gp-apply {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text);
|
||
padding: 6px 8px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12.5px;
|
||
text-align: left;
|
||
}
|
||
.gp-apply:hover {
|
||
background: var(--surface-2);
|
||
}
|
||
.gp-apply-layout {
|
||
color: var(--text-faint);
|
||
font-size: 11px;
|
||
}
|
||
.gp-del {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-faint);
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
line-height: 1;
|
||
}
|
||
.gp-del:hover {
|
||
color: var(--red);
|
||
background: var(--surface-2);
|
||
}
|
||
.gp-save {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 6px;
|
||
}
|
||
.gp-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
color: var(--text);
|
||
padding: 5px 8px;
|
||
font-size: 12px;
|
||
font-family: var(--ui-font);
|
||
}
|
||
.gp-name:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
}
|
||
.gp-save-btn {
|
||
border: 0;
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
border-radius: 6px;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.gp-save-btn:hover {
|
||
filter: brightness(1.05);
|
||
}
|
||
|
||
/* ── W4 git write: per-file Stage/Unstage toggle + commit/push bar ─────────── */
|
||
.df-file-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.df-file-stage {
|
||
margin-left: auto;
|
||
flex: 0 0 auto;
|
||
padding: 3px 10px;
|
||
font-size: 12px;
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 6px;
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
}
|
||
.df-file-stage:hover {
|
||
background: var(--surface-3);
|
||
border-color: var(--accent);
|
||
}
|
||
.df-commitbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
padding: 10px;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--surface-1);
|
||
}
|
||
.df-commit-msg {
|
||
flex: 1 1 220px;
|
||
min-width: 0;
|
||
resize: vertical;
|
||
padding: 8px;
|
||
font: inherit;
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 6px;
|
||
}
|
||
.df-commit-msg:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||
}
|
||
.df-commit-btn,
|
||
.df-push-btn {
|
||
flex: 0 0 auto;
|
||
padding: 8px 16px;
|
||
font: inherit;
|
||
font-weight: 600;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--accent);
|
||
background: var(--accent);
|
||
color: var(--on-accent);
|
||
cursor: pointer;
|
||
}
|
||
.df-push-btn {
|
||
background: var(--surface-2);
|
||
color: var(--text);
|
||
border-color: var(--border-strong);
|
||
}
|
||
.df-commit-btn:disabled,
|
||
.df-push-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
.df-op-status {
|
||
flex: 1 1 100%;
|
||
font-size: 13px;
|
||
word-break: break-word;
|
||
}
|
||
.df-op-error {
|
||
color: var(--red);
|
||
}
|
||
.df-op-notice {
|
||
color: var(--text-dim);
|
||
}
|
||
.df-op-busy {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* ── B1 diff viewer: shell, file cards, diff body, states ─────────────────────
|
||
These classes shipped with names but no rules, so the whole viewer rendered as
|
||
user-agent defaults — white OS buttons and a grey native dropdown on a dark warm
|
||
panel — and the diff body had no +/- signal of any kind. That last part is a
|
||
correctness bug, not a polish one: src/http/diff.ts:143-148 does `line.slice(1)`,
|
||
so the marker character never reaches the DOM and the sign existed only as a
|
||
class name that nothing drew.
|
||
|
||
Sits beside the W4 block above so all diff styling lives together. The two button
|
||
roles and the <select>'s skin/caret are joined to their existing blocks further up
|
||
(the "Role 2 of 2" ghost list, and .proj-fanout-mode) rather than restated here,
|
||
so no property is declared twice for the same selector.
|
||
|
||
Four surfaces, one hairline each: --sunk code well < --bg viewer body <
|
||
--surface-1 file card < --surface-2 (the W4 Stage button, which needs something
|
||
to sit on top of). Colour is the panel's warm two-tone and nothing else —
|
||
--dirty for insertions (the token already means "uncommitted work", which is what
|
||
a + in a working diff is) and --warn for deletions. No green: :root L23-29
|
||
reserves --ok for the one sync state. No --accent on a state, for the same reason.
|
||
|
||
Source order is load-bearing: .df-line and .df-status each sit on the same element
|
||
as their modifiers at identical specificity, so every base is declared before its
|
||
variants and the media query comes last. */
|
||
|
||
/* One hairline box at --bg on the panel's own --bg — almost invisible on purpose;
|
||
the file cards inside it are what you see. overflow:hidden is load-bearing: the
|
||
toolbar and the W4 commit bar both carry their own fill and are the first and
|
||
last children (diff.ts:407/437), so without it they square off the rounded
|
||
corners.
|
||
|
||
font-size is the root of the panel's type scale, and it has to be stated: no
|
||
ancestor sets one (html/body set only a family), so without this the viewer
|
||
inherits the UA's 16px and the three W4 controls that say `font: inherit`
|
||
(.df-commit-msg, .df-commit-btn, .df-push-btn) render as the loudest type in a
|
||
panel whose rail is 12.5px. Every other descendant declares its own size, so
|
||
13px moves those three and nothing else. */
|
||
.df-viewer {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
font-size: 13px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Four controls, one of them a <select> sized by the longest branch name, in a phone's
|
||
width. Wrapping is the only honest answer.
|
||
The rail is view-controls-left, dismiss-right: ✕ Close takes `margin-left: auto`,
|
||
the same `1fr auto` idiom .proj-wt-row spells with a grid. Packing all four left
|
||
instead left a long dead rail after ✕ Close on a wide panel, and on a phone put the
|
||
two tabs alone on line 1 with the rest of that line empty — a toolbar that stops
|
||
partway across is not a group, it is an unfinished row.
|
||
The slack goes to the auto margin and not to .df-base-select (whose 200px cap
|
||
stands) for two reasons: the picker is optional — it only exists when opts.bases is
|
||
non-empty (diff.ts:384-403), so widening it fixes one of the two toolbars the viewer
|
||
can build — and a branch name stretched across half the panel is not more readable
|
||
than one at 200px. The phone half of the void is fixed in the ≤410px block at the
|
||
bottom of the file, where the two tabs split line 1 (`flex: 1 1 0`) rather than
|
||
growing this rule. */
|
||
.df-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 9px 10px;
|
||
background: var(--surface-1);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.df-tab {
|
||
flex: none;
|
||
}
|
||
|
||
/* Selected view, in the panel's own "you are here" language (.proj-wt-tag-current),
|
||
not a filled button: switching views commits nothing, and a gold fill here would
|
||
outrank the Commit button forty pixels below, which is the one Role 1 action in
|
||
this viewer. */
|
||
.df-tab-active {
|
||
background: var(--accent-soft);
|
||
border-color: color-mix(in srgb, var(--accent) 36%, transparent);
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Both tabs go inert when a base revision is picked — a base diff cannot be staged.
|
||
Matched on :disabled as well, because the click handlers reassign `className`
|
||
wholesale (diff.ts:531/539/575/583) and can drop the modifier while the native
|
||
attribute survives. Must stay after .df-tab-active: same specificity, later wins.
|
||
|
||
That source order is doing real work here, not just guarding a hover: updateTabState
|
||
(diff.ts:411-418) only ADDS df-tab-disabled — it never removes df-tab-active — so
|
||
without these three resets the gold "Working" pill survives at half opacity while a
|
||
base diff is on screen, which reads as "selected, and you may click me". Nothing in
|
||
this rule needs a new selector; it just has to undo all three accent properties.
|
||
|
||
ONE dimming mechanism, not two. --text-faint AND opacity:.45 stacked to 1.64:1 over
|
||
the toolbar's --surface-1 and took the border with them: on screen both tabs were
|
||
blank rectangles, so you could not see what you would return to. Disabled has to
|
||
read as a label you cannot press, not as an erasure. --text-dim at .55 composites to
|
||
≈#67635d, 3.03:1 — quiet, still a word. The `color` line cannot simply be deleted to
|
||
get the same effect: .df-tab-active above sets color:var(--accent) at equal
|
||
specificity and would win, restoring the gold pill this rule exists to kill.
|
||
|
||
The border is RAISED a tier here, not lowered, and that is not a contradiction:
|
||
opacity multiplies the border along with the label, so this rule has to pre-pay for
|
||
the dimming the way the label does not. Stepping it down to --border (.07) as well
|
||
left .0385 effective — 1.10:1 on --surface-1, against 1.51:1 for every enabled ghost
|
||
button in the same rail — and the tabs read as words floating in nothing.
|
||
--border-strong (.14) × .55 lands at 1.23:1, the same weight as an ordinary --border
|
||
hairline (1.20:1) drawn at full opacity. Still a full reset of all three accent
|
||
properties, which is what the paragraph above requires. */
|
||
.df-tab-disabled,
|
||
.df-tab:disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
background: transparent;
|
||
border-color: var(--border-strong);
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* Skin and caret come from the .proj-fanout-mode rule this selector was added to;
|
||
only what is genuinely different lives here. Padding is longhand so that rule's
|
||
`padding-right: 30px` (which the caret offsets depend on) is not overwritten.
|
||
Sized to the ghost buttons beside it — 12.5px, 7px vertical — rather than to the
|
||
13px/8px form-field skin: a 1px height mismatch is invisible in a form and obvious
|
||
in a four-control rail. A <select>'s intrinsic width is its longest option and the
|
||
options are real branch names, so min-width:0 is what stops one 35-character
|
||
branch from claiming the whole toolbar. */
|
||
.df-base-select {
|
||
flex: 1 1 150px;
|
||
min-width: 0;
|
||
max-width: 200px;
|
||
box-sizing: border-box;
|
||
font: inherit;
|
||
font-family: var(--mono-font);
|
||
font-size: 12.5px;
|
||
color: var(--text-dim);
|
||
background-color: var(--surface-2);
|
||
border: 1px solid var(--border-strong);
|
||
border-radius: 7px;
|
||
padding-top: 7px;
|
||
padding-bottom: 7px;
|
||
padding-left: 10px;
|
||
text-overflow: ellipsis;
|
||
outline: none;
|
||
}
|
||
|
||
/* Chrome and Firefox paint the open dropdown from the control's own colours; macOS
|
||
draws it natively and ignores this. */
|
||
.df-base-select option {
|
||
color: var(--text);
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
.df-close {
|
||
flex: none;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* The "Loading…" state is a bare text node with no element and no class
|
||
(diff.ts:552), so this rule is the only thing that can style it — hence the size
|
||
and colour live on the container and every descendant declares its own.
|
||
text-align and min-height are here for the same reason: three states share this
|
||
slot and only two of them are elements. Left-aligned 40px "Loading…" between a
|
||
centred 100px .df-empty and a centred 100px .df-error meant every refetch — tab
|
||
switch, stage toggle, commit — collapsed the viewer and re-expanded it. Centred
|
||
with a floor, all three land in the same box. .df-result opts back out below. */
|
||
.df-content {
|
||
padding: 10px;
|
||
min-height: 92px;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* A gap, not margins on .df-file: renderDiffFile is also called standalone into
|
||
.approval-preview on the terminal page (tabs.ts:499), where a card margin would
|
||
strand a single file inside an already-padded well. text-align cancels the
|
||
centring .df-content applies to the three text states — a real diff is prose in
|
||
the code sense and reads from the left margin. */
|
||
.df-result {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
/* Correctness is global, chrome is scoped. The same card renders into
|
||
.approval-preview, which already has its own border, radius and a 30vh budget —
|
||
a second bordered card in there would read as a box in a box and eat the height.
|
||
So the typography, the gutter and the chips below apply everywhere (that surface
|
||
needs those fixes just as much) while the card's own border/fill only applies
|
||
inside the viewer. */
|
||
.df-file {
|
||
min-width: 0;
|
||
}
|
||
|
||
.df-viewer .df-file {
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* .df-file-header's flex/gap belong to the W4 rule above; these are the properties
|
||
it never had, declared on a child selector so that block stays byte-for-byte
|
||
untouched. Wrapping is not optional: at 344px a renamed file's "old → new" path
|
||
plus two counts, a status chip and a Stage button cannot share one line.
|
||
Scoped to .df-viewer for the same reason the card chrome is: this is the header's
|
||
own box, and .approval-preview (tabs.ts:492-501, which calls renderDiffFile) already
|
||
declares `padding: 8px 10px` of its own — unscoped, the header would nest a second
|
||
one inside it. The line-kind rules further down (.df-line/.df-added/.df-removed/…)
|
||
stay global on purpose: the approval preview should render a diff the same way, and
|
||
the sign gutter and the tints are the part it most needs.
|
||
|
||
Smaller unscoped padding, not none. Scoping removed the DOUBLE inset; it should not
|
||
have removed the inset. With only W4's `display:flex` left, the path in the approval
|
||
preview sat flush against the preview's edge and flush against .df-hunk-block's
|
||
border-top while every sibling kept its own (.df-hunk-header 5px 12px, .df-line
|
||
0 12px 0 6px, .df-binary 9px 12px — all still global). 6px 12px 6px 6px is a header
|
||
inset that adds to the preview's 8px 10px instead of doubling it; the scoped rule
|
||
below is (0,3,0) against this (0,2,0), so nothing inside the viewer changes.
|
||
|
||
A <button> does not inherit font-family, and the W4 .df-file-stage rule sets a size
|
||
but no family, so Stage was the one element in the panel rendering in the UA's Arial
|
||
beside three mono chips and a --ui-font path. One scoped line fixes it without
|
||
touching W4. */
|
||
.df-file > .df-file-header {
|
||
padding: 6px 12px 6px 6px;
|
||
}
|
||
|
||
.df-viewer .df-file > .df-file-header {
|
||
flex-wrap: wrap;
|
||
min-width: 0;
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
.df-viewer .df-file-stage {
|
||
font-family: inherit;
|
||
}
|
||
|
||
/* A path is an identifier, so it reads as one, and it is the header's elastic
|
||
column. End-ellipsis rather than a second truncation idiom — .proj-wt-path makes
|
||
the same call for the same reason. (The phone gets a full row of its own; see the
|
||
media query.)
|
||
`flex: 1 1 0` and not `1 1 auto`: with an `auto` basis the path's hypothetical
|
||
size is its max-content width, so in a wrapping header the browser breaks the line
|
||
before it ever shrinks the item — the ellipsis never fired, and a long path evicted
|
||
the status chip and Stage onto a second row instead (61px header vs 38px). A zero
|
||
basis makes shrinking the first resort, which is what makes the truncation real. */
|
||
.df-path {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
font-family: var(--mono-font);
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Counts are data: mono and tabular, so +9/-9 and +10/-10 do not jitter the chip
|
||
beside them as you scan down a list. Both are always rendered, even as "+0 -0"
|
||
(diff.ts:243-245), which is why neither shouts. */
|
||
.df-stats {
|
||
display: inline-flex;
|
||
flex: none;
|
||
gap: 8px;
|
||
font-family: var(--mono-font);
|
||
font-size: 11.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.df-stat-added {
|
||
color: var(--dirty);
|
||
}
|
||
|
||
.df-stat-removed {
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* One chip language across the panel — the same recipe as .proj-wt-tag, so a status
|
||
here and a tag on a worktree row read as the same kind of object. Colour is left
|
||
to the variants: all six are covered below, so declaring one here would be dead. */
|
||
.df-status {
|
||
flex: none;
|
||
font-family: var(--mono-font);
|
||
font-size: 11px;
|
||
letter-spacing: 0.04em;
|
||
padding: 2px 8px;
|
||
border-radius: 6px;
|
||
background: var(--sunk);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* Six statuses, three tiers — one colour each would be confetti on a 20-file list.
|
||
The ordinary ones are the quiet baseline that makes the other two legible.
|
||
--text-dim, not --text-faint: this chip is the ONLY place a file's status appears,
|
||
and --text-faint at 11px is 3.67:1 on --sunk. --text-dim is 7.76:1 and still reads
|
||
as the tier below --text, so the baseline stays quiet without going unreadable. */
|
||
.df-status-modified,
|
||
.df-status-renamed,
|
||
.df-status-binary {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.df-status-added {
|
||
color: var(--dirty);
|
||
}
|
||
|
||
/* New AND not yet known to git. The dashed hairline is a deliberate new idiom — the
|
||
panel's chips are otherwise all solid — because it is the only way to separate
|
||
untracked from added without spending a fifth colour on a six-value enum. */
|
||
.df-status-untracked {
|
||
color: var(--dirty);
|
||
border-style: dashed;
|
||
}
|
||
|
||
.df-status-deleted {
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* Stands in for the hunks that will never come, so the card keeps its shape. */
|
||
.df-binary {
|
||
padding: 9px 12px;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--sunk);
|
||
font-family: var(--mono-font);
|
||
font-size: 12px;
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
/* Code drops a level below the card, the way the design sinks read-only data below
|
||
the page. The border-top doubles as the divider between hunks, which is why the
|
||
header carries none. */
|
||
.df-hunk-block {
|
||
border-top: 1px solid var(--border);
|
||
background: var(--sunk);
|
||
}
|
||
|
||
/* "@@ -1,3 +1,4 @@" is a coordinate label, not content: quiet enough to scan past,
|
||
present enough to find — but findable means legible, so --text-dim (7.12:1 on
|
||
--surface-1) rather than --text-faint, which at 11px was 3.36:1. The rule the viewer
|
||
follows: --text-faint is kept where the mark is decoration rather than a statement,
|
||
and never where it is the only thing saying something.
|
||
One line — a wrapped @@ header is not more informative. */
|
||
.df-hunk-header {
|
||
padding: 5px 12px;
|
||
background: var(--surface-1);
|
||
border-bottom: 1px solid var(--border);
|
||
font-family: var(--mono-font);
|
||
font-size: 11px;
|
||
letter-spacing: 0.02em;
|
||
color: var(--text-dim);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* THE containment point. This is the one box whose content is unboundedly wide, so
|
||
this is where the scroll lives: a 300-column line scrolls inside its own hunk and
|
||
the page never moves. Without it .proj-panel scrolls sideways — it declares
|
||
overflow-y only, and per spec a visible axis paired with a non-visible one
|
||
computes to auto. `* { scrollbar-width: thin }` already themes the bar.
|
||
Per-hunk and NOT hoisted to .df-viewer .df-file, though that would give one
|
||
scrollbar per file instead of one per hunk: .df-file-header is sized to the
|
||
container, not to the scroll width, so a file-level scroller drags the file path
|
||
out of view the moment you scroll right and you lose the label for the code you
|
||
are reading. Deliberate — do not "fix" it. */
|
||
.df-lines {
|
||
overflow-x: auto;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
/* white-space:pre and the mono face are correctness, not polish: with the marker
|
||
stripped server-side the leading whitespace IS the indentation and nothing else
|
||
encodes it. width:max-content + min-width:100% is what makes a row's tint span the
|
||
whole line rather than stopping at the visible edge of the scroller. */
|
||
.df-line {
|
||
display: block;
|
||
width: max-content;
|
||
min-width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 0 12px 0 6px;
|
||
font-family: var(--mono-font);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
white-space: pre;
|
||
tab-size: 4;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* The sign the server threw away, put back as a real character in a two-column mono
|
||
gutter (1ch glyph + 1ch space, so the code below stays column-aligned). Colour
|
||
must not be the only encoding of added-vs-removed, and this is the only way to
|
||
restore the shape without touching diff.ts. user-select:none keeps a copied diff
|
||
clean, sign-free code — Firefox does put ::before content on the clipboard.
|
||
|
||
Assumed: every face in --mono-font has a weight-invariant advance. `ch` resolves
|
||
against the pseudo-element's OWN font, and .df-added/.df-removed make it bold while
|
||
context lines stay regular, so a mono face whose bold digit-zero is wider would shift
|
||
changed lines horizontally against unchanged ones and break the column alignment this
|
||
gutter exists to keep. The named faces in the stack (ui-monospace, SFMono-Regular,
|
||
Menlo, Consolas) hold the advance across weights; the last entry, the generic
|
||
`monospace`, resolves to whatever the platform picks and is not required to. Keep
|
||
the units in ch regardless — that is what keeps the gutter on the mono grid; if a
|
||
fallback face ever does drift, change the FONT, not the unit. */
|
||
.df-line::before {
|
||
content: ' ';
|
||
display: inline-block;
|
||
width: 1ch;
|
||
margin-right: 1ch;
|
||
text-align: center;
|
||
color: var(--text-faint);
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
|
||
/* Kind modifiers — after .df-line, same specificity, later wins.
|
||
The row wash follows the house "this row is in the marked set" idiom from
|
||
.proj-commit-unpushed (a low-alpha tint plus a bar), flat rather than that rule's
|
||
90deg fade because a diff line is arbitrarily long and a fade to transparent at
|
||
55% would drop the tint off the end of exactly the lines you most need marked.
|
||
The bar is box-shadow so ::before stays free for the sign.
|
||
|
||
The two washes are deliberately NOT the same hue at two alphas: --dirty and --warn
|
||
are both warm oranges, and at wash strength over --sunk they compute within four
|
||
points of each other — indistinguishable, so the tint would be doing no work.
|
||
Insertions take a neutral white lift instead (the same rgba(255,255,255,…) idiom
|
||
as --border), deletions take the warm --warn wash. Neutral-lift vs warm-wash reads
|
||
at a glance, and it spends no new hue — green is not available here, per :root
|
||
L23-29. Full-strength colour stays on the bar and the +/- glyph.
|
||
|
||
The insertion lift is .085 and not .045: at .045 it composited to #171614, ΔL* 4.3
|
||
against --sunk (1.09:1) — invisible — while the deletion wash lands at ΔL* 13.2 plus
|
||
a hue shift. Deletions read instantly and insertions read as context, which is
|
||
backwards for the half you are about to commit. At .085 the lift is ΔL* 9.1, still
|
||
the quieter of the two, which is the intended order.
|
||
The deletion wash is 24% and not 16%. At 16% the two composited to within 1.0 L* of
|
||
each other, so added-vs-removed rested almost entirely on a low-chroma hue shift —
|
||
and the two full-strength cues both live in the first 6px of the line, so scrolling a
|
||
long line right inside .df-lines carries the inset bar and the ::before glyph out of
|
||
the scrollport and leaves the wash alone to do the job. 24% separates in the browser
|
||
and is still warm rather than loud. The insertion lift stays at .085: neutral vs warm
|
||
is the axis, and lifting both would just re-close the gap.
|
||
The bar is 3px and the glyphs are bold for the same reason from the other end: on a
|
||
greyscale or dimmed phone screen the wash is all but gone, so the sign and the rule
|
||
down the side have to carry the distinction by shape alone. */
|
||
.df-added {
|
||
background: rgba(255, 255, 255, 0.085);
|
||
box-shadow: inset 3px 0 0 var(--dirty);
|
||
}
|
||
|
||
.df-added::before {
|
||
content: '+';
|
||
font-weight: 700;
|
||
color: var(--dirty);
|
||
}
|
||
|
||
/* One declaration, one token, and the token derives from --warn in :root — so a
|
||
re-skin of --warn moves the wash together with the bar and the glyph below instead
|
||
of leaving the row two hues. The colour lives in :root because that is where
|
||
THEME.md expects every colour to live (§接入约定: 不要写死颜色). */
|
||
.df-removed {
|
||
background: var(--warn-wash);
|
||
box-shadow: inset 3px 0 0 var(--warn);
|
||
}
|
||
|
||
.df-removed::before {
|
||
content: '-';
|
||
font-weight: 700;
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* Most of a diff is context. Stepping it down to --text-dim is what makes the
|
||
handful of tinted lines legible at a glance — the changed lines keep the base
|
||
--text and are the only full-strength text in the well. */
|
||
.df-context {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* "\ No newline at end of file" — informational, rare, and the server strips the
|
||
leading backslash too (src/http/diff.ts:148 does .slice(1).trim()), so the glyph
|
||
is restored here for the same reason as +/-.
|
||
--text-dim, not --text-faint, for the .tl-empty / .df-status reason: --text-faint at
|
||
12px is 3.66:1 on --sunk and this line is the ONLY place the missing newline is ever
|
||
stated — unlike .df-binary, whose fact the `binary` status chip repeats in --text-dim
|
||
right above it. That puts it level with .df-context rather than below it, and no rank
|
||
is drawn: the italic only marks it as not-code. Deliberate — the fact is stated
|
||
once, so legibility outranks hierarchy here. */
|
||
.df-meta {
|
||
color: var(--text-dim);
|
||
font-style: italic;
|
||
}
|
||
|
||
.df-meta::before {
|
||
content: '\\';
|
||
}
|
||
|
||
/* Line kind 'hunk'. The current server never emits it (the @@ line is routed to
|
||
DiffHunk.header), so this is reachable only through a hand-crafted API response —
|
||
styled defensively so it cannot render raw, with no design spent on it. Note this
|
||
is the LINE class: .df-hunk-header and .df-hunk-block above are different elements
|
||
that merely share a prefix. */
|
||
.df-hunk {
|
||
background: var(--surface-1);
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
.df-hunk::before {
|
||
content: '@';
|
||
}
|
||
|
||
/* .df-empty and .df-truncated-warning can both be on screen at once (a truncated
|
||
result with zero files), so neither may assume it is alone.
|
||
Own text-align because .df-result (its parent) sets left.
|
||
|
||
A bare centred sentence in a 90px void, geometrically identical to .df-error and
|
||
differing only in colour, sitting directly above a full commit form, reads as
|
||
"something failed to load" rather than "clean working tree". A mark and a tighter
|
||
void is the whole fix — it stays CSS-only, so diff.ts keeps its zero-innerHTML
|
||
guarantee. An em-dash and NOT a check mark: --ok and ✓ are spoken for by the sync
|
||
band's verified-in-sync state (:root L23-29), and "nothing to commit" is not that
|
||
claim. The dash says "nothing here", which is exactly what is true. */
|
||
.df-empty {
|
||
padding: 20px 0;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.df-empty::before {
|
||
content: '—';
|
||
display: block;
|
||
font-family: var(--mono-font);
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
color: var(--text-faint);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* Cautionary, not fatal — you are looking at part of the truth, not a failure. A
|
||
--warn rule down the side rather than a --warn sentence, which is what keeps it
|
||
distinct from .df-error. */
|
||
.df-truncated-warning {
|
||
padding: 7px 10px;
|
||
background: var(--sunk);
|
||
border: 1px solid var(--border);
|
||
border-left: 2px solid var(--warn);
|
||
border-radius: 7px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* Replaces the whole content area (diff.ts:559-560), so it is a full-viewer state
|
||
and is drawn in the same register as .df-empty, which occupies the same slot.
|
||
--warn is the panel's failure colour (.proj-wt-error, .proj-fanout-error); --red
|
||
stays reserved for connection state and for .df-op-error in the bar below, and two
|
||
different reds would claim the two failures are the same kind. */
|
||
.df-error {
|
||
padding: 24px 0;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: var(--warn);
|
||
}
|
||
|
||
/* Must sit AFTER the rules above — a media query adds no specificity, so an earlier
|
||
block at equal specificity simply loses. The viewer's edge-to-edge bleed at this
|
||
width lives with .proj-diff-panel, beside the panel padding it cancels. */
|
||
@media (max-width: 720px) {
|
||
/* A renamed path is two paths. Give it the whole row, so the counts, the status
|
||
chip and Stage keep a row of their own instead of the path squeezing to ~200px
|
||
and stranding Stage on a line by itself. The row is all it takes: the desktop
|
||
nowrap + end-ellipsis is inherited as-is now that `flex: 1 1 0` makes it fire.
|
||
It used to also switch to overflow-wrap:anywhere, which broke ".../module.ts →"
|
||
mid-word at 380px and orphaned the extension you scan for — and made this the
|
||
page's THIRD truncation idiom for paths. One idiom now. */
|
||
.df-path {
|
||
flex: 1 1 100%;
|
||
}
|
||
|
||
/* The basis is what decides where the rail breaks: below it the picker drops to a
|
||
line of its own and the tabs hold line 1 alone (which is the void the ≤410 block
|
||
at the bottom of the file fills), above it the picker joins them. That seam is not
|
||
a number worth writing down — it moves with the UI font and with whether the
|
||
panel's scrollbar takes width — so resize the panel and watch it rather than trust
|
||
a figure here.
|
||
The cap is raised from the desktop 200px rather than removed: with `max-width:
|
||
none` the picker grew to most of the rail at tablet widths, which is exactly what
|
||
the .df-toolbar comment rules out at desktop. */
|
||
.df-base-select {
|
||
flex: 1 1 210px;
|
||
max-width: 280px;
|
||
}
|
||
|
||
/* The message takes its own row so Commit and Push share the next one. At 390px the
|
||
220px basis plus Commit (81) plus Push (63) plus gaps overflowed the 360px content
|
||
box and broke the line before Push, orphaning it. Scoped to .df-viewer so the W4
|
||
rule stays byte-for-byte untouched — the same technique as .df-viewer
|
||
.df-file-stage. */
|
||
.df-viewer .df-commit-msg {
|
||
flex: 1 1 100%;
|
||
}
|
||
|
||
.df-content {
|
||
padding: 8px 6px;
|
||
}
|
||
|
||
/* Half a point of density. The gutter is in ch, so it tracks the smaller type and
|
||
the sign stays exactly two characters wide. */
|
||
.df-line {
|
||
font-size: 11.5px;
|
||
line-height: 1.45;
|
||
}
|
||
}
|
||
|
||
/* The stretch is scoped to the one band where the tabs are alone on their line, and
|
||
nowhere wider. Alone, they are two small pills with the rest of the line dead beside
|
||
them; stretched, that dead rail becomes tap target.
|
||
|
||
It must NOT extend to the whole ≤720px band, which is where it used to live. Once
|
||
the picker joins the tabs on line 1, a zero basis makes them absorb every pixel the
|
||
picker's cap frees — and those pixels are the auto margin that pins ✕ Close to the
|
||
right edge, so the dismiss control drifted back into the group.
|
||
|
||
410px and not 720px because the seam is the width at which the picker's basis can
|
||
first share the tab line, and that width is not one number: it moves with the UI
|
||
font, and the panel is narrower by a classic scrollbar on desktop than by an overlay
|
||
one on a phone. 410 covers the wider of the two models. Erring that way leaves the
|
||
rule live but idle across the few pixels where the phone model has already put the
|
||
picker on the tab line — which costs nothing — while erring the other way would
|
||
leave the void unfixed on the devices that have it. Not pinned tighter than the
|
||
nearest ten, for the same reason.
|
||
|
||
`flex: 1 1 0` and not `1 1 auto`: a zero basis still has min-width:auto, so each
|
||
tab's hypothetical size stays at its min-content word width and the line-breaking
|
||
above is computed from the same numbers either way — the wrap survives, only the
|
||
distribution of the leftover changes. */
|
||
@media (max-width: 410px) {
|
||
.df-tab {
|
||
flex: 1 1 0;
|
||
}
|
||
}
|