45 lines
3.3 KiB
TypeScript
45 lines
3.3 KiB
TypeScript
import { useDraggable, useDroppable } from "@dnd-kit/react";
|
|
import { pointerIntersection } from "@dnd-kit/collision";
|
|
import { GripVertical } from "lucide-react";
|
|
import type { CSSProperties, ReactNode } from "react";
|
|
|
|
function chartSlotPosition(index: number, columns: number): CSSProperties {
|
|
return { gridRow: Math.floor(index / columns) + 1, gridColumn: index % columns + 1 };
|
|
}
|
|
|
|
export function ChartDropSlot({ index, columns, occupied }: { index: number; columns: number; occupied: boolean }) {
|
|
const { ref, isDropTarget } = useDroppable({ id: `slot:${index}`, type: "slot", accept: "chart", data: { slot: index }, collisionDetector: pointerIntersection });
|
|
return <div ref={ref} data-chart-slot={index} style={chartSlotPosition(index, columns)}
|
|
className={`pointer-events-none relative flex min-h-0 min-w-0 items-center justify-center rounded-xl border border-dashed ${isDropTarget ? "z-10 border-primary/40 bg-primary/5 ring-1 ring-primary/15 ring-inset" : occupied ? "border-transparent" : "border-border bg-muted/20"}`}>
|
|
{!occupied ? <p className="text-center text-xs leading-6 text-muted-foreground"><span className="block tabular-nums">位置 {index + 1}</span>拖动图表到这里</p> : null}
|
|
</div>;
|
|
}
|
|
|
|
/** The chart stays under the same grid parent when its CSS position changes. */
|
|
export function ManagedChartSlot({ id, title, index, columns, located, helpId, register, children }: {
|
|
id: string;
|
|
title: string;
|
|
index: number;
|
|
columns: number;
|
|
located: boolean;
|
|
helpId: string;
|
|
register: (id: string, element: HTMLElement | null) => void;
|
|
children: ReactNode;
|
|
}) {
|
|
const { ref, handleRef, isDragSource } = useDraggable({ id, type: "chart", data: { title } });
|
|
return <article ref={(node) => { ref(node); register(id, node); }} tabIndex={-1}
|
|
data-managed-chart={id} data-chart-title={title} data-slot-index={index} data-chart-located={located} data-chart-dragging={isDragSource}
|
|
aria-label={`图表窗口 ${title}`} style={chartSlotPosition(index, columns)}
|
|
className="group/chart-slot relative z-0 flex min-h-0 min-w-0 flex-col rounded-xl outline-hidden">
|
|
<div className={`flex h-9 shrink-0 items-center rounded-lg ${located ? "bg-primary/10" : ""}`}>
|
|
<button ref={handleRef} type="button" data-slot="button" className={`flex h-full w-full touch-none select-none items-center justify-start gap-1 rounded-lg border border-transparent px-2 text-xs font-normal outline-hidden focus-visible:outline-1 focus-visible:outline-ring focus-visible:-outline-offset-1 active:cursor-grabbing ${isDragSource ? "cursor-grabbing bg-muted text-foreground" : "cursor-grab bg-transparent text-muted-foreground [@media(hover:hover)]:hover:bg-muted/70 [@media(hover:hover)]:hover:text-foreground"}`}
|
|
aria-label={`拖动图表 ${title}`} aria-describedby={helpId}>
|
|
<GripVertical size={14} aria-hidden /><span className="tabular-nums">位置 {index + 1}</span>
|
|
{located ? <span aria-hidden="true" className="ml-auto shrink-0 text-xs font-medium text-primary">已定位</span> : null}
|
|
</button>
|
|
</div>
|
|
<div className="min-h-0 min-w-0 flex-1">{children}</div>
|
|
<div aria-hidden="true" data-chart-hover-highlight="" className={`pointer-events-none absolute inset-0 z-10 rounded-xl bg-primary/[0.04] ${isDragSource ? "opacity-100" : "opacity-0 [@media(hover:hover)]:group-hover/chart-slot:opacity-100"}`} />
|
|
</article>;
|
|
}
|