Skip to content

feat(ui): LanguageSelector component with search + RadioGroup + Tabs#84

Merged
qnbs merged 4 commits into
mainfrom
feat/ui-modernization-language-selector-2026-06-06
Jun 6, 2026
Merged

feat(ui): LanguageSelector component with search + RadioGroup + Tabs#84
qnbs merged 4 commits into
mainfrom
feat/ui-modernization-language-selector-2026-06-06

Conversation

@qnbs

@qnbs qnbs commented Jun 6, 2026

Copy link
Copy Markdown
Owner

User description

UI Modernization - Phase 1

Added

  • LanguageSelector.tsx: Modern combobox with search functionality, flag emojis, RTL support, and reduced-motion awareness
  • RadioGroup.tsx: Accessible radio group component with proper ARIA attributes
  • Tabs.tsx: WAI-ARIA compliant tabs component with multiple variants (default, pills, underline)

Changed

  • WelcomePortal.tsx: Now uses the new LanguageSelector component instead of inline language buttons
  • SettingsShared.tsx: ToggleSwitch optimized for RTL with reduced-motion support

i18n

  • Added portal.language.searchPlaceholder translation key to all 11 locales (de, en, fr, es, it, ar, he, ja, zh, pt, el)

Design Principles

  • All components use semantic design tokens (--sc-*)
  • RTL-aware with [dir="rtl"] support
  • WCAG 2.2 AA compliant
  • Reduced motion support via CSS media queries

Testing

  • Ready for local lint/typecheck verification
  • Will await CI green before merge

CodeAnt-AI Description

Add searchable language pickers and reusable tab/radio controls

What Changed

  • The welcome screen now uses a searchable language menu with flags, native language names, and beta markers instead of a row of fixed language buttons
  • Language selection popovers now close when clicking away or pressing Escape, and the search field is focused when the menu opens
  • The settings toggle keeps working in right-to-left layouts without moving in the wrong direction, and its motion is simplified
  • New tabs and radio groups are available with accessible keyboard and screen reader behavior
  • The language search prompt was added in all supported locales

Impact

✅ Faster language switching
✅ Easier language finding for multilingual users
✅ Fewer right-to-left layout issues

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai

codeant-ai Bot commented Jun 6, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
storycraft-studio Ready Ready Preview, Comment Jun 6, 2026 3:13pm

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Jun 6, 2026
Comment thread components/settings/SettingsShared.tsx Outdated
type="button"
role="switch"
aria-checked={checked}
aria-label={ariaLabel ?? label}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Ensure the switch always has an accessible name by enforcing that at least one of label or ariaLabel is required, so the rendered control is never unnamed. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The switch can be rendered with both label and ariaLabel missing, because both props are optional. In that case aria-label becomes undefined and aria-labelledby is also omitted, leaving the role="switch" control unnamed. This is a real accessibility issue, so the suggestion is verified.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/settings/SettingsShared.tsx
**Line:** 22:22
**Comment:**
	*Custom Rule: Ensure the switch always has an accessible name by enforcing that at least one of `label` or `ariaLabel` is required, so the rendered control is never unnamed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

