fix(tasarim): piksel zemin yalnız CTA yüzeylerinde, kabın ölçüsüne göre, küçük kare yok

Bilal (24 Eyl 2026): ana sayfa CTA'sında doku hiç görünmüyordu, bazı
kartlarda çok azdı; sonra "çok fazla piksel, küçük olanlar hiç olmasın"
ve "/sonuc'taki kartın arkasına da piksel koymuşsun; pikseller CTA'larda
olacak sadece".

- Kök neden: PixelMasonry sabit 420×120 çizimi kaplayacak şekilde
  ölçekleniyordu; yüksek kartlarda görünen kısım boş banda düşüyordu.
  Artık kabın gerçek ölçüsüne göre üretiliyor (ResizeObserver), yalnız
  kenar çerçevesinde, orta boş; `haric` verilen öğenin (24'lük piksel
  tablosu) çevresi her genişlikte boş. Sunucuda boş, ölçülünce belirir.
- Küçük (tek hücre) kare yok: 4×4 makro hücrede 2–4 hücrelik kareler,
  iri olan seyrek ve silik. Ölçüm: kapanış kartı 1280 px'te 58 kare
  (tabloya binen 0), 375 px'te 27 kare.
- Beyaz içerik kartlarından zemin geri alındı (tadımlık, /listem üretim
  ve kilit kartları, boş durum, sıralama-yok, meraklısına, paket kartları,
  kaydet bandı, katalog üstü kompakt satır, /sonuc "listen hazır").
  Kalanlar: CtaBandi, PikselCtaYuzeyi, arama penceresi kartı.
  PikselZemin'in açık tonu kaldırıldı. DESIGN.md §10.8 güncellendi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 12:38:47 +03:00
parent c90c186b86
commit 613d9e012a
17 changed files with 173 additions and 144 deletions

View File

