fix(quick-260921-bi2): a11y - Marketplace-Bereich
useButtonType: alle Filter-, Karten- und Dialog-Schaltflaechen (keine davon
in einem Formular) erhalten type="button".
noSvgWithoutTitle: Modul-Icons neben dem Modulnamen und die Erfolgs-/Fehler-
Symbole neben der Toast-Nachricht werden dekorativ (aria-hidden="true"); der
bislang unbeschriftete Toast-Schliessen-Button erhaelt aria-label={t('close')}
aus dem bereits vorhandenen common.close-Schluessel, sein Icon wird dekorativ.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -134,7 +134,7 @@ export default function ModuleDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto space-y-4">
|
<div className="max-w-2xl mx-auto space-y-4">
|
||||||
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||||
<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="m15 18-6-6 6-6" />
|
<path d="m15 18-6-6 6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
{t('backToMarketplace')}
|
{t('backToMarketplace')}
|
||||||
@@ -150,7 +150,7 @@ export default function ModuleDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto space-y-6">
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||||
<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="m15 18-6-6 6-6" />
|
<path d="m15 18-6-6 6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
{t('backToMarketplace')}
|
{t('backToMarketplace')}
|
||||||
@@ -159,13 +159,13 @@ export default function ModuleDetailPage() {
|
|||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="rounded-lg bg-muted p-4 shrink-0">
|
<div className="rounded-lg bg-muted p-4 shrink-0">
|
||||||
{module.icon === 'Globe' ? (
|
{module.icon === 'Globe' ? (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||||
<path d="M2 12h20" />
|
<path d="M2 12h20" />
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.315 8.685a.98.98 0 0 1 .837-.276c.47.07.802.48.968.925a2.501 2.501 0 1 0 3.214-3.214c-.446-.166-.855-.497-.925-.968a.979.979 0 0 1 .276-.837l1.61-1.61a2.404 2.404 0 0 1 1.705-.707c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z" />
|
<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.315 8.685a.98.98 0 0 1 .837-.276c.47.07.802.48.968.925a2.501 2.501 0 1 0 3.214-3.214c-.446-.166-.855-.497-.925-.968a.979.979 0 0 1 .276-.837l1.61-1.61a2.404 2.404 0 0 1 1.705-.707c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
@@ -187,6 +187,7 @@ export default function ModuleDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={toggling}
|
disabled={toggling}
|
||||||
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:cursor-wait disabled:opacity-50 ${
|
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:cursor-wait disabled:opacity-50 ${
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function ActivationDialog({ open, moduleName, onConfirm, onCancel }: Acti
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
ref={cancelRef}
|
ref={cancelRef}
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted"
|
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted"
|
||||||
@@ -59,6 +60,7 @@ export function ActivationDialog({ open, moduleName, onConfirm, onCancel }: Acti
|
|||||||
{t('deactivateDialogCancel')}
|
{t('deactivateDialogCancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
ref={confirmRef}
|
ref={confirmRef}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
|
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => onChange('all')}
|
onClick={() => onChange('all')}
|
||||||
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
@@ -27,6 +28,7 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
|
|||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
|
type="button"
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => onChange(cat)}
|
onClick={() => onChange(cat)}
|
||||||
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function ModuleIcon({ icon }: { icon?: string }) {
|
|||||||
if (icon === 'Globe') {
|
if (icon === 'Globe') {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
@@ -54,6 +55,7 @@ function ModuleIcon({ icon }: { icon?: string }) {
|
|||||||
// Default puzzle-piece icon for generic modules
|
// Default puzzle-piece icon for generic modules
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
@@ -167,6 +169,7 @@ export function MarketplaceCard({
|
|||||||
{canManage && (
|
{canManage && (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggle(id, isActive);
|
onToggle(id, isActive);
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ export function MarketplaceSearch({ onChange }: MarketplaceSearchProps) {
|
|||||||
/>
|
/>
|
||||||
{value && (
|
{value && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setValue('')}
|
onClick={() => setValue('')}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
>
|
>
|
||||||
<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="M18 6 6 18" />
|
<path d="M18 6 6 18" />
|
||||||
<path d="m6 6 12 12" />
|
<path d="m6 6 12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function StatusFilter({ value, counts, onChange }: StatusFilterProps) {
|
|||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={value === tab.key}
|
aria-selected={value === tab.key}
|
||||||
onClick={() => onChange(tab.key)}
|
onClick={() => onChange(tab.key)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
interface Toast {
|
interface Toast {
|
||||||
@@ -27,6 +28,7 @@ export const useToastStore = create<ToastState>()((set) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export function ToastContainer() {
|
export function ToastContainer() {
|
||||||
|
const t = useTranslations('common');
|
||||||
const toasts = useToastStore((s) => s.toasts);
|
const toasts = useToastStore((s) => s.toasts);
|
||||||
const removeToast = useToastStore((s) => s.removeToast);
|
const removeToast = useToastStore((s) => s.removeToast);
|
||||||
|
|
||||||
@@ -40,21 +42,23 @@ export function ToastContainer() {
|
|||||||
className="flex items-center gap-3 rounded-lg border border-border bg-card p-4 shadow-lg motion-safe:animate-[slideIn_200ms_ease-out]"
|
className="flex items-center gap-3 rounded-lg border border-border bg-card p-4 shadow-lg motion-safe:animate-[slideIn_200ms_ease-out]"
|
||||||
>
|
>
|
||||||
{toast.type === 'success' ? (
|
{toast.type === 'success' ? (
|
||||||
<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="text-green-600 dark:text-green-400 shrink-0">
|
<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="text-green-600 dark:text-green-400 shrink-0">
|
||||||
<path d="M20 6 9 17l-5-5" />
|
<path d="M20 6 9 17l-5-5" />
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<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="text-destructive shrink-0">
|
<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="text-destructive shrink-0">
|
||||||
<path d="M18 6 6 18" />
|
<path d="M18 6 6 18" />
|
||||||
<path d="m6 6 12 12" />
|
<path d="m6 6 12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-foreground">{toast.message}</span>
|
<span className="text-sm text-foreground">{toast.message}</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => removeToast(toast.id)}
|
onClick={() => removeToast(toast.id)}
|
||||||
|
aria-label={t('close')}
|
||||||
className="ml-2 text-muted-foreground hover:text-foreground shrink-0"
|
className="ml-2 text-muted-foreground hover:text-foreground shrink-0"
|
||||||
>
|
>
|
||||||
<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">
|
<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">
|
||||||
<path d="M18 6 6 18" />
|
<path d="M18 6 6 18" />
|
||||||
<path d="m6 6 12 12" />
|
<path d="m6 6 12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export default function MarketplacePage() {
|
|||||||
) : modules.length === 0 ? (
|
) : modules.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[40vh] text-center">
|
<div className="flex flex-col items-center justify-center min-h-[40vh] text-center">
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="64"
|
width="64"
|
||||||
height="64"
|
height="64"
|
||||||
|
|||||||
Reference in New Issue
Block a user