fix(a11y): Add missing ARIA attributes for screen reader accessibility (#634)

* fix(a11y): add missing ARIA attributes for screen reader accessibility

Add comprehensive ARIA attributes across frontend components:

- aria-label on icon-only buttons (close, edit, delete, add, refresh)
- aria-required on required form fields (description, title, phase)
- role="alert" on validation error messages
- aria-expanded/aria-controls on collapsible sections
- role="radiogroup" on button groups acting as radio selects

Components updated:
- GitHubSetupModal: repo action buttons, owner/visibility selection
- TaskCreationWizard: description, image removal, toggles
- TaskEditDialog: description, advanced/images toggles
- AddFeatureDialog: title, description, phase fields
- AddProjectModal: action buttons, error messages
- KanbanBoard: add task, archive buttons
- TaskHeader, FeatureDetailPanel, RoadmapHeader: icon buttons
- WelcomeScreen, Sidebar, ProjectTabBar: various buttons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): address PR review feedback

- Remove redundant aria-required from Select component (keep only on SelectTrigger)
- Replace hardcoded aria-label strings with i18n translation keys
- Add translation strings for en and fr locales

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): convert remaining hardcoded aria-labels to i18n

- Add useTranslation to components missing i18n support
- Convert all hardcoded aria-label strings to translation keys
- Add accessibility translation keys to common and tasks namespaces
- Add French translations for all new aria-label keys
- Update radiogroup aria-labels in GitHubSetupModal to use i18n

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): add screen reader indication for external links

- Add sr-only text indicating links open in new window
- Add aria-hidden to decorative ExternalLink icons
- Add aria-labels for external link buttons
- Update SafeLink component in Insights for markdown links
- Add translation keys for external link accessibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): improve CollapsibleSection and FileTreeItem accessibility

CollapsibleSection:
- Add type="button" to prevent form submission
- Add aria-expanded and aria-controls for screen readers
- Add aria-hidden to decorative chevron icons
- Use React useId hook for unique content IDs

FileTreeItem:
- Add keyboard support (Enter/Space) for directory toggle
- Add role="button" and tabIndex for keyboard focus
- Add aria-expanded state for directories
- Add aria-labels for expand/collapse actions with i18n
- Add focus ring styling for keyboard navigation
- Mark decorative icons as aria-hidden

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): add aria-labels to icon buttons in FileExplorer and IssueList

- Add aria-label to refresh and close buttons in FileExplorerPanel
- Add aria-label to refresh button in IssueListHeader
- Mark decorative icons as aria-hidden

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): convert TaskHeader edit button strings to i18n

- Replace hardcoded aria-label and tooltip text with translation keys
- Add editTask and cannotEditWhileRunning keys to en/fr locales

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): convert remaining hardcoded strings to i18n

- WelcomeScreen: convert project button aria-label to i18n
- TaskCreationWizard: add useTranslation and convert remove image aria-label
- TaskEditDialog: add useTranslation and convert paste hint to i18n
- Insights: refactor SafeLink to use factory pattern with translated
  "opens in new window" text

Added translation keys for en/fr:
- welcome:recentProjects.openProjectAriaLabel
- tasks:images.removeImageAriaLabel
- tasks:images.pasteHint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): make ClaudeCodeStatusBadge aria-label match visible text

The aria-label was "Learn more (opens in new window)" but the visible
text was "Learn more about Claude Code" - these didn't match.

Added specific translation key navigation:claudeCode.learnMoreAriaLabel
that includes the full visible text plus "(opens in new window)" suffix.
Removed redundant sr-only span since aria-label now provides the complete
accessible name.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): pass markdownComponents as prop to MessageBubble

After moving markdownComponents inside the Insights component for i18n
support, MessageBubble (defined outside Insights) couldn't access it.

- Import Components type from react-markdown
- Add markdownComponents prop to MessageBubbleProps
- Update MessageBubble to use the prop instead of free variable
- Pass markdownComponents from Insights when rendering MessageBubble

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): add fallback string to learnMoreAriaLabel translation

Added fallback string to match the pattern used elsewhere in the file,
ensuring the aria-label has a sensible default if translation is missing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): add aria-keyshortcuts to navigation sidebar buttons

Screen readers can now announce keyboard shortcuts (K, A, G, etc.)
when focusing on navigation items.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(a11y): clarify close tab button removes project from app

