GrConfigProvider
Берут, когда размеры контролов едины на приложение.
Обзор
Когда брать
- размеры контролов едины на приложение —
sizeодин раз вместо пропа на каждом поле; - у компонента свои дефолты в этом проекте —
componentDefaultsменяет их, не трогая места употребления; - подключается перевод — адаптер
tиlocaleдоходят до всех компонентов пакета; - поддерево живёт в своём режиме или в другой теме сборки — тёмная панель внутри светлого приложения без глобального переключения;
- оверлеи должны лечь выше чужих —
zIndexBaseсдвигает всю шкалу слоёв пакета.
Когда взять другое
| Нужно | Берите |
|---|---|
| Значение нужно одному компоненту | проп на самом компоненте |
| Меняется палитра, а не дефолты | packages/granularity-tokens/docs/themes.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Режим для превью, а не для страницы
modeкладёт пару режима вdata-themeна поддерево: администратор выбирает режим портала и видит его, а сама админка остаётся в своём. Панели, улетающие вbody, берут режим хозяина, а не страницы, из которой вылетели.Customer portal modeINV-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`). Источником истины остаётся сам адаптер — провайдер лишь передаёт ему намерение. |
mode | string | undefined | undefined | Режим поддерева — остров: `dark` уезжает в `data-theme` парой текущей темы документа (`slate-dark`). Режима у темы нет — первый той же схемы, иначе её режим по умолчанию. Режим **документа** — работа `useTheme`/`initThemeEarly`. |
theme | string | undefined | undefined | Остров другой темы сборки: `theme="clay" mode="dark"` пишет `data-theme="clay-dark"`. Тема обязана быть в `themes` конфига granum — иначе её пар нет в CSS. Лист дополнительной темы остров подгружает сам по адресу из манифеста; страница, которая показывает тему всегда, импортирует его заранее (`virtual:granum/themes/<id>.css`) — тогда нет кадра без значений, а загрузчик узнаёт лист и второй раз его не качает. Без `mode` — режим документа, если он у темы есть, иначе той же схемы. |
portalTarget | string | HTMLElement | undefined | undefined | Куда монтировать оверлеи поддерева. По умолчанию — общий `#gr-portal` в `body`. Своё значение нужно там, где приложение живёт в контейнере: микрофронтенд, shadow DOM, CSS-скоупинг под конкретным корнем. |
zIndexBase | number | undefined | undefined | База шкалы слоёв. От неё пересчитываются все семь `--gr-z-*` с шагами темы — от нижней навигации (−150) до тостов (+200) — и ставятся на `<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>;
/**
* Слои `componentDefaults` порознь: темы — и всё, что над ней (приложение,
* провайдеры). Остров темы подменяет только слой темы. Необязательны:
* контекст, построенный руками, несёт один `componentDefaults`.
*/
themeDefaults?: ComputedRef<GrComponentDefaults | undefined>;
overrideDefaults?: ComputedRef<GrComponentDefaults>;
/**
* Тема поддерева — остров другой темы сборки. Необязательна: тип публичный,
* и контекст строят руками — например хост императивных диалогов.
*/
theme?: ComputedRef<GrThemeId | undefined>;
/** Режим поддерева — остров. Необязателен по той же причине, что и `theme`. */
mode?: ComputedRef<GrModeName | undefined>;
/**
* Куда монтируются оверлеи поддерева (`usePortalTarget`). Необязательно, как
* и `theme`: контекст строят руками — например хост императивных диалогов.
*/
portalTarget?: ComputedRef<string | HTMLElement | undefined>;
}Пропсы по умолчанию, которые несёт тема: componentDefaults в
extensions.granularity её записи в манифесте virtual:granum/themes. Так
тема меняет то, чего токеном не выразить, — вид индикатора загрузки, форму
группы. Без themeId — основная тема сборки. Нет или не та форма — undefined.
function themeComponentDefaults(manifest: GrThemeManifest | null | undefined, themeId?: GrThemeId): GrComponentDefaults | 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<Record<string, 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<Record<string, string>>;Атрибут режима для **телепортируемого** корня панели.
Панель уезжает в body, то есть вне DOM-поддерева провайдера, и data-theme
с его обёртки на неё не наследуется. В дереве компонентов она при этом
остаётся внутри — inject работает, — поэтому пару панель ставит себе сама.
Без этого острова режима и темы ломались бы на первом же открытом дропдауне.
``vue
<teleport to="body">
<div v-bind="themeAttrs">…</div>
</teleport>
``
function useGrThemeAttrs(): ComputedRef<Record<string, 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;
/**
* Режим поддерева — остров: `dark` уезжает в `data-theme` парой текущей темы
* документа (`slate-dark`). Режима у темы нет — первый той же схемы, иначе её
* режим по умолчанию. Режим **документа** — работа `useTheme`/`initThemeEarly`.
*/
mode?: GrModeName;
/**
* Остров другой темы сборки: `theme="clay" mode="dark"` пишет
* `data-theme="clay-dark"`. Тема обязана быть в `themes` конфига granum —
* иначе её пар нет в CSS. Лист дополнительной темы остров подгружает сам по
* адресу из манифеста; страница, которая показывает тему всегда, импортирует
* его заранее (`virtual:granum/themes/<id>.css`) — тогда нет кадра без
* значений, а загрузчик узнаёт лист и второй раз его не качает. Без `mode` —
* режим документа, если он у темы есть, иначе той же схемы.
*/
theme?: GrThemeId;
/**
* Куда монтировать оверлеи поддерева. По умолчанию — общий `#gr-portal` в
* `body`. Своё значение нужно там, где приложение живёт в контейнере:
* микрофронтенд, shadow DOM, CSS-скоупинг под конкретным корнем.
*/
portalTarget?: string | HTMLElement;
/**
* База шкалы слоёв. От неё пересчитываются все семь `--gr-z-*` с шагами
* темы — от нижней навигации (−150) до тостов (+200) — и ставятся на
* `<html>`: панели телепортируются в `body`, и переменные поддерева до них
* не доходят.
*/
zIndexBase?: number;
/** Тег обёртки. По умолчанию прозрачный `<div style="display:contents">`. */
tag?: string;
}Использование из прозы библиотеки
Порядок разрешения
Локальный проп компонента → componentDefaults[Component] → глобальный size
→ собственный дефолт компонента. Отсюда правило для авторов компонентов: проп,
настраиваемый через провайдер, объявляется с дефолтом undefined, иначе Vue
подставит своё значение раньше, чем компонент заглянет в конфиг.
componentDefaults верхнего провайдера ложатся поверх двух слоёв без провайдера:
пропсов по умолчанию текущей темы документа (extensions.granularity.componentDefaults
её записи в манифесте virtual:granum/themes, если он отдан granularityThemePlugin) и
приложения (createGranularity({ componentDefaults })). Они же действуют там, где
провайдера в дереве нет вовсе. Слой темы следует за useTheme().setTheme: после смены темы
компоненты берут её пропсы.
Провайдер с theme — остров темы — подменяет слой темы документа пропсами по
умолчанию своей темы или снимает его, если своих у неё нет: пропсы одной темы на
компонентах другой выглядели бы чужими. Приложение и провайдеры выше острова остаются сверху.
Прочитать эффективный конфиг из приложения можно 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. Источником
истины остаётся сам адаптер: провайдер не хранит локаль и не подменяет её.
Режим и тема поддерева
mode кладёт пару режима текущей темы документа в data-theme на обёртку: mode="dark" —
slate-dark. Каждый режим объявлен на своей паре —
[data-theme="slate-light"] наравне с :root, [data-theme="slate-dark"], —
поэтому остров работает в обе стороны без дополнительных стилей: и тёмный
внутри светлой страницы, и светлый внутри тёмной. Вместе с ролями остров
получает color-scheme своего режима, а
производные формулы (--gr-primary-hover и соседи) и ссылки покомпонентных
тем пересчитываются на нём, а не наследуются посчитанными от страницы.
theme — остров другой темы сборки: theme="clay" mode="dark" пишет
data-theme="clay-dark". Тема обязана быть в themes конфига granum — иначе её пар
нет ни в одном листе. Лист дополнительной темы остров подгружает сам по адресу из
манифеста; пока он грузится, поддерево показывает унаследованные значения, а ошибка
загрузки их и оставляет. Странице, которая показывает тему всегда, лист лучше
импортировать статически — virtual:granum/themes/clay.css: тогда кадра без значений
нет, а загрузчик узнаёт такой лист и второй раз его не качает. Режим острова — его
mode, иначе режим документа; если у темы такого режима нет — первый той же схемы, иначе
её режим по умолчанию (то же правило, что у setTheme), а в dev-сборке провайдер
предупреждает, что заданного режима у темы нет. Вложенный провайдер с одним mode меняет
режим острова. Имя режима в theme (theme="dark") — частая ошибка: о ней dev-сборка тоже
предупреждает, как и о листе, который не загрузился.
Пары берутся из манифеста тем, который получил granularityThemePlugin
(или initThemeEarly), — вместе с именем атрибута, если сборка его поменяла.
Свои токены приложения, ссылающиеся на роли (--app-accent: var(--gr-primary)),
стоит объявлять на той же границе — :root, [data-theme]:
значение var() подставляется там, где свойство объявлено, и объявленное только
на :root остров унаследует уже посчитанным.
Панели оверлеев телепортируются в body, то есть в DOM живут вне обёртки, —
но в дереве компонентов остаются внутри, поэтому inject до них доходит, и
пару они ставят себе сами. Так покрыты модалка, дровер, дропдаун, поповер,
тултип, селекты, тостер и просмотрщик изображений.
Тема и режим документа — работа useTheme/initThemeEarly. Пропы провайдера
именно про острова; двух механизмов на одно и то же в пакете нет.
Шкала слоёв
zIndexBase пересчитывает от базы все семь слоёв шкалы (bottom-nav −150,
navbar −100, dropdown +0, tooltip +50, modal +100, loading +150,
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.
Пропы этого примера