diff --git a/public/brand/.gitkeep b/public/brand/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/public/brand/icon.png b/public/brand/icon.png
new file mode 100644
index 0000000..b83858d
Binary files /dev/null and b/public/brand/icon.png differ
diff --git a/public/brand/logo-dark.svg b/public/brand/logo-dark.svg
new file mode 100644
index 0000000..700c969
--- /dev/null
+++ b/public/brand/logo-dark.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/public/brand/logo.svg b/public/brand/logo.svg
new file mode 100644
index 0000000..60fd7b4
--- /dev/null
+++ b/public/brand/logo.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png
new file mode 100644
index 0000000..0147bc1
Binary files /dev/null and b/public/icons/icon-192.png differ
diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png
new file mode 100644
index 0000000..faf8265
Binary files /dev/null and b/public/icons/icon-512.png differ
diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png
new file mode 100644
index 0000000..609e48e
Binary files /dev/null and b/public/icons/maskable-512.png differ
diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts
new file mode 100644
index 0000000..c55f90d
--- /dev/null
+++ b/scripts/generate-icons.ts
@@ -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 →
+ * 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);
+});
diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png
new file mode 100644
index 0000000..d442934
Binary files /dev/null and b/src/app/apple-icon.png differ
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index af98450..fbab8dd 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/app/globals.css b/src/app/globals.css
index 6a25b1b..ae2ff36 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -18,6 +18,14 @@
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--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-4: var(--chart-4);
--color-chart-3: var(--chart-3);
@@ -48,73 +56,98 @@
--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 {
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
+ --background: oklch(0.99 0.004 250);
+ --foreground: oklch(0.22 0.04 262);
--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-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
+ --popover-foreground: oklch(0.22 0.04 262);
+ --primary: oklch(0.49 0.17 258);
+ --primary-foreground: oklch(0.99 0.004 250);
+ --secondary: oklch(0.955 0.016 250);
+ --secondary-foreground: oklch(0.3 0.06 260);
+ --muted: oklch(0.955 0.012 250);
+ --muted-foreground: oklch(0.5 0.03 258);
+ --accent: oklch(0.935 0.03 250);
+ --accent-foreground: oklch(0.3 0.08 260);
+ --destructive: oklch(0.56 0.21 25);
+ --border: oklch(0.905 0.016 252);
+ --input: oklch(0.89 0.018 252);
+ --ring: oklch(0.6 0.15 258);
+
+ --income: oklch(0.58 0.15 152);
+ --income-foreground: oklch(0.99 0.004 250);
+ --expense: oklch(0.56 0.21 25);
+ --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;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
+ --sidebar: oklch(0.975 0.008 250);
+ --sidebar-foreground: oklch(0.22 0.04 262);
+ --sidebar-primary: oklch(0.49 0.17 258);
+ --sidebar-primary-foreground: oklch(0.99 0.004 250);
+ --sidebar-accent: oklch(0.935 0.03 250);
+ --sidebar-accent-foreground: oklch(0.3 0.08 260);
+ --sidebar-border: oklch(0.905 0.016 252);
+ --sidebar-ring: oklch(0.6 0.15 258);
}
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --background: oklch(0.18 0.04 264);
+ --foreground: oklch(0.96 0.01 250);
+ --card: oklch(0.22 0.045 264);
+ --card-foreground: oklch(0.96 0.01 250);
+ --popover: oklch(0.22 0.045 264);
+ --popover-foreground: oklch(0.96 0.01 250);
+ --primary: oklch(0.76 0.12 245);
+ --primary-foreground: oklch(0.18 0.04 264);
+ --secondary: oklch(0.27 0.045 264);
+ --secondary-foreground: oklch(0.96 0.01 250);
+ --muted: oklch(0.26 0.04 264);
+ --muted-foreground: oklch(0.73 0.03 250);
+ --accent: oklch(0.3 0.06 262);
+ --accent-foreground: oklch(0.96 0.01 250);
+ --destructive: oklch(0.7 0.18 22);
+ --border: oklch(1 0 0 / 11%);
+ --input: oklch(1 0 0 / 16%);
+ --ring: oklch(0.7 0.12 245);
+
+ --income: oklch(0.75 0.16 152);
+ --income-foreground: oklch(0.18 0.04 264);
+ --expense: oklch(0.7 0.18 22);
+ --expense-foreground: oklch(0.18 0.04 264);
+ --budget: oklch(0.73 0.13 245);
+ --budget-foreground: oklch(0.18 0.04 264);
+ --warning: oklch(0.79 0.15 62);
+ --warning-foreground: oklch(0.18 0.04 264);
+
+ --chart-1: var(--income);
+ --chart-2: var(--budget);
+ --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 {
@@ -128,3 +161,8 @@
@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);
+}
diff --git a/src/app/icon.png b/src/app/icon.png
new file mode 100644
index 0000000..0147bc1
Binary files /dev/null and b/src/app/icon.png differ
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
new file mode 100644
index 0000000..6e4962f
--- /dev/null
+++ b/src/app/manifest.ts
@@ -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" },
+ ],
+ };
+}
diff --git a/src/components/brand/logo.tsx b/src/components/brand/logo.tsx
new file mode 100644
index 0000000..d544b6c
--- /dev/null
+++ b/src/components/brand/logo.tsx
@@ -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 (
+
+
+
+
+ );
+}