feat(tabs): COD-358 add resizable vertical session rail

This commit is contained in:
Aamer Akhter
2026-08-23 12:21:02 -04:00
parent 88bb98de43
commit e3a2fb767f
26 changed files with 1856 additions and 36 deletions
+51
View File
@@ -34,6 +34,7 @@ const SCHEMAS = readFileSync(new URL('../src/web/schemas.ts', import.meta.url),
interface LayoutApp {
soloSessionId: string | null;
isSoloWindow: boolean;
sessions: Map<string, unknown>;
sessionOrder: string[];
_tallTabsEnabled?: boolean;
@@ -44,6 +45,7 @@ interface LayoutApp {
isSessionSidebarActive(): boolean;
isSessionSidebarCollapsed(): boolean;
applySessionListLayout(): void;
applyTabOrientation(): void;
toggleSessionSidebar(): void;
updateSidebarCount(): void;
closeSessionSidebarOnHandheld(): void;
@@ -86,6 +88,7 @@ const SHELL = `
</div>
</header>
<main class="main">
<div class="tab-rail" id="tabRail"></div>
<aside class="session-sidebar" id="sessionSidebar" aria-label="Sessions">
<div class="session-sidebar-head">
<span class="session-sidebar-title">Sessions</span>
@@ -154,6 +157,7 @@ function boot(
// terminal stack. Only the layout surface is under test here.
const app = Object.create(win.__CodemanApp.prototype) as LayoutApp;
app.soloSessionId = options.solo ?? null;
app.isSoloWindow = !!app.soloSessionId;
app.sessions = new Map();
app.sessionOrder = [];
app._elemCache = new Map();
@@ -177,6 +181,39 @@ describe('session list layout', () => {
expect(toggleBtn(win).classList.contains('btn-sidebar-toggle--hidden')).toBe(true);
});
it('preserves vertical rail ownership when the session-list layout reapplies', () => {
const { win, app } = boot({ stored: { sessionListLayout: 'header', tabOrientation: 'vertical' } });
app.applySessionListLayout();
app.applyTabOrientation();
expect(tabsEl(win).parentElement?.id).toBe('tabRail');
app.applySessionListLayout();
expect(tabsEl(win).parentElement?.id).toBe('tabRail');
expect(tabsEl(win).getAttribute('aria-orientation')).toBe('vertical');
});
it('keeps aria orientation synchronized when tab orientation moves hosts', () => {
const { win, app } = boot({ stored: { sessionListLayout: 'header', tabOrientation: 'vertical' } });
app.applySessionListLayout();
app.applyTabOrientation();
expect(tabsEl(win).parentElement?.id).toBe('tabRail');
expect(tabsEl(win).getAttribute('aria-orientation')).toBe('vertical');
win.localStorage.setItem(
'codeman-app-settings',
JSON.stringify({ sessionListLayout: 'header', tabOrientation: 'horizontal' })
);
delete (app as unknown as { _cachedAppSettings?: unknown })._cachedAppSettings;
app.applyTabOrientation();
expect(tabsEl(win).parentElement?.id).toBe('sessionTabsHost');
expect(tabsEl(win).getAttribute('aria-orientation')).toBe('horizontal');
});
it('re-parents the tab list into the sidebar and flips the a11y state', () => {
const { win, app } = boot({ stored: { sessionListLayout: 'sidebar' } });
expect(app.getSessionListLayout()).toBe('sidebar');
@@ -225,6 +262,20 @@ describe('session list layout', () => {
expect(tabsEl(win).parentElement?.id).toBe('sessionTabsHost');
});
it('forces horizontal tabs in a solo window even when vertical orientation is preferred', () => {
const { win, app } = boot({
stored: { sessionListLayout: 'header', tabOrientation: 'vertical' },
solo: 'sess-1',
});
app.applySessionListLayout();
app.applyTabOrientation();
expect(win.document.documentElement.dataset.tabOrientation).toBe('horizontal');
expect(tabsEl(win).parentElement?.id).toBe('sessionTabsHost');
expect(tabsEl(win).getAttribute('aria-orientation')).toBe('horizontal');
});
it('round-trips the collapse state through its own storage key', () => {
// Deliberately NOT in the app-settings blob: saveAppSettings() rebuilds that
// blob from the DOM controls, so a key without a control is wiped on Save.