diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md
index adfed78..fb4a37c 100644
--- a/docs/tasarim/DESIGN.md
+++ b/docs/tasarim/DESIGN.md
@@ -336,7 +336,13 @@ verilir (logo listesi istemciye inmez).
Hareket: yay `bounce: 0, duration: 0.35`, giriş ve çıkış aynı yoldan; "hareketi azalt"ta yalnız
opaklık.
-### 4.15 Liste satırı (program/üniversite listeleri) — `universite-program-tablosu.tsx`
+### 4.15 Liste satırı (program/üniversite listeleri) — `src/components/liste-satiri.ts`
+Sınıf dizileri tek dosyada (`SATIR_AD`, `SATIR_META`, `SATIR_CIP`, `SATIR_MOBIL_SAYI`,
+`SATIR_BUTON_KABI`…); üniversite, bölüm ve `/sonuc` tabloları oradan okur. Yeni bir liste
+tablosu bu sabitleri kullanır, sınıfı elle yazmaz.
+- Risk sütunu (`/sonuc`) sm'den itibaren ayrı sütun; telefonda adın altındaki sayı satırına
+ iner. Etiket §4.5: `size-2` nokta + `text-xs font-semibold text-{renk}-600` + `RISK_ETIKET`.
+- Veride BÜYÜK HARF gelen üniversite adı ekranda `trBaslikDuzeni` ile yazılır.
- **Ad kesilmez, sarar:** `whitespace-normal` + `block text-base font-semibold leading-snug`.
`truncate` ile "İngiliz Dili ve E…" yazılmaz.
- Ad linkinin hover'ı **mavi**: `transition-colors hover:text-primary hover:underline`
@@ -394,6 +400,11 @@ Haplar `h-11` (44 px), hover §2.1. Telefonda (sm altı) tek satır kalsın diye
aktif sayfa · Sonraki görünür; diğer numaralar `hidden sm:inline-flex` ile HTML'de durur
(tarayıcı botları tüm sayfaları görür). 375 px'te 7 sayfalık şerit iki satıra kırılıyordu.
+### 5.2.2 Dilim sekmeleri (Hayal / Dengeli / Güvenli) — `program-tablosu.tsx`
+Tek satırda üç hap 375 px'e sığmıyordu (418 px, sayfa yana kayıyordu). Telefonda
+`grid w-full grid-cols-3`, hapın içinde ikon+ad üstte, sayı altta; sm'den tek satır. Hap en az
+44 px (`min-h-11`), hover §2.1.
+
### 5.3 Verimli ve havadar
- İlişkili öğeler yan yana dizilir (iki sütun) — tek sütunda alt alta uzayan panel yapılmaz.
- Yan yana dizmek "hepsini tek şeride sıkıştırmak" değildir: 23 Eyl'de denenen yatay "bilgi
diff --git a/src/components/liste-satiri.ts b/src/components/liste-satiri.ts
new file mode 100644
index 0000000..b797f6b
--- /dev/null
+++ b/src/components/liste-satiri.ts
@@ -0,0 +1,45 @@
+/**
+ * Liste satırının (DESIGN.md §4.15) sınıf dizileri — tek kaynak. Üniversite,
+ * bölüm ve /sonuc program tabloları (ileride rapor listesi) aynı satırı
+ * çizer; sınıflar her dosyada ayrı yazılınca ölçüler kayıyordu. "use client"
+ * yok: sunucu bileşenleri de değer olarak okuyabilsin (§9.3).
+ */
+
+/** Satırın adı (bölüm/üniversite sayfasına link): sarar, hover mavi (§2.1 istisnası). */
+export const SATIR_AD =
+ "block text-base font-semibold leading-snug text-slate-900 transition-colors hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
+
+/** Link olmayan ad (slug yoksa). */
+export const SATIR_AD_DUZ =
+ "block text-base font-semibold leading-snug text-slate-900";
+
+/** Adın altındaki ikinci satır (varyant ya da üniversite adı). */
+export const SATIR_ALT_AD = "mt-0.5 block text-sm leading-snug text-slate-600";
+
+/** Satırdaki ikincil link (ör. üniversite adı) — §2.1 tek turuncu. */
+export const SATIR_IKINCIL_LINK =
+ "transition-colors hover:text-orange-500 hover:underline";
+
+/** Tür/il/süre satırı. */
+export const SATIR_META =
+ "mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500";
+
+/** Tür çipi (Devlet, Vakıf, SAY…). */
+export const SATIR_CIP =
+ "shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700";
+
+/** Telefonda sayısal sütunun adın altına indiği satır. */
+export const SATIR_MOBIL_SAYI =
+ "mt-1.5 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm text-slate-500 sm:hidden";
+
+/** Tablo başlık hücresi. */
+export const SATIR_BASLIK = "h-12 text-sm font-semibold text-slate-500";
+
+/** Adın bulunduğu hücre. */
+export const SATIR_AD_HUCRE = "w-full whitespace-normal py-4 pl-4 sm:pl-5";
+
+/** Butonların hücresi ve kabı: telefonda alt alta, sm'den yan yana. */
+export const SATIR_BUTON_HUCRE = "py-4 pr-4 text-right sm:pr-5";
+export const SATIR_BUTON_KABI =
+ "inline-flex flex-col items-center gap-2 sm:flex-row";
+export const SATIR_BUTON_BASLIK = "h-12 w-14 text-right sm:w-28";
diff --git a/src/features/katalog/components/bolum-program-listesi.tsx b/src/features/katalog/components/bolum-program-listesi.tsx
index b4e2310..3c87525 100644
--- a/src/features/katalog/components/bolum-program-listesi.tsx
+++ b/src/features/katalog/components/bolum-program-listesi.tsx
@@ -12,6 +12,19 @@ import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
} from "@/components/program-liste-verileri";
+import {
+ SATIR_AD,
+ SATIR_AD_DUZ,
+ SATIR_AD_HUCRE,
+ SATIR_ALT_AD,
+ SATIR_BASLIK,
+ SATIR_BUTON_BASLIK,
+ SATIR_BUTON_HUCRE,
+ SATIR_BUTON_KABI,
+ SATIR_CIP,
+ SATIR_META,
+ SATIR_MOBIL_SAYI,
+} from "@/components/liste-satiri";
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
import { ProgramNetleriButonu } from "./program-netleri";
import { UniLogo } from "@/components/uni-logo";
@@ -162,10 +175,10 @@ export function BolumProgramListesi({
-
+
Üniversite
-
+
Son {VERI_YIL_SAYISI} yıl
@@ -192,7 +205,7 @@ export function BolumProgramListesi({
onSirala={sirala}
/>
-
+
Netler ve listeye ekle
@@ -206,7 +219,7 @@ export function BolumProgramListesi({
return (
-
+
{p.universiteSlug ? (
@@ -215,23 +228,23 @@ export function BolumProgramListesi({
{p.universiteSlug ? (
{uniAd}
) : (
-
+
{uniAd}
)}
{varyant ? (
-
+
{varyant}
) : null}
-
+
{p.unitur ? (
-
+
{UNITUR_ETIKET[p.unitur]?.cip ??
trBaslikDuzeni(p.unitur)}
@@ -245,7 +258,7 @@ export function BolumProgramListesi({
{p.sure} yıl
) : null}
-
+
Taban sıra
{tabanMetni}
@@ -272,8 +285,8 @@ export function BolumProgramListesi({
{tabanMetni}
-
-
+
+
-
+
Program
-
+
Son {VERI_YIL_SAYISI} yıl
-
+
{SON_YIL} puan
-
+
Kontenjan
-
+
Taban sıra
-
+
Netler ve listeye ekle
@@ -76,28 +88,28 @@ export function UniversiteProgramTablosu({
const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—";
return (
-
+
{p.bolumSlug ? (
{p.isim.trim()}
) : (
-
+
{p.isim.trim()}
)}
-
-
+
+
{TUR_ETIKET[p.tur] ?? p.tur}
{p.sure ? (
{p.sure} yıl
) : null}
-
+
Taban sıra
{tabanMetni}
@@ -124,8 +136,8 @@ export function UniversiteProgramTablosu({
{tabanMetni}
-
-
+
+
+
{DILIMLER.map((d) => {
const aktif = aktifDilim === d.key;
return (
{aktif ? (
) : null}
-
-
- {d.etiket}
-
+
+
+
+ {d.etiket}
+
+
{sonuclar.toplam[d.key].toLocaleString("tr-TR")}
@@ -236,7 +257,7 @@ export function ProgramTablosu({
const toplam = sonuclar.toplam[d.key];
return (
-
+
{satirlar.length === 0 ? (
Bu dilimde filtrene uyan program bulunamadı.
@@ -244,17 +265,31 @@ export function ProgramTablosu({
) : (
-
- Risk
- Program
-
+
+
+ Risk
+
+
+ Program
+
+
Son {VERI_YIL_SAYISI} yıl
-
+
Kontenjan
- Taban sıra
-
+
+ Taban sıra
+
+
Netler ve listeye ekle
@@ -273,17 +308,19 @@ export function ProgramTablosu({
)}
{satirlar.length < toplam ? (
-
-
+
{satirlar.length.toLocaleString("tr-TR")} /{" "}
{toplam.toLocaleString("tr-TR")} program
@@ -297,6 +334,27 @@ export function ProgramTablosu({
);
}
+// Risk etiketinin metin rengi (§4.5): nokta + renkli metin, renk tek başına değil.
+const RISK_METIN: Record
= {
+ guvenli: "text-emerald-600",
+ "az-riskli": "text-amber-600",
+ riskli: "text-red-600",
+};
+
+function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) {
+ return (
+
+
+ {RISK_ETIKET[risk]}
+
+ );
+}
+
function ProgramSatiri({
program: p,
adaySira,
@@ -309,65 +367,74 @@ function ProgramSatiri({
const taban = programEtkinSira(p);
const risk = riskHesapla(taban, adaySira);
const devlet = p.unitur === "DEVLET";
+ const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—";
+ const bosKaldi =
+ p.kontenjanSon != null &&
+ p.yerlesenSon != null &&
+ p.yerlesenSon < p.kontenjanSon;
+ // Şişko ve sade (DESIGN.md §4.15): program adı kesilmez, üniversite adı
+ // altında ayrı satır; telefonda risk ve taban sıra adın altına iner,
+ // butonlar alt alta 44 px.
return (
-
- {risk ? (
-
-
- {RISK_ETIKET[risk]}
-
- ) : null}
+
+ {risk ? : null}
-
-
+
+
e.stopPropagation()}
>
{p.isim}
-
-
+ e.stopPropagation()}
>
- {devlet ? "Devlet" : "Vakıf"}
-
-
- e.stopPropagation()}
- >
- {p.universite}
-
- {p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
-
- {p.sure ? (
- · {p.sure} yıl
+ {trBaslikDuzeni(p.universite)}
+
+
+
+ {devlet ? "Devlet" : "Vakıf"}
+ {p.il ? (
+ {trBaslikDuzeni(p.il)}
) : null}
+ {p.sure ? (
+ {p.sure} yıl
+ ) : null}
+ {bosKaldi ? (
+
+ Boş kaldı
+
+ ) : null}
+
+
+ {risk ? : null}
+
+ Taban sıra
+
+ {tabanMetni}
+
+
+
-
+
{p.kontenjanSon != null ? (
-
- {p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? (
-
- boş kaldı
+
+ {p.kontenjanSon.toLocaleString("tr-TR")} kişi
+ {bosKaldi ? (
+
+ Boş kaldı
) : null}
- {p.kontenjanSon.toLocaleString("tr-TR")} kişi
) : (
"—"
)}
-
-
+
+
- {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
+ {tabanMetni}
-
-
+
+
diff --git a/src/features/liste/components/tercih-haritasi.tsx b/src/features/liste/components/tercih-haritasi.tsx
index 0af9813..187b3e5 100644
--- a/src/features/liste/components/tercih-haritasi.tsx
+++ b/src/features/liste/components/tercih-haritasi.tsx
@@ -513,10 +513,10 @@ export function TercihHaritasi({
onTercihSec(sira) : undefined}
- className="flex cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1.5 pl-1.5 pr-3 text-xs transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
+ className="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1.5 pl-1.5 pr-3.5 text-sm transition-[background-color,border-color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 active:scale-[0.97]"
>
{sira}
@@ -543,7 +543,7 @@ export function TercihHaritasi({
{bolum || kisaUniAdi(pin.universite)}
{bolum ? (
-
+
{kisaUniAdi(pin.universite)}
) : null}
diff --git a/src/features/liste/components/tercih-profili-kapisi.tsx b/src/features/liste/components/tercih-profili-kapisi.tsx
index 7ab9ce9..1c1d891 100644
--- a/src/features/liste/components/tercih-profili-kapisi.tsx
+++ b/src/features/liste/components/tercih-profili-kapisi.tsx
@@ -7,6 +7,7 @@ import { useReducedMotion } from "motion/react";
import useMeasure from "react-use-measure";
import { toast } from "sonner";
import { TransitionPanel } from "@/components/core/transition-panel";
+import { SectionEyebrow } from "@/components/pixel-decor";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -295,11 +296,9 @@ function KapıIcerik({
{duzenle ? (
-
- Tercihlerini güncelle
-
+
Tercihlerini güncelle
) : null}
-
+
{duzenle
? "Seçimlerini değiştir"
: "Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim"}
@@ -348,11 +347,9 @@ function KapıIcerik({
>
{duzenle ? (
-
- Sıralama ve tercihler
-
+
Sıralama ve tercihler
) : null}
-
+
{duzenle
? "Yeni sıralaman nedir?"
: "Başarı sıralaman nedir?"}
@@ -366,7 +363,7 @@ function KapıIcerik({