Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
# `formattedCell`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A `cell:` builder for `@tanstack/react-table` `ColumnDef` that wraps the raw column value in a matching `<FormattedX/>` primitive. Lets you write locale-aware columns without per-cell `<FormattedNumber>` / `<FormattedCurrency>` boilerplate. Returns a function tanstack accepts as the `cell` callback — the surrounding `ColumnDef` shape is preserved unchanged, so the rest of the column config (`header`, `sortingFn`, `filterFn`, `enableSorting`, …) stays consistent with the rest of your tanstack columns.
## Features
- One helper for the four common formatter kinds (`number`, `currency`, `date`, `relative-time`).
- Empty-cell guard — `null` / `undefined` values render as an empty string rather than throwing or rendering `"NaN"` / `"Invalid Date"`.
- Composes with all the underlying formatted-primitive options (`Intl.NumberFormatOptions`, currency display style, date preset names, relative-time unit + auto-refresh).
## Usage: Basic Example
```ts
import { formattedCell } from "@suaveplan/react-ui";
import { createColumnHelper } from "@suaveplan/react-ui";
const helper = createColumnHelper<Order>();
const columns = [
helper.accessor("qty", { cell: formattedCell({ kind: "number" }) }),
helper.accessor("subtotal", {
cell: formattedCell({ kind: "currency", currency: "USD" }),
}),
helper.accessor("placedAt", {
cell: formattedCell({ kind: "date", options: "medium" }),
}),
];
```
## Usage: Advanced Example
Mix and match — auto-refreshing relative time on a "last seen" column alongside currency formatting:
```ts
const columns = [
helper.accessor("name"),
helper.accessor("lastSeen", {
cell: formattedCell({ kind: "relative-time", auto: true }),
}),
helper.accessor("balance", {
cell: formattedCell({
kind: "currency",
currency: "EUR",
options: { display: "code", maxFractionDigits: 2 },
}),
}),
];
```
For a cell that wraps the formatted output in additional markup (badge, link, indicator), compose your own `cell` callback and skip this helper:
```ts
helper.accessor("balance", {
cell: (ctx) => (
<Badge variant={ctx.getValue() < 0 ? "destructive" : "default"}>
<FormattedCurrency amount={ctx.getValue()} currency="EUR" />
</Badge>
),
}),
```
## API Reference
### `formattedCell(spec): (ctx) => ReactElement | string`
- `spec`: discriminated union — `{ kind: "number"; options? } | { kind: "currency"; currency; options? } | { kind: "date"; options? } | { kind: "relative-time"; unit?; auto? }`.
Returns a function compatible with TanStack's `ColumnDef.cell`. The function reads `ctx.getValue()` and renders the matching primitive.
## Implementation Notes
- The helper does NOT pin a specific TanStack version — it consumes only `ctx.getValue()` which has been stable across recent TanStack Table releases.
- Numeric coercion uses `Number(raw)`; rows whose backend returns string-encoded numbers (`"1234.5"`) are accepted. Date kind passes `raw` through to `<FormattedDate>` which handles `Date | string | number` already.
- The helper returns React elements (or empty string), not raw strings; tanstack accepts both, so a column with this helper renders as expected without further wrapping.

View File

