mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
feat: draw tag colours in a colour-blind-safe palette when that setting is on
This commit is contained in:
@@ -1393,6 +1393,9 @@
|
||||
},
|
||||
"font": {
|
||||
"description_ios": "Atkinson Hyperlegible und Lexend sind so gestaltet, dass sich die Buchstaben gut unterscheiden; OpenDyslexic hilft manchen Menschen mit Legasthenie. Auf diesem Gerät gilt die Schrift für Nachrichten als reiner Text."
|
||||
},
|
||||
"cvd_palette": {
|
||||
"description_mobile": "Verwendet Farben für Tags und Kalender, die bei Rot-Grün- oder Blau-Gelb-Schwäche unterscheidbar bleiben."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -963,6 +963,9 @@
|
||||
"updates": "Updates"
|
||||
},
|
||||
"accessibility": {
|
||||
"cvd_palette": {
|
||||
"description_mobile": "Uses tag and calendar colours that stay distinct with red-green or blue-yellow colour blindness."
|
||||
},
|
||||
"font": {
|
||||
"description_ios": "Atkinson Hyperlegible and Lexend are designed to be easy to tell apart; OpenDyslexic helps some people with dyslexia. On this device the font is used for plain-text messages."
|
||||
},
|
||||
|
||||
@@ -76,6 +76,7 @@ export function AccessibilitySettings() {
|
||||
const update = useSettingsStore((s) => s.updateSetting);
|
||||
const fontSize = useSettingsStore((s) => s.fontSize);
|
||||
const appFont = useSettingsStore((s) => s.appFont);
|
||||
const colorBlindPalette = useSettingsStore((s) => s.colorBlindPalette);
|
||||
const animationsEnabled = useSettingsStore((s) => s.animationsEnabled);
|
||||
const toastDuration = useSettingsStore((s) => s.toastDuration);
|
||||
const sendConfirmation = useSettingsStore((s) => s.sendConfirmation);
|
||||
@@ -170,6 +171,20 @@ export function AccessibilitySettings() {
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('settings.accessibility.sections.contrast', 'Colour and contrast')}>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.cvd_palette.label', 'Colour-blind-safe colours')}
|
||||
description={t('settings.accessibility.cvd_palette.description_mobile', 'Uses tag and calendar colours that stay distinct with red-green or blue-yellow colour blindness.')}
|
||||
noBorder
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={colorBlindPalette}
|
||||
onChange={(v) => update('colorBlindPalette', v)}
|
||||
accessibilityLabel={t('settings.accessibility.cvd_palette.label', 'Colour-blind-safe colours')}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('settings.accessibility.sections.motion', 'Motion')}>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.reduce_motion.label', 'Reduce motion')}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function KeywordSettings() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
const palette = c.tags[kw.color];
|
||||
const palette = (c.tagsAsPicked ?? c.tags)[kw.color];
|
||||
return (
|
||||
<View key={kw.id} style={styles.kwRow}>
|
||||
<View style={[styles.kwDot, { backgroundColor: palette.dot }]} />
|
||||
@@ -178,7 +178,7 @@ export function KeywordSettings() {
|
||||
</Text>
|
||||
{scan.found.map((kw) => (
|
||||
<View key={kw.id} style={styles.kwRow}>
|
||||
<View style={[styles.kwDot, { backgroundColor: c.tags[kw.color]?.dot ?? c.textMuted }]} />
|
||||
<View style={[styles.kwDot, { backgroundColor: (c.tagsAsPicked ?? c.tags)[kw.color]?.dot ?? c.textMuted }]} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.kwLabel}>{kw.label}</Text>
|
||||
<Text style={styles.kwId}>{kw.keyword} · {kw.count}</Text>
|
||||
@@ -251,7 +251,7 @@ function SharedTagsSection() {
|
||||
) : (
|
||||
tags.map((kw) => (
|
||||
<View key={kw.id} style={styles.kwRow}>
|
||||
<View style={[styles.kwDot, { backgroundColor: c.tags[kw.color]?.dot ?? c.textMuted }]} />
|
||||
<View style={[styles.kwDot, { backgroundColor: (c.tagsAsPicked ?? c.tags)[kw.color]?.dot ?? c.textMuted }]} />
|
||||
<Text style={styles.kwLabel}>{kw.label}</Text>
|
||||
<Text style={styles.kwId}>$label:{kw.id}</Text>
|
||||
</View>
|
||||
@@ -313,7 +313,9 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
<Text style={styles.formLabel}>{t('settings.keywords.color_field', "Color")}</Text>
|
||||
<View style={styles.palette}>
|
||||
{PALETTE_KEYS.map((key) => {
|
||||
const p = c.tags[key];
|
||||
// The colours as picked, also with colour-blind-safe colours on
|
||||
// (webmail keyword-settings.tsx draws KEYWORD_PALETTE).
|
||||
const p = (c.tagsAsPicked ?? c.tags)[key];
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Colour-blind-safe colours (webmail lib/cvd-palette.ts and its tests,
|
||||
// cf4d02b49), and how the app's theme draws tags with the setting on.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CVD_COLORS, cvdColor, cvdHue, cvdTagPalette } from '../cvd-palette';
|
||||
import { resolvePalette } from '../../theme/colors';
|
||||
import { DARK_COLORS, LIGHT_COLORS } from '../../theme/tokens';
|
||||
import { mergeWithDefaults } from '../../stores/settings-store';
|
||||
|
||||
describe('cvdHue', () => {
|
||||
it('maps the default calendar and tag colours onto separate palette colours', () => {
|
||||
expect(cvdHue('#ef4444')).toBe('vermillion');
|
||||
expect(cvdHue('#f97316')).toBe('orange');
|
||||
expect(cvdHue('#eab308')).toBe('yellow');
|
||||
expect(cvdHue('#22c55e')).toBe('bluishGreen');
|
||||
expect(cvdHue('#06b6d4')).toBe('skyBlue');
|
||||
expect(cvdHue('#3b82f6')).toBe('blue');
|
||||
expect(cvdHue('#a855f7')).toBe('reddishPurple');
|
||||
});
|
||||
|
||||
it('keeps red and green apart, the pair red-green colour blindness merges', () => {
|
||||
expect(cvdHue('#dc2626')).not.toBe(cvdHue('#16a34a'));
|
||||
expect(cvdHue('#b91c1c')).toBe('vermillion');
|
||||
expect(cvdHue('#15803d')).toBe('bluishGreen');
|
||||
});
|
||||
|
||||
it('reads tag palette names, with their light and dark shades', () => {
|
||||
expect(cvdHue('red')).toBe('vermillion');
|
||||
expect(cvdHue('teal-light')).toBe('skyBlue');
|
||||
expect(cvdHue('indigo-dark')).toBe('blue');
|
||||
expect(cvdHue('gray')).toBe('grey');
|
||||
});
|
||||
|
||||
it('reads short hex, rgb() and hsl()', () => {
|
||||
expect(cvdHue('#f00')).toBe('vermillion');
|
||||
expect(cvdHue('rgb(0, 128, 0)')).toBe('bluishGreen');
|
||||
expect(cvdHue('hsl(220, 80%, 50%)')).toBe('blue');
|
||||
});
|
||||
|
||||
it('turns greys grey and gives up on what it cannot read', () => {
|
||||
expect(cvdHue('#777777')).toBe('grey');
|
||||
expect(cvdHue('var(--color-primary)')).toBeNull();
|
||||
expect(cvdHue('')).toBeNull();
|
||||
expect(cvdHue(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cvdColor', () => {
|
||||
it('draws a colour as it is while the setting is off', () => {
|
||||
expect(cvdColor('#3b82f6', false)).toBe('#3b82f6');
|
||||
});
|
||||
|
||||
it('draws the palette colour while it is on, and leaves unreadable colours alone', () => {
|
||||
expect(cvdColor('#3b82f6', true)).toBe(CVD_COLORS.blue);
|
||||
expect(cvdColor('var(--color-primary)', true)).toBe('var(--color-primary)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags drawn colour-blind-safe', () => {
|
||||
it('draws the dot in the palette colour, the fill as its tint, the label in the text colour', () => {
|
||||
const tags = cvdTagPalette(DARK_COLORS.tags, '#fafafa');
|
||||
expect(tags.red).toEqual({ dot: CVD_COLORS.vermillion, bg: 'rgba(213, 94, 0, 0.15)', text: '#fafafa' });
|
||||
expect(tags.green.dot).toBe(CVD_COLORS.bluishGreen);
|
||||
expect(tags.red.dot).not.toBe(tags.green.dot);
|
||||
expect(tags.gray.dot).toBe(CVD_COLORS.grey);
|
||||
});
|
||||
|
||||
it('switches every c.tags colour in the theme and keeps the picked ones for the picker', () => {
|
||||
const plain = resolvePalette('dark', null);
|
||||
expect(plain).toBe(DARK_COLORS);
|
||||
expect(plain.tags.red.dot).toBe('#ef4444');
|
||||
|
||||
const safe = resolvePalette('dark', null, true);
|
||||
expect(safe.tags.red.dot).toBe(CVD_COLORS.vermillion);
|
||||
expect(safe.tags.red.text).toBe(DARK_COLORS.text);
|
||||
expect(safe.tagsAsPicked).toBe(DARK_COLORS.tags);
|
||||
// The rest of the theme is untouched, and the palette object is stable.
|
||||
expect(safe.primary).toBe(DARK_COLORS.primary);
|
||||
expect(resolvePalette('dark', null, true)).toBe(safe);
|
||||
|
||||
const light = resolvePalette('light', 'builtin-roundcube-elastic', true);
|
||||
expect(light.tags.blue.dot).toBe(CVD_COLORS.blue);
|
||||
expect(light.tagsAsPicked).toBe(LIGHT_COLORS.tags);
|
||||
});
|
||||
|
||||
it('is off by default and kept when stored', () => {
|
||||
expect(mergeWithDefaults({}).colorBlindPalette).toBe(false);
|
||||
expect(mergeWithDefaults({ colorBlindPalette: true }).colorBlindPalette).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* The colour-blind-safe palette behind Settings > Accessibility >
|
||||
* "Colour-blind-safe colours" (colorBlindPalette). A port of the webmail's
|
||||
* lib/cvd-palette.ts (cf4d02b49).
|
||||
*
|
||||
* Tags and calendars keep the colours the user picked; this only changes how
|
||||
* they are drawn. Every colour is moved to the nearest of the Okabe-Ito
|
||||
* colours by hue: eight colours chosen to stay apart for protanopia,
|
||||
* deuteranopia and tritanopia (Okabe & Ito, "Color Universal Design", 2008).
|
||||
* Two picked colours of a similar hue (blue and indigo) can land on the same
|
||||
* one; the names next to them still tell them apart.
|
||||
*/
|
||||
|
||||
export const CVD_COLORS = {
|
||||
vermillion: '#D55E00',
|
||||
orange: '#E69F00',
|
||||
yellow: '#F0E442',
|
||||
bluishGreen: '#009E73',
|
||||
skyBlue: '#56B4E9',
|
||||
blue: '#0072B2',
|
||||
reddishPurple: '#CC79A7',
|
||||
grey: '#999999',
|
||||
} as const;
|
||||
|
||||
export type CvdHue = keyof typeof CVD_COLORS;
|
||||
|
||||
/** The hues of the tag palette (theme/tokens TAGS), as Tailwind's 500 shades. */
|
||||
const PALETTE_HUES: Record<string, string> = {
|
||||
red: '#ef4444',
|
||||
orange: '#f97316',
|
||||
amber: '#f59e0b',
|
||||
yellow: '#eab308',
|
||||
lime: '#84cc16',
|
||||
green: '#22c55e',
|
||||
teal: '#14b8a6',
|
||||
cyan: '#06b6d4',
|
||||
blue: '#3b82f6',
|
||||
indigo: '#6366f1',
|
||||
purple: '#a855f7',
|
||||
pink: '#ec4899',
|
||||
gray: '#6b7280',
|
||||
};
|
||||
|
||||
/** Hue (0-360) and saturation (0-1) of a colour, or null when it can't be read. */
|
||||
function hueAndSaturation(color: string): { hue: number; saturation: number } | null {
|
||||
let r: number, g: number, b: number;
|
||||
const value = color.trim().toLowerCase();
|
||||
const hex = /^#([0-9a-f]{3,8})$/.exec(value);
|
||||
if (hex) {
|
||||
let digits = hex[1];
|
||||
if (digits.length === 3 || digits.length === 4) digits = digits.slice(0, 3).split('').map((d) => d + d).join('');
|
||||
if (digits.length !== 6 && digits.length !== 8) return null;
|
||||
r = parseInt(digits.slice(0, 2), 16);
|
||||
g = parseInt(digits.slice(2, 4), 16);
|
||||
b = parseInt(digits.slice(4, 6), 16);
|
||||
} else {
|
||||
const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(value);
|
||||
const hsl = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/.exec(value);
|
||||
if (rgb) {
|
||||
[r, g, b] = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
||||
} else if (hsl) {
|
||||
return { hue: Number(hsl[1]) % 360, saturation: Number(hsl[2]) / 100 };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const [rn, gn, bn] = [r / 255, g / 255, b / 255];
|
||||
const max = Math.max(rn, gn, bn);
|
||||
const min = Math.min(rn, gn, bn);
|
||||
const delta = max - min;
|
||||
const lightness = (max + min) / 2;
|
||||
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
|
||||
let hue = 0;
|
||||
if (delta !== 0) {
|
||||
if (max === rn) hue = ((gn - bn) / delta) % 6;
|
||||
else if (max === gn) hue = (bn - rn) / delta + 2;
|
||||
else hue = (rn - gn) / delta + 4;
|
||||
hue *= 60;
|
||||
if (hue < 0) hue += 360;
|
||||
}
|
||||
return { hue, saturation };
|
||||
}
|
||||
|
||||
/**
|
||||
* The palette colour for `color`: a CSS colour (#hex, rgb(), hsl()) or a
|
||||
* tag palette name ("red", "teal-light"). Null when it can't be read.
|
||||
*/
|
||||
export function cvdHue(color: string | null | undefined): CvdHue | null {
|
||||
if (!color) return null;
|
||||
const paletteName = color.replace(/-(light|dark)$/, '');
|
||||
const parsed = hueAndSaturation(PALETTE_HUES[paletteName] ?? color);
|
||||
if (!parsed) return null;
|
||||
const { hue, saturation } = parsed;
|
||||
if (saturation < 0.15) return 'grey';
|
||||
if (hue < 18 || hue >= 345) return 'vermillion';
|
||||
if (hue < 44) return 'orange';
|
||||
if (hue < 70) return 'yellow';
|
||||
if (hue < 165) return 'bluishGreen';
|
||||
if (hue < 200) return 'skyBlue';
|
||||
if (hue < 255) return 'blue';
|
||||
return 'reddishPurple';
|
||||
}
|
||||
|
||||
/**
|
||||
* The colour to draw `color` in: itself, or its colour-blind-safe
|
||||
* replacement when `enabled`. A colour it can't read is drawn as it is.
|
||||
*/
|
||||
export function cvdColor(color: string, enabled: boolean): string {
|
||||
if (!enabled) return color;
|
||||
const hue = cvdHue(color);
|
||||
return hue ? CVD_COLORS[hue] : color;
|
||||
}
|
||||
|
||||
/** `#RRGGBB` at `alpha`, as rgba() (React Native has no 8-digit-hex tint helper here). */
|
||||
function withAlpha(hex: string, alpha: number): string {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
||||
}
|
||||
|
||||
export interface TagSwatch { dot: string; bg: string; text: string }
|
||||
|
||||
/**
|
||||
* A tag palette drawn colour-blind-safe, the way the webmail draws tags with
|
||||
* the setting on (CVD_CLASSES): the dot in the palette colour, the fill as a
|
||||
* 15 % tint of it, and the label in the text colour, since several of these
|
||||
* colours are too light to read as text.
|
||||
*/
|
||||
export function cvdTagPalette<K extends string>(tags: Record<K, TagSwatch>, textColor: string): Record<K, TagSwatch> {
|
||||
const out = {} as Record<K, TagSwatch>;
|
||||
for (const key of Object.keys(tags) as K[]) {
|
||||
const hex = CVD_COLORS[cvdHue(key) ?? cvdHue(tags[key].dot) ?? 'grey'];
|
||||
out[key] = { dot: hex, bg: withAlpha(hex, hex === CVD_COLORS.yellow ? 0.2 : 0.15), text: textColor };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export const SETTINGS_SEARCH_PATHS: Record<SettingsTabId, string[]> = {
|
||||
'settings.accessibility.presets.motion',
|
||||
'settings.accessibility.text_size',
|
||||
'settings.accessibility.font',
|
||||
'settings.accessibility.cvd_palette',
|
||||
'settings.accessibility.reduce_motion',
|
||||
'settings.accessibility.toast_duration',
|
||||
'settings.accessibility.confirm_send',
|
||||
@@ -269,7 +270,7 @@ export const SETTINGS_SEARCH_KEYWORDS: Record<SettingsTabId, string> = {
|
||||
recovery: 'recover restore undelete deleted trash emptied lost messages',
|
||||
language: 'locale region date time format translation',
|
||||
notifications: 'alert push badge unifiedpush relay device reminder',
|
||||
accessibility: 'a11y screen reader talkback voiceover low vision text size font bigger larger motion animation preset undo timeout duration confirm dyslexia dyslexic typeface atkinson lexend opendyslexic',
|
||||
accessibility: 'a11y screen reader talkback voiceover low vision text size font bigger larger motion animation preset undo timeout duration confirm dyslexia dyslexic typeface atkinson lexend opendyslexic colour color blind blindness palette',
|
||||
appearance: 'theme dark light font size text color animation density',
|
||||
layout: 'swipe gesture quick actions toolbar order sort unified folder count avatar',
|
||||
reading: 'mark read preview thread conversation archive delete attachment open monospace mono font plain text receipt',
|
||||
|
||||
@@ -166,6 +166,9 @@ export interface PersistedSettings {
|
||||
// settings.ts, same keys for export/import). The reading font for the
|
||||
// app's text and plain-text mail (theme/reading-fonts).
|
||||
appFont: AppFont;
|
||||
// Tag and calendar colours drawn in the colour-blind-safe palette
|
||||
// (lib/cvd-palette, theme/colors).
|
||||
colorBlindPalette: boolean;
|
||||
|
||||
// Composing
|
||||
autoSelectReplyIdentity: boolean;
|
||||
@@ -369,6 +372,7 @@ const DEFAULT_PERSISTED: PersistedSettings = {
|
||||
activeThemeId: null,
|
||||
// a11y-reading:
|
||||
appFont: 'default',
|
||||
colorBlindPalette: false,
|
||||
|
||||
autoSelectReplyIdentity: false,
|
||||
replyIdentityMatch: 'domain',
|
||||
|
||||
+28
-6
@@ -1,8 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { useSettingsStore } from '../stores/settings-store';
|
||||
import { LIGHT_COLORS, DARK_COLORS, type ThemePalette } from './tokens';
|
||||
import { getBuiltinTheme } from './builtin-themes';
|
||||
import { cvdColor, cvdTagPalette } from '../lib/cvd-palette';
|
||||
|
||||
export type { ThemePalette };
|
||||
|
||||
@@ -12,15 +13,24 @@ const paletteCache = new Map<string, ThemePalette>();
|
||||
* Base light/dark palette with the active built-in theme's overrides applied
|
||||
* (Settings → Themes). Memoised per (scheme, theme) pair so `useColors()`
|
||||
* keeps returning a stable object for `useMemo(() => makeStyles(c), [c])`.
|
||||
*
|
||||
* With "Colour-blind-safe colours" (Settings → Accessibility, webmail
|
||||
* colorBlindPalette) the tag colours are drawn in that palette
|
||||
* (lib/cvd-palette): every place that draws a tag from `c.tags` follows,
|
||||
* as the webmail's tag formatter does. The picked colours stay in
|
||||
* `tagsAsPicked` for the colour picker and tag settings.
|
||||
*/
|
||||
export function resolvePalette(scheme: 'light' | 'dark', themeId: string | null): ThemePalette {
|
||||
export function resolvePalette(scheme: 'light' | 'dark', themeId: string | null, colorBlind = false): ThemePalette {
|
||||
const base = scheme === 'light' ? LIGHT_COLORS : DARK_COLORS;
|
||||
const theme = getBuiltinTheme(themeId);
|
||||
if (!theme) return base;
|
||||
const key = `${scheme}:${theme.id}`;
|
||||
if (!theme && !colorBlind) return base;
|
||||
const key = `${scheme}:${theme?.id ?? ''}:${colorBlind ? 'cvd' : ''}`;
|
||||
const cached = paletteCache.get(key);
|
||||
if (cached) return cached;
|
||||
const merged: ThemePalette = { ...base, ...(scheme === 'light' ? theme.light : theme.dark) };
|
||||
let merged: ThemePalette = theme ? { ...base, ...(scheme === 'light' ? theme.light : theme.dark) } : base;
|
||||
if (colorBlind) {
|
||||
merged = { ...merged, tags: cvdTagPalette(merged.tags, merged.text), tagsAsPicked: merged.tags };
|
||||
}
|
||||
paletteCache.set(key, merged);
|
||||
return merged;
|
||||
}
|
||||
@@ -36,12 +46,24 @@ export function resolvePalette(scheme: 'light' | 'dark', themeId: string | null)
|
||||
export function useColors(): ThemePalette {
|
||||
const themePref = useSettingsStore((s) => s.theme);
|
||||
const activeThemeId = useSettingsStore((s) => s.activeThemeId);
|
||||
const colorBlind = useSettingsStore((s) => s.colorBlindPalette);
|
||||
const systemScheme = useColorScheme();
|
||||
const resolved =
|
||||
themePref === 'system'
|
||||
? systemScheme === 'light' ? 'light' : 'dark'
|
||||
: themePref;
|
||||
return useMemo(() => resolvePalette(resolved, activeThemeId), [resolved, activeThemeId]);
|
||||
return useMemo(() => resolvePalette(resolved, activeThemeId, colorBlind), [resolved, activeThemeId, colorBlind]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a colour the user picked (a calendar's, a tag's hex) in the
|
||||
* colour-blind-safe palette while that setting is on, and as it is otherwise
|
||||
* (webmail hooks/use-cvd-color.ts). For colours that don't come from
|
||||
* `c.tags`, such as calendar colours from the server.
|
||||
*/
|
||||
export function useCvdColor(): (color: string) => string {
|
||||
const enabled = useSettingsStore((s) => s.colorBlindPalette);
|
||||
return useCallback((color: string) => cvdColor(color, enabled), [enabled]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,6 +74,12 @@ export interface ThemePalette {
|
||||
|
||||
// Tags
|
||||
tags: TagColors;
|
||||
/**
|
||||
* The tag colours as the user picks them, set while tags are drawn in the
|
||||
* colour-blind-safe palette (theme/colors): the colour picker and the tag
|
||||
* settings show the colours themselves, as in the webmail.
|
||||
*/
|
||||
tagsAsPicked?: TagColors;
|
||||
|
||||
// Navigation
|
||||
navActive: string;
|
||||
|
||||
Reference in New Issue
Block a user