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:
parent
3a792698a6
commit
9be3320e5b
2036
package-lock.json
generated
2036
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"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:unit": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
@ -15,7 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-markdown": "^6.3.4",
|
||||
"@elastic/elasticsearch": "^9.1.1",
|
||||
"@elastic/elasticsearch": "^8.15.1",
|
||||
"@qdrant/qdrant-js": "^1.15.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { QueryProvider } from "@/components/providers/query-provider";
|
||||
import { ThemeProvider } from "@/components/theme/theme-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -24,9 +25,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
<ThemeProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -13,6 +13,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||
import { MarkdownEditor } from "@/components/editor/markdown-editor";
|
||||
import { toast } from "sonner";
|
||||
import { TagsDialog } from "@/components/files/tags-dialog";
|
||||
import { ModeToggle } from "@/components/theme/mode-toggle";
|
||||
|
||||
type FilesListResponse = {
|
||||
total: number;
|
||||
@ -320,6 +321,9 @@ export default function Home() {
|
||||
<main className="h-full flex flex-col">
|
||||
<header className="border-b p-3 flex items-center gap-3">
|
||||
<Breadcrumbs path={path} onNavigate={(p) => { setPage(1); setPath(p); }} />
|
||||
<div className="ml-auto">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="p-3 flex items-center gap-2 border-b">
|
||||
|
||||
51
src/components/theme/mode-toggle.tsx
Normal file
51
src/components/theme/mode-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/components/theme/theme-provider.tsx
Normal file
24
src/components/theme/theme-provider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -115,9 +115,6 @@ export async function ensureIndex(options?: { recreate?: boolean }) {
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
settings: {
|
||||
index: {
|
||||
knn: embeddingsEnabled ? true : undefined,
|
||||
},
|
||||
analysis: {
|
||||
normalizer: {
|
||||
lowercase_normalizer: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user