# ConfigProvider

**When to use:** Set global **default props** for library components once, near the root of your app, instead of repeating the same prop on every instance. If your app always wants floating labels, set `Input.alwaysFloatLabel` here and every `Input` picks it up.

**Keywords:** global defaults, i18n, internationalisation, localisation

**Import:** `import { ConfigProvider, useLocale, Presentation } from '@ahrowe/ui'`
**Types:** `import type { ComponentDefaults, ConfigProviderProps } from '@ahrowe/ui'`

At the app root, pass `defaultProps`, `presentation`, `navigate` and `locale` to [AhroweProvider](AhroweProvider.md), which includes a ConfigProvider. Use ConfigProvider on its own for a section with defaults of its own:

```tsx
import { AhroweProvider, ConfigProvider } from '@ahrowe/ui';

<AhroweProvider defaultProps={{ Input: { alwaysFloatLabel: true } }}>
  <App />
</AhroweProvider>

// Inside App: one form that wants different defaults
<ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: false } }}>
  <SignupForm />
</ConfigProvider>
```

## How defaults resolve

For each prop, the value is chosen in this order (first that's set wins):

1. The prop **explicitly passed** to the component instance
2. The matching **global default** from the nearest `ConfigProvider`
3. The component's own **built-in default**

So a global default only fills in props a given instance left out — any instance can still override it:

```tsx
<ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: true } }}>
  <Input label="Name" />                        {/* floats — inherits the global default */}
  <Input label="Note" alwaysFloatLabel={false} />{/* opts out — explicit prop wins        */}
</ConfigProvider>
```

## Nesting

`ConfigProvider`s compose. A nested provider extends the outer one per component, overriding only the keys it sets:

```tsx
<ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: true, useMatLabelStyle: true } }}>
  {/* Inputs here float and use the material label */}
  <ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: false } }}>
    {/* Inputs here don't float, but still use the material label from the outer provider */}
  </ConfigProvider>
</ConfigProvider>
```

## Key props

| Prop | Type | Description |
|------|------|-------------|
| `defaultProps` | `ComponentDefaults` | Default props keyed by component name, e.g. `{ Input: { alwaysFloatLabel: true } }` |
| `presentation` | `Presentation` | How every sheet-capable component presents by default — one switch for all of them (see below) |
| `navigate` | `(path: string) => void` | The app's router, for every link a component renders (see below) |
| `locale` | `string` | BCP 47 tag for the dates, week start and sort order components produce (see below) |
| `children` | `ReactNode` | Your app content |

## App-wide `presentation`

`Popover`, `Menu`, `Dropdown`, `MultiSelect` and `Modal` can each present as a bottom sheet instead of anchored to their trigger (centred, for `Modal`). They're unrelated components, so setting that through `defaultProps` would mean five entries, and missing one would leave sheets in half an app. `presentation` is the single switch:

```tsx
// Bottom sheets on small touch screens, everywhere, in one line
<ConfigProvider presentation={Presentation.Auto}>
  <App />
</ConfigProvider>
```

It resolves *under* the component-keyed defaults, so the full order for a `presentation` prop is:

1. The prop passed to the instance
2. `defaultProps.<Component>.presentation`
3. This app-wide `presentation`
4. The component's built-in default, `Presentation.Default` — anchored to the trigger, or centred for `Modal`

```tsx
// Sheets app-wide, except Dropdown, except this one Dropdown
<ConfigProvider
  presentation={Presentation.Auto}
  defaultProps={{ Dropdown: { presentation: Presentation.Default } }}
>
  <Dropdown label="Status" items={items} presentation={Presentation.Sheet} />
</ConfigProvider>
```

A nested provider inherits it unless it sets its own, the same way `defaultProps` compose. Everything built on those four components follows along: `SplitButton`, `ColorPicker`, `Breadcrumb`, `VirtualList`, and `DatePicker`/`TimeInput`/`KlipyPicker` in either their menu or their `isModal` mode. `InputDropdown` and `Tooltip` are deliberately never sheets and ignore it.

## Client-side routing (`navigate`)

Button, ActionIcon and ListItem with an `href`, and Breadcrumb, Menu and CommandPalette entries with one, are real links: a long press, Ctrl/⌘-click and a middle click work on them as on any `<a>`. Without `navigate` a plain click loads the page. With it, a plain click on a page of the app calls `navigate` with the path instead:

```tsx
import { Outlet, useNavigate } from 'react-router-dom';
import { ConfigProvider } from '@ahrowe/ui';

// The root route's element. useNavigate needs the router, and AhroweProvider is usually outside it.
function Root() {
  const navigate = useNavigate();
  return (
    <ConfigProvider navigate={navigate}>
      <Outlet />
    </ConfigProvider>
  );
}
```

It is called with the path, search and hash (`/clients/7?tab=projects`) for a plain left click on an `http:` or `https:` link to the page's own origin. The browser keeps everything else: a click with Ctrl, ⌘, Shift or Alt, a middle click, a link with a `target` or `download`, another site, `mailto:`, `tel:`, a `blob:` URL, and a jump to an anchor on the current page. An `onClick` of your own runs first, and a `preventDefault()` in it keeps `navigate` out.

- **Write absolute paths.** An `href` is resolved against the page's URL, like any link, because that is what a middle click does; `navigate` gets the resolved path. A router resolves a relative path against the route instead, so `href="projects/1"` on `/clients/7` is `/clients/projects/1`, not where `navigate('projects/1')` would go.
- **`navigate` decides.** It gets every path of the page's own origin, also one the router does not own: a server route such as `/logout`, a file under `/api`, a page outside a `basename`. Hand those back to the browser with `window.location.assign(path)`. A `basename` is part of the `href` but not of the path the router's `navigate` takes, so strip it:

  ```tsx
  navigate={(path) => {
    if (path === BASENAME || path.startsWith(`${BASENAME}/`)) navigate(path.slice(BASENAME.length) || '/');
    else window.location.assign(path);
  }}
  ```
- **Two places it cannot reach.** Toasts render in `AhroweProvider`'s own host, outside a `ConfigProvider` placed inside the router. `menuHotkeys` runs outside React, so an entry with an `href` run from its shortcut loads the page.

## Dates and sorting (`locale`)

`locale` is a BCP 47 tag, `'de-AT'`, for what a component formats or sorts itself: Calendar's dates and times, DatePicker's month and weekday names, the first day of the week in both, and the order VirtualList sorts text in. Unset, every `Intl` API falls back to the browser's locale.

```tsx
<AhroweProvider locale='de-AT'>
  <App />
</AhroweProvider>
```

Calendar and DatePicker also take a `locale` prop, for one instance. It is not a `defaultProps` key: for everything else the nearest provider's `locale` decides, so a nested provider changes it for every component in its section, and an outer default cannot outrank it. A tag `Intl` cannot read, such as `'de_AT'`, falls back to the outer provider's or the browser's, with a warning.

Set it whenever the app renders on the server. Unset, the server's language picks the month names and the first day of the week for the first render, and the browser's for the hydrated one, and React reports the difference.

Both components take `weekStartsOn` too, for an app that wants another first day than its locale's: `defaultProps={{ Calendar: { weekStartsOn: 1 }, DatePicker: { weekStartsOn: 1 } }}` starts every week on Monday.

`useLocale()` returns the same value, so the dates your app formats itself match the components'. It is `undefined` when no `locale` is set, which `Intl` reads as the browser's:

```tsx
const locale = useLocale();
const due = new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(dayToDate(invoice.dueDay));
```

It sets formats, not text. The strings a component renders are its `labels` (see **Translating the components**), and DatePicker's field is typed as `dd.mm.yyyy` in every locale.

## Supported components

Each entry is a `Partial<...Props>`, so any of that component's props can be defaulted:

- **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput` · `TemplateEditor`
- **Display:** `Button` · `CopyButton` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Loader` · `LoadingOverlay` · `Accordion` · `Divider` · `Timer` · `WeatherIcon` · `Stepper`
- **Overlays:** `ConfirmModal` · `Modal` · `CommandPalette` · `Popover`
- **Chat:** `ChatMessage` · `ChatMessageList` · `ChatComposer`
- **Media:** `Image` · `Carousel` · `RevealLens` · `PanZoom` · `Lightbox`
- **Editors:** `RoomDrawer`, `RoomViewer`
- **Scheduling:** `Calendar`, for `labels`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays`, `scrollToHour`, `workingHours` and `showWeekNumbers` only (see below)

```tsx
<ConfigProvider
  defaultProps={{
    Input: { alwaysFloatLabel: true },
    Dropdown: { useMatLabelStyle: true },
    NumberInput: { decimalScale: 2, thousandSeparator: '.' },
    Button: { styleType: ButtonStyleType.Filled },
    Card: { styleType: CardStyleType.Outlined },
    Skeleton: { animation: SkeletonAnimation.Pulse },
  }}
>
  <App />
</ConfigProvider>
```

`Textarea`, `NumberInput`, `DatePicker`, and `TemplateEditor` are built on top of `Input`, so they **also** inherit `Input` defaults for the props they forward to it. Their own component-keyed defaults take precedence for props they handle themselves. Example: with `Input: { alwaysFloatLabel: true }` set, every `Textarea` / `NumberInput` / `DatePicker` / `TemplateEditor` floats its label too, without needing a separate entry.

The `ComponentDefaults` type is the source of truth for which keys are currently supported — more components adopt it over time.

## Translating the components

Every string a component renders of its own is set through its `labels` prop, and a `ConfigProvider` sets them for the whole app at once:

```tsx
<ConfigProvider
  defaultProps={{
    Alert: { labels: { dismiss: 'Schließen' } },
    Modal: { labels: { close: 'Schließen' } },
    Pagination: { labels: { goToPage: (page) => `Zu Seite ${page}` } },
    VirtualList: { labels: { selectRow: 'Zeile auswählen', selectAllRows: 'Alle Zeilen auswählen' } },
  }}
>
  <App />
</ConfigProvider>
```

**Labels merge per key**, unlike every other default here. An instance that overrides one string keeps the provider's translations for the rest, so `<Alert labels={{ dismiss: 'Weg damit' }} />` under the provider above does not lose anything else Alert might have been given. Anything the component does not produce itself, a `title`, a column `label`, a button's children, is already yours and never appears in `labels`.

`ColorPicker` and `VirtualList` appear in `ComponentDefaults` with their labels only: the first is configured elsewhere, the second has generic props that a per-key default map cannot express. `Calendar` is generic too, so its entry is the props that do not depend on the event type: `labels`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays`, `scrollToHour`, `workingHours` and `showWeekNumbers`. `Toast` is not there at all, because `AhroweProvider`'s `toast` prop is already its app-wide configuration and carries `labels` itself.

Each component's doc lists its own keys under **Labels:**.

## Migration from `setLocale` and `getLocale`

`setLocale`, `getLocale` and `mapLocale` are gone. Only DatePicker read them, while Calendar took a `locale` of its own, so the two could disagree in one app. Both now read `locale` from the provider.

| Before | After |
| ------ | ----- |
| `setLocale('de_AT')` | `<AhroweProvider locale='de-AT'>`: a BCP 47 tag, with a hyphen |
| `getLocale('-')` | `useLocale()`. `undefined` when nothing is set, which `Intl` reads as the browser's locale; `getLocale` guessed one of seven supported locales and fell back to `en_US` |
| `mapLocale(locale)` | `new Intl.Locale(locale).language`, `'de'` for `'de-AT'` |
| `defaultProps={{ Calendar: { locale: 'de-AT' } }}` | `locale='de-AT'` on the provider |

DatePicker now also starts the week on the locale's first day, as Calendar already did: Sunday for `en-US`, where it was always Monday. `weekStartsOn={1}` keeps Monday.
