first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# `<InputNumber>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Locale-aware numeric input. Parses incoming text using the active locale's decimal and grouping separators, accepts locale-specific digit glyphs (`٣٫١٤` in Arabic-Indic), and emits a canonical `number | null` through `onValueChange`. Switches to a raw editable buffer on focus to avoid cursor jumps caused by re-formatting mid-keystroke; renders the locale-formatted value on blur. The plain `<Input>` is unaffected — `<InputNumber>` is a sibling primitive consumers opt into per call site, so existing form code is not disturbed.
## Features
- Per-locale separator detection via `Intl.NumberFormat.formatToParts(12345.6)` — handles en-GB `1,234.5`, de-DE `1.234,5`, fr-FR `1 234,5`, ar-AE Arabic-Indic glyphs.
- Digit-folding for non-Latin numbering systems — Eastern Arabic, Devanagari, Bengali, etc.
- Focus / blur buffer split — raw Latin during edit, locale formatted at rest.
- Empty input emits `null` (not `0` or `NaN`).
- Invalid input on blur reverts to the previously valid formatted value rather than corrupting state.
- Controlled OR uncontrolled — pass `value` (controlled) or `defaultValue` (uncontrolled).
- `displayOptions: Intl.NumberFormatOptions` lets the rest-state format render percent, currency, scientific, etc.
## Usage: Basic Example
```tsx
import { InputNumber } from "@suaveplan/react-ui";
const [amount, setAmount] = useState<number | null>(null);
<InputNumber value={amount} onValueChange={setAmount} />
```
## Usage: Advanced Example
Currency-style display (rest-state shows `1,234.50` while focused buffer shows `1234.5`):
```tsx
<InputNumber
value={price}
onValueChange={setPrice}
displayOptions={{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}}
inputMode="decimal"
aria-label="Unit price"
/>
```
Bind directly to a form library's controlled value, with locale override for a multi-currency entry form:
```tsx
<InputNumber
locale="de-DE"
value={field.value}
onValueChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
```
## API Reference
### `<InputNumber value? defaultValue? onValueChange? locale? displayOptions? {...rest}>`
- `value?: number | null` — controlled. `null` renders empty.
- `defaultValue?: number | null` — uncontrolled initial.
- `onValueChange?: (value: number | null) => void` — fires on blur.
- `locale?: string` — defaults to `useReactUiLocale().locale`.
- `displayOptions?: Intl.NumberFormatOptions`.
- All other `<input>` HTML attributes are forwarded EXCEPT `value`, `defaultValue`, `onChange`, `type` (the component owns those).
### `parseLocaleNumber(text, locale): number | null`
Standalone utility for parsing a locale-formatted numeric string. Used internally by the component; exported for consumers needing the same parse logic outside the component.
## Implementation Notes
- The `Intl.NumberFormat` instance is cached per `(locale, displayOptions)` via `useMemo`. The separator probe (`formatToParts(12345.6)`) is also cached per locale at module level.
- `onValueChange` fires only on blur to avoid emitting `null` for every transient unparseable keystroke. Forms that need keystroke-level validation can subscribe to `onChange` on the underlying `<input>` directly via the spread (we do not own `onChange` once the user has supplied one).
- The component uses `type="text"` with `inputMode="decimal"` rather than `type="number"`. `type="number"` rejects locale-specific separators in Chrome and triggers spinner UI consumers usually do not want.
- When a parent re-renders with an externally-changed `value` while the input is NOT focused, the displayed value re-formats. While focused, the focused-buffer text wins so the user is not interrupted mid-edit.

View File

