120 lines
3.1 KiB
TypeScript
120 lines
3.1 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { FileText } from "lucide-react";
|
||
|
|
import {
|
||
|
|
BaseBoxShapeUtil,
|
||
|
|
HTMLContainer,
|
||
|
|
T,
|
||
|
|
type TLBaseShape,
|
||
|
|
toDomPrecision,
|
||
|
|
} from "tldraw";
|
||
|
|
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
export type DocumentCardShape = TLBaseShape<
|
||
|
|
"document-card",
|
||
|
|
{
|
||
|
|
w: number;
|
||
|
|
h: number;
|
||
|
|
objectId: string;
|
||
|
|
title: string;
|
||
|
|
preview: string;
|
||
|
|
}
|
||
|
|
>;
|
||
|
|
|
||
|
|
function truncatePreview(text: string, max = 100): string {
|
||
|
|
const t = text.trim();
|
||
|
|
if (t.length <= max) return t;
|
||
|
|
return `${t.slice(0, max - 1)}…`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function DocumentCardBody({ shape }: { shape: DocumentCardShape }) {
|
||
|
|
const { title, preview } = shape.props;
|
||
|
|
const body = truncatePreview(preview);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<HTMLContainer>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"flex h-full w-full flex-col overflow-hidden rounded-xl border border-[hsl(var(--border))] border-l-[3px] border-l-[hsl(var(--teal))] bg-[hsl(var(--card))] text-[hsl(var(--card-foreground))] shadow-sm",
|
||
|
|
"ring-1 ring-[hsl(var(--teal)/0.2)]",
|
||
|
|
)}
|
||
|
|
style={{ fontSize: "11px", lineHeight: 1.45 }}
|
||
|
|
>
|
||
|
|
<div className="flex min-h-0 flex-1 flex-col px-2.5 pb-1.5 pt-2">
|
||
|
|
<div className="flex min-w-0 items-start gap-2">
|
||
|
|
<FileText
|
||
|
|
className="mt-0.5 size-4 shrink-0 text-[hsl(var(--teal))]"
|
||
|
|
strokeWidth={2}
|
||
|
|
aria-hidden
|
||
|
|
/>
|
||
|
|
<div className="min-w-0 flex-1 font-semibold leading-tight tracking-tight text-[13px]">
|
||
|
|
{title || "Untitled document"}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p
|
||
|
|
className="mt-2 line-clamp-3 min-h-[3.6em] text-[11px] leading-snug text-[hsl(var(--muted-foreground))]"
|
||
|
|
title={preview || undefined}
|
||
|
|
>
|
||
|
|
{body || "No preview yet."}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="border-t border-[hsl(var(--border)/0.65)] bg-[hsl(var(--muted)/0.35)] px-2.5 py-1 text-[9px] font-medium uppercase tracking-wider text-[hsl(var(--muted-foreground))]">
|
||
|
|
Document
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</HTMLContainer>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export class DocumentCardShapeUtil extends BaseBoxShapeUtil<any> {
|
||
|
|
static override type = "document-card";
|
||
|
|
|
||
|
|
static override props = {
|
||
|
|
w: T.number,
|
||
|
|
h: T.number,
|
||
|
|
objectId: T.string,
|
||
|
|
title: T.string,
|
||
|
|
preview: T.string,
|
||
|
|
};
|
||
|
|
|
||
|
|
override getDefaultProps(): DocumentCardShape["props"] {
|
||
|
|
return {
|
||
|
|
w: 220,
|
||
|
|
h: 140,
|
||
|
|
objectId: "",
|
||
|
|
title: "Document",
|
||
|
|
preview: "",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
override getAriaDescriptor(shape: DocumentCardShape) {
|
||
|
|
return shape.props.title;
|
||
|
|
}
|
||
|
|
|
||
|
|
override component(shape: DocumentCardShape) {
|
||
|
|
return <DocumentCardBody shape={shape} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
override indicator(shape: DocumentCardShape) {
|
||
|
|
return (
|
||
|
|
<rect
|
||
|
|
width={toDomPrecision(shape.props.w)}
|
||
|
|
height={toDomPrecision(shape.props.h)}
|
||
|
|
rx={10}
|
||
|
|
ry={10}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
override useLegacyIndicator() {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
override getIndicatorPath(shape: DocumentCardShape) {
|
||
|
|
const p = new Path2D();
|
||
|
|
p.roundRect(0, 0, shape.props.w, shape.props.h, 10);
|
||
|
|
return p;
|
||
|
|
}
|
||
|
|
}
|