feat(tasarim): liste çekmecesi "şişko ve sade" dile göre

- Başlık ortalı (§4.10), çarpı ve temizle düğmesi kanonik size-9
- Satırlar çerçevesiz liste satırı; ad sarar, üniversite adı ayrı
  satırda, risk etiketi metin + renkle adın altında, çıkar düğmesi 44 px
- Alt bant telefonda alt alta: başlık üç satıra kırılıp listenin yerini
  yiyordu (liste 232 → 279 px, 375 px ölçümü)
- 10-11 px yazılar ve mavi hover'lar kalktı; navbar sayaç rozeti 12 px

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 00:48:57 +03:00
parent f02494ef78
commit 73943b0954
3 changed files with 102 additions and 98 deletions

View File

@@ -275,6 +275,11 @@ açılan modal değil. Çekmece kabı ve çarpı `cta-sira-form.tsx`'teki çekme
kareler kısaltılır, not kısaltılır — içerik silinmez (§9.2 ölçüm). kareler kısaltılır, not kısaltılır — içerik silinmez (§9.2 ölçüm).
- Sıra: gösterge kareleri → tek cümlelik not (`text-sm text-slate-500`) → ikincil buton (§4.1) - Sıra: gösterge kareleri → tek cümlelik not (`text-sm text-slate-500`) → ikincil buton (§4.1)
→ kaynaklar (logo solda, `text-xs` iki satır). → kaynaklar (logo solda, `text-xs` iki satır).
- **Liste çekmecesi** (`liste-cekmecesi.tsx`, 24 Eyl 2026): başlık aynı düzende (temizle
düğmesi çarpıyla aynı sınıf setinde solda). Satırlar çerçeveli kart değil, düz liste satırı
(`border-b border-slate-100`); ad §4.15 gibi sarar, risk + taban sıra adın altında, çıkar
düğmesi `size-11`. Listeler uzun olduğu için bu çekmece kaydırılır (tek ekran kuralının
tek istisnası); alt bandı telefonda alt alta dizilir ki listeye en az ~280 px kalsın.
- **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken - **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken
çizilmez). Google'ın görmesi gereken metin ve **zorunlu atıflar** (ör. URAP sırası + payda + çizilmez). Google'ın görmesi gereken metin ve **zorunlu atıflar** (ör. URAP sırası + payda +
dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır. dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır.

View File

@@ -43,7 +43,8 @@ import {
type PanInfo, type PanInfo,
} from "motion/react"; } from "motion/react";
import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk"; import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
import { bolumBazAdi, turkishSlugify } from "@/lib/slug"; import { bolumBazAdi, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
import { SATIR_AD, SATIR_IKINCIL_LINK } from "@/components/liste-satiri";
import { olay } from "@/lib/analitik"; import { olay } from "@/lib/analitik";
import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari"; import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari";
import { import {
@@ -83,6 +84,12 @@ const RISK_NOKTA: Record<RiskSeviyesi, string> = {
"az-riskli": "bg-amber-500", "az-riskli": "bg-amber-500",
riskli: "bg-red-500", riskli: "bg-red-500",
}; };
// §4.5: nokta + renkli metin; renk tek başına anlam taşımaz
const RISK_METIN: Record<RiskSeviyesi, string> = {
guvenli: "text-emerald-600",
"az-riskli": "text-amber-600",
riskli: "text-red-600",
};
export function ListeCekmecesi() { export function ListeCekmecesi() {
const router = useRouter(); const router = useRouter();
@@ -123,35 +130,33 @@ export function ListeCekmecesi() {
<DrawerContent <DrawerContent
className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl" className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
> >
<DrawerHeader className="border-b border-slate-100 text-left md:text-left"> {/* Alt sayfa başlığı (DESIGN.md §4.10): ortalı başlık + bağlam;
<div className="flex items-center justify-between gap-3"> çarpı §4.3 kanonik sınıf setinde, temizle düğmesi aynı ölçüde
<DrawerTitle className="flex items-center gap-2 font-heading text-lg font-bold"> solda. */}
<ListChecks className="size-5 text-primary" aria-hidden /> <DrawerHeader className="relative border-b border-slate-100 px-4 pt-5 pb-4 text-center md:text-center">
Kendi Listem {liste.length > 0 ? (
<span className="font-heading text-sm font-bold tabular-nums text-slate-400"> <button
{liste.length}/{MANUEL_LISTE_MAX} type="button"
</span> onClick={temizle}
</DrawerTitle> title="Listeyi temizle"
<div className="flex items-center gap-1"> className="absolute top-3 left-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]"
{liste.length > 0 ? ( >
<button <Trash2 className="size-4" aria-hidden />
type="button" <span className="sr-only">Listeyi temizle</span>
onClick={temizle} </button>
title="Listeyi temizle" ) : null}
className="cursor-pointer rounded-full p-2 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-red-500" <DrawerClose className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
> <X className="size-4" aria-hidden />
<Trash2 className="size-4" aria-hidden /> <span className="sr-only">Kapat</span>
<span className="sr-only">Listeyi temizle</span> </DrawerClose>
</button> <DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
) : null} Kendi Listem
<DrawerClose className="cursor-pointer rounded-full p-2 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-600"> </DrawerTitle>
<X className="size-4" aria-hidden /> <p className="mt-1 px-10 text-center text-sm text-slate-500">
<span className="sr-only">Kapat</span> {liste.length} / {MANUEL_LISTE_MAX} tercih
</DrawerClose> </p>
</div>
</div>
<div <div
className="mt-3 h-1.5 overflow-hidden rounded-full bg-slate-100" className="mx-auto mt-3 h-2 w-full max-w-xs overflow-hidden rounded-full bg-slate-100"
role="progressbar" role="progressbar"
aria-valuenow={liste.length} aria-valuenow={liste.length}
aria-valuemin={0} aria-valuemin={0}
@@ -164,30 +169,10 @@ export function ListeCekmecesi() {
transition={{ type: "spring", duration: 0.5, bounce: 0 }} transition={{ type: "spring", duration: 0.5, bounce: 0 }}
/> />
</div> </div>
<ul
className="mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-slate-500"
aria-label="Yerleşme riski renkleri"
>
{(
[
["guvenli", "yeşil güvenli"],
["az-riskli", "turuncu az riskli"],
["riskli", "kırmızı riskli"],
] as const
).map(([seviye, aciklama]) => (
<li key={seviye} className="inline-flex items-center gap-1.5">
<span
className={`size-2 shrink-0 rounded-full ${RISK_NOKTA[seviye]}`}
aria-hidden
/>
<span>{aciklama}</span>
</li>
))}
</ul>
</DrawerHeader> </DrawerHeader>
<div <div
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4" className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-2"
> >
{liste.length === 0 ? ( {liste.length === 0 ? (
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden px-4 py-16 text-center"> <div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden px-4 py-16 text-center">
@@ -196,10 +181,10 @@ export function ListeCekmecesi() {
strokeWidth={1.25} strokeWidth={1.25}
aria-hidden aria-hidden
/> />
<p className="relative font-heading text-sm font-bold text-slate-700"> <p className="relative font-heading text-lg font-bold text-slate-900">
Henüz program seçmedin Henüz program seçmedin
</p> </p>
<p className="relative text-xs leading-relaxed text-slate-500"> <p className="relative text-sm leading-relaxed text-slate-500">
Sonuç sayfasından program ekleyerek listeni doldurmaya başla. Sonuç sayfasından program ekleyerek listeni doldurmaya başla.
</p> </p>
</div> </div>
@@ -228,29 +213,32 @@ export function ListeCekmecesi() {
<button <button
type="button" type="button"
onClick={programaEkleGit} onClick={programaEkleGit}
className={`flex h-12 w-full cursor-pointer items-center justify-center rounded-full border border-dashed border-slate-300 text-slate-400 transition-colors duration-200 hover:border-primary/50 hover:bg-primary/5 hover:text-primary active:scale-[0.98] ${ className={`flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-full border border-dashed border-slate-300 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97] ${
liste.length > 0 ? "mt-2" : "mt-1" liste.length > 0 ? "mt-3" : "mt-1"
}`} }`}
> >
<Plus className="size-5" aria-hidden /> <Plus className="size-5" aria-hidden />
<span className="sr-only">Sonuç sayfasından program ekle</span> Program ekle
</button> </button>
) : null} ) : null}
</div> </div>
<DrawerFooter className="border-t border-slate-100 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]"> <DrawerFooter className="border-t border-slate-100 pt-3 pb-[max(1rem,env(safe-area-inset-bottom))]">
{/* C2 kalıcı satır: girişsiz kullanıcıya listenin uçucu olduğu {/* C2 kalıcı satır: girişsiz kullanıcıya listenin uçucu olduğu
hatırlatılır; giriş doğal "kaydet" gerekçesi olarak sunulur. */} hatırlatılır; giriş doğal "kaydet" gerekçesi olarak sunulur. */}
{girisli === false ? ( {girisli === false ? (
<div className="mb-3 flex items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-left"> <div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 px-1 text-left">
<p className="min-w-0 text-xs leading-relaxed text-slate-600"> <p className="min-w-0 text-sm leading-relaxed text-slate-600">
{/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */} {/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */}
<IsletimSistemiLogosu className="mr-1.5 inline size-3.5 align-[-3px] text-slate-700" /> <IsletimSistemiLogosu className="mr-1.5 inline size-4 align-[-3px] text-slate-700" />
<span className="font-semibold text-slate-800"> <span className="font-semibold text-slate-800">
Listen yalnızca bu tarayıcıda. Listen yalnızca bu tarayıcıda.
</span>{" "} </span>
Giriş yap; her cihazdan eriş{" "} <span className="hidden sm:inline">
<CihazCifti className="align-[-3px] text-slate-400" /> {" "}
Giriş yap; her cihazdan eriş{" "}
<CihazCifti className="align-[-3px] text-slate-400" />
</span>
</p> </p>
<Link <Link
href={girisUrl} href={girisUrl}
@@ -258,16 +246,19 @@ export function ListeCekmecesi() {
cekmeceKapat(); cekmeceKapat();
olay("giris_cta_tiklandi", { kaynak: "liste_cekmecesi" }); olay("giris_cta_tiklandi", { kaynak: "liste_cekmecesi" });
}} }}
className="inline-flex shrink-0 items-center gap-1 text-xs font-semibold text-primary transition-colors duration-200 hover:text-primary/80" className="inline-flex min-h-11 shrink-0 items-center gap-1 text-sm font-semibold text-primary transition-colors duration-150 hover:text-orange-500"
> >
Giriş yap, kaydet Giriş yap, kaydet
<ArrowRight className="size-3.5" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Link> </Link>
</div> </div>
) : null} ) : null}
{/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary {/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary
zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */} zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */}
<div className="relative isolate flex items-center justify-between gap-4 overflow-hidden rounded-2xl bg-primary px-5 py-5 text-left text-white"> {/* Telefonda alt alta (başlık tek satır, buton tam genişlik):
yan yana dizince başlık üç satıra kırılıp listenin yerini
yiyordu (24 Eyl 2026, 375 px ölçümü). */}
<div className="relative isolate flex flex-col gap-3 overflow-hidden rounded-2xl bg-primary px-5 py-4 text-left text-white sm:flex-row sm:gap-4 sm:py-5 sm:items-center sm:justify-between">
<div <div
aria-hidden aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white" className="pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white"
@@ -275,10 +266,10 @@ export function ListeCekmecesi() {
<PixelField seed={8} /> <PixelField seed={8} />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-heading text-2xl font-bold"> <p className="font-heading text-xl font-bold sm:text-2xl">
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"} {riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
</p> </p>
<p className="mt-1 truncate text-sm leading-relaxed text-white"> <p className="mt-1 hidden truncate text-sm leading-relaxed text-white sm:block">
{riskEksik {riskEksik
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)." ? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
: "Her seçimin yerleşme riski hazır — son kararı sen ver."} : "Her seçimin yerleşme riski hazır — son kararı sen ver."}
@@ -287,7 +278,7 @@ export function ListeCekmecesi() {
{riskEksik ? ( {riskEksik ? (
<Button <Button
size="lg" size="lg"
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600" className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
onClick={() => { onClick={() => {
cekmeceKapat(); cekmeceKapat();
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam // Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
@@ -303,7 +294,7 @@ export function ListeCekmecesi() {
<Button <Button
asChild asChild
size="lg" size="lg"
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600" className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
> >
<Link href="/listem" onClick={cekmeceKapat}> <Link href="/listem" onClick={cekmeceKapat}>
Listemde aç Listemde aç
@@ -358,12 +349,12 @@ function CekmeceSatiri({
exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }} exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }}
transition={{ type: "spring", duration: 0.4, bounce: 0 }} transition={{ type: "spring", duration: 0.4, bounce: 0 }}
whileDrag={{ scale: 1.03, zIndex: 10 }} whileDrag={{ scale: 1.03, zIndex: 10 }}
className="relative mb-2 select-none last:mb-0" className="relative select-none border-b border-slate-100 last:border-b-0"
> >
{/* Sil bandı: kart sağa kaydıkça altından görünür */} {/* Sil bandı: kart sağa kaydıkça altından görünür */}
<motion.div <motion.div
style={{ opacity: silGorunur }} style={{ opacity: silGorunur }}
className="absolute inset-0 flex items-center rounded-xl bg-red-500 pl-4" className="absolute inset-0 flex items-center bg-red-500 pl-4"
aria-hidden aria-hidden
> >
<Trash2 className="size-4 text-white" /> <Trash2 className="size-4 text-white" />
@@ -377,7 +368,7 @@ function CekmeceSatiri({
dragElastic={{ left: 0, right: 0.7 }} dragElastic={{ left: 0, right: 0.7 }}
dragSnapToOrigin dragSnapToOrigin
onDragEnd={kaydirinca} onDragEnd={kaydirinca}
className="relative flex items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-3 py-2.5" className="relative flex items-center gap-3 bg-white py-3"
> >
<button <button
type="button" type="button"
@@ -385,58 +376,66 @@ function CekmeceSatiri({
e.stopPropagation(); // yatay kaydırmayı tetiklemesin e.stopPropagation(); // yatay kaydırmayı tetiklemesin
siraKontrol.start(e); siraKontrol.start(e);
}} }}
className="-m-1 shrink-0 cursor-grab touch-none rounded p-1 text-slate-300 active:cursor-grabbing" className="-ml-1 flex h-11 w-7 shrink-0 cursor-grab touch-none items-center justify-center rounded-lg text-slate-300 transition-colors hover:text-slate-500 active:cursor-grabbing"
> >
<GripVertical className="size-4" aria-hidden /> <GripVertical className="size-5" aria-hidden />
<span className="sr-only">{tercih.isim} sırasını değiştir</span> <span className="sr-only">{tercih.isim} sırasını değiştir</span>
</button> </button>
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-[11px] font-bold tabular-nums"> <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-sm font-bold text-slate-900">
{sira} {sira}
</span> </span>
<UniLogo ad={tercih.universite} boy="xs" />
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<Link <Link
href={bolumHref(tercih.isim)} href={bolumHref(tercih.isim)}
onClick={cekmeceKapat} onClick={cekmeceKapat}
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className={SATIR_AD}
> >
{tercih.isim} {tercih.isim}
</Link> </Link>
<span className="block truncate text-xs text-slate-500"> <span className="mt-1 flex items-center gap-2 text-sm leading-snug text-slate-600">
<UniLogo ad={tercih.universite} boy="xs" />
<Link <Link
href={uniHref(tercih.universite)} href={uniHref(tercih.universite)}
onClick={cekmeceKapat} onClick={cekmeceKapat}
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
title={`${tercih.universite} taban puanları sayfası`} title={`${tercih.universite} taban puanları sayfası`}
className="hover:text-primary hover:underline" className={`min-w-0 ${SATIR_IKINCIL_LINK}`}
> >
{tercih.universite} {trBaslikDuzeni(tercih.universite)}
</Link> </Link>
{tercih.il ? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}` : ""}
</span> </span>
{tercih.risk || tercih.efektifSira != null ? (
<span className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-slate-500">
{tercih.risk ? (
<span
className={`inline-flex items-center gap-1.5 text-xs font-semibold ${RISK_METIN[tercih.risk]}`}
>
<span
className={`size-2 shrink-0 rounded-full ${RISK_NOKTA[tercih.risk]}`}
aria-hidden
/>
{RISK_ETIKET[tercih.risk]}
</span>
) : null}
{tercih.efektifSira != null ? (
<span className="inline-flex items-center gap-1.5">
Taban sıra
<span className="font-semibold tabular-nums text-slate-900">
~{tercih.efektifSira.toLocaleString("tr-TR")}.
</span>
</span>
) : null}
</span>
) : null}
</span> </span>
{tercih.efektifSira != null ? (
<span className="shrink-0 text-xs tabular-nums text-slate-500">
~{tercih.efektifSira.toLocaleString("tr-TR")}.
</span>
) : null}
{tercih.risk ? (
<span className="inline-flex shrink-0 items-center gap-1.5 text-xs font-medium">
<span
className={`size-2.5 shrink-0 rounded-full ${RISK_NOKTA[tercih.risk]}`}
aria-hidden
/>
{RISK_ETIKET[tercih.risk]}
</span>
) : null}
<button <button
type="button" type="button"
onClick={() => cikar(tercih.id)} onClick={() => cikar(tercih.id)}
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
className="shrink-0 cursor-pointer rounded-full p-1.5 text-slate-300 transition-colors duration-200 hover:bg-slate-100 hover:text-red-500" className="inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.92]"
> >
<X className="size-3.5" aria-hidden /> <X className="size-4" aria-hidden />
<span className="sr-only">{tercih.isim} listeden çıkar</span> <span className="sr-only">{tercih.isim} listeden çıkar</span>
</button> </button>
</motion.div> </motion.div>

View File

@@ -46,7 +46,7 @@ export function ListemButonu() {
initial={azMotion ? false : { scale: 0.6 }} initial={azMotion ? false : { scale: 0.6 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
transition={{ type: "spring", duration: 0.4, bounce: 0.45 }} transition={{ type: "spring", duration: 0.4, bounce: 0.45 }}
className="flex size-5 items-center justify-center rounded-full bg-orange-500 font-heading text-[11px] font-bold tabular-nums text-white" className="flex h-6 min-w-6 items-center justify-center rounded-full bg-orange-500 px-1.5 font-heading text-xs font-bold text-white"
> >
{liste.length} {liste.length}
</motion.span> </motion.span>