1. 修复登录界面文字被遮挡问题
2. 大字模式关闭后显示异常问题
3. 重新调整大字模式是否开启时的字体显示效果
This commit is contained in:
yangshilin
2026-04-10 20:35:57 +08:00
parent abf8497c2e
commit 17b9fa3466
27 changed files with 390 additions and 161 deletions

View File

@@ -200,6 +200,26 @@
"lineHeightTight": 24,
"lineHeightLoose": 29,
"lineHeightXLoose": 34
},
"xlarge": {
"headingLarge": 44,
"headingMedium": 33,
"headingSmall": 28,
"titleLarge": 28,
"titleMedium": 25,
"titleSmall": 21,
"bodyLarge": 28,
"bodyMedium": 26,
"bodySmall": 18,
"captionLarge": 18,
"captionMedium": 16,
"captionSmall": 15,
"sectionTitle": 16,
"badge": 13,
"lineHeightNormal": 30,
"lineHeightTight": 28,
"lineHeightLoose": 34,
"lineHeightXLoose": 40
}
}
}

View File

@@ -960,13 +960,14 @@ export default function ConversationScreen() {
/** 大字模式:朗读图标与触控区与气泡字号同档放大 */
const chatReadAloudIconSize = largeText ? 24 : 20;
const chatReadAloudButtonSize = largeText ? 52 : 44;
const chatVoiceDurationStyle = useMemo(
() => ({
fontSize: largeText ? typography.headingSmall : typography.titleMedium,
const chatVoiceDurationStyle = useMemo(() => {
const fs = largeText ? typography.headingSmall : typography.titleMedium;
return {
fontSize: fs,
lineHeight: Math.ceil(fs * 1.25),
fontWeight: '500' as const,
}),
[typography, largeText],
);
};
}, [typography, largeText]);
const chatTypingLabelStyle = useMemo(
() => ({
fontSize: largeText ? typography.bodySmall : typography.captionLarge,
@@ -977,6 +978,9 @@ export default function ConversationScreen() {
const headerTitleFontStyle = useMemo(
() => ({
fontSize: largeText ? typography.headingMedium : typography.headingSmall,
lineHeight: largeText
? Math.ceil(typography.headingMedium * 1.4)
: Math.ceil(typography.headingSmall * 1.28),
}),
[typography, largeText],
);
@@ -992,13 +996,14 @@ export default function ConversationScreen() {
}),
[typography, inputLineHeight, largeText],
);
const connectionNoticeTitleStyle = useMemo(
() => ({
fontSize: largeText ? typography.titleSmall : typography.captionLarge,
const connectionNoticeTitleStyle = useMemo(() => {
const fs = largeText ? typography.titleSmall : typography.captionLarge;
return {
fontSize: fs,
lineHeight: Math.ceil(fs * 1.28),
fontWeight: '700' as const,
}),
[typography, largeText],
);
};
}, [typography, largeText]);
const connectionNoticeBodyStyle = useMemo(
() => ({
fontSize: largeText ? typography.bodySmall : typography.captionLarge,
@@ -1401,7 +1406,10 @@ export default function ConversationScreen() {
variant="chat"
title={
<View style={styles.headerTitleBlock}>
<Text style={[styles.headerTitle, headerTitleFontStyle]}>
<Text
style={[styles.headerTitle, headerTitleFontStyle]}
includeFontPadding={Platform.OS === 'android' ? false : undefined}
>
{tApp('name')}
</Text>
{showConnectionBadge ? (
@@ -1757,6 +1765,7 @@ const styles = StyleSheet.create({
},
voiceDurationText: {
fontSize: 18,
lineHeight: 24,
fontWeight: '500',
},
voiceDurationTextUser: {
@@ -1879,15 +1888,13 @@ const styles = StyleSheet.create({
},
voiceRecordDurationWrap: {
minWidth: 40,
height: 20,
minHeight: 20,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 1,
},
/** 字号/行高由 voiceRecordDurationStyle 提供,避免大字模式仍锁 20px 高裁切 */
voiceRecordDuration: {
fontSize: 12,
/** 与容器等高,避免 Android/iOS 数字相对胶囊上下偏移 */
height: 20,
lineHeight: 20,
paddingVertical: 0,
marginVertical: 0,
color: 'rgba(27, 27, 31, 0.86)',
@@ -1905,7 +1912,7 @@ const styles = StyleSheet.create({
width: '100%',
},
sendButton: {
height: 44,
minHeight: 44,
paddingHorizontal: 16,
paddingVertical: 11,
borderRadius: 22,

View File

@@ -6,6 +6,7 @@ import { useColorScheme } from '@/hooks/use-color-scheme';
import { useTranslation } from 'react-i18next';
import { TabBarIcon } from '@/components/tab-bar-icon';
import { useTypography } from '@/core/typography-context';
import { useSession } from '@/features/auth/hooks';
// Life-Echo bottom nav colors (from HTML reference)
@@ -27,6 +28,7 @@ const TAB_BAR_PADDING_HORIZONTAL = 12;
export default function TabsLayout() {
const { status } = useSession();
const typography = useTypography();
const { colorScheme } = useColorScheme();
const insets = useSafeAreaInsets();
const isDark = colorScheme === 'dark';
@@ -75,7 +77,8 @@ export default function TabsLayout() {
marginBottom: 0,
},
tabBarLabelStyle: {
fontSize: 11,
fontSize: typography.captionLarge,
lineHeight: typography.lineHeightTight,
fontWeight: '600',
textTransform: 'uppercase',
marginTop: 4,

View File

@@ -154,14 +154,16 @@ function ConversationCard({
{item.title}
</Text>
<Text
className="shrink-0 text-sm font-semibold uppercase tracking-wider text-muted-foreground"
variant="captionMedium"
className="shrink-0 font-semibold uppercase tracking-wider text-muted-foreground"
style={{ fontVariant: ['tabular-nums'] }}
>
{formatRelativeConversationListTime(item.latestMessageTime, t)}
</Text>
</View>
<Text
className="min-w-0 self-stretch text-base font-semibold text-muted-foreground"
variant="bodyMedium"
className="min-w-0 self-stretch font-semibold text-muted-foreground"
numberOfLines={1}
ellipsizeMode="tail"
>
@@ -202,7 +204,8 @@ function SwipeableConversationCard({
>
<Icon as={Trash2} className="text-destructive-foreground" size={24} />
<Text
className="mt-1 text-xs font-semibold text-destructive-foreground"
variant="captionSmall"
className="mt-1 font-semibold text-destructive-foreground"
selectable={false}
>
{t('delete')}
@@ -405,11 +408,13 @@ export default function ConversationsScreen() {
<Text
variant="h2"
className="text-center font-display text-primary"
style={{ borderWidth: 0, fontSize: 28, lineHeight: 38 }}
>
{t('greetingTitle')}
</Text>
<Text className="text-center text-base font-medium leading-6 text-muted-foreground">
<Text
variant="bodyLarge"
className="text-center font-medium text-muted-foreground"
>
{t('emptyGreetingSubtitle')}
</Text>
</View>
@@ -429,13 +434,15 @@ export default function ConversationsScreen() {
<Text
variant="h2"
className="text-center font-display text-primary"
style={{ borderWidth: 0, fontSize: 28, lineHeight: 38 }}
>
{showResumeEntry
? t('resumeChatTitle')
: t('greetingTitle')}
</Text>
<Text className="text-center text-base font-medium leading-6 text-muted-foreground">
<Text
variant="bodyLarge"
className="text-center font-medium text-muted-foreground"
>
{showResumeEntry
? t('resumeChatSubtitle')
: t('emptyGreetingSubtitle')}
@@ -449,10 +456,14 @@ export default function ConversationsScreen() {
{t('recentChats')}
</Text>
<Pressable
className="min-h-11 min-w-11 items-center justify-center active:opacity-70"
className="min-h-11 items-center justify-center px-2 active:opacity-70"
hitSlop={{ top: 6, bottom: 6, left: 4, right: 4 }}
onPress={() => {}}
>
<Text className="text-sm font-semibold text-primary">
<Text
variant="bodySmall"
className="font-semibold text-primary"
>
{t('viewAll')}
</Text>
</Pressable>

View File

@@ -23,6 +23,7 @@ import { Icon } from '@/components/ui/icon';
import { Skeleton } from '@/components/ui/skeleton';
import { Text } from '@/components/ui/text';
import { ScreenGutter } from '@/constants/layout';
import { useTypography } from '@/core/typography-context';
import { useCreateConversation } from '@/features/conversation/hooks';
import {
buildFrameworkChapterPlaceholders,
@@ -77,9 +78,18 @@ function ChapterCard({
onReadPress: () => void;
onContinuePress: () => void;
}) {
const typography = useTypography();
const { width } = useWindowDimensions();
const contentWidth = Math.min(width - ScreenGutter * 2, 672);
const gutter = ScreenGutter;
const completedTitleLineHeight = Math.max(
typography.lineHeightTight + 6,
typography.headingMedium + 6,
);
const draftingTitleLineHeight = Math.max(
typography.lineHeightTight + 4,
typography.titleLarge + 4,
);
const chapterIndex = item.orderIndex + 1;
const chapterLabel = t('chapterLabel').replace(
@@ -142,7 +152,8 @@ function ChapterCard({
<View style={{ padding: gutter, gap: 12 }}>
<View style={{ gap: 4, minHeight: 72 }}>
<Text
className="text-xs font-medium text-muted-foreground"
variant="captionMedium"
className="font-medium text-muted-foreground"
selectable
>
{chapterLabel}
@@ -150,7 +161,7 @@ function ChapterCard({
<Text
variant="h2"
className="text-foreground"
style={{ lineHeight: 34 }}
style={{ lineHeight: completedTitleLineHeight }}
numberOfLines={2}
selectable
>
@@ -161,7 +172,8 @@ function ChapterCard({
<View className="flex-row items-center gap-2">
<Icon as={FileText} className="text-muted-foreground" size={16} />
<Text
className="text-sm font-medium text-muted-foreground"
variant="bodySmall"
className="font-medium text-muted-foreground"
style={{ fontVariant: ['tabular-nums'] }}
selectable
>
@@ -174,7 +186,10 @@ function ChapterCard({
style={{ borderCurve: 'continuous' }}
onPress={onReadPress}
>
<Text className="text-base font-semibold text-primary-foreground">
<Text
variant="bodyLarge"
className="font-semibold text-primary-foreground"
>
{t('readMemory')}
</Text>
</Pressable>
@@ -197,7 +212,8 @@ function ChapterCard({
<View className="gap-1">
<View>
<Text
className="text-xs font-medium text-muted-foreground"
variant="captionMedium"
className="font-medium text-muted-foreground"
selectable
>
{chapterLabel}
@@ -205,7 +221,7 @@ function ChapterCard({
<Text
variant="h4"
className="mt-0.5 text-foreground"
style={{ lineHeight: 28 }}
style={{ lineHeight: draftingTitleLineHeight }}
numberOfLines={2}
selectable
>
@@ -213,7 +229,11 @@ function ChapterCard({
</Text>
</View>
<View className="mt-2 flex-row items-center gap-2">
<Text className="text-sm font-medium text-secondary" selectable>
<Text
variant="bodySmall"
className="font-medium text-secondary"
selectable
>
{t('statusDrafting')}
</Text>
</View>
@@ -223,7 +243,7 @@ function ChapterCard({
style={{ borderCurve: 'continuous' }}
onPress={onContinuePress}
>
<Text className="text-base font-semibold text-secondary">
<Text variant="bodyLarge" className="font-semibold text-secondary">
{t('continueWriting')}
</Text>
</Pressable>
@@ -237,7 +257,7 @@ function MemoirLoadError({ onRetry }: { onRetry: () => void }) {
const { t } = useTranslation('memoir');
return (
<View className="items-center gap-4 rounded-2xl border border-dashed border-border bg-muted/20 p-10">
<Text className="text-center text-base text-destructive">
<Text variant="bodyLarge" className="text-center text-destructive">
{t('loadErrorMessage')}
</Text>
<Pressable
@@ -245,7 +265,10 @@ function MemoirLoadError({ onRetry }: { onRetry: () => void }) {
style={{ borderCurve: 'continuous' }}
onPress={onRetry}
>
<Text className="font-semibold text-primary-foreground">
<Text
variant="bodyMedium"
className="font-semibold text-primary-foreground"
>
{t('loadErrorRetry')}
</Text>
</Pressable>

View File

@@ -90,11 +90,15 @@ function LanguageRow({
<Icon as={Globe} className="text-muted-foreground" size={20} />
<View>
<Text className="font-medium text-foreground">{label}</Text>
<Text className="text-sm text-muted-foreground">{description}</Text>
<Text variant="bodySmall" className="text-muted-foreground">
{description}
</Text>
</View>
</View>
<View className="flex-row items-center gap-2">
<Text className="text-sm text-muted-foreground">{currentLabel}</Text>
<Text variant="bodySmall" className="text-muted-foreground">
{currentLabel}
</Text>
<Icon as={ChevronRight} className="text-muted-foreground" size={20} />
</View>
</Pressable>
@@ -120,7 +124,9 @@ function SettingRow({
<Icon as={SettingIcon} className="text-muted-foreground" size={20} />
<View>
<Text className="font-medium text-foreground">{label}</Text>
<Text className="text-sm text-muted-foreground">{description}</Text>
<Text variant="bodySmall" className="text-muted-foreground">
{description}
</Text>
</View>
</View>
<Switch checked={value} onCheckedChange={onValueChange} />
@@ -203,7 +209,7 @@ export default function ProfileScreen() {
<Text variant="h3" className="text-foreground">
{user?.nickname ?? t('userNamePlaceholder')}
</Text>
<Text className="text-sm text-muted-foreground">
<Text variant="bodySmall" className="text-muted-foreground">
{t('userTier', { tier: tierLabel })}
</Text>
</View>

View File

@@ -1,12 +1,20 @@
import { router } from 'expo-router';
import { ArrowLeft } from 'lucide-react-native';
import React from 'react';
import { Pressable, View } from 'react-native';
import { Platform, Pressable, Text as RNText, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text';
import { useTypography } from '@/core/typography-context';
import { ScreenGutter } from '@/constants/layout';
import { useAppSettings } from '@/hooks/use-app-settings';
/** 默认最小触控目标 48dp大字模式下与标题字号匹配略向左扩展便于够到边缘 */
const BACK_HIT_MIN = 48;
const BACK_HIT_MIN_LARGE = 56;
const BACK_ICON_SIZE = 24;
const BACK_ICON_SIZE_LARGE = 30;
const BACK_EXTRA_HIT_LEFT = 4;
export type ScreenHeaderVariant = 'default' | 'chat' | 'reading';
@@ -64,21 +72,44 @@ export function ScreenHeader({
useSafeArea = true,
}: ScreenHeaderProps) {
const insets = useSafeAreaInsets();
const { largeText } = useAppSettings();
const typography = useTypography();
const colors = VARIANT_COLORS[variant];
const backTouchMin = largeText ? BACK_HIT_MIN_LARGE : BACK_HIT_MIN;
const backIconSize = largeText ? BACK_ICON_SIZE_LARGE : BACK_ICON_SIZE;
const handleBack = onBack ?? (() => router.back());
const bgColor = backgroundColor ?? colors.background;
const titleColor = colors.title;
const iconColor = colors.icon ?? colors.iconSecondary;
/**
* 不要用外层 minHeight「框死」整块栏paddingTop 含安全区时会把内容区压扁。
* 标题行最小高度随当前排版 token 计算(关大字也要留够,避免裁字 / Android font padding
*/
const barPaddingBottom = largeText ? 18 : 16;
const titleRowPaddingV = largeText ? 8 : 4;
const titleFontSize =
variant === 'chat'
? largeText
? typography.headingMedium
: typography.headingSmall
: Math.max(typography.titleLarge, typography.headingSmall);
const titleLineMin =
variant === 'chat'
? Math.ceil(titleFontSize * (largeText ? 1.45 : 1.38)) +
(largeText ? 14 : 10)
: Math.ceil(titleFontSize * 1.32) + (largeText ? 10 : 8);
const titleRowMinHeight = Math.max(backTouchMin, titleLineMin);
const containerStyle = {
flexDirection: 'row' as const,
alignItems: 'center' as const,
justifyContent: 'space-between' as const,
paddingHorizontal: Math.max(ScreenGutter, 16),
paddingTop: useSafeArea ? Math.max(insets.top, 12) : 12,
paddingBottom: 16,
minHeight: 56,
paddingBottom: barPaddingBottom,
...(bgColor && { backgroundColor: bgColor }),
...(absolute && {
position: 'absolute' as const,
@@ -105,41 +136,74 @@ export function ScreenHeader({
gap: 12,
flex: 1,
minWidth: 0,
paddingVertical: titleRowPaddingV,
minHeight: titleRowMinHeight,
}}
>
{showBack && (
<Pressable
onPress={handleBack}
hitSlop={
Platform.OS === 'android'
? {
top: largeText ? 10 : 6,
bottom: largeText ? 10 : 6,
right: 8,
left: BACK_EXTRA_HIT_LEFT,
}
: {
top: largeText ? 8 : 4,
bottom: largeText ? 8 : 4,
left: 2,
right: 4,
}
}
android_ripple={
Platform.OS === 'android'
? { borderless: true, radius: backTouchMin / 2 }
: undefined
}
style={({ pressed }) => ({
padding: 8,
minWidth: backTouchMin,
minHeight: backTouchMin,
marginLeft: -BACK_EXTRA_HIT_LEFT,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 9999,
opacity: pressed ? 0.7 : 1,
opacity: Platform.OS === 'android' ? 1 : pressed ? 0.7 : 1,
})}
accessibilityRole="button"
accessibilityLabel={backAccessibilityLabel}
>
{iconColor ? (
<Icon as={ArrowLeft} size={24} color={iconColor} />
<Icon as={ArrowLeft} size={backIconSize} color={iconColor} />
) : (
<Icon as={ArrowLeft} size={24} className="text-foreground" />
<Icon
as={ArrowLeft}
size={backIconSize}
className="text-foreground"
/>
)}
</Pressable>
)}
{typeof title === 'string' ? (
<Text
<RNText
numberOfLines={1}
selectable
style={{
flex: 1,
fontSize: variant === 'chat' ? 24 : 18,
fontSize: variant === 'chat' ? titleFontSize : 18,
lineHeight:
variant === 'chat'
? Math.ceil(titleFontSize * 1.35)
: Math.ceil(18 * 1.32),
fontWeight: '700',
letterSpacing: -0.5,
...(titleColor && { color: titleColor }),
}}
className={!titleColor ? 'text-foreground' : undefined}
>
{title}
</Text>
</RNText>
) : (
<View style={{ flex: 1, minWidth: 0 }}>{title}</View>
)}

View File

@@ -1,6 +1,7 @@
import { buttonTextVariants, buttonVariants } from '@/components/ui/button';
import { NativeOnlyAnimatedView } from '@/components/ui/native-only-animated-view';
import { TextClassContext } from '@/components/ui/text';
import { useTypography } from '@/core/typography-context';
import { cn } from '@/lib/utils';
import * as AlertDialogPrimitive from '@rn-primitives/alert-dialog';
import * as React from 'react';
@@ -97,11 +98,20 @@ function AlertDialogFooter({ className, ...props }: ViewProps) {
function AlertDialogTitle({
className,
style,
...props
}: AlertDialogPrimitive.TitleProps & React.RefAttributes<AlertDialogPrimitive.TitleRef>) {
const typography = useTypography();
return (
<AlertDialogPrimitive.Title
className={cn('min-w-0 text-foreground text-lg font-semibold', className)}
className={cn('min-w-0 text-foreground font-semibold', className)}
style={[
{
fontSize: typography.titleLarge,
lineHeight: typography.lineHeightTight,
},
style,
]}
{...props}
/>
);
@@ -113,15 +123,20 @@ function AlertDialogDescription({
...props
}: AlertDialogPrimitive.DescriptionProps &
React.RefAttributes<AlertDialogPrimitive.DescriptionRef>) {
const typography = useTypography();
return (
<AlertDialogPrimitive.Description
className={cn(
// shrink: RN 中 Text 在 flex 内需 flexShrink 才能按父宽换行
'w-full min-w-0 shrink text-muted-foreground text-sm',
'w-full min-w-0 shrink text-muted-foreground',
className
)}
style={[
Platform.OS === 'web' ? undefined : { flexShrink: 1 },
{
fontSize: typography.bodyMedium,
lineHeight: typography.lineHeightLoose,
},
style,
]}
{...props}
@@ -135,7 +150,15 @@ function AlertDialogAction({
}: AlertDialogPrimitive.ActionProps & React.RefAttributes<AlertDialogPrimitive.ActionRef>) {
return (
<TextClassContext.Provider value={buttonTextVariants({ className })}>
<AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />
<AlertDialogPrimitive.Action
className={cn(
buttonVariants(),
// 大字模式下文与行高变大,固定 h-10 会裁切标签(如「知道了」)
'h-auto min-h-14 py-3.5',
className
)}
{...props}
/>
</TextClassContext.Provider>
);
}
@@ -147,7 +170,11 @@ function AlertDialogCancel({
return (
<TextClassContext.Provider value={buttonTextVariants({ className, variant: 'outline' })}>
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: 'outline' }), className)}
className={cn(
buttonVariants({ variant: 'outline' }),
'h-auto min-h-14 py-3.5',
className
)}
{...props}
/>
</TextClassContext.Provider>

View File

@@ -3,7 +3,13 @@ import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
import {
Platform,
StyleSheet,
Text as RNText,
type Role,
type TextStyle,
} from 'react-native';
/**
* Maps Text variant to design-token keys for fontSize and lineHeight.
@@ -122,6 +128,22 @@ const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
const TextClassContext = React.createContext<string | undefined>(undefined);
/** Android 上 lineHeight < fontSize 会裁切字形(如标题配 lineHeightTight */
const MIN_LINE_HEIGHT_RATIO = 1.2;
function ensureMinimumLineHeight(style: TextStyle): TextStyle {
const fs = style.fontSize;
const lh = style.lineHeight;
if (typeof fs !== 'number' || typeof lh !== 'number') {
return style;
}
const minLh = Math.ceil(fs * MIN_LINE_HEIGHT_RATIO);
if (lh < minLh) {
return { ...style, lineHeight: minLh };
}
return style;
}
function Text({
className,
asChild = false,
@@ -151,12 +173,19 @@ function Text({
}
: undefined;
const flatStyle = StyleSheet.flatten([
{ textAlign: 'left' as const },
typographyStyle,
style,
]) as TextStyle;
const resolvedStyle = ensureMinimumLineHeight(flatStyle);
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}
role={variant ? ROLE[variant] : undefined}
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
style={[{ textAlign: 'left' }, typographyStyle, style]}
style={resolvedStyle}
{...props}
/>
);

View File

@@ -1,12 +1,14 @@
/**
* Typography context for large text mode (大字模式).
* Global typography from design tokens.
*
* - **关大字模式** → `typography.large`(舒适基准,不用过小的 `normal` 档)
* - **开大字模式** → `typography.xlarge`(一级页、正文等整体再放大一档)
*
* 对话气泡等仍可在页面内用 `largeText` 做更大一级的 token 选择。
*
* Uses React Context instead of NativeWind vars() because vars() returns empty
* objects on native (iOS/Android), so CSS variables never cascade to children.
* See: https://github.com/nativewind/nativewind/issues/1113
*
* NativeWind vars() docs (works on web only):
* https://www.nativewind.dev/docs/api/vars
* objects on native (iOS/Android). See:
* https://github.com/nativewind/nativewind/issues/1113
*/
import React, {
createContext,
@@ -19,18 +21,23 @@ import { useAppSettings } from '@/hooks/use-app-settings';
import tokens from '../../design-tokens.json';
export type TypographyMode = 'normal' | 'large';
export type TypographyScale = 'large' | 'xlarge';
export type TypographyTokens = Record<string, number>;
const TypographyContext = createContext<TypographyTokens | null>(null);
const SCALE: Record<TypographyScale, TypographyTokens> = {
large: tokens.typography.large as TypographyTokens,
xlarge: tokens.typography.xlarge as TypographyTokens,
};
export function TypographyProvider({ children }: PropsWithChildren) {
const { largeText } = useAppSettings();
const typography = useMemo(() => {
const mode: TypographyMode = largeText ? 'large' : 'normal';
return tokens.typography[mode] as TypographyTokens;
}, [largeText]);
const typography = useMemo(
() => SCALE[largeText ? 'xlarge' : 'large'],
[largeText],
);
return (
<TypographyContext.Provider value={typography}>

View File

@@ -25,7 +25,9 @@ const typographyVars = (mode) =>
]),
);
// Use default theme for Tailwind base (runtime theme switch via ThemeProvider)
// Use default theme for Tailwind base (runtime theme switch via ThemeProvider).
// Typography vars use the same comfortable scale as TypographyProvider (large),
// not the compact `normal` tier — see typography-context.tsx
const defaultLight = tokens.colors.default.light;
const defaultDark = tokens.colors.default.dark;
@@ -35,7 +37,7 @@ const rootVariables = Object.fromEntries([
value,
]),
['--radius', px(tokens.radius.default)],
...Object.entries(typographyVars('normal')),
...Object.entries(typographyVars('large')),
]);
const darkVariables = Object.fromEntries(