mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Multica</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<body class="h-full overflow-hidden antialiased font-sans">
|
||||
<div id="root" class="h-full"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
<h1>Multica Desktop</h1>
|
||||
</div>
|
||||
<ThemeProvider>
|
||||
<QueryProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
28
apps/desktop/src/renderer/src/globals.css
Normal file
28
apps/desktop/src/renderer/src/globals.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
@@ -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 <IssueDetail issueId={id} />;
|
||||
}
|
||||
139
apps/desktop/src/renderer/src/pages/login.tsx
Normal file
139
apps/desktop/src/renderer/src/pages/login.tsx
Normal file
@@ -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 (
|
||||
<div className="flex h-screen flex-col">
|
||||
<TitleBar />
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<Card className="w-[380px]">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4">
|
||||
<MulticaIcon bordered size="lg" />
|
||||
</div>
|
||||
<CardTitle>Sign in to Multica</CardTitle>
|
||||
<CardDescription>
|
||||
{step === "email"
|
||||
? "Enter your email to get a login code"
|
||||
: `We sent a code to ${email}`}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{step === "email" ? (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSendCode();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
<Button className="w-full" disabled={!email || loading}>
|
||||
{loading ? "Sending..." : "Send Code"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleVerify();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Verification Code</Label>
|
||||
<Input
|
||||
id="code"
|
||||
placeholder="Enter 6-digit code"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
<Button className="w-full" disabled={!code || loading}>
|
||||
{loading ? "Verifying..." : "Verify"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setStep("email");
|
||||
setCode("");
|
||||
setError("");
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
apps/desktop/src/renderer/src/pages/placeholder.tsx
Normal file
12
apps/desktop/src/renderer/src/pages/placeholder.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function PlaceholderPage({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Coming soon — requires page extraction to @multica/views.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
apps/desktop/src/renderer/src/router.tsx
Normal file
31
apps/desktop/src/renderer/src/router.tsx
Normal file
@@ -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: <DashboardShell />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/issues" replace /> },
|
||||
{ path: "issues", element: <IssuesPage /> },
|
||||
{ path: "issues/:id", element: <IssueDetailPage /> },
|
||||
{ path: "my-issues", element: <MyIssuesPage /> },
|
||||
{ path: "runtimes", element: <RuntimesPage /> },
|
||||
{ path: "skills", element: <SkillsPage /> },
|
||||
{ path: "agents", element: <PlaceholderPage title="Agents" /> },
|
||||
{ path: "inbox", element: <PlaceholderPage title="Inbox" /> },
|
||||
{ path: "settings", element: <PlaceholderPage title="Settings" /> },
|
||||
{ path: "board", element: <PlaceholderPage title="Board" /> },
|
||||
],
|
||||
},
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
]);
|
||||
Reference in New Issue
Block a user