diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md
index 4456a23..445b797 100644
--- a/docs/tasarim/DESIGN.md
+++ b/docs/tasarim/DESIGN.md
@@ -133,9 +133,13 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
(`next-themes`, ``). Renklerin tek kaynağı `src/app/globals.css` içindeki
`.dark` bloğu.
-**Geçiş ve kısayol (7 Eki 2026):** tema değişimi anlık değildir; yeni tema header'daki
-KolayTercih işaretinin biçiminde logodan büyüyerek açılır (View Transitions + CSS maskesi:
-`src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket azaltılmışsa düz değişir).
+**Geçiş ve kısayol (7 Eki 2026):** tema değişimi anlık değildir; yeni tema
+KolayTercih işaretinin biçiminde önce ekranın altından ortaya süzülür, sonra
+merkezden büyüyerek ekranı kaplar — büyüme boyunca logo normal, boşluklu
+hâliyle görünür; en büyük haline yaklaştıkça kareler arası boşluklar
+merkezden dışa doğru yumuşak biçimde kapanır (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
altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
bağlantıdır.
diff --git a/public/kolay-tercih-mark-dolu.svg b/public/kolay-tercih-mark-dolu.svg
new file mode 100644
index 0000000..796bfa8
--- /dev/null
+++ b/public/kolay-tercih-mark-dolu.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/app/globals.css b/src/app/globals.css
index 26153ae..5267f7e 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -478,21 +478,39 @@
}
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken yeni
- görüntü, header'daki KolayTercih işaretinin biçiminde logodan büyüyerek
- açılır. Eski görüntü yerinde durur; yeni görüntünün maskesi iki katman:
- (1) işaretin SVG'si, merkezi --kt-tema-x/y'de, kenarı 0 → --kt-tema-son;
- (2) aynı merkezden biraz geç kalkan bir daire — işaretin kareleri arasındaki
- boşlukları kapatır ki geçiş sonunda eski temadan iz kalmasın. Hareket
- azaltılmışsa JS geçişi hiç başlatmaz, tema düz değişir. */
+ görüntü, KolayTercih işaretinin biçiminde önce ekranın ALTINDAN ortaya
+ süzülür, sonra merkezden büyüyerek ekranı kaplar. Eski görüntü yerinde
+ durur; yeni görüntünün maskesi üç katman (hepsi `add` ile toplanır,
+ mask-composite varsayılanı add — bu yüzden sıralama yalnızca okunurluk
+ içindir):
+ (1) işaretin NORMAL çizimi (kolay-tercih-mark.svg'nin gömülü kopyası — kareler arası boşluk
+ görünür, logo logonun kendisi gibi durur), merkezi --kt-tema-x/y'de,
+ boyu --kt-tema-kenar: uçuş ve büyüme boyunca bu görünür;
+ (2) aynı geometride BOŞLUKLARI DOLU çizim (kolay-tercih-mark-dolu.svg),
+ boyu --kt-tema-kenar × --kt-tema-kapanis (0 → 1): logo en büyük haline
+ ulaşmaya yakın dolu çizim merkezden dışa doğru büyür, kareler arası
+ boşlukları yumuşak biçimde kapatır;
+ (3) tam ekran dolgu: --kt-tema-dolgu (0 → 1) ile işaret ekranı tam
+ kapsadıktan sonra dış çentiklerde kalan eski zeminin son anda dolması.
+ Yuvarlak büyüme yok — logo ekranı kaplayınca geçiş tamamlanır.
+ Konum, boy ve dolgular 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: "";
inherits: true;
initial-value: 0px;
}
-@property --kt-tema-yaricap {
- syntax: "";
+@property --kt-tema-kapanis {
+ syntax: "";
inherits: true;
- initial-value: 0px;
+ initial-value: 0;
+}
+@property --kt-tema-dolgu {
+ syntax: "";
+ inherits: true;
+ initial-value: 0;
}
.kt-tema-gecis {
@@ -508,40 +526,42 @@
}
.kt-tema-gecis::view-transition-new(root) {
z-index: 2;
+ /* İki SVG de data URI olarak gömülü, dosyadan url() ile DEĞİL: Chrome'da
+ view-transition pseudo elemanı dosyadan yüklenen ikinci bir maske
+ katmanını (boyu 0'dan büyük olduğu an) tümden şeffaf çiziyor — logo
+ hiç görünmüyordu (8 Eki 2026 tanısı). Gömülü hâlde iki katman da
+ doğru birleşiyor. Çizimler public/kolay-tercih-mark.svg ve
+ public/kolay-tercih-mark-dolu.svg ile aynı geometridir; oradaki dosya
+ değişirse burası da güncellenir. */
mask-image:
- url("/kolay-tercih-mark.svg"),
- radial-gradient(
- circle at var(--kt-tema-x) var(--kt-tema-y),
- #000 calc(var(--kt-tema-yaricap) - 1px),
- transparent var(--kt-tema-yaricap)
+ url("data:image/svg+xml,"),
+ url("data:image/svg+xml,"),
+ linear-gradient(
+ rgb(0 0 0 / var(--kt-tema-dolgu)),
+ rgb(0 0 0 / var(--kt-tema-dolgu))
);
mask-repeat: no-repeat;
mask-size:
+ calc(var(--kt-tema-kenar) * var(--kt-tema-kapanis))
+ calc(var(--kt-tema-kenar) * var(--kt-tema-kapanis)),
var(--kt-tema-kenar) var(--kt-tema-kenar),
100% 100%;
mask-position:
+ calc(var(--kt-tema-x) - var(--kt-tema-kenar) * var(--kt-tema-kapanis) / 2)
+ calc(var(--kt-tema-y) - var(--kt-tema-kenar) * var(--kt-tema-kapanis) / 2),
calc(var(--kt-tema-x) - var(--kt-tema-kenar) / 2)
calc(var(--kt-tema-y) - var(--kt-tema-kenar) / 2),
0 0;
- animation:
- kt-tema-isaret 0.9s cubic-bezier(0.65, 0, 0.35, 1) both,
- kt-tema-daire 0.9s cubic-bezier(0.65, 0, 0.35, 1) both;
+ /* 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-isaret {
+@keyframes kt-tema-omur {
from {
- --kt-tema-kenar: 0px;
+ opacity: 1;
}
to {
- --kt-tema-kenar: var(--kt-tema-son);
- }
-}
-/* Daire işaretin ardından gelir: ilk %35'te sıfırda bekler. */
-@keyframes kt-tema-daire {
- 0%,
- 35% {
- --kt-tema-yaricap: 0px;
- }
- 100% {
- --kt-tema-yaricap: var(--kt-tema-son);
+ opacity: 1;
}
}
diff --git a/src/components/animasyonlu-mark.tsx b/src/components/animasyonlu-mark.tsx
index 5cbca50..406e12a 100644
--- a/src/components/animasyonlu-mark.tsx
+++ b/src/components/animasyonlu-mark.tsx
@@ -166,15 +166,12 @@ function donguyuBaslat(
* yerinden başlar.
*/
export function AnimasyonluMark({
- id,
className,
sadeceHover = false,
oynat = true,
varyant = "rota",
renk = "marka",
}: {
- /** Kök elemanın id'si (ör. tema geçişinin merkez noktası, lib/tema-gecisi.ts). */
- id?: string;
className?: string;
sadeceHover?: boolean;
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
@@ -287,7 +284,6 @@ export function AnimasyonluMark({
return (
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
- kareler danışman panelindeki nabız animasyonuyla canlanır.
- id: tema geçişi maskesi bu noktadan yayılır (lib/tema-gecisi.ts). */}
-
+ kareler danışman panelindeki nabız animasyonuyla canlanır. */}
+
Kolay
diff --git a/src/lib/tema-gecisi.ts b/src/lib/tema-gecisi.ts
index 4d1e6e9..f6826f5 100644
--- a/src/lib/tema-gecisi.ts
+++ b/src/lib/tema-gecisi.ts
@@ -1,21 +1,48 @@
// 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.
+// çevirmez; yeni tema, KolayTercih işaretinin biçiminde önce ekranın altından
+// ortaya süzülür, sonra merkezden büyüyerek ekranı kaplar. View Transitions
+// API + CSS maskesi (globals.css `.kt-tema-gecis`) eski/yeni görüntü çiftini
+// kurar; maskenin konumu, boyu, boşluk kapanışı ve son dolgu motion ile
+// sürülür. 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";
+import { animate, type AnimationPlaybackControls } from "motion";
-/** Geçiş sırasında 'de duran sınıf ve maskenin merkez değişkenleri. */
+/** Geçiş sırasında 'de duran sınıf (globals.css). */
const GECIS_SINIFI = "kt-tema-gecis";
+// Saniye. Faz 1 (uçuş): işaret ekranın altından ortaya gelir — normal,
+// boşluklu hâliyle. Faz 2 (kaplama): merkezden büyüyerek ekranı aşar;
+// kaplamanın KAPANIS_BASI…KAPANIS_BITIS aralığında dolu çizim merkezden
+// dışa büyür ve kareler arası boşlukları kapatır (en büyük hâle ulaşmadan).
+// EASE fazlar arasında geçişin eski koreografisiyle aynı hissi verir.
+const UCUS_SURE = 0.45;
+const KAPLAMA_SURE = 0.6;
+const KAPANIS_BASI = 0.55;
+const KAPANIS_BITIS = 0.9;
+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;
+
+// İşaretin dış kenarları (public/kolay-tercih-mark-dolu.svg, 180'lik viewBox;
+// orijinal logonun boşlukları doldurulmuş çizimi, dış uzantılar aynı):
+// x 27–147, y 29–151. Merkeze (90) göre oranlar — dolgu, bu sınır kutusu
+// ekranı tam kapsadığı andan itibaren dolar; erken dolarsa ekrandaki eski
+// zemin topluca solardı.
+const KENAR_ORAN = {
+ sol: (90 - 27) / 180,
+ sag: (147 - 90) / 180,
+ ust: (90 - 29) / 180,
+ alt: (151 - 90) / 180,
+} as const;
+
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.
+ * destekleniyorsa işaret ekranın altından ortaya süzülür ve oradan büyüyerek
+ * yeni temayı açar.
*/
export function temayiGecisle(uygula: () => void) {
if (typeof document === "undefined") {
@@ -23,33 +50,103 @@ export function temayiGecisle(uygula: () => void) {
return;
}
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
- if (!document.startViewTransition || azHareket || suruyor) {
+ if (!document.startViewTransition || azHareket) {
uygula();
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;
- const odak = document.getElementById(TEMA_ODAK_ID)?.getBoundingClientRect();
+ const en = window.innerWidth;
+ const boy = window.innerHeight;
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),
+
+ // 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 en büyük
+ // hâlde köşe karesi bile ekranı aşsın (eski hesapla aynı çarpan).
+ const sonKenar = Math.ceil(Math.hypot(merkezX, merkezY) * 3.4);
+ // Dolgu, işaretin dış kutusu ekranı tam kapsadığı anda başlar (erken
+ // dolarsa marka henüz ekrana ulaşmadan eski zemin topluca solardı). Alt
+ // sınır 0.9: kapsama hesaba katılmayan dar bir marjla geç kalırsa bile
+ // dolgu kaplamanın son ~%10'unda bitsin; aksi hâlde ekranın köşelerinde
+ // eski zemin belirgin biçimde kalır.
+ const kapsaKenar = Math.max(
+ en / 2 / Math.max(KENAR_ORAN.sol, KENAR_ORAN.sag),
+ boy / 2 / Math.max(KENAR_ORAN.ust, KENAR_ORAN.alt),
+ );
+ const dolguBasi = Math.min(
+ 1,
+ Math.max((kapsaKenar - ucusKenar) / (sonKenar - ucusKenar), 0.9),
);
- 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.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-kapanis", "0");
+ kok.style.setProperty("--kt-tema-dolgu", "0");
+ kok.style.setProperty("--kt-tema-sure", `${GECIS_SURESI}s`);
kok.classList.add(GECIS_SINIFI);
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`),
+ }),
+ // Kapanış: dolu çizimin boyu kenar × kapanış — büyümenin ortasından
+ // sonuna doğru boşluklar merkezden dışa dolar, logo en büyük hâle
+ // ulaşmadan kapanır. Boyut sürüldüğü için ek bir katmanın alfa
+ // çakışması gerekmez; maske toplamı (add) değişmez.
+ animate(0, 1, {
+ duration: KAPLAMA_SURE * (KAPANIS_BITIS - KAPANIS_BASI),
+ delay: UCUS_SURE + KAPLAMA_SURE * KAPANIS_BASI,
+ ease: EASE,
+ onUpdate: (kapanis) => kok.style.setProperty("--kt-tema-kapanis", `${kapanis}`),
+ }),
+ // Dolgu: işaretin dış çentiklerinde kalan eski zemin son anda dolar —
+ // ekranı kapsayan boşluklar (kareler arası şeritler) maskenin kendisinde
+ // dolu çizimle kapanmıştır, burada değil.
+ animate(0, 1, {
+ duration: KAPLAMA_SURE * (1 - dolguBasi),
+ delay: UCUS_SURE + KAPLAMA_SURE * dolguBasi,
+ ease: EASE,
+ onUpdate: (dolgu) => kok.style.setProperty("--kt-tema-dolgu", `${dolgu}`),
+ }),
+ ];
+
const gecis = document.startViewTransition(uygula);
+ // Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder:
+ // `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin.
+ gecis.ready.catch(() => {});
gecis.finished
.catch(() => {})
.finally(() => {
+ kontroller.forEach((k) => k.stop());
kok.classList.remove(GECIS_SINIFI);
suruyor = false;
});