mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-21 20:12:15 -07:00
website: redesign part 32
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
main.wallets {
|
||||
.wallets__amount {
|
||||
.muted {
|
||||
color: color-mix(in oklch, currentColor 45%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ main.wallets {
|
||||
.wallets__selector {
|
||||
min-width: 0;
|
||||
|
||||
> div {
|
||||
> nav {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user