first commit
This commit is contained in:
@@ -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.
|
||||
117
docs/ui/react/react-ui/src/components/input/input.md
Normal file
117
docs/ui/react/react-ui/src/components/input/input.md
Normal 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user