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

@@ -49,7 +49,7 @@ ve üniversite sayfası. Bu hissi veren şeyler, ekrandan bağımsız:
### 1.2 Genel kurallar ### 1.2 Genel kurallar
- Zemin her zaman beyaz; koyu zemin yalnızca CTA bandı ve çekmece başlığı gibi tek bir - Zemin her zaman beyaz; koyu zemin yalnızca CTA bandı ve çekmece başlığı gibi tek bir
vurgu yüzeyinde (`PixelField` ile). vurgu yüzeyinde (`PixelMasonry` ile: büyüklü küçüklü, hafif kaymış kareler).
- İmza dekoratiftir ama rastgele değildir: deterministik ızgara (hücre 10, nokta 7.4, rx 2), - İmza dekoratiftir ama rastgele değildir: deterministik ızgara (hücre 10, nokta 7.4, rx 2),
imleç yaklaşınca turuncuya ısınan pikseller (`usePixelHeat`), uzaklaşınca yavaş soğuma. imleç yaklaşınca turuncuya ısınan pikseller (`usePixelHeat`), uzaklaşınca yavaş soğuma.
Piksel **veri taşıyorsa** (ör. `PixelSira`) kalır; iki bölüm arasına **süs olarak** konmuş Piksel **veri taşıyorsa** (ör. `PixelSira`) kalır; iki bölüm arasına **süs olarak** konmuş
@@ -200,7 +200,8 @@ bir dosyaya (`src/components/…`) çıkarılıp iki yer de oradan kullanır.
``` ```
Sınıfı: `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary` + başta `PixelMark`. Sınıfı: `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary` + başta `PixelMark`.
`@/components/ui/badge` **kullanılmaz**; elle pill span yazılmaz. Filtre çipleri, nav butonları `@/components/ui/badge` **kullanılmaz**; elle pill span yazılmaz. Filtre çipleri, nav butonları
ve kimlik hapları (§4.12) bu kuralın dışındadır. ve kimlik hapları (§4.12) bu kuralın dışındadır. Rozet **sayfa** bölüm başlıklarının üstündedir;
çekmece ve modal başlıklarında kullanılmaz (§4.10, Bilal 24 Eyl 2026).
### 4.3 Modal kapatma çarpısı — `DialogContent` varsayılanı, `src/components/ui/dialog.tsx` ### 4.3 Modal kapatma çarpısı — `DialogContent` varsayılanı, `src/components/ui/dialog.tsx`
`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]` + `<XIcon className="size-4" aria-hidden />` + `sr-only "Kapat"`. `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]` + `<XIcon className="size-4" aria-hidden />` + `sr-only "Kapat"`.
@@ -217,8 +218,36 @@ Alt sayfada (`Drawer`) aynı sınıf seti `DrawerClose` üstünde kullanılır (
Tablolar, çekmece ve rapor listesi `<RiskEtiketi risk={…} />` kullanır; elle yazılmaz. Tablolar, çekmece ve rapor listesi `<RiskEtiketi risk={…} />` kullanır; elle yazılmaz.
Nokta + metin: `<span className="size-2 rounded-full bg-emerald-500" />` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla. Nokta + metin: `<span className="size-2 rounded-full bg-emerald-500" />` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla.
### 4.6 Form ### 4.6 Form alanı — `Input`, `Select`, `Textarea` (`src/components/ui/`)
shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı alanın altında, ne olduğunu ve ne yapılacağını söyler. Sayısal alanlarda uç değerler (0, negatif, 1.000.000 üstü) ele alınır. Form alanı da bir **hap**tır: üst çubuktaki arama/kullanıcı haplarıyla aynı kalıp (48 px, tam
yuvarlak, slate-200 kenarlık, 16 px yazı). shadcn'in ince varsayılanı (`h-8 rounded-lg
border-input md:text-sm`) bu projede yoktur; temel bileşenler 24 Eyl 2026'da bu dile çevrildi.
- **Metin alanı (`Input`)**: `h-12 w-full rounded-full border border-slate-200 bg-white px-5 text-base text-slate-900 placeholder:text-slate-400`;
hover `border-slate-300`; odak `border-primary ring-3 ring-ring/50`; hata (`aria-invalid`)
`border-red-500 ring-3 ring-red-500/20`; devre dışı `bg-slate-50 opacity-50`. `md:text-sm` yok — gövde
16 px altına inmez (§3) ve iOS odakta yakınlaştırmaz. Kullanım yerinde yükseklik/köşe/zemin
**yazılmaz** (`className` yalnız yerleşim için: `sm:flex-1`, `pl-12`). Tek istisna hero: telefonda
bir kademe iri, `h-14 text-lg sm:h-12 sm:text-base`.
- **Başta ikon** (sıralama alanındaki kupa): kapsayıcı `relative`, ikon
`pointer-events-none absolute left-5 top-1/2 size-4 -translate-y-1/2 text-slate-400`, alan `pl-12`.
- **Seçim (`SelectTrigger`)**: aynı hap — `h-12 pl-5 pr-4 text-base`, ok `text-slate-400`;
`size="sm"` → `h-9 px-4 text-sm` (yalnız masaüstü tablo aksiyonu, §8). Açılan liste
`rounded-2xl border border-slate-200 bg-white p-1.5 shadow-lg`; satır `rounded-full py-2 pl-4 pr-9 text-base`,
klavye/odak `bg-slate-100`, seçili işareti `text-primary`.
- **Çok satır (`Textarea`)**: hap değil sade kutu (§4.4): `min-h-24 rounded-2xl border border-slate-200 bg-white px-5 py-3 text-base`;
hover/odak/hata metin alanıyla aynı. Sohbet kutusu (`prompt-input.tsx`) kenarlıksız ve şeffaf
üstüne yazar; tek istisna.
- **Kenarlıksız ham `<input>`** yalnız kendi hap kabının içinde: il arama
(`sihirbaz-adimlar.tsx`, `h-11 rounded-full border-slate-200` kap) ve katalog arama modalı
(üst şeritte `h-16`). Kenarlıklı ham input yazılmaz; `Input` kullanılır.
- Etiket her alanda: görünür `text-sm font-semibold text-slate-700` ya da `sr-only`. Hata
mesajı alanın altında `role="alert"` + `text-sm font-medium text-red-600`; ne olduğunu ve ne
yapılacağını söyler (§10.4). Sayısal alanlarda uç değerler (0, negatif, 1.000.000 üstü) ele alınır.
- Alan + buton yan yana: `flex flex-col gap-3 sm:flex-row`; buton `h-12` (§4.1 `size="lg"`),
alan `sm:flex-1`. Telefonda alt alta, ikisi de tam genişlik.
- Denetim: `ince-form-alani` (hata) — `Input`/`SelectTrigger`/`Textarea` üstüne `h-7`–`h-10`,
`rounded-sm/md/lg/xl`, `bg-background`/`border-input` yazılmış; `ham-input` (uyarı) —
kenarlıklı ham `<input>`.
### 4.7 İkonlar ### 4.7 İkonlar
Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz. Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz.
@@ -282,6 +311,12 @@ açılan modal değil. Çekmece kabı ve çarpı `cta-sira-form.tsx`'teki çekme
düğmesi `size-11` **çerçevesiz `CircleMinus`** — çarpı yalnız çekmeceyi kapatır (Bilal, 25 Eyl düğmesi `size-11` **çerçevesiz `CircleMinus`** — çarpı yalnız çekmeceyi kapatır (Bilal, 25 Eyl
2026: "2 çarpı var"). Listeler uzun olduğu için bu çekmece kaydırılır (tek ekran kuralının 2026: "2 çarpı var"). Listeler uzun olduğu için bu çekmece kaydırılır (tek ekran kuralının
tek istisnası); alt bandı telefonda alt alta dizilir ki listeye en az ~280 px kalsın. tek istisnası); alt bandı telefonda alt alta dizilir ki listeye en az ~280 px kalsın.
- **Rozet yok, ek liste yok** (Bilal, 24 Eyl 2026: "buralarda badge kullanma"): çekmece ve
sihirbaz modallarının başlığı üstünde `SectionEyebrow` durmaz; başlık + tek cümle bağlam yeter.
"Saniyede gör" çekmecesi (`cta-sira-form.tsx`) bu düzene çevrildi: `DrawerTitle` ortalı,
`DrawerDescription` bağlam, altında form; "neler alacaksın" çizgili ikon listesi kaldırıldı
(bağlam cümlesi zaten söylüyor). Sihirbaz adımlarında çekmece başlığı `sr-only`, görünür
başlık adımın kendisinde. Denetim: `modal-rozet` (hata).
- **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken - **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken
çizilmez). Google'ın görmesi gereken metin ve **zorunlu atıflar** (ör. URAP sırası + payda + çizilmez). Google'ın görmesi gereken metin ve **zorunlu atıflar** (ör. URAP sırası + payda +
dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır. dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır.
@@ -357,6 +392,15 @@ tablosu bu sabitleri kullanır, sınıfı elle yazmaz.
- Telefonda yalnız ad + satır butonları sütun olarak kalır; sayısal sütunlar (taban sıra vb.) - Telefonda yalnız ad + satır butonları sütun olarak kalır; sayısal sütunlar (taban sıra vb.)
adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden` adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden`
içinde değer `font-semibold text-slate-900`. içinde değer `font-semibold text-slate-900`.
- **Üniversite alt satırı tek akan satır, telefonda bir kademe küçük** (Bilal, 24 Eyl 2026:
"mobilde çok şişko, her şey iç içe; üni adını daha küçük yazabiliriz"): adın altında üniversite
adı (`SATIR_ALT_AD`) ile tür çipi/il/süre (`SATIR_META`) `SATIR_ALT_GRUP` içinde her
genişlikte yan yana akar, sığmazsa sarar; telefonda `text-xs`, sm'den `text-sm`. `/sonuc`,
rapor listesi, tadımlık satırı ve çekmece satırı (`liste-cekmecesi.tsx`) böyledir. Bu, §3
"text-xs yalnız ince yazıda" kuralının bilinçli istisnasıdır (12 px tabanının üstünde).
Katalog listelerinde (ad zaten üniversite/bölüm) `SATIR_GRUP`: telefonda ad kendi satırında,
çipler 8 px altında; sm'den aynı satırda. Ölçüm 375 px, bölüm sayfası: satır 129 px ve
yüksekliği metin değil, alt alta iki 44 px düğme belirliyor — metin küçültmek orayı kısaltmaz.
- Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`. - Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`.
- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. Telefonda - Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. Telefonda
iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun
@@ -467,6 +511,7 @@ Hareket kuralları (Apple, `motion/react`):
| Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi | | Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi |
| Beyaz zemin, tek koyu vurgu yüzeyi | İstenmemiş karanlık mod, `dark:` sınıfı | | Beyaz zemin, tek koyu vurgu yüzeyi | İstenmemiş karanlık mod, `dark:` sınıfı |
| Kapatma çarpısı `DialogContent` varsayılanı | Başka boyut/renkte çarpı | | Kapatma çarpısı `DialogContent` varsayılanı | Başka boyut/renkte çarpı |
| Form alanı hap: `Input`/`SelectTrigger` varsayılanı (`h-12 rounded-full`), yalnız yerleşim sınıfı eklenir | Kullanım yerinde `h-9 rounded-lg bg-background`; kenarlıklı ham `<input>`; `md:text-sm` |
## 8. Duyarlılık ## 8. Duyarlılık
@@ -518,7 +563,7 @@ Bu dilde sorunların çoğu koddan görünmez (taşma, sığma, hizalanma). Tasa
### 9.4 Site taraması için denetim uyarıları ### 9.4 Site taraması için denetim uyarıları
`pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm `pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm
listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide), listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide),
`mavi-hover` (§2.1), `iri-sayi-tabular` (Cal Sans + tabular-nums). `mavi-hover` (§2.1), `iri-sayi-tabular` (Cal Sans + tabular-nums), `ham-input` (§4.6 kenarlıklı ham input).
`mavi-hover` §4.15 kanonik liste satırı adını (`text-base font-semibold leading-snug` + `mavi-hover` §4.15 kanonik liste satırı adını (`text-base font-semibold leading-snug` +
`hover:text-primary hover:underline` aynı satırda) kendiliğinden atlar — istisna uyarı `hover:text-primary hover:underline` aynı satırda) kendiliğinden atlar — istisna uyarı
vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyarır; önce §4.15'e vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyarır; önce §4.15'e
@@ -590,22 +635,76 @@ Seri (streak), kayıp korkusu, puan/XP, rozet koleksiyonu, liderlik tablosu, mas
aciliyet/kıtlık, utandırarak onay. Tercih günlük alışkanlık değil, tek seferlik kaygılı bir aciliyet/kıtlık, utandırarak onay. Tercih günlük alışkanlık değil, tek seferlik kaygılı bir
karar. karar.
### 10.8 Funnel CTA yüzeyi ve 24'lük piksel liste ### 10.8 CTA bandı — `CtaBandi`, `src/components/cta-bandi.tsx`
Bilal'in isteği (25 Eyl 2026): "CTA alanlarını arama içindeki alandan ilham, piksel detaylı yap;
pikseller bir kümede kümeleşmesin, interaktif kreatif olsun." **Kanonik CTA bandı — mavi zemin + `PixelMasonry`** (Bilal'in kararı, 24 Eyl 2026: "Kendi Listem"
- **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki çekmecesindeki bandın eski hâli daha güzeldi; örnek odur). Kabuk tek kaynaktan gelir, içerik
kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp çağıranındır:
silinen satırlar (`FUNNEL_METINLERI`, `src/lib/funnel-metinleri.ts`), tek turuncu CTA.
Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli, katalog ```tsx
sayfalarının altındaki `CtaSiraForm` kart varyantı (üstteki kompakt satır form olduğu için <CtaBandi seed={8}> {/* buyuk: ortalı, px-5 py-8 sm:p-12 */}
açık zeminde kalır). Eski <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">…</h2>
`bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz. <p className="mx-auto mt-3 max-w-xl text-base leading-relaxed text-white/90">…</p>
<Button size="lg" className="mt-6 w-full cursor-pointer bg-orange-500 text-white hover:bg-orange-600 active:scale-[0.97] sm:w-auto">
Yapay Zeka listemi kur<ArrowRight className="size-4" aria-hidden />
</Button>
</CtaBandi>
<CtaBandi etiket="div" boyut="kompakt" className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
…başlık + alt satır solda, buton sağda (çekmece)…
</CtaBandi>
```
Çekmecedeki kompakt bandın açık hâli (bileşenin ürettiği sınıflar):
```tsx
<div className="relative isolate flex flex-col gap-3 overflow-hidden rounded-2xl bg-primary px-5 py-4 text-left text-white sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:py-5">
<div aria-hidden className="pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white">
<PixelMasonry seed={8} />
</div>
<div className="min-w-0">
<p className="font-heading text-xl font-bold sm:text-2xl">Risk analizini biz yaptık</p>
<p className="mt-1 hidden truncate text-sm leading-relaxed text-white sm:block">
Her seçimin yerleşme riski hazır — son kararı sen ver.
</p>
</div>
<Button size="lg" className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto">
Listemde aç
<ArrowRight className="size-4" aria-hidden />
</Button>
</div>
```
- Zemin `bg-primary` (sitenin mavisi), köşe `rounded-2xl`, yazı beyaz; alt satır da beyaz
(`text-white`, `slate-300` değil). Piksel dokusu `PixelMasonry` (Bilal, 24 Eyl 2026: "arka
planda büyüklü küçüklü, masonry şeklinde"): 1–3 hücrelik kareler 3×3'lük makro hücrelere
kaymış düşer, çakışmaz ama hizaya girmez; orta bant (%22–78) boş, kenarlara doğru yoğun; iri
kare daha silik; imleç yaklaşınca turuncuya ısınır (`usePixelHeat`). Kap `relative isolate
overflow-hidden`, doku `-inset-y-6 -z-10`. Eşit ızgaralı eski `PixelField` kaldırıldı.
- Tek turuncu buton, metin önce ok sonda (§4.1). Telefonda alt alta ve tam genişlik; `sm`'de
yan yana, buton sağda.
- Büyük (sayfa kapanışı) hâlinde aynı band ortalı ve iri: `px-5 py-8 text-center sm:p-12`,
`-inset-y-10`, h2 `font-heading text-2xl font-bold sm:text-3xl`, paragraf `text-white/90`.
- Kullanıldığı yerler: `/sonuc`'taki iki sihirbaz paneli, katalog altındaki `CtaSiraForm`
kart varyantı, "Kendi Listem" çekmecesinin altı (kompakt). Aynı sayfadaki iki band farklı
`seed` alır. Band üstündeki ikincil metin `text-white/90`, ince yazı `text-white/70`;
`slate-300`/`slate-400` yazılmaz.
**Kapanış yüzeyi — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx` (yalnız ana
sayfa kapanışı "Tercih listen, pişmanlık listesi olmasın").** Bilal'in kararı (24 Eyl 2026):
"orası bayağı iyiydi; özellikle 24'lük pikseller tablo gibi — bu tür kreatif, interaktif şeyler
çok güzel." Arama penceresindeki kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950
text-white`, arka planda `PixelMasonry` (seed ile), tek turuncu CTA. **Yazılıp silinen satır
yok** (Bilal, 24 Eyl 2026: kaldırıldı); `FUNNEL_METINLERI` yalnız arama penceresindeki kartta
(`arama-funnel-karti.tsx`) kalır.
- **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun - **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun
nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13 nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13
dengeli · 6 güvenli, 110 ms arayla), yalnız ekrandayken. İmleç yakın kareleri büyütür; kart dengeli · 6 güvenli, 110 ms arayla), yalnız ekrandayken. İmleç yakın kareleri büyütür; kart
içindeki düğmeye imleç/odak gelince 24 kare birden dolar; dokunmada basılan kare ve komşuları içindeki düğmeye imleç/odak gelince 24 kare birden dolar; dokunmada basılan kare ve komşuları
nabız atar (`kt-piksel-nabiz`). Hareketi azalt: dolu, durağan. Renk her zaman nabız atar (`kt-piksel-nabiz`). Hareketi azalt: dolu, durağan. Renk her zaman
`PikselListeAciklama` metniyle. `PikselListeAciklama` metniyle.
- **Gerçek liste:** çekmecede ilerleme çubuğunun yerinde öğrencinin kendi listesi (`gercek`, - Bu yüzey **sayfada bir kez** ve yalnız kapanışta durur; ara paneller ve çekmece mavi banttır.
12×2): her kare o tercihin riskiyle, riski bilinmeyen beyaz; metin karşılığı yanındaki risk Yeni bir kreatif fikri (veriyi piksele çeviren, imlece tepki veren) önce burada denenir.
dağılımında. - Çekmece başlığında denenen "gerçek liste" pikseli (12×2, her kare tercihin riski) **kaldırıldı**:
başlığı çok dolu gösteriyordu (Bilal, 24 Eyl 2026). Başlık sayı + ilerleme çubuğu + risk
dağılımı metniyle kalır.

