refactor(app): unify status colours on one token family
Three systems were reporting the same states: a full and a muted status tier in the theme, plus raw palette steps in StatusBadge and getStatusDotColor. A PR icon was muted in the sidebar and full-strength in the PR panel, and the Resolved badge used a theme-independent green so it painted a dark block in light mode. The muted tier is deleted and every status surface now reads the one token per signal. Dots keep a band of their own. A dot is a few points of solid colour with no shape to read and no label attached, and the running one pulses, so at the status band's lightness it sat below the check icons and host badge on its own row — backwards, since the dot is the row's state. Chroma caps at 90% of gamut rather than 100% because at the edge green reaches C=0.215 while blue manages 0.116 and the set stops reading as one family; dark lightness caps at 0.72 because red desaturates to pink above it. Host badges needed a foreground variant of the identity table. That table is tuned for a white letter on the colour as a fill; used as text it cannot clear 4.5:1 on both themes at once, since light needs luminance <= 0.183 and dark needs >= 0.212. The new set is generated on the status family's exact lightness and chroma fraction, so a host name and a CI check in the same meta row carry equal weight instead of the brighter one winning the row. Also: PR avatars had their own eight-hex palette and hash duplicating identity-colors; workspace rows under a project group now indent to the same rail as status-grouped rows; group headers are muted; and the pulsing dot is faster and fades further.
This commit is contained in:
@@ -36,11 +36,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
additions: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.statusMutedSuccess,
|
||||
color: theme.colors.statusSuccess,
|
||||
},
|
||||
deletions: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.statusMutedDanger,
|
||||
color: theme.colors.statusDanger,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { Theme } from "@/styles/theme";
|
||||
const ThemedSquarePlus = withUnistyles(SquarePlus);
|
||||
const ThemedSquareMinus = withUnistyles(SquareMinus);
|
||||
|
||||
const successMapping = (theme: Theme) => ({ color: theme.colors.statusMutedSuccess });
|
||||
const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusMutedDanger });
|
||||
const successMapping = (theme: Theme) => ({ color: theme.colors.statusSuccess });
|
||||
const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
||||
|
||||
const ICON_SIZE = 14;
|
||||
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import {
|
||||
type HostRuntimeConnectionStatus,
|
||||
useHostRuntimeConnectionStatus,
|
||||
} from "@/runtime/host-runtime";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
function hostStatusDotColor(status: HostRuntimeConnectionStatus, theme: Theme) {
|
||||
if (status === "online") return theme.colors.palette.green[400];
|
||||
if (status === "connecting") return theme.colors.palette.amber[500];
|
||||
return theme.colors.palette.red[500];
|
||||
}
|
||||
|
||||
export function HostStatusDot({ serverId }: { serverId: string }) {
|
||||
const { theme } = useUnistyles();
|
||||
const status = useHostRuntimeConnectionStatus(serverId);
|
||||
const backgroundColor = hostStatusDotColor(status, theme);
|
||||
const dotStyle = useMemo(() => [styles.dot, { backgroundColor }], [backgroundColor]);
|
||||
|
||||
return <View style={dotStyle} />;
|
||||
return <View style={[styles.dot, statusStyle(status)]} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create(() => ({
|
||||
function statusStyle(status: HostRuntimeConnectionStatus) {
|
||||
if (status === "online") return styles.dotOnline;
|
||||
if (status === "connecting") return styles.dotConnecting;
|
||||
return styles.dotOffline;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
},
|
||||
dotOnline: {
|
||||
backgroundColor: theme.colors.statusSuccess,
|
||||
},
|
||||
dotConnecting: {
|
||||
backgroundColor: theme.colors.statusWarning,
|
||||
},
|
||||
dotOffline: {
|
||||
backgroundColor: theme.colors.statusDanger,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -105,6 +105,7 @@ import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
SidebarWorkspaceRowContent,
|
||||
sidebarWorkspaceRowStyles,
|
||||
SidebarWorkspaceShortcutBadge,
|
||||
resolveTrailingActionVisibility,
|
||||
SidebarWorkspaceTrailingActionBase,
|
||||
@@ -172,13 +173,13 @@ const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
const redColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.statusMutedDanger,
|
||||
color: theme.colors.statusDanger,
|
||||
});
|
||||
const greenColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.statusMutedSuccess,
|
||||
color: theme.colors.statusSuccess,
|
||||
});
|
||||
const purpleColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.statusMutedMerged,
|
||||
color: theme.colors.statusMerged,
|
||||
});
|
||||
|
||||
function getPrIconUniMapping(state: PrHint["state"]) {
|
||||
@@ -303,6 +304,9 @@ interface WorkspaceRowInnerProps {
|
||||
isPinned?: boolean;
|
||||
onTogglePin?: () => void;
|
||||
reserveIdleStatusIndicatorSpace?: boolean;
|
||||
/** Row sits under a group header (a project row). Pinned rows are not grouped.
|
||||
* Drives the indent that lands the row on the header label's rail. */
|
||||
inGroup?: boolean;
|
||||
}
|
||||
|
||||
export function PrBadge({ hint, style }: { hint: PrHint; style?: StyleProp<ViewStyle> }) {
|
||||
@@ -378,13 +382,16 @@ function getProjectWorkspaceRowStyle({
|
||||
isDragging,
|
||||
selected,
|
||||
isHovered,
|
||||
inGroup,
|
||||
}: {
|
||||
isDragging: boolean;
|
||||
selected: boolean;
|
||||
isHovered: boolean;
|
||||
inGroup: boolean;
|
||||
}) {
|
||||
return [
|
||||
styles.workspaceRow,
|
||||
inGroup && sidebarWorkspaceRowStyles.rowIndented,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
@@ -1061,6 +1068,7 @@ function WorkspaceRowInner({
|
||||
isPinned,
|
||||
onTogglePin,
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inGroup = false,
|
||||
}: WorkspaceRowInnerProps) {
|
||||
const _isCompact = useIsCompactFormFactor();
|
||||
const isTouchPlatform = platformIsNative;
|
||||
@@ -1094,6 +1102,7 @@ function WorkspaceRowInner({
|
||||
isDragging,
|
||||
selected,
|
||||
isHovered,
|
||||
inGroup,
|
||||
});
|
||||
return (
|
||||
<View
|
||||
@@ -1190,6 +1199,7 @@ function WorkspaceRowWithMenu({
|
||||
canPin,
|
||||
onToggleWorkspacePin,
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inGroup = false,
|
||||
isCreating = false,
|
||||
}: {
|
||||
workspace: SidebarWorkspaceEntry;
|
||||
@@ -1207,6 +1217,9 @@ function WorkspaceRowWithMenu({
|
||||
canPin: boolean;
|
||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||
reserveIdleStatusIndicatorSpace?: boolean;
|
||||
/** Row sits under a group header (a project row). Pinned rows are not grouped.
|
||||
* Drives the indent that lands the row on the header label's rail. */
|
||||
inGroup?: boolean;
|
||||
isCreating?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -1348,6 +1361,7 @@ function WorkspaceRowWithMenu({
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
inGroup={inGroup}
|
||||
/>
|
||||
<AdaptiveRenameModal
|
||||
visible={isRenameOpen}
|
||||
@@ -1375,6 +1389,9 @@ interface WorkspaceRowItemProps {
|
||||
canPin: boolean;
|
||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||
reserveIdleStatusIndicatorSpace?: boolean;
|
||||
/** Row sits under a group header (a project row). Pinned rows are not grouped.
|
||||
* Drives the indent that lands the row on the header label's rail. */
|
||||
inGroup?: boolean;
|
||||
isCreating?: boolean;
|
||||
selectionEnabled: boolean;
|
||||
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||
@@ -1396,6 +1413,7 @@ function WorkspaceRowItem({
|
||||
canPin,
|
||||
onToggleWorkspacePin,
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inGroup = false,
|
||||
isCreating = false,
|
||||
selectionEnabled,
|
||||
activeWorkspaceSelection,
|
||||
@@ -1424,6 +1442,7 @@ function WorkspaceRowItem({
|
||||
canPin={canPin}
|
||||
onToggleWorkspacePin={onToggleWorkspacePin}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
inGroup={inGroup}
|
||||
isCreating={isCreating}
|
||||
selected={isWorkspaceSelected({
|
||||
selection: activeWorkspaceSelection,
|
||||
@@ -1467,6 +1486,7 @@ function areWorkspaceRowItemPropsEqual(
|
||||
previous.canPin === next.canPin &&
|
||||
previous.onToggleWorkspacePin === next.onToggleWorkspacePin &&
|
||||
previous.reserveIdleStatusIndicatorSpace === next.reserveIdleStatusIndicatorSpace &&
|
||||
previous.inGroup === next.inGroup &&
|
||||
previous.isCreating === next.isCreating &&
|
||||
previous.onWorkspacePress === next.onWorkspacePress &&
|
||||
previous.drag === next.drag &&
|
||||
@@ -1493,6 +1513,7 @@ function WorkspaceRow({
|
||||
canPin,
|
||||
onToggleWorkspacePin,
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inGroup = false,
|
||||
isCreating = false,
|
||||
selected,
|
||||
}: {
|
||||
@@ -1510,6 +1531,9 @@ function WorkspaceRow({
|
||||
canPin: boolean;
|
||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||
reserveIdleStatusIndicatorSpace?: boolean;
|
||||
/** Row sits under a group header (a project row). Pinned rows are not grouped.
|
||||
* Drives the indent that lands the row on the header label's rail. */
|
||||
inGroup?: boolean;
|
||||
isCreating?: boolean;
|
||||
selected: boolean;
|
||||
}) {
|
||||
@@ -1534,6 +1558,7 @@ function WorkspaceRow({
|
||||
canPin={canPin}
|
||||
onToggleWorkspacePin={onToggleWorkspacePin}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
inGroup={inGroup}
|
||||
isCreating={isCreating}
|
||||
/>
|
||||
);
|
||||
@@ -1633,6 +1658,7 @@ function ProjectBlock({
|
||||
workspace={item}
|
||||
workspaceEntry={workspaceEntriesByKey.get(item.workspaceKey) ?? null}
|
||||
hostBadge={hostBadgeByServerId.get(item.serverId) ?? null}
|
||||
inGroup
|
||||
shortcutNumber={shortcutIndexByWorkspaceKey.get(item.workspaceKey) ?? null}
|
||||
showShortcutBadge={showShortcutBadges}
|
||||
canCopyBranchName={project.projectKind === "git"}
|
||||
@@ -2499,7 +2525,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minWidth: 0,
|
||||
},
|
||||
projectTitle: {
|
||||
color: theme.colors.foreground,
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: "400",
|
||||
minWidth: 0,
|
||||
|
||||
@@ -42,8 +42,8 @@ const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
const amberColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.palette.amber[500],
|
||||
const needsInputColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -175,7 +175,7 @@ function ProjectStatusBadge({
|
||||
testID="project-status-badge"
|
||||
>
|
||||
{content.kind === "alert" ? (
|
||||
<ThemedCircleAlert size={STATUS_BADGE_ALERT_SIZE} uniProps={amberColorMapping} />
|
||||
<ThemedCircleAlert size={STATUS_BADGE_ALERT_SIZE} uniProps={needsInputColorMapping} />
|
||||
) : (
|
||||
<ProjectStatusDot bucket={content.bucket} />
|
||||
)}
|
||||
|
||||
@@ -15,11 +15,13 @@ import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
// one that moves. Callers own the dot's geometry and color and pass it in as a style — this
|
||||
// module owns nothing but the pulse.
|
||||
//
|
||||
// Opacity only, and shallow: the dot sits in a column of static dots in neighboring rows, so
|
||||
// anything that changes its size or drops it near invisible reads as jitter down the list.
|
||||
// One full cycle is out-and-back, so the visible period is twice the half period.
|
||||
const PULSE_HALF_PERIOD_MS = 900;
|
||||
const PULSE_MIN_OPACITY = 0.65;
|
||||
// Opacity only: the dot sits in a column of static dots in neighboring rows, so anything that
|
||||
// changes its size reads as jitter down the list. The floor stays clear of invisible — a dot
|
||||
// that blinks out reads as a glitch rather than as work in progress, and the row loses its
|
||||
// status marker on every cycle. One full cycle is out-and-back, so the visible period is
|
||||
// twice the half period.
|
||||
const PULSE_HALF_PERIOD_MS = 700;
|
||||
const PULSE_MIN_OPACITY = 0.45;
|
||||
|
||||
// One clock for every pulsing dot in the app. Dots that each start their own animation drift
|
||||
// apart as rows mount at different times, and a column of them fading out of step is noise —
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
||||
import { useOpenKebabMenuVisibility } from "@/components/sidebar/use-open-kebab-menu-visibility";
|
||||
import { getSidebarRowBackdrop } from "@/components/sidebar/sidebar-row-backdrop";
|
||||
import { getStatusDotColor } from "@/utils/status-dot-color";
|
||||
import { selectWorkspaceScriptSummary } from "@/components/sidebar/workspace-meta-row";
|
||||
import {
|
||||
SidebarWorkspaceTrailingContent,
|
||||
@@ -63,9 +64,20 @@ import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pi
|
||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
const amberColorMapping = (theme: Theme) => ({ color: theme.colors.palette.amber[500] });
|
||||
const redColorMapping = (theme: Theme) => ({ color: theme.colors.palette.red[500] });
|
||||
const greenColorMapping = (theme: Theme) => ({ color: theme.colors.palette.green[500] });
|
||||
// One mapping per bucket, resolved through the status-dot producer so a group header and
|
||||
// the rows under it cannot disagree about what "failed" looks like.
|
||||
const needsInputColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
||||
});
|
||||
const failedColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined,
|
||||
});
|
||||
const attentionColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
||||
});
|
||||
const runningColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "running" }) ?? undefined,
|
||||
});
|
||||
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedChevronRight = withUnistyles(ChevronRight);
|
||||
@@ -386,13 +398,13 @@ function StatusGroupLeadingVisual({
|
||||
function StatusGroupIcon({ bucket }: { bucket: StatusGroup["bucket"] }) {
|
||||
switch (bucket) {
|
||||
case "needs_input":
|
||||
return <ThemedCircleAlert size={14} uniProps={amberColorMapping} />;
|
||||
return <ThemedCircleAlert size={14} uniProps={needsInputColorMapping} />;
|
||||
case "failed":
|
||||
return <ThemedCircleX size={14} uniProps={redColorMapping} />;
|
||||
return <ThemedCircleX size={14} uniProps={failedColorMapping} />;
|
||||
case "attention":
|
||||
return <ThemedCircleCheck size={14} uniProps={greenColorMapping} />;
|
||||
return <ThemedCircleCheck size={14} uniProps={attentionColorMapping} />;
|
||||
case "running":
|
||||
return <ThemedCircleDot size={14} uniProps={amberColorMapping} />;
|
||||
return <ThemedCircleDot size={14} uniProps={runningColorMapping} />;
|
||||
case "done":
|
||||
return <ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
}
|
||||
@@ -921,7 +933,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minWidth: 0,
|
||||
},
|
||||
statusGroupTitle: {
|
||||
color: theme.colors.foreground,
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: "400",
|
||||
minWidth: 0,
|
||||
|
||||
@@ -29,24 +29,15 @@ import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sideba
|
||||
const SCRIM_WIDTH = 48;
|
||||
const SCRIM_SOLID_OFFSET = "55%";
|
||||
|
||||
/**
|
||||
* How far a workspace row sits inside the status header above it. Only status grouping
|
||||
* indents: a project row carries an icon and its workspaces line up under it already, but a
|
||||
* status header is just a label, so its rows need the offset to read as belonging to it.
|
||||
*
|
||||
* It is row padding rather than a margin on the list, because the row's hover and selected
|
||||
* backgrounds have to keep spanning the group's full width. Indenting the container instead
|
||||
* pulls the highlight in with the content and the row stops lining up with its header.
|
||||
*/
|
||||
const WORKSPACE_ROW_INDENT = 12;
|
||||
|
||||
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 amberColorMapping = (theme: Theme) => ({ color: theme.colors.palette.amber[500] });
|
||||
const needsInputColorMapping = (theme: Theme) => ({
|
||||
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
||||
});
|
||||
|
||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||
const ThemedMonitor = withUnistyles(Monitor);
|
||||
@@ -249,7 +240,7 @@ function WorkspaceStatusIndicator({
|
||||
if (bucket === "needs_input") {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-needs_input">
|
||||
<ThemedCircleAlert size={14} uniProps={amberColorMapping} />
|
||||
<ThemedCircleAlert size={14} uniProps={needsInputColorMapping} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -342,10 +333,16 @@ function getStatusDotColorStyle(bucket: SidebarStateBucket) {
|
||||
}
|
||||
|
||||
export const sidebarWorkspaceRowStyles = StyleSheet.create((theme) => ({
|
||||
// Layered over the row's own padding rather than replacing it, so the indent stays one
|
||||
// number here instead of being baked into a row style.
|
||||
// How far a workspace row sits inside the group header above it — a project row or a
|
||||
// status group header. Both groupings share this one indent, so every grouped workspace row
|
||||
// in the sidebar sits on the same rail regardless of how the list is grouped. Pinned rows
|
||||
// are not grouped and stay flush.
|
||||
//
|
||||
// It is row padding rather than a margin on the list, because the row's hover and selected
|
||||
// backgrounds have to keep spanning the group's full width. Indenting the container instead
|
||||
// pulls the highlight in with the content and the row stops lining up with its header.
|
||||
rowIndented: {
|
||||
paddingLeft: theme.spacing[2] + WORKSPACE_ROW_INDENT,
|
||||
paddingLeft: theme.spacing[2] + theme.spacing[2],
|
||||
},
|
||||
rowRight: {
|
||||
flexDirection: "row",
|
||||
@@ -575,7 +572,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.palette.green[500],
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
||||
},
|
||||
standaloneRunningDot: {
|
||||
width: 8,
|
||||
@@ -608,11 +605,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
opacity: 1,
|
||||
},
|
||||
statusDotNeedsInput: {
|
||||
backgroundColor: theme.colors.palette.amber[500],
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
statusDotFailed: {
|
||||
backgroundColor: theme.colors.palette.red[500],
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined,
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
statusDotRunning: {
|
||||
@@ -620,7 +617,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
statusDotAttention: {
|
||||
backgroundColor: theme.colors.palette.green[500],
|
||||
backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -85,16 +85,10 @@ function Pie({ fraction, color }: { fraction: number; color: string }) {
|
||||
|
||||
const ThemedCheckIndicatorSvg = withUnistyles(CheckIndicatorSvg);
|
||||
|
||||
/**
|
||||
* Passing CI is the uneventful case and asks nothing of the reader, so it gets the quiet
|
||||
* tier — green enough to name the state, close enough to the subtitle's weight that a
|
||||
* sidebar full of them doesn't bury the one red row. Failure and a run still deciding keep
|
||||
* real colour, because those are the two that want something from you.
|
||||
*/
|
||||
const COLOR_MAPPINGS: Record<CheckSummary["state"], (theme: Theme) => { color: string }> = {
|
||||
passed: (theme) => ({ color: theme.colors.statusMutedSuccess }),
|
||||
failed: (theme) => ({ color: theme.colors.statusMutedDanger }),
|
||||
running: (theme) => ({ color: theme.colors.statusMutedWarning }),
|
||||
passed: (theme) => ({ color: theme.colors.statusSuccess }),
|
||||
failed: (theme) => ({ color: theme.colors.statusDanger }),
|
||||
running: (theme) => ({ color: theme.colors.statusWarning }),
|
||||
};
|
||||
|
||||
export function CheckIndicator({ summary, size }: { summary: CheckSummary; size: number }) {
|
||||
|
||||
@@ -39,8 +39,8 @@ const ThemedGlobe = withUnistyles(Globe);
|
||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||
|
||||
const foregroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const mergedMapping = (theme: Theme) => ({ color: theme.colors.statusMutedMerged });
|
||||
const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusMutedDanger });
|
||||
const mergedMapping = (theme: Theme) => ({ color: theme.colors.statusMerged });
|
||||
const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
||||
|
||||
/**
|
||||
* The subtitle under a workspace title: which host it lives on, its change request, that
|
||||
@@ -212,7 +212,7 @@ function ScriptItem({ summary }: { summary: WorkspaceScriptSummary }) {
|
||||
);
|
||||
}
|
||||
|
||||
const successMapping = (theme: Theme) => ({ color: theme.colors.statusMutedSuccess });
|
||||
const successMapping = (theme: Theme) => ({ color: theme.colors.statusSuccess });
|
||||
|
||||
const PR_ICONS = {
|
||||
open: ThemedGitPullRequest,
|
||||
@@ -261,7 +261,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flexShrink: 0,
|
||||
},
|
||||
scriptPort: {
|
||||
color: theme.colors.statusMutedSuccess,
|
||||
color: theme.colors.statusSuccess,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 16,
|
||||
flexShrink: 0,
|
||||
@@ -280,19 +280,19 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
// Matches the indicator — see COLOR_MAPPINGS in check-indicator.tsx.
|
||||
checksTextPassed: {
|
||||
color: theme.colors.statusMutedSuccess,
|
||||
color: theme.colors.statusSuccess,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 16,
|
||||
flexShrink: 0,
|
||||
},
|
||||
checksTextFailed: {
|
||||
color: theme.colors.statusMutedDanger,
|
||||
color: theme.colors.statusDanger,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 16,
|
||||
flexShrink: 0,
|
||||
},
|
||||
checksTextRunning: {
|
||||
color: theme.colors.statusMutedWarning,
|
||||
color: theme.colors.statusWarning,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 16,
|
||||
flexShrink: 0,
|
||||
|
||||
@@ -97,8 +97,12 @@ export function SyncedLoader({ size = 10, color }: { size?: number; color: strin
|
||||
const reduceMotion = useReducedMotion();
|
||||
const step = useSyncedLoaderStep(active, reduceMotion);
|
||||
|
||||
// The 2x3 grid fills `size` exactly on its long axis: the dot is whatever is left after
|
||||
// the two gaps, not a floored integer. Flooring cost the grid up to a third of a dot per
|
||||
// row — at size 10 it drew 8pt of ink in a 10pt box and read as a small mark in a large
|
||||
// slot. Fractional dots are fine here; these are sub-pixel radii on a moving glyph.
|
||||
const gap = Math.max(1, Math.round(size * 0.12));
|
||||
const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3));
|
||||
const dotSize = Math.max(2, (size - gap * 2) / 3);
|
||||
const gridWidth = dotSize * 2 + gap;
|
||||
const gridHeight = dotSize * 3 + gap * 2;
|
||||
|
||||
|
||||
@@ -45,13 +45,15 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: 3,
|
||||
},
|
||||
// Tinted from the one status token rather than a palette step, so the pill tracks the
|
||||
// theme. `1a`/`33` are the 10%/20% alpha suffixes the identity table uses.
|
||||
pillSuccess: {
|
||||
backgroundColor: theme.colors.palette.green[900],
|
||||
borderColor: theme.colors.palette.green[800],
|
||||
backgroundColor: `${theme.colors.statusSuccess}1a`,
|
||||
borderColor: `${theme.colors.statusSuccess}33`,
|
||||
},
|
||||
pillError: {
|
||||
backgroundColor: theme.colors.palette.red[900],
|
||||
borderColor: theme.colors.palette.red[800],
|
||||
backgroundColor: `${theme.colors.statusDanger}1a`,
|
||||
borderColor: `${theme.colors.statusDanger}33`,
|
||||
},
|
||||
pillText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
@@ -59,9 +61,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
pillTextSuccess: {
|
||||
color: theme.colors.palette.green[400],
|
||||
color: theme.colors.statusSuccess,
|
||||
},
|
||||
pillTextError: {
|
||||
color: theme.colors.palette.red[500],
|
||||
color: theme.colors.statusDanger,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -363,9 +363,9 @@ const ThemedCheck = withUnistyles(Check);
|
||||
|
||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusMutedSuccess });
|
||||
const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusMutedWarning });
|
||||
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusMutedDanger });
|
||||
const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusSuccess });
|
||||
const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning });
|
||||
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
||||
|
||||
function InfoRow({
|
||||
icon: Icon,
|
||||
@@ -660,16 +660,16 @@ const styles = StyleSheet.create((theme) => ({
|
||||
checksStatusTextFailed: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.statusMutedDanger,
|
||||
color: theme.colors.statusDanger,
|
||||
},
|
||||
checksStatusTextPending: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.statusMutedWarning,
|
||||
color: theme.colors.statusWarning,
|
||||
},
|
||||
checksStatusTextPassed: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.statusMutedSuccess,
|
||||
color: theme.colors.statusSuccess,
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user