refactor: Update Components to Follow Material Design 3 Guidelines

Update 13 components to comply with MD3 specs:
- Switch: rewrite with MD3 dimensions (52x32 track, 16/24 thumb),
  animated thumb size, proper color tokens and border
- BottomTabBar: fix active indicator (64x32, borderRadius 16),
  label weight/color, container padding
- SearchBar: fix placeholder color, explicit borderRadius 28
- Chip: fix fontSize 14, consistent MD3 color tokens
- Actionbar: remove rounded corners, tonal elevation
- Menu: borderRadius 4, elevation 2, surfaceContainer bg
- SegmentedControl: height 40, borderRadius 20
- List: icon/subheader use onSurfaceVariant, description fontSize 14,
  divider uses outlineVariant
- BottomSheet: add 28dp top corner radius
- SwitchItem: add paddingHorizontal 16, description fontSize 14
- ConfirmationDialog: content fontSize 14
- IconButtonV2: disabled color uses onSurface at 38% opacity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rajarshee Chatterjee
2026-02-08 14:41:00 +05:30
parent f1fdafd37f
commit ab1e36d280
15 changed files with 173 additions and 94 deletions
+1 -3
View File
@@ -73,10 +73,8 @@ export const Actionbar: React.FC<ActionbarProps> = ({
const styles = StyleSheet.create({
actionbarContainer: {
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
bottom: 0,
elevation: 1,
elevation: 0,
flexDirection: 'row',
justifyContent: 'space-around',
position: 'absolute',
+35 -1
View File
@@ -9,7 +9,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useBackHandler } from '@hooks/index';
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
import BottomSheetBackdrop from './BottomSheetBackdrop';
import { useWindowDimensions } from 'react-native';
import { StyleSheet, useWindowDimensions, View } from 'react-native';
import { useTheme } from '@hooks/persisted';
interface BottomSheetProps
extends Omit<BottomSheetModalProps, 'ref' | 'onChange' | 'snapPoints'> {
@@ -18,6 +19,33 @@ interface BottomSheetProps
snapPoints?: number[];
}
// const MD3DragHandle = () => {
// const theme = useTheme();
// return (
// <View style={[handleStyles.container, { backgroundColor: theme.surface }]}>
// <View
// style={[
// handleStyles.handle,
// { backgroundColor: theme.onSurfaceVariant },
// ]}
// />
// </View>
// );
// };
// const handleStyles = StyleSheet.create({
// container: {
// alignItems: 'center',
// paddingVertical: 12,
// },
// handle: {
// width: 32,
// height: 4,
// borderRadius: 2,
// opacity: 0.4,
// },
// });
const BottomSheet: React.FC<BottomSheetProps> = ({
bottomSheetRef,
children,
@@ -29,6 +57,7 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
const indexRef = useRef<number>(null);
const { bottom, top } = useSafeAreaInsets();
const { height } = useWindowDimensions();
const theme = useTheme();
const renderBackdrop = useCallback(
(backdropProps: BottomSheetBackdropProps) => (
<BottomSheetBackdrop {...backdropProps} />
@@ -61,6 +90,11 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
ref={bottomSheetRef}
backdropComponent={renderBackdrop}
handleComponent={null}
backgroundStyle={{
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
backgroundColor: theme.surface,
}}
containerStyle={[{ paddingBottom: bottom }, containerStyle]}
onChange={index => {
onChange?.(index);
+7 -6
View File
@@ -100,7 +100,7 @@ function CustomBottomTabBar({
transitionDuration: 250,
transitionTimingFunction: 'ease-in-out',
marginBottom: showLabel ? 4 : 20,
width: isFocused ? 52 : 32,
width: isFocused ? 64 : 32,
backgroundColor: isFocused
? theme.primaryContainer
: 'transparent',
@@ -116,8 +116,8 @@ function CustomBottomTabBar({
style={[
styles.label,
{
color: theme.onSurfaceVariant,
fontWeight: '700',
color: isFocused ? theme.onSurface : theme.onSurfaceVariant,
fontWeight: '500',
},
]}
numberOfLines={1}
@@ -138,7 +138,8 @@ export type { CustomBottomTabBarProps };
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingVertical: 4,
paddingTop: 12,
paddingBottom: 16,
paddingHorizontal: 0,
minHeight: 80,
},
@@ -154,8 +155,8 @@ const styles = StyleSheet.create({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: 2,
borderRadius: 24,
height: 32,
borderRadius: 16,
},
label: {
height: 16,
+3 -6
View File
@@ -2,7 +2,6 @@ import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { ThemeColors } from '../../theme/types';
import { overlay } from 'react-native-paper';
interface ChipProps {
label: string;
@@ -14,9 +13,7 @@ const Chip: React.FC<ChipProps> = ({ label, theme }) => (
style={[
styles.chipContainer,
{
backgroundColor: theme.isDark
? overlay(1, theme.surface)
: theme.secondaryContainer,
backgroundColor: theme.secondaryContainer,
},
]}
>
@@ -28,7 +25,7 @@ const Chip: React.FC<ChipProps> = ({ label, theme }) => (
style={[
styles.label,
{
color: theme.isDark ? theme.onSurface : theme.onSecondaryContainer,
color: theme.onSecondaryContainer,
},
]}
>
@@ -48,7 +45,7 @@ const styles = StyleSheet.create({
overflow: 'hidden',
},
label: {
fontSize: 12,
fontSize: 14,
},
pressable: {
alignItems: 'center',
@@ -65,7 +65,7 @@ const styles = StyleSheet.create({
shadowColor: 'transparent',
},
content: {
fontSize: 16,
fontSize: 14,
letterSpacing: 0,
},
});
+5 -1
View File
@@ -41,7 +41,11 @@ const IconButton: React.FC<Props> = ({
<MaterialCommunityIcons
name={name}
size={size}
color={disabled ? theme.outline : color || theme.onSurface}
color={
disabled
? Color(theme.onSurface).alpha(0.38).string()
: color || theme.onSurface
}
/>
</Pressable>
</View>
+13 -8
View File
@@ -33,7 +33,7 @@ const SubHeader = ({
children: ReactNode;
theme: ThemeColors;
}) => (
<PaperList.Subheader style={{ color: theme.primary }}>
<PaperList.Subheader style={{ color: theme.onSurfaceVariant }}>
{children}
</PaperList.Subheader>
);
@@ -51,13 +51,13 @@ const Item: React.FC<ListItemProps> = ({
if (icon) {
return (
<PaperList.Icon
color={theme.primary}
color={theme.onSurfaceVariant}
icon={icon}
style={styles.iconCtn}
/>
);
}
}, [icon, theme.primary]);
}, [icon, theme.onSurfaceVariant]);
const rightIcon = useCallback(() => {
if (right) {
return (
@@ -68,7 +68,7 @@ const Item: React.FC<ListItemProps> = ({
/>
);
}
}, [right, theme.primary]);
}, [right, theme.onSurfaceVariant]);
return (
<PaperList.Item
title={title}
@@ -93,7 +93,9 @@ const Item: React.FC<ListItemProps> = ({
};
const Divider = ({ theme }: { theme: ThemeColors }) => (
<PaperDivider style={[styles.divider, { backgroundColor: theme.outline }]} />
<PaperDivider
style={[styles.divider, { backgroundColor: theme.outlineVariant }]}
/>
);
const InfoItem = ({
@@ -119,7 +121,11 @@ const InfoItem = ({
);
const Icon = ({ icon, theme }: { icon: string; theme: ThemeColors }) => (
<PaperList.Icon color={theme.primary} icon={icon} style={styles.margin0} />
<PaperList.Icon
color={theme.onSurfaceVariant}
icon={icon}
style={styles.margin0}
/>
);
interface ColorItemProps {
@@ -174,12 +180,11 @@ const styles = StyleSheet.create({
marginEnd: 16,
},
description: {
fontSize: 12,
fontSize: 14,
lineHeight: 20,
},
divider: {
height: 1,
opacity: 0.5,
},
iconCtn: {
paddingStart: 16,
+7 -7
View File
@@ -173,7 +173,7 @@ const Menu: React.FC<MenuProps> & { Item: React.FC<MenuItemProps> } = ({
key={menuLayout ? 'ready' : 'measuring'}
style={[
styles.menuContainer,
{ backgroundColor: theme.surface },
{ backgroundColor: theme.surface2 || theme.surface },
contentStyle,
menuPosition,
]}
@@ -220,14 +220,14 @@ Menu.Item = MenuItem;
const styles = StyleSheet.create({
menuContainer: {
borderRadius: 8,
elevation: 8,
borderRadius: 4,
elevation: 2,
shadowOffset: {
width: 0,
height: 2,
height: 1,
},
shadowOpacity: 0.25,
shadowRadius: 6,
shadowOpacity: 0.2,
shadowRadius: 3,
overflow: 'hidden',
position: 'absolute',
zIndex: 1001,
@@ -246,7 +246,7 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(0, 0, 0, 0.1)',
},
menuItem: {
paddingHorizontal: 16,
paddingHorizontal: 12,
paddingVertical: 12,
minHeight: 48,
justifyContent: 'center',
+2 -2
View File
@@ -82,7 +82,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
ref={searchbarRef}
style={[styles.textInput, { color: theme.onSurface }]}
placeholder={placeholder}
placeholderTextColor={theme.onSurface}
placeholderTextColor={theme.onSurfaceVariant}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
@@ -160,7 +160,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
},
searchbarContainer: {
borderRadius: 50,
borderRadius: 28,
marginBottom: 12,
marginHorizontal: 16,
minHeight: 56,
@@ -88,7 +88,7 @@ export function SegmentedControl<T extends string = string>({
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
height: 48,
height: 40,
},
segment: {
flex: 1,
@@ -99,16 +99,16 @@ const styles = StyleSheet.create({
segmentFirst: {
borderLeftWidth: 1,
borderRightWidth: 1,
borderTopLeftRadius: 24,
borderBottomLeftRadius: 24,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
segmentMiddle: {
borderRightWidth: 1,
},
segmentLast: {
borderRightWidth: 1,
borderTopRightRadius: 24,
borderBottomRightRadius: 24,
borderTopRightRadius: 20,
borderBottomRightRadius: 20,
},
segmentPressable: {
flex: 1,
+88 -41
View File
@@ -10,73 +10,118 @@ import Animated, {
} from 'react-native-reanimated';
import { useTheme } from '@hooks/persisted';
// MD3 Switch dimensions
const TRACK_WIDTH = 52;
const TRACK_HEIGHT = 32;
const TRACK_RADIUS = TRACK_HEIGHT / 2;
const THUMB_SIZE_OFF = 16;
const THUMB_SIZE_ON = 24;
const TRACK_BORDER_WIDTH = 2;
// Thumb positions: centered vertically, padded from edges
const THUMB_OFFSET_OFF = (TRACK_HEIGHT - THUMB_SIZE_OFF) / 2;
const THUMB_OFFSET_ON =
TRACK_WIDTH - THUMB_SIZE_ON - (TRACK_HEIGHT - THUMB_SIZE_ON) / 2;
interface SwitchProps {
value: boolean;
onValueChange?: () => void;
size?: number;
style?: StyleProp<ViewStyle>;
}
const Switch = ({ value, size = 22, onValueChange, style }: SwitchProps) => {
const Switch = ({ value, onValueChange, style }: SwitchProps) => {
const theme = useTheme();
// Value for Switch Animation
// const switchTranslate = useSharedValue(value ? size : size / 6);
// Background color animation progress
const progress = useSharedValue(value ? size : 0);
const progress = useSharedValue(value ? 1 : 0);
// Animate switch movement
const customSpringStyles = useAnimatedStyle(() => ({
useEffect(() => {
progress.value = withTiming(value ? 1 : 0, { duration: 200 });
}, [progress, value]);
// Animate thumb position
const thumbPositionStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: withSpring(value ? size : size / 6, {
mass: 1,
damping: 15,
stiffness: 120,
overshootClamping: false,
}),
translateX: withSpring(
value ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF,
{
mass: 1,
damping: 15,
stiffness: 120,
overshootClamping: false,
},
),
},
],
}));
// Precompute background color animation
const backgroundColor = useDerivedValue(
// Animate thumb size
const thumbSizeStyle = useAnimatedStyle(() => {
const size = withSpring(value ? THUMB_SIZE_ON : THUMB_SIZE_OFF, {
mass: 1,
damping: 15,
stiffness: 120,
overshootClamping: false,
});
return {
width: size,
height: size,
borderRadius: size,
};
});
// Animate track background color
const trackBgColor = useDerivedValue(
() =>
interpolateColor(
progress.value,
[0, size],
[theme.outline, theme.primary],
[0, 1],
[theme.surfaceVariant, theme.primary],
),
[value],
[theme.surfaceVariant, theme.primary],
);
const backgroundColorStyle = useAnimatedStyle(() => ({
backgroundColor: backgroundColor.value,
const trackColorStyle = useAnimatedStyle(() => ({
backgroundColor: trackBgColor.value,
}));
useEffect(() => {
progress.value = withTiming(value ? size : 0);
}, [progress, size, value]);
// Animate track border
const trackBorderStyle = useAnimatedStyle(() => ({
borderWidth: withTiming(value ? 0 : TRACK_BORDER_WIDTH, { duration: 200 }),
borderColor: theme.outline,
}));
// Animate thumb color
const thumbBgColor = useDerivedValue(
() =>
interpolateColor(
progress.value,
[0, 1],
[theme.outline, theme.onPrimary],
),
[theme.outline, theme.onPrimary],
);
const thumbColorStyle = useAnimatedStyle(() => ({
backgroundColor: thumbBgColor.value,
}));
return (
<Pressable onPress={onValueChange}>
<Animated.View
style={[
styles.container,
styles.track,
style,
{
width: size * 2 + size / 6,
height: size + size / 3,
borderRadius: size,
},
backgroundColorStyle,
trackColorStyle,
trackBorderStyle,
]}
>
<Animated.View
style={[
styles.circle,
customSpringStyles,
{ height: size, width: size, borderRadius: size },
styles.thumb,
thumbPositionStyle,
thumbSizeStyle,
thumbColorStyle,
]}
/>
</Animated.View>
@@ -87,18 +132,20 @@ const Switch = ({ value, size = 22, onValueChange, style }: SwitchProps) => {
export default React.memo(Switch);
const styles = StyleSheet.create({
circle: {
backgroundColor: 'white',
elevation: 4,
thumb: {
elevation: 1,
shadowColor: 'black',
shadowOffset: {
width: 0,
height: 2,
height: 1,
},
shadowOpacity: 0.2,
shadowRadius: 2.5,
shadowOpacity: 0.15,
shadowRadius: 1,
},
container: {
track: {
width: TRACK_WIDTH,
height: TRACK_HEIGHT,
borderRadius: TRACK_RADIUS,
justifyContent: 'center',
},
});
+2 -4
View File
@@ -16,7 +16,6 @@ interface SwitchItemProps {
description?: string;
onPress: () => void;
theme: ThemeColors;
size?: number;
style?: StyleProp<ViewStyle>;
}
@@ -26,7 +25,6 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
onPress,
theme,
value,
size,
style,
}) => (
<Pressable
@@ -46,7 +44,6 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
value={value}
onValueChange={onPress}
style={styles.switch}
size={size}
/>
</Pressable>
);
@@ -58,10 +55,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
},
description: {
fontSize: 12,
fontSize: 14,
lineHeight: 20,
},
label: {
-2
View File
@@ -86,7 +86,6 @@ const MoreScreen = ({ navigation }: MoreStackScreenProps) => {
<Switch
value={downloadedOnlyMode}
onValueChange={enableDownloadedOnlyMode}
size={24}
/>
</Pressable>
<Pressable
@@ -120,7 +119,6 @@ const MoreScreen = ({ navigation }: MoreStackScreenProps) => {
<Switch
value={incognitoMode}
onValueChange={enableIncognitoMode}
size={24}
/>
</Pressable>
<List.Divider theme={theme} />
@@ -227,14 +227,12 @@ const JumpToChapterModal = ({
value={openChapter}
theme={theme}
onPress={() => setOpenChapter(!openChapter)}
size={20}
/>
<SwitchItem
label={getString('novelScreen.jumpToChapterModal.chapterName')}
value={mode}
theme={theme}
onPress={() => setMode(!mode)}
size={20}
/>
</View>
{result.length ? (
@@ -16,7 +16,6 @@ const ReaderSheetPreferenceItem: React.FC<ReaderSheetPreferenceItemProps> = ({
onPress,
theme,
}) => {
const size = 20;
return (
<Pressable
style={styles.container}
@@ -30,14 +29,14 @@ const ReaderSheetPreferenceItem: React.FC<ReaderSheetPreferenceItemProps> = ({
fallback={
<View
style={{
width: size * 2 + size / 6,
height: size + size / 3,
borderRadius: size,
width: 52,
height: 32,
borderRadius: 16,
}}
/>
}
>
<Switch value={value} onValueChange={onPress} size={size} />
<Switch value={value} onValueChange={onPress} />
</Suspense>
</Pressable>
);