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