ubiquitous-invention/apps/web/components/whiteboard/shapes/task-card.tsx

187 lines
5.7 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import { Calendar, User } from "lucide-react";
import {
BaseBoxShapeUtil,
HTMLContainer,
T,
type TLBaseShape,
toDomPrecision,
} from "tldraw";
import { cn } from "@/lib/utils";
export type TaskCardShape = TLBaseShape<
"task-card",
{
w: number;
h: number;
objectId: string;
title: string;
status: "open" | "in_progress" | "done" | "closed";
priority: string;
assignees: string;
dueDate: string;
}
>;
chore(lint+types): green pnpm lint && pnpm type-check from a clean clone Path-A first task: get the repo's two repo-wide quality gates passing. Both were failing from a clean clone in ways that were silently hiding each other. Headline fixes: * apps/web: add an eslint 9 flat config (eslint.config.mjs) using FlatCompat against next/core-web-vitals + next/typescript, and switch the `lint` script from `next lint` to `eslint .`. Previously `next lint` fell into its interactive setup prompt because there was no config at all in apps/web, which made `pnpm lint` permanently fail before any rule ever ran. * packages/shared/src/utils/id.ts: replace `randomUUID` from `node:crypto` with `globalThis.crypto.randomUUID`. `@tasks/shared` is forbidden from using Node-only APIs (per AGENTS.md / repo-overview.mdc) because it has to be importable from the browser bundle. Adjacent fixes pulled in to make the gates actually green: * apps/mcp-server/tsconfig.json: drop vestigial rootDir / declaration* / outDir / sourceMap (build is via tsup, not tsc emit) and add allowImportingTsExtensions. The MCP server uses `.ts`-extension re-export shims (db.ts / schema.ts / shared-types.ts) so tsup can inline workspace .ts sources into the bundle. * apps/collab-server/tsconfig.json: same simplification. * apps/mcp-server/package.json: add @types/node so `process.env` in packages/database/src/client.ts (transitively pulled into the MCP server's type-check) resolves. * apps/web/components/ui/input.tsx: empty `interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}` -> `type` alias. * apps/web/server/lib/workspace-guard.ts: `from(args.table as any)` -> `as unknown as PgTable` with a comment. Standard drizzle escape hatch for structural generic tables. * apps/web/components/whiteboard/shapes/{document,project,task}-card.tsx: `BaseBoxShapeUtil<any>` -> `BaseBoxShapeUtil<{Shape}>` plus inline `declare module "@tldraw/tlschema"` augmentation of TLGlobalShapePropsMap. Required adding @tldraw/tlschema as a direct devDep of apps/web so the augmentation target resolves; previously it was only present transitively under tldraw's own deps. Result: `pnpm lint && pnpm type-check` exits 0 across all 6 packages. 16 unused-import / exhaustive-deps warnings remain; they're pre-existing housekeeping and out of scope for this task. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-fix-lint-and-shared-types.md (status: done). Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:11:52 -04:00
declare module "@tldraw/tlschema" {
interface TLGlobalShapePropsMap {
"task-card": TaskCardShape["props"];
}
}
const STATUS_DOT: Record<TaskCardShape["props"]["status"], string> = {
open: "bg-zinc-400 dark:bg-zinc-500",
in_progress: "bg-blue-500 dark:bg-blue-400",
done: "bg-emerald-500 dark:bg-emerald-400",
closed: "bg-slate-500 dark:bg-slate-400",
};
function priorityClass(priority: string): string {
const p = priority.toLowerCase();
if (p.includes("high")) return "bg-red-500/15 text-red-700 ring-1 ring-red-500/30 dark:text-red-300";
if (p.includes("low")) return "bg-emerald-500/15 text-emerald-700 ring-1 ring-emerald-500/30 dark:text-emerald-300";
return "bg-amber-500/15 text-amber-800 ring-1 ring-amber-500/30 dark:text-amber-200";
}
function initialsFromName(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function TaskCardBody({ shape }: { shape: TaskCardShape }) {
const { title, status, priority, assignees, dueDate } = shape.props;
const names = assignees
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.slice(0, 2);
return (
<HTMLContainer>
<div
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-[hsl(var(--card-foreground))] shadow-sm",
"ring-1 ring-[hsl(var(--primary)/0.12)]",
)}
style={{ fontSize: "11px", lineHeight: 1.35 }}
>
<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">
<span
className={cn("mt-1.5 size-2 shrink-0 rounded-full", STATUS_DOT[status])}
title={status.replace("_", " ")}
aria-hidden
/>
<div className="min-w-0 flex-1 font-semibold leading-tight tracking-tight text-[13px]">
{title || "Untitled task"}
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<span
className={cn(
"inline-flex max-w-full truncate rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide",
priorityClass(priority),
)}
>
{priority || "—"}
</span>
{names.length > 0 ? (
<div className="flex items-center gap-0.5">
{names.map((name, i) => (
<span
key={`${name}-${i}`}
className="flex size-6 items-center justify-center rounded-full bg-[hsl(var(--muted))] text-[9px] font-semibold text-[hsl(var(--muted-foreground))] ring-1 ring-[hsl(var(--border))]"
title={name}
>
{initialsFromName(name)}
</span>
))}
</div>
) : (
<span className="inline-flex items-center gap-0.5 text-[10px] text-[hsl(var(--muted-foreground))]">
<User className="size-3 shrink-0 opacity-70" aria-hidden />
<span>Unassigned</span>
</span>
)}
</div>
{dueDate ? (
<div className="mt-auto flex items-center gap-1 pt-2 text-[10px] text-[hsl(var(--muted-foreground))]">
<Calendar className="size-3 shrink-0 opacity-80" aria-hidden />
<span className="truncate">{dueDate}</span>
</div>
) : (
<div className="mt-auto pt-2 text-[10px] text-[hsl(var(--muted-foreground)/0.6)]">No due date</div>
)}
</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))]">
Task
</div>
</div>
</HTMLContainer>
);
}
chore(lint+types): green pnpm lint && pnpm type-check from a clean clone Path-A first task: get the repo's two repo-wide quality gates passing. Both were failing from a clean clone in ways that were silently hiding each other. Headline fixes: * apps/web: add an eslint 9 flat config (eslint.config.mjs) using FlatCompat against next/core-web-vitals + next/typescript, and switch the `lint` script from `next lint` to `eslint .`. Previously `next lint` fell into its interactive setup prompt because there was no config at all in apps/web, which made `pnpm lint` permanently fail before any rule ever ran. * packages/shared/src/utils/id.ts: replace `randomUUID` from `node:crypto` with `globalThis.crypto.randomUUID`. `@tasks/shared` is forbidden from using Node-only APIs (per AGENTS.md / repo-overview.mdc) because it has to be importable from the browser bundle. Adjacent fixes pulled in to make the gates actually green: * apps/mcp-server/tsconfig.json: drop vestigial rootDir / declaration* / outDir / sourceMap (build is via tsup, not tsc emit) and add allowImportingTsExtensions. The MCP server uses `.ts`-extension re-export shims (db.ts / schema.ts / shared-types.ts) so tsup can inline workspace .ts sources into the bundle. * apps/collab-server/tsconfig.json: same simplification. * apps/mcp-server/package.json: add @types/node so `process.env` in packages/database/src/client.ts (transitively pulled into the MCP server's type-check) resolves. * apps/web/components/ui/input.tsx: empty `interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}` -> `type` alias. * apps/web/server/lib/workspace-guard.ts: `from(args.table as any)` -> `as unknown as PgTable` with a comment. Standard drizzle escape hatch for structural generic tables. * apps/web/components/whiteboard/shapes/{document,project,task}-card.tsx: `BaseBoxShapeUtil<any>` -> `BaseBoxShapeUtil<{Shape}>` plus inline `declare module "@tldraw/tlschema"` augmentation of TLGlobalShapePropsMap. Required adding @tldraw/tlschema as a direct devDep of apps/web so the augmentation target resolves; previously it was only present transitively under tldraw's own deps. Result: `pnpm lint && pnpm type-check` exits 0 across all 6 packages. 16 unused-import / exhaustive-deps warnings remain; they're pre-existing housekeeping and out of scope for this task. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-fix-lint-and-shared-types.md (status: done). Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:11:52 -04:00
export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCardShape> {
static override type = "task-card";
static override props = {
w: T.number,
h: T.number,
objectId: T.string,
title: T.string,
status: T.literalEnum("open", "in_progress", "done", "closed"),
priority: T.string,
assignees: T.string,
dueDate: T.string,
};
override getDefaultProps(): TaskCardShape["props"] {
return {
w: 240,
h: 160,
objectId: "",
title: "Task",
status: "open",
priority: "Medium",
assignees: "",
dueDate: "",
};
}
override getAriaDescriptor(shape: TaskCardShape) {
return shape.props.title;
}
override component(shape: TaskCardShape) {
return <TaskCardBody shape={shape} />;
}
override indicator(shape: TaskCardShape) {
return (
<rect
width={toDomPrecision(shape.props.w)}
height={toDomPrecision(shape.props.h)}
rx={10}
ry={10}
/>
);
}
override useLegacyIndicator() {
return false;
}
override getIndicatorPath(shape: TaskCardShape) {
const p = new Path2D();
p.roundRect(0, 0, shape.props.w, shape.props.h, 10);
return p;
}
}