feat(tema): D kısayolu, logodan yayılan tema geçişi, /surum-notlari beta sayfası (→ 1.1.0)

- lib/tema-gecisi.ts: View Transitions + CSS maskesi (kolay-tercih-mark.svg),
  merkez header'daki işaret; reduced-motion ve desteksiz tarayıcıda düz geçiş
- tema-kisayolu.tsx: D tuşu açık ↔ koyu (yazı alanı/değiştirici tuşta pasif)
- tema-secici.tsx: seçim de maskeyle oynar, altında Kbd ile D ipucu
- /surum-notlari: beta açıklaması, geri bildirim (destek@kolaytercih.com),
  sürüm günlüğü; Beta rozeti ve footer buraya bağlanır, sitemap'e eklendi
- DESIGN.md §2.3 notu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 23:25:38 +03:00
parent b2bab50337
commit 5f7ea8bbcd
13 changed files with 395 additions and 42 deletions

56
src/lib/tema-gecisi.ts Normal file
View File

@@ -0,0 +1,56 @@
// Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda
// çevirmez; yeni tema, header'daki KolayTercih işaretinin biçiminde logodan
// başlayıp büyüyerek ekrana yayılır (View Transitions API + CSS maskesi,
// globals.css `.kt-tema-gecis`). Tarayıcı View Transitions bilmiyorsa ya da
// kullanıcı hareketi azaltmışsa tema düz değişir.
/** Header'daki işaretin kök elemanı (site-header.tsx, AnimasyonluMark). */
export const TEMA_ODAK_ID = "kt-tema-odak";
/** Geçiş sırasında <html>'de duran sınıf ve maskenin merkez değişkenleri. */
const GECIS_SINIFI = "kt-tema-gecis";
let suruyor = false;
/**
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
* destekleniyorsa yeni tema işaretin bulunduğu noktadan maskeyle açılır;
* işaret ekranda değilse (krom gizli sayfalar) sol üst köşeden açılır.
*/
export function temayiGecisle(uygula: () => void) {
if (typeof document === "undefined") {
uygula();
return;
}
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!document.startViewTransition || azHareket || suruyor) {
uygula();
return;
}
const odak = document.getElementById(TEMA_ODAK_ID)?.getBoundingClientRect();
const kok = document.documentElement;
const x = odak && odak.width > 0 ? odak.left + odak.width / 2 : 40;
const y = odak && odak.height > 0 ? odak.top + odak.height / 2 : 48;
// Maske merkezden ekranın en uzak köşesine kadar büyümeli.
const uzak = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y),
);
kok.style.setProperty("--kt-tema-x", `${x}px`);
kok.style.setProperty("--kt-tema-y", `${y}px`);
// İşaretin görünür karesi viewBox'ın ~%67'si; kenar bu oranla çarpılır ki
// en büyük hâlde köşe karesi bile ekranı aşsın.
kok.style.setProperty("--kt-tema-son", `${Math.ceil(uzak * 3.4)}px`);
kok.classList.add(GECIS_SINIFI);
suruyor = true;
const gecis = document.startViewTransition(uygula);
gecis.finished
.catch(() => {})
.finally(() => {
kok.classList.remove(GECIS_SINIFI);
suruyor = false;
});
}