website: redesign part 33

This commit is contained in:
nym21
2026-06-21 17:12:25 +02:00
parent 2e401379a0
commit b3031b3375
31 changed files with 818 additions and 417 deletions
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

+126
View File
@@ -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;
}
+130
View File
@@ -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;
}
}
}
}
+198
View File
@@ -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%;
}
}
}
}