import { useCallback, useMemo, useState } from "react";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { ChevronDown, Pencil } from "lucide-react-native";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AdaptiveRenameModal } from "@/components/rename-modal";
import { WorkspaceMetaRow } from "@/components/sidebar/workspace-meta-row";
import { useToast } from "@/contexts/toast-context";
import { SettingsSection } from "@/screens/settings/settings-section";
import {
HOST_BADGE_DISPLAYS,
HOST_COLORS,
resolveHostBadgeDisplay,
type HostBadgeDisplay,
type HostColor,
} from "@/hosts/appearance";
import { useLocalDaemonServerIdState } from "@/hooks/use-is-local-daemon";
import { useHostMutations } from "@/runtime/host-runtime";
import { identityColor } from "@/styles/identity-colors";
import { settingsStyles } from "@/styles/settings";
import { ICON_SIZE, type Theme } from "@/styles/theme";
import type { HostProfile } from "@/types/host-connection";
const ThemedChevronDown = withUnistyles(ChevronDown);
const ThemedPencil = withUnistyles(Pencil);
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
function dropdownTriggerStyle({ pressed }: PressableStateCallbackType) {
return pressed ? [styles.trigger, styles.triggerPressed] : styles.trigger;
}
function HostRenameButton({ host }: { host: HostProfile }) {
const { t } = useTranslation();
const { renameHost } = useHostMutations();
const [isEditing, setIsEditing] = useState(false);
const handleSubmit = useCallback(
async (value: string) => {
const nextLabel = value.trim();
if (nextLabel === host.label.trim()) return;
await renameHost(host.serverId, nextLabel);
},
[host.label, host.serverId, renameHost],
);
const openEditor = useCallback(() => setIsEditing(true), []);
const closeEditor = useCallback(() => setIsEditing(false), []);
return (
<>
>
);
}
function colorLabel(t: TFunction, color: HostColor): string {
return t(`settings.host.appearance.color.options.${color}`);
}
function badgeDisplayLabel(t: TFunction, display: HostBadgeDisplay): string {
return t(`settings.host.appearance.badge.options.${display}`);
}
function ColorSwatch({ color }: { color: HostColor }) {
const swatchStyle = useMemo(
() => [styles.swatch, color === "none" ? null : { backgroundColor: identityColor(color) }],
[color],
);
return ;
}
function ColorMenuItem({
color,
selected,
onChange,
}: {
color: HostColor;
selected: boolean;
onChange: (color: HostColor) => void;
}) {
const { t } = useTranslation();
const handleSelect = useCallback(() => onChange(color), [color, onChange]);
const leading = useMemo(() => , [color]);
return (
{colorLabel(t, color)}
);
}
function ColorRow({ color, onChange }: { color: HostColor; onChange: (color: HostColor) => void }) {
const { t } = useTranslation();
const selectedLabel = colorLabel(t, color);
return (
{t("settings.host.appearance.color.label")}
{selectedLabel}
{HOST_COLORS.map((option) => (
))}
);
}
function BadgeDisplayRow({
badgeDisplay,
onChange,
}: {
badgeDisplay: HostBadgeDisplay;
onChange: (badgeDisplay: HostBadgeDisplay) => void;
}) {
const { t } = useTranslation();
const selectedLabel = badgeDisplayLabel(t, badgeDisplay);
return (
{t("settings.host.appearance.badge.label")}
{selectedLabel}
{HOST_BADGE_DISPLAYS.map((option) => (
))}
);
}
function BadgeDisplayMenuItem({
display,
selected,
onChange,
}: {
display: HostBadgeDisplay;
selected: boolean;
onChange: (display: HostBadgeDisplay) => void;
}) {
const { t } = useTranslation();
const handleSelect = useCallback(() => onChange(display), [display, onChange]);
return (
{badgeDisplayLabel(t, display)}
);
}
/**
* Shows the badge exactly as the sidebar will draw it, next to a sample workspace title on
* the sidebar surface. The real component, never a restyled copy — a preview that can drift
* from the thing it previews is worse than no preview.
*/
function BadgePreview({
host,
badgeDisplay,
}: {
host: HostProfile;
badgeDisplay: HostBadgeDisplay;
}) {
const { t } = useTranslation();
const hostBadge = useMemo(
() =>
badgeDisplay === "hidden"
? null
: {
serverId: host.serverId,
label: host.label,
color: host.appearance.color,
showLabel: badgeDisplay === "name",
},
[badgeDisplay, host.serverId, host.label, host.appearance.color],
);
// The real sidebar row, so the preview can't drift from what the setting actually does.
return (
{t("settings.host.appearance.preview.workspaceName")}
);
}
export function HostAppearanceSection({ host }: { host: HostProfile }) {
const { t } = useTranslation();
const toast = useToast();
const { setHostColor, setHostBadgeDisplay } = useHostMutations();
const localDaemon = useLocalDaemonServerIdState();
const isLocalHost = localDaemon.status === "resolved" && localDaemon.serverId === host.serverId;
const badgeDisplay = resolveHostBadgeDisplay({
appearance: host.appearance,
isLocalHost,
localHostResolutionPending: localDaemon.status !== "resolved",
});
const handleColorChange = useCallback(
async (color: HostColor) => {
try {
await setHostColor(host.serverId, color);
} catch {
toast.error(t("errors.unableToSave"));
}
},
[host.serverId, setHostColor, t, toast],
);
const handleBadgeDisplayChange = useCallback(
async (next: HostBadgeDisplay) => {
try {
await setHostBadgeDisplay(host.serverId, next);
} catch {
toast.error(t("errors.unableToSave"));
}
},
[host.serverId, setHostBadgeDisplay, t, toast],
);
return (
{t("settings.host.appearance.name.label")}
{host.label}
{badgeDisplay === null ? null : (
<>
>
)}
);
}
const styles = StyleSheet.create((theme) => ({
trigger: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
triggerPressed: {
opacity: 0.85,
},
triggerText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
},
swatch: {
width: ICON_SIZE.md,
height: ICON_SIZE.md,
borderRadius: ICON_SIZE.md / 2,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
renameButton: {
padding: theme.spacing[1],
borderRadius: theme.borderRadius.md,
},
nameValue: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
flexShrink: 1,
},
nameText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
flexShrink: 1,
},
preview: {
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing[1],
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
borderTopWidth: theme.borderWidth[1],
borderTopColor: theme.colors.border,
backgroundColor: theme.colors.surfaceSidebar,
},
previewTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
flexShrink: 1,
},
}));