feat(home): dock the desktop tab list as a left rail with age stamps

The open-tabs list on the welcome screen was a fixed 256px card floating
vertically centered in the left gutter, which read as debris rather than
chrome and left 12px type stranded on a wide display.

- Dock it: left/top/bottom 0, full height, hairline right border and a soft
  background fade. The centered welcome content still does not move.
- Scale it off one knob: width clamp(250px, 19vw, 430px) plus a fluid
  font-size on .home-sessions, every child sized in em. Measured 250px/12.2px
  at the 1180px gate, 380px/15px at 2000px, 430px/17px at 2938px; the gap to
  the centered content never goes negative.
- Show when each session was first created and last active, on a full-width
  footer line so it does not fight the status pill, exact dates in the title.
  Both stamps refresh in place on a 20s clock (disarmed when the home screen
  goes away) rather than by re-rendering, which would restart every row's
  blink animation and working ring twice a minute.
- Mute idle green: dot and pill mix toward --text-muted, so idle reads as
  greyed-out next to the vivid green of a working session. Mixed rather than
  hardcoded, so every skin keeps its own green.

Verified in a browser at 1180/2000/2938px and on a light skin, plus
test/home-sessions.test.ts, frontend-syntax, public-assets, prettier and a
PostCSS parse of styles.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-10 02:56:35 +02:00
parent c942bb5dfb
commit c891a8045d
4 changed files with 240 additions and 51 deletions
+118 -40
View File
@@ -14014,15 +14014,24 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
}
/* ══════════════════════════════════════════════════════════════════════════
Home screen: open tabs in the left gutter (home-sessions.js)
Home screen: open tabs docked down the left edge (home-sessions.js)
The welcome content is 560px wide and centered, so this column lives in dead
The welcome content is 560px wide and centered, so this rail lives in dead
space. It is `position: absolute` precisely so that stays true: the centered
content does not move by a pixel whether the column renders or not. That in
content does not move by a pixel whether the rail renders or not. That in
turn is why the width gate below has to exist — in a narrow window there is
no gutter to sit in, and an absolute box would simply overlap the search
panel. JS gates on the same 1180px so the two can never disagree.
It is FLUSH to the left edge and FULL HEIGHT on purpose: a floating, vertically
centered card in the middle of the gutter reads as debris, a docked rail with
an edge to hang off reads as part of the chrome.
Everything inside scales off ONE knob — the `font-size` below — because every
child sizes in `em`. Widen the window and the whole rail grows with it instead
of leaving 12px type stranded on a 2560px display. Do not reintroduce `rem`
or fixed px sizes for type/spacing here; that is what breaks the scaling.
The working dot is the phone's, exactly: pulsing green ringed by the very
same `tab-load-spin` a tab shows while it loads (reused from above, never
re-declared), plus a green halo. One signal, one motion, both home screens.
@@ -14030,14 +14039,20 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
left: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
gap: 8px;
width: 256px;
max-height: calc(100% - 3rem);
gap: 0.9em;
/* Ramps with the viewport, floored so it still clears the centered content at
the 1180px gate and capped so it never becomes the main event. */
width: clamp(250px, 19vw, 430px);
padding: 1.5em 1.1em;
background: linear-gradient(90deg, color-mix(in srgb, var(--bg-card) 65%, transparent) 0%, transparent 100%);
border-right: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
/* The single scale knob: ~12px at the gate, ~17px on a 2560px display. */
font-size: clamp(12.2px, calc(8px + 0.35vw), 17px);
text-align: left;
z-index: 1;
}
@@ -14059,12 +14074,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-header {
display: flex;
align-items: center;
gap: 8px;
padding: 0 6px;
gap: 0.66em;
flex-shrink: 0;
padding: 0 0.5em;
}
.home-sessions-title {
font-size: 0.66rem;
font-size: 0.87em;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
@@ -14075,25 +14091,29 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
min-width: 1.5em;
height: 1.35em;
padding: 0 0.42em;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.6rem;
font-size: 0.79em;
font-weight: 700;
font-family: monospace;
}
/* `min-height: 0` is what lets this scroll inside the full-height rail: without
it the flex item's auto minimum keeps growing and the overflow never engages. */
.home-sessions-list {
display: flex;
flex-direction: column;
gap: 4px;
gap: 0.35em;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 2px 6px;
padding: 0.16em 0.16em 0.5em;
}
.home-sessions-list::-webkit-scrollbar {
@@ -14108,15 +14128,16 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
gap: 0.66em;
width: 100%;
padding: 7px 9px;
border-radius: 9px;
padding: 0.6em 0.75em;
border-radius: 0.75em;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-dim);
font-family: inherit;
font-size: 0.76rem;
font-size: 1em;
text-align: left;
cursor: pointer;
transition: background var(--transition-smooth), border-color var(--transition-smooth), color var(--transition-smooth);
@@ -14136,14 +14157,14 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
display: inline-flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
width: 1.3em;
height: 1.3em;
flex-shrink: 0;
border-radius: 3px;
border-radius: 0.25em;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-size: 0.76em;
font-weight: 700;
font-family: monospace;
}
@@ -14151,8 +14172,8 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-dot {
position: relative;
flex-shrink: 0;
width: 9px;
height: 9px;
width: 0.75em;
height: 0.75em;
border-radius: 50%;
background: var(--text-muted);
}
@@ -14166,8 +14187,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
background: var(--yellow);
}
/* Idle is green, but a MUTED green: still unmistakably "fine", visibly greyer
and darker than the vivid green a working session gets, so a glance down the
rail separates "running right now" from "sitting there" without reading a
word. Mixed toward --text-muted rather than hardcoded, so every skin keeps
its own green. */
.home-sessions-dot--idle {
background: var(--green);
background: color-mix(in srgb, var(--green) 42%, var(--text-muted));
}
.home-sessions-dot--done {
@@ -14189,7 +14215,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-dot--working::after {
content: '';
position: absolute;
inset: -4px;
inset: -0.34em;
border: 2px solid color-mix(in srgb, var(--green) 25%, transparent);
border-top-color: var(--green);
border-radius: 50%;
@@ -14199,7 +14225,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row-body {
display: flex;
flex-direction: column;
gap: 1px;
gap: 0.12em;
min-width: 0;
flex: 1;
}
@@ -14207,7 +14233,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row-title {
display: flex;
align-items: center;
gap: 5px;
gap: 0.42em;
min-width: 0;
color: var(--text);
font-weight: 600;
@@ -14220,21 +14246,66 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
}
.home-sessions-row-sub {
font-size: 0.66rem;
font-size: 0.87em;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Age line: when the session was first created and when it last did anything.
Both are relative and refreshed in place on a timer (home-sessions.js), never
by a re-render — a re-render would restart the row's blink animation. */
.home-sessions-row-meta {
display: flex;
align-items: center;
gap: 0.4em;
/* Wraps to its own full-width line under the row (the row is `flex-wrap`),
so the stamps get the whole width instead of the sliver left beside the
status pill and ellipsize away on the narrow end of the rail. */
flex: 0 0 100%;
min-width: 0;
margin-top: -0.2em;
font-size: 0.78em;
font-family: monospace;
color: var(--text-muted);
opacity: 0.75;
white-space: nowrap;
overflow: hidden;
}
.home-sessions-meta-item {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.home-sessions-meta-key {
margin-right: 0.35em;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.home-sessions-meta-sep {
opacity: 0.45;
}
/* The freshest signal on the row: while a session is actually doing something,
its "active" stamp is the one the eye should land on. */
.home-sessions-row--working .home-sessions-meta-active {
color: var(--green);
opacity: 0.95;
}
.home-sessions-mode {
flex-shrink: 0;
padding: 0 4px;
border-radius: 3px;
padding: 0 0.35em;
border-radius: 0.25em;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.55rem;
font-size: 0.72em;
font-weight: 700;
font-family: monospace;
text-transform: uppercase;
@@ -14242,12 +14313,12 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-pill {
flex-shrink: 0;
padding: 2px 6px;
padding: 0.2em 0.55em;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-size: 0.76em;
font-weight: 700;
letter-spacing: 0.02em;
white-space: nowrap;
@@ -14266,13 +14337,20 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
color: var(--yellow);
}
.home-sessions-pill--working,
.home-sessions-pill--idle {
.home-sessions-pill--working {
background: color-mix(in srgb, var(--green) 15%, transparent);
border-color: color-mix(in srgb, var(--green) 40%, transparent);
color: var(--green);
}
/* Same muting as the idle dot, one step further: the pill is a block of color,
so it reads louder than a 9px dot at the same mix. */
.home-sessions-pill--idle {
background: color-mix(in srgb, var(--green) 7%, transparent);
border-color: color-mix(in srgb, var(--green) 18%, var(--border));
color: color-mix(in srgb, var(--green) 45%, var(--text-muted));
}
/* Row accents: same language as the session tabs and the phone overview — red
means a question is pending, yellow means it wants input, green means work is
happening. Nothing else on this screen may reuse these colors. */