@@ -0,0 +1,62 @@
# `<FormattedCurrency>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders a monetary `amount` in the given ISO 4217 `currency` through the `currency` formatter plugin. The active locale controls symbol placement, decimal precision, and thousands grouping. Use this primitive anywhere a price, balance, or transaction amount appears in the UI so the formatting stays consistent and locale-correct without per-call `Intl.NumberFormat` boilerplate.
## Features
- Locale-aware symbol/code/name display (`options.display`) and precision controls (`min`/`maxFractionDigits`).
- `as` prop renders any element or component; default `<span>`.
- Forwards extra props (`className`, `aria-label`, `data-*`).
## Usage: Basic Example
```tsx
import { FormattedCurrency } from "@suaveplan/react-ui";
<FormattedCurrency amount={9.5} currency="USD" /> // en-GB → "US$9.50"
```
## Usage: Advanced Example
Render the currency code instead of the symbol, with one decimal:
```tsx
<FormattedCurrency
amount={total}
currency="GBP"
options={{ display: "code", minFractionDigits: 1, maxFractionDigits: 1 }}
as="output"
className="font-mono"
/>
```
Compose inside a card body alongside descriptive text:
```tsx
<Card>
<CardHeader>Order total</CardHeader>
<CardContent>
<FormattedCurrency amount={subtotal + tax} currency="EUR" as="strong" />
</CardContent>
</Card>
```
## API Reference
### `<FormattedCurrency amount currency options? as? {...rest}>`
- `amount: number` — value to render.
- `currency: string` — ISO 4217 currency code.
- `options?: { display?, minFractionDigits?, maxFractionDigits? }`.
- `as?: ElementType` — default `"span"`.
## Implementation Notes
- Routes through `useFormatter("currency", …)`. To swap the underlying `formatCurrency` for a BigDecimal-aware variant, register a custom plugin under id `"currency"` (mount with `disableBuiltInFormatters` and supply both the override and any other plugins).
- The CLDR data shipped with the runtime determines symbol forms. Some locales render `"US$"` rather than `"$"` for USD when disambiguating from local-currency dollars; this is correct.
- `aria-label` should describe what the amount represents (`aria-label="Order total"`) rather than re-reading the formatted number — assistive tech reads the rendered text already.

View File

@@ -0,0 +1,60 @@
# `<FormattedDate>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders a `Date | string | number` value formatted in the active locale through the `date` formatter plugin. Accepts ISO strings, epoch milliseconds, or `Date` instances; invalid inputs render as an empty string and emit a single dev-mode warning rather than throwing — a malformed date in a render tree should not crash the page.
## Features
- Polymorphic input — `Date`, ISO string, epoch ms.
- Polymorphic output — render through any `as` element.
- Defensive invalid-value handling (empty render + dev warn).
- Full pass-through to the underlying `formatDate` helper, including its preset strings (`'short' | 'medium' | 'long' | 'full' | 'date-short' | 'time-short' | …`) and raw `Intl.DateTimeFormatOptions`.
## Usage: Basic Example
```tsx
import { FormattedDate } from "@suaveplan/react-ui";
<FormattedDate value={new Date()} /> // en-GB → "9 May 2026"
```
## Usage: Advanced Example
```tsx
<FormattedDate
value="2026-05-09T13:45:00Z"
options={{ dateStyle: "long", timeStyle: "short" }}
as="time"
dateTime="2026-05-09T13:45:00Z"
/>
```
Render a row's `createdAt` timestamp inside a `<DataTable>` cell (defensive about backend nulls):
```tsx
<TableCell>
{row.createdAt
? <FormattedDate value={row.createdAt} options="medium" />
: "—"}
</TableCell>
```
## API Reference
### `<FormattedDate value options? as? {...rest}>`
- `value: Date | string | number`.
- `options?: Intl.DateTimeFormatOptions | string` — preset name or full options bag.
- `as?: ElementType` — default `"span"`.
Invalid `value` → renders empty string, emits `console.warn` in dev only, never throws.
## Implementation Notes
- Coerces strings/numbers via `new Date(value)` and rejects `NaN` results. The `Date` instance is then passed through `useFormatter("date", { value, options })`.
- Pairs naturally with `<time dateTime={iso}>` for semantic markup; the consumer supplies `dateTime` on the underlying element.
- For relative wording ("3 hours ago"), prefer `<FormattedRelativeTime>`; for absolute date/time, use this primitive.

View File