View File

@@ -99,6 +99,12 @@ const KURALLAR: Kural[] = [
muaf: /src\/components\/pixel-decor\.tsx$/, muaf: /src\/components\/pixel-decor\.tsx$/,
bul: regexKurali(/rounded-full border border-slate-200 bg-white px-3\.5 py-1\.5/), bul: regexKurali(/rounded-full border border-slate-200 bg-white px-3\.5 py-1\.5/),
}, },
{
ad: "modal-rozet",
seviye: "hata",
mesaj: "Çekmece/modal içinde SectionEyebrow yok; başlık + tek cümle bağlam (DESIGN.md §4.10)",
bul: blokIcinde(["DrawerContent", "DialogContent"], /<SectionEyebrow\b/),
},
{ {
ad: "cta-yildiz", ad: "cta-yildiz",
seviye: "hata", seviye: "hata",
@@ -154,6 +160,37 @@ const KURALLAR: Kural[] = [
return kanonik ? [] : [{ index: m.index, parca: m[0] }]; return kanonik ? [] : [{ index: m.index, parca: m[0] }];
}, },
}, },
{
ad: "ince-form-alani",
seviye: "hata",
mesaj: "Input/SelectTrigger/Textarea üstüne yükseklik, köşe ya da zemin yazılmaz; hap varsayılanı kullanılır (DESIGN.md §4.6)",
bul: (icerik) => {
const sonuc: Array<{ index: number; parca: string }> = [];
// Açılış etiketinden ilk > işaretine kadar olan blok (self-closing dahil)
const acilis = /<(?:Input|SelectTrigger|Textarea)\b[^>]*?>/g;
const yasak =
/\b(?:(?:sm|md|lg):)?(?:h-(?:7|8|9|10)|rounded-(?:sm|md|lg|xl)|bg-background|border-input)\b/;
for (const m of icerik.matchAll(acilis)) {
const y = yasak.exec(m[0]);
if (y) sonuc.push({ index: (m.index ?? 0) + y.index, parca: y[0] });
}
return sonuc;
},
},
{
ad: "ham-input",
seviye: "uyari",
mesaj: "Kenarlıklı ham <input> yerine Input bileşeni; kenarlıksız ham input yalnız hap kabının içinde (DESIGN.md §4.6)",
bul: (icerik) => {
const sonuc: Array<{ index: number; parca: string }> = [];
for (const m of icerik.matchAll(/<input\b[^>]*?>/g)) {
if (/type=["'](?:hidden|checkbox|radio|range|file)["']/.test(m[0])) continue;
const k = /\bborder(?:-[a-z0-9/]+)?\b/.exec(m[0]);
if (k) sonuc.push({ index: (m.index ?? 0) + k.index, parca: k[0] });
}
return sonuc;
},
},
{ {
ad: "yabanci-font", ad: "yabanci-font",
seviye: "hata", seviye: "hata",

View File

@@ -681,9 +681,10 @@ export default function Home() {
<section className="px-4 pb-24 pt-6 sm:px-6"> <section className="px-4 pb-24 pt-6 sm:px-6">
<Parallax strength={12} className="mx-auto max-w-6xl"> <Parallax strength={12} className="mx-auto max-w-6xl">
<Reveal> <Reveal>
{/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen {/* Kapanış yüzeyi (DESIGN.md §10.8): koyu kart + 24'lük
piksel zemini yerine 24'lük listenin interaktif kreatifi */} listenin interaktif kreatifi. Bilal'in kararı (24 Eyl 2026):
<PikselCtaYuzeyi metinOfseti={7}> bu yüzey yalnız burada; diğer CTA'lar mavi CtaBandi. */}
<PikselCtaYuzeyi seed={7}>
<h2 className="font-heading text-3xl font-bold text-balance sm:text-4xl"> <h2 className="font-heading text-3xl font-bold text-balance sm:text-4xl">
Tercih listen, pişmanlık listesi olmasın Tercih listen, pişmanlık listesi olmasın
</h2> </h2>

View File

@@ -0,0 +1,59 @@
"use client";
import type { ComponentProps, Ref } from "react";
import { PixelMasonry } from "@/components/pixel-decor";
import { cn } from "@/lib/utils";
/**
* CTA bandı — DESIGN.md §10.8 kanonik yüzey: sitenin mavisi (`bg-primary`),
* arka planda büyüklü küçüklü, masonry gibi kaymış ve imleç yaklaşınca
* turuncuya ısınan piksel dokusu (`PixelMasonry`), beyaz yazı, tek turuncu buton.
*
* - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk.
* - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak.
*
* İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur.
* Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band.
*/
export function CtaBandi({
children,
className,
seed = 8,
boyut = "buyuk",
etiket = "section",
ref,
...props
}: Omit<ComponentProps<"div">, "ref"> & {
/** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */
seed?: number;
boyut?: "buyuk" | "kompakt";
/** Çekmece altı gibi başlıksız yerlerde `div` */
etiket?: "section" | "div";
ref?: Ref<HTMLElement>;
}) {
const buyuk = boyut === "buyuk";
// İki etiket de HTMLElement'tir; JSX tipi için div'e daraltılır.
const Etiket = etiket as "div";
return (
<Etiket
ref={ref as Ref<HTMLDivElement>}
className={cn(
"relative isolate overflow-hidden rounded-2xl bg-primary text-white",
buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5",
className,
)}
{...props}
>
<div
aria-hidden
className={cn(
"pointer-events-none absolute inset-x-0 -z-10 text-white",
buyuk ? "-inset-y-10" : "-inset-y-6",
)}
>
<PixelMasonry seed={seed} />
</div>
{children}
</Etiket>
);
}

View File

@@ -13,23 +13,32 @@ export const SATIR_AD =
export const SATIR_AD_DUZ = export const SATIR_AD_DUZ =
"block text-base font-semibold leading-snug text-slate-900"; "block text-base font-semibold leading-snug text-slate-900";
/** Adın altındaki ikinci satır (varyant ya da üniversite adı). */ /** Adın altındaki ikinci satır (varyant ya da üniversite adı). Telefonda
export const SATIR_ALT_AD = "mt-0.5 block text-sm leading-snug text-slate-600"; * bir kademe küçük (Bilal, 24 Eyl 2026: "mobilde çok şişko, üni adını daha
* küçük yazabiliriz"); tür/il/süre ile aynı akan satırda (SATIR_GRUP). */
export const SATIR_ALT_AD =
"block text-xs leading-snug text-slate-600 sm:text-sm";
/** Üniversite adı (SATIR_ALT_AD) + tür/il/süre (SATIR_META) — adın altında
* her genişlikte tek akan satır, sığmazsa sarar. /sonuc, rapor ve tadımlık
* satırı kullanır. Bilal (24 Eyl 2026): telefonda katmanlar "iç içe". */
export const SATIR_ALT_GRUP =
"mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 sm:mt-0.5 sm:gap-x-3 sm:gap-y-1";
/** Satırdaki ikincil link (ör. üniversite adı) — §2.1 tek turuncu. */ /** Satırdaki ikincil link (ör. üniversite adı) — §2.1 tek turuncu. */
export const SATIR_IKINCIL_LINK = export const SATIR_IKINCIL_LINK =
"transition-colors hover:text-orange-500 hover:underline"; "transition-colors hover:text-orange-500 hover:underline";
/** Tür/il/süre satırı. Geniş ekranda (sm+) üstündeki adla aynı satıra /** Tür/il/süre satırı. Kenar boşluğu sarmaldan gelir (SATIR_GRUP /
* çıkar (SATIR_GRUP), telefonda altına iner. */ * SATIR_ALT_GRUP); telefonda üniversite adı gibi bir kademe küçük. */
export const SATIR_META = export const SATIR_META =
"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500 sm:mt-0"; "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 sm:text-sm";
/** Ad (ya da üniversite adı) + tür/il/süre satırını geniş ekranda yan yana /** Ana ad + tür/il/süre sarmalı (katalog listeleri: ad zaten üniversite ya da
* dizen sarmal. Bilal'in isteği (25 Eyl 2026): geniş ekranda satırlar üç * bölüm). Telefonda ad kendi satırında kalır, çipler altına iner (8 px);
* katlı ve "çok kalın" duruyordu. Telefonda alt alta kalır. */ * geniş ekranda (Bilal, 25 Eyl 2026: satırlar "çok kalın") aynı satıra çıkar. */
export const SATIR_GRUP = export const SATIR_GRUP =
"sm:flex sm:flex-wrap sm:items-center sm:gap-x-3 sm:gap-y-1"; "space-y-2 sm:flex sm:flex-wrap sm:items-center sm:gap-x-3 sm:gap-y-1 sm:space-y-0";
/** Tür çipi (Devlet, Vakıf, SAY…). */ /** Tür çipi (Devlet, Vakıf, SAY…). */
export const SATIR_CIP = export const SATIR_CIP =

View File

@@ -2,38 +2,28 @@
import { useState, type ReactNode } from "react"; import { useState, type ReactNode } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { TypingAnimation } from "@/components/ui/typing-animation"; import { PixelMasonry } from "@/components/pixel-decor";
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste"; import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
/** Listeyi `ofset` kadar döndürür: aynı sayfadaki yüzeyler farklı satırla
* başlar. Rastgele değil, deterministik — sunucu ve istemci aynı çizer. */
function dondur<T>(dizi: readonly T[], ofset: number): T[] {
const n = ofset % dizi.length;
return [...dizi.slice(n), ...dizi.slice(0, n)];
}
/** /**
* Funnel CTA yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili: * Kapanış yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili:
* koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar, * koyu slate-950 zemin, ince mavi kenar, turuncu CTA. Arka planda büyüklü
* turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük * küçüklü, masonry gibi kaymış piksel dokusu (PixelMasonry); yüzeyin
* listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da * kreatifi 24'lük listenin kendisidir (PikselListe). Kartın içindeki bir
* linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur". * düğmeye ya da linke imleç/odak gelince 24 kare birden dolar — "listen
* böyle hazır olur". Yazılıp silinen satır yok (Bilal, 24 Eyl 2026).
*/ */
export function PikselCtaYuzeyi({ export function PikselCtaYuzeyi({
children, children,
className, className,
metinOfseti = 0, seed = 7,
kayanMetin = true,
}: { }: {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
/** Aynı sayfadaki iki yüzey aynı satırla başlamasın */ /** Piksel dokusunun deseni; aynı sayfadaki iki yüzey farklı olsun */
metinOfseti?: number; seed?: number;
kayanMetin?: boolean;
}) { }) {
const [tamamla, setTamamla] = useState(false); const [tamamla, setTamamla] = useState(false);
const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti));
const hedefMi = (t: EventTarget | null) => const hedefMi = (t: EventTarget | null) =>
t instanceof Element && t.closest("a, button") != null; t instanceof Element && t.closest("a, button") != null;
@@ -49,20 +39,15 @@ export function PikselCtaYuzeyi({
className, className,
)} )}
> >
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
>
<PixelMasonry seed={seed} />
</div>
<div className="grid items-center gap-8 px-5 py-10 sm:px-10 sm:py-12 lg:grid-cols-[minmax(0,1fr)_16rem] lg:gap-12"> <div className="grid items-center gap-8 px-5 py-10 sm:px-10 sm:py-12 lg:grid-cols-[minmax(0,1fr)_16rem] lg:gap-12">
<div className="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left"> <div className="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left">
{children} {children}
{kayanMetin ? (
<TypingAnimation
as="p"
words={metinler}
loop
typeSpeed={38}
deleteSpeed={18}
pauseDelay={2600}
className="mt-5 min-h-[2.75em] font-heading text-base font-bold leading-snug tracking-[-0.02em] text-slate-300 sm:min-h-[1.4em]"
/>
) : null}
</div> </div>
<div className="order-first flex flex-col items-center lg:order-none"> <div className="order-first flex flex-col items-center lg:order-none">
<PikselListe <PikselListe

View File

@@ -72,11 +72,14 @@ function pixelRect(
} }
/** /**
* Geniş alanlar için seyrek piksel zemini (ör. koyu CTA bandı). * CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi
* İçeriğin oturduğu orta bölge tamamen boş bırakılır; pikseller * hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry
* kenarlara doğru yoğunlaşır. * şeklinde"). İçeriğin oturduğu orta bant boş kalır; kareler kenarlara doğru
* yoğunlaşır. 3×3 hücrelik makro hücrelere 1–3 hücrelik tek kare düşer ve
* makro hücre içinde kaydırılır: kareler çakışmaz ama hizaya da girmez
* (§1.1 "doğal duruş"). Her kare imleç ısı fırçasına katılır.
*/ */
export function PixelField({ export function PixelMasonry({
seed = 7, seed = 7,
className, className,
}: { }: {
@@ -84,42 +87,99 @@ export function PixelField({
className?: string; className?: string;
}) { }) {
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef); usePixelHeat(svgRef, { radius: 44 });
const COLS = 42; const COLS = 42;
const ROWS = 12; const ROWS = 12;
const MAKRO = 3;
// Çıktı seed'e göre deterministik; 504 iterasyon + ~150 element üretimini // Çıktı seed'e göre deterministik; render başına yeniden üretme
// her render'da (ör. çekmece reorder'ında) tekrarlama (rendering-hoist-jsx)
const pixels = useMemo(() => { const pixels = useMemo(() => {
const cx = (COLS - 1) / 2; const cx = (COLS - 1) / 2;
const cy = (ROWS - 1) / 2; const cy = (ROWS - 1) / 2;
const maxD = Math.sqrt(cx * cx + cy * cy); const maxD = Math.sqrt(cx * cx + cy * cy);
const BAND_MIN = COLS * 0.22;
const BAND_MAX = COLS * 0.78;
const sonuc: ReactNode[] = []; const sonuc: ReactNode[] = [];
let accentIdx = 0; let accentIdx = 0;
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon const kare = (
// (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır x: number,
const BAND_MIN = COLS * 0.22; y: number,
const BAND_MAX = COLS * 0.78; n: number,
key: string,
for (let y = 0; y < ROWS; y++) { accent: boolean,
for (let x = 0; x < COLS; x++) { ) => {
if (x > BAND_MIN && x < BAND_MAX) continue; const boy = n * CELL - 2 * INSET;
// Kenarlara doğru artan doluluk const shared = {
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD; x: x * CELL + INSET,
const density = 0.1 + 0.42 * d * d; y: y * CELL + INSET,
if (hash(x, y, seed) > density) continue; width: boy,
const accent = hash(x, y, seed + 99) > 0.96; height: boy,
sonuc.push( rx: 2 * n,
pixelRect(x, y, { };
key: `${x}-${y}`, if (accent) {
accent, return (
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0, <rect
}), key={key}
{...shared}
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
style={{
animationDuration: "3.6s",
animationDelay: `${(accentIdx++ % 8) * 0.45}s`,
}}
/>
); );
} }
// İri kare daha silik: yazının yanında bağırmasın
const op = opacityFor(x, y) * (n === 1 ? 1 : n === 2 ? 0.8 : 0.6);
return (
<rect
key={key}
{...shared}
data-px=""
data-cx={x * CELL + (n * CELL) / 2}
data-cy={y * CELL + (n * CELL) / 2}
data-op={op}
fill="currentColor"
fillOpacity={op}
/>
);
};
for (let my = 0; my < ROWS / MAKRO; my++) {
for (let mx = 0; mx < COLS / MAKRO; mx++) {
const ox = mx * MAKRO;
const oy = my * MAKRO;
const mcx = ox + MAKRO / 2;
if (mcx > BAND_MIN && mcx < BAND_MAX) continue;
// Kenarlara doğru artan doluluk
const d = Math.sqrt((mcx - cx) ** 2 + (oy + MAKRO / 2 - cy) ** 2) / maxD;
const density = 0.3 + 0.6 * d * d;
if (hash(mx, my, seed) > density) {
// Boş makro hücrede ara sıra tek küçük piksel: doku kopmasın
if (hash(mx, my, seed + 3) < 0.25) {
const px = ox + Math.floor(hash(mx, my, seed + 4) * MAKRO);
const py = oy + Math.floor(hash(mx, my, seed + 5) * MAKRO);
sonuc.push(kare(px, py, 1, `k-${mx}-${my}`, false));
}
continue;
}
const r = hash(mx, my, seed + 7);
const n = r < 0.5 ? 1 : r < 0.85 ? 2 : 3;
const serbest = MAKRO - n;
const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1));
const ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1));
const accent = n < 3 && hash(mx, my, seed + 99) > 0.94;
sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent));
// Büyük karenin yanına küçük bir eş: büyüklü küçüklü ritim
if (n < 3 && hash(mx, my, seed + 17) < 0.45) {
const ex = kx + n < ox + MAKRO ? kx + n : kx - 1;
const ey = ky + n < oy + MAKRO ? ky + n : ky - 1;
if (ex >= ox && ey >= oy) sonuc.push(kare(ex, ey, 1, `e-${mx}-${my}`, false));
}
}
} }
return sonuc; return sonuc;
}, [seed]); }, [seed]);
@@ -156,7 +216,7 @@ export function PixelDivider({
const COLS = 12; const COLS = 12;
const ROWS = 2; const ROWS = 2;
// Deterministik çıktı — render başına yeniden üretme (PixelField ile aynı) // Deterministik çıktı — render başına yeniden üretme (PixelMasonry ile aynı)
const pixels = useMemo(() => { const pixels = useMemo(() => {
const cx = (COLS - 1) / 2; const cx = (COLS - 1) / 2;
const sonuc: ReactNode[] = []; const sonuc: ReactNode[] = [];

View File

@@ -2,13 +2,18 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
/**
* Metin alanı — DESIGN.md §4.6. Üst çubuktaki arama/kullanıcı haplarıyla aynı
* kalıp: 48 px, tam yuvarlak, slate-200 kenarlık, 16 px yazı (telefonda da).
* Başta ikon gerekiyorsa kapsayıcı `relative`, ikon `left-5`, alan `pl-12`.
*/
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", "h-12 w-full min-w-0 rounded-full border border-slate-200 bg-white px-5 text-base text-slate-900 transition-colors duration-150 outline-none placeholder:text-slate-400 hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20",
className className
)} )}
{...props} {...props}

View File

@@ -44,14 +44,14 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "flex w-fit cursor-pointer items-center justify-between gap-2 rounded-full border border-slate-200 bg-white py-2 pr-4 pl-5 text-base text-slate-900 whitespace-nowrap transition-colors duration-150 outline-none select-none hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20 data-placeholder:text-slate-400 data-[size=default]:h-12 data-[size=sm]:h-9 data-[size=sm]:pr-3 data-[size=sm]:pl-4 data-[size=sm]:text-sm *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" /> <ChevronDownIcon className="pointer-events-none size-4 text-slate-400" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
) )
@@ -69,7 +69,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
data-align-trigger={position === "item-aligned"} data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )} className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl border border-slate-200 bg-white text-slate-900 shadow-lg duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position} position={position}
align={align} align={align}
{...props} {...props}
@@ -78,7 +78,7 @@ function SelectContent({
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
data-position={position} data-position={position}
className={cn( className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", "p-1.5 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && "" position === "popper" && ""
)} )}
> >
@@ -97,7 +97,7 @@ function SelectLabel({
return ( return (
<SelectPrimitive.Label <SelectPrimitive.Label
data-slot="select-label" data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)} className={cn("px-4 py-1.5 text-sm font-semibold text-slate-500", className)}
{...props} {...props}
/> />
) )
@@ -112,12 +112,12 @@ function SelectItem({
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "relative flex w-full cursor-pointer items-center gap-2 rounded-full py-2 pr-9 pl-4 text-base text-slate-900 outline-hidden select-none focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> <span className="pointer-events-none absolute right-3 flex size-4 items-center justify-center text-primary">
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" /> <CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
@@ -134,7 +134,7 @@ function SelectSeparator({
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} className={cn("pointer-events-none -mx-1.5 my-1 h-px bg-slate-200", className)}
{...props} {...props}
/> />
) )
@@ -148,7 +148,7 @@ function SelectScrollUpButton({
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", "z-10 flex cursor-default items-center justify-center bg-white py-1 text-slate-400 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
@@ -167,7 +167,7 @@ function SelectScrollDownButton({
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", "z-10 flex cursor-default items-center justify-center bg-white py-1 text-slate-400 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}

View File

@@ -2,12 +2,17 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
/**
* Çok satırlı alan — DESIGN.md §4.6. Hap değil sade kutu (§4.4): rounded-2xl,
* slate-200 kenarlık, 16 px yazı. Hover/odak/hata hâli `Input` ile aynı.
* Sohbet kutusu (`prompt-input.tsx`) kenarlıksız/şeffaf üstüne yazar.
*/
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return ( return (
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", "flex field-sizing-content min-h-24 w-full rounded-2xl border border-slate-200 bg-white px-5 py-3 text-base text-slate-900 transition-colors duration-150 outline-none placeholder:text-slate-400 hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20",
className className
)} )}
{...props} {...props}

View File

@@ -257,7 +257,7 @@ export function BolumProgramListesi({
</span> </span>
</span> </span>
{varyant ? ( {varyant ? (
<span className={SATIR_ALT_AD}> <span className={`mt-0.5 ${SATIR_ALT_AD}`}>
{varyant} {varyant}
</span> </span>
) : null} ) : null}

View File

@@ -15,7 +15,7 @@ import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { PikselListe } from "@/components/piksel-liste"; import { CtaBandi } from "@/components/cta-bandi";
import { UniLogo } from "@/components/uni-logo"; import { UniLogo } from "@/components/uni-logo";
import { RiskEtiketi } from "@/components/risk-etiketi"; import { RiskEtiketi } from "@/components/risk-etiketi";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -122,6 +122,7 @@ export function ListeCekmecesi() {
// drawer içinde erken ölçüm satırları kaydırıyordu // drawer içinde erken ölçüm satırları kaydırıyordu
const [oturdu, setOturdu] = useState(false); const [oturdu, setOturdu] = useState(false);
const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100);
// Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz // Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz
const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const) const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const)
.map((r) => [r, liste.filter((t) => t.risk === r).length] as const) .map((r) => [r, liste.filter((t) => t.risk === r).length] as const)
@@ -184,33 +185,10 @@ export function ListeCekmecesi() {
<DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900"> <DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
Kendi Listem Kendi Listem
</DrawerTitle> </DrawerTitle>
{/* Durum satırı: solda listenin piksel hâli (ilerleme çubuğunun {/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3). Bilal'in
yerine), sağda sayı ve risk dağılımı — dikine uzamasın diye kararı (24 Eyl 2026): buradaki piksel liste başlığı çok dolu
yan yana (DESIGN.md §5.3). Durum ekran okuyucuya duyurulur. */} gösteriyordu; ilerleme çubuğuna geri dönüldü. */}
<div className="mt-3 flex items-center justify-center gap-3"> <p role="status" className="mt-1 px-10 text-center text-sm text-slate-500">
<div
className="w-40 shrink-0 rounded-xl bg-slate-950 px-2.5 py-2"
role="progressbar"
aria-valuenow={liste.length}
aria-valuemin={0}
aria-valuemax={MANUEL_LISTE_MAX}
aria-label="Liste doluluğu"
>
<PikselListe
gercek={liste.map((t) =>
t.risk === "riskli"
? "hayal"
: t.risk === "az-riskli"
? "dengeli"
: t.risk === "guvenli"
? "guvenli"
: "belirsiz",
)}
sutun={12}
/>
</div>
<div className="min-w-0 text-left">
<p role="status" className="text-sm text-slate-500">
{liste.length === MANUEL_LISTE_MAX ? ( {liste.length === MANUEL_LISTE_MAX ? (
// Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık, // Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık,
// güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık. // güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık.
@@ -224,8 +202,23 @@ export function ListeCekmecesi() {
</> </>
)} )}
</p> </p>
<div
className="mx-auto mt-3 h-2 w-full max-w-xs overflow-hidden rounded-full bg-slate-100"
role="progressbar"
aria-valuenow={liste.length}
aria-valuemin={0}
aria-valuemax={MANUEL_LISTE_MAX}
aria-label="Liste doluluğu"
>
<motion.div
className="h-full rounded-full bg-primary"
animate={{ width: `${doluluk}%` }}
transition={{ type: "spring", duration: 0.5, bounce: 0 }}
/>
</div>
{/* Listenin dengesi metinle (renk tek başına değil, §2.2) */}
{riskSayilari.length > 0 ? ( {riskSayilari.length > 0 ? (
<p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1"> <p className="mt-2.5 flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
{riskSayilari.map(([seviye, adet]) => ( {riskSayilari.map(([seviye, adet]) => (
<span key={seviye} className="inline-flex items-center gap-1.5"> <span key={seviye} className="inline-flex items-center gap-1.5">
<RiskEtiketi risk={seviye} /> <RiskEtiketi risk={seviye} />
@@ -236,8 +229,6 @@ export function ListeCekmecesi() {
))} ))}
</p> </p>
) : null} ) : null}
</div>
</div>
{/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli {/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli
programlar). Yalnız riskleri bilinen ve en az 6 tercihli programlar). Yalnız riskleri bilinen ve en az 6 tercihli
listede; vaat değil, gerekçe. */} listede; vaat değil, gerekçe. */}
@@ -331,18 +322,23 @@ export function ListeCekmecesi() {
</div> </div>
) : null} ) : null}
{/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary {/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary
zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */} zemin + büyüklü küçüklü piksel dokusu (PixelMasonry, CtaBandi) */}
{/* Telefonda alt alta (başlık tek satır, buton tam genişlik): {/* Telefonda alt alta (başlık tek satır, buton tam genişlik):
yan yana dizince başlık üç satıra kırılıp listenin yerini yan yana dizince başlık üç satıra kırılıp listenin yerini
yiyordu (24 Eyl 2026, 375 px ölçümü). */} yiyordu (24 Eyl 2026, 375 px ölçümü). */}
{/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın {/* CTA bandı (DESIGN.md §10.8, tek kaynak CtaBandi). Bilal'in
koyu zemini; listenin piksel hâli başlıkta. */} kararı (24 Eyl 2026): koyu slate-950 yüzeyden bu hâle dönüldü. */}
<div className="flex flex-col gap-3 rounded-2xl border border-blue-400/20 bg-slate-950 px-5 py-4 text-left text-white sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:py-5"> <CtaBandi
<div className="min-w-0 sm:order-1 sm:flex-1"> etiket="div"
boyut="kompakt"
seed={8}
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4"
>
<div className="min-w-0">
<p className="font-heading text-xl font-bold sm:text-2xl"> <p className="font-heading text-xl font-bold sm:text-2xl">
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"} {riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
</p> </p>
<p className="mt-1 hidden truncate text-sm leading-relaxed text-slate-300 sm:block"> <p className="mt-1 hidden truncate text-sm leading-relaxed text-white sm:block">
{riskEksik {riskEksik
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)." ? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
: "Her seçimin yerleşme riski hazır — son kararı sen ver."} : "Her seçimin yerleşme riski hazır — son kararı sen ver."}
@@ -351,7 +347,7 @@ export function ListeCekmecesi() {
{riskEksik ? ( {riskEksik ? (
<Button <Button
size="lg" size="lg"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto" className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
onClick={() => { onClick={() => {
cekmeceKapat(); cekmeceKapat();
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam // Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
@@ -367,7 +363,7 @@ export function ListeCekmecesi() {
<Button <Button
asChild asChild
size="lg" size="lg"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto" className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
> >
<Link href="/listem" onClick={cekmeceKapat}> <Link href="/listem" onClick={cekmeceKapat}>
Listemde aç Listemde aç
@@ -375,7 +371,7 @@ export function ListeCekmecesi() {
</Link> </Link>
</Button> </Button>
)} )}
</div> </CtaBandi>
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@@ -466,7 +462,7 @@ function CekmeceSatiri({
> >
{tercih.isim} {tercih.isim}
</Link> </Link>
<span className="mt-1 flex items-center gap-2 text-sm leading-snug text-slate-600"> <span className="mt-1 flex items-center gap-2 text-xs leading-snug text-slate-600 sm:text-sm">
<UniLogo ad={tercih.universite} boy="xs" /> <UniLogo ad={tercih.universite} boy="xs" />
<Link <Link
href={uniHref(tercih.universite)} href={uniHref(tercih.universite)}

View File

@@ -52,7 +52,7 @@ import {
SATIR_BUTON_HUCRE, SATIR_BUTON_HUCRE,
SATIR_BUTON_KABI, SATIR_BUTON_KABI,
SATIR_CIP, SATIR_CIP,
SATIR_GRUP, SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK, SATIR_IKINCIL_LINK,
SATIR_META, SATIR_META,
SATIR_MOBIL_SAYI, SATIR_MOBIL_SAYI,
@@ -360,7 +360,7 @@ function ProgramSatiri({
> >
{p.isim} {p.isim}
</Link> </Link>
<span className={SATIR_GRUP}> <span className={SATIR_ALT_GRUP}>
<span className={SATIR_ALT_AD}> <span className={SATIR_ALT_AD}>
<Link <Link
href={`/universite/${uniSayfaSlug(p.universite)}`} href={`/universite/${uniSayfaSlug(p.universite)}`}

View File

@@ -7,7 +7,6 @@ import { useReducedMotion } from "motion/react";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { toast } from "sonner"; import { toast } from "sonner";
import { TransitionPanel } from "@/components/core/transition-panel"; import { TransitionPanel } from "@/components/core/transition-panel";
import { SectionEyebrow } from "@/components/pixel-decor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -293,11 +292,10 @@ function KapıIcerik({
} }
} }
> >
<div ref={panelRef} className="flex flex-col gap-5 px-px"> {/* p-1: geçiş paneli taşmayı kırpar; alanların 3 px odak halkası
(Input/Select/Button ring-3) sığsın (Bilal, 24 Eyl 2026). */}
<div ref={panelRef} className="flex flex-col gap-5 p-1">
<div> <div>
{duzenle ? (
<SectionEyebrow>Tercihlerini güncelle</SectionEyebrow>
) : null}
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900 first:mt-0"> <h2 className="mt-3 font-heading text-2xl font-bold text-slate-900 first:mt-0">
{duzenle {duzenle
? "Seçimlerini değiştir" ? "Seçimlerini değiştir"
@@ -343,12 +341,9 @@ function KapıIcerik({
<form <form
ref={panelRef} ref={panelRef}
onSubmit={siraGonder} onSubmit={siraGonder}
className="flex flex-col gap-5 px-px" className="flex flex-col gap-5 p-1"
> >
<div> <div>
{duzenle ? (
<SectionEyebrow>Sıralama ve tercihler</SectionEyebrow>
) : null}
<h2 className="mt-3 font-heading text-xl font-bold text-slate-900 first:mt-0"> <h2 className="mt-3 font-heading text-xl font-bold text-slate-900 first:mt-0">
{duzenle {duzenle
? "Yeni sıralaman nedir?" ? "Yeni sıralaman nedir?"
@@ -370,7 +365,7 @@ function KapıIcerik({
<div className="flex flex-col gap-2 sm:flex-row"> <div className="flex flex-col gap-2 sm:flex-row">
<div className="relative sm:flex-1"> <div className="relative sm:flex-1">
<Trophy <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 aria-hidden
/> />
<Input <Input
@@ -383,13 +378,13 @@ function KapıIcerik({
onFocus={preloadSihirbazAdimlar} onFocus={preloadSihirbazAdimlar}
aria-invalid={hata ? true : undefined} aria-invalid={hata ? true : undefined}
aria-describedby={hata ? "kapisi-siralama-hata" : undefined} aria-describedby={hata ? "kapisi-siralama-hata" : undefined}
className="h-12 bg-background pl-11 text-base" className="pl-12"
/> />
</div> </div>
<Select value={tur} onValueChange={setTur}> <Select value={tur} onValueChange={setTur}>
<SelectTrigger <SelectTrigger
aria-label="Puan türü" aria-label="Puan türü"
className="w-full bg-background data-[size=default]:h-12 sm:w-44" className="w-full sm:w-44"
> >
<SelectValue placeholder="Puan türü" /> <SelectValue placeholder="Puan türü" />
</SelectTrigger> </SelectTrigger>
@@ -436,7 +431,7 @@ function KapıIcerik({
</div> </div>
</form> </form>
<div ref={panelRef} className="px-px"> <div ref={panelRef} className="p-1">
{facetler && sira != null ? ( {facetler && sira != null ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{hata ? ( {hata ? (

View File

@@ -39,7 +39,7 @@ import {
SATIR_BUTON_HUCRE, SATIR_BUTON_HUCRE,
SATIR_CIP, SATIR_CIP,
SATIR_DUGME_BOYU, SATIR_DUGME_BOYU,
SATIR_GRUP, SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK, SATIR_IKINCIL_LINK,
SATIR_META, SATIR_META,
SATIR_MOBIL_SAYI, SATIR_MOBIL_SAYI,
@@ -248,7 +248,7 @@ export function RaporListesi({
) : ( ) : (
<span className={SATIR_AD_DUZ}>{t.programId}</span> <span className={SATIR_AD_DUZ}>{t.programId}</span>
)} )}
<span className={SATIR_GRUP}> <span className={SATIR_ALT_GRUP}>
{p?.universite ? ( {p?.universite ? (
<span className={SATIR_ALT_AD}> <span className={SATIR_ALT_AD}>
<Link <Link

View File

@@ -29,7 +29,7 @@ import {
SATIR_ALT_AD, SATIR_ALT_AD,
SATIR_AD_DUZ, SATIR_AD_DUZ,
SATIR_CIP, SATIR_CIP,
SATIR_GRUP, SATIR_ALT_GRUP,
SATIR_META, SATIR_META,
} from "@/components/liste-satiri"; } from "@/components/liste-satiri";
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler"; import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
@@ -157,7 +157,7 @@ export function TadimlikSatiri({
<UniLogo ad={p.universite} boy="sm" /> <UniLogo ad={p.universite} boy="sm" />
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className={SATIR_AD_DUZ}>{p.isim}</span> <span className={SATIR_AD_DUZ}>{p.isim}</span>
<span className={SATIR_GRUP}> <span className={SATIR_ALT_GRUP}>
<span className={SATIR_ALT_AD}> <span className={SATIR_ALT_AD}>
{trBaslikDuzeni(p.universite)} {trBaslikDuzeni(p.universite)}
</span> </span>

View File

@@ -3,14 +3,7 @@
import { useId, useState } from "react"; import { useId, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import { ArrowRight, Trophy, X } from "lucide-react";
ArrowRight,
ChartNoAxesCombined,
Database,
ListChecks,
Trophy,
X,
} from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -20,7 +13,7 @@ import {
DrawerDescription, DrawerDescription,
DrawerTitle, DrawerTitle,
} from "@/components/ui/drawer"; } 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 { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { import {
SihirbazAdimlarLazy, SihirbazAdimlarLazy,
@@ -29,7 +22,7 @@ import {
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { olay, siraKovasi } from "@/lib/analitik"; import { olay, siraKovasi } from "@/lib/analitik";
import { cn } from "@/lib/utils"; 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 { onbellekliJsonGetir } from "@/lib/istek-cache";
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili"; import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
import type { SihirbazFacetleri } from "@/types/yokatlas"; import type { SihirbazFacetleri } from "@/types/yokatlas";
@@ -45,13 +38,6 @@ const PUAN_TURLERI_SECENEK = [
{ deger: "tyt", etiket: "TYT" }, { deger: "tyt", etiket: "TYT" },
] as const; ] 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. // 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. // listem-bos-cta.tsx): büyük turuncu buton, piksel ayraç. Sınıf seti tek yerde.
const PROFILLI_CTA_SINIF = const PROFILLI_CTA_SINIF =
@@ -65,22 +51,22 @@ function turEtiketi(tur: string) {
function KayitliSira({ function KayitliSira({
sira, sira,
tur, tur,
koyu = false, beyaz = false,
}: { }: {
sira: number; sira: number;
tur: string; tur: string;
/** Koyu funnel yüzeyinde (PikselCtaYuzeyi) */ /** Mavi CTA bandının üstünde (CtaBandi): beyaz yazı */
koyu?: boolean; beyaz?: boolean;
}) { }) {
return ( return (
<span <span
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm", "inline-flex items-center gap-1.5 text-sm",
koyu ? "text-slate-300" : "text-slate-600", beyaz ? "text-white/90" : "text-slate-600",
)} )}
> >
<Trophy <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 aria-hidden
/> />
<span> <span>
@@ -88,7 +74,7 @@ function KayitliSira({
<strong <strong
className={cn( className={cn(
"font-semibold tabular-nums", "font-semibold tabular-nums",
koyu ? "text-white" : "text-slate-900", beyaz ? "text-white" : "text-slate-900",
)} )}
> >
{sira.toLocaleString("tr-TR")}. {sira.toLocaleString("tr-TR")}.
@@ -278,7 +264,7 @@ export function CtaSiraForm({
> >
<div className="relative sm:flex-1"> <div className="relative sm:flex-1">
<Trophy <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 aria-hidden
/> />
<Input <Input
@@ -291,7 +277,7 @@ export function CtaSiraForm({
onFocus={preloadSihirbazAdimlar} onFocus={preloadSihirbazAdimlar}
aria-invalid={hata ? true : undefined} aria-invalid={hata ? true : undefined}
aria-describedby={hata ? hataId : undefined} aria-describedby={hata ? hataId : undefined}
className="h-12 bg-background pl-11 text-base" className="pl-12"
/> />
</div> </div>
<Button <Button
@@ -319,20 +305,16 @@ export function CtaSiraForm({
); );
// Çekmece iki varyantta da aynıdır; kompakt satırda sıra adımı sayfada // Ç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 = ( const cekmece = (
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly> <Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
<DrawerContent <DrawerContent
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl" 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]"> <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 /> <X className="size-4" aria-hidden />
<span className="sr-only">Kapat</span> <span className="sr-only">Kapat</span>
@@ -341,37 +323,28 @@ export function CtaSiraForm({
<div <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" 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" ? ( <DrawerTitle
<div className="flex flex-col gap-5"> className={
<div> siraAdimi
<SectionEyebrow>Saniyede gör</SectionEyebrow> ? "px-10 text-center font-heading text-2xl font-bold text-slate-900"
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900"> : "sr-only"
}
>
Sıralamana yakın programlar hangileri? Sıralamana yakın programlar hangileri?
</h2> </DrawerTitle>
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600"> <DrawerDescription
Puan türünü seç, başarı sıralamanı yaz; hayal, dengeli ve className={
güvenli dilimlerindeki programları bu sayfadan ayrılmadan siraAdimi
gör. Ücretsiz, üyelik gerekmez. ? "mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500"
</p> : "sr-only"
</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 }) => ( Puan türünü seç, sıralamanı yaz; hayal, dengeli ve güvenli
<li dilimlerindeki programları gerçek YÖK Atlas verisiyle bu sayfadan
key={etiket} ayrılmadan gör. Ücretsiz, üyelik gerekmez.
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-600" </DrawerDescription>
> {siraAdimi ? (
<Ikon className="size-3.5 text-primary" aria-hidden /> <div className="mt-5">{siraFormu}</div>
{etiket}
</li>
))}
</ul>
</div>
) : facetler && sira != null ? ( ) : facetler && sira != null ? (
<SihirbazAdimlarLazy <SihirbazAdimlarLazy
key={`${sira}-${tur}`} key={`${sira}-${tur}`}
@@ -447,18 +420,17 @@ export function CtaSiraForm({
); );
} }
// Kart varyantı sayfa sonundaki funnel CTA'sıdır: arama kartının koyu // Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi).
// yüzeyi + 24'lük piksel liste (DESIGN.md §10.8).
if (profil) { if (profil) {
return ( return (
<PikselCtaYuzeyi metinOfseti={5}> <CtaBandi seed={5}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2> </h2>
<div className="mt-3"> <div className="mt-3">
<KayitliSira sira={profil.sira} tur={profil.tur} koyu /> <KayitliSira sira={profil.sira} tur={profil.tur} beyaz />
</div> </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 Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
programların hazır. programların hazır.
</p> </p>
@@ -472,17 +444,17 @@ export function CtaSiraForm({
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Link> </Link>
</Button> </Button>
</PikselCtaYuzeyi> </CtaBandi>
); );
} }
return ( return (
<div ref={formRef}> <div ref={formRef}>
<PikselCtaYuzeyi metinOfseti={9}> <CtaBandi seed={9}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
</h2> </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 ?? {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."} "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> </p>
@@ -497,7 +469,7 @@ export function CtaSiraForm({
Sıralamanı gir, programları gör Sıralamanı gir, programları gör
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Button> </Button>
</PikselCtaYuzeyi> </CtaBandi>
{cekmece} {cekmece}
</div> </div>
); );

View File

@@ -181,9 +181,9 @@ export function HeroForm() {
aria-invalid={hata ? true : undefined} aria-invalid={hata ? true : undefined}
aria-describedby={hata ? "siralama-hata" : undefined} aria-describedby={hata ? "siralama-hata" : undefined}
// flex-1 yalnızca yatay dizilimde (sm+): mobilde form flex-col olduğu // flex-1 yalnızca yatay dizilimde (sm+): mobilde form flex-col olduğu
// için flex-1 inputun yüksekliğini eziyordu. Mobilde geniş padding // için flex-1 inputun yüksekliğini eziyordu. Telefonda hero alanı
// + büyük yazı; sm ve üzeri eski görünüm. // bir kademe iri (DESIGN.md §4.6); sm ve üzeri temel Input.
className="h-14 bg-background px-6 py-3 text-lg sm:h-12 sm:flex-1 sm:px-2.5 sm:py-1 sm:text-base" className="h-14 text-lg sm:h-12 sm:flex-1 sm:text-base"
/> />
<Button <Button
type="submit" type="submit"

View File

@@ -7,7 +7,7 @@ import { ArrowRight } from "lucide-react";
import { animate } from "motion/mini"; import { animate } from "motion/mini";
import { useReducedMotion } from "motion/react"; import { useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; import { CtaBandi } from "@/components/cta-bandi";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik"; import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
import type { SihirbazFacetleri } from "@/types/yokatlas"; import type { SihirbazFacetleri } from "@/types/yokatlas";
@@ -279,12 +279,11 @@ export function SihirbazBolumu({
</Button> </Button>
</section> </section>
) : girisBekliyor ? ( ) : girisBekliyor ? (
<div ref={hazirPanelRef} className="mt-16 min-w-0"> <CtaBandi ref={hazirPanelRef} seed={13} className="mt-16 min-w-0">
<PikselCtaYuzeyi metinOfseti={3}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
</h2> </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">
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24 Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap, tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi, seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi,
@@ -305,17 +304,16 @@ export function SihirbazBolumu({
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Link> </Link>
</Button> </Button>
<p className="mt-3 text-sm text-slate-400"> <p className="mt-3 text-sm text-white/70">
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
</p> </p>
</PikselCtaYuzeyi> </CtaBandi>
</div>
) : ( ) : (
<PikselCtaYuzeyi className="mt-16 min-w-0" metinOfseti={11}> <CtaBandi seed={19} className="mt-16 min-w-0">
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl"> <h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
24 tercihi tek tek elemek yerine, yapay zekâ kursun 24 tercihi tek tek elemek yerine, yapay zekâ kursun
</h2> </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">
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve
güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme
@@ -329,7 +327,7 @@ export function SihirbazBolumu({
Yapay Zeka listemi kur Yapay Zeka listemi kur
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</Button> </Button>
</PikselCtaYuzeyi> </CtaBandi>
)} )}
{monteEdildi ? ( {monteEdildi ? (