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

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

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

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

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

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

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

```ts title="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` эта строка закомментирована как раз
ради библиотек компонентов.

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

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

:::callout{type="info"}
Если сброс лежит в `<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](https://github.com/efureev/granum/blob/main/docs/ru/engines-and-dialects.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/engines-and-dialects.md)).

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

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

**Что видно.**

```
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`

**Что видно.**

```
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'
```

Или уберите импорт вовсе и поставьте [авто-импорт](/ru/docs/configuration):
резолвер сам вставит подпуть.

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

**Что видно.**

```
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] })` — или
привозят провайдером. Разница описана на странице
[установки](/ru/docs/installation): правило от провайдера не меняет отпечаток
словаря приложения, а правило из конфига меняет, и granum пересчитывает классы
всех пакетов заново. На одном-двух провайдерах это незаметно.

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

:::callout{type="info"}
Собственные иконки пакета — стрелка селекта, крестик очистки, галочка, спиннер —
вкомпилированы в `dist` и от вашего конфига не зависят.
:::

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

**Что видно.**

```
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](/ru/docs/ssr).

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

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

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

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

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

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

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

Если симптом не отсюда — заведите
[issue в репозитории](https://github.com/efureev/granularity/issues).
Случай, который пришлось разбирать дважды, попадает на эту страницу.
