Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e5cff38
Add NIP-49 encrypted local key backup: Rust layer + egress guard
Jul 26, 2026
1c01889
Encrypted-by-default key backup UI + ncryptsec import (frontend half)
Jul 26, 2026
dde3718
Address implementation-review blockers: import ordering, site-granula…
Jul 26, 2026
7a6a2cd
Merge branch 'main' into eva/nip49-local-backup
Jul 28, 2026
51ea1b1
fix(desktop): rename settings row to Password Backup per spec
Jul 28, 2026
72f00af
fix(desktop): make saved .ncryptsec backups importable from file
Jul 28, 2026
d7e52ea
test(desktop): align main's smoke specs with encrypted-by-default bac…
Jul 28, 2026
855d25e
Merge remote-tracking branch 'origin/main' into eva/nip49-local-backup
Jul 28, 2026
88d08e4
feat(desktop): guide and verify password-protected backups (#3617)
tellaho Jul 29, 2026
6a24905
Merge remote-tracking branch 'origin/main' into eva/nip49-local-backup
Jul 29, 2026
e8c7f15
feat(desktop): integrate backup options into identity onboarding
tellaho Jul 29, 2026
ca754a5
fix(desktop): decouple backup surface navigation
tellaho Jul 29, 2026
38edd28
feat(desktop): explain system keychain protection during onboarding
tellaho Jul 30, 2026
e90a206
feat(desktop): refine backup onboarding security flow
tellaho Jul 30, 2026
72e4e31
fix(desktop): clarify keychain onboarding copy
tellaho Jul 30, 2026
1432a32
fix(desktop): improve backup password control contrast
tellaho Jul 30, 2026
8aaa80d
fix(desktop): darken backup password field controls
tellaho Jul 30, 2026
6b624e9
feat(desktop): illustrate encrypted backup flow
tellaho Jul 30, 2026
4a8aec8
fix(desktop): invert onboarding primary buttons
tellaho Jul 30, 2026
bfc79df
fix(desktop): scope inverted buttons to backup flows
tellaho Jul 30, 2026
09bc460
fix(desktop): wait for explicit backup download
tellaho Jul 30, 2026
9a7faa9
feat(desktop): refine optional backup verification
tellaho Jul 30, 2026
6261595
feat(desktop): polish backup flow completion
tellaho Jul 30, 2026
7b87864
test(desktop): align onboarding backup integration coverage
Jul 30, 2026
aa99e5c
feat(desktop): integrate onboarding key backup
Jul 30, 2026
de4547a
feat(desktop): add guided encrypted backup restore
tellaho Jul 30, 2026
3269fc7
refactor(desktop): streamline backup options ingress
tellaho Jul 30, 2026
52f86ca
chore(desktop): merge main into nip49 onboarding integration
Jul 30, 2026
d74384f
test(desktop): allow backup restore affordance in nip49 scan
Jul 30, 2026
f7fd927
merge: reconcile main data deletion updates
Jul 30, 2026
c30be77
style(desktop): apply canonical rustfmt wrapping
Jul 30, 2026
e8da1f3
fix(desktop): follow optional backup onboarding flow
Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 96 additions & 63 deletions desktop/scripts/texture-card/generate-card-texture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,83 +12,116 @@ import path from "node:path";
import { fileURLToPath } from "node:url";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT = path.resolve(
HERE,
"../../src/shared/ui/assets/card-texture.png",
);

// CSS-pixel source geometry. Screenshotting at DPR 2 produces a crisp asset.
const CARD_SIZE = 640;
const OUTSET = 96;
const CAPTURE_SIZE = CARD_SIZE + OUTSET * 2;
const OUTPUT_DIRECTORY = path.resolve(HERE, "../../src/shared/ui/assets");
const DPR = 2;

// Approved texture parameters, archived from the former runtime SVG filter.
const BLUR = 66;
const DILATE = Math.round(BLUR * 0.85);
const THRESHOLD_BIAS = 0.302;
const SLOPE = 8;
const FREQUENCY = 0.999;
const OCTAVES = 3;
const SEED = 5315;

await mkdir(path.dirname(OUTPUT), { recursive: true });
const TEXTURES = [
{
filename: "card-texture.png",
color: "white",
cardSize: 640,
outset: 96,
blur: 66,
innerBand: 112,
},
{
filename: "card-texture-dark.png",
color: "#171b21",
cardSize: 640,
outset: 96,
blur: 66,
innerBand: 112,
},
{
filename: "card-texture-compact.png",
color: "white",
cardSize: 320,
outset: 24,
blur: 24,
innerBand: 44,
},
{
filename: "card-texture-dark-compact.png",
color: "#171b21",
cardSize: 320,
outset: 24,
blur: 24,
innerBand: 44,
},
];

await mkdir(OUTPUT_DIRECTORY, { recursive: true });

const browser = await chromium.launch();
try {
const page = await browser.newPage({
deviceScaleFactor: DPR,
viewport: { height: CAPTURE_SIZE, width: CAPTURE_SIZE },
});
for (const texture of TEXTURES) {
const captureSize = texture.cardSize + texture.outset * 2;
const dilate = Math.round(texture.blur * 0.85);
const output = path.join(OUTPUT_DIRECTORY, texture.filename);
const page = await browser.newPage({
deviceScaleFactor: DPR,
viewport: { height: captureSize, width: captureSize },
});

await page.setContent(`<!doctype html>
<style>
html, body { margin: 0; width: 100%; height: 100%; background: transparent; }
#stage { position: relative; width: ${CAPTURE_SIZE}px; height: ${CAPTURE_SIZE}px; }
#core {
position: absolute;
inset: ${OUTSET + BLUR / 2}px;
background: white;
filter: blur(${BLUR / 3}px);
}
</style>
<div id="stage">
<svg width="${CAPTURE_SIZE}" height="${CAPTURE_SIZE}" aria-hidden="true">
<defs>
<filter id="texture" x="0" y="0" width="100%" height="100%"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="${DILATE}" result="squared" />
<feGaussianBlur in="squared" stdDeviation="${BLUR}" result="ramp" />
<feTurbulence type="fractalNoise" baseFrequency="${FREQUENCY}"
numOctaves="${OCTAVES}" seed="${SEED}" result="grain" />
<feColorMatrix in="grain" result="grainAlpha" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
1 0 0 0 0" />
<feComposite in="ramp" in2="grainAlpha" operator="arithmetic"
k1="0" k2="1" k3="-1" k4="${-THRESHOLD_BIAS}" result="dithered" />
<feComponentTransfer in="dithered" result="specks">
<feFuncA type="linear" slope="${SLOPE}" intercept="0" />
</feComponentTransfer>
<feFlood flood-color="white" result="white" />
<feComposite in="white" in2="specks" operator="in" />
</filter>
</defs>
<rect x="${OUTSET}" y="${OUTSET}" width="${CARD_SIZE}" height="${CARD_SIZE}"
fill="white" filter="url(#texture)" />
</svg>
<span id="core"></span>
</div>`);
await page.setContent(`<!doctype html>
<style>
html, body { margin: 0; width: 100%; height: 100%; background: transparent; }
#stage { position: relative; width: ${captureSize}px; height: ${captureSize}px; }
#core {
position: absolute;
inset: ${texture.outset + texture.blur / 2}px;
background: ${texture.color};
filter: blur(${texture.blur / 3}px);
}
</style>
<div id="stage">
<svg width="${captureSize}" height="${captureSize}" aria-hidden="true">
<defs>
<filter id="texture" x="0" y="0" width="100%" height="100%"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="${dilate}" result="squared" />
<feGaussianBlur in="squared" stdDeviation="${texture.blur}" result="ramp" />
<feTurbulence type="fractalNoise" baseFrequency="${FREQUENCY}"
numOctaves="${OCTAVES}" seed="${SEED}" result="grain" />
<feColorMatrix in="grain" result="grainAlpha" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
1 0 0 0 0" />
<feComposite in="ramp" in2="grainAlpha" operator="arithmetic"
k1="0" k2="1" k3="-1" k4="${-THRESHOLD_BIAS}" result="dithered" />
<feComponentTransfer in="dithered" result="specks">
<feFuncA type="linear" slope="${SLOPE}" intercept="0" />
</feComponentTransfer>
<feFlood flood-color="${texture.color}" result="surfaceColor" />
<feComposite in="surfaceColor" in2="specks" operator="in" />
</filter>
</defs>
<rect x="${texture.outset}" y="${texture.outset}" width="${texture.cardSize}" height="${texture.cardSize}"
fill="${texture.color}" filter="url(#texture)" />
</svg>
<span id="core"></span>
</div>`);

await page.locator("#stage").screenshot({
omitBackground: true,
path: OUTPUT,
});
await page.locator("#stage").screenshot({
omitBackground: true,
path: output,
});
await page.close();

console.log(`Generated ${output}`);
console.log(`Asset: ${captureSize * DPR}×${captureSize * DPR}px @${DPR}x`);
console.log(
`Runtime slice: ${(texture.outset + texture.innerBand) * DPR}px; outset: ${texture.outset}px`,
);
}
} finally {
await browser.close();
}

console.log(`Generated ${OUTPUT}`);
console.log(`Asset: ${CAPTURE_SIZE * DPR}×${CAPTURE_SIZE * DPR}px @${DPR}x`);
console.log(`Runtime slice: ${(OUTSET + 112) * DPR}px; outset: ${OUTSET}px`);
Loading
Loading