From 502d9440860c616ad00094915318e2486c21d292 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 23 Oct 2026 03:57:21 +0000 Subject: [PATCH 1/4] Fix phone layout, contrast, focus and touch targets (T10) VS-05: the Current Debate grids use minmax(0, 1fr) below md and the motion heading can shrink, so long titles truncate instead of widening the page. AX-02: a skip link is the first Tab stop on the landing, app and public shells. AX-04/AX-09: pills, the active nav tint, Timer speech chips and the use-case subtitles reach 4.5:1 in light and dark. AX-07: switch, checkbox, input, tabs and small buttons reach 44px on coarse pointers and 24px elsewhere; flowchart edges get a zoom-aware hit stroke and the React Flow credit a real target. VS-01/VS-03/VS-09: the landing nav collapses below lg, the logo has a transparent surround, the locked Reasoning field stays readable, and the language menu closes on a choice. Co-Authored-By: Claude Opus 5.5 --- e2e/quality-backlog.spec.ts | 53 ++++++++++++++++++ public/favicon-72.png | Bin 3974 -> 7983 bytes src/components/LanguageSwitcher.tsx | 14 +++-- src/components/ModelSelector.tsx | 3 +- .../features/debate/CurrentDebateMenu.tsx | 2 +- src/components/landing/Footer.tsx | 10 ++-- src/components/landing/Navbar.tsx | 12 ++-- src/components/landing/Pricing.tsx | 2 +- src/components/landing/UseCases.tsx | 2 +- src/components/landing/WhatsNew.tsx | 2 +- src/components/layout/AppLayout.tsx | 13 +++-- src/components/layout/PageHeader.tsx | 2 +- src/components/layout/PublicPageShell.tsx | 4 +- src/components/layout/SkipLink.tsx | 26 +++++++++ src/components/ui/button.tsx | 2 +- src/components/ui/checkbox.tsx | 2 +- src/components/ui/input.tsx | 2 +- src/components/ui/switch.tsx | 2 +- src/components/ui/tabs.tsx | 4 +- src/index.css | 19 +++++++ src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/hu.json | 1 + src/pages/CurrentDebatePage.tsx | 21 +++---- src/pages/FlowchartPage.tsx | 6 ++ src/pages/LandingPage.tsx | 4 +- src/pages/SourcesPage.tsx | 2 +- src/pages/TimerPage.tsx | 52 ++++++++--------- 28 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 src/components/layout/SkipLink.tsx diff --git a/e2e/quality-backlog.spec.ts b/e2e/quality-backlog.spec.ts index 3221683bc..fde1855fa 100644 --- a/e2e/quality-backlog.spec.ts +++ b/e2e/quality-backlog.spec.ts @@ -104,3 +104,56 @@ test('Flowchart deletes a selected node at once and Undo puts it back', async ({ await page.getByRole('button', { name: 'Undo' }).click(); await expect(page.getByText('Argument 1', { exact: true })).toHaveCount(1); }); + +// VS-05: a long motion and long item titles once widened the dashboard past a phone screen. +for (const locale of locales) { + test(`Current Debate fits a 360px phone with sources attached in ${locale}`, { tag: '@smoke' }, async ({ page }) => { + const motion = 'This House believes that national governments should nationalise all critical digital infrastructure including undersea cables'; + await page.setViewportSize({ width: 360, height: 780 }); + await setLocale(page, locale); + await stubSupabaseSession(page); + await seedCurrentDebate(page, motion); + await page.addInitScript(({ motion }) => { + const debateId = '00000000-0000-4000-8000-00000000d001'; + const now = Date.now(); + const sources = [0, 1, 2].map((index) => ({ + id: `source-${index}`, title: `${motion} source ${index}`, content: 'Evidence', type: 'text', + debateId, createdAt: now - index, isActive: true, folder: 'General', + })); + const savedCases = [0, 1].map((index) => ({ + id: `case-${index}`, title: `${motion} case ${index}`, data: { motion }, createdAt: now - index, + debateId, side: index ? 'con' : 'pro', + })); + window.localStorage.setItem('debate-trainer-storage', JSON.stringify({ + state: { ownerUserId: '00000000-0000-0000-0000-000000000001', sourceLibrary: sources, savedCases, sourceFolders: ['General'] }, + version: 1, + })); + }, { motion }); + + await page.goto('/app/debate'); + await expect(page.getByTestId('debate-sources').getByText(`${motion} source 0`)).toBeVisible(); + // The Load button stays on screen beside its truncated title. + const load = await page.getByTestId('debate-side-pro').getByRole('button', { name: new RegExp(`${motion} case 0`) }).boundingBox(); + expect(load).not.toBeNull(); + expect(load!.x + load!.width).toBeLessThanOrEqual(360); + + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(360); + }); +} + +// VS-09: picking a language closes the menu; the choice already persists. +test('language menu closes once a language is chosen', { tag: '@smoke' }, async ({ page }, testInfo) => { + await setLocale(page, 'en'); + await page.goto('/'); + if (testInfo.project.name === 'mobile-chromium') { + await page.getByRole('button', { name: 'Open menu' }).click(); + } + await page.getByRole('button', { name: 'Language' }).click(); + const menu = page.getByRole('radiogroup', { name: 'Interface language' }); + await expect(menu).toBeVisible(); + + await menu.getByRole('radio', { name: 'Magyar' }).click(); + + await expect(page.getByRole('radiogroup')).toHaveCount(0); + await expect(page.locator('html')).toHaveAttribute('lang', 'hu'); +}); diff --git a/public/favicon-72.png b/public/favicon-72.png index 9cb4f8aacaf899500fbc4293709e96408562c122..e6b54f6a64ede4923cbc168f4db5a7c1194bd29f 100644 GIT binary patch literal 7983 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 literal 3974 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 diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 1b5e4aade..20ae26ef7 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Check, Globe } from 'lucide-react'; import { useSettingsStore } from '@/lib/stores/settings-store'; @@ -38,13 +39,18 @@ function LanguageChoices({ label, value, onChange }: { label: string; value: App ); } -/** Two taps to change language: open, pick. The AI output language sits underneath. */ +/** Two taps to change language: open, pick. The menu closes on the pick; the AI output language sits underneath. */ export function LanguageSwitcher() { const { t } = useTranslation(); const { language, outputLanguage, setLanguage, setOutputLanguage } = useSettingsStore(); + const [open, setOpen] = useState(false); + const choose = (set: (language: AppLanguage) => void) => (next: AppLanguage) => { + set(next); + setOpen(false); + }; return ( - + )} @@ -53,10 +53,10 @@ function SideSection({ side, items }: { side: 'pro' | 'con'; items: DebateItem[] const { t } = useTranslation(); const relevant = items.filter((item) => item.side === side || item.side === 'both'); return ( - - + + {t(sideLabelKey(side))} - {t('debate.itemCount', { count: relevant.length, defaultValue: '{{count}} items' })} + {t('debate.itemCount', { count: relevant.length, defaultValue: '{{count}} items' })} {relevant.length === 0 @@ -155,14 +155,15 @@ export default function CurrentDebatePage() { )} -
+ {/* grid-cols-1 is minmax(0, 1fr): a long title truncates instead of widening the page. */} +
-
- - +
+ + {t('debate.sources', 'Sources')} )} From d80137d322a9fbc16ed8e729f0e9c726302c0860 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 23 Oct 2026 04:36:31 +0000 Subject: [PATCH 3/4] Address review round 1: focusable main only for the skip, tidy hit areas - The skip link makes main focusable just for its jump, so a click inside main still leaves focus on body. - Debate context rows keep the checkbox's touch area inside their own row. - The free tools pages get the skip link too. - Flowchart passes React Flow a stable onMove. - The new e2e tests match names as strings and open the menu whenever it shows. Co-Authored-By: Claude Opus 5.5 --- e2e/quality-backlog.spec.ts | 10 +++++----- .../features/debate/DebateContextControl.tsx | 4 ++-- src/components/free-tools/ToolPageLayout.tsx | 4 +++- src/components/layout/AppLayout.tsx | 2 +- src/components/layout/PublicPageShell.tsx | 2 +- src/components/layout/SkipLink.tsx | 7 ++++++- src/pages/FlowchartPage.tsx | 6 +++++- src/pages/LandingPage.tsx | 2 +- 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/e2e/quality-backlog.spec.ts b/e2e/quality-backlog.spec.ts index fde1855fa..9a28a9804 100644 --- a/e2e/quality-backlog.spec.ts +++ b/e2e/quality-backlog.spec.ts @@ -133,7 +133,7 @@ for (const locale of locales) { await page.goto('/app/debate'); await expect(page.getByTestId('debate-sources').getByText(`${motion} source 0`)).toBeVisible(); // The Load button stays on screen beside its truncated title. - const load = await page.getByTestId('debate-side-pro').getByRole('button', { name: new RegExp(`${motion} case 0`) }).boundingBox(); + const load = await page.getByTestId('debate-side-pro').getByRole('button', { name: `${motion} case 0` }).boundingBox(); expect(load).not.toBeNull(); expect(load!.x + load!.width).toBeLessThanOrEqual(360); @@ -142,12 +142,12 @@ for (const locale of locales) { } // VS-09: picking a language closes the menu; the choice already persists. -test('language menu closes once a language is chosen', { tag: '@smoke' }, async ({ page }, testInfo) => { +test('language menu closes once a language is chosen', { tag: '@smoke' }, async ({ page }) => { await setLocale(page, 'en'); await page.goto('/'); - if (testInfo.project.name === 'mobile-chromium') { - await page.getByRole('button', { name: 'Open menu' }).click(); - } + // Below lg the section links and the language control sit in the menu. + const openMenu = page.getByRole('button', { name: 'Open menu' }); + if (await openMenu.isVisible()) await openMenu.click(); await page.getByRole('button', { name: 'Language' }).click(); const menu = page.getByRole('radiogroup', { name: 'Interface language' }); await expect(menu).toBeVisible(); diff --git a/src/components/features/debate/DebateContextControl.tsx b/src/components/features/debate/DebateContextControl.tsx index 3d264e3e6..78e1b280d 100644 --- a/src/components/features/debate/DebateContextControl.tsx +++ b/src/components/features/debate/DebateContextControl.tsx @@ -234,7 +234,7 @@ function SourceRow({ source, onChange }: { source: ContextSourceItem; onChange: const note = source.included ? statusNote(source, t) : (!source.active ? t('debateContext.inactive', 'Turned off in your Library.') : null); return (
  • - onChange(checked === true)} className="mt-0.5" /> + onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
  • - onChange(checked === true)} className="mt-0.5" /> + onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
  • ); } diff --git a/src/components/layout/SkipLink.tsx b/src/components/layout/SkipLink.tsx index 81aadd920..8c454c8ad 100644 --- a/src/components/layout/SkipLink.tsx +++ b/src/components/layout/SkipLink.tsx @@ -4,7 +4,8 @@ export const MAIN_CONTENT_ID = 'main-content'; /** * The first Tab stop on a page: hidden until focused, then it jumps past the header. - * Focus moves to the main landmark without adding a hash route to the URL. + * Focus moves to the main landmark without adding a hash route to the URL. The landmark + * is focusable only for that jump, so a click elsewhere in it still leaves focus on body. */ export function SkipLink() { const { t } = useTranslation(); @@ -15,6 +16,10 @@ export function SkipLink() { const target = document.getElementById(MAIN_CONTENT_ID); if (!target) return; event.preventDefault(); + if (!target.hasAttribute('tabindex')) { + target.setAttribute('tabindex', '-1'); + target.addEventListener('blur', () => target.removeAttribute('tabindex'), { once: true }); + } target.focus({ preventScroll: true }); target.scrollIntoView({ block: 'start' }); }} diff --git a/src/pages/FlowchartPage.tsx b/src/pages/FlowchartPage.tsx index a3fbd460c..a777c7634 100644 --- a/src/pages/FlowchartPage.tsx +++ b/src/pages/FlowchartPage.tsx @@ -314,6 +314,10 @@ export default function FlowchartPage() { // A new chart remounts the canvas element, so the observer follows the key. const canvasRef = useRef(null); const canvasSize = useRef({ width: 0, height: 0 }); + // Edge tap strokes are sized in screen pixels from this (see index.css). + const writeFlowZoom = useCallback((_: unknown, viewport: { zoom: number }) => { + canvasRef.current?.style.setProperty('--flow-zoom', String(viewport.zoom)); + }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; @@ -907,7 +911,7 @@ export default function FlowchartPage() {
    canvasRef.current?.style.setProperty('--flow-zoom', String(viewport.zoom))} + onMove={writeFlowZoom} // The page scrolls under the wheel; zoom uses pinch, the controls, or Ctrl+wheel. zoomOnScroll={false} preventScrolling={false} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index f8b19c15a..3627f6190 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -21,7 +21,7 @@ const LandingPage = () => {
    -
    +
    From a93855b458f7e05aca7c4fd33c9d6c7348c1a43c Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 23 Oct 2026 04:53:11 +0000 Subject: [PATCH 4/4] Wait for the header before choosing where the language control is isVisible() does not wait, so on a phone it could run before the navbar rendered and skip opening the menu. Co-Authored-By: Claude Opus 5.5 --- e2e/quality-backlog.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/quality-backlog.spec.ts b/e2e/quality-backlog.spec.ts index 9a28a9804..542e8c911 100644 --- a/e2e/quality-backlog.spec.ts +++ b/e2e/quality-backlog.spec.ts @@ -146,9 +146,11 @@ test('language menu closes once a language is chosen', { tag: '@smoke' }, async await setLocale(page, 'en'); await page.goto('/'); // Below lg the section links and the language control sit in the menu. + const language = page.getByRole('button', { name: 'Language' }); const openMenu = page.getByRole('button', { name: 'Open menu' }); + await expect(language.or(openMenu).first()).toBeVisible(); if (await openMenu.isVisible()) await openMenu.click(); - await page.getByRole('button', { name: 'Language' }).click(); + await language.click(); const menu = page.getByRole('radiogroup', { name: 'Interface language' }); await expect(menu).toBeVisible();