return (
<div
role="radiogroup"
aria-label={name}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Ensure the radiogroup always has a guaranteed accessible name by requiring a non-optional labeling prop (or a required labelled-by reference) instead of relying on an optional value that can be undefined. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The component passes an optional name prop directly to aria-label, so the radiogroup can render without an accessible name when name is undefined. That is a real accessibility issue, so the suggestion correctly identifies a violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/RadioGroup.tsx
**Line:** 33:33
**Comment:**
	*Custom Rule: Ensure the radiogroup always has a guaranteed accessible name by requiring a non-optional labeling prop (or a required labelled-by reference) instead of relying on an optional value that can be undefined.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread components/ui/Tabs.tsx
Comment on lines +48 to +49
aria-controls={`${groupId}-${tab.id}-panel`}
id={`${groupId}-${tab.id}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Make the tab-to-panel ARIA linkage consistent so each tab id matches the corresponding panel aria-labelledby and each tab aria-controls points to the actual panel id. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The tab's aria-controls references ${groupId}-${tab.id}-panel, but the corresponding TabPanel in this file uses id={${tabId}-panel} without the groupId prefix. That means the tab is pointing at a non-matching panel id, so the accessibility linkage is inconsistent and the violation is real.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/Tabs.tsx
**Line:** 48:49
**Comment:**
	*Custom Rule: Make the tab-to-panel ARIA linkage consistent so each tab `id` matches the corresponding panel `aria-labelledby` and each tab `aria-controls` points to the actual panel `id`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread components/ui/RadioGroup.tsx Outdated
<input
type="radio"
id={itemId}
name={groupId}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The component accepts a name prop but ignores it and forces every input to use groupId as the radio name. This breaks caller expectations for form field naming and standard form submission contracts. Pass through the provided name (with a fallback) instead of always using groupId. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Forms using RadioGroup submit unexpected field names.
- ⚠️ Server-side validation based on field names may fail.
Steps of Reproduction ✅
1. Open `components/ui/RadioGroup.tsx` and note the `RadioGroupProps` definition with
optional `name` prop at lines 10–16 and the component implementation at lines 19–91.

2. Observe that the `name` prop is destructured at line 24 but never used when rendering
the `<input type="radio">` elements at lines 42–60.

3. Specifically, each radio input uses `name={groupId}` at line 45, where `groupId` is
generated via `useId()` at line 28, instead of using the caller-provided `name` prop.

4. To reproduce in a real UI, create any form component that imports `RadioGroup` from
`components/ui/RadioGroup.tsx` and renders it inside a `<form>` with
`name="preferredLanguage"`, then inspect the DOM: all `<input type="radio">` elements will
have the auto-generated `groupId` as their `name` attribute, not `"preferredLanguage"`, so
form submission will not contain the expected field key.

5. A `Grep` search for `RadioGroup` across `*.tsx` files shows no current callers outside
`components/ui/RadioGroup.tsx`, meaning this is a latent API bug that will affect the
first consumer that relies on HTML form naming semantics.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/RadioGroup.tsx
**Line:** 45:45
**Comment:**
	*Api Mismatch: The component accepts a `name` prop but ignores it and forces every input to use `groupId` as the radio name. This breaks caller expectations for form field naming and standard form submission contracts. Pass through the provided `name` (with a fallback) instead of always using `groupId`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread components/ui/Tabs.tsx Outdated
@@ -0,0 +1,88 @@
import type React from 'react';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: React is imported as a type-only import but then used as a runtime value via React.memo. Type-only imports are erased at runtime, so this can crash when evaluating the module. Import React as a normal value (or import memo directly) before using it in executable code. [import error]

Severity Level: Critical 🚨
- ❌ Any route importing Tabs crashes during rendering.
- ⚠️ Blocks adoption of new Tabs component across app.
Steps of Reproduction ✅
1. Open `components/ui/Tabs.tsx` and note the type-only import `import type React from
'react';` at line 1 and the component definition using `React.memo` at lines 18–62 and
72–87.

2. Because `React` is imported with `import type`, TypeScript erases this binding in the
emitted JavaScript when `importsNotUsedAsValues` is configured (the common default), so
there is no runtime `React` variable.

3. Create any component (e.g., `SomePage.tsx`) that imports `{ Tabs }` from
`components/ui/Tabs.tsx` and renders `<Tabs>`; this forces the `Tabs` module to be
evaluated at runtime.

4. When the module executes, `React.memo(...)` at line 18 is evaluated, but `React` is
`undefined` because the import was erased, causing a `ReferenceError: React is not
defined` and crashing the route where `<Tabs>` is used.

5. A `Grep` search for `Tabs` shows the only `Tabs` symbol defined in
`components/ui/Tabs.tsx` and no external callers yet, so the crash will occur immediately
once any consumer starts using this new component.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/Tabs.tsx
**Line:** 1:1
**Comment:**
	*Import Error: `React` is imported as a type-only import but then used as a runtime value via `React.memo`. Type-only imports are erased at runtime, so this can crash when evaluating the module. Import `React` as a normal value (or import `memo` directly) before using it in executable code.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread components/ui/Tabs.tsx
type="button"
role="tab"
aria-selected={isActive}
aria-controls={`${groupId}-${tab.id}-panel`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The tab button aria-controls points to an ID prefixed with groupId, but TabPanel IDs are generated without that prefix, so the control relationship is broken. Align both components to generate the same panel ID format. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Screen readers cannot connect tabs to their panels.
- ⚠️ Accessibility of new Tabs component significantly reduced.
Steps of Reproduction ✅
1. Open `components/ui/Tabs.tsx` and inspect the `Tabs` component at lines 18–62: each
`<button role="tab">` is rendered inside the `tabs.map` loop at lines 40–57.

2. At line 48, each tab button sets `aria-controls={`${groupId}-${tab.id}-panel`}`, where
`groupId` comes from `useId()` at line 20 and `tab.id` is the logical tab identifier.

3. In the same file, inspect the `TabPanel` component at lines 72–87: it renders a
`<section role="tabpanel">` with `id={`${tabId}-panel`}` at line 78, without any `groupId`
prefix.

4. Use these components together in any page (e.g., import `{ Tabs, TabPanel }` from
`components/ui/Tabs.tsx` and render `<Tabs tabs={[{ id: 'one', label: 'One' }]} ... />`
plus `<TabPanel tabId="one" activeTab="one">…</TabPanel>`); when you inspect the DOM, each
tab button will have `aria-controls="react-<id>-one-panel"` while the corresponding panel
has `id="one-panel"`, so there is no element with the ID referenced by `aria-controls`.

5. This mismatch means assistive technologies cannot follow the tab-to-panel relationship;
`Grep` confirms `TabPanel` is only defined in `components/ui/Tabs.tsx` currently, so this
is a latent accessibility bug that will appear for the first consumer of these components.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/Tabs.tsx
**Line:** 48:48
**Comment:**
	*Api Mismatch: The tab button `aria-controls` points to an ID prefixed with `groupId`, but `TabPanel` IDs are generated without that prefix, so the control relationship is broken. Align both components to generate the same panel ID format.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread components/ui/Tabs.tsx Outdated
<section
role="tabpanel"
id={`${tabId}-panel`}
aria-labelledby={tabId}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The panel uses aria-labelledby={tabId}, but the corresponding tab button ID is generated as ${groupId}-${tab.id}, so the panel label reference never resolves. Use the exact tab button ID format when setting aria-labelledby. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Panels lack accessible labels for screen readers.
- ⚠️ Violates WAI-ARIA expectations for tabpanel labelling.
Steps of Reproduction ✅
1. In `components/ui/Tabs.tsx`, inspect the tab button markup inside `Tabs` at lines
40–57: each button sets `id={`${groupId}-${tab.id}`}` at line 49, combining the internal
`groupId` with the logical `tab.id`.

2. In the same file, inspect the `TabPanel` component at lines 72–87: the `<section
role="tabpanel">` sets `aria-labelledby={tabId}` at line 79, where `tabId` is the
`TabPanelProps.tabId` value passed by the caller.

3. Use `Tabs` and `TabPanel` together (e.g., `<Tabs tabs={[{ id: 'one', label: 'One' }]}
... />` and `<TabPanel tabId="one" activeTab="one">…</TabPanel>`): the tab button will
have `id="react-<id>-one"` while the panel will have `aria-labelledby="one"`.

4. Because no element has `id="one"`—the actual tab's ID includes the `groupId`
prefix—`aria-labelledby` on the panel points to a non-existent label element, so screen
readers do not correctly associate the panel with its tab label.

5. `Grep` confirms `TabPanel` has no external callers yet, so this is an accessibility
wiring bug that will manifest as soon as any feature adopts the new tab components.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** components/ui/Tabs.tsx
**Line:** 79:79
**Comment:**
	*Api Mismatch: The panel uses `aria-labelledby={tabId}`, but the corresponding tab button ID is generated as `${groupId}-${tab.id}`, so the panel label reference never resolves. Use the exact tab button ID format when setting `aria-labelledby`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented Jun 6, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@qnbs
qnbs force-pushed the feat/ui-modernization-language-selector-2026-06-06 branch 2 times, most recently from 3363916 to 947c5f0 Compare June 6, 2026 14:35
- Add LanguageSelector.tsx: Modern combobox with search, flag icons, RTL support
- Add RadioGroup.tsx: Accessible radio group component with proper ARIA
- Add Tabs.tsx: WAI-ARIA compliant tabs component
- Update WelcomePortal.tsx: Use new LanguageSelector component
- Update SettingsShared.tsx: RTL-aware ToggleSwitch with reduced-motion support
- Add translation key 'portal.language.searchPlaceholder' to all 11 locales
@qnbs
qnbs force-pushed the feat/ui-modernization-language-selector-2026-06-06 branch from 947c5f0 to d5284a8 Compare June 6, 2026 14:38
@qnbs qnbs self-assigned this Jun 6, 2026
@qnbs
qnbs enabled auto-merge (squash) June 6, 2026 14:45
@qnbs
qnbs disabled auto-merge June 6, 2026 14:45
@qnbs
qnbs enabled auto-merge (squash) June 6, 2026 14:45
qnbs added 3 commits June 6, 2026 14:51
…RadioGroup, Tabs

- ToggleSwitch: Ensure accessible name fallback to 'Toggle' when both label and ariaLabel are missing
- RadioGroup: Make 'name' prop required for guaranteed accessible name on radiogroup
- Tabs: Make 'ariaLabel' prop required instead of hardcoded 'Tabs' string

QNBS-v3: WCAG 2.2 AA compliance fixes
- RadioGroup: Pass through 'name' prop to input elements (was ignored)
- Tabs: Add groupId prop to TabPanel for consistent ARIA linkage
- Tabs: aria-controls and aria-labelledby now use matching IDs

QNBS-v3: WCAG 2.2 AA compliance - API mismatch fixes
- LanguageSelector, RadioGroup, Tabs components completed
- All 6 CodeAnt AI accessibility issues fixed
- Ready for CI verification and merge
@qnbs
qnbs force-pushed the feat/ui-modernization-language-selector-2026-06-06 branch from f490f4c to 907dbd7 Compare June 6, 2026 15:12
@qnbs
qnbs disabled auto-merge June 6, 2026 18:04
@qnbs
qnbs enabled auto-merge (squash) June 6, 2026 18:05
@qnbs
qnbs disabled auto-merge June 6, 2026 18:05
@qnbs
qnbs merged commit 224ea7f into main Jun 6, 2026
16 checks passed
@qnbs
qnbs deleted the feat/ui-modernization-language-selector-2026-06-06 branch June 6, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant