fix(app): distinguish selected sidebar rows
Give selected rows their own semantic surface so selection remains visible through hover while badges, status rings, and action scrims inherit the correct backdrop.
This commit is contained in:
@@ -388,8 +388,8 @@ function getProjectWorkspaceRowStyle({
|
||||
}) {
|
||||
return [
|
||||
styles.workspaceRow,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
isPressed && styles.workspaceRowPressed,
|
||||
];
|
||||
@@ -2751,7 +2751,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
...theme.shadow.md,
|
||||
},
|
||||
sidebarRowSelected: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
backgroundColor: theme.colors.surfaceSidebarSelected,
|
||||
},
|
||||
workspaceRowContainer: {
|
||||
position: "relative",
|
||||
|
||||
@@ -207,6 +207,8 @@ function getStatusBadgeBackdropStyle(backdrop: SidebarSurfaceBackdrop): ViewStyl
|
||||
return styles.statusBadgeOnSidebar;
|
||||
case "surfaceSidebarHover":
|
||||
return styles.statusBadgeOnSidebarHover;
|
||||
case "surfaceSidebarSelected":
|
||||
return styles.statusBadgeOnSidebarSelected;
|
||||
case "surface2":
|
||||
return styles.statusBadgeOnSurface2;
|
||||
}
|
||||
@@ -309,6 +311,7 @@ const styles = StyleSheet.create((theme) => {
|
||||
},
|
||||
statusBadgeOnSidebar: { backgroundColor: theme.colors.surfaceSidebar },
|
||||
statusBadgeOnSidebarHover: { backgroundColor: theme.colors.surfaceSidebarHover },
|
||||
statusBadgeOnSidebarSelected: { backgroundColor: theme.colors.surfaceSidebarSelected },
|
||||
statusBadgeOnSurface2: { backgroundColor: theme.colors.surface2 },
|
||||
statusDotRunning: statusDot("running"),
|
||||
statusDotFailed: statusDot("failed"),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getSidebarRowBackdrop } from "./sidebar-row-backdrop";
|
||||
|
||||
describe("getSidebarRowBackdrop", () => {
|
||||
it("keeps selected rows on the selected surface while hovered", () => {
|
||||
expect(getSidebarRowBackdrop({ selected: true, isHovered: true })).toBe(
|
||||
"surfaceSidebarSelected",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the hover surface for an unselected hovered row", () => {
|
||||
expect(getSidebarRowBackdrop({ isHovered: true })).toBe("surfaceSidebarHover");
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export function getSidebarRowBackdrop({
|
||||
isHovered?: boolean;
|
||||
}): SidebarSurfaceBackdrop {
|
||||
if (isDragging || isPressed) return "surface2";
|
||||
if (selected || isHovered) return "surfaceSidebarHover";
|
||||
if (selected) return "surfaceSidebarSelected";
|
||||
if (isHovered) return "surfaceSidebarHover";
|
||||
return "surfaceSidebar";
|
||||
}
|
||||
|
||||
@@ -1025,8 +1025,8 @@ function getStatusWorkspaceRowStyle({
|
||||
return [
|
||||
styles.workspaceRow,
|
||||
inStatusGroup && sidebarWorkspaceRowStyles.rowIndented,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
isPressed && styles.workspaceRowPressed,
|
||||
];
|
||||
@@ -1131,6 +1131,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
...theme.shadow.md,
|
||||
},
|
||||
sidebarRowSelected: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
backgroundColor: theme.colors.surfaceSidebarSelected,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -73,6 +73,7 @@ const ThemedTrailingActionScrimSvg = withUnistyles(TrailingActionScrimSvg);
|
||||
const scrimColorMappings: Record<SidebarSurfaceBackdrop, (theme: Theme) => { color: string }> = {
|
||||
surfaceSidebar: (theme) => ({ color: theme.colors.surfaceSidebar }),
|
||||
surfaceSidebarHover: (theme) => ({ color: theme.colors.surfaceSidebarHover }),
|
||||
surfaceSidebarSelected: (theme) => ({ color: theme.colors.surfaceSidebarSelected }),
|
||||
surface2: (theme) => ({ color: theme.colors.surface2 }),
|
||||
};
|
||||
|
||||
|
||||
@@ -494,8 +494,8 @@ function getWorkspaceRowStyle({
|
||||
}) {
|
||||
return [
|
||||
styles.workspaceRow,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isHovered && styles.workspaceRowHovered,
|
||||
selected && styles.sidebarRowSelected,
|
||||
isDragging && styles.workspaceRowDragging,
|
||||
isPressed && styles.workspaceRowPressed,
|
||||
];
|
||||
@@ -535,7 +535,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
...theme.shadow.md,
|
||||
},
|
||||
sidebarRowSelected: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
backgroundColor: theme.colors.surfaceSidebarSelected,
|
||||
},
|
||||
workspaceCreatingText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
|
||||
@@ -50,6 +50,8 @@ function getBackdropStyle(backdrop: SurfaceBackdrop | null | undefined) {
|
||||
return styles.backdropSurfaceSidebar;
|
||||
case "surfaceSidebarHover":
|
||||
return styles.backdropSurfaceSidebarHover;
|
||||
case "surfaceSidebarSelected":
|
||||
return styles.backdropSurfaceSidebarSelected;
|
||||
case "surface2":
|
||||
return styles.backdropSurface2;
|
||||
default:
|
||||
@@ -91,6 +93,7 @@ export const styles = StyleSheet.create((theme) => {
|
||||
backdropSurface1: { backgroundColor: theme.colors.surface1 },
|
||||
backdropSurfaceSidebar: { backgroundColor: theme.colors.surfaceSidebar },
|
||||
backdropSurfaceSidebarHover: { backgroundColor: theme.colors.surfaceSidebarHover },
|
||||
backdropSurfaceSidebarSelected: { backgroundColor: theme.colors.surfaceSidebarSelected },
|
||||
backdropSurface2: { backgroundColor: theme.colors.surface2 },
|
||||
|
||||
// The closed ring the quarter runs on. Same colour rather than a grey so the indicator is one
|
||||
|
||||
@@ -16,9 +16,10 @@ export type SurfaceBackdrop =
|
||||
| "surface1"
|
||||
| "surface2"
|
||||
| "surfaceSidebar"
|
||||
| "surfaceSidebarHover";
|
||||
| "surfaceSidebarHover"
|
||||
| "surfaceSidebarSelected";
|
||||
|
||||
export type SidebarSurfaceBackdrop = Extract<
|
||||
SurfaceBackdrop,
|
||||
"surfaceSidebar" | "surfaceSidebarHover" | "surface2"
|
||||
"surfaceSidebar" | "surfaceSidebarHover" | "surfaceSidebarSelected" | "surface2"
|
||||
>;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { darkPureBlackTheme, getNextThemePreference, THEME_OPTIONS } from "./theme";
|
||||
import {
|
||||
darkPureBlackTheme,
|
||||
darkTheme,
|
||||
getNextThemePreference,
|
||||
lightTheme,
|
||||
THEME_OPTIONS,
|
||||
} from "./theme";
|
||||
|
||||
describe("Theme catalog", () => {
|
||||
it("owns the picker and shortcut order", () => {
|
||||
@@ -30,9 +36,12 @@ describe("Pure black theme", () => {
|
||||
expect(darkPureBlackTheme.colors.accentBright).toBe("#7ccba0");
|
||||
});
|
||||
|
||||
it("keeps selected sidebar rows distinct from the black sidebar", () => {
|
||||
it("derives sidebar interaction surfaces from the surface scale", () => {
|
||||
expect(darkPureBlackTheme.colors.surfaceSidebar).toBe("#000000");
|
||||
expect(darkPureBlackTheme.colors.surfaceSidebarHover).toBe("#161616");
|
||||
expect(darkPureBlackTheme.colors.surfaceSidebarHover).toBe(darkPureBlackTheme.colors.surface1);
|
||||
expect(darkPureBlackTheme.colors.surfaceSidebarSelected).toBe(
|
||||
darkPureBlackTheme.colors.surface2,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps ANSI black output readable on its zero-luminance terminal background", () => {
|
||||
@@ -40,3 +49,13 @@ describe("Pure black theme", () => {
|
||||
expect(darkPureBlackTheme.colors.terminal.brightBlack).toBe("#8a8a8a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sidebar interaction surfaces", () => {
|
||||
it.each([lightTheme, darkTheme])(
|
||||
"derives hover and selection from the surface scale",
|
||||
(theme) => {
|
||||
expect(theme.colors.surfaceSidebarHover).toBe(theme.colors.surface1);
|
||||
expect(theme.colors.surfaceSidebarSelected).toBe(theme.colors.surface2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -204,17 +204,22 @@ const darkStatusDotColors = {
|
||||
};
|
||||
|
||||
// Semantic color tokens - Layer-based system
|
||||
const lightSemanticColors = {
|
||||
const lightSurfaces = {
|
||||
// Surfaces (layers) - shifted one step lighter
|
||||
surface0: "#ffffff", // App background
|
||||
surface1: "#fafafa", // Subtle hover (was zinc-100, now zinc-50)
|
||||
surface2: "#f4f4f5", // Elevated: badges, inputs, sheets (was zinc-200, now zinc-100)
|
||||
surface3: "#e4e4e7", // Highest elevation (was zinc-300, now zinc-200)
|
||||
surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300)
|
||||
} as const;
|
||||
|
||||
const lightSemanticColors = {
|
||||
...lightSurfaces,
|
||||
surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
|
||||
surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main)
|
||||
surfaceSidebarHover: "#e9e9ec", // Sidebar hover (darker in light mode)
|
||||
surfaceWorkspace: "#ffffff", // Workspace main background
|
||||
surfaceSidebar: lightSurfaces.surface2, // Sidebar background (darker than main)
|
||||
surfaceSidebarHover: lightSurfaces.surface1,
|
||||
surfaceSidebarSelected: lightSurfaces.surface2,
|
||||
surfaceWorkspace: lightSurfaces.surface0, // Workspace main background
|
||||
|
||||
// Text
|
||||
foreground: "#1a1a1e",
|
||||
@@ -294,7 +299,6 @@ interface DarkThemeConfig {
|
||||
surface4: string;
|
||||
surfaceDiffEmpty: string;
|
||||
surfaceSidebar: string;
|
||||
surfaceSidebarHover: string;
|
||||
foregroundMuted: string;
|
||||
foregroundExtraMuted: string;
|
||||
border: string;
|
||||
@@ -333,7 +337,8 @@ function buildDarkSemanticColors(tint: DarkThemeConfig) {
|
||||
surface4: tint.surface4,
|
||||
surfaceDiffEmpty: tint.surfaceDiffEmpty,
|
||||
surfaceSidebar: tint.surfaceSidebar,
|
||||
surfaceSidebarHover: tint.surfaceSidebarHover,
|
||||
surfaceSidebarHover: tint.surface1,
|
||||
surfaceSidebarSelected: tint.surface2,
|
||||
surfaceWorkspace: tint.surface1,
|
||||
|
||||
foreground: "#fafafa",
|
||||
@@ -397,7 +402,6 @@ const paseoDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#595B5B",
|
||||
surfaceDiffEmpty: "#252827",
|
||||
surfaceSidebar: "#141716",
|
||||
surfaceSidebarHover: "#1c1f1e",
|
||||
foregroundMuted: "#A1A5A4",
|
||||
foregroundExtraMuted: "#717574",
|
||||
border: "#252B2A",
|
||||
@@ -418,7 +422,6 @@ const zincDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#52525b",
|
||||
surfaceDiffEmpty: "#242427",
|
||||
surfaceSidebar: "#131316",
|
||||
surfaceSidebarHover: "#1b1b1e",
|
||||
foregroundMuted: "#a1a1aa",
|
||||
foregroundExtraMuted: "#71717a",
|
||||
border: "#27272a",
|
||||
@@ -440,7 +443,6 @@ const midnightDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#535564",
|
||||
surfaceDiffEmpty: "#222430",
|
||||
surfaceSidebar: "#121420",
|
||||
surfaceSidebarHover: "#1a1c28",
|
||||
foregroundMuted: "#9a9db0",
|
||||
foregroundExtraMuted: "#6b6e82",
|
||||
border: "#242636",
|
||||
@@ -461,7 +463,6 @@ const claudeDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#605d5b",
|
||||
surfaceDiffEmpty: "#2a2826",
|
||||
surfaceSidebar: "#1a1918",
|
||||
surfaceSidebarHover: "#222120",
|
||||
foregroundMuted: "#ada9a5",
|
||||
foregroundExtraMuted: "#78746f",
|
||||
border: "#2c2a27",
|
||||
@@ -482,7 +483,6 @@ const ghosttyDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#5b6175",
|
||||
surfaceDiffEmpty: "#323643",
|
||||
surfaceSidebar: "#21252d",
|
||||
surfaceSidebarHover: "#292d36",
|
||||
foregroundMuted: "#c8ccd8",
|
||||
foregroundExtraMuted: "#a0a4b2",
|
||||
border: "#353a47",
|
||||
@@ -656,8 +656,6 @@ const pureBlackDarkColors = buildDarkSemanticColors({
|
||||
surface4: "#2d2d2d",
|
||||
surfaceDiffEmpty: "#0c0c0c",
|
||||
surfaceSidebar: "#000000",
|
||||
// Selected sidebar rows share this surface with hover, so it must remain visible at rest.
|
||||
surfaceSidebarHover: "#161616",
|
||||
foregroundMuted: "#a1a1aa",
|
||||
foregroundExtraMuted: "#71717a",
|
||||
border: "#1c1c1c",
|
||||
|
||||
Reference in New Issue
Block a user