From 2099f20ed1a10ccbe63417a8d573c90f7cf821a9 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 12:08:24 +0300 Subject: [PATCH] =?UTF-8?q?feat(tasarim):=20form=20alanlar=C4=B1=20hap,=20?= =?UTF-8?q?CTA=20band=C4=B1=20tek=20kaynak,=20masonry=20piksel=20zemin,=20?= =?UTF-8?q?=C3=A7ekmeceler=20=C2=A74.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/tasarim/DESIGN.md | 135 +++++++++++++++--- scripts/tasarim-denetim.ts | 37 +++++ src/app/page.tsx | 7 +- src/components/cta-bandi.tsx | 59 ++++++++ src/components/liste-satiri.ts | 27 ++-- src/components/piksel-cta-yuzeyi.tsx | 47 +++--- src/components/pixel-decor.tsx | 114 +++++++++++---- src/components/ui/input.tsx | 7 +- src/components/ui/select.tsx | 20 +-- src/components/ui/textarea.tsx | 7 +- .../components/bolum-program-listesi.tsx | 2 +- .../liste/components/liste-cekmecesi.tsx | 124 ++++++++-------- .../liste/components/program-tablosu.tsx | 4 +- .../components/tercih-profili-kapisi.tsx | 21 ++- .../rapor/components/rapor-listesi.tsx | 4 +- .../rapor/components/tadimlik-satiri.tsx | 4 +- .../sihirbaz/components/cta-sira-form.tsx | 120 ++++++---------- .../sihirbaz/components/hero-form.tsx | 6 +- .../components/sihirbaz-cagri-karti.tsx | 66 +++++---- 19 files changed, 516 insertions(+), 295 deletions(-) create mode 100644 src/components/cta-bandi.tsx diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index 3268a68..ab265ed 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -49,7 +49,7 @@ ve üniversite sayfası. Bu hissi veren şeyler, ekrandan bağımsız: ### 1.2 Genel kurallar - 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), 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ş @@ -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`. `@/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` `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]` + `` + `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 `` kullanır; elle yazılmaz. Nokta + metin: `` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla. -### 4.6 Form -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. +### 4.6 Form alanı — `Input`, `Select`, `Textarea` (`src/components/ui/`) +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 ``** 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 ``. ### 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. @@ -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 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. +- **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 ç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. @@ -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.) 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`. +- **Ü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`. - 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 @@ -467,6 +511,7 @@ Hareket kuralları (Apple, `motion/react`): | Üç 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ı | | 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 ``; `md:text-sm` | ## 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ı `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), -`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` + `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 @@ -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 karar. -### 10.8 Funnel CTA yüzeyi ve 24'lük piksel liste -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." -- **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki - kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp - 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 - sayfalarının altındaki `CtaSiraForm` kart varyantı (üstteki kompakt satır form olduğu için - açık zeminde kalır). Eski - `bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz. +### 10.8 CTA bandı — `CtaBandi`, `src/components/cta-bandi.tsx` + +**Kanonik CTA bandı — mavi zemin + `PixelMasonry`** (Bilal'in kararı, 24 Eyl 2026: "Kendi Listem" +çekmecesindeki bandın eski hâli daha güzeldi; örnek odur). Kabuk tek kaynaktan gelir, içerik +çağıranındır: + +```tsx + {/* buyuk: ortalı, px-5 py-8 sm:p-12 */} +

…

+

…

+ +
+ + + …başlık + alt satır solda, buton sağda (çekmece)… + +``` + +Çekmecedeki kompakt bandın açık hâli (bileşenin ürettiği sınıflar): + +```tsx +
+
+ +
+
+

Risk analizini biz yaptık

+

+ Her seçimin yerleşme riski hazır — son kararı sen ver. +

