mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
feat(tabs): give the vertical rail the home screen's per-session detail
The vertical tab rail (tabOrientation 'vertical') listed names and nothing
else, while the rich sidebar and both home screens already answered the
question a docked column exists to answer: which of these sessions wants me
next, and how long has it been like that. The rail is a docked column too, so
it now draws the same row.
- New per-device setting tabRailDetail ('rich' | 'simple', default rich),
App Settings -> Appearance -> Tabs, in SettingsUpdateSchema + displayKeys and
stamped as data-tab-rail-detail by the pre-paint script, so a detailed rail
does not flash through simple rows on every load.
- ONE gate for both vertical surfaces: isRichTabRows() =
isSessionSidebarRich() || isTabRailRich(). The row model, the markup and the
20s in-place clock are the existing rich-sidebar ones, classified by
_mobileOverviewState/_mobileOverviewSince, so the rail, the sidebar, the
desktop home rail and the phone overview cannot disagree about what
"working" means or which stamp measures it.
- Detail rides on its OWN attribute, exactly as the sidebar's does, so every
existing [data-tab-orientation='vertical'] rule keeps matching both variants
untouched. A flip of detail ALONE still forces a full render (the stamps line
is emitted by the row template, not toggled by CSS) and re-runs
applyTabWrapSettings(), which owns the folder line and is now rail-aware.
- CSS: every rich paint rule gains a rail twin as a COMMA-GROUPED selector,
never :is() - an :is() list takes its most specific argument, which would
lift the sidebar arm from (0,3,1) to the rail's (0,5,1) and let these rules
outrank things they never used to.
- Width is why there are thresholds. At 256px the stamps line ellipsizes
mid-word, the same reason the rich sidebar is 300px, so a rail that has never
been sized defaults to 320 (RICH_DEFAULT_WIDTH, the existing Wide preset,
which also keeps the settings select on a named choice). A width the user has
chosen is never overridden: below 288px the created stamp is dropped rather
than truncated (tab-rail-tight, CSS only) and below 240px the rows go back to
simple (tab-rail-compact, which re-renders).
- The rich clock is armed and disarmed by applyTabOrientation() as well as
applySessionListLayout(); a leaked interval would rewrite stamps in a list
that no longer has any.
Also fixes a data-loss bug in the inline tab rename that predates the rail and
reproduces in every layout, header strip included: Escape set the input to ''
and blurred it, and the blur handler commits - so cancelling a rename PUT an
empty name, and the tab fell back to its folder label (measured against a live
server: ["rail-alpha","","rail-gamma"]). Escape now calls cancelRename(), which
invalidates the edit so the blur that follows the input's removal is a no-op.
Tests: rail-detail gate, the three ways it turns back off (simple, compact,
horizontal), sidebar-wins, render-on-detail-flip and the plumbing/CSS guards in
test/session-list-layout.test.ts; the rename cancel in test/inline-rename.test.ts
(browser suite), pinned by running it against the old code first. Verified live
against a real server on an isolated instance: detailed/simple/compact/header/
sidebar variants, click-select, the ... menu, inline rename, Alt+N, the in-place
stamp tick and a full settings-picker round-trip including reload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,45 @@ describe('Inline rename input', () => {
|
||||
expect(renameStillActive).toBe(true);
|
||||
});
|
||||
|
||||
it('Escape cancels the rename instead of committing an empty name', async () => {
|
||||
await resetState();
|
||||
expect(await startRename('esc-cancel', 'rail-beta')).toBe(true);
|
||||
|
||||
// Escape used to clear the field and blur, and the blur handler commits —
|
||||
// so cancelling a rename PUT an empty name, and the tab fell back to its
|
||||
// folder label (measured against a live server, in the header strip as well
|
||||
// as both vertical layouts). The observable here is the REQUEST: this
|
||||
// harness's server has no such session, so a failed PUT would leave the
|
||||
// local map looking innocent.
|
||||
const result = await page.evaluate(async () => {
|
||||
const app = (window as unknown as { app: { _activeRename: unknown } }).app;
|
||||
const calls: string[] = [];
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = (async (input: RequestInfo | URL) => {
|
||||
calls.push(String(input));
|
||||
return new Response('{"success":true}', { status: 200 });
|
||||
}) as typeof window.fetch;
|
||||
|
||||
const inputEl = document.querySelector('input.tab-rename-input') as HTMLInputElement;
|
||||
inputEl.value = 'typed-but-abandoned';
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
// The blur that follows the input's removal must not resurrect the commit.
|
||||
inputEl.dispatchEvent(new Event('blur'));
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
window.fetch = origFetch;
|
||||
return {
|
||||
renamePuts: calls.filter((url) => url.includes('/api/sessions/esc-cancel/name')),
|
||||
renameActive: !!app._activeRename,
|
||||
inputStillInDom: document.body.contains(inputEl),
|
||||
};
|
||||
});
|
||||
|
||||
expect(result.renamePuts).toEqual([]);
|
||||
expect(result.renameActive).toBe(false);
|
||||
expect(result.inputStillInDom).toBe(false);
|
||||
});
|
||||
|
||||
it('CJK guard: regular Enter (no IME) DOES commit', async () => {
|
||||
await resetState();
|
||||
expect(await startRename('regular-enter', 'OldName')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user