mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 18:41:24 +02:00
✨ 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:
parent
6cc9f70a4a
commit
02c82c7547
33 changed files with 1474 additions and 143 deletions
114
packages/shared-theme-ui/src/components/A11yQuickToggles.svelte
Normal file
114
packages/shared-theme-ui/src/components/A11yQuickToggles.svelte
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<script lang="ts">
|
||||
import type { A11yStore } from '@manacore/shared-theme';
|
||||
|
||||
interface Props {
|
||||
/** A11y store instance */
|
||||
store: A11yStore;
|
||||
/** Contrast toggle title */
|
||||
contrastTitle?: string;
|
||||
/** Motion toggle title */
|
||||
motionTitle?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
store,
|
||||
contrastTitle = 'Hoher Kontrast',
|
||||
motionTitle = 'Animationen reduzieren',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="a11y-quick-toggles">
|
||||
<!-- Contrast Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => store.setContrast(store.contrast === 'high' ? 'normal' : 'high')}
|
||||
class="a11y-btn"
|
||||
class:active={store.contrast === 'high'}
|
||||
title={contrastTitle}
|
||||
aria-pressed={store.contrast === '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={() => store.setReduceMotion(!store.reduceMotion)}
|
||||
class="a11y-btn"
|
||||
class:active={store.reduceMotion}
|
||||
title={motionTitle}
|
||||
aria-pressed={store.reduceMotion}
|
||||
>
|
||||
<svg class="a11y-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
{#if store.reduceMotion}
|
||||
<!-- Pause icon when motion is reduced -->
|
||||
<rect x="6" y="4" width="4" height="16" rx="1" />
|
||||
<rect x="14" y="4" width="4" height="16" rx="1" />
|
||||
{:else}
|
||||
<!-- Play/motion icon -->
|
||||
<polygon points="5 3 19 12 5 21 5 3" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.a11y-quick-toggles {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(245, 245, 245, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .a11y-quick-toggles {
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.a11y-btn.active {
|
||||
background: hsl(var(--color-primary) / 0.2);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
:global(.dark) .a11y-btn.active {
|
||||
background: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.a11y-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
||||
188
packages/shared-theme-ui/src/components/A11ySettings.svelte
Normal file
188
packages/shared-theme-ui/src/components/A11ySettings.svelte
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<script lang="ts">
|
||||
import type { A11yStore, ContrastLevel, ColorblindMode } from '@manacore/shared-theme';
|
||||
import { COLORBLIND_OPTIONS, CONTRAST_OPTIONS } from '@manacore/shared-theme';
|
||||
import type { A11yTranslations } from '../types';
|
||||
import { defaultA11yTranslations } from '../types';
|
||||
|
||||
interface Props {
|
||||
/** A11y store instance */
|
||||
store: A11yStore;
|
||||
/** Custom translations */
|
||||
translations?: Partial<A11yTranslations>;
|
||||
/** Show reset button */
|
||||
showReset?: boolean;
|
||||
}
|
||||
|
||||
let { store, translations = {}, showReset = true }: Props = $props();
|
||||
|
||||
// Merge translations with defaults
|
||||
const t = $derived({ ...defaultA11yTranslations, ...translations });
|
||||
|
||||
// Colorblind mode labels mapped to translations
|
||||
const colorblindLabels: Record<ColorblindMode, string> = $derived({
|
||||
none: t.colorblindNone,
|
||||
deuteranopia: t.colorblindDeuteranopia,
|
||||
protanopia: t.colorblindProtanopia,
|
||||
monochrome: t.colorblindMonochrome,
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="a11y-settings space-y-6">
|
||||
<!-- Contrast Setting -->
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">{t.contrastLabel}</span>
|
||||
<div class="inline-flex rounded-lg bg-muted p-1" role="group" aria-label={t.contrastLabel}>
|
||||
{#each CONTRAST_OPTIONS as option}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => store.setContrast(option.value)}
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors
|
||||
{store.contrast === option.value
|
||||
? 'bg-surface text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
title={option.description}
|
||||
>
|
||||
{option.value === 'normal' ? t.contrastNormal : t.contrastHigh}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colorblind Mode -->
|
||||
<div class="setting-group">
|
||||
<label for="colorblind-select" class="setting-label">{t.colorblindLabel}</label>
|
||||
<select
|
||||
id="colorblind-select"
|
||||
class="select-input"
|
||||
value={store.colorblind}
|
||||
onchange={(e) => store.setColorblind(e.currentTarget.value as ColorblindMode)}
|
||||
>
|
||||
{#each COLORBLIND_OPTIONS as option}
|
||||
<option value={option.value}>
|
||||
{colorblindLabels[option.value]}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Reduce Motion -->
|
||||
<div class="setting-group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="reduce-motion" class="setting-label mb-0">{t.reduceMotionLabel}</label>
|
||||
<p class="text-sm text-muted-foreground">{t.reduceMotionDescription}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if store.reduceMotionExplicit}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => store.resetReduceMotion()}
|
||||
class="text-xs text-muted-foreground hover:text-foreground underline"
|
||||
>
|
||||
{t.systemDefault}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
id="reduce-motion"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={store.reduceMotion}
|
||||
aria-label={t.reduceMotionLabel}
|
||||
onclick={() => store.setReduceMotion(!store.reduceMotion)}
|
||||
class="toggle-switch"
|
||||
class:active={store.reduceMotion}
|
||||
>
|
||||
<span class="toggle-thumb" class:active={store.reduceMotion}></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Button -->
|
||||
{#if showReset}
|
||||
<div class="pt-2 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => store.resetAll()}
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{t.reset}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.a11y-settings {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
width: 100%;
|
||||
max-width: 20rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-input));
|
||||
color: hsl(var(--color-foreground));
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.select-input:hover {
|
||||
border-color: hsl(var(--color-border-strong));
|
||||
}
|
||||
|
||||
.select-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-ring));
|
||||
box-shadow: 0 0 0 2px hsl(var(--color-ring) / 0.2);
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 2.75rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 9999px;
|
||||
background: hsl(var(--color-muted));
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 9999px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-thumb.active {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue