refactor: standardize status badges and fix composer whitespace handling
This commit is contained in:
@@ -22,6 +22,7 @@ import { getProviderIcon } from "@/components/provider-icons";
|
||||
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||
import { HighlightedText } from "@/components/ui/highlighted-text";
|
||||
import { StatusBadge, type StatusBadgeVariant } from "@/components/ui/status-badge";
|
||||
import type { AgentSearchMatch } from "@getpaseo/protocol/messages";
|
||||
import type { MatchRange } from "@getpaseo/protocol/search/text-match";
|
||||
|
||||
@@ -115,28 +116,10 @@ function SessionBadge({
|
||||
icon?: ReactElement;
|
||||
tone?: "neutral" | "warning" | "danger";
|
||||
}) {
|
||||
const badgeStyle = useMemo(
|
||||
() => [
|
||||
styles.badge,
|
||||
tone === "warning" && styles.badgeWarning,
|
||||
tone === "danger" && styles.badgeDanger,
|
||||
],
|
||||
[tone],
|
||||
);
|
||||
const badgeTextStyle = useMemo(
|
||||
() => [
|
||||
styles.badgeText,
|
||||
tone === "warning" && styles.badgeTextWarning,
|
||||
tone === "danger" && styles.badgeTextDanger,
|
||||
],
|
||||
[tone],
|
||||
);
|
||||
return (
|
||||
<View style={badgeStyle}>
|
||||
{icon}
|
||||
<Text style={badgeTextStyle}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
let variant: StatusBadgeVariant = "muted";
|
||||
if (tone === "warning") variant = "warning";
|
||||
else if (tone === "danger") variant = "error";
|
||||
return <StatusBadge label={label} variant={variant} leading={icon} />;
|
||||
}
|
||||
|
||||
function WorkspaceTitlePrefix({
|
||||
@@ -742,33 +725,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
marginLeft: theme.spacing[4],
|
||||
textAlign: "right" as const,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
gap: theme.spacing[1],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
badgeWarning: {
|
||||
backgroundColor: "rgba(245, 158, 11, 0.12)",
|
||||
},
|
||||
badgeDanger: {
|
||||
backgroundColor: "rgba(239, 68, 68, 0.14)",
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
badgeTextWarning: {
|
||||
color: theme.colors.palette.amber[500],
|
||||
},
|
||||
badgeTextDanger: {
|
||||
color: theme.colors.palette.red[300],
|
||||
},
|
||||
sheetOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { StatusBadge } from "./status-badge";
|
||||
|
||||
interface MountedBadge {
|
||||
root: Root;
|
||||
container: HTMLDivElement;
|
||||
}
|
||||
|
||||
const mountedBadges: MountedBadge[] = [];
|
||||
|
||||
function mountBadge(variant: "success" | "warning" | "error" | "muted"): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => root.render(<StatusBadge label="Status" variant={variant} />));
|
||||
mountedBadges.push({ root, container });
|
||||
|
||||
const badge = container.firstElementChild;
|
||||
if (!(badge instanceof HTMLElement)) {
|
||||
throw new Error("StatusBadge did not render a badge element");
|
||||
}
|
||||
return badge;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const mounted of mountedBadges.splice(0)) {
|
||||
act(() => mounted.root.unmount());
|
||||
mounted.container.remove();
|
||||
}
|
||||
});
|
||||
|
||||
describe("StatusBadge", () => {
|
||||
it.each(["success", "warning", "error", "muted"] as const)(
|
||||
"uses the semantic badge shell for the %s variant",
|
||||
(variant) => {
|
||||
const badge = mountBadge(variant);
|
||||
const style = getComputedStyle(badge);
|
||||
|
||||
expect(style.backgroundColor).toBe("rgb(228, 228, 231)");
|
||||
expect(style.borderColor).toBe("rgb(228, 228, 231)");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["success", "rgb(21, 128, 61)"],
|
||||
["warning", "rgb(217, 119, 6)"],
|
||||
["error", "rgb(185, 28, 28)"],
|
||||
["muted", "rgb(102, 102, 102)"],
|
||||
] as const)("uses the semantic %s signal for its text", (variant, expectedColor) => {
|
||||
const badge = mountBadge(variant);
|
||||
const text = badge.lastElementChild;
|
||||
if (!(text instanceof HTMLElement)) {
|
||||
throw new Error("StatusBadge did not render its label");
|
||||
}
|
||||
|
||||
expect(getComputedStyle(text).color).toBe(expectedColor);
|
||||
});
|
||||
});
|
||||
@@ -1,34 +1,29 @@
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo, type ReactNode } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
type StatusBadgeVariant = "success" | "error" | "muted";
|
||||
export type StatusBadgeVariant = "success" | "warning" | "error" | "muted";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
label: string;
|
||||
variant?: StatusBadgeVariant;
|
||||
leading?: ReactNode;
|
||||
}
|
||||
|
||||
export function StatusBadge({ label, variant = "muted" }: StatusBadgeProps) {
|
||||
const pillStyle = useMemo(
|
||||
() => [
|
||||
styles.pill,
|
||||
variant === "success" && styles.pillSuccess,
|
||||
variant === "error" && styles.pillError,
|
||||
],
|
||||
[variant],
|
||||
);
|
||||
export function StatusBadge({ label, variant = "muted", leading }: StatusBadgeProps) {
|
||||
const textStyle = useMemo(
|
||||
() => [
|
||||
styles.pillText,
|
||||
variant === "success" && styles.pillTextSuccess,
|
||||
variant === "warning" && styles.pillTextWarning,
|
||||
variant === "error" && styles.pillTextError,
|
||||
],
|
||||
[variant],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={pillStyle}>
|
||||
<View style={styles.pill}>
|
||||
{leading}
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -38,6 +33,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
pill: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
@@ -45,16 +41,6 @@ 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.statusSuccess}1a`,
|
||||
borderColor: `${theme.colors.statusSuccess}33`,
|
||||
},
|
||||
pillError: {
|
||||
backgroundColor: `${theme.colors.statusDanger}1a`,
|
||||
borderColor: `${theme.colors.statusDanger}33`,
|
||||
},
|
||||
pillText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
@@ -63,6 +49,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
pillTextSuccess: {
|
||||
color: theme.colors.statusSuccess,
|
||||
},
|
||||
pillTextWarning: {
|
||||
color: theme.colors.statusWarning,
|
||||
},
|
||||
pillTextError: {
|
||||
color: theme.colors.statusDanger,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user