spb/note-spboucher-ai
Public
1"use client";2import { useState } from "react";3import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, type DragEndEvent } from "@dnd-kit/core";4import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";5import { CSS } from "@dnd-kit/utilities";6import { Eye, EyeOff, GripVertical, Plus } from "lucide-react";7import { cn } from "@/lib/utils";8import type { Property } from "@/lib/databases/types";9import { useDb, usePropertyMutations } from "./hooks";10import { TypeIcon } from "./meta";11import { PropertyEditor } from "./PropertyEditor";1213/** Popover body: visibility toggles, drag reorder, open editor, add property. */14export function PropertyManager({ onClose }: { onClose: () => void }) {15 const ctx = useDb(); const m = usePropertyMutations(ctx.db, ctx.db.pageId); const [editing, setEditing] = useState<Property | "new" | null>(null);16 const hidden = ctx.view.config.hidden ?? []; const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));17 if (editing) return <PropertyEditor prop={editing === "new" ? undefined : editing} onClose={() => setEditing(null)} onBack={() => setEditing(null)} />;18 const onDragEnd = (e: DragEndEvent) => { const { active, over } = e; if (!over || active.id === over.id) return; const ids = ctx.props.map((p) => p.id); const next = arrayMove(ids, ids.indexOf(String(active.id)), ids.indexOf(String(over.id))); m.reorder.mutate(next); };19 const toggle = (id: string) => ctx.patchView({ hidden: hidden.includes(id) ? hidden.filter((x) => x !== id) : [...hidden, id] });20 return (21 <div className="w-64">22 <div className="mb-1 flex items-center justify-between text-[12px] font-medium text-fg-muted">Properties<span className="text-fg-faint">{ctx.props.length}</span></div>23 <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}><SortableContext items={ctx.props.map((p) => p.id)} strategy={verticalListSortingStrategy}>24 <div className="max-h-72 space-y-0.5 overflow-y-auto">{ctx.props.map((p) => <Row key={p.id} p={p} hidden={hidden.includes(p.id)} onToggle={() => toggle(p.id)} onEdit={() => setEditing(p)} />)}</div>25 </SortableContext></DndContext>26 <button type="button" onClick={() => setEditing("new")} className="mt-1 flex h-9 w-full items-center gap-2 rounded-md px-2 text-[13px] text-fg-muted hover:bg-bg-sunk hover:text-fg"><Plus className="size-4" /> New property</button>27 </div>28 );29}30function Row({ p, hidden, onToggle, onEdit }: { p: Property; hidden: boolean; onToggle: () => void; onEdit: () => void }) {31 const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: p.id, disabled: p.type === "title" });32 return (33 <div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }} className={cn("flex h-9 items-center gap-1 rounded-md pr-1 hover:bg-bg-sunk", isDragging && "z-10 bg-bg-elev shadow-pop", hidden && "opacity-60")}>34 <button type="button" {...attributes} {...listeners} className={cn("grid size-7 shrink-0 cursor-grab place-items-center text-fg-faint", p.type === "title" && "invisible")}><GripVertical className="size-3.5" /></button>35 <button type="button" onClick={onEdit} className="flex min-w-0 flex-1 items-center gap-1.5 text-left text-[13px]"><TypeIcon type={p.type} /><span className="truncate">{p.name}</span></button>36 {p.type !== "title" && <button type="button" onClick={onToggle} className="grid size-7 shrink-0 place-items-center rounded text-fg-faint hover:text-fg">{hidden ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}</button>}37 </div>38 );39}40