Offer the three proposed font groups in Themes
The theme menu now carries a fourth choice beside light/dark and the accent: the font pairing. All three pairings are free for commercial use (SIL OFL), have a Vietnamese subset, and are self-hosted — the landing still makes no CDN request. - styles/fonts.css: 22 @font-face blocks for Plus Jakarta Sans, Inter, JetBrains Mono, Fraunces, Be Vietnam Pro, Courier Prime and Space Mono, vietnamese/latin-ext/latin subsets only, under public/assets/fonts. - styles/tokens.css: [data-fonts="studio|editorial|native"] sets --font, --font-heading and --mono. Studio (Plus Jakarta Sans + Inter + JetBrains Mono) is the default. - theme/: FontSetId + FONT_SETS, persisted as rc.fonts, applied as <html data-fonts> next to data-theme and data-accent. - TopBar and the landing nav both get the picker; on the landing the theme tool now opens a small popover (light/dark + font group) instead of toggling on click. - landing.css: --lp-display/--lp-mono now resolve to the chosen group, so the picker retypes the whole page. Syne.woff2 goes with its @font-face.
This commit is contained in:
@@ -1,29 +1,36 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { ACCENTS, applyTheme, readAccent, readTheme, type AccentId, type ThemeMode } from './themes';
|
||||
import { ACCENTS, applyTheme, readAccent, readFonts, readTheme, type AccentId, type FontSetId, type ThemeMode } from './themes';
|
||||
|
||||
interface ThemeValue {
|
||||
mode: ThemeMode;
|
||||
accent: AccentId;
|
||||
fonts: FontSetId;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
setAccent: (accent: AccentId) => void;
|
||||
setFonts: (fonts: FontSetId) => void;
|
||||
}
|
||||
|
||||
const Ctx = createContext<ThemeValue | null>(null);
|
||||
|
||||
// The tokens live in CSS; this provider only owns the two attributes on <html>
|
||||
// and the persisted choice. No component ever reads a colour from JS.
|
||||
// The tokens live in CSS; this provider only owns the three attributes on
|
||||
// <html> and the persisted choice. No component ever reads a colour from JS.
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [mode, setModeState] = useState<ThemeMode>(readTheme);
|
||||
const [accent, setAccentState] = useState<AccentId>(readAccent);
|
||||
const [fonts, setFontsState] = useState<FontSetId>(readFonts);
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(mode, accent);
|
||||
}, [mode, accent]);
|
||||
applyTheme(mode, accent, fonts);
|
||||
}, [mode, accent, fonts]);
|
||||
|
||||
const setMode = useCallback((next: ThemeMode) => setModeState(next), []);
|
||||
const setAccent = useCallback((next: AccentId) => setAccentState(next), []);
|
||||
const setFonts = useCallback((next: FontSetId) => setFontsState(next), []);
|
||||
|
||||
const value = useMemo(() => ({ mode, accent, setMode, setAccent }), [mode, accent, setMode, setAccent]);
|
||||
const value = useMemo(
|
||||
() => ({ mode, accent, fonts, setMode, setAccent, setFonts }),
|
||||
[mode, accent, fonts, setMode, setAccent, setFonts]
|
||||
);
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
@@ -33,5 +40,5 @@ export function useTheme(): ThemeValue {
|
||||
return value;
|
||||
}
|
||||
|
||||
export { ACCENTS };
|
||||
export type { AccentId, ThemeMode };
|
||||
export { ACCENTS, FONT_SETS } from './themes';
|
||||
export type { AccentId, FontSetId, ThemeMode } from './themes';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export type ThemeMode = 'light' | 'dark';
|
||||
export type AccentId = 'amber' | 'blue' | 'emerald' | 'rose' | 'violet';
|
||||
export type FontSetId = 'studio' | 'editorial' | 'native';
|
||||
|
||||
export const ACCENTS: { id: AccentId; label: string }[] = [
|
||||
{ id: 'amber', label: 'Amber' },
|
||||
@@ -12,6 +13,16 @@ export const ACCENTS: { id: AccentId; label: string }[] = [
|
||||
{ id: 'violet', label: 'Violet' },
|
||||
];
|
||||
|
||||
// The three pairings proposed for the landing: heading / body / mono. All free
|
||||
// for commercial use (SIL OFL), all with a Vietnamese subset, and all
|
||||
// self-hosted under public/assets/fonts — the page still needs no CDN.
|
||||
// The stacks themselves live in styles/tokens.css, keyed by data-fonts.
|
||||
export const FONT_SETS: { id: FontSetId; label: string; note: string }[] = [
|
||||
{ id: 'studio', label: 'Studio', note: 'Plus Jakarta Sans · Inter · JetBrains Mono' },
|
||||
{ id: 'editorial', label: 'Editorial', note: 'Fraunces · Be Vietnam Pro · Courier Prime' },
|
||||
{ id: 'native', label: 'Thuần Việt', note: 'Be Vietnam Pro · Space Mono' },
|
||||
];
|
||||
|
||||
export const THEME_COLOR: Record<ThemeMode, string> = {
|
||||
light: '#ffffff',
|
||||
dark: '#0a0a0a',
|
||||
@@ -19,6 +30,7 @@ export const THEME_COLOR: Record<ThemeMode, string> = {
|
||||
|
||||
const KEY_THEME = 'rc.theme';
|
||||
const KEY_ACCENT = 'rc.accent';
|
||||
const KEY_FONTS = 'rc.fonts';
|
||||
|
||||
export function readTheme(): ThemeMode {
|
||||
const stored = localStorage.getItem(KEY_THEME);
|
||||
@@ -31,11 +43,18 @@ export function readAccent(): AccentId {
|
||||
return ACCENTS.some((a) => a.id === stored) ? (stored as AccentId) : 'amber';
|
||||
}
|
||||
|
||||
export function applyTheme(mode: ThemeMode, accent: AccentId): void {
|
||||
export function readFonts(): FontSetId {
|
||||
const stored = localStorage.getItem(KEY_FONTS);
|
||||
return FONT_SETS.some((f) => f.id === stored) ? (stored as FontSetId) : 'studio';
|
||||
}
|
||||
|
||||
export function applyTheme(mode: ThemeMode, accent: AccentId, fonts: FontSetId): void {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = mode;
|
||||
root.dataset.accent = accent;
|
||||
root.dataset.fonts = fonts;
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLOR[mode]);
|
||||
localStorage.setItem(KEY_THEME, mode);
|
||||
localStorage.setItem(KEY_ACCENT, accent);
|
||||
localStorage.setItem(KEY_FONTS, fonts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user