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",
|
"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",
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
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> = {
|
const body: Record<string, unknown> = {
|
||||||
settings: {
|
settings: {
|
||||||
index: {
|
|
||||||
knn: embeddingsEnabled ? true : undefined,
|
|
||||||
},
|
|
||||||
analysis: {
|
analysis: {
|
||||||
normalizer: {
|
normalizer: {
|
||||||
lowercase_normalizer: {
|
lowercase_normalizer: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user