v1-beta-tema #9
@@ -133,6 +133,13 @@ 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
|
||||
`.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).
|
||||
`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.
|
||||
|
||||
**Nasıl çalışır:** bileşenlere `dark:` yazılmaz; palet token'ları çevrilir.
|
||||
|
||||
| Açık temadaki sınıf | Koyu temada ne olur |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kolaytercih",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -476,3 +476,72 @@
|
||||
.toaster [data-sonner-toast][data-styled="true"] [data-button]:hover {
|
||||
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ü, 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. */
|
||||
@property --kt-tema-kenar {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --kt-tema-yaricap {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: 0px;
|
||||
}
|
||||
|
||||
.kt-tema-gecis {
|
||||
view-transition-name: root;
|
||||
}
|
||||
.kt-tema-gecis::view-transition-old(root),
|
||||
.kt-tema-gecis::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
.kt-tema-gecis::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
.kt-tema-gecis::view-transition-new(root) {
|
||||
z-index: 2;
|
||||
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)
|
||||
);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
@keyframes kt-tema-isaret {
|
||||
from {
|
||||
--kt-tema-kenar: 0px;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ const staticEntries: MetadataRoute.Sitemap = [
|
||||
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
|
||||
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
||||
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
|
||||
{ url: `${SITE_URL}/surum-notlari`, priority: 0.3, changeFrequency: "monthly" },
|
||||
{ url: `${SITE_URL}/gizlilik`, priority: 0.2, changeFrequency: "yearly" },
|
||||
{
|
||||
url: `${SITE_URL}/cerez-politikasi`,
|
||||
|
||||
149
src/app/surum-notlari/page.tsx
Normal file
149
src/app/surum-notlari/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ArrowRight, Mail } from "lucide-react";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { PagePixelDivider, SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Kbd } from "@/components/ui/kbd";
|
||||
import { SURUM } from "@/lib/surum";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Sürüm notları",
|
||||
description:
|
||||
"KolayTercih beta sürüm notları: neler değişti, neler geliyor ve geri bildirimini nasıl iletirsin.",
|
||||
path: "/surum-notlari",
|
||||
});
|
||||
|
||||
/**
|
||||
* Sürüm günlüğü. En yeni en üstte; her sürüm package.json'daki numarayla
|
||||
* aynı adı taşır (AGENTS.md "Sürümleme"). Yeni bir sürüm çıkarken buraya bir
|
||||
* kayıt eklenir; "Geliyor" listesi bitenlerden arındırılır.
|
||||
*/
|
||||
const SURUMLER: {
|
||||
surum: string;
|
||||
tarih: string;
|
||||
baslik: string;
|
||||
maddeler: string[];
|
||||
}[] = [
|
||||
{
|
||||
surum: "1.1.0",
|
||||
tarih: "7 Ekim 2026",
|
||||
baslik: "Tema kısayolu ve sürüm notları",
|
||||
maddeler: [
|
||||
"D tuşu açık / koyu temayı değiştirir; geçiş logodan yayılarak yumuşak oynar.",
|
||||
"Bu sayfa: beta durumu, sürüm günlüğü ve geri bildirim adresi. Header'daki Beta rozeti buraya getirir.",
|
||||
],
|
||||
},
|
||||
{
|
||||
surum: "1.0.0",
|
||||
tarih: "7 Ekim 2026",
|
||||
baslik: "Beta açılışı",
|
||||
maddeler: [
|
||||
"Sıralama, alan, il ve devlet/vakıf tercihine göre 24 tercihlik dengeli liste: hayal, dengeli ve güvenli dilimler.",
|
||||
"Her program için YÖK Atlas verisinden taban sıralama, kontenjan ve yıllara göre eğilim.",
|
||||
"Yapay zekâ destekli tercih raporu ve liste üzerinde soru sorabildiğin danışman sohbeti.",
|
||||
"Üniversite ve bölüm kataloğu, üniversite sıralamaları, rehber yazıları.",
|
||||
"Açık, koyu ve sistem teması.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const GELIYOR = [
|
||||
"Listeni PDF olarak indirme ve paylaşma.",
|
||||
"Yerleşme sonuçları açıklandığında listenin gerçekleşme karnesi.",
|
||||
"Veli görünümü: listeyi birlikte inceleyip not bırakma.",
|
||||
];
|
||||
|
||||
export default function SurumNotlariPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||
<SectionEyebrow>Beta · v{SURUM}</SectionEyebrow>
|
||||
<Parallax fromStart strength={10}>
|
||||
<h1 className="mt-4 font-heading text-3xl font-bold">Sürüm notları</h1>
|
||||
</Parallax>
|
||||
<PagePixelDivider seed={61} className="mt-6" />
|
||||
|
||||
<section
|
||||
aria-labelledby="beta-baslik"
|
||||
className="mt-6 rounded-2xl border border-slate-200 bg-card p-5 shadow-xs sm:p-6"
|
||||
>
|
||||
<h2 id="beta-baslik" className="font-heading text-xl font-bold">
|
||||
KolayTercih şu an beta sürümünde
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-700">
|
||||
Ürün çalışıyor ve gerçek YÖK Atlas verisiyle liste üretiyor; ama
|
||||
hâlâ her gün bir şeyler düzeltiyor, eksik gördüğümüz yerleri
|
||||
tamamlıyoruz. Bir hata, yanlış bir sayı, anlaşılmayan bir ekran
|
||||
ya da “keşke şöyle olsa” dediğin bir şey görürsen bize yaz: her
|
||||
geri bildirim okunur ve çoğu bir sonraki sürüme girer.
|
||||
</p>
|
||||
<a
|
||||
href="mailto:destek@kolaytercih.com?subject=KolayTercih%20beta%20geri%20bildirimi"
|
||||
className="mt-5 inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-slate-50 transition-[background-color,transform] duration-150 hover:bg-slate-700 active:scale-[0.97]"
|
||||
>
|
||||
<Mail className="size-4" aria-hidden />
|
||||
Geri bildirim gönder
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</a>
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
destek@kolaytercih.com · Hangi sayfada, hangi sıralamayla ne
|
||||
gördüğünü yazarsan daha hızlı çözeriz. Kart bilgini asla e-postayla
|
||||
paylaşma.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="gunluk-baslik" className="mt-12">
|
||||
<h2 id="gunluk-baslik" className="font-heading text-xl font-bold">
|
||||
Neler değişti
|
||||
</h2>
|
||||
<ol className="mt-4 space-y-8">
|
||||
{SURUMLER.map((s) => (
|
||||
<li key={s.surum} className="relative pl-6">
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute top-2 left-0 size-2.5 rounded-[2px] bg-primary"
|
||||
/>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h3 className="font-heading text-lg font-bold tabular-nums">
|
||||
v{s.surum}
|
||||
</h3>
|
||||
<span className="text-sm font-semibold text-slate-700">
|
||||
{s.baslik}
|
||||
</span>
|
||||
<time className="text-xs text-slate-500">{s.tarih}</time>
|
||||
</div>
|
||||
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
|
||||
{s.maddeler.map((m) => (
|
||||
<li key={m}>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="geliyor-baslik" className="mt-12">
|
||||
<h2 id="geliyor-baslik" className="font-heading text-xl font-bold">
|
||||
Sırada ne var
|
||||
</h2>
|
||||
<ul className="mt-4 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
|
||||
{GELIYOR.map((m) => (
|
||||
<li key={m}>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-4 text-xs text-slate-500">
|
||||
Sıra geri bildirimle değişir; en çok istenen öne alınır.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="mt-12 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
||||
İpucu: <Kbd>D</Kbd> açık / koyu temayı değiştirir,
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>K</Kbd> aramayı açar.
|
||||
</p>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -166,12 +166,15 @@ 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. */
|
||||
@@ -284,6 +287,7 @@ export function AnimasyonluMark({
|
||||
|
||||
return (
|
||||
<span
|
||||
id={id}
|
||||
ref={kokRef}
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
|
||||
@@ -199,6 +199,12 @@ export function SiteFooter() {
|
||||
>
|
||||
İletişim
|
||||
</Link>
|
||||
<Link
|
||||
href="/surum-notlari"
|
||||
className="hover:text-slate-700 hover:underline"
|
||||
>
|
||||
Sürüm notları
|
||||
</Link>
|
||||
</nav>
|
||||
<TemaSecici className="mt-6" />
|
||||
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
import { SurumRozeti } from "@/components/surum-rozeti";
|
||||
import { TEMA_ODAK_ID } from "@/lib/tema-gecisi";
|
||||
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
||||
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
|
||||
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
||||
@@ -23,8 +24,13 @@ export function SiteHeader() {
|
||||
className="group flex min-w-0 shrink items-center font-bricolage"
|
||||
>
|
||||
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
|
||||
kareler danışman panelindeki nabız animasyonuyla canlanır. */}
|
||||
<AnimasyonluMark sadeceHover className="size-8 sm:size-11" />
|
||||
kareler danışman panelindeki nabız animasyonuyla canlanır.
|
||||
id: tema geçişi maskesi bu noktadan yayılır (lib/tema-gecisi.ts). */}
|
||||
<AnimasyonluMark
|
||||
id={TEMA_ODAK_ID}
|
||||
sadeceHover
|
||||
className="size-8 sm:size-11"
|
||||
/>
|
||||
<span className="truncate transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity">
|
||||
<span className="text-lg font-light tracking-tighter sm:text-3xl">
|
||||
Kolay
|
||||
@@ -33,10 +39,12 @@ export function SiteHeader() {
|
||||
Tercih
|
||||
</span>
|
||||
</span>
|
||||
{/* Beta rozeti + sürüm (surum-rozeti.tsx); yazı logosuyla aynı
|
||||
fade'i taşır ki NavBaslik yuvası açılınca birlikte sönsün. */}
|
||||
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
|
||||
</Link>
|
||||
{/* Beta rozeti + sürüm (surum-rozeti.tsx): /surum-notlari'na giden
|
||||
ayrı bir Link — logo Link'inin içinde bağlantı içinde bağlantı
|
||||
olmasın. Yazı logosuyla aynı fade'i taşır ki NavBaslik yuvası
|
||||
açılınca birlikte sönsün. */}
|
||||
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
|
||||
<div
|
||||
id="nav-baslik-yuvasi"
|
||||
className="pointer-events-none absolute inset-y-0 right-0 left-9 flex min-w-0 items-center sm:left-12 lg:right-auto lg:w-52 xl:w-72"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import { SURUM } from "@/lib/surum";
|
||||
|
||||
/**
|
||||
@@ -7,20 +8,25 @@ import { SURUM } from "@/lib/surum";
|
||||
* (`bg-indigo-500`, §2.2 istisnası). Sürüm rozetin içinde değil, yanında
|
||||
* küçük puntoyla; dar ekranda yalnız rozet kalır. Logo fade'iyle birlikte
|
||||
* söner (NavBaslik yuvası açılınca), o yüzden site-header'daki fade
|
||||
* sınıflarını aynen taşır.
|
||||
* sınıflarını aynen taşır. Rozet /surum-notlari'na bağlantıdır (beta
|
||||
* açıklaması, sürüm günlüğü, geri bildirim adresi).
|
||||
*/
|
||||
export function SurumRozeti({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<span className={`inline-flex shrink-0 items-center gap-1.5 ${className}`}>
|
||||
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white">
|
||||
<Link
|
||||
href="/surum-notlari"
|
||||
aria-label={`Beta sürüm notları, sürüm ${SURUM}`}
|
||||
className={`group/rozet inline-flex shrink-0 items-center gap-1.5 rounded-full outline-none focus-visible:ring-3 focus-visible:ring-ring/50 ${className}`}
|
||||
>
|
||||
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white transition-opacity duration-150 group-hover/rozet:opacity-85">
|
||||
Beta
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline"
|
||||
aria-label={`Sürüm ${SURUM}`}
|
||||
>
|
||||
v{SURUM}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
38
src/components/tema-kisayolu.tsx
Normal file
38
src/components/tema-kisayolu.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { temayiGecisle } from "@/lib/tema-gecisi";
|
||||
|
||||
/** Tuş yazı alanında basıldıysa kısayol değildir. */
|
||||
function yaziAlanindaMi(hedef: EventTarget | null) {
|
||||
if (!(hedef instanceof HTMLElement)) return false;
|
||||
if (hedef.isContentEditable) return true;
|
||||
const etiket = hedef.tagName;
|
||||
return etiket === "INPUT" || etiket === "TEXTAREA" || etiket === "SELECT";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function TemaKisayolu() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const dinle = (e: KeyboardEvent) => {
|
||||
if (e.key !== "d" && e.key !== "D") return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || e.repeat) return;
|
||||
if (yaziAlanindaMi(e.target)) return;
|
||||
e.preventDefault();
|
||||
const hedef = resolvedTheme === "dark" ? "light" : "dark";
|
||||
temayiGecisle(() => setTheme(hedef));
|
||||
};
|
||||
window.addEventListener("keydown", dinle);
|
||||
return () => window.removeEventListener("keydown", dinle);
|
||||
}, [resolvedTheme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { TemaKisayolu } from "@/components/tema-kisayolu";
|
||||
|
||||
// Gövde zemini (bg-slate-50) — açık ve koyu temadaki karşılığı
|
||||
const ZEMIN = { light: "#f8fafc", dark: "#080e19" };
|
||||
@@ -25,7 +26,8 @@ function AdresCubuguRengi() {
|
||||
* Tema (DESIGN.md §2.3): <html>'e `dark` sınıfı yazılır, renkler
|
||||
* 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.
|
||||
* 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).
|
||||
*/
|
||||
export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -37,6 +39,7 @@ export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<AdresCubuguRengi />
|
||||
<TemaKisayolu />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { Kbd } from "@/components/ui/kbd";
|
||||
import { temayiGecisle } from "@/lib/tema-gecisi";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SECENEKLER = [
|
||||
@@ -16,7 +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).
|
||||
* 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.
|
||||
*/
|
||||
export function TemaSecici({ className }: { className?: string }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -27,13 +30,11 @@ export function TemaSecici({ className }: { className?: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center gap-2", className)}>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Tema"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1",
|
||||
className,
|
||||
)}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1"
|
||||
>
|
||||
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
|
||||
const secili = tarayicida && theme === deger;
|
||||
@@ -43,7 +44,7 @@ export function TemaSecici({ className }: { className?: string }) {
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={secili}
|
||||
onClick={() => setTheme(deger)}
|
||||
onClick={() => temayiGecisle(() => setTheme(deger))}
|
||||
className={cn(
|
||||
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-semibold transition-[background-color,color,transform] duration-150 outline-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
|
||||
secili
|
||||
@@ -57,5 +58,10 @@ export function TemaSecici({ className }: { className?: string }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<Kbd>D</Kbd>
|
||||
<span>ile açık / koyu arasında geç</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
56
src/lib/tema-gecisi.ts
Normal file
56
src/lib/tema-gecisi.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user