feat: initial ui redesign from demo

This commit is contained in:
LukeGus
2026-05-11 01:23:11 -05:00
parent eaa758effe
commit 33dcde0827
349 changed files with 23984 additions and 31634 deletions
+148 -118
View File
@@ -1,53 +1,88 @@
/* eslint-disable react-refresh/only-export-components */
import { prepareClientCacheVersion } from "@/lib/client-cache-version";
import { StrictMode, Suspense, lazy, useEffect, useState, useRef } from "react";
import { StrictMode, Suspense, lazy, useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import "./ui/index.css";
import { ThemeProvider } from "@/components/theme-provider";
import "./i18n/i18n";
import "./ui/i18n/i18n";
import { isElectron } from "@/lib/electron";
import { Toaster } from "@/components/sonner";
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
import { applyAccentColor, applyFontSize } from "@/lib/theme";
import type { FontSizeId } from "@/types/ui-types";
import { useServiceWorker } from "@/hooks/use-service-worker";
const DesktopApp = lazy(() => import("@/ui/desktop/DesktopApp.tsx"));
const MobileApp = lazy(() =>
import("@/ui/mobile/MobileApp.tsx").then((module) => ({
default: module.MobileApp,
})),
);
const HostManagerApp = lazy(
() => import("./ui/desktop/apps/host-manager/HostManagerApp.tsx"),
);
const TerminalApp = lazy(
() => import("./ui/desktop/apps/features/terminal/TerminalApp.tsx"),
const AppShell = lazy(() =>
import("@/AppShell").then((m) => ({ default: m.AppShell })),
);
// Full-screen apps opened via query params (e.g. from external links or Electron)
const TerminalApp = lazy(() => import("@/features/terminal/TerminalApp"));
const FileManagerApp = lazy(
() => import("./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"),
);
const TunnelApp = lazy(
() => import("./ui/desktop/apps/features/tunnel/TunnelApp.tsx"),
() => import("@/features/file-manager/FileManagerApp"),
);
const TunnelApp = lazy(() => import("@/features/tunnel/TunnelApp"));
const ServerStatsApp = lazy(
() => import("./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"),
);
const DockerApp = lazy(
() => import("./ui/desktop/apps/features/docker/DockerApp.tsx"),
);
const GuacamoleApp = lazy(
() => import("@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"),
() => import("@/features/server-stats/ServerStatsApp"),
);
const DockerApp = lazy(() => import("@/features/docker/DockerApp"));
const GuacamoleApp = lazy(() => import("@/features/guacamole/GuacamoleApp"));
const ElectronVersionCheck = lazy(() =>
import("@/ui/desktop/user/ElectronVersionCheck.tsx").then((module) => ({
import("@/user/ElectronVersionCheck").then((module) => ({
default: module.ElectronVersionCheck,
})),
);
const FullscreenApp: React.FC = () => {
type Phase = "idle-auth" | "fading-in" | "idle-app" | "fading-out";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
const lastSwitchTime = useRef(0);
const isCurrentlyMobile = useRef(window.innerWidth < 768);
const hasSwitchedOnce = useRef(false);
useEffect(() => {
let timeoutId: number;
const handleResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const newWidth = window.innerWidth;
const newIsMobile = newWidth < 768;
const now = Date.now();
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
setWidth(newWidth);
return;
}
if (
newIsMobile !== isCurrentlyMobile.current &&
now - lastSwitchTime.current > 5000
) {
lastSwitchTime.current = now;
isCurrentlyMobile.current = newIsMobile;
hasSwitchedOnce.current = true;
setWidth(newWidth);
} else {
setWidth(newWidth);
}
}, 2000);
};
window.addEventListener("resize", handleResize);
return () => {
clearTimeout(timeoutId);
window.removeEventListener("resize", handleResize);
};
}, []);
return width;
}
function FullscreenApp() {
const searchParams = new URLSearchParams(window.location.search);
const view = searchParams.get("view");
const hostId = searchParams.get("hostId");
switch (view) {
case "host-manager":
return <HostManagerApp />;
case "terminal":
return <TerminalApp hostId={hostId || undefined} />;
case "file-manager":
@@ -63,53 +98,83 @@ const FullscreenApp: React.FC = () => {
case "telnet":
return <GuacamoleApp hostId={hostId || undefined} />;
default:
return <DesktopApp />;
return null;
}
};
import { useServiceWorker } from "@/hooks/use-service-worker";
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
const lastSwitchTime = useRef(0);
const isCurrentlyMobile = useRef(window.innerWidth < 768);
const hasSwitchedOnce = useRef(false);
function App() {
const stored = getStoredAuth();
const [phase, setPhase] = useState<Phase>(
stored?.loggedIn ? "idle-app" : "idle-auth",
);
const [authUsername, setAuthUsername] = useState(stored?.username ?? "");
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const handleResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const newWidth = window.innerWidth;
const newIsMobile = newWidth < 768;
const now = Date.now();
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
setWidth(newWidth);
return;
}
if (
newIsMobile !== isCurrentlyMobile.current &&
now - lastSwitchTime.current > 5000
) {
lastSwitchTime.current = now;
isCurrentlyMobile.current = newIsMobile;
hasSwitchedOnce.current = true;
setWidth(newWidth);
} else {
setWidth(newWidth);
}
}, 2000);
};
window.addEventListener("resize", handleResize);
const savedAccent = localStorage.getItem("termix-accent");
if (savedAccent) applyAccentColor(savedAccent);
const savedSize = localStorage.getItem(
"termix-font-size",
) as FontSizeId | null;
applyFontSize(savedSize ?? "lg");
return () => {
clearTimeout(timeoutId);
window.removeEventListener("resize", handleResize);
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
return width;
function handleLogin(u: string) {
setAuthUsername(u);
setPhase("fading-in");
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
}
function handleLogout() {
clearStoredAuth();
setPhase("fading-out");
timerRef.current = setTimeout(() => {
setAuthUsername("");
setPhase("idle-auth");
}, 450);
}
const showApp =
phase === "idle-app" || phase === "fading-in" || phase === "fading-out";
const showAuth =
phase === "idle-auth" || phase === "fading-in" || phase === "fading-out";
const appOpacity = phase === "idle-app" ? 1 : 0;
const authOpacity = phase === "idle-auth" ? 1 : 0;
return (
<>
{showApp && (
<div
className="fixed inset-0 z-0 transition-opacity duration-[450ms] ease-in-out"
style={{
opacity: appOpacity,
pointerEvents: phase === "idle-app" ? "auto" : "none",
}}
>
<Suspense fallback={null}>
<AppShell username={authUsername} onLogout={handleLogout} />
</Suspense>
</div>
)}
{showAuth && (
<div
className="fixed inset-0 z-10 transition-opacity duration-[450ms] ease-in-out"
style={{
opacity: authOpacity,
pointerEvents: phase === "idle-auth" ? "auto" : "none",
}}
>
<Auth onLogin={handleLogin} />
</div>
)}
<Toaster position="bottom-right" />
</>
);
}
function RootApp() {
@@ -125,61 +190,26 @@ function RootApp() {
(window as Window & { opera?: string }).opera ||
"";
const isTermixMobile = /Termix-Mobile/.test(userAgent);
const searchParams = new URLSearchParams(window.location.search);
const isFullscreen = searchParams.has("view");
const renderApp = () => {
if (isFullscreen) {
return <FullscreenApp />;
}
if (isFullscreen) {
return (
<Suspense fallback={null}>
<FullscreenApp />
</Suspense>
);
}
if (isElectron()) {
return <DesktopApp />;
}
if (isElectron() && showVersionCheck) {
return (
<Suspense fallback={null}>
<ElectronVersionCheck onContinue={() => setShowVersionCheck(false)} />
</Suspense>
);
}
if (isTermixMobile) {
return <MobileApp key="mobile" />;
}
return isMobile ? <MobileApp key="mobile" /> : <DesktopApp key="desktop" />;
};
return (
<>
{!isFullscreen && (
<div
className="fixed inset-0 pointer-events-none"
style={{
backgroundColor: "var(--bg-base)",
backgroundImage: `linear-gradient(
135deg,
transparent 0%,
transparent 49%,
rgba(128, 128, 128, 0.03) 49%,
rgba(128, 128, 128, 0.03) 51%,
transparent 51%,
transparent 100%
)`,
backgroundSize: "80px 80px",
zIndex: 0,
}}
/>
)}
<div className="relative min-h-screen" style={{ zIndex: 1 }}>
{isElectron() && showVersionCheck && !isFullscreen ? (
<Suspense fallback={null}>
<ElectronVersionCheck
onContinue={() => setShowVersionCheck(false)}
isAuthenticated={false}
/>
</Suspense>
) : (
<Suspense fallback={null}>{renderApp()}</Suspense>
)}
</div>
</>
);
return <App />;
}
prepareClientCacheVersion().finally(() => {