Обзор
Компоненты
- 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
Ресурсы
Browse Catalog Dialog
Каталог — большой диалог витрины: табы Навыки/Коннекторы, поиск, Фильтр/Сортировка и сетка карточек.
"use client";
import { IconLink, IconPlus, IconSettings } from "@tabler/icons-react";
import * as React from "react";
import {
BrowseCatalogDialog,
type CatalogItem,
type CatalogSort,
type CatalogTab,
} from "@/components/ui/browse-catalog-dialog";
import { BackButton } from "@/components/ui/back-button";
import { Button } from "@/components/ui/button";
import {
ChatFileTree,
ChatFileTreeFile,
ChatFileTreeFolder,
} from "@/components/ui/chat-file-tree";
import {
ChatSkillDoc,
type SkillDocMode,
} from "@/components/ui/chat-skill-doc";
import { DirectoryDetail } from "@/components/ui/directory-detail";
// Brand monogram — the app passes a real favicon; the demo uses initials.
function Brand({ children }: { children: string }) {
return (
<span className="font-semibold text-foreground text-sm">{children}</span>
);
}
const TRUST_NOTE =
"Подключайте только коннекторы от разработчиков, которым доверяете. Oracul не контролирует, какие инструменты они предоставляют, и не может гарантировать их работоспособность.";
const SKILLS: CatalogItem[] = [
{ id: "weekly-report", name: "weekly-report", publisher: "Oracul", installs: 12400, description: "Еженедельный отчёт из метрик по шаблону команды." },
{ id: "documents-ru", name: "documents-ru", publisher: "Oracul", installs: 9800, description: "Документы на русском: договоры, акты, письма." },
{ id: "pdf-tools", name: "pdf-tools", publisher: "Сообщество", installs: 6100, description: "Чтение, объединение и извлечение данных из PDF.", installed: true },
{ id: "brand-voice", name: "brand-voice", publisher: "Сообщество", installs: 2300, description: "Правила тональности бренда и редполитика." },
];
const CONNECTORS: CatalogItem[] = [
{ id: "miro", name: "Miro", icon: <Brand>Mi</Brand>, publisher: "Miro", installs: 18700, description: "Доски, фреймы и стикеры прямо из чата.", installed: true },
{ id: "notion", name: "Notion", icon: <Brand>No</Brand>, publisher: "Notion", installs: 15200, description: "Страницы, базы данных и заметки." },
{ id: "yadisk", name: "Яндекс Диск", icon: <Brand>Я</Brand>, publisher: "Яндекс", installs: 8400, description: "Файлы и папки облака для анализа." },
{ id: "teletype", name: "Teletype", icon: <Brand>Te</Brand>, publisher: "Teletype", installs: 3100, description: "Публикация и черновики постов." },
];
// Per-connector detail data — carousel examples, capability, docs (1:1 with the
// frontend CatalogDetail; the app supplies this from the connector snapshot).
const CONNECTOR_DETAIL: Record<
string,
{ tools: string[]; examples: { title: string; body: string }[]; capability: string; docsUrl: string }
> = {
miro: {
tools: ["Список досок", "Создать фрейм", "Добавить стикер", "Экспорт доски", "Поиск по доскам"],
examples: [
{ title: "Покажи мои последние доски в Miro", body: "Нашёл 3 доски: «Спринт 12», «Карта пути», «Ретро Q2»." },
{ title: "Создай фрейм «План на неделю»", body: "Готово — фрейм добавлен на доску «Спринт 12»." },
],
capability: "Интерактивный",
docsUrl: "https://developers.miro.com",
},
notion: {
tools: ["Поиск страниц", "Создать страницу", "Обновить базу", "Добавить запись"],
examples: [
{ title: "Найди страницу «Онбординг» в Notion", body: "Нашёл страницу и 2 связанные базы данных." },
{ title: "Добавь задачу в базу «Бэклог»", body: "Запись создана со статусом «To do»." },
],
capability: "Интерактивный",
docsUrl: "https://developers.notion.com",
},
yadisk: {
tools: ["Список файлов", "Скачать файл", "Загрузить файл"],
examples: [
{ title: "Покажи файлы в папке «Отчёты»", body: "В папке 12 файлов, последний обновлён вчера." },
],
capability: "Только чтение",
docsUrl: "https://yandex.ru/dev/disk",
},
teletype: {
tools: ["Список постов", "Создать черновик", "Опубликовать"],
examples: [
{ title: "Создай черновик из этого диалога", body: "Черновик «Заметки о DS» готов к публикации." },
],
capability: "Интерактивный",
docsUrl: "https://teletype.in",
},
};
function SkillDetail({ item, onBack }: { item: CatalogItem; onBack: () => void }) {
const [file, setFile] = React.useState("SKILL.md");
const [mode, setMode] = React.useState<SkillDocMode>("rendered");
const isSkill = file === "SKILL.md";
const body = `# ${item.name}\n\n${item.description}\n\n## Когда использовать\n\nАктивируйте навык, когда задача соответствует его описанию — Oracul подберёт его автоматически.\n\n## Инструкции\n\n1. Соберите входные данные.\n2. Следуйте шаблону команды.\n3. Верните результат в нужном формате.`;
const source = `---\nname: ${item.name}\ndescription: ${item.description}\n---\n\n${body}`;
const license = `MIT License\n\nCopyright (c) 2026 ${item.publisher ?? "Oracul"}\n\nРазрешается свободное использование, копирование и распространение\nданного навыка при сохранении уведомления об авторских правах.`;
return (
<div className="flex h-full flex-col">
<BackButton className="mb-4" onClick={onBack} />
<div className="mb-4 flex items-start justify-between gap-4">
<div className="min-w-0">
<h3 className="truncate font-medium text-foreground text-lg">
{item.name}
</h3>
<p className="mt-0.5 text-muted-foreground text-sm">
{item.publisher}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
aria-label="Скопировать ссылку"
onClick={() =>
navigator.clipboard?.writeText(
`https://oracul.co/skills/${item.id}`,
)
}
size="icon"
variant="ghost"
>
<IconLink className="-rotate-45" stroke={1.5} />
</Button>
<Button variant={item.installed ? "outline" : "default"}>
{item.installed ? "Отключить" : "Добавить"}
</Button>
</div>
</div>
<div className="flex min-h-0 flex-1 gap-4">
<div className="w-50 shrink-0">
<ChatFileTree
onSelect={(n) => setFile(n.id)}
selectedId={file}
>
<ChatFileTreeFolder defaultOpen name={item.name}>
<ChatFileTreeFile id="SKILL.md" name="SKILL.md" />
<ChatFileTreeFile id="LICENSE.txt" name="LICENSE.txt" />
</ChatFileTreeFolder>
</ChatFileTree>
</div>
<div className="min-h-0 flex-1 overflow-hidden rounded-xl border">
<ChatSkillDoc
body={isSkill ? body : license}
description={isSkill ? item.description : undefined}
mode={isSkill ? mode : "source"}
onCopy={() =>
navigator.clipboard?.writeText(isSkill ? source : license)
}
onModeChange={isSkill ? setMode : undefined}
source={isSkill ? source : license}
title={`${item.name}/${file}`}
version={isSkill ? "v1.0" : undefined}
/>
</div>
</div>
</div>
);
}
export default function Particle() {
const [open, setOpen] = React.useState(false);
const [tab, setTab] = React.useState<CatalogTab>("connectors");
const [query, setQuery] = React.useState("");
const [sort, setSort] = React.useState<CatalogSort>("popular");
return (
<>
<Button onClick={() => setOpen(true)} variant="outline">
Открыть каталог
</Button>
<BrowseCatalogDialog
connectors={CONNECTORS}
onOpenChange={setOpen}
onQueryChange={setQuery}
onSortChange={setSort}
onTabChange={setTab}
open={open}
query={query}
renderAction={(item) =>
item.installed ? (
<Button aria-label={`Открыть ${item.name}`} onClick={(e) => e.stopPropagation()} size="icon-sm" variant="ghost">
<IconSettings className="size-4" stroke={1.5} />
</Button>
) : (
<Button aria-label={`Добавить ${item.name}`} onClick={(e) => e.stopPropagation()} size="icon-sm" variant="ghost">
<IconPlus className="size-4" stroke={1.5} />
</Button>
)
}
renderDetail={(item, t, onBack) => {
if (t !== "connectors") return <SkillDetail item={item} onBack={onBack} />;
const d = CONNECTOR_DETAIL[item.id];
return (
<DirectoryDetail
capability={d?.capability}
className="p-0"
connected={item.installed}
developerName={item.publisher}
developerNote={TRUST_NOTE}
developerUrl={d?.docsUrl}
docsUrl={d?.docsUrl}
examples={d?.examples}
icon={item.icon}
name={item.name}
note={item.description}
onBack={onBack}
onCopyUrl={() =>
navigator.clipboard?.writeText(
`https://oracul.co/connectors/${item.id}`,
)
}
tagline={item.description}
tools={d?.tools ?? []}
/>
);
}}
skills={SKILLS}
sort={sort}
tab={tab}
/>
</>
);
}
Установка
pnpm dlx shadcn@latest add @oracul/browse-catalog-dialog
Использование
import { BrowseCatalogDialog } from "@/components/ui/browse-catalog-dialog";
<BrowseCatalogDialog
open={open}
onOpenChange={setOpen}
tab={tab}
onTabChange={setTab}
query={query}
onQueryChange={setQuery}
skills={skills}
connectors={connectors}
sort={sort}
onSortChange={setSort}
renderAction={(item) => <AddButton id={item.id} />}
onSelect={(item) => openDetail(item)}
/>API
Презентационный (plain-props): приложение владеет данными каталога, установкой и детальной панелью навыка (file-tree). Компонент рендерит шелл, табы, поиск, меню Фильтр/Сортировка и сетку directory-card. DS-шкала и токены, без хардкода.
| Prop | Тип | Описание |
|---|---|---|
open / onOpenChange | bool / (v)=>void | Состояние диалога. |
tab / onTabChange | "skills" | "connectors" | Активный таб. |
query / onQueryChange | string | Поиск. |
skills / connectors | CatalogItem[] | Элементы по табам. |
sort / onSortChange | "popular" | "recent" | "name" | Сортировка. |
renderAction | (item, tab) => ReactNode | Действие в углу карточки (напр. «Добавить»). |
onSelect | (item, tab) => void | Клик по карточке. |
На этой странице