diff --git a/web/package.json b/web/package.json index 4185ce0..aa595ca 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "bones": "node --env-file-if-exists=../.env scripts/bones.mjs", "check:links": "node scripts/check-links.mjs dist", "check:i18n": "node scripts/check-i18n.mjs", + "test": "node --experimental-strip-types --test test/*.test.mjs", "check:hreflang": "node scripts/check-hreflang.mjs dist" }, "dependencies": { diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png index 40ddcc3..25ef08c 100644 Binary files a/web/public/apple-touch-icon.png and b/web/public/apple-touch-icon.png differ diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png index 9c7e061..c92803e 100644 Binary files a/web/public/favicon-32.png and b/web/public/favicon-32.png differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico index ebaea8d..0f400d0 100644 Binary files a/web/public/favicon.ico and b/web/public/favicon.ico differ diff --git a/web/public/favicon.svg b/web/public/favicon.svg index c38e8ef..3512f50 100644 --- a/web/public/favicon.svg +++ b/web/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/web/public/icon-192.png b/web/public/icon-192.png index 3b07de3..98d6d83 100644 Binary files a/web/public/icon-192.png and b/web/public/icon-192.png differ diff --git a/web/public/icon-512.png b/web/public/icon-512.png index 4487fc4..1c0b13d 100644 Binary files a/web/public/icon-512.png and b/web/public/icon-512.png differ diff --git a/web/scripts/make-assets.mjs b/web/scripts/make-assets.mjs index 19589c2..bc4df0a 100644 --- a/web/scripts/make-assets.mjs +++ b/web/scripts/make-assets.mjs @@ -6,6 +6,7 @@ * files change roughly never. * * node scripts/make-assets.mjs + * node scripts/make-assets.mjs --no-card # icon set only, needs no browser * * Playwright and sharp are not dependencies of this workspace. Both arrive transitively * under `astro` (which ships sharp for its image pipeline) and are resolved through it, @@ -14,10 +15,12 @@ * * The card is drawn in a real browser rather than assembled by hand because it uses the * site's own fonts and tokens: a social card that does not look like the site is worth - * less than no card. The moai is the same rect geometry as `components/Moai.astro`. + * less than no card. The moai on the card is the same rect geometry as + * `components/Moai.astro`. The icon set is a different mark: the pixel cashu nut carried + * over from the old site (`src/assets/cashu-logo.png`), composed with sharp alone. */ import { createRequire } from 'node:module'; -import { mkdir, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -100,14 +103,8 @@ p { margin-top: 28px; font-size: 27px; line-height: 1.45; color: #9A927E; max-wi
cashumints.space
`; -/* The icon master: the moai on the site's background, generous padding. */ -const iconHtml = `${moaiSvg(320)}`; +/* The icon source: the old site's pixel cashu nut, 1000x1000 with transparent margins. */ +const logoPath = path.join(root, 'src', 'assets', 'cashu-logo.png'); /** * A 32x32 ICO wrapping a PNG payload. @@ -138,46 +135,64 @@ function icoFromPng(png) { await mkdir(publicDir, { recursive: true }); -const browser = await chromium.launch(); -try { - const page = await browser.newPage({ - viewport: { width: 1200, height: 630 }, - deviceScaleFactor: 1, - }); +/* + * The icon master: the nut trimmed and centred on the brand ground, at the footprint the + * moai used to occupy (glyph ≈ 380px tall in the 512 tile). Content this size also stays + * inside the circle a maskable icon is cropped to. + */ +const glyph = await sharp(logoPath).trim().resize(384, 384, { fit: 'inside' }).png().toBuffer(); +const master = await sharp({ + create: { width: 512, height: 512, channels: 4, background: '#0E0D0B' }, +}) + .composite([{ input: glyph, gravity: 'centre' }]) + .png() + .toBuffer(); - await page.setContent(cardHtml, { waitUntil: 'load' }); - await page.evaluate(() => document.fonts.ready); - await page.screenshot({ path: path.join(publicDir, 'og.png') }); - console.log('og.png'); +for (const [name, size] of [ + ['icon-512.png', 512], + ['icon-192.png', 192], + ['favicon-32.png', 32], +]) { + await sharp(master).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(publicDir, name)); + console.log(name); +} - await page.setViewportSize({ width: 512, height: 512 }); - await page.setContent(iconHtml, { waitUntil: 'load' }); - const master = await page.screenshot({ type: 'png' }); +// iOS ignores transparency and composites on white; the master's ground is already opaque. +await sharp(master) + .resize(180, 180) + .png({ compressionLevel: 9 }) + .toFile(path.join(publicDir, 'apple-touch-icon.png')); +console.log('apple-touch-icon.png'); - for (const [name, size] of [ - ['icon-512.png', 512], - ['icon-192.png', 192], - ['favicon-32.png', 32], - ]) { - await sharp(master).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(publicDir, name)); - console.log(name); - } +const png32 = await sharp(master).resize(32, 32).png({ compressionLevel: 9 }).toBuffer(); +await writeFile(path.join(publicDir, 'favicon.ico'), icoFromPng(png32)); +console.log('favicon.ico'); - // iOS ignores transparency and composites on white, so flatten onto the brand ground. - await sharp(master) - .resize(180, 180) - .flatten({ background: '#0E0D0B' }) - .png({ compressionLevel: 9 }) - .toFile(path.join(publicDir, 'apple-touch-icon.png')); - console.log('apple-touch-icon.png'); +// The standalone SVG favicon: the nut itself, transparent like the old site served it. +// The PNG rides inside the SVG so one source file covers every size; `pixelated` keeps +// the pixel art crisp if anything scales it up. +const logoB64 = (await readFile(logoPath)).toString('base64'); +await writeFile( + path.join(publicDir, 'favicon.svg'), + `` + + ``, +); +console.log('favicon.svg'); - const png32 = await sharp(master).resize(32, 32).png({ compressionLevel: 9 }).toBuffer(); - await writeFile(path.join(publicDir, 'favicon.ico'), icoFromPng(png32)); - console.log('favicon.ico'); +if (!process.argv.includes('--no-card')) { + const browser = await chromium.launch(); + try { + const page = await browser.newPage({ + viewport: { width: 1200, height: 630 }, + deviceScaleFactor: 1, + }); - // The standalone SVG favicon: the same mark, at any size, for browsers that take it. - await writeFile(path.join(publicDir, 'favicon.svg'), moaiSvg(11)); - console.log('favicon.svg'); -} finally { - await browser.close(); + await page.setContent(cardHtml, { waitUntil: 'load' }); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: path.join(publicDir, 'og.png') }); + console.log('og.png'); + } finally { + await browser.close(); + } } diff --git a/web/src/assets/cashu-logo.png b/web/src/assets/cashu-logo.png new file mode 100644 index 0000000..1509511 Binary files /dev/null and b/web/src/assets/cashu-logo.png differ diff --git a/web/src/components/CopyableId.astro b/web/src/components/CopyableId.astro index 88a27c0..db0cee6 100644 --- a/web/src/components/CopyableId.astro +++ b/web/src/components/CopyableId.astro @@ -32,12 +32,14 @@ const t = useI18n(locale); const { value, label, kind = t('mint.kind.identifier'), class: className } = Astro.props; --- +{/* The accessible name repeats the visible text: a voice-control user says what + they see, and a name that omits it cannot be spoken at. */} diff --git a/web/src/components/LanguageSwitcher.astro b/web/src/components/LanguageSwitcher.astro index 4518c8d..9b35097 100644 --- a/web/src/components/LanguageSwitcher.astro +++ b/web/src/components/LanguageSwitcher.astro @@ -3,7 +3,7 @@ * The language switcher, in two shapes. * * `menu` is the header control: a compact button showing the current language's code, - * opening a list of the three. `inline` is the footer's plain row of three links. Both + * opening a searchable list. `inline` is the footer's plain row of links. Both * are built from the same list and the same hrefs, because they are the same control * and a reader who finds one and then the other should not have to work out whether * they do the same thing. @@ -67,25 +67,44 @@ const currentCode = locale.toUpperCase(); - +
+ + +
) : (