/* eslint-disable react-refresh/only-export-components */ import { useEffect, useRef, type ComponentType, type ReactNode } from "react"; import { KeyRound, Settings, Terminal } from "lucide-react"; import { byOrderThenId, createRegistry } from "@/lib/registry"; /** Core host editor tabs. Plugins add theirs through registerHostEditorSection. */ export type CoreHostTabId = "general" | "ssh"; export type HostTabId = CoreHostTabId | (string & {}); export type CredentialTabId = "general" | "auth"; type HostTab = { id: HostTabId; label: string; icon: ReactNode; }; type CredentialTab = { id: CredentialTabId; label: string; icon: ReactNode; }; export interface HostEditorSectionRenderProps { // The editor form is a plain object. Sections read the fields they own. // eslint-disable-next-line @typescript-eslint/no-explicit-any form: any; setField: (key: string, value: unknown) => void; /** Applies several fields at once, against the latest form. */ updateForm: ( patch: (form: Record) => Record, ) => void; host?: unknown; credentials?: unknown[]; adminTargetUserId?: string; protocols: Record; } /** * A host editor tab contributed at runtime. "top" tabs sit in the main strip * beside General and SSH; "ssh" tabs sit in the SSH group's second strip. */ export interface HostEditorSectionDef { id: string; pluginId?: string; group: "top" | "ssh"; labelKey: string; /** Already-translated label, used instead of labelKey when set. */ label?: string; icon?: ComponentType<{ className?: string }>; /** Position among the group's tabs, core ones included. */ order?: number; /** Whether to offer the tab for these protocols. Defaults to always. */ visible?: (protocols: Record) => boolean; component: ComponentType; } const sections = createRegistry(byOrderThenId); export const registerHostEditorSection = sections.register; export const unregisterHostEditorSection = sections.unregister; export const getHostEditorSection = sections.get; export const hostEditorSectionList = sections.list; export const useHostEditorSections = sections.useList; export const resetHostEditorSections = sections.reset; const CORE_SSH_GROUP = new Set(["ssh"]); /** Whether a tab lives in the SSH group's second strip. */ export function isSshGroupTab(id: string): boolean { return CORE_SSH_GROUP.has(id) || sections.get(id)?.group === "ssh"; } function sectionVisible( section: HostEditorSectionDef, protocols: Record, ): boolean { if (!section.visible) return true; try { return section.visible(protocols); } catch { return false; } } function mergeByOrder( core: (HostTab & { order: number })[], group: "top" | "ssh", t: (key: string) => string, protocols: Record | undefined, ): HostTab[] { const registered = sections .list() .filter( (section) => section.group === group && (!protocols || sectionVisible(section, protocols)), ) .map((section) => { const Icon = section.icon; return { id: section.id, label: section.label ?? t(section.labelKey), icon: Icon ? : null, order: section.order ?? 1000, }; }); return [...core, ...registered] .sort((a, b) => a.order - b.order) .map(({ order: _order, ...tab }) => tab); } /** * The main strip. With `protocols`, registered tabs are filtered by their * `visible`; without, every registered top tab is listed. */ export function makeHostTabs( t: (key: string) => string, protocols?: Record, ): HostTab[] { return mergeByOrder( [ { id: "general", label: t("hosts.tabGeneral"), icon: , order: 0, }, { id: "ssh", label: t("hosts.tabSsh"), icon: , order: 10, }, ], "top", t, protocols, ); } export function makeHostSshSubTabs( t: (key: string) => string, protocols?: Record, ): HostTab[] { return mergeByOrder( [ { id: "ssh", label: t("hosts.tabGeneral"), icon: , order: 0, }, ], "ssh", t, protocols, ); } export function makeCredentialTabs( t: (key: string) => string, ): CredentialTab[] { return [ { id: "general", label: t("hosts.tabGeneral"), icon: , }, { id: "auth", label: t("hosts.tabAuthentication"), icon: , }, ]; } export function TabStrip({ tabs, activeTab, onTabChange, isActive, variant = "primary", }: { tabs: { id: string; label: string; icon: ReactNode }[]; activeTab: string; onTabChange: (id: string) => void; isActive?: (id: string) => boolean; variant?: "primary" | "secondary"; }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const onWheel = (e: WheelEvent) => { if (e.deltaY === 0) return; e.preventDefault(); el.scrollLeft += e.deltaY; }; el.addEventListener("wheel", onWheel, { passive: false }); return () => el.removeEventListener("wheel", onWheel); }, []); const renderTab = (tab: (typeof tabs)[0]) => { const active = isActive ? isActive(tab.id) : activeTab === tab.id; return ( ); }; return (
{tabs.map(renderTab)}
); }