mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Move the New schedule button out of the screen header (the only screen using MenuHeader rightContent) into the content filter row, and match the History screen's full-width layout instead of the 720px centered column. Tighten the shared sm SegmentedControl and Button vertical padding so the status switcher and button sit flush with the host-filter pill; this is a global sm change and also affects settings and the schedule form sheet.
206 lines
5.4 KiB
TypeScript
206 lines
5.4 KiB
TypeScript
import { useCallback, useMemo, type ReactNode } from "react";
|
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
|
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
|
|
type SegmentedControlSize = "sm" | "md";
|
|
|
|
type SegmentedControlIconRenderer = (props: { color: string; size: number }) => ReactNode;
|
|
|
|
export interface SegmentedControlOption<T extends string> {
|
|
value: T;
|
|
label: string;
|
|
icon?: SegmentedControlIconRenderer;
|
|
disabled?: boolean;
|
|
testID?: string;
|
|
}
|
|
|
|
interface SegmentedControlProps<T extends string> {
|
|
options: SegmentedControlOption<T>[];
|
|
value: T;
|
|
onValueChange: (value: T) => void;
|
|
size?: SegmentedControlSize;
|
|
hideLabels?: boolean;
|
|
style?: StyleProp<ViewStyle>;
|
|
testID?: string;
|
|
}
|
|
|
|
export function SegmentedControl<T extends string>({
|
|
options,
|
|
value,
|
|
onValueChange,
|
|
size = "md",
|
|
hideLabels = false,
|
|
style,
|
|
testID,
|
|
}: SegmentedControlProps<T>) {
|
|
const { theme } = useUnistyles();
|
|
const containerSizeStyle = size === "sm" ? styles.containerSm : styles.containerMd;
|
|
const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd;
|
|
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
|
|
const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md;
|
|
|
|
const containerStyle = useMemo(
|
|
() => [styles.container, containerSizeStyle, style],
|
|
[containerSizeStyle, style],
|
|
);
|
|
|
|
return (
|
|
<View style={containerStyle} testID={testID}>
|
|
{options.map((option) => {
|
|
const isSelected = option.value === value;
|
|
const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted;
|
|
|
|
return (
|
|
<SegmentItem
|
|
key={option.value}
|
|
option={option}
|
|
isSelected={isSelected}
|
|
iconColor={iconColor}
|
|
iconSize={iconSize}
|
|
hideLabels={hideLabels}
|
|
segmentSizeStyle={segmentSizeStyle}
|
|
labelSizeStyle={labelSizeStyle}
|
|
currentValue={value}
|
|
onValueChange={onValueChange}
|
|
/>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SegmentItem<T extends string>({
|
|
option,
|
|
isSelected,
|
|
iconColor,
|
|
iconSize,
|
|
hideLabels,
|
|
segmentSizeStyle,
|
|
labelSizeStyle,
|
|
currentValue,
|
|
onValueChange,
|
|
}: {
|
|
option: SegmentedControlOption<T>;
|
|
isSelected: boolean;
|
|
iconColor: string;
|
|
iconSize: number;
|
|
hideLabels: boolean;
|
|
segmentSizeStyle: StyleProp<ViewStyle>;
|
|
labelSizeStyle: StyleProp<TextStyle>;
|
|
currentValue: T;
|
|
onValueChange: (value: T) => void;
|
|
}) {
|
|
const labelStyle = useMemo(
|
|
() => [styles.label, labelSizeStyle, isSelected && styles.labelSelected],
|
|
[labelSizeStyle, isSelected],
|
|
);
|
|
const handlePress = useCallback(() => {
|
|
if (!option.disabled && option.value !== currentValue) {
|
|
onValueChange(option.value);
|
|
}
|
|
}, [option.disabled, option.value, currentValue, onValueChange]);
|
|
const pressableStyle = useCallback(
|
|
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.segment,
|
|
segmentSizeStyle,
|
|
isSelected && styles.segmentSelected,
|
|
Boolean(hovered) && !isSelected && styles.segmentHover,
|
|
pressed && !isSelected && styles.segmentPressed,
|
|
option.disabled && styles.segmentDisabled,
|
|
],
|
|
[isSelected, option.disabled, segmentSizeStyle],
|
|
);
|
|
const accessibilityState = useMemo(
|
|
() => ({ selected: isSelected, disabled: option.disabled }),
|
|
[isSelected, option.disabled],
|
|
);
|
|
return (
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityState={accessibilityState}
|
|
disabled={option.disabled}
|
|
testID={option.testID}
|
|
onPress={handlePress}
|
|
style={pressableStyle}
|
|
>
|
|
{option.icon ? (
|
|
<View style={styles.iconContainer}>
|
|
{option.icon({ color: iconColor, size: iconSize })}
|
|
</View>
|
|
) : null}
|
|
{hideLabels ? null : (
|
|
<Text style={labelStyle} numberOfLines={1}>
|
|
{option.label}
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
flexDirection: "row",
|
|
alignItems: "stretch",
|
|
backgroundColor: theme.colors.surface2,
|
|
borderRadius: theme.borderRadius.lg,
|
|
gap: 2,
|
|
},
|
|
containerSm: {
|
|
padding: 2,
|
|
},
|
|
containerMd: {
|
|
padding: 3,
|
|
},
|
|
segment: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flexShrink: 0,
|
|
borderRadius: theme.borderRadius.lg,
|
|
gap: theme.spacing[1],
|
|
},
|
|
segmentSm: {
|
|
paddingVertical: theme.spacing[1.5],
|
|
paddingHorizontal: theme.spacing[4],
|
|
},
|
|
segmentMd: {
|
|
paddingVertical: theme.spacing[3],
|
|
paddingHorizontal: theme.spacing[6],
|
|
},
|
|
segmentSelected: {
|
|
backgroundColor: theme.colors.surface0,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowOpacity: 0.08,
|
|
shadowRadius: 2,
|
|
elevation: 1,
|
|
},
|
|
segmentHover: {
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
segmentPressed: {
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
segmentDisabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
iconContainer: {
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
label: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontWeight: theme.fontWeight.normal,
|
|
},
|
|
labelSm: {
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
labelMd: {
|
|
fontSize: theme.fontSize.base,
|
|
},
|
|
labelSelected: {
|
|
color: theme.colors.foreground,
|
|
},
|
|
}));
|