chore: init monorepo snapshot

This commit is contained in:
liaibo
2025-11-23 10:55:04 +08:00
commit c70ff52869
941 changed files with 246586 additions and 0 deletions
@@ -0,0 +1,257 @@
"use client";
import Link from "next/link";
import { useCallback, useMemo, useRef, useState } from "react";
import {
DndContext,
PointerSensor,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
type DragStartEvent,
} from "@dnd-kit/core";
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual";
import { ChevronRight, GripVertical, MoreHorizontal, Plus } from "lucide-react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import type { DocumentNode } from "@/lib/documents";
import { cn } from "@/lib/utils";
import { flattenDocumentTree } from "@/lib/sidebar-tree";
interface PrivateTreeProps {
nodes: DocumentNode[];
expanded: Set<string>;
activeId: string;
onToggleExpand: (id: string) => void;
onMove: (nodeId: string, parentId: string | null, index: number) => void;
onCreateChild: (parentId: string | null) => void;
onContextMenu: (event: React.MouseEvent, node: DocumentNode) => void;
}
const ROW_HEIGHT = 36;
export function PrivateTree({
nodes,
expanded,
activeId,
onToggleExpand,
onMove,
onCreateChild,
onContextMenu,
}: PrivateTreeProps) {
const scrollAreaRef = useRef<HTMLDivElement>(null);
const [activeDragId, setActiveDragId] = useState<string | null>(null);
const flatNodes = useMemo(() => flattenDocumentTree(nodes, expanded), [nodes, expanded]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
}),
);
// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({
count: flatNodes.length,
getScrollElement: () => scrollAreaRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 10,
});
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveDragId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const activeId = event.active.id as string;
const overId = event.over?.id as string | undefined;
setActiveDragId(null);
if (!overId || activeId === overId) {
return;
}
const activeIndex = flatNodes.findIndex((item) => item.node.id === activeId);
const overIndex = flatNodes.findIndex((item) => item.node.id === overId);
if (activeIndex === -1 || overIndex === -1) {
return;
}
const targetParent = flatNodes[overIndex].parentId;
const siblingList = flatNodes.filter((item) => item.parentId === targetParent);
const siblingIndex = siblingList.findIndex((item) => item.node.id === overId);
const position = siblingIndex === -1 ? siblingList.length : siblingIndex;
onMove(activeId, targetParent, position);
},
[flatNodes, onMove],
);
const handleDragCancel = useCallback(() => {
setActiveDragId(null);
}, []);
if (flatNodes.length === 0) {
return <div className="px-4 py-6 text-sm text-gray-400"></div>;
}
return (
<ScrollAreaPrimitive.Root type="auto" className="relative h-full w-full">
<ScrollAreaPrimitive.Viewport ref={scrollAreaRef} className="h-full w-full">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={flatNodes.map((item) => item.node.id)} strategy={verticalListSortingStrategy}>
<div className="relative h-full">
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = flatNodes[virtualRow.index];
return (
<VirtualRow key={item.node.id} virtualRow={virtualRow}>
<SortableTreeRow
node={item.node}
depth={item.depth}
expanded={expanded.has(item.node.id)}
hasChildren={item.node.children.length > 0}
activeId={activeId}
isDragging={activeDragId === item.node.id}
onToggleExpand={() => onToggleExpand(item.node.id)}
onCreateChild={() => onCreateChild(item.node.id)}
onContextMenu={(event) => onContextMenu(event, item.node)}
/>
</VirtualRow>
);
})}
</div>
</div>
</SortableContext>
</DndContext>
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.ScrollAreaScrollbar
orientation="vertical"
className="flex w-2.5 touch-none select-none border-l border-l-transparent bg-transparent px-0.5 py-2"
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-[#c9d6f8]" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function VirtualRow({
children,
virtualRow,
}: {
children: React.ReactNode;
virtualRow: VirtualItem;
}) {
return (
<div
className="absolute left-0 right-0"
style={{
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{children}
</div>
);
}
interface SortableTreeRowProps {
node: DocumentNode;
depth: number;
expanded: boolean;
hasChildren: boolean;
activeId: string;
isDragging: boolean;
onToggleExpand: () => void;
onCreateChild: () => void;
onContextMenu: (event: React.MouseEvent) => void;
}
function SortableTreeRow({
node,
depth,
expanded,
hasChildren,
activeId,
isDragging,
onToggleExpand,
onCreateChild,
onContextMenu,
}: SortableTreeRowProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: node.id,
});
const style = {
transform: CSS.Translate.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
className={cn(
"flex h-full items-center gap-1 border-b border-transparent px-2 text-sm text-gray-700 hover:bg-[#f5f7fb]",
activeId === node.id && "bg-[#e8f2ff] text-[#2563eb]",
isDragging && "opacity-60",
)}
onContextMenu={onContextMenu}
>
<button
type="button"
aria-label="拖拽排序"
className="flex h-6 w-6 items-center justify-center rounded-md text-gray-400 hover:text-gray-600"
{...listeners}
{...attributes}
>
<GripVertical className="h-4 w-4" />
</button>
<div
className="flex flex-1 items-center gap-2 rounded-md px-1 py-1"
style={{ paddingLeft: depth * 14 }}
>
{hasChildren ? (
<button
type="button"
aria-label="展开或折叠"
onClick={onToggleExpand}
className="flex h-5 w-5 items-center justify-center rounded-md hover:bg-gray-100"
>
<ChevronRight className={cn("h-3.5 w-3.5 transition-transform", expanded && "rotate-90")} />
</button>
) : (
<span className="h-5 w-5" />
)}
<Link href={`/documents/${node.id}`} className="flex-1 truncate text-left">
{node.title || "无标题"}
</Link>
</div>
<button
type="button"
aria-label="新建子页面"
onClick={onCreateChild}
className="rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
>
<Plus className="h-3.5 w-3.5" />
</button>
<button
type="button"
aria-label="更多操作"
onClick={(event) => {
event.preventDefault();
onContextMenu(event);
}}
className="rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
import type { DocumentRecord } from "@/lib/documents";
import type { WorkspaceSummary } from "@/lib/workspaces";
export type SidebarSectionId = "starred" | "public" | "shared" | "private" | "templates";
export interface TrashRecord {
id: string;
title: string | null;
deleted_at: string;
parent_id: string | null;
access_scope: DocumentRecord["access_scope"];
}
export interface SidebarInitialData {
activeWorkspaceId: string;
workspaces: WorkspaceSummary[];
documents: DocumentRecord[];
trashedDocuments: TrashRecord[];
}