mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-25 09:48:10 -07:00
website: redesign part 33
This commit is contained in:
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user