fix(ui): program ayrıntı çekmecesi — önbellekli açılışta sahne atlanır, sahne ≈1,2 s, istihdam cümlesi büyük harfle

Tasarımcı B1/B3/B5 (7 Eki). Çekmece ikinci kez açılınca AcilisSahnesi
hazir=true ile monte oluyor, logo kareleri sayfa ekrana kaymadan oynuyor,
gövde ~1 s boş kalıyordu: `hazir` verilmiş bileşende sahne yalnız
false → true geçişinde oynar; montajda true ise (önbellek) 0,25 s solma.
Verilmezse (akademik göstergeler, sunucudan hazır) her açılışta oynar.
Kartsız sahne süresi ≈2 s'den ≈1,2 s'ye: kare ve blok aralığı 0,05 s,
netleşme 0,35 s, logo belir/dur/sön 0,28/0,16/0,3 s. "Mezun istihdamı"
paragrafının ilk harfi büyük (tr-TR). DESIGN.md §4.19 gerçek sürelerle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-08 23:59:16 +03:00
parent fce7ab8c2e
commit 2ec9ecdc17
3 changed files with 62 additions and 26 deletions

View File

@@ -661,8 +661,14 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
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.
kaynak) 0,05 s arayla bulanıktan netleşir; toplam ≈ 1,2 s (kareler 0,1–0,2 s'de belirir,
0,94 s'de sönmüş olur; ilk blok ≈ 0,95 s'de, sonuncusu ≈ 1,3 s'de net — 8 Eki 2026, önceki
0,08'lik aralık ≈ 2 s tutuyordu). Sahne yalnız veri ilk geldiğinde (`hazir` false → true)
oynar; önbellekli yeniden açılışta sahne atlanır, içerik 0,25 s solmayla gelir — gövde hiçbir
an boş kalmaz (iskelet ya da içerik). Hareketi azalt: logo yok, 0,25 s solma (§6). Akademik
göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır (her açılışta oynar).
Gövde kaydırılınca içerik tutamaç ve çarpının altına maskesiz girmesin diye `AltSayfaGovde`
üst kenarında 20 px saydam → dolu `mask-image` var (§4.10; tüm çekmeceler).
### 4.20 Danışman sohbeti — `sohbet-client.tsx`, `arac-pilleri.tsx`
- **Soru formu hazırlanırken** (7 Eki 2026): `soru_sor` aracı tamamlanana dek cevap iskeleti değil

View File

@@ -10,7 +10,11 @@
// `[data-logo]` kabındaki üç `[data-logo-kare]` belirir, kısa durur, sonra
// sönerek kaybolur (scale 0.6 + opacity 0; Bilal, 7 Eki 2026) ve
// `[data-sonra]` bloklar netleşerek gelir.
// "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6).
// "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6). Veri
// bekleyen çekmecede (`hazir` verilmiş) sahne yalnız veri ilk geldiğinde
// (false → true) oynar; önbellekli yeniden açılışta aynı 0,25 s solma
// (8 Eki 2026: ikinci açılışta kareler çekmece ekrana kaymadan oynuyor,
// gövde ~1 s boş kalıyordu).
import { useLayoutEffect, useRef, type ReactNode } from "react";
import { animate, useReducedMotion } from "motion/react";
@@ -36,15 +40,25 @@ const ICERIK_BASI = 0.95;
const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const;
const YERLESME = [0.32, 0.72, 0, 1] as const;
// Kartsız mod: logo karesi belir → dur → sön. Belirme ve duruş kartlı
// modla aynı anlara denk gelir (KARE_SURE·0.24, ·0.52); sönme 0,4 s.
const LOGO_SURE = KARE_SURE * KARE_ZAMANLARI[2] + 0.4;
// Kartsız mod (saniye): logo karesi belir → dur → sön; kareler 0,05 s arayla
// gelir, içerik blokları sönmeyle çaprazlanarak 0,05 s arayla netleşir.
// Toplam ≈ 1,2 s: son logo karesi 0,2'de başlar, sönme 0,94'te biter, ilk
// blok ≈ 0,95'te net, 7–8 blokla sonuncusu ≈ 1,3'te (8 Eki 2026; önceki
// 0,08'lik aralıklarla ≈ 2 s sürüyor, DESIGN.md §4.19 "≈ 1 s" ile çelişiyordu).
const LOGO_BELIRME = 0.28;
const LOGO_DURUS = 0.16;
const LOGO_SONME = 0.3;
const LOGO_SURE = LOGO_BELIRME + LOGO_DURUS + LOGO_SONME;
const LOGO_ZAMANLARI = [
0,
(KARE_SURE * KARE_ZAMANLARI[1]) / LOGO_SURE,
(KARE_SURE * KARE_ZAMANLARI[2]) / LOGO_SURE,
LOGO_BELIRME / LOGO_SURE,
(LOGO_BELIRME + LOGO_DURUS) / LOGO_SURE,
1,
];
const KARTSIZ_SLOT_ADIMI = 0.05;
const KARTSIZ_ICERIK_BASI = 0.6;
const KARTSIZ_BLOK_ADIMI = 0.05;
const KARTSIZ_NETLESME = 0.35;
/** Kartsız modun logo kareleri, belirme sırasıyla (`data-logo-kare` değerleri). */
export const LOGO_SLOTLARI = SLOT_SIRASI;
@@ -80,13 +94,16 @@ export function logoKareStili(slot: Slot, kapBoyu = 64) {
* Kareler yalnız transform ile oynar (yerleşim ölçülür, bozulmaz); içerik o
* sırada görünmez olduğu için kare ölçeklenirken yazı ezilmez.
*
* `hazir`: false iken sahne oynamaz (iskelet görünür kalır), true olunca
* oynar — veri bekleyen çekmece için. Varsayılan true (içerik sunucudan
* hazır gelen çekmece).
* `hazir`: veri bekleyen çekmece için. false iken sahne oynamaz (iskelet
* görünür kalır); sahne yalnız false → true geçişinde oynar. Bileşen
* `hazir={true}` ile monte olursa veri önbellektedir (çekmece yeniden
* açıldı): sahne atlanır, içerik 0,25 s solmayla gelir — kareler o sırada
* çekmece daha ekrana kaymadan oynuyor, gövde boş kalıyordu. Verilmezse
* (içerik sunucudan hazır gelen çekmece) sahne her montajda oynar.
*/
export function AcilisSahnesi({
children,
hazir = true,
hazir,
className = "mt-5",
}: {
children: ReactNode;
@@ -95,12 +112,16 @@ export function AcilisSahnesi({
}) {
const kapRef = useRef<HTMLDivElement>(null);
const azHareket = useReducedMotion();
// Montajda veri hazır mıydı? (ref: Strict Mode'un çift effect'inde de
// aynı kalır; yeniden montajda — çekmece yeniden açılınca — yeniden ölçülür)
const onbellekli = useRef(hazir === true);
const oynat = hazir ?? true;
useLayoutEffect(() => {
const kap = kapRef.current;
// hazir false → true tek geçiş: sahne o geçişte bir kez oynar (ref
// tutulmaz; Strict Mode'un çift effect'i temizlik sonrası yeniden kurar).
if (!kap || !hazir) return;
if (!kap || !oynat) return;
const sahne = kap.querySelector<HTMLElement>("[data-sahne]");
const logo = kap.querySelector<HTMLElement>("[data-logo]");
const kareler = Array.from(
@@ -117,8 +138,10 @@ export function AcilisSahnesi({
const kartli = !!sahne && kareler.length > 0;
const kartsiz = !kartli && !!logo && logoKareleri.length > 0;
if (azHareket || (!kartli && !kartsiz)) {
// Logo kabı sahnesiz anlamsız: yer kaplamasın.
if (azHareket || onbellekli.current || (!kartli && !kartsiz)) {
// Sahnesiz yol (hareketi azalt, önbellekli açılış): içerik solarak
// gelir, gövde hiçbir an boş kalmaz. Logo kabı sahnesiz anlamsız: yer
// kaplamasın.
if (logo) {
logo.style.display = "none";
dokunulan.push(logo);
@@ -133,7 +156,7 @@ export function AcilisSahnesi({
}
// Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır.
const netlestir = (el: HTMLElement, gecikme: number) => {
const netlestir = (el: HTMLElement, gecikme: number, sure = 0.5) => {
el.style.opacity = "0";
dokunulan.push(el);
const k = animate(
@@ -143,7 +166,7 @@ export function AcilisSahnesi({
filter: ["blur(10px)", "blur(0px)"],
y: [10, 0],
},
{ duration: 0.5, delay: gecikme, ease: GUCLU_CIKIS },
{ duration: sure, delay: gecikme, ease: GUCLU_CIKIS },
);
k.then(() => {
el.style.filter = "";
@@ -168,11 +191,12 @@ export function AcilisSahnesi({
if (kartsiz) {
// Kartsız: logo kareleri belirir, durur, söner; kap sönerken sıfır boya
// iner ki altında boşluk kalmasın; içerik ICERIK_BASI'ndan itibaren gelir.
// iner ki altında boşluk kalmasın; içerik KARTSIZ_ICERIK_BASI'ndan
// itibaren, sönmeyle çaprazlanarak gelir.
let sonKare = 0;
logoKareleri.forEach((el) => {
const slot = (el.dataset.logoKare ?? "sol") as Slot;
const gecikme = BASLANGIC + SLOT_SIRASI.indexOf(slot) * SLOT_ADIMI;
const gecikme = BASLANGIC + SLOT_SIRASI.indexOf(slot) * KARTSIZ_SLOT_ADIMI;
sonKare = Math.max(sonKare, gecikme);
el.style.opacity = "0";
dokunulan.push(el);
@@ -197,14 +221,16 @@ export function AcilisSahnesi({
const kapat = animate(
logo!,
{ height: [logo!.offsetHeight, 0] },
{ duration: 0.4, delay: sonmeBasi, ease: YERLESME },
{ duration: LOGO_SONME, delay: sonmeBasi, ease: YERLESME },
);
kapat.then(() => {
logo!.style.display = "none";
});
kontroller.push(kapat);
sonra.forEach((el, i) => netlestir(el, ICERIK_BASI + i * 0.08));
sonra.forEach((el, i) =>
netlestir(el, KARTSIZ_ICERIK_BASI + i * KARTSIZ_BLOK_ADIMI, KARTSIZ_NETLESME),
);
return temizle;
}
@@ -298,7 +324,7 @@ export function AcilisSahnesi({
);
return temizle;
}, [azHareket, hazir]);
}, [azHareket, oynat]);
return (
<div ref={kapRef} className={className}>

View File

@@ -7,8 +7,9 @@
// 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).
// backlog). Açılış: veri ilk gelince logo kareleri belirir, söner, içerik
// bloklar hâlinde netleşir (AcilisSahnesi kartsız modu); veri önbellekteyken
// (çekmece yeniden açıldı) sahne yok, içerik 0,25 s solmayla gelir.
// 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
@@ -226,7 +227,7 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
);
}
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo söner, bloklar
// yukarıdan aşağıya 0,08 s arayla netleşir.
// yukarıdan aşağıya 0,05 s arayla netleşir.
return (
<>
{/* Logo kareleri: sahne sırasında görünür, sönünce kap sıfır boya iner. */}
@@ -473,12 +474,15 @@ function Istihdam({ veri }: { veri: ProgramAyrintisi }) {
v.kazanc ? `kazanç grubu ${v.kazanc.toLocaleLowerCase("tr-TR")}` : null,
].filter(Boolean);
if (parcalar.length === 0) return null;
// Cümle başı büyük (§10.4); hangi parçanın başa geldiği veriye bağlı.
const birlesik = parcalar.join(", ");
const cumle = birlesik.charAt(0).toLocaleUpperCase("tr-TR") + birlesik.slice(1);
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"}{" "}
{cumle} (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>