feat(ui): Arc bileşenleri, landing bölümleri ve bildirim yığını

Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart
destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program
duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 17:12:53 +03:00
parent bc920f6111
commit 3c995402c6
38 changed files with 5474 additions and 697 deletions

View File

@@ -470,6 +470,111 @@ export function PixelSira({
);
}
/**
* Piksel sahne (DESIGN.md §4.31): hero'daki dot-matrix Türkiye'nin dilini
* küçük resimlere taşır. Harita gibi satır satır bir ızgaradan çizilir:
* `#` doku pikseli, `-` silik piksel, `o` turuncu vurgu (hero'daki şehirler
* gibi nabız atar), başka her karakter boş. Doku pikselleri imleç ısı
* fırçasına katılır. Resim kartın söylediğini gösterir (süs değil); anlamı
* yanındaki başlık taşır, bu yüzden aria-hidden.
*/
export function PixelSahne({
izgara,
ton = "doku",
yaricap = 26,
className,
}: {
izgara: readonly string[];
/** "doku": hero haritasının silik mavisi. "dolgun": aynı doku koyu
* tonda (0,64–1) — rakam gibi okunması gereken şekiller için. */
ton?: "doku" | "dolgun";
/** Isı fırçasının yarıçapı (viewBox birimi; hücre 10) */
yaricap?: number;
className?: string;
}) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef, { radius: yaricap });
const cols = Math.max(...izgara.map((satir) => satir.length));
const pixels = useMemo(() => {
const opaklik = (x: number, y: number) =>
ton === "dolgun" ? 0.4 + opacityFor(x, y) * 2 : opacityFor(x, y);
const sonuc: ReactNode[] = [];
let vurgu = 0;
izgara.forEach((satir, y) => {
for (let x = 0; x < satir.length; x++) {
const c = satir[x];
if (c === "o") {
sonuc.push(
pixelRect(x, y, {
key: `${x}-${y}`,
accent: true,
accentDelay: (vurgu++ % 8) * 0.45,
}),
);
} else if (c === "#" || c === "-") {
const op =
Math.round(opaklik(x, y) * (c === "-" ? 0.5 : 1) * 100) / 100;
sonuc.push(
<rect
key={`${x}-${y}`}
x={x * CELL + INSET}
y={y * CELL + INSET}
width={DOT}
height={DOT}
rx={2}
data-px=""
data-cx={x * CELL + CELL / 2}
data-cy={y * CELL + CELL / 2}
data-op={op}
fill="currentColor"
fillOpacity={op}
/>,
);
}
}
});
return sonuc;
}, [izgara, ton]);
return (
<svg
ref={svgRef}
viewBox={`0 0 ${cols * CELL} ${izgara.length * CELL}`}
aria-hidden="true"
focusable="false"
className={cn("h-auto w-full text-primary", className)}
>
{pixels}
</svg>
);
}
// 5×7 dot-matrix rakamlar. Yeni bir rakam gerekirse ızgarası buraya eklenir.
const RAKAMLAR = {
1: ["..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###."],
2: [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"],
3: [".###.", "#...#", "....#", "..##.", "....#", "#...#", ".###."],
} as const;
/** Hero'nun piksel diliyle yazılmış iri rakam (ana sayfa adım numaraları). */
export function PixelRakam({
rakam,
className,
}: {
rakam: keyof typeof RAKAMLAR;
className?: string;
}) {
return (
<PixelSahne
izgara={RAKAMLAR[rakam]}
ton="dolgun"
yaricap={22}
className={className}
/>
);
}
/** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */
export function SectionEyebrow({
children,