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:
+9
-5
@@ -48,11 +48,15 @@ project (`@shopify/react-native-skia` is aliased to `src/engine/skiaShim.ts` in
|
||||
`cinemaShader` `colorUtils` `defaultRecipes` `exifWrite` `frameUtils` `jpegDpi`
|
||||
`paramDefs` `recipeShare` `skiaImage` `toneShader`.
|
||||
|
||||
The landing page needs no CDN: `frontend/public/assets/Syne.woff2` is the
|
||||
bundled display face and `frontend/public/assets/samples/s*.jpg` are the six
|
||||
placeholder negatives the film strip, preset tester and QR card show (swap them
|
||||
for real graded stills whenever we have them). Its one foreign request is the
|
||||
QR image from `api.qrserver.com`, which degrades to an empty slot offline.
|
||||
The landing page needs no CDN: `frontend/public/assets/fonts/*.woff2` are the
|
||||
seven self-hosted faces behind the three font groups the Themes menu offers
|
||||
(Plus Jakarta Sans / Inter / JetBrains Mono, Fraunces / Be Vietnam Pro /
|
||||
Courier Prime, Be Vietnam Pro / Space Mono — all SIL OFL, pulled from Google
|
||||
Fonts, vietnamese + latin + latin-ext subsets), and
|
||||
`frontend/public/assets/samples/s*.jpg` are the six placeholder negatives the
|
||||
film strip, preset tester and QR card show (swap them for real graded stills
|
||||
whenever we have them). Its one foreign request is the QR image from
|
||||
`api.qrserver.com`, which degrades to an empty slot offline.
|
||||
|
||||
When the app changes one of them, copy it back in — the renderer is only
|
||||
"parity" for as long as these stay in sync:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="vi" data-theme="dark" data-accent="amber">
|
||||
<html lang="vi" data-theme="dark" data-accent="amber" data-fonts="studio">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useI18n } from './i18n/I18nProvider';
|
||||
import { useTheme } from './theme/ThemeProvider';
|
||||
import { FONT_SETS, useTheme } from './theme/ThemeProvider';
|
||||
|
||||
// Landing page, built to the landing blueprint: the 10-section funnel. The
|
||||
// marketing prose is bilingual and lives here next to the markup as `Txt`
|
||||
@@ -99,8 +99,10 @@ function Icon({ name }: { name: Feature['icon'] }) {
|
||||
|
||||
export function Landing() {
|
||||
const { t, lang, setLang } = useI18n();
|
||||
const { mode, setMode } = useTheme();
|
||||
const { mode, fonts, setMode, setFonts } = useTheme();
|
||||
const [menu, setMenu] = useState(false);
|
||||
const [themeMenu, setThemeMenu] = useState(false);
|
||||
const tools = useRef<HTMLSpanElement>(null);
|
||||
const [presetId, setPresetId] = useState(PRESETS[0].id);
|
||||
const [temp, setTemp] = useState(0);
|
||||
const [sat, setSat] = useState(100);
|
||||
@@ -121,6 +123,16 @@ export function Landing() {
|
||||
}, [c]);
|
||||
useEffect(() => () => clearTimeout(toastTimer.current), []);
|
||||
|
||||
// The theme popover closes on a click anywhere else, like the workspace's.
|
||||
useEffect(() => {
|
||||
if (!themeMenu) return;
|
||||
const away = (e: MouseEvent) => {
|
||||
if (!tools.current?.contains(e.target as Node)) setThemeMenu(false);
|
||||
};
|
||||
document.addEventListener('mousedown', away);
|
||||
return () => document.removeEventListener('mousedown', away);
|
||||
}, [themeMenu]);
|
||||
|
||||
// The simulator's output: three knobs, one CSS filter string, applied live.
|
||||
const labFilter = `sepia(${(Math.abs(temp) / 260).toFixed(3)}) hue-rotate(${(-temp * 0.45).toFixed(1)}deg) saturate(${sat}%) contrast(${con}%)`;
|
||||
|
||||
@@ -149,18 +161,56 @@ export function Landing() {
|
||||
{nav.map((entry) => <a key={entry.href} href={entry.href}>{c(entry.label)}</a>)}
|
||||
</div>
|
||||
<span className="lp-spacer" />
|
||||
{/* Theme + language, the same two switches the workspace TopBar has. */}
|
||||
<span className="lp-tools">
|
||||
<button
|
||||
type="button"
|
||||
className="lp-tool"
|
||||
title={t('nav.theme')}
|
||||
aria-label={t('nav.theme')}
|
||||
aria-pressed={mode === 'light'}
|
||||
onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
◐
|
||||
</button>
|
||||
{/* Theme + language, the same two switches the workspace TopBar has.
|
||||
Theme opens a popover: light/dark plus the font group, which
|
||||
retypes the whole page. */}
|
||||
<span className="lp-tools" ref={tools}>
|
||||
<div className="menu-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="lp-tool"
|
||||
title={t('nav.theme')}
|
||||
aria-label={t('nav.theme')}
|
||||
aria-expanded={themeMenu}
|
||||
onClick={() => setThemeMenu((v) => !v)}
|
||||
>
|
||||
◐
|
||||
</button>
|
||||
{themeMenu ? (
|
||||
<div className="popover lp-theme-pop">
|
||||
<div className="chip-row">
|
||||
<button
|
||||
type="button"
|
||||
className={`chip${mode === 'light' ? ' on' : ''}`}
|
||||
onClick={() => setMode('light')}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`chip${mode === 'dark' ? ' on' : ''}`}
|
||||
onClick={() => setMode('dark')}
|
||||
>
|
||||
Dark
|
||||
</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>
|
||||
<button
|
||||
type="button"
|
||||
className="lp-tool"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './styles/fonts.css';
|
||||
import './styles/tokens.css';
|
||||
import './styles/app.css';
|
||||
import './styles/landing.css';
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Self-hosted faces for the three font groups offered in Themes: pulled once
|
||||
from Google Fonts (all SIL OFL) and trimmed to the vietnamese/latin-ext/latin
|
||||
subsets. To regenerate, request the css2 API for a family with a browser
|
||||
User-Agent and save the woff2 each subset points at. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Plus Jakarta Sans';
|
||||
font-style: normal;
|
||||
font-weight: 200 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/PlusJakartaSans-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Plus Jakarta Sans';
|
||||
font-style: normal;
|
||||
font-weight: 200 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/PlusJakartaSans-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Plus Jakarta Sans';
|
||||
font-style: normal;
|
||||
font-weight: 200 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/PlusJakartaSans-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Inter-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Inter-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Inter-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/JetBrainsMono-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/JetBrainsMono-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/JetBrainsMono-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Fraunces-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Fraunces-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/Fraunces-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Courier Prime';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/CourierPrime-400-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Courier Prime';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/CourierPrime-400-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Courier Prime';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/CourierPrime-700-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Courier Prime';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/CourierPrime-700-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-400-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-400-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-400-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-700-vietnamese.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-700-latin-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/assets/fonts/SpaceMono-700-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
/* Landing page — dark cinematic theme per the landing blueprint. Self-contained:
|
||||
its own palette, its own class prefix (.lp-*), no dependency on the workspace
|
||||
theme, so the marketing page looks the same in light or dark mode. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Syne';
|
||||
font-style: normal;
|
||||
font-weight: 700 800;
|
||||
font-display: swap;
|
||||
src: url('/assets/Syne.woff2') format('woff2');
|
||||
}
|
||||
theme, so the marketing page looks the same in light or dark mode. The two
|
||||
faces it does borrow — heading + mono — come from the Themes font group
|
||||
(styles/tokens.css), so the nav picker retypes the whole page. */
|
||||
|
||||
.lp {
|
||||
--lp-bg: #090a0f;
|
||||
@@ -25,8 +19,8 @@
|
||||
/* Amber for text sitting on the page background — darker in light mode,
|
||||
where plain #fbbf24 would fail contrast on white. */
|
||||
--lp-ink-amber: var(--lp-amber);
|
||||
--lp-mono: 'Cousine', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
--lp-display: 'Syne', ui-sans-serif, system-ui, sans-serif;
|
||||
--lp-mono: var(--mono);
|
||||
--lp-display: var(--font-heading);
|
||||
|
||||
background: var(--lp-bg);
|
||||
color: var(--lp-text);
|
||||
@@ -137,6 +131,12 @@ html { scroll-behavior: smooth; scroll-padding-top: 78px; }
|
||||
cursor: pointer;
|
||||
}
|
||||
.lp-tool:hover { color: var(--lp-text); border-color: rgba(251, 191, 36, 0.5); }
|
||||
/* The theme popover borrows the workspace's .popover/.chip shell, re-skinned for
|
||||
the landing palette. */
|
||||
.lp-theme-pop { background: var(--lp-card); border-color: var(--lp-line); }
|
||||
.lp-theme-pop .chip { background: transparent; border-color: var(--lp-line); color: var(--lp-muted); }
|
||||
.lp-theme-pop .chip:hover { color: var(--lp-text); }
|
||||
.lp-theme-pop .chip.on { color: var(--lp-ink-amber); border-color: var(--lp-ink-amber); background: rgba(251, 191, 36, 0.12); }
|
||||
.lp-burger { display: none; }
|
||||
|
||||
@media (max-width: 1160px) {
|
||||
|
||||
@@ -8,9 +8,31 @@
|
||||
--rail-w: 84px;
|
||||
--chip-w: 300px;
|
||||
--font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-heading: var(--font);
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
/* --- font groups -------------------------------------------------------- */
|
||||
/* The three pairings offered in Themes; the faces themselves are self-hosted
|
||||
by styles/fonts.css, and the picker's list lives in theme/themes.ts. */
|
||||
[data-fonts="studio"] {
|
||||
--font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-heading: "Plus Jakarta Sans", "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
[data-fonts="editorial"] {
|
||||
--font: "Be Vietnam Pro", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-heading: "Fraunces", "Be Vietnam Pro", Georgia, serif;
|
||||
--mono: "Courier Prime", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
[data-fonts="native"] {
|
||||
--font: "Be Vietnam Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-heading: "Be Vietnam Pro", ui-sans-serif, system-ui, sans-serif;
|
||||
--mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
/* --- base themes --------------------------------------------------------- */
|
||||
[data-theme="light"] {
|
||||
--bg: #ffffff;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user