diff --git a/src/components/SidebarDrawer.tsx b/src/components/SidebarDrawer.tsx index 94bf05e8..d7342572 100644 --- a/src/components/SidebarDrawer.tsx +++ b/src/components/SidebarDrawer.tsx @@ -1459,10 +1459,9 @@ function makeStyles(c: ThemePalette) { color: c.text, }, rowLabelAccount: { - ...typography.caption, - fontWeight: '600', - textTransform: 'uppercase', - letterSpacing: 0.5, + fontSize: 13, + lineHeight: 18, + fontWeight: '500', color: c.textSecondary, }, counts: { diff --git a/src/components/email/QuickReplyBox.tsx b/src/components/email/QuickReplyBox.tsx index 2dd0593d..cadb8fff 100644 --- a/src/components/email/QuickReplyBox.tsx +++ b/src/components/email/QuickReplyBox.tsx @@ -155,7 +155,7 @@ export function QuickReplyBox({ email, jmapAccountId, onMoreOptions, onSent, onF onFocus={onFocus} onBlur={onBlur} placeholder={t('email_viewer.quick_reply_placeholder', 'Write a quick reply...')} - placeholderTextColor={c.textMuted} + placeholderTextColor={c.mutedForeground} style={styles.input} multiline editable={!sending} @@ -189,7 +189,7 @@ function makeStyles(c: ThemePalette) { borderWidth: 1, borderColor: c.border, borderRadius: radius.md, - backgroundColor: c.surface, + backgroundColor: c.background, padding: spacing.sm, gap: spacing.sm, }, @@ -211,10 +211,10 @@ function makeStyles(c: ThemePalette) { gap: spacing.xs, backgroundColor: c.primary, paddingHorizontal: spacing.md, - paddingVertical: 6, - borderRadius: radius.full, + minHeight: 36, + borderRadius: radius.sm, }, sendDisabled: { opacity: 0.5 }, - sendText: { ...typography.caption, color: c.primaryForeground, fontWeight: '600' }, + sendText: { ...typography.bodyMedium, color: c.primaryForeground }, }); } diff --git a/src/components/email/ThreadMessageCard.tsx b/src/components/email/ThreadMessageCard.tsx index b7718f36..a70b0e28 100644 --- a/src/components/email/ThreadMessageCard.tsx +++ b/src/components/email/ThreadMessageCard.tsx @@ -74,16 +74,16 @@ export function ThreadMessageCard({ {!bare && ( - - + + {t('email_viewer.reply', 'Reply')} - - + + {t('email_viewer.reply_all', 'Reply All')} - - + + {t('email_viewer.forward', 'Forward')} @@ -159,13 +159,14 @@ function makeStyles(c: ThemePalette) { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, + // Webmail thread card: outline buttons, h-9, rounded-md. paddingHorizontal: spacing.md, - paddingVertical: 6, - borderRadius: radius.full, + minHeight: 36, + borderRadius: radius.sm, borderWidth: 1, borderColor: c.border, - backgroundColor: c.surface, + backgroundColor: 'transparent', }, - actionLabel: { ...typography.caption, color: c.textSecondary }, + actionLabel: { ...typography.bodyMedium, color: c.text }, }); } diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index c7d3a10b..6389a1fd 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -11,7 +11,7 @@ import { } from '../components/icons'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens'; +import { spacing, radius, typography, componentSizes, mix, type ThemePalette } from '../theme/tokens'; import { useColors, useResolvedTheme } from '../theme/colors'; import { useTypography, useDensity } from '../theme/dynamic'; import SidebarDrawer from '../components/SidebarDrawer'; @@ -168,6 +168,7 @@ const EmailRow = React.memo(function EmailRow({ style={({ pressed }) => [ styles.emailRow, { paddingVertical: density.rowPaddingVertical }, + unread && styles.emailRowUnread, pressed && styles.emailRowPressed, active && styles.emailRowActive, selected && styles.emailRowSelected, @@ -2002,10 +2003,9 @@ function makeStyles(c: ThemePalette) { // A minimum, not a fixed height: at a larger system font the text used // to be cut off at the top. minHeight: componentSizes.inputHeight, // h-10 = 40px - backgroundColor: c.surface, - borderRadius: radius.sm, - borderWidth: 1, - borderColor: c.border, + // Webmail mobile header: a filled pill, no border. + backgroundColor: c.muted, + borderRadius: radius.full, gap: spacing.sm, }, // No lineHeight and no Android default padding on a TextInput: both clip @@ -2298,6 +2298,9 @@ function makeStyles(c: ThemePalette) { borderBottomWidth: 1, borderBottomColor: c.border, // border-b border-border }, + // Webmail: unread rows get bg-accent/30. Opaque, since the row covers + // its swipe actions. + emailRowUnread: { backgroundColor: mix(c.accent, c.background, 0.3) }, emailRowPressed: { backgroundColor: c.surface }, emailRowSelected: { backgroundColor: c.selection }, // The message open next to the list (webmail: bg-selection with a primary diff --git a/src/screens/EmailThreadScreen.tsx b/src/screens/EmailThreadScreen.tsx index d1879a44..342c43bc 100644 --- a/src/screens/EmailThreadScreen.tsx +++ b/src/screens/EmailThreadScreen.tsx @@ -1683,11 +1683,12 @@ function makeStyles(c: ThemePalette) { subjectStar: { paddingTop: 4, }, + // Webmail viewer on a phone: text-lg font-bold tracking-tight. subjectText: { flex: 1, - fontSize: 20, + fontSize: 18, fontWeight: '700', - lineHeight: 28, + lineHeight: 26, color: c.text, letterSpacing: -0.2, }, diff --git a/src/theme/__tests__/mix.test.ts b/src/theme/__tests__/mix.test.ts new file mode 100644 index 00000000..b5152626 --- /dev/null +++ b/src/theme/__tests__/mix.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { mix, DARK_COLORS, LIGHT_COLORS } from '../tokens'; + +describe('mix', () => { + it('blends like a translucent layer, but opaque', () => { + expect(mix('#ffffff', '#000000', 0.5)).toBe('#808080'); + expect(mix('#1e3a8a', '#0a0a0a', 0)).toBe('#0a0a0a'); + expect(mix('#1e3a8a', '#0a0a0a', 1)).toBe('#1e3a8a'); + expect(mix('rgba(0,0,0,0.5)', '#0a0a0a', 0.3)).toBe('#0a0a0a'); + }); + + it('gives the webmail unread row tint (bg-accent/30)', () => { + // #1e3a8a at 30% over #0a0a0a, and #dbeafe at 30% over white. + expect(mix(DARK_COLORS.accent, DARK_COLORS.background, 0.3)).toBe('#101830'); + expect(mix(LIGHT_COLORS.accent, LIGHT_COLORS.background, 0.3)).toBe('#f4f9ff'); + }); +}); diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index 68bc78d0..5b146b88 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -329,3 +329,22 @@ export const componentSizes = { toggleHeight: 24, toggleWidth: 44, toggleThumb: 20, fab: 56, badgeSize: 16, } as const; + +/** + * `top` over `bottom` at `amount` opacity, as an opaque #rrggbb: the + * webmail's `bg-accent/30` for a surface that must hide what is behind it + * (a list row over its swipe actions). Takes #rrggbb; anything else + * returns `bottom` unchanged. + */ +export function mix(top: string, bottom: string, amount: number): string { + const parse = (hex: string) => { + const m = /^#([0-9a-f]{6})$/i.exec(hex); + if (!m) return null; + const n = parseInt(m[1], 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; + }; + const a = parse(top); + const b = parse(bottom); + if (!a || !b) return bottom; + return `#${a.map((v, i) => Math.round(v * amount + b[i] * (1 - amount)).toString(16).padStart(2, '0')).join('')}`; +}