GrConfigProvider

  • ядро
  • Утилиты
  • API · 8 props

Берут, когда размеры контролов едины на приложение.

import { GrConfigProvider } from '@feugene/granularity/components/GrConfigProvider'

Обзор

Когда брать

  • размеры контролов едины на приложение — size один раз вместо пропа на каждом поле;
  • у компонента свои дефолты в этом проекте — componentDefaults меняет их, не трогая места употребления;
  • подключается перевод — адаптер t и locale доходят до всех компонентов пакета;
  • поддерево живёт в своей теме — тёмная панель внутри светлого приложения без глобального переключения;
  • оверлеи должны лечь выше чужих — zIndexBase сдвигает всю шкалу слоёв пакета.

Когда взять другое

НужноБерите
Значение нужно одному компонентупроп на самом компоненте
Меняется палитра, а не дефолтыpackages/granularity/docs/theming.md
Нужны переводы, а не их подключениеpackages/granularity/docs/localization.md

Рендерится прозрачно (display: contents) и работает через provide/inject, поэтому раскладку не меняет и может стоять где угодно — в том числе несколько раз вложенно.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 4 у каждого — живое демо и его код

  • Один тулбар, две плотности

    Бэк-офис плотный, складской планшет держат в перчатках — один и тот же тулбар в провайдере с size="sm" и size="lg". Внутри никто не повторяет size, поэтому поле, селект и кнопка держат одну высоту; ряд, который расходится на ступень, — это ряд, размеренный вручную.

    Back office · size «sm»Mouse and keyboard, many rows on screen
    Warehouse tablet · size «lg»Touch, at arm's length
  • Тихие кнопки в списке

    component-defaults делает каждую GrButton в списке маленькой нейтральной ghost-кнопкой, и двенадцать действий не топят суммы. Единственное важное действие — «Remind» у просроченного счёта — говорит variant="primary" на себе и побеждает: дефолт — это только то, что не сказано.

    Sent invoices
    Northwind TradersINV-0418 · €1,284.50
    Tailspin ToysINV-0417 · €2,150.00
    Contoso LtdINV-0415 · €432.00
  • Тема для превью, а не для страницы

    theme кладёт data-theme на поддерево: администратор выбирает тему портала и видит её, а сама админка остаётся в своей. Панели, улетающие в body, берут тему хозяина, а не страницы, из которой вылетели.

    Customer portal theme
    INV-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

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefinedДефолтный размер контролов для вложенных компонентов.
componentDefaultsGrComponentDefaults | undefinedundefinedДефолтные пропсы по компонентам: `{ GrButton: { variant: 'secondary' } }`.
i18nGranularityI18nAdapter | null | undefinedundefinedАдаптер переводов (fint-i18n-совместимый). Прокидывается вложенным компонентам.
localestring | undefinedundefinedПросьба к адаптеру переключить язык (`syncLocale`, у инстанса fint-i18n — `setLocale`). Источником истины остаётся сам адаптер — провайдер лишь передаёт ему намерение.
themestring | undefinedundefinedТема поддерева: значение уезжает в `data-theme`. Тема **документа** — работа `useTheme`/`initThemeEarly`, здесь именно остров.
portalTargetstring | HTMLElement | undefinedundefinedКуда монтировать оверлеи поддерева. По умолчанию — общий `#gr-portal` в `body`. Своё значение нужно там, где приложение живёт в контейнере: микрофронтенд, shadow DOM, CSS-скоупинг под конкретным корнем.
zIndexBasenumber | undefinedundefinedБаза шкалы слоёв. Переменные `--gr-z-*` пересчитываются от неё и ставятся на `<html>`: панели телепортируются в `body`, и переменные поддерева до них не доходят.
tagstring | undefined"div"Тег обёртки. По умолчанию прозрачный `<div style="display:contents">`.

Слоты

SlotTypeОписание
defaultanyПоддерево, которому адресованы настройки.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrComponentSize, GrConfigurableComponent, GrComponentDefaults } from '@feugene/granularity/components/GrConfigProvider'

Файлdist/types/components/shared/configContext.d.ts

GrComponentSizetype
type GrComponentSize = typeof GR_COMPONENT_SIZES[number];
GrConfigurableComponenttype

Имя компонента, у которого есть настраиваемые через конфиг пропы.

type GrConfigurableComponent = keyof GrComponentDefaultsRegistry;
GrComponentDefaultstype

Дефолтные пропсы по имени компонента: { GrButton: { variant: 'secondary' } }. Набор ключей закрыт реестром, а не Record<string, unknown>: через конфиг должны настраиваться только «оформительские» пропы. Пустить туда что угодно — значит разрешить задать глобальный modelValue или обработчик и получить баг, который ищется часами.

type GrComponentDefaults = {
    [TComponent in GrConfigurableComponent]?: Partial<GrComponentDefaultsRegistry[TComponent]>;
};
GrConfigContextinterface
interface GrConfigContext {
    /** Дефолтный размер контролов (если проп `size` не задан на компоненте). */
    size: ComputedRef<GrComponentSize | undefined>;
    /** Дефолтные пропсы по компонентам. */
    componentDefaults: ComputedRef<GrComponentDefaults>;
    /**
     * Тема поддерева (`data-theme`). Необязательна: тип публичный, и контекст
     * строят руками — например хост императивных диалогов.
     */
    theme?: ComputedRef<string | undefined>;
    /**
     * Куда монтируются оверлеи поддерева (`usePortalTarget`). Необязательно, как
     * и `theme`: контекст строят руками — например хост императивных диалогов.
     */
    portalTarget?: ComputedRef<string | HTMLElement | undefined>;
}
useGrConfigfunction

Возвращает ближайший GrConfigProvider или пустой конфиг, если провайдера нет.

function useGrConfig(): GrConfigContext;
GrConfigSourcetype

Источник конфига для {@link resolveGrConfig}: сам контекст, AppContext или инстанс компонента — у двух последних читается provides.

type GrConfigSource = GrConfigContext | {
    provides?: Record<symbol | string, unknown>;
};
UseGrComponentSizeOptionstype
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[];
};
useGrComponentSizefunction

Эффективный размер компонента. Приоритет: локальный проп → 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;
useGrComponentDefaultsfunction

Дефолтные пропсы конкретного компонента из ближайшего провайдера.

function useGrComponentDefaults<TComponent extends GrConfigurableComponent>(name: TComponent): ComputedRef<GrDefaultsOf<TComponent>>;
useGrComponentPropfunction

Эффективное значение «оформительского» пропа: локальный проп → 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;
}
useGrThemeAttrsfunction

Атрибут темы для **телепортируемого** корня панели. Панель уезжает в 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

GrComponentDefaultsRegistryinterface

Открытый реестр компонентов, у которых есть настраиваемые через конфиг пропы. Объявление живёт здесь, а не в 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

grConfigProviderConfigconst
const grConfigProviderConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrConfigProvider">;

Файлdist/types/components/GrConfigProvider/GrConfigProvider.vue.d.ts

GrConfigProviderPropsinterface
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 для модального окна не значит ничего. Размер окна задаётся точечно:

vue
<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, который пакет создаёт сам при первом открытии.

vue
<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.