feat(tema): D kısayolu, logodan yayılan tema geçişi, /surum-notlari beta sayfası (→ 1.1.0)

- lib/tema-gecisi.ts: View Transitions + CSS maskesi (kolay-tercih-mark.svg),
  merkez header'daki işaret; reduced-motion ve desteksiz tarayıcıda düz geçiş
- tema-kisayolu.tsx: D tuşu açık ↔ koyu (yazı alanı/değiştirici tuşta pasif)
- tema-secici.tsx: seçim de maskeyle oynar, altında Kbd ile D ipucu
- /surum-notlari: beta açıklaması, geri bildirim (destek@kolaytercih.com),
  sürüm günlüğü; Beta rozeti ve footer buraya bağlanır, sitemap'e eklendi
- DESIGN.md §2.3 notu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 23:25:38 +03:00
parent b2bab50337
commit 5f7ea8bbcd
13 changed files with 395 additions and 42 deletions

View File

@@ -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(

View File

@@ -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ı

View File

@@ -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"

View File

@@ -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>
);
}

View 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;
}

View File

@@ -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>
);

View File

@@ -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,35 +30,38 @@ export function TemaSecici({ className }: { className?: string }) {
);
return (
<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,
)}
>
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
const secili = tarayicida && theme === deger;
return (
<button
key={deger}
type="button"
role="radio"
aria-checked={secili}
onClick={() => 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
? "bg-slate-900 text-slate-50"
: "text-slate-600 hover:text-orange-500",
)}
>
<Ikon className="size-4" aria-hidden />
{etiket}
</button>
);
})}
<div className={cn("flex flex-col items-center gap-2", className)}>
<div
role="radiogroup"
aria-label="Tema"
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;
return (
<button
key={deger}
type="button"
role="radio"
aria-checked={secili}
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
? "bg-slate-900 text-slate-50"
: "text-slate-600 hover:text-orange-500",
)}
>
<Ikon className="size-4" aria-hidden />
{etiket}
</button>
);
})}
</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>
);
}