refactor: strip useUnistyles in favor of withUnistyles + StyleSheet
Replaces broad useUnistyles() subscriptions in composer, workspace, and related components with withUnistyles wrappers and StyleSheet.create so re-renders stay scoped to the tokens actually consumed. Lifts the design scales (spacing, iconSize, fontSize, etc.) to top-level exports in theme.ts so component literals share a single source of truth with the unistyles theme.
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
SquareTerminal,
|
||||
X,
|
||||
} from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { SortableInlineList } from "@/components/sortable-inline-list";
|
||||
import type {
|
||||
DraggableListDragHandleProps,
|
||||
@@ -58,39 +58,53 @@ import {
|
||||
type WorkspaceTabMenuEntry,
|
||||
} from "@/screens/workspace/workspace-tab-menu";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
const DROPDOWN_WIDTH = 220;
|
||||
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
||||
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedX = withUnistyles(X);
|
||||
const ThemedCopy = withUnistyles(Copy);
|
||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
|
||||
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
||||
const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedColumns2 = withUnistyles(Columns2);
|
||||
const ThemedRows2 = withUnistyles(Rows2);
|
||||
|
||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
||||
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
||||
}
|
||||
|
||||
function TabContextMenuItem({
|
||||
entry,
|
||||
iconColor,
|
||||
}: {
|
||||
entry: Extract<WorkspaceTabMenuEntry, { kind: "item" }>;
|
||||
iconColor: string;
|
||||
}) {
|
||||
const leading = useMemo(() => {
|
||||
switch (entry.icon) {
|
||||
case "copy":
|
||||
return <Copy size={16} color={iconColor} />;
|
||||
return <ThemedCopy size={16} uniProps={mutedColorMapping} />;
|
||||
case "rotate-cw":
|
||||
return <RotateCw size={16} color={iconColor} />;
|
||||
return <ThemedRotateCw size={16} uniProps={mutedColorMapping} />;
|
||||
case "arrow-left-to-line":
|
||||
return <ArrowLeftToLine size={16} color={iconColor} />;
|
||||
return <ThemedArrowLeftToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "arrow-right-to-line":
|
||||
return <ArrowRightToLine size={16} color={iconColor} />;
|
||||
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "copy-x":
|
||||
return <CopyX size={16} color={iconColor} />;
|
||||
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
|
||||
case "x":
|
||||
return <X size={16} color={iconColor} />;
|
||||
return <ThemedX size={16} uniProps={mutedColorMapping} />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, [entry.icon, iconColor]);
|
||||
}, [entry.icon]);
|
||||
const trailing = useMemo(
|
||||
() => (entry.hint ? <Text style={styles.menuItemHint}>{entry.hint}</Text> : undefined),
|
||||
[entry.hint],
|
||||
@@ -255,7 +269,6 @@ function TabChip({
|
||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||
dragHandleProps: DraggableListDragHandleProps | undefined;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const { closeButtonTestId, contextMenuTestId, menuEntries } = resolvedTab;
|
||||
const middleClickRef = useMiddleClickClose(
|
||||
useCallback(() => void onCloseTab(tab.tabId), [onCloseTab, tab.tabId]),
|
||||
@@ -392,21 +405,17 @@ function TabChip({
|
||||
>
|
||||
{({ hovered: closeHovered, pressed }) =>
|
||||
isClosingTab ? (
|
||||
<ActivityIndicator
|
||||
<ThemedActivityIndicator
|
||||
size={12}
|
||||
color={
|
||||
closeHovered || pressed
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
uniProps={
|
||||
closeHovered || pressed ? foregroundColorMapping : mutedColorMapping
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<X
|
||||
<ThemedX
|
||||
size={12}
|
||||
color={
|
||||
closeHovered || pressed
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
uniProps={
|
||||
closeHovered || pressed ? foregroundColorMapping : mutedColorMapping
|
||||
}
|
||||
/>
|
||||
)
|
||||
@@ -432,11 +441,7 @@ function TabChip({
|
||||
entry.kind === "separator" ? (
|
||||
<ContextMenuSeparator key={entry.key} />
|
||||
) : (
|
||||
<TabContextMenuItem
|
||||
key={entry.key}
|
||||
entry={entry}
|
||||
iconColor={theme.colors.foregroundMuted}
|
||||
/>
|
||||
<TabContextMenuItem key={entry.key} entry={entry} />
|
||||
),
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
@@ -473,7 +478,6 @@ export function WorkspaceDesktopTabsRow({
|
||||
tabDropPreviewIndex = null,
|
||||
showPaneSplitActions = true,
|
||||
}: WorkspaceDesktopTabsRowProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||
const newTerminalKeys = useShortcutKeys("workspace-terminal-new");
|
||||
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
||||
@@ -499,11 +503,11 @@ export function WorkspaceDesktopTabsRow({
|
||||
tabGap: 0,
|
||||
maxTabWidth: 200,
|
||||
tabIconWidth: 14,
|
||||
tabHorizontalPadding: theme.spacing[3],
|
||||
tabHorizontalPadding: 12,
|
||||
estimatedCharWidth: 7,
|
||||
closeButtonWidth: 22,
|
||||
}),
|
||||
[tabsActionsWidth, theme.spacing],
|
||||
[tabsActionsWidth],
|
||||
);
|
||||
|
||||
const tabLabelLengths = useMemo(
|
||||
@@ -668,7 +672,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
accessibilityLabel="New agent tab"
|
||||
style={newTabActionButtonStyle}
|
||||
>
|
||||
<SquarePen size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<ThemedSquarePen size={14} uniProps={mutedColorMapping} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
@@ -690,7 +694,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
}
|
||||
style={newTerminalActionButtonStyle}
|
||||
>
|
||||
<SquareTerminal size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
@@ -712,7 +716,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
accessibilityLabel="Split pane right"
|
||||
style={newTabActionButtonStyle}
|
||||
>
|
||||
<Columns2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
@@ -730,7 +734,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
accessibilityLabel="Split pane down"
|
||||
style={newTabActionButtonStyle}
|
||||
>
|
||||
<Rows2 size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
<View style={styles.newTabTooltipRow}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useUnistyles } from "react-native-unistyles";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import {
|
||||
Archive,
|
||||
Download,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
|
||||
import { useGitActions } from "@/hooks/use-git-actions";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
interface WorkspaceGitActionsProps {
|
||||
serverId: string;
|
||||
@@ -18,24 +18,31 @@ interface WorkspaceGitActionsProps {
|
||||
hideLabels?: boolean;
|
||||
}
|
||||
|
||||
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
|
||||
const ThemedDownload = withUnistyles(Download);
|
||||
const ThemedUpload = withUnistyles(Upload);
|
||||
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
||||
const ThemedGitMerge = withUnistyles(GitMerge);
|
||||
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
|
||||
const ThemedArchive = withUnistyles(Archive);
|
||||
|
||||
const mutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
|
||||
const ICONS = {
|
||||
commit: <ThemedGitCommitHorizontal size={16} uniProps={mutedColorMapping} />,
|
||||
pull: <ThemedDownload size={16} uniProps={mutedColorMapping} />,
|
||||
push: <ThemedUpload size={16} uniProps={mutedColorMapping} />,
|
||||
viewPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
||||
createPr: <ThemedGitHubIcon size={16} uniProps={mutedColorMapping} />,
|
||||
merge: <ThemedGitMerge size={16} uniProps={mutedColorMapping} />,
|
||||
mergeFromBase: <ThemedRefreshCcw size={16} uniProps={mutedColorMapping} />,
|
||||
archive: <ThemedArchive size={16} uniProps={mutedColorMapping} />,
|
||||
};
|
||||
|
||||
export function WorkspaceGitActions({ serverId, cwd, hideLabels }: WorkspaceGitActionsProps) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
const icons = useMemo(
|
||||
() => ({
|
||||
commit: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
|
||||
pull: <Download size={16} color={theme.colors.foregroundMuted} />,
|
||||
push: <Upload size={16} color={theme.colors.foregroundMuted} />,
|
||||
viewPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
||||
createPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
||||
merge: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
|
||||
mergeFromBase: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
|
||||
archive: <Archive size={16} color={theme.colors.foregroundMuted} />,
|
||||
}),
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
|
||||
const { gitActions, isGit } = useGitActions({ serverId, cwd, icons });
|
||||
const { gitActions, isGit } = useGitActions({ serverId, cwd, icons: ICONS });
|
||||
|
||||
if (!isGit) {
|
||||
return null;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "react-native";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Check, ChevronDown } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import type { EditorTargetDescriptorPayload, EditorTargetId } from "@server/shared/messages";
|
||||
import { EditorAppIcon } from "@/components/icons/editor-app-icons";
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-
|
||||
import { resolvePreferredEditorId, usePreferredEditor } from "@/hooks/use-preferred-editor";
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
interface WorkspaceOpenInEditorButtonProps {
|
||||
serverId: string;
|
||||
@@ -33,18 +34,25 @@ interface EditorMenuItemProps {
|
||||
editor: EditorTargetDescriptorPayload;
|
||||
isPreferred: boolean;
|
||||
onOpen: (editorId: EditorTargetId) => void;
|
||||
foregroundMuted: string;
|
||||
}
|
||||
|
||||
function EditorMenuItem({ editor, isPreferred, onOpen, foregroundMuted }: EditorMenuItemProps) {
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedEditorAppIcon = withUnistyles(EditorAppIcon);
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedCheckIcon = withUnistyles(Check);
|
||||
|
||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
function EditorMenuItem({ editor, isPreferred, onOpen }: EditorMenuItemProps) {
|
||||
const handleSelect = useCallback(() => onOpen(editor.id), [onOpen, editor.id]);
|
||||
const leading = useMemo(
|
||||
() => <EditorAppIcon editorId={editor.id} size={16} color={foregroundMuted} />,
|
||||
[editor.id, foregroundMuted],
|
||||
() => <ThemedEditorAppIcon editorId={editor.id} size={16} uniProps={mutedColorMapping} />,
|
||||
[editor.id],
|
||||
);
|
||||
const trailing = useMemo(
|
||||
() => (isPreferred ? <Check size={16} color={foregroundMuted} /> : undefined),
|
||||
[isPreferred, foregroundMuted],
|
||||
() => (isPreferred ? <ThemedCheckIcon size={16} uniProps={mutedColorMapping} /> : undefined),
|
||||
[isPreferred],
|
||||
);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
@@ -63,7 +71,6 @@ export function WorkspaceOpenInEditorButton({
|
||||
cwd,
|
||||
hideLabels,
|
||||
}: WorkspaceOpenInEditorButtonProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
@@ -174,17 +181,17 @@ export function WorkspaceOpenInEditorButton({
|
||||
accessibilityLabel={`Open workspace in ${primaryOption.label}`}
|
||||
>
|
||||
{openMutation.isPending ? (
|
||||
<ActivityIndicator
|
||||
<ThemedActivityIndicator
|
||||
size="small"
|
||||
color={theme.colors.foreground}
|
||||
uniProps={foregroundColorMapping}
|
||||
style={styles.splitButtonSpinnerOnly}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.splitButtonContent}>
|
||||
<EditorAppIcon
|
||||
<ThemedEditorAppIcon
|
||||
editorId={primaryOption.id}
|
||||
size={16}
|
||||
color={theme.colors.foregroundMuted}
|
||||
uniProps={mutedColorMapping}
|
||||
/>
|
||||
{!hideLabels && <Text style={styles.splitButtonText}>Open</Text>}
|
||||
</View>
|
||||
@@ -198,7 +205,7 @@ export function WorkspaceOpenInEditorButton({
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Choose editor"
|
||||
>
|
||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||
<ThemedChevronDown size={16} uniProps={mutedColorMapping} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
@@ -212,7 +219,6 @@ export function WorkspaceOpenInEditorButton({
|
||||
editor={editor}
|
||||
isPreferred={editor.id === effectivePreferredEditorId}
|
||||
onOpen={handleOpenEditor}
|
||||
foregroundMuted={theme.colors.foregroundMuted}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
} from "lucide-react-native";
|
||||
import { GestureDetector } from "react-native-gesture-handler";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import invariant from "tiny-invariant";
|
||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
||||
@@ -135,6 +136,32 @@ const EMPTY_UI_TABS: WorkspaceTab[] = [];
|
||||
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
||||
const EMPTY_SET = new Set<string>();
|
||||
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedEllipsis = withUnistyles(Ellipsis);
|
||||
const ThemedEllipsisVertical = withUnistyles(EllipsisVertical);
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedCopy = withUnistyles(Copy);
|
||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
|
||||
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
|
||||
const ThemedCopyX = withUnistyles(CopyX);
|
||||
const ThemedX = withUnistyles(X);
|
||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedSettings = withUnistyles(Settings);
|
||||
const ThemedPanelRight = withUnistyles(PanelRight);
|
||||
const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon);
|
||||
|
||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 };
|
||||
|
||||
const MENU_NEW_AGENT_ICON = <ThemedSquarePen size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_NEW_TERMINAL_ICON = <ThemedSquareTerminal size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_COPY_ICON = <ThemedCopy size={16} uniProps={mutedColorMapping} />;
|
||||
const MENU_SETTINGS_ICON = <ThemedSettings size={16} uniProps={mutedColorMapping} />;
|
||||
|
||||
interface WorkspaceScreenProps {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
@@ -298,14 +325,10 @@ function MobileTabTrailingAccessory({
|
||||
menuTestIDBase,
|
||||
presentationLabel,
|
||||
menuEntries,
|
||||
iconColor,
|
||||
iconSizeSm,
|
||||
}: {
|
||||
menuTestIDBase: string;
|
||||
presentationLabel: string;
|
||||
menuEntries: WorkspaceTabMenuEntry[];
|
||||
iconColor: string;
|
||||
iconSizeSm: number;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -316,14 +339,14 @@ function MobileTabTrailingAccessory({
|
||||
hitSlop={8}
|
||||
style={mobileTabMenuTriggerStyle}
|
||||
>
|
||||
<Ellipsis size={iconSizeSm} color={iconColor} />
|
||||
<ThemedEllipsis size={14} uniProps={mutedColorMapping} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" width={220} testID={menuTestIDBase}>
|
||||
{menuEntries.map((entry) =>
|
||||
entry.kind === "separator" ? (
|
||||
<DropdownMenuSeparator key={entry.key} />
|
||||
) : (
|
||||
<MobileTabDropdownMenuItem key={entry.key} entry={entry} iconColor={iconColor} />
|
||||
<MobileTabDropdownMenuItem key={entry.key} entry={entry} />
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
@@ -333,29 +356,27 @@ function MobileTabTrailingAccessory({
|
||||
|
||||
function MobileTabDropdownMenuItem({
|
||||
entry,
|
||||
iconColor,
|
||||
}: {
|
||||
entry: Extract<WorkspaceTabMenuEntry, { kind: "item" }>;
|
||||
iconColor: string;
|
||||
}) {
|
||||
const leading = useMemo(() => {
|
||||
switch (entry.icon) {
|
||||
case "copy":
|
||||
return <Copy size={16} color={iconColor} />;
|
||||
return <ThemedCopy size={16} uniProps={mutedColorMapping} />;
|
||||
case "rotate-cw":
|
||||
return <RotateCw size={16} color={iconColor} />;
|
||||
return <ThemedRotateCw size={16} uniProps={mutedColorMapping} />;
|
||||
case "arrow-left-to-line":
|
||||
return <ArrowLeftToLine size={16} color={iconColor} />;
|
||||
return <ThemedArrowLeftToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "arrow-right-to-line":
|
||||
return <ArrowRightToLine size={16} color={iconColor} />;
|
||||
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
|
||||
case "copy-x":
|
||||
return <CopyX size={16} color={iconColor} />;
|
||||
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
|
||||
case "x":
|
||||
return <X size={16} color={iconColor} />;
|
||||
return <ThemedX size={16} uniProps={mutedColorMapping} />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, [entry.icon, iconColor]);
|
||||
}, [entry.icon]);
|
||||
const trailing = useMemo(
|
||||
() => (entry.hint ? <Text style={styles.menuItemHint}>{entry.hint}</Text> : undefined),
|
||||
[entry.hint],
|
||||
@@ -408,7 +429,6 @@ function MobileWorkspaceTabOption({
|
||||
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const menuTestIDBase = `workspace-tab-menu-${tab.key}`;
|
||||
const menuEntries = buildWorkspaceTabMenuEntries({
|
||||
surface: "mobile",
|
||||
@@ -425,9 +445,6 @@ function MobileWorkspaceTabOption({
|
||||
onCloseOtherTabs,
|
||||
});
|
||||
|
||||
const iconColor = theme.colors.foregroundMuted;
|
||||
const iconSizeSm = theme.iconSize.sm;
|
||||
|
||||
const fallbackLabel = getFallbackTabOptionLabel(tab);
|
||||
const trailingAccessory = useMemo(
|
||||
() => (
|
||||
@@ -435,11 +452,9 @@ function MobileWorkspaceTabOption({
|
||||
menuTestIDBase={menuTestIDBase}
|
||||
presentationLabel={fallbackLabel}
|
||||
menuEntries={menuEntries}
|
||||
iconColor={iconColor}
|
||||
iconSizeSm={iconSizeSm}
|
||||
/>
|
||||
),
|
||||
[menuTestIDBase, fallbackLabel, menuEntries, iconColor, iconSizeSm],
|
||||
[menuTestIDBase, fallbackLabel, menuEntries],
|
||||
);
|
||||
|
||||
const renderPresentation = useCallback(
|
||||
@@ -483,7 +498,6 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
||||
onCloseTabsBelow,
|
||||
onCloseOtherTabs,
|
||||
}: MobileWorkspaceTabSwitcherProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const anchorRef = useRef<View>(null);
|
||||
const tabIndexByKey = useMemo(() => {
|
||||
@@ -572,7 +586,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
/>
|
||||
</View>
|
||||
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
|
||||
</Pressable>
|
||||
|
||||
<Combobox
|
||||
@@ -730,14 +744,9 @@ function WorkspaceHeaderMenuTriggerIcon({
|
||||
open: boolean;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const Icon = isMobile ? EllipsisVertical : Ellipsis;
|
||||
return (
|
||||
<Icon
|
||||
size={theme.iconSize.md}
|
||||
color={hovered || open ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||
/>
|
||||
);
|
||||
const Icon = isMobile ? ThemedEllipsisVertical : ThemedEllipsis;
|
||||
const colorMapping = hovered || open ? foregroundColorMapping : mutedColorMapping;
|
||||
return <Icon size={16} uniProps={colorMapping} />;
|
||||
}
|
||||
|
||||
function WorkspaceHeaderMenu({
|
||||
@@ -939,7 +948,6 @@ interface RenderWorkspaceContentInput {
|
||||
paneId: string | null;
|
||||
tab: WorkspaceTabDescriptor;
|
||||
}) => WorkspacePaneContentModel;
|
||||
foregroundMutedColor: string;
|
||||
}
|
||||
|
||||
function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.ReactNode {
|
||||
@@ -953,13 +961,12 @@ function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.React
|
||||
isRouteFocused,
|
||||
focusedPaneId,
|
||||
buildMobilePaneContentModel,
|
||||
foregroundMutedColor,
|
||||
} = input;
|
||||
|
||||
if (showMissingWorkspaceDescriptor) {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<ActivityIndicator color={foregroundMutedColor} />
|
||||
<ThemedActivityIndicator uniProps={mutedColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -975,7 +982,7 @@ function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.React
|
||||
if (!activeTabDescriptor && !hasHydratedAgents) {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<ActivityIndicator color={foregroundMutedColor} />
|
||||
<ThemedActivityIndicator uniProps={mutedColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1115,9 +1122,7 @@ function WorkspaceScreenContent({
|
||||
workspaceId,
|
||||
isRouteFocused,
|
||||
}: WorkspaceScreenContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const _insets = useSafeAreaInsets();
|
||||
const mainBackgroundColor = theme.colors.surfaceWorkspace;
|
||||
const toast = useToast();
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||
@@ -2562,7 +2567,6 @@ function WorkspaceScreenContent({
|
||||
isRouteFocused,
|
||||
focusedPaneId,
|
||||
buildMobilePaneContentModel,
|
||||
foregroundMutedColor: theme.colors.foregroundMuted,
|
||||
});
|
||||
|
||||
const buildDesktopPaneContentModel = useCallback(
|
||||
@@ -2659,27 +2663,12 @@ function WorkspaceScreenContent({
|
||||
);
|
||||
}, []);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, { backgroundColor: mainBackgroundColor }],
|
||||
[mainBackgroundColor],
|
||||
);
|
||||
const containerStyle = containerWithWorkspaceBackgroundStyle;
|
||||
|
||||
const menuNewAgentIcon = useMemo(
|
||||
() => <SquarePen size={16} color={theme.colors.foregroundMuted} />,
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
const menuNewTerminalIcon = useMemo(
|
||||
() => <SquareTerminal size={16} color={theme.colors.foregroundMuted} />,
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
const menuCopyIcon = useMemo(
|
||||
() => <Copy size={16} color={theme.colors.foregroundMuted} />,
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
const menuSettingsIcon = useMemo(
|
||||
() => <Settings size={16} color={theme.colors.foregroundMuted} />,
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
const menuNewAgentIcon = MENU_NEW_AGENT_ICON;
|
||||
const menuNewTerminalIcon = MENU_NEW_TERMINAL_ICON;
|
||||
const menuCopyIcon = MENU_COPY_ICON;
|
||||
const menuSettingsIcon = MENU_SETTINGS_ICON;
|
||||
|
||||
const headerRight = useMemo(
|
||||
() => (
|
||||
@@ -2721,12 +2710,10 @@ function WorkspaceScreenContent({
|
||||
>
|
||||
{({ hovered, pressed }) => {
|
||||
const active = isExplorerOpen || hovered || pressed;
|
||||
const iconColor = active
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted;
|
||||
const colorMapping = active ? foregroundColorMapping : mutedColorMapping;
|
||||
return (
|
||||
<>
|
||||
<SourceControlPanelIcon size={theme.iconSize.md} color={iconColor} />
|
||||
<ThemedSourceControlPanelIcon size={16} uniProps={colorMapping} />
|
||||
{workspaceDescriptor?.diffStat ? (
|
||||
<DiffStat
|
||||
additions={workspaceDescriptor.diffStat.additions}
|
||||
@@ -2766,9 +2753,9 @@ function WorkspaceScreenContent({
|
||||
accessibilityState={explorerToggleAccessibilityState}
|
||||
>
|
||||
{({ hovered }) => {
|
||||
const color =
|
||||
isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
return <PanelRight size={theme.iconSize.md} color={color} />;
|
||||
const colorMapping =
|
||||
isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping;
|
||||
return <ThemedPanelRight size={16} uniProps={colorMapping} />;
|
||||
}}
|
||||
</HeaderToggleButton>
|
||||
) : null}
|
||||
@@ -2786,12 +2773,16 @@ function WorkspaceScreenContent({
|
||||
accessibilityState={explorerToggleAccessibilityState}
|
||||
>
|
||||
{({ hovered }) => {
|
||||
const color =
|
||||
isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
const colorMapping =
|
||||
isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping;
|
||||
return isGitCheckout ? (
|
||||
<SourceControlPanelIcon size={theme.iconSize.lg} color={color} strokeWidth={1.5} />
|
||||
<ThemedSourceControlPanelIcon
|
||||
size={20}
|
||||
uniProps={colorMapping}
|
||||
{...sourceControlPanelStrokeWidth15}
|
||||
/>
|
||||
) : (
|
||||
<PanelRight size={theme.iconSize.lg} color={color} />
|
||||
<ThemedPanelRight size={20} uniProps={colorMapping} />
|
||||
);
|
||||
}}
|
||||
</HeaderToggleButton>
|
||||
@@ -2812,10 +2803,6 @@ function WorkspaceScreenContent({
|
||||
isExplorerOpen,
|
||||
explorerToggleAccessibilityState,
|
||||
explorerToggleStyle,
|
||||
theme.colors.foreground,
|
||||
theme.colors.foregroundMuted,
|
||||
theme.iconSize.md,
|
||||
theme.iconSize.lg,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3039,6 +3026,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
containerWorkspaceBackground: {
|
||||
backgroundColor: theme.colors.surfaceWorkspace,
|
||||
},
|
||||
threePaneRow: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
@@ -3333,4 +3323,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const containerWithWorkspaceBackgroundStyle = [
|
||||
styles.container,
|
||||
styles.containerWorkspaceBackground,
|
||||
];
|
||||
|
||||
const EXPLORER_TOGGLE_KEYS: ShortcutKey[] = ["mod", "E"];
|
||||
|
||||
@@ -43,7 +43,17 @@ vi.mock("react-native-unistyles", () => ({
|
||||
StyleSheet: {
|
||||
create: (factory: unknown) => (typeof factory === "function" ? factory(theme) : factory),
|
||||
},
|
||||
useUnistyles: () => ({ theme }),
|
||||
withUnistyles:
|
||||
(Component: React.ComponentType<Record<string, unknown>>) =>
|
||||
({
|
||||
uniProps,
|
||||
...rest
|
||||
}: {
|
||||
uniProps?: (theme: unknown) => Record<string, unknown>;
|
||||
} & Record<string, unknown>) => {
|
||||
const themed = uniProps ? uniProps(theme) : {};
|
||||
return React.createElement(Component, { ...rest, ...themed });
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/constants/platform", () => ({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GestureResponderEvent } from "react-native";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ChevronDown, ExternalLink, Globe, Play, SquareTerminal } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
@@ -17,6 +17,7 @@ import { useToast } from "@/contexts/toast-context";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import { resolveWorkspaceScriptLink } from "@/utils/workspace-script-links";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
type ScriptActionIcon = "start" | "view";
|
||||
|
||||
@@ -39,28 +40,48 @@ interface ScriptActionButtonProps {
|
||||
testID: string;
|
||||
}
|
||||
|
||||
const ThemedPlay = withUnistyles(Play);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
const ThemedGlobe = withUnistyles(Globe);
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||
|
||||
const foregroundColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
});
|
||||
const mutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
const blueColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.palette.blue[500],
|
||||
});
|
||||
const greenColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.palette.green[500],
|
||||
});
|
||||
const redColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.palette.red[500],
|
||||
});
|
||||
const playFillTransparent = { fill: "transparent" };
|
||||
|
||||
interface ScriptActionButtonChildrenProps {
|
||||
hovered?: boolean;
|
||||
icon: ScriptActionIcon;
|
||||
label: string;
|
||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||
}
|
||||
|
||||
function ScriptActionButtonChildren({
|
||||
hovered,
|
||||
icon,
|
||||
label,
|
||||
theme,
|
||||
}: ScriptActionButtonChildrenProps): ReactElement {
|
||||
const color = hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
const iconProps = { size: 10, color };
|
||||
const colorMapping = hovered ? foregroundColorMapping : mutedColorMapping;
|
||||
const iconElement =
|
||||
icon === "view" ? (
|
||||
<SquareTerminal {...iconProps} />
|
||||
<ThemedSquareTerminal size={10} uniProps={colorMapping} />
|
||||
) : (
|
||||
<Play {...iconProps} fill="transparent" />
|
||||
<ThemedPlay size={10} uniProps={colorMapping} {...playFillTransparent} />
|
||||
);
|
||||
const labelStyle = useMemo(() => [styles.actionButtonLabel, { color }], [color]);
|
||||
const labelStyle = hovered ? actionButtonLabelHoveredStyle : styles.actionButtonLabel;
|
||||
return (
|
||||
<>
|
||||
{iconElement}
|
||||
@@ -77,8 +98,6 @@ function ScriptActionButton({
|
||||
onPress,
|
||||
testID,
|
||||
}: ScriptActionButtonProps): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
const handlePress = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -89,9 +108,9 @@ function ScriptActionButton({
|
||||
|
||||
const renderChildren = useCallback(
|
||||
({ hovered }: { hovered?: boolean }) => (
|
||||
<ScriptActionButtonChildren hovered={hovered} icon={icon} label={label} theme={theme} />
|
||||
<ScriptActionButtonChildren hovered={hovered} icon={icon} label={label} />
|
||||
),
|
||||
[icon, label, theme],
|
||||
[icon, label],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -123,32 +142,26 @@ interface HostLinkChildrenProps {
|
||||
hovered?: boolean;
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||
}
|
||||
|
||||
function HostLinkChildren({
|
||||
hovered,
|
||||
disabled,
|
||||
label,
|
||||
theme,
|
||||
}: HostLinkChildrenProps): ReactElement {
|
||||
function HostLinkChildren({ hovered, disabled, label }: HostLinkChildrenProps): ReactElement {
|
||||
const showIcon = !disabled && (hovered || isNative);
|
||||
const color = hovered && !disabled ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
const hostLabelStyle = useMemo(() => [styles.hostLabel, { color }], [color]);
|
||||
const isActive = Boolean(hovered) && !disabled;
|
||||
const colorMapping = isActive ? foregroundColorMapping : mutedColorMapping;
|
||||
const hostLabelStyle = isActive ? hostLabelActiveStyle : styles.hostLabel;
|
||||
return (
|
||||
<>
|
||||
<Text style={hostLabelStyle} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<View style={styles.hostIconSlot}>
|
||||
{showIcon ? <ExternalLink size={10} color={color} /> : null}
|
||||
{showIcon ? <ThemedExternalLink size={10} uniProps={colorMapping} /> : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HostLinkRow({ label, url, scriptName }: HostLinkProps): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const disabled = !url;
|
||||
|
||||
const handlePress = useCallback(
|
||||
@@ -161,9 +174,9 @@ function HostLinkRow({ label, url, scriptName }: HostLinkProps): ReactElement {
|
||||
|
||||
const renderChildren = useCallback(
|
||||
({ hovered }: { hovered?: boolean }) => (
|
||||
<HostLinkChildren hovered={hovered} disabled={disabled} label={label} theme={theme} />
|
||||
<HostLinkChildren hovered={hovered} disabled={disabled} label={label} />
|
||||
),
|
||||
[disabled, label, theme],
|
||||
[disabled, label],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -181,9 +194,7 @@ function HostLinkRow({ label, url, scriptName }: HostLinkProps): ReactElement {
|
||||
}
|
||||
|
||||
function ExitCodeBadge({ code }: { code: number }): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const color = code === 0 ? theme.colors.foregroundMuted : theme.colors.palette.red[300];
|
||||
const exitTextStyle = useMemo(() => [styles.exitBadgeText, { color }], [color]);
|
||||
const exitTextStyle = code === 0 ? styles.exitBadgeText : exitBadgeTextErrorStyle;
|
||||
return (
|
||||
<View style={styles.exitBadge}>
|
||||
<Text style={exitTextStyle}>exit {code}</Text>
|
||||
@@ -206,26 +217,24 @@ interface ScriptRowProps {
|
||||
: null
|
||||
: null;
|
||||
isStartPending: boolean;
|
||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||
onStartScript: (scriptName: string) => void;
|
||||
onViewTerminal?: (terminalId: string) => void;
|
||||
}
|
||||
|
||||
function resolveScriptIconColor(args: {
|
||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||
function resolveScriptIconColorMapping(args: {
|
||||
script: WorkspaceDescriptor["scripts"][number];
|
||||
isService: boolean;
|
||||
isRunning: boolean;
|
||||
}): string {
|
||||
const { theme, script, isService, isRunning } = args;
|
||||
}): (theme: Theme) => { color: string } {
|
||||
const { script, isService, isRunning } = args;
|
||||
if (isService) {
|
||||
if (isRunning && script.health === "healthy") return theme.colors.palette.green[500];
|
||||
if (isRunning && script.health === "unhealthy") return theme.colors.palette.red[500];
|
||||
if (isRunning) return theme.colors.palette.blue[500];
|
||||
return theme.colors.foregroundMuted;
|
||||
if (isRunning && script.health === "healthy") return greenColorMapping;
|
||||
if (isRunning && script.health === "unhealthy") return redColorMapping;
|
||||
if (isRunning) return blueColorMapping;
|
||||
return mutedColorMapping;
|
||||
}
|
||||
if (isRunning) return theme.colors.palette.blue[500];
|
||||
return theme.colors.foregroundMuted;
|
||||
if (isRunning) return blueColorMapping;
|
||||
return mutedColorMapping;
|
||||
}
|
||||
|
||||
function ScriptRow({
|
||||
@@ -233,7 +242,6 @@ function ScriptRow({
|
||||
liveTerminalIdSet,
|
||||
activeConnection,
|
||||
isStartPending,
|
||||
theme,
|
||||
onStartScript,
|
||||
onViewTerminal,
|
||||
}: ScriptRowProps): ReactElement {
|
||||
@@ -268,9 +276,8 @@ function ScriptRow({
|
||||
}
|
||||
}
|
||||
|
||||
const iconColor = resolveScriptIconColor({ theme, script, isService, isRunning });
|
||||
|
||||
const ScriptIcon = isService ? Globe : SquareTerminal;
|
||||
const iconColorMapping = resolveScriptIconColorMapping({ script, isService, isRunning });
|
||||
const ScriptIcon = isService ? ThemedGlobe : ThemedSquareTerminal;
|
||||
const showExitBadge = !isRunning && exitCode !== null;
|
||||
|
||||
const handleView = useCallback(() => {
|
||||
@@ -282,13 +289,8 @@ function ScriptRow({
|
||||
}, [onStartScript, script.scriptName]);
|
||||
|
||||
const scriptNameStyle = useMemo(
|
||||
() => [
|
||||
styles.scriptName,
|
||||
{
|
||||
color: isRunning ? theme.colors.foreground : theme.colors.foregroundMuted,
|
||||
},
|
||||
],
|
||||
[isRunning, theme.colors.foreground, theme.colors.foregroundMuted],
|
||||
() => (isRunning ? scriptNameActiveStyle : styles.scriptName),
|
||||
[isRunning],
|
||||
);
|
||||
|
||||
let primaryAction: ReactElement | null = null;
|
||||
@@ -322,7 +324,7 @@ function ScriptRow({
|
||||
style={styles.scriptItem}
|
||||
>
|
||||
<View style={styles.scriptHeader}>
|
||||
<ScriptIcon size={14} color={iconColor} style={styles.scriptIcon} />
|
||||
<ScriptIcon size={14} uniProps={iconColorMapping} style={styles.scriptIcon} />
|
||||
<Text style={scriptNameStyle} numberOfLines={1}>
|
||||
{script.scriptName}
|
||||
</Text>
|
||||
@@ -355,7 +357,6 @@ export function WorkspaceScriptsButton({
|
||||
onViewTerminal,
|
||||
hideLabels,
|
||||
}: WorkspaceScriptsButtonProps): ReactElement | null {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||
const activeConnection = useHostRuntimeSnapshot(serverId)?.activeConnection ?? null;
|
||||
@@ -402,6 +403,7 @@ export function WorkspaceScriptsButton({
|
||||
}
|
||||
|
||||
const hasAnyRunning = scripts.some((s) => s.lifecycle === "running");
|
||||
const triggerPlayMapping = hasAnyRunning ? blueColorMapping : mutedColorMapping;
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
@@ -414,15 +416,9 @@ export function WorkspaceScriptsButton({
|
||||
accessibilityLabel="Workspace scripts"
|
||||
>
|
||||
<View style={styles.splitButtonContent}>
|
||||
<Play
|
||||
size={14}
|
||||
color={
|
||||
hasAnyRunning ? theme.colors.palette.blue[500] : theme.colors.foregroundMuted
|
||||
}
|
||||
fill="transparent"
|
||||
/>
|
||||
<ThemedPlay size={14} uniProps={triggerPlayMapping} {...playFillTransparent} />
|
||||
{!hideLabels && <Text style={styles.splitButtonText}>Scripts</Text>}
|
||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
|
||||
</View>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -440,7 +436,6 @@ export function WorkspaceScriptsButton({
|
||||
liveTerminalIdSet={liveTerminalIdSet}
|
||||
activeConnection={activeConnection}
|
||||
isStartPending={startScriptMutation.isPending}
|
||||
theme={theme}
|
||||
onStartScript={handleStartScript}
|
||||
onViewTerminal={onViewTerminal}
|
||||
/>
|
||||
@@ -511,6 +506,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
lineHeight: 18,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
scriptNameActive: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
@@ -531,6 +530,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flexShrink: 1,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 14,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
hostLabelActive: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
hostIconSlot: {
|
||||
width: 10,
|
||||
@@ -549,6 +552,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: 10,
|
||||
lineHeight: 12,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
exitBadgeTextError: {
|
||||
color: theme.colors.palette.red[300],
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: "row",
|
||||
@@ -558,5 +565,14 @@ const styles = StyleSheet.create((theme) => ({
|
||||
actionButtonLabel: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
actionButtonLabelHovered: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
}));
|
||||
|
||||
const actionButtonLabelHoveredStyle = [styles.actionButtonLabel, styles.actionButtonLabelHovered];
|
||||
const hostLabelActiveStyle = [styles.hostLabel, styles.hostLabelActive];
|
||||
const scriptNameActiveStyle = [styles.scriptName, styles.scriptNameActive];
|
||||
const exitBadgeTextErrorStyle = [styles.exitBadgeText, styles.exitBadgeTextError];
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import { Check } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import invariant from "tiny-invariant";
|
||||
import { SyncedLoader } from "@/components/synced-loader";
|
||||
import { ensurePanelsRegistered } from "@/panels/register-panels";
|
||||
import { getPanelRegistration } from "@/panels/panel-registry";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
||||
import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
export interface WorkspaceTabPresentation {
|
||||
key: string;
|
||||
@@ -103,22 +104,22 @@ interface WorkspaceTabIconProps {
|
||||
statusDotBorderColor?: string;
|
||||
}
|
||||
|
||||
const ThemedCheckIcon = withUnistyles(Check);
|
||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
export function WorkspaceTabIcon({
|
||||
presentation,
|
||||
active = false,
|
||||
size = 14,
|
||||
statusDotBorderColor,
|
||||
}: WorkspaceTabIconProps): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const iconColor = active ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
const statusDotColor =
|
||||
presentation.statusBucket === null
|
||||
? null
|
||||
: getStatusDotColor({
|
||||
theme,
|
||||
bucket: presentation.statusBucket,
|
||||
showDoneAsInactive: false,
|
||||
});
|
||||
const iconColor = active ? styles.iconActive.color : styles.iconInactive.color;
|
||||
const bucket = presentation.statusBucket;
|
||||
let statusDotColor: string | null = null;
|
||||
if (bucket === "needs_input") statusDotColor = styles.statusDotNeedsInput.color;
|
||||
else if (bucket === "failed") statusDotColor = styles.statusDotFailed.color;
|
||||
else if (bucket === "running") statusDotColor = styles.statusDotRunning.color;
|
||||
else if (bucket === "attention") statusDotColor = styles.statusDotAttention.color;
|
||||
const statusDotSize = isEmphasizedStatusDotBucket(presentation.statusBucket)
|
||||
? EMPHASIZED_STATUS_DOT_SIZE
|
||||
: DEFAULT_STATUS_DOT_SIZE;
|
||||
@@ -139,27 +140,20 @@ export function WorkspaceTabIcon({
|
||||
styles.statusDot,
|
||||
{
|
||||
backgroundColor: statusDotColor ?? undefined,
|
||||
borderColor: statusDotBorderColor ?? theme.colors.surface0,
|
||||
borderColor: statusDotBorderColor ?? styles.statusDotBorderDefault.borderColor,
|
||||
width: statusDotSize,
|
||||
height: statusDotSize,
|
||||
right: statusDotOffset,
|
||||
bottom: statusDotOffset,
|
||||
},
|
||||
],
|
||||
[statusDotColor, statusDotBorderColor, theme.colors.surface0, statusDotSize, statusDotOffset],
|
||||
[statusDotColor, statusDotBorderColor, statusDotSize, statusDotOffset],
|
||||
);
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return (
|
||||
<View style={agentIconWrapperStyle}>
|
||||
<SyncedLoader
|
||||
size={size - 1}
|
||||
color={
|
||||
theme.colorScheme === "light"
|
||||
? theme.colors.palette.amber[700]
|
||||
: theme.colors.palette.amber[500]
|
||||
}
|
||||
/>
|
||||
<SyncedLoader size={size - 1} color={styles.syncedLoader.color} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -187,7 +181,6 @@ export function WorkspaceTabOptionRow({
|
||||
onPress,
|
||||
trailingAccessory,
|
||||
}: WorkspaceTabOptionRowProps): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.optionMainPressable,
|
||||
@@ -213,7 +206,7 @@ export function WorkspaceTabOptionRow({
|
||||
</Pressable>
|
||||
{selected ? (
|
||||
<View style={styles.optionTrailingSlot}>
|
||||
<Check size={16} color={theme.colors.foregroundMuted} />
|
||||
<ThemedCheckIcon size={16} uniProps={mutedColorMapping} />
|
||||
</View>
|
||||
) : null}
|
||||
{trailingAccessory ? (
|
||||
@@ -238,6 +231,33 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
},
|
||||
statusDotBorderDefault: {
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
statusDotNeedsInput: {
|
||||
color: theme.colors.palette.amber[500],
|
||||
},
|
||||
statusDotFailed: {
|
||||
color: theme.colors.palette.red[500],
|
||||
},
|
||||
statusDotRunning: {
|
||||
color: theme.colors.palette.blue[500],
|
||||
},
|
||||
statusDotAttention: {
|
||||
color: theme.colors.palette.green[500],
|
||||
},
|
||||
iconActive: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
iconInactive: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
syncedLoader: {
|
||||
color:
|
||||
theme.colorScheme === "light"
|
||||
? theme.colors.palette.amber[700]
|
||||
: theme.colors.palette.amber[500],
|
||||
},
|
||||
optionRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user