"use client";
import { useEffect, useRef, useState } from "react";
import { Check, ExternalLink, Sparkles, RefreshCw, AlertCircle, Maximize2 } from "lucide-react";
import { Spinner, Tip } from "@/components/ui/primitives";
import { cn } from "@/lib/utils";
import type { Property, RowOut, RelationRef, FileRef } from "@/lib/databases/types";
import { isError, isPending, READONLY_TYPES } from "@/lib/databases/types";
import { displayValue, fmtDateValue, relativeDate, numberOf, optionOf, fmtNumber, isEmpty } from "@/lib/databases/values";
import { useDb } from "../hooks";
import { OptionChip, SelectPopover } from "./SelectCell";
import { RelationChip, RelationPopover } from "./RelationCell";
import { FileChip, FilePopover } from "./FileCell";
const toLocalInput = (v: unknown, time: boolean) => { if (typeof v !== "string" || !v) return ""; if (!time) return v.slice(0, 10); const d = new Date(v); if (Number.isNaN(d.getTime())) return ""; const z = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}`; };
/** Read-only rendering used by every view. */
export function CellDisplay({ p, value, row, compact }: { p: Property; value: unknown; row?: RowOut; compact?: boolean }) {
if (isPending(value)) return generating…;
if (isError(value)) return error;
if (isEmpty(value)) return null;
switch (p.type) {
case "title": return {row?.icon ? `${row.icon} ` : ""}{String(value)};
case "checkbox": return {value ? : null};
case "select": case "status": { const o = optionOf(p, value); return o ? : {String(value)}; }
case "multi_select": return {(Array.isArray(value) ? value : [value]).map((id) => { const o = optionOf(p, id); return ; })};
case "relation": return {(value as RelationRef[]).slice(0, compact ? 3 : 50).map((r) => )}{compact && (value as RelationRef[]).length > 3 && +{(value as RelationRef[]).length - 3}};
case "file": return {(value as FileRef[]).map((f) => )};
case "url": { const s = String(value); return e.stopPropagation()} className="inline-flex max-w-full items-center gap-1 truncate text-accent hover:underline">{s.replace(/^https?:\/\/(www\.)?/, "")}; }
case "date": return {fmtDateValue(value, p.config.includeTime)}{!compact && {relativeDate(value)}};
case "created_time": case "last_edited_time": return {fmtDateValue(value, true)};
case "number": return {fmtNumber(numberOf(value), p.config.format)};
case "formula": return typeof value === "boolean" ? : {displayValue(p, value)};
case "rollup": return Array.isArray(value) ? {value.map((x, i) => {String(x)})} : {displayValue(p, value)};
case "ai": return {String(value)};
default: return {String(value)};
}
}
/** Editable cell (table + side peek). Controlled editing state so the table can drive it from the keyboard. */
export function Cell({ p, row, editing, onEdit, mode = "table" }: { p: Property; row: RowOut; editing: boolean; onEdit: (o: boolean) => void; mode?: "table" | "form" }) {
const ctx = useDb(); const value = row.values[p.id]; const set = (v: unknown) => ctx.setCell(row.id, { [p.id]: v });
const readonly = READONLY_TYPES.has(p.type);
const base = cn("flex min-h-9 w-full items-center gap-1 px-2 py-1 text-[13px]", mode === "form" && "min-h-10 rounded-md hover:bg-bg-sunk", !readonly && "cursor-text");
if (p.type === "checkbox") return ;
if (p.type === "ai") return ;
if (readonly) return
;
if (p.type === "select" || p.type === "status" || p.type === "multi_select") return ctx.addOption(p.id, n)} open={editing} onOpenChange={onEdit}>
;
if (p.type === "relation") return
;
if (p.type === "file") return
;
if (p.type === "date") return ;
return ;
}
function TextCell({ p, row, value, editing, onEdit, onChange, className, mode }: { p: Property; row: RowOut; value: unknown; editing: boolean; onEdit: (o: boolean) => void; onChange: (v: unknown) => void; className: string; mode: "table" | "form" }) {
const ctx = useDb(); const [draft, setDraft] = useState(""); const ref = useRef(null);
useEffect(() => { if (editing) { setDraft(value == null ? "" : String(value)); setTimeout(() => { ref.current?.focus(); ref.current?.select?.(); }, 0); } }, [editing]); // eslint-disable-line react-hooks/exhaustive-deps
const commit = () => { if (!editing) return; const v = p.type === "number" ? (draft.trim() === "" ? null : numberOf(draft)) : draft; if (v !== (value ?? (p.type === "number" ? null : ""))) onChange(v); onEdit(false); };
if (editing) {
const common = { ref: ref as any, value: draft, onChange: (e: any) => setDraft(e.target.value), onBlur: commit, onKeyDown: (e: React.KeyboardEvent) => { if (e.key === "Enter" && !(e.shiftKey && mode === "form")) { e.preventDefault(); commit(); } if (e.key === "Escape") { e.preventDefault(); onEdit(false); } e.stopPropagation(); }, className: "w-full bg-transparent text-[13px] outline-none" };
return {mode === "form" && p.type === "text" ? : }
;
}
return (
onEdit(true)}>
{p.type === "title" && mode === "table" && }
);
}
function DateCell({ p, value, editing, onEdit, onChange, className }: { p: Property; value: unknown; editing: boolean; onEdit: (o: boolean) => void; onChange: (v: unknown) => void; className: string }) {
const time = !!p.config.includeTime; const ref = useRef(null);
useEffect(() => { if (editing) setTimeout(() => { ref.current?.focus(); try { ref.current?.showPicker?.(); } catch {} }, 0); }, [editing]);
if (editing) return { const v = e.target.value; onChange(!v ? null : time ? new Date(v).toISOString() : v); }} onBlur={() => onEdit(false)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === "Escape") { e.preventDefault(); onEdit(false); } if (e.key === "Backspace" || e.key === "Delete") { if (!e.currentTarget.value) onChange(null); } e.stopPropagation(); }} />
;
return onEdit(true)}>
;
}
function AiCell({ p, row, mode }: { p: Property; row: RowOut; mode: "table" | "form" }) {
const ctx = useDb(); const v = row.values[p.id]; const pending = isPending(v);
return (
{isEmpty(v) && !pending && !isError(v) ? : }
{!isEmpty(v) && !pending && }
);
}