feat(brand): mavi/beyaz tema, yer tutucu logo, ikon üretimi ve PWA manifest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- YER TUTUCU: gerçek BÜTÇEM logosu ile değiştirin (koyu tema). -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="64" viewBox="0 0 240 64" role="img" aria-label="BÜTÇEM">
|
||||||
|
<rect x="1" y="1" width="238" height="62" rx="12" fill="none" stroke="#6bb9f8" stroke-width="2" stroke-dasharray="6 4"/>
|
||||||
|
<text x="120" y="40" text-anchor="middle" font-family="sans-serif" font-size="26" font-weight="700" fill="#6bb9f8">BÜTÇEM</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- YER TUTUCU: gerçek BÜTÇEM logosu ile değiştirin (açık tema). -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="64" viewBox="0 0 240 64" role="img" aria-label="BÜTÇEM">
|
||||||
|
<rect x="1" y="1" width="238" height="62" rx="12" fill="none" stroke="#105bbe" stroke-width="2" stroke-dasharray="6 4"/>
|
||||||
|
<text x="120" y="40" text-anchor="middle" font-family="sans-serif" font-size="26" font-weight="700" fill="#105bbe">BÜTÇEM</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 459 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Uygulama ikonlarını logodan üretir: npm run icons
|
||||||
|
*
|
||||||
|
* Kaynak: public/brand/icon.png (kare, en az 512×512).
|
||||||
|
* Çıktılar (commit edilir):
|
||||||
|
* 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)
|
||||||
|
*
|
||||||
|
* Logoyu değiştirdikten sonra bu betiği yeniden çalıştırın.
|
||||||
|
*/
|
||||||
|
import { 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");
|
||||||
|
/** Saydamlık kabul etmeyen ikonlarda arka plan: marka mavisi (--primary, açık tema). */
|
||||||
|
const BRAND_BG = "#105bbe";
|
||||||
|
|
||||||
|
async function png(size: number, opts: { flatten?: boolean; padding?: number } = {}) {
|
||||||
|
const inner = Math.round(size * (1 - (opts.padding ?? 0) * 2));
|
||||||
|
const resized = await sharp(SOURCE).resize(inner, inner, { fit: "contain" }).png().toBuffer();
|
||||||
|
let img = sharp({
|
||||||
|
create: {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
channels: 4,
|
||||||
|
background: opts.flatten || opts.padding ? BRAND_BG : { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
}).composite([{ input: resized, gravity: "center" }]);
|
||||||
|
if (opts.flatten) img = img.flatten({ background: BRAND_BG });
|
||||||
|
return img.png({ compressionLevel: 9 }).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PNG görüntülerinden ICO dosyası oluşturur (Vista+ tüm tarayıcılar PNG gömülü ICO destekler). */
|
||||||
|
function toIco(images: { size: number; data: Buffer }[]): Buffer {
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header.writeUInt16LE(0, 0); // reserved
|
||||||
|
header.writeUInt16LE(1, 2); // type: icon
|
||||||
|
header.writeUInt16LE(images.length, 4);
|
||||||
|
const entries: Buffer[] = [];
|
||||||
|
let offset = 6 + images.length * 16;
|
||||||
|
for (const { size, data } of images) {
|
||||||
|
const e = Buffer.alloc(16);
|
||||||
|
e.writeUInt8(size >= 256 ? 0 : size, 0);
|
||||||
|
e.writeUInt8(size >= 256 ? 0 : size, 1);
|
||||||
|
e.writeUInt8(0, 2); // palette
|
||||||
|
e.writeUInt8(0, 3); // reserved
|
||||||
|
e.writeUInt16LE(1, 4); // color planes
|
||||||
|
e.writeUInt16LE(32, 6); // bpp
|
||||||
|
e.writeUInt32LE(data.length, 8);
|
||||||
|
e.writeUInt32LE(offset, 12);
|
||||||
|
offset += data.length;
|
||||||
|
entries.push(e);
|
||||||
|
}
|
||||||
|
return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
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 out: [string, Buffer][] = [
|
||||||
|
["src/app/icon.png", await png(192)],
|
||||||
|
["src/app/apple-icon.png", 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 })],
|
||||||
|
[
|
||||||
|
"src/app/favicon.ico",
|
||||||
|
toIco(await Promise.all([16, 32, 48].map(async (size) => ({ size, data: await png(size) })))),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
for (const [file, data] of out) {
|
||||||
|
await writeFile(path.join(ROOT, file), data);
|
||||||
|
console.log(`✓ ${file} (${data.length} bayt)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err: unknown) => {
|
||||||
|
console.error(err instanceof Error ? err.message : err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -18,6 +18,14 @@
|
|||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-income: var(--income);
|
||||||
|
--color-income-foreground: var(--income-foreground);
|
||||||
|
--color-expense: var(--expense);
|
||||||
|
--color-expense-foreground: var(--expense-foreground);
|
||||||
|
--color-budget: var(--budget);
|
||||||
|
--color-budget-foreground: var(--budget-foreground);
|
||||||
|
--color-warning: var(--warning);
|
||||||
|
--color-warning-foreground: var(--warning-foreground);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
@@ -48,73 +56,98 @@
|
|||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BÜTÇEM marka paleti — mavi ve beyaz tonları (oklch). Sabit renk kodu yalnızca burada.
|
||||||
|
* Anlamsal renkler: --income (gelir/yeşil), --expense (gider/kırmızı),
|
||||||
|
* --budget (bütçe-plan/mavi), --warning (uyarı/turuncu). Grafikler de bunları kullanır.
|
||||||
|
*/
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.99 0.004 250);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.22 0.04 262);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.22 0.04 262);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.22 0.04 262);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.49 0.17 258);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.99 0.004 250);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.955 0.016 250);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.3 0.06 260);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.955 0.012 250);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.5 0.03 258);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.935 0.03 250);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.3 0.08 260);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.56 0.21 25);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.905 0.016 252);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.89 0.018 252);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.6 0.15 258);
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
--income: oklch(0.58 0.15 152);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--income-foreground: oklch(0.99 0.004 250);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--expense: oklch(0.56 0.21 25);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--expense-foreground: oklch(0.99 0.004 250);
|
||||||
|
--budget: oklch(0.55 0.17 258);
|
||||||
|
--budget-foreground: oklch(0.99 0.004 250);
|
||||||
|
--warning: oklch(0.68 0.17 55);
|
||||||
|
--warning-foreground: oklch(0.22 0.04 262);
|
||||||
|
|
||||||
|
--chart-1: var(--income);
|
||||||
|
--chart-2: var(--budget);
|
||||||
|
--chart-3: var(--expense);
|
||||||
|
--chart-4: var(--warning);
|
||||||
|
--chart-5: oklch(0.65 0.08 230);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.975 0.008 250);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.22 0.04 262);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.49 0.17 258);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.99 0.004 250);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.935 0.03 250);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.3 0.08 260);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.905 0.016 252);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.6 0.15 258);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.18 0.04 264);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.96 0.01 250);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.22 0.045 264);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.96 0.01 250);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.22 0.045 264);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.96 0.01 250);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.76 0.12 245);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.18 0.04 264);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.27 0.045 264);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.96 0.01 250);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.26 0.04 264);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.73 0.03 250);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.3 0.06 262);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.96 0.01 250);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.7 0.18 22);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 11%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 16%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.7 0.12 245);
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
--income: oklch(0.75 0.16 152);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--income-foreground: oklch(0.18 0.04 264);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--expense: oklch(0.7 0.18 22);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--expense-foreground: oklch(0.18 0.04 264);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--budget: oklch(0.73 0.13 245);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--budget-foreground: oklch(0.18 0.04 264);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--warning: oklch(0.79 0.15 62);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--warning-foreground: oklch(0.18 0.04 264);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--chart-1: var(--income);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--chart-2: var(--budget);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--chart-3: var(--expense);
|
||||||
|
--chart-4: var(--warning);
|
||||||
|
--chart-5: oklch(0.7 0.07 230);
|
||||||
|
--sidebar: oklch(0.2 0.045 264);
|
||||||
|
--sidebar-foreground: oklch(0.96 0.01 250);
|
||||||
|
--sidebar-primary: oklch(0.76 0.12 245);
|
||||||
|
--sidebar-primary-foreground: oklch(0.18 0.04 264);
|
||||||
|
--sidebar-accent: oklch(0.28 0.055 262);
|
||||||
|
--sidebar-accent-foreground: oklch(0.96 0.01 250);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 11%);
|
||||||
|
--sidebar-ring: oklch(0.7 0.12 245);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -128,3 +161,8 @@
|
|||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobil alt menü + iOS ana ekran çentiği için güvenli alan */
|
||||||
|
@utility pb-safe {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,26 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { tr } from "@/i18n/tr";
|
||||||
|
|
||||||
|
/** PWA manifest: "Ana ekrana ekle" ile kurulabilir. İkonlar: npm run icons. */
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
id: "/",
|
||||||
|
name: tr.app.name,
|
||||||
|
short_name: tr.app.name,
|
||||||
|
description: tr.app.tagline,
|
||||||
|
lang: "tr-TR",
|
||||||
|
dir: "ltr",
|
||||||
|
start_url: "/dashboard",
|
||||||
|
scope: "/",
|
||||||
|
display: "standalone",
|
||||||
|
orientation: "portrait",
|
||||||
|
background_color: "#fafcfe",
|
||||||
|
theme_color: "#105bbe",
|
||||||
|
categories: ["finance", "productivity"],
|
||||||
|
icons: [
|
||||||
|
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
|
||||||
|
{ src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
|
||||||
|
{ src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { tr } from "@/i18n/tr";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marka logosu: açık temada logo.svg, koyu temada logo-dark.svg (CSS ile seçilir,
|
||||||
|
* hydration uyumsuzluğu olmaz). Dosyalar public/brand/ altında.
|
||||||
|
*/
|
||||||
|
export function Logo({ className, priority }: { className?: string; priority?: boolean }) {
|
||||||
|
const common = { width: 120, height: 32, priority, unoptimized: true } as const;
|
||||||
|
return (
|
||||||
|
<span className={cn("inline-flex items-center", className)}>
|
||||||
|
<Image
|
||||||
|
src="/brand/logo.svg"
|
||||||
|
alt={tr.app.name}
|
||||||
|
className="h-8 w-auto dark:hidden"
|
||||||
|
{...common}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/brand/logo-dark.svg"
|
||||||
|
alt=""
|
||||||
|
className="hidden h-8 w-auto dark:block"
|
||||||
|
{...common}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||