@@ -0,0 +1,117 @@
# Input
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [input-number](./input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:auto-docs --> _(no submodu…
- [input.variants](./input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-number](./input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./input.md) — <!-- begin:auto-docs --> _(no submodules)_- [input.variants](./input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-number](./input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:au…
- [input.variants](./input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-number](./input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:auto-docs --> _(no submod…
- [input.variants](./input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-number](./input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./input.md) — <!-- begin:auto-docs --> _(no submodules)_- [input.variants](./input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
## Purpose
`<Input>` is the chassis primitive form input for `@suaveplan/react-ui`. It renders a native `<input>` element with theme-driven variant resolution, forwards refs to the underlying DOM node, and threads its `placeholder` through the active i18n instance so consumers can never accidentally ship a hardcoded English placeholder. The component is intentionally thin — the shadcn upstream Input recipe is structurally simple and the chassis follows suit, exposing a single `default` variant + `default` size at the cva level so themes carry the visual variation rather than forcing the component to enumerate it.
`<Input>` participates in the same `data-*` attribute convention as the rest of `@suaveplan/react-ui`: `data-invalid="true"` and `data-disabled="true"` are written when the corresponding props are set, so theme overrides can target error state via a single CSS selector without depending on `aria-invalid` (which the consumer may or may not have set independently).
## Features
- Forwards refs to the underlying `<input>` so consumers can focus, select, measure, or animate it directly.
- All native `InputHTMLAttributes` are forwarded as-is (`onChange`, `value`, `defaultValue`, `name`, `autoComplete`, `inputMode`, `pattern`, etc.).
- `placeholder` typed as `TranslationKey | undefined` — the chassis enforces that the placeholder text is a registered translation key, never a raw English literal. Passing `undefined` skips the attribute entirely.
- `invalid` boolean prop drives `data-invalid="true"` on the rendered element so the cva config's `data-[invalid=true]:border-destructive` rule activates.
- `disabled` mirrors as `data-disabled="true"` for the parity with shadcn's selector convention.
- Theme variant overrides via `useThemedVariants("Input", inputVariants)`. A `ThemePlugin.variantOverrides.Input` may add brand-new size or appearance variants (see the type-safety trade-off in `proposal.md` §"Type-safety trade-off").
- `motion-reduce:transition-none` collapses focus/border transitions for users with `prefers-reduced-motion: reduce`.
- Focus styling via `focus-visible:ring-2 focus-visible:ring-ring` — keyboard users get a visible ring; pointer focus does not.
- File-input styling via `file:border-0 file:bg-transparent file:text-sm file:font-medium` so `<Input type="file">` inherits a sensible button look.
## Usage: Basic Example
```tsx
import { Input } from "@suaveplan/react-ui";
export function NameField({
onChange,
}: {
onChange: (next: string) => void;
}) {
return (
<Input
name="full-name"
autoComplete="name"
onChange={(event) => onChange(event.currentTarget.value)}
/>
);
}
```
## Usage: Advanced Example
```tsx
import { Input } from "@suaveplan/react-ui";
export function EmailField({
value,
error,
onChange,
}: {
value: string;
error: string | undefined;
onChange: (next: string) => void;
}) {
return (
<Input
type="email"
name="email"
autoComplete="email"
inputMode="email"
placeholder="react-ui.empty.noResults"
value={value}
invalid={error !== undefined}
aria-invalid={error !== undefined}
aria-describedby={error !== undefined ? "email-error" : undefined}
onChange={(event) => onChange(event.currentTarget.value)}
/>
);
}
```
## API Reference
### `<Input>`
`forwardRef<HTMLInputElement, InputProps>` — renders a `<input type={type}>` (default `"text"`).
#### `InputProps`
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `"default"` | `"default"` | Visual variant. The chassis ships only `default`; themes may extend. |
| `size` | `"default"` | `"default"` | Sizing variant. The chassis ships only `default`; themes may extend. |
| `placeholder` | `TranslationKey \| undefined` | `undefined` | Translation key resolved against the active i18n instance. Hardcoded literals are rejected at compile time. |
| `invalid` | `boolean` | `undefined` | When `true`, writes `data-invalid="true"` on the element so the cva error rules activate. |
| `disabled` | `boolean` | `undefined` | Forwarded to the native `<input>` AND mirrored as `data-disabled="true"`. |
| `type` | _native_ | `"text"` | Native input type. |
| _all native_ `InputHTMLAttributes<HTMLInputElement>` | _native_ | _native_ | Forwarded as-is. |
#### `inputVariants`
The bare cva resolver. Exported so external consumers (`@suaveplan/react-ui-theme-*`, ejected source under `npx shadcn@latest add @suaveplan/input`) can re-use the variants directly without going through `useThemedVariants`.
#### Errors
- `useTranslation()` is called unconditionally inside the component. When the surrounding tree lacks a `<TranslationProvider>` (or `<ReactUiProvider>`), the hook returns its identity-fallback shape — `t(key)` returns the key as the literal. This matches the `react-i18n` "no-throw in production" contract; the component renders, but the rendered placeholder is the raw key.
## Implementation Notes
- The variants module is intentionally separate (`input.variants.ts`) so an ejected source copy keeps the cva config in a dedicated file, mirroring the upstream shadcn convention.
- `useThemedVariants("Input", inputVariants)` is called unconditionally so the hook order is stable across renders. The hook's memoisation key is `(themeName, "Input")` so a re-render with the same theme returns the same resolver reference.
- `placeholder` is omitted entirely when `undefined`, NOT set to the empty string. An empty placeholder attribute would still allocate the placeholder pseudo-element in the user-agent stylesheet and could collide with screen-reader heuristics.
- `data-invalid` and `data-disabled` are written when `true`, omitted entirely when `false` or `undefined`. The CVA selector convention is `data-[invalid=true]:…` so an explicit `data-invalid="false"` would not match, but it would clutter the rendered HTML.
- The chassis does NOT inject any `aria-invalid` attribute. Consumers who want `aria-invalid="true"` MUST pass it themselves alongside `invalid`. This separation lets the component represent purely visual error state (e.g. "the user has not yet attempted to submit, but we want to indicate the field is required").
- `<Input type="file">` inherits the file-input styling rules from the cva base; consumers do not need to layer additional classes for the upload button.

View File

@@ -0,0 +1,52 @@
# Input Variants (cva config)
## Purpose
`inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split from `input.tsx` into its own module so an ejected source copy under `npx shadcn@latest add @suaveplan/input` keeps the cva configuration in a dedicated file — exactly mirroring the upstream shadcn convention. Theme overrides flow through `useThemedVariants("Input", inputVariants)`, so the runtime cva resolver can be replaced or extended by an active `ThemePlugin` without touching the component source.
The module exports a single `inputVariants` callable resolver (`(props?) => string`) tagged with the component name `"Input"` via `defineVariants`. Consumers that want the bare resolver — for example to render the same variants on a non-`<input>` element — import `inputVariants` directly and apply the result via `cn()` themselves.
## Features
- Single `default` cva variant + single `default` size — the shadcn upstream Input recipe is structurally simple, and the chassis follows suit so themes carry the visual variation rather than forcing the component to enumerate it.
- `data-[invalid=true]:border-destructive` + `data-[invalid=true]:focus-visible:ring-destructive` so the error state styling activates when the component writes `data-invalid="true"`.
- `placeholder:text-muted-foreground` so the placeholder colour comes from the active theme's muted-foreground token.
- `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` for keyboard-only focus styling.
- `disabled:cursor-not-allowed disabled:opacity-50` for the native `disabled` state.
- `file:border-0 file:bg-transparent file:text-sm file:font-medium` so `<Input type="file">` inherits a sensible button look without extra classes.
- `motion-reduce:transition-none` so reduced-motion users see no border / focus transitions.
## Usage: Basic Example
```ts
import { inputVariants } from "@suaveplan/react-ui";
const className = inputVariants({ variant: "default", size: "default" });
```
## Usage: Advanced Example
```ts
import { inputVariants, cn } from "@suaveplan/react-ui";
export function CustomTextarea(props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {
return (
<textarea
{...props}
className={cn(inputVariants({}), "h-32 resize-y", props.className)}
/>
);
}
```
## API Reference
### `inputVariants(props?: VariantProps<typeof inputVariants>): string`
Returns the cva-merged class string for the supplied variant + size combination. Both fields are optional; omitted fields fall back to the cva `defaultVariants` (`variant: "default"`, `size: "default"`).
## Implementation Notes
- The resolver is tagged with `componentName: "Input"` (non-enumerable, non-writable) via `defineVariants` so debug tooling can identify the cva instance at runtime.
- The single-variant / single-size shape is deliberate — sizing variation typically belongs to a wrapping `<FieldGroup>` / `<InputGroup>` (Wave 3b) rather than the input itself.
- Tokens reference theme-driven CSS custom properties (`border-input`, `bg-transparent`, `text-muted-foreground`, `border-destructive`) — the actual colour values come from the active `ThemePlugin.cssVariables` payload.