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:
2026-09-17 19:20:23 +07:00
parent 51e572b9ce
commit cc88471256
33 changed files with 361 additions and 42 deletions
+15 -8
View File
@@ -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';