website: redesign part 32

This commit is contained in:
nym21
2026-06-19 23:16:27 +02:00
parent 00f7d69ea6
commit 45ab6ebf71
28 changed files with 234 additions and 264 deletions
+1
View File
@@ -116,6 +116,7 @@
<link rel="stylesheet" href="/learn/contents/style.css" />
<link rel="stylesheet" href="/build/style.css" />
<link rel="stylesheet" href="/wallets/style.css" />
<link rel="stylesheet" href="/wallets/amount/style.css" />
<link rel="stylesheet" href="/wallets/layout/style.css" />
<link rel="stylesheet" href="/wallets/form/style.css" />
<link rel="stylesheet" href="/wallets/dialog/style.css" />
+5 -5
View File
@@ -20,8 +20,8 @@ function createSourceInput() {
const input = document.createElement("input");
input.name = "source";
input.type = redaction.isHidden() ? "password" : "text";
input.setAttribute("data-wallets-private-input", "");
input.type = "text";
redaction.setInput(input);
input.autocomplete = "off";
input.placeholder = "xpub or descriptor...";
input.required = true;
@@ -38,10 +38,10 @@ export function createAddForm(options) {
const title = document.createElement("h2");
const name = document.createElement("input");
const source = createSourceInput();
const actions = document.createElement("div");
const actions = document.createElement("footer");
const cancel = document.createElement("button");
const submit = document.createElement("button");
const status = document.createElement("p");
const status = document.createElement("output");
const fields = [
createField("name", name),
createField("xpub or descriptor", source),
@@ -55,8 +55,8 @@ export function createAddForm(options) {
cancel.type = "button";
cancel.append("Cancel");
submit.type = "submit";
submit.classList.add("primary");
submit.append("Add");
status.setAttribute("role", "status");
actions.append(cancel, submit);
form.append(
title,
+1 -7
View File
@@ -4,17 +4,11 @@ main.wallets {
display: grid;
gap: 0.75rem;
> div {
> footer {
display: flex;
gap: 0.5rem;
justify-content: end;
}
button[type="submit"] {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
}
}
}
+28 -30
View File
@@ -3,10 +3,19 @@ import { redaction } from "../redaction/index.js";
const SATS_PER_BTC = 100_000_000;
const FRACTION_DIGITS = 8;
const FIXED_PRIVATE_TEXT = "*****";
const amounts = /** @type {BtcAmountRecord[]} */ ([]);
/**
* @typedef {Object} BtcAmountOptions
* @property {boolean} [signed]
*
* @typedef {Object} BtcAmount
* @property {number} sats
* @property {boolean} signed
*
* @typedef {Object} BtcAmountRecord
* @property {HTMLElement} element
* @property {BtcAmount} amount
*/
/**
@@ -126,30 +135,21 @@ function getBtcParts(sats, options = {}) {
return parts;
}
/**
* @param {number} sats
* @param {BtcAmountOptions} [options]
*/
export function formatBtc(sats, options = {}) {
return getBtcParts(sats, options).map((part) => part.text).join("");
}
/**
* @param {HTMLElement} element
* @param {number} sats
* @param {BtcAmountOptions} [options]
* @param {BtcAmount} amount
*/
function renderBtcAmount(element, sats, options = {}) {
function renderBtcAmount(element, amount) {
if (redaction.isHidden()) {
element.textContent = FIXED_PRIVATE_TEXT;
return;
}
element.replaceChildren(...getBtcParts(sats, options).map((part) => {
element.replaceChildren(...getBtcParts(amount.sats, amount).map((part) => {
const span = document.createElement("span");
if (part.muted) {
span.setAttribute("data-wallets-btc-muted", "");
span.classList.add("muted");
}
span.append(part.text);
@@ -165,28 +165,26 @@ function renderBtcAmount(element, sats, options = {}) {
*/
export function createBtcAmount(tag, sats, options = {}) {
const element = document.createElement(tag);
const amount = {
sats,
signed: options.signed === true,
};
element.setAttribute("data-wallets-btc-amount", String(sats));
element.setAttribute(
"data-wallets-btc-signed",
options.signed ? "true" : "false",
);
renderBtcAmount(element, sats, options);
element.classList.add("wallets__amount");
amounts.push({ element, amount });
renderBtcAmount(element, amount);
return element;
}
/**
* @param {HTMLElement} root
*/
export function syncBtcAmounts(root) {
const amounts = root.querySelectorAll("[data-wallets-btc-amount]");
export function syncBtcAmounts() {
for (let index = amounts.length - 1; index >= 0; index -= 1) {
const { element, amount } = amounts[index];
for (const amount of amounts) {
const element = /** @type {HTMLElement} */ (amount);
const sats = Number(element.getAttribute("data-wallets-btc-amount"));
const signed = element.getAttribute("data-wallets-btc-signed") === "true";
renderBtcAmount(element, sats, { signed });
if (!element.isConnected) {
amounts.splice(index, 1);
} else {
renderBtcAmount(element, amount);
}
}
}
+7
View File
@@ -0,0 +1,7 @@
main.wallets {
.wallets__amount {
.muted {
color: color-mix(in oklch, currentColor 45%, transparent);
}
}
}
+1
View File
@@ -15,6 +15,7 @@ export function createEmpty(options) {
text.append("No wallet imported yet");
button.type = "button";
button.classList.add("primary");
button.append("Add wallet");
button.addEventListener("click", options.onAdd);
empty.append(text, button);
+1 -7
View File
@@ -3,17 +3,11 @@ main.wallets {
display: grid;
gap: 1rem;
place-content: center;
min-height: 16rem;
min-height: calc(100dvh - 2 * var(--offset));
text-align: center;
p {
margin: 0;
}
> button {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
}
}
+2 -5
View File
@@ -1,5 +1,4 @@
import { brk } from "../utils/client.js";
import { createGroupedAddress } from "./wallet/address/index.js";
import {
setStatus,
withBusy,
@@ -81,8 +80,8 @@ export function createWalletsPage() {
}
privacyButton.addEventListener("click", () => {
redaction.toggle(main, privacyButton, createGroupedAddress);
syncBtcAmounts(main);
redaction.toggle(privacyButton);
syncBtcAmounts();
});
lockButton.addEventListener("click", () => {
@@ -209,8 +208,6 @@ export function createWalletsPage() {
const current = vault.current();
const empty = !needsSetup && !locked && !current;
main.toggleAttribute("data-wallets-page-locked", locked || needsSetup);
main.toggleAttribute("data-wallets-page-empty", empty);
header.hidden = locked || needsSetup || empty;
selectorElement.hidden = locked || needsSetup || empty;
lockButton.hidden = locked || needsSetup || !vault.hasPassword;
+6 -3
View File
@@ -19,19 +19,22 @@ import { createElement } from "../dom.js";
export function createLayout() {
const main = createElement("main", "wallets");
const header = document.createElement("header");
const actions = document.createElement("div");
const actions = document.createElement("menu");
const addButton = document.createElement("button");
const privacyButton = document.createElement("button");
const lockButton = document.createElement("button");
const selector = createElement("section", "wallets__selector");
const walletList = document.createElement("div");
const content = document.createElement("section");
const walletList = document.createElement("nav");
const content = document.createElement("article");
const addDialog = createElement("dialog", "wallets__dialog");
addButton.type = "button";
addButton.classList.add("primary");
addButton.append("Add watch-only wallet");
privacyButton.type = "button";
privacyButton.classList.add("primary");
lockButton.type = "button";
lockButton.classList.add("primary");
lockButton.append("Lock");
content.setAttribute("aria-live", "polite");
walletList.setAttribute("tabindex", "0");
+5 -8
View File
@@ -9,25 +9,22 @@ main.wallets {
justify-content: start;
}
> div {
> menu {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: end;
margin: 0;
padding: 0;
list-style: none;
@media (max-width: 34rem) {
justify-content: start;
}
> button {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
}
}
> section[aria-live] {
> article {
display: grid;
gap: 1.5rem;
}
+5 -5
View File
@@ -21,16 +21,16 @@ function bindResetHold(button, onReset) {
clearTimeout(timer);
timer = undefined;
button.removeAttribute("data-wallets-holding");
button.classList.remove("holding");
}
function start() {
if (timer !== undefined) return;
button.setAttribute("data-wallets-holding", "");
button.classList.add("holding");
timer = window.setTimeout(() => {
timer = undefined;
button.removeAttribute("data-wallets-holding");
button.classList.remove("holding");
onReset();
}, RESET_HOLD_MS);
}
@@ -68,7 +68,7 @@ export function createLock(options) {
const password = document.createElement("input");
const button = document.createElement("button");
const reset = document.createElement("button");
const status = document.createElement("p");
const status = document.createElement("output");
title.append("Unlock vault");
password.name = "password";
@@ -78,10 +78,10 @@ export function createLock(options) {
password.placeholder = "Password";
password.required = true;
button.type = "submit";
button.classList.add("primary");
button.append("Unlock");
reset.type = "button";
reset.append("Reset vault");
status.setAttribute("role", "status");
form.append(password, button);
form.addEventListener("submit", (event) => {
event.preventDefault();
+5 -10
View File
@@ -3,7 +3,9 @@ main.wallets {
display: grid;
gap: 1rem;
place-content: center;
min-height: 16rem;
width: min(100%, 28rem);
min-height: calc(100dvh - 2 * var(--offset));
margin-inline: auto;
text-align: center;
> h1 {
@@ -15,20 +17,13 @@ main.wallets {
> form {
display: grid;
grid-template-columns: minmax(12rem, 18rem) auto;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: end;
justify-content: center;
@media (max-width: 34rem) {
grid-template-columns: 1fr;
}
> button {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
}
> button {
@@ -49,7 +44,7 @@ main.wallets {
transform-origin: left;
}
&[data-wallets-holding]::before {
&.holding::before {
transform: scaleX(1);
transition: transform 2s linear;
}
-7
View File
@@ -83,13 +83,6 @@ function rapidHashV3(bytes) {
let a = readU64(bytes, length - 16) ^ BigInt(length);
let b = readU64(bytes, length - 8);
if (length > 32) {
seed = rapidMix(
readU64(bytes, 16) ^ DEFAULT_SECRETS[2],
readU64(bytes, 24) ^ seed,
);
}
a ^= DEFAULT_SECRETS[1];
b ^= seed;
+58 -51
View File
@@ -1,6 +1,15 @@
const FIXED_PRIVATE_TEXT = "*****";
let hidden = false;
const effects = /** @type {RedactionEffect[]} */ ([]);
/**
* @typedef {"exact" | "fixed"} RedactionMode
*
* @typedef {Object} RedactionEffect
* @property {HTMLElement} element
* @property {() => void} sync
*/
function isHidden() {
return hidden;
@@ -17,23 +26,30 @@ function createText(value) {
/**
* @param {string} value
* @param {string | null} mode
* @param {RedactionMode} mode
*/
function mask(value, mode) {
return mode === "fixed" ? FIXED_PRIVATE_TEXT : createText(value);
}
/**
* @param {HTMLElement} element
* @param {() => void} sync
*/
function addEffect(element, sync) {
effects.push({ element, sync });
sync();
}
/**
* @param {HTMLElement} element
* @param {string} value
* @param {"exact" | "fixed"} [mode]
* @param {RedactionMode} [mode]
*/
function setValue(element, value, mode = "exact") {
element.setAttribute("data-wallets-private-value", value);
element.setAttribute("data-wallets-private-mode", mode);
element.textContent = hidden
? mask(value, mode)
: value;
addEffect(element, () => {
element.textContent = hidden ? mask(value, mode) : value;
});
}
/**
@@ -41,15 +57,36 @@ function setValue(element, value, mode = "exact") {
* @param {string} value
*/
function setTitle(element, value) {
element.setAttribute("data-wallets-private-title", value);
element.title = hidden ? createText(value) : value;
addEffect(element, () => {
element.title = hidden ? createText(value) : value;
});
}
/**
* @param {HTMLElement} element
* @param {string} value
* @param {(text: string) => void} render
*/
function setAddress(element, value, render) {
addEffect(element, () => {
render(hidden ? createText(value) : value);
});
}
/**
* @param {HTMLInputElement} input
*/
function setInput(input) {
addEffect(input, () => {
input.type = hidden ? "password" : "text";
});
}
/**
* @template {keyof HTMLElementTagNameMap} Tag
* @param {Tag} tag
* @param {string} value
* @param {"exact" | "fixed"} [mode]
* @param {RedactionMode} [mode]
*/
function createValue(tag, value, mode = "exact") {
const element = document.createElement(tag);
@@ -59,44 +96,14 @@ function createValue(tag, value, mode = "exact") {
return element;
}
/**
* @param {HTMLElement} root
* @param {(text: string) => HTMLElement} createAddress
*/
function sync(root, createAddress) {
const values = root.querySelectorAll("[data-wallets-private-value]");
const titles = root.querySelectorAll("[data-wallets-private-title]");
const addresses = root.querySelectorAll("[data-wallets-private-address]");
const inputs = root.querySelectorAll("[data-wallets-private-input]");
function sync() {
for (let index = effects.length - 1; index >= 0; index -= 1) {
const effect = effects[index];
for (const value of values) {
const text = value.getAttribute("data-wallets-private-value") ?? "";
const mode = value.getAttribute("data-wallets-private-mode");
value.textContent = hidden
? mask(text, mode)
: text;
}
for (const element of titles) {
const title = /** @type {HTMLElement} */ (element);
const text = title.getAttribute("data-wallets-private-title") ?? "";
title.title = hidden
? createText(text)
: text;
}
for (const address of addresses) {
const text = address.getAttribute("data-wallets-private-address") ?? "";
const next = hidden ? createText(text) : text;
address.replaceChildren(...createAddress(next).childNodes);
}
for (const input of inputs) {
if (input instanceof HTMLInputElement) {
input.type = hidden ? "password" : "text";
if (!effect.element.isConnected) {
effects.splice(index, 1);
} else {
effect.sync();
}
}
}
@@ -110,13 +117,11 @@ function syncButton(button) {
}
/**
* @param {HTMLElement} root
* @param {HTMLButtonElement} button
* @param {(text: string) => HTMLElement} createAddress
*/
function toggle(root, button, createAddress) {
function toggle(button) {
hidden = !hidden;
sync(root, createAddress);
sync();
syncButton(button);
}
@@ -125,6 +130,8 @@ export const redaction = /** @type {const} */ ({
createText,
setValue,
setTitle,
setAddress,
setInput,
createValue,
syncButton,
toggle,
+21 -11
View File
@@ -8,14 +8,21 @@
* @typedef {Object} WalletSelectorOptions
* @property {() => string} getSelectedId
* @property {(walletId: string) => void} onSelect
*
* @typedef {Object} WalletSelectorButton
* @property {HTMLButtonElement} button
* @property {string} id
*/
/**
* @param {HTMLElement} walletList
* @param {StoredWallet[]} wallets
* @param {WalletSelectorOptions} options
* @returns {WalletSelectorButton[]}
*/
function renderButtons(walletList, wallets, options) {
const buttons = /** @type {WalletSelectorButton[]} */ ([]);
walletList.replaceChildren();
for (const wallet of wallets) {
@@ -24,13 +31,15 @@ function renderButtons(walletList, wallets, options) {
button.type = "button";
button.setAttribute("aria-pressed", selected ? "true" : "false");
button.setAttribute("data-wallet-id", wallet.id);
button.append(wallet.name);
button.addEventListener("click", () => {
options.onSelect(wallet.id);
});
buttons.push({ button, id: wallet.id });
walletList.append(button);
}
return buttons;
}
/**
@@ -38,29 +47,29 @@ function renderButtons(walletList, wallets, options) {
* @param {WalletSelectorOptions} options
*/
export function createSelector(walletList, options) {
function selectSnappedWallet() {
const buttons = [...walletList.querySelectorAll("button")];
/** @type {WalletSelectorButton[]} */
let buttons = [];
function selectSnappedWallet() {
if (buttons.length === 0) return;
const listRect = walletList.getBoundingClientRect();
const listCenter = listRect.left + listRect.width / 2;
const closest = buttons.reduce((best, button) => {
const rect = button.getBoundingClientRect();
const closest = buttons.reduce((best, item) => {
const rect = item.button.getBoundingClientRect();
const center = rect.left + rect.width / 2;
const distance = Math.abs(center - listCenter);
return distance < best.distance
? { button, distance }
? { item, distance }
: best;
}, {
button: buttons[0],
item: buttons[0],
distance: Number.POSITIVE_INFINITY,
});
const id = closest.button.getAttribute("data-wallet-id");
if (id && id !== options.getSelectedId()) {
options.onSelect(id);
if (closest.item.id !== options.getSelectedId()) {
options.onSelect(closest.item.id);
}
}
@@ -90,12 +99,13 @@ export function createSelector(walletList, options) {
return {
clear() {
walletList.replaceChildren();
buttons = [];
},
/**
* @param {StoredWallet[]} wallets
*/
render(wallets) {
renderButtons(walletList, wallets, options);
buttons = renderButtons(walletList, wallets, options);
},
};
}
+1 -1
View File
@@ -2,7 +2,7 @@ main.wallets {
.wallets__selector {
min-width: 0;
> div {
> nav {
display: flex;
gap: 1rem;
min-width: 0;
+3 -3
View File
@@ -22,11 +22,11 @@ function createDescriptionText(text) {
export function createSetup(options) {
const section = createElement("section", "wallets__setup");
const title = document.createElement("h1");
const description = document.createElement("div");
const description = document.createElement("article");
const form = document.createElement("form");
const password = document.createElement("input");
const button = document.createElement("button");
const status = document.createElement("p");
const status = document.createElement("output");
title.append("Wallets");
description.append(
@@ -49,8 +49,8 @@ export function createSetup(options) {
password.placeholder = "Set password";
password.required = true;
button.type = "submit";
button.classList.add("primary");
button.append("Continue");
status.setAttribute("role", "status");
form.append(password, button);
form.addEventListener("submit", (event) => {
event.preventDefault();
+3 -10
View File
@@ -3,8 +3,8 @@ main.wallets {
display: grid;
gap: 1rem;
place-content: center;
max-width: 36rem;
min-height: 16rem;
width: min(100%, 36rem);
min-height: calc(100dvh - 2 * var(--offset));
margin-inline: auto;
h1 {
@@ -27,7 +27,7 @@ main.wallets {
margin: 0;
}
> div {
> article {
display: grid;
gap: 0.75rem;
color: var(--gray);
@@ -41,18 +41,11 @@ main.wallets {
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: end;
justify-content: center;
width: 100%;
@media (max-width: 34rem) {
grid-template-columns: 1fr;
}
> button {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
}
}
}
+8 -10
View File
@@ -11,12 +11,8 @@ main.wallets {
padding: var(--offset) var(--page-x);
scroll-padding-top: var(--offset);
&:is([data-wallets-page-empty], [data-wallets-page-locked]) {
min-height: 100dvh;
align-content: center;
}
[role="status"] {
output {
display: block;
min-height: var(--line-height-sm);
margin: 0;
color: var(--gray);
@@ -24,10 +20,6 @@ main.wallets {
line-height: var(--line-height-sm);
}
[data-wallets-btc-muted] {
color: color-mix(in oklch, currentColor 45%, transparent);
}
:is(input, select, button) {
min-width: 0;
height: var(--control-height);
@@ -49,6 +41,12 @@ main.wallets {
outline-offset: 2px;
}
button.primary {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
}
input::placeholder {
color: color-mix(in oklch, var(--gray) 70%, transparent);
}
+5 -12
View File
@@ -9,18 +9,11 @@ main.wallets {
@media (max-width: 34rem) {
justify-content: start;
}
> button {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
&:disabled {
border-color: color-mix(in oklch, var(--gray) 35%, transparent);
color: var(--gray);
background: transparent;
cursor: default;
}
> button:disabled {
border-color: color-mix(in oklch, var(--gray) 35%, transparent);
color: var(--gray);
background: transparent;
cursor: default;
}
}
}
+13 -16
View File
@@ -17,15 +17,14 @@ export function createGroupedAddress(text) {
const group = document.createElement("span");
for (const character of groups[groupIndex]) {
const span = document.createElement("span");
if (Number.isNaN(Number(character))) {
group.append(character);
} else {
const number = document.createElement("var");
span.setAttribute(
"data-wallets-address-character",
Number.isNaN(Number(character)) ? "letter" : "number",
);
span.append(character);
group.append(span);
number.append(character);
group.append(number);
}
}
element.append(group);
@@ -41,12 +40,11 @@ export function createGroupedAddress(text) {
* @param {string} address
*/
function createPrivateAddress(address) {
const hidden = redaction.createText(address);
const element = redaction.isHidden()
? createGroupedAddress(hidden)
: createGroupedAddress(address);
const element = createGroupedAddress(address);
element.setAttribute("data-wallets-private-address", address);
redaction.setAddress(element, address, (text) => {
element.replaceChildren(...createGroupedAddress(text).childNodes);
});
return element;
}
@@ -55,10 +53,9 @@ function createPrivateAddress(address) {
* @param {WalletAddress} row
*/
function createAddressBadge(row) {
const badge = document.createElement("span");
const badge = document.createElement("b");
const label = row.branchLabel?.toLowerCase() ?? "address";
badge.setAttribute("data-wallets-address-branch", label);
badge.append(label, ` #${formatNumber(row.index)}`);
return badge;
@@ -69,7 +66,7 @@ function createAddressBadge(row) {
*/
export function createAddressCellContent(row) {
const element = createElement("div", "wallets__address-cell");
const anonSet = document.createElement("span");
const anonSet = document.createElement("small");
anonSet.append(`anon set: ${formatNumber(row.historyBucketSize)}`);
element.append(
@@ -3,7 +3,7 @@ main.wallets {
display: grid;
gap: 0.25rem;
> span:first-child {
> b {
display: inline-flex;
align-items: center;
justify-self: start;
@@ -12,10 +12,11 @@ main.wallets {
border-radius: 0.25rem;
padding: 0 0.25rem;
color: color-mix(in oklch, var(--white) 76%, var(--gray));
font-weight: 400;
line-height: 1;
}
> span:last-child {
> small {
color: var(--gray);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
@@ -29,15 +30,13 @@ main.wallets {
max-width: 40rem;
> span {
white-space: nowrap;
}
[data-wallets-address-character="letter"] {
color: var(--white);
}
white-space: nowrap;
[data-wallets-address-character="number"] {
color: color-mix(in oklch, var(--white) 50%, var(--gray));
> var {
color: color-mix(in oklch, var(--white) 50%, var(--gray));
font-style: normal;
}
}
}
}
+1 -2
View File
@@ -22,11 +22,10 @@ import { renderTransactions } from "./transactions/index.js";
export function createWalletPanel() {
const actions = createElement("section", "wallets__wallet-actions");
const summary = createElement("section", "wallets__summary");
const status = document.createElement("p");
const status = document.createElement("output");
const results = createElement("section", "wallets__results");
actions.setAttribute("aria-label", "Wallet actions");
status.setAttribute("role", "status");
summary.setAttribute("aria-label", "Wallets summary");
results.setAttribute("aria-label", "Wallets results");
+14 -12
View File
@@ -56,7 +56,7 @@ function createReceiveQr(receiveAddress) {
* @param {ReceiveAddress} receiveAddress
*/
function createReceiveAddress(receiveAddress) {
const element = document.createElement("div");
const element = document.createElement("p");
element.append(createGroupedAddress(receiveAddress.address));
@@ -73,24 +73,28 @@ async function copyReceiveAddress(receiveAddress, copy) {
}
/**
* @param {HTMLElement} host
* @param {ReceiveAddress} receiveAddress
*/
function openReceiveDialog(receiveAddress) {
const main = document.querySelector("main.wallets") ?? document.body;
function openReceiveDialog(host, receiveAddress) {
const dialog = createElement(
"dialog",
"wallets__dialog wallets__receive-dialog",
);
const content = document.createElement("div");
const actions = document.createElement("div");
const content = document.createElement("article");
const actions = document.createElement("footer");
const copy = document.createElement("button");
const closeForm = document.createElement("form");
const close = document.createElement("button");
copy.type = "button";
copy.classList.add("primary");
copy.append("Copy");
close.type = "button";
closeForm.method = "dialog";
close.type = "submit";
close.append("Close");
actions.append(copy, close);
closeForm.append(close);
actions.append(copy, closeForm);
content.append(
createReceiveTitle(receiveAddress),
createReceiveQr(receiveAddress),
@@ -98,16 +102,13 @@ function openReceiveDialog(receiveAddress) {
actions,
);
dialog.append(content);
main.append(dialog);
host.append(dialog);
copy.addEventListener("click", () => {
void copyReceiveAddress(receiveAddress, copy).catch(() => {
copy.textContent = "Copy failed";
});
});
close.addEventListener("click", () => {
dialog.close();
});
dialog.addEventListener("close", () => {
dialog.remove();
});
@@ -128,10 +129,11 @@ export function renderReceiveButton(element, receiveAddress) {
button.type = "button";
button.disabled = !receiveAddress;
button.classList.add("primary");
button.append("Receive");
button.addEventListener("click", () => {
if (receiveAddress) {
openReceiveDialog(receiveAddress);
openReceiveDialog(element, receiveAddress);
}
});
element.append(button);
@@ -2,11 +2,11 @@ main.wallets {
.wallets__receive-dialog {
width: min(100% - 2rem, 32rem);
> div {
> article {
display: grid;
gap: 1rem;
h2 {
> h2 {
margin: 0;
font-size: var(--font-size-lg);
font-weight: 400;
@@ -22,22 +22,20 @@ main.wallets {
image-rendering: pixelated;
}
> div:first-of-type {
> p {
margin: 0;
color: var(--white);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
> div:last-of-type {
> footer {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: end;
> button:first-child {
border-color: var(--orange);
color: var(--black);
background: var(--orange);
> form {
margin: 0;
}
}
}
@@ -18,8 +18,6 @@ const historyByBucketKey =
/**
* @typedef {Object} AddressHistory
* @property {unknown[]} transactions
* @property {number} fetchedAddressCount
* @property {number} bucketSize
*/
/**
@@ -29,17 +27,6 @@ function createBucketKey(addresses) {
return [...addresses].sort().join("\n");
}
/**
* @param {WalletAddress} address
*/
function assertHistoryIsReasonable(address) {
if (address.txCount > MAX_SELECTED_ADDRESS_TXS) {
throw new Error(
`History disabled for addresses over ${MAX_SELECTED_ADDRESS_TXS} transactions`,
);
}
}
/**
* @param {AddressHistoryClient} client
* @param {readonly string[]} addresses
@@ -67,13 +54,12 @@ async function fetchBucketHistory(client, addresses) {
* @returns {Promise<AddressHistory>}
*/
async function load(client, address) {
assertHistoryIsReasonable(address);
if (address.historyAddresses.length === 0) {
if (
address.txCount > MAX_SELECTED_ADDRESS_TXS ||
address.historyAddresses.length === 0
) {
return {
transactions: [],
fetchedAddressCount: 0,
bucketSize: address.historyBucketSize,
};
}
@@ -94,8 +80,6 @@ async function load(client, address) {
return {
transactions: bucketHistory.get(address.address) ?? [],
fetchedAddressCount: address.historyAddresses.length,
bucketSize: address.historyBucketSize,
};
}
@@ -47,10 +47,10 @@ function appendTransactionDetail(element, transaction) {
* @param {WalletTransaction} transaction
*/
function createTransactionDetails(transaction) {
const content = document.createElement("div");
const content = document.createElement("section");
const txid = document.createElement("code");
const meta = document.createElement("p");
const list = document.createElement("div");
const list = document.createElement("ul");
redaction.setTitle(txid, transaction.txid);
redaction.setValue(txid, transaction.txid);
@@ -62,7 +62,10 @@ function createTransactionDetails(transaction) {
createBtcAmount("span", transaction.fee),
);
for (const address of transaction.addresses) {
list.append(createAddressCellContent(address.walletAddress));
const item = document.createElement("li");
item.append(createAddressCellContent(address.walletAddress));
list.append(item);
}
content.append(txid, meta, list);
@@ -74,7 +77,7 @@ function createTransactionDetails(transaction) {
*/
function createTransactionRow(transaction) {
const row = document.createElement("li");
const main = document.createElement("div");
const main = document.createElement("header");
const label = document.createElement("strong");
const amount = createBtcAmount(
"span",
@@ -87,8 +90,12 @@ function createTransactionRow(transaction) {
const summary = document.createElement("summary");
label.append(typeLabels[transaction.type]);
amount.dataset.walletsTxAmount =
transaction.amount >= 0 ? "positive" : "negative";
if (transaction.amount > 0) {
amount.classList.add("positive");
}
if (transaction.amount < 0) {
amount.classList.add("negative");
}
redaction.setTitle(txid, transaction.txid);
redaction.setValue(txid, formatTxid(transaction.txid));
summary.append("Details");
@@ -55,7 +55,7 @@ main.wallets {
grid-template-columns: 1fr;
}
> div:first-child {
> header {
display: flex;
gap: 1rem;
align-items: baseline;
@@ -71,11 +71,11 @@ main.wallets {
color: var(--white);
white-space: nowrap;
&[data-wallets-tx-amount="positive"] {
&.positive {
color: var(--green);
}
&[data-wallets-tx-amount="negative"] {
&.negative {
color: var(--red);
}
}
@@ -135,7 +135,7 @@ main.wallets {
}
}
> div {
> section {
display: grid;
gap: 1rem;
@@ -145,9 +145,12 @@ main.wallets {
font-family: inherit;
}
> div {
> ul {
display: grid;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
}
}