Конфигурация
Дефолты на поддерево через GrConfigProvider, порядок их разрешения, авто-импорт и runtime-адаптер.
Конфигурация здесь двухслойная и слои не пересекаются. На сборке настраивается
что попадёт в CSS — это uno.config.ts и страница
установки. В рантайме настраивается как компоненты
ведут себя по умолчанию — и это GrConfigProvider, о котором вся страница.
Дефолты на поддерево
<GrConfigProvider
size="sm"
:component-defaults="{ GrButton: { variant: 'outline' }, GrInput: { clearable: true } }"
>
<App />
</GrConfigProvider>Провайдер рендерится прозрачно (display: contents) и работает через
provide/inject, поэтому раскладку не меняет и может стоять где угодно — в
том числе несколько раз вложенно. Дочерний провайдер мержится поверх
родительского на уровне пропа, а не блока: переопределив один variant, вы
не потеряете остальные дефолты GrButton.
Порядок разрешения
- Локальный проп на самом компоненте.
componentDefaults[Component]ближайшего провайдера.- Глобальный
sizeпровайдера. - Собственный дефолт компонента.
Отсюда правило, которое стоит знать и потребителю: проп, настраиваемый через
провайдер, объявлен с дефолтом undefined. Иначе Vue подставил бы своё значение
раньше, чем компонент заглянет в конфиг, и провайдер молча перестал бы работать.
Прочитать эффективный конфиг из приложения можно useGrConfig() — это публичный
API, а не внутренность.
Две шкалы размеров
size у провайдера — про контролы: xs | sm | md | lg. У оверлеев шкала
своя — sm | md | lg | xl | full, — и глобальный size её не трогает, потому
что xs для модального окна не значит ничего. Размер окна задаётся точечно:
<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 остаётся плотным: в бандл попадает ровно то, что
встретилось в шаблонах.
yarn add -D @feugene/unplugin-granularity unplugin-vue-componentsimport 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:
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(...).