@@ -677,9 +677,11 @@ karar.
- Zemin `bg-primary` (sitenin mavisi), köşe `rounded-2xl`, yazı beyaz; alt satır da beyaz - 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 (`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 planda büyüklü küçüklü, masonry şeklinde"): 2–4 hücrelik kareler 4×4'lük makro hücrelere
kaymış düşer, çakışmaz ama hizaya girmez; orta bant (%22–78) boş, kenarlara doğru yoğun; iri kaymış düşer, çakışmaz ama hizaya girmez. Doku **kabın gerçek ölçüsüne göre** üretilir
kare daha silik; **durağan** — imleç ısı fırçasına katılmaz, üzerine gelince hiçbir şey (ResizeObserver; sabit çizimi ölçeklemek yüksek kartlarda görünen kısmı boş banda düşürüyordu,
24 Eyl 2026'da yaşandı): kısa kenarın %30'u kadar kenar çerçevesi kenara doğru yoğunlaşır,
orta tamamen boş; küçük kare yok (2–4 hücre), iri kare daha silik; **durağan** — imleç ısı fırçasına katılmaz, üzerine gelince hiçbir şey
olmaz (Bilal, 24 Eyl 2026); yalnız seyrek turuncu vurgu kareleri kendi kendine yavaşça nefes olmaz (Bilal, 24 Eyl 2026); yalnız seyrek turuncu vurgu kareleri kendi kendine yavaşça nefes
alır. Kap `relative isolate alır. Kap `relative isolate
overflow-hidden`, doku `-inset-y-6 -z-10`. Eşit ızgaralı eski `PixelField` kaldırıldı. overflow-hidden`, doku `-inset-y-6 -z-10`. Eşit ızgaralı eski `PixelField` kaldırıldı.
@@ -691,19 +693,16 @@ karar.
kart varyantı, "Kendi Listem" çekmecesinin altı (kompakt). Aynı sayfadaki iki band farklı kart varyantı, "Kendi Listem" çekmecesinin altı (kompakt). Aynı sayfadaki iki band farklı
`seed` alır. `seed` alır.
**Her CTA benzeri yüzeyin arkasında piksel zemin — `PikselZemin`, `src/components/piksel-zemin.tsx`** **Piksel zemin yalnız CTA yüzeylerinde — `PikselZemin`, `src/components/piksel-zemin.tsx`**
(Bilal, 24 Eyl 2026: "piksel arka planları her CTA benzeri kısmın arkasında olmalı, masonry gibi (Bilal, 24 Eyl 2026: önce "her CTA benzeri kısmın arkasında olmalı", sonra "/sonuc'taki kartın
büyüklü küçüklü"). CTA benzeri yüzey = tek bir aksiyona çağıran kart, bant, boş durum ya da arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece"). CTA yüzeyi = renkli zeminli
sayfa bölümü. Kap `relative isolate overflow-hidden`; zemin ilk çocuk. Açık kartta çağrı yüzeyi: mavi `CtaBandi`, koyu kapanış kartı `PikselCtaYuzeyi`, arama penceresindeki koyu
`ton="acik"` (silik mavi, `text-primary/70`), mavi bant ve koyu kartta `ton="koyu"` (beyaz). kart. Beyaz içerik kartlarına (tadımlık, `/listem` üretim/kilit kartları, paket kartı, kaydet
`CtaBandi` ve `PikselCtaYuzeyi` de bunu kullanır. Kapsananlar: `/sonuc` "24 tercihlik listen bandı, katalog üstü kompakt satır, boş durumlar, sıralama-yok sayfası) **konmaz** — 24 Eyl'de
hazır" kartı, `/listem` dört üretim kartı + boş durum + kilit kartı, tadımlık kartı, denendi ve geri alındı. Kap `relative isolate overflow-hidden`; zemin ilk çocuk; kareler beyaz.
sıralama-yok sayfası, meraklısına kapanış kartı, ana sayfa ve `/paket` ana paket kartı, kaydet Doku kabın gerçek ölçüsüne göre üretilir: yalnız kenar çerçevesinde (kısa kenarın %30'u), orta
bandı, katalog üstü kompakt satır, arama penceresi kartı. **Kapsam dışı:** form kartları (giriş, tamamen boş; **küçük kare yok**, 2–4 hücrelik (17/27/37 px) kareler, iri olan seyrek ve daha
ödeme formu), durum kartları (ödeme sonucu), modal düğmeleri, hata/404 sayfası, yan kredi silik (Bilal: "çok fazla piksel, küçük olanlar hiç olmasın").
kartları — bunlar tek aksiyona çağıran yüzey değil. Yeni bir CTA yüzeyi açan ajan zemini
ekler ve bu listeye yazar. 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 **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): sayfa kapanışı "Tercih listen, pişmanlık listesi olmasın").** Bilal'in kararı (24 Eyl 2026):
@@ -719,10 +718,9 @@ yok** (Bilal, 24 Eyl 2026: kaldırıldı); `FUNNEL_METINLERI` yalnız arama penc
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.
- Piksel tablosunun arkası boş kalır (Bilal, 24 Eyl 2026: "arkasına çok piksel yığılmış"): - Piksel tablosunun arkası boş kalır (Bilal, 24 Eyl 2026: "arkasına çok piksel yığılmış"):
telefon/tablette liste üstte ortada, dokunun boş bandında; `lg`'de liste sağ sütunda olduğu tablo kabı `haric` ile zemine verilir, doku onun çevresini (14 px payla) her genişlikte boş
için doku yalnız sol %16'da ve seyrek (`PikselZemin bosBant={[0.16, 1]} yogunluk={0.7}`). bırakır. Kural genel: bir kreatif ya da iri sayı piksel zeminin üstüne geliyorsa `haric` ile
Kural genel: bir kreatif ya da iri sayı piksel zeminin üstüne geliyorsa `bosBant` ile o boşaltılır; doku ile kreatif üst üste binmez.
bölge boşaltılır; doku ile kreatif üst üste binmez.
- Bu yüzey **sayfada bir kez** ve yalnız kapanışta durur; ara paneller ve çekmece mavi banttır. - 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. 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ı**: - Çekmece başlığında denenen "gerçek liste" pikseli (12×2, her kare tercihin riski) **kaldırıldı**:

View File

@@ -28,7 +28,6 @@ import {
SectionEyebrow, SectionEyebrow,
} from "@/components/pixel-decor"; } from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { PikselZemin } from "@/components/piksel-zemin";
import { MAX_REVIZYON, URUNLER } from "@/lib/credits"; import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami"; import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
import { import {
@@ -555,9 +554,8 @@ export default function Home() {
> >
<Reveal <Reveal
delay={0.08} delay={0.08}
className="relative isolate overflow-hidden rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm" className="rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm"
> >
<PikselZemin seed={43} />
<div className="flex flex-wrap items-baseline justify-between gap-2"> <div className="flex flex-wrap items-baseline justify-between gap-2">
<h3 className="font-heading text-xl font-bold"> <h3 className="font-heading text-xl font-bold">
{URUNLER.paket.label} {URUNLER.paket.label}

View File

@@ -73,7 +73,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
aria-label="Tercih listeni kur" aria-label="Tercih listeni kur"
className="relative isolate mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-slate-950 px-4 py-5 text-white sm:px-5" className="relative isolate mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-slate-950 px-4 py-5 text-white sm:px-5"
> >
<PikselZemin seed={61} ton="koyu" /> <PikselZemin seed={61} />
<div className="grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5"> <div className="grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5">
<div className="min-w-0"> <div className="min-w-0">
<TypingAnimation <TypingAnimation

View File

@@ -44,7 +44,7 @@ export function CtaBandi({
)} )}
{...props} {...props}
> >
<PikselZemin seed={seed} ton="koyu" className={buyuk ? "-inset-y-10" : undefined} /> <PikselZemin seed={seed} className={buyuk ? "-inset-y-10" : undefined} />
{children} {children}
</Etiket> </Etiket>
); );

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, type ReactNode } from "react"; import { useRef, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PikselZemin } from "@/components/piksel-zemin"; import { PikselZemin } from "@/components/piksel-zemin";
import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste"; import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
@@ -13,9 +13,6 @@ import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
* düğmeye ya da linke imleç/odak gelince 24 kare birden dolar — "listen * 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). * böyle hazır olur". Yazılıp silinen satır yok (Bilal, 24 Eyl 2026).
*/ */
// Geniş ekranda doku yalnız genişliğin ilk %16'sında (metnin sol boşluğu)
const SOL_KENAR: [number, number] = [0.16, 1];
export function PikselCtaYuzeyi({ export function PikselCtaYuzeyi({
children, children,
className, className,
@@ -27,6 +24,9 @@ export function PikselCtaYuzeyi({
seed?: number; seed?: number;
}) { }) {
const [tamamla, setTamamla] = useState(false); const [tamamla, setTamamla] = useState(false);
// Piksel tablosunun çevresi dokuda boş kalır (Bilal, 24 Eyl 2026:
// "tablonun arkasına çok piksel yığılmış"); ölçü gerçek yerleşimden.
const listeRef = useRef<HTMLDivElement>(null);
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;
@@ -42,23 +42,15 @@ export function PikselCtaYuzeyi({
className, className,
)} )}
> >
{/* Telefon/tablet: liste üstte ortada, dokunun boş bandında. Geniş <PikselZemin seed={seed} haric={listeRef} className="-inset-y-10" />
ekranda liste sağ sütunda — orası tamamen boş kalır, doku yalnız
sol kenarda ve seyrek (Bilal, 24 Eyl 2026: "tablonun arkasına çok
piksel yığılmış"). */}
<PikselZemin seed={seed} ton="koyu" className="-inset-y-10 lg:hidden" />
<PikselZemin
seed={seed}
ton="koyu"
bosBant={SOL_KENAR}
yogunluk={0.7}
className="-inset-y-10 hidden lg:block"
/>
<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}
</div> </div>
<div className="order-first flex flex-col items-center lg:order-none"> <div
ref={listeRef}
className="order-first flex flex-col items-center lg:order-none"
>
<PikselListe <PikselListe
tamamla={tamamla} tamamla={tamamla}
className="w-full max-w-[9rem] sm:max-w-[11rem] lg:max-w-[14rem]" className="w-full max-w-[9rem] sm:max-w-[11rem] lg:max-w-[14rem]"

View File

@@ -1,31 +1,36 @@
"use client"; "use client";
import type { RefObject } from "react";
import { PixelMasonry } from "@/components/pixel-decor"; import { PixelMasonry } from "@/components/pixel-decor";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
/** /**
* CTA yüzeylerinin piksel zemini (DESIGN.md §10.8) — tek kaynak. Bilal'in * CTA yüzeylerinin piksel zemini (DESIGN.md §10.8) — tek kaynak. Bilal'in
* kararı (24 Eyl 2026): "piksel arka planları her CTA benzeri kısmın * kararı (24 Eyl 2026): pikseller **yalnız CTA yüzeylerinde** (mavi bant,
* arkasında olmalı, masonry gibi büyüklü küçüklü." Kap `relative isolate * kapanış kartı, arama penceresi kartı); beyaz içerik kartlarında değil
* overflow-hidden` olmalı; zemin ilk çocuk olarak konur. * ("/sonuc'taki kartın arkasına da piksel koymuşsun; pikseller CTA'larda
* olacak sadece"). Beyaz kareler; kap `relative isolate overflow-hidden`
* olmalı, zemin ilk çocuk olarak konur.
* *
* - `acik`: beyaz/açık kart — silik mavi kareler. * Doku kabın gerçek ölçüsüne göre üretilir; kenar çerçevesi yoğun, orta
* - `koyu`: mavi bant ya da koyu kart — beyaz kareler. * seyrek. Üstünde kreatif ya da iri sayı varsa `haric` ile o öğe verilir,
* çevresi boş kalır.
*/ */
export function PikselZemin({ export function PikselZemin({
seed = 7, seed = 7,
ton = "acik",
bosBant,
yogunluk, yogunluk,
kenar,
haric,
className, className,
}: { }: {
/** Aynı ekrandaki iki yüzey farklı desen alsın */ /** Aynı ekrandaki iki yüzey farklı desen alsın */
seed?: number; seed?: number;
ton?: "acik" | "koyu"; /** Doluluk çarpanı (1 varsayılan) */
/** Boş bırakılacak yatay bant (varsayılan orta %22–78); bkz. PixelMasonry */
bosBant?: [number, number];
/** Doluluk çarpanı */
yogunluk?: number; yogunluk?: number;
/** Kenar çerçevesinin genişliği, kısa kenarın oranı (0.3 varsayılan) */
kenar?: number;
/** Çevresi boş bırakılacak öğe */
haric?: RefObject<HTMLElement | null>;
/** Dikey taşma payı vb. (varsayılan `-inset-y-6`) */ /** Dikey taşma payı vb. (varsayılan `-inset-y-6`) */
className?: string; className?: string;
}) { }) {
@@ -33,12 +38,16 @@ export function PikselZemin({
<div <div
aria-hidden aria-hidden
className={cn( className={cn(
"pointer-events-none absolute inset-x-0 -inset-y-6 -z-10", "pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white",
ton === "acik" ? "text-primary/70" : "text-white",
className, className,
)} )}
> >
<PixelMasonry seed={seed} bosBant={bosBant} yogunluk={yogunluk} /> <PixelMasonry
seed={seed}
yogunluk={yogunluk}
kenar={kenar}
haric={haric}
/>
</div> </div>
); );
} }

View File

@@ -1,6 +1,14 @@
"use client"; "use client";
import { useMemo, useRef, type CSSProperties, type ReactNode } from "react"; import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
type RefObject,
} from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { usePixelHeat } from "@/components/use-pixel-heat"; import { usePixelHeat } from "@/components/use-pixel-heat";
@@ -74,41 +82,86 @@ function pixelRect(
/** /**
* CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi * CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi
* hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry * hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry
* şeklinde"). İçeriğin oturduğu orta bant boş kalır; kareler kenarlara doğru * şeklinde"). Kabın **gerçek ölçüsüne** göre üretilir (ResizeObserver):
* yoğunlaşır. 3×3 hücrelik makro hücrelere 1–3 hücrelik tek kare düşer ve * sabit bir çizimi kaplayacak şekilde ölçeklemek yüksek kartlarda görünen
* makro hücre içinde kaydırılır: kareler çakışmaz ama hizaya da girmez * kısmı boş banda düşürüyordu ("hiç doku yok"). Yalnız kenar çerçevesinde,
* (§1.1 "doğal duruş"). Durağan zemin: imleç ısı fırçasına katılmaz * orta tamamen boş; `haric` verilen öğenin (ör. 24'lük piksel tablosu)
* (Bilal, 24 Eyl 2026: "üzerine gelindiğinde bir şey olmasın"). * çevresi de boş kalır. Küçük (tek hücrelik) kare **yok** (Bilal, 24 Eyl
* 2026: "çok fazla piksel, küçük olanlar hiç olmasın"): 4×4 hücrelik makro
* hücrelere 2–4 hücrelik tek kare düşer ve makro hücre içinde kaydırılır:
* çakışmaz ama hizaya da girmez (§1.1 "doğal duruş"). Durağan zemin:
* imleç ısı fırçasına katılmaz.
* Sunucuda boş çizilir, ölçülünce belirir (dekor; aria-hidden).
*/ */
// Varsayılan boş bant modül sabiti: her render'da yeni dizi olup useMemo'yu
// boşa düşürmesin (rendering-hoist-jsx)
const BOS_BANT_VARSAYILAN: [number, number] = [0.22, 0.78];
export function PixelMasonry({ export function PixelMasonry({
seed = 7, seed = 7,
bosBant = BOS_BANT_VARSAYILAN,
yogunluk = 1, yogunluk = 1,
kenar = 0.3,
haric,
className, className,
}: { }: {
seed?: number; seed?: number;
/** Genişliğin bu oranları arasındaki bant boş kalır (içerik orada) */
bosBant?: [number, number];
/** Doluluk çarpanı; 1 varsayılan, 0.6 seyrek */ /** Doluluk çarpanı; 1 varsayılan, 0.6 seyrek */
yogunluk?: number; yogunluk?: number;
/** Kenar çerçevesinin genişliği: kısa kenarın oranı (0.3 = %30) */
kenar?: number;
/** Çevresi boş bırakılacak öğe (kreatif, iri sayı) */
haric?: RefObject<HTMLElement | null>;
className?: string; className?: string;
}) { }) {
const COLS = 42; const svgRef = useRef<SVGSVGElement>(null);
const ROWS = 12; const [olcu, setOlcu] = useState<{
const MAKRO = 3; w: number;
h: number;
hx: number;
hy: number;
hw: number;
hh: number;
} | null>(null);
useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
const olc = () => {
const r = svg.getBoundingClientRect();
const hr = haric?.current?.getBoundingClientRect();
const sonraki = {
w: Math.round(r.width),
h: Math.round(r.height),
hx: hr ? Math.round(hr.left - r.left) : 0,
hy: hr ? Math.round(hr.top - r.top) : 0,
hw: hr ? Math.round(hr.width) : 0,
hh: hr ? Math.round(hr.height) : 0,
};
setOlcu((onceki) =>
onceki &&
onceki.w === sonraki.w &&
onceki.h === sonraki.h &&
onceki.hx === sonraki.hx &&
onceki.hy === sonraki.hy &&
onceki.hw === sonraki.hw &&
onceki.hh === sonraki.hh
? onceki
: sonraki,
);
};
olc();
const ro = new ResizeObserver(olc);
ro.observe(svg);
const h = haric?.current;
if (h) ro.observe(h);
return () => ro.disconnect();
}, [haric]);
const MAKRO = 4;
const PAY = 14; // hariç öğenin çevresinde bırakılan boşluk (px)
// Çıktı seed'e göre deterministik; render başına yeniden üretme
const pixels = useMemo(() => { const pixels = useMemo(() => {
const cx = (COLS - 1) / 2; if (!olcu || olcu.w === 0 || olcu.h === 0) return null;
const cy = (ROWS - 1) / 2; const { w, h, hx, hy, hw, hh } = olcu;
const maxD = Math.sqrt(cx * cx + cy * cy); const cols = Math.ceil(w / CELL);
const BAND_MIN = COLS * bosBant[0]; const rows = Math.ceil(h / CELL);
const BAND_MAX = COLS * bosBant[1]; const kisa = Math.min(w, h);
const sonuc: ReactNode[] = []; const sonuc: ReactNode[] = [];
let accentIdx = 0; let accentIdx = 0;
@@ -141,55 +194,57 @@ export function PixelMasonry({
); );
} }
// İri kare daha silik: yazının yanında bağırmasın // İ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); const op = opacityFor(x, y) * (n === 2 ? 0.9 : n === 3 ? 0.7 : 0.55);
return ( return (
<rect key={key} {...shared} fill="currentColor" fillOpacity={op} /> <rect key={key} {...shared} fill="currentColor" fillOpacity={op} />
); );
}; };
for (let my = 0; my < ROWS / MAKRO; my++) { const haricte = (px: number, py: number) =>
for (let mx = 0; mx < COLS / MAKRO; mx++) { hw > 0 &&
px >= hx - PAY &&
px <= hx + hw + PAY &&
py >= hy - PAY &&
py <= hy + hh + PAY;
for (let my = 0; my < rows / MAKRO; my++) {
for (let mx = 0; mx < cols / MAKRO; mx++) {
const ox = mx * MAKRO; const ox = mx * MAKRO;
const oy = my * MAKRO; const oy = my * MAKRO;
const mcx = ox + MAKRO / 2; const cxPx = (ox + MAKRO / 2) * CELL;
if (mcx > BAND_MIN && mcx < BAND_MAX) continue; const cyPx = (oy + MAKRO / 2) * CELL;
// Kenarlara doğru artan doluluk if (haricte(cxPx, cyPx)) continue;
const d = Math.sqrt((mcx - cx) ** 2 + (oy + MAKRO / 2 - cy) ** 2) / maxD; // En yakın kenara uzaklık, kısa kenarın oranı olarak (0 kenar, 0.5 orta)
const density = (0.3 + 0.6 * d * d) * yogunluk; const k = Math.min(cxPx, w - cxPx, cyPx, h - cyPx) / kisa;
if (hash(mx, my, seed) > density) { // Yalnız çerçevede, kenara doğru artan doluluk; orta boş
// Boş makro hücrede ara sıra tek küçük piksel: doku kopmasın if (k >= kenar) continue;
if (hash(mx, my, seed + 3) < 0.25 * yogunluk) { const density = (0.06 + 0.5 * (1 - k / kenar) ** 1.6) * yogunluk;
const px = ox + Math.floor(hash(mx, my, seed + 4) * MAKRO); if (hash(mx, my, seed) > density) continue;
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 r = hash(mx, my, seed + 7);
const n = r < 0.5 ? 1 : r < 0.85 ? 2 : 3; // 2–4 hücre: orta boy sık, iri seyrek
const n = r < 0.5 ? 2 : r < 0.85 ? 3 : 4;
const serbest = MAKRO - n; const serbest = MAKRO - n;
const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1)); 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 ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1));
const accent = n < 3 && hash(mx, my, seed + 99) > 0.94; const accent = n === 2 && hash(mx, my, seed + 99) > 0.95;
sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent)); 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, bosBant, yogunluk]); }, [olcu, seed, yogunluk, kenar]);
return ( return (
<svg <svg
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`} ref={svgRef}
preserveAspectRatio="xMidYMid slice" viewBox={olcu ? `0 0 ${olcu.w} ${olcu.h}` : undefined}
preserveAspectRatio="xMinYMin slice"
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
className={cn("h-full w-full", className)} className={cn(
"h-full w-full transition-opacity duration-500 motion-reduce:transition-none",
pixels ? "opacity-100" : "opacity-0",
className,
)}
> >
{pixels} {pixels}
</svg> </svg>

