GrConfigProvider
Provides global defaults (control size, per-component props, i18n) to nested components — imperative dialogs included.
Overview
When to take it
- the sizes of the controls are uniform across the application —
sizeonce instead of a prop on every field; - a component has its own defaults in this project —
componentDefaultschanges them without touching the places of use; - a translation is being connected — the
tadapter andlocalereach every component of the package; - a subtree lives in a theme of its own — a dark panel inside a light application without a global switch;
- overlays have to lie above someone else’s —
zIndexBaseshifts the whole layer scale of the package.
When to take something else
| Need | Take |
|---|---|
| The value is needed by a single component | a prop on the component itself |
| The palette changes, not the defaults | packages/granularity/docs/theming.md |
| Translations are needed, not their connection | packages/granularity/docs/localization.md |
It renders transparently (display: contents) and works through provide/inject,
so it does not change the layout and may stand anywhere — including several times
nested.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<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>Examples 4 each with a live demo and its code
One toolbar, two densities
The back office is dense, the warehouse tablet is used with gloves — the same toolbar, wrapped in a provider with
size="sm"andsize="lg". Nothing inside repeatssize, so the field, the select and the button keep one height; the row that ends up a step off is the one sized by hand.Back office · size «sm»Mouse and keyboard, many rows on screenWarehouse tablet · size «lg»Touch, at arm's lengthQuiet buttons in a list
component-defaultsmakes every GrButton in the list a small neutral ghost, so twelve actions do not drown the amounts. The one action that matters — «Remind» on an overdue invoice — saysvariant="primary"on itself and wins: a default is only what is left unsaid.Sent invoices Northwind TradersINV-0418 · €1,284.50Tailspin ToysINV-0417 · €2,150.00Contoso LtdINV-0415 · €432.00A theme for a preview, not for the page
themewritesdata-themeon the subtree: the admin picks the portal’s theme and sees it, while the admin page stays in its own. Panels that fly out intobodytake the theme of their owner, not of the page they left.Customer portal themeINV-0418Due in 3 daysAmount€1,284.50A Spanish island in an English admin
i18nhands a translation adapter to the subtree: the upload area, the pagination labels and «21–40 de 236» speak Spanish, the admin around stays English. The package ships the catalogue (@feugene/granularity/i18n); a ten-line adapter withtandtereads it, and until it loads the components keep their English fallback.Portal de clientes · Iberia Muebles S.L.Built-in strings of the components come from the provider's adapter Drag files here or click to select
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The default size of the controls for the nested components. |
componentDefaults | GrComponentDefaults | undefined | undefined | Default props per component: `{ GrButton: { variant: 'secondary' } }`. |
i18n | GranularityI18nAdapter | null | undefined | undefined | The translation adapter (fint-i18n compatible). It is passed to the nested components. |
locale | string | undefined | undefined | A request to the adapter to switch the language (`syncLocale`, on a fint-i18n instance — `setLocale`). The source of truth remains the adapter itself — the provider only passes on the intent. |
theme | string | undefined | undefined | The theme of the subtree: the value travels into `data-theme`. The theme of the **document** is the job of `useTheme`/`initThemeEarly`; this is precisely an island. |
portalTarget | string | HTMLElement | undefined | undefined | Where to mount the overlays of the subtree. By default — the shared `#gr-portal` in `body`. A value of your own is needed where the application lives in a container: a micro-frontend, a shadow DOM, CSS scoping under a particular root. |
zIndexBase | number | undefined | undefined | The base of the layer scale. The `--gr-z-*` variables are recomputed from it and set on `<html>`: panels are teleported into `body`, and the variables of a subtree do not reach them. |
tag | string | undefined | "div" | The tag of the wrapper. By default a transparent `<div style="display:contents">`. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The subtree the settings are addressed to. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrComponentSize, GrConfigurableComponent, GrComponentDefaults } from '@feugene/granularity/components/GrConfigProvider'Filedist/types/components/shared/configContext.d.ts
type GrComponentSize = typeof GR_COMPONENT_SIZES[number];type GrConfigurableComponent = keyof GrComponentDefaultsRegistry;type GrComponentDefaults = {
[TComponent in GrConfigurableComponent]?: Partial<GrComponentDefaultsRegistry[TComponent]>;
};interface GrConfigContext {
size: ComputedRef<GrComponentSize | undefined>;
componentDefaults: ComputedRef<GrComponentDefaults>;
theme?: ComputedRef<string | undefined>;
portalTarget?: ComputedRef<string | HTMLElement | undefined>;
}function useGrConfig(): GrConfigContext;type GrConfigSource = GrConfigContext | {
provides?: Record<symbol | string, unknown>;
};type UseGrComponentSizeOptions<TSize extends string> = {
component?: GrConfigurableComponent;
fallback?: TSize;
supported?: readonly TSize[];
};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>>;
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]>>;
export declare function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;function useGrComponentDefaults<TComponent extends GrConfigurableComponent>(name: TComponent): ComputedRef<GrDefaultsOf<TComponent>>;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]>>;
export declare function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;
}function useGrThemeAttrs(): ComputedRef<{
'data-theme'?: string;
}>;Filedist/types/composables/useGrComponentConfig.d.ts
interface GrComponentDefaultsRegistry {
}Filedist/types/components/GrConfigProvider/config.d.ts
const grConfigProviderConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrConfigProvider">;Filedist/types/components/GrConfigProvider/GrConfigProvider.vue.d.ts
interface GrConfigProviderProps {
size?: GrComponentSize;
componentDefaults?: GrComponentDefaults;
i18n?: GranularityI18nAdapter | null;
locale?: string;
theme?: string;
portalTarget?: string | HTMLElement;
zIndexBase?: number;
tag?: string;
}Usage from the library prose
The resolution order
The local prop of the component → componentDefaults[Component] → the global size
→ the component’s own default. Hence the rule for the authors of components: a prop
configurable through the provider is declared with an undefined default, otherwise
Vue substitutes its own value before the component looks into the config.
The effective config can be read from the application with useGrConfig() — it is
public.
Two size scales
The size of the provider is about controls (xs | sm | md | lg). Overlays have
a scale of their own (sm | md | lg | xl | full), and the global size does not
touch them: xs means nothing for a modal window. The size of a window is set
pointwise:
<GrConfigProvider :component-defaults="{ GrModal: { size: 'lg' } }">That is how GrModal, GrDialog, GrConfirmDialog, GrPromptDialog,
GrCommandPalette and GrDrawer are configured.
i18n
The adapter is handed down always and through a facade rather than by value: an
adapter created asynchronously (the usual loading of a locale) will reach the
children when it appears, and swapping the adapter on a change of language redraws
the strings. If the prop is not set, the facade delegates to the adapter found
higher up the tree — installation through app.use() keeps working.
locale is a request to the adapter to switch: the provider calls syncLocale,
and on an @feugene/fint-i18n instance, which has no such method, setLocale. The
source of truth remains the adapter itself: the provider neither stores the locale
nor replaces it.
The theme of a subtree
theme puts the value into data-theme on the wrapper. Both themes are declared with
an attribute selector — [data-theme='light'] alongside :root, and
[data-theme='dark'] — so an island works both ways with no extra styles: a dark one
inside a light page and a light one inside a dark page. Together with the roles the
island gets the color-scheme of its theme, and the derived formulas
(--gr-primary-hover and its neighbours) and the references of per-component themes
are recomputed on it rather than inherited already computed from the page.
The application’s own tokens that refer to roles (--app-accent: var(--gr-primary))
are best declared on the same selectors — :root, [data-theme], .light, .dark: a
var() value is substituted where the property is declared, and one declared only on
:root is inherited by the island already computed.
The panels of overlays are teleported into body, that is, in the DOM they live
outside the wrapper — but in the component tree they stay inside, so inject reaches
them and they set the theme for themselves. That covers the modal, the drawer, the
dropdown, the popover, the tooltip, the selects, the toaster and the image viewer.
The theme of the document is the job of useTheme/initThemeEarly. The prop of
the provider is precisely about an island; there are no two mechanisms for the same
thing in the package.
The layer scale
zIndexBase recomputes --gr-z-* from the base (dropdown +0, tooltip +50,
modal +100, toast +200) and sets them on <html>, restoring the previous
values on unmount.
On :root rather than on the wrapper for exactly the reason the theme is set by the
panels themselves: a panel moves into body and does not see the variables of the
subtree. “Layers by subtree” would be a false promise, so the scale is one per
document — a second provider with a different base warns about the conflict in a dev
build.
The same result is achieved with four lines of CSS; the prop is needed where the base comes from the runtime (a micro-frontend inside someone else’s application).
The mounting point of the overlays
portalTarget names the container the overlays of the subtree move into: modals,
select panels, toasts, imperative dialogs. By default that is the shared
#gr-portal in body, which the package creates itself on the first opening.
<GrConfigProvider portal-target="#my-app-portal">
<App />
</GrConfigProvider>It is needed where the application lives in a container of its own: a micro-frontend
inside someone else’s page, a shadow DOM, CSS scoping under a particular root. The
value is inherited by nested providers, and an individual component may override it
with the teleportTo prop.
The provider only names the target — it does not create the DOM: the container has to
exist by the time an overlay opens. There is a single requirement on it, but a strict
one: no transform, filter, contain, perspective or will-change — they
create a containing block for position: fixed, and floating panels will start
computing their position from the container rather than from the viewport. The
details — ../z-index.md.
Props in this example