diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index ae67355..c3a7f1a 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -133,14 +133,11 @@ 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 -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; büyümenin ikinci yarısında kareler arasında kalan eski -görüntü tam ekran dolguyla yeni temaya solar. Ekranda tek silüet vardır, -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.) +**Geçiş ve kısayol (7 Eki 2026, 9 Eki'de sadeleşti):** tema değişimi anlık değildir; +tüm sayfa tek parça, 640 ms ease-in-out crossfade ile yeni temaya geçer (View +Transitions: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket +azaltılmışsa düz değişir). Logo maskeli yayılma denendi ve kaldırıldı (Bilal'in +kararı, 9 Eki 2026): mursela-nextjs'teki sade crossfade yeter. `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/src/app/globals.css b/src/app/globals.css index a0efa8c..7bde5d3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -477,73 +477,47 @@ background: var(--color-slate-700); } -/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken yeni - görüntü, KolayTercih işaretinin biçiminde önce ekranın altından 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 iki katman (mask-composite varsayılanı - add, toplanır): - (1) işaretin çizimi (kolay-tercih-mark.svg'nin gömülü kopyası — üç kare, - 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: ""; - inherits: true; - initial-value: 0px; -} -@property --kt-tema-dolgu { - syntax: ""; - inherits: true; - initial-value: 0; -} - +/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken tüm + sayfa tek parça crossfade yapar — eski görüntü solar, yeni belirir. + mursela-nextjs'teki tema geçişiyle aynı: 640 ms, ease-in-out. Sınıf + (`kt-tema-gecis`) geçiş süresince 'de durur; stil yalnız tema + değişimine uygulanır. Hareket azaltılmışsa JS geçişi hiç başlatmaz, + tema düz değişir. */ .kt-tema-gecis { 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-new(root) { - animation: none; 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) { z-index: 1; + animation: kt-tema-cikis var(--kt-tema-sure) var(--kt-tema-egri) both; } .kt-tema-gecis::view-transition-new(root) { - z-index: 2; - mask-image: - 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: - 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; + z-index: 0; + animation: kt-tema-giris var(--kt-tema-sure) var(--kt-tema-egri) both; } -@keyframes kt-tema-omur { +@keyframes kt-tema-cikis { from { opacity: 1; } + to { + opacity: 0; + } +} +@keyframes kt-tema-giris { + from { + opacity: 0; + } to { opacity: 1; } diff --git a/src/components/tema-kisayolu.tsx b/src/components/tema-kisayolu.tsx index 3894826..d11382f 100644 --- a/src/components/tema-kisayolu.tsx +++ b/src/components/tema-kisayolu.tsx @@ -16,7 +16,7 @@ function yaziAlanindaMi(hedef: EventTarget | null) { * 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ı * 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() { const { resolvedTheme, setTheme } = useTheme(); diff --git a/src/components/tema-saglayici.tsx b/src/components/tema-saglayici.tsx index a22bf54..fbdc1eb 100644 --- a/src/components/tema-saglayici.tsx +++ b/src/components/tema-saglayici.tsx @@ -27,7 +27,7 @@ function AdresCubuguRengi() { * 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 * 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 }) { return ( diff --git a/src/components/tema-secici.tsx b/src/components/tema-secici.tsx index baf4566..290f2ad 100644 --- a/src/components/tema-secici.tsx +++ b/src/components/tema-secici.tsx @@ -18,8 +18,8 @@ const aboneOlma = () => () => {}; /** * 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, - * hydration sonrası işaretlenir (yerleşim oynamaz). Seçim logodan yayılan - * maskeyle uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur. + * hydration sonrası işaretlenir (yerleşim oynamaz). Seçim sayfa geneli crossfade + * ile uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur. */ export function TemaSecici({ className }: { className?: string }) { const { theme, setTheme } = useTheme(); diff --git a/src/lib/tema-gecisi.ts b/src/lib/tema-gecisi.ts index 82178fd..fc2c42d 100644 --- a/src/lib/tema-gecisi.ts +++ b/src/lib/tema-gecisi.ts @@ -1,33 +1,20 @@ // 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 -// 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 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. +// çevirmez; tüm sayfa tek parça, yumuşak bir crossfade ile yeni temaya geçer +// (View Transitions API; süre ve eğri globals.css `.kt-tema-gecis` +// bloğunda, mursela-nextjs'teki tema geçişiyle aynı: 640 ms ease-in-out). +// Tarayıcı View Transitions bilmiyorsa ya da kullanıcı hareketi azaltmışsa +// tema düz değişir. -import { animate, type AnimationPlaybackControls } from "motion"; - -/** Geçiş sırasında 'de duran sınıf (globals.css). */ +/** Geçiş sırasında 'de duran sınıf: crossfade stili yalnız tema + * değişimine uygulanır, başka view transition'lara karışmaz. */ 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; /** * `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 - * yeni temayı açar. + * sürerken gelen istek yok sayılır: art arda D'ye basınca ekrandaki görüntü + * ile tema durumu birbirine girmesin. */ export function temayiGecisle(uygula: () => void) { if (typeof document === "undefined") { @@ -39,66 +26,12 @@ export function temayiGecisle(uygula: () => void) { 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 en = window.innerWidth; - const boy = window.innerHeight; 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); 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); // Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder: // `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin. @@ -106,7 +39,6 @@ export function temayiGecisle(uygula: () => void) { gecis.finished .catch(() => {}) .finally(() => { - kontroller.forEach((k) => k.stop()); kok.classList.remove(GECIS_SINIFI); suruyor = false; });