View File

@@ -6,7 +6,6 @@
// hariç her yerde görünür; kapatma oturum boyunca kalıcıdır (sessionStorage). // hariç her yerde görünür; kapatma oturum boyunca kalıcıdır (sessionStorage).
// Exit-intent/agresif popup YOK (PRD kuralı). // Exit-intent/agresif popup YOK (PRD kuralı).
import { PikselZemin } from "@/components/piksel-zemin";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
@@ -61,9 +60,8 @@ export function KaydetBanner() {
ref={gorunumRef} ref={gorunumRef}
// Telefonda metin kendi satırında, düğme altında tam genişlik: // Telefonda metin kendi satırında, düğme altında tam genişlik:
// yan yana dizince başlık kelime kelime kırılıyordu (375 px). // yan yana dizince başlık kelime kelime kırılıyordu (375 px).
className="pointer-events-auto relative isolate mx-auto flex max-w-xl flex-wrap items-center gap-3 overflow-hidden rounded-2xl border border-slate-200 bg-white p-4 pr-14 shadow-xl sm:flex-nowrap sm:gap-4" className="pointer-events-auto relative mx-auto flex max-w-xl flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 pr-14 shadow-xl sm:flex-nowrap sm:gap-4"
> >
<PikselZemin seed={47} />
{/* Kullanıcının o anki işletim sisteminin logosu: "bu tarayıcıda" {/* Kullanıcının o anki işletim sisteminin logosu: "bu tarayıcıda"
mesajını somutlaştırır */} mesajını somutlaştırır */}
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-700"> <div className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-700">

