mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
feat: port the webmail's built-in colour themes to the Themes pane
This commit is contained in:
@@ -1,87 +1,92 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||||
import { Upload, Check, Palette } from 'lucide-react-native';
|
import { Check } from 'lucide-react-native';
|
||||||
import { SettingsSection, SettingItem } from './settings-section';
|
import { SettingsSection } from './settings-section';
|
||||||
import Button from '../Button';
|
|
||||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||||
import { useColors } from '../../theme/colors';
|
import { resolvePalette, useColors, useResolvedTheme } from '../../theme/colors';
|
||||||
|
import { BUILTIN_THEMES } from '../../theme/builtin-themes';
|
||||||
import { useSettingsStore } from '../../stores/settings-store';
|
import { useSettingsStore } from '../../stores/settings-store';
|
||||||
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
|
|
||||||
interface Theme {
|
interface ThemeCard {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
author: string;
|
description: string;
|
||||||
builtIn?: boolean;
|
|
||||||
variants?: ('light' | 'dark')[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUILT_IN: Theme[] = [
|
/**
|
||||||
{ id: null, name: 'Default', author: 'Bulwark', builtIn: true, variants: ['light', 'dark'] },
|
* Built-in colour themes ported from the webmail (lib/builtin-themes.ts).
|
||||||
{ id: 'qui', name: 'Qui', author: 'Bulwark', builtIn: true, variants: ['dark'] },
|
* Custom zip themes and the marketplace stay webmail-only: the native app has
|
||||||
{ id: 'sepia', name: 'Sepia', author: 'Bulwark', builtIn: true, variants: ['light'] },
|
* no CSS pipeline, only the token subset each theme overrides.
|
||||||
];
|
*/
|
||||||
|
|
||||||
export function ThemesSettings() {
|
export function ThemesSettings() {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
|
const t = useLocaleStore((s) => s.t);
|
||||||
|
const scheme = useResolvedTheme();
|
||||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||||
const active = useSettingsStore((s) => s.activeThemeId);
|
const active = useSettingsStore((s) => s.activeThemeId);
|
||||||
const update = useSettingsStore((s) => s.updateSetting);
|
const update = useSettingsStore((s) => s.updateSetting);
|
||||||
const [themes] = useState<Theme[]>(BUILT_IN);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hydrated) void hydrate();
|
if (!hydrated) void hydrate();
|
||||||
}, [hydrated, hydrate]);
|
}, [hydrated, hydrate]);
|
||||||
|
|
||||||
|
const cards: ThemeCard[] = [
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
name: t('settings.themes.default_name', 'Bulwark'),
|
||||||
|
description: t('settings.themes.default_description', 'The default light and dark palettes.'),
|
||||||
|
},
|
||||||
|
...BUILTIN_THEMES.map((theme) => ({ id: theme.id, name: theme.name, description: theme.description })),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title="Themes"
|
title={t('settings.themes.title', 'Themes')}
|
||||||
description="Customize the appearance with color themes."
|
description={t(
|
||||||
experimental
|
'settings.themes.mobile_description',
|
||||||
experimentalDescription="Themes is an experimental feature. Custom themes may not cover all UI elements, and theme formats could change in future updates."
|
'Pick a colour theme. Themes follow the light/dark setting from Appearance; custom theme packages are managed in the webmail.',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<View style={styles.grid}>
|
<View style={styles.grid}>
|
||||||
{themes.map((theme) => {
|
{cards.map((card) => {
|
||||||
const isActive = active === theme.id;
|
const isActive = (active ?? null) === card.id;
|
||||||
|
const preview = resolvePalette(scheme, card.id);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={theme.id ?? 'default'}
|
key={card.id ?? 'default'}
|
||||||
onPress={() => update('activeThemeId', theme.id)}
|
onPress={() => update('activeThemeId', card.id)}
|
||||||
|
accessibilityRole="radio"
|
||||||
|
accessibilityState={{ selected: isActive, checked: isActive }}
|
||||||
|
accessibilityLabel={card.name}
|
||||||
style={[styles.card, isActive && styles.cardActive]}
|
style={[styles.card, isActive && styles.cardActive]}
|
||||||
>
|
>
|
||||||
<View style={styles.preview}>
|
<View style={[styles.preview, { backgroundColor: preview.background, borderColor: preview.border }]}>
|
||||||
<Palette size={32} color={c.mutedForeground} style={{ opacity: 0.4 }} />
|
<View style={[styles.previewRail, { backgroundColor: preview.surface, borderRightColor: preview.border }]} />
|
||||||
|
<View style={styles.previewBody}>
|
||||||
|
<View style={[styles.previewBar, { backgroundColor: preview.primary, width: '55%' }]} />
|
||||||
|
<View style={[styles.previewBar, { backgroundColor: preview.text, width: '80%', opacity: 0.8 }]} />
|
||||||
|
<View style={[styles.previewBar, { backgroundColor: preview.mutedForeground, width: '65%', opacity: 0.6 }]} />
|
||||||
|
<View style={styles.previewDots}>
|
||||||
|
{[preview.success, preview.warning, preview.error, preview.info].map((color, i) => (
|
||||||
|
<View key={i} style={[styles.previewDot, { backgroundColor: color }]} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ width: '100%' }}>
|
<View style={{ width: '100%' }}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<Text style={styles.cardName} numberOfLines={1}>{theme.name}</Text>
|
<Text style={styles.cardName} numberOfLines={1}>{card.name}</Text>
|
||||||
{isActive && <Check size={16} color={c.primary} />}
|
{isActive && <Check size={16} color={c.primary} />}
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.cardAuthor} numberOfLines={1}>{theme.author}</Text>
|
<Text style={styles.cardDescription} numberOfLines={2}>{card.description}</Text>
|
||||||
{theme.variants && (
|
|
||||||
<View style={styles.variants}>
|
|
||||||
{theme.variants.map((v) => (
|
|
||||||
<View key={v} style={styles.variantPill}>
|
|
||||||
<Text style={styles.variantText}>{v}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
label="Upload Theme"
|
|
||||||
description="Install a custom theme from a .zip file."
|
|
||||||
>
|
|
||||||
<Button variant="outline" size="sm" icon={<Upload size={14} color={c.text} />}>
|
|
||||||
Upload .zip
|
|
||||||
</Button>
|
|
||||||
</SettingItem>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,30 +110,27 @@ function makeStyles(c: ThemePalette) {
|
|||||||
},
|
},
|
||||||
cardActive: {
|
cardActive: {
|
||||||
borderColor: c.primary,
|
borderColor: c.primary,
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.05)',
|
backgroundColor: c.primaryBg,
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
aspectRatio: 16 / 10,
|
aspectRatio: 16 / 10,
|
||||||
borderRadius: radius.md,
|
borderRadius: radius.md,
|
||||||
backgroundColor: c.muted,
|
borderWidth: 1,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
previewRail: { width: '22%', borderRightWidth: 1 },
|
||||||
|
previewBody: { flex: 1, padding: spacing.sm, gap: 6, justifyContent: 'center' },
|
||||||
|
previewBar: { height: 6, borderRadius: 3 },
|
||||||
|
previewDots: { flexDirection: 'row', gap: 4, marginTop: 2 },
|
||||||
|
previewDot: { width: 8, height: 8, borderRadius: 4 },
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
cardName: { ...typography.bodyMedium, color: c.text, flex: 1 },
|
cardName: { ...typography.bodyMedium, color: c.text, flex: 1 },
|
||||||
cardAuthor: { ...typography.caption, color: c.mutedForeground },
|
cardDescription: { ...typography.caption, color: c.mutedForeground, marginTop: 2 },
|
||||||
variants: { flexDirection: 'row', gap: 4, marginTop: 4 },
|
|
||||||
variantPill: {
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: radius.xs,
|
|
||||||
backgroundColor: c.muted,
|
|
||||||
},
|
|
||||||
variantText: { fontSize: 10, color: c.mutedForeground },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const TABS: TabDef[] = [
|
|||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
{ id: 'about_data', label: 'About & Data', icon: Info, group: 'advanced', implemented: true },
|
{ id: 'about_data', label: 'About & Data', icon: Info, group: 'advanced', implemented: true },
|
||||||
{ id: 'themes', label: 'Themes', icon: Palette, group: 'advanced', experimental: true, implemented: true },
|
{ id: 'themes', label: 'Themes', icon: Palette, group: 'advanced', implemented: true },
|
||||||
// Plugins run in the webmail only; the pane is an explainer reachable by
|
// Plugins run in the webmail only; the pane is an explainer reachable by
|
||||||
// deep link, not from the list. Debug logging lives under About & Data.
|
// deep link, not from the list. Debug logging lives under About & Data.
|
||||||
{ id: 'plugins', label: 'Plugins', icon: Puzzle, group: 'advanced', experimental: true, implemented: true, hidden: true },
|
{ id: 'plugins', label: 'Plugins', icon: Puzzle, group: 'advanced', experimental: true, implemented: true, hidden: true },
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
// GENERATED from the webmail's lib/builtin-themes.ts CSS variables by
|
||||||
|
// scripts in the parity work (2026-08-29). Each theme overrides the subset of
|
||||||
|
// ThemePalette that the CSS defines; everything else keeps the base palette.
|
||||||
|
// Regenerate rather than hand-edit when the webmail themes change.
|
||||||
|
import type { ThemePalette } from './tokens';
|
||||||
|
|
||||||
|
export type PaletteOverride = Partial<Omit<ThemePalette, 'calendar' | 'tags'>>;
|
||||||
|
|
||||||
|
export interface BuiltinTheme {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
light: PaletteOverride;
|
||||||
|
dark: PaletteOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUILTIN_THEMES: BuiltinTheme[] = [
|
||||||
|
{
|
||||||
|
id: 'builtin-qui',
|
||||||
|
name: 'Qui',
|
||||||
|
description: 'Clean, modern zinc-and-blue theme inspired by autobrr/qui',
|
||||||
|
light: {
|
||||||
|
primary: '#3b82f6',
|
||||||
|
primaryDark: '#3b82f6',
|
||||||
|
primaryLight: '#3b82f6',
|
||||||
|
primaryBg: '#dbeafe',
|
||||||
|
primaryBgHover: '#dbeafe',
|
||||||
|
primaryForeground: '#ffffff',
|
||||||
|
background: '#ffffff',
|
||||||
|
surface: '#f4f4f5',
|
||||||
|
surfaceHover: '#dbeafe',
|
||||||
|
surfaceActive: '#f4f4f5',
|
||||||
|
card: '#ffffff',
|
||||||
|
cardForeground: '#09090b',
|
||||||
|
text: '#09090b',
|
||||||
|
textSecondary: '#71717a',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
textInverse: '#ffffff',
|
||||||
|
textLink: '#3b82f6',
|
||||||
|
border: '#e4e4e7',
|
||||||
|
borderLight: '#e4e4e7',
|
||||||
|
borderFocus: '#3b82f6',
|
||||||
|
success: '#22c55e',
|
||||||
|
successForeground: '#ffffff',
|
||||||
|
warning: '#eab308',
|
||||||
|
warningForeground: '#ffffff',
|
||||||
|
error: '#ef4444',
|
||||||
|
errorForeground: '#ffffff',
|
||||||
|
info: '#3b82f6',
|
||||||
|
infoForeground: '#ffffff',
|
||||||
|
unread: '#3b82f6',
|
||||||
|
read: '#71717a',
|
||||||
|
navActive: '#3b82f6',
|
||||||
|
navInactive: '#71717a',
|
||||||
|
navBadge: '#ef4444',
|
||||||
|
secondary: '#f4f4f5',
|
||||||
|
secondaryForeground: '#18181b',
|
||||||
|
muted: '#f4f4f5',
|
||||||
|
mutedForeground: '#71717a',
|
||||||
|
accent: '#dbeafe',
|
||||||
|
accentForeground: '#1d4ed8',
|
||||||
|
selection: '#dbeafe',
|
||||||
|
selectionForeground: '#1d4ed8',
|
||||||
|
popover: '#ffffff',
|
||||||
|
popoverForeground: '#09090b',
|
||||||
|
chart1: '#3b82f6',
|
||||||
|
chart2: '#22c55e',
|
||||||
|
chart3: '#f59e0b',
|
||||||
|
chart4: '#ef4444',
|
||||||
|
chart5: '#8b5cf6',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#3b82f6',
|
||||||
|
primaryDark: '#3b82f6',
|
||||||
|
primaryLight: '#3b82f6',
|
||||||
|
primaryBg: '#172554',
|
||||||
|
primaryBgHover: '#172554',
|
||||||
|
primaryForeground: '#ffffff',
|
||||||
|
background: '#09090b',
|
||||||
|
surface: '#18181b',
|
||||||
|
surfaceHover: '#172554',
|
||||||
|
surfaceActive: '#18181b',
|
||||||
|
card: '#141414',
|
||||||
|
cardForeground: '#fafafa',
|
||||||
|
text: '#fafafa',
|
||||||
|
textSecondary: '#a1a1aa',
|
||||||
|
textMuted: '#a1a1aa',
|
||||||
|
textInverse: '#09090b',
|
||||||
|
textLink: '#3b82f6',
|
||||||
|
border: '#27272a',
|
||||||
|
borderLight: '#27272a',
|
||||||
|
borderFocus: '#3b82f6',
|
||||||
|
success: '#16a34a',
|
||||||
|
successForeground: '#ffffff',
|
||||||
|
warning: '#ca8a04',
|
||||||
|
warningForeground: '#ffffff',
|
||||||
|
error: '#ef4444',
|
||||||
|
errorForeground: '#fafafa',
|
||||||
|
info: '#60a5fa',
|
||||||
|
infoForeground: '#ffffff',
|
||||||
|
unread: '#60a5fa',
|
||||||
|
read: '#a1a1aa',
|
||||||
|
navActive: '#3b82f6',
|
||||||
|
navInactive: '#a1a1aa',
|
||||||
|
navBadge: '#ef4444',
|
||||||
|
secondary: '#18181b',
|
||||||
|
secondaryForeground: '#fafafa',
|
||||||
|
muted: '#18181b',
|
||||||
|
mutedForeground: '#a1a1aa',
|
||||||
|
accent: '#172554',
|
||||||
|
accentForeground: '#93c5fd',
|
||||||
|
selection: 'rgba(59, 130, 246, 0.25)',
|
||||||
|
selectionForeground: '#93c5fd',
|
||||||
|
popover: '#18181b',
|
||||||
|
popoverForeground: '#fafafa',
|
||||||
|
chart1: '#60a5fa',
|
||||||
|
chart2: '#4ade80',
|
||||||
|
chart3: '#fbbf24',
|
||||||
|
chart4: '#f87171',
|
||||||
|
chart5: '#a78bfa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'builtin-nord',
|
||||||
|
name: 'Nord',
|
||||||
|
description: 'Arctic, north-bluish color palette inspired by nordtheme.com',
|
||||||
|
light: {
|
||||||
|
primary: '#5e81ac',
|
||||||
|
primaryDark: '#5e81ac',
|
||||||
|
primaryLight: '#81a1c1',
|
||||||
|
primaryBg: '#81a1c1',
|
||||||
|
primaryBgHover: '#81a1c1',
|
||||||
|
primaryForeground: '#eceff4',
|
||||||
|
background: '#eceff4',
|
||||||
|
surface: '#e5e9f0',
|
||||||
|
surfaceHover: '#81a1c1',
|
||||||
|
surfaceActive: '#d8dee9',
|
||||||
|
card: '#eceff4',
|
||||||
|
cardForeground: '#2e3440',
|
||||||
|
text: '#2e3440',
|
||||||
|
textSecondary: '#4c566a',
|
||||||
|
textMuted: '#4c566a',
|
||||||
|
textInverse: '#eceff4',
|
||||||
|
textLink: '#5e81ac',
|
||||||
|
border: '#d8dee9',
|
||||||
|
borderLight: '#d8dee9',
|
||||||
|
borderFocus: '#81a1c1',
|
||||||
|
success: '#a3be8c',
|
||||||
|
successForeground: '#2e3440',
|
||||||
|
warning: '#ebcb8b',
|
||||||
|
warningForeground: '#2e3440',
|
||||||
|
error: '#bf616a',
|
||||||
|
errorForeground: '#eceff4',
|
||||||
|
info: '#81a1c1',
|
||||||
|
infoForeground: '#eceff4',
|
||||||
|
unread: '#5e81ac',
|
||||||
|
read: '#4c566a',
|
||||||
|
navActive: '#5e81ac',
|
||||||
|
navInactive: '#4c566a',
|
||||||
|
navBadge: '#bf616a',
|
||||||
|
secondary: '#e5e9f0',
|
||||||
|
secondaryForeground: '#2e3440',
|
||||||
|
muted: '#d8dee9',
|
||||||
|
mutedForeground: '#4c566a',
|
||||||
|
accent: '#81a1c1',
|
||||||
|
accentForeground: '#2e3440',
|
||||||
|
selection: '#d8dee9',
|
||||||
|
selectionForeground: '#5e81ac',
|
||||||
|
popover: '#eceff4',
|
||||||
|
popoverForeground: '#2e3440',
|
||||||
|
chart1: '#5e81ac',
|
||||||
|
chart2: '#a3be8c',
|
||||||
|
chart3: '#ebcb8b',
|
||||||
|
chart4: '#bf616a',
|
||||||
|
chart5: '#b48ead',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#88c0d0',
|
||||||
|
primaryDark: '#88c0d0',
|
||||||
|
primaryLight: '#88c0d0',
|
||||||
|
primaryBg: '#434c5e',
|
||||||
|
primaryBgHover: '#434c5e',
|
||||||
|
primaryForeground: '#2e3440',
|
||||||
|
background: '#2e3440',
|
||||||
|
surface: '#3b4252',
|
||||||
|
surfaceHover: '#434c5e',
|
||||||
|
surfaceActive: '#3b4252',
|
||||||
|
card: '#3b4252',
|
||||||
|
cardForeground: '#eceff4',
|
||||||
|
text: '#eceff4',
|
||||||
|
textSecondary: '#d8dee9',
|
||||||
|
textMuted: '#d8dee9',
|
||||||
|
textInverse: '#2e3440',
|
||||||
|
textLink: '#88c0d0',
|
||||||
|
border: '#3b4252',
|
||||||
|
borderLight: '#3b4252',
|
||||||
|
borderFocus: '#88c0d0',
|
||||||
|
success: '#a3be8c',
|
||||||
|
successForeground: '#2e3440',
|
||||||
|
warning: '#ebcb8b',
|
||||||
|
warningForeground: '#2e3440',
|
||||||
|
error: '#bf616a',
|
||||||
|
errorForeground: '#eceff4',
|
||||||
|
info: '#88c0d0',
|
||||||
|
infoForeground: '#2e3440',
|
||||||
|
unread: '#88c0d0',
|
||||||
|
read: '#d8dee9',
|
||||||
|
navActive: '#88c0d0',
|
||||||
|
navInactive: '#d8dee9',
|
||||||
|
navBadge: '#bf616a',
|
||||||
|
secondary: '#3b4252',
|
||||||
|
secondaryForeground: '#eceff4',
|
||||||
|
muted: '#3b4252',
|
||||||
|
mutedForeground: '#d8dee9',
|
||||||
|
accent: '#434c5e',
|
||||||
|
accentForeground: '#88c0d0',
|
||||||
|
selection: 'rgba(136, 192, 208, 0.2)',
|
||||||
|
selectionForeground: '#88c0d0',
|
||||||
|
popover: '#3b4252',
|
||||||
|
popoverForeground: '#eceff4',
|
||||||
|
chart1: '#88c0d0',
|
||||||
|
chart2: '#a3be8c',
|
||||||
|
chart3: '#ebcb8b',
|
||||||
|
chart4: '#bf616a',
|
||||||
|
chart5: '#b48ead',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'builtin-catppuccin',
|
||||||
|
name: 'Catppuccin',
|
||||||
|
description: 'Soothing pastel theme with Latte (light) and Mocha (dark) variants',
|
||||||
|
light: {
|
||||||
|
primary: '#8839ef',
|
||||||
|
primaryDark: '#8839ef',
|
||||||
|
primaryLight: '#8839ef',
|
||||||
|
primaryBg: '#8839ef',
|
||||||
|
primaryBgHover: '#8839ef',
|
||||||
|
primaryForeground: '#eff1f5',
|
||||||
|
background: '#eff1f5',
|
||||||
|
surface: '#e6e9ef',
|
||||||
|
surfaceHover: '#8839ef',
|
||||||
|
surfaceActive: '#dce0e8',
|
||||||
|
card: '#eff1f5',
|
||||||
|
cardForeground: '#4c4f69',
|
||||||
|
text: '#4c4f69',
|
||||||
|
textSecondary: '#6c6f85',
|
||||||
|
textMuted: '#6c6f85',
|
||||||
|
textInverse: '#eff1f5',
|
||||||
|
textLink: '#8839ef',
|
||||||
|
border: '#ccd0da',
|
||||||
|
borderLight: '#ccd0da',
|
||||||
|
borderFocus: '#8839ef',
|
||||||
|
success: '#40a02b',
|
||||||
|
successForeground: '#eff1f5',
|
||||||
|
warning: '#df8e1d',
|
||||||
|
warningForeground: '#eff1f5',
|
||||||
|
error: '#d20f39',
|
||||||
|
errorForeground: '#eff1f5',
|
||||||
|
info: '#1e66f5',
|
||||||
|
infoForeground: '#eff1f5',
|
||||||
|
unread: '#8839ef',
|
||||||
|
read: '#6c6f85',
|
||||||
|
navActive: '#8839ef',
|
||||||
|
navInactive: '#6c6f85',
|
||||||
|
navBadge: '#d20f39',
|
||||||
|
secondary: '#e6e9ef',
|
||||||
|
secondaryForeground: '#4c4f69',
|
||||||
|
muted: '#dce0e8',
|
||||||
|
mutedForeground: '#6c6f85',
|
||||||
|
accent: '#8839ef',
|
||||||
|
accentForeground: '#eff1f5',
|
||||||
|
selection: '#dce0e8',
|
||||||
|
selectionForeground: '#8839ef',
|
||||||
|
popover: '#eff1f5',
|
||||||
|
popoverForeground: '#4c4f69',
|
||||||
|
chart1: '#8839ef',
|
||||||
|
chart2: '#40a02b',
|
||||||
|
chart3: '#df8e1d',
|
||||||
|
chart4: '#d20f39',
|
||||||
|
chart5: '#1e66f5',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#cba6f7',
|
||||||
|
primaryDark: '#cba6f7',
|
||||||
|
primaryLight: '#cba6f7',
|
||||||
|
primaryBg: '#45475a',
|
||||||
|
primaryBgHover: '#45475a',
|
||||||
|
primaryForeground: '#1e1e2e',
|
||||||
|
background: '#1e1e2e',
|
||||||
|
surface: '#313244',
|
||||||
|
surfaceHover: '#45475a',
|
||||||
|
surfaceActive: '#313244',
|
||||||
|
card: '#313244',
|
||||||
|
cardForeground: '#cdd6f4',
|
||||||
|
text: '#cdd6f4',
|
||||||
|
textSecondary: '#a6adc8',
|
||||||
|
textMuted: '#a6adc8',
|
||||||
|
textInverse: '#1e1e2e',
|
||||||
|
textLink: '#cba6f7',
|
||||||
|
border: '#45475a',
|
||||||
|
borderLight: '#45475a',
|
||||||
|
borderFocus: '#cba6f7',
|
||||||
|
success: '#a6e3a1',
|
||||||
|
successForeground: '#1e1e2e',
|
||||||
|
warning: '#f9e2af',
|
||||||
|
warningForeground: '#1e1e2e',
|
||||||
|
error: '#f38ba8',
|
||||||
|
errorForeground: '#1e1e2e',
|
||||||
|
info: '#89b4fa',
|
||||||
|
infoForeground: '#1e1e2e',
|
||||||
|
unread: '#cba6f7',
|
||||||
|
read: '#a6adc8',
|
||||||
|
navActive: '#cba6f7',
|
||||||
|
navInactive: '#a6adc8',
|
||||||
|
navBadge: '#f38ba8',
|
||||||
|
secondary: '#313244',
|
||||||
|
secondaryForeground: '#cdd6f4',
|
||||||
|
muted: '#313244',
|
||||||
|
mutedForeground: '#a6adc8',
|
||||||
|
accent: '#45475a',
|
||||||
|
accentForeground: '#cba6f7',
|
||||||
|
selection: 'rgba(203, 166, 247, 0.2)',
|
||||||
|
selectionForeground: '#cba6f7',
|
||||||
|
popover: '#313244',
|
||||||
|
popoverForeground: '#cdd6f4',
|
||||||
|
chart1: '#cba6f7',
|
||||||
|
chart2: '#a6e3a1',
|
||||||
|
chart3: '#f9e2af',
|
||||||
|
chart4: '#f38ba8',
|
||||||
|
chart5: '#89b4fa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'builtin-solarized',
|
||||||
|
name: 'Solarized',
|
||||||
|
description: 'Precision colors for machines and people by Ethan Schoonover',
|
||||||
|
light: {
|
||||||
|
primary: '#268bd2',
|
||||||
|
primaryDark: '#268bd2',
|
||||||
|
primaryLight: '#268bd2',
|
||||||
|
primaryBg: '#268bd2',
|
||||||
|
primaryBgHover: '#268bd2',
|
||||||
|
primaryForeground: '#fdf6e3',
|
||||||
|
background: '#fdf6e3',
|
||||||
|
surface: '#eee8d5',
|
||||||
|
surfaceHover: '#268bd2',
|
||||||
|
surfaceActive: '#eee8d5',
|
||||||
|
card: '#fdf6e3',
|
||||||
|
cardForeground: '#657b83',
|
||||||
|
text: '#657b83',
|
||||||
|
textSecondary: '#93a1a1',
|
||||||
|
textMuted: '#93a1a1',
|
||||||
|
textInverse: '#fdf6e3',
|
||||||
|
textLink: '#268bd2',
|
||||||
|
border: '#eee8d5',
|
||||||
|
borderLight: '#eee8d5',
|
||||||
|
borderFocus: '#268bd2',
|
||||||
|
success: '#859900',
|
||||||
|
successForeground: '#fdf6e3',
|
||||||
|
warning: '#b58900',
|
||||||
|
warningForeground: '#fdf6e3',
|
||||||
|
error: '#dc322f',
|
||||||
|
errorForeground: '#fdf6e3',
|
||||||
|
info: '#268bd2',
|
||||||
|
infoForeground: '#fdf6e3',
|
||||||
|
unread: '#268bd2',
|
||||||
|
read: '#93a1a1',
|
||||||
|
navActive: '#268bd2',
|
||||||
|
navInactive: '#93a1a1',
|
||||||
|
navBadge: '#dc322f',
|
||||||
|
secondary: '#eee8d5',
|
||||||
|
secondaryForeground: '#586e75',
|
||||||
|
muted: '#eee8d5',
|
||||||
|
mutedForeground: '#93a1a1',
|
||||||
|
accent: '#268bd2',
|
||||||
|
accentForeground: '#fdf6e3',
|
||||||
|
selection: '#eee8d5',
|
||||||
|
selectionForeground: '#268bd2',
|
||||||
|
popover: '#fdf6e3',
|
||||||
|
popoverForeground: '#657b83',
|
||||||
|
chart1: '#268bd2',
|
||||||
|
chart2: '#859900',
|
||||||
|
chart3: '#b58900',
|
||||||
|
chart4: '#dc322f',
|
||||||
|
chart5: '#6c71c4',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#268bd2',
|
||||||
|
primaryDark: '#268bd2',
|
||||||
|
primaryLight: '#268bd2',
|
||||||
|
primaryBg: '#073642',
|
||||||
|
primaryBgHover: '#073642',
|
||||||
|
primaryForeground: '#002b36',
|
||||||
|
background: '#002b36',
|
||||||
|
surface: '#073642',
|
||||||
|
surfaceHover: '#073642',
|
||||||
|
surfaceActive: '#073642',
|
||||||
|
card: '#073642',
|
||||||
|
cardForeground: '#93a1a1',
|
||||||
|
text: '#839496',
|
||||||
|
textSecondary: '#586e75',
|
||||||
|
textMuted: '#586e75',
|
||||||
|
textInverse: '#002b36',
|
||||||
|
textLink: '#268bd2',
|
||||||
|
border: '#073642',
|
||||||
|
borderLight: '#073642',
|
||||||
|
borderFocus: '#268bd2',
|
||||||
|
success: '#859900',
|
||||||
|
successForeground: '#002b36',
|
||||||
|
warning: '#b58900',
|
||||||
|
warningForeground: '#002b36',
|
||||||
|
error: '#dc322f',
|
||||||
|
errorForeground: '#fdf6e3',
|
||||||
|
info: '#268bd2',
|
||||||
|
infoForeground: '#002b36',
|
||||||
|
unread: '#268bd2',
|
||||||
|
read: '#586e75',
|
||||||
|
navActive: '#268bd2',
|
||||||
|
navInactive: '#586e75',
|
||||||
|
navBadge: '#dc322f',
|
||||||
|
secondary: '#073642',
|
||||||
|
secondaryForeground: '#93a1a1',
|
||||||
|
muted: '#073642',
|
||||||
|
mutedForeground: '#586e75',
|
||||||
|
accent: '#073642',
|
||||||
|
accentForeground: '#268bd2',
|
||||||
|
selection: 'rgba(38, 139, 210, 0.2)',
|
||||||
|
selectionForeground: '#268bd2',
|
||||||
|
popover: '#073642',
|
||||||
|
popoverForeground: '#93a1a1',
|
||||||
|
chart1: '#268bd2',
|
||||||
|
chart2: '#859900',
|
||||||
|
chart3: '#b58900',
|
||||||
|
chart4: '#dc322f',
|
||||||
|
chart5: '#6c71c4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getBuiltinTheme(id: string | null | undefined): BuiltinTheme | null {
|
||||||
|
if (!id) return null;
|
||||||
|
return BUILTIN_THEMES.find((t) => t.id === id) ?? null;
|
||||||
|
}
|
||||||
+25
-2
@@ -1,24 +1,47 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { useColorScheme } from 'react-native';
|
import { useColorScheme } from 'react-native';
|
||||||
import { useSettingsStore } from '../stores/settings-store';
|
import { useSettingsStore } from '../stores/settings-store';
|
||||||
import { LIGHT_COLORS, DARK_COLORS, type ThemePalette } from './tokens';
|
import { LIGHT_COLORS, DARK_COLORS, type ThemePalette } from './tokens';
|
||||||
|
import { getBuiltinTheme } from './builtin-themes';
|
||||||
|
|
||||||
export type { ThemePalette };
|
export type { ThemePalette };
|
||||||
|
|
||||||
|
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])`.
|
||||||
|
*/
|
||||||
|
export function resolvePalette(scheme: 'light' | 'dark', themeId: string | null): ThemePalette {
|
||||||
|
const base = scheme === 'light' ? LIGHT_COLORS : DARK_COLORS;
|
||||||
|
const theme = getBuiltinTheme(themeId);
|
||||||
|
if (!theme) return base;
|
||||||
|
const key = `${scheme}:${theme.id}`;
|
||||||
|
const cached = paletteCache.get(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
const merged: ThemePalette = { ...base, ...(scheme === 'light' ? theme.light : theme.dark) };
|
||||||
|
paletteCache.set(key, merged);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the active palette for the current render. Resolves the user's
|
* Returns the active palette for the current render. Resolves the user's
|
||||||
* theme preference ('light' | 'dark' | 'system') against the OS scheme.
|
* theme preference ('light' | 'dark' | 'system') against the OS scheme and
|
||||||
|
* applies the selected built-in theme.
|
||||||
*
|
*
|
||||||
* Prefer this hook in new and migrated components. Static `import { colors }`
|
* Prefer this hook in new and migrated components. Static `import { colors }`
|
||||||
* still works for not-yet-migrated code (it always returns the dark palette).
|
* still works for not-yet-migrated code (it always returns the dark palette).
|
||||||
*/
|
*/
|
||||||
export function useColors(): ThemePalette {
|
export function useColors(): ThemePalette {
|
||||||
const themePref = useSettingsStore((s) => s.theme);
|
const themePref = useSettingsStore((s) => s.theme);
|
||||||
|
const activeThemeId = useSettingsStore((s) => s.activeThemeId);
|
||||||
const systemScheme = useColorScheme();
|
const systemScheme = useColorScheme();
|
||||||
const resolved =
|
const resolved =
|
||||||
themePref === 'system'
|
themePref === 'system'
|
||||||
? systemScheme === 'light' ? 'light' : 'dark'
|
? systemScheme === 'light' ? 'light' : 'dark'
|
||||||
: themePref;
|
: themePref;
|
||||||
return resolved === 'light' ? LIGHT_COLORS : DARK_COLORS;
|
return useMemo(() => resolvePalette(resolved, activeThemeId), [resolved, activeThemeId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user