# Установка

> Что ставится, почему именно это и куда — dependencies или devDependencies. Плюс иконки и опциональные интеграции.

- Page: https://granularity.tech/ru/docs/installation
- Last reviewed: 2026-10-16

[Быстрый старт](/ru/docs/getting-started) даёт один рабочий путь без объяснений.
Эта страница объясняет: почему состав зависимостей именно такой, что из него
попадает в бандл, а что живёт только на сборке.

## Требования

| Что | Версия | Почему жёстко |
|---|---|---|
| Node | ≥ 22 | Объявлено в `engines`, пакет ESM-only |
| ES-модули | обязательно | Пакет ESM-only (`"type": "module"`, без условия `require`); CommonJS-сборки нет и не планируется |
| Vue | `^3.5` | peer-зависимость |
| `@floating-ui/dom` | `^1.8` | **обязательная** runtime peer-зависимость |
| `@feugene/granum` | `^1.0` | конвейер CSS; на сборке, в бандл не попадает |
| `@feugene/granum-engine-wind` | `^1.0` | словарь утилит для него |
| Vite | `^8` | granum объявляет его необязательным peer |

`@feugene/fint-i18n` — peer тоже, но помеченный необязательным: без него
компоненты говорят по-английски встроенными строками, а не падают. Подробности —
на странице [локализации](/ru/docs/localization).

## Единственный поддерживаемый способ

Подключение идёт через `@feugene/granum` — конвейер, который читает манифест
пакета и собирает из него один лист. Прямые импорты CSS-файлов и подключение
`styles.css` в обход конвейера не поддерживаются — они работают ровно до первого
обновления и ломаются молча. Компонент из корня пакета granum не пропустит вовсе:
это ошибка сборки `ComponentRootImportError`, компоненты берут из подпутей.

Манифест — не соглашение об именах файлов, а объявленный экспорт:
`@feugene/granularity/granum.manifest.json`. Поэтому granum не сканирует
`node_modules` и не угадывает раскладку пакета: такого канала у него нет вовсе.

Исключение названо в самом пакете: `@feugene/granularity/styles.css` — готовый
лист с токенами, базовым слоем, preflight и обеими темами для тех, кто конвейер
не запускает вовсе. Его части экспортированы и по отдельности —
`@feugene/granularity/styles/*` (`tokens.css`, `base.css`, `preflight.css`,
`themes/light.css`, `themes/dark.css`) — для того же случая. Ни то ни другое не
даёт утилитарных классов, которыми нарисованы шаблоны компонентов.

## Что и куда

```bash
# в dependencies — попадает в приложение
yarn add vue @feugene/granularity @floating-ui/dom @unocss/reset

# в devDependencies — исполняется только на сборке
yarn add -D @feugene/granum @feugene/granum-engine-wind
```

**`@feugene/granularity` — в `dependencies`:** его компоненты импортируются из
исходников приложения и попадают в бандл.

**`@floating-ui/dom` — в `dependencies` приложения, а не внутрь пакета.** На нём
построено позиционирование всех выпадающих панелей — `GrSelect`,
`GrAutocomplete`, `GrTreeSelect`, `GrDropdown`, `GrDropdownMenu`,
`GrContextMenu`, `GrTooltip`, `GrPopover`, `GrColorPicker` — и, через них,
`GrPagination`, `GrDataTable` и `GrSidebar`. Пакет держит библиотеку внешней и
не бандлит внутрь `dist` — иначе приложение, которое само использует
floating-ui, получило бы вторую копию.

**`@feugene/granum` и движок — в `devDependencies`:** они исполняются на сборке
и ни одной строкой не попадают в бандл. У обоих **ноль зависимостей**: словарь
`preset-wind3` вендорен внутрь движка, и ставить `unocss` не нужно — его в
дереве не будет вовсе.

**`@unocss/reset` — в `dependencies`,** как в быстром старте ядра: его лист
попадает в CSS приложения — одной строкой `@import` из вашего CSS, а не импортом
из кода.

:::callout{type="info"}
Модальный слой — `GrModal`, `GrDialog`, `GrDrawer`, `GrImageViewer`,
`GrCommandPalette` — не требует внешних зависимостей вовсе. Ловушка фокуса,
`inert` для фона, порядок Esc и возврат фокуса — собственные примитивы пакета.
`@headlessui/vue` был убран в 0.15.0 и peer-зависимостью не является: если он
остался в проекте ради этого пакета, его можно снести.
:::

## Наращивание конфига

Минимальный рабочий конфиг — движок, провайдер и папка с исходниками:

```js title="granum.config.mjs"
import { windEngine } from '@feugene/granum-engine-wind'
import { defineGranumConfig } from '@feugene/granum/vite'

export default defineGranumConfig({
  engine: windEngine(),
  providers: ['@feugene/granularity'],
  appSources: { dirs: ['src'] },
})
```

```ts title="vite.config.ts"
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import { granum } from '@feugene/granum/vite'
import granumConfig from './granum.config.mjs'

export default defineConfig({
  plugins: [Vue(), granum(granumConfig)],
})
```

Он уже подмешивает токены, базовый слой и тему `light`, включает все компоненты
провайдера с их стилями и поднимает правила, варианты и safelist. Дальше
опциями сужают и уточняют:

