feat(a11y): add accessibility settings and theme improvements

Add comprehensive accessibility support across shared packages:
- A11y store with contrast, colorblind mode, and reduce motion settings
- A11yQuickToggles and A11ySettings UI components
- PillNavigation and PillDropdown components in shared-ui
- Calendar app updates to integrate new theme/a11y features

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-02 22:56:09 +01:00
parent 6cc9f70a4a
commit 02c82c7547
33 changed files with 1474 additions and 143 deletions

View file

@ -11,6 +11,8 @@
onToggle?: (open: boolean) => void;
/** Optional header content (e.g., mode selector) */
header?: Snippet;
/** Optional footer content (e.g., a11y toggles) */
footer?: Snippet;
}
let {
@ -21,6 +23,7 @@
isOpen = false,
onToggle,
header,
footer,
}: Props = $props();
let internalOpen = $state(false);
@ -248,6 +251,13 @@
{/if}
{/if}
{/each}
<!-- Optional footer (e.g., a11y toggles) -->
{#if footer}
<div class="dropdown-footer">
{@render footer()}
</div>
{/if}
</div>
{/if}
</div>
@ -487,4 +497,34 @@
flex: 1;
text-align: left;
}
/* Footer for custom content (e.g., a11y toggles) */
.dropdown-footer {
animation: fanIn 0.15s ease-out forwards;
opacity: 0;
transform: translateY(10px);
position: relative;
z-index: 1;
margin-top: 0.25rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
:global(.dark) .dropdown-footer {
border-top-color: rgba(255, 255, 255, 0.15);
}
.fan-up .dropdown-footer {
transform: translateY(-10px);
margin-top: 0;
margin-bottom: 0.25rem;
padding-top: 0;
padding-bottom: 0.5rem;
border-top: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
:global(.dark) .fan-up .dropdown-footer {
border-bottom-color: rgba(255, 255, 255, 0.15);
}
</style>

View file

@ -120,6 +120,17 @@
allAppsHref?: string;
/** All Apps label (default: "Alle Apps") */
allAppsLabel?: string;
// A11y Settings
/** A11y contrast level */
a11yContrast?: 'normal' | 'high';
/** Called when a11y contrast changes */
onA11yContrastChange?: (contrast: 'normal' | 'high') => void;
/** A11y reduce motion setting */
a11yReduceMotion?: boolean;
/** Called when a11y reduce motion changes */
onA11yReduceMotionChange?: (reduce: boolean) => void;
/** Show a11y quick toggles in theme dropdown */
showA11yQuickToggles?: boolean;
}
let {
@ -156,6 +167,11 @@
loginHref,
allAppsHref,
allAppsLabel = 'Alle Apps',
a11yContrast = 'normal',
onA11yContrastChange,
a11yReduceMotion = false,
onA11yReduceMotionChange,
showA11yQuickToggles = false,
}: Props = $props();
// Type guards for elements
@ -241,6 +257,10 @@
// Icon SVG paths
const icons: Record<string, string> = {
mic: 'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z',
calendar:
'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
folder:
'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z',
archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
music:
@ -447,6 +467,44 @@
</button>
</div>
{/snippet}
{#snippet footer()}
{#if showA11yQuickToggles}
<div class="a11y-quick-toggles glass-pill">
<!-- Contrast Toggle -->
<button
type="button"
onclick={() => onA11yContrastChange?.(a11yContrast === 'high' ? 'normal' : 'high')}
class="a11y-btn"
class:active={a11yContrast === 'high'}
title="Hoher Kontrast"
aria-pressed={a11yContrast === 'high'}
>
<svg class="a11y-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<path d="M12 2v20M12 2a10 10 0 0 1 0 20" fill="currentColor" />
</svg>
</button>
<!-- Reduce Motion Toggle -->
<button
type="button"
onclick={() => onA11yReduceMotionChange?.(!a11yReduceMotion)}
class="a11y-btn"
class:active={a11yReduceMotion}
title="Animationen reduzieren"
aria-pressed={a11yReduceMotion}
>
<svg class="a11y-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
{#if a11yReduceMotion}
<rect x="6" y="4" width="4" height="16" rx="1" />
<rect x="14" y="4" width="4" height="16" rx="1" />
{:else}
<polygon points="5 3 19 12 5 21 5 3" />
{/if}
</svg>
</button>
</div>
{/if}
{/snippet}
</PillDropdown>
{/if}
@ -1167,4 +1225,71 @@
width: 1rem;
height: 1rem;
}
/* A11y quick toggles in dropdown footer */
:global(.a11y-quick-toggles) {
display: flex !important;
align-items: center !important;
gap: 0.25rem !important;
padding: 0.25rem !important;
border-radius: 9999px !important;
background: rgba(245, 245, 245, 0.95) !important;
border: 1px solid rgba(0, 0, 0, 0.1) !important;
color: #374151 !important;
}
:global(.dark .a11y-quick-toggles) {
background: rgba(40, 40, 40, 0.95) !important;
border: 1px solid rgba(255, 255, 255, 0.15) !important;
color: #f3f4f6 !important;
}
:global(.a11y-btn) {
display: flex;
align-items: center;
justify-content: center;
padding: 0.375rem;
border: none;
background: transparent;
border-radius: 9999px;
cursor: pointer;
color: #6b7280;
transition: all 0.15s;
}
:global(.dark .a11y-btn) {
color: #9ca3af;
}
:global(.a11y-btn:hover:not(.active)) {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
:global(.dark .a11y-btn:hover:not(.active)) {
background: rgba(255, 255, 255, 0.1);
color: #f3f4f6;
}
:global(.a11y-btn.active) {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 20%,
white 80%
);
color: var(--pill-primary-color, var(--color-primary-500, #3b82f6));
}
:global(.dark .a11y-btn.active) {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 30%,
transparent 70%
);
}
:global(.a11y-icon) {
width: 1rem;
height: 1rem;
}
</style>