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:
2026-09-21 09:21:43 +02:00
parent 969fd01f5c
commit 27a6e2952c
10 changed files with 40 additions and 11 deletions
+7 -1
View File
@@ -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"
+13 -8
View File
@@ -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>