;
contentRef: React.RefObject
;
}): ReactElement | null {
- const { theme } = useUnistyles();
const bottomSheetInternal = useBottomSheetModalInternal(true);
const [triggerRect, setTriggerRect] = useState(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
@@ -297,7 +305,7 @@ function WorkspaceHoverCardContent({
{prHint?.checks && prHint.checks.length > 0 ? (
<>
-
+
>
) : null}
@@ -306,49 +314,49 @@ function WorkspaceHoverCardContent({
);
}
+const ThemedExternalLink = withUnistyles(ExternalLink);
+const ThemedGitHubIcon = withUnistyles(GitHubIcon);
+
+const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
+const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
+
function ChecksSummaryContent({
checks,
- theme,
hovered,
}: {
checks: NonNullable;
- theme: ReturnType["theme"];
hovered: boolean;
}) {
const failed = checks.filter((c) => c.status === "failure").length;
const pending = checks.filter((c) => c.status === "pending").length;
- let badgeColor: string;
let badgeLabel: string;
+ let dotStyle: StyleProp;
+ let statusTextStyle: StyleProp;
if (failed > 0) {
- badgeColor = theme.colors.palette.red[500];
badgeLabel = `${failed} failed`;
+ dotStyle = styles.checksDotFailed;
+ statusTextStyle = styles.checksStatusTextFailed;
} else if (pending > 0) {
- badgeColor = theme.colors.palette.amber[500];
badgeLabel = `${pending} running`;
+ dotStyle = styles.checksDotPending;
+ statusTextStyle = styles.checksStatusTextPending;
} else {
- badgeColor = theme.colors.palette.green[500];
badgeLabel = `${checks.length} passed`;
+ dotStyle = styles.checksDotPassed;
+ statusTextStyle = styles.checksStatusTextPassed;
}
- const iconColor = hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
- const labelStyle = useMemo(
- () => [styles.checksSummaryLabel, hovered && styles.checksSummaryLabelHovered],
- [hovered],
- );
- const dotStyle = useMemo(() => [styles.checksDot, { backgroundColor: badgeColor }], [badgeColor]);
- const statusTextStyle = useMemo(
- () => [styles.checksStatusText, { color: badgeColor }],
- [badgeColor],
- );
+ const labelStyle = hovered ? checksSummaryLabelHoveredCombined : styles.checksSummaryLabel;
+ const iconUniProps = hovered ? foregroundColorMapping : foregroundMutedColorMapping;
return (
<>
{hovered ? (
-
+
) : (
-
+
)}
Checks
@@ -362,38 +370,32 @@ function ChecksSummaryContent({
function ChecksSummaryPressable({
checks,
url,
- theme,
}: {
checks: NonNullable;
url: string;
- theme: ReturnType["theme"];
}) {
const handlePress = useCallback(() => {
void openExternalUrl(`${url}/checks`);
}, [url]);
- const pressableStyle = useCallback(
- ({ hovered }: { pressed: boolean; hovered?: boolean }) => [
- styles.checksSummaryRow,
- Boolean(hovered) && styles.listRowHovered,
- ],
- [],
- );
-
const renderChildren = useCallback(
({ hovered }: { pressed: boolean; hovered?: boolean }) => (
-
+
),
- [checks, theme],
+ [checks],
);
return (
-
+
{renderChildren}
);
}
+function checksSummaryPressableStyle({ hovered = false }: { pressed: boolean; hovered?: boolean }) {
+ return [styles.checksSummaryRow, hovered && styles.listRowHovered];
+}
+
const styles = StyleSheet.create((theme) => ({
portalOverlay: {
position: "absolute",
@@ -467,13 +469,42 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
justifyContent: "flex-end",
},
- checksDot: {
+ checksDotFailed: {
width: 6,
height: 6,
borderRadius: 3,
+ backgroundColor: theme.colors.palette.red[500],
},
- checksStatusText: {
+ checksDotPending: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ backgroundColor: theme.colors.palette.amber[500],
+ },
+ checksDotPassed: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ backgroundColor: theme.colors.palette.green[500],
+ },
+ checksStatusTextFailed: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
+ color: theme.colors.palette.red[500],
+ },
+ checksStatusTextPending: {
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.normal,
+ color: theme.colors.palette.amber[500],
+ },
+ checksStatusTextPassed: {
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.normal,
+ color: theme.colors.palette.green[500],
},
}));
+
+const checksSummaryLabelHoveredCombined = [
+ styles.checksSummaryLabel,
+ styles.checksSummaryLabelHovered,
+];
diff --git a/packages/app/src/components/workspace-setup-dialog.tsx b/packages/app/src/components/workspace-setup-dialog.tsx
index 167311dcc..8a90ddb54 100644
--- a/packages/app/src/components/workspace-setup-dialog.tsx
+++ b/packages/app/src/components/workspace-setup-dialog.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Text, View } from "react-native";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { StyleSheet } from "react-native-unistyles";
import { createNameId } from "mnemonic-id";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Composer } from "@/components/composer";
@@ -140,7 +140,6 @@ function buildCreateAgentOptions({
}
export function WorkspaceSetupDialog() {
- const { theme } = useUnistyles();
const toast = useToast();
const pendingWorkspaceSetup = useWorkspaceSetupStore((state) => state.pendingWorkspaceSetup);
const clearWorkspaceSetup = useWorkspaceSetupStore((state) => state.clearWorkspaceSetup);
@@ -349,10 +348,6 @@ export function WorkspaceSetupDialog() {
addImagesRef.current = addImages;
}, []);
- const composerInputWrapperStyle = useMemo(
- () => ({ backgroundColor: theme.colors.surface2 }),
- [theme.colors.surface2],
- );
const iconSource = useMemo(() => (iconDataUri ? { uri: iconDataUri } : null), [iconDataUri]);
const statusControlsWithDisabled = useMemo(
() =>
@@ -415,7 +410,7 @@ export function WorkspaceSetupDialog() {
autoFocus
commandDraftConfig={composerState?.commandDraftConfig}
statusControls={statusControlsWithDisabled}
- inputWrapperStyle={composerInputWrapperStyle}
+ inputWrapperStyle={styles.composerInputWrapper}
onAddImages={handleAddImagesCallback}
/>
@@ -463,4 +458,7 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructive,
lineHeight: 20,
},
+ composerInputWrapper: {
+ backgroundColor: theme.colors.surface2,
+ },
}));
diff --git a/packages/app/src/panels/agent-panel.test.tsx b/packages/app/src/panels/agent-panel.test.tsx
index 9a124b046..e2f8b99cb 100644
--- a/packages/app/src/panels/agent-panel.test.tsx
+++ b/packages/app/src/panels/agent-panel.test.tsx
@@ -108,7 +108,7 @@ vi.mock("react-native-unistyles", () => {
StyleSheet: {
create: createPanelTestStyles,
},
- useUnistyles: () => ({ theme }),
+ withUnistyles: (component: T) => component,
};
});
diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx
index 1d6bce30d..b9356eda5 100644
--- a/packages/app/src/panels/agent-panel.tsx
+++ b/packages/app/src/panels/agent-panel.tsx
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
import invariant from "tiny-invariant";
import { shallow, useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
@@ -44,6 +44,7 @@ import {
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import { type Agent, useSessionStore } from "@/stores/session-store";
+import type { Theme } from "@/styles/theme";
import type { PendingPermission } from "@/types/shared";
import type { StreamItem } from "@/types/stream";
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
@@ -125,9 +126,8 @@ function buildChatAgentFromState(
function renderChatAgentNonReadyView(args: {
viewState: AgentScreenViewState;
effectiveAgent: AgentScreenAgent | null;
- theme: ReturnType["theme"];
}): React.ReactElement | null {
- const { viewState, effectiveAgent, theme } = args;
+ const { viewState, effectiveAgent } = args;
if (viewState.tag === "not_found") {
return (
@@ -151,7 +151,7 @@ function renderChatAgentNonReadyView(args: {
return (
-
+
);
@@ -419,7 +419,6 @@ function AgentPanelBody({
connectionStatus: HostRuntimeConnectionStatus;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) {
- const { theme } = useUnistyles();
const { isArchivingAgent: _isArchivingAgent } = useArchiveAgent();
const hasSession = useSessionStore((state) => Boolean(state.sessions[serverId]));
const projectPlacement = useStoreWithEqualityFn(
@@ -549,7 +548,7 @@ function AgentPanelBody({
return (
-
+
);
@@ -585,7 +584,6 @@ function ChatAgentContent({
connectionStatus: HostRuntimeConnectionStatus;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) {
- const { theme } = useUnistyles();
const panelToast = useToastHost();
const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef(null);
@@ -945,7 +943,6 @@ function ChatAgentContent({
const nonReadyView = renderChatAgentNonReadyView({
viewState,
effectiveAgent,
- theme,
});
if (nonReadyView) return nonReadyView;
invariant(effectiveAgent, "effectiveAgent is defined when the non-ready view is absent");
@@ -990,7 +987,7 @@ function ChatAgentContent({
viewState.sync.status === "catching_up" &&
viewState.sync.ui === "overlay" ? (
-
+
) : null}
@@ -1004,7 +1001,7 @@ function ChatAgentContent({
{isArchivingCurrentAgent ? (
-
+
Archiving agent...
Please wait while we archive this agent.
@@ -1340,6 +1337,15 @@ function AgentSessionUnavailableState({
);
}
+const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
+
+const foregroundMutedColorMapping = (theme: Theme) => ({
+ color: theme.colors.foregroundMuted,
+});
+const foregroundColorMapping = (theme: Theme) => ({
+ color: theme.colors.foreground,
+});
+
const styles = StyleSheet.create((theme) => ({
root: {
flex: 1,
diff --git a/packages/app/src/panels/setup-panel.tsx b/packages/app/src/panels/setup-panel.tsx
index e5ca777e6..626b669d4 100644
--- a/packages/app/src/panels/setup-panel.tsx
+++ b/packages/app/src/panels/setup-panel.tsx
@@ -9,11 +9,12 @@ import {
View,
} from "react-native";
import invariant from "tiny-invariant";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import { usePaneContext } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
+import type { Theme } from "@/styles/theme";
import {
useWorkspaceSetupStore,
type WorkspaceSetupSnapshot,
@@ -62,15 +63,13 @@ function useSetupPanelDescriptor(
type CommandStatus = "running" | "completed" | "failed";
function CommandStatusIcon({ status }: { status: CommandStatus }) {
- const { theme } = useUnistyles();
-
if (status === "running") {
- return ;
+ return ;
}
if (status === "completed") {
- return ;
+ return ;
}
- return ;
+ return ;
}
function formatDuration(ms: number): string {
@@ -151,7 +150,6 @@ function buildCommandRowState(args: BuildCommandRowPropsArgs) {
}
function SetupPanel() {
- const { theme } = useUnistyles();
const { serverId, target } = usePaneContext();
invariant(target.kind === "setup", "SetupPanel requires setup target");
@@ -231,7 +229,7 @@ function SetupPanel() {
{isWaiting ? (
-
+
Setting up workspace...
) : null}
@@ -269,7 +267,6 @@ function SetupPanel() {
hasError={rowState.hasError}
processedLog={rowState.processedLog}
errorMessage={snapshot?.error ?? null}
- foregroundMutedColor={theme.colors.foregroundMuted}
onToggle={toggleExpanded}
/>
);
@@ -292,7 +289,6 @@ interface SetupCommandRowProps {
hasError: boolean;
processedLog: string;
errorMessage: string | null;
- foregroundMutedColor: string;
onToggle: (index: number, isAutoExpanded: boolean) => void;
}
@@ -305,7 +301,6 @@ function SetupCommandRow({
hasError,
processedLog,
errorMessage,
- foregroundMutedColor,
onToggle,
}: SetupCommandRowProps) {
const handlePress = useCallback(() => {
@@ -341,7 +336,7 @@ function SetupCommandRow({
{command.durationMs != null ? (
{formatDuration(command.durationMs)}
) : null}
-
+
{showDetail ? (
@@ -388,12 +383,14 @@ export const setupPanelRegistration: PanelRegistration<"setup"> = {
useDescriptor: useSetupPanelDescriptor,
};
-function SetupCommandChevron({ showDetail, color }: { showDetail: boolean; color: string }) {
+function SetupCommandChevron({ showDetail }: { showDetail: boolean }) {
const chevronStyle = useMemo(
() => [styles.chevron, showDetail && styles.chevronExpanded],
[showDetail],
);
- return ;
+ return (
+
+ );
}
function StandaloneLogView({ commands, log }: { commands: SetupCommand[]; log: string }) {
@@ -432,6 +429,24 @@ function TopLevelSetupError({
);
}
+const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
+const ThemedCheckCircle2 = withUnistyles(CheckCircle2);
+const ThemedCircleAlert = withUnistyles(CircleAlert);
+const ThemedChevronRight = withUnistyles(ChevronRight);
+
+const foregroundColorMapping = (theme: Theme) => ({
+ color: theme.colors.foreground,
+});
+const foregroundMutedColorMapping = (theme: Theme) => ({
+ color: theme.colors.foregroundMuted,
+});
+const greenColorMapping = (theme: Theme) => ({
+ color: theme.colors.palette.green[500],
+});
+const redColorMapping = (theme: Theme) => ({
+ color: theme.colors.palette.red[500],
+});
+
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
index 8a9eb8eb1..67b13c2b4 100644
--- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
+++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
@@ -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;
- iconColor: string;
}) {
const leading = useMemo(() => {
switch (entry.icon) {
case "copy":
- return ;
+ return ;
case "rotate-cw":
- return ;
+ return ;
case "arrow-left-to-line":
- return ;
+ return ;
case "arrow-right-to-line":
- return ;
+ return ;
case "copy-x":
- return ;
+ return ;
case "x":
- return ;
+ return ;
default:
return undefined;
}
- }, [entry.icon, iconColor]);
+ }, [entry.icon]);
const trailing = useMemo(
() => (entry.hint ? {entry.hint} : undefined),
[entry.hint],
@@ -255,7 +269,6 @@ function TabChip({
onCloseTab: (tabId: string) => Promise | 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 ? (
-
) : (
-
)
@@ -432,11 +441,7 @@ function TabChip({
entry.kind === "separator" ? (
) : (
-
+
),
)}
@@ -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}
>
-
+
@@ -690,7 +694,7 @@ export function WorkspaceDesktopTabsRow({
}
style={newTerminalActionButtonStyle}
>
-
+
@@ -712,7 +716,7 @@ export function WorkspaceDesktopTabsRow({
accessibilityLabel="Split pane right"
style={newTabActionButtonStyle}
>
-
+
@@ -730,7 +734,7 @@ export function WorkspaceDesktopTabsRow({
accessibilityLabel="Split pane down"
style={newTabActionButtonStyle}
>
-
+
diff --git a/packages/app/src/screens/workspace/workspace-git-actions.tsx b/packages/app/src/screens/workspace/workspace-git-actions.tsx
index 1b5f65b10..dc7e8179b 100644
--- a/packages/app/src/screens/workspace/workspace-git-actions.tsx
+++ b/packages/app/src/screens/workspace/workspace-git-actions.tsx
@@ -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: ,
+ pull: ,
+ push: ,
+ viewPr: ,
+ createPr: ,
+ merge: ,
+ mergeFromBase: ,
+ archive: ,
+};
+
export function WorkspaceGitActions({ serverId, cwd, hideLabels }: WorkspaceGitActionsProps) {
- const { theme } = useUnistyles();
-
- const icons = useMemo(
- () => ({
- commit: ,
- pull: ,
- push: ,
- viewPr: ,
- createPr: ,
- merge: ,
- mergeFromBase: ,
- archive: ,
- }),
- [theme.colors.foregroundMuted],
- );
-
- const { gitActions, isGit } = useGitActions({ serverId, cwd, icons });
+ const { gitActions, isGit } = useGitActions({ serverId, cwd, icons: ICONS });
if (!isGit) {
return null;
diff --git a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx
index ae8f2b9df..09da0eb20 100644
--- a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx
+++ b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx
@@ -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(
- () => ,
- [editor.id, foregroundMuted],
+ () => ,
+ [editor.id],
);
const trailing = useMemo(
- () => (isPreferred ? : undefined),
- [isPreferred, foregroundMuted],
+ () => (isPreferred ? : undefined),
+ [isPreferred],
);
return (
{openMutation.isPending ? (
-
) : (
-
{!hideLabels && Open}
@@ -198,7 +205,7 @@ export function WorkspaceOpenInEditorButton({
accessibilityRole="button"
accessibilityLabel="Choose editor"
>
-
+
))}
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 87862072f..7fb713692 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -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();
const EMPTY_SET = new Set();
+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 = ;
+const MENU_NEW_TERMINAL_ICON = ;
+const MENU_COPY_ICON = ;
+const MENU_SETTINGS_ICON = ;
+
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 (
@@ -316,14 +339,14 @@ function MobileTabTrailingAccessory({
hitSlop={8}
style={mobileTabMenuTriggerStyle}
>
-
+
{menuEntries.map((entry) =>
entry.kind === "separator" ? (
) : (
-
+
),
)}
@@ -333,29 +356,27 @@ function MobileTabTrailingAccessory({
function MobileTabDropdownMenuItem({
entry,
- iconColor,
}: {
entry: Extract;
- iconColor: string;
}) {
const leading = useMemo(() => {
switch (entry.icon) {
case "copy":
- return ;
+ return ;
case "rotate-cw":
- return ;
+ return ;
case "arrow-left-to-line":
- return ;
+ return ;
case "arrow-right-to-line":
- return ;
+ return ;
case "copy-x":
- return ;
+ return ;
case "x":
- return ;
+ return ;
default:
return undefined;
}
- }, [entry.icon, iconColor]);
+ }, [entry.icon]);
const trailing = useMemo(
() => (entry.hint ? {entry.hint} : undefined),
[entry.hint],
@@ -408,7 +429,6 @@ function MobileWorkspaceTabOption({
onCloseTabsBelow: (tabId: string) => Promise | void;
onCloseOtherTabs: (tabId: string) => Promise | 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(null);
const tabIndexByKey = useMemo(() => {
@@ -572,7 +586,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
normalizedWorkspaceId={normalizedWorkspaceId}
/>
-
+
- );
+ const Icon = isMobile ? ThemedEllipsisVertical : ThemedEllipsis;
+ const colorMapping = hovered || open ? foregroundColorMapping : mutedColorMapping;
+ return ;
}
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 (
-
+
);
}
@@ -975,7 +982,7 @@ function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.React
if (!activeTabDescriptor && !hasHydratedAgents) {
return (
-
+
);
}
@@ -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(
- () => ,
- [theme.colors.foregroundMuted],
- );
- const menuNewTerminalIcon = useMemo(
- () => ,
- [theme.colors.foregroundMuted],
- );
- const menuCopyIcon = useMemo(
- () => ,
- [theme.colors.foregroundMuted],
- );
- const menuSettingsIcon = useMemo(
- () => ,
- [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 (
<>
-
+
{workspaceDescriptor?.diffStat ? (
{({ hovered }) => {
- const color =
- isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
- return ;
+ const colorMapping =
+ isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping;
+ return ;
}}
) : 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 ? (
-
+
) : (
-
+
);
}}
@@ -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"];
diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx
index 751d410b3..4eb8f10b7 100644
--- a/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx
+++ b/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx
@@ -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>) =>
+ ({
+ uniProps,
+ ...rest
+ }: {
+ uniProps?: (theme: unknown) => Record;
+ } & Record) => {
+ const themed = uniProps ? uniProps(theme) : {};
+ return React.createElement(Component, { ...rest, ...themed });
+ },
}));
vi.mock("@/constants/platform", () => ({
diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.tsx
index a72689bae..77aff3b8d 100644
--- a/packages/app/src/screens/workspace/workspace-scripts-button.tsx
+++ b/packages/app/src/screens/workspace/workspace-scripts-button.tsx
@@ -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["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" ? (
-
+
) : (
-
+
);
- 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 }) => (
-
+
),
- [icon, label, theme],
+ [icon, label],
);
return (
@@ -123,32 +142,26 @@ interface HostLinkChildrenProps {
hovered?: boolean;
disabled: boolean;
label: string;
- theme: ReturnType["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 (
<>
{label}
- {showIcon ? : null}
+ {showIcon ? : null}
>
);
}
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 }) => (
-
+
),
- [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 (
exit {code}
@@ -206,26 +217,24 @@ interface ScriptRowProps {
: null
: null;
isStartPending: boolean;
- theme: ReturnType["theme"];
onStartScript: (scriptName: string) => void;
onViewTerminal?: (terminalId: string) => void;
}
-function resolveScriptIconColor(args: {
- theme: ReturnType["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}
>
-
+
{script.scriptName}
@@ -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 (
@@ -414,15 +416,9 @@ export function WorkspaceScriptsButton({
accessibilityLabel="Workspace scripts"
>
-
+
{!hideLabels && Scripts}
-
+
@@ -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];
diff --git a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx
index 6f1d7db65..ac03e040c 100644
--- a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx
+++ b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx
@@ -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 (
-
+
);
}
@@ -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({
{selected ? (
-
+
) : 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",
diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts
index 4b20000f5..43617e483 100644
--- a/packages/app/src/styles/theme.ts
+++ b/packages/app/src/styles/theme.ts
@@ -412,74 +412,83 @@ const ghosttyDarkColors = buildDarkSemanticColors({
destructive: "#c44a55", // red with slight cool lean against the slate-blue surfaces
});
+export const SPACING = {
+ 0: 0,
+ 1: 4,
+ 1.5: 6,
+ 2: 8,
+ 3: 12,
+ 4: 16,
+ 6: 24,
+ 8: 32,
+ 12: 48,
+ 16: 64,
+ 20: 80,
+ 24: 96,
+ 32: 128,
+} as const;
+
+export const FONT_SIZE = {
+ xs: 12,
+ sm: 14,
+ base: 16,
+ lg: 18,
+ xl: 20,
+ "2xl": 22,
+ "3xl": 26,
+ "4xl": 34,
+} as const;
+
+export const LINE_HEIGHT = {
+ diff: 22,
+} as const;
+
+export const ICON_SIZE = {
+ xs: 12,
+ sm: 14,
+ md: 16,
+ lg: 20,
+} as const;
+
+export const FONT_WEIGHT = {
+ normal: "normal" as const,
+ medium: "500" as const,
+ semibold: "600" as const,
+ bold: "bold" as const,
+} as const;
+
+export const BORDER_RADIUS = {
+ none: 0,
+ sm: 2,
+ base: 4,
+ md: 6,
+ lg: 8,
+ xl: 12,
+ "2xl": 16,
+ full: 9999,
+} as const;
+
+export const BORDER_WIDTH = {
+ 0: 0,
+ 1: 1,
+ 2: 2,
+} as const;
+
+export const OPACITY = {
+ 0: 0,
+ 50: 0.5,
+ 100: 1,
+} as const;
+
const commonTheme = {
- spacing: {
- 0: 0,
- 1: 4,
- 1.5: 6,
- 2: 8,
- 3: 12,
- 4: 16,
- 6: 24,
- 8: 32,
- 12: 48,
- 16: 64,
- 20: 80,
- 24: 96,
- 32: 128,
- },
-
- fontSize: {
- xs: 12,
- sm: 14,
- base: 16,
- lg: 18,
- xl: 20,
- "2xl": 22,
- "3xl": 26,
- "4xl": 34,
- },
-
- lineHeight: {
- diff: 22,
- },
-
- iconSize: {
- xs: 12,
- sm: 14,
- md: 16,
- lg: 20,
- },
-
- fontWeight: {
- normal: "normal" as const,
- medium: "500" as const,
- semibold: "600" as const,
- bold: "bold" as const,
- },
-
- borderRadius: {
- none: 0,
- sm: 2,
- base: 4,
- md: 6,
- lg: 8,
- xl: 12,
- "2xl": 16,
- full: 9999,
- },
-
- borderWidth: {
- 0: 0,
- 1: 1,
- 2: 2,
- },
-
- opacity: {
- 0: 0,
- 50: 0.5,
- 100: 1,
- },
+ spacing: SPACING,
+ fontSize: FONT_SIZE,
+ lineHeight: LINE_HEIGHT,
+ iconSize: ICON_SIZE,
+ fontWeight: FONT_WEIGHT,
+ borderRadius: BORDER_RADIUS,
+ borderWidth: BORDER_WIDTH,
+ opacity: OPACITY,
} as const;
const darkShadow = {