Установка
Что ставится, почему именно это и куда — dependencies или devDependencies. Плюс иконки и опциональные интеграции.
Быстрый старт даёт один рабочий путь без объяснений. Эта страница объясняет: почему состав зависимостей именно такой, что из него попадает в бандл, а что живёт только на сборке.
Требования
| Что | Версия | Почему жёстко |
|---|---|---|
| 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 тоже, но помеченный необязательным: без него
компоненты говорят по-английски встроенными строками, а не падают. Подробности —
на странице локализации.
Единственный поддерживаемый способ
Подключение идёт через @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) — для того же случая. Ни то ни другое не
даёт утилитарных классов, которыми нарисованы шаблоны компонентов.
Что и куда
# в 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-зависимостью не является: если он
остался в проекте ради этого пакета, его можно снести.
Наращивание конфига
Минимальный рабочий конфиг — движок, провайдер и папка с исходниками:
import { windEngine } from '@feugene/granum-engine-wind'
import { defineGranumConfig } from '@feugene/granum/vite'
export default defineGranumConfig({
engine: windEngine(),
providers: ['@feugene/granularity'],
appSources: { dirs: ['src'] },
})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-компонент,
либо класс:
<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 не лежат: без правила класс останется классом — место под иконку
будет, картинки не будет, и сборка при этом пройдёт молча.
yarn add -D @iconify-json/lucideПравило пишет приложение — ни granum, ни движок готового не везут — и отдаёт его фабрике движка:
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, и порядок между ними несущий
Лист вставляет не интеграция, а приложение: в точке входа его импортируют руками.
import './reset.css'
import 'virtual:granum.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. Сам он не импортирует ни одного компонента: что передадите, то и зарегистрируется.
Оба дополняют основной способ установки, а не заменяют его. Настройка обоих — в конфигурации.