frontend: rebuild bookmark page with drag-and-drop, search, and theme system

- bookmark management with dnd-kit reordering, bulk edit, search,
  category filter/rename, and meta auto-fetch
- migrate /bookmarks → /dashboard/bookmarks under (main) layout
- homepage redesign with category grid, /-key search, dock tooltips
- theme toggle + use-theme, sonner toasts, alert-dialog/skeleton,
  visual refresh of auth pages

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
root
2026-05-02 22:53:17 +00:00
parent 832512469a
commit 694b02e848
26 changed files with 2377 additions and 561 deletions

View File

@@ -0,0 +1,26 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/lib/use-theme";
export function ThemeToggle({ className }: { className?: string }) {
const { theme, toggle, mounted } = useTheme();
return (
<button
type="button"
onClick={toggle}
aria-label={mounted ? (theme === "dark" ? "切换到浅色" : "切换到深色") : "切换主题"}
className={
className ??
"flex h-full w-full items-center justify-center text-foreground/80 hover:text-foreground"
}
>
{mounted && theme === "dark" ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import * as React from "react";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { cn } from "@/lib/utils";
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
return (
<AlertDialogPrimitive.Trigger
data-slot="alert-dialog-trigger"
{...props}
/>
);
}
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: AlertDialogPrimitive.Popup.Props) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl bg-popover p-5 text-sm text-popover-foreground ring-1 ring-foreground/10 outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 sm:max-w-md",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-1.5", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"mt-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: AlertDialogPrimitive.Title.Props) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-base font-medium leading-none", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: AlertDialogPrimitive.Description.Props) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
function AlertDialogClose({ ...props }: AlertDialogPrimitive.Close.Props) {
return (
<AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
);
}
export {
AlertDialog,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};

View File

@@ -29,7 +29,7 @@ const DEFAULT_DISTANCE = 140
const DEFAULT_DISABLEMAGNIFICATION = false
const dockVariants = cva(
"supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md"
"mx-auto flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border border-border bg-background/70 p-2 shadow-lg shadow-black/5 backdrop-blur-md supports-backdrop-blur:bg-background/60 dark:shadow-black/40"
)
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
@@ -139,8 +139,7 @@ const DockIcon = ({
ref={ref}
style={{ width: scaleSize, height: scaleSize, padding }}
className={cn(
"flex aspect-square cursor-pointer items-center justify-center rounded-full",
disableMagnification && "hover:bg-muted-foreground transition-colors",
"flex aspect-square cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-muted",
className
)}
{...props}

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton };