Skip to content

Commit f5d5df8

Browse files
committed
feat(docs): add colorblind-friendly accessibility themes
- Add protanopia, deuteranopia, and tritanopia theme definitions - Add high contrast theme for improved accessibility - Update theme selector with color swatch UI for colorblind themes - Add checkmark indicator for selected colorblind theme - Include theme preview in accessibility section
1 parent e1008ba commit f5d5df8

File tree

7 files changed

+196
-34
lines changed

7 files changed

+196
-34
lines changed

apps/docs/src/components/app/AppThemeSelector.vue

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
3232
const accessibilityOptions: ThemeOption[] = [
3333
{ id: 'high-contrast', label: 'High Contrast', icon: 'theme-high-contrast', theme: 'high-contrast' },
34+
{ id: 'protanopia', label: 'Protanopia', icon: 'theme-protanopia', theme: 'protanopia' },
35+
{ id: 'deuteranopia', label: 'Deuteranopia', icon: 'theme-deuteranopia', theme: 'deuteranopia' },
36+
{ id: 'tritanopia', label: 'Tritanopia', icon: 'theme-tritanopia', theme: 'tritanopia' },
3437
]
3538
3639
const vuetifyOptions: ThemeOption[] = [
@@ -92,22 +95,27 @@
9295
<!-- Accessibility -->
9396
<div class="mb-3">
9497
<div class="text-xs font-medium text-on-surface-variant mb-2 px-1">Accessibility</div>
95-
<button
96-
v-for="option in accessibilityOptions"
97-
:key="option.id"
98-
:aria-pressed="toggle.preference.value === option.id"
99-
:class="[
100-
'w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs font-medium transition-colors',
101-
toggle.preference.value === option.id
102-
? 'bg-primary/15 text-primary'
103-
: 'hover:bg-surface-tint text-on-surface',
104-
]"
105-
type="button"
106-
@click="selectTheme(option.id)"
107-
>
108-
<AppIcon :icon="option.icon" size="14" />
109-
<span>{{ option.label }}</span>
110-
</button>
98+
<div class="grid grid-cols-2 gap-1">
99+
<button
100+
v-for="option in accessibilityOptions"
101+
:key="option.id"
102+
:aria-pressed="toggle.preference.value === option.id"
103+
:class="[
104+
'flex flex-col items-start gap-1.5 px-2 py-1.5 rounded text-xs font-medium transition-colors',
105+
toggle.preference.value === option.id
106+
? 'bg-primary/15 text-primary'
107+
: 'hover:bg-surface-tint text-on-surface',
108+
]"
109+
type="button"
110+
@click="selectTheme(option.id)"
111+
>
112+
<div class="flex items-center gap-1.5">
113+
<AppIcon :icon="option.icon" size="14" />
114+
<span>{{ option.label }}</span>
115+
</div>
116+
<AppThemePreview v-if="option.theme" :theme="option.theme" />
117+
</button>
118+
</div>
111119
</div>
112120

113121
<!-- Vuetify Themes -->

