ubiquitous-invention/apps/web/components/whiteboard/shapes/document-card.tsx
Randall Stillwell a508ece6e7 feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:

- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)

Made-with: Cursor
2026-03-26 22:39:16 -05:00

119 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;
}
}