feat(brand): BÜTÇEM logosu ve ikon seti

- public/brand: logo.svg, logo-dark.svg, icon.svg, icon-maskable.svg (yazı Noto Sans Bold yolları, OFL)
- npm run icons artık icon.svg / icon-maskable.svg'den üretir; maskable ve Apple ikonları köşesiz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:44:59 +03:00
parent e57e7b5517
commit 381214c2cb
13 changed files with 87 additions and 14 deletions
+40 -5
View File
@@ -1,23 +1,30 @@
/**
* Uygulama ikonlarını logodan üretir: npm run icons
*
* Kaynak: public/brand/icon.png (kare, en az 512×512).
* Kaynak: public/brand/icon.svg (varsa; önce 512×512 icon.png olarak render edilir)
* veya doğrudan public/brand/icon.png (kare, en az 512×512).
* Çıktılar (commit edilir):
* public/brand/icon.png 512 px (icon.svg varsa ondan üretilir)
* src/app/favicon.ico 16/32/48 px (PNG gömülü ICO)
* src/app/icon.png 192 px → <link rel="icon">
* src/app/apple-icon.png 180 px, saydamsız → iOS ana ekran
* public/icons/icon-192.png PWA manifest
* public/icons/icon-512.png PWA manifest
* public/icons/maskable-512.png PWA manifest (purpose: maskable, %80 güvenli alan)
* public/icons/maskable-512.png PWA manifest (purpose: maskable)
*
* public/brand/icon-maskable.svg (köşesiz, tam zemin) varsa maskable ve Apple ikonları ondan
* üretilir; yoksa icon.png marka mavisi zemine %80 güvenli alanla yerleştirilir.
*
* Logoyu değiştirdikten sonra bu betiği yeniden çalıştırın.
*/
import { mkdir, writeFile } from "node:fs/promises";
import { access, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
const ROOT = process.cwd();
const SOURCE = path.join(ROOT, "public/brand/icon.png");
const SOURCE_SVG = path.join(ROOT, "public/brand/icon.svg");
const MASKABLE_SVG = path.join(ROOT, "public/brand/icon-maskable.svg");
/** Saydamlık kabul etmeyen ikonlarda arka plan: marka mavisi (--primary, açık tema). */
const BRAND_BG = "#105bbe";
@@ -60,19 +67,47 @@ function toIco(images: { size: number; data: Buffer }[]): Buffer {
return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]);
}
async function exists(file: string) {
return access(file).then(
() => true,
() => false,
);
}
async function main() {
if (await exists(SOURCE_SVG)) {
// Vektörden yüksek yoğunlukta render: küçük boyutlarda da keskin kenarlar.
const png512 = await sharp(SOURCE_SVG, { density: 288 }).resize(512, 512).png().toBuffer();
await writeFile(SOURCE, png512);
console.log(`✓ public/brand/icon.png ← icon.svg (${png512.length} bayt)`);
}
const meta = await sharp(SOURCE).metadata();
if (!meta.width || !meta.height || meta.width !== meta.height || meta.width < 512) {
throw new Error(`${SOURCE} kare ve en az 512×512 olmalı (şu an ${meta.width}×${meta.height}).`);
}
await mkdir(path.join(ROOT, "public/icons"), { recursive: true });
const fullBleed = (await exists(MASKABLE_SVG))
? (size: number) =>
sharp(MASKABLE_SVG, { density: 288 })
.resize(size, size)
.flatten({ background: BRAND_BG })
.png()
.toBuffer()
: null;
const out: [string, Buffer][] = [
["src/app/icon.png", await png(192)],
["src/app/apple-icon.png", await png(180, { flatten: true })],
[
"src/app/apple-icon.png",
fullBleed ? await fullBleed(180) : await png(180, { flatten: true }),
],
["public/icons/icon-192.png", await png(192)],
["public/icons/icon-512.png", await png(512)],
["public/icons/maskable-512.png", await png(512, { padding: 0.1 })],
[
"public/icons/maskable-512.png",
fullBleed ? await fullBleed(512) : await png(512, { padding: 0.1 }),
],
[
"src/app/favicon.ico",
toIco(await Promise.all([16, 32, 48].map(async (size) => ({ size, data: await png(size) })))),