"use client";
import * as React from "react";
import { AnimatePresence, motion } from "framer-motion";
import { IconCheck, IconCloudUpload, IconExternalLink, IconFile, IconFileTypePdf, IconPhoto, IconX } from "@tabler/icons-react";
import type { ColineFilePreview, ColineUploadedFile } from "@colineapp/sdk/v2";
import { cn } from "../lib/utils";
import { staggerDelay, transitions } from "../lib/motion";
import { useColine } from "../provider";
import { Button } from "./button";
import { Spinner } from "./spinner";
function FileTypeIcon({ mimeType }: { mimeType: string | null | undefined }) {
if (mimeType?.startsWith("image/")) return ;
if (mimeType === "application/pdf") return ;
return ;
}
/**
* Any readable workspace file, previewed: images and video inline, other
* files as a card that opens them in Coline. Uses the viewer's access;
* apps preview each other's files without integrations.
*/
export function FilePreview({
fileId,
variant = "card",
className,
}: {
fileId: string;
variant?: "inline" | "card" | "full";
className?: string;
}) {
const coline = useColine();
// Keyed by file so a new fileId shows the skeleton, not the last file.
const [loaded, setLoaded] = React.useState<{ fileId: string; preview: ColineFilePreview | null; error: string | null } | null>(
null,
);
React.useEffect(() => {
let cancelled = false;
coline.files
.preview(fileId)
.then((result) => {
if (!cancelled) setLoaded({ fileId, preview: result, error: null });
})
.catch((cause: unknown) => {
if (!cancelled) setLoaded({ fileId, preview: null, error: cause instanceof Error ? cause.message : String(cause) });
});
return () => {
cancelled = true;
};
}, [coline, fileId]);
const current = loaded?.fileId === fileId ? loaded : null;
const preview = current?.preview ?? null;
const error = current?.error ?? null;
const open = () => void coline.navigation.openFile(fileId);
if (error) {
return (
Preview unavailable
);
}
if (!preview) {
return ;
}
const media = preview.media;
if (variant !== "inline" && media?.mimeType?.startsWith("image/")) {
return (
{preview.name}
);
}
if (variant !== "inline" && media?.mimeType?.startsWith("video/")) {
return (
);
}
return (
);
}
interface UploadState {
id: string;
name: string;
status: "uploading" | "done" | "failed";
error?: string;
file?: ColineUploadedFile;
}
/**
* Drop files (or click to choose) and they upload to Drive through
* Coline, with per-file status. Large files stream directly to storage.
*/
export function FileDropzone({
onUploaded,
accept,
multiple = true,
driveId,
parentId,
label = "Drop files here",
hint = "or click to choose",
className,
}: {
onUploaded?: (file: ColineUploadedFile) => void;
accept?: string;
multiple?: boolean;
driveId?: string;
parentId?: string | null;
label?: string;
hint?: string;
className?: string;
}) {
const coline = useColine();
const inputRef = React.useRef(null);
const [dragging, setDragging] = React.useState(false);
const [uploads, setUploads] = React.useState([]);
const depth = React.useRef(0);
const upload = (files: File[]) => {
for (const file of files) {
const id = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
setUploads((current) => [{ id, name: file.name, status: "uploading" as const }, ...current].slice(0, 8));
void coline.files
.upload({
name: file.name,
data: file,
...(file.type ? { mimeType: file.type } : {}),
...(driveId ? { driveId } : {}),
...(parentId !== undefined ? { parentId } : {}),
})
.then((uploaded) => {
setUploads((current) => current.map((entry) => (entry.id === id ? { ...entry, status: "done", file: uploaded } : entry)));
onUploaded?.(uploaded);
})
.catch((cause: unknown) => {
const message = cause instanceof Error ? cause.message : String(cause);
setUploads((current) => current.map((entry) => (entry.id === id ? { ...entry, status: "failed", error: message } : entry)));
});
}
};
return (
);
}
/** A file picked from the workspace (host-rendered picker). */
export function useFilePicker() {
const coline = useColine();
return React.useCallback((types?: string[]) => coline.pickers.file(types ? { types } : undefined), [coline]);
}
/** Ask where to keep a file, then move it there. Resolves null when dismissed. */
export function SaveAsButton({
fileId,
children = "Save to…",
onSaved,
}: {
fileId: string;
children?: React.ReactNode;
onSaved?: (file: { fileId: string; driveId: string; parentId: string | null }) => void;
}) {
const coline = useColine();
const [pending, setPending] = React.useState(false);
return (
);
}