Обзор
Компоненты
- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Auth Surface
- Avatar
- Badge
- Browse Catalog Dialog
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Command
- Connector Setup Dialog
- Cookie Banner
- Dialog
- Directory Card
- Directory Detail
- Directory Skeleton
- DrawerНовое
- Token Parts Input
- Empty
- Field
- Fieldset
- File Preview Modal
- File Preview Skeleton
- Form
- Frame
- Group
- Icon
- Input
- Input Group
- Kbd
- Label
- Legal Shell
- Menu
- Mermaid Diagram
- Mind Map Diagram
- Not Found Screen
- Onboarding Frame
- Popover
- PDF Thumbnail
- Personalization Landing
- Preview Card
- Pricing Page
- Progress
- Radio Group
- Ring Spinner
- Scroll Area
- Select
- Separator
- Settings Page
- Settings Skills
- Settings Connectors
- Settings Capabilities
- Settings Usage
- Settings Account
- Settings Billing
- Sheet
- Sidebar
- Skeleton
- Skill Create Dialog
- Slider
- Spinner
- Stat
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Tooltip
AI-компоненты
- Компоненты AI
- Chat Conversation
- Chat Message
- Chat Response
- Chat Suggestion
- Chat Prompt Input
- Slash Highlighted Textarea
- Chat Search Dialog
- Chat Skill Doc
- Chat Connector Detail
- Chat Attachments
- Chat File Card
- Chat Token Chip
- Chat Code Block
- Chat Image
- Chat Inline Citation
- Chat Sources
- Chat Web Search
- Chat Research
- Chat Source
- Chat Actions
- Chat Context
- Chat Loader
- Chat Compaction
- Chat Timeline
- Chat Snippet
- Chat Terminal
- Chat Stack Trace
- Chat Test Results
- Chat File Tree
- Chat Environment Variables
- Chat Audio Player
- Chat Transcription
- Chat Speech Input
- Chat Mic Selector
- Chat Voice Selector
- Chat Agent
- Chat Persona
- Chat Connection
- Chat Connector Suggestion
- Chat Queue
- Chat Checkpoint
- Chat Confirmation
- Chat Artifact
- Chat JSX Preview
- Chat Schema Display
- Chat Package Info
- Chat Commit
- Chat Plan
- Chat Open In Chat
- Chat Sandbox
- Chat Model Selector
- Chat Canvas
- Chat Node
- Chat Edge
Ресурсы
File Preview Modal
Модалка превью файла — шапка (имя + мета + X) и скролл-тело: картинка, исходник, PDF или таблица.
Клик по карточке → модалка. Клик по бейджу (PDF / MD / XLSX / Вставлено) → правая панель превью.
"use client";
import { IconChevronLeft } from "@tabler/icons-react";
import { useState } from "react";
import {
ChatArtifact,
ChatArtifactAction,
ChatArtifactClose,
ChatArtifactContent,
ChatArtifactDescription,
ChatArtifactHeader,
ChatArtifactInfo,
ChatArtifactTitle,
} from "@/components/ui/chat-artifact";
import {
type ChatAttachmentItem,
ChatAttachments,
} from "@/components/ui/chat-attachments";
import {
FilePreviewModal,
FilePreviewSource,
} from "@/components/ui/file-preview-modal";
import { PdfThumbnail } from "@/components/ui/pdf-thumbnail";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const IMG = `data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="272"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#9fd0ff"/><stop offset="1" stop-color="#ffd29f"/></linearGradient></defs><rect width="400" height="272" fill="url(#g)"/><text x="50%" y="50%" font-family="sans-serif" font-size="22" fill="#fff" text-anchor="middle" dominant-baseline="middle">oracul-amocrm</text></svg>`,
)}`;
const PAGE = `data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="280" height="280"><rect width="280" height="280" fill="#fff"/><g fill="#e2e0db"><rect x="28" y="30" width="120" height="10" rx="2"/><rect x="28" y="58" width="224" height="6" rx="2"/><rect x="28" y="72" width="224" height="6" rx="2"/><rect x="28" y="86" width="160" height="6" rx="2"/><rect x="28" y="120" width="224" height="6" rx="2"/></g><circle cx="210" cy="225" r="30" fill="none" stroke="#7fb0e0" stroke-width="2"/></svg>`,
)}`;
const MD = `---
name: dance-coach
version: 1.1.0
---
# 💃 Dance Coach
Опытный хореограф и танцевальный педагог: стили,
техника, история, подбор направления.`;
const PASTED = "185.103.71.121:40879@fd136ece5d:9c5fb2f1f6";
const XLSX_ROWS = [
["4", "Flora Delivery", "Email"],
["4", "Flora Delivery", "Telegram"],
["4", "Flora Delivery", "WhatsApp"],
["4", "Flora Delivery", "amoCRM интеграция"],
["4", "Flora Delivery", "ВКонтакте"],
];
const ITEMS: ChatAttachmentItem[] = [
{
id: "img",
name: "oracul-amocrm.jpg",
kind: "image",
url: "blob:img",
thumbnail: IMG,
},
{
id: "pdf",
name: "Счёт №7378.pdf",
kind: "doc",
url: "blob:pdf",
ext: "PDF",
},
{
id: "md",
name: "dance-coach.md",
kind: "doc",
url: "blob:md",
ext: "MD",
meta: "131 строка",
},
{
id: "xlsx",
name: "услуги.xlsx",
kind: "doc",
url: "blob:xlsx",
ext: "XLSX",
},
{
id: "pasted",
name: "Вставленный текст",
kind: "pasted",
url: "blob:pasted",
textContent: PASTED,
},
];
const META: Record<string, string> = {
img: "40.5 KB · image/jpeg",
pdf: "118.5 KB · application/pdf",
md: "8.8 KB · 131 строка · text/markdown",
xlsx: "44.9 KB · application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
pasted: "43 B · 2 строки · text/plain",
};
const SOURCE_NOTE = "Форматирование может отличаться от источника";
function isSource(item: ChatAttachmentItem) {
return (
item.kind === "pasted" ||
(item.kind === "doc" && ["MD", "TXT", "CSV"].includes(item.ext ?? ""))
);
}
function panelDesc(item: ChatAttachmentItem) {
return isSource(item) ? `${META[item.id]} · ${SOURCE_NOTE}` : META[item.id];
}
function XlsxTable() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID проекта</TableHead>
<TableHead>Имя проекта</TableHead>
<TableHead>Название услуги</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{XLSX_ROWS.map((row) => (
<TableRow key={row.join()}>
{row.map((cell, i) => (
<TableCell key={`${row.join()}-${i}`}>{cell}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
function body(item: ChatAttachmentItem) {
if (item.kind === "image") {
return (
<div className="flex justify-center">
<img
alt=""
className="h-auto w-full rounded-lg object-contain"
src={IMG}
/>
</div>
);
}
if (item.kind === "pasted") {
return <FilePreviewSource>{PASTED}</FilePreviewSource>;
}
if (item.ext === "PDF") {
return (
<PdfThumbnail filename={item.name} pages={1}>
<img alt="" className="block size-full object-cover" src={PAGE} />
</PdfThumbnail>
);
}
if (item.ext === "XLSX") {
return <XlsxTable />;
}
return <FilePreviewSource>{MD}</FilePreviewSource>;
}
export default function Particle() {
const [modalItem, setModalItem] = useState<ChatAttachmentItem | null>(null);
const [panelItem, setPanelItem] = useState<ChatAttachmentItem | null>(null);
return (
<div className="flex h-125 w-full gap-3">
<div className="flex min-w-0 flex-1 flex-col gap-3 rounded-xl border bg-card p-4">
<ChatAttachments
attachments={ITEMS}
onBadgeClick={setPanelItem}
onPreview={setModalItem}
/>
<p className="text-muted-foreground text-sm">
Клик по карточке → модалка. Клик по бейджу (PDF / MD / XLSX /
Вставлено) → правая панель превью.
</p>
</div>
{panelItem ? (
<ChatArtifact className="w-96 shrink-0">
<ChatArtifactHeader>
<div className="flex min-w-0 items-center gap-1">
<ChatArtifactAction
label="Назад"
onClick={() => setPanelItem(null)}
>
<IconChevronLeft stroke={1.5} />
</ChatArtifactAction>
<ChatArtifactInfo>
<ChatArtifactTitle className="truncate">
{panelItem.name}
</ChatArtifactTitle>
<ChatArtifactDescription className="truncate">
{panelDesc(panelItem)}
</ChatArtifactDescription>
</ChatArtifactInfo>
</div>
<ChatArtifactClose onClick={() => setPanelItem(null)} />
</ChatArtifactHeader>
<ChatArtifactContent className="p-4">
{body(panelItem)}
</ChatArtifactContent>
</ChatArtifact>
) : null}
<FilePreviewModal
filename={modalItem?.name}
lightBackground={modalItem?.kind !== "image"}
meta={modalItem ? META[modalItem.id] : undefined}
onOpenChange={(o) => {
if (!o) setModalItem(null);
}}
open={modalItem !== null}
>
{modalItem ? body(modalItem) : null}
</FilePreviewModal>
</div>
);
}
Установка
pnpm dlx shadcn@latest add @oracul/file-preview-modal
Использование
import {
FilePreviewModal,
FilePreviewSource,
} from "@/components/ui/file-preview-modal";
<FilePreviewModal
open={open}
onOpenChange={setOpen}
filename="dance-coach.md"
meta="8.8 KB · 131 строка · text/markdown"
>
<FilePreviewSource>{source}</FilePreviewSource>
</FilePreviewModal>Тело выбирает приложение по типу файла: картинка (<img>), исходник
(FilePreviewSource), PDF (pdf-thumbnail) или
таблица (Table). Тяжёлые рендереры (pdf.js,
парсинг xlsx) остаются на стороне приложения.
API
FilePreviewModal
| Prop | Тип | Описание |
|---|---|---|
open | boolean | Открыта ли модалка. |
onOpenChange | (open: boolean) => void | Смена состояния (backdrop/Esc/X). |
filename | ReactNode | Имя файла (заголовок). |
meta | ReactNode | Строка «размер · N строк · mime». |
children | ReactNode | Тело превью. |
lightBackground | boolean | Тёплая светлая подложка #F8F8F6 (для всего, кроме картинок). |
className | string | Доп. классы попапа. |
FilePreviewSource
Mono-блок для текстовых превью (children, className) — белая «бумага» в
светлой теме.
На этой странице