diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index b8be510..9cfaeb5 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -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 diff --git a/src/components/ui/acilis-sahnesi.tsx b/src/components/ui/acilis-sahnesi.tsx index 34e91c5..31efb96 100644 --- a/src/components/ui/acilis-sahnesi.tsx +++ b/src/components/ui/acilis-sahnesi.tsx @@ -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(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("[data-sahne]"); const logo = kap.querySelector("[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 (
diff --git a/src/features/katalog/components/program-ayrinti.tsx b/src/features/katalog/components/program-ayrinti.tsx index 9f8e439..8c3cacd 100644 --- a/src/features/katalog/components/program-ayrinti.tsx +++ b/src/features/katalog/components/program-ayrinti.tsx @@ -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 (

Mezun istihdamı

- {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}