fix: tint unread mail and draw the mail search, subject, quick reply and thread buttons like the webmail

This commit is contained in:
Linus Rath
2026-10-15 15:04:00 +02:00
parent 6f515e15f7
commit b048eaca03
7 changed files with 66 additions and 26 deletions
+3 -4
View File
@@ -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: {
+5 -5
View File
@@ -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 },
});
}
+11 -10
View File
@@ -74,16 +74,16 @@ export function ThreadMessageCard({
<MessageContent {...content} compact={false} onToggleStar={bare ? undefined : onToggleStar} />
{!bare && (
<View style={[styles.actions, !replyable && styles.actionsDisabled]}>
<Pressable style={styles.actionBtn} onPress={reply('reply')} hitSlop={9}>
<Reply size={16} color={c.textSecondary} />
<Pressable style={styles.actionBtn} onPress={reply('reply')} hitSlop={6}>
<Reply size={16} color={c.text} />
<Text style={styles.actionLabel}>{t('email_viewer.reply', 'Reply')}</Text>
</Pressable>
<Pressable style={styles.actionBtn} onPress={reply('replyAll')} hitSlop={9}>
<ReplyAll size={16} color={c.textSecondary} />
<Pressable style={styles.actionBtn} onPress={reply('replyAll')} hitSlop={6}>
<ReplyAll size={16} color={c.text} />
<Text style={styles.actionLabel}>{t('email_viewer.reply_all', 'Reply All')}</Text>
</Pressable>
<Pressable style={styles.actionBtn} onPress={reply('forward')} hitSlop={9}>
<Forward size={16} color={c.textSecondary} />
<Pressable style={styles.actionBtn} onPress={reply('forward')} hitSlop={6}>
<Forward size={16} color={c.text} />
<Text style={styles.actionLabel}>{t('email_viewer.forward', 'Forward')}</Text>
</Pressable>
</View>
@@ -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 },
});
}
+8 -5
View File
@@ -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
+3 -2
View File
@@ -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,
},
+17
View File
@@ -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');
});
});
+19
View File
@@ -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('')}`;
}