Add basic monochrome popup UI with Tailwind CSS
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:
2026-02-24 10:12:19 +07:00
parent 065f0eaa81
commit d9eda1d503
8 changed files with 1333 additions and 40 deletions

View File

@@ -4,10 +4,471 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AutistMask</title>
<link rel="stylesheet" href="styles/main.css" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app"></div>
<body class="bg-bg text-fg font-mono text-sm">
<div id="app" class="p-2">
<!-- ============ LOCK SCREEN ============ -->
<div id="view-lock" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [locked]
</h1>
<div class="mb-2">
<label class="block mb-1">Password:</label>
<input
type="password"
id="unlock-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="enter password to unlock"
/>
</div>
<button
id="btn-unlock"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
unlock
</button>
<div
id="unlock-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ SETUP: WELCOME ============ -->
<div id="view-setup" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [setup]
</h1>
<p class="mb-3">No wallet found. Create or import one.</p>
<div class="flex gap-2">
<button
id="btn-setup-create"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
create new wallet
</button>
<button
id="btn-setup-import"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import seed phrase
</button>
</div>
</div>
<!-- ============ SETUP: CREATE ============ -->
<div id="view-create" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [create wallet]
</h1>
<p class="mb-2">
Write down this seed phrase and store it safely. It is the
only way to recover your wallet.
</p>
<div
id="create-mnemonic"
class="border border-border p-2 mb-3 font-mono break-all select-all"
></div>
<div class="mb-2">
<label class="block mb-1">Set password:</label>
<input
type="password"
id="create-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/>
</div>
<div class="mb-2">
<label class="block mb-1">Confirm password:</label>
<input
type="password"
id="create-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/>
</div>
<div class="flex gap-2">
<button
id="btn-create-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
i backed it up, create wallet
</button>
<button
id="btn-create-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
<div
id="create-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ SETUP: IMPORT ============ -->
<div id="view-import" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [import wallet]
</h1>
<div class="mb-2">
<label class="block mb-1"
>Seed phrase (12 or 24 words):</label
>
<textarea
id="import-mnemonic"
rows="3"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg resize-y"
placeholder="word1 word2 word3 ..."
></textarea>
</div>
<div class="mb-2">
<label class="block mb-1">Set password:</label>
<input
type="password"
id="import-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/>
</div>
<div class="mb-2">
<label class="block mb-1">Confirm password:</label>
<input
type="password"
id="import-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/>
</div>
<div class="flex gap-2">
<button
id="btn-import-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import wallet
</button>
<button
id="btn-import-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
<div
id="import-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ MAIN / ACCOUNT VIEW ============ -->
<div id="view-main" class="view hidden">
<div
class="flex justify-between items-center border-b border-border pb-1 mb-2"
>
<h1 class="font-bold">AutistMask</h1>
<div class="flex gap-2">
<button
id="btn-settings"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="settings"
>
[=]
</button>
<button
id="btn-lock"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="lock"
>
[x]
</button>
</div>
</div>
<!-- account selector -->
<div class="mb-2">
<div class="flex justify-between items-center">
<select
id="account-selector"
class="border border-border p-1 font-mono text-sm bg-bg text-fg flex-1 mr-2"
></select>
<button
id="btn-copy-address"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
title="copy address"
>
[cp]
</button>
</div>
<div
id="current-address"
class="text-xs text-muted mt-1 break-all"
></div>
</div>
<!-- balance -->
<div class="border-b border-border-light pb-2 mb-2">
<div class="text-base font-bold">
<span id="eth-balance">0.0000</span> ETH
</div>
</div>
<!-- action buttons -->
<div class="flex gap-2 mb-3">
<button
id="btn-send"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
>
send
</button>
<button
id="btn-receive"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
>
receive
</button>
</div>
<!-- tokens -->
<div>
<div
class="flex justify-between items-center border-b border-border pb-1 mb-1"
>
<h2 class="font-bold">Tokens</h2>
<button
id="btn-add-token"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
>
[+]
</button>
</div>
<div id="token-list">
<div class="text-muted text-xs py-1">
no tokens added
</div>
</div>
</div>
</div>
<!-- ============ SEND VIEW ============ -->
<div id="view-send" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [send]
</h1>
<div class="mb-2">
<label class="block mb-1">Token:</label>
<select
id="send-token"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
>
<option value="ETH">ETH</option>
</select>
</div>
<div class="mb-2">
<label class="block mb-1">To address:</label>
<input
type="text"
id="send-to"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0x..."
/>
</div>
<div class="mb-2">
<label class="block mb-1">Amount:</label>
<input
type="text"
id="send-amount"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0.0"
/>
</div>
<div
id="send-gas-estimate"
class="text-xs text-muted mb-2 hidden"
></div>
<div class="flex gap-2">
<button
id="btn-send-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
send
</button>
<button
id="btn-send-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
<div
id="send-status"
class="mt-2 border border-border p-1 hidden"
></div>
</div>
<!-- ============ RECEIVE VIEW ============ -->
<div id="view-receive" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [receive]
</h1>
<p class="mb-2">Your address:</p>
<div
id="receive-address"
class="border border-border p-2 font-mono break-all select-all mb-3"
></div>
<div class="flex gap-2">
<button
id="btn-receive-copy"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
copy to clipboard
</button>
<button
id="btn-receive-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
</div>
<!-- ============ ADD TOKEN VIEW ============ -->
<div id="view-add-token" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [add token]
</h1>
<div class="mb-2">
<label class="block mb-1">Token contract address:</label>
<input
type="text"
id="add-token-address"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0x..."
/>
</div>
<div
id="add-token-info"
class="text-xs text-muted mb-2 hidden"
></div>
<div class="flex gap-2">
<button
id="btn-add-token-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
add token
</button>
<button
id="btn-add-token-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
<div
id="add-token-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ SETTINGS VIEW ============ -->
<div id="view-settings" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [settings]
</h1>
<h2 class="font-bold mb-1">RPC Endpoint</h2>
<div class="mb-3">
<input
type="text"
id="settings-rpc"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="https://..."
/>
<button
id="btn-save-rpc"
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
>
save
</button>
</div>
<h2 class="font-bold mb-1">Accounts</h2>
<div class="mb-3">
<button
id="btn-derive-account"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
derive next account
</button>
</div>
<h2 class="font-bold mb-1">Wallets</h2>
<div class="mb-3">
<button
id="btn-show-seed"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
show seed phrase
</button>
<button
id="btn-import-additional"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import another wallet
</button>
</div>
<div
id="settings-seed-display"
class="border border-border p-2 font-mono break-all mb-3 hidden"
></div>
<h2 class="font-bold mb-1">Tokens</h2>
<div id="settings-token-list" class="mb-3">
<div class="text-muted text-xs">no tokens added</div>
</div>
<div class="border-t border-border pt-2">
<button
id="btn-settings-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
</button>
</div>
</div>
<!-- ============ APPROVAL VIEW ============ -->
<div id="view-approve" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [approve request]
</h1>
<div class="mb-2">
<div class="text-xs text-muted mb-1">
Site: <span id="approve-origin"></span>
</div>
<div class="font-bold mb-1" id="approve-type"></div>
</div>
<pre
id="approve-details"
class="border border-border p-2 text-xs overflow-auto mb-3 max-h-64"
></pre>
<div class="flex gap-2">
<button
id="btn-approve"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
approve
</button>
<button
id="btn-reject"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
reject
</button>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>

View File

@@ -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);

View File

@@ -1,18 +1,18 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
@import "tailwindcss";
@theme {
--font-mono:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
--color-bg: #ffffff;
--color-fg: #000000;
--color-muted: #666666;
--color-border: #000000;
--color-border-light: #cccccc;
--color-hover: #eeeeee;
}
body {
width: 360px;
min-height: 600px;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
}
#app {
padding: 16px;
}