) => {
+ if (e.pointerType !== "mouse") return;
+ const svg = svgRef.current;
+ if (!svg) return;
+ const r = svg.getBoundingClientRect();
+ setImlec({
+ x: ((e.clientX - r.left) / r.width) * sutun * CELL,
+ y: ((e.clientY - r.top) / r.height) * satir * CELL,
+ });
+ }, [sutun, satir]);
+
+ return (
+ setImlec(null)}
+ className={cn("h-auto w-full touch-manipulation", className)}
+ >
+ {kareler.map((k, i) => {
+ const x = (i % sutun) * CELL;
+ const y = Math.floor(i / sutun) * CELL;
+ const cx = x + CELL / 2;
+ const cy = y + CELL / 2;
+ // İmlece yakın kare büyür (en çok %18), uzaklaştıkça etkisi söner
+ let olcek = 1;
+ if (imlec && !hareketAzalt) {
+ const d = Math.hypot(imlec.x - cx, imlec.y - cy);
+ olcek = 1 + Math.max(0, 1 - d / 16) * 0.18;
+ }
+ const nabizda =
+ nabiz &&
+ Math.abs((nabiz.i % sutun) - (i % sutun)) <= 1 &&
+ Math.abs(Math.floor(nabiz.i / sutun) - Math.floor(i / sutun)) <= 1;
+ return (
+ {
+ if (e.pointerType === "mouse" || hareketAzalt) return;
+ setNabiz({ i, t: Date.now() });
+ }}
+ className={cn(
+ RENK[k],
+ "origin-center transition-[fill,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] [transform-box:fill-box] motion-reduce:transition-none",
+ nabizda && "kt-piksel-nabiz",
+ )}
+ style={{ transform: olcek !== 1 ? `scale(${olcek})` : undefined }}
+ />
+ );
+ })}
+
+ );
+}
+
+/** Renklerin metin karşılığı (§2.2: renk tek başına anlam taşımaz). */
+export function PikselListeAciklama({
+ className,
+ sayilar = { hayal: 5, dengeli: 13, guvenli: 6 },
+}: {
+ className?: string;
+ sayilar?: { hayal: number; dengeli: number; guvenli: number };
+}) {
+ const oge = (renk: string, sayi: number, etiket: string): ReactNode => (
+
+
+ {sayi} {etiket.toLocaleLowerCase("tr-TR")}
+
+ );
+ return (
+
+ {oge("bg-red-500", sayilar.hayal, DILIM_ETIKET.hayal)}
+ {oge("bg-amber-400", sayilar.dengeli, DILIM_ETIKET.dengeli)}
+ {oge("bg-emerald-500", sayilar.guvenli, DILIM_ETIKET.garanti)}
+
+ );
+}
diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx
index dd066b1..c2d6ea5 100644
--- a/src/features/liste/components/liste-cekmecesi.tsx
+++ b/src/features/liste/components/liste-cekmecesi.tsx
@@ -15,7 +15,7 @@ import { useState } from "react";
import { toast } from "sonner";
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { PixelField } from "@/components/pixel-decor";
+import { PikselListe } from "@/components/piksel-liste";
import { UniLogo } from "@/components/uni-logo";
import { RiskEtiketi } from "@/components/risk-etiketi";
import { Button } from "@/components/ui/button";
@@ -122,7 +122,6 @@ export function ListeCekmecesi() {
// drawer içinde erken ölçüm satırları kaydırıyordu
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
const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const)
.map((r) => [r, liste.filter((t) => t.risk === r).length] as const)
@@ -185,55 +184,66 @@ export function ListeCekmecesi() {
Kendi Listem
- {/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3) */}
-
- {liste.length === MANUEL_LISTE_MAX ? (
- // 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.
-
-
- Listen tamam · {liste.length} / {MANUEL_LISTE_MAX}
-
- ) : (
- <>
- {liste.length} / {MANUEL_LISTE_MAX} tercih
- >
- )}
-
-
-
-
- {/* Listenin dengesi metinle (renk tek başına değil, §2.2) */}
- {riskSayilari.length > 0 ? (
-
- {riskSayilari.map(([seviye, adet]) => (
-
-
-
- {adet}
+ {/* Durum satırı: solda listenin piksel hâli (ilerleme çubuğunun
+ yerine), sağda sayı ve risk dağılımı — dikine uzamasın diye
+ yan yana (DESIGN.md §5.3). Durum ekran okuyucuya duyurulur. */}
+
+
+
+ t.risk === "riskli"
+ ? "hayal"
+ : t.risk === "az-riskli"
+ ? "dengeli"
+ : t.risk === "guvenli"
+ ? "guvenli"
+ : "belirsiz",
+ )}
+ sutun={12}
+ />
+
+
+
+ {liste.length === MANUEL_LISTE_MAX ? (
+ // 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.
+
+
+ Listen tamam · {liste.length} / {MANUEL_LISTE_MAX}
-
- ))}
-
- ) : null}
+ ) : (
+ <>
+ {liste.length} / {MANUEL_LISTE_MAX} tercih
+ >
+ )}
+
+ {riskSayilari.length > 0 ? (
+
+ {riskSayilari.map(([seviye, adet]) => (
+
+
+
+ {adet}
+
+
+ ))}
+
+ ) : null}
+
+
{/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli
programlar). Yalnız riskleri bilinen ve en az 6 tercihli
listede; vaat değil, gerekçe. */}
{guvenliYok ? (
- Güvenli program yok; alta birkaç tane eklemek açıkta kalma riskini
- azaltır.
+ Güvenli yok; alta birkaç tane eklemek riski azaltır.
) : null}
@@ -325,18 +335,14 @@ export function ListeCekmecesi() {
{/* 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
yiyordu (24 Eyl 2026, 375 px ölçümü). */}
-
-
-
+ {/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın
+ koyu zemini; listenin piksel hâli başlıkta. */}
+
+
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
-
+
{riskEksik
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
@@ -345,7 +351,7 @@ export function ListeCekmecesi() {
{riskEksik ? (
{
cekmeceKapat();
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
@@ -361,7 +367,7 @@ export function ListeCekmecesi() {
Listemde aç
diff --git a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
index 5777d3c..9215a00 100644
--- a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
+++ b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
@@ -7,7 +7,7 @@ import { ArrowRight } from "lucide-react";
import { animate } from "motion/mini";
import { useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button";
-import { PixelField } from "@/components/pixel-decor";
+import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
import type { SihirbazFacetleri } from "@/types/yokatlas";
@@ -279,70 +279,57 @@ export function SihirbazBolumu({
) : girisBekliyor ? (
-
-
-
- Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
-
-
- Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
- tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
- seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3
- kredi, kalan 2 kredinle danışmana soru sorarsın).
-
-
-
- olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
- }
+
+
+
+ Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
+
+
+ Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
+ tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
+ seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi,
+ kalan 2 kredinle danışmana soru sorarsın).
+
+
- 5 deneme kredinle Yapay Zeka listeni kur — giriş yap
-
-
-
-
- Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
-
-
+
+ olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
+ }
+ >
+ 5 deneme kredinle Yapay Zeka listeni kur — giriş yap
+
+
+
+
+ Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
+
+
+
) : (
-
-
-
+
+
24 tercihi tek tek elemek yerine, yapay zekâ kursun
-
+
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
- alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli
- ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı
- yerleşme riski ve gerekçesiyle — senin yerine dizer.
+ alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve
+ güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme
+ riski ve gerekçesiyle — senin yerine dizer.
Yapay Zeka listemi kur
-
+
)}
{monteEdildi ? (
diff --git a/src/lib/funnel-metinleri.ts b/src/lib/funnel-metinleri.ts
new file mode 100644
index 0000000..d296c39
--- /dev/null
+++ b/src/lib/funnel-metinleri.ts
@@ -0,0 +1,36 @@
+/**
+ * CTA yüzeylerinde yazılıp silinen kısa satırlar (arama penceresindeki
+ * kart ve PikselCtaYuzeyi). "use client" yok: sunucu da okuyabilsin (§9.3).
+ */
+export const FUNNEL_METINLERI: readonly string[] = [
+ "Sıralamana en uygun listeyi birlikte kuralım.",
+ "Geleceğin 24 satırlık bir plana sığar.",
+ "Gerçek YÖK Atlas verisiyle güvenle seç.",
+ "Her tercihinin bir nedeni olsun.",
+ "Riskleri biz hesaplayalım, kararı sen ver.",
+ "Sana uygun bölümler bir adım uzağında.",
+ "Dengeli bir liste, rahat bir tercih dönemi.",
+ "Tercihlerini şansa bırakma.",
+ "Doğru strateji, doğru yerleşme.",
+ "Hayalindeki bölüm için doğru plan.",
+ "Verilerle konuşan bir tercih listesi.",
+ "Üç adımda sana özel 24 tercih.",
+ "Tercih dönemini stressiz geçir.",
+ "Geçen yılın verisi, senin avantajın.",
+ "Güvenli ve iddialı tercihleri dengele.",
+ "Sıralaman ne söylüyor? Birlikte bakalım.",
+ "Her satırı senin için hesaplıyoruz.",
+ "Akıllı tercih, içi rahat bir bekleyiş.",
+ "Yerleşme riskini görerek seç.",
+ "Sana özel liste birkaç dakikada hazır.",
+ "İlgi alanların ve şehrin, tek listede.",
+ "Boş kalan tercih, kaçan fırsattır.",
+ "Uzman işi bir liste, senin onayınla.",
+ "Kararını veriye dayandır.",
+ "Doğru tercih, doğru bilgiyle başlar.",
+ "Listeni bugün kur, için rahat olsun.",
+ "Yerleşme riskin renklerle önünde.",
+ "En önemli 24 kararını birlikte verelim.",
+ "Senin sıralaman, sana özel plan.",
+ "İyi bir liste, iyi bir başlangıçtır.",
+];