Add basic monochrome popup UI with Tailwind CSS
All checks were successful
check / check (push) Successful in 11s
All checks were successful
check / check (push) Successful in 11s
Black-on-white, monospace, Universal Paperclips aesthetic. All views: lock, setup/create/import, main account, send, receive, add token, settings, and approval. Vanilla JS view switching with stub state. README updated with full UI design philosophy, external services documentation, and view descriptions.
This commit is contained in:
@@ -1,2 +1,372 @@
|
||||
// AutistMask popup UI
|
||||
// TODO: lock screen, account list, send/receive views
|
||||
// AutistMask popup UI — view management and event wiring
|
||||
// All wallet logic will live in background/; this file is purely UI.
|
||||
|
||||
const views = [
|
||||
"lock",
|
||||
"setup",
|
||||
"create",
|
||||
"import",
|
||||
"main",
|
||||
"send",
|
||||
"receive",
|
||||
"add-token",
|
||||
"settings",
|
||||
"approve",
|
||||
];
|
||||
|
||||
function showView(name) {
|
||||
for (const v of views) {
|
||||
const el = document.getElementById(`view-${v}`);
|
||||
if (el) {
|
||||
el.classList.toggle("hidden", v !== name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- mock state (will be replaced by background messaging) --
|
||||
const state = {
|
||||
locked: true,
|
||||
hasWallet: false,
|
||||
password: null,
|
||||
accounts: [],
|
||||
selectedAccount: 0,
|
||||
tokens: [],
|
||||
rpcUrl: "https://eth.llamarpc.com",
|
||||
mnemonic: null,
|
||||
};
|
||||
|
||||
// -- helpers --
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function showError(id, msg) {
|
||||
const el = $(id);
|
||||
el.textContent = msg;
|
||||
el.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideError(id) {
|
||||
$(id).classList.add("hidden");
|
||||
}
|
||||
|
||||
function truncateAddress(addr) {
|
||||
if (!addr) return "";
|
||||
return addr.slice(0, 6) + "..." + 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 updateTokenList() {
|
||||
const list = $("token-list");
|
||||
if (state.tokens.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">no tokens added</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = state.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>` +
|
||||
`</div>`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function updateSendTokenSelect() {
|
||||
const sel = $("send-token");
|
||||
sel.innerHTML = '<option value="ETH">ETH</option>';
|
||||
state.tokens.forEach((t) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = t.address;
|
||||
opt.textContent = t.symbol;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
// -- 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");
|
||||
} else if (state.locked) {
|
||||
showView("lock");
|
||||
} else {
|
||||
showView("main");
|
||||
}
|
||||
|
||||
// -- Lock screen --
|
||||
$("btn-unlock").addEventListener("click", () => {
|
||||
const pw = $("unlock-password").value;
|
||||
if (!pw) {
|
||||
showError("unlock-error", "enter a password");
|
||||
return;
|
||||
}
|
||||
hideError("unlock-error");
|
||||
// TODO: send unlock message to background
|
||||
state.locked = false;
|
||||
updateAccountSelector();
|
||||
updateTokenList();
|
||||
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");
|
||||
});
|
||||
|
||||
// -- 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");
|
||||
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");
|
||||
});
|
||||
|
||||
$("btn-create-back").addEventListener("click", () => {
|
||||
showView("setup");
|
||||
});
|
||||
|
||||
// -- Import wallet --
|
||||
$("btn-import-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");
|
||||
return;
|
||||
}
|
||||
if (!pw) {
|
||||
showError("import-error", "enter a password");
|
||||
return;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError("import-error", "passwords do not match");
|
||||
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");
|
||||
});
|
||||
|
||||
$("btn-import-back").addEventListener("click", () => {
|
||||
showView("setup");
|
||||
});
|
||||
|
||||
// -- Main view --
|
||||
$("btn-lock").addEventListener("click", () => {
|
||||
state.locked = true;
|
||||
$("unlock-password").value = "";
|
||||
showView("lock");
|
||||
});
|
||||
|
||||
$("btn-settings").addEventListener("click", () => {
|
||||
$("settings-rpc").value = state.rpcUrl;
|
||||
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-copy-address").addEventListener("click", () => {
|
||||
const addr = state.accounts[state.selectedAccount]?.address;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-send").addEventListener("click", () => {
|
||||
updateSendTokenSelect();
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-gas-estimate").classList.add("hidden");
|
||||
$("send-status").classList.add("hidden");
|
||||
showView("send");
|
||||
});
|
||||
|
||||
$("btn-receive").addEventListener("click", () => {
|
||||
$("receive-address").textContent =
|
||||
state.accounts[state.selectedAccount]?.address || "";
|
||||
showView("receive");
|
||||
});
|
||||
|
||||
$("btn-add-token").addEventListener("click", () => {
|
||||
$("add-token-address").value = "";
|
||||
$("add-token-info").classList.add("hidden");
|
||||
hideError("add-token-error");
|
||||
showView("add-token");
|
||||
});
|
||||
|
||||
// -- Send --
|
||||
$("btn-send-confirm").addEventListener("click", () => {
|
||||
const to = $("send-to").value.trim();
|
||||
const amount = $("send-amount").value.trim();
|
||||
if (!to) {
|
||||
showError("send-status", "enter a recipient address");
|
||||
$("send-status").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
if (!amount || isNaN(parseFloat(amount))) {
|
||||
showError("send-status", "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.classList.remove("hidden");
|
||||
});
|
||||
|
||||
$("btn-send-back").addEventListener("click", () => {
|
||||
showView("main");
|
||||
});
|
||||
|
||||
// -- Receive --
|
||||
$("btn-receive-copy").addEventListener("click", () => {
|
||||
const addr = $("receive-address").textContent;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-receive-back").addEventListener("click", () => {
|
||||
showView("main");
|
||||
});
|
||||
|
||||
// -- 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");
|
||||
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");
|
||||
});
|
||||
|
||||
$("btn-add-token-back").addEventListener("click", () => {
|
||||
showView("main");
|
||||
});
|
||||
|
||||
// -- Settings --
|
||||
$("btn-save-rpc").addEventListener("click", () => {
|
||||
state.rpcUrl = $("settings-rpc").value.trim();
|
||||
// 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();
|
||||
showView("main");
|
||||
});
|
||||
|
||||
// -- Approval --
|
||||
$("btn-approve").addEventListener("click", () => {
|
||||
// TODO: send approval to background
|
||||
showView("main");
|
||||
});
|
||||
|
||||
$("btn-reject").addEventListener("click", () => {
|
||||
// TODO: send rejection to background
|
||||
showView("main");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
Reference in New Issue
Block a user