+
+ +
+``` + +- 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 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 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 `PikselListeAciklama` metniyle. -- **Gerçek liste:** çekmecede ilerleme çubuğunun yerinde öğrencinin kendi listesi (`gercek`, - 12×2): her kare o tercihin riskiyle, riski bilinmeyen beyaz; metin karşılığı yanındaki risk - dağılımında. +- Bu yüzey **sayfada bir kez** ve yalnız kapanışta durur; ara paneller ve çekmece mavi banttır. + Yeni bir kreatif fikri (veriyi piksele çeviren, imlece tepki veren) önce burada denenir. +- Ç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. diff --git a/scripts/tasarim-denetim.ts b/scripts/tasarim-denetim.ts index 4875bc7..6bd6175 100644 --- a/scripts/tasarim-denetim.ts +++ b/scripts/tasarim-denetim.ts @@ -99,6 +99,12 @@ const KURALLAR: Kural[] = [ muaf: /src\/components\/pixel-decor\.tsx$/, 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"], / { + 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 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(/]*?>/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", seviye: "hata", diff --git a/src/app/page.tsx b/src/app/page.tsx index 0439e78..7ad8b6b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -681,9 +681,10 @@ export default function Home() {
- {/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen - piksel zemini yerine 24'lük listenin interaktif kreatifi */} - + {/* Kapanış yüzeyi (DESIGN.md §10.8): koyu kart + 24'lük + listenin interaktif kreatifi. Bilal'in kararı (24 Eyl 2026): + bu yüzey yalnız burada; diğer CTA'lar mavi CtaBandi. */} +

Tercih listen, pişmanlık listesi olmasın

diff --git a/src/components/cta-bandi.tsx b/src/components/cta-bandi.tsx new file mode 100644 index 0000000..ee50d0f --- /dev/null +++ b/src/components/cta-bandi.tsx @@ -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, "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; +}) { + const buyuk = boyut === "buyuk"; + // İki etiket de HTMLElement'tir; JSX tipi için div'e daraltılır. + const Etiket = etiket as "div"; + return ( + } + 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} + > +
+ +
+ {children} +
+ ); +} diff --git a/src/components/liste-satiri.ts b/src/components/liste-satiri.ts index 3452bb8..8b17c74 100644 --- a/src/components/liste-satiri.ts +++ b/src/components/liste-satiri.ts @@ -13,23 +13,32 @@ export const SATIR_AD = export const SATIR_AD_DUZ = "block text-base font-semibold leading-snug text-slate-900"; -/** Adın altındaki ikinci satır (varyant ya da üniversite adı). */ -export const SATIR_ALT_AD = "mt-0.5 block text-sm leading-snug text-slate-600"; +/** Adın altındaki ikinci satır (varyant ya da üniversite adı). Telefonda + * 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. */ export const SATIR_IKINCIL_LINK = "transition-colors hover:text-orange-500 hover:underline"; -/** Tür/il/süre satırı. Geniş ekranda (sm+) üstündeki adla aynı satıra - * çıkar (SATIR_GRUP), telefonda altına iner. */ +/** Tür/il/süre satırı. Kenar boşluğu sarmaldan gelir (SATIR_GRUP / + * SATIR_ALT_GRUP); telefonda üniversite adı gibi bir kademe küçük. */ 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 - * dizen sarmal. Bilal'in isteği (25 Eyl 2026): geniş ekranda satırlar üç - * katlı ve "çok kalın" duruyordu. Telefonda alt alta kalır. */ +/** Ana ad + tür/il/süre sarmalı (katalog listeleri: ad zaten üniversite ya da + * bölüm). Telefonda ad kendi satırında kalır, çipler altına iner (8 px); + * geniş ekranda (Bilal, 25 Eyl 2026: satırlar "çok kalın") aynı satıra çıkar. */ 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…). */ export const SATIR_CIP = diff --git a/src/components/piksel-cta-yuzeyi.tsx b/src/components/piksel-cta-yuzeyi.tsx index c9d75ad..ff01744 100644 --- a/src/components/piksel-cta-yuzeyi.tsx +++ b/src/components/piksel-cta-yuzeyi.tsx @@ -2,38 +2,28 @@ import { useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; -import { TypingAnimation } from "@/components/ui/typing-animation"; -import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri"; +import { PixelMasonry } from "@/components/pixel-decor"; 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(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: - * koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar, - * turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük - * listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da - * linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur". + * Kapanış yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili: + * koyu slate-950 zemin, ince mavi kenar, turuncu CTA. Arka planda büyüklü + * küçüklü, masonry gibi kaymış piksel dokusu (PixelMasonry); yüzeyin + * kreatifi 24'lük listenin kendisidir (PikselListe). Kartın içindeki bir + * 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({ children, className, - metinOfseti = 0, - kayanMetin = true, + seed = 7, }: { children: ReactNode; className?: string; - /** Aynı sayfadaki iki yüzey aynı satırla başlamasın */ - metinOfseti?: number; - kayanMetin?: boolean; + /** Piksel dokusunun deseni; aynı sayfadaki iki yüzey farklı olsun */ + seed?: number; }) { const [tamamla, setTamamla] = useState(false); - const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti)); const hedefMi = (t: EventTarget | null) => t instanceof Element && t.closest("a, button") != null; @@ -49,20 +39,15 @@ export function PikselCtaYuzeyi({ className, )} > +
+ +
{children} - {kayanMetin ? ( - - ) : null}
(null); - usePixelHeat(svgRef); + usePixelHeat(svgRef, { radius: 44 }); const COLS = 42; const ROWS = 12; + const MAKRO = 3; - // Çıktı seed'e göre deterministik; 504 iterasyon + ~150 element üretimini - // her render'da (ör. çekmece reorder'ında) tekrarlama (rendering-hoist-jsx) + // Çıktı seed'e göre deterministik; render başına yeniden üretme const pixels = useMemo(() => { const cx = (COLS - 1) / 2; const cy = (ROWS - 1) / 2; const maxD = Math.sqrt(cx * cx + cy * cy); + const BAND_MIN = COLS * 0.22; + const BAND_MAX = COLS * 0.78; const sonuc: ReactNode[] = []; let accentIdx = 0; - // Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon - // (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır - const BAND_MIN = COLS * 0.22; - const BAND_MAX = COLS * 0.78; - - for (let y = 0; y < ROWS; y++) { - for (let x = 0; x < COLS; x++) { - if (x > BAND_MIN && x < BAND_MAX) continue; - // Kenarlara doğru artan doluluk - const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD; - const density = 0.1 + 0.42 * d * d; - if (hash(x, y, seed) > density) continue; - const accent = hash(x, y, seed + 99) > 0.96; - sonuc.push( - pixelRect(x, y, { - key: `${x}-${y}`, - accent, - accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0, - }), + const kare = ( + x: number, + y: number, + n: number, + key: string, + accent: boolean, + ) => { + const boy = n * CELL - 2 * INSET; + const shared = { + x: x * CELL + INSET, + y: y * CELL + INSET, + width: boy, + height: boy, + rx: 2 * n, + }; + if (accent) { + return ( + ); } + // İ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 ( + + ); + }; + + 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; }, [seed]); @@ -156,7 +216,7 @@ export function PixelDivider({ const COLS = 12; 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 cx = (COLS - 1) / 2; const sonuc: ReactNode[] = []; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index d763cd9..b99ef37 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -2,13 +2,18 @@ import * as React from "react" 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">) { return ( {children} - + ) @@ -69,7 +69,7 @@ function SelectContent({ @@ -97,7 +97,7 @@ function SelectLabel({ return ( ) @@ -112,12 +112,12 @@ function SelectItem({ - + @@ -134,7 +134,7 @@ function SelectSeparator({ return ( ) @@ -148,7 +148,7 @@ function SelectScrollUpButton({ ) { return (