From 4406bf588e468c3604b9c94544574ebf38d79c01 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 2 Aug 2026 21:32:42 +0200 Subject: [PATCH] fix(app): preserve host badge identity Keep host badges visible under row pressure and apply the selected host color consistently to the icon, label, and tinted background. --- .../app/e2e/browser/host-appearance.spec.ts | 1 + packages/app/e2e/support/helpers/hosts.ts | 37 ++++++++++++++++++- .../app/src/components/sidebar/host-badge.tsx | 35 +++++++++++++++--- packages/app/src/styles/identity-colors.ts | 5 +++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/app/e2e/browser/host-appearance.spec.ts b/packages/app/e2e/browser/host-appearance.spec.ts index 64575ecb9..d97189b60 100644 --- a/packages/app/e2e/browser/host-appearance.spec.ts +++ b/packages/app/e2e/browser/host-appearance.spec.ts @@ -129,6 +129,7 @@ test.describe("Host appearance", () => { expect(titleBox).not.toBeNull(); expect(rowBox).not.toBeNull(); expect(kebabBox).not.toBeNull(); + await expect(badge).toHaveCSS("flex-shrink", "0"); expect(titleBox!.width).toBeGreaterThan(0); expect(kebabBox!.x + kebabBox!.width).toBeLessThanOrEqual(rowBox!.x + rowBox!.width); }); diff --git a/packages/app/e2e/support/helpers/hosts.ts b/packages/app/e2e/support/helpers/hosts.ts index f5c7d2156..8536eef5b 100644 --- a/packages/app/e2e/support/helpers/hosts.ts +++ b/packages/app/e2e/support/helpers/hosts.ts @@ -144,6 +144,32 @@ export async function selectAllHostsFilter(page: Page): Promise { await page.getByTestId("sidebar-host-filter-all").click(); } +// Playwright reports resolved colors, so the expected value is derived from the same identity +// table the app reads. A hex literal in the test would be a second copy of the palette. +function toRgb(hex: string): string { + const { r, g, b } = parseHex(hex); + return `rgb(${r}, ${g}, ${b})`; +} + +function toRgba(hex: string, alpha: number): string { + const { r, g, b } = parseHex(hex); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +interface RgbChannels { + r: number; + g: number; + b: number; +} + +function parseHex(hex: string): RgbChannels { + return { + r: Number.parseInt(hex.slice(1, 3), 16), + g: Number.parseInt(hex.slice(3, 5), 16), + b: Number.parseInt(hex.slice(5, 7), 16), + }; +} + export async function openHostAppearanceSettings(page: Page, serverId: string): Promise { await openSettings(page); await selectSettingsHost(page, serverId); @@ -222,7 +248,11 @@ export async function expectHostBadgeTinted( ): Promise { const badge = hostBadge(page, target); await expect(badge).toBeVisible({ timeout: 15_000 }); - await expect(badge.getByText(target.hostName)).toHaveCSS("color", "rgb(113, 113, 122)"); + await expect(badge.getByText(target.hostName)).toHaveCSS( + "color", + toRgb(identityColor(target.color)), + ); + await expect(badge).toHaveCSS("background-color", toRgba(identityColor(target.color), 0.1)); await expect(badge.locator("svg")).toHaveAttribute("stroke", identityColor(target.color)); } @@ -234,6 +264,11 @@ export async function expectHostAppearancePreview( await expect(preview).toBeVisible(); const badge = preview.getByTestId(`sidebar-host-badge-${input.serverId}`); await expect(badge).toHaveText(input.hostName); + await expect(badge.getByText(input.hostName)).toHaveCSS( + "color", + toRgb(identityColor(input.color)), + ); + await expect(badge).toHaveCSS("background-color", toRgba(identityColor(input.color), 0.1)); await expect(badge.locator("svg")).toHaveAttribute("stroke", identityColor(input.color)); } diff --git a/packages/app/src/components/sidebar/host-badge.tsx b/packages/app/src/components/sidebar/host-badge.tsx index 3724158d9..1d52c795f 100644 --- a/packages/app/src/components/sidebar/host-badge.tsx +++ b/packages/app/src/components/sidebar/host-badge.tsx @@ -1,13 +1,32 @@ -import { Text, View } from "react-native"; +import { Text, View, type TextStyle, type ViewStyle } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { Server } from "lucide-react-native"; import { HOST_COLORS, type HostColor } from "@/hosts/appearance"; -import { identityColor } from "@/styles/identity-colors"; +import { identityColor, identityTint } from "@/styles/identity-colors"; import type { Theme } from "@/styles/theme"; const ThemedServer = withUnistyles(Server); const MAX_NAMED_BADGE_WIDTH = 96; +interface HostBadgeColorOverride { + badge: ViewStyle | null; + text: TextStyle | null; +} + +function buildColorOverrides(): Record { + const byColor = {} as Record; + for (const color of HOST_COLORS) { + byColor[color] = + color === "none" + ? { badge: null, text: null } + : { + badge: { backgroundColor: identityTint(color) }, + text: { color: identityColor(color) }, + }; + } + return byColor; +} + // The identity table is theme-independent, so its icon mapping is fixed for the life of the // module. It must not be rebuilt per render: a fresh `uniProps` identity makes withUnistyles // re-subscribe every pass. @@ -22,6 +41,7 @@ function buildIconMappings(): Record { color: strin return byColor; } +const COLOR_OVERRIDES = buildColorOverrides(); const ICON_MAPPINGS = buildIconMappings(); /** @@ -41,14 +61,19 @@ export function HostBadge({ color: HostColor; showLabel: boolean; }) { + const override = COLOR_OVERRIDES[color]; return ( {showLabel ? ( - + {label} ) : null} @@ -71,7 +96,7 @@ const hostBadgeStyles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface2, maxWidth: MAX_NAMED_BADGE_WIDTH, minWidth: 0, - flexShrink: 1, + flexShrink: 0, }, // Icon-only drops the label, so the asymmetric padding that made room for text leaves a // lopsided pill. Symmetric padding recenters the icon. diff --git a/packages/app/src/styles/identity-colors.ts b/packages/app/src/styles/identity-colors.ts index a2488f025..f8209373f 100644 --- a/packages/app/src/styles/identity-colors.ts +++ b/packages/app/src/styles/identity-colors.ts @@ -44,6 +44,11 @@ export function identityColor(name: IdentityColorName): string { return IDENTITY_COLORS[name]; } +/** The same hue at 10% alpha, for fills that sit behind text of the full-strength color. */ +export function identityTint(name: IdentityColorName): string { + return `${IDENTITY_COLORS[name]}1a`; +} + function hashIdentityKey(key: string): number { let hash = 0; for (const character of key) {