Files
paseo/packages/app/src/workspace-pins/pinnable-menu-item.tsx
T
Mohamed Boudra 713a042dc5 Add pinnable new-tab targets to the tab row and sidebar
Any new-tab target (new agent, terminal, browser, or a terminal profile)
can be pinned as a quick-launch button. Pins are global client app state
(persisted), not daemon config, so the same set shows on every workspace;
profile pins resolve per host and are dropped where that profile is absent.

The sidebar mirror launches via a navigate-then-launch intent store rather
than calling the create handlers directly: terminal creation only works in
the mounted/focused workspace, so the sidebar requests the launch and the
workspace screen consumes it on focus.
2026-06-15 14:09:29 +07:00

120 lines
3.4 KiB
TypeScript

import { useCallback, useMemo, useState, type ReactElement } from "react";
import { Pressable, View, type GestureResponderEvent } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import { Pin, PinOff } from "lucide-react-native";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { isNative } from "@/constants/platform";
import { useIsCompactFormFactor } from "@/constants/layout";
import type { Theme } from "@/styles/theme";
import { pinnedTargetKey, type PinnedTabTarget } from "@/workspace-pins/target";
import { usePinnedTargetsStore } from "@/workspace-pins/store";
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const ThemedPin = withUnistyles(Pin);
const ThemedPinOff = withUnistyles(PinOff);
interface PinnableMenuItemProps {
target: PinnedTabTarget;
label: string;
leading: ReactElement;
disabled?: boolean;
onSelect?: () => void;
testID?: string;
}
export function PinnableMenuItem({
target,
label,
leading,
disabled,
onSelect,
testID,
}: PinnableMenuItemProps): ReactElement {
const { t } = useTranslation();
const isCompact = useIsCompactFormFactor();
const [isHovered, setIsHovered] = useState(false);
const isPinned = usePinnedTargetsStore((state) => state.isPinned(target));
const toggle = usePinnedTargetsStore((state) => state.toggle);
const handlePointerEnter = useCallback(() => setIsHovered(true), []);
const handlePointerLeave = useCallback(() => setIsHovered(false), []);
const handleTogglePin = useCallback(
(event: GestureResponderEvent) => {
event.stopPropagation();
toggle(target);
},
[target, toggle],
);
const showToggle = isHovered || isNative || isCompact;
const slotStyle = useMemo(
() => [styles.pinToggleSlot, showToggle ? styles.pinToggleShown : styles.pinToggleHidden],
[showToggle],
);
const trailing = useMemo(
() => (
<View style={slotStyle} pointerEvents={showToggle ? "auto" : "none"}>
<Pressable
onPress={handleTogglePin}
hitSlop={8}
style={styles.pinToggleButton}
accessibilityRole="button"
accessibilityLabel={
isPinned
? t("workspace.tabs.actions.unpinTarget")
: t("workspace.tabs.actions.pinTarget")
}
testID={`workspace-pin-toggle-${pinnedTargetKey(target)}`}
>
{isPinned ? (
<ThemedPinOff size={14} uniProps={mutedColorMapping} />
) : (
<ThemedPin size={14} uniProps={mutedColorMapping} />
)}
</Pressable>
</View>
),
[handleTogglePin, isPinned, showToggle, slotStyle, t, target],
);
return (
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
<DropdownMenuItem
testID={testID}
leading={leading}
trailing={trailing}
disabled={disabled}
onSelect={onSelect}
>
{label}
</DropdownMenuItem>
</View>
);
}
const styles = StyleSheet.create(() => ({
pinToggleSlot: {
width: 22,
height: 22,
alignItems: "center",
justifyContent: "center",
},
pinToggleHidden: {
opacity: 0,
},
pinToggleShown: {
opacity: 1,
},
pinToggleButton: {
width: 22,
height: 22,
alignItems: "center",
justifyContent: "center",
},
}));