Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions e2e/quality-backlog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,58 @@ 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: `${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 }) => {
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 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');
});
Binary file modified public/favicon-72.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 10 additions & 4 deletions src/components/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<Popover>
<Popover open={open} onOpenChange={setOpen}>
<HeaderTooltip label={t('langSwitcher.title', 'Language')}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground" aria-label={t('langSwitcher.label', 'Language')}>
Expand All @@ -53,9 +59,9 @@ export function LanguageSwitcher() {
</PopoverTrigger>
</HeaderTooltip>
<PopoverContent align="end" className="w-60 space-y-3 p-2">
<LanguageChoices label={t('langSwitcher.uiLanguage', 'Interface language')} value={language} onChange={setLanguage} />
<LanguageChoices label={t('langSwitcher.uiLanguage', 'Interface language')} value={language} onChange={choose(setLanguage)} />
<div className="border-t" />
<LanguageChoices label={t('langSwitcher.outputLanguage', 'AI output language')} value={outputLanguage} onChange={setOutputLanguage} />
<LanguageChoices label={t('langSwitcher.outputLanguage', 'AI output language')} value={outputLanguage} onChange={choose(setOutputLanguage)} />
</PopoverContent>
</Popover>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ export function ModelSelector({ feature, className, showLabel = false }: ModelSe
setFeatureConfig(feature, { reasoningEffort: value });
}}
>
{/* Locked, not broken: the dimmed field keeps its value readable (4.5:1) instead of fading to 50%. */}
<SelectTrigger
className="h-10 w-full text-sm"
className="h-10 w-full text-sm disabled:bg-muted/50 disabled:text-muted-foreground disabled:opacity-100"
aria-label={t('modelSelector.reasoning', 'Reasoning')}
>
<SelectValue />
Expand Down
2 changes: 1 addition & 1 deletion src/components/features/debate/CurrentDebateMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function CurrentDebateMenu({ variant }: { variant: 'desktop' | 'mobile' }

const itemClass = cn(
'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',
active ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-accent hover:text-foreground',
active ? 'bg-primary/5 text-primary' : 'text-muted-foreground hover:bg-accent hover:text-foreground',
);

if (!debate) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/features/debate/DebateContextControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<li className="flex items-start gap-3 rounded-md px-1 py-1.5 pointer-coarse:min-h-11">
<Checkbox id={id} checked={source.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5" />
<Checkbox id={id} checked={source.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
<label htmlFor={id} className="min-w-0 flex-1 cursor-pointer text-sm">
<span className="block break-words">{source.title}</span>
{note && <span className="block text-xs text-muted-foreground">{note}</span>}
Expand All @@ -250,7 +250,7 @@ function OutputRow({ output, onChange, onPin, pinDisabled }: { output: ContextOu
const kind = t(`debate.kinds.${output.ref.kind}`);
return (
<li className="flex items-start gap-3 rounded-md px-1 py-1.5 pointer-coarse:min-h-11">
<Checkbox id={id} checked={output.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5" />
<Checkbox id={id} checked={output.included} onCheckedChange={(checked) => onChange(checked === true)} className="mt-0.5 pointer-coarse:after:-top-2" />
<label htmlFor={id} className="min-w-0 flex-1 cursor-pointer text-sm">
<span className="block break-words">{output.title || kind}</span>
<span className="block text-xs text-muted-foreground">
Expand Down
4 changes: 3 additions & 1 deletion src/components/free-tools/ToolPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Footer from '@/components/landing/Footer';
import { usePageHead } from '@/lib/seo/use-page-head';
import { jsonLd, type FaqEntry, type ToolRoute } from '@/lib/seo/tool-pages';
import { cn } from '@/lib/utils';
import { MAIN_CONTENT_ID, SkipLink } from '@/components/layout/SkipLink';

export const FREE_TOOLS = [
{ 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.' },
Expand Down Expand Up @@ -98,6 +99,7 @@ export function ToolPageLayout({ route, title, intro, shortAnswer, tool, childre

return (
<div className="min-h-screen bg-background">
<SkipLink />
<header className="border-b border-border/60 print:hidden">
<div className="container flex h-14 items-center justify-between gap-4 px-4">
<Link to="/" className="flex items-center gap-2 rounded-md pointer-coarse:min-h-11">
Expand All @@ -111,7 +113,7 @@ export function ToolPageLayout({ route, title, intro, shortAnswer, tool, childre
</Link>
</div>
</header>
<main className="container mx-auto px-4 pb-16 pt-6">
<main id={MAIN_CONTENT_ID} className="container mx-auto px-4 pb-16 pt-6 focus-visible:outline-none">
<nav aria-label={t('layout.breadcrumb')} className="text-sm text-muted-foreground print:hidden">
<ol className="flex flex-wrap items-center gap-1">
<li><Link to="/" className="hover:text-foreground">DebateLab</Link></li>
Expand Down
10 changes: 5 additions & 5 deletions src/components/landing/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ const Footer = () => {
</div>

<nav aria-label={t('footer.navLabel')} className="flex flex-wrap items-center justify-center gap-4 text-sm">
<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">
<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">
{t('tools:footer.freeTools')}
</Link>
{/* Static guide pages, not app routes: plain links load them from the server. */}
<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">
<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">
{t('footer.pricingGuide')}
</a>
<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">
<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">
{t('footer.compareTools')}
</a>
<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">
<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">
{t('footer.terms')}
</Link>
<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">
<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">
{t('footer.privacy')}
</Link>
</nav>
Expand Down
12 changes: 6 additions & 6 deletions src/components/landing/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const Navbar = () => {
>
<div className="section-container">
<div className="flex h-16 items-center justify-between md:h-20">
<Link to="/" className="group flex items-center gap-2 pointer-coarse:min-h-11">
<Link to="/" className="group flex shrink-0 items-center gap-2 rounded-lg pr-1 pointer-coarse:min-h-11">
<img
src="/favicon-72.png"
alt={t('common.logoAlt')}
Expand All @@ -48,19 +48,19 @@ const Navbar = () => {
<span className="text-xl font-bold text-foreground">DebateLab</span>
</Link>

<div className="hidden items-center gap-7 md:flex">
<div className="hidden items-center gap-7 lg:flex">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-muted-foreground transition-colors duration-200 hover:text-foreground focus-visible:text-foreground rounded-sm"
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"
>
{link.label}
</a>
))}
</div>

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

{isOpen && (
// Opacity and a 4px drop in 180ms; reduced motion keeps the fade only.
<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">
<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">
<div className="section-container flex flex-col gap-1 py-4">
{navLinks.map((link) => (
<a
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/Pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Pricing = () => {
<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">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-2xl font-semibold text-foreground">{t('pricing.trial')}</h3>
<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">
<span className="rounded-full border border-border/70 bg-muted px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t('pricing.trialBadge')}
</span>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/UseCases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const UseCases = () => {
<persona.icon aria-hidden="true" className="w-7 h-7 text-primary" />
</div>
<h3 className="text-2xl font-bold mb-1">{persona.title}</h3>
<p className="text-primary/80 font-medium mb-6">{persona.subtitle}</p>
<p className="text-primary font-medium mb-6">{persona.subtitle}</p>

<ul className="space-y-3">
{persona.points.map((point, i) => (
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/WhatsNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const WhatsNew = () => {
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-primary/20 bg-primary/10">
<update.icon aria-hidden="true" className="h-5 w-5 text-primary" />
</div>
<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">
<span className="rounded-full border border-border/70 bg-muted px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{update.badge}
</span>
</div>
Expand Down
Loading
Loading