apps/docs/src/components/app/settings/AppSettingsTheme.vue

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
{ id: 'dark', label: 'Dark', icon: 'theme-dark', theme: 'dark' },
4343
]
4444
45-
const accessibilityOptions: ThemeOption[] = [
46-
{ id: 'high-contrast', label: 'High Contrast', icon: 'theme-high-contrast', theme: 'high-contrast' },
45+
const colorblindOptions: ThemeOption[] = [
46+
{ id: 'protanopia', label: 'Protanopia', icon: 'theme-protanopia', theme: 'protanopia' },
47+
{ id: 'deuteranopia', label: 'Deuteranopia', icon: 'theme-deuteranopia', theme: 'deuteranopia' },
48+
{ id: 'tritanopia', label: 'Tritanopia', icon: 'theme-tritanopia', theme: 'tritanopia' },
4749
]
4850
4951
const vuetifyOptions: ThemeOption[] = [
@@ -187,22 +189,46 @@
187189
<!-- Accessibility -->
188190
<div>
189191
<div class="text-xs font-medium text-on-surface-variant mb-2">Accessibility</div>
190-
<button
191-
v-for="option in accessibilityOptions"
192-
:key="option.id"
193-
:aria-pressed="toggle.preference.value === option.id"
194-
:class="[
195-
'w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition-colors',
196-
toggle.preference.value === option.id
197-
? 'border-primary bg-primary/10 text-primary'
198-
: 'border-divider hover:border-primary/50 text-on-surface',
199-
]"
200-
type="button"
201-
@click="toggle.setPreference(option.id)"
202-
>
203-
<AppIcon :icon="option.icon" size="16" />
204-
<span>{{ option.label }}</span>
205-
</button>
192+
<div class="flex gap-2">
193+
<!-- High Contrast - larger button -->
194+
<button
195+
:aria-pressed="toggle.preference.value === 'high-contrast'"
196+
:class="[
197+
'flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg border text-sm transition-colors',
198+
toggle.preference.value === 'high-contrast'
199+
? 'border-primary bg-primary/10 text-primary'
200+
: 'border-divider hover:border-primary/50 text-on-surface',
201+
]"
202+
type="button"
203+
@click="toggle.setPreference('high-contrast')"
204+
>
205+
<AppIcon icon="theme-high-contrast" size="16" />
206+
<span class="font-medium">High Contrast</span>
207+
</button>
208+
209+
<!-- Colorblind themes - small color squares -->
210+
<button
211+
v-for="option in colorblindOptions"
212+
:key="option.id"
213+
:aria-label="`${option.label} theme`"
214+
:aria-pressed="toggle.preference.value === option.id"
215+
:class="[
216+
'h-9 w-6 shrink-0 rounded border transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background',
217+
toggle.preference.value === option.id
218+
? 'border-primary ring-2 ring-primary/50 opacity-100'
219+
: 'opacity-50 hover:opacity-100',
220+
]"
221+
:style="{
222+
backgroundColor: themes[option.theme!].colors.primary,
223+
color: themes[option.theme!].colors['on-primary'],
224+
}"
225+
:title="option.label"
226+
type="button"
227+
@click="toggle.setPreference(option.id)"
228+
>
229+
<AppIcon v-if="toggle.preference.value === option.id" icon="check-circle" size="12" />
230+
</button>
231+
</div>
206232
</div>
207233

208234
<!-- Vuetify Themes -->

