163 lines
4.8 KiB
TypeScript
163 lines
4.8 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import type { LucideIcon } from "lucide-react-native";
|
|
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
|
|
import { ICON_SIZE } from "@/styles/theme";
|
|
import type { Theme } from "@/styles/theme";
|
|
import { Shortcut } from "@/components/ui/shortcut";
|
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
|
|
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
|
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
|
|
type SidebarHeaderRowVariant = "header" | "compact";
|
|
|
|
interface SidebarHeaderRowProps {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
onPress: () => void;
|
|
isActive?: boolean;
|
|
testID?: string;
|
|
nativeID?: string;
|
|
accessibilityLabel?: string;
|
|
/**
|
|
* "header" (default): a sidebar-height row with its own bottom separator —
|
|
* the lone header at the top of a sidebar (settings "Back to workspace").
|
|
* "compact": a workspace-row-height row with no separator, for entries that
|
|
* sit in a header group whose wrapper owns the single divider.
|
|
*/
|
|
variant?: SidebarHeaderRowVariant;
|
|
shortcutKeys?: ShortcutKey[][] | null;
|
|
}
|
|
|
|
export function SidebarHeaderRow({
|
|
icon: Icon,
|
|
label,
|
|
onPress,
|
|
isActive = false,
|
|
testID,
|
|
nativeID,
|
|
accessibilityLabel,
|
|
variant = "header",
|
|
shortcutKeys = null,
|
|
}: SidebarHeaderRowProps) {
|
|
const ThemedIcon = useMemo(() => withUnistyles(Icon), [Icon]);
|
|
|
|
const containerStyle = useMemo(
|
|
() => (variant === "compact" ? styles.containerCompact : styles.container),
|
|
[variant],
|
|
);
|
|
|
|
const buttonStyle = useCallback(
|
|
({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
styles.button,
|
|
variant === "compact" && styles.buttonCompact,
|
|
(Boolean(hovered) || isActive) && styles.buttonHovered,
|
|
],
|
|
[isActive, variant],
|
|
);
|
|
|
|
const renderChildren = useCallback(
|
|
(state: PressableStateCallbackType & { hovered?: boolean }) => {
|
|
const isHighlighted = Boolean(state.hovered) || isActive;
|
|
return (
|
|
<>
|
|
<ThemedIcon
|
|
size={ICON_SIZE.sm}
|
|
uniProps={isHighlighted ? foregroundColorMapping : foregroundMutedColorMapping}
|
|
/>
|
|
<SidebarHeaderRowLabel label={label} isHighlighted={isHighlighted} />
|
|
{shortcutKeys && Boolean(state.hovered) ? (
|
|
<Shortcut chord={shortcutKeys} style={styles.shortcut} />
|
|
) : null}
|
|
</>
|
|
);
|
|
},
|
|
[ThemedIcon, isActive, label, shortcutKeys],
|
|
);
|
|
|
|
return (
|
|
<View style={containerStyle}>
|
|
<Pressable
|
|
onPress={onPress}
|
|
testID={testID}
|
|
nativeID={nativeID}
|
|
accessible
|
|
accessibilityRole="button"
|
|
accessibilityLabel={accessibilityLabel ?? label}
|
|
style={buttonStyle}
|
|
>
|
|
{renderChildren}
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SidebarHeaderRowLabel({
|
|
label,
|
|
isHighlighted,
|
|
}: {
|
|
label: string;
|
|
isHighlighted: boolean;
|
|
}) {
|
|
const labelStyle = useMemo(
|
|
() => [styles.label, isHighlighted && styles.labelHighlighted],
|
|
[isHighlighted],
|
|
);
|
|
return <Text style={labelStyle}>{label}</Text>;
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
height: {
|
|
xs: HEADER_INNER_HEIGHT_MOBILE,
|
|
md: HEADER_INNER_HEIGHT,
|
|
},
|
|
paddingHorizontal: theme.spacing[2],
|
|
justifyContent: "center",
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
userSelect: "none",
|
|
},
|
|
containerCompact: {
|
|
paddingHorizontal: theme.spacing[2],
|
|
justifyContent: "center",
|
|
userSelect: "none",
|
|
},
|
|
button: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
// Match the sidebar workspace-row shape (height, padding, radius) so the
|
|
// compact header entries sit tight against the workspace list below.
|
|
minHeight: 36,
|
|
paddingVertical: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.lg,
|
|
},
|
|
// Compact header entries (New workspace / History) sit tighter than the
|
|
// workspace-row shape the base button mirrors.
|
|
buttonCompact: {
|
|
minHeight: 32,
|
|
paddingVertical: theme.spacing[1.5],
|
|
// Match the project rows' inner padding so the icons align on one vertical
|
|
// edge with the workspace list below (base button uses a wider spacing[3]).
|
|
paddingHorizontal: theme.spacing[2],
|
|
},
|
|
buttonHovered: {
|
|
backgroundColor: theme.colors.surfaceSidebarHover,
|
|
},
|
|
label: {
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.normal,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
labelHighlighted: {
|
|
color: theme.colors.foreground,
|
|
},
|
|
shortcut: {
|
|
marginLeft: "auto",
|
|
},
|
|
}));
|