| Опция | Что делает |
|---|---|
| `components` | Список компонентов вместо `'all'`, либо `'imports'` — по импортам в `appSources`. Единственное место, где вы платите за гранулярность вниманием |
| `themes.names` | Какие темы попадут в лист. Без него granum берёт темы, которые провайдер объявил умолчательными: у ядра это одна `light`. Тёмную называют явно — `['light', 'dark']`. Как выбирается набор — в документации granum ([ru](https://github.com/efureev/granum/blob/main/docs/ru/themes-and-tokens.md), [en](https://github.com/efureev/granum/blob/main/docs/en/themes-and-tokens.md)) |
| `appSources.dirs` | Где искать классы вашей разметки |
| `css.layers` | Слои каскада. По умолчанию включены; выключение стоит предсказуемости, а не байтов |
| `pruneTokens.mode` | `'on'` — выбросить из листа токены, которых никто не использует; `'report'` — только перечислить их в отчёте. По умолчанию `'off'` |
| `js.guard` | Импорт компонента вне `components`: ошибка (по умолчанию), предупреждение или молчание |

Отдельной опции «где искать классы в `node_modules`» нет и не нужно: классы
компонентов приезжают из манифеста пакета, уже посчитанными на его сборке.
У пресета v1 это было ручной строкой `content.filesystem`, и утилита,
встречающаяся только внутри лениво загружаемого острова, до листа не доезжала.

## Иконки

**Свои иконки пакет привозит с собой.** Стрелка селекта, крестик очистки,
галочка выбранной опции, спиннер, ручка переноса компилируются в `dist` при
сборке пакета. Ставить `unplugin-icons` или коллекцию ради них не нужно.

Своя иконка приходит двумя способами. Пропы `icon` принимают либо Vue-компонент,
либо класс:

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { GrTabs } from '@feugene/granularity/components/GrTabs'
import IconUser from '~icons/lucide/user'

const tab = ref('a')
</script>

<template>
  <GrTabs v-model="tab" :tabs="[{ value: 'a', label: 'Profile', icon: IconUser }]" />
  <GrTabs v-model="tab" :tabs="[{ value: 'a', label: 'Profile', icon: 'i-lucide-user' }]" />
</template>
```

Первая строка передаёт компонент — откуда он, пакету всё равно. Здесь он
приходит из `unplugin-icons`, и этот плагин вместе с коллекцией
`@iconify-json/lucide` ставит и настраивает приложение. Вторая передаёт класс:
CSS для него генерирует ваш конфиг, а не пакет.

:::callout{type="warning"}
`i-lucide-*` — утилита, и делает её ваша сборка. Правила иконок в словаре
`preset-wind3` не лежат: без правила класс останется классом — место под иконку
будет, картинки не будет, и сборка при этом пройдёт молча.
:::

```bash
yarn add -D @iconify-json/lucide
```

Правило пишет приложение — ни granum, ни движок готового не везут — и отдаёт его
фабрике движка:

```js title="granum.config.mjs"
import { windEngine } from '@feugene/granum-engine-wind'

export default defineGranumConfig({
  engine: windEngine({ rules: [iconRule] }),
  providers: ['@feugene/granularity'],
})
```

:::callout{type="info"}
Правило, переданное фабрике, меняет **отпечаток словаря** приложения. Он
перестаёт совпадать с тем, под которым считались манифесты пакетов, и granum
пересчитывает классы каждого провайдера по его файлам.

Это заметно уже на одном пакете: с одним ядром пересчёт занимает около 0,5 с
холодной сборки при нулевой разнице результата (ядро 1.1.1, granum 2.1), на
витрине дизайн-системы с восемью провайдерами — 609 мс. Документация granum
советует на этот случай вынести правила в отдельный пакет-провайдер
([ru](https://github.com/efureev/granum/blob/main/docs/ru/usage-in-apps.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/usage-in-apps.md)):
правила, приехавшие от провайдера, отпечаток приложения не трогают. Так устроен
и портал — правила иконок приезжают девятым провайдером.
:::

## Два импорта CSS, и порядок между ними несущий

Лист вставляет не интеграция, а приложение: в точке входа его импортируют
руками.

```ts title="src/main.ts"
import './reset.css'
import 'virtual:granum.css'
```

```css title="src/reset.css"
@import '@unocss/reset/tailwind-compat.css' layer(reset);
```

Сброс уезжает в **свой слой**, и это не оформление. granum отдаёт всё своё в
слои `granum.*`, а нелейерный CSS бьёт ЛЮБОЙ `@layer` независимо от
специфичности: вне слоя `button { color: inherit }` из сброса перебил бы утилиты
компонента, и кнопка приехала бы с цветом текста страницы.

Слой обязан объявиться **раньше** слоёв granum — место слоя в каскаде задаёт его
первое появление, — поэтому `reset.css` импортируется первым. Перепутанный
порядок не даёт ни ошибки, ни предупреждения: сборка проходит, ломается только
картинка.

## Опциональные интеграции

- **[`@feugene/unplugin-granularity`](https://www.npmjs.com/package/@feugene/unplugin-granularity)** —
  резолвер для `unplugin-vue-components`: авто-импорт компонентов и директив в
  шаблонах. Вставляет статические импорты в SFC, поэтому tree-shaking остаётся
  идеальным.
- **`createGranularity` из `@feugene/granularity/vue`** — runtime-адаптер для
  директив в `render`/JSX, общего `provide` и `globalProperties`. Сам он не
  импортирует ни одного компонента: что передадите, то и зарегистрируется.

Оба дополняют основной способ установки, а не заменяют его. Настройка обоих — в
[конфигурации](/ru/docs/configuration).
