mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-21 12:08:24 -07:00
website: redesign part 33
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
dialog {
|
||||
--dialog-space: 1.5rem;
|
||||
|
||||
width: min(100% - 2rem, 30rem);
|
||||
border: 0;
|
||||
border-radius: var(--dialog-space);
|
||||
padding: var(--dialog-space);
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
|
||||
&::backdrop {
|
||||
background: color-mix(in oklch, var(--black) 72%, transparent);
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--white);
|
||||
background: var(--gray);
|
||||
|
||||
&:hover {
|
||||
color: var(--white);
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--white);
|
||||
background: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,24 +17,5 @@ main.home {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.3125rem;
|
||||
color: var(--white);
|
||||
background: var(--gray);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@
|
||||
<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="/dialog/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" />
|
||||
<link rel="stylesheet" href="/wallets/add/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/empty/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/lock/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/setup/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/start/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/start/reset/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/wallet/actions/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/selector/style.css" />
|
||||
<link rel="stylesheet" href="/wallets/wallet/summary/style.css" />
|
||||
|
||||
@@ -7,10 +7,58 @@ body {
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
body {
|
||||
> main {
|
||||
min-height: 100dvh;
|
||||
color: var(--white);
|
||||
body > main {
|
||||
min-height: 100dvh;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:where(button, main.home nav a) {
|
||||
display: inline-flex;
|
||||
appearance: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-radius: 0.3125rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--black);
|
||||
background: var(--gray);
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--orange);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
|
||||
:is(input, textarea)[aria-invalid="true"] {
|
||||
border-color: var(--red);
|
||||
color: var(--red);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
--line-height-sm: calc(1.25 / 0.875);
|
||||
--font-size-base: 1rem;
|
||||
--line-height-base: calc(1.5 / 1);
|
||||
--font-size-lg: 1.25rem;
|
||||
--line-height-lg: calc(1.5 / 1.25);
|
||||
|
||||
--page-x: 2rem;
|
||||
--layer-header: 10;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createField } from "../form/index.js";
|
||||
import { createElement } from "../dom.js";
|
||||
import { redaction } from "../redaction/index.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AddWalletFormSubmit
|
||||
* @property {HTMLInputElement} name
|
||||
* @property {HTMLInputElement} source
|
||||
* @property {HTMLTextAreaElement} source
|
||||
* @property {HTMLButtonElement} submit
|
||||
* @property {HTMLElement} status
|
||||
* @property {HTMLFormElement} form
|
||||
*/
|
||||
|
||||
@@ -17,15 +17,15 @@ import { redaction } from "../redaction/index.js";
|
||||
*/
|
||||
|
||||
function createSourceInput() {
|
||||
const input = document.createElement("input");
|
||||
const input = document.createElement("textarea");
|
||||
|
||||
input.name = "source";
|
||||
input.type = "text";
|
||||
redaction.setInput(input);
|
||||
input.autocomplete = "off";
|
||||
input.placeholder = "xpub or descriptor...";
|
||||
input.placeholder = "xpub... or wsh(sortedmulti(...))";
|
||||
input.required = true;
|
||||
input.spellcheck = false;
|
||||
input.rows = 4;
|
||||
|
||||
return input;
|
||||
}
|
||||
@@ -34,44 +34,48 @@ function createSourceInput() {
|
||||
* @param {AddWalletFormOptions} options
|
||||
*/
|
||||
export function createAddForm(options) {
|
||||
const form = document.createElement("form");
|
||||
const form = createElement("form", "add");
|
||||
const title = document.createElement("h2");
|
||||
const description = document.createElement("p");
|
||||
const name = document.createElement("input");
|
||||
const source = createSourceInput();
|
||||
const actions = document.createElement("footer");
|
||||
const cancel = document.createElement("button");
|
||||
const submit = document.createElement("button");
|
||||
const status = document.createElement("output");
|
||||
const fields = [
|
||||
createField("name", name),
|
||||
createField("xpub or descriptor", source),
|
||||
];
|
||||
|
||||
title.append("Watch wallet");
|
||||
title.append("Add wallet");
|
||||
description.append(
|
||||
"Import an xpub or watch-only descriptor. Spending keys are never needed.",
|
||||
);
|
||||
name.name = "name";
|
||||
name.autocomplete = "off";
|
||||
name.placeholder = "Wallet name";
|
||||
name.placeholder = "Wallet 1";
|
||||
name.required = true;
|
||||
cancel.type = "button";
|
||||
cancel.append("Cancel");
|
||||
submit.type = "submit";
|
||||
submit.classList.add("primary");
|
||||
submit.append("Add");
|
||||
actions.append(cancel, submit);
|
||||
form.append(
|
||||
title,
|
||||
description,
|
||||
...fields,
|
||||
actions,
|
||||
status,
|
||||
);
|
||||
cancel.addEventListener("click", options.onCancel);
|
||||
source.addEventListener("input", () => {
|
||||
source.removeAttribute("aria-invalid");
|
||||
});
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void options.onSubmit({
|
||||
name,
|
||||
source,
|
||||
submit,
|
||||
status,
|
||||
form,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
main.wallets {
|
||||
.wallets__dialog {
|
||||
> form {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
.add {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
|
||||
> footer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: end;
|
||||
> h2 {
|
||||
color: var(--black);
|
||||
font-size: 2.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
:is(input, textarea) {
|
||||
border: 1px solid var(--gray);
|
||||
color: var(--black);
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--gray);
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
main.wallets {
|
||||
.wallets__dialog {
|
||||
width: min(100% - 2rem, 30rem);
|
||||
border: 1px solid color-mix(in oklch, var(--gray) 36%, transparent);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
color: var(--white);
|
||||
background: var(--black);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
background: color-mix(in oklch, var(--black) 72%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,36 @@ import { createElement } from "../dom.js";
|
||||
/**
|
||||
* @typedef {Object} EmptyOptions
|
||||
* @property {() => void} onAdd
|
||||
* @property {() => void} [onClear]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {EmptyOptions} options
|
||||
*/
|
||||
export function createEmpty(options) {
|
||||
const empty = createElement("section", "wallets__empty");
|
||||
const empty = createElement("section", "empty");
|
||||
const title = document.createElement("h1");
|
||||
const text = document.createElement("p");
|
||||
const actions = document.createElement("menu");
|
||||
const button = document.createElement("button");
|
||||
|
||||
text.append("No wallet imported yet");
|
||||
title.append("No wallet yet");
|
||||
text.append("Import an xpub or watch-only descriptor to start watching activity.");
|
||||
button.type = "button";
|
||||
button.classList.add("primary");
|
||||
button.append("Add wallet");
|
||||
button.addEventListener("click", options.onAdd);
|
||||
empty.append(text, button);
|
||||
actions.append(button);
|
||||
|
||||
if (options.onClear) {
|
||||
const clear = document.createElement("button");
|
||||
|
||||
clear.type = "button";
|
||||
clear.append("Clear");
|
||||
clear.addEventListener("click", options.onClear);
|
||||
actions.append(clear);
|
||||
}
|
||||
|
||||
empty.append(title, text, actions);
|
||||
|
||||
return empty;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
main.wallets {
|
||||
.wallets__empty {
|
||||
.empty {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
place-content: center;
|
||||
min-height: calc(100dvh - 2 * var(--offset));
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 4rem;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 31rem;
|
||||
margin: 0;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
|
||||
> menu {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @param {string} label
|
||||
* @param {HTMLInputElement | HTMLSelectElement} control
|
||||
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} control
|
||||
*/
|
||||
export function createField(label, control) {
|
||||
const element = document.createElement("label");
|
||||
|
||||
@@ -7,12 +7,11 @@ import { createEmpty } from "./empty/index.js";
|
||||
import { getErrorMessage } from "./errors.js";
|
||||
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 { readWalletSourceText } from "./add/source.js";
|
||||
import { scanStatus } from "./wallet/status.js";
|
||||
import { createSelector } from "./selector/index.js";
|
||||
import { createSetup } from "./setup/index.js";
|
||||
import { createStart } from "./start/index.js";
|
||||
import {
|
||||
createWalletPanel,
|
||||
renderWalletPanel,
|
||||
@@ -33,7 +32,7 @@ export function createWalletsPage() {
|
||||
header,
|
||||
addButton,
|
||||
privacyButton,
|
||||
lockButton,
|
||||
sessionButton,
|
||||
selector: selectorElement,
|
||||
walletList,
|
||||
content,
|
||||
@@ -67,6 +66,16 @@ export function createWalletsPage() {
|
||||
render();
|
||||
}
|
||||
|
||||
function startEphemeral() {
|
||||
vault.startEphemeral();
|
||||
render();
|
||||
}
|
||||
|
||||
function clearEphemeral() {
|
||||
vault.clearEphemeral();
|
||||
render();
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
addDialog.replaceChildren(createAddForm({
|
||||
onCancel() {
|
||||
@@ -84,7 +93,12 @@ export function createWalletsPage() {
|
||||
syncBtcAmounts();
|
||||
});
|
||||
|
||||
lockButton.addEventListener("click", () => {
|
||||
sessionButton.addEventListener("click", () => {
|
||||
if (vault.isEphemeral()) {
|
||||
clearEphemeral();
|
||||
return;
|
||||
}
|
||||
|
||||
lock();
|
||||
});
|
||||
|
||||
@@ -92,22 +106,21 @@ export function createWalletsPage() {
|
||||
openAdd();
|
||||
});
|
||||
|
||||
function renderLocked() {
|
||||
content.replaceChildren(createLock({
|
||||
onUnlock(password, button, status) {
|
||||
return unlock(password, button, status);
|
||||
/**
|
||||
* @param {"create" | "unlock"} mode
|
||||
*/
|
||||
function renderStart(mode) {
|
||||
content.replaceChildren(createStart({
|
||||
mode,
|
||||
onPassword(password, button, status) {
|
||||
return mode === "unlock"
|
||||
? unlock(password, button, status)
|
||||
: setup(password, button, status);
|
||||
},
|
||||
onReset() {
|
||||
reset();
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSetup() {
|
||||
content.replaceChildren(createSetup({
|
||||
onCreate(password, button, status) {
|
||||
return setup(password, button, status);
|
||||
onEphemeral() {
|
||||
startEphemeral();
|
||||
},
|
||||
onReset: mode === "unlock" ? reset : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -170,18 +183,24 @@ export function createWalletsPage() {
|
||||
* @param {string} password
|
||||
* @param {HTMLButtonElement} button
|
||||
* @param {HTMLElement} status
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function unlock(password, button, status) {
|
||||
let unlocked = false;
|
||||
|
||||
await withBusy(button, "Unlock", "Unlocking", async () => {
|
||||
setStatus(status, "");
|
||||
|
||||
try {
|
||||
await vault.unlock(password);
|
||||
unlocked = true;
|
||||
render();
|
||||
} catch {
|
||||
setStatus(status, "Invalid password");
|
||||
unlocked = false;
|
||||
}
|
||||
});
|
||||
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +209,7 @@ export function createWalletsPage() {
|
||||
* @param {HTMLElement} status
|
||||
*/
|
||||
async function setup(password, button, status) {
|
||||
await withBusy(button, "Continue", "Creating", async () => {
|
||||
await withBusy(button, "Create", "Creating", async () => {
|
||||
setStatus(status, "");
|
||||
|
||||
try {
|
||||
@@ -205,20 +224,22 @@ export function createWalletsPage() {
|
||||
function renderContent() {
|
||||
const needsSetup = vault.needsSetup();
|
||||
const locked = vault.isLocked();
|
||||
const ephemeral = vault.isEphemeral();
|
||||
const current = vault.current();
|
||||
const empty = !needsSetup && !locked && !current;
|
||||
|
||||
header.hidden = locked || needsSetup || empty;
|
||||
selectorElement.hidden = locked || needsSetup || empty;
|
||||
lockButton.hidden = locked || needsSetup || !vault.hasPassword;
|
||||
sessionButton.hidden = locked || needsSetup || (!vault.hasPassword && !ephemeral);
|
||||
sessionButton.textContent = ephemeral ? "Clear" : "Lock";
|
||||
|
||||
if (needsSetup) {
|
||||
renderSetup();
|
||||
renderStart("create");
|
||||
return;
|
||||
}
|
||||
|
||||
if (locked) {
|
||||
renderLocked();
|
||||
renderStart("unlock");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,6 +248,7 @@ export function createWalletsPage() {
|
||||
onAdd() {
|
||||
openAdd();
|
||||
},
|
||||
onClear: ephemeral ? clearEphemeral : undefined,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
@@ -246,28 +268,24 @@ export function createWalletsPage() {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {HTMLInputElement} options.name
|
||||
* @param {HTMLInputElement} options.source
|
||||
* @param {HTMLTextAreaElement} options.source
|
||||
* @param {HTMLButtonElement} options.submit
|
||||
* @param {HTMLElement} options.status
|
||||
* @param {HTMLFormElement} options.form
|
||||
*/
|
||||
async function submitAdd({
|
||||
name,
|
||||
source,
|
||||
submit,
|
||||
status,
|
||||
form,
|
||||
}) {
|
||||
await withBusy(submit, "Add", "Adding", async () => {
|
||||
setStatus(status, "Checking wallet");
|
||||
source.removeAttribute("aria-invalid");
|
||||
|
||||
try {
|
||||
const value = readWalletSourceText(source.value);
|
||||
|
||||
await generateAddressesFromWalletSource(value, { count: 1 });
|
||||
|
||||
setStatus(status, "Saving");
|
||||
|
||||
await vault.addWallet({
|
||||
name: name.value,
|
||||
source: value,
|
||||
@@ -276,8 +294,9 @@ export function createWalletsPage() {
|
||||
form.reset();
|
||||
addDialog.close();
|
||||
render();
|
||||
} catch (error) {
|
||||
setStatus(status, getErrorMessage(error));
|
||||
} catch {
|
||||
source.setAttribute("aria-invalid", "true");
|
||||
source.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createElement } from "../dom.js";
|
||||
* @property {HTMLElement} header
|
||||
* @property {HTMLButtonElement} addButton
|
||||
* @property {HTMLButtonElement} privacyButton
|
||||
* @property {HTMLButtonElement} lockButton
|
||||
* @property {HTMLButtonElement} sessionButton
|
||||
* @property {HTMLElement} selector
|
||||
* @property {HTMLElement} walletList
|
||||
* @property {HTMLElement} content
|
||||
@@ -22,24 +22,21 @@ export function createLayout() {
|
||||
const actions = document.createElement("menu");
|
||||
const addButton = document.createElement("button");
|
||||
const privacyButton = document.createElement("button");
|
||||
const lockButton = document.createElement("button");
|
||||
const sessionButton = document.createElement("button");
|
||||
const selector = createElement("section", "wallets__selector");
|
||||
const walletList = document.createElement("nav");
|
||||
const content = document.createElement("article");
|
||||
const addDialog = createElement("dialog", "wallets__dialog");
|
||||
const addDialog = document.createElement("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");
|
||||
sessionButton.type = "button";
|
||||
sessionButton.append("Lock");
|
||||
content.setAttribute("aria-live", "polite");
|
||||
walletList.setAttribute("tabindex", "0");
|
||||
walletList.setAttribute("aria-label", "Wallets");
|
||||
actions.append(addButton, privacyButton, lockButton);
|
||||
actions.append(addButton, privacyButton, sessionButton);
|
||||
header.append(actions);
|
||||
selector.append(walletList);
|
||||
main.append(header, selector, content, addDialog);
|
||||
@@ -49,7 +46,7 @@ export function createLayout() {
|
||||
header,
|
||||
addButton,
|
||||
privacyButton,
|
||||
lockButton,
|
||||
sessionButton,
|
||||
selector,
|
||||
walletList,
|
||||
content,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { createElement } from "../dom.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} LockOptions
|
||||
* @property {(password: string, button: HTMLButtonElement, status: HTMLElement) => void | Promise<void>} onUnlock
|
||||
* @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.classList.remove("holding");
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (timer !== undefined) return;
|
||||
|
||||
button.classList.add("holding");
|
||||
timer = window.setTimeout(() => {
|
||||
timer = undefined;
|
||||
button.classList.remove("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 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 = document.createElement("output");
|
||||
|
||||
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.classList.add("primary");
|
||||
button.append("Unlock");
|
||||
reset.type = "button";
|
||||
reset.append("Reset vault");
|
||||
form.append(password, button);
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void options.onUnlock(password.value, button, status);
|
||||
});
|
||||
bindResetHold(reset, options.onReset);
|
||||
section.append(title, form, reset, status);
|
||||
queueMicrotask(() => {
|
||||
password.focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
return section;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
main.wallets {
|
||||
.wallets__unlock {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
place-content: center;
|
||||
width: min(100%, 28rem);
|
||||
min-height: calc(100dvh - 2 * var(--offset));
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
> 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;
|
||||
}
|
||||
|
||||
&.holding::before {
|
||||
transform: scaleX(1);
|
||||
transition: transform 2s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,11 +74,15 @@ function setAddress(element, value, render) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLInputElement} input
|
||||
* @param {HTMLInputElement | HTMLTextAreaElement} input
|
||||
*/
|
||||
function setInput(input) {
|
||||
addEffect(input, () => {
|
||||
input.type = hidden ? "password" : "text";
|
||||
if (input instanceof HTMLTextAreaElement) {
|
||||
input.style.setProperty("-webkit-text-security", hidden ? "disc" : "");
|
||||
} else {
|
||||
input.type = hidden ? "password" : "text";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { createElement } from "../dom.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} SetupOptions
|
||||
* @property {(password: string, button: HTMLButtonElement, status: HTMLElement) => void | Promise<void>} onCreate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function createDescriptionText(text) {
|
||||
const paragraph = document.createElement("p");
|
||||
|
||||
paragraph.append(text);
|
||||
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SetupOptions} options
|
||||
*/
|
||||
export function createSetup(options) {
|
||||
const section = createElement("section", "wallets__setup");
|
||||
const title = document.createElement("h1");
|
||||
const description = document.createElement("article");
|
||||
const form = document.createElement("form");
|
||||
const password = document.createElement("input");
|
||||
const button = document.createElement("button");
|
||||
const status = document.createElement("output");
|
||||
|
||||
title.append("Wallets");
|
||||
description.append(
|
||||
createDescriptionText(
|
||||
"A privacy-preserving xpub viewer that runs in your browser and never uploads your xpub.",
|
||||
),
|
||||
createDescriptionText(
|
||||
"Import an xpub or watch-only descriptor to view a Bitcoin wallet without spending access.",
|
||||
),
|
||||
createDescriptionText(
|
||||
"Addresses are derived locally, checked through prefix buckets, and saved encrypted in this browser.",
|
||||
),
|
||||
createDescriptionText(
|
||||
"Privacy benefits can be drastically reduced if those addresses are already linked together on-chain.",
|
||||
),
|
||||
);
|
||||
password.name = "password";
|
||||
password.type = "password";
|
||||
password.autocomplete = "new-password";
|
||||
password.placeholder = "Set password";
|
||||
password.required = true;
|
||||
button.type = "submit";
|
||||
button.classList.add("primary");
|
||||
button.append("Continue");
|
||||
form.append(password, button);
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void options.onCreate(password.value, button, status);
|
||||
});
|
||||
section.append(title, description, form, status);
|
||||
|
||||
return section;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
main.wallets {
|
||||
.wallets__setup {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
place-content: center;
|
||||
width: min(100%, 36rem);
|
||||
min-height: calc(100dvh - 2 * var(--offset));
|
||||
margin-inline: auto;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
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;
|
||||
}
|
||||
|
||||
> article {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
> form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 235 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 355 KiB |
@@ -0,0 +1,126 @@
|
||||
import { createElement } from "../dom.js";
|
||||
import { createResetButton } from "./reset/index.js";
|
||||
|
||||
/**
|
||||
* @typedef {"create" | "unlock"} StartMode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StartOptions
|
||||
* @property {StartMode} mode
|
||||
* @property {(password: string, button: HTMLButtonElement, status: HTMLElement) => boolean | void | Promise<boolean | void>} onPassword
|
||||
* @property {() => void} onEphemeral
|
||||
* @property {() => void} [onReset]
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @param {StartOptions} options
|
||||
*/
|
||||
export function createStart(options) {
|
||||
const section = createElement("section", "start");
|
||||
const story = document.createElement("article");
|
||||
const title = document.createElement("h1");
|
||||
const titleBreak = document.createElement("br");
|
||||
const titleAccent = document.createElement("span");
|
||||
const lead = document.createElement("p");
|
||||
const details = document.createElement("ul");
|
||||
const warningRule = document.createElement("hr");
|
||||
const warning = document.createElement("p");
|
||||
const modes = document.createElement("div");
|
||||
const persistent = document.createElement("section");
|
||||
const persistentTitle = document.createElement("h2");
|
||||
const persistentText = document.createElement("p");
|
||||
const form = document.createElement("form");
|
||||
const password = document.createElement("input");
|
||||
const submit = document.createElement("button");
|
||||
const divider = document.createElement("p");
|
||||
const temporary = document.createElement("section");
|
||||
const temporaryTitle = document.createElement("h2");
|
||||
const temporaryText = document.createElement("p");
|
||||
const temporaryButton = document.createElement("button");
|
||||
const status = document.createElement("output");
|
||||
const unlock = options.mode === "unlock";
|
||||
|
||||
titleAccent.append("wallets");
|
||||
title.append("Watch-only", titleBreak, titleAccent);
|
||||
lead.append("View a Bitcoin wallet privately, without spending access.");
|
||||
details.append(
|
||||
createDetail("Open xpubs and watch-only descriptors."),
|
||||
createDetail("Addresses are derived on your device."),
|
||||
createDetail("Anonymity sets increase lookup privacy."),
|
||||
createDetail("Save encrypted wallets, or use a temporary session."),
|
||||
);
|
||||
warning.append(
|
||||
"Use a VPN for extra network privacy.",
|
||||
document.createElement("br"),
|
||||
"On-chain address links will reduce anonymity.",
|
||||
);
|
||||
story.append(title, lead, details, warningRule, warning);
|
||||
persistentTitle.append("Persistent vault");
|
||||
persistentText.append(
|
||||
unlock
|
||||
? "Unlock the encrypted vault saved in this browser."
|
||||
: "Create an encrypted vault saved in this browser.",
|
||||
);
|
||||
password.name = "password";
|
||||
password.type = "password";
|
||||
password.autocomplete = unlock ? "current-password" : "new-password";
|
||||
password.autofocus = true;
|
||||
password.placeholder = unlock ? "Password" : "Set password";
|
||||
password.required = true;
|
||||
submit.type = "submit";
|
||||
submit.append(unlock ? "Unlock" : "Create");
|
||||
form.append(password, submit);
|
||||
function clearInvalid() {
|
||||
password.removeAttribute("aria-invalid");
|
||||
}
|
||||
|
||||
password.addEventListener("input", clearInvalid);
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
clearInvalid();
|
||||
void (async () => {
|
||||
const valid = await options.onPassword(password.value, submit, status);
|
||||
|
||||
if (valid === false) {
|
||||
password.setAttribute("aria-invalid", "true");
|
||||
password.focus({ preventScroll: true });
|
||||
}
|
||||
})();
|
||||
});
|
||||
persistent.append(persistentTitle, persistentText, form);
|
||||
|
||||
if (options.onReset) {
|
||||
persistent.append(createResetButton(options.onReset));
|
||||
}
|
||||
|
||||
divider.append("OR");
|
||||
temporaryTitle.append("Temporary vault");
|
||||
temporaryText.append("Wallets are never saved to this browser.");
|
||||
temporaryButton.type = "button";
|
||||
temporaryButton.append("Start temporary");
|
||||
temporaryButton.addEventListener("click", () => {
|
||||
options.onEphemeral();
|
||||
});
|
||||
temporary.append(temporaryTitle, temporaryText, temporaryButton);
|
||||
persistent.append(status);
|
||||
modes.append(persistent, divider, temporary);
|
||||
section.append(story, modes);
|
||||
queueMicrotask(() => {
|
||||
password.focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function createDetail(text) {
|
||||
const item = document.createElement("li");
|
||||
|
||||
item.append(text);
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { createElement } from "../../dom.js";
|
||||
|
||||
const FILL_MS = 2_000;
|
||||
const DRAIN_MS = 600;
|
||||
const LABEL = "Reset vault";
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
function clampProgress(value) {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
* @param {() => void} onReset
|
||||
*/
|
||||
function bindHold(button, onReset) {
|
||||
/** @type {number | undefined} */
|
||||
let frame;
|
||||
let holding = false;
|
||||
let progress = 0;
|
||||
let previous = 0;
|
||||
|
||||
function render() {
|
||||
button.style.setProperty("--reset-progress", String(progress));
|
||||
button.style.setProperty("--reset-progress-width", `${progress * 100}%`);
|
||||
button.classList.toggle("active", progress > 0);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (frame === undefined) return;
|
||||
|
||||
cancelAnimationFrame(frame);
|
||||
frame = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} now
|
||||
*/
|
||||
function tick(now) {
|
||||
const elapsed = now - previous;
|
||||
const rate = elapsed / (holding ? FILL_MS : DRAIN_MS);
|
||||
|
||||
previous = now;
|
||||
progress = clampProgress(progress + (holding ? rate : -rate));
|
||||
render();
|
||||
|
||||
if (holding && progress === 1) {
|
||||
stop();
|
||||
holding = false;
|
||||
progress = 0;
|
||||
button.classList.remove("holding");
|
||||
render();
|
||||
onReset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!holding && progress === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
frame = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (frame !== undefined) return;
|
||||
|
||||
previous = performance.now();
|
||||
frame = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function release() {
|
||||
if (!holding) return;
|
||||
|
||||
holding = false;
|
||||
button.classList.remove("holding");
|
||||
run();
|
||||
}
|
||||
|
||||
function hold() {
|
||||
stop();
|
||||
|
||||
holding = true;
|
||||
button.classList.add("holding");
|
||||
run();
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
button.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
|
||||
button.setPointerCapture(event.pointerId);
|
||||
hold();
|
||||
});
|
||||
button.addEventListener("pointerup", release);
|
||||
button.addEventListener("pointercancel", release);
|
||||
button.addEventListener("lostpointercapture", release);
|
||||
button.addEventListener("keydown", (event) => {
|
||||
if (event.repeat || (event.key !== " " && event.key !== "Enter")) return;
|
||||
|
||||
event.preventDefault();
|
||||
hold();
|
||||
});
|
||||
button.addEventListener("keyup", (event) => {
|
||||
if (event.key === " " || event.key === "Enter") {
|
||||
release();
|
||||
}
|
||||
});
|
||||
button.addEventListener("blur", release);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => void} onReset
|
||||
*/
|
||||
export function createResetButton(onReset) {
|
||||
const button = createElement("button", "reset");
|
||||
const label = document.createElement("span");
|
||||
|
||||
button.type = "button";
|
||||
button.dataset.label = LABEL;
|
||||
button.title = "Hold to reset";
|
||||
label.append(LABEL);
|
||||
button.append(label);
|
||||
bindHold(button, onReset);
|
||||
|
||||
return button;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
main.wallets {
|
||||
.start {
|
||||
.reset {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
justify-self: start;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-color: transparent;
|
||||
color: color-mix(in oklch, var(--gray) 76%, transparent);
|
||||
background: transparent;
|
||||
font-size: var(--font-size-sm);
|
||||
--reset-progress: 0;
|
||||
--reset-progress-width: 0%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
z-index: -1;
|
||||
background: var(--red);
|
||||
transform: scaleX(var(--reset-progress));
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: attr(data-label);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: inherit;
|
||||
color: var(--black);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
clip-path: inset(0 calc(100% - var(--reset-progress-width)) 0 0);
|
||||
}
|
||||
|
||||
span {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-visible, :active):not(.holding) {
|
||||
color: var(--red);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
&.active::before,
|
||||
&.active::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
main.wallets {
|
||||
.start {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(19rem, 26rem);
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
width: min(100%, 68rem);
|
||||
min-height: calc(100dvh - 2 * var(--offset));
|
||||
margin-inline: auto;
|
||||
|
||||
@media (max-width: 56rem) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
align-content: center;
|
||||
width: min(100%, 39rem);
|
||||
margin-inline: 0 auto;
|
||||
}
|
||||
|
||||
> article {
|
||||
display: grid;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 4.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 0.95;
|
||||
|
||||
span {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> article > p:first-of-type {
|
||||
max-width: 35rem;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
|
||||
> article > ul {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin: 0.5rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
> article li {
|
||||
display: grid;
|
||||
grid-template-columns: 1rem minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
max-width: 34rem;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border: 1px solid var(--orange);
|
||||
border-radius: 50%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
> article > hr {
|
||||
width: min(100%, 34rem);
|
||||
height: 0.5px;
|
||||
border: 0;
|
||||
margin: 0.125rem 0 0;
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
> article > p:last-of-type {
|
||||
max-width: 34rem;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: grid;
|
||||
gap: 0.875rem;
|
||||
width: 100%;
|
||||
|
||||
> section {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
> p {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 0.625rem;
|
||||
align-items: center;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
height: 0.5px;
|
||||
background: var(--gray);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 0.5px;
|
||||
background: var(--gray);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--white);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 400;
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
form {
|
||||
--height: 2.375rem;
|
||||
|
||||
display: flex;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
:is(input, button) {
|
||||
height: var(--height);
|
||||
padding: 0 1rem;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
min-block-size: 0;
|
||||
border: 1px solid var(--gray);
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
color: var(--white);
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--gray);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
border-top-right-radius: 0.375rem;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> section:last-child {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
> section:last-child > button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
main.wallets {
|
||||
--offset: 4rem;
|
||||
--content-width: 72rem;
|
||||
--control-height: 2.75rem;
|
||||
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
@@ -20,41 +19,30 @@ main.wallets {
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
:is(input, select, button) {
|
||||
:is(input, select, textarea) {
|
||||
appearance: none;
|
||||
min-width: 0;
|
||||
height: var(--control-height);
|
||||
border: 1px solid color-mix(in oklch, var(--gray) 45%, transparent);
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0 0.875rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
color: var(--white);
|
||||
background: color-mix(in oklch, var(--black) 72%, var(--white));
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
textarea {
|
||||
min-height: 7rem;
|
||||
resize: vertical;
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
:is(input, select, button):focus-visible {
|
||||
:is(input, select, textarea):focus-visible {
|
||||
outline: 2px solid var(--orange);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border-color: var(--orange);
|
||||
color: var(--black);
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
:is(input, textarea)::placeholder {
|
||||
color: color-mix(in oklch, var(--gray) 70%, transparent);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
border-color: var(--gray);
|
||||
color: var(--black);
|
||||
background: var(--gray);
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export function createVault() {
|
||||
let selectedId = "";
|
||||
let locked = hasVault();
|
||||
let password = "";
|
||||
let ephemeral = false;
|
||||
/** @type {Map<string, WalletRuntime>} */
|
||||
const runtimes = new Map();
|
||||
|
||||
@@ -68,6 +69,7 @@ export function createVault() {
|
||||
function lock() {
|
||||
clear();
|
||||
password = "";
|
||||
ephemeral = false;
|
||||
locked = hasVault();
|
||||
}
|
||||
|
||||
@@ -75,9 +77,24 @@ export function createVault() {
|
||||
vaultStorage.reset();
|
||||
clear();
|
||||
password = "";
|
||||
ephemeral = false;
|
||||
locked = false;
|
||||
}
|
||||
|
||||
function startEphemeral() {
|
||||
clear();
|
||||
password = "";
|
||||
ephemeral = true;
|
||||
locked = false;
|
||||
}
|
||||
|
||||
function clearEphemeral() {
|
||||
clear();
|
||||
password = "";
|
||||
ephemeral = false;
|
||||
locked = hasVault();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pagePassword
|
||||
*/
|
||||
@@ -85,6 +102,7 @@ export function createVault() {
|
||||
await vaultStorage.setup(pagePassword);
|
||||
clear();
|
||||
password = pagePassword;
|
||||
ephemeral = false;
|
||||
locked = false;
|
||||
}
|
||||
|
||||
@@ -96,6 +114,7 @@ export function createVault() {
|
||||
syncSelected();
|
||||
runtimes.clear();
|
||||
password = pagePassword;
|
||||
ephemeral = false;
|
||||
locked = false;
|
||||
|
||||
for (const wallet of wallets) {
|
||||
@@ -107,6 +126,16 @@ export function createVault() {
|
||||
* @param {AddWalletInput} input
|
||||
*/
|
||||
async function addWallet(input) {
|
||||
if (ephemeral) {
|
||||
const wallet = vaultStorage.createWallet(input);
|
||||
|
||||
wallets = [...wallets, wallet];
|
||||
selectedId = wallet.id;
|
||||
locked = false;
|
||||
runtimes.set(wallet.id, createRuntime(wallet.source));
|
||||
return;
|
||||
}
|
||||
|
||||
const added = await vaultStorage.addWallet(wallets, input, password);
|
||||
|
||||
wallets = added.wallets;
|
||||
@@ -126,16 +155,21 @@ export function createVault() {
|
||||
return password !== "";
|
||||
},
|
||||
needsSetup() {
|
||||
return !hasVault() && !password;
|
||||
return !hasVault() && !password && !ephemeral;
|
||||
},
|
||||
isLocked() {
|
||||
return locked && hasVault();
|
||||
return !ephemeral && locked && hasVault();
|
||||
},
|
||||
isEphemeral() {
|
||||
return ephemeral;
|
||||
},
|
||||
current,
|
||||
isCurrent,
|
||||
select,
|
||||
lock,
|
||||
reset,
|
||||
startEphemeral,
|
||||
clearEphemeral,
|
||||
setup,
|
||||
unlock,
|
||||
addWallet,
|
||||
|
||||
@@ -66,6 +66,21 @@ function reset() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AddWalletInput} input
|
||||
*/
|
||||
function createWallet(input) {
|
||||
const time = now();
|
||||
|
||||
return {
|
||||
id: createWalletId(),
|
||||
name: input.name.trim(),
|
||||
source: input.source.trim(),
|
||||
createdAt: time,
|
||||
updatedAt: time,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pagePassword
|
||||
*/
|
||||
@@ -107,14 +122,7 @@ async function writeWallets(wallets, pagePassword) {
|
||||
* @param {string} pagePassword
|
||||
*/
|
||||
async function addWallet(wallets, input, pagePassword) {
|
||||
const time = now();
|
||||
const wallet = {
|
||||
id: createWalletId(),
|
||||
name: input.name.trim(),
|
||||
source: input.source.trim(),
|
||||
createdAt: time,
|
||||
updatedAt: time,
|
||||
};
|
||||
const wallet = createWallet(input);
|
||||
const nextWallets = [...wallets, wallet];
|
||||
|
||||
await writeWallets(nextWallets, pagePassword);
|
||||
@@ -128,6 +136,7 @@ async function addWallet(wallets, input, pagePassword) {
|
||||
export const vaultStorage = /** @type {const} */ ({
|
||||
has,
|
||||
reset,
|
||||
createWallet,
|
||||
setup,
|
||||
load,
|
||||
addWallet,
|
||||
|
||||
@@ -77,10 +77,7 @@ async function copyReceiveAddress(receiveAddress, copy) {
|
||||
* @param {ReceiveAddress} receiveAddress
|
||||
*/
|
||||
function openReceiveDialog(host, receiveAddress) {
|
||||
const dialog = createElement(
|
||||
"dialog",
|
||||
"wallets__dialog wallets__receive-dialog",
|
||||
);
|
||||
const dialog = createElement("dialog", "receive");
|
||||
const content = document.createElement("article");
|
||||
const actions = document.createElement("footer");
|
||||
const copy = document.createElement("button");
|
||||
@@ -88,7 +85,6 @@ function openReceiveDialog(host, receiveAddress) {
|
||||
const close = document.createElement("button");
|
||||
|
||||
copy.type = "button";
|
||||
copy.classList.add("primary");
|
||||
copy.append("Copy");
|
||||
closeForm.method = "dialog";
|
||||
close.type = "submit";
|
||||
@@ -129,7 +125,6 @@ export function renderReceiveButton(element, receiveAddress) {
|
||||
|
||||
button.type = "button";
|
||||
button.disabled = !receiveAddress;
|
||||
button.classList.add("primary");
|
||||
button.append("Receive");
|
||||
button.addEventListener("click", () => {
|
||||
if (receiveAddress) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
main.wallets {
|
||||
.wallets__receive-dialog {
|
||||
dialog.receive {
|
||||
width: min(100% - 2rem, 32rem);
|
||||
|
||||
> article {
|
||||
@@ -24,7 +24,7 @@ main.wallets {
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
color: var(--white);
|
||||
color: var(--black);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user