feat(v2): 9–10 Ekim çalışma ağacı — deneyim dili 1. dalga, PWA manifest, revizyon niyet modeli, listem/tadımlık bileşen denetimi

9–10 Ekim oturumlarının commit'lenmemiş işi tek parça alındı
(raporlar: docs/ekip-raporlari/2026-10-09, 2026-10-10):
- Landing deneyim dili 1. dalga: sayfa geçişi (ViewTransition), piksel morf/şerit,
  sorun sahneleri, adım akışı, sayan sayı, mobil menü, adım göstergesi, logoya tepki;
  DESIGN.md §11 ve AGENTS.md kuralları, tasarım denetimine yeni kurallar
- Tema: tema ikonu, D kısayolu, tema geçişi
- PWA: manifest.ts ve ikonlar
- Danışman: revizyon niyet modeli (OPENROUTER_NIYET_MODEL), sohbet geçmişi, soru API'si
- /listem ve /sonuc tadımlığı bileşen denetimi düzeltmeleri
- eslint: .venv-tts/ yok sayılır

tsc, eslint ve tasarım denetimi sıfır hata.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-11 00:34:29 +03:00
parent da0e9dff7a
commit aec8a2ad76
80 changed files with 4134 additions and 654 deletions

View File

@@ -43,6 +43,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import { SayiRulo } from "@/components/ui/sayi-rulo";
import { ARAMA_AC_OLAYI } from "@/components/mobil-menu";
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
type BolumSonucu = {
@@ -172,6 +174,13 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
// modal her açılıp kapandığında remove/add döngüsü dönmesin
// (client-event-listeners)
const kisayolTetiklendi = useEffectEvent(() => modalDegisti(!acik));
// Telefon menüsündeki "Ara" hapı (mobil-menu.tsx) pencereyi olayla açtırır.
const aramaIstendi = useEffectEvent(() => modalDegisti(true));
useEffect(() => {
window.addEventListener(ARAMA_AC_OLAYI, aramaIstendi);
return () => window.removeEventListener(ARAMA_AC_OLAYI, aramaIstendi);
}, []);
useEffect(() => {
function kisayolDinle(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
@@ -279,9 +288,7 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
>
<Trophy className="size-4 shrink-0 text-orange-500" aria-hidden />
<span className="font-semibold tracking-tight text-slate-900">
<span className="tabular-nums">
{profil.sira.toLocaleString("tr-TR")}
</span>
<SayiRulo deger={profil.sira} className="tabular-nums" />
<span className="font-medium text-slate-500">
{" "}
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
@@ -331,58 +338,42 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
</button>
</nav>
{/* lg altında 2. satır: sıralama + ara (header flex-wrap + order-3) */}
<div className="order-3 flex w-full basis-full items-center gap-1.5 lg:hidden">
{profil ? (
<button
type="button"
onClick={profilDuzenlemeyiAc}
onPointerEnter={preloadTercihProfiliKapisi}
onFocus={preloadTercihProfiliKapisi}
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-card py-1 pl-2.5 pr-1.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-2 sm:text-sm"
>
<Trophy
className="size-3.5 shrink-0 text-orange-500 sm:size-4"
aria-hidden
/>
<span className="truncate tracking-tight">
<span className="tabular-nums">
{profil.sira.toLocaleString("tr-TR")}
</span>
<span className="font-medium text-slate-500">
{" "}
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
</span>
</span>
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">
<SquarePen className="size-3" aria-hidden />
Değiştir
</span>
</button>
) : (
<button
type="button"
onClick={siralamaGirVeyaSonucaGit}
onPointerEnter={preloadTercihProfiliKapisi}
onFocus={preloadTercihProfiliKapisi}
aria-label="Başarı sıralamanı gir"
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-card py-1 pl-2.5 pr-2.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-3 sm:text-sm"
>
<Trophy
className="size-3.5 shrink-0 text-orange-500 sm:size-4"
aria-hidden
/>
<span className="truncate">Sıralama gir</span>
</button>
)}
{/* lg altı: tek satırın ortasındaki sıralama hapı (DESIGN.md §4.36).
Arama ve geri kalan her şey sağdaki baloncuk menüde. Sayfa başlığı
çubuğa çıkınca (NavBaslik) hap yalnız kupaya iner, başlığa yer açar. */}
<div className="order-2 flex shrink-0 items-center lg:hidden">
<button
type="button"
onClick={() => modalDegisti(true)}
aria-label="Bölüm, üniversite veya rehber ara"
className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-card text-slate-700 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:size-12"
onClick={profil ? profilDuzenlemeyiAc : siralamaGirVeyaSonucaGit}
onPointerEnter={preloadTercihProfiliKapisi}
onFocus={preloadTercihProfiliKapisi}
aria-label={
profil
? `Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`
: "Başarı sıralamanı gir"
}
className="flex h-9 max-w-40 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-card px-2.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] motion-reduce:transition-none sm:h-12 sm:max-w-none sm:gap-2 sm:px-4 sm:text-sm"
>
<Search className="size-4" aria-hidden />
<Trophy className="size-3.5 shrink-0 text-orange-500 sm:size-4" aria-hidden />
<span className="truncate tracking-tight in-data-[nav-baslik]:hidden">
{profil ? (
<>
<SayiRulo deger={profil.sira} className="tabular-nums" />
<span className="hidden font-medium text-slate-500 sm:inline">
{" "}
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
</span>
</>
) : (
"Sıralama gir"
)}
</span>
{profil ? (
<SquarePen
className="size-3 shrink-0 text-slate-400 in-data-[nav-baslik]:hidden"
aria-hidden
/>
) : null}
</button>
</div>

View File

@@ -55,6 +55,7 @@ import {
AcilisSahnesi,
LOGO_SLOTLARI,
logoKareStili,
LOGO_KAP_BOYU,
} from "@/components/ui/acilis-sahnesi";
import {
Table,
@@ -183,7 +184,15 @@ export function ProgramAyrintiButonu({
<span className="sr-only">Kapat</span>
</AltSayfaKapat>
<AltSayfaGovde className="px-4 pt-3 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6">
<AltSayfaBaslik className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
{/* Başlık veri gelene dek görünmez: logo kareleri küçülüp buraya
uçar, yazı onlardan netleşir (AcilisSahnesi `data-logo-hedef`). */}
<AltSayfaBaslik
data-logo-hedef
className={cn(
"px-10 text-center font-heading text-2xl font-bold text-slate-900",
(durum === "bekliyor" || durum === "yukleniyor") && "opacity-0",
)}
>
{isim}
</AltSayfaBaslik>
<AltSayfaAciklama className="mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500">
@@ -226,14 +235,20 @@ 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,05 s arayla netleşir.
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo küçülerek başlığa
// dönüşür, bloklar 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. */}
<div data-logo className="relative mx-auto h-16 w-16" aria-hidden>
{/* Logo kareleri: sahne sırasında görünür, küçülerek başlığa uçar; kap
sıfır boya iner. */}
<div data-logo className="relative mx-auto size-44" aria-hidden>
{LOGO_SLOTLARI.map((slot) => (
<span key={slot} data-logo-kare={slot} className="absolute" style={logoKareStili(slot)} />
<span
key={slot}
data-logo-kare={slot}
className="absolute"
style={logoKareStili(slot, LOGO_KAP_BOYU)}
/>
))}
</div>
<Kunye veri={veri} />