Screen readers now announce "Close tab (removes project from app)"
instead of just "Close tab" to match the confirmation dialog behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Merge develop

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Orinks
2026-01-05 13:46:54 -05:00
committed by GitHub
parent e2937320cf
commit 3930b12c41
31 changed files with 254 additions and 83 deletions
@@ -208,6 +208,7 @@ export function AddFeatureDialog({
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={isSaving}
aria-required="true"
/>
</div>
@@ -223,6 +224,7 @@ export function AddFeatureDialog({
onChange={(e) => setDescription(e.target.value)}
rows={3}
disabled={isSaving}
aria-required="true"
/>
</div>
@@ -253,7 +255,7 @@ export function AddFeatureDialog({
onValueChange={setPhaseId}
disabled={isSaving}
>
<SelectTrigger id="add-feature-phase">
<SelectTrigger id="add-feature-phase" aria-required="true">
<SelectValue placeholder={t('addFeature.selectPhase')} />
</SelectTrigger>
<SelectContent>
@@ -338,7 +340,7 @@ export function AddFeatureDialog({
{/* Error */}
{error && (
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive" role="alert">
<X className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
@@ -167,6 +167,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
'bg-card hover:bg-accent hover:border-accent transition-all duration-200',
'text-left group'
)}
aria-label={t('addProject.openExistingAriaLabel')}
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<FolderOpen className="h-6 w-6 text-primary" />
@@ -188,6 +189,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
'bg-card hover:bg-accent hover:border-accent transition-all duration-200',
'text-left group'
)}
aria-label={t('addProject.createNewAriaLabel')}
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-success/10">
<FolderPlus className="h-6 w-6 text-success" />
@@ -203,7 +205,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
</div>
{error && (
<div className="text-sm text-destructive bg-destructive/10 rounded-lg p-3 mt-2">
<div className="text-sm text-destructive bg-destructive/10 rounded-lg p-3 mt-2" role="alert">
{error}
</div>
)}
@@ -272,7 +274,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
</div>
{error && (
<div className="text-sm text-destructive bg-destructive/10 rounded-lg p-3">
<div className="text-sm text-destructive bg-destructive/10 rounded-lg p-3" role="alert">
{error}
</div>
)}
@@ -294,9 +294,10 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
size="sm"
className="w-full text-xs text-muted-foreground gap-1"
onClick={() => window.electronAPI?.openExternal?.('https://claude.ai/code')}
aria-label={t('navigation:claudeCode.learnMoreAriaLabel', 'Learn more about Claude Code (opens in new window)')}
>
{t('navigation:claudeCode.learnMore', 'Learn more about Claude Code')}
<ExternalLink className="h-3 w-3" />
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</Button>
</div>
</PopoverContent>
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { TrendingUp, ExternalLink, AlertCircle } from 'lucide-react';
import {
Dialog,
@@ -21,6 +22,8 @@ export function CompetitorAnalysisViewer({
open,
onOpenChange,
}: CompetitorAnalysisViewerProps) {
const { t } = useTranslation('common');
if (!analysis) return null;
return (
@@ -66,9 +69,11 @@ export function CompetitorAnalysisViewer({
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline flex items-center gap-1 text-sm ml-4"
aria-label={t('accessibility.visitExternalLink', { name: competitor.name })}
>
<ExternalLink className="h-3 w-3" />
<ExternalLink className="h-3 w-3" aria-hidden="true" />
Visit
<span className="sr-only">({t('accessibility.opensInNewWindow')})</span>
</a>
)}
</div>
@@ -1,4 +1,5 @@
import { motion, AnimatePresence } from 'motion/react';
import { useTranslation } from 'react-i18next';
import { X, FolderTree, RefreshCw } from 'lucide-react';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -34,6 +35,7 @@ const contentVariants = {
};
export function FileExplorerPanel({ projectPath }: FileExplorerPanelProps) {
const { t } = useTranslation('common');
const { isOpen, close, clearCache, loadDirectory } = useFileExplorerStore();
const handleRefresh = () => {
@@ -80,17 +82,18 @@ export function FileExplorerPanel({ projectPath }: FileExplorerPanelProps) {
size="icon"
className="h-6 w-6"
onClick={handleRefresh}
title="Refresh"
aria-label={t('buttons.refresh')}
>
<RefreshCw className="h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={close}
aria-label={t('buttons.close')}
>
<X className="h-3.5 w-3.5" />
<X className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
</div>
</div>
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect, type DragEvent } from 'react';
import { useState, useRef, useEffect, type DragEvent, type KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronRight, ChevronDown, Folder, File, FileCode, FileJson, FileText, FileImage, Loader2 } from 'lucide-react';
import { cn } from '../lib/utils';
import type { FileNode } from '../../shared/types';
@@ -70,6 +71,7 @@ export function FileTreeItem({
isLoading,
onToggle,
}: FileTreeItemProps) {
const { t } = useTranslation('common');
const [isDragging, setIsDragging] = useState(false);
const dragImageRef = useRef<HTMLDivElement | null>(null);
@@ -98,6 +100,16 @@ export function FileTreeItem({
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
if (node.isDirectory) {
onToggle();
}
}
};
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
e.stopPropagation();
setIsDragging(true);
@@ -147,37 +159,47 @@ export function FileTreeItem({
return (
<div
role={node.isDirectory ? 'button' : undefined}
tabIndex={node.isDirectory ? 0 : undefined}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onKeyDown={node.isDirectory ? handleKeyDown : undefined}
className={cn(
'flex items-center gap-1 py-1 px-2 rounded cursor-grab select-none',
'hover:bg-accent/50 transition-colors',
node.isDirectory && 'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
isDragging && 'opacity-50 bg-accent ring-2 ring-primary'
)}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
aria-label={node.isDirectory ? t('accessibility.toggleFolder', { name: node.name }) : undefined}
aria-expanded={node.isDirectory ? isExpanded : undefined}
>
{/* Expand/collapse chevron for directories */}
{node.isDirectory ? (
<button
type="button"
className="flex items-center justify-center w-4 h-4 hover:bg-accent rounded"
onClick={(e) => {
e.stopPropagation();
onToggle();
}}
aria-label={isExpanded ? t('accessibility.collapseFolder', { name: node.name }) : t('accessibility.expandFolder', { name: node.name })}
aria-expanded={isExpanded}
tabIndex={-1}
>
{isLoading ? (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" aria-hidden="true" />
) : isExpanded ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
<ChevronDown className="h-3 w-3 text-muted-foreground" aria-hidden="true" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
<ChevronRight className="h-3 w-3 text-muted-foreground" aria-hidden="true" />
)}
</button>
) : (
<span className="w-4" />
<span className="w-4" aria-hidden="true" />
)}
{/* Icon */}
@@ -489,6 +489,7 @@ export function GitHubSetupModal({
<button
onClick={() => setRepoAction('create')}
className="flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-dashed hover:border-primary hover:bg-primary/5 transition-colors"
aria-label={t('githubSetup.createRepoAriaLabel')}
>
<Plus className="h-8 w-8 text-muted-foreground" />
<span className="text-sm font-medium">Create New Repo</span>
@@ -499,6 +500,7 @@ export function GitHubSetupModal({
<button
onClick={() => setRepoAction('link')}
className="flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-dashed hover:border-primary hover:bg-primary/5 transition-colors"
aria-label={t('githubSetup.linkRepoAriaLabel')}
>
<Link className="h-8 w-8 text-muted-foreground" />
<span className="text-sm font-medium">Link Existing</span>
@@ -516,6 +518,7 @@ export function GitHubSetupModal({
<button
onClick={() => setRepoAction(null)}
className="text-primary hover:underline"
aria-label={t('githubSetup.goBackAriaLabel')}
>
Back
</button>
@@ -531,7 +534,7 @@ export function GitHubSetupModal({
Loading accounts...
</div>
) : (
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2" role="radiogroup" aria-label={t('common:accessibility.repositoryOwnerAriaLabel')}>
{/* Personal account */}
{githubUsername && (
<button
@@ -542,6 +545,9 @@ export function GitHubSetupModal({
: 'border-muted hover:border-primary/50'
}`}
disabled={isCreatingRepo}
role="radio"
aria-checked={selectedOwner === githubUsername}
aria-label={t('githubSetup.selectOwnerAriaLabel', { owner: githubUsername })}
>
<User className="h-4 w-4" />
<span className="text-sm">{githubUsername}</span>
@@ -558,6 +564,9 @@ export function GitHubSetupModal({
: 'border-muted hover:border-primary/50'
}`}
disabled={isCreatingRepo}
role="radio"
aria-checked={selectedOwner === org.login}
aria-label={t('githubSetup.selectOrgAriaLabel', { org: org.login })}
>
<Building className="h-4 w-4" />
<span className="text-sm">{org.login}</span>
@@ -591,7 +600,7 @@ export function GitHubSetupModal({
<div className="space-y-2">
<Label>Visibility</Label>
<div className="flex gap-2">
<div className="flex gap-2" role="radiogroup" aria-label={t('common:accessibility.repositoryVisibilityAriaLabel')}>
<button
onClick={() => setIsPrivateRepo(true)}
className={`flex items-center gap-2 px-3 py-2 rounded-md border ${
@@ -600,6 +609,9 @@ export function GitHubSetupModal({
: 'border-muted hover:border-primary/50'
}`}
disabled={isCreatingRepo}
role="radio"
aria-checked={isPrivateRepo}
aria-label={t('githubSetup.selectVisibilityAriaLabel', { visibility: 'private' })}
>
<Lock className="h-4 w-4" />
<span className="text-sm">Private</span>
@@ -612,6 +624,9 @@ export function GitHubSetupModal({
: 'border-muted hover:border-primary/50'
}`}
disabled={isCreatingRepo}
role="radio"
aria-checked={!isPrivateRepo}
aria-label={t('githubSetup.selectVisibilityAriaLabel', { visibility: 'public' })}
>
<Globe className="h-4 w-4" />
<span className="text-sm">Public</span>
@@ -628,6 +643,7 @@ export function GitHubSetupModal({
<button
onClick={() => setRepoAction(null)}
className="text-primary hover:underline"
aria-label={t('githubSetup.goBackAriaLabel')}
>
Back
</button>
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
MessageSquare,
Send,
@@ -15,7 +16,7 @@ import {
PanelLeftClose,
PanelLeft
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import ReactMarkdown, { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
@@ -46,42 +47,40 @@ import {
TASK_COMPLEXITY_COLORS
} from '../../shared/constants';
// Safe link renderer for ReactMarkdown to prevent phishing and ensure external links open safely
const SafeLink = ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
// Validate URL - only allow http, https, and relative links
const isValidUrl = href && (
href.startsWith('http://') ||
href.startsWith('https://') ||
href.startsWith('/') ||
href.startsWith('#')
);
// createSafeLink - factory function that creates a SafeLink component with i18n support
const createSafeLink = (opensInNewWindowText: string) => {
return function SafeLink({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
// Validate URL - only allow http, https, and relative links
const isValidUrl = href && (
href.startsWith('http://') ||
href.startsWith('https://') ||
href.startsWith('/') ||
href.startsWith('#')
);
if (!isValidUrl) {
// For invalid or potentially malicious URLs, render as plain text
return <span className="text-muted-foreground">{children}</span>;
}
if (!isValidUrl) {
// For invalid or potentially malicious URLs, render as plain text
return <span className="text-muted-foreground">{children}</span>;
}
// External links get security attributes
const isExternal = href?.startsWith('http://') || href?.startsWith('https://');
// External links get security attributes and accessibility indicator
const isExternal = href?.startsWith('http://') || href?.startsWith('https://');
return (
<a
href={href}
{...props}
{...(isExternal && {
target: '_blank',
rel: 'noopener noreferrer',
})}
className="text-primary hover:underline"
>
{children}
</a>
);
};
// Markdown components with safe link rendering
const markdownComponents = {
a: SafeLink,
return (
<a
href={href}
{...props}
{...(isExternal && {
target: '_blank',
rel: 'noopener noreferrer',
})}
className="text-primary hover:underline"
>
{children}
{isExternal && <span className="sr-only"> {opensInNewWindowText}</span>}
</a>
);
};
};
interface InsightsProps {
@@ -89,6 +88,7 @@ interface InsightsProps {
}
export function Insights({ projectId }: InsightsProps) {
const { t } = useTranslation('common');
const session = useInsightsStore((state) => state.session);
const sessions = useInsightsStore((state) => state.sessions);
const status = useInsightsStore((state) => state.status);
@@ -96,6 +96,11 @@ export function Insights({ projectId }: InsightsProps) {
const currentTool = useInsightsStore((state) => state.currentTool);
const isLoadingSessions = useInsightsStore((state) => state.isLoadingSessions);
// Create markdown components with translated accessibility text
const markdownComponents = useMemo(() => ({
a: createSafeLink(t('accessibility.opensInNewWindow')),
}), [t]);
const [inputValue, setInputValue] = useState('');
const [creatingTask, setCreatingTask] = useState<string | null>(null);
const [taskCreated, setTaskCreated] = useState<Set<string>>(new Set());
@@ -295,6 +300,7 @@ export function Insights({ projectId }: InsightsProps) {
<MessageBubble
key={message.id}
message={message}
markdownComponents={markdownComponents}
onCreateTask={() => handleCreateTask(message)}
isCreatingTask={creatingTask === message.id}
taskCreated={taskCreated.has(message.id)}
@@ -387,6 +393,7 @@ export function Insights({ projectId }: InsightsProps) {
interface MessageBubbleProps {
message: InsightsChatMessage;
markdownComponents: Components;
onCreateTask: () => void;
isCreatingTask: boolean;
taskCreated: boolean;
@@ -394,6 +401,7 @@ interface MessageBubbleProps {
function MessageBubble({
message,
markdownComponents,
onCreateTask,
isCreatingTask,
taskCreated
@@ -219,6 +219,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
size="icon"
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
onClick={onAddClick}
aria-label={t('kanban.addTaskAriaLabel')}
>
<Plus className="h-4 w-4" />
</Button>
@@ -229,7 +230,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
size="icon"
className="h-7 w-7 hover:bg-muted-foreground/10 hover:text-muted-foreground transition-colors"
onClick={onArchiveAll}
title={t('tooltips.archiveAllDone')}
aria-label={t('tooltips.archiveAllDone')}
>
<Archive className="h-4 w-4" />
</Button>
@@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus } from 'lucide-react';
import { cn } from '../lib/utils';
import { Button } from './ui/button';
@@ -26,6 +27,8 @@ export function ProjectTabBar({
className,
onSettingsClick
}: ProjectTabBarProps) {
const { t } = useTranslation('common');
// Keyboard shortcuts for tab navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -115,7 +118,7 @@ export function ProjectTabBar({
size="icon"
className="h-8 w-8"
onClick={onAddProject}
title="Add Project"
aria-label={t('projectTab.addProjectAriaLabel')}
>
<Plus className="h-4 w-4" />
</Button>
@@ -375,9 +375,11 @@ export function RateLimitModal() {
size="sm"
className="gap-2"
onClick={handleUpgrade}
aria-label={t('accessibility.upgradeSubscriptionAriaLabel')}
>
<ExternalLink className="h-4 w-4" />
<ExternalLink className="h-4 w-4" aria-hidden="true" />
{t('rateLimit.upgradeSubscription')}
<span className="sr-only">({t('accessibility.opensInNewWindow')})</span>
</Button>
</div>
</div>
@@ -276,6 +276,7 @@ export function Sidebar({
key={item.id}
onClick={() => handleNavClick(item.id)}
disabled={!selectedProjectId}
aria-keyshortcuts={item.shortcut}
className={cn(
'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-all duration-200',
'hover:bg-accent hover:text-accent-foreground',
@@ -354,6 +355,7 @@ export function Sidebar({
variant="ghost"
size="icon"
onClick={() => window.open('https://github.com/AndyMik90/Auto-Claude/issues', '_blank')}
aria-label={t('tooltips.help')}
>
<HelpCircle className="h-4 w-4" />
</Button>
@@ -160,7 +160,7 @@ export function SortableProjectTab({
isActive && 'opacity-100'
)}
onClick={onClose}
aria-label={t('projectTab.closeTab')}
aria-label={t('projectTab.closeTabAriaLabel')}
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent, type DragEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw, FolderTree, GitBranch } from 'lucide-react';
import {
Dialog,
@@ -59,6 +60,7 @@ export function TaskCreationWizard({
open,
onOpenChange
}: TaskCreationWizardProps) {
const { t } = useTranslation('tasks');
// Get selected agent profile from settings
const { settings } = useSettingsStore();
const selectedProfile = DEFAULT_AGENT_PROFILES.find(
@@ -797,6 +799,8 @@ export function TaskCreationWizard({
onDrop={handleTextareaDrop}
rows={5}
disabled={isCreating}
aria-required="true"
aria-describedby="description-help"
className={cn(
"resize-y min-h-[120px] max-h-[400px] relative bg-transparent",
// Visual feedback when dragging over textarea
@@ -815,7 +819,7 @@ export function TaskCreationWizard({
/>
)}
</div>
<p className="text-xs text-muted-foreground">
<p id="description-help" className="text-xs text-muted-foreground">
Files and images can be copy/pasted or dragged & dropped into the description.
</p>
@@ -852,6 +856,7 @@ export function TaskCreationWizard({
e.stopPropagation();
setImages(prev => prev.filter(img => img.id !== image.id));
}}
aria-label={t('images.removeImageAriaLabel', { filename: image.filename })}
>
<X className="h-3 w-3" />
</button>
@@ -915,6 +920,8 @@ export function TaskCreationWizard({
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
)}
disabled={isCreating}
aria-expanded={showAdvanced}
aria-controls="advanced-options-section"
>
<span>Classification (optional)</span>
{showAdvanced ? (
@@ -926,7 +933,7 @@ export function TaskCreationWizard({
{/* Advanced Options */}
{showAdvanced && (
<div className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div id="advanced-options-section" className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div className="grid grid-cols-2 gap-4">
{/* Category */}
<div className="space-y-2">
@@ -1058,6 +1065,8 @@ export function TaskCreationWizard({
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
)}
disabled={isCreating}
aria-expanded={showGitOptions}
aria-controls="git-options-section"
>
<span className="flex items-center gap-2">
<GitBranch className="h-4 w-4" />
@@ -1077,7 +1086,7 @@ export function TaskCreationWizard({
{/* Git Options */}
{showGitOptions && (
<div className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div id="git-options-section" className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div className="space-y-2">
<Label htmlFor="base-branch" className="text-sm font-medium text-foreground">
Base Branch (optional)
@@ -1110,7 +1119,7 @@ export function TaskCreationWizard({
{/* Error */}
{error && (
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive" role="alert">
<X className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
@@ -25,6 +25,7 @@
* ```
*/
import { useState, useEffect, useCallback, useRef, type ClipboardEvent, type DragEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { Loader2, Image as ImageIcon, ChevronDown, ChevronUp, X } from 'lucide-react';
import {
Dialog,
@@ -87,6 +88,7 @@ interface TaskEditDialogProps {
}
export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDialogProps) {
const { t } = useTranslation('tasks');
// Get selected agent profile from settings for defaults
const { settings } = useSettingsStore();
const selectedProfile = DEFAULT_AGENT_PROFILES.find(
@@ -485,12 +487,14 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
onDrop={handleTextareaDrop}
rows={5}
disabled={isSaving}
aria-required="true"
aria-describedby="edit-description-help"
className={cn(
isDragOverTextarea && !isSaving && "border-primary bg-primary/5 ring-2 ring-primary/20"
)}
/>
<p className="text-xs text-muted-foreground">
Tip: Paste screenshots directly with {navigator.platform.includes('Mac') ? '⌘V' : 'Ctrl+V'} to add reference images.
<p id="edit-description-help" className="text-xs text-muted-foreground">
{t('images.pasteHint', { shortcut: navigator.platform.includes('Mac') ? '⌘V' : 'Ctrl+V' })}
</p>
</div>
@@ -547,6 +551,8 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
)}
disabled={isSaving}
aria-expanded={showAdvanced}
aria-controls="edit-advanced-options"
>
<span>Classification (optional)</span>
{showAdvanced ? (
@@ -558,7 +564,7 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
{/* Advanced Options */}
{showAdvanced && (
<div className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div id="edit-advanced-options" className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
<div className="grid grid-cols-2 gap-4">
{/* Category */}
<div className="space-y-2">
@@ -668,6 +674,8 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
)}
disabled={isSaving}
aria-expanded={showImages}
aria-controls="edit-images-section"
>
<span className="flex items-center gap-2">
<ImageIcon className="h-4 w-4" />
@@ -687,7 +695,7 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
{/* Image Upload Section */}
{showImages && (
<div className="space-y-3 p-4 rounded-lg border border-border bg-muted/30">
<div id="edit-images-section" className="space-y-3 p-4 rounded-lg border border-border bg-muted/30">
<p className="text-xs text-muted-foreground">
Attach screenshots, mockups, or diagrams to provide visual context for the AI.
</p>
@@ -723,7 +731,7 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
{/* Error */}
{error && (
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive" role="alert">
<X className="h-4 w-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
@@ -91,6 +91,7 @@ export function WelcomeScreen({
key={project.id}
onClick={() => onSelectProject(project.id)}
className="w-full flex items-center gap-3 rounded-lg px-3 py-3 text-left transition-colors hover:bg-accent/50 group"
aria-label={t('welcome:recentProjects.openProjectAriaLabel', { name: project.name })}
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/20 text-accent-foreground shrink-0">
<Folder className="h-5 w-5" />
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Github, RefreshCw, Search, Filter, Wand2, Loader2, Layers } from 'lucide-react';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
@@ -35,6 +36,8 @@ export function IssueListHeader({
onAnalyzeAndGroup,
isAnalyzing,
}: IssueListHeaderProps) {
const { t } = useTranslation('common');
return (
<div className="shrink-0 p-4 border-b border-border">
<div className="flex items-center justify-between mb-4">
@@ -60,8 +63,9 @@ export function IssueListHeader({
size="icon"
onClick={onRefresh}
disabled={isLoading}
aria-label={t('buttons.refresh')}
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} aria-hidden="true" />
</Button>
</div>
</div>
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { ReactNode, useId } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface CollapsibleSectionProps {
@@ -18,11 +18,16 @@ export function CollapsibleSection({
badge,
children,
}: CollapsibleSectionProps) {
const contentId = useId();
return (
<section className="space-y-3">
<button
type="button"
onClick={onToggle}
className="w-full flex items-center justify-between text-sm font-semibold text-foreground hover:text-foreground/80"
aria-expanded={isExpanded}
aria-controls={contentId}
>
<div className="flex items-center gap-2">
{icon}
@@ -30,14 +35,14 @@ export function CollapsibleSection({
{badge}
</div>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
<ChevronUp className="h-4 w-4" aria-hidden="true" />
) : (
<ChevronDown className="h-4 w-4" />
<ChevronDown className="h-4 w-4" aria-hidden="true" />
)}
</button>
{isExpanded && (
<div className="space-y-4 pl-6 pt-2">
<div id={contentId} className="space-y-4 pl-6 pt-2">
{children}
</div>
)}
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ChevronRight,
Lightbulb,
@@ -31,6 +32,7 @@ export function FeatureDetailPanel({
onDelete,
competitorInsights = [],
}: FeatureDetailPanelProps) {
const { t } = useTranslation('common');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleDelete = () => {
@@ -69,10 +71,11 @@ export function FeatureDetailPanel({
e.stopPropagation();
setShowDeleteConfirm(true);
}}
aria-label={t('accessibility.deleteFeatureAriaLabel')}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button type="button" variant="ghost" size="icon" onClick={onClose}>
<Button type="button" variant="ghost" size="icon" onClick={onClose} aria-label={t('accessibility.closeFeatureDetailsAriaLabel')}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Target, Users, BarChart3, RefreshCw, Plus, TrendingUp } from 'lucide-react';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@@ -7,6 +8,7 @@ import { ROADMAP_PRIORITY_COLORS } from '../../../shared/constants';
import type { RoadmapHeaderProps } from './types';
export function RoadmapHeader({ roadmap, competitorAnalysis, onAddFeature, onRefresh, onViewCompetitorAnalysis }: RoadmapHeaderProps) {
const { t } = useTranslation('common');
const stats = getFeatureStats(roadmap);
return (
@@ -55,7 +57,7 @@ export function RoadmapHeader({ roadmap, competitorAnalysis, onAddFeature, onRef
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={onRefresh}>
<Button variant="outline" size="icon" onClick={onRefresh} aria-label={t('accessibility.regenerateRoadmapAriaLabel')}>
<RefreshCw className="h-4 w-4" />
</Button>
</TooltipTrigger>
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { X, Pencil, AlertTriangle } from 'lucide-react';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
@@ -25,6 +26,8 @@ export function TaskHeader({
onClose,
onEdit
}: TaskHeaderProps) {
const { t } = useTranslation('tasks');
return (
<div className="flex items-start justify-between p-4 pb-3">
<div className="flex-1 min-w-0 pr-2">
@@ -91,16 +94,17 @@ export function TaskHeader({
className="hover:bg-primary/10 hover:text-primary transition-colors"
onClick={onEdit}
disabled={isRunning && !isStuck}
aria-label={isRunning && !isStuck ? t('kanban.cannotEditWhileRunning') : t('kanban.editTask')}
>
<Pencil className="h-4 w-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRunning && !isStuck ? 'Cannot edit while task is running' : 'Edit task'}
{isRunning && !isStuck ? t('kanban.cannotEditWhileRunning') : t('kanban.editTask')}
</TooltipContent>
</Tooltip>
<Button variant="ghost" size="icon" className="hover:bg-destructive/10 hover:text-destructive transition-colors" onClick={onClose}>
<Button variant="ghost" size="icon" className="hover:bg-destructive/10 hover:text-destructive transition-colors" onClick={onClose} aria-label={t('kanban.closeTaskDetailsAriaLabel')}>
<X className="h-4 w-4" />
</Button>
</div>
@@ -5,7 +5,23 @@
"hideArchived": "Hide archived",
"showArchivedTasks": "Show archived tasks",
"hideArchivedTasks": "Hide archived tasks",
"closeTab": "Close tab"
"closeTab": "Close tab",
"closeTabAriaLabel": "Close tab (removes project from app)",
"addProjectAriaLabel": "Add project"
},
"accessibility": {
"deleteFeatureAriaLabel": "Delete feature",
"closeFeatureDetailsAriaLabel": "Close feature details",
"regenerateRoadmapAriaLabel": "Regenerate Roadmap",
"repositoryOwnerAriaLabel": "Repository owner",
"repositoryVisibilityAriaLabel": "Repository visibility",
"opensInNewWindow": "opens in new window",
"visitExternalLink": "Visit {{name}} (opens in new window)",
"upgradeSubscriptionAriaLabel": "Upgrade subscription (opens in new window)",
"learnMoreAriaLabel": "Learn more (opens in new window)",
"toggleFolder": "Toggle {{name}} folder",
"expandFolder": "Expand {{name}} folder",
"collapseFolder": "Collapse {{name}} folder"
},
"buttons": {
"save": "Save",
@@ -38,7 +38,13 @@
"branchDescription": "Choose which branch Auto Claude should use as the base for creating task branches.",
"whyBranch": "Why select a branch?",
"branchExplanation": "Auto Claude creates isolated workspaces for each task. Selecting the right base branch ensures your tasks start with the latest code from your main development line.",
"ready": "Auto Claude is ready to use! You can now create tasks that will be automatically based on the {{branchName}} branch."
"ready": "Auto Claude is ready to use! You can now create tasks that will be automatically based on the {{branchName}} branch.",
"createRepoAriaLabel": "Create new repository on GitHub",
"linkRepoAriaLabel": "Link to existing repository",
"goBackAriaLabel": "Go back to repository selection",
"selectOwnerAriaLabel": "Select {{owner}} as repository owner",
"selectOrgAriaLabel": "Select {{org}} as repository owner",
"selectVisibilityAriaLabel": "Set repository visibility to {{visibility}}"
},
"worktrees": {
"title": "Worktrees",
@@ -109,7 +115,9 @@
"nameRequired": "Please enter a project name",
"locationRequired": "Please select a location",
"failedToOpen": "Failed to open project",
"failedToCreate": "Failed to create project"
"failedToCreate": "Failed to create project",
"openExistingAriaLabel": "Open existing project folder",
"createNewAriaLabel": "Create new project"
},
"customModel": {
"title": "Custom Model Configuration",
@@ -43,6 +43,7 @@
"latest": "Latest",
"lastChecked": "Last checked",
"learnMore": "Learn more about Claude Code",
"learnMoreAriaLabel": "Learn more about Claude Code (opens in new window)",
"updateWarningTitle": "Update Claude Code?",
"updateWarningDescription": "Updating will close all running Claude Code sessions. Any unsaved work in those sessions may be lost. Make sure to save your work before proceeding.",
"updateAnyway": "Update Anyway"
@@ -59,7 +59,11 @@
"emptyDoneHint": "Approved tasks appear here",
"emptyDefault": "No tasks",
"dropHere": "Drop here",
"showArchived": "Show archived"
"showArchived": "Show archived",
"addTaskAriaLabel": "Add new task to backlog",
"closeTaskDetailsAriaLabel": "Close task details",
"editTask": "Edit task",
"cannotEditWhileRunning": "Cannot edit while task is running"
},
"execution": {
"phases": {
@@ -98,5 +102,9 @@
},
"metadata": {
"severity": "severity"
},
"images": {
"removeImageAriaLabel": "Remove image {{filename}}",
"pasteHint": "Tip: Paste screenshots directly with {{shortcut}} to add reference images."
}
}
@@ -11,6 +11,7 @@
"title": "Recent Projects",
"empty": "No projects yet",
"emptyDescription": "Create a new project or open an existing one to get started",
"openFolder": "Open Folder"
"openFolder": "Open Folder",
"openProjectAriaLabel": "Open project {{name}}"
}
}
@@ -5,7 +5,23 @@
"hideArchived": "Masquer archivés",
"showArchivedTasks": "Afficher les tâches archivées",
"hideArchivedTasks": "Masquer les tâches archivées",
"closeTab": "Fermer l'onglet"
"closeTab": "Fermer l'onglet",
"closeTabAriaLabel": "Fermer l'onglet (retire le projet de l'application)",
"addProjectAriaLabel": "Ajouter un projet"
},
"accessibility": {
"deleteFeatureAriaLabel": "Supprimer la fonctionnalité",
"closeFeatureDetailsAriaLabel": "Fermer les détails de la fonctionnalité",
"regenerateRoadmapAriaLabel": "Régénérer la feuille de route",
"repositoryOwnerAriaLabel": "Propriétaire du dépôt",
"repositoryVisibilityAriaLabel": "Visibilité du dépôt",
"opensInNewWindow": "s'ouvre dans une nouvelle fenêtre",
"visitExternalLink": "Visiter {{name}} (s'ouvre dans une nouvelle fenêtre)",
"upgradeSubscriptionAriaLabel": "Mettre à niveau l'abonnement (s'ouvre dans une nouvelle fenêtre)",
"learnMoreAriaLabel": "En savoir plus (s'ouvre dans une nouvelle fenêtre)",
"toggleFolder": "Basculer le dossier {{name}}",
"expandFolder": "Déplier le dossier {{name}}",
"collapseFolder": "Replier le dossier {{name}}"
},
"buttons": {
"save": "Enregistrer",
@@ -38,7 +38,13 @@
"branchDescription": "Choisissez quelle branche Auto Claude doit utiliser comme base pour créer les branches de tâches.",
"whyBranch": "Pourquoi sélectionner une branche ?",
"branchExplanation": "Auto Claude crée des espaces de travail isolés pour chaque tâche. Sélectionner la bonne branche de base garantit que vos tâches démarrent avec le code le plus récent de votre ligne de développement principale.",
"ready": "Auto Claude est prêt à l'emploi ! Vous pouvez maintenant créer des tâches qui seront automatiquement basées sur la branche {{branchName}}."
"ready": "Auto Claude est prêt à l'emploi ! Vous pouvez maintenant créer des tâches qui seront automatiquement basées sur la branche {{branchName}}.",
"createRepoAriaLabel": "Créer un nouveau dépôt sur GitHub",
"linkRepoAriaLabel": "Lier à un dépôt existant",
"goBackAriaLabel": "Retourner à la sélection du dépôt",
"selectOwnerAriaLabel": "Sélectionner {{owner}} comme propriétaire du dépôt",
"selectOrgAriaLabel": "Sélectionner {{org}} comme propriétaire du dépôt",
"selectVisibilityAriaLabel": "Définir la visibilité du dépôt sur {{visibility}}"
},
"worktrees": {
"title": "Worktrees",
@@ -109,7 +115,9 @@
"nameRequired": "Veuillez entrer un nom de projet",
"locationRequired": "Veuillez sélectionner un emplacement",
"failedToOpen": "Échec de l'ouverture du projet",
"failedToCreate": "Échec de la création du projet"
"failedToCreate": "Échec de la création du projet",
"openExistingAriaLabel": "Ouvrir un dossier de projet existant",
"createNewAriaLabel": "Créer un nouveau projet"
},
"customModel": {
"title": "Configuration du modèle personnalisé",
@@ -43,6 +43,7 @@
"latest": "Dernière",
"lastChecked": "Dernière vérification",
"learnMore": "En savoir plus sur Claude Code",
"learnMoreAriaLabel": "En savoir plus sur Claude Code (s'ouvre dans une nouvelle fenêtre)",
"updateWarningTitle": "Mettre à jour Claude Code ?",
"updateWarningDescription": "La mise à jour fermera toutes les sessions Claude Code en cours. Tout travail non sauvegardé dans ces sessions pourrait être perdu. Assurez-vous de sauvegarder votre travail avant de continuer.",
"updateAnyway": "Mettre à jour quand même"
@@ -59,7 +59,11 @@
"emptyDoneHint": "Les tâches approuvées apparaissent ici",
"emptyDefault": "Aucune tâche",
"dropHere": "Déposer ici",
"showArchived": "Afficher les archivées"
"showArchived": "Afficher les archivées",
"addTaskAriaLabel": "Ajouter une nouvelle tâche au backlog",
"closeTaskDetailsAriaLabel": "Fermer les détails de la tâche",
"editTask": "Modifier la tâche",
"cannotEditWhileRunning": "Impossible de modifier pendant l'exécution"
},
"execution": {
"phases": {
@@ -98,5 +102,9 @@
},
"metadata": {
"severity": "sévérité"
},
"images": {
"removeImageAriaLabel": "Supprimer l'image {{filename}}",
"pasteHint": "Astuce : Collez des captures d'écran directement avec {{shortcut}} pour ajouter des images de référence."
}
}
@@ -11,6 +11,7 @@
"title": "Projets récents",
"empty": "Aucun projet",
"emptyDescription": "Créez un nouveau projet ou ouvrez-en un existant pour commencer",
"openFolder": "Ouvrir le dossier"
"openFolder": "Ouvrir le dossier",
"openProjectAriaLabel": "Ouvrir le projet {{name}}"
}
}