Files
Codeman/test/run-mode-menu-scroll.test.ts
T
DevvynandClaude Sonnet 5.5 90649fc363 fix(ui): make the Run dropdown scrollable
The menu opens upward (bottom: 100%) with no max-height or overflow, so with the stock CLIs plus custom endpoint entries it was taller than the room above the toolbar and its top was off-screen and unreachable, worst on phones. Cap it to the space between the header and the toolbar (dvh, vh fallback), scroll inside it with overscroll containment, and stop its overflow:auto children (history, saved URLs) being squashed.

Tests: a CI-gate source guard and a real mobile-browser test (touch swipe reaches the last entry), which fails without the CSS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
2026-10-06 19:06:39 +08:00

45 lines
2.0 KiB
TypeScript

/**
* The Run dropdown grows UPWARD from the toolbar (`bottom: 100%`). With every CLI, the custom
* endpoint entries ("Claude Code (llama.cpp)" ...), Terminal and the saved URLs it can be taller
* than the space above the toolbar, which put its top off-screen with nothing to scroll: on a
* phone the entries up there were unreachable. This pins the rule that makes it scroll, in the CI
* gate; the real touch scroll is test/run-mode-menu-scroll.browser.test.ts.
*/
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const css = readFileSync(new URL('../src/web/public/styles.css', import.meta.url), 'utf8');
/** The declaration block of the FIRST rule whose selector is exactly `selector`. */
function rule(selector: string): string {
const start = css.search(new RegExp(`(^|\\n)${selector.replace(/[.>*]/g, '\\$&')}\\s*\\{`));
expect(start, `no rule for ${selector}`).toBeGreaterThanOrEqual(0);
const open = css.indexOf('{', start);
return css.slice(open + 1, css.indexOf('}', open));
}
describe('Run dropdown scrolls when it is taller than the room above the toolbar', () => {
const menu = rule('.run-mode-menu');
it('scrolls vertically and keeps the scroll inside the menu', () => {
expect(menu).toMatch(/overflow-y:\s*auto/);
expect(menu).toMatch(/overscroll-behavior:\s*contain/);
expect(menu).toMatch(/touch-action:\s*pan-y/);
});
it('is capped to the space between the header and the toolbar, in dvh with a vh fallback', () => {
const caps = [...menu.matchAll(/max-height:\s*([^;]+);/g)].map((m) => m[1]);
expect(caps).toHaveLength(2);
expect(caps[0]).toContain('100vh');
expect(caps[1]).toContain('100dvh');
for (const cap of caps) {
expect(cap).toContain('var(--header-height)');
expect(cap).toContain('var(--toolbar-height)');
}
});
it('does not let scrolling children (history, saved URLs) be squashed to nothing', () => {
expect(rule('.run-mode-menu > *')).toMatch(/flex-shrink:\s*0/);
});
});