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

  • Обновления и помощь
  • 9 мин
  • кода: 8

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

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

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

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

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

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

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

src/main.ts
import './reset.css'
import 'virtual:granum.css'

Рамки пропали, кнопки бесцветные, заголовки одного кегля

Что видно. Похоже на предыдущее, но CSS явно есть: цвета текста и фона на месте, а всё, что рисует библиотека, — нет.

Почему. Сброс объявлен после слоёв granum и потому выигрывает у всей библиотеки. Нелейерный CSS бьёт ЛЮБОЙ @layer независимо от специфичности, а слой, объявленный позже, бьёт объявленный раньше. * { border-width: 0 } из сброса снимает рамки, h1…h6 { font-size: inherit } — кегль заголовков, а button { background-color: transparent } из @unocss/reset/tailwind.css — ещё и фон кнопок. В tailwind-compat.css эта строка закомментирована как раз ради библиотек компонентов.

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

Файл обязан импортироваться первым: место слоя в каскаде задаёт его первое появление. Ни ошибки, ни предупреждения перепутанный порядок не даёт.

Если сброс лежит в <style> внутри шаблона страницы, порядок ссылок может оказаться не тем, на который вы рассчитываете: сборщик вставляет такой блок после <link> на лист. Тогда порядок задают объявлением — отдельной строкой @layer reset; в файле, который импортируется раньше всех.

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

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

Почему. Компонента нет в списке components. Добавьте — или поставьте 'all', тогда в лист приедут все.

По умолчанию это не молчит: js.guard роняет сборку на импорте компонента вне выборки. Если вы поставили 'warn' или 'off', симптом вернётся к прежнему — голый компонент без единого сообщения.

Полупрозрачный фон приехал глухим

Что видно. Подложка оверлея непрозрачна, bg-[var(--gr-bg)]/55 заливает сплошным цветом.

Почему. Выключен extraRules у движка. preset-wind3 не умеет применять альфу к произвольному цвету в скобках: он отдаёт background-color: var(--gr-bg), молча теряя альфу — класс совпал, CSS неверный, ошибки нет. Движок добирает это одним правилом поверх пресета, и оно включено по умолчанию. Сборка при этом молчит, а granum doctor покажет provider-dialect-mismatch: пакет собран под диалект unocss/preset-wind3+granum@66, а движок без доп-правил говорит на unocss/preset-wind3@66 — это другой словарь, хотя имена классов те же. Диалекты описаны в документации granum (ru, en).

Раньше таких правил было восемь семейств — sr-only, animate-spin, divide-*, space-*, object-* и прочее, — потому что дыры затыкались в preset-mini. В словаре preset-wind3 семь из восьми родные, и добирать осталось только альфу.

Сборка падает на импорте из пакета

Что видно.

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

Почему. Библиотека не установлена. Это обязательная peer-зависимость: на ней держится позиционирование всех плавающих панелей — GrSelect, GrDropdown, GrDropdownMenu, GrContextMenu, GrAutocomplete, GrTreeSelect, GrTooltip, GrPopover — и всего, что их содержит: GrDataTable, GrPagination, GrColorPicker, GrSidebar. Импорт из корня пакета тянет её всегда. Это ошибка сборки, а не рантайма: так её показывает dev-сервер, а в vite build она звучит как Rolldown failed to resolve import "@floating-ui/dom". Пакет держит библиотеку внешней намеренно — иначе приложение, которое само использует floating-ui, получило бы вторую копию.

bash
yarn add @floating-ui/dom

Сборка падает с ComponentRootImportError

Что видно.

plaintext
ComponentRootImportError: Components 'GrButton', 'GrCard' are imported from the
package root '@feugene/granularity' in 'src/App.vue'. Import components from
'@feugene/granularity/components/GrButton', … or from 'virtual:granum/components'

Почему. Компонент импортирован из корня пакета: import { GrButton } from '@feugene/granularity'. Селекция granum видит компонент только по подпути, а корень тянет весь пакет — в dev и в SSR это все его компоненты. С granum 2 это ошибка guard, а не тихая поломка. Утилиты, плагины, константы и типы из корня брать можно — запрет касается только компонентов.

ts
import { GrButton } from '@feugene/granularity/components/GrButton'

Или уберите импорт вовсе и поставьте авто-импорт: резолвер сам вставит подпуть.

Сборка падает с InvalidConfigError

Что видно.

plaintext
Invalid granum config at 'appSource': unknown option — did you mean 'appSources'?

Почему. granum 2 проверяет granum.config строго: опечатка в ключе, ключ пресета v1 (themes.themeFiles, scan) и токен с префиксом -- в tokenOverrides или define — ошибка с путём до поля, а не молча проигнорированная настройка. Исправьте поле, которое назвала ошибка: для опечатки она подскажет ближайший ключ, для ключа v1 — куда он делся.

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

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

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

Правило отдают фабрике движка — windEngine({ rules: [iconRule] }) — или привозят провайдером. Разница описана на странице установки: правило от провайдера не меняет отпечаток словаря приложения, а правило из конфига меняет, и granum пересчитывает классы всех пакетов заново. На одном-двух провайдерах это незаметно.

Альтернатива — передать иконку Vue-компонентом, тогда настраивать нечего.

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

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

Что видно.

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

При этом импорт из корня пакета типизируется нормально — но компонент из корня granum отвергает ошибкой сборки, так что это не выход.

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

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

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

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

  • резолвер не подключён к unplugin-vue-components — проверьте массив resolvers, а не только сам плагин;
  • компонент приезжает из спутника, которого нет в package.json приложения: резолвер берёт пакеты оттуда, а компоненты — из их exports. Имя, которого ни один пакет не экспортирует (опечатка, свой компонент), он не резолвит;
  • у GranularityResolver задан свой prefix или exclude, и имя под него не подходит. Регистр значения не имеет: <gr-button> плагин сам приводит к GrButton.

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

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

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

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

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

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

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

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

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

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

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