Files
paseo/packages/app/src/components/status-ring/frame.tsx
T
Mohamed Boudra d31372ef12 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.
2026-08-16 17:01:17 +02:00

125 lines
4.7 KiB
TypeScript

import type { ReactNode } from "react";
import { StyleSheet as RNStyleSheet, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import type { SurfaceBackdrop } from "@/styles/surface-backdrop";
import { getStatusDotColor } from "@/utils/status-dot-color";
import {
STATUS_RING_FRAME_SIZE,
STATUS_RING_HEAD_OPACITY,
STATUS_RING_SIZE,
STATUS_RING_STROKE,
STATUS_RING_TRACK_OPACITY,
} from "@/components/status-ring/geometry";
import { STATUS_INDICATOR_FILLED_DOT_SIZE } from "@/utils/status-indicator-geometry";
export interface StatusRingProps {
// The surface the ring is drawn on top of, named rather than resolved, because theme colours
// are only legible inside `StyleSheet.create` — see docs/unistyles.md. The frame fills itself
// with this so the ring reads as a hole punched in whatever it overlaps: the agent icon under a
// tab dot, the project tile under a sidebar badge. Leave it out where the ring sits on flat
// background and has nothing to knock out.
backdrop?: SurfaceBackdrop | null;
}
/**
* The static half of the running indicator: the knockout, the track, and the centre dot. The
* platform entry points supply the rotating quarter and own only how it is driven.
*
* The ring has no colour prop. It only ever means one thing — an agent is running — so it reads
* that one colour out of the theme itself. Passing the colour in would materialise it at the call
* site, outside anything Unistyles tracks, and leave the mark in the old theme until something
* incidental re-rendered the row.
*/
export function StatusRingFrame({ backdrop, children }: StatusRingProps & { children: ReactNode }) {
return (
<View style={[styles.frame, getBackdropStyle(backdrop)]}>
<View style={styles.track} />
{children}
<View style={styles.centerDot} />
</View>
);
}
function getBackdropStyle(backdrop: SurfaceBackdrop | null | undefined) {
switch (backdrop) {
case "surface0":
return styles.backdropSurface0;
case "surface1":
return styles.backdropSurface1;
case "surfaceSidebar":
return styles.backdropSurfaceSidebar;
case "surfaceSidebarHover":
return styles.backdropSurfaceSidebarHover;
case "surfaceSidebarSelected":
return styles.backdropSurfaceSidebarSelected;
case "surface2":
return styles.backdropSurface2;
default:
return null;
}
}
// The rotating quarter's geometry, deliberately plain React Native rather than Unistyles: on
// native this style lands on a Reanimated `Animated.View`, and applying a theme-tracked style to
// one crashes when the theme changes — Unistyles and Reanimated both mutate the same native node.
// See docs/unistyles.md. The colour it needs lives on `styles.arc`, one level in, on a view
// Reanimated never touches.
export const rotatorStyles = RNStyleSheet.create({
rotator: {
position: "absolute",
width: STATUS_RING_SIZE,
height: STATUS_RING_SIZE,
},
});
export const styles = StyleSheet.create((theme) => {
const circle = {
width: STATUS_RING_SIZE,
height: STATUS_RING_SIZE,
borderRadius: theme.borderRadius.full,
borderWidth: STATUS_RING_STROKE,
} as const;
const runningColor = getStatusDotColor({ theme, bucket: "running" }) ?? undefined;
return {
frame: {
width: STATUS_RING_FRAME_SIZE,
height: STATUS_RING_FRAME_SIZE,
borderRadius: theme.borderRadius.full,
alignItems: "center",
justifyContent: "center",
},
backdropSurface0: { backgroundColor: theme.colors.surface0 },
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
// object at one hue; the opacity is what puts it behind the quarter instead of competing.
track: {
...circle,
position: "absolute",
borderColor: runningColor,
opacity: STATUS_RING_TRACK_OPACITY,
},
// The moving quarter. Three sides transparent leaves the top border alone, which on a fully
// rounded box is a 90° arc — the edge that makes the rotation legible against the track.
arc: {
...circle,
borderColor: "transparent",
borderTopColor: runningColor,
opacity: STATUS_RING_HEAD_OPACITY,
},
centerDot: {
width: STATUS_INDICATOR_FILLED_DOT_SIZE,
height: STATUS_INDICATOR_FILLED_DOT_SIZE,
borderRadius: theme.borderRadius.full,
backgroundColor: runningColor,
},
};
});