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
|
(`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 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.
|
**Nasıl çalışır:** bileşenlere `dark:` yazılmaz; palet token'ları çevrilir.
|
||||||
|
|
||||||
| Açık temadaki sınıf | Koyu temada ne olur |
|
| Açık temadaki sınıf | Koyu temada ne olur |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kolaytercih",
|
"name": "kolaytercih",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -476,3 +476,72 @@
|
|||||||
.toaster [data-sonner-toast][data-styled="true"] [data-button]:hover {
|
.toaster [data-sonner-toast][data-styled="true"] [data-button]:hover {
|
||||||
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
|
||||||
|
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}/paket`, priority: 0.7, changeFrequency: "monthly" },
|
||||||
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
||||||
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
|
{ 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}/gizlilik`, priority: 0.2, changeFrequency: "yearly" },
|
||||||
{
|
{
|
||||||
url: `${SITE_URL}/cerez-politikasi`,
|
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.
|
* yerinden başlar.
|
||||||
*/
|
*/
|
||||||
export function AnimasyonluMark({
|
export function AnimasyonluMark({
|
||||||
|
id,
|
||||||
className,
|
className,
|
||||||
sadeceHover = false,
|
sadeceHover = false,
|
||||||
oynat = true,
|
oynat = true,
|
||||||
varyant = "rota",
|
varyant = "rota",
|
||||||
renk = "marka",
|
renk = "marka",
|
||||||
}: {
|
}: {
|
||||||
|
/** Kök elemanın id'si (ör. tema geçişinin merkez noktası, lib/tema-gecisi.ts). */
|
||||||
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
sadeceHover?: boolean;
|
sadeceHover?: boolean;
|
||||||
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
|
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
|
||||||
@@ -284,6 +287,7 @@ export function AnimasyonluMark({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
id={id}
|
||||||
ref={kokRef}
|
ref={kokRef}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -199,6 +199,12 @@ export function SiteFooter() {
|
|||||||
>
|
>
|
||||||
İletişim
|
İletişim
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/surum-notlari"
|
||||||
|
className="hover:text-slate-700 hover:underline"
|
||||||
|
>
|
||||||
|
Sürüm notları
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<TemaSecici className="mt-6" />
|
<TemaSecici className="mt-6" />
|
||||||
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları
|
{/* 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 Link from "next/link";
|
||||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||||
import { SurumRozeti } from "@/components/surum-rozeti";
|
import { SurumRozeti } from "@/components/surum-rozeti";
|
||||||
|
import { TEMA_ODAK_ID } from "@/lib/tema-gecisi";
|
||||||
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
||||||
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
|
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
|
||||||
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
||||||
@@ -23,8 +24,13 @@ export function SiteHeader() {
|
|||||||
className="group flex min-w-0 shrink items-center font-bricolage"
|
className="group flex min-w-0 shrink items-center font-bricolage"
|
||||||
>
|
>
|
||||||
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
|
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
|
||||||
kareler danışman panelindeki nabız animasyonuyla canlanır. */}
|
kareler danışman panelindeki nabız animasyonuyla canlanır.
|
||||||
<AnimasyonluMark sadeceHover className="size-8 sm:size-11" />
|
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="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">
|
<span className="text-lg font-light tracking-tighter sm:text-3xl">
|
||||||
Kolay
|
Kolay
|
||||||
@@ -33,10 +39,12 @@ export function SiteHeader() {
|
|||||||
Tercih
|
Tercih
|
||||||
</span>
|
</span>
|
||||||
</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>
|
</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
|
<div
|
||||||
id="nav-baslik-yuvasi"
|
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"
|
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";
|
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
|
* (`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
|
* 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ö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 }) {
|
export function SurumRozeti({ className = "" }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex shrink-0 items-center gap-1.5 ${className}`}>
|
<Link
|
||||||
<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">
|
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
|
Beta
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
aria-hidden
|
||||||
className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline"
|
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}
|
v{SURUM}
|
||||||
</span>
|
</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 { useEffect } from "react";
|
||||||
import { ThemeProvider, useTheme } from "next-themes";
|
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ığı
|
// Gövde zemini (bg-slate-50) — açık ve koyu temadaki karşılığı
|
||||||
const ZEMIN = { light: "#f8fafc", dark: "#080e19" };
|
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
|
* 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;
|
* 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.
|
* 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 }) {
|
export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -37,6 +39,7 @@ export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<AdresCubuguRengi />
|
<AdresCubuguRengi />
|
||||||
|
<TemaKisayolu />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Monitor, Moon, Sun } from "lucide-react";
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { Kbd } from "@/components/ui/kbd";
|
||||||
|
import { temayiGecisle } from "@/lib/tema-gecisi";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const SECENEKLER = [
|
const SECENEKLER = [
|
||||||
@@ -16,7 +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).
|
* 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 }) {
|
export function TemaSecici({ className }: { className?: string }) {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@@ -27,13 +30,11 @@ export function TemaSecici({ className }: { className?: string }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={cn("flex flex-col items-center gap-2", className)}>
|
||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label="Tema"
|
aria-label="Tema"
|
||||||
className={cn(
|
className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1"
|
||||||
"inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
|
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
|
||||||
const secili = tarayicida && theme === deger;
|
const secili = tarayicida && theme === deger;
|
||||||
@@ -43,7 +44,7 @@ export function TemaSecici({ className }: { className?: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={secili}
|
aria-checked={secili}
|
||||||
onClick={() => setTheme(deger)}
|
onClick={() => temayiGecisle(() => setTheme(deger))}
|
||||||
className={cn(
|
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]",
|
"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
|
secili
|
||||||
@@ -57,5 +58,10 @@ export function TemaSecici({ className }: { className?: string }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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