apps/docs/src/composables/useThemeToggle.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const PREFERENCE_ORDER = [
1515
'light',
1616
'dark',
1717
'high-contrast',
18+
'protanopia',
19+
'deuteranopia',
20+
'tritanopia',
1821
'blackguard',
1922
'polaris',
2023
'nebula',
@@ -26,6 +29,9 @@ const PREFERENCE_ICONS: Record<string, string> = {
2629
'light': 'theme-light',
2730
'dark': 'theme-dark',
2831
'high-contrast': 'theme-high-contrast',
32+
'protanopia': 'theme-protanopia',
33+
'deuteranopia': 'theme-deuteranopia',
34+
'tritanopia': 'theme-tritanopia',
2935
'blackguard': 'theme-blackguard',
3036
'polaris': 'theme-polaris',
3137
'nebula': 'theme-nebula',
@@ -37,6 +43,9 @@ const PREFERENCE_LABELS: Record<string, string> = {
3743
'light': 'Light',
3844
'dark': 'Dark',
3945
'high-contrast': 'High Contrast',
46+
'protanopia': 'Protanopia',
47+
'deuteranopia': 'Deuteranopia',
48+
'tritanopia': 'Tritanopia',
4049
'blackguard': 'Blackguard',
4150
'polaris': 'Polaris',
4251
'nebula': 'Nebula',

apps/docs/src/plugins/icons.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
mdiDotsGrid,
3434
mdiGradientVertical,
3535
mdiDownload,
36+
mdiEye,
3637
mdiEyeOffOutline,
3738
mdiFirefox,
3839
mdiFolderZipOutline,
@@ -128,6 +129,9 @@ export const [useIconContext, provideIconContext, context] = createTokensContext
128129
'theme-light': mdiWeatherSunny,
129130
'theme-dark': mdiWeatherNight,
130131
'theme-high-contrast': mdiContrastCircle,
132+
'theme-protanopia': mdiEye,
133+
'theme-deuteranopia': mdiEye,
134+
'theme-tritanopia': mdiEye,
131135
'theme-settings': mdiPalette,
132136
'theme-system': mdiMonitor,
133137
'theme-blackguard': mdiSpaceInvaders,

apps/docs/src/themes/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,105 @@ export const themes = {
125125
'github': '#24292f',
126126
},
127127
},
128+
'protanopia': {
129+
id: 'protanopia',
130+
label: 'Protanopia',
131+
icon: 'theme-protanopia',
132+
dark: true,
133+
colors: {
134+
'primary': '#56b4e9',
135+
'secondary': '#e69f00',
136+
'accent': '#f0e442',
137+
'error': '#d55e00',
138+
'info': '#56b4e9',
139+
'success': '#009e73',
140+
'warning': '#e69f00',
141+
'background': '#1a1a2e',
142+
'surface': '#16213e',
143+
'surface-tint': '#1f2b4a',
144+
'surface-variant': '#1a2540',
145+
'divider': '#3a4a6a',
146+
'pre': '#141c30',
147+
'on-primary': '#000000',
148+
'on-secondary': '#000000',
149+
'on-accent': '#000000',
150+
'on-error': '#ffffff',
151+
'on-info': '#000000',
152+
'on-success': '#000000',
153+
'on-warning': '#000000',
154+
'on-background': '#e8e8e8',
155+
'on-surface': '#e8e8e8',
156+
'on-surface-variant': '#a0a8b8',
157+
'glass-surface': 'rgba(22, 33, 62, 0.8)',
158+
'scrollbar-thumb': 'rgba(86, 180, 233, 0.4)',
159+
},
160+
},
161+
'deuteranopia': {
162+
id: 'deuteranopia',
163+
label: 'Deuteranopia',
164+
icon: 'theme-deuteranopia',
165+
dark: true,
166+
colors: {
167+
'primary': '#648fff',
168+
'secondary': '#ffb000',
169+
'accent': '#dc267f',
170+
'error': '#fe6100',
171+
'info': '#648fff',
172+
'success': '#785ef0',
173+
'warning': '#ffb000',
174+
'background': '#1b1b2f',
175+
'surface': '#1f1f3a',
176+
'surface-tint': '#282848',
177+
'surface-variant': '#232340',
178+
'divider': '#3d3d5c',
179+
'pre': '#181830',
180+
'on-primary': '#000000',
181+
'on-secondary': '#000000',
182+
'on-accent': '#ffffff',
183+
'on-error': '#000000',
184+
'on-info': '#000000',
185+
'on-success': '#ffffff',
186+
'on-warning': '#000000',
187+
'on-background': '#e8e8f0',
188+
'on-surface': '#e8e8f0',
189+
'on-surface-variant': '#a0a0b8',
190+
'glass-surface': 'rgba(31, 31, 58, 0.8)',
191+
'scrollbar-thumb': 'rgba(100, 143, 255, 0.4)',
192+
},
193+
},
194+
'tritanopia': {
195+
id: 'tritanopia',
196+
label: 'Tritanopia',
197+
icon: 'theme-tritanopia',
198+
dark: true,
199+
colors: {
200+
'primary': '#ff6b6b',
201+
'secondary': '#4ecdc4',
202+
'accent': '#ffe66d',
203+
'error': '#ff4757',
204+
'info': '#45b7d1',
205+
'success': '#26de81',
206+
'warning': '#fed330',
207+
'background': '#1a1a1a',
208+
'surface': '#2d2d2d',
209+
'surface-tint': '#383838',
210+
'surface-variant': '#333333',
211+
'divider': '#4a4a4a',
212+
'pre': '#242424',
213+
'on-primary': '#000000',
214+
'on-secondary': '#000000',
215+
'on-accent': '#000000',
216+
'on-error': '#000000',
217+
'on-info': '#000000',
218+
'on-success': '#000000',
219+
'on-warning': '#000000',
220+
'on-background': '#f0f0f0',
221+
'on-surface': '#f0f0f0',
222+
'on-surface-variant': '#b0b0b0',
223+
'glass-surface': 'rgba(45, 45, 45, 0.8)',
224+
'scrollbar-thumb': 'rgba(255, 107, 107, 0.4)',
225+
},
226+
},
128227
'blackguard': {
129228
id: 'blackguard',
130229
label: 'Blackguard',

playground/src/components.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ declare module 'vue' {
1616
AvatarImage: typeof import('./../../packages/0/src/components/Avatar/AvatarImage.vue')['default']
1717
AvatarRoot: typeof import('./../../packages/0/src/components/Avatar/AvatarRoot.vue')['default']
1818
BreadcrumbsRoot: typeof import('./../../packages/0/src/components/Breadcrumbs/BreadcrumbsRoot.vue')['default']
19+
CheckboxGroup: typeof import('./../../packages/0/src/components/Checkbox/CheckboxGroup.vue')['default']
20+
CheckboxHiddenInput: typeof import('./../../packages/0/src/components/Checkbox/CheckboxHiddenInput.vue')['default']
21+
CheckboxIndicator: typeof import('./../../packages/0/src/components/Checkbox/CheckboxIndicator.vue')['default']
22+
CheckboxRoot: typeof import('./../../packages/0/src/components/Checkbox/CheckboxRoot.vue')['default']
23+
CheckboxSelectAll: typeof import('./../../packages/0/src/components/Checkbox/CheckboxSelectAll.vue')['default']
1924
DialogActivator: typeof import('./../../packages/0/src/components/Dialog/DialogActivator.vue')['default']
2025
DialogClose: typeof import('./../../packages/0/src/components/Dialog/DialogClose.vue')['default']
2126
DialogContent: typeof import('./../../packages/0/src/components/Dialog/DialogContent.vue')['default']
@@ -40,6 +45,10 @@ declare module 'vue' {
4045
PopoverActivator: typeof import('./../../packages/0/src/components/Popover/PopoverActivator.vue')['default']
4146
PopoverContent: typeof import('./../../packages/0/src/components/Popover/PopoverContent.vue')['default']
4247
PopoverRoot: typeof import('./../../packages/0/src/components/Popover/PopoverRoot.vue')['default']
48+
RadioGroup: typeof import('./../../packages/0/src/components/Radio/RadioGroup.vue')['default']
49+
RadioHiddenInput: typeof import('./../../packages/0/src/components/Radio/RadioHiddenInput.vue')['default']
50+
RadioIndicator: typeof import('./../../packages/0/src/components/Radio/RadioIndicator.vue')['default']
51+
RadioRoot: typeof import('./../../packages/0/src/components/Radio/RadioRoot.vue')['default']
4352
RouterLink: typeof import('vue-router')['RouterLink']
4453
RouterView: typeof import('vue-router')['RouterView']
4554
SelectionItem: typeof import('./../../packages/0/src/components/Selection/SelectionItem.vue')['default']
@@ -48,6 +57,7 @@ declare module 'vue' {
4857
SingleRoot: typeof import('./../../packages/0/src/components/Single/SingleRoot.vue')['default']
4958
StepItem: typeof import('./../../packages/0/src/components/Step/StepItem.vue')['default']
5059
StepRoot: typeof import('./../../packages/0/src/components/Step/StepRoot.vue')['default']
60+
TabsItem: typeof import('./../../packages/0/src/components/Tabs/TabsItem.vue')['default']
5161
TabsList: typeof import('./../../packages/0/src/components/Tabs/TabsList.vue')['default']
5262
TabsPanel: typeof import('./../../packages/0/src/components/Tabs/TabsPanel.vue')['default']
5363
TabsRoot: typeof import('./../../packages/0/src/components/Tabs/TabsRoot.vue')['default']

playground/src/composables.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ declare global {
1212
const DEFAULT_LIGHT: typeof import('../../packages/paper/src/composables/useTheme/index').DEFAULT_LIGHT
1313
const EffectScope: typeof import('vue').EffectScope
1414
const FeaturesAdapter: typeof import('../../packages/0/src/composables/useFeatures/index').FeaturesAdapter
15+
const FlagsmithFeatureAdapter: typeof import('../../packages/0/src/composables/useFeatures/index').FlagsmithFeatureAdapter
1516
const IN_BROWSER: typeof import('../../packages/0/src/constants/globals').IN_BROWSER
17+
const LaunchDarklyFeatureAdapter: typeof import('../../packages/0/src/composables/useFeatures/index').LaunchDarklyFeatureAdapter
1618
const MemoryAdapter: typeof import('../../packages/0/src/composables/useStorage/index').MemoryAdapter
1719
const PermissionAdapter: typeof import('../../packages/0/src/composables/usePermissions/index').PermissionAdapter
1820
const PinoLoggerAdapter: typeof import('../../packages/0/src/composables/useLogger/index').PinoLoggerAdapter
21+
const PostHogFeatureAdapter: typeof import('../../packages/0/src/composables/useFeatures/index').PostHogFeatureAdapter
1922
const SELF_CLOSING_TAGS: typeof import('../../packages/0/src/constants/htmlElements').SELF_CLOSING_TAGS
2023
const SUPPORTS_INTERSECTION_OBSERVER: typeof import('../../packages/0/src/constants/globals').SUPPORTS_INTERSECTION_OBSERVER
2124
const SUPPORTS_MATCH_MEDIA: typeof import('../../packages/0/src/constants/globals').SUPPORTS_MATCH_MEDIA
@@ -382,10 +385,13 @@ declare module 'vue' {
382385
readonly DEFAULT_LIGHT: UnwrapRef<typeof import('../../packages/paper/src/composables/useTheme/index')['DEFAULT_LIGHT']>
383386
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
384387
readonly FeaturesAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useFeatures/index')['FeaturesAdapter']>
388+
readonly FlagsmithFeatureAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useFeatures/index')['FlagsmithFeatureAdapter']>
385389
readonly IN_BROWSER: UnwrapRef<typeof import('../../packages/0/src/constants/globals')['IN_BROWSER']>
390+
readonly LaunchDarklyFeatureAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useFeatures/index')['LaunchDarklyFeatureAdapter']>
386391
readonly MemoryAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useStorage/index')['MemoryAdapter']>
387392
readonly PermissionAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/usePermissions/index')['PermissionAdapter']>
388393
readonly PinoLoggerAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useLogger/index')['PinoLoggerAdapter']>
394+
readonly PostHogFeatureAdapter: UnwrapRef<typeof import('../../packages/0/src/composables/useFeatures/index')['PostHogFeatureAdapter']>
389395
readonly SELF_CLOSING_TAGS: UnwrapRef<typeof import('../../packages/0/src/constants/htmlElements')['SELF_CLOSING_TAGS']>
390396
readonly SUPPORTS_INTERSECTION_OBSERVER: UnwrapRef<typeof import('../../packages/0/src/constants/globals')['SUPPORTS_INTERSECTION_OBSERVER']>
391397
readonly SUPPORTS_MATCH_MEDIA: UnwrapRef<typeof import('../../packages/0/src/constants/globals')['SUPPORTS_MATCH_MEDIA']>

0 commit comments

Comments
 (0)