@@ -0,0 +1,59 @@
# `<FormattedList>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Joins an array of strings with the active locale's list-formatting conventions through the `list` formatter plugin. English produces `"a, b, and c"`; German `"a, b und c"`; Arabic `"a، b، و c"`. Use this primitive whenever a UI surfaces a comma-or-and-joined list (selected filters, grouped tags, comma-separated options) instead of writing the join logic manually — manual joins almost always get RTL spacing and Oxford-comma rules wrong outside the writer's locale.
## Features
- Locale-correct conjunction (`"a, b, and c"`), disjunction (`"a or b"`), and unit (`"3 ft 7 in"`) joins.
- `style` controls width (`"long" | "short" | "narrow"`).
- Polymorphic `as`.
- Empty array renders the empty string (no separator artefacts).
## Usage: Basic Example
```tsx
import { FormattedList } from "@suaveplan/react-ui";
<FormattedList items={["Acme", "Initech", "Globex"]} />
// en-GB → "Acme, Initech, and Globex"
```
## Usage: Advanced Example
```tsx
<FormattedList
items={selectedTagNames}
options={{ type: "disjunction", style: "short" }}
as="span"
aria-label="Active filters"
/>
```
Compose with `<FormattedNumber>` to render a list of numeric values:
```tsx
function PriceList({ amounts, currency }: { amounts: number[]; currency: string }) {
const parts = amounts.map((a) => formatCurrency(a, currency));
return <FormattedList items={parts} />;
}
```
## API Reference
### `<FormattedList items options? as? {...rest}>`
- `items: readonly string[]` — strings to join.
- `options.style?: "long" | "short" | "narrow"` — default `"long"`.
- `options.type?: "conjunction" | "disjunction" | "unit"` — default `"conjunction"`.
- `as?: ElementType` — default `"span"`.
## Implementation Notes
- The plugin caches `Intl.ListFormat` per `(locale, style, type)` so repeated renders with the same style do not re-construct the formatter.
- The component does not format non-string items — pre-format numbers / dates / currencies with the matching primitive and pass the resulting strings as `items`.
- For RTL locales the list separators (Arabic comma `،` and `و`) are inserted automatically by the underlying `Intl.ListFormat`.

View File

@@ -0,0 +1,63 @@
# `<FormattedMessage>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Alias of `<Trans>` from `@suaveplan/react-i18n`, re-exported from `@suaveplan/react-ui` so consumers do not need a second import to render translated strings alongside `<FormattedNumber>`, `<FormattedDate>`, and the rest of the formatted primitives. The branded `TranslationKey<KeyPath>` type contract carries through unchanged; the underlying ICU parsing, rich-text tag interpolation, and post-processor pipeline are identical to `<Trans>`. Use whichever name reads better in context.
## Features
- Alias only — every prop, behavior, and edge case matches `<Trans>` exactly.
- Branded `TranslationKey` typing carries through.
- ICU rich-text tag interpolation via the `components` prop.
- Pluralization via the `count` prop and CLDR rules.
- Registry post-processors (pseudo-localization, redaction) apply automatically.
## Usage: Basic Example
```tsx
import { FormattedMessage } from "@suaveplan/react-ui";
<FormattedMessage i18nKey="greeting" values={{ name: "Alice" }} />
```
## Usage: Advanced Example
Rich-text tag interpolation alongside the other formatted primitives:
```tsx
<FormattedMessage
i18nKey="receipt.summary"
values={{
item: "Coffee",
when: <FormattedDate value={purchasedAt} options="medium" />,
}}
components={{
link: <a href="/receipts" />,
}}
/>
```
Plural-aware translation:
```tsx
<FormattedMessage
i18nKey="inbox.unread"
count={unreadCount}
values={{ count: unreadCount }}
/>
```
## API Reference
### `<FormattedMessage {...transProps}>`
Identical to `<Trans>`. See `@suaveplan/react-i18n`'s `<Trans>` documentation for the full prop reference (`i18nKey`, `values`, `components`, `count`, `namespace`, `componentName`, `children` fallback).
## Implementation Notes
- Implemented as `(props) => <Trans {...props} />`. Tests assert this exact aliasing — when `<Trans>` evolves, `<FormattedMessage>` evolves with it.
- The two names exist so consumers writing `<FormattedNumber>`, `<FormattedDate>`, `<FormattedCurrency>` next to a translated string have a consistently-named primitive (`<FormattedMessage>`). Some teams prefer `<Trans>`; either name resolves to the same component.
- This component does not add a registry-aware indirection. It is a literal alias.

View File

