GrConfigProvider
Берут, когда размеры контролов едины на приложение.
Обзор
Когда брать
- размеры контролов едины на приложение —
sizeодин раз вместо пропа на каждом поле; - у компонента свои дефолты в этом проекте —
componentDefaultsменяет их, не трогая места употребления; - подключается перевод — адаптер
tиlocaleдоходят до всех компонентов пакета; - поддерево живёт в своей теме — тёмная панель внутри светлого приложения без глобального переключения;
- оверлеи должны лечь выше чужих —
zIndexBaseсдвигает всю шкалу слоёв пакета.
Когда взять другое
| Нужно | Берите |
|---|---|
| Значение нужно одному компоненту | проп на самом компоненте |
| Меняется палитра, а не дефолты | packages/granularity/docs/theming.md |
| Нужны переводы, а не их подключение | packages/granularity/docs/localization.md |
Рендерится прозрачно (display: contents) и работает через provide/inject,
поэтому раскладку не меняет и может стоять где угодно — в том числе несколько
раз вложенно.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrConfigProvider><div class="flex items-center gap-2"><div class="min-w-0 flex-1"><GrInput modelValue="INV-0418" ariaLabel="Invoice number" /></div><GrButton>Find</GrButton><GrButton variant="outline">Export</GrButton></div></GrConfigProvider>Примеры 4 у каждого — живое демо и его код
Один тулбар, две плотности
Бэк-офис плотный, складской планшет держат в перчатках — один и тот же тулбар в провайдере с
size="sm"иsize="lg". Внутри никто не повторяетsize, поэтому поле, селект и кнопка держат одну высоту; ряд, который расходится на ступень, — это ряд, размеренный вручную.Back office · size «sm»Mouse and keyboard, many rows on screenWarehouse tablet · size «lg»Touch, at arm's lengthТихие кнопки в списке
component-defaultsделает каждую GrButton в списке маленькой нейтральной ghost-кнопкой, и двенадцать действий не топят суммы. Единственное важное действие — «Remind» у просроченного счёта — говоритvariant="primary"на себе и побеждает: дефолт — это только то, что не сказано.Sent invoices Northwind TradersINV-0418 · €1,284.50Tailspin ToysINV-0417 · €2,150.00Contoso LtdINV-0415 · €432.00Тема для превью, а не для страницы
themeкладётdata-themeна поддерево: администратор выбирает тему портала и видит её, а сама админка остаётся в своей. Панели, улетающие вbody, берут тему хозяина, а не страницы, из которой вылетели.Customer portal themeINV-0418Due in 3 daysAmount€1,284.50Испанский остров в английской админке
i18nпередаёт поддереву адаптер переводов: зона загрузки, подписи пагинации и «21–40 de 236» говорят по-испански, а админка вокруг остаётся английской. Каталог поставляет пакет (@feugene/granularity/i18n), его читает адаптер в десять строк сtиte, а пока каталог грузится, компоненты держат встроенный английский.Portal de clientes · Iberia Muebles S.L.Built-in strings of the components come from the provider's adapter Перетащите файлы сюда или нажмите для выбора
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Дефолтный размер контролов для вложенных компонентов. |
componentDefaults | GrComponentDefaults | undefined | undefined | Дефолтные пропсы по компонентам: `{ GrButton: { variant: 'secondary' } }`. |
i18n | GranularityI18nAdapter | null | undefined | undefined | Адаптер переводов (fint-i18n-совместимый). Прокидывается вложенным компонентам. |
locale | string | undefined | undefined | Просьба к адаптеру переключить язык (`syncLocale`, у инстанса fint-i18n — `setLocale`). Источником истины остаётся сам адаптер — провайдер лишь передаёт ему намерение. |
theme | string | undefined | undefined | Тема поддерева: значение уезжает в `data-theme`. Тема **документа** — работа `useTheme`/`initThemeEarly`, здесь именно остров. |
portalTarget | string | HTMLElement | undefined | undefined | Куда монтировать оверлеи поддерева. По умолчанию — общий `#gr-portal` в `body`. Своё значение нужно там, где приложение живёт в контейнере: микрофронтенд, shadow DOM, CSS-скоупинг под конкретным корнем. |
zIndexBase | number | undefined | undefined | База шкалы слоёв. Переменные `--gr-z-*` пересчитываются от неё и ставятся на `<html>`: панели телепортируются в `body`, и переменные поддерева до них не доходят. |
tag | string | undefined | "div" | Тег обёртки. По умолчанию прозрачный `<div style="display:contents">`. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Поддерево, которому адресованы настройки. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrComponentSize, GrConfigurableComponent, GrComponentDefaults } from '@feugene/granularity/components/GrConfigProvider'Файлdist/types/components/shared/configContext.d.ts
type GrComponentSize = typeof GR_COMPONENT_SIZES[number];Имя компонента, у которого есть настраиваемые через конфиг пропы.
type GrConfigurableComponent = keyof GrComponentDefaultsRegistry;Дефолтные пропсы по имени компонента: { GrButton: { variant: 'secondary' } }.
Набор ключей закрыт реестром, а не Record<string, unknown>: через конфиг
должны настраиваться только «оформительские» пропы. Пустить туда что угодно —
значит разрешить задать глобальный modelValue или обработчик и получить баг,
который ищется часами.
type GrComponentDefaults = {
[TComponent in GrConfigurableComponent]?: Partial<GrComponentDefaultsRegistry[TComponent]>;
};interface GrConfigContext {
/** Дефолтный размер контролов (если проп `size` не задан на компоненте). */
size: ComputedRef<GrComponentSize | undefined>;
/** Дефолтные пропсы по компонентам. */
componentDefaults: ComputedRef<GrComponentDefaults>;
/**
* Тема поддерева (`data-theme`). Необязательна: тип публичный, и контекст
* строят руками — например хост императивных диалогов.
*/
theme?: ComputedRef<string | undefined>;
/**
* Куда монтируются оверлеи поддерева (`usePortalTarget`). Необязательно, как
* и `theme`: контекст строят руками — например хост императивных диалогов.
*/
portalTarget?: ComputedRef<string | HTMLElement | undefined>;
}Возвращает ближайший GrConfigProvider или пустой конфиг, если провайдера нет.
function useGrConfig(): GrConfigContext;Источник конфига для {@link resolveGrConfig}: сам контекст, AppContext или
инстанс компонента — у двух последних читается provides.
type GrConfigSource = GrConfigContext | {
provides?: Record<symbol | string, unknown>;
};type UseGrComponentSizeOptions<TSize extends string> = {
/**
* Имя компонента, чтобы учесть точечный `componentDefaults[Component].size`.
* Без него читается только глобальный `size` провайдера.
*/
component?: GrConfigurableComponent;
/** Размер, когда ни проп, ни конфиг ничего не дали. По умолчанию `md`. */
fallback?: TSize;
/**
* Размеры, которые компонент реально умеет. Значение из конфига вне этого
* списка игнорируется в пользу `fallback`.
*
* Обязателен для компонентов, у которых size-шкала уже общей (`GrSlider`,
* `GrRating`, `GrSwitch`, `GrLink`, `GrStatistic` не знают про `xs`): без него
* `<GrConfigProvider size="xs">` дал бы `sizeMap['xs'] === undefined`, то есть
* компонент без размерных классов — молча, без ошибки сборки.
*/
supported?: readonly TSize[];
};Эффективный размер компонента. Приоритет:
локальный проп → componentDefaults[Component].size → глобальный size
провайдера → fallback. Точечная настройка компонента должна побеждать
общую — иначе componentDefaults нельзя было бы использовать как исключение
из глобального правила.
Передавайте size геттером, чтобы сохранить реактивность.
Локальный проп не проверяется по supported: он уже сужен типом пропа самого
компонента, и передать туда лишнее — явная ошибка вызывающего кода. Проверка
нужна конфигу: это глобальный канал, который не знает, кто его читает.
function useGrComponentSize<TSize extends string = GrComponentSize>(localSize: () => TSize | undefined, options?: UseGrComponentSizeOptions<TSize>): ComputedRef<TSize>;
/** Дефолтные пропсы конкретного компонента из ближайшего провайдера. */
export declare function useGrComponentDefaults<TComponent extends GrConfigurableComponent>(name: TComponent): ComputedRef<GrDefaultsOf<TComponent>>;
/**
* Эффективное значение «оформительского» пропа: локальный проп →
* `componentDefaults[Component][prop]` → собственный дефолт компонента.
*
* Обязательное условие на стороне компонента: у такого пропа в `withDefaults`
* должен стоять `undefined`, а «настоящий» дефолт — переехать сюда, в `fallback`.
* Иначе конфиг не сработает никогда: Vue отдаёт `props.x` уже с подставленным
* дефолтом, и отличить «пользователь передал значение» от «сработал дефолт»
* изнутри компонента невозможно.
*/
export declare function useGrComponentProp<TComponent extends GrConfigurableComponent, TProp extends keyof GrDefaultsOf<TComponent>>(component: TComponent, prop: TProp, localValue: () => GrDefaultsOf<TComponent>[TProp], fallback: NonNullable<GrDefaultsOf<TComponent>[TProp]>): ComputedRef<NonNullable<GrDefaultsOf<TComponent>[TProp]>>;
/**
* Атрибут темы для **телепортируемого** корня панели.
*
* Панель уезжает в `body`, то есть вне DOM-поддерева провайдера, и `data-theme`
* с его обёртки на неё не наследуется. В дереве компонентов она при этом
* остаётся внутри — `inject` работает, — поэтому тему панель ставит себе сама.
* Без этого «тёмный остров» ломался бы на первом же открытом дропдауне.
*
* ```vue
* <teleport to="body">
* <div v-bind="themeAttrs">…</div>
* </teleport>
* ```
*/
export declare function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;Дефолтные пропсы конкретного компонента из ближайшего провайдера.
function useGrComponentDefaults<TComponent extends GrConfigurableComponent>(name: TComponent): ComputedRef<GrDefaultsOf<TComponent>>;Эффективное значение «оформительского» пропа: локальный проп →
componentDefaults[Component][prop] → собственный дефолт компонента.
Обязательное условие на стороне компонента: у такого пропа в withDefaults
должен стоять undefined, а «настоящий» дефолт — переехать сюда, в fallback.
Иначе конфиг не сработает никогда: Vue отдаёт props.x уже с подставленным
дефолтом, и отличить «пользователь передал значение» от «сработал дефолт»
изнутри компонента невозможно.
function useGrComponentProp<TComponent extends GrConfigurableComponent, TProp extends keyof GrDefaultsOf<TComponent>>(component: TComponent, prop: TProp, localValue: () => GrDefaultsOf<TComponent>[TProp], fallback: NonNullable<GrDefaultsOf<TComponent>[TProp]>): ComputedRef<NonNullable<GrDefaultsOf<TComponent>[TProp]>>;
/**
* Атрибут темы для **телепортируемого** корня панели.
*
* Панель уезжает в `body`, то есть вне DOM-поддерева провайдера, и `data-theme`
* с его обёртки на неё не наследуется. В дереве компонентов она при этом
* остаётся внутри — `inject` работает, — поэтому тему панель ставит себе сама.
* Без этого «тёмный остров» ломался бы на первом же открытом дропдауне.
*
* ```vue
* <teleport to="body">
* <div v-bind="themeAttrs">…</div>
* </teleport>
* ```
*/
export declare function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;
}Атрибут темы для **телепортируемого** корня панели.
Панель уезжает в body, то есть вне DOM-поддерева провайдера, и data-theme
с его обёртки на неё не наследуется. В дереве компонентов она при этом
остаётся внутри — inject работает, — поэтому тему панель ставит себе сама.
Без этого «тёмный остров» ломался бы на первом же открытом дропдауне.
``vue
<teleport to="body">
<div v-bind="themeAttrs">…</div>
</teleport>
``
function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;
}>;Файлdist/types/composables/useGrComponentConfig.d.ts
Открытый реестр компонентов, у которых есть настраиваемые через конфиг пропы.
Объявление живёт здесь, а не в shared/configContext, потому что это
**точка расширения**: компонент дополняет реестр из своей папки через
declaration merging, и то же самое обязан уметь companion-пакет. Дополнять
можно только тот модуль, который тип объявляет; через реэкспорт-баррель
слияние работает лишь до первой чужой аугментации, а дальше молча
отваливается — типы расходятся с рантаймом, ошибки нет.
``ts
// GrButton/defaults.ts
declare module '../../composables/useGrComponentConfig' {
interface GrComponentDefaultsRegistry {
GrButton: GrButtonConfigurableProps
}
}
`
Companion-пакет пишет тот же блок с публичным именем модуля
@feugene/granularity/composables/useGrComponentConfig.
Реестр пуст по умолчанию и состоит ровно из тех компонентов, которые попали
в граф типов потребителя: импортировали только GrButton — в
componentDefaults` типизирована только кнопка.
interface GrComponentDefaultsRegistry {
}Файлdist/types/components/GrConfigProvider/config.d.ts
const grConfigProviderConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrConfigProvider">;Файлdist/types/components/GrConfigProvider/GrConfigProvider.vue.d.ts
interface GrConfigProviderProps {
/** Дефолтный размер контролов для вложенных компонентов. */
size?: GrComponentSize;
/** Дефолтные пропсы по компонентам: `{ GrButton: { variant: 'secondary' } }`. */
componentDefaults?: GrComponentDefaults;
/** Адаптер переводов (fint-i18n-совместимый). Прокидывается вложенным компонентам. */
i18n?: GranularityI18nAdapter | null;
/**
* Просьба к адаптеру переключить язык (`syncLocale`, у инстанса fint-i18n —
* `setLocale`). Источником истины остаётся сам адаптер — провайдер лишь
* передаёт ему намерение.
*/
locale?: string;
/**
* Тема поддерева: значение уезжает в `data-theme`. Тема **документа** —
* работа `useTheme`/`initThemeEarly`, здесь именно остров.
*/
theme?: string;
/**
* Куда монтировать оверлеи поддерева. По умолчанию — общий `#gr-portal` в
* `body`. Своё значение нужно там, где приложение живёт в контейнере:
* микрофронтенд, shadow DOM, CSS-скоупинг под конкретным корнем.
*/
portalTarget?: string | HTMLElement;
/**
* База шкалы слоёв. Переменные `--gr-z-*` пересчитываются от неё и ставятся
* на `<html>`: панели телепортируются в `body`, и переменные поддерева до
* них не доходят.
*/
zIndexBase?: number;
/** Тег обёртки. По умолчанию прозрачный `<div style="display:contents">`. */
tag?: string;
}Использование из прозы библиотеки
Порядок разрешения
Локальный проп компонента → componentDefaults[Component] → глобальный size
→ собственный дефолт компонента. Отсюда правило для авторов компонентов: проп,
настраиваемый через провайдер, объявляется с дефолтом undefined, иначе Vue
подставит своё значение раньше, чем компонент заглянет в конфиг.
Прочитать эффективный конфиг из приложения можно useGrConfig() — он
публичный.
Две шкалы размеров
size у провайдера — про контролы (xs | sm | md | lg). У оверлеев шкала
своя (sm | md | lg | xl | full), и глобальный size их не трогает: xs для
модального окна не значит ничего. Размер окна задаётся точечно:
<GrConfigProvider :component-defaults="{ GrModal: { size: 'lg' } }">Так настраиваются GrModal, GrDialog, GrConfirmDialog, GrPromptDialog,
GrCommandPalette и GrDrawer.
i18n
Адаптер отдаётся вниз всегда и через фасад, а не значением: адаптер,
созданный асинхронно (обычная загрузка локали), доедет до детей, когда
появится, а подмена адаптера при смене языка перерисует строки. Если проп не
задан, фасад делегирует адаптеру, найденному выше по дереву, — установка через
app.use() продолжает работать.
locale — просьба к адаптеру переключиться: провайдер зовёт syncLocale, а у
инстанса @feugene/fint-i18n, где такого метода нет, — setLocale. Источником
истины остаётся сам адаптер: провайдер не хранит локаль и не подменяет её.
Тема поддерева
theme кладёт значение в data-theme на обёртку. Обе темы объявлены
атрибутным селектором — [data-theme='light'] наравне с :root,
[data-theme='dark'], — поэтому остров работает в обе стороны без
дополнительных стилей: и тёмный внутри светлой страницы, и светлый внутри
тёмной. Вместе с ролями остров получает color-scheme своей темы, а
производные формулы (--gr-primary-hover и соседи) и ссылки покомпонентных
тем пересчитываются на нём, а не наследуются посчитанными от страницы.
Свои токены приложения, ссылающиеся на роли (--app-accent: var(--gr-primary)),
стоит объявлять на тех же селекторах — :root, [data-theme], .light, .dark:
значение var() подставляется там, где свойство объявлено, и объявленное только
на :root остров унаследует уже посчитанным.
Панели оверлеев телепортируются в 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, который пакет создаёт сам при первом открытии.
<GrConfigProvider portal-target="#my-app-portal">
<App />
</GrConfigProvider>Нужно там, где приложение живёт в своём контейнере: микрофронтенд внутри чужой
страницы, shadow DOM, CSS-скоупинг под конкретным корнем. Значение наследуется
вложенными провайдерами, а отдельный компонент может переопределить его пропом
teleportTo.
Провайдер только называет цель — DOM он не создаёт: контейнер должен
существовать к моменту открытия оверлея. Требование к нему одно, зато жёсткое:
никаких transform, filter, contain, perspective и will-change —
они создают containing block для position: fixed, и floating-панели начнут
считать позицию от контейнера, а не от вьюпорта. Подробности —
../z-index.md.
Пропы этого примера