From 2a2e6daed626f3e721c6540cb577aed6790ae376 Mon Sep 17 00:00:00 2001 From: jevb Date: Fri, 27 Mar 2026 13:10:21 +0100 Subject: [PATCH] chore: stage QuickSwitchOverlay files missed from earlier commit --- .../src/components/QuickSwitchOverlay.ts | 124 ++++++++++++++++++ .../components/QuickSwitchOverlay.test.ts | 80 +++++++++++ 2 files changed, 204 insertions(+) create mode 100644 Client/tauri-client/src/components/QuickSwitchOverlay.ts create mode 100644 Client/tauri-client/tests/unit/components/QuickSwitchOverlay.test.ts diff --git a/Client/tauri-client/src/components/QuickSwitchOverlay.ts b/Client/tauri-client/src/components/QuickSwitchOverlay.ts new file mode 100644 index 00000000..70c9fb1b --- /dev/null +++ b/Client/tauri-client/src/components/QuickSwitchOverlay.ts @@ -0,0 +1,124 @@ +/** + * QuickSwitchOverlay — modal for switching between saved server profiles. + * Appears when the user clicks the disconnect/switch button in UserBar. + * Uses @lib/dom helpers exclusively. Never sets innerHTML with user content. + */ + +import { createElement, appendChildren, setText } from "@lib/dom"; +import type { MountableComponent } from "@lib/safe-render"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface QuickSwitchProfile { + readonly name: string; + readonly host: string; +} + +export interface QuickSwitchOverlayOptions { + readonly profiles: readonly QuickSwitchProfile[]; + readonly currentHost: string; + readonly onSwitch: (host: string, name: string) => void; + readonly onAddServer: () => void; + readonly onClose: () => void; +} + +// --------------------------------------------------------------------------- +// Factory +// --------------------------------------------------------------------------- + +export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): MountableComponent { + const ac = new AbortController(); + let root: HTMLDivElement | null = null; + + function mount(container: Element): void { + root = createElement("div", { + class: "quick-switch-backdrop", + "data-testid": "quick-switch-overlay", + }); + + // Close on backdrop click (not on modal content) + root.addEventListener("click", (e) => { + if (e.target === root) options.onClose(); + }, { signal: ac.signal }); + + const modal = createElement("div", { class: "quick-switch-modal" }); + + // Header + const header = createElement("div", { class: "quick-switch-header" }); + const title = createElement("h2", {}, "Switch Server"); + const subtitle = createElement("p", { class: "quick-switch-subtitle" }, + "You\u2019ll disconnect from the current server."); + appendChildren(header, title, subtitle); + + // Server list + const list = createElement("div", { class: "quick-switch-list" }); + + for (const profile of options.profiles) { + const isCurrent = profile.host === options.currentHost; + const item = createElement("div", { + class: `quick-switch-item${isCurrent ? " current" : ""}`, + "data-testid": "server-item", + "data-host": profile.host, + }); + + const icon = createElement("div", { class: "quick-switch-icon" }); + setText(icon, profile.name.charAt(0).toUpperCase()); + + const info = createElement("div", { class: "quick-switch-info" }); + const nameEl = createElement("div", { class: "quick-switch-name" }, profile.name); + const hostEl = createElement("div", { class: "quick-switch-host" }, + `${profile.host}${isCurrent ? " \u00B7 Connected" : ""}`); + appendChildren(info, nameEl, hostEl); + + if (isCurrent) { + const dot = createElement("div", { class: "quick-switch-connected-dot" }); + appendChildren(item, icon, info, dot); + } else { + appendChildren(item, icon, info); + item.addEventListener("click", () => { + options.onSwitch(profile.host, profile.name); + }, { signal: ac.signal }); + } + + list.appendChild(item); + } + + // Add new server button + const addItem = createElement("div", { + class: "quick-switch-item add-new", + "data-testid": "add-server-btn", + }); + const addIcon = createElement("div", { class: "quick-switch-icon add" }, "+"); + const addInfo = createElement("div", { class: "quick-switch-info" }); + const addName = createElement("div", { class: "quick-switch-name" }, "Add new server"); + const addHost = createElement("div", { class: "quick-switch-host" }, "Connect to another OwnCord server"); + appendChildren(addInfo, addName, addHost); + appendChildren(addItem, addIcon, addInfo); + addItem.addEventListener("click", () => options.onAddServer(), { signal: ac.signal }); + list.appendChild(addItem); + + // Footer + const footer = createElement("div", { class: "quick-switch-footer" }, "Press Escape to cancel"); + + appendChildren(modal, header, list, footer); + root.appendChild(modal); + container.appendChild(root); + + // Escape key closes overlay + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") options.onClose(); + }, { signal: ac.signal }); + } + + function destroy(): void { + ac.abort(); + if (root !== null) { + root.remove(); + root = null; + } + } + + return { mount, destroy }; +} diff --git a/Client/tauri-client/tests/unit/components/QuickSwitchOverlay.test.ts b/Client/tauri-client/tests/unit/components/QuickSwitchOverlay.test.ts new file mode 100644 index 00000000..8b85a426 --- /dev/null +++ b/Client/tauri-client/tests/unit/components/QuickSwitchOverlay.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createQuickSwitchOverlay } from "@components/QuickSwitchOverlay"; + +describe("QuickSwitchOverlay", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); + + it("renders server list from profiles", () => { + const overlay = createQuickSwitchOverlay({ + profiles: [ + { name: "My Server", host: "localhost:8443" }, + { name: "LAN Party", host: "10.0.0.5:8443" }, + ], + currentHost: "localhost:8443", + onSwitch: vi.fn(), + onAddServer: vi.fn(), + onClose: vi.fn(), + }); + overlay.mount(container); + const items = container.querySelectorAll("[data-testid='server-item']"); + expect(items.length).toBe(2); + overlay.destroy?.(); + }); + + it("highlights current server", () => { + const overlay = createQuickSwitchOverlay({ + profiles: [{ name: "My Server", host: "localhost:8443" }], + currentHost: "localhost:8443", + onSwitch: vi.fn(), + onAddServer: vi.fn(), + onClose: vi.fn(), + }); + overlay.mount(container); + const current = container.querySelector("[data-testid='server-item'].current"); + expect(current).not.toBeNull(); + overlay.destroy?.(); + }); + + it("calls onSwitch when clicking a different server", () => { + const onSwitch = vi.fn(); + const overlay = createQuickSwitchOverlay({ + profiles: [ + { name: "Server A", host: "a:8443" }, + { name: "Server B", host: "b:8443" }, + ], + currentHost: "a:8443", + onSwitch, + onAddServer: vi.fn(), + onClose: vi.fn(), + }); + overlay.mount(container); + const items = container.querySelectorAll("[data-testid='server-item']"); + (items[1] as HTMLElement).click(); + expect(onSwitch).toHaveBeenCalledWith("b:8443", "Server B"); + overlay.destroy?.(); + }); + + it("calls onClose on escape key", () => { + const onClose = vi.fn(); + const overlay = createQuickSwitchOverlay({ + profiles: [{ name: "My Server", host: "localhost:8443" }], + currentHost: "localhost:8443", + onSwitch: vi.fn(), + onAddServer: vi.fn(), + onClose, + }); + overlay.mount(container); + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); + expect(onClose).toHaveBeenCalled(); + overlay.destroy?.(); + }); +});