diff --git a/website_next/index.html b/website_next/index.html index d6612eff8..8c8aef6c2 100644 --- a/website_next/index.html +++ b/website_next/index.html @@ -123,7 +123,7 @@ - + diff --git a/website_next/wallets/add/index.js b/website_next/wallets/add/index.js index 8c6955e6b..52611fbab 100644 --- a/website_next/wallets/add/index.js +++ b/website_next/wallets/add/index.js @@ -1,4 +1,3 @@ -import { createElement } from "../dom.js"; import { createField } from "../form/index.js"; import { redaction } from "../redaction/index.js"; @@ -35,14 +34,14 @@ function createSourceInput() { * @param {AddWalletFormOptions} options */ export function createAddForm(options) { - const form = createElement("form", "wallets__dialog-form"); + const form = document.createElement("form"); const title = document.createElement("h2"); const name = document.createElement("input"); const source = createSourceInput(); - const actions = createElement("div", "wallets__dialog-actions"); + const actions = document.createElement("div"); const cancel = document.createElement("button"); const submit = document.createElement("button"); - const status = createElement("p", "wallets__status"); + const status = document.createElement("p"); const fields = [ createField("name", name), createField("xpub or descriptor", source), diff --git a/website_next/wallets/add/inference.js b/website_next/wallets/add/inference.js deleted file mode 100644 index 44f7f7eda..000000000 --- a/website_next/wallets/add/inference.js +++ /dev/null @@ -1,49 +0,0 @@ -import { fetchWalletAddresses } from "../lookup/index.js"; -import { - generateAddressesFromKey, - isOutputDescriptor, -} from "../derive/index.js"; -import { parseOutputDescriptor } from "../derive/descriptor.js"; -import { addressScripts } from "../derive/script.js"; - -const RECEIVE_PATH = /** @type {const} */ ([0]); - -/** - * @typedef {import("../derive/address.js").AddressScript} AddressScript - * @typedef {import("../scan/branch.js").AddressClient} AddressClient - * @typedef {import("../lookup/index.js").WalletAddress} WalletAddress - */ - -/** - * @param {WalletAddress} address - */ -function hasHistory(address) { - return address.received > 0 || address.sent > 0 || address.txCount > 0; -} - -/** - * @param {AddressClient} client - * @param {string} source - * @returns {Promise} - */ -export async function inferAddressScript(client, source) { - if (isOutputDescriptor(source)) { - return parseOutputDescriptor(source).script; - } - - for (const { id } of addressScripts) { - const generated = await generateAddressesFromKey(source, { - start: 0, - count: 1, - script: id, - path: RECEIVE_PATH, - }); - const [address] = await fetchWalletAddresses(client, generated); - - if (address && hasHistory(address)) { - return id; - } - } - - return addressScripts[0].id; -} diff --git a/website_next/wallets/add/style.css b/website_next/wallets/add/style.css index 5a4434284..4afa740fd 100644 --- a/website_next/wallets/add/style.css +++ b/website_next/wallets/add/style.css @@ -1,12 +1,20 @@ main.wallets { - .wallets__dialog-form { - display: grid; - gap: 0.75rem; - } + .wallets__dialog { + > form { + display: grid; + gap: 0.75rem; - .wallets__dialog-actions { - display: flex; - gap: 0.5rem; - justify-content: end; + > div { + display: flex; + gap: 0.5rem; + justify-content: end; + } + + button[type="submit"] { + border-color: var(--orange); + color: var(--black); + background: var(--orange); + } + } } } diff --git a/website_next/wallets/amount/index.js b/website_next/wallets/amount/index.js new file mode 100644 index 000000000..9193b9c76 --- /dev/null +++ b/website_next/wallets/amount/index.js @@ -0,0 +1,192 @@ +import { redaction } from "../redaction/index.js"; + +const SATS_PER_BTC = 100_000_000; +const FRACTION_DIGITS = 8; +const FIXED_PRIVATE_TEXT = "*****"; + +/** + * @typedef {Object} BtcAmountOptions + * @property {boolean} [signed] + */ + +/** + * @typedef {Object} BtcPart + * @property {string} text + * @property {boolean} muted + */ + +/** + * @param {BtcPart[]} parts + * @param {string} text + * @param {boolean} muted + */ +function pushPart(parts, text, muted) { + const last = parts[parts.length - 1]; + + if (last && last.muted === muted) { + last.text += text; + return; + } + + parts.push({ text, muted }); +} + +/** + * @param {number} value + */ +function formatInteger(value) { + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +/** + * @param {number} sats + */ +function splitBtc(sats) { + const absolute = Math.abs(sats); + + return { + whole: Math.floor(absolute / SATS_PER_BTC), + fraction: String(absolute % SATS_PER_BTC).padStart(FRACTION_DIGITS, "0"), + }; +} + +/** + * @param {string} fraction + * @param {(index: number) => boolean} isMuted + * @param {(index: number) => boolean} isSpaceMuted + */ +function getFractionParts(fraction, isMuted, isSpaceMuted) { + const parts = /** @type {BtcPart[]} */ ([]); + + for (let index = 0; index < fraction.length; index += 1) { + pushPart(parts, fraction[index], isMuted(index)); + + if (index === 1 || index === 4) { + pushPart(parts, " ", isSpaceMuted(index)); + } + } + + return parts; +} + +/** + * @param {number} sats + * @param {BtcAmountOptions} [options] + */ +function getBtcParts(sats, options = {}) { + const parts = /** @type {BtcPart[]} */ ([]); + const { whole, fraction } = splitBtc(sats); + const firstFractionDigit = fraction.search(/[1-9]/); + const lastFractionDigit = Math.max(...[...fraction].map((digit, index) => { + return digit === "0" ? -1 : index; + })); + + if (options.signed && sats > 0) pushPart(parts, "+", false); + if (sats < 0) pushPart(parts, "-", false); + + pushPart(parts, "₿", true); + + if (whole === 0) { + const mutedUntil = firstFractionDigit === -1 + ? FRACTION_DIGITS + : firstFractionDigit; + + pushPart(parts, "0.", true); + for (const part of getFractionParts( + fraction, + (index) => index < mutedUntil, + (index) => index < mutedUntil, + )) { + pushPart(parts, part.text, part.muted); + } + + return parts; + } + + pushPart(parts, formatInteger(whole), false); + + if (lastFractionDigit === -1) { + pushPart(parts, ".", true); + for (const part of getFractionParts(fraction, () => true, () => true)) { + pushPart(parts, part.text, part.muted); + } + + return parts; + } + + pushPart(parts, ".", false); + for (const part of getFractionParts( + fraction, + (index) => index > lastFractionDigit, + (index) => index >= lastFractionDigit, + )) { + pushPart(parts, part.text, part.muted); + } + + 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] + */ +function renderBtcAmount(element, sats, options = {}) { + if (redaction.isHidden()) { + element.textContent = FIXED_PRIVATE_TEXT; + return; + } + + element.replaceChildren(...getBtcParts(sats, options).map((part) => { + const span = document.createElement("span"); + + if (part.muted) { + span.setAttribute("data-wallets-btc-muted", ""); + } + span.append(part.text); + + return span; + })); +} + +/** + * @template {keyof HTMLElementTagNameMap} Tag + * @param {Tag} tag + * @param {number} sats + * @param {BtcAmountOptions} [options] + */ +export function createBtcAmount(tag, sats, options = {}) { + const element = document.createElement(tag); + + element.setAttribute("data-wallets-btc-amount", String(sats)); + element.setAttribute( + "data-wallets-btc-signed", + options.signed ? "true" : "false", + ); + renderBtcAmount(element, sats, options); + + return element; +} + +/** + * @param {HTMLElement} root + */ +export function syncBtcAmounts(root) { + const amounts = root.querySelectorAll("[data-wallets-btc-amount]"); + + 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 }); + } +} diff --git a/website_next/wallets/dialog/style.css b/website_next/wallets/dialog/style.css index 0320ef0f1..6b62202ad 100644 --- a/website_next/wallets/dialog/style.css +++ b/website_next/wallets/dialog/style.css @@ -10,10 +10,9 @@ main.wallets { h2 { margin: 0; } - } - .wallets__dialog::backdrop { - background: color-mix(in oklch, var(--black) 72%, transparent); + &::backdrop { + background: color-mix(in oklch, var(--black) 72%, transparent); + } } - } diff --git a/website_next/wallets/empty/style.css b/website_next/wallets/empty/style.css index 46cc8b080..ab1fd0b7b 100644 --- a/website_next/wallets/empty/style.css +++ b/website_next/wallets/empty/style.css @@ -6,9 +6,14 @@ main.wallets { min-height: 16rem; text-align: center; - h2, p { margin: 0; } + + > button { + border-color: var(--orange); + color: var(--black); + background: var(--orange); + } } } diff --git a/website_next/wallets/form/index.js b/website_next/wallets/form/index.js index a8959c44c..de352783c 100644 --- a/website_next/wallets/form/index.js +++ b/website_next/wallets/form/index.js @@ -1,12 +1,10 @@ -import { createElement } from "../dom.js"; - /** * @param {string} label * @param {HTMLInputElement | HTMLSelectElement} control */ export function createField(label, control) { - const element = createElement("label", "wallets__field"); - const text = createElement("span", "wallets__label"); + const element = document.createElement("label"); + const text = document.createElement("span"); text.append(label); element.append(text, control); diff --git a/website_next/wallets/form/style.css b/website_next/wallets/form/style.css index 87a628b87..06e2e3892 100644 --- a/website_next/wallets/form/style.css +++ b/website_next/wallets/form/style.css @@ -1,15 +1,14 @@ main.wallets { - .wallets__field { + label { display: grid; gap: 0.375rem; min-width: 0; - } - .wallets__label { - color: var(--gray); - font-size: var(--font-size-xs); - line-height: var(--line-height-xs); - text-transform: uppercase; + > span { + color: var(--gray); + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + text-transform: uppercase; + } } - } diff --git a/website_next/wallets/format.js b/website_next/wallets/format.js index 5b5d25830..2f062797e 100644 --- a/website_next/wallets/format.js +++ b/website_next/wallets/format.js @@ -5,15 +5,6 @@ export function formatNumber(value) { return new Intl.NumberFormat("en-US").format(value); } -/** - * @param {number} sats - */ -export function formatBtc(sats) { - return `${(sats / 100_000_000).toLocaleString("en-US", { - maximumFractionDigits: 8, - })} BTC`; -} - /** * @param {number} dollars */ diff --git a/website_next/wallets/index.js b/website_next/wallets/index.js index aa55553c6..dae28b35a 100644 --- a/website_next/wallets/index.js +++ b/website_next/wallets/index.js @@ -10,20 +10,19 @@ import { createAddForm } from "./add/index.js"; import { createLayout } from "./layout/index.js"; import { createLock } from "./lock/index.js"; import { redaction } from "./redaction/index.js"; -import { inferAddressScript } from "./add/inference.js"; import { readWalletSourceText } from "./add/source.js"; import { scanStatus } from "./wallet/status.js"; import { createSelector } from "./selector/index.js"; -import { renderSettings } from "./wallet/settings/index.js"; import { createSetup } from "./setup/index.js"; import { createWalletPanel, renderWalletPanel, } from "./wallet/index.js"; import { createVault } from "./vault/index.js"; +import { generateAddressesFromWalletSource } from "./derive/index.js"; +import { syncBtcAmounts } from "./amount/index.js"; /** - * @typedef {import("./derive/address.js").AddressScript} AddressScript * @typedef {import("./scan/index.js").WalletScan} WalletScan * @typedef {import("./vault/index.js").StoredWallet} StoredWallet * @typedef {import("./vault/index.js").WalletRuntime} WalletRuntime @@ -83,6 +82,7 @@ export function createWalletsPage() { privacyButton.addEventListener("click", () => { redaction.toggle(main, privacyButton, createGroupedAddress); + syncBtcAmounts(main); }); lockButton.addEventListener("click", () => { @@ -120,11 +120,6 @@ export function createWalletsPage() { const panel = createWalletPanel(); content.replaceChildren(...panel.nodes); - renderSettings(panel.settings, wallet, { - onScriptChange(script, select, status) { - return updateScript(wallet, script, select, status); - }, - }); if (runtime.scan) { renderWalletData(runtime.scan, panel); @@ -135,7 +130,6 @@ export function createWalletsPage() { scanStatus.setPending(panel.status); void runtime.load({ client: brk, - script: wallet.script, onProgress(progress) { scanStatus.setProgress(panel.status, progress); }, @@ -209,34 +203,6 @@ export function createWalletsPage() { }); } - /** - * @param {StoredWallet} wallet - * @param {AddressScript} script - * @param {HTMLSelectElement} select - * @param {HTMLElement} status - */ - async function updateScript( - wallet, - script, - select, - status, - ) { - if (script === wallet.script) return; - - select.disabled = true; - setStatus(status, "Saving"); - - try { - await vault.updateWalletScript(wallet, script); - render(); - } catch (error) { - select.value = wallet.script; - setStatus(status, getErrorMessage(error)); - } finally { - select.disabled = false; - } - } - function renderContent() { const needsSetup = vault.needsSetup(); const locked = vault.isLocked(); @@ -296,18 +262,18 @@ export function createWalletsPage() { form, }) { await withBusy(submit, "Add", "Adding", async () => { - setStatus(status, "Checking address type"); + setStatus(status, "Checking wallet"); try { const value = readWalletSourceText(source.value); - const script = await inferAddressScript(brk, value); + + await generateAddressesFromWalletSource(value, { count: 1 }); setStatus(status, "Saving"); await vault.addWallet({ name: name.value, source: value, - script, }); form.reset(); diff --git a/website_next/wallets/layout/index.js b/website_next/wallets/layout/index.js index 4c928cbcc..350ee0144 100644 --- a/website_next/wallets/layout/index.js +++ b/website_next/wallets/layout/index.js @@ -18,14 +18,14 @@ import { createElement } from "../dom.js"; */ export function createLayout() { const main = createElement("main", "wallets"); - const header = createElement("header", "wallets__header"); - const actions = createElement("div", "wallets__actions"); + const header = document.createElement("header"); + const actions = document.createElement("div"); const addButton = document.createElement("button"); const privacyButton = document.createElement("button"); const lockButton = document.createElement("button"); const selector = createElement("section", "wallets__selector"); - const walletList = createElement("div", "wallets__wallet-list"); - const content = createElement("section", "wallets__content"); + const walletList = document.createElement("div"); + const content = document.createElement("section"); const addDialog = createElement("dialog", "wallets__dialog"); addButton.type = "button"; diff --git a/website_next/wallets/layout/style.css b/website_next/wallets/layout/style.css index b25d3895d..0dd63ff3f 100644 --- a/website_next/wallets/layout/style.css +++ b/website_next/wallets/layout/style.css @@ -1,32 +1,34 @@ main.wallets { - .wallets__header { + > header { display: flex; gap: 1rem; align-items: center; justify-content: end; + + @media (max-width: 34rem) { + justify-content: start; + } + + > div { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: end; + + @media (max-width: 34rem) { + justify-content: start; + } + + > button { + border-color: var(--orange); + color: var(--black); + background: var(--orange); + } + } } - .wallets__actions { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - justify-content: end; - } - - .wallets__content { + > section[aria-live] { display: grid; gap: 1.5rem; } } - -@media (max-width: 34rem) { - main.wallets { - .wallets__header { - justify-content: start; - } - - .wallets__actions { - justify-content: start; - } - } -} diff --git a/website_next/wallets/lock/index.js b/website_next/wallets/lock/index.js index 1bdb69701..5a4311101 100644 --- a/website_next/wallets/lock/index.js +++ b/website_next/wallets/lock/index.js @@ -6,26 +6,80 @@ import { createElement } from "../dom.js"; * @property {() => void} onReset */ +const RESET_HOLD_MS = 2_000; + +/** + * @param {HTMLButtonElement} button + * @param {() => void} onReset + */ +function bindResetHold(button, onReset) { + /** @type {number | undefined} */ + let timer; + + function cancel() { + if (timer === undefined) return; + + clearTimeout(timer); + timer = undefined; + button.removeAttribute("data-wallets-holding"); + } + + function start() { + if (timer !== undefined) return; + + button.setAttribute("data-wallets-holding", ""); + timer = window.setTimeout(() => { + timer = undefined; + button.removeAttribute("data-wallets-holding"); + onReset(); + }, RESET_HOLD_MS); + } + + button.addEventListener("pointerdown", (event) => { + if (event.button !== 0) return; + + button.setPointerCapture(event.pointerId); + start(); + }); + button.addEventListener("pointerup", cancel); + button.addEventListener("pointercancel", cancel); + button.addEventListener("lostpointercapture", cancel); + button.addEventListener("keydown", (event) => { + if (event.repeat || (event.key !== " " && event.key !== "Enter")) return; + + event.preventDefault(); + start(); + }); + button.addEventListener("keyup", (event) => { + if (event.key === " " || event.key === "Enter") { + cancel(); + } + }); + button.addEventListener("blur", cancel); +} + /** * @param {LockOptions} options */ export function createLock(options) { const section = createElement("section", "wallets__unlock"); - const form = createElement("form", "wallets__unlock-form"); + const title = document.createElement("h1"); + const form = document.createElement("form"); const password = document.createElement("input"); const button = document.createElement("button"); const reset = document.createElement("button"); - const status = createElement("p", "wallets__status"); + const status = document.createElement("p"); + title.append("Unlock vault"); password.name = "password"; password.type = "password"; password.autocomplete = "current-password"; + password.autofocus = true; password.placeholder = "Password"; password.required = true; button.type = "submit"; button.append("Unlock"); reset.type = "button"; - reset.className = "wallets__reset"; reset.append("Reset vault"); status.setAttribute("role", "status"); form.append(password, button); @@ -33,8 +87,11 @@ export function createLock(options) { event.preventDefault(); void options.onUnlock(password.value, button, status); }); - reset.addEventListener("click", options.onReset); - section.append(form, reset, status); + bindResetHold(reset, options.onReset); + section.append(title, form, reset, status); + queueMicrotask(() => { + password.focus({ preventScroll: true }); + }); return section; } diff --git a/website_next/wallets/lock/style.css b/website_next/wallets/lock/style.css index 2d680f03c..84b61ac2e 100644 --- a/website_next/wallets/lock/style.css +++ b/website_next/wallets/lock/style.css @@ -6,31 +6,53 @@ main.wallets { min-height: 16rem; text-align: center; - h2, - p { + > h1 { margin: 0; + font-size: 3rem; + font-weight: 400; + line-height: 1; } - } - .wallets__unlock-form { - display: grid; - grid-template-columns: minmax(12rem, 18rem) auto; - gap: 0.75rem; - align-items: end; - justify-content: center; - } + > form { + display: grid; + grid-template-columns: minmax(12rem, 18rem) auto; + gap: 0.75rem; + align-items: end; + justify-content: center; - .wallets__reset { - justify-self: center; - color: var(--gray); - background: transparent; - } -} + @media (max-width: 34rem) { + grid-template-columns: 1fr; + } -@media (max-width: 34rem) { - main.wallets { - .wallets__unlock-form { - grid-template-columns: 1fr; + > button { + border-color: var(--orange); + color: var(--black); + background: var(--orange); + } + } + + > button { + position: relative; + isolation: isolate; + justify-self: center; + overflow: hidden; + color: var(--gray); + background: transparent; + + &::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: color-mix(in oklch, var(--red) 34%, transparent); + transform: scaleX(0); + transform-origin: left; + } + + &[data-wallets-holding]::before { + transform: scaleX(1); + transition: transform 2s linear; + } } } } diff --git a/website_next/wallets/scan/index.js b/website_next/wallets/scan/index.js index 0eda95900..1a239ff94 100644 --- a/website_next/wallets/scan/index.js +++ b/website_next/wallets/scan/index.js @@ -1,9 +1,13 @@ import { scanBranches } from "./branches.js"; +import { isOutputDescriptor } from "../derive/index.js"; +import { parseOutputDescriptor } from "../derive/descriptor.js"; +import { addressScripts } from "../derive/script.js"; /** * @typedef {import("../derive/address.js").AddressScript} AddressScript * @typedef {import("../derive/index.js").AddressType} AddressType * @typedef {Awaited>["addresses"][number]} WalletAddress + * @typedef {Awaited>} ScriptScan */ /** @@ -27,32 +31,116 @@ import { scanBranches } from "./branches.js"; * @property {number} unusedInRow */ +/** + * @typedef {Object} ScanScript + * @property {AddressScript} id + * @property {string} label + */ + +const descriptorScripts = /** @type {const} */ ({ + v0_p2wsh_sortedmulti: "P2WSH", +}); + +/** + * @param {string} source + * @returns {readonly ScanScript[]} + */ +function getSourceScripts(source) { + if (isOutputDescriptor(source)) { + const script = parseOutputDescriptor(source).script; + + return [{ + id: script, + label: descriptorScripts[script], + }]; + } + + return addressScripts; +} + +/** + * @param {WalletAddress} a + * @param {WalletAddress} b + */ +function compareWalletAddresses(a, b) { + return ( + (b.typeIndex ?? -1) - (a.typeIndex ?? -1) || + a.script.localeCompare(b.script) || + a.branchLabel.localeCompare(b.branchLabel) || + a.index - b.index + ); +} + +/** + * @param {ScriptScan} scan + */ +function getLatestSeenIndex(scan) { + return scan.addresses.reduce((latest, address) => { + return Math.max(latest, address.typeIndex ?? -1); + }, -1); +} + +/** + * @param {readonly ScriptScan[]} scans + */ +function selectReceiveAddress(scans) { + let receiveAddress = scans.find((scan) => { + return scan.receiveAddress; + })?.receiveAddress; + let selectedSeenIndex = -1; + + for (const scan of scans) { + const seenIndex = getLatestSeenIndex(scan); + const hasActivity = scan.addresses.length > 0; + + if ( + hasActivity && + scan.receiveAddress && + seenIndex >= selectedSeenIndex + ) { + receiveAddress = scan.receiveAddress; + selectedSeenIndex = seenIndex; + } + } + + return receiveAddress; +} + /** * @param {Object} options * @param {WalletScanClient} options.client * @param {string} options.source - * @param {AddressScript} options.script * @param {(progress: WalletScanProgress) => void} [options.onProgress] * @returns {Promise} */ export async function scanWalletAddresses({ client, source, - script, onProgress, }) { - const scan = await scanBranches(client, source, { - script, - onProgress, - }); - const addresses = /** @type {WalletAddress[]} */ (scan.addresses); + const scans = /** @type {ScriptScan[]} */ ([]); + + for (const script of getSourceScripts(source)) { + scans.push(await scanBranches(client, source, { + script: script.id, + onProgress(progress) { + onProgress?.({ + ...progress, + branchLabel: `${script.label} ${progress.branchLabel}`, + }); + }, + })); + } + + const addresses = scans.flatMap((scan) => scan.addresses) + .sort(compareWalletAddresses); const btcUsdPrice = /** @type {number} */ ( await client.getLivePrice({ cache: false }) ); return { addresses, - receiveAddress: scan.receiveAddress, + receiveAddress: selectReceiveAddress(scans), btcUsdPrice, }; } diff --git a/website_next/wallets/selector/index.js b/website_next/wallets/selector/index.js index b04a4e705..9460f4a20 100644 --- a/website_next/wallets/selector/index.js +++ b/website_next/wallets/selector/index.js @@ -23,7 +23,6 @@ function renderButtons(walletList, wallets, options) { const selected = wallet.id === options.getSelectedId(); button.type = "button"; - button.className = "wallets__wallet-button"; button.setAttribute("aria-pressed", selected ? "true" : "false"); button.setAttribute("data-wallet-id", wallet.id); button.append(wallet.name); @@ -40,7 +39,7 @@ function renderButtons(walletList, wallets, options) { */ export function createSelector(walletList, options) { function selectSnappedWallet() { - const buttons = [...walletList.querySelectorAll(".wallets__wallet-button")]; + const buttons = [...walletList.querySelectorAll("button")]; if (buttons.length === 0) return; diff --git a/website_next/wallets/selector/style.css b/website_next/wallets/selector/style.css index 69b79b7de..88505fdaa 100644 --- a/website_next/wallets/selector/style.css +++ b/website_next/wallets/selector/style.css @@ -1,56 +1,43 @@ main.wallets { .wallets__selector { min-width: 0; - } - .wallets__wallet-list { - display: flex; - gap: 1rem; - min-width: 0; - overflow-x: auto; - padding-bottom: 0.25rem; - overscroll-behavior-inline: contain; - scroll-padding-inline: var(--page-x); - scroll-snap-type: x proximity; - } + > div { + display: flex; + gap: 1rem; + min-width: 0; + overflow-x: auto; + padding-bottom: 0.25rem; + overscroll-behavior-inline: contain; + scroll-padding-inline: var(--page-x); + scroll-snap-type: x proximity; - .wallets__wallet-button { - flex: 0 0 auto; - scroll-snap-align: center; - border: 0; - padding: 0; - color: var(--white); - background: transparent; - font-family: var(--font-serif); - font-size: 4rem; - font-weight: 400; - line-height: 1; - opacity: 0.48; - cursor: pointer; - } + > button { + flex: 0 0 auto; + height: auto; + scroll-snap-align: center; + border: 0; + padding: 0; + color: var(--white); + background: transparent; + font-size: 4rem; + font-weight: 400; + line-height: 1; + opacity: 0.48; + cursor: pointer; - .wallets__wallet-button[aria-pressed="true"] { - opacity: 1; - } + @media (max-width: 56rem) { + font-size: 3rem; + } - .wallets__wallet-button:focus-visible { - outline: 2px solid var(--orange); - outline-offset: 2px; - } -} + @media (max-width: 34rem) { + font-size: 2.5rem; + } -@media (max-width: 56rem) { - main.wallets { - .wallets__wallet-button { - font-size: 3rem; - } - } -} - -@media (max-width: 34rem) { - main.wallets { - .wallets__wallet-button { - font-size: 2.5rem; + &[aria-pressed="true"] { + opacity: 1; + } + } } } } diff --git a/website_next/wallets/setup/index.js b/website_next/wallets/setup/index.js index b762c0bd8..73e7db466 100644 --- a/website_next/wallets/setup/index.js +++ b/website_next/wallets/setup/index.js @@ -22,11 +22,11 @@ function createDescriptionText(text) { export function createSetup(options) { const section = createElement("section", "wallets__setup"); const title = document.createElement("h1"); - const description = createElement("div", "wallets__setup-description"); - const form = createElement("form", "wallets__setup-form"); + const description = document.createElement("div"); + const form = document.createElement("form"); const password = document.createElement("input"); const button = document.createElement("button"); - const status = createElement("p", "wallets__status"); + const status = document.createElement("p"); title.append("Wallets"); description.append( diff --git a/website_next/wallets/setup/style.css b/website_next/wallets/setup/style.css index aea280acf..2460f77e2 100644 --- a/website_next/wallets/setup/style.css +++ b/website_next/wallets/setup/style.css @@ -6,58 +6,53 @@ main.wallets { max-width: 36rem; min-height: 16rem; margin-inline: auto; - text-align: center; h1 { margin: 0; - font-family: var(--font-serif); + text-align: center; font-size: 5rem; font-weight: 400; line-height: 0.9; + + @media (max-width: 56rem) { + font-size: 4rem; + } + + @media (max-width: 34rem) { + font-size: 3rem; + } } p { margin: 0; } - } - .wallets__setup-description { - display: grid; - gap: 0.625rem; - color: var(--gray); - font-size: var(--font-size-md); - line-height: var(--line-height-md); - } + > div { + display: grid; + gap: 0.75rem; + color: var(--gray); + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + text-align: left; + } - .wallets__setup-form { - display: grid; - grid-template-columns: minmax(12rem, 18rem) auto; - gap: 0.75rem; - align-items: end; - justify-content: center; - } -} + > form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: end; + justify-content: center; + width: 100%; -@media (max-width: 56rem) { - main.wallets { - .wallets__setup { - h1 { - font-size: 4rem; + @media (max-width: 34rem) { + grid-template-columns: 1fr; + } + + > button { + border-color: var(--orange); + color: var(--black); + background: var(--orange); } } } } - -@media (max-width: 34rem) { - main.wallets { - .wallets__setup { - h1 { - font-size: 3rem; - } - } - - .wallets__setup-form { - grid-template-columns: 1fr; - } - } -} diff --git a/website_next/wallets/style.css b/website_next/wallets/style.css index 287f95ca8..2220c9852 100644 --- a/website_next/wallets/style.css +++ b/website_next/wallets/style.css @@ -16,7 +16,7 @@ main.wallets { align-content: center; } - .wallets__status { + [role="status"] { min-height: var(--line-height-sm); margin: 0; color: var(--gray); @@ -24,8 +24,11 @@ main.wallets { line-height: var(--line-height-sm); } - :is(input, select), - button:not(.wallets__wallet-button) { + [data-wallets-btc-muted] { + color: color-mix(in oklch, currentColor 45%, transparent); + } + + :is(input, select, button) { min-width: 0; height: var(--control-height); border: 1px solid color-mix(in oklch, var(--gray) 45%, transparent); @@ -37,11 +40,11 @@ main.wallets { line-height: 1; } - button:not(.wallets__wallet-button) { + button { cursor: pointer; } - :is(input, select, button:not(.wallets__wallet-button)):focus-visible { + :is(input, select, button):focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } @@ -50,21 +53,7 @@ main.wallets { color: color-mix(in oklch, var(--gray) 70%, transparent); } - :is( - .wallets__actions, - .wallets__empty, - .wallets__setup-form, - .wallets__unlock-form - ) button, - .wallets__dialog-form button[type="submit"], - .wallets__receive-button, - .wallets__receive-actions button:first-child { - border-color: var(--orange); - color: var(--black); - background: var(--orange); - } - - button:not(.wallets__wallet-button):disabled { + button:disabled { border-color: var(--gray); color: var(--black); background: var(--gray); diff --git a/website_next/wallets/vault/index.js b/website_next/wallets/vault/index.js index dbe1f9a87..80984ec97 100644 --- a/website_next/wallets/vault/index.js +++ b/website_next/wallets/vault/index.js @@ -4,7 +4,6 @@ import { createRuntime } from "./runtime.js"; /** * @typedef {import("./storage.js").StoredWallet} StoredWallet * @typedef {import("./storage.js").AddWalletInput} AddWalletInput - * @typedef {import("../derive/address.js").AddressScript} AddressScript * @typedef {ReturnType} WalletRuntime */ @@ -116,19 +115,6 @@ export function createVault() { runtimes.set(added.wallet.id, createRuntime(added.wallet.source)); } - /** - * @param {StoredWallet} wallet - * @param {AddressScript} script - */ - async function updateWalletScript(wallet, script) { - wallets = await vaultStorage.updateWalletScript(wallets, { - walletId: wallet.id, - script, - }, password); - runtimes.set(wallet.id, createRuntime(wallet.source)); - syncSelected(); - } - return { get wallets() { return wallets; @@ -153,6 +139,5 @@ export function createVault() { setup, unlock, addWallet, - updateWalletScript, }; } diff --git a/website_next/wallets/vault/runtime.js b/website_next/wallets/vault/runtime.js index 2c0c96274..cfec880c0 100644 --- a/website_next/wallets/vault/runtime.js +++ b/website_next/wallets/vault/runtime.js @@ -4,11 +4,9 @@ import { scanWalletAddresses } from "../scan/index.js"; * @typedef {import("../scan/index.js").WalletScan} WalletScan * @typedef {import("../scan/index.js").WalletScanClient} WalletScanClient * @typedef {import("../scan/index.js").WalletScanProgress} WalletScanProgress - * @typedef {import("../derive/address.js").AddressScript} AddressScript * * @typedef {Object} LoadOptions * @property {WalletScanClient} client - * @property {AddressScript} script * @property {(progress: WalletScanProgress) => void} [onProgress] */ @@ -31,7 +29,6 @@ export function createRuntime(source) { pending = scanWalletAddresses({ client: options.client, source, - script: options.script, onProgress: options.onProgress, }).then((nextScan) => { scan = nextScan; diff --git a/website_next/wallets/vault/storage.js b/website_next/wallets/vault/storage.js index 44a3cdebe..5f7e5b7dd 100644 --- a/website_next/wallets/vault/storage.js +++ b/website_next/wallets/vault/storage.js @@ -4,14 +4,12 @@ const STORAGE_KEY = "bitview.wallets.v3"; /** * @typedef {import("./encryption.js").EncryptedSecret} EncryptedSecret - * @typedef {import("../derive/address.js").AddressScript} AddressScript */ /** * @typedef {Object} StoredWallet * @property {string} id * @property {string} name - * @property {AddressScript} script * @property {string} source * @property {number} createdAt * @property {number} updatedAt @@ -20,16 +18,9 @@ const STORAGE_KEY = "bitview.wallets.v3"; /** * @typedef {Object} AddWalletInput * @property {string} name - * @property {AddressScript} script * @property {string} source */ -/** - * @typedef {Object} UpdateWalletScriptInput - * @property {string} walletId - * @property {AddressScript} script - */ - /** * @typedef {Object} WalletVault * @property {StoredWallet[]} wallets @@ -120,7 +111,6 @@ async function addWallet(wallets, input, pagePassword) { const wallet = { id: createWalletId(), name: input.name.trim(), - script: input.script, source: input.source.trim(), createdAt: time, updatedAt: time, @@ -135,33 +125,10 @@ async function addWallet(wallets, input, pagePassword) { }; } -/** - * @param {StoredWallet[]} wallets - * @param {UpdateWalletScriptInput} input - * @param {string} pagePassword - */ -async function updateWalletScript(wallets, input, pagePassword) { - const time = now(); - const nextWallets = wallets.map((wallet) => { - return wallet.id === input.walletId - ? { - ...wallet, - script: input.script, - updatedAt: time, - } - : wallet; - }); - - await writeWallets(nextWallets, pagePassword); - - return nextWallets; -} - export const vaultStorage = /** @type {const} */ ({ has, reset, setup, load, addWallet, - updateWalletScript, }); diff --git a/website_next/wallets/wallet/actions/style.css b/website_next/wallets/wallet/actions/style.css new file mode 100644 index 000000000..b1a36da48 --- /dev/null +++ b/website_next/wallets/wallet/actions/style.css @@ -0,0 +1,26 @@ +main.wallets { + .wallets__wallet-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: end; + justify-content: end; + + @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; + } + } + } +} diff --git a/website_next/wallets/wallet/address/index.js b/website_next/wallets/wallet/address/index.js index 6943869f2..375bfc52a 100644 --- a/website_next/wallets/wallet/address/index.js +++ b/website_next/wallets/wallet/address/index.js @@ -14,14 +14,14 @@ export function createGroupedAddress(text) { const groups = text.match(/.{1,4}/g) ?? []; for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) { - const group = createElement("span", "wallets__address-group"); + const group = document.createElement("span"); for (const character of groups[groupIndex]) { - const span = createElement( - "span", - Number.isNaN(Number(character)) - ? "wallets__address-letter" - : "wallets__address-number", + const span = document.createElement("span"); + + span.setAttribute( + "data-wallets-address-character", + Number.isNaN(Number(character)) ? "letter" : "number", ); span.append(character); @@ -55,7 +55,7 @@ function createPrivateAddress(address) { * @param {WalletAddress} row */ function createAddressBadge(row) { - const badge = createElement("span", "wallets__address-badge"); + const badge = document.createElement("span"); const label = row.branchLabel?.toLowerCase() ?? "address"; badge.setAttribute("data-wallets-address-branch", label); @@ -69,7 +69,7 @@ function createAddressBadge(row) { */ export function createAddressCellContent(row) { const element = createElement("div", "wallets__address-cell"); - const anonSet = createElement("span", "wallets__address-meta"); + const anonSet = document.createElement("span"); anonSet.append(`anon set: ${formatNumber(row.historyBucketSize)}`); element.append( diff --git a/website_next/wallets/wallet/address/style.css b/website_next/wallets/wallet/address/style.css index 5bc1b58d1..fa336fadf 100644 --- a/website_next/wallets/wallet/address/style.css +++ b/website_next/wallets/wallet/address/style.css @@ -2,24 +2,24 @@ main.wallets { .wallets__address-cell { display: grid; gap: 0.25rem; - } - .wallets__address-meta { - color: var(--gray); - font-size: var(--font-size-xs); - line-height: var(--line-height-xs); - } + > span:first-child { + display: inline-flex; + align-items: center; + justify-self: start; + min-height: 1rem; + border: 1px solid color-mix(in oklch, var(--gray) 28%, transparent); + border-radius: 0.25rem; + padding: 0 0.25rem; + color: color-mix(in oklch, var(--white) 76%, var(--gray)); + line-height: 1; + } - .wallets__address-badge { - display: inline-flex; - align-items: center; - justify-self: start; - min-height: 1rem; - border: 1px solid color-mix(in oklch, var(--gray) 28%, transparent); - border-radius: 0.25rem; - padding: 0 0.25rem; - color: color-mix(in oklch, var(--white) 76%, var(--gray)); - line-height: 1; + > span:last-child { + color: var(--gray); + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + } } .wallets__address { @@ -27,17 +27,17 @@ main.wallets { flex-wrap: wrap; gap: 0 0.375rem; max-width: 40rem; - } - .wallets__address-group { - white-space: nowrap; - } + > span { + white-space: nowrap; + } - .wallets__address-letter { - color: var(--white); - } + [data-wallets-address-character="letter"] { + color: var(--white); + } - .wallets__address-number { - color: color-mix(in oklch, var(--white) 50%, var(--gray)); + [data-wallets-address-character="number"] { + color: color-mix(in oklch, var(--white) 50%, var(--gray)); + } } } diff --git a/website_next/wallets/wallet/index.js b/website_next/wallets/wallet/index.js index 3cf395016..2901a16f0 100644 --- a/website_next/wallets/wallet/index.js +++ b/website_next/wallets/wallet/index.js @@ -9,7 +9,7 @@ import { renderTransactions } from "./transactions/index.js"; * @typedef {Parameters[0]} TransactionClient * * @typedef {Object} WalletPanel - * @property {HTMLElement} settings + * @property {HTMLElement} actions * @property {HTMLElement} summary * @property {HTMLElement} status * @property {HTMLElement} results @@ -20,22 +20,22 @@ import { renderTransactions } from "./transactions/index.js"; * @returns {WalletPanel} */ export function createWalletPanel() { - const settings = createElement("section", "wallets__settings"); + const actions = createElement("section", "wallets__wallet-actions"); const summary = createElement("section", "wallets__summary"); - const status = createElement("p", "wallets__status"); + const status = document.createElement("p"); const results = createElement("section", "wallets__results"); - settings.setAttribute("aria-label", "Wallet settings"); + actions.setAttribute("aria-label", "Wallet actions"); status.setAttribute("role", "status"); summary.setAttribute("aria-label", "Wallets summary"); results.setAttribute("aria-label", "Wallets results"); return { - settings, + actions, summary, status, results, - nodes: [settings, summary, status, results], + nodes: [actions, summary, status, results], }; } @@ -46,7 +46,7 @@ export function createWalletPanel() { */ export function renderWalletPanel(scan, panel, client) { renderWalletSummary(panel.summary, scan.addresses, scan.btcUsdPrice); - renderReceiveButton(panel.settings, scan.receiveAddress); + renderReceiveButton(panel.actions, scan.receiveAddress); panel.results.replaceChildren("Loading activity"); void transactionCache.load(client, scan.addresses).then((transactions) => { if (panel.results.isConnected) { diff --git a/website_next/wallets/wallet/receive/index.js b/website_next/wallets/wallet/receive/index.js index e721b2cbe..01ff2ea74 100644 --- a/website_next/wallets/wallet/receive/index.js +++ b/website_next/wallets/wallet/receive/index.js @@ -46,7 +46,6 @@ function createReceiveQr(receiveAddress) { const image = document.createElement("img"); const uri = `bitcoin:${receiveAddress.address}`; - image.className = "wallets__receive-qr"; image.alt = `QR code for ${receiveAddress.address}`; image.src = createQrDataUrl(uri); @@ -57,7 +56,7 @@ function createReceiveQr(receiveAddress) { * @param {ReceiveAddress} receiveAddress */ function createReceiveAddress(receiveAddress) { - const element = createElement("div", "wallets__receive-address"); + const element = document.createElement("div"); element.append(createGroupedAddress(receiveAddress.address)); @@ -82,8 +81,8 @@ function openReceiveDialog(receiveAddress) { "dialog", "wallets__dialog wallets__receive-dialog", ); - const content = createElement("div", "wallets__receive-card"); - const actions = createElement("div", "wallets__receive-actions"); + const content = document.createElement("div"); + const actions = document.createElement("div"); const copy = document.createElement("button"); const close = document.createElement("button"); @@ -128,7 +127,6 @@ export function renderReceiveButton(element, receiveAddress) { const button = document.createElement("button"); button.type = "button"; - button.className = "wallets__receive-button"; button.disabled = !receiveAddress; button.append("Receive"); button.addEventListener("click", () => { diff --git a/website_next/wallets/wallet/receive/style.css b/website_next/wallets/wallet/receive/style.css index 8e7542797..1beb16b43 100644 --- a/website_next/wallets/wallet/receive/style.css +++ b/website_next/wallets/wallet/receive/style.css @@ -1,46 +1,45 @@ main.wallets { - .wallets__receive-button:disabled { - border-color: color-mix(in oklch, var(--gray) 35%, transparent); - color: var(--gray); - background: transparent; - cursor: default; - } - .wallets__receive-dialog { width: min(100% - 2rem, 32rem); - } - .wallets__receive-card { - display: grid; - gap: 1rem; + > div { + display: grid; + gap: 1rem; - h2 { - margin: 0; - font-size: var(--font-size-lg); - font-weight: 400; - line-height: var(--line-height-lg); + h2 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: 400; + line-height: var(--line-height-lg); + } + + > img { + justify-self: center; + width: min(100%, 18rem); + aspect-ratio: 1; + padding: 1rem; + background: var(--white); + image-rendering: pixelated; + } + + > div:first-of-type { + color: var(--white); + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + } + + > div:last-of-type { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: end; + + > button:first-child { + border-color: var(--orange); + color: var(--black); + background: var(--orange); + } + } } } - - .wallets__receive-qr { - justify-self: center; - width: min(100%, 18rem); - aspect-ratio: 1; - padding: 1rem; - background: var(--white); - image-rendering: pixelated; - } - - .wallets__receive-address { - color: var(--white); - font-size: var(--font-size-sm); - line-height: var(--line-height-sm); - } - - .wallets__receive-actions { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - justify-content: end; - } } diff --git a/website_next/wallets/wallet/settings/index.js b/website_next/wallets/wallet/settings/index.js deleted file mode 100644 index 6d4ed6fba..000000000 --- a/website_next/wallets/wallet/settings/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import { - createAddressScriptSelect, - readAddressScript, -} from "./script.js"; -import { createElement } from "../../dom.js"; -import { createField } from "../../form/index.js"; -import { isOutputDescriptor } from "../../derive/index.js"; - -/** - * @typedef {import("../../derive/address.js").AddressScript} AddressScript - * @typedef {import("../../vault/index.js").StoredWallet} StoredWallet - */ - -/** - * @typedef {Object} WalletSettingsOptions - * @property {(script: AddressScript, select: HTMLSelectElement, status: HTMLElement) => void | Promise} onScriptChange - */ - -/** - * @param {HTMLElement} element - * @param {StoredWallet} wallet - * @param {WalletSettingsOptions} options - */ -export function renderSettings(element, wallet, options) { - if (isOutputDescriptor(wallet.source)) { - element.replaceChildren(); - return; - } - - const script = createAddressScriptSelect( - /** @type {AddressScript} */ (wallet.script), - ); - const status = createElement("p", "wallets__status"); - - status.setAttribute("role", "status"); - script.addEventListener("change", () => { - void options.onScriptChange(readAddressScript(script), script, status); - }); - element.replaceChildren(createField("Address type", script), status); -} diff --git a/website_next/wallets/wallet/settings/script.js b/website_next/wallets/wallet/settings/script.js deleted file mode 100644 index 8dfe7e479..000000000 --- a/website_next/wallets/wallet/settings/script.js +++ /dev/null @@ -1,33 +0,0 @@ -import { addressScripts } from "../../derive/script.js"; - -/** - * @typedef {import("../../derive/address.js").AddressScript} AddressScript - */ - -/** - * @param {AddressScript} [value] - */ -export function createAddressScriptSelect(value) { - const select = document.createElement("select"); - - select.name = "script"; - - for (const { id, label } of addressScripts) { - const option = document.createElement("option"); - - option.value = id; - option.selected = id === value; - option.append(label); - select.append(option); - } - - return select; -} - -/** - * @param {HTMLSelectElement} select - * @returns {AddressScript} - */ -export function readAddressScript(select) { - return /** @type {AddressScript} */ (select.value); -} diff --git a/website_next/wallets/wallet/settings/style.css b/website_next/wallets/wallet/settings/style.css deleted file mode 100644 index 932b3f99a..000000000 --- a/website_next/wallets/wallet/settings/style.css +++ /dev/null @@ -1,21 +0,0 @@ -main.wallets { - .wallets__settings { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: end; - justify-content: end; - } - - .wallets__settings .wallets__field { - min-width: min(100%, 14rem); - } -} - -@media (max-width: 34rem) { - main.wallets { - .wallets__settings { - justify-content: start; - } - } -} diff --git a/website_next/wallets/wallet/summary/index.js b/website_next/wallets/wallet/summary/index.js index cd08ad057..31c86cf38 100644 --- a/website_next/wallets/wallet/summary/index.js +++ b/website_next/wallets/wallet/summary/index.js @@ -1,5 +1,5 @@ -import { createElement } from "../../dom.js"; -import { formatBtc, formatUsd } from "../../format.js"; +import { createBtcAmount } from "../../amount/index.js"; +import { formatUsd } from "../../format.js"; import { redaction } from "../../redaction/index.js"; /** @@ -11,8 +11,8 @@ import { redaction } from "../../redaction/index.js"; * @param {number} btcUsdPrice */ function createBalanceSummary(balance, btcUsdPrice) { - const element = createElement("p", "wallets__balance"); - const btc = redaction.createValue("strong", formatBtc(balance), "fixed"); + const element = document.createElement("p"); + const btc = createBtcAmount("strong", balance); const usd = redaction.createValue( "span", formatUsd((balance / 100_000_000) * btcUsdPrice), diff --git a/website_next/wallets/wallet/summary/style.css b/website_next/wallets/wallet/summary/style.css index df02ce694..978135dd5 100644 --- a/website_next/wallets/wallet/summary/style.css +++ b/website_next/wallets/wallet/summary/style.css @@ -1,35 +1,26 @@ main.wallets { .wallets__summary { min-height: 5rem; - } - .wallets__balance { - display: grid; - gap: 0.5rem; - margin: 0; + > p { + display: grid; + gap: 0.5rem; + margin: 0; - strong { - min-width: 0; - overflow-wrap: anywhere; - color: var(--white); - font-size: 3rem; - font-weight: 620; - line-height: 1; - } - - span { - color: var(--gray); - font-size: var(--font-size-lg); - line-height: var(--line-height-lg); - } - } -} - -@media (max-width: 34rem) { - main.wallets { - .wallets__balance { strong { - font-size: 2.25rem; + min-width: 0; + overflow-wrap: anywhere; + color: var(--white); + font-family: var(--font-serif); + font-size: 4rem; + font-weight: 400; + line-height: 1; + } + + > span { + color: var(--gray); + font-size: var(--font-size-lg); + line-height: var(--line-height-lg); } } } diff --git a/website_next/wallets/wallet/transactions/index.js b/website_next/wallets/wallet/transactions/index.js index b92ea3a58..7a22192f5 100644 --- a/website_next/wallets/wallet/transactions/index.js +++ b/website_next/wallets/wallet/transactions/index.js @@ -1,5 +1,5 @@ import { createElement } from "../../dom.js"; -import { formatBtc } from "../../format.js"; +import { createBtcAmount } from "../../amount/index.js"; import { redaction } from "../../redaction/index.js"; import { createAddressCellContent } from "../address/index.js"; @@ -21,107 +21,82 @@ function formatTxid(txid) { } /** - * @param {number} sats - */ -function formatSignedBtc(sats) { - if (sats > 0) return `+${formatBtc(sats)}`; - if (sats < 0) return `-${formatBtc(Math.abs(sats))}`; - - return formatBtc(sats); -} - -/** + * @param {HTMLElement} element * @param {WalletTransaction} transaction */ -function getTransactionDetail(transaction) { +function appendTransactionDetail(element, transaction) { if (transaction.type === "consolidation") { - return `${transaction.addresses.length} wallet addresses · fee only`; + element.append( + `${transaction.addresses.length} wallet addresses · fee only`, + ); + return; } if (transaction.type === "send") { - return `to external wallet · fee ${formatBtc(transaction.fee)}`; + element.append( + "to external wallet · fee ", + createBtcAmount("span", transaction.fee), + ); + return; } - return transaction.status; + element.append(transaction.status); } /** * @param {WalletTransaction} transaction */ function createTransactionDetails(transaction) { - const dialog = createElement("dialog", "wallets__dialog wallets__tx-dialog"); - const content = createElement("div", "wallets__tx-details"); - const title = document.createElement("h2"); + const content = document.createElement("div"); const txid = document.createElement("code"); const meta = document.createElement("p"); - const list = createElement("div", "wallets__tx-addresses"); - const close = document.createElement("button"); + const list = document.createElement("div"); - title.append(typeLabels[transaction.type]); redaction.setTitle(txid, transaction.txid); redaction.setValue(txid, transaction.txid); meta.append( transaction.status, " · ", - redaction.createValue("span", formatSignedBtc(transaction.amount), "fixed"), + createBtcAmount("span", transaction.amount, { signed: true }), " · fee ", - redaction.createValue("span", formatBtc(transaction.fee), "fixed"), + createBtcAmount("span", transaction.fee), ); for (const address of transaction.addresses) { list.append(createAddressCellContent(address.walletAddress)); } - close.type = "button"; - close.append("Close"); - content.append(title, txid, meta, list, close); - dialog.append(content); - close.addEventListener("click", () => { - dialog.close(); - }); - dialog.addEventListener("close", () => { - dialog.remove(); - }); - dialog.addEventListener("click", (event) => { - if (event.target === dialog) { - dialog.close(); - } - }); + content.append(txid, meta, list); - return dialog; + return content; } /** * @param {WalletTransaction} transaction */ function createTransactionRow(transaction) { - const row = createElement("li", "wallets__tx"); - const main = createElement("div", "wallets__tx-main"); + const row = document.createElement("li"); + const main = document.createElement("div"); const label = document.createElement("strong"); - const amount = redaction.createValue( + const amount = createBtcAmount( "span", - formatSignedBtc(transaction.amount), - "fixed", + transaction.amount, + { signed: true }, ); - const detail = createElement("p", "wallets__tx-detail"); + const detail = document.createElement("p"); const txid = document.createElement("code"); - const more = document.createElement("button"); + const details = document.createElement("details"); + const summary = document.createElement("summary"); label.append(typeLabels[transaction.type]); amount.dataset.walletsTxAmount = transaction.amount >= 0 ? "positive" : "negative"; redaction.setTitle(txid, transaction.txid); redaction.setValue(txid, formatTxid(transaction.txid)); - more.type = "button"; - more.append("View more"); - detail.append(getTransactionDetail(transaction), " · ", txid); + summary.append("Details"); + appendTransactionDetail(detail, transaction); + detail.append(" · ", txid); + details.append(summary, createTransactionDetails(transaction)); main.append(label, amount); - row.append(main, detail, more); - more.addEventListener("click", () => { - const dialog = createTransactionDetails(transaction); - const mainElement = document.querySelector("main.wallets") ?? document.body; - - mainElement.append(dialog); - dialog.showModal(); - }); + row.append(main, detail, details); return row; } @@ -164,11 +139,11 @@ export function renderTransactions(element, transactions) { } for (const [date, group] of groups) { - const section = createElement("section", "wallets__tx-group"); + const section = document.createElement("section"); const heading = document.createElement("h3"); - const list = createElement("ol", "wallets__tx-list"); + const list = document.createElement("ol"); - heading.append(date); + heading.append(redaction.createValue("span", date, "fixed")); for (const transaction of group) { list.append(createTransactionRow(transaction)); } diff --git a/website_next/wallets/wallet/transactions/style.css b/website_next/wallets/wallet/transactions/style.css index 09dbb065b..c7a69c87b 100644 --- a/website_next/wallets/wallet/transactions/style.css +++ b/website_next/wallets/wallet/transactions/style.css @@ -9,9 +9,7 @@ main.wallets { display: grid; gap: 1.25rem; - h2, - h3, - p { + :is(h2, h3, p) { margin: 0; } @@ -29,113 +27,132 @@ main.wallets { line-height: var(--line-height-xs); text-transform: uppercase; } - } - .wallets__tx-group { - display: grid; - gap: 0.5rem; - } + > section { + display: grid; + gap: 0.5rem; - .wallets__tx-list { - display: grid; - gap: 0.25rem; - margin: 0; - padding: 0; - list-style: none; - } + > ol { + display: grid; + gap: 0.25rem; + margin: 0; + padding: 0; + list-style: none; - .wallets__tx { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.25rem 1rem; - align-items: center; - padding: 0.875rem 0; - border-bottom: 1px solid color-mix(in oklch, var(--gray) 18%, transparent); - } + > li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.25rem 1rem; + align-items: center; + padding: 0.875rem 0; + border-bottom: 1px solid color-mix( + in oklch, + var(--gray) 18%, + transparent + ); - .wallets__tx-main { - display: flex; - gap: 1rem; - align-items: baseline; - justify-content: space-between; - min-width: 0; + @media (max-width: 34rem) { + grid-template-columns: 1fr; + } - strong { - color: var(--white); - font-weight: 500; - } + > div:first-child { + display: flex; + gap: 1rem; + align-items: baseline; + justify-content: space-between; + min-width: 0; - span { - color: var(--white); - white-space: nowrap; - } + strong { + color: var(--white); + font-weight: 500; + } - span[data-wallets-tx-amount="positive"] { - color: var(--green); - } + > span { + color: var(--white); + white-space: nowrap; - span[data-wallets-tx-amount="negative"] { - color: var(--red); - } - } + &[data-wallets-tx-amount="positive"] { + color: var(--green); + } - .wallets__tx-detail { - grid-column: 1; - min-width: 0; - color: var(--gray); - font-size: var(--font-size-sm); - line-height: var(--line-height-sm); + &[data-wallets-tx-amount="negative"] { + color: var(--red); + } + } + } - code { - color: inherit; - font-family: inherit; - } - } + > p { + grid-column: 1; + min-width: 0; + color: var(--gray); + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); - .wallets__tx button { - grid-column: 2; - grid-row: 1 / span 2; - height: 2rem; - padding-inline: 0.625rem; - background: transparent; - } + code { + color: inherit; + font-family: inherit; + } + } - .wallets__tx-dialog { - width: min(100% - 2rem, 42rem); - } + > details { + grid-column: 2; + grid-row: 1 / span 2; - .wallets__tx-details { - display: grid; - gap: 1rem; + @media (max-width: 34rem) { + grid-column: 1; + grid-row: auto; + justify-self: start; + } - h2, - p { - margin: 0; - } + &[open] { + display: grid; + grid-column: 1 / -1; + grid-row: auto; + gap: 0.75rem; + } - code { - overflow-wrap: anywhere; - color: var(--white); - font-family: inherit; - } - } + > summary { + display: grid; + place-items: center; + height: 2rem; + border: 1px solid color-mix( + in oklch, + var(--gray) 45%, + transparent + ); + border-radius: 0.375rem; + padding-inline: 0.625rem; + color: var(--white); + background: transparent; + line-height: 1; + list-style: none; + cursor: pointer; - .wallets__tx-addresses { - display: grid; - gap: 0.75rem; - } -} + &::marker, + &::-webkit-details-marker { + display: none; + content: ""; + } + } -@media (max-width: 34rem) { - main.wallets { - .wallets__tx { - grid-template-columns: 1fr; - } + > div { + display: grid; + gap: 1rem; - .wallets__tx button { - grid-column: 1; - grid-row: auto; - justify-self: start; + code { + overflow-wrap: anywhere; + color: var(--white); + font-family: inherit; + } + + > div { + display: grid; + gap: 0.75rem; + } + } + } + } + } } } }