From 86e90e52118dfea8ba654056d1dc4acca19c1773 Mon Sep 17 00:00:00 2001 From: Renat Date: Thu, 19 Mar 2026 03:54:19 +0500 Subject: [PATCH] feat: refine dark theme experience --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src/App.css | 386 ++++++++++++++++++++ src/App.tsx | 550 ++++++++++++++++++----------- src/components/AccountCard.tsx | 128 +++---- src/components/AddAccountModal.tsx | 115 +++--- src/components/UpdateChecker.tsx | 78 ++-- src/components/UsageBar.tsx | 46 +-- 8 files changed, 923 insertions(+), 384 deletions(-) diff --git a/package.json b/package.json index f302c55..048c221 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "tauri": "sh ./scripts/tauri.sh" + "tauri": "tauri" }, "dependencies": { "@tauri-apps/api": "^2.10.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f062a82..639debf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "codex-switcher" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/src/App.css b/src/App.css index f1d8c73..fb84385 100644 --- a/src/App.css +++ b/src/App.css @@ -1 +1,387 @@ @import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +:root { + --theme-bg: #f8fafc; + --theme-surface: #ffffff; + --theme-surface-elevated: #f3f6fa; + --theme-surface-hover: #e9eef5; + + --theme-border-subtle: #d8e0ea; + --theme-border-strong: #b8c4d3; + + --theme-text-primary: #11151b; + --theme-text-secondary: #526072; + --theme-text-muted: #708096; + --theme-text-on-light: #11151b; + + --theme-primary-bg: #11151b; + --theme-primary-hover: #202735; + --theme-primary-text: #ffffff; + + --theme-secondary-bg: #ffffff; + --theme-secondary-hover: #f1f5f9; + --theme-secondary-border: #d6dde8; + --theme-secondary-text: #2d3748; + + --theme-warning-bg: #fff3df; + --theme-warning-hover: #fde8c3; + --theme-warning-border: #edcd90; + --theme-warning-text: #8b662f; + + --theme-danger-bg: #fff0f1; + --theme-danger-hover: #f7dde1; + --theme-danger-border: #e8c3c9; + --theme-danger-text: #9d4c57; + + --theme-success-bg: #edf5ef; + --theme-success-border: #cbdbcf; + --theme-success-text: #567560; + + --theme-disabled-bg: #eef2f6; + --theme-disabled-border: #dde4ec; + --theme-disabled-text: #94a3b8; + + --theme-track: #e1e7ef; + --theme-progress-good: #7a8f7d; + --theme-progress-warn: #b18d5f; + --theme-progress-danger: #b26d73; + + --theme-selected-bg: #edf1f6; + --theme-selected-text: #11151b; + --theme-focus-ring: 0 0 0 3px rgba(17, 21, 27, 0.08); +} + +html, +body, +#root { + min-height: 100%; +} + +html { + color-scheme: light; +} + +html.dark { + color-scheme: dark; + --theme-bg: #0c0f14; + --theme-surface: #151a22; + --theme-surface-elevated: #1b212b; + --theme-surface-hover: #222a36; + + --theme-border-subtle: #2a3240; + --theme-border-strong: #394354; + + --theme-text-primary: #e7ebf0; + --theme-text-secondary: #9aa3b2; + --theme-text-muted: #6f7b8c; + --theme-text-on-light: #11151b; + + --theme-primary-bg: #d7dce3; + --theme-primary-hover: #e2e6ec; + --theme-primary-text: #11151b; + + --theme-secondary-bg: #1a2029; + --theme-secondary-hover: #232b36; + --theme-secondary-border: #313b4a; + --theme-secondary-text: #d4dae3; + + --theme-warning-bg: rgba(167, 125, 58, 0.16); + --theme-warning-hover: rgba(167, 125, 58, 0.24); + --theme-warning-border: rgba(167, 125, 58, 0.32); + --theme-warning-text: #d2ae72; + + --theme-danger-bg: rgba(164, 85, 92, 0.16); + --theme-danger-hover: rgba(164, 85, 92, 0.24); + --theme-danger-border: rgba(164, 85, 92, 0.3); + --theme-danger-text: #e0a1a8; + + --theme-success-bg: rgba(96, 124, 103, 0.16); + --theme-success-border: rgba(96, 124, 103, 0.3); + --theme-success-text: #a8c2ae; + + --theme-disabled-bg: #161b23; + --theme-disabled-border: #222a35; + --theme-disabled-text: #566274; + + --theme-track: #232c39; + --theme-progress-good: #7e9383; + --theme-progress-warn: #b99967; + --theme-progress-danger: #b56e74; + + --theme-selected-bg: rgba(215, 220, 227, 0.08); + --theme-selected-text: #e7ebf0; + --theme-focus-ring: 0 0 0 3px rgba(215, 220, 227, 0.14); +} + +body { + margin: 0; + background-color: var(--theme-bg); + color: var(--theme-text-primary); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + transition: + background-color 180ms ease, + color 180ms ease; +} + +html.dark body { + background-color: var(--theme-bg); + color: var(--theme-text-primary); +} + +.theme-shell { + background-color: var(--theme-bg); + color: var(--theme-text-primary); +} + +.theme-panel { + border: 1px solid var(--theme-border-subtle); + background-color: var(--theme-surface); +} + +.theme-panel-elevated { + border: 1px solid var(--theme-border-subtle); + background-color: var(--theme-surface-elevated); +} + +.theme-button-primary, +.theme-button-secondary, +.theme-button-warning, +.theme-button-danger, +.theme-menu-item, +.theme-select-button, +.theme-select-option, +.theme-field { + transition: + background-color 160ms ease, + border-color 160ms ease, + color 160ms ease, + box-shadow 160ms ease; +} + +.theme-button-primary:focus-visible, +.theme-button-secondary:focus-visible, +.theme-button-warning:focus-visible, +.theme-button-danger:focus-visible, +.theme-menu-item:focus-visible, +.theme-select-button:focus-visible, +.theme-select-option:focus-visible, +.theme-field:focus-visible { + outline: none; + box-shadow: var(--theme-focus-ring); +} + +.theme-button-primary { + background-color: var(--theme-primary-bg); + color: var(--theme-primary-text); +} + +.theme-button-primary:hover:not(:disabled) { + background-color: var(--theme-primary-hover); +} + +.theme-button-secondary { + border: 1px solid var(--theme-secondary-border); + background-color: var(--theme-secondary-bg); + color: var(--theme-secondary-text); +} + +.theme-button-secondary:hover:not(:disabled) { + border-color: var(--theme-border-strong); + background-color: var(--theme-secondary-hover); +} + +.theme-button-warning { + border: 1px solid var(--theme-warning-border); + background-color: var(--theme-warning-bg); + color: var(--theme-warning-text); +} + +.theme-button-warning:hover:not(:disabled) { + background-color: var(--theme-warning-hover); +} + +.theme-button-danger { + border: 1px solid var(--theme-danger-border); + background-color: var(--theme-danger-bg); + color: var(--theme-danger-text); +} + +.theme-button-danger:hover:not(:disabled) { + background-color: var(--theme-danger-hover); +} + +.theme-button-primary:disabled, +.theme-button-secondary:disabled, +.theme-button-warning:disabled, +.theme-button-danger:disabled { + cursor: not-allowed; + opacity: 0.68; +} + +.theme-button-disabled { + border: 1px solid var(--theme-disabled-border); + background-color: var(--theme-disabled-bg); + color: var(--theme-disabled-text); +} + +.theme-menu-item { + color: var(--theme-secondary-text); +} + +.theme-menu-item:hover:not(:disabled) { + background-color: var(--theme-surface-hover); +} + +.theme-status-chip { + border: 1px solid var(--theme-border-subtle); + background-color: var(--theme-surface-elevated); + color: var(--theme-text-secondary); +} + +.theme-status-chip--warning { + border-color: var(--theme-warning-border); + background-color: var(--theme-warning-bg); + color: var(--theme-warning-text); +} + +.theme-status-chip--success { + border-color: var(--theme-success-border); + background-color: var(--theme-success-bg); + color: var(--theme-success-text); +} + +.theme-card { + border: 1px solid var(--theme-border-subtle); + background-color: var(--theme-surface); +} + +.theme-card--active { + border-color: var(--theme-border-strong); + box-shadow: 0 16px 34px rgba(15, 23, 42, 0.08); +} + +html.dark .theme-card--active { + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.03), + rgba(255, 255, 255, 0) + ); + box-shadow: 0 18px 38px rgba(0, 0, 0, 0.28); +} + +.theme-card--inactive:hover { + border-color: var(--theme-border-strong); +} + +.theme-plan-badge { + border: 1px solid var(--theme-secondary-border); + background-color: var(--theme-surface-elevated); + color: var(--theme-secondary-text); +} + +.theme-plan-badge--success { + border-color: var(--theme-success-border); + background-color: var(--theme-success-bg); + color: var(--theme-success-text); +} + +.theme-plan-badge--warning { + border-color: var(--theme-warning-border); + background-color: var(--theme-warning-bg); + color: var(--theme-warning-text); +} + +.theme-field { + border: 1px solid var(--theme-secondary-border); + background-color: var(--theme-surface-elevated); + color: var(--theme-text-primary); +} + +.theme-field::placeholder { + color: var(--theme-text-muted); +} + +.theme-field:focus-visible { + border-color: var(--theme-border-strong); +} + +.theme-progress-track { + background-color: var(--theme-track); +} + +.theme-progress-fill--good { + background-color: var(--theme-progress-good); +} + +.theme-progress-fill--warn { + background-color: var(--theme-progress-warn); +} + +.theme-progress-fill--danger { + background-color: var(--theme-progress-danger); +} + +.theme-select-button { + border: 1px solid var(--theme-secondary-border); + background-color: var(--theme-secondary-bg); + color: var(--theme-secondary-text); +} + +.theme-select-button:hover:not(:disabled) { + border-color: var(--theme-border-strong); + background-color: var(--theme-secondary-hover); +} + +.theme-select-menu { + border: 1px solid var(--theme-border-subtle); + background-color: var(--theme-surface-elevated); + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14); +} + +html.dark .theme-select-menu { + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4); +} + +.theme-select-option { + color: var(--theme-secondary-text); +} + +.theme-select-option:hover:not(:disabled) { + background-color: var(--theme-surface-hover); +} + +.theme-select-option--active { + background-color: var(--theme-selected-bg); + color: var(--theme-selected-text); +} + +.theme-scrim { + background-color: rgba(2, 6, 23, 0.55); +} + +.theme-toast-success { + border: 1px solid var(--theme-success-border); + background-color: var(--theme-success-bg); + color: var(--theme-success-text); +} + +.theme-toast-warning { + border: 1px solid var(--theme-warning-border); + background-color: var(--theme-warning-bg); + color: var(--theme-warning-text); +} + +.theme-toast-danger { + border: 1px solid var(--theme-danger-border); + background-color: var(--theme-danger-bg); + color: var(--theme-danger-text); +} diff --git a/src/App.tsx b/src/App.tsx index f0ffdaa..d8c4e5e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,49 @@ import { AccountCard, AddAccountModal, UpdateChecker } from "./components"; import type { CodexProcessInfo } from "./types"; import "./App.css"; +type Theme = "light" | "dark"; +type OtherAccountsSort = + | "deadline_asc" + | "deadline_desc" + | "remaining_desc" + | "remaining_asc"; + +const THEME_STORAGE_KEY = "codex-switcher-theme"; +const OTHER_ACCOUNTS_SORT_OPTIONS: Array<{ value: OtherAccountsSort; label: string }> = [ + { value: "deadline_asc", label: "Reset: earliest to latest" }, + { value: "deadline_desc", label: "Reset: latest to earliest" }, + { value: "remaining_desc", label: "% remaining: highest to lowest" }, + { value: "remaining_asc", label: "% remaining: lowest to highest" }, +]; + +const shellClass = "theme-shell min-h-screen transition-colors"; +const panelClass = "theme-panel"; +const softButtonClass = + "theme-button-secondary whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-50"; +const primaryButtonClass = + "theme-button-primary whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-50"; +const warningButtonClass = + "theme-button-warning whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-50"; +const menuItemClass = + "theme-menu-item w-full rounded-xl px-3 py-2 text-left text-sm disabled:opacity-50"; + +function getInitialTheme(): Theme { + if (typeof window === "undefined") return "light"; + + try { + const savedTheme = window.localStorage.getItem(THEME_STORAGE_KEY); + if (savedTheme === "light" || savedTheme === "dark") { + return savedTheme; + } + } catch (error) { + console.error("Failed to read theme preference:", error); + } + + return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches + ? "dark" + : "light"; +} + function App() { const { accounts, @@ -30,6 +73,7 @@ function App() { saveMaskedAccountIds, } = useAccounts(); + const [theme, setTheme] = useState(() => getInitialTheme()); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [configModalMode, setConfigModalMode] = useState<"slim_export" | "slim_import">( @@ -54,20 +98,16 @@ function App() { isError: boolean; } | null>(null); const [maskedAccounts, setMaskedAccounts] = useState>(new Set()); - const [otherAccountsSort, setOtherAccountsSort] = useState< - "deadline_asc" | "deadline_desc" | "remaining_desc" | "remaining_asc" - >("deadline_asc"); + const [otherAccountsSort, setOtherAccountsSort] = useState("deadline_asc"); const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const [isSortMenuOpen, setIsSortMenuOpen] = useState(false); const actionsMenuRef = useRef(null); + const sortMenuRef = useRef(null); const toggleMask = (accountId: string) => { setMaskedAccounts((prev) => { const next = new Set(prev); - if (next.has(accountId)) { - next.delete(accountId); - } else { - next.add(accountId); - } + next.has(accountId) ? next.delete(accountId) : next.add(accountId); void saveMaskedAccountIds(Array.from(next)); return next; }); @@ -79,7 +119,9 @@ function App() { const toggleMaskAll = () => { setMaskedAccounts((prev) => { const shouldMaskAll = !accounts.every((account) => prev.has(account.id)); - const next = shouldMaskAll ? new Set(accounts.map((account) => account.id)) : new Set(); + const next = shouldMaskAll + ? new Set(accounts.map((account) => account.id)) + : new Set(); void saveMaskedAccountIds(Array.from(next)); return next; }); @@ -89,47 +131,68 @@ function App() { try { const info = await invoke("check_codex_processes"); setProcessInfo(info); + return info; } catch (err) { console.error("Failed to check processes:", err); + return null; } }, []); - // Check processes on mount and periodically useEffect(() => { - checkProcesses(); - const interval = setInterval(checkProcesses, 3000); // Check every 3 seconds + const root = document.documentElement; + root.classList.toggle("dark", theme === "dark"); + try { + window.localStorage.setItem(THEME_STORAGE_KEY, theme); + } catch (error) { + console.error("Failed to save theme preference:", error); + } + }, [theme]); + + useEffect(() => { + void checkProcesses(); + const interval = setInterval(() => void checkProcesses(), 3000); return () => clearInterval(interval); }, [checkProcesses]); - // Load masked accounts from storage on mount useEffect(() => { loadMaskedAccountIds().then((ids) => { - if (ids.length > 0) { - setMaskedAccounts(new Set(ids)); - } + if (ids.length > 0) setMaskedAccounts(new Set(ids)); }); }, [loadMaskedAccountIds]); useEffect(() => { - if (!isActionsMenuOpen) return; + if (!isActionsMenuOpen && !isSortMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { - if (!actionsMenuRef.current) return; - if (!actionsMenuRef.current.contains(event.target as Node)) { + const target = event.target as Node; + + if (actionsMenuRef.current && !actionsMenuRef.current.contains(target)) { setIsActionsMenuOpen(false); } + + if (sortMenuRef.current && !sortMenuRef.current.contains(target)) { + setIsSortMenuOpen(false); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsActionsMenuOpen(false); + setIsSortMenuOpen(false); + } }; document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [isActionsMenuOpen]); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [isActionsMenuOpen, isSortMenuOpen]); const handleSwitch = async (accountId: string) => { - // Check processes before switching - await checkProcesses(); - if (processInfo && !processInfo.can_switch) { - return; - } + const info = await checkProcesses(); + if (info && !info.can_switch) return; try { setSwitchingId(accountId); @@ -243,8 +306,7 @@ function App() { showWarmupToast(`Slim text exported (${accounts.length} accounts).`); } catch (err) { console.error("Failed to export slim text:", err); - const message = err instanceof Error ? err.message : String(err); - setConfigModalError(message); + setConfigModalError(err instanceof Error ? err.message : String(err)); showWarmupToast("Slim export failed", true); } finally { setIsExportingSlim(false); @@ -276,8 +338,7 @@ function App() { ); } catch (err) { console.error("Failed to import slim text:", err); - const message = err instanceof Error ? err.message : String(err); - setConfigModalError(message); + setConfigModalError(err instanceof Error ? err.message : String(err)); showWarmupToast("Slim import failed", true); } finally { setIsImportingSlim(false); @@ -290,16 +351,10 @@ function App() { const selected = await save({ title: "Export Full Encrypted Account Config", defaultPath: "codex-switcher-full.cswf", - filters: [ - { - name: "Codex Switcher Full Backup", - extensions: ["cswf"], - }, - ], + filters: [{ name: "Codex Switcher Full Backup", extensions: ["cswf"] }], }); if (!selected) return; - await exportAccountsFullEncryptedFile(selected); showWarmupToast("Full encrypted file exported."); } catch (err) { @@ -316,16 +371,10 @@ function App() { const selected = await open({ multiple: false, title: "Import Full Encrypted Account Config", - filters: [ - { - name: "Codex Switcher Full Backup", - extensions: ["cswf"], - }, - ], + filters: [{ name: "Codex Switcher Full Backup", extensions: ["cswf"] }], }); if (!selected || Array.isArray(selected)) return; - const summary = await importAccountsFullEncryptedFile(selected); setMaskedAccounts(new Set()); showWarmupToast( @@ -339,29 +388,32 @@ function App() { } }; - const activeAccount = accounts.find((a) => a.is_active); - const otherAccounts = accounts.filter((a) => !a.is_active); + const activeAccount = accounts.find((account) => account.is_active); + const otherAccounts = accounts.filter((account) => !account.is_active); const hasRunningProcesses = processInfo && processInfo.count > 0; const sortedOtherAccounts = useMemo(() => { const getResetDeadline = (resetAt: number | null | undefined) => resetAt ?? Number.POSITIVE_INFINITY; - - const getRemainingPercent = (usedPercent: number | null | undefined) => { - if (usedPercent === null || usedPercent === undefined) { - return Number.NEGATIVE_INFINITY; - } - return Math.max(0, 100 - usedPercent); - }; + const getRemainingPercent = (usedPercent: number | null | undefined) => + usedPercent === null || usedPercent === undefined + ? Number.NEGATIVE_INFINITY + : Math.max(0, 100 - usedPercent); return [...otherAccounts].sort((a, b) => { - if (otherAccountsSort === "deadline_asc" || otherAccountsSort === "deadline_desc") { + if ( + otherAccountsSort === "deadline_asc" || + otherAccountsSort === "deadline_desc" + ) { const deadlineDiff = getResetDeadline(a.usage?.primary_resets_at) - getResetDeadline(b.usage?.primary_resets_at); if (deadlineDiff !== 0) { - return otherAccountsSort === "deadline_asc" ? deadlineDiff : -deadlineDiff; + return otherAccountsSort === "deadline_asc" + ? deadlineDiff + : -deadlineDiff; } + const remainingDiff = getRemainingPercent(b.usage?.primary_used_percent) - getRemainingPercent(a.usage?.primary_used_percent); @@ -378,6 +430,7 @@ function App() { if (otherAccountsSort === "remaining_asc" && remainingDiff !== 0) { return -remainingDiff; } + const deadlineDiff = getResetDeadline(a.usage?.primary_resets_at) - getResetDeadline(b.usage?.primary_resets_at); @@ -386,32 +439,53 @@ function App() { }); }, [otherAccounts, otherAccountsSort]); + const selectedSortLabel = + OTHER_ACCOUNTS_SORT_OPTIONS.find((option) => option.value === otherAccountsSort)?.label ?? + OTHER_ACCOUNTS_SORT_OPTIONS[0].label; + return ( -
- {/* Header */} -
-
+
+
+
-
-
+
+
C
-
-

+
+

Codex Switcher

{processInfo && ( + className="h-1.5 w-1.5 rounded-full" + style={{ + backgroundColor: hasRunningProcesses + ? "var(--theme-warning-text)" + : "var(--theme-success-text)", + }} + /> {hasRunningProcesses ? `${processInfo.count} Codex running` @@ -420,86 +494,123 @@ function App() { )}
-

+

Multi-account manager for Codex CLI

-
+
-
{isActionsMenuOpen && ( -
+
+
+ +
@@ -509,7 +620,7 @@ function App() { openImportSlimTextModal(); }} disabled={isImportingSlim} - className="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-gray-100 text-gray-700 disabled:opacity-50" + className={menuItemClass} > {isImportingSlim ? "Importing..." : "Import Slim Text"} @@ -519,9 +630,11 @@ function App() { void handleExportFullFile(); }} disabled={isExportingFull} - className="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-gray-100 text-gray-700 disabled:opacity-50" + className={menuItemClass} > - {isExportingFull ? "Exporting..." : "Export Full Encrypted File"} + {isExportingFull + ? "Exporting..." + : "Export Full Encrypted File"}
)} @@ -541,50 +656,51 @@ function App() {
- {/* Main Content */} -
+
{loading && accounts.length === 0 ? (
-
-

Loading accounts...

+
+

Loading accounts...

) : error ? ( -
-
Failed to load accounts
-

{error}

+
+
+ Failed to load accounts +
+

{error}

) : accounts.length === 0 ? ( -
-
- 👤 +
+
+ A
-

+

No accounts yet

-

+

Add your first Codex account to get started

-
) : (
- {/* Active Account */} {activeAccount && (
-

+

Active Account

{ }} - onWarmup={() => - handleWarmupAccount(activeAccount.id, activeAccount.name) - } + onSwitch={() => {}} + onWarmup={() => handleWarmupAccount(activeAccount.id, activeAccount.name)} onDelete={() => handleDelete(activeAccount.id)} onRefresh={() => refreshSingleUsage(activeAccount.id)} onRename={(newName) => renameAccount(activeAccount.id, newName)} @@ -596,45 +712,34 @@ function App() { />
)} - - {/* Other Accounts */} {otherAccounts.length > 0 && (
-
-

+
+

Other Accounts ({otherAccounts.length})

-

-
+
{sortedOtherAccounts.map((account) => ( )}
- - {/* Refresh Success Toast */} {refreshSuccess && ( -
- Usage refreshed successfully +
+ Usage refreshed successfully
)} - - {/* Warm-up Toast */} {warmupToast && (
{warmupToast.message}
)} - - {/* Delete Confirmation Toast */} {deleteConfirmId && ( -
+
Click delete again to confirm removal
)} - {/* Add Account Modal */} setIsAddModalOpen(false)} @@ -709,28 +848,33 @@ function App() { onCancelOAuth={cancelOAuthLogin} /> - {/* Import/Export Config Modal */} {isConfigModalOpen && ( -
-
-
-

+
+
+
+

{configModalMode === "slim_export" ? "Export Slim Text" : "Import Slim Text"}

-
+
{configModalMode === "slim_import" ? ( -

+

Existing accounts are kept. Only missing accounts are imported.

) : ( -

+

This slim string contains account secrets. Keep it private.

)} @@ -745,18 +889,21 @@ function App() { : "Export string will appear here" : "Paste config string here" } - className="w-full h-48 px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg text-sm text-gray-800 placeholder-gray-400 focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 font-mono" + className="theme-field h-48 w-full rounded-lg px-4 py-3 font-mono text-sm" /> {configModalError && ( -
+
{configModalError}
)}
-
+
@@ -773,7 +920,7 @@ function App() { } }} disabled={!configPayload || isExportingSlim} - className="px-4 py-2.5 text-sm font-medium rounded-lg bg-gray-900 hover:bg-gray-800 text-white transition-colors disabled:opacity-50" + className={primaryButtonClass} > {configCopied ? "Copied" : "Copy String"} @@ -781,7 +928,7 @@ function App() { @@ -791,6 +938,7 @@ function App() {
)} +
); } diff --git a/src/components/AccountCard.tsx b/src/components/AccountCard.tsx index 7e1483e..026eb7f 100644 --- a/src/components/AccountCard.tsx +++ b/src/components/AccountCard.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, type KeyboardEvent, type ReactNode } from "react"; import type { AccountWithUsage } from "../types"; import { UsageBar } from "./UsageBar"; @@ -27,10 +27,16 @@ function formatLastRefresh(date: Date | null): string { return date.toLocaleDateString(); } -function BlurredText({ children, blur }: { children: React.ReactNode; blur: boolean }) { +function BlurredText({ + children, + blur, +}: { + children: ReactNode; + blur: boolean; +}) { return ( {children} @@ -90,9 +96,9 @@ export function AccountCard({ setIsEditing(false); }; - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter") { - handleRename(); + void handleRename(); } else if (e.key === "Escape") { setEditName(account.name); setIsEditing(false); @@ -106,34 +112,38 @@ export function AccountCard({ : "Unknown"; const planColors: Record = { - pro: "bg-indigo-50 text-indigo-700 border-indigo-200", - plus: "bg-emerald-50 text-emerald-700 border-emerald-200", - team: "bg-blue-50 text-blue-700 border-blue-200", - enterprise: "bg-amber-50 text-amber-700 border-amber-200", - free: "bg-gray-50 text-gray-600 border-gray-200", - api_key: "bg-orange-50 text-orange-700 border-orange-200", + pro: "theme-plan-badge", + plus: "theme-plan-badge theme-plan-badge--success", + team: "theme-plan-badge", + enterprise: "theme-plan-badge theme-plan-badge--warning", + free: "theme-plan-badge", + api_key: "theme-plan-badge theme-plan-badge--warning", }; const planKey = account.plan_type?.toLowerCase() || "api_key"; const planColorClass = planColors[planKey] || planColors.free; - return (
- {/* Header */} -
-
-
+
+
+
{account.is_active && ( - - + + )} {isEditing ? ( @@ -142,13 +152,13 @@ export function AccountCard({ type="text" value={editName} onChange={(e) => setEditName(e.target.value)} - onBlur={handleRename} + onBlur={() => void handleRename()} onKeyDown={handleKeyDown} - className="font-semibold text-gray-900 bg-gray-100 px-2 py-0.5 rounded border border-gray-300 focus:outline-none focus:border-gray-500 w-full" + className="theme-field w-full rounded px-2 py-0.5 font-semibold" /> ) : (

{ if (masked) return; setEditName(account.name); @@ -161,68 +171,76 @@ export function AccountCard({ )}

{account.email && ( -

+

{account.email}

)}
- {/* Eye toggle */} {onToggleMask && ( )} - {/* Plan badge */} - + {planDisplay}
- {/* Usage */}
- {/* Last refresh time */} -
+
Last updated: {formatLastRefresh(lastRefresh)}
- {/* Actions */}
{account.is_active ? ( ) : (
diff --git a/src/components/AddAccountModal.tsx b/src/components/AddAccountModal.tsx index 72f30e2..8c0fbb6 100644 --- a/src/components/AddAccountModal.tsx +++ b/src/components/AddAccountModal.tsx @@ -27,8 +27,8 @@ export function AddAccountModal({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [oauthPending, setOauthPending] = useState(false); - const [authUrl, setAuthUrl] = useState(""); - const [copied, setCopied] = useState(false); + const [authUrl, setAuthUrl] = useState(""); + const [copied, setCopied] = useState(false); const isPrimaryDisabled = loading || (activeTab === "oauth" && oauthPending); const resetForm = () => { @@ -42,7 +42,7 @@ export function AddAccountModal({ const handleClose = () => { if (oauthPending) { - onCancelOAuth(); + void onCancelOAuth(); } resetForm(); onClose(); @@ -62,7 +62,6 @@ export function AddAccountModal({ setOauthPending(true); setLoading(false); - // Wait for completion await onCompleteOAuth(); handleClose(); } catch (err) { @@ -76,12 +75,7 @@ export function AddAccountModal({ try { const selected = await open({ multiple: false, - filters: [ - { - name: "JSON", - extensions: ["json"], - }, - ], + filters: [{ name: "JSON", extensions: ["json"] }], title: "Select auth.json file", }); @@ -117,21 +111,27 @@ export function AddAccountModal({ if (!isOpen) return null; return ( -
-
- {/* Header */} -
-

Add Account

+
+
+
+

+ Add Account +

- {/* Tabs */} -
+
{(["oauth", "import"] as Tab[]).map((tab) => ( ))}
- {/* Content */} -
- {/* Account Name (always shown) */} +
-
- {/* Tab-specific content */} {activeTab === "oauth" && ( -
+
{oauthPending ? ( -
-
-

Waiting for browser login...

-

- Please open the following link in your browser to proceed: +

+
+

+ Waiting for browser login...

-
+

+ Open the following link in your browser to continue: +

+
@@ -213,8 +218,8 @@ export function AddAccountModal({
) : (

- Click the button below to generate a login link. - You will need to open it in your browser to authenticate. + Click the button below to generate a login link. You will need to + open it in your browser to authenticate.

)}
@@ -222,46 +227,44 @@ export function AddAccountModal({ {activeTab === "import" && (
-