fix(program-ayrinti): tek durak + sabit iskelet boyu (açılış sıçraması), "Yıllara göre" kaldırıldı, logo→içerik açılışı; DESIGN §4.19

Çekmece varsayılan tek durakta (0,92) açılır; duraklar/ilkDurak kaldırıldı
(kısık durakta gövde kaydırılamıyordu, veri gelince boy değişince konum
sıçrıyordu). İskelet min-h-[70dvh] sarmalda. Yıl tablosu silindi (API
`gecmis` alanı duruyor; backlog). Bölümler data-sonra, başlık altında
data-logo kabı; AcilisSahnesi hazir={durum === "hazir"}. DESIGN.md §4.19
ve §4.10 durak atfı güncellendi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 23:16:13 +03:00
parent 7d52e8d5f1
commit bed839768c
2 changed files with 64 additions and 87 deletions

View File

@@ -369,9 +369,11 @@ Parçalar: `AltSayfa` (kök) · `AltSayfaTetik` · `AltSayfaIcerik` (perde + yü
`AltSayfaSurukle` (kaydırmayan başlık alanı) · `AltSayfaBaslik` / `AltSayfaAciklama` ·
`AltSayfaKapat`. Yüzey sınıfı bileşenin içindedir (`rounded-t-3xl border border-b-0
border-slate-200 bg-card shadow-2xl sm:max-w-2xl`, tavan `92dvh`); kullanım yerinde kap sınıfı
**yazılmaz**. Boy içeriğe göredir. `duraklar` (ekran yüksekliğinin oranı) yalnız uzun, kaydırılan
içerikte verilir: program ayrıntısı `[0.6, 0.92]` ile %60'ta açılır, yukarı çekince ya da
kaydırınca tam boya çıkar (§4.19). İçinde kendi sürüklemesi olan öğe (sıralama tutamağı)
**yazılmaz**. Boy içeriğe göredir. `duraklar` (ekran yüksekliğinin oranı) bugün hiçbir yerde
verilmez; her çekmece varsayılan tek durakta (0,92) açılır. Program ayrıntısı 7 Eki 2026'ya
kadar `[0.6, 0.92]` ile %60'ta açılıyordu; kısık durakta gövde kaydırılamadığı ve veri gelince
boy değiştiği için "takılmış" görünüyordu, tek durağa döndü (§4.19). Yeni bir çift durak
istiyorsan açılışta içerik boyunun sabit olduğundan emin ol. İçinde kendi sürüklemesi olan öğe (sıralama tutamağı)
`data-alt-sayfa-yoksay` alır. Bildirimdeki düğmeye basmak sayfayı kapatmaz. Hareketi azalt:
kayma yok, yalnız opaklık.
@@ -638,8 +640,7 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
tek çekmece açar; veri `/api/program-ayrinti`. Sıra: başlık = program adı, bağlam =
üniversite · il · fakülte; **künye hapları** (§4.12 ölçüsü, link değil: süre + tür, dil,
öğretim türü (örgün değilse), burs/ücret, akreditasyon, baraj, öğretim üyesi sayısı,
yalnız kız/erkek koşulu, kampüs ilçesi — yalnız verisi olanlar); "Yıllara göre" tablosu
(yıl, taban sıra, puan, kontenjan, yerleşen); "Kontenjan türleri (yıl)" `dl` satırları
yalnız kız/erkek koşulu, kampüs ilçesi — yalnız verisi olanlar); "Kontenjan türleri (yıl)" `dl` satırları
(yalnız >0 olanlar; özel kontenjanın ayrı taban sırasının elimizde olmadığı notu);
"Son yerleşenin netleri" (eski tablo + OBP katsayısı satırı; 0,18 = meslek lisesi ek
puanı notu); "Yerleşme koşulları (N)" yerel `details` ile kapalı başlar; "Mezun
@@ -648,6 +649,16 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
süs ayraç yok, kart yok. İçerik uzun olduğu için çekmece **kaydırılır** (liste çekmecesi
gibi; §4.10 tek ekran kuralının ikinci istisnası). Açık listem satırında aynı çekmece
`varyant="metin"` ile "Program ayrıntısı" metin düğmesinden açılır ("Haritada gör" kalıbı).
- **7 Eki 2026 — tek durak, sabit iskelet, logo → içerik:** çekmece varsayılan tek durakta
(0,92) açılır; `duraklar`/`ilkDurak` verilmez. Yükleme iskeleti `min-h-[70dvh]` sarmalda
açılır ki veri gelince sayfa yalnız büyüsün, konum sıçramasın. "Yıllara göre" yıl tablosu
kaldırıldı (API `gecmis` alanı duruyor; backlog). Açılış koreografisi `AcilisSahnesi`
kartsız modu (`src/components/ui/acilis-sahnesi.tsx`, `hazir={durum === "hazir"}`): veri
gelince başlığın altında üç logo karesi (`data-logo` kabı, `relative mx-auto h-16 w-16`)
belirir, kısa durur, sönerek kaybolur (scale 0.6 + opacity 0; kap sıfır boya iner), içerik
blokları (`data-sonra`: künye, kontenjan türleri, netler, koşullar, istihdam, üniversite,
kaynak) 0,08 s arayla bulanıktan netleşir; toplam ≈ 1 s. Hareketi azalt: logo yok, 0,25 s
solma (§6). Akademik göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır.
### 4.20 Danışman sohbeti — `sohbet-client.tsx`, `arac-pilleri.tsx`
Sohbet kabuğu **prompt-kit** bileşenleridir (`src/components/ui/`: `prompt-input`, `message`,

View File

@@ -3,13 +3,18 @@
// Program satırındaki "ayrıntı" düğmesi: programın tüm açık verisini alttan
// açılan sayfada gösterir (DESIGN.md §4.10 çekmece, §4.19 program ayrıntı
// çekmecesi). Veri /api/program-ayrinti'den gelir: künye (dil, öğretim türü,
// burs/ücret, akreditasyon, baraj, kadro, kampüs, cinsiyet koşulu), yıllara
// göre sıra/puan/kontenjan/yerleşen, kontenjan türleri, son yerleşenin
// netleri, yerleşme koşulları, TÜİK mezun istihdamı, üniversite özeti.
// burs/ücret, akreditasyon, baraj, kadro, kampüs, cinsiyet koşulu), kontenjan
// türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı,
// üniversite özeti. API'nin `gecmis` (yıllara göre sıra/puan/kontenjan)
// alanı burada gösterilmez (Bilal, 7 Eki 2026: yıl tablosu kaldırıldı;
// backlog). Açılış: veri gelince logo kareleri belirir, söner, içerik
// bloklar hâlinde netleşir (AcilisSahnesi kartsız modu).
// Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026
// SPA'sına geçerken cinsiyet panelini yayından kaldırdı (bkz. scripts/detay.ts).
// Çekmece içerik ağırlıklı olduğu için liste çekmecesi gibi kaydırılır (§4.10
// tek ekran kuralının ikinci istisnası).
// tek ekran kuralının ikinci istisnası). Tek durak (varsayılan 0,92): çift
// durakla açılış, kısık durakta gövde kaydırılamadığı ve veri gelince boy
// değiştiği için takılmış görünüyordu (7 Eki 2026).
import { SATIR_BASLIK, SATIR_DUGME_BOYU } from "@/components/liste-satiri";
import { useState } from "react";
@@ -45,6 +50,11 @@ import {
AltSayfaBaslik,
AltSayfaTetik,
} from "@/components/ui/alt-sayfa";
import {
AcilisSahnesi,
LOGO_SLOTLARI,
logoKareStili,
} from "@/components/ui/acilis-sahnesi";
import {
Table,
TableBody,
@@ -163,13 +173,10 @@ export function ProgramAyrintiButonu({
)}
</AltSayfaTetik>
{/* Alt sayfa kabı tek kaynak (alt-sayfa.tsx), çarpı §4.3. İçerik uzun:
sayfa ekranın %60'ında açılır (başlık + künye), yukarı çekince ya da
kaydırınca tam boya çıkar (§4.19). */}
<AltSayfaIcerik
duraklar={[0.6, 0.92]}
ilkDurak={0}
onClick={(e) => e.stopPropagation()}
>
tek durakta tam boya açılır, gövde kaydırılır (§4.19). İskelet
min-h-[70dvh] ile açılır ki veri gelince sayfa yalnız büyüsün,
konum sıçramasın (alt-sayfa.tsx ResizeObserver). */}
<AltSayfaIcerik onClick={(e) => e.stopPropagation()}>
<AltSayfaKapat 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 />
<span className="sr-only">Kapat</span>
@@ -189,9 +196,12 @@ export function ProgramAyrintiButonu({
.join(" · ")
: (universite ?? "Programın tüm açık verisi")}
</AltSayfaAciklama>
<div className="mt-5">
<AcilisSahnesi
hazir={durum === "hazir"}
className={cn("mt-5", durum !== "hazir" && "min-h-[70dvh]")}
>
<AyrintiIcerik durum={durum} veri={veri} />
</div>
</AcilisSahnesi>
</AltSayfaGovde>
</AltSayfaIcerik>
</AltSayfa>
@@ -215,11 +225,19 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
</p>
);
}
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo söner, bloklar
// yukarıdan aşağıya 0,08 s arayla netleşir.
return (
<>
{/* Logo kareleri: sahne sırasında görünür, sönünce kap sıfır boya iner. */}
<div data-logo className="relative mx-auto h-16 w-16" aria-hidden>
{LOGO_SLOTLARI.map((slot) => (
<span key={slot} data-logo-kare={slot} className="absolute" style={logoKareStili(slot)} />
))}
</div>
<Kunye veri={veri} />
<YillaraGore veri={veri} />
<KontenjanTurleriBolumu veri={veri} />
<div data-sonra>
<h3 className={BOLUM_BASLIK}>Son yerleşenin netleri</h3>
<p className={NOT}>
Bölüme en düşük puanla yerleşen adayın sınav netleri (YÖK Atlas).
@@ -227,10 +245,11 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
<div className="mt-3">
<NetlerIcerik netler={veri.netler} />
</div>
</div>
<Kosullar veri={veri} />
<Istihdam veri={veri} />
<UniversiteOzeti veri={veri} />
<p className="mt-7 border-t border-slate-100 pt-3 text-xs leading-5 text-slate-500">
<p data-sonra className="mt-7 border-t border-slate-100 pt-3 text-xs leading-5 text-slate-500">
Kaynaklar: YÖK Atlas (yerleştirme, kontenjan, netler, koşullar; {SON_YIL}{" "}
yerleştirmesi), TÜİK (mezun istihdamı), URAP ve YÖK İstatistik (üniversite).
</p>
@@ -270,7 +289,7 @@ function Kunye({ veri }: { veri: ProgramAyrintisi }) {
if (o?.ilce) haplar.push({ ikon: MapPin, metin: `${trBaslikDuzeni(o.ilce)} kampüsü` });
if (haplar.length === 0) return null;
return (
<ul className="flex flex-wrap justify-center gap-2">
<ul data-sonra className="flex flex-wrap justify-center gap-2">
{haplar.map((h) => (
<li key={h.metin} className={HAP}>
<h.ikon className={HAP_IKON} aria-hidden />
@@ -281,59 +300,6 @@ function Kunye({ veri }: { veri: ProgramAyrintisi }) {
);
}
function YillaraGore({ veri }: { veri: ProgramAyrintisi }) {
const satirlar = veri.gecmis.filter(
(g) => g.sira != null || g.puan != null || g.kontenjan != null || g.yerlesen != null,
);
if (satirlar.length === 0) return null;
const sonYil = satirlar[0].yil;
const hucre = (yil: number) =>
cn(
"py-2.5 text-right text-sm tabular-nums",
yil === sonYil ? "font-semibold text-slate-800" : "text-slate-500",
);
return (
<>
<h3 className={BOLUM_BASLIK}>Yıllara göre</h3>
<p className={NOT}>
Taban sırası (ve geniş ekranda puanı) o yıl yerleşen son adayınki;
kontenjan ve yerleşen o yılın yerleştirmesi. Sırası boş yıl: program o
yıl dolmadı ya da yoktu.
</p>
<Table className="mt-3">
<TableHeader>
<TableRow>
<TableHead className={cn(SATIR_BASLIK, "pl-0")}>Yıl</TableHead>
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Taban sıra</TableHead>
{/* Telefonda beş sütun 375 px'e sığmıyor (yatay kaydırma yok, §5.2.3): puan sm'den itibaren */}
<TableHead className={cn(SATIR_BASLIK, "hidden text-right sm:table-cell")}>Puan</TableHead>
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Kontenjan</TableHead>
<TableHead className={cn(SATIR_BASLIK, "text-right")}>Yerleşen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{satirlar.map((g) => (
<TableRow key={g.yil}>
<TableCell
className={cn(
"py-2.5 pl-0 text-sm tabular-nums",
g.yil === sonYil ? "font-semibold text-slate-900" : "text-slate-700",
)}
>
{g.yil}
</TableCell>
<TableCell className={hucre(g.yil)}>{g.sira != null ? sayi(g.sira) : "—"}</TableCell>
<TableCell className={cn(hucre(g.yil), "hidden sm:table-cell")}>{g.puan != null ? ondalik(g.puan) : "—"}</TableCell>
<TableCell className={hucre(g.yil)}>{g.kontenjan != null ? sayi(g.kontenjan) : "—"}</TableCell>
<TableCell className={hucre(g.yil)}>{g.yerlesen != null ? sayi(g.yerlesen) : "—"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}
function KontenjanTurleriBolumu({ veri }: { veri: ProgramAyrintisi }) {
const k = veri.kontenjanTurleri;
if (!k) return null;
@@ -341,7 +307,7 @@ function KontenjanTurleriBolumu({ veri }: { veri: ProgramAyrintisi }) {
if (satirlar.length === 0) return null;
const ozelVar = satirlar.some((t) => t !== "genel");
return (
<>
<div data-sonra>
<h3 className={BOLUM_BASLIK}>Kontenjan türleri ({k.yil})</h3>
<p className={NOT}>
{ozelVar
@@ -362,7 +328,7 @@ function KontenjanTurleriBolumu({ veri }: { veri: ProgramAyrintisi }) {
</div>
))}
</dl>
</>
</div>
);
}
@@ -476,7 +442,7 @@ function NetlerIcerik({ netler }: { netler: ProgramNetSatiri[] }) {
function Kosullar({ veri }: { veri: ProgramAyrintisi }) {
if (veri.kosullar.length === 0) return null;
return (
<details className="group mt-7">
<details data-sonra className="group mt-7">
<summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-3 font-heading text-base font-bold text-slate-900 transition-colors hover:text-orange-500 [&::-webkit-details-marker]:hidden">
Yerleşme koşulları ({veri.kosullar.length})
<span className="text-sm font-semibold text-slate-500 group-open:hidden">Göster</span>
@@ -509,13 +475,13 @@ function Istihdam({ veri }: { veri: ProgramAyrintisi }) {
if (parcalar.length === 0) return null;
const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : "";
return (
<>
<div data-sonra>
<h3 className={BOLUM_BASLIK}>Mezun istihdamı</h3>
<p className={NOT}>
{parcalar.join(", ")} (TÜİK {v.yil}, ülke geneli, {v.duzey === "lisans" ? "lisans" : "ön lisans"}{" "}
mezunları; üniversiteye göre değişir, kırılım yok).{ort}
</p>
</>
</div>
);
}
@@ -529,7 +495,7 @@ function UniversiteOzeti({ veri }: { veri: ProgramAyrintisi }) {
: null,
].filter(Boolean);
return (
<>
<div data-sonra>
<h3 className={BOLUM_BASLIK}>Üniversite</h3>
<p className={NOT}>
{parcalar.join(" · ")}.{" "}
@@ -540,6 +506,6 @@ function UniversiteOzeti({ veri }: { veri: ProgramAyrintisi }) {
Üniversite sayfası
</a>
</p>
</>
</div>
);
}