Skip to content

Commit 6659cb6

Browse files
committed
Merge main (#214) into fix/t11-perf-offline
2 parents 02e6509 + 87603cd commit 6659cb6

30 files changed

Lines changed: 211 additions & 78 deletions

‎e2e/quality-backlog.spec.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,58 @@ test('Flowchart deletes a selected node at once and Undo puts it back', async ({
104104
await page.getByRole('button', { name: 'Undo' }).click();
105105
await expect(page.getByText('Argument 1', { exact: true })).toHaveCount(1);
106106
});
107+
108+
// VS-05: a long motion and long item titles once widened the dashboard past a phone screen.
109+
for (const locale of locales) {
110+
test(`Current Debate fits a 360px phone with sources attached in ${locale}`, { tag: '@smoke' }, async ({ page }) => {
111+
const motion = 'This House believes that national governments should nationalise all critical digital infrastructure including undersea cables';
112+
await page.setViewportSize({ width: 360, height: 780 });
113+
await setLocale(page, locale);
114+
await stubSupabaseSession(page);
115+
await seedCurrentDebate(page, motion);
116+
await page.addInitScript(({ motion }) => {
117+
const debateId = '00000000-0000-4000-8000-00000000d001';
118+
const now = Date.now();
119+
const sources = [0, 1, 2].map((index) => ({
120+
id: `source-${index}`, title: `${motion} source ${index}`, content: 'Evidence', type: 'text',
121+
debateId, createdAt: now - index, isActive: true, folder: 'General',
122+
}));
123+
const savedCases = [0, 1].map((index) => ({
124+
id: `case-${index}`, title: `${motion} case ${index}`, data: { motion }, createdAt: now - index,
125+
debateId, side: index ? 'con' : 'pro',
126+
}));
127+
window.localStorage.setItem('debate-trainer-storage', JSON.stringify({
128+
state: { ownerUserId: '00000000-0000-0000-0000-000000000001', sourceLibrary: sources, savedCases, sourceFolders: ['General'] },
129+
version: 1,
130+
}));
131+
}, { motion });
132+
133+
await page.goto('/app/debate');
134+
await expect(page.getByTestId('debate-sources').getByText(`${motion} source 0`)).toBeVisible();
135+
// The Load button stays on screen beside its truncated title.
136+
const load = await page.getByTestId('debate-side-pro').getByRole('button', { name: `${motion} case 0` }).boundingBox();
137+
expect(load).not.toBeNull();
138+
expect(load!.x + load!.width).toBeLessThanOrEqual(360);
139+
140+
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(360);
141+
});
142+
}
143+
144+
// VS-09: picking a language closes the menu; the choice already persists.
145+
test('language menu closes once a language is chosen', { tag: '@smoke' }, async ({ page }) => {
146+
await setLocale(page, 'en');
147+
await page.goto('/');
148+
// Below lg the section links and the language control sit in the menu.
149+
const language = page.getByRole('button', { name: 'Language' });
150+
const openMenu = page.getByRole('button', { name: 'Open menu' });
151+
await expect(language.or(openMenu).first()).toBeVisible();
152+
if (await openMenu.isVisible()) await openMenu.click();
153+
await language.click();
154+
const menu = page.getByRole('radiogroup', { name: 'Interface language' });
155+
await expect(menu).toBeVisible();
156+
157+
await menu.getByRole('radio', { name: 'Magyar' }).click();
158+
159+
await expect(page.getByRole('radiogroup')).toHaveCount(0);
160+
await expect(page.locator('html')).toHaveAttribute('lang', 'hu');
161+
});

‎public/favicon-72.png‎

3.92 KB
Loading

‎src/components/LanguageSwitcher.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState } from 'react';
12
import { useTranslation } from 'react-i18next';
23
import { Check, Globe } from 'lucide-react';
34
import { useSettingsStore } from '@/lib/stores/settings-store';
@@ -38,13 +39,18 @@ function LanguageChoices({ label, value, onChange }: { label: string; value: App
3839
);
3940
}
4041

41-
/** Two taps to change language: open, pick. The AI output language sits underneath. */
42+
/** Two taps to change language: open, pick. The menu closes on the pick; the AI output language sits underneath. */
4243
export function LanguageSwitcher() {
4344
const { t } = useTranslation();
4445
const { language, outputLanguage, setLanguage, setOutputLanguage } = useSettingsStore();
46+
const [open, setOpen] = useState(false);
47+
const choose = (set: (language: AppLanguage) => void) => (next: AppLanguage) => {
48+
set(next);
49+
setOpen(false);
50+
};
4551

4652
return (
47-
<Popover>
53+
<Popover open={open} onOpenChange={setOpen}>
4854
<HeaderTooltip label={t('langSwitcher.title', 'Language')}>
4955
<PopoverTrigger asChild>
5056
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground" aria-label={t('langSwitcher.label', 'Language')}>
@@ -53,9 +59,9 @@ export function LanguageSwitcher() {
5359
</PopoverTrigger>
5460
</HeaderTooltip>
5561
<PopoverContent align="end" className="w-60 space-y-3 p-2">
56-
<LanguageChoices label={t('langSwitcher.uiLanguage', 'Interface language')} value={language} onChange={setLanguage} />
62+
<LanguageChoices label={t('langSwitcher.uiLanguage', 'Interface language')} value={language} onChange={choose(setLanguage)} />
5763
<div className="border-t" />
58-
<LanguageChoices label={t('langSwitcher.outputLanguage', 'AI output language')} value={outputLanguage} onChange={setOutputLanguage} />
64+
<LanguageChoices label={t('langSwitcher.outputLanguage', 'AI output language')} value={outputLanguage} onChange={choose(setOutputLanguage)} />
5965
</PopoverContent>
6066
</Popover>
6167
);

‎src/components/ModelSelector.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ export function ModelSelector({ feature, className, showLabel = false }: ModelSe
107107
setFeatureConfig(feature, { reasoningEffort: value });
108108
}}
109109
>
110+
{/* Locked, not broken: the dimmed field keeps its value readable (4.5:1) instead of fading to 50%. */}
110111
<SelectTrigger
111-
className="h-10 w-full text-sm"
112+
className="h-10 w-full text-sm disabled:bg-muted/50 disabled:text-muted-foreground disabled:opacity-100"
112113
aria-label={t('modelSelector.reasoning', 'Reasoning')}
113114
>
114115
<SelectValue />

‎src/components/features/debate/CurrentDebateMenu.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function CurrentDebateMenu({ variant }: { variant: 'desktop' | 'mobile' }
2929

3030
const itemClass = cn(
3131
'flex min-w-0 items-center gap-1.5 rounded-md py-2 text-sm font-medium transition-colors pointer-coarse:min-h-11 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
32-
active ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-accent hover:text-foreground',
32+
active ? 'bg-primary/5 text-primary' : 'text-muted-foreground hover:bg-accent hover:text-foreground',
3333
);
3434

3535
if (!debate) {

‎src/components/features/debate/DebateContextControl.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ function SourceRow({ source, onChange }: { source: ContextSourceItem; onChange:
234234
const note = source.included ? statusNote(source, t) : (!source.active ? t('debateContext.inactive', 'Turned off in your Library.') : null);
235235
return (
236236
<li className="flex items-start gap-3 rounded-md px-1 py-1.5 pointer-coarse:min-h-11">
237-
<Checkbox id={id} checked={source.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5" />
237+
<Checkbox id={id} checked={source.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
238238
<label htmlFor={id} className="min-w-0 flex-1 cursor-pointer text-sm">
239239
<span className="block break-words">{source.title}</span>
240240
{note && <span className="block text-xs text-muted-foreground">{note}</span>}
@@ -250,7 +250,7 @@ function OutputRow({ output, onChange, onPin, pinDisabled }: { output: ContextOu
250250
const kind = t(`debate.kinds.${output.ref.kind}`);
251251
return (
252252
<li className="flex items-start gap-3 rounded-md px-1 py-1.5 pointer-coarse:min-h-11">
253-
<Checkbox id={id} checked={output.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5" />
253+
<Checkbox id={id} checked={output.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
254254
<label htmlFor={id} className="min-w-0 flex-1 cursor-pointer text-sm">
255255
<span className="block break-words">{output.title || kind}</span>
256256
<span className="block text-xs text-muted-foreground">

‎src/components/free-tools/ToolPageLayout.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Footer from '@/components/landing/Footer';
55
import { usePageHead } from '@/lib/seo/use-page-head';
66
import { jsonLd, type FaqEntry, type ToolRoute } from '@/lib/seo/tool-pages';
77
import { cn } from '@/lib/utils';
8+
import { MAIN_CONTENT_ID, SkipLink } from '@/components/layout/SkipLink';
89

910
export const FREE_TOOLS = [
1011
{ route: '/tools/debate-timer', name: 'Debate timer', blurb: 'Round timer for PF, LD, Policy, BP, World Schools and Asian Parliamentary, with protected time, POI knocks and prep banks.' },
@@ -98,6 +99,7 @@ export function ToolPageLayout({ route, title, intro, shortAnswer, tool, childre
9899

99100
return (
100101
<div className="min-h-screen bg-background">
102+
<SkipLink />
101103
<header className="border-b border-border/60 print:hidden">
102104
<div className="container flex h-14 items-center justify-between gap-4 px-4">
103105
<Link to="/" className="flex items-center gap-2 rounded-md pointer-coarse:min-h-11">
@@ -111,7 +113,7 @@ export function ToolPageLayout({ route, title, intro, shortAnswer, tool, childre
111113
</Link>
112114
</div>
113115
</header>
114-
<main className="container mx-auto px-4 pb-16 pt-6">
116+
<main id={MAIN_CONTENT_ID} className="container mx-auto px-4 pb-16 pt-6 focus-visible:outline-none">
115117
<nav aria-label={t('layout.breadcrumb')} className="text-sm text-muted-foreground print:hidden">
116118
<ol className="flex flex-wrap items-center gap-1">
117119
<li><Link to="/" className="hover:text-foreground">DebateLab</Link></li>

‎src/components/landing/Footer.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,20 @@ const Footer = () => {
2323
</div>
2424

2525
<nav aria-label={t('footer.navLabel')} className="flex flex-wrap items-center justify-center gap-4 text-sm">
26-
<Link to="/tools" className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11">
26+
<Link to="/tools" className="inline-flex min-h-6 items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:justify-center">
2727
{t('tools:footer.freeTools')}
2828
</Link>
2929
{/* Static guide pages, not app routes: plain links load them from the server. */}
30-
<a href="/debatelab-pricing" className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11">
30+
<a href="/debatelab-pricing" className="inline-flex min-h-6 items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:justify-center">
3131
{t('footer.pricingGuide')}
3232
</a>
33-
<a href="/best-ai-debate-tools" className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11">
33+
<a href="/best-ai-debate-tools" className="inline-flex min-h-6 items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:justify-center">
3434
{t('footer.compareTools')}
3535
</a>
36-
<Link to="/terms" className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11">
36+
<Link to="/terms" className="inline-flex min-h-6 items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:justify-center">
3737
{t('footer.terms')}
3838
</Link>
39-
<Link to="/privacy" className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11">
39+
<Link to="/privacy" className="inline-flex min-h-6 items-center text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:justify-center">
4040
{t('footer.privacy')}
4141
</Link>
4242
</nav>

‎src/components/landing/Navbar.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Navbar = () => {
3737
>
3838
<div className="section-container">
3939
<div className="flex h-16 items-center justify-between md:h-20">
40-
<Link to="/" className="group flex items-center gap-2 pointer-coarse:min-h-11">
40+
<Link to="/" className="group flex shrink-0 items-center gap-2 rounded-lg pr-1 pointer-coarse:min-h-11">
4141
<img
4242
src="/favicon-72.png"
4343
alt={t('common.logoAlt')}
@@ -48,19 +48,19 @@ const Navbar = () => {
4848
<span className="text-xl font-bold text-foreground">DebateLab</span>
4949
</Link>
5050

51-
<div className="hidden items-center gap-7 md:flex">
51+
<div className="hidden items-center gap-7 lg:flex">
5252
{navLinks.map((link) => (
5353
<a
5454
key={link.href}
5555
href={link.href}
56-
className="text-sm font-medium text-muted-foreground transition-colors duration-200 hover:text-foreground focus-visible:text-foreground rounded-sm"
56+
className="inline-flex min-h-6 items-center rounded-sm text-sm font-medium text-muted-foreground transition-colors duration-200 hover:text-foreground focus-visible:text-foreground"
5757
>
5858
{link.label}
5959
</a>
6060
))}
6161
</div>
6262

63-
<div className="hidden items-center gap-3 md:flex">
63+
<div className="hidden items-center gap-3 lg:flex">
6464
<LanguageSwitcher />
6565
<Link to="/app" {...appPrefetch} className="btn-primary-gradient inline-flex items-center gap-2 text-sm">
6666
<span className="relative z-10">{t('nav.startTrial')}</span>
@@ -71,7 +71,7 @@ const Navbar = () => {
7171
<button
7272
type="button"
7373
onClick={() => setIsOpen(!isOpen)}
74-
className="flex h-11 w-11 items-center justify-center rounded-md text-foreground md:hidden"
74+
className="flex h-11 w-11 items-center justify-center rounded-md text-foreground lg:hidden"
7575
aria-label={isOpen ? t('nav.closeMenu') : t('nav.openMenu')}
7676
aria-expanded={isOpen}
7777
>
@@ -82,7 +82,7 @@ const Navbar = () => {
8282

8383
{isOpen && (
8484
// Opacity and a 4px drop in 180ms; reduced motion keeps the fade only.
85-
<div className="border-b border-border bg-background md:hidden animate-in fade-in-0 slide-in-from-top-1 duration-180 ease-out-strong">
85+
<div className="border-b border-border bg-background lg:hidden animate-in fade-in-0 slide-in-from-top-1 duration-180 ease-out-strong">
8686
<div className="section-container flex flex-col gap-1 py-4">
8787
{navLinks.map((link) => (
8888
<a

‎src/components/landing/Pricing.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const Pricing = () => {
4242
<div className="h-full rounded-2xl border border-border/70 bg-card p-7 shadow-sm transition-[border-color,box-shadow] duration-200 hover:border-primary/30 hover:shadow-lg">
4343
<div className="mb-4 flex items-center justify-between">
4444
<h3 className="text-2xl font-semibold text-foreground">{t('pricing.trial')}</h3>
45-
<span className="rounded-full border border-border/70 bg-secondary/60 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
45+
<span className="rounded-full border border-border/70 bg-muted px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
4646
{t('pricing.trialBadge')}
4747
</span>
4848
</div>

0 commit comments

Comments
 (0)