From 77f48d9f261eb76b46378cb2de398e7ea3e9752a Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:35:51 +0800 Subject: [PATCH] feat(desktop): add CSS, router, pages, and app entry with provider nesting - globals.css with Tailwind + design tokens from @multica/ui - Hash router with dashboard shell, issues, my-issues, runtimes, skills pages - Login page with email OTP flow (no Google OAuth) - IssueDetailPage wrapper extracting route param for IssueDetail - App.tsx with ThemeProvider > QueryProvider > RouterProvider nesting - main.tsx without StrictMode to avoid Zustand double-render issues Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/desktop/src/renderer/index.html | 12 +- apps/desktop/src/renderer/src/App.tsx | 27 ++-- apps/desktop/src/renderer/src/globals.css | 28 ++++ apps/desktop/src/renderer/src/main.tsx | 10 +- .../renderer/src/pages/issue-detail-page.tsx | 8 + apps/desktop/src/renderer/src/pages/login.tsx | 139 ++++++++++++++++++ .../src/renderer/src/pages/placeholder.tsx | 12 ++ apps/desktop/src/renderer/src/router.tsx | 31 ++++ 8 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 apps/desktop/src/renderer/src/globals.css create mode 100644 apps/desktop/src/renderer/src/pages/issue-detail-page.tsx create mode 100644 apps/desktop/src/renderer/src/pages/login.tsx create mode 100644 apps/desktop/src/renderer/src/pages/placeholder.tsx create mode 100644 apps/desktop/src/renderer/src/router.tsx diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index fbfd3b0b1..470bf7d96 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -1,16 +1,12 @@ - + + Multica - - - -
+ +
diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index cd1be2e4a..6711b81bd 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,17 +1,16 @@ -function App(): React.JSX.Element { +import { RouterProvider } from "react-router-dom"; +import { ThemeProvider } from "./components/theme-provider"; +import { Toaster } from "@multica/ui/components/ui/sonner"; +import { QueryProvider } from "@multica/core/provider"; +import { router } from "./router"; + +export default function App() { return ( -
-

Multica Desktop

-
+ + + + + + ); } - -export default App; diff --git a/apps/desktop/src/renderer/src/globals.css b/apps/desktop/src/renderer/src/globals.css new file mode 100644 index 000000000..ebd01f84a --- /dev/null +++ b/apps/desktop/src/renderer/src/globals.css @@ -0,0 +1,28 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "@multica/ui/styles/tokens.css"; + +@custom-variant dark (&:is(.dark *)); + +@source "../../../../packages/ui/**/*.tsx"; +@source "../../../../packages/core/**/*.tsx"; +@source "../../../../packages/views/**/*.tsx"; +@source "./**/*.tsx"; + +@layer base { + * { + @apply border-border outline-ring/50; + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); + } + *::-webkit-scrollbar { width: 6px; height: 6px; } + *::-webkit-scrollbar-track { background: var(--scrollbar-track); } + *::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; } + *::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index f8fc6f511..16e637c66 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -1,9 +1,5 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom/client"; import App from "./App"; +import "./globals.css"; -createRoot(document.getElementById("root")!).render( - - - , -); +ReactDOM.createRoot(document.getElementById("root")!).render(); diff --git a/apps/desktop/src/renderer/src/pages/issue-detail-page.tsx b/apps/desktop/src/renderer/src/pages/issue-detail-page.tsx new file mode 100644 index 000000000..0c2e82219 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/issue-detail-page.tsx @@ -0,0 +1,8 @@ +import { useParams } from "react-router-dom"; +import { IssueDetail } from "@multica/views/issues/components"; + +export function IssueDetailPage() { + const { id } = useParams<{ id: string }>(); + if (!id) return null; + return ; +} diff --git a/apps/desktop/src/renderer/src/pages/login.tsx b/apps/desktop/src/renderer/src/pages/login.tsx new file mode 100644 index 000000000..bf0bfefd5 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/login.tsx @@ -0,0 +1,139 @@ +import { useState, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuthStore } from "@/platform/auth"; +import { useWorkspaceStore } from "@/platform/workspace"; +import { api } from "@/platform/api"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@multica/ui/components/ui/card"; +import { Input } from "@multica/ui/components/ui/input"; +import { Button } from "@multica/ui/components/ui/button"; +import { Label } from "@multica/ui/components/ui/label"; +import { MulticaIcon } from "../components/multica-icon"; +import { TitleBar } from "../components/title-bar"; + +export function LoginPage() { + const navigate = useNavigate(); + const [step, setStep] = useState<"email" | "code">("email"); + const [email, setEmail] = useState(""); + const [code, setCode] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSendCode = useCallback(async () => { + setLoading(true); + setError(""); + try { + await useAuthStore.getState().sendCode(email); + setStep("code"); + } catch { + setError("Failed to send code"); + } finally { + setLoading(false); + } + }, [email]); + + const handleVerify = useCallback(async () => { + setLoading(true); + setError(""); + try { + await useAuthStore.getState().verifyCode(email, code); + const wsList = await api.listWorkspaces(); + useWorkspaceStore.getState().hydrateWorkspace(wsList); + navigate("/issues", { replace: true }); + } catch { + setError("Invalid code"); + } finally { + setLoading(false); + } + }, [email, code, navigate]); + + return ( +
+ +
+ + +
+ +
+ Sign in to Multica + + {step === "email" + ? "Enter your email to get a login code" + : `We sent a code to ${email}`} + +
+ + {step === "email" ? ( +
{ + e.preventDefault(); + handleSendCode(); + }} + > +
+
+ + setEmail(e.target.value)} + autoFocus + /> +
+ {error &&

{error}

} + +
+
+ ) : ( +
{ + e.preventDefault(); + handleVerify(); + }} + > +
+
+ + setCode(e.target.value)} + autoFocus + /> +
+ {error &&

{error}

} + + +
+
+ )} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/src/pages/placeholder.tsx b/apps/desktop/src/renderer/src/pages/placeholder.tsx new file mode 100644 index 000000000..d746debde --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/placeholder.tsx @@ -0,0 +1,12 @@ +export function PlaceholderPage({ title }: { title: string }) { + return ( +
+
+

{title}

+

+ Coming soon — requires page extraction to @multica/views. +

+
+
+ ); +} diff --git a/apps/desktop/src/renderer/src/router.tsx b/apps/desktop/src/renderer/src/router.tsx new file mode 100644 index 000000000..81ea6e4c4 --- /dev/null +++ b/apps/desktop/src/renderer/src/router.tsx @@ -0,0 +1,31 @@ +import { createHashRouter, Navigate } from "react-router-dom"; +import { DashboardShell } from "./components/dashboard-shell"; +import { LoginPage } from "./pages/login"; +import { IssueDetailPage } from "./pages/issue-detail-page"; +import { PlaceholderPage } from "./pages/placeholder"; + +// Extracted pages from @multica/views +import { IssuesPage } from "@multica/views/issues/components"; +import { MyIssuesPage } from "@multica/views/my-issues"; +import { RuntimesPage } from "@multica/views/runtimes"; +import { SkillsPage } from "@multica/views/skills"; + +export const router = createHashRouter([ + { + path: "/", + element: , + children: [ + { index: true, element: }, + { path: "issues", element: }, + { path: "issues/:id", element: }, + { path: "my-issues", element: }, + { path: "runtimes", element: }, + { path: "skills", element: }, + { path: "agents", element: }, + { path: "inbox", element: }, + { path: "settings", element: }, + { path: "board", element: }, + ], + }, + { path: "/login", element: }, +]);