diff --git a/packages/auth/src/ui/css.ts b/packages/auth/src/ui/css.ts index 6ef407f4..0dad01af 100644 --- a/packages/auth/src/ui/css.ts +++ b/packages/auth/src/ui/css.ts @@ -486,6 +486,56 @@ form:has([data-component='input']:invalid) [data-component='button']:hover { font-variant-numeric: tabular-nums; } +/* A code field drawn as one box per character, by \`segments.ts\`. The real + field is still the one that submits and the one the button's dimming reads, + so it is hidden from sight and not removed. */ +[data-component='segments'] { + position: relative; + width: 100%; +} + +[data-component='segments'][data-enhanced] > [data-component='input'] { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; +} + +[data-component='segments'] [data-slot='boxes'] { + display: flex; + width: 100%; + align-items: center; + gap: 0.5rem; +} + +[data-component='input'][data-variant='box'] { + flex: 1 1 0%; + min-width: 0; + padding: 0.9rem 0; + text-align: center; + font-family: var(--font-mona); + font-size: 1.5rem; + line-height: 2rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + text-transform: uppercase; +} + +/* A box selects its character on focus so typing replaces it; that is + behaviour, not something to show. */ +[data-component='input'][data-variant='box']::selection { + background: transparent; +} + +[data-component='segments'] [data-slot='separator'] { + flex: none; + color: var(--color-muted-foreground2); + font-family: var(--font-mona); + font-size: 1.5rem; + font-weight: 700; + user-select: none; +} + /* A wrong answer shakes the field it came from, once. Every error is a new page, so an animation that runs on load runs exactly when one arrives — no script needed to trigger it. */ diff --git a/packages/auth/src/ui/render.tsx b/packages/auth/src/ui/render.tsx index 70fadd9f..9a55d10b 100644 --- a/packages/auth/src/ui/render.tsx +++ b/packages/auth/src/ui/render.tsx @@ -17,6 +17,7 @@ /** @jsxImportSource hono/jsx */ import { Layout } from './base.js'; +import segments from './segments.js'; import type { Alert, ChooseScreen, @@ -52,7 +53,10 @@ export interface HtmlRendererOptions { theme?: Theme; } -/** The default renderer: server-rendered HTML, no client-side script. */ +/** + * The default renderer: server-rendered HTML that works with script off. The + * one script, `segments.ts`, only redraws a code field as boxes. + */ export function HtmlRenderer(options?: HtmlRendererOptions): Renderer { const theme = options?.theme; @@ -243,27 +247,38 @@ function Input(props: { field: Field }) { const sizes = Array.from({ length: groups }, (_, i) => Math.min(field.group ?? field.length, field.length - i * (field.group ?? field.length)) ); + // One box per character once `segments.ts` has run; this single field + // until then, and for good if it never does. return ( - `${char}{${n}}`).join('[\\- ]?')} - size={field.length + groups - 1} - required - spellcheck={false} - autocapitalize="characters" - inputmode={field.numeric ? 'numeric' : 'text'} - autocomplete={field.autocomplete} - autofocus={field.autofocus} - /> + <> +
+ `${char}{${n}}`).join('[\\- ]?')} + size={field.length + groups - 1} + required + spellcheck={false} + autocapitalize="characters" + inputmode={field.numeric ? 'numeric' : 'text'} + autocomplete={field.autocomplete} + autofocus={field.autofocus} + /> +
+