View File

@@ -1,4 +1,3 @@
import { PikselZemin } from "@/components/piksel-zemin";
import { Suspense } from "react"; import { Suspense } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -59,8 +58,7 @@ export function PaketSatinAl() {
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]" className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]"
> >
{/* Ana paket */} {/* Ana paket */}
<section className="relative isolate overflow-hidden rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm"> <section className="rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm">
<PikselZemin seed={43} />
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<h2 className="font-heading text-xl font-bold"> <h2 className="font-heading text-xl font-bold">
Tercih Dönemi Paketi Tercih Dönemi Paketi

View File

@@ -7,7 +7,6 @@
// context'ten okur. (Sihirbaz hunisi statik bir şemayla anlatılır — bkz. // context'ten okur. (Sihirbaz hunisi statik bir şemayla anlatılır — bkz.
// huni-semasi.tsx.) // huni-semasi.tsx.)
import { PikselZemin } from "@/components/piksel-zemin";
import { import {
createContext, createContext,
useContext, useContext,
@@ -235,8 +234,7 @@ export function DemoKapanisCta() {
const profil = useTercihProfili(); const profil = useTercihProfili();
return ( return (
<div className="relative isolate overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 text-center"> <div className="rounded-2xl border border-slate-200 bg-white p-6 text-center">
<PikselZemin seed={41} />
<p className="font-heading text-base font-bold text-slate-900"> <p className="font-heading text-base font-bold text-slate-900">
{profil {profil
? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı." ? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı."

View File

@@ -22,7 +22,6 @@
// Kural: action cevabının hemen ardından yönlendiriciye TEK aksiyon gönder // Kural: action cevabının hemen ardından yönlendiriciye TEK aksiyon gönder
// (router.replace); ikinci aksiyon (refresh) ancak o gezinme bitince. // (router.replace); ikinci aksiyon (refresh) ancak o gezinme bitince.
import { PikselZemin } from "@/components/piksel-zemin";
import { useEffect, useRef, useState, type ReactNode } from "react"; import { useEffect, useRef, useState, type ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -265,8 +264,7 @@ export function ListeUretici({
// Paketli kullanıcıya paket satma: onun ürünü top-up. Liste ve sohbet // Paketli kullanıcıya paket satma: onun ürünü top-up. Liste ve sohbet
// geçmişi kalıcı; kredi yükleyince kaldığı yerden devam eder. // geçmişi kalıcı; kredi yükleyince kaldığı yerden devam eder.
return ( return (
<section className="relative isolate mx-auto w-full max-w-2xl overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12"> <section className="mx-auto w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
<PikselZemin seed={23} />
<Lock className="mx-auto size-8 text-slate-400" aria-hidden /> <Lock className="mx-auto size-8 text-slate-400" aria-hidden />
<h1 className="mt-4 font-heading text-xl font-bold"> <h1 className="mt-4 font-heading text-xl font-bold">
Kredin bu liste için yetmiyor Kredin bu liste için yetmiyor
@@ -296,8 +294,7 @@ export function ListeUretici({
if (durum.ad === "hata") { if (durum.ad === "hata") {
return ( return (
<section className="relative isolate mx-auto w-full max-w-2xl overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12"> <section className="mx-auto w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
<PikselZemin seed={23} />
<h1 className="font-heading text-xl font-bold">Liste kurulamadı</h1> <h1 className="font-heading text-xl font-bold">Liste kurulamadı</h1>
<p className="mx-auto mt-2 max-w-md text-sm text-slate-600"> <p className="mx-auto mt-2 max-w-md text-sm text-slate-600">
{durum.mesaj} {durum.mesaj}
@@ -347,8 +344,7 @@ function SunucuYokEkrani() {
} }
return ( return (
<section className="relative isolate mx-auto w-full max-w-2xl overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12"> <section className="mx-auto w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
<PikselZemin seed={23} />
<h1 className="font-heading text-xl font-bold"> <h1 className="font-heading text-xl font-bold">
Listen hazır, sunucuya şu an ulaşamıyoruz Listen hazır, sunucuya şu an ulaşamıyoruz
</h1> </h1>
@@ -379,8 +375,7 @@ function SunucuYokEkrani() {
* "Tekrar dene" yok: tekrar taze requestId üretip ikinci üretimi başlatırdı. */ * "Tekrar dene" yok: tekrar taze requestId üretip ikinci üretimi başlatırdı. */
function SuruyorEkrani() { function SuruyorEkrani() {
return ( return (
<section className="relative isolate mx-auto w-full max-w-2xl overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12"> <section className="mx-auto w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
<PikselZemin seed={23} />
<h1 className="font-heading text-xl font-bold">Listen zaten hazırlanıyor</h1> <h1 className="font-heading text-xl font-bold">Listen zaten hazırlanıyor</h1>
<p className="mx-auto mt-2 max-w-md text-sm text-slate-600"> <p className="mx-auto mt-2 max-w-md text-sm text-slate-600">
Bunu başka bir sekmede başlatmışsın. İki kez kurmayalım — kredin bu Bunu başka bir sekmede başlatmışsın. İki kez kurmayalım — kredin bu

View File

@@ -4,7 +4,6 @@
// ekranda. Harita ⇄ liste köprüsü (pinden satıra, satırdan haritaya) ve // ekranda. Harita ⇄ liste köprüsü (pinden satıra, satırdan haritaya) ve
// paketsiz paywall kartları burada; veri çekimi sunucuda (page.tsx) kalır. // paketsiz paywall kartları burada; veri çekimi sunucuda (page.tsx) kalır.
import { PikselZemin } from "@/components/piksel-zemin";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { import {
@@ -338,9 +337,8 @@ export function ListemGovde({
<section <section
ref={kilitKartRef} ref={kilitKartRef}
className="relative isolate overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm" className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"
> >
<PikselZemin seed={59} />
<div className="border-b border-slate-100 p-6"> <div className="border-b border-slate-100 p-6">
<h3 className="font-heading text-xl font-bold tracking-tight"> <h3 className="font-heading text-xl font-bold tracking-tight">
Tablo ücretsiz. Tablo ücretsiz.

View File

@@ -19,7 +19,6 @@ import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske";
import type { Mesaj } from "./sohbet-client"; import type { Mesaj } from "./sohbet-client";
import { ListemGovde } from "./listem-govde"; import { ListemGovde } from "./listem-govde";
import { ListemBosCta } from "./listem-bos-cta"; import { ListemBosCta } from "./listem-bos-cta";
import { PikselZemin } from "@/components/piksel-zemin";
import { ListeUretici } from "./liste-uretici"; import { ListeUretici } from "./liste-uretici";
import { UretimBasladiOlayi } from "./uretim-basladi-olayi"; import { UretimBasladiOlayi } from "./uretim-basladi-olayi";
@@ -66,8 +65,7 @@ export async function ListemIcerik({
if (!raporVar) { if (!raporVar) {
const bosDurum = ( const bosDurum = (
<section className="relative isolate mx-auto flex max-w-2xl flex-col items-center overflow-hidden px-4 py-16 text-center"> <section className="mx-auto flex max-w-2xl flex-col items-center px-4 py-16 text-center">
<PikselZemin seed={29} />
<Sparkles className="size-10 text-primary" aria-hidden /> <Sparkles className="size-10 text-primary" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold"> <h1 className="mt-4 font-heading text-2xl font-bold">
Henüz Yapay Zeka listen yok Henüz Yapay Zeka listen yok

View File

@@ -8,7 +8,6 @@
// varsa /api/tadimlik'ten kategoriye özel satır çekilip yerinde değiştirilir // varsa /api/tadimlik'ten kategoriye özel satır çekilip yerinde değiştirilir
// (seçim sunucuda kalır). Havuzda içerik yoksa hiç render edilmez. // (seçim sunucuda kalır). Havuzda içerik yoksa hiç render edilmez.
import { PikselZemin } from "@/components/piksel-zemin";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
@@ -151,8 +150,7 @@ export function TadimlikSatiri({
</p> </p>
</div> </div>
<div className="relative isolate mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6"> <div className="mt-6 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
<PikselZemin seed={31} />
{/* Rapor listesinin satırıyla aynı düzen (§4.15): ad sarar, {/* Rapor listesinin satırıyla aynı düzen (§4.15): ad sarar,
üniversite adı altında, taban sıra sağda iri. */} üniversite adı altında, taban sıra sağda iri. */}
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">

View File

@@ -23,7 +23,6 @@ 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 { CtaBandi } from "@/components/cta-bandi"; import { CtaBandi } from "@/components/cta-bandi";
import { PikselZemin } from "@/components/piksel-zemin";
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";
@@ -374,11 +373,10 @@ export function CtaSiraForm({
<section <section
aria-labelledby={baslikId} aria-labelledby={baslikId}
className={cn( className={cn(
"relative isolate overflow-hidden rounded-2xl border p-4 transition-colors duration-200 sm:p-5", "rounded-2xl border p-4 transition-colors duration-200 sm:p-5",
profil ? "border-primary/20 bg-primary/5" : "border-slate-200 bg-white", profil ? "border-primary/20 bg-primary/5" : "border-slate-200 bg-white",
)} )}
> >
<PikselZemin seed={53} />
<div className="grid"> <div className="grid">
<div <div
ref={profil ? undefined : formRef} ref={profil ? undefined : formRef}

View File

@@ -8,7 +8,6 @@ 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 { CtaBandi } from "@/components/cta-bandi"; import { CtaBandi } from "@/components/cta-bandi";
import { PikselZemin } from "@/components/piksel-zemin";
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";
@@ -263,8 +262,7 @@ export function SihirbazBolumu({
edilmez; panel yalnızca funnel CTA'larını gösterir, üretim ve edilmez; panel yalnızca funnel CTA'larını gösterir, üretim ve
listenin kendisi /listem'de yaşar. */} listenin kendisi /listem'de yaşar. */}
{raporVar ? ( {raporVar ? (
<section className="relative isolate mt-16 overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12"> <section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
<PikselZemin seed={21} />
<h2 className="font-heading text-2xl font-bold sm:text-3xl"> <h2 className="font-heading text-2xl font-bold sm:text-3xl">
24 tercihlik listen hazır 24 tercihlik listen hazır
</h2> </h2>

View File

@@ -4,7 +4,6 @@
// sayfayı tazele (URL değişmez); yoksa kapıyı aç. Sıra bilgisi localStorage'daki // sayfayı tazele (URL değişmez); yoksa kapıyı aç. Sıra bilgisi localStorage'daki
// TercihProfili ile tüm uygulamada ortak, sunucu tablosu çerezden okur. // TercihProfili ile tüm uygulamada ortak, sunucu tablosu çerezden okur.
import { PikselZemin } from "@/components/piksel-zemin";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
@@ -43,8 +42,7 @@ export function SiraGerekli() {
} }
return ( return (
<main className="relative isolate mx-auto flex max-w-2xl flex-col items-center overflow-hidden px-4 py-24 text-center"> <main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
<PikselZemin seed={37} />
<h1 className="font-heading text-3xl font-bold text-balance text-slate-900 sm:text-4xl"> <h1 className="font-heading text-3xl font-bold text-balance text-slate-900 sm:text-4xl">
Önce sıralamanı gir Önce sıralamanı gir
</h1> </h1>