feat(app): add running status ring and lighten status dots
Keep the animated ring phase-locked across hosts and derive its knockout surface from each host's actual interaction state. Align filled status circles at 6px while preserving the larger needs-input alert. Co-authored-by: Christoph Leiter <christoph@leiter.io>
This commit is contained in:
co-authored by
Christoph Leiter
parent
6ae6231d1f
commit
c0daf8e066
@@ -4,7 +4,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
|
||||
import { CircleAlert, Folder, FolderGit2, Monitor } from "lucide-react-native";
|
||||
import { ProjectStatusIndicator } from "@/components/sidebar/project-leading-visual";
|
||||
import type { SurfaceBackdrop } from "@/styles/surface-backdrop";
|
||||
import type { SidebarSurfaceBackdrop } from "@/styles/surface-backdrop";
|
||||
import {
|
||||
WorkspaceMetaRow,
|
||||
type WorkspaceServiceSummary,
|
||||
@@ -19,12 +19,14 @@ import {
|
||||
import { useAppSettings } from "@/hooks/use-settings";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
||||
import { getStatusDotColor } from "@/utils/status-dot-color";
|
||||
import {
|
||||
STATUS_INDICATOR_ALERT_SIZE,
|
||||
STATUS_INDICATOR_DOT_SIZE,
|
||||
STATUS_INDICATOR_FILLED_DOT_SIZE,
|
||||
} from "@/utils/status-indicator-geometry";
|
||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||
import { StatusRing } from "@/components/status-ring";
|
||||
import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sidebar-workspace-title";
|
||||
|
||||
// The scrim spans more than the kebab so the fade starts left of the diff stat. Solid from
|
||||
@@ -32,11 +34,6 @@ import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sideba
|
||||
const SCRIM_WIDTH = 48;
|
||||
const SCRIM_SOLID_OFFSET = "55%";
|
||||
|
||||
const DEFAULT_STATUS_DOT_SIZE = 7;
|
||||
const EMPHASIZED_STATUS_DOT_SIZE = 9;
|
||||
const DEFAULT_STATUS_DOT_OFFSET = 0;
|
||||
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
||||
|
||||
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
const needsInputColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.surface0,
|
||||
@@ -143,7 +140,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
leadingProjectIconDataUri?: string | null;
|
||||
serviceSummary?: WorkspaceServiceSummary | null;
|
||||
/** The row's current background, so the project status badge can knock out of it. */
|
||||
backdrop: SurfaceBackdrop;
|
||||
backdrop: SidebarSurfaceBackdrop;
|
||||
isHovered: boolean;
|
||||
isLoading: boolean;
|
||||
isCreating?: boolean;
|
||||
@@ -221,14 +218,14 @@ function WorkspaceStatusIndicator({
|
||||
loading?: boolean;
|
||||
reserveIdleSpace?: boolean;
|
||||
}) {
|
||||
// Busy is a dot here for the same reason it is on a project icon: every status in the
|
||||
// sidebar is a dot, and a row with a project icon simply moves that dot onto the icon.
|
||||
// A row starting up and a row working are both busy, so they share the dot and differ only
|
||||
// in testID.
|
||||
// Busy is the only status that moves, and it is the ring rather than a dot for the same
|
||||
// reason it is a dot elsewhere: every status in the sidebar sits in this one slot, so busy
|
||||
// has to fill it without displacing anything. A row starting up and a row working are both
|
||||
// busy, so they share the ring and differ only in testID.
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-loading">
|
||||
<View style={styles.standaloneRunningDot} />
|
||||
<StatusRing />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -236,7 +233,7 @@ function WorkspaceStatusIndicator({
|
||||
if (shouldRenderSyncedStatusLoader({ bucket })) {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-running">
|
||||
<View style={styles.standaloneRunningDot} />
|
||||
<StatusRing />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -275,50 +272,16 @@ function WorkspaceStatusIndicator({
|
||||
else KindIcon = ThemedFolder;
|
||||
|
||||
const dotColorStyle = getStatusDotColorStyle(bucket);
|
||||
const statusDotSize = isEmphasizedStatusDotBucket(bucket)
|
||||
? EMPHASIZED_STATUS_DOT_SIZE
|
||||
: DEFAULT_STATUS_DOT_SIZE;
|
||||
const statusDotOffset =
|
||||
statusDotSize === EMPHASIZED_STATUS_DOT_SIZE
|
||||
? EMPHASIZED_STATUS_DOT_OFFSET
|
||||
: DEFAULT_STATUS_DOT_OFFSET;
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot} testID={`workspace-status-indicator-${bucket}`}>
|
||||
<KindIcon size={14} uniProps={foregroundMutedColorMapping} />
|
||||
{dotColorStyle ? (
|
||||
<StatusDotOverlay
|
||||
dotColorStyle={dotColorStyle}
|
||||
size={statusDotSize}
|
||||
offset={statusDotOffset}
|
||||
/>
|
||||
) : null}
|
||||
{dotColorStyle ? <StatusDotOverlay dotColorStyle={dotColorStyle} /> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusDotOverlay({
|
||||
dotColorStyle,
|
||||
size,
|
||||
offset,
|
||||
}: {
|
||||
dotColorStyle: ViewStyle;
|
||||
size: number;
|
||||
offset: number;
|
||||
}) {
|
||||
const overlayStyle = useMemo(
|
||||
() => [
|
||||
styles.statusDotOverlay,
|
||||
dotColorStyle,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
right: offset,
|
||||
bottom: offset,
|
||||
},
|
||||
],
|
||||
[dotColorStyle, offset, size],
|
||||
);
|
||||
return <View style={overlayStyle} />;
|
||||
function StatusDotOverlay({ dotColorStyle }: { dotColorStyle: ViewStyle }) {
|
||||
return <View style={[styles.statusDotOverlay, dotColorStyle]} />;
|
||||
}
|
||||
|
||||
function getStatusDotColorStyle(bucket: SidebarStateBucket) {
|
||||
@@ -569,24 +532,22 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
statusDotOverlay: {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: STATUS_INDICATOR_DOT_SIZE,
|
||||
height: STATUS_INDICATOR_DOT_SIZE,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
},
|
||||
standaloneStatusDot: {
|
||||
width: STATUS_INDICATOR_DOT_SIZE,
|
||||
height: STATUS_INDICATOR_DOT_SIZE,
|
||||
width: STATUS_INDICATOR_FILLED_DOT_SIZE,
|
||||
height: STATUS_INDICATOR_FILLED_DOT_SIZE,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
||||
},
|
||||
standaloneRunningDot: {
|
||||
width: STATUS_INDICATOR_DOT_SIZE,
|
||||
height: STATUS_INDICATOR_DOT_SIZE,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "running" }) ?? undefined,
|
||||
},
|
||||
idleStatusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
width: STATUS_INDICATOR_FILLED_DOT_SIZE,
|
||||
height: STATUS_INDICATOR_FILLED_DOT_SIZE,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.foregroundExtraMuted,
|
||||
opacity: 0.3,
|
||||
|
||||
Reference in New Issue
Block a user