feat(ui): add dark mode with next-themes provider (default=dark) and ModeToggle in header; wired into layout; verified shadcn registry

This commit is contained in:
nicholai 2025-09-13 07:47:55 -06:00
parent 3a792698a6
commit 9be3320e5b
7 changed files with 2090 additions and 39 deletions

2036
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
"build": "next build --turbopack", "build": "next build --turbopack",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"create:index": "tsx -r dotenv/config -r tsconfig-paths/register scripts/create-index.ts", "create:index": "DOTENV_CONFIG_PATH=.env.local tsx -r dotenv/config -r tsconfig-paths/register scripts/create-index.ts",
"test": "vitest", "test": "vitest",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@ -15,7 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-markdown": "^6.3.4", "@codemirror/lang-markdown": "^6.3.4",
"@elastic/elasticsearch": "^9.1.1", "@elastic/elasticsearch": "^8.15.1",
"@qdrant/qdrant-js": "^1.15.1", "@qdrant/qdrant-js": "^1.15.1",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",

View File

@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { QueryProvider } from "@/components/providers/query-provider"; import { QueryProvider } from "@/components/providers/query-provider";
import { ThemeProvider } from "@/components/theme/theme-provider";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -24,9 +25,11 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<QueryProvider>{children}</QueryProvider> <ThemeProvider>
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -13,6 +13,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { MarkdownEditor } from "@/components/editor/markdown-editor"; import { MarkdownEditor } from "@/components/editor/markdown-editor";
import { toast } from "sonner"; import { toast } from "sonner";
import { TagsDialog } from "@/components/files/tags-dialog"; import { TagsDialog } from "@/components/files/tags-dialog";
import { ModeToggle } from "@/components/theme/mode-toggle";
type FilesListResponse = { type FilesListResponse = {
total: number; total: number;
@ -320,6 +321,9 @@ export default function Home() {
<main className="h-full flex flex-col"> <main className="h-full flex flex-col">
<header className="border-b p-3 flex items-center gap-3"> <header className="border-b p-3 flex items-center gap-3">
<Breadcrumbs path={path} onNavigate={(p) => { setPage(1); setPath(p); }} /> <Breadcrumbs path={path} onNavigate={(p) => { setPage(1); setPath(p); }} />
<div className="ml-auto">
<ModeToggle />
</div>
</header> </header>
<section className="p-3 flex items-center gap-2 border-b"> <section className="p-3 flex items-center gap-2 border-b">

View File

@ -0,0 +1,51 @@
"use client";
import * as React from "react";
import { useTheme } from "next-themes";
import { Sun, Moon, Laptop } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
/**
* ModeToggle shows the current theme and lets the user switch between
* Light, Dark (default), and System.
*/
export function ModeToggle() {
const { theme, setTheme, resolvedTheme } = useTheme();
const icon = React.useMemo(() => {
const t = (theme ?? resolvedTheme) as string | undefined;
if (t === "light") return <Sun className="h-4 w-4" />;
if (t === "system") return <Laptop className="h-4 w-4" />;
return <Moon className="h-4 w-4" />; // default dark
}, [theme, resolvedTheme]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" aria-label="Toggle theme">
{icon}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop className="mr-2 h-4 w-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,24 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
/**
* ThemeProvider wraps the app and controls the html class for dark mode.
* Default is dark per user requirement; system is still allowed if selected.
*/
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
{...props}
>
{children}
</NextThemesProvider>
);
}

View File

@ -115,9 +115,6 @@ export async function ensureIndex(options?: { recreate?: boolean }) {
const body: Record<string, unknown> = { const body: Record<string, unknown> = {
settings: { settings: {
index: {
knn: embeddingsEnabled ? true : undefined,
},
analysis: { analysis: {
normalizer: { normalizer: {
lowercase_normalizer: { lowercase_normalizer: {