feat(onboarding): 5 clickable mnemonic suggestions per letter
- Dot words: ≤3 letters (was ≤2) — two-letter words like "go/it" were too limiting - Dash words: ≥4 letters (unchanged in practice) - Added DOT_WORDS and DASH_WORDS banks (~60 and ~200 words) - suggestPhrases() generates 5 random valid suggestions per letter - MnemonicBuilder shows clickable suggestion pills; "↻ new" refreshes them - Selected suggestion highlights in gold; user can still type their own Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { NOVICE_LETTERS, MORSE, validatePhrase } from '../lib/morse'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { NOVICE_LETTERS, MORSE, validatePhrase, suggestPhrases } from '../lib/morse'
|
||||
|
||||
type Props = {
|
||||
token: string
|
||||
@@ -13,6 +13,7 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
const [phrase, setPhrase] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
|
||||
const letter = LETTERS[index]
|
||||
const pattern = MORSE[letter] ?? ''
|
||||
@@ -20,6 +21,12 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
const display = pattern.split('').map(c => c === '.' ? '·' : '—').join(' ')
|
||||
const validation = phrase.trim() ? validatePhrase(phrase.trim(), letter) : null
|
||||
|
||||
useEffect(() => {
|
||||
setSuggestions(suggestPhrases(letter, 5))
|
||||
setPhrase('')
|
||||
setError(null)
|
||||
}, [letter])
|
||||
|
||||
async function save() {
|
||||
if (!validation?.valid) return
|
||||
setSaving(true)
|
||||
@@ -47,8 +54,6 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
onComplete()
|
||||
} else {
|
||||
setIndex(i => i + 1)
|
||||
setPhrase('')
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,14 +93,14 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
Create your mnemonic phrase:
|
||||
</label>
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--color-brown)', marginBottom: '0.5rem' }}>
|
||||
{pattern.length} word{pattern.length !== 1 ? 's' : ''} — short words (1-2 letters) for dots,
|
||||
long words (3+ letters) for dashes.
|
||||
{pattern.length} word{pattern.length !== 1 ? 's' : ''} —
|
||||
{' '}≤3 letters for dots · (short), 4+ letters for dashes — (long).
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={phrase}
|
||||
onChange={e => { setPhrase(e.target.value); setError(null) }}
|
||||
placeholder={`${pattern.length} word${pattern.length !== 1 ? 's' : ''}, e.g. "${examplePhrase(pattern)}"`}
|
||||
placeholder={`e.g. "${examplePhrase(pattern)}"`}
|
||||
disabled={saving}
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
/>
|
||||
@@ -115,6 +120,61 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
<p style={{ color: 'crimson', fontSize: '0.9rem', marginBottom: '0.5rem' }}>{error}</p>
|
||||
)}
|
||||
|
||||
{/* Suggestions */}
|
||||
<div style={{ marginTop: '0.75rem', marginBottom: '1rem' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '0.4rem',
|
||||
}}>
|
||||
<span style={{ fontSize: '0.85rem', color: 'var(--color-brown)' }}>
|
||||
Suggestions — click to use:
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSuggestions(suggestPhrases(letter, 5))}
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: '0.1rem 0.4rem',
|
||||
fontSize: '0.8rem',
|
||||
background: 'none',
|
||||
border: '1px solid var(--color-brown)',
|
||||
borderRadius: '0.25rem',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-brown)',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
↻ new
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
{suggestions.map(s => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => { setPhrase(s); setError(null) }}
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: '0.35rem 0.7rem',
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
background: phrase === s ? 'var(--color-gold)' : 'var(--color-parchment)',
|
||||
border: `1px solid ${phrase === s ? 'var(--color-gold)' : '#c8a96e'}`,
|
||||
borderRadius: '0.25rem',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-forest)',
|
||||
textAlign: 'left',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '1rem' }}>
|
||||
<button
|
||||
@@ -139,5 +199,5 @@ export function MnemonicBuilder({ token, onComplete }: Props) {
|
||||
|
||||
// Generate a simple example phrase for the placeholder
|
||||
function examplePhrase(pattern: string): string {
|
||||
return pattern.split('').map(c => c === '.' ? 'go' : 'WALKING').join(' ')
|
||||
return pattern.split('').map(c => c === '.' ? 'run' : 'WALK').join(' ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user