mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
routing & app shell
This commit is contained in:
parent
6a4d59ccf5
commit
4def158714
15 changed files with 453 additions and 121 deletions
21
apps/desktop/app/(app)/layout.tsx
Normal file
21
apps/desktop/app/(app)/layout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { ReactNode } from "react"
|
||||
import { AppNav } from "@/components/app-nav"
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { Titlebar } from "@/components/titlebar"
|
||||
|
||||
// Shell for the authenticated app (dashboard, settings). The login/ and
|
||||
// spotlight/ routes live OUTSIDE this group, so they don't inherit the nav +
|
||||
// guard chrome.
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<div className="flex h-screen flex-col">
|
||||
<Titlebar />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<AppNav />
|
||||
<main className="min-w-0 flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
||||
68
apps/desktop/app/(app)/page.tsx
Normal file
68
apps/desktop/app/(app)/page.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"use client"
|
||||
|
||||
import { Button } from "@ui/components/button"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/card"
|
||||
import { Search } from "lucide-react"
|
||||
import { SearchCommand, useCommandK } from "@/components/search-command"
|
||||
|
||||
const MOCK_MEMORIES = [
|
||||
{ id: "1", title: "Q3 planning notes", desc: "Roadmap, OKRs, hiring plan" },
|
||||
{
|
||||
id: "2",
|
||||
title: "Tauri vs Electron",
|
||||
desc: "Why we picked Tauri for the desktop app",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Onboarding flow",
|
||||
desc: "Detect installed AI tools, one-click connect",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "SMFS notes",
|
||||
desc: "One container, two interfaces (API + folder)",
|
||||
},
|
||||
]
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { open, setOpen } = useCommandK()
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-4xl p-8">
|
||||
<div className="mb-8 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl">Your memories</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Everything you have saved, in one place.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setOpen(true)}>
|
||||
<Search />
|
||||
Search
|
||||
<kbd className="ml-1 rounded bg-muted px-1.5 py-0.5 text-xs">⌘K</kbd>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{MOCK_MEMORIES.map((memory) => (
|
||||
<Card
|
||||
key={memory.id}
|
||||
className="cursor-pointer gap-0 py-5 transition-colors hover:bg-accent/40"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{memory.title}</CardTitle>
|
||||
<CardDescription>{memory.desc}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SearchCommand open={open} onOpenChange={setOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
apps/desktop/app/(app)/settings/page.tsx
Normal file
67
apps/desktop/app/(app)/settings/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"use client"
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/card"
|
||||
import Link from "next/link"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
type AppInfo = {
|
||||
name: string
|
||||
version: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [info, setInfo] = useState<AppInfo | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
invoke<AppInfo>("app_info")
|
||||
.then(setInfo)
|
||||
.catch(() => setInfo(null))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl p-8">
|
||||
<h1 className="mb-6 font-semibold text-2xl">Settings</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">About</CardTitle>
|
||||
<CardDescription>
|
||||
Native runtime details, read from the Rust core over IPC.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<Row label="App" value={info?.name ?? "…"} />
|
||||
<Row label="Version" value={info?.version ?? "…"} />
|
||||
<Row label="Platform" value={info?.platform ?? "…"} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="mt-6 text-muted-foreground text-sm">
|
||||
Not signed in?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Go to login
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-medium tabular-nums">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,71 +1,20 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/*
|
||||
* Desktop window chrome — supplements the shared @repo/ui theme, which is
|
||||
* imported first in app/layout.tsx (`@ui/globals.css`) and provides the
|
||||
* Tailwind layer + design tokens. Keep this file plain CSS (no @apply / no
|
||||
* tailwind import) so there is a single Tailwind entry point.
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
/* No rubber-band overscroll inside the native window. */
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(125% 125% at 50% 0%, #1b1b2f 0%, #0c0c12 60%);
|
||||
color: #ececf1;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 360px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin: 0.25rem 0 1.5rem;
|
||||
color: #9a9ab0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.row dt {
|
||||
color: #9a9ab0;
|
||||
}
|
||||
|
||||
.row dd {
|
||||
margin: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
font-size: 0.875rem;
|
||||
/* The draggable title bar should feel like native chrome, not selectable text. */
|
||||
[data-tauri-drag-region] {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
import type { Metadata } from "next"
|
||||
import { Space_Grotesk } from "next/font/google"
|
||||
import type { ReactNode } from "react"
|
||||
import "@ui/globals.css"
|
||||
import "./globals.css"
|
||||
import { Providers } from "./providers"
|
||||
|
||||
export const metadata = {
|
||||
const font = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Supermemory",
|
||||
description: "Supermemory Desktop",
|
||||
description: "Your memories, wherever you are",
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${font.variable} antialiased`} suppressHydrationWarning>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
31
apps/desktop/app/login/page.tsx
Normal file
31
apps/desktop/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import { Button } from "@ui/components/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
// Phase 1 stub. Phase 3 wires "Sign in" to the system-browser OAuth flow
|
||||
// (invoke('auth_begin_oauth')) + the session-token deep-link handoff.
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
{/* Keep the frameless window draggable from the top edge. */}
|
||||
<div data-tauri-drag-region className="h-10 shrink-0" />
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<h1 className="font-semibold text-2xl">Supermemory</h1>
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
Sign in to access your memories.
|
||||
</p>
|
||||
<Button className="mt-6 w-full" onClick={() => router.replace("/")}>
|
||||
Sign in with browser
|
||||
</Button>
|
||||
<p className="mt-3 text-muted-foreground text-xs">
|
||||
Browser-based sign-in arrives in a later phase.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
type AppInfo = {
|
||||
name: string
|
||||
version: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [info, setInfo] = useState<AppInfo | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Call into the Rust core over Tauri's IPC bridge. If this resolves, the
|
||||
// native layer is alive and wired to the webview — the Phase 0 goal.
|
||||
invoke<AppInfo>("app_info")
|
||||
.then(setInfo)
|
||||
.catch((err) => setError(String(err)))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<main className="screen">
|
||||
<section className="card">
|
||||
<h1 className="title">Supermemory</h1>
|
||||
<p className="muted">Desktop · Phase 0</p>
|
||||
|
||||
{info ? (
|
||||
<dl className="info">
|
||||
<div className="row">
|
||||
<dt>App</dt>
|
||||
<dd>{info.name}</dd>
|
||||
</div>
|
||||
<div className="row">
|
||||
<dt>Version</dt>
|
||||
<dd>{info.version}</dd>
|
||||
</div>
|
||||
<div className="row">
|
||||
<dt>Platform</dt>
|
||||
<dd>{info.platform}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : error ? (
|
||||
<p className="error">Native bridge error: {error}</p>
|
||||
) : (
|
||||
<p className="muted">Connecting to the native core…</p>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
38
apps/desktop/app/providers.tsx
Normal file
38
apps/desktop/app/providers.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"use client"
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { Toaster } from "@ui/components/sonner"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { type ReactNode, useState } from "react"
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
// One QueryClient for the app's lifetime; lazy init avoids re-creating it on
|
||||
// every render (and on Fast Refresh in dev).
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// The app is dark-only, matching apps/web (forcedTheme="dark").
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem={false}
|
||||
forcedTheme="dark"
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
23
apps/desktop/app/spotlight/page.tsx
Normal file
23
apps/desktop/app/spotlight/page.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"use client"
|
||||
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
// Phase 1 stub for the frameless spotlight window. Phase 5 wires the Rust
|
||||
// global-shortcut, window show/hide on blur/Esc, input focus on show, and real
|
||||
// /v3/search results.
|
||||
export default function SpotlightPage() {
|
||||
return (
|
||||
<div className="flex h-screen items-start justify-center bg-transparent p-3">
|
||||
<div className="w-full overflow-hidden rounded-xl border border-border/60 bg-popover/95 shadow-2xl backdrop-blur">
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<Search className="size-4 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
aria-label="Search your memories"
|
||||
placeholder="Search your memories…"
|
||||
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
apps/desktop/components/app-nav.tsx
Normal file
38
apps/desktop/components/app-nav.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"use client"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { Cog, House } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
const LINKS = [
|
||||
{ href: "/", label: "Home", icon: House },
|
||||
{ href: "/settings", label: "Settings", icon: Cog },
|
||||
] as const
|
||||
|
||||
export function AppNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className="flex w-48 shrink-0 flex-col gap-1 border-border/60 border-r p-3">
|
||||
{LINKS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors",
|
||||
active
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
35
apps/desktop/components/auth-guard.tsx
Normal file
35
apps/desktop/components/auth-guard.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { type ReactNode, useEffect } from "react"
|
||||
|
||||
// Phase 1 stub. Phase 2/3 replaces `useAuthStatus` with a real check of the
|
||||
// keychain-backed token (invoke('auth_get_token')) and returns "unauthenticated"
|
||||
// when it is missing. The state machine + redirect are wired now so later phases
|
||||
// only swap the source of truth, not the guard's shape.
|
||||
type AuthStatus = "loading" | "authenticated" | "unauthenticated"
|
||||
|
||||
function useAuthStatus(): AuthStatus {
|
||||
return "authenticated"
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: { children: ReactNode }) {
|
||||
const status = useAuthStatus()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.replace("/login")
|
||||
}
|
||||
}, [status, router])
|
||||
|
||||
if (status !== "authenticated") {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
|
||||
Loading…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
76
apps/desktop/components/search-command.tsx
Normal file
76
apps/desktop/components/search-command.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/components/command"
|
||||
import { FileText, Hash, MessageSquare } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
// Mock data for Phase 1. Phase 5 swaps this for /v3/search results and wires the
|
||||
// OS-global hotkey via the Rust global-shortcut plugin. The point here is to
|
||||
// mount the real command-palette UI (cmdk + shared theme) behind an in-app
|
||||
// Cmd/Ctrl+K — the precursor to the spotlight window.
|
||||
const MOCK_RESULTS = [
|
||||
{ id: "1", title: "Q3 planning notes", kind: "doc" },
|
||||
{ id: "2", title: "Tauri vs Electron — research", kind: "doc" },
|
||||
{ id: "3", title: "engineering", kind: "space" },
|
||||
{ id: "4", title: "Desktop app architecture", kind: "chat" },
|
||||
] as const
|
||||
|
||||
const ICONS = { doc: FileText, space: Hash, chat: MessageSquare }
|
||||
|
||||
export function SearchCommand({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
||||
<CommandInput placeholder="Search your memories…" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Memories">
|
||||
{MOCK_RESULTS.map((result) => {
|
||||
const Icon = ICONS[result.kind]
|
||||
return (
|
||||
<CommandItem
|
||||
key={result.id}
|
||||
value={result.title}
|
||||
onSelect={() => onOpenChange(false)}
|
||||
>
|
||||
<Icon />
|
||||
{result.title}
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
// In-app Cmd/Ctrl+K toggles the palette. (The OS-global hotkey arrives in Phase 5
|
||||
// from the Rust side; this is the in-window shortcut.)
|
||||
export function useCommandK() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
setOpen((prev) => !prev)
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown)
|
||||
return () => document.removeEventListener("keydown", onKeyDown)
|
||||
}, [])
|
||||
|
||||
return { open, setOpen }
|
||||
}
|
||||
16
apps/desktop/components/titlebar.tsx
Normal file
16
apps/desktop/components/titlebar.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"use client"
|
||||
|
||||
// Draggable strip for the frameless main window. On macOS the native traffic
|
||||
// lights overlay the top-left (tauri.conf.json -> titleBarStyle: "Overlay"),
|
||||
// so we pad left to clear them. `data-tauri-drag-region` is the hook that lets
|
||||
// Tauri move the OS window when this strip is dragged.
|
||||
export function Titlebar({ title = "Supermemory" }: { title?: string }) {
|
||||
return (
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
className="flex h-10 shrink-0 items-center justify-center border-border/60 border-b bg-background/80 pl-20 font-medium text-muted-foreground text-xs backdrop-blur"
|
||||
>
|
||||
{title}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -9,16 +9,23 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/lib": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.14",
|
||||
"@tauri-apps/api": "^2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^16.0.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
apps/desktop/postcss.config.mjs
Normal file
5
apps/desktop/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
}
|
||||
|
||||
export default config
|
||||
Loading…
Add table
Reference in a new issue