Redesign UI for non-technical users
All checks were successful
check / check (push) Successful in 13s

Replace jargon-heavy terminal-style UI with plain-language views.
New data model: wallets (HD or private key) contain addresses.
Main view lists all addresses grouped by wallet with balances.
HD wallets get a "+" to add addresses; key wallets have one.
Two import paths: recovery phrase and private key.
All labels use plain English, full-sentence errors, inline help
text. README updated with full UI philosophy, language guide,
data model, and navigation docs.
This commit is contained in:
2026-02-24 10:21:52 +07:00
parent e41efc969d
commit 8431488849
3 changed files with 652 additions and 335 deletions

View File

@@ -1,12 +1,15 @@
// AutistMask popup UI — view management and event wiring
// All wallet logic will live in background/; this file is purely UI.
const views = [
const VIEWS = [
"lock",
"setup",
"welcome",
"create",
"import",
"import-phrase",
"import-key",
"main",
"add-wallet",
"address",
"send",
"receive",
"add-token",
@@ -15,7 +18,7 @@ const views = [
];
function showView(name) {
for (const v of views) {
for (const v of VIEWS) {
const el = document.getElementById(`view-${v}`);
if (el) {
el.classList.toggle("hidden", v !== name);
@@ -24,15 +27,18 @@ function showView(name) {
}
// -- mock state (will be replaced by background messaging) --
// A wallet is either { type: "hd", name, mnemonic, addresses: [...] }
// or { type: "key", name, privateKey, addresses: [single] }.
// Each address is { address, balance, tokens: [...] }.
const state = {
locked: true,
hasWallet: false,
password: null,
accounts: [],
selectedAccount: 0,
tokens: [],
wallets: [],
selectedWallet: null,
selectedAddress: null,
rpcUrl: "https://eth.llamarpc.com",
mnemonic: null,
isFirstSetup: true,
};
// -- helpers --
@@ -52,63 +58,190 @@ function hideError(id) {
function truncateAddress(addr) {
if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4);
return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
}
function updateAccountSelector() {
const sel = $("account-selector");
sel.innerHTML = "";
state.accounts.forEach((acct, i) => {
const opt = document.createElement("option");
opt.value = i;
opt.textContent = `Account ${i}: ${truncateAddress(acct.address)}`;
sel.appendChild(opt);
});
sel.value = state.selectedAccount;
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
function makeStubAddress() {
const hex = Array.from({ length: 40 }, () =>
Math.floor(Math.random() * 16).toString(16),
).join("");
return {
address: "0x" + hex,
balance: "0.0000",
tokens: [],
};
}
function updateTokenList() {
const list = $("token-list");
if (state.tokens.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">no tokens added</div>';
// -- render wallet list on main view --
function renderWalletList() {
const container = $("wallet-list");
if (state.wallets.length === 0) {
container.innerHTML =
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
return;
}
list.innerHTML = state.tokens
let html = "";
state.wallets.forEach((wallet, wi) => {
html += `<div class="mb-3">`;
html += `<div class="flex justify-between items-center border-b border-border pb-1 mb-1">`;
html += `<span class="font-bold">${wallet.name}</span>`;
if (wallet.type === "hd") {
html += `<button class="btn-add-address border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-wallet="${wi}" title="Add another address to this wallet">+</button>`;
}
html += `</div>`;
wallet.addresses.forEach((addr, ai) => {
html += `<div class="address-row flex justify-between items-center py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
html += `<span class="text-xs">${truncateAddress(addr.address)}</span>`;
html += `<span class="text-xs">${addr.balance} ETH</span>`;
html += `</div>`;
});
html += `</div>`;
});
container.innerHTML = html;
// bind clicks on address rows
container.querySelectorAll(".address-row").forEach((row) => {
row.addEventListener("click", () => {
state.selectedWallet = parseInt(row.dataset.wallet, 10);
state.selectedAddress = parseInt(row.dataset.address, 10);
showAddressDetail();
});
});
// bind clicks on + buttons within HD wallets
container.querySelectorAll(".btn-add-address").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const wi = parseInt(btn.dataset.wallet, 10);
state.wallets[wi].addresses.push(makeStubAddress());
renderWalletList();
});
});
}
function showAddressDetail() {
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
$("address-title").textContent = wallet.name;
$("address-full").textContent = addr.address;
$("address-copied-msg").textContent = "";
$("address-eth-balance").textContent = addr.balance;
$("address-usd-value").textContent = "";
updateTokenList(addr);
updateSendTokenSelect(addr);
showView("address");
}
function updateTokenList(addr) {
const list = $("token-list");
if (addr.tokens.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
return;
}
list.innerHTML = addr.tokens
.map(
(t) =>
`<div class="py-1 border-b border-border-light flex justify-between">` +
`<span>${t.symbol}</span>` +
`<span>${t.balance || "0.0000"}</span>` +
`<span>${t.balance || "0"}</span>` +
`</div>`,
)
.join("");
}
function updateSendTokenSelect() {
function updateSendTokenSelect(addr) {
const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>';
state.tokens.forEach((t) => {
addr.tokens.forEach((t) => {
const opt = document.createElement("option");
opt.value = t.address;
opt.value = t.contractAddress;
opt.textContent = t.symbol;
sel.appendChild(opt);
});
}
function currentAddress() {
if (state.selectedWallet === null || state.selectedAddress === null) {
return null;
}
return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
}
function addWalletAndGoToMain(wallet) {
state.wallets.push(wallet);
state.hasWallet = true;
state.isFirstSetup = false;
renderWalletList();
showView("main");
}
function showImportView(type) {
if (type === "phrase") {
$("import-mnemonic").value = "";
hideError("import-phrase-error");
const needsPw = state.isFirstSetup;
$("import-phrase-password-section").classList.toggle(
"hidden",
!needsPw,
);
$("import-phrase-password-confirm-section").classList.toggle(
"hidden",
!needsPw,
);
showView("import-phrase");
} else {
$("import-private-key").value = "";
hideError("import-key-error");
const needsPw = state.isFirstSetup;
$("import-key-password-section").classList.toggle("hidden", !needsPw);
$("import-key-password-confirm-section").classList.toggle(
"hidden",
!needsPw,
);
showView("import-key");
}
}
function showCreateView() {
// TODO: generate real mnemonic via background
$("create-mnemonic").textContent =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
hideError("create-error");
showView("create");
}
function validatePasswords(pwId, pw2Id, errorId) {
if (!state.isFirstSetup) return true;
const pw = $(pwId).value;
const pw2 = $(pw2Id).value;
if (!pw) {
showError(errorId, "Please choose a password.");
return false;
}
if (pw.length < 8) {
showError(errorId, "Password must be at least 8 characters.");
return false;
}
if (pw !== pw2) {
showError(errorId, "Passwords do not match.");
return false;
}
state.password = pw;
return true;
}
// -- init --
function init() {
// For now, always show setup (no wallet exists yet).
// Once background messaging is wired, this will check actual state.
if (!state.hasWallet) {
showView("setup");
showView("welcome");
} else if (state.locked) {
showView("lock");
} else {
renderWalletList();
showView("main");
}
@@ -116,100 +249,119 @@ function init() {
$("btn-unlock").addEventListener("click", () => {
const pw = $("unlock-password").value;
if (!pw) {
showError("unlock-error", "enter a password");
showError("unlock-error", "Please enter your password.");
return;
}
hideError("unlock-error");
// TODO: send unlock message to background
state.locked = false;
updateAccountSelector();
updateTokenList();
renderWalletList();
showView("main");
});
// -- Setup --
$("btn-setup-create").addEventListener("click", () => {
// TODO: request mnemonic generation from background
$("create-mnemonic").textContent =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
showView("create");
});
$("btn-setup-import").addEventListener("click", () => {
showView("import");
});
// -- Welcome --
$("btn-welcome-new").addEventListener("click", showCreateView);
$("btn-welcome-recovery").addEventListener("click", () =>
showImportView("phrase"),
);
$("btn-welcome-key").addEventListener("click", () => showImportView("key"));
// -- Create wallet --
$("btn-create-confirm").addEventListener("click", () => {
const pw = $("create-password").value;
const pw2 = $("create-password-confirm").value;
if (!pw) {
showError("create-error", "enter a password");
return;
}
if (pw !== pw2) {
showError("create-error", "passwords do not match");
if (
!validatePasswords(
"create-password",
"create-password-confirm",
"create-error",
)
) {
return;
}
hideError("create-error");
// TODO: send create wallet message to background
state.hasWallet = true;
state.locked = false;
state.password = pw;
state.mnemonic = $("create-mnemonic").textContent;
state.accounts = [
{
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
const walletNum = state.wallets.length + 1;
addWalletAndGoToMain({
type: "hd",
name: "Wallet " + walletNum,
mnemonic: $("create-mnemonic").textContent,
addresses: [makeStubAddress()],
});
});
$("btn-create-back").addEventListener("click", () => {
showView("setup");
showView(state.isFirstSetup ? "welcome" : "add-wallet");
});
// -- Import wallet --
$("btn-import-confirm").addEventListener("click", () => {
// -- Import recovery phrase --
$("btn-import-phrase-confirm").addEventListener("click", () => {
const mnemonic = $("import-mnemonic").value.trim();
const pw = $("import-password").value;
const pw2 = $("import-password-confirm").value;
if (!mnemonic) {
showError("import-error", "enter a seed phrase");
showError(
"import-phrase-error",
"Please enter your recovery phrase.",
);
return;
}
if (!pw) {
showError("import-error", "enter a password");
const words = mnemonic.split(/\s+/);
if (words.length !== 12 && words.length !== 24) {
showError(
"import-phrase-error",
"Recovery phrase must be 12 or 24 words. You entered " +
words.length +
".",
);
return;
}
if (pw !== pw2) {
showError("import-error", "passwords do not match");
if (
!validatePasswords(
"import-phrase-password",
"import-phrase-password-confirm",
"import-phrase-error",
)
) {
return;
}
hideError("import-error");
// TODO: validate mnemonic and send to background
state.hasWallet = true;
state.locked = false;
state.password = pw;
state.mnemonic = mnemonic;
state.accounts = [
{
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
hideError("import-phrase-error");
const walletNum = state.wallets.length + 1;
addWalletAndGoToMain({
type: "hd",
name: "Wallet " + walletNum,
mnemonic: mnemonic,
addresses: [makeStubAddress()],
});
});
$("btn-import-back").addEventListener("click", () => {
showView("setup");
$("btn-import-phrase-back").addEventListener("click", () => {
showView(state.isFirstSetup ? "welcome" : "add-wallet");
});
// -- Import private key --
$("btn-import-key-confirm").addEventListener("click", () => {
const key = $("import-private-key").value.trim();
if (!key) {
showError("import-key-error", "Please enter your private key.");
return;
}
if (
!validatePasswords(
"import-key-password",
"import-key-password-confirm",
"import-key-error",
)
) {
return;
}
hideError("import-key-error");
const walletNum = state.wallets.length + 1;
addWalletAndGoToMain({
type: "key",
name: "Wallet " + walletNum,
privateKey: key,
addresses: [makeStubAddress()],
});
});
$("btn-import-key-back").addEventListener("click", () => {
showView(state.isFirstSetup ? "welcome" : "add-wallet");
});
// -- Main view --
@@ -224,33 +376,50 @@ function init() {
showView("settings");
});
$("account-selector").addEventListener("change", (e) => {
state.selectedAccount = parseInt(e.target.value, 10);
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
$("btn-add-wallet").addEventListener("click", () => {
showView("add-wallet");
});
$("btn-copy-address").addEventListener("click", () => {
const addr = state.accounts[state.selectedAccount]?.address;
// -- Add wallet menu (from main view) --
$("btn-add-wallet-new").addEventListener("click", showCreateView);
$("btn-add-wallet-phrase").addEventListener("click", () =>
showImportView("phrase"),
);
$("btn-add-wallet-key").addEventListener("click", () =>
showImportView("key"),
);
$("btn-add-wallet-back").addEventListener("click", () => {
showView("main");
});
// -- Address detail --
$("address-full").addEventListener("click", () => {
const addr = $("address-full").textContent;
if (addr) {
navigator.clipboard.writeText(addr);
$("address-copied-msg").textContent = "Copied!";
setTimeout(() => {
$("address-copied-msg").textContent = "";
}, 2000);
}
});
$("btn-address-back").addEventListener("click", () => {
renderWalletList();
showView("main");
});
$("btn-send").addEventListener("click", () => {
updateSendTokenSelect();
$("send-to").value = "";
$("send-amount").value = "";
$("send-gas-estimate").classList.add("hidden");
$("send-fee-estimate").classList.add("hidden");
$("send-status").classList.add("hidden");
showView("send");
});
$("btn-receive").addEventListener("click", () => {
$("receive-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
const addr = currentAddress();
$("receive-address").textContent = addr ? addr.address : "";
showView("receive");
});
@@ -266,23 +435,23 @@ function init() {
const to = $("send-to").value.trim();
const amount = $("send-amount").value.trim();
if (!to) {
showError("send-status", "enter a recipient address");
showError("send-status", "Please enter a recipient address.");
$("send-status").classList.remove("hidden");
return;
}
if (!amount || isNaN(parseFloat(amount))) {
showError("send-status", "enter a valid amount");
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
showError("send-status", "Please enter a valid amount.");
$("send-status").classList.remove("hidden");
return;
}
// TODO: construct and send transaction via background
const el = $("send-status");
el.textContent = "transaction sent (stub)";
el.textContent = "Sent! (stub)";
el.classList.remove("hidden");
});
$("btn-send-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Receive --
@@ -294,30 +463,35 @@ function init() {
});
$("btn-receive-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Add Token --
$("btn-add-token-confirm").addEventListener("click", () => {
const addr = $("add-token-address").value.trim();
if (!addr || !addr.startsWith("0x")) {
showError("add-token-error", "enter a valid contract address");
const contractAddr = $("add-token-address").value.trim();
if (!contractAddr || !contractAddr.startsWith("0x")) {
showError(
"add-token-error",
"Please enter a valid contract address starting with 0x.",
);
return;
}
hideError("add-token-error");
// TODO: look up token name/symbol/decimals from contract via background
state.tokens.push({
address: addr,
symbol: "TKN",
decimals: 18,
balance: "0.0000",
});
updateTokenList();
showView("main");
const addr = currentAddress();
if (addr) {
addr.tokens.push({
contractAddress: contractAddr,
symbol: "TKN",
decimals: 18,
balance: "0",
});
}
showAddressDetail();
});
$("btn-add-token-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Settings --
@@ -326,45 +500,21 @@ function init() {
// TODO: persist via background
});
$("btn-derive-account").addEventListener("click", () => {
const idx = state.accounts.length;
// TODO: derive from seed via background
state.accounts.push({
address: `0x${idx.toString(16).padStart(40, "0")}`,
balance: "0.0000",
});
updateAccountSelector();
});
$("btn-show-seed").addEventListener("click", () => {
const display = $("settings-seed-display");
if (display.classList.contains("hidden")) {
// TODO: require password re-entry, get from background
display.textContent = state.mnemonic || "(no seed loaded)";
display.classList.remove("hidden");
} else {
display.classList.add("hidden");
}
});
$("btn-import-additional").addEventListener("click", () => {
showView("import");
});
$("btn-settings-back").addEventListener("click", () => {
updateAccountSelector();
updateTokenList();
renderWalletList();
showView("main");
});
// -- Approval --
$("btn-approve").addEventListener("click", () => {
// TODO: send approval to background
renderWalletList();
showView("main");
});
$("btn-reject").addEventListener("click", () => {
// TODO: send rejection to background
renderWalletList();
showView("main");
});
}