You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
5.3 KiB
136 lines
5.3 KiB
|
5 months ago
|
# `<PinInput>` — eidos
|
||
|
|
|
||
|
|
Multi-cell OTP / verification code input. A transparent
|
||
|
|
absolutely-positioned `<input>` captures every keystroke and paste;
|
||
|
|
visible bordered cells follow the hidden input's caret. Field-aware
|
||
|
|
(disabled / readonly / required / invalid OR-merge with the enclosing
|
||
|
|
Field.Provider).
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
```svelte
|
||
|
|
<script lang="ts">
|
||
|
|
import { PinInput } from '$uix/eidos/components/pin-input';
|
||
|
|
let code = $state('');
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<PinInput
|
||
|
|
bind:value={code}
|
||
|
|
length={6}
|
||
|
|
type="numeric"
|
||
|
|
size="md"
|
||
|
|
onComplete={(value) => console.log('filled:', value)}
|
||
|
|
>
|
||
|
|
{#snippet children({ cells })}
|
||
|
|
{#each cells as cell, i}
|
||
|
|
<PinInput.Cell {cell} index={i} />
|
||
|
|
{/each}
|
||
|
|
{/snippet}
|
||
|
|
</PinInput>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Parts
|
||
|
|
|
||
|
|
| Part | Notes |
|
||
|
|
|---|---|
|
||
|
|
| `PinInput` (root) | Provider; container + hidden capture input. Exposes `{ cells, isFocused, isHovering, value }` via `children` snippet. |
|
||
|
|
| `Cell` | One visible cell per character position. Receives `cell` (`PinInputCellData`) and `index` from the snippet. |
|
||
|
|
|
||
|
|
## Eidos-layer props
|
||
|
|
|
||
|
|
| Prop | Type | Default | Notes |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` (responsive) | `'md'` | Cell dimensions + glyph size. |
|
||
|
|
|
||
|
|
## Headless props (forwarded to soma)
|
||
|
|
|
||
|
|
`value`, `onValueChange`, `onComplete`, `length`, `type`, `pattern`,
|
||
|
|
`pasteTransformer`, `mask`, `placeholder`, `disabled`, `readonly`,
|
||
|
|
`required`, `invalid`, `name`, `autocomplete`, `inputmode`,
|
||
|
|
`aria-label`. See `src/uix/soma/components/pin-input/types.ts`.
|
||
|
|
|
||
|
|
## Cell data-* (emitted by morfo)
|
||
|
|
|
||
|
|
| Attr | When |
|
||
|
|
|---|---|
|
||
|
|
| `data-active` | The cell holds the caret (focused + about to be typed in). |
|
||
|
|
| `data-filled` | The cell has a character. |
|
||
|
|
| `data-disabled` | Cascaded from the provider's disabled state. |
|
||
|
|
|
||
|
|
## Passive justification
|
||
|
|
|
||
|
|
Passive **at the morfo level** — pin-input's morfo declares parts
|
||
|
|
(Provider, Cell, Input) but no `events` array. The user-perceived
|
||
|
|
interactions (typing, paste, completion) are mediated by the hidden
|
||
|
|
`<input>`'s native events; sema-side perceptual feedback piggy-backs
|
||
|
|
on the `commit` family base when the consumer hooks `onComplete`.
|
||
|
|
|
||
|
|
The component IS interactive from the user's perspective. "Passive"
|
||
|
|
here is a contract-layer classification, not a UX one — same logic
|
||
|
|
as `<AlertDialog>` (delegates events to the underlying primitive).
|
||
|
|
|
||
|
|
## Baseline
|
||
|
|
|
||
|
|
WAI-ARIA Textbox pattern:
|
||
|
|
<https://www.w3.org/WAI/ARIA/apg/patterns/textbox/>
|
||
|
|
|
||
|
|
The OTP-input UX has no formal ARIA pattern. Soma follows the
|
||
|
|
de-facto behaviour established by Radix-style implementations and
|
||
|
|
the iOS / Android native code-input controls:
|
||
|
|
|
||
|
|
- Single hidden `<input>` (not one input per cell) so paste, IME and
|
||
|
|
autofill (`one-time-code`) work naturally.
|
||
|
|
- Paste distributes across cells via the morfo's
|
||
|
|
`pasteTransformer` hook.
|
||
|
|
- Backspace clears the previous cell when the current one is empty.
|
||
|
|
- Numeric `type` sets `inputmode='numeric'` + `autocomplete='one-time-code'`
|
||
|
|
so iOS / Android offer the SMS-autofill banner.
|
||
|
|
|
||
|
|
## Comparativa
|
||
|
|
|
||
|
|
| Lib | Single hidden input | Mask | Paste distribute | Type presets | Custom `pattern` | Snippet cells |
|
||
|
|
|---|---|---|---|---|---|---|
|
||
|
|
| **Radix Primitives** | n/a (not provided) | — | — | — | — | — |
|
||
|
|
| **Bits UI** | ✓ | ✓ | ✓ | numeric/alphanumeric/alphabetic | ✓ | ✓ |
|
||
|
|
| **Ark UI** | input per cell | ✓ | ✓ | numeric/alphanumeric | — | — |
|
||
|
|
| **Chakra v3** | input per cell | ✓ | ✓ | numeric/alphanumeric | — | — |
|
||
|
|
| **shadcn/ui (`input-otp`)** | ✓ | ✓ | ✓ | numeric (default) | ✓ | ✓ |
|
||
|
|
| **Eidos (this)** | ✓ | ✓ | ✓ | numeric / alphanumeric / alphabetic / custom | ✓ | ✓ |
|
||
|
|
|
||
|
|
Aligned with the shadcn / Bits family (single hidden input). Ark and
|
||
|
|
Chakra use one input per cell which works but loses paste / IME
|
||
|
|
quality.
|
||
|
|
|
||
|
|
## Decisiones
|
||
|
|
|
||
|
|
- **Snippet-driven Cell rendering.** Forces the consumer to iterate
|
||
|
|
`cells` and render `<PinInput.Cell />` per index. Auto-rendering
|
||
|
|
would hide the per-index `index` arg which downstream consumers
|
||
|
|
use for analytics / focus targeting.
|
||
|
|
- **Cell consumes `data-active` for the focus ring**, not a literal
|
||
|
|
`<input:focus>`. Only the hidden input is focused; the visible
|
||
|
|
cells receive a synthetic focus ring via `data-active` to match
|
||
|
|
the caret position.
|
||
|
|
- **No `variant` knob.** Pin inputs are a singular visual pattern
|
||
|
|
(bordered boxes); variants like outline-only / soft / ghost noise
|
||
|
|
the contract. Consumers can recolor cells via `data-color` if
|
||
|
|
needed.
|
||
|
|
- **Size as t-shirt** (`sm` / `md` / `lg` / `xl`). Matches the rest
|
||
|
|
of UIX's input scale.
|
||
|
|
|
||
|
|
## Gaps
|
||
|
|
|
||
|
|
| Gap | Disposición | Detalle |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| Group separator (e.g. `123-456`) | **diferir** | Soma's `pasteTransformer` strips dashes; rendering a visible separator between groups (Bits' `<PinInput.Group>` part) isn't in the morfo. Add when a real consumer needs it. |
|
||
|
|
| Animated caret transition between cells | **diferir** | Soma emits `data-active` instantly; a transitioned bar could be a nice polish. |
|
||
|
|
| `data-color` cascade | **diferir** | Coloured cells (success on complete, danger on invalid) would propagate from the root's `data-color`. Add when the consumer pattern stabilises. |
|
||
|
|
|
||
|
|
## Reference
|
||
|
|
|
||
|
|
- Bits UI: <https://bits-ui.com/docs/components/pin-input>
|
||
|
|
- Ark UI: <https://ark-ui.com/docs/components/pin-input>
|
||
|
|
- Chakra v3: <https://chakra-ui.com/docs/components/pin-input>
|
||
|
|
- shadcn/ui (`input-otp`):
|
||
|
|
<https://ui.shadcn.com/docs/components/input-otp>
|