feat(tasarim): form alanları hap, CTA bandı tek kaynak, masonry piksel zemin, çekmeceler §4.10

- Input/Select/Textarea: shadcn'in ince varsayılanı yerine üst çubuk
  haplarıyla aynı kalıp (h-12, rounded-full, slate-200, 16 px; md:text-sm
  yok); kullanım yerleri yalnız yerleşim sınıfı taşır. DESIGN.md §4.6
  tam sınıf dizisi; denetime ince-form-alani (hata) ve ham-input (uyarı)
- CtaBandi (mavi zemin + piksel doku) tek kaynak: /sonuc iki panel, katalog
  altı kart, "Kendi Listem" çekmecesi altı. Ana sayfa kapanışı koyu
  PikselCtaYuzeyi + 24'lük piksel liste kreatifiyle kalır (Bilal: "orası
  bayağı iyiydi"); yazılıp silinen satır kaldırıldı
- PixelMasonry: CTA arka planları büyüklü küçüklü, hafif kaymış kareler;
  eşit ızgaralı PixelField kaldırıldı
- Çekmece başlığındaki 12×2 piksel liste kaldırıldı (çok dolu); sayı +
  ilerleme çubuğu + risk dağılımı geri geldi
- "Saniyede gör" çekmecesi §4.10 düzeninde: ortalı DrawerTitle + bağlam,
  rozet ve çizgili "neler alacaksın" listesi yok; kapı modalındaki iki
  rozet kalktı; denetime modal-rozet (hata)
- Kapı panelinde 3 px odak halkası kırpılmıyor (px-px → p-1)
- Liste satırı: üniversite adı + tür/il/süre adın altında tek akan satır,
  telefonda text-xs (SATIR_ALT_GRUP); katalog listelerinde ad kendi
  satırında (SATIR_GRUP space-y-2). Ölçüm 375 px canlı bölüm sayfası:
  satır yüksekliğini metin değil alt alta iki 44 px düğme belirliyor

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 12:08:24 +03:00
parent b8b7925ba9
commit 2099f20ed1
19 changed files with 516 additions and 295 deletions

View File

