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.
svelte-kit-vice/src/uix/eidos/components/pin-input/README.md

136 lines
5.3 KiB

feat(pin-input): port from soma to eidos (Tier 1 sprint, 5/5 — DONE) Multi-cell OTP / verification code input. Two parts (Provider, Cell) with a snippet-driven cell iteration. Architecture: - A transparent absolutely-positioned <input> captures every keystroke, paste and IME event (soma). - Visible bordered cells follow the hidden input's caret via data-active / data-filled emitted by the morfo. - Single-input approach (shadcn / Bits family) — preserves paste distribution, autocomplete='one-time-code' for SMS autofill on iOS/Android, and IME quality. Eidos surface: - size: 'sm' | 'md' | 'lg' | 'xl' (responsive), cascades cell dimension + glyph size tokens to all cells - Recipe paints square bordered cells with focus ring tracking data-active, subtle bg lift on data-filled, threat-toned border when [data-invalid] cascades from the provider Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - apg URL added (Textbox pattern — OTP has no formal ARIA pattern) - Passive at the morfo layer (no events array; perceptual feedback via the commit family base when onComplete fires) - README documents the passive classification Comparativa: aligned with shadcn / Bits UI (single hidden input). Ark and Chakra use one input per cell which loses paste / IME quality. Sidebar nav: 'Pin input' added under Forms (next to Tags input). === Sprint Tier 1 — DONE === 1. toggle-group 61ffbade 2. alert-dialog d5630a52 3. dropdown-menu 4e477e63 4. context-menu 21eb8a62 5. pin-input (this) Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>

Powered by TurnKey Linux.