Типовые поломки

Симптом, причина, починка. Собрано по тем случаям, которые ломались на самом деле, а не по воображаемым.

Почти все поломки на входе — это одно из трёх: не хватает импорта CSS, конфиг пресета разошёлся сам с собой, или в проекте нет обязательной peer-зависимости. Ниже — по симптому.

Компонент серый и голый

Что видно. Кнопка без цвета, без скруглений, без отступов — как будто CSS нет вовсе.

Почему. Не подключён фундамент: токены, базовый слой и темы приезжают виртуальным модулем слоя. Проверьте, что в точке входа есть оба модуля и именно в этом порядке:

src/main.ts
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno:granular.css' // токены, base, темы
import 'virtual:uno.css' // утилитарные классы

Если layer: 'granular' в опциях пресета не задан, отдельного модуля слоя нет и всё приезжает одним virtual:uno.css — тогда достаточно второй строки.

Цвета есть, вёрстки нет

Что видно. Компонент цветной, но элементы сложены в столбик, отступы съехали, иконка налезает на текст.

Почему. Забыт virtual:uno.css. Утилитарные классы, которыми нарисованы шаблоны компонентов, генерируются именно в него, а слой granular несёт только фундамент.

Стили пропали у одного конкретного компонента

Что видно. Остальные в порядке, а новый добавленный — голый.

Почему. Одно из двух:

  1. Компонента нет в списке components пресета. Добавьте — или уберите список вовсе, тогда в лист приедут все.
  2. granularContent(options) и presetGranularNode(options) получили разные объекты опций. Экстрактор при этом сканирует не то, что генерирует пресет. Держите один объект на оба вызова.

Спиннер не крутится, скрытый текст виден

Что видно. animate-spin не работает, разделители не рисуются, а служебные подписи — заголовок диалога, подпись таблицы — показываются пользователю обычным текстом.

Почему. Выключен includeExtraRules. Компоненты пользуются утилитами, которых в presetMini нет вовсе — sr-only, animate-spin, divide-*, space-*, backdrop-*, — и пресет добирает их отдельным пакетом. Это условие работоспособности, а не украшение: с includeExtraRules: false класс остаётся в разметке, CSS не появляется, и сборка проходит молча.

Приложение падает на импорте выпадающего списка

Что видно.

plaintext
Failed to resolve import "@floating-ui/dom"

Почему. Библиотека не установлена. Это обязательная peer-зависимость: на ней держится позиционирование GrSelect, GrDropdown, GrAutocomplete, GrTreeSelect, GrTooltip и GrPopover. Пакет держит её внешней намеренно — иначе приложение, которое само использует floating-ui, получило бы вторую копию.

bash
yarn add @floating-ui/dom

Место под иконку есть, иконки нет

Что видно. Пустой квадрат там, где ожидалась иконка, переданная классом (icon="i-lucide-user").

Почему. Класс i-lucide-* — утилита UnoCSS, и генерирует её ваш конфиг, а не пакет. Нужен presetIcons и коллекция иконок; без них класс остаётся классом, и сборка проходит молча. Альтернатива — передать иконку Vue-компонентом, тогда настраивать нечего.

Собственные иконки пакета — стрелка селекта, крестик очистки, галочка, спиннер — вкомпилированы в dist и от вашего конфига не зависят.

TypeScript не находит типы подпути

Что видно.

plaintext
Cannot find module '@feugene/granularity/components/GrButton'
or its corresponding type declarations. ts(2307)

При этом импорт из корня пакета типизируется нормально, и сборка проходит.

Почему. "moduleResolution": "node" в tsconfig.json. Старый алгоритм не читает поле exports, а типы подпутей объявлены только там. Нужен "bundler" или "node16".

Авто-импорт не срабатывает

Что видно. <GrButton> в шаблоне превращается в неизвестный элемент, Vue ругается в консоли.

Почему. Одно из трёх:

  • резолвер не подключён к unplugin-vue-components — проверьте массив resolvers, а не только сам плагин;
  • компонент приезжает из спутника, а его резолвер зарегистрирован после ядрового. Резолвер ядра жадный: он забирает любое имя на Gr. Резолверы спутников работают по явному списку и потому идут первыми;
  • имя написано в кебаб-кейсе там, где резолвер ждёт паскаль. Резолвер узнаёт компоненты по префиксу, и префикс должен совпасть.

Тёмная тема мигает при загрузке

Что видно. На долю секунды светлый фон, потом тёмный.

Почему. Сервер (или статический HTML) не знает выбор пользователя, а атрибут темы ставится уже после гидрации. Лечится инлайновым скриптом в <head> до первой отрисовки — готовый код на странице SSR.

Панель оверлея прилипла не туда

Что видно. Выпадающая панель или модалка позиционируется относительно контейнера, а не окна; при прокрутке уезжает.

Почему. У контейнера, названного в portalTarget, есть transform, filter, contain, perspective или will-change. Любое из них создаёт containing block для position: fixed, и панель начинает считать позицию от контейнера. Требование к контейнеру портала жёсткое и звучит именно так: ни одного из этих свойств.

В тестах оверлеи не находятся

Что видно. Тест монтирует компонент, открывает панель — и не находит её в документе. Ошибок в консоли нет.

Почему. Между тестами не вызывается resetGranularityDom. Корень портала и хост живого региона кэшированы модулями: после document.body.innerHTML = '' они указывают на узлы вне документа, и следующий монтаж телепортирует оверлеи туда. Подробности — на странице тестирования.

Ничего из перечисленного

Если симптом не отсюда — напишите в обсуждения репозитория. Случай, который пришлось разбирать дважды, попадает на эту страницу.

Последняя ревизия: 2026-09-01