@@ -3,14 +3,7 @@
import { useId, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
ArrowRight,
ChartNoAxesCombined,
Database,
ListChecks,
Trophy,
X,
} from "lucide-react";
import { ArrowRight, Trophy, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -20,7 +13,7 @@ import {
DrawerDescription,
DrawerTitle,
} from "@/components/ui/drawer";
import { PagePixelDivider, SectionEyebrow } from "@/components/pixel-decor";
import { PagePixelDivider } from "@/components/pixel-decor";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import {
SihirbazAdimlarLazy,
@@ -29,7 +22,7 @@ import {
import { authClient } from "@/lib/auth-client";
import { olay, siraKovasi } from "@/lib/analitik";
import { cn } from "@/lib/utils";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { CtaBandi } from "@/components/cta-bandi";
import { onbellekliJsonGetir } from "@/lib/istek-cache";
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
import type { SihirbazFacetleri } from "@/types/yokatlas";
@@ -45,13 +38,6 @@ const PUAN_TURLERI_SECENEK = [
{ deger: "tyt", etiket: "TYT" },
] as const;
// Kanıt üçlüsü arama funnel kartıyla aynı dili konuşur (bkz. arama-funnel-karti)
const KANITLAR = [
{ ikon: ListChecks, etiket: "24 tercihlik plan" },
{ ikon: ChartNoAxesCombined, etiket: "Risk analizi" },
{ ikon: Database, etiket: "YÖK Atlas verisi" },
] as const;
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
// listem-bos-cta.tsx): büyük turuncu buton, piksel ayraç. Sınıf seti tek yerde.
const PROFILLI_CTA_SINIF =
@@ -65,22 +51,22 @@ function turEtiketi(tur: string) {
function KayitliSira({
sira,
tur,
koyu = false,
beyaz = false,
}: {
sira: number;
tur: string;
/** Koyu funnel yüzeyinde (PikselCtaYuzeyi) */
koyu?: boolean;
/** Mavi CTA bandının üstünde (CtaBandi): beyaz yazı */
beyaz?: boolean;
}) {
return (
<span
className={cn(
"inline-flex items-center gap-1.5 text-sm",
koyu ? "text-slate-300" : "text-slate-600",
beyaz ? "text-white/90" : "text-slate-600",
)}
>
<Trophy
className={cn("size-4 shrink-0", koyu ? "text-orange-500" : "text-primary")}
className={cn("size-4 shrink-0", beyaz ? "text-white" : "text-primary")}
aria-hidden
/>
<span>
@@ -88,7 +74,7 @@ function KayitliSira({
<strong
className={cn(
"font-semibold tabular-nums",
koyu ? "text-white" : "text-slate-900",
beyaz ? "text-white" : "text-slate-900",
)}
>
{sira.toLocaleString("tr-TR")}.
@@ -278,7 +264,7 @@ export function CtaSiraForm({
>
<div className="relative sm:flex-1">
<Trophy
className="pointer-events-none absolute left-4 top-1/2 size-4 -translate-y-1/2 text-slate-400"
className="pointer-events-none absolute left-5 top-1/2 size-4 -translate-y-1/2 text-slate-400"
aria-hidden
/>
<Input
@@ -291,7 +277,7 @@ export function CtaSiraForm({
onFocus={preloadSihirbazAdimlar}
aria-invalid={hata ? true : undefined}
aria-describedby={hata ? hataId : undefined}
className="h-12 bg-background pl-11 text-base"
className="pl-12"
/>
</div>
<Button
@@ -319,20 +305,16 @@ export function CtaSiraForm({
);
// Çekmece iki varyantta da aynıdır; kompakt satırda sıra adımı sayfada
// olduğundan çekmece yalnız sihirbazı taşır.
// olduğundan çekmece yalnız sihirbazı taşır. Sıra adımında başlık kanonik
// alt sayfa düzeninde (DESIGN.md §4.10: ortalı başlık + tek cümle bağlam,
// rozet yok); sihirbaz adımlarında başlıklar adımların kendisinde, çekmece
// başlığı yalnız ekran okuyucuya.
const siraAdimi = !kompakt && adim === "sira";
const cekmece = (
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
<DrawerContent
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
>
{/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */}
<DrawerTitle className="sr-only">
Sıralamana yakın programlar hangileri?
</DrawerTitle>
<DrawerDescription className="sr-only">
YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan
ayrılmadan gör.
</DrawerDescription>
<DrawerClose 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>
@@ -341,37 +323,28 @@ export function CtaSiraForm({
<div
className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"
>
{!kompakt && adim === "sira" ? (
<div className="flex flex-col gap-5">
<div>
<SectionEyebrow>Saniyede gör</SectionEyebrow>
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900">
Sıralamana yakın programlar hangileri?
</h2>
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600">
Puan türünü seç, başarı sıralamanı yaz; hayal, dengeli ve
güvenli dilimlerindeki programları bu sayfadan ayrılmadan
gör. Ücretsiz, üyelik gerekmez.
</p>
</div>
{siraFormu}
<ul
aria-label="Neler alacaksın"
className="flex flex-wrap gap-x-4 gap-y-2 border-t border-slate-100 pt-4"
>
{KANITLAR.map(({ ikon: Ikon, etiket }) => (
<li
key={etiket}
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-600"
>
<Ikon className="size-3.5 text-primary" aria-hidden />
{etiket}
</li>
))}
</ul>
</div>
<DrawerTitle
className={
siraAdimi
? "px-10 text-center font-heading text-2xl font-bold text-slate-900"
: "sr-only"
}
>
Sıralamana yakın programlar hangileri?
</DrawerTitle>
<DrawerDescription
className={
siraAdimi
? "mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500"
: "sr-only"
}
>
Puan türünü seç, sıralamanı yaz; hayal, dengeli ve güvenli
dilimlerindeki programları gerçek YÖK Atlas verisiyle bu sayfadan
ayrılmadan gör. Ücretsiz, üyelik gerekmez.
</DrawerDescription>
{siraAdimi ? (
<div className="mt-5">{siraFormu}</div>
) : facetler && sira != null ? (
<SihirbazAdimlarLazy
key={`${sira}-${tur}`}
@@ -447,18 +420,17 @@ export function CtaSiraForm({
);
}
// Kart varyantı sayfa sonundaki funnel CTA'sıdır: arama kartının koyu
// yüzeyi + 24'lük piksel liste (DESIGN.md §10.8).
// Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi).
if (profil) {
return (
<PikselCtaYuzeyi metinOfseti={5}>
<CtaBandi seed={5}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2>
<div className="mt-3">
<KayitliSira sira={profil.sira} tur={profil.tur} koyu />
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz />
</div>
<p className="mt-2 max-w-xl text-base leading-relaxed text-slate-300">
<p className="mx-auto mt-2 max-w-xl text-base leading-relaxed text-white/90">
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
programların hazır.
</p>
@@ -472,17 +444,17 @@ export function CtaSiraForm({
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
</PikselCtaYuzeyi>
</CtaBandi>
);
}
return (
<div ref={formRef}>
<PikselCtaYuzeyi metinOfseti={9}>
<CtaBandi seed={9}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2>
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
<p className="mx-auto mt-3 max-w-xl text-base leading-relaxed text-white/90">
{aciklama ??
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
</p>
@@ -497,7 +469,7 @@ export function CtaSiraForm({
Sıralamanı gir, programları gör
<ArrowRight className="size-4" aria-hidden />
</Button>
</PikselCtaYuzeyi>
</CtaBandi>
{cekmece}
</div>
);