Установка

  • Начало работы
  • 8 мин
  • кода: 8

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

Ревизия 2026-10-16

На этой странице

Быстрый старт даёт один рабочий путь без объяснений. Эта страница объясняет: почему состав зависимостей именно такой, что из него попадает в бандл, а что живёт только на сборке.

Требования

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

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

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

Подключение идёт через @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, а не импортом из кода.

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

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

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

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'] },
})
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, en)
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 для него генерирует ваш конфиг, а не пакет.

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

bash
yarn add -D @iconify-json/lucide

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

granum.config.mjs
import { windEngine } from '@feugene/granum-engine-wind'

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

Правило, переданное фабрике, меняет отпечаток словаря приложения. Он перестаёт совпадать с тем, под которым считались манифесты пакетов, и granum пересчитывает классы каждого провайдера по его файлам.

Это заметно уже на одном пакете: с одним ядром пересчёт занимает около 0,5 с холодной сборки при нулевой разнице результата (ядро 1.1.1, granum 2.1), на витрине дизайн-системы с восемью провайдерами — 609 мс. Документация granum советует на этот случай вынести правила в отдельный пакет-провайдер (ru, en): правила, приехавшие от провайдера, отпечаток приложения не трогают. Так устроен и портал — правила иконок приезжают девятым провайдером.

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

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

src/main.ts
import './reset.css'
import 'virtual:granum.css'
src/reset.css
@import '@unocss/reset/tailwind-compat.css' layer(reset);

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

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

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

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

Оба дополняют основной способ установки, а не заменяют его. Настройка обоих — в конфигурации.