The sidebar can be pinned to a set of hosts or to a set of labels, but not to a set of projects. If you keep many projects on one host, the project is the filter you want. Add a Project page to the sidebar display menu, next to Host. It multi-selects projects, and an empty selection means every project. Apply the filter at two points. Project grouping reads the projects array, but Status grouping builds its rows from the workspace entries. A filter applied to only one of the two leaves the other mode unfiltered. Apply nothing when a stored selection names no visible project. The project list is narrowed by the host filter, and it is empty before any host connects, so dropping keys that do not match would destroy the selection on every cold start. Count the project filter in the sidebar header gate. Pinning every chat in a project drops that project from the unpinned list. Filtering to that project then removed the header, and the header carries the display menu, which is the only way back to the Project page. Generalise the label filter empty state to all sidebar filters, and move its strings out of the labels namespace. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1157 lines
38 KiB
TypeScript
1157 lines
38 KiB
TypeScript
import {
|
|
memo,
|
|
useCallback,
|
|
useMemo,
|
|
useState,
|
|
type MutableRefObject,
|
|
type ReactNode,
|
|
type Ref,
|
|
} from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
View,
|
|
Text,
|
|
Pressable,
|
|
ScrollView,
|
|
type GestureResponderEvent,
|
|
type PressableStateCallbackType,
|
|
} from "react-native";
|
|
import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
|
import type { GestureType } from "react-native-gesture-handler";
|
|
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
|
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
|
import { type SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
|
import type { StatusBucket } from "@/hooks/sidebar-status-view-model";
|
|
import type { SidebarWorkspaceGroup } from "@/components/sidebar/sidebar-labels";
|
|
import { SidebarFilterEmptyState } from "@/components/sidebar/empty-states";
|
|
import type { HostBadgeModel } from "@/hosts/appearance";
|
|
import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform";
|
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
|
import { StyleSheet } from "react-native-unistyles";
|
|
import type { Theme } from "@/styles/theme";
|
|
import type { SidebarSurfaceBackdrop } from "@/styles/surface-backdrop";
|
|
import { withUnistyles } from "react-native-unistyles";
|
|
import {
|
|
ChevronDown,
|
|
ChevronRight,
|
|
CircleAlert,
|
|
CircleCheck,
|
|
CircleDot,
|
|
CircleX,
|
|
} from "lucide-react-native";
|
|
import { useToast } from "@/contexts/toast-context";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
|
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
|
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
|
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
|
import { useWorkspaceArchive } from "@/workspace/use-workspace-archive";
|
|
import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning";
|
|
import * as Clipboard from "expo-clipboard";
|
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
|
import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention";
|
|
import {
|
|
SidebarWorkspaceRowFrame,
|
|
SidebarWorkspaceRowContent,
|
|
resolveTrailingActionVisibility,
|
|
SidebarWorkspaceTrailingActionBase,
|
|
SidebarWorkspaceTrailingActionOverlay,
|
|
SidebarWorkspaceTrailingActionSlot,
|
|
sidebarWorkspaceRowStyles,
|
|
} from "@/components/sidebar/sidebar-workspace-row-content";
|
|
import { useOpenKebabMenuVisibility } from "@/components/sidebar/use-open-kebab-menu-visibility";
|
|
import { getSidebarRowBackdrop } from "@/components/sidebar/sidebar-row-backdrop";
|
|
import { getStatusDotColor } from "@/utils/status-dot-color";
|
|
import { selectWorkspaceServiceSummary } from "@/components/sidebar/workspace-meta-row";
|
|
import {
|
|
SidebarWorkspaceTrailingContent,
|
|
useSidebarWorkspaceTrailing,
|
|
type SidebarWorkspaceTrailing,
|
|
} from "@/components/sidebar/workspace-trailing";
|
|
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
|
import {
|
|
SidebarWorkspaceContextMenu,
|
|
SidebarWorkspaceMenu,
|
|
} from "@/components/sidebar/sidebar-workspace-menu";
|
|
import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header";
|
|
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
|
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
|
import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pin";
|
|
import { DraggableList, type DraggableRenderItemInfo } from "@/components/draggable-list";
|
|
import type { DraggableListDragHandleProps } from "@/components/draggable-list.types";
|
|
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
|
|
|
// Themed icon wrappers
|
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
|
color: theme.colors.foregroundMuted,
|
|
});
|
|
// One mapping per bucket, resolved through the status-dot producer so a group header and
|
|
// the rows under it cannot disagree about what "failed" looks like.
|
|
const needsInputColorMapping = (theme: Theme) => ({
|
|
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
|
});
|
|
const failedColorMapping = (theme: Theme) => ({
|
|
color: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined,
|
|
});
|
|
const attentionColorMapping = (theme: Theme) => ({
|
|
color: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
|
});
|
|
const runningColorMapping = (theme: Theme) => ({
|
|
color: getStatusDotColor({ theme, bucket: "running" }) ?? undefined,
|
|
});
|
|
|
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
|
const ThemedChevronRight = withUnistyles(ChevronRight);
|
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
|
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
|
const ThemedCircleDot = withUnistyles(CircleDot);
|
|
const ThemedCircleX = withUnistyles(CircleX);
|
|
const EMPTY_SHORTCUT_INDEX = new Map<string, number>();
|
|
|
|
function statusWorkspaceKeyExtractor(workspace: SidebarWorkspaceEntry): string {
|
|
return workspace.workspaceKey;
|
|
}
|
|
|
|
interface StatusWorkspaceListProps {
|
|
groups: SidebarWorkspaceGroup[];
|
|
pinnedWorkspaces: SidebarWorkspaceEntry[];
|
|
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
|
|
shortcutIndexByWorkspaceKey: Map<string, number>;
|
|
showShortcutBadges: boolean;
|
|
onWorkspacePress?: () => void;
|
|
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
|
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
|
onPinnedWorkspaceReorder: (workspaces: SidebarWorkspaceEntry[]) => void;
|
|
listHeaderComponent?: ReactNode;
|
|
/** Swaps the group list for the label filter's empty state. Never the header above it. */
|
|
sidebarFilterEmpty?: boolean;
|
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
|
dragGestureHostPresented?: boolean;
|
|
}
|
|
|
|
export function SidebarStatusWorkspaceList({
|
|
groups,
|
|
pinnedWorkspaces,
|
|
projectIconByProjectViewKey,
|
|
shortcutIndexByWorkspaceKey,
|
|
showShortcutBadges,
|
|
onWorkspacePress,
|
|
hostBadgeByServerId,
|
|
supportsPinningByServerId,
|
|
onToggleWorkspacePin,
|
|
onPinnedWorkspaceReorder,
|
|
listHeaderComponent,
|
|
sidebarFilterEmpty = false,
|
|
parentGestureRef,
|
|
dragGestureHostPresented,
|
|
}: StatusWorkspaceListProps) {
|
|
const collapsedWorkspaceGroupKeys = useSidebarCollapsedSectionsStore(
|
|
(state) => state.collapsedWorkspaceGroupKeys,
|
|
);
|
|
const pinnedCollapsed = useSidebarCollapsedSectionsStore((state) => state.collapsedPinned);
|
|
const togglePinnedCollapsed = useSidebarCollapsedSectionsStore(
|
|
(state) => state.togglePinnedCollapsed,
|
|
);
|
|
const {
|
|
visibleItems: visiblePinnedWorkspaces,
|
|
expanded: pinnedWorkspacesExpanded,
|
|
canToggle: canTogglePinnedWorkspaces,
|
|
toggleExpanded: togglePinnedWorkspacesExpanded,
|
|
} = useLimitedSidebarGroup(pinnedWorkspaces);
|
|
|
|
const statusShortcutIndex = showShortcutBadges
|
|
? shortcutIndexByWorkspaceKey
|
|
: EMPTY_SHORTCUT_INDEX;
|
|
const renderPinnedWorkspace = useCallback(
|
|
({
|
|
item: workspace,
|
|
drag,
|
|
isActive,
|
|
dragHandleProps,
|
|
}: DraggableRenderItemInfo<SidebarWorkspaceEntry>) => (
|
|
<StatusWorkspaceRow
|
|
workspace={workspace}
|
|
{...buildStatusRowProjectPresentation({
|
|
workspace,
|
|
projectIconByProjectViewKey,
|
|
hostBadgeByServerId,
|
|
})}
|
|
inStatusGroup={false}
|
|
shortcutNumber={statusShortcutIndex.get(workspace.workspaceKey) ?? null}
|
|
showShortcutBadge={showShortcutBadges}
|
|
canPin={supportsPinningByServerId.get(workspace.serverId) === true}
|
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
onWorkspacePress={onWorkspacePress}
|
|
drag={drag}
|
|
isDragging={isActive}
|
|
dragHandleProps={dragHandleProps}
|
|
/>
|
|
),
|
|
[
|
|
hostBadgeByServerId,
|
|
onToggleWorkspacePin,
|
|
onWorkspacePress,
|
|
projectIconByProjectViewKey,
|
|
showShortcutBadges,
|
|
statusShortcutIndex,
|
|
supportsPinningByServerId,
|
|
],
|
|
);
|
|
const content = (
|
|
<>
|
|
{pinnedWorkspaces.length > 0 ? (
|
|
<View style={styles.pinnedSection} testID="sidebar-pinned-section">
|
|
<PinnedSectionHeader collapsed={pinnedCollapsed} onToggle={togglePinnedCollapsed} />
|
|
{pinnedCollapsed ? null : (
|
|
<>
|
|
<DraggableList
|
|
testID="sidebar-pinned-list"
|
|
data={visiblePinnedWorkspaces}
|
|
keyExtractor={statusWorkspaceKeyExtractor}
|
|
renderItem={renderPinnedWorkspace}
|
|
onDragEnd={onPinnedWorkspaceReorder}
|
|
scrollEnabled={false}
|
|
useDragHandle
|
|
nestable={platformIsNative}
|
|
simultaneousGestureRef={parentGestureRef}
|
|
gestureHostPresented={dragGestureHostPresented}
|
|
/>
|
|
{canTogglePinnedWorkspaces ? (
|
|
<SidebarGroupToggleRow
|
|
expanded={pinnedWorkspacesExpanded}
|
|
onPress={togglePinnedWorkspacesExpanded}
|
|
testID="sidebar-pinned-show-more"
|
|
/>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</View>
|
|
) : null}
|
|
{listHeaderComponent}
|
|
{sidebarFilterEmpty ? (
|
|
<SidebarFilterEmptyState />
|
|
) : (
|
|
<StatusGroupList
|
|
groups={groups}
|
|
collapsedWorkspaceGroupKeys={collapsedWorkspaceGroupKeys}
|
|
projectIconByProjectViewKey={projectIconByProjectViewKey}
|
|
shortcutIndex={statusShortcutIndex}
|
|
showShortcutBadges={showShortcutBadges}
|
|
onWorkspacePress={onWorkspacePress}
|
|
hostBadgeByServerId={hostBadgeByServerId}
|
|
supportsPinningByServerId={supportsPinningByServerId}
|
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{platformIsNative ? (
|
|
<NestableScrollContainer
|
|
style={styles.list}
|
|
contentContainerStyle={styles.listContent}
|
|
showsVerticalScrollIndicator={false}
|
|
testID="sidebar-status-list-scroll"
|
|
>
|
|
{content}
|
|
</NestableScrollContainer>
|
|
) : (
|
|
<ScrollView
|
|
style={styles.list}
|
|
contentContainerStyle={styles.listContent}
|
|
showsVerticalScrollIndicator={false}
|
|
testID="sidebar-status-list-scroll"
|
|
>
|
|
{content}
|
|
</ScrollView>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function StatusGroupList({
|
|
groups,
|
|
collapsedWorkspaceGroupKeys,
|
|
projectIconByProjectViewKey,
|
|
shortcutIndex,
|
|
showShortcutBadges,
|
|
onWorkspacePress,
|
|
hostBadgeByServerId,
|
|
supportsPinningByServerId,
|
|
onToggleWorkspacePin,
|
|
}: {
|
|
groups: SidebarWorkspaceGroup[];
|
|
collapsedWorkspaceGroupKeys: ReadonlySet<string>;
|
|
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
|
|
shortcutIndex: Map<string, number>;
|
|
showShortcutBadges: boolean;
|
|
onWorkspacePress?: () => void;
|
|
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
|
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
|
}) {
|
|
return (
|
|
<>
|
|
{groups.map((group) => (
|
|
<StatusGroupRows
|
|
key={group.key}
|
|
group={group}
|
|
collapsed={collapsedWorkspaceGroupKeys.has(group.key)}
|
|
projectIconByProjectViewKey={projectIconByProjectViewKey}
|
|
shortcutIndex={shortcutIndex}
|
|
showShortcutBadges={showShortcutBadges}
|
|
onWorkspacePress={onWorkspacePress}
|
|
hostBadgeByServerId={hostBadgeByServerId}
|
|
supportsPinningByServerId={supportsPinningByServerId}
|
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function StatusGroupRows({
|
|
group,
|
|
collapsed,
|
|
projectIconByProjectViewKey,
|
|
shortcutIndex,
|
|
showShortcutBadges,
|
|
onWorkspacePress,
|
|
hostBadgeByServerId,
|
|
supportsPinningByServerId,
|
|
onToggleWorkspacePin,
|
|
}: {
|
|
group: SidebarWorkspaceGroup;
|
|
collapsed: boolean;
|
|
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
|
|
shortcutIndex: Map<string, number>;
|
|
showShortcutBadges: boolean;
|
|
onWorkspacePress?: () => void;
|
|
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
|
supportsPinningByServerId: ReadonlyMap<string, boolean>;
|
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
|
}) {
|
|
const {
|
|
visibleItems: visibleWorkspaces,
|
|
expanded: workspacesExpanded,
|
|
canToggle: canToggleWorkspaces,
|
|
toggleExpanded: toggleWorkspacesExpanded,
|
|
} = useLimitedSidebarGroup(group.rows);
|
|
|
|
return (
|
|
<View style={collapsed ? undefined : styles.statusGroupBlockExpanded}>
|
|
<StatusGroupHeader group={group} collapsed={collapsed} />
|
|
{!collapsed ? (
|
|
<View
|
|
style={styles.statusWorkspaceListContainer}
|
|
testID={`sidebar-status-group-rows-${group.key}`}
|
|
>
|
|
{visibleWorkspaces.map((workspace) => (
|
|
<StatusWorkspaceRow
|
|
key={workspace.workspaceKey}
|
|
workspace={workspace}
|
|
{...buildStatusRowProjectPresentation({
|
|
workspace,
|
|
projectIconByProjectViewKey,
|
|
hostBadgeByServerId,
|
|
})}
|
|
shortcutNumber={shortcutIndex.get(workspace.workspaceKey) ?? null}
|
|
showShortcutBadge={showShortcutBadges}
|
|
canPin={supportsPinningByServerId.get(workspace.serverId) === true}
|
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
onWorkspacePress={onWorkspacePress}
|
|
/>
|
|
))}
|
|
{canToggleWorkspaces ? (
|
|
<SidebarGroupToggleRow
|
|
expanded={workspacesExpanded}
|
|
onPress={toggleWorkspacesExpanded}
|
|
indented
|
|
testID={`sidebar-status-group-show-more-${group.key}`}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface StatusRowProjectPresentation {
|
|
hostBadge: HostBadgeModel | null;
|
|
projectName: string;
|
|
projectIconDataUri: string | null;
|
|
}
|
|
|
|
function buildStatusRowProjectPresentation({
|
|
workspace,
|
|
projectIconByProjectViewKey,
|
|
hostBadgeByServerId,
|
|
}: {
|
|
workspace: SidebarWorkspaceEntry;
|
|
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
|
|
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
|
}): StatusRowProjectPresentation {
|
|
return {
|
|
hostBadge: hostBadgeByServerId.get(workspace.serverId) ?? null,
|
|
projectName: workspace.projectName,
|
|
projectIconDataUri: projectIconByProjectViewKey.get(workspace.projectViewKey) ?? null,
|
|
};
|
|
}
|
|
|
|
function StatusGroupHeader({
|
|
group,
|
|
collapsed,
|
|
}: {
|
|
group: SidebarWorkspaceGroup;
|
|
collapsed: boolean;
|
|
}) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const toggleWorkspaceGroupCollapsed = useSidebarCollapsedSectionsStore(
|
|
(state) => state.toggleWorkspaceGroupCollapsed,
|
|
);
|
|
const handlePress = useCallback(() => {
|
|
toggleWorkspaceGroupCollapsed(group.key);
|
|
}, [group.key, toggleWorkspaceGroupCollapsed]);
|
|
const handleHoverIn = useCallback(() => setIsHovered(true), []);
|
|
const handleHoverOut = useCallback(() => setIsHovered(false), []);
|
|
const rowStyle = useCallback(
|
|
({ pressed }: PressableStateCallbackType) => [
|
|
styles.statusGroupRow,
|
|
isHovered && styles.statusGroupRowHovered,
|
|
pressed && styles.statusGroupRowPressed,
|
|
],
|
|
[isHovered],
|
|
);
|
|
const accessibilityState = useMemo(() => ({ expanded: !collapsed }), [collapsed]);
|
|
|
|
return (
|
|
<View onPointerEnter={handleHoverIn} onPointerLeave={handleHoverOut}>
|
|
<Pressable
|
|
accessibilityRole={platformIsWeb ? undefined : "button"}
|
|
accessibilityLabel={`${group.label} group`}
|
|
accessibilityState={accessibilityState}
|
|
style={rowStyle}
|
|
onPress={handlePress}
|
|
testID={`sidebar-status-group-${group.key}`}
|
|
>
|
|
<View style={styles.statusGroupRowLeft}>
|
|
<View style={styles.statusGroupLeadingVisualSlot}>
|
|
<StatusGroupLeadingVisual
|
|
leading={group.leading}
|
|
collapsed={collapsed}
|
|
showChevron={isHovered}
|
|
/>
|
|
</View>
|
|
<View style={styles.statusGroupTitleGroup}>
|
|
<Text style={styles.statusGroupTitle} numberOfLines={1}>
|
|
{group.label}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function StatusGroupLeadingVisual({
|
|
leading,
|
|
collapsed,
|
|
showChevron,
|
|
}: {
|
|
leading: SidebarWorkspaceGroup["leading"];
|
|
collapsed: boolean;
|
|
showChevron: boolean;
|
|
}) {
|
|
if (!showChevron) {
|
|
return <StatusGroupIcon bucket={leading.bucket} />;
|
|
}
|
|
if (collapsed) {
|
|
return <ThemedChevronRight size={14} uniProps={foregroundMutedColorMapping} />;
|
|
}
|
|
return <ThemedChevronDown size={14} uniProps={foregroundMutedColorMapping} />;
|
|
}
|
|
|
|
function StatusGroupIcon({ bucket }: { bucket: StatusBucket }) {
|
|
switch (bucket) {
|
|
case "needs_input":
|
|
return <ThemedCircleAlert size={14} uniProps={needsInputColorMapping} />;
|
|
case "failed":
|
|
return <ThemedCircleX size={14} uniProps={failedColorMapping} />;
|
|
case "attention":
|
|
return <ThemedCircleCheck size={14} uniProps={attentionColorMapping} />;
|
|
case "running":
|
|
return <ThemedCircleDot size={14} uniProps={runningColorMapping} />;
|
|
case "done":
|
|
return <ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />;
|
|
}
|
|
}
|
|
|
|
const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
|
workspace,
|
|
hostBadge,
|
|
projectName,
|
|
projectIconDataUri,
|
|
shortcutNumber,
|
|
showShortcutBadge,
|
|
canPin,
|
|
onToggleWorkspacePin,
|
|
reserveIdleStatusIndicatorSpace = true,
|
|
inStatusGroup = true,
|
|
onWorkspacePress,
|
|
drag,
|
|
isDragging = false,
|
|
dragHandleProps,
|
|
}: {
|
|
workspace: SidebarWorkspaceEntry;
|
|
hostBadge: HostBadgeModel | null;
|
|
projectName: string;
|
|
projectIconDataUri: string | null;
|
|
shortcutNumber: number | null;
|
|
showShortcutBadge: boolean;
|
|
canPin: boolean;
|
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
|
reserveIdleStatusIndicatorSpace?: boolean;
|
|
/**
|
|
* Whether the row sits under a status header, which is what it indents from. Pinned rows
|
|
* are a flat list under their own header and sit flush.
|
|
*/
|
|
inStatusGroup?: boolean;
|
|
onWorkspacePress?: () => void;
|
|
drag?: () => void;
|
|
isDragging?: boolean;
|
|
dragHandleProps?: DraggableListDragHandleProps;
|
|
}) {
|
|
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
|
const selected =
|
|
activeWorkspaceSelection?.serverId === workspace.serverId &&
|
|
activeWorkspaceSelection?.workspaceId === workspace.workspaceId;
|
|
|
|
const handlePress = useCallback(() => {
|
|
if (!workspace.serverId) return;
|
|
onWorkspacePress?.();
|
|
navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId });
|
|
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
|
|
|
|
return (
|
|
<StatusWorkspaceRowWithMenu
|
|
workspace={workspace}
|
|
hostBadge={hostBadge}
|
|
projectName={projectName}
|
|
projectIconDataUri={projectIconDataUri}
|
|
selected={selected}
|
|
shortcutNumber={shortcutNumber}
|
|
showShortcutBadge={showShortcutBadge}
|
|
canPin={canPin}
|
|
onToggleWorkspacePin={onToggleWorkspacePin}
|
|
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
|
inStatusGroup={inStatusGroup}
|
|
onPress={handlePress}
|
|
drag={drag}
|
|
isDragging={isDragging}
|
|
dragHandleProps={dragHandleProps}
|
|
/>
|
|
);
|
|
});
|
|
|
|
function StatusWorkspaceRowWithMenu({
|
|
workspace,
|
|
hostBadge,
|
|
projectName,
|
|
projectIconDataUri,
|
|
selected,
|
|
shortcutNumber,
|
|
showShortcutBadge,
|
|
canPin,
|
|
onToggleWorkspacePin,
|
|
reserveIdleStatusIndicatorSpace = true,
|
|
inStatusGroup = true,
|
|
onPress,
|
|
drag,
|
|
isDragging = false,
|
|
dragHandleProps,
|
|
}: {
|
|
workspace: SidebarWorkspaceEntry;
|
|
hostBadge: HostBadgeModel | null;
|
|
projectName: string;
|
|
projectIconDataUri: string | null;
|
|
selected: boolean;
|
|
shortcutNumber: number | null;
|
|
showShortcutBadge: boolean;
|
|
canPin: boolean;
|
|
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
|
reserveIdleStatusIndicatorSpace?: boolean;
|
|
/**
|
|
* Whether the row sits under a status header, which is what it indents from. Pinned rows
|
|
* are a flat list under their own header and sit flush.
|
|
*/
|
|
inStatusGroup?: boolean;
|
|
onPress: () => void;
|
|
drag?: () => void;
|
|
isDragging?: boolean;
|
|
dragHandleProps?: DraggableListDragHandleProps;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
const [isHidingWorkspace, setIsHidingWorkspace] = useState(false);
|
|
const [isRenameOpen, setIsRenameOpen] = useState(false);
|
|
const isArchiving = workspace.archivingAt !== null || isHidingWorkspace;
|
|
|
|
const redirectAfterArchive = useCallback(() => {
|
|
redirectIfArchivingActiveWorkspace({
|
|
serverId: workspace.serverId,
|
|
workspaceId: workspace.workspaceId,
|
|
activeWorkspaceSelection: selected
|
|
? { serverId: workspace.serverId, workspaceId: workspace.workspaceId }
|
|
: null,
|
|
});
|
|
}, [selected, workspace]);
|
|
|
|
const archiveController = useWorkspaceArchive({
|
|
serverId: workspace.serverId,
|
|
workspaceId: workspace.workspaceId,
|
|
workspaceKind: workspace.workspaceKind,
|
|
name: workspace.name,
|
|
...toWorktreeArchiveRisk(workspace),
|
|
onArchiveStarted: redirectAfterArchive,
|
|
onSetHiding: setIsHidingWorkspace,
|
|
});
|
|
|
|
const handleArchive = useCallback(() => {
|
|
if (isArchiving) return;
|
|
archiveController.archive();
|
|
}, [archiveController, isArchiving]);
|
|
|
|
const handleCopyPath = useCallback(() => {
|
|
let copyTargetDirectory: string;
|
|
try {
|
|
copyTargetDirectory = requireWorkspaceDirectory({
|
|
workspaceId: workspace.workspaceId,
|
|
workspaceDirectory: workspace.workspaceDirectory,
|
|
});
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "Workspace path not available");
|
|
return;
|
|
}
|
|
void Clipboard.setStringAsync(copyTargetDirectory);
|
|
toast.copied("Path copied");
|
|
}, [toast, workspace.workspaceDirectory, workspace.workspaceId]);
|
|
|
|
const handleCopyBranchName = useCallback(() => {
|
|
void Clipboard.setStringAsync(workspace.name);
|
|
toast.copied("Branch name copied");
|
|
}, [toast, workspace.name]);
|
|
|
|
const renameMutation = useMutation({
|
|
mutationFn: async (title: string) => {
|
|
const client = getHostRuntimeStore().getClient(workspace.serverId);
|
|
if (!client) throw new Error(t("workspace.terminal.hostDisconnected"));
|
|
await client.setWorkspaceTitle(workspace.workspaceId, title.length === 0 ? null : title);
|
|
},
|
|
});
|
|
|
|
const handleOpenRename = useCallback(() => setIsRenameOpen(true), []);
|
|
const handleCloseRename = useCallback(() => setIsRenameOpen(false), []);
|
|
const handleSubmitRename = useCallback(
|
|
async (value: string) => {
|
|
await renameMutation.mutateAsync(value.trim());
|
|
},
|
|
[renameMutation],
|
|
);
|
|
const isPinned = workspace.pinnedAt != null;
|
|
const handleTogglePin = useCallback(() => {
|
|
onToggleWorkspacePin(workspace);
|
|
}, [onToggleWorkspacePin, workspace]);
|
|
const onTogglePin = canPin ? handleTogglePin : undefined;
|
|
|
|
const archiveShortcutKeys = useShortcutKeys("archive-workspace");
|
|
const { hasClearableAttention, clearAttention } = useClearWorkspaceAttention({
|
|
serverId: workspace.serverId,
|
|
workspaceId: workspace.workspaceId,
|
|
});
|
|
const handleMarkAsRead = useCallback(() => {
|
|
void clearAttention().catch((error) => {
|
|
toast.error(error instanceof Error ? error.message : "Failed to mark workspace as read");
|
|
});
|
|
}, [clearAttention, toast]);
|
|
|
|
useKeyboardActionHandler({
|
|
handlerId: `workspace-archive-${workspace.workspaceKey}`,
|
|
actions: ["workspace.archive"],
|
|
enabled: selected && !isArchiving,
|
|
priority: 0,
|
|
handle: () => {
|
|
handleArchive();
|
|
return true;
|
|
},
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<StatusWorkspaceRowInner
|
|
workspace={workspace}
|
|
hostBadge={hostBadge}
|
|
projectName={projectName}
|
|
projectIconDataUri={projectIconDataUri}
|
|
selected={selected}
|
|
shortcutNumber={shortcutNumber}
|
|
showShortcutBadge={showShortcutBadge}
|
|
onPress={onPress}
|
|
isArchiving={isArchiving}
|
|
archiveLabel={t("sidebar.workspace.actions.archive")}
|
|
archiveStatus={isArchiving ? "pending" : "idle"}
|
|
archivePendingLabel={t("sidebar.workspace.actions.archiving")}
|
|
onArchive={handleArchive}
|
|
onCopyBranchName={workspace.projectKind === "git" ? handleCopyBranchName : undefined}
|
|
onCopyPath={handleCopyPath}
|
|
onRename={handleOpenRename}
|
|
onMarkAsRead={hasClearableAttention ? handleMarkAsRead : undefined}
|
|
archiveShortcutKeys={selected ? archiveShortcutKeys : null}
|
|
isPinned={isPinned}
|
|
onTogglePin={onTogglePin}
|
|
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
|
inStatusGroup={inStatusGroup}
|
|
drag={drag}
|
|
isDragging={isDragging}
|
|
dragHandleProps={dragHandleProps}
|
|
/>
|
|
<AdaptiveRenameModal
|
|
visible={isRenameOpen}
|
|
title="Rename workspace"
|
|
initialValue={workspace.title ?? workspace.name}
|
|
placeholder={workspace.name}
|
|
submitLabel="Rename"
|
|
onClose={handleCloseRename}
|
|
onSubmit={handleSubmitRename}
|
|
testID={`sidebar-workspace-rename-modal-${workspace.workspaceKey}`}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface StatusWorkspaceRowInnerProps {
|
|
workspace: SidebarWorkspaceEntry;
|
|
hostBadge: HostBadgeModel | null;
|
|
projectName: string;
|
|
projectIconDataUri: string | null;
|
|
selected: boolean;
|
|
shortcutNumber: number | null;
|
|
showShortcutBadge: boolean;
|
|
onPress: () => void;
|
|
isArchiving: boolean;
|
|
archiveLabel?: string;
|
|
archiveStatus?: "idle" | "pending" | "success";
|
|
archivePendingLabel?: string;
|
|
onArchive?: () => void;
|
|
onCopyBranchName?: () => void;
|
|
onCopyPath?: () => void;
|
|
onRename?: () => void;
|
|
onMarkAsRead?: () => void;
|
|
archiveShortcutKeys?: ShortcutKey[][] | null;
|
|
isPinned?: boolean;
|
|
onTogglePin?: () => void;
|
|
reserveIdleStatusIndicatorSpace?: boolean;
|
|
/** Pinned rows are flat under their own header; status-group rows indent from theirs. */
|
|
inStatusGroup?: boolean;
|
|
drag?: () => void;
|
|
isDragging?: boolean;
|
|
dragHandleProps?: DraggableListDragHandleProps;
|
|
}
|
|
|
|
function StatusWorkspaceRowInner(props: StatusWorkspaceRowInnerProps) {
|
|
if (props.drag) {
|
|
return <DraggableStatusWorkspaceRowInner {...props} drag={props.drag} />;
|
|
}
|
|
return <StatusWorkspaceRowInnerContent {...props} />;
|
|
}
|
|
|
|
function DraggableStatusWorkspaceRowInner(
|
|
props: StatusWorkspaceRowInnerProps & { drag: () => void },
|
|
) {
|
|
const dragInteraction = useLongPressDragInteraction({
|
|
drag: props.drag,
|
|
menuController: null,
|
|
});
|
|
return <StatusWorkspaceRowInnerContent {...props} dragInteraction={dragInteraction} />;
|
|
}
|
|
|
|
function StatusWorkspaceRowInnerContent({
|
|
workspace,
|
|
hostBadge,
|
|
projectName,
|
|
projectIconDataUri,
|
|
selected,
|
|
shortcutNumber,
|
|
showShortcutBadge,
|
|
onPress,
|
|
isArchiving,
|
|
archiveLabel,
|
|
archiveStatus = "idle",
|
|
archivePendingLabel,
|
|
onArchive,
|
|
onCopyBranchName,
|
|
onCopyPath,
|
|
onRename,
|
|
onMarkAsRead,
|
|
archiveShortcutKeys,
|
|
isPinned,
|
|
onTogglePin,
|
|
reserveIdleStatusIndicatorSpace = true,
|
|
inStatusGroup = true,
|
|
isDragging = false,
|
|
dragHandleProps,
|
|
dragInteraction,
|
|
}: StatusWorkspaceRowInnerProps & {
|
|
dragInteraction?: ReturnType<typeof useLongPressDragInteraction>;
|
|
}) {
|
|
const isCompact = useIsCompactFormFactor();
|
|
const isTouchPlatform = platformIsNative || isCompact;
|
|
const [isPressed, setIsPressed] = useState(false);
|
|
const trailing = useSidebarWorkspaceTrailing();
|
|
const {
|
|
role: _dragRole,
|
|
tabIndex: _dragTabIndex,
|
|
"aria-roledescription": _dragRoleDescription,
|
|
...dragAttributes
|
|
} = dragHandleProps?.attributes ?? {};
|
|
|
|
const isDesktop = !isTouchPlatform;
|
|
const serviceSummary = isDesktop ? selectWorkspaceServiceSummary(workspace.scripts) : null;
|
|
|
|
const accessibilityState = useMemo(() => ({ selected }), [selected]);
|
|
const didLongPressRef = dragInteraction?.didLongPressRef;
|
|
const startDragPress = dragInteraction?.handlePressIn;
|
|
const moveDragPress = dragInteraction?.handleTouchMove;
|
|
const endDragPress = dragInteraction?.handlePressOut;
|
|
const handlePress = useCallback(() => {
|
|
if (didLongPressRef?.current) {
|
|
didLongPressRef.current = false;
|
|
return;
|
|
}
|
|
onPress();
|
|
}, [didLongPressRef, onPress]);
|
|
const handlePressIn = useCallback(
|
|
(event: GestureResponderEvent) => {
|
|
setIsPressed(true);
|
|
startDragPress?.(event);
|
|
},
|
|
[startDragPress],
|
|
);
|
|
const handlePressOut = useCallback(() => {
|
|
setIsPressed(false);
|
|
endDragPress?.();
|
|
}, [endDragPress]);
|
|
|
|
return (
|
|
<SidebarWorkspaceRowFrame workspace={workspace} isDragging={isDragging}>
|
|
{({ isHovered, contextMenuOpen, onContextMenuOpenChange, hoverHandlers }) => {
|
|
const showShortcut = showShortcutBadge && shortcutNumber !== null;
|
|
const {
|
|
showTrailing,
|
|
showKebab: showKebabInSlot,
|
|
showScrim,
|
|
renderSlot,
|
|
reserveSlotWidth,
|
|
} = resolveTrailingActionVisibility({
|
|
workspace,
|
|
trailing,
|
|
hasArchiveAction: Boolean(onArchive),
|
|
isHovered,
|
|
isTouchPlatform,
|
|
showShortcut,
|
|
});
|
|
const workspaceRowStyle = getStatusWorkspaceRowStyle({
|
|
isPressed,
|
|
selected,
|
|
isHovered,
|
|
inStatusGroup,
|
|
isDragging,
|
|
});
|
|
const backdrop = getSidebarRowBackdrop({ isDragging, isPressed, selected, isHovered });
|
|
return (
|
|
<View
|
|
{...dragAttributes}
|
|
{...dragHandleProps?.listeners}
|
|
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
|
style={styles.workspaceRowContainer}
|
|
{...hoverHandlers}
|
|
>
|
|
<SidebarWorkspaceContextMenu
|
|
contextMenuOpen={contextMenuOpen}
|
|
onContextMenuOpenChange={onContextMenuOpenChange}
|
|
workspace={workspace}
|
|
leadingProjectName={projectName}
|
|
hostBadgeLabel={hostBadge?.label}
|
|
serviceSummary={serviceSummary}
|
|
workspaceKey={workspace.workspaceKey}
|
|
onCopyPath={onCopyPath}
|
|
onCopyBranchName={onCopyBranchName}
|
|
onRename={onRename}
|
|
onMarkAsRead={onMarkAsRead}
|
|
onArchive={onArchive}
|
|
archiveLabel={archiveLabel}
|
|
archiveStatus={archiveStatus}
|
|
archivePendingLabel={archivePendingLabel}
|
|
archiveShortcutKeys={archiveShortcutKeys}
|
|
isPinned={isPinned}
|
|
onTogglePin={onTogglePin}
|
|
openInFileManagerPath={workspace.workspaceDirectory}
|
|
disabled={isArchiving}
|
|
accessibilityRole="button"
|
|
accessibilityState={accessibilityState}
|
|
style={workspaceRowStyle}
|
|
highlightStyle={styles.workspaceRowPressed}
|
|
onPressIn={handlePressIn}
|
|
onTouchMove={moveDragPress}
|
|
onPressOut={handlePressOut}
|
|
onPress={handlePress}
|
|
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
|
>
|
|
<SidebarWorkspaceRowContent
|
|
workspace={workspace}
|
|
hostBadge={hostBadge}
|
|
leadingProjectName={projectName}
|
|
leadingProjectIconDataUri={projectIconDataUri}
|
|
serviceSummary={serviceSummary}
|
|
backdrop={backdrop}
|
|
isHovered={isHovered}
|
|
isLoading={isArchiving}
|
|
shortcutNumber={shortcutNumber}
|
|
showShortcutBadge={showShortcutBadge}
|
|
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
|
>
|
|
{renderSlot ? (
|
|
<StatusWorkspaceActionSlot
|
|
workspace={workspace}
|
|
backdrop={backdrop}
|
|
trailing={trailing}
|
|
showBase={showTrailing}
|
|
showKebab={showKebabInSlot}
|
|
showScrim={showScrim}
|
|
reserveSlotWidth={reserveSlotWidth}
|
|
isPinned={isPinned}
|
|
onTogglePin={onTogglePin}
|
|
onCopyPath={onCopyPath}
|
|
onCopyBranchName={onCopyBranchName}
|
|
onRename={onRename}
|
|
onMarkAsRead={onMarkAsRead}
|
|
onArchive={onArchive}
|
|
archiveLabel={archiveLabel}
|
|
archiveStatus={archiveStatus}
|
|
archivePendingLabel={archivePendingLabel}
|
|
archiveShortcutKeys={archiveShortcutKeys}
|
|
/>
|
|
) : null}
|
|
</SidebarWorkspaceRowContent>
|
|
</SidebarWorkspaceContextMenu>
|
|
</View>
|
|
);
|
|
}}
|
|
</SidebarWorkspaceRowFrame>
|
|
);
|
|
}
|
|
|
|
function StatusWorkspaceActionSlot({
|
|
workspace,
|
|
backdrop,
|
|
trailing,
|
|
showBase,
|
|
showKebab,
|
|
showScrim,
|
|
reserveSlotWidth,
|
|
isPinned,
|
|
onTogglePin,
|
|
onCopyPath,
|
|
onCopyBranchName,
|
|
onRename,
|
|
onMarkAsRead,
|
|
onArchive,
|
|
archiveLabel,
|
|
archiveStatus,
|
|
archivePendingLabel,
|
|
archiveShortcutKeys,
|
|
}: {
|
|
workspace: SidebarWorkspaceEntry;
|
|
backdrop: SidebarSurfaceBackdrop;
|
|
trailing: SidebarWorkspaceTrailing;
|
|
showBase: boolean;
|
|
showKebab: boolean;
|
|
showScrim: boolean;
|
|
reserveSlotWidth: boolean;
|
|
isPinned?: boolean;
|
|
onTogglePin?: () => void;
|
|
onCopyPath?: () => void;
|
|
onCopyBranchName?: () => void;
|
|
onRename?: () => void;
|
|
onMarkAsRead?: () => void;
|
|
onArchive?: () => void;
|
|
archiveLabel?: string;
|
|
archiveStatus?: "idle" | "pending" | "success";
|
|
archivePendingLabel?: string;
|
|
archiveShortcutKeys?: ShortcutKey[][] | null;
|
|
}) {
|
|
const kebab = useOpenKebabMenuVisibility(showKebab);
|
|
return (
|
|
<SidebarWorkspaceTrailingActionSlot reserveWidth={reserveSlotWidth}>
|
|
<SidebarWorkspaceTrailingActionBase visible={showBase}>
|
|
<SidebarWorkspaceTrailingContent workspace={workspace} trailing={trailing} />
|
|
</SidebarWorkspaceTrailingActionBase>
|
|
<SidebarWorkspaceTrailingActionOverlay
|
|
visible={kebab.showKebab}
|
|
scrimBackdrop={showScrim ? backdrop : undefined}
|
|
>
|
|
{kebab.showKebab && onArchive ? (
|
|
<SidebarWorkspaceMenu
|
|
{...kebab.menuProps}
|
|
workspaceKey={workspace.workspaceKey}
|
|
serverId={workspace.serverId}
|
|
workspaceId={workspace.workspaceId}
|
|
workspaceLabels={workspace.labels}
|
|
onCopyPath={onCopyPath}
|
|
onCopyBranchName={onCopyBranchName}
|
|
onRename={onRename}
|
|
onMarkAsRead={onMarkAsRead}
|
|
onArchive={onArchive}
|
|
archiveLabel={archiveLabel}
|
|
archiveStatus={archiveStatus}
|
|
archivePendingLabel={archivePendingLabel}
|
|
archiveShortcutKeys={archiveShortcutKeys}
|
|
isPinned={isPinned}
|
|
onTogglePin={onTogglePin}
|
|
/>
|
|
) : null}
|
|
</SidebarWorkspaceTrailingActionOverlay>
|
|
</SidebarWorkspaceTrailingActionSlot>
|
|
);
|
|
}
|
|
|
|
function getStatusWorkspaceRowStyle({
|
|
isPressed,
|
|
selected,
|
|
isHovered,
|
|
inStatusGroup,
|
|
isDragging,
|
|
}: {
|
|
isPressed: boolean;
|
|
selected: boolean;
|
|
isHovered: boolean;
|
|
inStatusGroup: boolean;
|
|
isDragging: boolean;
|
|
}) {
|
|
return [
|
|
styles.workspaceRow,
|
|
inStatusGroup && sidebarWorkspaceRowStyles.rowIndented,
|
|
isHovered && styles.workspaceRowHovered,
|
|
selected && styles.sidebarRowSelected,
|
|
isDragging && styles.workspaceRowDragging,
|
|
isPressed && styles.workspaceRowPressed,
|
|
];
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
list: {
|
|
flex: 1,
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: theme.spacing[2],
|
|
// Keep status mode's Pinned/Workspaces boundary identical to project mode.
|
|
paddingTop: 2,
|
|
paddingBottom: theme.spacing[4],
|
|
},
|
|
pinnedSection: {
|
|
marginBottom: theme.spacing[1],
|
|
},
|
|
// Matches `projectBlockExpanded` in sidebar-workspace-list.tsx. See the note there.
|
|
statusGroupBlockExpanded: {
|
|
paddingBottom: theme.spacing[3],
|
|
},
|
|
statusWorkspaceListContainer: {},
|
|
statusGroupRow: {
|
|
minHeight: 36,
|
|
paddingVertical: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[2],
|
|
borderRadius: theme.borderRadius.lg,
|
|
marginBottom: theme.spacing[2],
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: theme.spacing[2],
|
|
userSelect: "none",
|
|
},
|
|
statusGroupRowHovered: {
|
|
backgroundColor: theme.colors.surfaceSidebarHover,
|
|
},
|
|
statusGroupRowPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
statusGroupRowLeft: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
statusGroupLeadingVisualSlot: {
|
|
position: "relative",
|
|
width: theme.iconSize.md,
|
|
height: theme.iconSize.md,
|
|
flexShrink: 0,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
statusGroupTitleGroup: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[1],
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
statusGroupTitle: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: "400",
|
|
minWidth: 0,
|
|
flexShrink: 1,
|
|
},
|
|
workspaceRowContainer: {
|
|
position: "relative",
|
|
},
|
|
workspaceRow: {
|
|
minHeight: 36,
|
|
marginBottom: theme.spacing[0.5],
|
|
paddingVertical: theme.spacing[2],
|
|
paddingLeft: theme.spacing[2],
|
|
paddingRight: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.lg,
|
|
flexDirection: "column",
|
|
alignItems: "stretch",
|
|
justifyContent: "flex-start",
|
|
gap: theme.spacing[1],
|
|
userSelect: "none",
|
|
},
|
|
workspaceRowHovered: {
|
|
backgroundColor: theme.colors.surfaceSidebarHover,
|
|
},
|
|
workspaceRowPressed: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
workspaceRowDragging: {
|
|
backgroundColor: theme.colors.surface2,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
transform: [{ scale: 1.02 }],
|
|
zIndex: 3,
|
|
...theme.shadow.md,
|
|
},
|
|
sidebarRowSelected: {
|
|
backgroundColor: theme.colors.surfaceSidebarSelected,
|
|
},
|
|
}));
|