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
2 changes: 1 addition & 1 deletion dashboard/src/__tests__/touch-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Mobile touch targets (issue #2350)', () => {
const lines = src.split('\n');
let found = false;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('aria-label="New Session')) {
if (lines[i].includes("aria.newSession") || lines[i].includes('aria-label="New Session')) {
// Search within 5 lines in both directions for className with min-h
for (let j = Math.max(0, i - 5); j <= Math.min(lines.length - 1, i + 5); j++) {
if (lines[j].includes('className="') && lines[j].includes('min-h-[44px]')) {
Expand Down
10 changes: 6 additions & 4 deletions dashboard/src/components/CreatePipelineModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* components/CreatePipelineModal.tsx — Modal dialog for creating new pipelines.
*/

import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react'
import { useT } from '../i18n/context';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { useNavigate } from 'react-router-dom';
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
Expand All @@ -26,6 +27,7 @@ function makeStep(): StepRow {
}

export default function CreatePipelineModal({ open, onClose }: CreatePipelineModalProps) {
const t = useT();
const navigate = useNavigate();
const nameRef = useRef<HTMLInputElement>(null);
const trapRef = useFocusTrap(open);
Expand Down Expand Up @@ -118,11 +120,11 @@ export default function CreatePipelineModal({ open, onClose }: CreatePipelineMod
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleClose} />

<div ref={trapRef} role="dialog" aria-modal="true" aria-label="Create new pipeline" className="relative w-full max-w-2xl mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto">
<div ref={trapRef} role="dialog" aria-modal="true" aria-label={t('aria.createNewPipeline')} className="relative w-full max-w-2xl mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">New Pipeline</h2>
<button aria-label="Close"
<button aria-label={t('aria.close')}
onClick={handleClose}
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
>
Expand All @@ -142,7 +144,7 @@ export default function CreatePipelineModal({ open, onClose }: CreatePipelineMod
value={pipelineName}
onChange={(e) => setPipelineName(e.target.value)}
placeholder="my-pipeline"
aria-label="Pipeline Name"
aria-label={t('aria.pipelineName')}
className="w-full min-h-[44px] px-3 py-2.5 text-sm bg-[var(--color-void)] border border-[var(--color-void-lighter)] rounded text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-accent)]"
/>
</div>
Expand Down
10 changes: 6 additions & 4 deletions dashboard/src/components/CreateSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* components/CreateSessionModal.tsx — Modal dialog for creating new sessions.
*/

import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react'
import { useT } from '../i18n/context';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { useNavigate } from 'react-router-dom';
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
Expand All @@ -21,6 +22,7 @@ function makeRow(): BatchRow {
}

export default function CreateSessionModal({ open, onClose }: CreateSessionModalProps) {
const t = useT();
const navigate = useNavigate();
const workDirRef = useRef<HTMLInputElement>(null);
const abortRef = useRef<AbortController | null>(null);
Expand Down Expand Up @@ -192,7 +194,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
/>

{/* Modal */}
<div ref={trapRef} role="dialog" aria-modal="true" aria-label="Create new session" className={`relative w-full ${mode === 'batch' ? 'max-w-2xl' : 'max-w-md'} mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto`}>
<div ref={trapRef} role="dialog" aria-modal="true" aria-label={t('aria.createNewSession')} className={`relative w-full ${mode === 'batch' ? 'max-w-2xl' : 'max-w-md'} mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto`}>
{/* Header */}
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
<div className="flex items-center gap-4">
Expand Down Expand Up @@ -235,7 +237,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
)}
</div>
</div>
<button aria-label="Close"
<button aria-label={t('aria.close')}
onClick={handleClose}
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
>
Expand Down Expand Up @@ -388,7 +390,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
<button
type="button"
onClick={() => removeBatchRow(i)}
aria-label="Remove row"
aria-label={t('aria.removeRow')}
disabled={batchRows.length <= 1}
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-error)] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
Expand Down
12 changes: 7 additions & 5 deletions dashboard/src/components/KeyboardShortcutsHelp/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'react'
import { useT } from '../../i18n/context';
import { X, Keyboard } from 'lucide-react';
import { SHORTCUTS } from '../../hooks/useKeyboardShortcuts';

Expand All @@ -9,13 +10,14 @@ export function KeyboardShortcutsHelp({
open: boolean;
onClose: () => void;
}) {
const translate = useT();
const [visible, setVisible] = useState(false);

useEffect(() => {
if (open) setVisible(true);
else {
const t = setTimeout(() => setVisible(false), 200);
return () => clearTimeout(t);
const timer = setTimeout(() => setVisible(false), 200);
return () => clearTimeout(timer);
}
}, [open]);

Expand All @@ -29,7 +31,7 @@ export function KeyboardShortcutsHelp({
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label="Keyboard shortcuts"
aria-label={translate('aria.keyboardShortcuts')}
>
<div
className="w-full max-w-md rounded-xl border border-[var(--color-void-lighter)]/60 bg-[var(--color-surface)] p-6 shadow-2xl"
Expand All @@ -43,7 +45,7 @@ export function KeyboardShortcutsHelp({
<button
onClick={onClose}
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-void-lighter)]/50 hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Close"
aria-label={translate('aria.close')}
>
<X className="h-4 w-4" />
</button>
Expand Down
18 changes: 10 additions & 8 deletions dashboard/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { logger } from '../utils/logger';
*/

import { NavLink, Outlet } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'react'
import { useT } from '../i18n/context';
import Breadcrumb from './shared/Breadcrumb';
import { ErrorBoundary } from './shared/ErrorBoundary';
import { useTheme } from '../hooks/useTheme';
Expand Down Expand Up @@ -105,6 +106,7 @@ function isMobileSidebarViewport(): boolean {
}

export default function Layout() {
const t = useT();
const sseConnected = useStore((s) => s.sseConnected);
const setSseConnected = useStore((s) => s.setSseConnected);
const sseError = useStore((s) => s.sseError);
Expand Down Expand Up @@ -406,7 +408,7 @@ export default function Layout() {

{/* ── Sidebar ─────────────────────────────────────────── */}
<aside
aria-label="Primary sidebar"
aria-label={t('aria.primarySidebar')}
className={`
fixed inset-y-0 left-0 z-40 flex flex-col border-r border-white/5 bg-transparent backdrop-blur-xl
transition-all duration-300 ease-in-out
Expand All @@ -428,15 +430,15 @@ export default function Layout() {
tabIndex={hiddenMobileSidebarControlTabIndex}
disabled={isMobileSidebarHidden}
className="md:hidden inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)]"
aria-label="Close menu"
aria-label={t('aria.closeMenu')}
aria-hidden={isMobileSidebarHidden ? 'true' : undefined}
>
<X className="h-5 w-5" />
</button>
</div>

{/* Nav links */}
<nav className="flex flex-col gap-4 px-3 py-6 flex-1 overflow-y-auto overflow-x-hidden" aria-label="Main navigation">
<nav className="flex flex-col gap-4 px-3 py-6 flex-1 overflow-y-auto overflow-x-hidden" aria-label={t('aria.mainNavigation')}>
{NAV_GROUPS.map((group) => (
<div key={group.label} className="flex flex-col gap-1">
{!isCollapsed && (
Expand Down Expand Up @@ -471,7 +473,7 @@ export default function Layout() {
{/* Bottom section: Settings + toggle + logout */}
<div className="border-t border-white/5 px-3 py-4 flex flex-col gap-2">
{identityLabel && identityDetailLabel && !isCollapsed && (
<div className="px-3 py-2" aria-label="Signed in user">
<div className="px-3 py-2" aria-label={t('aria.signedInUser')}>
<p className="truncate text-xs font-medium text-slate-700 dark:text-[var(--color-text-primary)]">{identityLabel}</p>
<p className="truncate text-[11px] text-slate-500 dark:text-[var(--color-text-muted)]">
{identityDetailLabel}
Expand Down Expand Up @@ -519,7 +521,7 @@ export default function Layout() {
onClick={handleLogout}
tabIndex={hiddenMobileSidebarControlTabIndex}
className={`flex items-center gap-2.5 rounded-lg px-3 py-3 min-h-[44px] text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors w-full ${isCollapsed ? 'justify-center' : ''}`}
aria-label="Sign out"
aria-label={t('aria.signOut')}
>
<LogOut className="h-4 w-4 shrink-0" />
{!isCollapsed && <span className="truncate">Sign out</span>}
Expand All @@ -540,7 +542,7 @@ export default function Layout() {
tabIndex={isMobileDrawerOpen ? -1 : undefined}
aria-hidden={isMobileDrawerOpen ? 'true' : undefined}
className="md:hidden inline-flex h-11 w-11 items-center justify-center rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Open menu"
aria-label={t('aria.openMenu')}
>
<Menu className="h-5 w-5" />
</button>
Expand All @@ -559,7 +561,7 @@ export default function Layout() {
<button
type="button"
onClick={openNewSession}
aria-label="New Session (⌘N)"
aria-label={t('aria.newSessionCmd')}
title="New Session (⌘N)"
className="inline-flex h-11 w-11 items-center justify-center rounded-lg p-2.5 min-h-[44px] min-w-[44px] text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors"
>
Expand Down
8 changes: 5 additions & 3 deletions dashboard/src/components/NewSessionDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Width: 480px desktop, full-width mobile.
*/

import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react'
import { useT } from '../i18n/context';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { useNavigate } from 'react-router-dom';
import { Loader2, Plus, X } from 'lucide-react';
Expand All @@ -22,6 +23,7 @@ const PERMISSION_MODES = [
];

export function NewSessionDrawer() {
const t = useT();
const navigate = useNavigate();
const addToast = useToastStore((t) => t.addToast);
const { newSessionOpen, closeNewSession } = useDrawerStore();
Expand Down Expand Up @@ -126,7 +128,7 @@ export function NewSessionDrawer() {
key="drawer-panel"
role="dialog"
aria-modal="true"
aria-label="New Session"
aria-label={t('aria.newSession')}
ref={trapRef as React.Ref<HTMLDivElement>}
initial={{ x: '100%' }}
animate={{ x: 0 }}
Expand All @@ -143,7 +145,7 @@ export function NewSessionDrawer() {
<button
type="button"
onClick={closeNewSession}
aria-label="Close drawer"
aria-label={t('aria.closeDrawer')}
className="rounded-lg p-2 text-[var(--color-text-muted)] hover:bg-white/5 hover:text-[var(--color-text-primary)] transition-colors"
>
<X className="h-4 w-4" />
Expand Down
8 changes: 5 additions & 3 deletions dashboard/src/components/SaveTemplateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* components/SaveTemplateModal.tsx — Modal dialog for saving a session as a template.
*/

import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react'
import { useT } from '../i18n/context';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { X, Loader2 } from 'lucide-react';
import { createTemplate } from '../api/client';
Expand All @@ -15,6 +16,7 @@ interface SaveTemplateModalProps {
}

export default function SaveTemplateModal({ open, onClose, sessionId }: SaveTemplateModalProps) {
const t = useT();
const abortRef = useRef<AbortController | null>(null);
const trapRef = useFocusTrap(open);

Expand Down Expand Up @@ -95,13 +97,13 @@ export default function SaveTemplateModal({ open, onClose, sessionId }: SaveTemp
ref={trapRef}
role="dialog"
aria-modal="true"
aria-label="Save session as template"
aria-label={t('aria.saveAsTemplate')}
className="relative w-full max-w-md mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto"
>
{/* Header */}
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">Save as Template</h2>
<button aria-label="Close"
<button aria-label={t('aria.close')}
onClick={handleClose}
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
>
Expand Down
6 changes: 4 additions & 2 deletions dashboard/src/components/TemplateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* components/TemplateModal.tsx — Modal dialog for creating and editing session templates.
*/

import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react'
import { useT } from '../i18n/context';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { X, Loader2 } from 'lucide-react';
import { createTemplate, updateTemplate } from '../api/client';
Expand All @@ -28,6 +29,7 @@ interface TemplateModalProps {
}

export default function TemplateModal({ open, onClose, template, onSaved }: TemplateModalProps) {
const t = useT();
const abortRef = useRef<AbortController | null>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
const trapRef = useFocusTrap(open);
Expand Down Expand Up @@ -164,7 +166,7 @@ export default function TemplateModal({ open, onClose, template, onSaved }: Temp
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">
{isEditing ? 'Edit Template' : 'Create Template'}
</h2>
<button aria-label="Close"
<button aria-label={t('aria.close')}
onClick={handleClose}
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
>
Expand Down
13 changes: 8 additions & 5 deletions dashboard/src/components/ToastContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
* - Colors via CSS vars: success=emerald, warning=amber, error=red, info=slate
*/

import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'
import { useT } from '../i18n/context';
import { X, CheckCircle, AlertTriangle, Info, AlertCircle, Trash2, Undo } from 'lucide-react';
import { useToastStore } from '../store/useToastStore';
import type { ToastType } from '../store/useToastStore';
Expand Down Expand Up @@ -43,6 +44,7 @@ function ToastItem({
description?: string;
undoAction?: () => void;
}) {
const t = useT();
const removeToast = useToastStore((s) => s.removeToast);
const [progress, setProgress] = useState(100);
const Icon = TYPE_ICONS[type];
Expand Down Expand Up @@ -131,7 +133,7 @@ function ToastItem({
<button
onClick={handleUndo}
className="shrink-0 flex items-center gap-1 rounded px-2 py-1 text-xs font-medium opacity-80 hover:opacity-100 transition-opacity bg-current/10"
aria-label="Undo"
aria-label={t('aria.undo')}
>
<Undo className="h-3 w-3" />
Undo
Expand All @@ -140,7 +142,7 @@ function ToastItem({
<button
onClick={() => removeToast(id)}
className="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100 transition-opacity"
aria-label="Dismiss"
aria-label={t('aria.dismiss')}
>
<X className="h-3.5 w-3.5" />
</button>
Expand All @@ -150,22 +152,23 @@ function ToastItem({

export default function ToastContainer() {
const toasts = useToastStore((s) => s.toasts);
const t = useT();
const removeToast = useToastStore((s) => s.removeToast);

if (toasts.length === 0) return null;

return (
<div
aria-live="polite"
aria-label="Notifications"
aria-label={t('aria.notifications')}
className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none"
>
{toasts.length > 1 && (
<div className="flex justify-end pointer-events-auto">
<button
onClick={() => toasts.forEach((t) => removeToast(t.id))}
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Dismiss all notifications"
aria-label={t('aria.dismissAll')}
>
<Trash2 className="h-3 w-3" />
Clear all
Expand Down
Loading
Loading