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

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

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

View File

@@ -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<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:
* 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,
)}
>
<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="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left">
{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 className="order-first flex flex-col items-center lg:order-none">
<PikselListe