🎨 feat(web/default): add Anthropic theme preset and configurable serif typography
Introduce a switchable Anthropic-inspired color preset and a new Font customization axis so users can adopt the editorial serif look across the entire UI, including sidebar navigation, tabs, form controls, buttons, and table headers. Theme preset Add anthropic to the theme preset registry with warm cream canvas, slate foreground, and clay/coral accent tokens for light and dark modes Define explicit surface colors for the Anthropic preset instead of relying on the semantic surface bridge Exclude anthropic from the primary-color surface bridge so bespoke warm neutrals are not overridden by accent-tinted mixes Typography system Add @fontsource-variable/lora and a global --font-serif token with CJK serif fallbacks (Noto Serif SC, Source Han Serif, Songti SC, etc.) Introduce a --font-body token and drive <body> font-family from it Add a Font axis (default | sans | serif) parallel to radius/scale Resolve font: 'default' against preset defaults (anthropic → serif) Persist font preference via cookie and apply data-theme-font on <body> Apply serif OpenType features (kern, liga, calt, tnum) and heading display tuning when serif is active Remove per-component sans opt-outs so serif inherits through sidebar, tabs, inputs, buttons, badges, and table headers via natural CSS cascade Keep monospace contexts unchanged via Tailwind preflight and .font-mono UI and i18n Add Font selector to the theme config drawer (Auto / Sans / Serif) Add "Font" and "Select body font" translations for en, zh, fr, ja, ru, vi Misc Tighten group and status badge sizing for better balance with serif text
This commit is contained in:
+86
@@ -34,6 +34,7 @@ import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
|
||||
import {
|
||||
type ContentLayout,
|
||||
THEME_PRESETS,
|
||||
type ThemeFont,
|
||||
type ThemePreset,
|
||||
type ThemeRadius,
|
||||
type ThemeScale,
|
||||
@@ -104,6 +105,7 @@ export function ConfigDrawer() {
|
||||
<div className={sideDrawerFormClassName()}>
|
||||
<ThemeConfig />
|
||||
<PresetConfig />
|
||||
<FontConfig />
|
||||
<RadiusConfig />
|
||||
<ScaleConfig />
|
||||
<SidebarConfig />
|
||||
@@ -302,6 +304,90 @@ function PresetConfig() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Font options shown in the theme drawer.
|
||||
*
|
||||
* Each option renders a live "Aa" preview in the font it represents.
|
||||
* `Auto` deliberately leaves `fontFamily` undefined so the preview inherits
|
||||
* the currently active body font — that way the user sees what `Auto` will
|
||||
* actually look like for the active preset (Anthropic → serif glyphs,
|
||||
* everything else → sans glyphs) without us having to duplicate the
|
||||
* preset-default mapping in the UI.
|
||||
*/
|
||||
const FONT_OPTIONS: {
|
||||
value: ThemeFont
|
||||
label: string
|
||||
// CSS font-family applied to the "Aa" preview. `undefined` = inherit
|
||||
// from the current theme (used by the `default` option).
|
||||
preview?: string
|
||||
}[] = [
|
||||
{ value: 'default', label: 'Auto', preview: undefined },
|
||||
{ value: 'sans', label: 'Sans', preview: 'var(--font-sans)' },
|
||||
{ value: 'serif', label: 'Serif', preview: 'var(--font-serif)' },
|
||||
]
|
||||
|
||||
function FontConfig() {
|
||||
const { t } = useTranslation()
|
||||
const { defaults, customization, setFont } = useThemeCustomization()
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle
|
||||
title={t('Font')}
|
||||
showReset={customization.font !== defaults.font}
|
||||
onReset={() => setFont(defaults.font)}
|
||||
/>
|
||||
<Radio
|
||||
value={customization.font}
|
||||
onValueChange={(v) => setFont(v as ThemeFont)}
|
||||
className='grid w-full grid-cols-3 gap-4'
|
||||
aria-label={t('Select body font')}
|
||||
>
|
||||
{FONT_OPTIONS.map((option) => (
|
||||
<Item
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className='group flex flex-col items-stretch outline-none'
|
||||
aria-label={
|
||||
option.value === 'default' ? t('System default') : option.label
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'ring-border relative h-12 rounded-md ring-[1px] transition',
|
||||
'group-data-checked:ring-primary group-data-checked:shadow-md',
|
||||
'group-focus-visible:ring-2',
|
||||
'group-hover:ring-primary/60'
|
||||
)}
|
||||
>
|
||||
<CircleCheck
|
||||
className={cn(
|
||||
'fill-primary absolute top-0 right-0 z-10 size-5 translate-x-1/2 -translate-y-1/2 stroke-white',
|
||||
'group-data-unchecked:hidden'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='text-foreground absolute inset-0 flex items-center justify-center text-lg leading-none font-medium'
|
||||
style={
|
||||
option.preview
|
||||
? { fontFamily: option.preview }
|
||||
: // `font: inherit` defers to the active theme so the
|
||||
// "Auto" tile previews what the resolved font will be.
|
||||
{ font: 'inherit', fontSize: '1.125rem' }
|
||||
}
|
||||
>
|
||||
Aa
|
||||
</span>
|
||||
</div>
|
||||
<div className='mt-1.5 text-center text-xs'>{option.label}</div>
|
||||
</Item>
|
||||
))}
|
||||
</Radio>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RADIUS_OPTIONS: {
|
||||
value: ThemeRadius
|
||||
label: string
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ export function GroupBadge(props: GroupBadgeProps) {
|
||||
{badge}
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex h-6 items-center rounded-full px-2 font-mono text-sm leading-none font-medium tabular-nums',
|
||||
'inline-flex h-5 items-center rounded-full px-2 font-mono text-xs leading-none font-medium tabular-nums',
|
||||
getGroupRatioClassName(ratio)
|
||||
)}
|
||||
>
|
||||
|
||||
+3
-3
@@ -74,8 +74,8 @@ export const textColorMap = {
|
||||
export type StatusVariant = keyof typeof dotColorMap
|
||||
|
||||
const sizeMap = {
|
||||
sm: 'h-6 gap-1 px-2 text-sm leading-none',
|
||||
md: 'h-6 gap-1 px-2 text-sm leading-none',
|
||||
sm: 'h-5 gap-1 px-2 text-xs leading-none',
|
||||
md: 'h-6 gap-1 px-2 text-xs leading-none',
|
||||
lg: 'h-7 gap-1.5 px-2.5 text-sm leading-none',
|
||||
} as const
|
||||
|
||||
@@ -168,7 +168,7 @@ export function StatusBadge({
|
||||
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
|
||||
{...props}
|
||||
>
|
||||
{Icon && <Icon className='size-3.5 shrink-0' />}
|
||||
{Icon && <Icon className='size-3 shrink-0' />}
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user