web-ui: top-align single-column views; reveal toggle on password and PIN fields
Login / Import / Create / Settings were vertically centred in the content column (align-self: center), which on tall screens left a dead band above the page heading. Now top-aligned; horizontal centring unchanged. New PasswordInput component wraps every password / PIN / passphrase field with an eye button that toggles the text. Also: wallet-order drag&drop, path-search sweep, settings panel sections, change-address policy.
This commit is contained in:
parent
d50286a9a5
commit
ad3923c18c
7 changed files with 94 additions and 32 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { PasswordInput } from './PasswordInput.js';
|
||||
import {
|
||||
validateSeedPhrase,
|
||||
validateElectrumPhrase,
|
||||
|
|
@ -313,15 +314,14 @@ export function ImportScreen(): JSX.Element {
|
|||
) : (
|
||||
<>
|
||||
<label htmlFor="passphrase">Passphrase (25th word)</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="passphrase"
|
||||
type="password"
|
||||
|
||||
value={passphrase}
|
||||
onChange={(e) => setPassphrase(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder="Leave empty if the source wallet didn't set one"
|
||||
/>
|
||||
placeholder="Leave empty if the source wallet didn't set one" />
|
||||
<div className="field-help">
|
||||
Mixes into the seed derivation. Same phrase + different passphrase = different
|
||||
wallet, different addresses, different balance. Must exactly match what the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { PasswordInput } from './PasswordInput.js';
|
||||
import { Keystore, LocalStorageAdapter, type StoredSeed } from '@dgb-wallet/keystore';
|
||||
import { useActiveSeed, type WalletKind } from './ActiveSeed.js';
|
||||
import { useSessionAuth } from './session-auth.js';
|
||||
|
|
@ -163,14 +164,13 @@ export function LoginPage({
|
|||
</button>
|
||||
{isLegacy && (
|
||||
<div className="wallet-picker-legacy">
|
||||
<input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
|
||||
value={legacyPass[s.label] ?? ''}
|
||||
onChange={(e) => setLegacyPass({ ...legacyPass, [s.label]: e.target.value })}
|
||||
placeholder="original passphrase"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void tryLegacyOpen(s.label); }}
|
||||
autoFocus
|
||||
/>
|
||||
autoFocus />
|
||||
<button
|
||||
className="primary"
|
||||
onClick={() => void tryLegacyOpen(s.label)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { PasswordInput } from './PasswordInput.js';
|
||||
import { useSessionAuth, PIN_MAX_ATTEMPTS } from './session-auth.js';
|
||||
|
||||
// First-run master-password setup. Renders when session state ===
|
||||
|
|
@ -35,21 +36,19 @@ export function SetMasterPasswordScreen({ onDone }: { onDone: () => void }): JSX
|
|||
</p>
|
||||
|
||||
<label>New master password</label>
|
||||
<input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
|
||||
value={pw}
|
||||
onChange={(e) => setPw(e.target.value)}
|
||||
placeholder="at least 8 characters"
|
||||
autoFocus
|
||||
/>
|
||||
autoFocus />
|
||||
<label>Confirm</label>
|
||||
<input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
|
||||
value={pw2}
|
||||
onChange={(e) => setPw2(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }}
|
||||
placeholder="type it again"
|
||||
/>
|
||||
placeholder="type it again" />
|
||||
|
||||
<div className="auth-hints">
|
||||
<span className={okLen ? 'ok' : 'off'}>✓ 8+ characters</span>
|
||||
|
|
@ -110,14 +109,13 @@ export function MasterUnlockScreen(): JSX.Element {
|
|||
</p>
|
||||
|
||||
<label>{mode === 'pin' ? 'PIN' : 'Master password'}</label>
|
||||
<input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
|
||||
inputMode={mode === 'pin' ? 'numeric' : undefined}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }}
|
||||
autoFocus
|
||||
/>
|
||||
autoFocus />
|
||||
<button className="primary" disabled={busy || !value} onClick={submit}>
|
||||
{busy ? 'Unlocking…' : 'Unlock'}
|
||||
</button>
|
||||
|
|
|
|||
44
packages/web-ui/src/PasswordInput.tsx
Normal file
44
packages/web-ui/src/PasswordInput.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { forwardRef, useState, type InputHTMLAttributes } from 'react';
|
||||
|
||||
// Password / PIN / passphrase field with a reveal toggle. A drop-in for
|
||||
// `<input type="password">`: every other prop passes straight through,
|
||||
// the ref lands on the <input>. The eye button is type="button" so it
|
||||
// never submits a surrounding form, and it is excluded from the tab
|
||||
// order (tabIndex -1) so Enter / Tab behaviour on the field itself is
|
||||
// unchanged — mouse or touch users get the toggle, keyboard users keep
|
||||
// their flow.
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
|
||||
function PasswordInput({ className, style, ...props }, ref) {
|
||||
const [shown, setShown] = useState(false);
|
||||
return (
|
||||
<span className="pw-wrap" style={style}>
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
type={shown ? 'text' : 'password'}
|
||||
className={`pw-input ${className ?? ''}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="pw-toggle"
|
||||
onClick={() => setShown((s) => !s)}
|
||||
tabIndex={-1}
|
||||
aria-label={shown ? 'Hide' : 'Show'}
|
||||
title={shown ? 'Hide' : 'Show'}
|
||||
>
|
||||
{shown ? (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
|
||||
<path d="M14.12 14.12a3 3 0 1 1-4.24-4.24" /><line x1="1" y1="1" x2="23" y2="23" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { PasswordInput } from './PasswordInput.js';
|
||||
import { useSessionAuth } from './session-auth.js';
|
||||
|
||||
// Small modal that asks for the session PIN before letting a
|
||||
|
|
@ -61,9 +62,9 @@ export function PinConfirmModal({
|
|||
<div className="modal">
|
||||
<h3 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>{title}</h3>
|
||||
{message && <p className="field-help" style={{ margin: 0 }}>{message}</p>}
|
||||
<input
|
||||
<PasswordInput
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
|
||||
inputMode="numeric"
|
||||
placeholder="Session PIN"
|
||||
value={pin}
|
||||
|
|
@ -74,8 +75,7 @@ export function PinConfirmModal({
|
|||
}}
|
||||
maxLength={12}
|
||||
autoComplete="off"
|
||||
style={{ padding: '10px 12px', fontSize: 15, letterSpacing: '0.2em' }}
|
||||
/>
|
||||
style={{ padding: '10px 12px', fontSize: 15, letterSpacing: '0.2em' }} />
|
||||
{err && <div className="field-help" role="alert" style={{ color: 'var(--danger, #e05252)' }}>{err}</div>}
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 4 }}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { PasswordInput } from './PasswordInput.js';
|
||||
import { useActiveSeed } from './ActiveSeed.js';
|
||||
import { useBackendConfig, AUTO_REFRESH_CHOICES, CHANGE_POLICY_CHOICES, type ChangePolicy } from './backend-config.js';
|
||||
import { BackendMatrix } from './BackendMatrix.js';
|
||||
|
|
@ -339,15 +340,14 @@ function SessionRows(): JSX.Element {
|
|||
password, which re-enables the PIN.
|
||||
</div>
|
||||
<div className="settings-inline-row">
|
||||
<input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
|
||||
inputMode="numeric"
|
||||
placeholder="4-6 digit PIN"
|
||||
value={pinInput}
|
||||
onChange={(e) => setPinInput(e.target.value)}
|
||||
maxLength={12}
|
||||
style={{ maxWidth: 180 }}
|
||||
/>
|
||||
style={{ maxWidth: 180 }} />
|
||||
<button
|
||||
className="settings-row-btn"
|
||||
onClick={onSetPin}
|
||||
|
|
@ -414,9 +414,9 @@ function ChangeMasterPassword(): JSX.Element {
|
|||
is no reset without it.
|
||||
</div>
|
||||
<div className="settings-inline-row settings-inline-wrap">
|
||||
<input type="password" placeholder="current password" value={current} onChange={(e) => setCurrent(e.target.value)} autoComplete="current-password" />
|
||||
<input type="password" placeholder="new password (8+ chars)" value={next} onChange={(e) => setNext(e.target.value)} autoComplete="new-password" />
|
||||
<input type="password" placeholder="confirm new password" value={confirm} onChange={(e) => setConfirm(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }} autoComplete="new-password" />
|
||||
<PasswordInput placeholder="current password" value={current} onChange={(e) => setCurrent(e.target.value)} autoComplete="current-password" />
|
||||
<PasswordInput placeholder="new password (8+ chars)" value={next} onChange={(e) => setNext(e.target.value)} autoComplete="new-password" />
|
||||
<PasswordInput placeholder="confirm new password" value={confirm} onChange={(e) => setConfirm(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }} autoComplete="new-password" />
|
||||
<button className="settings-row-btn" onClick={submit} disabled={!ready}>
|
||||
{busy ? 'Re-encrypting…' : 'Change password'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,11 @@ body:has(.shell-dashboard) {
|
|||
.shell:not(.shell-dashboard) .content {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
/* Horizontal centering only. align-self:center also centred the
|
||||
column VERTICALLY, which on a tall viewport floated Import /
|
||||
Login / Settings into the middle of the page with a dead band
|
||||
above the heading. */
|
||||
align-self: flex-start;
|
||||
}
|
||||
.shell-dashboard .content {
|
||||
max-width: none;
|
||||
|
|
@ -994,6 +998,22 @@ textarea, input {
|
|||
transition: border-color 0.15s;
|
||||
}
|
||||
textarea:focus, input:focus { border-color: var(--accent); }
|
||||
|
||||
/* Password / PIN field with reveal toggle (PasswordInput.tsx). The wrap
|
||||
takes the input's place in the layout; the input keeps every base
|
||||
style and gains right padding for the eye button. */
|
||||
.pw-wrap { position: relative; display: block; width: 100%; }
|
||||
.pw-wrap .pw-input { padding-right: 42px; }
|
||||
.pw-toggle {
|
||||
position: absolute; top: 50%; right: 8px; transform: translateY(-50%);
|
||||
width: 28px; height: 28px; padding: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: transparent; border: 0; border-radius: 6px;
|
||||
color: var(--text-muted); cursor: pointer;
|
||||
}
|
||||
.pw-toggle:hover { color: var(--text); background: rgba(255,255,255,0.06); }
|
||||
.settings-inline-row .pw-wrap { flex: 1; min-width: 160px; width: auto; }
|
||||
.wallet-picker-legacy .pw-wrap { flex: 1; min-width: 160px; width: auto; }
|
||||
textarea { min-height: 100px; line-height: 1.5; }
|
||||
|
||||
button.primary {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue