- Replace the crown logo with logo.svg / logo_bk.svg across both frontends; regenerate favicons and PWA icons via scripts/generate-brand-icons.cjs - Custom button: admin chooses whether it shows before login or only after - Dashboard: drop the survey-majority banner, hide the custom-order row Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
3.8 KiB
JavaScript
96 lines
3.8 KiB
JavaScript
// Builds every brand image of the two frontends from the two source SVGs in
|
|
// the repo root (see docs/BRANDING.md):
|
|
// logo.svg black mark on transparent — pages, favicons, PWA icons
|
|
// logo_bk.svg the same mark on a white rounded card — dark backdrops
|
|
//
|
|
// node scripts/generate-brand-icons.cjs
|
|
//
|
|
// sharp is resolved from apps/api, which already depends on it.
|
|
const { createRequire } = require('node:module');
|
|
const { readFileSync, writeFileSync } = require('node:fs');
|
|
const { join } = require('node:path');
|
|
|
|
const root = join(__dirname, '..');
|
|
const sharp = createRequire(join(root, 'apps/api/package.json'))('sharp');
|
|
|
|
const userApp = join(root, 'apps/user-app-v2');
|
|
const admin = join(root, 'apps/admin-panel');
|
|
|
|
// Illustrator's "<title>Asset 1</title>" is noise in an <img>.
|
|
const clean = (file) => readFileSync(join(root, file), 'utf8').replace(/\s*<title>[^<]*<\/title>/, '');
|
|
const logo = clean('logo.svg');
|
|
const card = clean('logo_bk.svg');
|
|
|
|
/** The mark rendered `width` px wide, transparent. */
|
|
const mark = (width) => sharp(Buffer.from(logo), { density: 600 }).resize({ width }).png().toBuffer();
|
|
|
|
/**
|
|
* Square icon: the mark centred on white. `fill` is the mark's width as a
|
|
* share of the side; `radius` rounds the white square (0 = full bleed, as the
|
|
* OS masks PWA icons itself).
|
|
*/
|
|
async function icon(size, { fill, radius }) {
|
|
const r = Math.round(size * radius);
|
|
const plate = Buffer.from(
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
|
|
`<rect width="${size}" height="${size}" rx="${r}" ry="${r}" fill="#fff"/></svg>`,
|
|
);
|
|
return sharp(plate)
|
|
.composite([{ input: await mark(Math.round(size * fill)), gravity: 'center' }])
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
/** .ico holding PNG entries (supported by every current browser). */
|
|
function ico(pngs) {
|
|
const header = Buffer.alloc(6);
|
|
header.writeUInt16LE(1, 2);
|
|
header.writeUInt16LE(pngs.length, 4);
|
|
const entries = [];
|
|
let offset = 6 + 16 * pngs.length;
|
|
for (const { size, data } of pngs) {
|
|
const e = Buffer.alloc(16);
|
|
e.writeUInt8(size >= 256 ? 0 : size, 0);
|
|
e.writeUInt8(size >= 256 ? 0 : size, 1);
|
|
e.writeUInt16LE(1, 4);
|
|
e.writeUInt16LE(32, 6);
|
|
e.writeUInt32LE(data.length, 8);
|
|
e.writeUInt32LE(offset, 12);
|
|
offset += data.length;
|
|
entries.push(e);
|
|
}
|
|
return Buffer.concat([header, ...entries, ...pngs.map((p) => p.data)]);
|
|
}
|
|
|
|
function write(path, data) {
|
|
writeFileSync(path, data);
|
|
console.log('wrote', path.slice(root.length + 1));
|
|
}
|
|
|
|
(async () => {
|
|
write(join(userApp, 'public/img/brand-logo.svg'), logo);
|
|
write(join(userApp, 'public/img/brand-logo-card.svg'), card);
|
|
write(join(userApp, 'src/assets/img/brand-logo.svg'), logo);
|
|
write(join(admin, 'src/assets/images/logo/brand-logo.svg'), logo);
|
|
|
|
// Raster copies at the old public URLs, for anything linking them directly.
|
|
const big = await mark(1024);
|
|
write(join(userApp, 'public/img/brand-logo.png'), big);
|
|
write(join(userApp, 'public/img/brand-logo.webp'), await sharp(big).webp({ quality: 90 }).toBuffer());
|
|
|
|
// PWA: one file serves "any" and "maskable", so the mark stays inside the
|
|
// maskable safe circle (80% of the side): a 1.45:1 mark at 64% width does.
|
|
write(join(userApp, 'public/icon-192.png'), await icon(192, { fill: 0.64, radius: 0 }));
|
|
write(join(userApp, 'public/icon-512.png'), await icon(512, { fill: 0.64, radius: 0 }));
|
|
|
|
// Tab icons: white rounded chip, so the black mark shows on dark tab strips too.
|
|
const chip = { fill: 0.86, radius: 0.2 };
|
|
const favicon = ico(await Promise.all([16, 32, 48, 64].map(async (size) => ({ size, data: await icon(size, chip) }))));
|
|
write(join(userApp, 'public/favicon.ico'), favicon);
|
|
write(join(admin, 'public/favicon.ico'), favicon);
|
|
write(join(admin, 'public/favicon.png'), await icon(192, chip));
|
|
})().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|