mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -254,7 +254,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Phone overview home screen** (`mobile-overview.js`, phones only, per-device `mobileOverviewEnabled`, default ON): under 430px the "C" logo shows a session overview (NEEDS YOU / CURRENT SESSIONS / PAST SESSIONS) instead of the welcome overlay; tablet and desktop are unchanged. The branch lives in `showWelcome()`/`hideWelcome()` (terminal-ui.js) behind `shouldUseMobileOverview()`, which is **width-driven** (`getDeviceType() === 'mobile'`) because this is a layout decision, unlike the settings namespace which stays handheld-based. ⚠️ The container ships with the `hidden` attribute and only this module removes it: never give `.mobile-overview` a bare `display` rule, since desktop does not load `mobile.css` (`media="(max-width: 1023px)"`) and would then render it unstyled. Live re-renders ride on the tail of `_renderSessionTabsImmediate()` (every state change it needs already funnels there); PAST rows come from one `_fetchUnifiedSessions(60)` per home-screen visit and resume through the shared `resumeHistorySession()`, so they behave exactly like the welcome screen's Resume list. ⚠️ Two things must stay in lockstep with surfaces outside this module, because divergence reads as a bug rather than a style: the split Run button carries the **toolbar's own classes** (`btn-toolbar btn-run mode-<backend>` / `btn-run-gear`) so the per-backend gradient and the light-skin overrides apply unchanged (mobile.css must therefore set no `background`/`color` on it), and row status uses the **session-tab language** (green dot when fine, `pulse` while working, yellow blinking row when waiting for input, red blinking row when a question is pending, mirroring `tab-alert-idle`/`tab-alert-action`). The picker mirrors the toolbar run-mode menu (`setRunMode()` + `run()`, `openWebviewFromMenu()` for saved dashboards) and deliberately omits its Recent-Sessions block, since PAST SESSIONS is that. Status pills carry `data-i18n-skip` (generic words like "idle" collide with state strings elsewhere).
|
||||
|
||||
**Desktop home tab column** (`home-sessions.js`, desktop only): the welcome overlay centers ~560px of content in a ~1400px window, so its left gutter is dead space; it now carries the open tabs as a vertical list. Rows are in **tab order**, not sorted by urgency like the phone overview, because the row badges are the Alt+1..9 indices. State classification is REUSED from mobile-overview.js (`_mobileOverviewState`/`_mobileOverviewCaseFor`), which is why the module loads after it. ⚠️ The column is `position: absolute` so the centered content never moves, which is exactly why it needs a **width gate in two places** — `HOME_SESSIONS_MIN_WIDTH` (1180) in the JS plus a `max-width: 1179px` media query as the backstop for a resize that outruns the matchMedia listener; drift between them means a column overlapping the search panel, and `test/home-sessions.test.ts` pins them equal. ⚠️ `.home-sessions` is `display: flex`, so `[hidden]` must be re-asserted as `display: none` or the module's only visibility lever does nothing. Working state is deliberately byte-identical to the phone's: pulsing green dot + the `tab-load-spin` ring reused from the tab strip + the same green halo (added to `.mobile-overview-dot--working` at the same time), so "working" reads the same on every surface. Live re-renders ride the tail of `_renderSessionTabsImmediate()` alongside the phone overview.
|
||||
**Desktop home tab rail** (`home-sessions.js`, desktop only): the welcome overlay centers ~560px of content in a ~1400px window, so its left gutter is dead space; it carries the open tabs as a rail **docked flush to the left edge, full height** (a vertically centered card floating mid-gutter read as debris). Rows are in **tab order**, not sorted by urgency like the phone overview, because the row badges are the Alt+1..9 indices, and each carries **created / last-active** stamps. State classification is REUSED from mobile-overview.js (`_mobileOverviewState`/`_mobileOverviewCaseFor`), which is why the module loads after it. ⚠️ The rail is `position: absolute` so the centered content never moves, which is exactly why it needs a **width gate in two places** — `HOME_SESSIONS_MIN_WIDTH` (1180) in the JS plus a `max-width: 1179px` media query as the backstop for a resize that outruns the matchMedia listener; drift between them means a rail overlapping the search panel, and `test/home-sessions.test.ts` pins them equal. ⚠️ `.home-sessions` is `display: flex`, so `[hidden]` must be re-asserted as `display: none` or the module's only visibility lever does nothing. ⚠️ Size scales with the viewport off **one knob**: `width: clamp(250px, 19vw, 430px)` plus a fluid `font-size` on `.home-sessions`, with every child sized in `em` — reintroducing `rem`/px type inside the block silently breaks the scaling, and widening the clamp past the gutter reintroduces the overlap the gate exists to prevent. The age stamps are refreshed **in place** by a 20s clock (`_tickHomeSessionsTimes()`, disarmed in `hideHomeSessions()`), never by re-rendering, which would restart every row's blink and working ring. Working state is deliberately byte-identical to the phone's: pulsing green dot + the `tab-load-spin` ring reused from the tab strip + the same green halo (added to `.mobile-overview-dot--working` at the same time), so "working" reads the same on every surface; **idle** is deliberately NOT that green — dot and pill mix toward `--text-muted` so a glance separates running from sitting. Live re-renders ride the tail of `_renderSessionTabsImmediate()` alongside the phone overview.
|
||||
|
||||
**Command palette + shortcut registry**: `Ctrl/Cmd/Alt+K` opens the session palette; shortcuts live in a rebindable registry (`DEFAULT_SHORTCUTS`/`getShortcutRegistry()`/`matchesShortcutEvent()` in app.js, overrides in `settings.shortcutOverrides`). ⚠️ Palette-chord keys must ALSO be swallowed in `attachCustomKeyEventHandler` (terminal-ui.js) or xterm writes the control byte (0x0B) into the PTY. ⚠️ `saveAppSettings()` rebuilds settings from the DOM, so keys edited elsewhere (`shortcutOverrides`, `showTokenCount`, `showCost`) need explicit `_prev` carry-over. ⚠️ **Smart copy (`Ctrl+C`)** lives in that same handler: with a selection it copies, with none it must `return true` **without** `preventDefault()` or the interrupt is lost. `copyTerminalSelection` is deliberately absent from `SHORTCUT_ACTIONS` because the generic capture loop preventDefaults every match it dispatches. → [architecture-invariants#command-palette-and-shortcut-registry](docs/architecture-invariants.md#command-palette-and-shortcut-registry)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @fileoverview Desktop home screen session list: the open tabs as a vertical
|
||||
* column down the left of the welcome overlay.
|
||||
* @fileoverview Desktop home screen session list: the open tabs as a rail docked
|
||||
* down the left edge of the welcome overlay.
|
||||
*
|
||||
* The welcome screen centers ~560px of content in a window that is usually
|
||||
* 1400px+, so the two gutters are dead space. The left one now carries the same
|
||||
@@ -8,11 +8,19 @@
|
||||
* one row per live tab, in TAB ORDER (not sorted by state) so it reads as the
|
||||
* tab strip rotated, and so Alt+1..9 still matches what you see.
|
||||
*
|
||||
* DESKTOP ONLY, and only in a wide enough window: the column is absolutely
|
||||
* DESKTOP ONLY, and only in a wide enough window: the rail is absolutely
|
||||
* positioned so the centered welcome content never moves, which means it can
|
||||
* only exist where the gutter is genuinely wider than the column. Below
|
||||
* only exist where the gutter is genuinely wider than the rail. Below
|
||||
* `HOME_SESSIONS_MIN_WIDTH` nothing renders; on a phone the mobile overview owns
|
||||
* the home screen entirely and this surface stays out of its way.
|
||||
* the home screen entirely and this surface stays out of its way. Width and type
|
||||
* both scale with the viewport (see the `.home-sessions` block in styles.css) —
|
||||
* a fixed 256px card looks abandoned on a 2560px display.
|
||||
*
|
||||
* Each row carries when the session was FIRST CREATED and when it was LAST
|
||||
* ACTIVE, both relative. Those two stamps go stale on their own (a sitting
|
||||
* session emits no event), so a slow clock refreshes them IN PLACE from the
|
||||
* epoch-ms values parked on the elements, rather than re-rendering: a re-render
|
||||
* would restart every row's blink animation and its working ring.
|
||||
*
|
||||
* The working state is deliberately identical to the phone's: a pulsing green
|
||||
* dot ringed by the spinner a tab shows while it loads (`tab-load-spin`, reused
|
||||
@@ -27,19 +35,23 @@
|
||||
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||
* @dependency app.js (this.sessions, this.cases, this.pendingHooks, selectSession)
|
||||
* @dependency mobile-overview.js (_mobileOverviewState, _mobileOverviewCaseFor, shouldUseMobileOverview)
|
||||
* @dependency ralph-panel.js (formatRelativeTime — the app's one relative-time formatter)
|
||||
* @dependency webview-tabs.js (this.webviews, this.webviewOrder, openWebview)
|
||||
* @dependency mobile-handlers.js (MobileDetection)
|
||||
* @loadorder 12.56 of 16, after mobile-overview.js, before entrance-animations.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* Narrowest window that gets the column. The welcome content is 560px wide and
|
||||
* centered, so at 1180px each gutter is 310px — enough for the 256px column plus
|
||||
* its 20px offset and still a visible gap. Anything narrower would overlap the
|
||||
* Narrowest window that gets the rail. The welcome content is 560px wide and
|
||||
* centered, so at 1180px each gutter is 310px — enough for the rail at its
|
||||
* 250px floor and still a visible gap. Anything narrower would overlap the
|
||||
* search panel, which is why this is a width gate and not a device-type gate.
|
||||
*/
|
||||
const HOME_SESSIONS_MIN_WIDTH = 1180;
|
||||
|
||||
/** How often the relative stamps are rewritten while the home screen is up. */
|
||||
const HOME_SESSIONS_CLOCK_MS = 20000;
|
||||
|
||||
/** Pill copy per state. Same words as the phone overview, same reasons. */
|
||||
const HOME_SESSIONS_PILL_LABEL = {
|
||||
needs: 'needs you',
|
||||
@@ -87,6 +99,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
this._wireHomeSessions(el);
|
||||
if (!this.shouldShowHomeSessions()) {
|
||||
el.hidden = true;
|
||||
this._stopHomeSessionsClock();
|
||||
return;
|
||||
}
|
||||
el.hidden = false;
|
||||
@@ -96,6 +109,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
hideHomeSessions() {
|
||||
const el = document.getElementById('homeSessions');
|
||||
if (el) el.hidden = true;
|
||||
this._stopHomeSessionsClock();
|
||||
},
|
||||
|
||||
/** Re-render only when showing (called from the tab renderer's tail). */
|
||||
@@ -173,6 +187,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
dir: this._shortenHomePath ? this._shortenHomePath(session.workingDir) : session.workingDir || '',
|
||||
state,
|
||||
pill: HOME_SESSIONS_PILL_LABEL[state] || state,
|
||||
// Epoch ms, straight off the session payload; formatting happens at
|
||||
// render time so the clock below can redo it without a re-render.
|
||||
createdAt: Number(session.createdAt) || 0,
|
||||
lastActivityAt: Number(session.lastActivityAt) || 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -193,6 +211,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!rows.length && !webviews.length) {
|
||||
el.hidden = true;
|
||||
el.replaceChildren();
|
||||
this._stopHomeSessionsClock();
|
||||
return;
|
||||
}
|
||||
el.hidden = false;
|
||||
@@ -205,6 +224,95 @@ Object.assign(CodemanApp.prototype, {
|
||||
for (const row of rows) list.appendChild(this._buildHomeSessionRow(row));
|
||||
for (const webview of webviews) list.appendChild(this._buildHomeSessionsWebviewRow(webview));
|
||||
el.appendChild(list);
|
||||
|
||||
this._startHomeSessionsClock();
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Age stamps: created / last active
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* The "created 2h ago · active 3m ago" footer line. Both stamps keep their raw
|
||||
* epoch-ms on the element (`data-hs-ts`) so `_tickHomeSessionsTimes()` can
|
||||
* rewrite the text without rebuilding the row.
|
||||
*/
|
||||
_buildHomeSessionsMeta(row) {
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'home-sessions-row-meta';
|
||||
// Relative times are generated text, and "created"/"active" here are the
|
||||
// same generic words that mean something else on other surfaces.
|
||||
meta.setAttribute('data-i18n-skip', '');
|
||||
|
||||
meta.appendChild(this._buildHomeSessionsStamp('created', row.createdAt, 'home-sessions-meta-created'));
|
||||
|
||||
const sep = document.createElement('span');
|
||||
sep.className = 'home-sessions-meta-sep';
|
||||
sep.setAttribute('aria-hidden', 'true');
|
||||
sep.textContent = '·';
|
||||
meta.appendChild(sep);
|
||||
|
||||
meta.appendChild(this._buildHomeSessionsStamp('active', row.lastActivityAt, 'home-sessions-meta-active'));
|
||||
|
||||
return meta;
|
||||
},
|
||||
|
||||
/** One labelled stamp: a dim key, the relative value, full date in the title. */
|
||||
_buildHomeSessionsStamp(key, timestamp, className) {
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = `home-sessions-meta-item ${className}`;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'home-sessions-meta-key';
|
||||
label.textContent = key;
|
||||
wrap.appendChild(label);
|
||||
|
||||
const value = document.createElement('span');
|
||||
value.dataset.hsTs = String(timestamp || 0);
|
||||
value.textContent = this._homeSessionsAgo(timestamp);
|
||||
wrap.appendChild(value);
|
||||
|
||||
if (timestamp)
|
||||
wrap.title = `${key === 'created' ? 'First created' : 'Last active'}: ${new Date(timestamp).toLocaleString()}`;
|
||||
return wrap;
|
||||
},
|
||||
|
||||
/** Relative label for a stamp. `formatRelativeTime` is the app's one formatter. */
|
||||
_homeSessionsAgo(timestamp) {
|
||||
if (!timestamp) return '—';
|
||||
return this.formatRelativeTime(timestamp) || '—';
|
||||
},
|
||||
|
||||
/**
|
||||
* Rewrites the stamps in place every `HOME_SESSIONS_CLOCK_MS`. In place, not a
|
||||
* re-render: replacing the rows would restart the blink animation on every
|
||||
* waiting row and the ring on every working one, twice a minute, for nothing.
|
||||
*/
|
||||
_startHomeSessionsClock() {
|
||||
if (this._homeSessionsClock) return;
|
||||
this._homeSessionsClock = setInterval(() => {
|
||||
if (!this.isHomeSessionsVisible()) {
|
||||
this._stopHomeSessionsClock();
|
||||
return;
|
||||
}
|
||||
this._tickHomeSessionsTimes();
|
||||
}, HOME_SESSIONS_CLOCK_MS);
|
||||
},
|
||||
|
||||
_stopHomeSessionsClock() {
|
||||
if (!this._homeSessionsClock) return;
|
||||
clearInterval(this._homeSessionsClock);
|
||||
this._homeSessionsClock = null;
|
||||
},
|
||||
|
||||
_tickHomeSessionsTimes() {
|
||||
const el = document.getElementById('homeSessions');
|
||||
if (!el) return;
|
||||
for (const node of el.querySelectorAll('[data-hs-ts]')) {
|
||||
const ts = Number(node.dataset.hsTs) || 0;
|
||||
const text = this._homeSessionsAgo(ts);
|
||||
if (node.textContent !== text) node.textContent = text;
|
||||
}
|
||||
},
|
||||
|
||||
_buildHomeSessionsHeader(count) {
|
||||
@@ -287,6 +395,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
pill.textContent = row.pill;
|
||||
item.appendChild(pill);
|
||||
|
||||
// Wraps onto its own line (the row is flex-wrap) so it gets the full width.
|
||||
item.appendChild(this._buildHomeSessionsMeta(row));
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
|
||||
@@ -323,9 +323,9 @@
|
||||
|
||||
<!-- Welcome Overlay (shown when no session active) -->
|
||||
<div class="welcome-overlay" id="welcomeOverlay">
|
||||
<!-- Open tabs as a vertical column in the left gutter (home-sessions.js).
|
||||
<!-- Open tabs as a rail docked to the left edge (home-sessions.js).
|
||||
Absolutely positioned so the centered content below never moves, and
|
||||
therefore only rendered where the gutter is wider than the column;
|
||||
therefore only rendered where the gutter is wider than the rail;
|
||||
ships `hidden` and only that module reveals it. -->
|
||||
<aside class="home-sessions" id="homeSessions" hidden></aside>
|
||||
<div class="welcome-content">
|
||||
|
||||
+118
-40
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user