refactor(app): give the host badge one home

The server glyph, its identity-colour table and the muted label lived inside
the sidebar meta row, and the three stores that decide whether a host has a
badge at all were assembled at one call site. Both are about to have a second
caller in the workspace header.

Move the badge to hosts/host-badge and the resolution to useHostBadges. The
badge now owns its glyph size, and the meta row matches it, since it is the one
item on that line that also appears off it.
This commit is contained in:
Mohamed Boudra
2026-08-04 12:04:32 +02:00
parent bf6cd09d6c
commit 22cb7f9996
6 changed files with 117 additions and 68 deletions
+72
View File
@@ -0,0 +1,72 @@
import { Text, View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Server } from "lucide-react-native";
import { HOST_COLORS, type HostBadgeModel, type HostColor } from "@/hosts/appearance";
import { identityColor } from "@/styles/identity-colors";
import type { Theme } from "@/styles/theme";
/**
* The glyph size the badge draws at. Rows that place the badge alongside their own glyphs
* match this so the line reads as one rank of peers.
*/
export const HOST_BADGE_ICON_SIZE = 12;
const ThemedServer = withUnistyles(Server);
const mutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
// Theme-independent, so the mapping per host color is fixed for the life of the module. A
// fresh `uniProps` identity on every render makes withUnistyles re-subscribe each pass.
const HOST_ICON_MAPPINGS: Record<HostColor, (theme: Theme) => { color: string }> = (() => {
const byColor = {} as Record<HostColor, (theme: Theme) => { color: string }>;
for (const color of HOST_COLORS) {
byColor[color] = color === "none" ? mutedMapping : () => ({ color: identityColor(color) });
}
return byColor;
})();
/**
* Which machine something lives on, drawn the same way everywhere it appears: a server glyph
* in the host's identity color, and the host's name when the host is configured to show one.
*
* A hostname is the least interesting thing on any line that carries it and the only one whose
* length nobody chose, so the badge yields space before its neighbours rather than alongside
* them — see `flexShrink` below.
*/
export function HostBadge({ badge }: { badge: HostBadgeModel }) {
return (
<View
style={styles.badge}
testID={`host-badge-${badge.serverId}`}
accessibilityLabel={badge.label}
>
<ThemedServer size={HOST_BADGE_ICON_SIZE} uniProps={HOST_ICON_MAPPINGS[badge.color]} />
{badge.showLabel ? (
<Text style={styles.label} numberOfLines={1}>
{badge.label}
</Text>
) : null}
</View>
);
}
const styles = StyleSheet.create((theme) => ({
badge: {
flexDirection: "row",
alignItems: "center",
gap: 3,
minWidth: 0,
// Outweighs a sibling's `flexShrink: 1` by enough that the badge is effectively fully
// squeezed before that sibling gives up its first pixel. An equal factor would split the
// loss in proportion to length, which hands the most space to the longest hostname —
// exactly backwards. The icon has a fixed width, so the badge never disappears outright.
flexShrink: 100,
},
label: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
lineHeight: 16,
flexShrink: 1,
minWidth: 0,
},
}));
+30
View File
@@ -0,0 +1,30 @@
import { useMemo } from "react";
import { useHosts } from "@/runtime/host-runtime";
import { useLocalDaemonServerId, useLocalDaemonServerIdState } from "@/hooks/use-is-local-daemon";
import { selectHostBadges, type HostBadgeModel } from "@/hosts/appearance";
/**
* Every host's badge, resolved from the three things that decide one: the host registry, which
* host is local, and each host's own appearance. `enabled` is the caller's own "off" — a surface
* that has its own reason to hide badges passes false rather than filtering the result, so the
* per-host setting stays the only thing that decides name vs icon vs hidden.
*/
export function useHostBadges({
enabled,
}: {
enabled: boolean;
}): ReadonlyMap<string, HostBadgeModel> {
const hosts = useHosts();
const localServerId = useLocalDaemonServerId();
const localDaemon = useLocalDaemonServerIdState();
return useMemo(
() =>
selectHostBadges({
hosts,
localServerId,
localHostResolutionPending: localDaemon.status !== "resolved",
enabled,
}),
[hosts, localDaemon.status, localServerId, enabled],
);
}