fix(quick-260921-bi2): a11y - Layout-Navigation und Einstellungsformulare
useButtonType: Hamburger-, Profil-, Kategorie-, Ein-/Ausklapp- und Logout-
Buttons (kein Formular in diesen Dateien) erhalten type="button".
noSvgWithoutTitle: Icons neben sichtbarem Text werden dekorativ
(aria-hidden="true"). Zwei bislang unbenannte interaktive Elemente erhalten
zusaetzlich ein aria-label, weil ihr Text im eingeklappten Sidebar-Zustand
verschwindet bzw. ganz fehlte: die Dashboard-/Marketplace-Links und der
"Einstellungen"-Button in der Seitenleiste (t('dashboard')/t('marketplace')/
t('settings'), alle bereits vorhandene Schluessel) sowie der mobile
Schliessen-Button (tCommon('close')).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -26,6 +26,7 @@ export default function SettingsLayout({
|
||||
className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
|
||||
@@ -99,11 +99,13 @@ export function Header() {
|
||||
{/* Left: Logo + Mobile hamburger */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors md:hidden"
|
||||
aria-label={t('common.menu')}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -145,6 +147,7 @@ export function Header() {
|
||||
{/* User avatar dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="h-8 w-8 rounded-full overflow-hidden bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
aria-label={tHeader('userMenu')}
|
||||
@@ -192,6 +195,7 @@ export function Header() {
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -221,7 +225,7 @@ export function Header() {
|
||||
: 'text-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
{tHeader('admin.label')}
|
||||
@@ -233,6 +237,7 @@ export function Header() {
|
||||
<div className="border-t border-border my-1" />
|
||||
<div className="px-2 py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
logout();
|
||||
@@ -240,6 +245,7 @@ export function Header() {
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
|
||||
@@ -28,8 +28,13 @@ export function SidebarFooter() {
|
||||
)}
|
||||
|
||||
{/* Settings */}
|
||||
<button className="flex items-center gap-3 rounded-md px-2 py-1.5 text-sm text-sidebar-foreground hover:bg-muted transition-colors">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('settings')}
|
||||
className="flex items-center gap-3 rounded-md px-2 py-1.5 text-sm text-sidebar-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
|
||||
@@ -21,6 +21,7 @@ interface SidebarModule {
|
||||
|
||||
export function Sidebar() {
|
||||
const t = useTranslations('sidebar');
|
||||
const tCommon = useTranslations('common');
|
||||
const { isCollapsed, isMobileOpen, toggle, setMobileOpen } = useSidebarStore();
|
||||
const [activeModules, setActiveModules] = useState<SidebarModule[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -94,8 +95,8 @@ export function Sidebar() {
|
||||
<nav className="flex-1 overflow-y-auto p-3">
|
||||
<ul className="flex flex-col gap-1">
|
||||
<li>
|
||||
<Link href="/" className={linkClass('/')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<Link href="/" className={linkClass('/')} aria-label={t('dashboard')}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
@@ -105,8 +106,8 @@ export function Sidebar() {
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/marketplace" className={linkClass('/marketplace')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<Link href="/marketplace" className={linkClass('/marketplace')} aria-label={t('marketplace')}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<circle cx="9" cy="21" r="1" />
|
||||
<circle cx="20" cy="21" r="1" />
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
|
||||
@@ -140,6 +141,7 @@ export function Sidebar() {
|
||||
{filteredCategories.map(([category, mods]) => (
|
||||
<li key={category}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCategory(category)}
|
||||
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
|
||||
isActive(`/modules/${category}`)
|
||||
@@ -147,13 +149,13 @@ export function Sidebar() {
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span className="capitalize">{category.replace(/-/g, ' ')}</span>
|
||||
<span className="ml-auto flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground/60">{mods.length}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={`transition-transform duration-200 ${openCategories.has(category) ? 'rotate-180' : ''}`}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={`transition-transform duration-200 ${openCategories.has(category) ? 'rotate-180' : ''}`}>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -188,11 +190,12 @@ export function Sidebar() {
|
||||
|
||||
<div className="hidden md:block border-t border-sidebar-border p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
className="flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-sm text-sidebar-foreground hover:bg-muted transition-colors"
|
||||
aria-label={isCollapsed ? t('expand') : t('collapse')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 transition-transform duration-200 ${isCollapsed ? 'rotate-180' : ''}`}>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 transition-transform duration-200 ${isCollapsed ? 'rotate-180' : ''}`}>
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
{!isCollapsed && <span className="truncate">{t('collapse')}</span>}
|
||||
@@ -241,10 +244,12 @@ export function Sidebar() {
|
||||
wordmarkClassName="dark:text-primary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
aria-label={tCommon('close')}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
|
||||
@@ -22,6 +22,7 @@ export function LocaleSwitcher() {
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={switchLocale}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center rounded-md px-2.5 py-1.5 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors disabled:opacity-50"
|
||||
|
||||
@@ -367,7 +367,7 @@ export function AccountSettingsForm() {
|
||||
className="flex items-center justify-center rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isPwPending ? (
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg aria-hidden="true" className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
|
||||
@@ -162,6 +162,7 @@ export function CalendarSettingsPanel() {
|
||||
aria-label="Sync error"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -180,6 +181,7 @@ export function CalendarSettingsPanel() {
|
||||
) : testResults[source.id] === 'success' ? (
|
||||
<span className="text-green-500" aria-label="Connection OK">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -196,6 +198,7 @@ export function CalendarSettingsPanel() {
|
||||
) : testResults[source.id] === 'error' ? (
|
||||
<span className="text-orange-500" aria-label="Connection error">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -243,6 +246,7 @@ export function CalendarSettingsPanel() {
|
||||
aria-label={`Edit ${source.name}`}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -266,6 +270,7 @@ export function CalendarSettingsPanel() {
|
||||
aria-label={`Delete ${source.name}`}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
|
||||
@@ -130,6 +130,7 @@ export function SearchProviderForm() {
|
||||
aria-label={`Remove ${provider.name}`}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
|
||||
@@ -127,6 +127,7 @@ export function WidgetSettingsPanel({
|
||||
)}
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
|
||||
@@ -33,6 +33,7 @@ export function ThemeToggle() {
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={cycleTheme}
|
||||
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||
aria-label={label}
|
||||
@@ -42,6 +43,7 @@ export function ThemeToggle() {
|
||||
<span className="h-5 w-5" />
|
||||
) : theme === 'light' ? (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -64,6 +66,7 @@ export function ThemeToggle() {
|
||||
</svg>
|
||||
) : theme === 'dark' ? (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -78,6 +81,7 @@ export function ThemeToggle() {
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
|
||||
Reference in New Issue
Block a user