Конфигурация

Дефолты на поддерево через GrConfigProvider, порядок их разрешения, авто-импорт и runtime-адаптер.

Конфигурация здесь двухслойная и слои не пересекаются. На сборке настраивается что попадёт в CSS — это uno.config.ts и страница установки. В рантайме настраивается как компоненты ведут себя по умолчанию — и это GrConfigProvider, о котором вся страница.

Дефолты на поддерево

vue
<GrConfigProvider
  size="sm"
  :component-defaults="{ GrButton: { variant: 'outline' }, GrInput: { clearable: true } }"
>
  <App />
</GrConfigProvider>

Провайдер рендерится прозрачно (display: contents) и работает через provide/inject, поэтому раскладку не меняет и может стоять где угодно — в том числе несколько раз вложенно. Дочерний провайдер мержится поверх родительского на уровне пропа, а не блока: переопределив один variant, вы не потеряете остальные дефолты GrButton.

Порядок разрешения

  1. Локальный проп на самом компоненте.
  2. componentDefaults[Component] ближайшего провайдера.
  3. Глобальный size провайдера.
  4. Собственный дефолт компонента.

Отсюда правило, которое стоит знать и потребителю: проп, настраиваемый через провайдер, объявлен с дефолтом undefined. Иначе Vue подставил бы своё значение раньше, чем компонент заглянет в конфиг, и провайдер молча перестал бы работать.

Прочитать эффективный конфиг из приложения можно useGrConfig() — это публичный API, а не внутренность.

Две шкалы размеров

size у провайдера — про контролы: xs | sm | md | lg. У оверлеев шкала своя — sm | md | lg | xl | full, — и глобальный size её не трогает, потому что xs для модального окна не значит ничего. Размер окна задаётся точечно:

vue
<GrConfigProvider :component-defaults="{ GrModal: { size: 'lg' } }">

Так настраиваются GrModal, GrDialog, GrConfirmDialog, GrPromptDialog, GrCommandPalette и GrDrawer.

Тема поддерева

theme кладёт значение в data-theme на обёртку. Темы объявлены атрибутным селектором, поэтому тёмный остров внутри светлой страницы работает без дополнительных стилей.

Панели оверлеев телепортируются в body и в DOM живут вне обёртки — но в дереве компонентов остаются внутри, поэтому inject до них доходит и тему они ставят себе сами. Модалка, дровер, дропдаун, поповер, тултип, селекты, тостер и просмотрщик изображений покрыты.

Тема документа — не эта работа: ею занимаются useTheme и initThemeEarly, см. темизацию. Двух механизмов на одно и то же в пакете нет, и проп провайдера именно про остров.

Шкала слоёв и точка монтирования

zIndexBase пересчитывает --gr-z-* от базы (dropdown +0, tooltip +50, modal +100, toast +200) и ставит их на <html>, возвращая прежние значения при размонтировании. На :root, а не на обёртку, — по той же причине, по которой панели ставят тему сами: панель уезжает в body и переменных поддерева не видит. Шкала одна на документ, и второй провайдер с другой базой в dev-сборке предупредит о конфликте.

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

portalTarget называет контейнер, куда уезжают оверлеи поддерева. По умолчанию это общий #gr-portal в body, который пакет создаёт сам при первом открытии.

Провайдер только называет цель — DOM он не создаёт, контейнер должен существовать к моменту открытия оверлея. Требование к контейнеру одно, зато жёсткое: никаких transform, filter, contain, perspective и will-change. Они создают containing block для position: fixed, и floating-панели начнут считать позицию от контейнера, а не от вьюпорта.

Авто-импорт

Рекомендуемый способ для боевого приложения — резолвер для unplugin-vue-components. Он вставляет в SFC статические импорты на подпути пакета, поэтому tree-shaking остаётся плотным: в бандл попадает ровно то, что встретилось в шаблонах.

bash
yarn add -D @feugene/unplugin-granularity unplugin-vue-components
vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { GranularityResolver } from '@feugene/unplugin-granularity'

export default defineConfig({
  plugins: [vue(), Components({ resolvers: [GranularityResolver()] })],
})

CSS подключать при этом не нужно: у большинства компонентов своего CSS нет вовсе, а у тех, у кого есть, он вписан в их чанк и приезжает вместе с импортом.

Резолвер ядра жадный — он забирает любое имя на Gr. Резолверы пакетов-спутников работают по явному списку и потому регистрируются до него: GranularityChronoResolver() первым, GranularityResolver() последним.

Runtime-адаптер

Резолвер сканирует шаблоны. Директивы, применённые в render, JSX или TSX, он не видит — для них есть createGranularity:

src/main.ts
import { createApp } from 'vue'
import { createGranularity } from '@feugene/granularity/vue'
import { GrButton } from '@feugene/granularity/components/GrButton'
import { vHotkey } from '@feugene/granularity/directives'
import App from './App.vue'

createApp(App)
  .use(createGranularity({
    components: [GrButton],
    directives: [{ name: 'hotkey', directive: vHotkey }],
  }))
  .mount('#app')

Адаптер сам не импортирует ни одного компонента — он принимает их аргументом. В бандл попадёт ровно то, что вы передали, и ничего сверх. Обратная сторона того же свойства: передать сюда «всё» — значит потерять гранулярность, и tree-shaking не спасёт, потому что импорт сделали вы.

Кроме компонентов и директив адаптер принимает provides и globalProperties — это удобный один вход для bootstrap-файла вместо россыпи app.use(...).

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