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:
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
59
src/components/cta-bandi.tsx
Normal file
59
src/components/cta-bandi.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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)}`}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user