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.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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",
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user