@@ -0,0 +1,58 @@
# `<FormattedNumber>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders a numeric value through the `number` formatter plugin in the active `<RegistryProvider>`. The active locale's `Intl.NumberFormat` produces the displayed string — grouping separators, decimal separators, and numbering system (Latin, Arabic-Indic, Devanagari, …) follow the locale automatically. The component is `as`-polymorphic so consumers can render the value as any HTML element or React component without an extra wrapper.
## Features
- Locale-aware grouping, decimal separators, and numbering system.
- Full `Intl.NumberFormatOptions` pass-through for percent, scientific, currency-like, compact, and unit notation.
- `as` prop accepts any element name or React component, defaulting to `<span>`.
- Forwards arbitrary props on `as` through to the underlying element (`className`, `aria-*`, `data-*`, ref-acceptable when `as` is a forwardRef).
## Usage: Basic Example
```tsx
import { FormattedNumber } from "@suaveplan/react-ui";
<FormattedNumber value={1234567} /> // en-GB → "1,234,567"
```
## Usage: Advanced Example
Compose with theme tokens and accessible labels:
```tsx
<FormattedNumber
value={total}
options={{ style: "percent", minimumFractionDigits: 1 }}
as="strong"
className="text-success-foreground"
aria-label="Conversion rate"
/>
```
Force a specific numbering system regardless of locale (Arabic-Indic):
```tsx
<FormattedNumber value={1234} options={{ numberingSystem: "arab" }} />
```
## API Reference
### `<FormattedNumber value options? as? {...rest}>`
- `value: number` — the value to format.
- `options?: Intl.NumberFormatOptions` — passed through to the formatter plugin.
- `as?: ElementType` — defaults to `"span"`. Accepts HTML element names or React components.
- `...rest` — forwarded to the rendered element.
## Implementation Notes
- Memoized via `useFormatter("number", { value, options })`. The args object is stably stringified for the cache key, so two structurally-equal `options` objects share a cache hit.
- Throws `REACT_I18N.REGISTRY_NOT_PROVIDED` when used outside a `<RegistryProvider>`. Consumers using `<ReactUiProvider>` get the registry automatically.
- The `as` prop is purely structural — it does not change the formatted string. Wrap in `<output>`, `<strong>`, `<data>`, or any custom component as needed.

View File

@@ -0,0 +1,61 @@
# `<FormattedRelativeTime>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders a relative-time phrase ("5 minutes ago", "in 2 days") for a target moment relative to "now", through the `relative-time` formatter plugin. When `auto` is enabled, the component re-renders on a configurable interval so the displayed string ages with the wall clock without consumer intervention. SSR-safe: the first render uses the static initial timestamp captured during render, so server output and client first-paint match — auto-refresh kicks in only after `useEffect` runs on the client.
## Features
- Auto-unit selection (seconds → minutes → hours → days → weeks → months → years) when `unit` is omitted.
- Optional `auto` mode with `refreshIntervalMs` (default `60_000`) and clean unmount teardown.
- Polymorphic `as` — pair with `<time dateTime={iso}>` for semantic markup.
- Accepts `Date | string | number`.
## Usage: Basic Example
```tsx
import { FormattedRelativeTime } from "@suaveplan/react-ui";
<FormattedRelativeTime value={message.createdAt} />
// "5 minutes ago"
```
## Usage: Advanced Example
Auto-refreshing badge inside a Toast description, wrapped in a `<time>` element:
```tsx
<FormattedRelativeTime
value={notification.timestamp}
auto
refreshIntervalMs={30_000}
as="time"
dateTime={new Date(notification.timestamp).toISOString()}
/>
```
Force a coarse unit so a "moments ago" UI does not flicker:
```tsx
<FormattedRelativeTime value={recentlyCreated} unit="minutes" />
```
## API Reference
### `<FormattedRelativeTime value unit? options? auto? refreshIntervalMs? as? {...rest}>`
- `value: Date | string | number`.
- `unit?: "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years"`. Omit for auto.
- `options?: Intl.RelativeTimeFormatOptions`.
- `auto?: boolean` — re-renders on `refreshIntervalMs`.
- `refreshIntervalMs?: number` — default `60_000`.
- `as?: ElementType` — default `"span"`.
## Implementation Notes
- The interval timer is cleared on unmount.
- The initial `now()` snapshot is captured in `useState` lazy init so SSR + client first paint produce identical output. The interval is only started in `useEffect`, after hydration.
- For long-lived "ago" stamps inside long lists (e.g. 1000-row activity logs), prefer NOT enabling `auto` per row — auto on every row schedules 1000 timers. Either enable on a small visible window (virtualized list view) or render once at fetch time.

