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:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user