refactor(tema): logo maskesi kaldırıldı, tema geçişi sade crossfade (640 ms)

Bilal'in kararı (9 Eki 2026): SVG maskeli yayılma şimdilik yok; tüm sayfa
tek parça crossfade ile geçer, mursela-nextjs'teki tema geçişiyle aynı
süre ve eğri (640 ms, cubic-bezier(0.6, 0, 0.2, 1)). motion bağımlılığı
ve maske değişkenleri tema-gecisi.ts'ten çıktı; DESIGN §2.3 notu güncel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-09 00:15:34 +03:00
parent 4c84a1a974
commit a51724fdcc
6 changed files with 44 additions and 141 deletions

View File

@@ -133,14 +133,11 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
(`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki (`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki
`.dark` bloğu. `.dark` bloğu.
**Geçiş ve kısayol (7 Eki 2026):** tema değişimi anlık değildir; yeni tema **Geçiş ve kısayol (7 Eki 2026, 9 Eki'de sadeleşti):** tema değişimi anlık değildir;
KolayTercih işaretinin biçiminde önce ekranın altından ortaya süzülür, sonra tüm sayfa tek parça, 640 ms ease-in-out crossfade ile yeni temaya geçer (View
merkezden büyüyerek ekranı kaplar — büyüme boyunca logo normal, boşluklu Transitions: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket
hâliyle görünür; büyümenin ikinci yarısında kareler arasında kalan eski azaltılmışsa düz değişir). Logo maskeli yayılma denendi ve kaldırıldı (Bilal'in
görüntü tam ekran dolguyla yeni temaya solar. Ekranda tek silüet vardır, kararı, 9 Eki 2026): mursela-nextjs'teki sade crossfade yeter.
ikinci bir şekil büyümez ("logo içinde logo" olmaz). (View Transitions + CSS
maskesi, maskeyi motion sürer: `src/lib/tema-gecisi.ts`, `globals.css`
`.kt-tema-gecis`; hareket azaltılmışsa düz değişir.)
`D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin `D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin
altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
bağlantıdır. bağlantıdır.

View File

@@ -477,73 +477,47 @@
background: var(--color-slate-700); background: var(--color-slate-700);
} }
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken yeni /* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken tüm
görüntü, KolayTercih işaretinin biçiminde önce ekranın altından ortaya sayfa tek parça crossfade yapar — eski görüntü solar, yeni belirir.
süzülür, sonra merkezden büyüyerek ekranı kaplar. Eski görüntü yerinde mursela-nextjs'teki tema geçişiyle aynı: 640 ms, ease-in-out. Sınıf
durur; yeni görüntünün maskesi iki katman (mask-composite varsayılanı (`kt-tema-gecis`) geçiş süresince <html>'de durur; stil yalnız tema
add, toplanır): değişimine uygulanır. Hareket azaltılmışsa JS geçişi hiç başlatmaz,
(1) işaretin çizimi (kolay-tercih-mark.svg'nin gömülü kopyası — üç kare, tema düz değişir. */
aralarındaki boşlukla, logo logonun kendisi gibi durur), merkezi
--kt-tema-x/y'de, boyu --kt-tema-kenar;
(2) tam ekran dolgu, alfası --kt-tema-dolgu (0 → 1): büyümenin ikinci
yarısında kareler arasında ve ekran kenarlarında kalan eski görüntü
yumuşakça yeni temaya solar. İkinci bir şekil yok — "logo içinde logo"
görünmesin (8 Eki 2026, Bilal).
SVG data URI olarak gömülü, dosyadan url() ile DEĞİL: Chrome, view
transition pseudo elemanında dosyadan yüklenen birden çok maske
katmanını tümden şeffaf çiziyordu. Çizim public/kolay-tercih-mark.svg
ile aynı geometridir; o değişirse burası da güncellenir.
Konum, boy ve dolgu motion ile sürülür (lib/tema-gecisi.ts); buradaki
kt-tema-omur yalnızca geçişin pseudo elemanı motion bitene kadar yaşasın
diye durur (süre --kt-tema-sure, JS'den gelir). Hareket azaltılmışsa JS
geçişi hiç başlatmaz, tema düz değişir. */
@property --kt-tema-kenar {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
@property --kt-tema-dolgu {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
.kt-tema-gecis { .kt-tema-gecis {
view-transition-name: root; view-transition-name: root;
--kt-tema-sure: 640ms;
--kt-tema-egri: cubic-bezier(0.6, 0, 0.2, 1);
} }
.kt-tema-gecis::view-transition-old(root), .kt-tema-gecis::view-transition-old(root),
.kt-tema-gecis::view-transition-new(root) { .kt-tema-gecis::view-transition-new(root) {
animation: none;
mix-blend-mode: normal; mix-blend-mode: normal;
will-change: opacity;
backface-visibility: hidden;
}
.kt-tema-gecis::view-transition-group(root) {
animation-duration: var(--kt-tema-sure);
animation-timing-function: var(--kt-tema-egri);
} }
.kt-tema-gecis::view-transition-old(root) { .kt-tema-gecis::view-transition-old(root) {
z-index: 1; z-index: 1;
animation: kt-tema-cikis var(--kt-tema-sure) var(--kt-tema-egri) both;
} }
.kt-tema-gecis::view-transition-new(root) { .kt-tema-gecis::view-transition-new(root) {
z-index: 2; z-index: 0;
mask-image: animation: kt-tema-giris var(--kt-tema-sure) var(--kt-tema-egri) both;
url("data:image/svg+xml,<svg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20180%20180%22><rect%20x=%2227%22%20y=%2256%22%20width=%2257%22%20height=%2257%22%20rx=%2213%22/><rect%20x=%2291%22%20y=%2229%22%20width=%2256%22%20height=%2256%22%20rx=%2213%22/><rect%20x=%2291%22%20y=%2295%22%20width=%2256%22%20height=%2256%22%20rx=%2213%22/></svg>"),
linear-gradient(
rgb(0 0 0 / var(--kt-tema-dolgu)),
rgb(0 0 0 / var(--kt-tema-dolgu))
);
mask-repeat: no-repeat;
mask-size:
var(--kt-tema-kenar) var(--kt-tema-kenar),
100% 100%;
mask-position:
calc(var(--kt-tema-x) - var(--kt-tema-kenar) / 2)
calc(var(--kt-tema-y) - var(--kt-tema-kenar) / 2),
0 0;
/* Değeri değişmeyen ama gerçek bir animasyon: view transition, üzerindeki
tüm animasyonlar bitene kadar kalkmaz; maskeyi JS (motion) sürerken
ömrü bu tutar. */
animation: kt-tema-omur var(--kt-tema-sure, 1.05s) linear both;
} }
@keyframes kt-tema-omur { @keyframes kt-tema-cikis {
from { from {
opacity: 1; opacity: 1;
} }
to {
opacity: 0;
}
}
@keyframes kt-tema-giris {
from {
opacity: 0;
}
to { to {
opacity: 1; opacity: 1;
} }

View File

@@ -16,7 +16,7 @@ function yaziAlanindaMi(hedef: EventTarget | null) {
* Klavye kısayolu: `D` açık ↔ koyu temayı değiştirir (ipucu footer'daki * Klavye kısayolu: `D` açık ↔ koyu temayı değiştirir (ipucu footer'daki
* TemaSecici'nin altında, Kbd ile). Değiştirici tuşla (⌘/Ctrl/Alt) ya da yazı * TemaSecici'nin altında, Kbd ile). Değiştirici tuşla (⌘/Ctrl/Alt) ya da yazı
* alanındayken çalışmaz; "sistem" seçiliyken o anki çözümlenmiş temanın * alanındayken çalışmaz; "sistem" seçiliyken o anki çözümlenmiş temanın
* tersine, açık bir seçime geçer. Geçiş logodan yayılan maskeyle oynar. * tersine, açık bir seçime geçer. Geçiş sayfa geneli crossfade ile oynar.
*/ */
export function TemaKisayolu() { export function TemaKisayolu() {
const { resolvedTheme, setTheme } = useTheme(); const { resolvedTheme, setTheme } = useTheme();

View File

@@ -27,7 +27,7 @@ function AdresCubuguRengi() {
* globals.css'teki `.dark` token eşlemesinden gelir. Varsayılan açık tema; * globals.css'teki `.dark` token eşlemesinden gelir. Varsayılan açık tema;
* kullanıcı footer'daki TemaSecici ile koyu ya da "sistem"e geçer, seçim * kullanıcı footer'daki TemaSecici ile koyu ya da "sistem"e geçer, seçim
* tarayıcıda saklanır; `D` tuşu açık ↔ koyu geçişi yapar (TemaKisayolu). * tarayıcıda saklanır; `D` tuşu açık ↔ koyu geçişi yapar (TemaKisayolu).
* Tema değişimi logodan yayılan maskeyle oynar (lib/tema-gecisi.ts). * Tema değişimi sayfa geneli crossfade ile oynar (lib/tema-gecisi.ts).
*/ */
export function TemaSaglayici({ children }: { children: React.ReactNode }) { export function TemaSaglayici({ children }: { children: React.ReactNode }) {
return ( return (

View File

@@ -18,8 +18,8 @@ const aboneOlma = () => () => {};
/** /**
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca * Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez, * tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
* hydration sonrası işaretlenir (yerleşim oynamaz). Seçim logodan yayılan * hydration sonrası işaretlenir (yerleşim oynamaz). Seçim sayfa geneli crossfade
* maskeyle uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur. * ile uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur.
*/ */
export function TemaSecici({ className }: { className?: string }) { export function TemaSecici({ className }: { className?: string }) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();

View File

@@ -1,33 +1,20 @@
// Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda // Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda
// çevirmez; yeni tema, KolayTercih işaretinin biçiminde önce ekranın altından // çevirmez; tüm sayfa tek parça, yumuşak bir crossfade ile yeni temaya geçer
// ortaya süzülür, sonra merkezden büyüyerek ekranı kaplar. View Transitions // (View Transitions API; süre ve eğri globals.css `.kt-tema-gecis`
// API + CSS maskesi (globals.css `.kt-tema-gecis`) eski/yeni görüntü çiftini // bloğunda, mursela-nextjs'teki tema geçişiyle aynı: 640 ms ease-in-out).
// kurar; maskenin konumu, boyu ve son dolgu motion ile sürülür. Tarayıcı View Transitions bilmiyorsa ya da kullanıcı hareketi // Tarayıcı View Transitions bilmiyorsa ya da kullanıcı hareketi azaltmışsa
// azaltmışsa tema düz değişir. // tema düz değişir.
import { animate, type AnimationPlaybackControls } from "motion"; /** Geçiş sırasında <html>'de duran sınıf: crossfade stili yalnız tema
* değişimine uygulanır, başka view transition'lara karışmaz. */
/** Geçiş sırasında <html>'de duran sınıf (globals.css). */
const GECIS_SINIFI = "kt-tema-gecis"; const GECIS_SINIFI = "kt-tema-gecis";
// Saniye. Faz 1 (uçuş): işaret ekranın altından ortaya gelir. Faz 2
// (kaplama): merkezden büyüyerek ekranı aşar; DOLGU_BASI'ndan sonra tam ekran
// dolgu alfası yükselir ve kareler arasında / kenarlarda kalan eski görüntü
// yeni temaya solar. Ekranda tek silüet vardır, ikinci bir şekil büyümez.
const UCUS_SURE = 0.45;
const KAPLAMA_SURE = 0.6;
const DOLGU_BASI = 0.5;
const EASE = [0.65, 0, 0.35, 1] as const;
// CSS'teki kt-tema-omur'un süresi: motion bitmeden pseudo eleman kalkmasın
// diye toplamın biraz üstü (globals.css'te --kt-tema-sure ile okunur).
const GECIS_SURESI = UCUS_SURE + KAPLAMA_SURE + 0.1;
let suruyor = false; let suruyor = false;
/** /**
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş * `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
* destekleniyorsa işaret ekranın altından ortaya süzülür ve oradan büyüyerek * sürerken gelen istek yok sayılır: art arda D'ye basınca ekrandaki görüntü
* yeni temayı açar. * ile tema durumu birbirine girmesin.
*/ */
export function temayiGecisle(uygula: () => void) { export function temayiGecisle(uygula: () => void) {
if (typeof document === "undefined") { if (typeof document === "undefined") {
@@ -39,66 +26,12 @@ export function temayiGecisle(uygula: () => void) {
uygula(); uygula();
return; return;
} }
// Geçiş sürerken gelen istek yok sayılır (uygulanmaz): ara anahtarlamalar
// hem koreografiyi bozar hem de ekrandaki görüntü ile tema durumu birbirine
// girer; D'ye art arda basınca görünen "sıçrama" budur.
if (suruyor) return; if (suruyor) return;
const en = window.innerWidth;
const boy = window.innerHeight;
const kok = document.documentElement; const kok = document.documentElement;
// Uçuş boyunca işaretin maskeden çıkan kenarı: ekranı kaplamayacak ama
// belirgin bir işaret olacak kadar (min ekran kenarının %22'si; işaretin
// görünür karesi bunun ~%67'si).
const ucusKenar = Math.round(Math.min(en, boy) * 0.22);
// Uçuş ve kaplamanın ortak ekseni ekranın ortası; başlangıçta işaret
// tamamen ekranın altında durur (alttan gelir).
const merkezX = en / 2;
const merkezY = boy / 2;
const baslangicY = boy + ucusKenar / 2;
// Kaplama merkezden ekranın en uzak köşesine kadar büyümeli. İşaretin
// görünür karesi viewBox'ın ~%67'si; kenar bu oranla çarpılır ki kareler
// ekranı geniş ölçüde kaplasın — kalan boşlukları dolgu solması kapatır.
const sonKenar = Math.ceil(Math.hypot(merkezX, merkezY) * 3);
kok.style.setProperty("--kt-tema-x", `${merkezX}px`);
kok.style.setProperty("--kt-tema-y", `${baslangicY}px`);
kok.style.setProperty("--kt-tema-kenar", `${ucusKenar}px`);
kok.style.setProperty("--kt-tema-dolgu", "0");
kok.style.setProperty("--kt-tema-sure", `${GECIS_SURESI}s`);
kok.classList.add(GECIS_SINIFI); kok.classList.add(GECIS_SINIFI);
suruyor = true; suruyor = true;
const kontroller: AnimationPlaybackControls[] = [
// Faz 1 — uçuş: alttan ortaya, x sabit.
animate(0, 1, {
duration: UCUS_SURE,
ease: EASE,
onUpdate: (p) => {
kok.style.setProperty(
"--kt-tema-y",
`${baslangicY + (merkezY - baslangicY) * p}px`,
);
},
}),
// Faz 2 — kaplama: merkezden ekranı aşana kadar.
animate(ucusKenar, sonKenar, {
duration: KAPLAMA_SURE,
delay: UCUS_SURE,
ease: EASE,
onUpdate: (kenar) => kok.style.setProperty("--kt-tema-kenar", `${kenar}px`),
}),
// Dolgu: büyümenin ikinci yarısında kareler arasında ve ekran
// kenarlarında kalan eski görüntü yeni temaya solar.
animate(0, 1, {
duration: KAPLAMA_SURE * (1 - DOLGU_BASI),
delay: UCUS_SURE + KAPLAMA_SURE * DOLGU_BASI,
ease: EASE,
onUpdate: (dolgu) => kok.style.setProperty("--kt-tema-dolgu", `${dolgu}`),
}),
];
const gecis = document.startViewTransition(uygula); const gecis = document.startViewTransition(uygula);
// Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder: // Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder:
// `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin. // `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin.
@@ -106,7 +39,6 @@ export function temayiGecisle(uygula: () => void) {
gecis.finished gecis.finished
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
kontroller.forEach((k) => k.stop());
kok.classList.remove(GECIS_SINIFI); kok.classList.remove(GECIS_SINIFI);
suruyor = false; suruyor = false;
}); });