View File

@@ -0,0 +1,66 @@
# `<Plural>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Selects one of the supplied children based on the CLDR plural rule for `value` against the active locale, through the `plural` formatter plugin. English collapses zero into `other`; Arabic distinguishes `zero`/`one`/`two`/`few`/`many`/`other`; Russian and Polish have language-specific buckets. The `other` slot is REQUIRED — every locale's CLDR ruleset has at least an `other` category, and TypeScript enforces that the consumer supplies it.
## Features
- TypeScript-enforced `other` slot.
- Optional `zero`, `one`, `two`, `few`, `many` slots — fall through to `other` when the active locale doesn't use the category OR the consumer didn't supply it.
- Children are arbitrary React nodes — strings, fragments, or composed primitives.
## Usage: Basic Example
```tsx
import { Plural, FormattedNumber } from "@suaveplan/react-ui";
<Plural
value={count}
one={<><FormattedNumber value={count} /> item</>}
other={<><FormattedNumber value={count} /> items</>}
/>
```
## Usage: Advanced Example
A multi-category switch that renders correctly across English, Russian, and Arabic:
```tsx
<Plural
value={n}
one={<>{n} файл</>}
few={<>{n} файла</>}
many={<>{n} файлов</>}
other={<>{n} файла</>}
/>
```
Combining with `<FormattedMessage>` for a fully translated phrase:
```tsx
<Plural
value={unread}
zero={<FormattedMessage i18nKey="inbox.empty" />}
one={<FormattedMessage i18nKey="inbox.one" values={{ count: unread }} />}
other={<FormattedMessage i18nKey="inbox.other" values={{ count: unread }} />}
/>
```
## API Reference
### `<Plural value other zero? one? two? few? many?>`
- `value: number` — selector.
- `other: ReactNode` — required.
- `zero | one | two | few | many?: ReactNode` — optional category slots.
Returns the React node for the selected category, falling back to `other` if a more specific category was not supplied.
## Implementation Notes
- The CLDR ruleset comes from `@suaveplan/i18n-icu-parser`'s `PluralizationEngine`. New locales pick up their categories automatically through that engine; this component does not maintain its own plural-rule table.
- For ICU `{count, plural, …}` messages stored in the catalog, prefer `<FormattedMessage>` with a `count` prop; this component is for inline composition where the categories' rendered children are React node compositions rather than translation keys.

View File

