mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16: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).
|
**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)
|
**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
|
* @fileoverview Desktop home screen session list: the open tabs as a rail docked
|
||||||
* column down the left of the welcome overlay.
|
* down the left edge of the welcome overlay.
|
||||||
*
|
*
|
||||||
* The welcome screen centers ~560px of content in a window that is usually
|
* 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
|
* 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
|
* 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.
|
* 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
|
* 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
|
* `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
|
* 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
|
* 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
|
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||||
* @dependency app.js (this.sessions, this.cases, this.pendingHooks, selectSession)
|
* @dependency app.js (this.sessions, this.cases, this.pendingHooks, selectSession)
|
||||||
* @dependency mobile-overview.js (_mobileOverviewState, _mobileOverviewCaseFor, shouldUseMobileOverview)
|
* @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 webview-tabs.js (this.webviews, this.webviewOrder, openWebview)
|
||||||
* @dependency mobile-handlers.js (MobileDetection)
|
* @dependency mobile-handlers.js (MobileDetection)
|
||||||
* @loadorder 12.56 of 16, after mobile-overview.js, before entrance-animations.js
|
* @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
|
* Narrowest window that gets the rail. The welcome content is 560px wide and
|
||||||
* centered, so at 1180px each gutter is 310px — enough for the 256px column plus
|
* centered, so at 1180px each gutter is 310px — enough for the rail at its
|
||||||
* its 20px offset and still a visible gap. Anything narrower would overlap the
|
* 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.
|
* search panel, which is why this is a width gate and not a device-type gate.
|
||||||
*/
|
*/
|
||||||
const HOME_SESSIONS_MIN_WIDTH = 1180;
|
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. */
|
/** Pill copy per state. Same words as the phone overview, same reasons. */
|
||||||
const HOME_SESSIONS_PILL_LABEL = {
|
const HOME_SESSIONS_PILL_LABEL = {
|
||||||
needs: 'needs you',
|
needs: 'needs you',
|
||||||
@@ -87,6 +99,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this._wireHomeSessions(el);
|
this._wireHomeSessions(el);
|
||||||
if (!this.shouldShowHomeSessions()) {
|
if (!this.shouldShowHomeSessions()) {
|
||||||
el.hidden = true;
|
el.hidden = true;
|
||||||
|
this._stopHomeSessionsClock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.hidden = false;
|
el.hidden = false;
|
||||||
@@ -96,6 +109,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
hideHomeSessions() {
|
hideHomeSessions() {
|
||||||
const el = document.getElementById('homeSessions');
|
const el = document.getElementById('homeSessions');
|
||||||
if (el) el.hidden = true;
|
if (el) el.hidden = true;
|
||||||
|
this._stopHomeSessionsClock();
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Re-render only when showing (called from the tab renderer's tail). */
|
/** 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 || '',
|
dir: this._shortenHomePath ? this._shortenHomePath(session.workingDir) : session.workingDir || '',
|
||||||
state,
|
state,
|
||||||
pill: HOME_SESSIONS_PILL_LABEL[state] || 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) {
|
if (!rows.length && !webviews.length) {
|
||||||
el.hidden = true;
|
el.hidden = true;
|
||||||
el.replaceChildren();
|
el.replaceChildren();
|
||||||
|
this._stopHomeSessionsClock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.hidden = false;
|
el.hidden = false;
|
||||||
@@ -205,6 +224,95 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
for (const row of rows) list.appendChild(this._buildHomeSessionRow(row));
|
for (const row of rows) list.appendChild(this._buildHomeSessionRow(row));
|
||||||
for (const webview of webviews) list.appendChild(this._buildHomeSessionsWebviewRow(webview));
|
for (const webview of webviews) list.appendChild(this._buildHomeSessionsWebviewRow(webview));
|
||||||
el.appendChild(list);
|
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) {
|
_buildHomeSessionsHeader(count) {
|
||||||
@@ -287,6 +395,9 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
pill.textContent = row.pill;
|
pill.textContent = row.pill;
|
||||||
item.appendChild(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;
|
return item;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -323,9 +323,9 @@
|
|||||||
|
|
||||||
<!-- Welcome Overlay (shown when no session active) -->
|
<!-- Welcome Overlay (shown when no session active) -->
|
||||||
<div class="welcome-overlay" id="welcomeOverlay">
|
<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
|
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. -->
|
ships `hidden` and only that module reveals it. -->
|
||||||
<aside class="home-sessions" id="homeSessions" hidden></aside>
|
<aside class="home-sessions" id="homeSessions" hidden></aside>
|
||||||
<div class="welcome-content">
|
<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
|
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
|
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
|
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.
|
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
|
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
|
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.
|
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 {
|
.home-sessions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 0;
|
||||||
top: 50%;
|
top: 0;
|
||||||
transform: translateY(-50%);
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 0.9em;
|
||||||
width: 256px;
|
/* Ramps with the viewport, floored so it still clears the centered content at
|
||||||
max-height: calc(100% - 3rem);
|
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;
|
text-align: left;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -14059,12 +14074,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-header {
|
.home-sessions-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 0.66em;
|
||||||
padding: 0 6px;
|
flex-shrink: 0;
|
||||||
|
padding: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-sessions-title {
|
.home-sessions-title {
|
||||||
font-size: 0.66rem;
|
font-size: 0.87em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -14075,25 +14091,29 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 18px;
|
min-width: 1.5em;
|
||||||
height: 16px;
|
height: 1.35em;
|
||||||
padding: 0 5px;
|
padding: 0 0.42em;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 0.6rem;
|
font-size: 0.79em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-family: monospace;
|
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 {
|
.home-sessions-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 0.35em;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 2px 2px 6px;
|
padding: 0.16em 0.16em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-sessions-list::-webkit-scrollbar {
|
.home-sessions-list::-webkit-scrollbar {
|
||||||
@@ -14108,15 +14128,16 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-row {
|
.home-sessions-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.66em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 7px 9px;
|
padding: 0.6em 0.75em;
|
||||||
border-radius: 9px;
|
border-radius: 0.75em;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.76rem;
|
font-size: 1em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background var(--transition-smooth), border-color var(--transition-smooth), color var(--transition-smooth);
|
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;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 15px;
|
width: 1.3em;
|
||||||
height: 15px;
|
height: 1.3em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 3px;
|
border-radius: 0.25em;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.58rem;
|
font-size: 0.76em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
@@ -14151,8 +14172,8 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-dot {
|
.home-sessions-dot {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 9px;
|
width: 0.75em;
|
||||||
height: 9px;
|
height: 0.75em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--text-muted);
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
@@ -14166,8 +14187,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
background: var(--yellow);
|
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 {
|
.home-sessions-dot--idle {
|
||||||
background: var(--green);
|
background: color-mix(in srgb, var(--green) 42%, var(--text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-sessions-dot--done {
|
.home-sessions-dot--done {
|
||||||
@@ -14189,7 +14215,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-dot--working::after {
|
.home-sessions-dot--working::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -4px;
|
inset: -0.34em;
|
||||||
border: 2px solid color-mix(in srgb, var(--green) 25%, transparent);
|
border: 2px solid color-mix(in srgb, var(--green) 25%, transparent);
|
||||||
border-top-color: var(--green);
|
border-top-color: var(--green);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -14199,7 +14225,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-row-body {
|
.home-sessions-row-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 0.12em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -14207,7 +14233,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
.home-sessions-row-title {
|
.home-sessions-row-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0.42em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -14220,21 +14246,66 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-sessions-row-sub {
|
.home-sessions-row-sub {
|
||||||
font-size: 0.66rem;
|
font-size: 0.87em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.home-sessions-mode {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0 4px;
|
padding: 0 0.35em;
|
||||||
border-radius: 3px;
|
border-radius: 0.25em;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.55rem;
|
font-size: 0.72em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -14242,12 +14313,12 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
|
|
||||||
.home-sessions-pill {
|
.home-sessions-pill {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 2px 6px;
|
padding: 0.2em 0.55em;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.58rem;
|
font-size: 0.76em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -14266,13 +14337,20 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-sessions-pill--working,
|
.home-sessions-pill--working {
|
||||||
.home-sessions-pill--idle {
|
|
||||||
background: color-mix(in srgb, var(--green) 15%, transparent);
|
background: color-mix(in srgb, var(--green) 15%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--green) 40%, transparent);
|
border-color: color-mix(in srgb, var(--green) 40%, transparent);
|
||||||
color: var(--green);
|
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
|
/* 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
|
means a question is pending, yellow means it wants input, green means work is
|
||||||
happening. Nothing else on this screen may reuse these colors. */
|
happening. Nothing else on this screen may reuse these colors. */
|
||||||
|
|||||||
Reference in New Issue
Block a user