# Архітектура фронтенду

Як у Oleafly настільний застосунок поділено між фронтенд-робочим простором, сервісами Rust і зафіксованими sidecar-бінарниками.

Фронтенд Oleafly — це робочий простір pnpm: оболонка застосунку живе в корені репозиторію (`src/`), а багаторазовий фронтенд-код — у `packages/*` у вигляді дванадцяти вузькоспрямованих пакетів. Бекенд Tauri живе в `src-tauri/`, а багаторазовий код провайдерів ШІ та агентів — у `crates/oleafly-agent/`. Див. [схему архітектури в README](https://github.com/Oleafly/Oleafly/blob/main/README.md#architecture) для загальної картини фронтенд/Rust/sidecar і сторінку [Розробка](/docs/engineering/development/) щодо щоденних команд.

Цей документ охоплює: що собою являє кожен пакет, правила інверсії залежностей, які зберігають їх чистими, реєстр внесків, що під'єднує можливості до оболонки, як працює розв'язання модулів, а також як додати або винести пакет.

## Навіщо робочий простір

Колись кожна можливість напряму дотягувалася до сторів Zustand застосунку, до клієнта Tauri IPC і до UI-кітів shadcn. Це робило можливості невіддільними від оболонки: нічого не можна було тестувати, повторно використовувати чи осмислювати окремо. Робочий простір інвертує цю ситуацію. Пакети тримають логіку можливостей і оголошують вузькі інтерфейси, названі портами, для всього, що їм потрібно. Застосунок реалізує ці порти поверх своїх сторів і клієнта Tauri та впроваджує їх. Застосунок залежить від пакетів, але ніколи навпаки.

Пакети споживаються **як вихідний код TypeScript** (їхній `main` вказує на `src/index.ts`). Ніякого етапу збірки, публікацій чи розбіжностей версій: Vite, Vitest і `tsc` розв'язують `@oleafly/*` одразу до вихідних кодів пакетів через аліаси (див. [Розв'язання модулів](#module-resolution)).

## Пакети

| Пакет | Що це | Впроваджувані порти | Ключові залежності |
| --- | --- | --- | --- |
| `@oleafly/latex` | Чиста логіка LaTeX/рисунків: модель діаграм, серіалізатор TikZ (`modelToTikz`, запис-читання вбудованої моделі), конструктор автономних документів (`buildStandaloneDoc`), допоміжні функції для імен рисунків і їхніх байтів | немає (чистий) | немає |
| `@oleafly/ai-core` | Метадані провайдерів для відображення та вибір моделей на фронтенді, що утримуються в паритеті з каталогом Rust | немає (чистий) | немає |
| `@oleafly/latex-intelligence` | Валідовані типи та доступ до даних корпусу LaTeX, який використовують інтелектуальні функції редактора | немає (чистий) | немає |
| `@oleafly/registry` | Реєстр внесків: вкладки рейки, команди палітри/омнібара, набори інструментів ШІ ([подробиці нижче](#the-contribution-registry)) | немає (внески самі несуть свою поведінку) | react (типи) |
| `@oleafly/preflight` | Рушій передперевірки: типізація документів, правила для джерел/PDF/посилань, розбір ATS, підготовка доступного експорту, оцінювання. `pdf-extract` постачається як підшлях `@oleafly/preflight/pdf-extract`, щоб pdf.js не потрапляв у тестові графи node | немає (чистий) | `pdfjs-dist` (лише підшлях) |
| `@oleafly/pdf-to-latex` | Локальний конвеєр відтворення PDF у LaTeX: групування рядків, виявлення заголовків і колонок, визначення стилю, відображення символів, витягання рисунків, екранування та збирання документа. Адаптер pdf.js постачається як підшлях `@oleafly/pdf-to-latex/pdf-adapter` з тієї самої причини щодо тестових графів node | немає (чистий) | `pdfjs-dist` (лише підшлях) |
| `@oleafly/wysiwyg` | Ядро обмеженого візуального редактора: спільна схема, розбір/серіалізація LaTeX і Markdown, а також сирі вузли inline/block, які зберігають непідтримуване джерело видимим замість того, щоб його відкидати | немає (чистий) | немає |
| `@oleafly/editor` | Ядро CodeMirror для LaTeX, Typst і Markdown: `CodeMirrorEditor`, контролер редагування (`insertAtCursor`, `gotoLine`, …), мови + автодоповнення, тема, згортання коду, закріплені заголовки, лінтери, latex-mask, передперегляд математики (KaTeX для LaTeX, Typst для Typst), допоміжні засоби редагування Typst і перетворення математики LaTeX у Typst, вбудований режим «Візуально» для LaTeX і Typst (`@oleafly/editor/visual`), панель пошуку, лінтери орфографії/граматики | `EditorHost` (модель документа + налаштування, у формі хуків), `SpellHost` (перевірка орфографії/Harper/словник), `setBibKeysProvider`, `extraExtensions`/`extraKeymap` | `@codemirror/*`, `@replit/codemirror-vim`, `@replit/codemirror-emacs`, `katex` |
| `@oleafly/preview` | Віртуалізований переглядач pdf.js (`PdfViewer`), контролер сторінок SyncTeX (`gotoRect`, `pageClickToBp`), воркер pdf.js і поліфіли WebView (`@oleafly/preview/polyfills`, імпортується першим у `main.tsx`) | проп `onOpenLink` (посилання, що відкриваються в системному браузері), `setPdfLogger` | `pdfjs-dist` |
| `@oleafly/diagram` | Візуальний композер діаграм для TikZ, Typst (fletcher) і Mermaid: полотно React Flow, інспектор фігур, вкладки «Малювання»/«Код», потік компіляція-перегляд-вставка. Кожна мова в `src/languages/` має записувач, зчитувач і свої форми вставки та роботи з файлами, а полотно і код лишаються синхронними в обидва боки | `DiagramHost` (компіляція LaTeX, рендеринг Typst і Mermaid, файлове введення-виведення, вставка в редактор, виправлення ШІ), контекст `DiagramKit` (Button/Tooltip/Select/toast/тема) | `@xyflow/react`, `@codemirror/*`, `@oleafly/latex`, `@oleafly/editor` |
| `@oleafly/ai-tools` | Визначення інструментів для проєктів, досліджень, пам'яті та рисунків, із мутаціями, що враховують підтвердження | `AiToolsHost` (файли, компіляція, індекс символів, конвеєр рисунків, редактор) | `@oleafly/latex` |
| `@oleafly/templates` | Галерея шаблонів нових проєктів (двокроковий майстер, категорії, передперегляди, разові завантаження ресурсів із прогресом) | `TemplatesHost` (передперегляди, завантаження ресурсів, журналювання), `TemplatesKit` (Button/Tooltip), параметри кольорів як пропси | немає, крім UI-утиліт |

Напрямок залежностей, строго контрольований:

Пакет ніколи не повинен імпортувати з `src/` (`@/…`), зі стору Zustand, з `@tauri-apps/*` або з UI-компонентів застосунку. Перевірка чистоти — частина гігієни винесення:

```bash
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src   # must return nothing
```

## Патерн портів

Пакети оголошують інтерфейси. Застосунок реалізує їх один раз, зазвичай як об'єкт-адаптер на рівні модуля, що замикається на своїх сторах. Чотири повторювані форми:

**Хост-порти**: один `interface XHost` на пакет, що покриває потрібні йому сервіси. Застосунок будує адаптер з імперативними читаннями сторів:

```ts
// src/lib/ai-tools.ts (app adapter, abridged)
const HOST: AiToolsHost = {
  getProjectId: () => useFilesStore.getState().projectId,
  readFileContent,                       // Tauri client passthrough
  recompile: () => useCompileStore.getState().recompile(),
  insertAtCursor,                        // editor controller
  // ...
};
export const createOleaflyTools = (opts?) => createOleaflyToolsCore(HOST, opts);
```

Те, що клопоти, властиві лише застосунку, лишаються всередині адаптера, також тримає залежності поза пакетами. Облікові дані провайдерів і запити лишаються в Rust, тож фронтенд-пакети отримують лише стан і результати, яких вимагають їхні інтерфейси.

**UI-кіти**: React-контекст (або проп) зі структурно типізованими компонентами: `Button: ComponentType<{ variant?: …; onClick?: () => void }>`. Застосунок пропускає свої компоненти shadcn без змін, бо менший структурний тип сумісний. Кіти також несуть `toast` і хук `useThemeMode()`.

**Порти у формі хуків**: для пакетів, які мають *підписуватися* на стан застосунку. `EditorHost` редактора має члени-хуки (`useActivePath()`, `useSettings()`) плюс імперативних двійників (`getActivePath()`) для слухачів подій. Правила: об'єкт host повинен бути на рівні модуля (стабільна тотожність), а його члени `use*` мають викликати хуки безумовно.

**Модульні синглтони**: для наскрізного зв'язування `setPdfLogger(fn)`, `setSpellHost(host)`, `setBibKeysProvider(fn)` мають стандартні реалізації no-op і встановлюються шимом застосунку як побічний ефект імпорту.

### Шими

Кожне винесення лишало тонкий файл за старим шляхом `@/…`, який реекспортує (або обгортає) пакет, тож споживачі та наявні шляхи `vi.mock(...)` ніколи не зазнавали змін. Приклади: `src/components/editor/cm/controller.ts` (реекспорт), `src/components/pdf/PdfViewer.tsx` (обгортка, що впроваджує відкривач посилань shell-плагіна), `src/lib/ai-tools.ts` (адаптер + початкові сигнатури фабрик). Новий код може імпортувати `@oleafly/*` безпосередньо. Шими існують заради сумісності, а не як бажаний шлях.

## Реєстр внесків

`@oleafly/registry` — це спосіб, у який можливості під'єднуються до оболонки застосунку, не вимагаючи, щоб оболонка про них знала. Три види внесків:

- **Вкладки рейки** (`registerRailTab`): іконка, розділ (`explore` / `review` / `assist`, розділювачі малюються між непорожніми розділами), необов'язкова видимість `when(ctx)`, необов'язковий хук `useBadge()` (наприклад, кількість змін у Git) і компонент бічної панелі `panel`.
- **Команди** (`registerCommand`): подаються до палітри команд (Cmd+K, групуються за `group`) та/або до омнібара (Cmd+P) через `surfaces`. Мітки й іконки можуть бути функціями від контексту (наприклад, «Перемкнутися на світлу тему»).
- **Набори інструментів ШІ** (`registerAiToolset`): режим чату (`"chat"`, `"figure"`), зіставлений із фабрикою набору інструментів. Панель чату шукає набір інструментів активного режиму замість жорстко прописаного.

`AppContext` (`{ projectId, projectKind, theme }`) будується кожною поверхнею рендерингу з її власних підписок, тож умови `when`/`label` залишаються реактивними.

Реєстрація **статична**: `src/contributions/` реєструє кожен вбудований елемент (вкладки, команди, набори інструментів), а `main.tsx` викликає `registerContributions()` один раз перед монтуванням оболонки. `Rail`, `Sidebar`, `CommandPalette`, `SearchOmnibar` і `ChatPanel` рендерять лише те, що зареєстровано.

Щоб додати вкладку чи команду можливості, зареєструйте її. Без правок оболонки:

```ts
// src/contributions/my-feature.tsx (called from src/contributions/index.ts)
registerRailTab({
  id: "review", label: "Review", icon: MessageSquare,
  section: "review", order: 55,
  when: (ctx) => ctx.projectKind !== "image",
  panel: ReviewPanel,
});
registerCommand({
  id: "omnibar.review", surfaces: ["omnibar"], order: 45,
  label: "Open review", keywords: "review comments",
  run: () => useSettingsStore.getState().setRailTab("review"),
});
```

Це внутрішній API, а не SDK для плагінів: форми можуть вільно змінюватися, і нічого не завантажується динамічно. Відкладено до появи справжнього споживача: формати меню експорту, схеми налаштувань і поведінка за видами проєктів як внески.

## Розв'язання модулів

Оскільки пакети споживаються як вихідний код, той самий аліас оголошується у трьох місцях, і всі три мають лишатися узгодженими під час додавання пакета:

1. `tsconfig.json` → `compilerOptions.paths`. Підшляхи на кшталт `@oleafly/preflight/pdf-extract` потребують окремого явного запису.
2. `vite.config.ts` → `resolve.alias`. Рядкові аліаси збігаються за префіксом, тож підшляхи розв'язуються без додаткового запису.
3. `vitest.config.ts` → той самий `resolve.alias`, **плюс** `test.include` повинен охоплювати `packages/**/*.test.ts`. Інакше перенесені тести непомітно випадають із прогону.

Також: кореневий `package.json` залежить від кожного пакета через `"workspace:*"`, а в `src/styles/globals.css` є `@source "../../packages"`, щоб Tailwind v4 сканував вихідні коди пакетів у пошуках імен класів.

Імпорти, притаманні лише Vite (воркери `?worker&url`, імпорти CSS), працюють без змін із вихідних кодів пакетів, бо аліаси вказують на джерело. Тримайте все, що тягне за собою pdf.js, поза барелами, які імпортують тести в середовищі node: саме тому `pdf-extract` і переглядач пакета preview доступні лише там, де існує DOM.

## Робота з пакетами

- **Тести** лежать поруч із вихідним кодом (`packages/*/src/*.test.ts`) і виконуються тим самим `pnpm test`, що й тести застосунку. Стежте за *кількістю* тестів, коли переміщуєте файли.
- **Залежності** оголошуються нестрого (`"react": "*"`). pnpm дедуплікує їх щодо кореня, де живуть справжні мінімальні версії. Не оголошуйте залежностей, яких пакет не імпортує.
- **Перевірки для будь-якої зміни пакета**: `pnpm exec tsc --noEmit`, `pnpm build`, `pnpm test` (незмінна кількість) і наведений вище grep чистоти.
- **Винесення нового**: перемістіть файли через `git mv` (за ними слідує історія), визначте порти, лишіть шими за старими шляхами, під'єднайте три аліаси, а тоді запустіть перевірки. Не виносьте можливість, над якою активно працюєте.
- **Ліцензування**: кожен пакет — `AGPL-3.0-or-later`, як і застосунок.

## Що лишається в застосунку

У `src/` лишається все, що є власне фронтенд-застосунком: стори Zustand, клієнт Tauri IPC (`src/lib/tauri.ts`), UI-кіт shadcn, компоненти макета/оболонки, адаптери портів і шими, `src/contributions/`, а також зв'язувальний код можливостей, який за своєю природою прив'язаний до сторів (UI панелі передперевірки, плагін вбудованого редагування ШІ та інтелектуальні функції для коду й наведення, впроваджені в редактор як `extraExtensions`). Команди Tauri та інтеграція з операційною системою лишаються в `src-tauri/`. Клієнти провайдерів, потокова передача та оркестрація агентів належать до `crates/oleafly-agent/`, щоб секрети й запити до провайдерів залишалися поза webview.