@@ -0,0 +1,67 @@
# `<ReactUiI18nBridge>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<ReactUiI18nBridge>` is the single mount point that propagates the active locale and direction from `@suaveplan/react-i18n` into every third-party context the rest of `react-ui` consumes. Without it, locale + direction state lives in N independent providers (Radix, vaul, sonner, react-day-picker) and drifts independently — every component would need its own wiring. The bridge centralises propagation: one mount, locale-aware behaviour everywhere downstream.
## Features
- Mounts `@radix-ui/react-direction`'s `<DirectionProvider>` so every Radix-derived primitive (popover, dropdown-menu, dialog, tooltip, select, menubar, context-menu, hover-card, scroll-area, tabs, toggle-group, …) inherits RTL state without per-component wiring.
- Exposes a `ReactUiLocaleContext` via `useReactUiLocale()` for `<DataTable>`, `<Calendar>`, and any other component wanting a low-overhead locale read.
- Optionally syncs `document.documentElement.lang` and `document.documentElement.dir` on locale change (default on; opt-out via `syncDocumentAttributes={false}` for SSR / nested-app scenarios).
- Default `useReactUiLocale()` value (`{ locale: "en-GB", direction: "ltr" }`) makes the hook safe to call unconditionally outside the bridge.
## Usage: Basic Example
```tsx
import { TranslationProvider } from "@suaveplan/react-i18n";
import { ReactUiI18nBridge } from "@suaveplan/react-ui";
<TranslationProvider config={i18nConfig}>
<ReactUiI18nBridge>
<App />
</ReactUiI18nBridge>
</TranslationProvider>
```
## Usage: Advanced Example
Read locale from inside any descendant without re-subscribing to the i18n bus:
```tsx
import { useReactUiLocale } from "@suaveplan/react-ui";
function MyTable() {
const { locale, direction } = useReactUiLocale();
return <DataTable locale={locale} dir={direction} … />;
}
```
Disable document attribute syncing for an embedded micro-frontend that does not own `<html>`:
```tsx
<ReactUiI18nBridge syncDocumentAttributes={false}>
<EmbeddedApp />
</ReactUiI18nBridge>
```
## API Reference
### `<ReactUiI18nBridge children syncDocumentAttributes?>`
- `children: ReactNode` — required.
- `syncDocumentAttributes?: boolean` — defaults to `true`. Set `false` for SSR / embedded scenarios.
### `useReactUiLocale(): ReactUiLocaleContextValue`
Returns `{ locale: string; direction: "ltr" | "rtl" }`. Defaults to `{ locale: "en-GB", direction: "ltr" }` outside the bridge.
## Implementation Notes
- The bridge does NOT wrap a sonner `<Toaster>` directly — that primitive is a sibling that consumers mount once at the app root. Sonner reads `dir` from the closest `<html dir>` attribute, which the bridge syncs by default.
- Two `<ReactUiI18nBridge>` instances in the same tree get two independent `ReactUiLocaleContext` values; React's closest-ancestor resolution wins. This is intentional for nested apps with different locale scopes.
- The document attribute sync runs in `useEffect` after the bridge mounts; SSR output is unaffected by this branch. The first client paint may show a one-frame mismatch if the SSR'd `<html lang>` differs from the active locale, but the bridge corrects it on first effect run.
- The bridge must be mounted INSIDE `<TranslationProvider>` because it calls `useLocale()` and `useDirection()` from `react-i18n`. Mounting outside throws.

View File

@@ -0,0 +1,87 @@
# `StoriesShell`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Shared provider-stack helper for the `react-ui-i18n` surface stories. The 11 i18n primitive story files each need a `<TranslationProvider>` + `<ThemeProvider>` + `<RegistryProvider>` + `<ReactUiI18nBridge>` wrap so the rendered surface matches production-runtime composition. Centralising the wrap in one helper keeps the per-primitive stories focused on what is actually being demonstrated and avoids drift across the 11 files when the provider stack evolves.
## Features
- One `<StoriesShell locale>` wrap mounts the full chassis i18n stack.
- Inlines `createI18n` from `@suaveplan/i18n/plugin` so each story gets a fresh instance per render.
- `syncDocumentAttributes={false}` on the bridge so story renders never mutate the host document — important when the storybook iframe and the parent page share a `<html>` root.
- Re-exports `StoryMeta` + `StoryObj` interface shapes so the per-primitive files do not redeclare them.
- Re-exports `STORY_LOCALES` for primitives that demonstrate locale variants in a single story.
## Usage: Basic Example
```tsx
import { StoriesShell, type StoryMeta, type StoryObj } from "../stories-shell/stories-shell.js";
import { FormattedNumber } from "./formatted-number.js";
const meta: StoryMeta<typeof FormattedNumber> = {
component: FormattedNumber,
title: "react-ui/i18n/FormattedNumber",
tags: ["autodocs"],
};
export default meta;
export const Default: StoryObj<{ value: number }> = {
render: () => (
<StoriesShell locale="en-GB">
<FormattedNumber value={1234567} />
</StoriesShell>
),
};
```
## Usage: Advanced Example
A multi-locale variants story walks `STORY_LOCALES`:
```tsx
import { STORY_LOCALES, StoriesShell } from "../stories-shell/stories-shell.js";
export const WithI18n: StoryObj<unknown> = {
render: () => (
<div className="flex flex-col gap-2">
{STORY_LOCALES.map((locale) => (
<div
key={locale}
dir={locale === "ar-AE" ? "rtl" : "ltr"}
className="flex items-center gap-2"
>
<span style={{ width: 80 }}>{locale}</span>
<StoriesShell locale={locale}>
<FormattedNumber value={1234567} />
</StoriesShell>
</div>
))}
</div>
),
};
```
## API Reference
### `<StoriesShell locale children>`
- `locale: SupportedLocale` — initial locale for the story tree.
- `children: ReactNode` — required.
### `STORY_LOCALES: readonly SupportedLocale[]`
Five canonical bundled locales (`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`).
### `StoryMeta<T>`, `StoryObj<TArgs>`
Inlined Storybook-compatible meta / story shapes — `@storybook/react` is intentionally NOT a dep.
## Implementation Notes
- The shell creates a fresh `I18nInstance` per render. Stories are demos; the cost of a per-render instance is acceptable and avoids stale-state bleed across hot-reloads.
- `syncDocumentAttributes={false}` is non-negotiable — turning it on inside Storybook would mutate the parent page's `<html lang>` based on whichever story rendered last, breaking the surrounding chrome.
- `lightTheme` is hardcoded; primitives that need a different theme can wrap with their own `<ThemeProvider>` inside `<StoriesShell>` (the inner provider wins).
- The shell does NOT mount any user-supplied registry plugins. Stories that need a registered plugin (e.g. a `date-fns` adapter for `<Calendar>`) extend the shell with a custom wrapper.

