diff --git a/index.html b/index.html
index d42f9d9..dfac19e 100644
--- a/index.html
+++ b/index.html
@@ -140,11 +140,10 @@
Choose a BIP39 word by typing it or by clicking numbers (1-2048).
+
-
diff --git a/src/modules/bip39/infrastructure/elements.ts b/src/modules/bip39/infrastructure/elements.ts
index a615db1..6cc149f 100644
--- a/src/modules/bip39/infrastructure/elements.ts
+++ b/src/modules/bip39/infrastructure/elements.ts
@@ -37,9 +37,6 @@ export const elements = {
get wordInput() {
return getElementById('word-input');
},
- get wordInputLabel() {
- return getElementById('word-input-label');
- },
get wordSuggestions() {
return getElementById('word-suggestions');
},
diff --git a/src/modules/display/infrastructure/toast.ts b/src/modules/display/infrastructure/toast.ts
index a650258..84e4046 100644
--- a/src/modules/display/infrastructure/toast.ts
+++ b/src/modules/display/infrastructure/toast.ts
@@ -30,40 +30,51 @@ export function showDisabledBoxToast(): void {
}
export function showToast(id: string, message: string, duration: number = 3000, className: string = 'toast'): void {
+ cleanupExistingToast(id);
+ const toast = createToastElement(id, message, className);
+ scheduleToastAnimations(id, toast, duration);
+}
+
+function cleanupExistingToast(id: string): void {
const existingToast = document.getElementById(id);
- if (existingToast) {
- existingToast.remove();
- }
+ existingToast?.remove();
+
+ clearToastTimers(id);
+}
+function clearToastTimers(id: string): void {
const timers = toastTimers.get(id);
if (timers) {
if (timers.show) clearTimeout(timers.show);
if (timers.hide) clearTimeout(timers.hide);
if (timers.remove) clearTimeout(timers.remove);
}
+}
- toastTimers.set(id, { show: null, hide: null, remove: null });
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const currentTimers = toastTimers.get(id)!;
-
+function createToastElement(id: string, message: string, className: string): HTMLElement {
const toast = document.createElement('div');
toast.id = id;
toast.className = className;
toast.textContent = message;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'polite');
-
document.body.appendChild(toast);
- currentTimers.show = setTimeout(() => {
- toast.classList.add('show');
- }, 10);
+ return toast;
+}
+
+function scheduleToastAnimations(id: string, toast: HTMLElement, duration: number): void {
+ toastTimers.set(id, { show: null, hide: null, remove: null });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const timers = toastTimers.get(id)!;
+
+ timers.show = setTimeout(() => toast.classList.add('show'), 10);
- currentTimers.hide = setTimeout(() => {
+ timers.hide = setTimeout(() => {
toast.classList.remove('show');
- currentTimers.remove = setTimeout(() => {
+ timers.remove = setTimeout(() => {
toast.remove();
- toastTimers.delete(id); // Clean up timer storage
+ toastTimers.delete(id);
}, 300);
}, duration);
}
diff --git a/src/modules/i18n/domain/i18n.ts b/src/modules/i18n/domain/i18n.ts
index c3b17b7..4d5b10a 100644
--- a/src/modules/i18n/domain/i18n.ts
+++ b/src/modules/i18n/domain/i18n.ts
@@ -13,7 +13,6 @@ export interface Translations {
invalidWordMessage: string;
wordlistLoadError: string;
// Word input translations
- wordInputLabel: string;
wordInputPlaceholder: string;
// Modal translations
modalTitle: string;
@@ -67,7 +66,7 @@ export const translations: Record = {
languageLabel: 'Language',
index: 'Index:',
resetButton: 'Reset',
- infoText: 'Choose a BIP39 word by typing it or by clicking numbers (1-2048).',
+ infoText: 'Choose a BIP39 word by typing it in the input field below or by clicking the number boxes (1-2048)',
privacyTitle: 'Privacy Protected',
privacyTooltip: 'This application runs entirely in your browser. No data is transmitted, stored, or tracked.',
toggleTheme: 'Toggle dark/light mode',
@@ -76,7 +75,6 @@ export const translations: Record = {
invalidWordMessage: 'This word is not in the BIP39 wordlist',
wordlistLoadError: '⚠️ Failed to load wordlist. Please refresh the page.',
// Word input translations
- wordInputLabel: 'Type a word or click numbers to select',
wordInputPlaceholder: 'abandon',
// Modal translations
modalTitle: 'What is BIP39?',
@@ -133,7 +131,8 @@ export const translations: Record = {
languageLabel: 'Idioma',
index: 'Índice:',
resetButton: 'Reiniciar',
- infoText: 'Elige una palabra BIP39 escribiéndola o haciendo clic en números (1-2048).',
+ infoText:
+ 'Elige una palabra BIP39 escribiéndola en el campo de texto o haciendo clic en las casillas numéricas (1-2048)',
privacyTitle: 'Privacidad Protegida',
privacyTooltip:
'Esta aplicación se ejecuta completamente en tu navegador. No se transmite, almacena ni rastrea ningún dato.',
@@ -143,7 +142,6 @@ export const translations: Record = {
invalidWordMessage: 'Esta palabra no está en la lista BIP39',
wordlistLoadError: '⚠️ Error al cargar la lista de palabras. Por favor, recarga la página.',
// Word input translations
- wordInputLabel: 'Escribe una palabra o haz clic en números',
wordInputPlaceholder: 'ábaco',
// Modal translations
modalTitle: '¿Qué es BIP39?',
@@ -200,7 +198,8 @@ export const translations: Record = {
languageLabel: 'Langue',
index: 'Indice:',
resetButton: 'Réinitialiser',
- infoText: 'Choisissez un mot BIP39 en le tapant ou en cliquant sur des numéros (1-2048).',
+ infoText:
+ 'Choisissez un mot BIP39 en le tapant dans le champ de saisie ci-dessous ou en cliquant sur les cases numériques (1-2048)',
privacyTitle: 'Confidentialité Protégée',
privacyTooltip:
"Cette application s'exécute entièrement dans votre navigateur. Aucune donnée n'est transmise, stockée ou suivie.",
@@ -210,7 +209,6 @@ export const translations: Record = {
invalidWordMessage: "Ce mot n'est pas dans la liste BIP39",
wordlistLoadError: '⚠️ Échec du chargement de la liste de mots. Veuillez actualiser la page.',
// Word input translations
- wordInputLabel: 'Tapez un mot ou cliquez sur des numéros',
wordInputPlaceholder: 'abaisser',
// Modal translations
modalTitle: "Qu'est-ce que BIP39?",
@@ -267,7 +265,7 @@ export const translations: Record = {
languageLabel: 'Jazyk',
index: 'Index:',
resetButton: 'Resetovat',
- infoText: 'Vyberte slovo BIP39 jeho napsáním nebo kliknutím na čísla (1-2048).',
+ infoText: 'Vyberte slovo BIP39 jeho napsáním do vstupního pole níže nebo kliknutím na číselná pole (1-2048)',
privacyTitle: 'Soukromí Chráněno',
privacyTooltip:
'Tato aplikace běží zcela ve vašem prohlížeči. Žádná data nejsou přenášena, ukládána ani sledována.',
@@ -277,7 +275,6 @@ export const translations: Record = {
invalidWordMessage: 'Toto slovo není v seznamu BIP39',
wordlistLoadError: '⚠️ Nepodařilo se načíst seznam slov. Obnovte prosím stránku.',
// Word input translations
- wordInputLabel: 'Napište slovo nebo klikněte na čísla',
wordInputPlaceholder: 'abdikace',
// Modal translations
modalTitle: 'Co je BIP39?',
@@ -334,7 +331,8 @@ export const translations: Record = {
languageLabel: 'Lingua',
index: 'Indice:',
resetButton: 'Ripristina',
- infoText: 'Scegli una parola BIP39 scrivendola o cliccando sui numeri (1-2048).',
+ infoText:
+ 'Scegli una parola BIP39 scrivendola nel campo di input qui sotto o cliccando sulle caselle numeriche (1-2048)',
privacyTitle: 'Privacy Protetta',
privacyTooltip:
'Questa applicazione viene eseguita interamente nel tuo browser. Nessun dato viene trasmesso, memorizzato o tracciato.',
@@ -344,7 +342,6 @@ export const translations: Record = {
invalidWordMessage: 'Questa parola non è nella lista BIP39',
wordlistLoadError: '⚠️ Impossibile caricare la lista di parole. Ricarica la pagina.',
// Word input translations
- wordInputLabel: 'Scrivi una parola o clicca sui numeri',
wordInputPlaceholder: 'abaco',
// Modal translations
modalTitle: "Cos'è BIP39?",
@@ -401,7 +398,8 @@ export const translations: Record = {
languageLabel: 'Idioma',
index: 'Índice:',
resetButton: 'Reiniciar',
- infoText: 'Escolha uma palavra BIP39 digitando-a ou clicando nos números (1-2048).',
+ infoText:
+ 'Escolha uma palavra BIP39 digitando-a no campo de entrada abaixo ou clicando nas caixas numéricas (1-2048)',
privacyTitle: 'Privacidade Protegida',
privacyTooltip:
'Este aplicativo é executado inteiramente no seu navegador. Nenhum dado é transmitido, armazenado ou rastreado.',
@@ -411,7 +409,6 @@ export const translations: Record = {
invalidWordMessage: 'Esta palavra não está na lista BIP39',
wordlistLoadError: '⚠️ Falha ao carregar a lista de palavras. Atualize a página.',
// Word input translations
- wordInputLabel: 'Digite uma palavra ou clique nos números',
wordInputPlaceholder: 'abacate',
// Modal translations
modalTitle: 'O que é BIP39?',
@@ -468,7 +465,7 @@ export const translations: Record = {
languageLabel: '言語',
index: 'インデックス:',
resetButton: 'リセット',
- infoText: 'BIP39単語を入力するか、数字をクリックして選択します(1-2048)。',
+ infoText: 'BIP39単語を下の入力フィールドに入力するか、数字ボックスをクリックして選択します(1-2048)',
privacyTitle: 'プライバシー保護',
privacyTooltip: 'このアプリケーションはブラウザ内で完全に動作します。データの送信、保存、追跡は一切行われません。',
toggleTheme: 'ダークモード/ライトモードを切り替え',
@@ -477,7 +474,6 @@ export const translations: Record = {
invalidWordMessage: 'この単語はBIP39リストにありません',
wordlistLoadError: '⚠️ ワードリストの読み込みに失敗しました。ページを更新してください。',
// Word input translations
- wordInputLabel: '単語を入力するか、数字をクリック',
wordInputPlaceholder: 'あいこくしん',
// Modal translations
modalTitle: 'BIP39とは?',
@@ -534,7 +530,7 @@ export const translations: Record = {
languageLabel: '언어',
index: '인덱스:',
resetButton: '재설정',
- infoText: 'BIP39 단어를 입력하거나 숫자를 클릭하여 선택하세요 (1-2048).',
+ infoText: 'BIP39 단어를 아래 입력 필드에 입력하거나 숫자 상자를 클릭하여 선택하세요 (1-2048)',
privacyTitle: '개인정보 보호',
privacyTooltip: '이 애플리케이션은 브라우저에서 완전히 실행됩니다. 데이터 전송, 저장 또는 추적이 없습니다.',
toggleTheme: '다크 모드/라이트 모드 전환',
@@ -543,7 +539,6 @@ export const translations: Record = {
invalidWordMessage: '이 단어는 BIP39 목록에 없습니다',
wordlistLoadError: '⚠️ 단어 목록을 로드하지 못했습니다. 페이지를 새로고침하세요.',
// Word input translations
- wordInputLabel: '단어를 입력하거나 숫자를 클릭',
wordInputPlaceholder: '가격',
// Modal translations
modalTitle: 'BIP39란 무엇인가요?',
@@ -600,7 +595,7 @@ export const translations: Record = {
languageLabel: '语言',
index: '索引:',
resetButton: '重置',
- infoText: '通过输入或点击数字选择 BIP39 单词 (1-2048)。',
+ infoText: '在下面的输入字段中输入 BIP39 单词或通过点击数字框选择 (1-2048)',
privacyTitle: '隐私保护',
privacyTooltip: '此应用程序完全在您的浏览器中运行。不会传输、存储或跟踪任何数据。',
toggleTheme: '切换深色/浅色模式',
@@ -609,7 +604,6 @@ export const translations: Record = {
invalidWordMessage: '此单词不在 BIP39 列表中',
wordlistLoadError: '⚠️ 加载单词列表失败。请刷新页面。',
// Word input translations
- wordInputLabel: '输入单词或点击数字',
wordInputPlaceholder: '的',
// Modal translations
modalTitle: '什么是BIP39?',
@@ -652,7 +646,7 @@ export const translations: Record = {
languageLabel: '語言',
index: '索引:',
resetButton: '重置',
- infoText: '通過輸入或點擊數字選擇 BIP39 單詞 (1-2048)。',
+ infoText: '在下面的輸入字段中輸入 BIP39 單詞或通過點擊數字框選擇 (1-2048)',
privacyTitle: '隱私保護',
privacyTooltip: '此應用程序完全在您的瀏覽器中運行。不會傳輸、存儲或跟蹤任何數據。',
toggleTheme: '切換深色/淺色模式',
@@ -661,7 +655,6 @@ export const translations: Record = {
invalidWordMessage: '此單詞不在 BIP39 列表中',
wordlistLoadError: '⚠️ 載入單詞列表失敗。請重新整理頁面。',
// Word input translations
- wordInputLabel: '輸入單詞或點擊數字',
wordInputPlaceholder: '的',
// Modal translations
modalTitle: '什麼是BIP39?',
diff --git a/src/modules/language/infrastructure/language.ts b/src/modules/language/infrastructure/language.ts
index 44bb6ee..f4c07ee 100644
--- a/src/modules/language/infrastructure/language.ts
+++ b/src/modules/language/infrastructure/language.ts
@@ -168,7 +168,6 @@ function updateBasicUITranslations(): void {
}
function updateWordInputTranslations(): void {
- elements.wordInputLabel.textContent = currentTranslations.wordInputLabel;
elements.wordInput.placeholder = currentTranslations.wordInputPlaceholder;
}
diff --git a/src/modules/wordInput/infrastructure/wordInput.ts b/src/modules/wordInput/infrastructure/wordInput.ts
index 95f2964..aaa531d 100644
--- a/src/modules/wordInput/infrastructure/wordInput.ts
+++ b/src/modules/wordInput/infrastructure/wordInput.ts
@@ -16,7 +16,11 @@ export function setupWordInput(): void {
elements.wordInput.addEventListener('focus', handleWordInput);
elements.wordInput.addEventListener('blur', handleWordInputBlur);
- // Handle click outside to close suggestions
+ const clearBtn = document.getElementById('clear-input-btn');
+ if (clearBtn) {
+ clearBtn.addEventListener('click', handleClearInput);
+ }
+
document.addEventListener('click', e => {
if (!elements.wordInput.contains(e.target as Node) && !elements.wordSuggestions.contains(e.target as Node)) {
hideSuggestions();
@@ -24,6 +28,16 @@ export function setupWordInput(): void {
});
}
+function handleClearInput(): void {
+ elements.wordInput.value = '';
+ elements.wordInput.classList.remove('error');
+ resetBoxes();
+ updateDisplay();
+ hideSuggestions();
+ toggleClearButton(false);
+ elements.wordInput.focus();
+}
+
function handleWordInputBlur(): void {
hideSuggestions();
validateWordInput();
@@ -42,6 +56,11 @@ function validateWordInput(): void {
if (wordExists) {
elements.wordInput.classList.remove('error');
+ const wordIndex = getWordIndex(value, state.wordlist);
+ if (wordIndex !== -1) {
+ setStateFromIndex(wordIndex);
+ updateDisplay();
+ }
return;
}
@@ -55,6 +74,8 @@ function validateWordInput(): void {
function handleWordInput(): void {
const value = elements.wordInput.value.trim().toLowerCase();
+ toggleClearButton(elements.wordInput.value.length > 0);
+
if (!value) {
hideSuggestions();
return;
@@ -68,6 +89,12 @@ function handleWordInput(): void {
return;
}
+ // Hide suggestions if only 1 match and it's an exact match
+ if (matches.length === 1 && matches[0].toLowerCase() === value) {
+ hideSuggestions();
+ return;
+ }
+
// Show suggestions
showSuggestions(matches.slice(0, 10)); // Limit to 10 suggestions
selectedSuggestionIndex = -1;
@@ -202,6 +229,20 @@ export function clearWordInput(): void {
elements.wordInput.value = '';
elements.wordInput.classList.remove('error');
hideSuggestions();
+ toggleClearButton(false);
+}
+
+function toggleClearButton(show: boolean): void {
+ const clearBtn = document.getElementById('clear-input-btn') as HTMLButtonElement | null;
+ if (clearBtn) {
+ if (show) {
+ clearBtn.disabled = false;
+ clearBtn.removeAttribute('aria-disabled');
+ } else {
+ clearBtn.disabled = true;
+ clearBtn.setAttribute('aria-disabled', 'true');
+ }
+ }
}
export function syncWordInputFromState(): void {
@@ -212,10 +253,12 @@ export function syncWordInputFromState(): void {
const word = getWordByIndex(wordIndex, state.wordlist);
if (word && elements.wordInput.value !== word) {
elements.wordInput.value = word;
+ toggleClearButton(true);
}
} else {
if (elements.wordInput.value !== '') {
elements.wordInput.value = '';
+ toggleClearButton(false);
}
}
}
diff --git a/src/styles/components.css b/src/styles/components.css
index 704fe59..a5ea935 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -202,9 +202,10 @@
.info-description {
color: var(--text-secondary);
margin: 0 0 3rem 0;
- font-size: 1.15rem;
+ font-size: 1.4rem;
line-height: 1.6;
text-align: left;
+ letter-spacing: 0.05em;
}
/* Section Styles */
@@ -213,16 +214,6 @@
margin-bottom: 2.5rem;
}
-.section-title {
- font-size: 1.3rem;
- font-weight: 600;
- color: var(--primary-color);
- letter-spacing: 0.05em;
- text-transform: uppercase;
- margin-bottom: 1.5rem;
- text-align: center;
-}
-
/* Visually hidden class for accessibility */
.visually-hidden {
position: absolute;
@@ -453,8 +444,6 @@
box-shadow: 0 2px 6px rgba(247, 147, 26, 0.2);
}
-/* Word input label is now section-title */
-
.word-input-wrapper {
position: relative;
width: 100%;
@@ -463,7 +452,7 @@
.word-input {
width: 100%;
- padding: 1rem;
+ padding: 1rem 3.5rem 1rem 1rem;
font-size: 2.2rem;
font-weight: 700;
font-family: 'Courier New', monospace;
@@ -499,6 +488,51 @@
letter-spacing: 0.05em;
}
+.clear-input-btn {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 2px solid var(--border);
+ background: var(--background);
+ color: var(--text-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ padding: 0;
+ opacity: 0.9;
+ z-index: 10;
+}
+
+.clear-input-btn:hover {
+ opacity: 1;
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+ box-shadow: 0 4px 8px rgba(247, 147, 26, 0.3);
+}
+
+.clear-input-btn:disabled,
+.clear-input-btn[aria-disabled='true'] {
+ opacity: 0.3;
+ cursor: not-allowed;
+ background: var(--surface-light);
+ color: var(--text-secondary);
+ border-color: var(--border);
+ pointer-events: none;
+}
+
+.clear-input-btn svg {
+ width: 20px;
+ height: 20px;
+ stroke-width: 2.5;
+}
+
/* Word Info (Index display) */
.word-info {
margin-top: 1.5rem;
diff --git a/src/styles/responsive.css b/src/styles/responsive.css
index 556e177..3024b60 100644
--- a/src/styles/responsive.css
+++ b/src/styles/responsive.css
@@ -11,7 +11,7 @@
.word-input {
font-size: 2rem;
- padding: 1.25rem;
+ padding: 1.25rem 3.5rem 1.25rem 1.25rem;
}
.word-input::placeholder {
@@ -26,11 +26,6 @@
min-height: 500px;
}
- .section-title {
- font-size: 1rem;
- margin-bottom: 1.25rem;
- }
-
.title-section {
width: 100%;
}
@@ -102,13 +97,24 @@
.word-input {
font-size: 1.75rem;
- padding: 1.125rem 1.25rem;
+ padding: 1.125rem 3.5rem 1.125rem 1.25rem;
}
.word-input::placeholder {
font-size: 1.75rem;
}
+ .clear-input-btn {
+ width: 32px;
+ height: 32px;
+ right: 10px;
+ }
+
+ .clear-input-btn svg {
+ width: 18px;
+ height: 18px;
+ }
+
.word-info {
margin-top: 1.25rem;
}
@@ -206,11 +212,6 @@
margin-bottom: 1.75rem;
}
- .section-title {
- font-size: 0.9rem;
- margin-bottom: 1.25rem;
- }
-
.title-section h1 {
font-size: 1.65rem;
}
@@ -234,13 +235,24 @@
.word-input {
font-size: 1.5rem;
- padding: 1rem;
+ padding: 1rem 3rem 1rem 1rem;
}
.word-input::placeholder {
font-size: 1.5rem;
}
+ .clear-input-btn {
+ width: 30px;
+ height: 30px;
+ right: 8px;
+ }
+
+ .clear-input-btn svg {
+ width: 16px;
+ height: 16px;
+ }
+
.word-info {
margin-top: 1rem;
}
@@ -302,13 +314,24 @@
.word-input {
font-size: 1.35rem;
- padding: 0.95rem;
+ padding: 0.95rem 3rem 0.95rem 0.95rem;
}
.word-input::placeholder {
font-size: 1.35rem;
}
+ .clear-input-btn {
+ width: 28px;
+ height: 28px;
+ right: 8px;
+ }
+
+ .clear-input-btn svg {
+ width: 16px;
+ height: 16px;
+ }
+
.binary-display code {
font-size: 0.9rem;
}
@@ -324,17 +347,13 @@
padding: 1.25rem 0.75rem;
}
- .section-title {
- font-size: 0.85rem;
- }
-
.title-section h1 {
font-size: 1.5rem;
}
.word-input {
font-size: 1.25rem;
- padding: 0.875rem;
+ padding: 0.875rem 0.875rem 0.875rem 2.75rem;
}
.word-input::placeholder {
diff --git a/test/unit/bip39/infrastructure/dom.test.ts b/test/unit/bip39/infrastructure/dom.test.ts
index 9aec4de..a24ffb0 100644
--- a/test/unit/bip39/infrastructure/dom.test.ts
+++ b/test/unit/bip39/infrastructure/dom.test.ts
@@ -74,7 +74,6 @@ describe('DOM Elements', () => {
it('should have word input elements defined', () => {
expect(elements.wordInput).toBeDefined();
- expect(elements.wordInputLabel).toBeDefined();
expect(elements.wordSuggestions).toBeDefined();
});
diff --git a/test/unit/i18n/domain/i18n.test.ts b/test/unit/i18n/domain/i18n.test.ts
index b2aec07..6d67d9b 100644
--- a/test/unit/i18n/domain/i18n.test.ts
+++ b/test/unit/i18n/domain/i18n.test.ts
@@ -66,7 +66,7 @@ describe('i18n', () => {
expect(en.title).toBeDefined();
expect(en.resetButton).toBeDefined();
expect(en.infoText).toBeDefined();
- expect(en.wordInputLabel).toBeDefined();
+ expect(en.wordInputPlaceholder).toBeDefined();
expect(en.modalTitle).toBeDefined();
});
});