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
+16 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useI18n, LANGS } from '../i18n/I18nProvider';
import { ACCENTS, useTheme } from '../theme/ThemeProvider';
import { ACCENTS, FONT_SETS, useTheme } from '../theme/ThemeProvider';
import type { User } from '../api';
// Header: the app's TopBar minus everything camera-only, plus the two controls
@@ -29,7 +29,7 @@ export function TopBar({
onLogout: () => void;
}) {
const { t, lang, setLang } = useI18n();
const { mode, accent, setMode, setAccent } = useTheme();
const { mode, accent, fonts, setMode, setAccent, setFonts } = useTheme();
const [menu, setMenu] = useState<'theme' | 'lang' | 'account' | null>(null);
const wrap = useRef<HTMLDivElement>(null);
@@ -79,6 +79,20 @@ export function TopBar({
</button>
))}
</div>
<div className="chip-row" style={{ marginTop: 8 }}>
{FONT_SETS.map((entry) => (
<button
key={entry.id}
type="button"
data-key={`fonts-${entry.id}`}
title={entry.note}
className={`chip${fonts === entry.id ? ' on' : ''}`}
onClick={() => setFonts(entry.id)}
>
{entry.label}
</button>
))}
</div>
</div>
) : null}
</div>