View File

@@ -0,0 +1,75 @@
# `tanstack-bridge`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Locale-aware `sortingFn` and `filterFn` factories for `@tanstack/react-table` columns, backed by `Intl.Collator`. Manual `a.localeCompare(b)` calls on column data do not pick up the active runtime locale unless explicitly threaded through; these factories take a locale and return functions that respect that locale's collation rules — Swedish places `ä` after `z`, German default folds `ß` to `ss`, Turkish dotted/dotless `i` are distinct. Use these for any TanStack Table whose column values are strings users see in the active language.
## Features
- `localeAwareSortingFn(locale)` — collator-backed comparator suitable as a TanStack `SortingFn`. Variant sensitivity (case + diacritics distinguished). Undefined / null values sort first.
- `localeAwareFilterFn(locale)` — case + diacritic insensitive substring match (`sensitivity: "base"`, `usage: "search"`). Empty filter matches every row.
- Per-locale `Intl.Collator` cache — one collator per `(locale)` for sort, one for filter; cleared via `clearTanstackBridgeCaches()` in tests.
- Structural typing — does not pin a TanStack version.
## Usage: Basic Example
```tsx
import { localeAwareSortingFn, localeAwareFilterFn } from "@suaveplan/react-ui";
const columns: ColumnDef<Customer>[] = [
{
accessorKey: "name",
sortingFn: localeAwareSortingFn(locale),
filterFn: localeAwareFilterFn(locale),
},
];
```
## Usage: Advanced Example
Wire the locale once per render via `useReactUiLocale` and reuse across every string column:
```tsx
import {
localeAwareFilterFn,
localeAwareSortingFn,
useReactUiLocale,
} from "@suaveplan/react-ui";
function CustomerTable() {
const { locale } = useReactUiLocale();
const sort = useMemo(() => localeAwareSortingFn(locale), [locale]);
const filter = useMemo(() => localeAwareFilterFn(locale), [locale]);
const columns = useMemo<ColumnDef<Customer>[]>(
() => [
{ accessorKey: "name", sortingFn: sort, filterFn: filter },
{ accessorKey: "company", sortingFn: sort, filterFn: filter },
],
[sort, filter]
);
return <DataTable columns={columns} data={rows} />;
}
```
## API Reference
### `localeAwareSortingFn(locale: string)`
Returns `(rowA, rowB, columnId) => number`. Compares column values via a cached `Intl.Collator(locale, { sensitivity: "variant" })`. Equal values return `0`; undefined / null values return `-1` / `1` so empty rows cluster.
### `localeAwareFilterFn(locale: string)`
Returns `(row, columnId, filterValue) => boolean`. Performs a substring match through a cached `Intl.Collator(locale, { sensitivity: "base", usage: "search" })`. Empty / null filter matches every row.
### `clearTanstackBridgeCaches()`
Test helper; releases the per-locale collator caches.
## Implementation Notes
- The filter implementation is an O(n*m) sliding window comparing every needle-length substring via the collator. This is fine for in-memory tables of a few thousand rows; for 10k+ rows or noisy keystroke inputs, prefer a server-side filter or debounce the input.
- The factories return closures — calling twice with the same locale returns two different function references. To avoid TanStack's column-config invalidation, memoize the returned function with `useMemo` keyed on locale.
- `localeAwareSortingFn` does NOT respect the table's per-column sort direction toggle — TanStack flips the sign of the comparator's return value when the user clicks "sort descending". Our function returns the natural ascending comparator and TanStack handles the rest.

View File

@@ -0,0 +1,87 @@
# `useValidationMessageBridge`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Centralised resolver factory turning a `SuaveplanError` (or any thrown value) into a locale-resolved user-facing message. Mirrors the resolution rules of `useErrorMessage` from `@suaveplan/react-i18n` but returns a function — so a single hook call gives you a resolver you can apply to many errors per render: a list of validation errors on a form, a batch of toasts, a fan-out of notifications, or a status panel summarising recent failures. Every UI surface in `react-ui` (Toast, Alert, Dialog, ConfirmDeleteDialog) that needs to render an error should resolve through this bridge so the catalog-lookup behaviour stays consistent across the chassis.
## Features
- One hook call yields a resolver suitable for many errors.
- Walks the `cause` chain to surface the deepest `SuaveplanError`.
- Catalog lookup (`errors:<code>`) on hit; falls back to `error.message` on miss.
- Coerces non-`Error` inputs (strings, undefined) without throwing.
- Stable function reference across renders (memoized by `useCallback`).
## Usage: Basic Example
```tsx
import { useValidationMessageBridge } from "@suaveplan/react-ui";
function FormErrors({ errors }: { errors: SuaveplanError[] }) {
const resolve = useValidationMessageBridge();
return (
<ul>
{errors.map((e, i) => <li key={i}>{resolve(e)}</li>)}
</ul>
);
}
```
## Usage: Advanced Example
Render a batch of validation errors inside an Alert with the bridge resolver:
```tsx
function ValidationSummary({ errors }: { errors: unknown[] }) {
const resolve = useValidationMessageBridge();
return (
<Alert variant="destructive">
<AlertTitle>Please fix the following</AlertTitle>
<AlertDescription>
<ul>
{errors.map((err, i) => (
<li key={i}>{resolve(err)}</li>
))}
</ul>
</AlertDescription>
</Alert>
);
}
```
A try/catch handler that surfaces a toast with the resolved message:
```tsx
function useSubmit() {
const resolve = useValidationMessageBridge();
const { toast } = useToast();
return useCallback(async (input: FormValues) => {
try {
await save(input);
} catch (err) {
toast({ variant: "destructive", description: resolve(err) });
}
}, [resolve, toast]);
}
```
## API Reference
### `useValidationMessageBridge(): ValidationMessageResolver`
Returns `(error: unknown) => string`. Resolution order:
1. `null` / `undefined` → `""`.
2. Walk cause chain → first `SuaveplanError` wins. Look up `errors:<code>`; on hit return the translation, on miss return the surfaced error's `.message`.
3. Plain `Error` → `error.message`.
4. `string` → itself.
5. Anything else → `""`.
## Implementation Notes
- The hook reads the i18n instance via `useI18n()` and memoizes the resolver with `useCallback([i18n])` so the function reference is stable as long as the i18n instance is. Languages-changed events bump i18n's snapshot counter; the bridge's resolver re-binds when that propagates so subsequent calls see the new locale's catalog.
- Cause-chain walking is bounded at depth 10 to prevent runaway loops on accidentally-cyclic error structures.
- The bridge does NOT perform any side effects (no logging, no telemetry). For metric emission on errors, wrap the call site or compose with `@suaveplan/logger`.