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>
598 lines
17 KiB
TypeScript
598 lines
17 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
useEffect,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
type CSSProperties,
|
||
type ReactNode,
|
||
type RefObject,
|
||
} from "react";
|
||
import { cn } from "@/lib/utils";
|
||
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||
|
||
/**
|
||
* Hero'daki dot-matrix dilini (TurkeyPixelMap) bölüm ayraçlarına ve
|
||
* dekorlara taşıyan piksel süsleri. Rastgelelik yerine deterministik
|
||
* hash kullanılır ki sunucu ve istemci aynı deseni üretsin.
|
||
*
|
||
* Hepsi hero'daki imleç ısı fırçasına katılır (usePixelHeat): imleç
|
||
* yaklaşınca pikseller turuncuya ısınır, uzaklaşınca yavaşça soğur.
|
||
* Turuncu vurgu (accent) pikselleri hero'daki şehirler gibi fırçaya
|
||
* katılmaz.
|
||
*/
|
||
|
||
// Hero ile aynı ızgara ölçüleri
|
||
const CELL = 10;
|
||
const DOT = 7.4;
|
||
const INSET = (CELL - DOT) / 2;
|
||
|
||
// Hero ile aynı doku paleti
|
||
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||
const opacityFor = (x: number, y: number) =>
|
||
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
|
||
|
||
// Deterministik sözde-rastgele: aynı girdi her ortamda aynı deseni verir
|
||
const hash = (x: number, y: number, seed: number) => {
|
||
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||
return n - Math.floor(n);
|
||
};
|
||
|
||
function pixelRect(
|
||
x: number,
|
||
y: number,
|
||
extra: { key: string; accent?: boolean; accentDelay?: number },
|
||
) {
|
||
const shared = {
|
||
x: x * CELL + INSET,
|
||
y: y * CELL + INSET,
|
||
width: DOT,
|
||
height: DOT,
|
||
rx: 2,
|
||
};
|
||
if (extra.accent) {
|
||
return (
|
||
<rect
|
||
key={extra.key}
|
||
{...shared}
|
||
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
|
||
style={{
|
||
animationDuration: "3.6s",
|
||
animationDelay: `${extra.accentDelay ?? 0}s`,
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
const op = opacityFor(x, y);
|
||
return (
|
||
<rect
|
||
key={extra.key}
|
||
{...shared}
|
||
data-px=""
|
||
data-cx={x * CELL + CELL / 2}
|
||
data-cy={y * CELL + CELL / 2}
|
||
data-op={op}
|
||
fill="currentColor"
|
||
fillOpacity={op}
|
||
/>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
* şeklinde"). Kabın **gerçek ölçüsüne** göre üretilir (ResizeObserver):
|
||
* sabit bir çizimi kaplayacak şekilde ölçeklemek yüksek kartlarda görünen
|
||
* kısmı boş banda düşürüyordu ("hiç doku yok"). Yalnız kenar çerçevesinde,
|
||
* orta tamamen boş; `haric` verilen öğenin (ör. 24'lük piksel tablosu)
|
||
* ç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).
|
||
*/
|
||
export function PixelMasonry({
|
||
seed = 7,
|
||
yogunluk = 1,
|
||
kenar = 0.3,
|
||
haric,
|
||
className,
|
||
}: {
|
||
seed?: number;
|
||
/** Doluluk çarpanı; 1 varsayılan, 0.6 seyrek */
|
||
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;
|
||
}) {
|
||
const svgRef = useRef<SVGSVGElement>(null);
|
||
const [olcu, setOlcu] = useState<{
|
||
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)
|
||
|
||
const pixels = useMemo(() => {
|
||
if (!olcu || olcu.w === 0 || olcu.h === 0) return null;
|
||
const { w, h, hx, hy, hw, hh } = olcu;
|
||
const cols = Math.ceil(w / CELL);
|
||
const rows = Math.ceil(h / CELL);
|
||
const kisa = Math.min(w, h);
|
||
const sonuc: ReactNode[] = [];
|
||
let accentIdx = 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 (
|
||
<rect
|
||
key={key}
|
||
{...shared}
|
||
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
|
||
style={{
|
||
animationDuration: "3.6s",
|
||
animationDelay: `${(accentIdx++ % 8) * 0.45}s`,
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
// İri kare daha silik: yazının yanında bağırmasın
|
||
const op = opacityFor(x, y) * (n === 2 ? 0.9 : n === 3 ? 0.7 : 0.55);
|
||
return (
|
||
<rect key={key} {...shared} fill="currentColor" fillOpacity={op} />
|
||
);
|
||
};
|
||
|
||
const haricte = (px: number, py: number) =>
|
||
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 oy = my * MAKRO;
|
||
const cxPx = (ox + MAKRO / 2) * CELL;
|
||
const cyPx = (oy + MAKRO / 2) * CELL;
|
||
if (haricte(cxPx, cyPx)) continue;
|
||
// En yakın kenara uzaklık, kısa kenarın oranı olarak (0 kenar, 0.5 orta)
|
||
const k = Math.min(cxPx, w - cxPx, cyPx, h - cyPx) / kisa;
|
||
// Yalnız çerçevede, kenara doğru artan doluluk; orta boş
|
||
if (k >= kenar) continue;
|
||
const density = (0.06 + 0.5 * (1 - k / kenar) ** 1.6) * yogunluk;
|
||
if (hash(mx, my, seed) > density) continue;
|
||
const r = hash(mx, my, seed + 7);
|
||
// 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 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 === 2 && hash(mx, my, seed + 99) > 0.95;
|
||
sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent));
|
||
}
|
||
}
|
||
return sonuc;
|
||
}, [olcu, seed, yogunluk, kenar]);
|
||
|
||
return (
|
||
<svg
|
||
ref={svgRef}
|
||
viewBox={olcu ? `0 0 ${olcu.w} ${olcu.h}` : undefined}
|
||
preserveAspectRatio="xMinYMin slice"
|
||
aria-hidden="true"
|
||
focusable="false"
|
||
className={cn(
|
||
"h-full w-full transition-opacity duration-500 motion-reduce:transition-none",
|
||
pixels ? "opacity-100" : "opacity-0",
|
||
className,
|
||
)}
|
||
>
|
||
{pixels}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Bölümler arasında sert border çizgileri yerine kullanılan, ortada
|
||
* yoğunlaşıp kenarlara doğru dağılan piksel ayraç. Hero'nun dot-matrix
|
||
* dilini bölüm geçişlerine taşır.
|
||
*/
|
||
export function PixelDivider({
|
||
seed = 5,
|
||
className,
|
||
}: {
|
||
seed?: number;
|
||
className?: string;
|
||
}) {
|
||
const svgRef = useRef<SVGSVGElement>(null);
|
||
usePixelHeat(svgRef);
|
||
|
||
const COLS = 12;
|
||
const ROWS = 2;
|
||
|
||
// Deterministik çıktı — render başına yeniden üretme (PixelMasonry ile aynı)
|
||
const pixels = useMemo(() => {
|
||
const cx = (COLS - 1) / 2;
|
||
const sonuc: ReactNode[] = [];
|
||
let accentPlaced = false;
|
||
|
||
for (let y = 0; y < ROWS; y++) {
|
||
for (let x = 0; x < COLS; x++) {
|
||
// Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme
|
||
const dx = Math.abs(x - cx) / cx;
|
||
const rowFalloff = y === 0 ? 0.9 : 0.35;
|
||
const density = (1 - dx) ** 1.1 * rowFalloff;
|
||
if (hash(x, y, seed) > density) continue;
|
||
const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8;
|
||
if (accent) accentPlaced = true;
|
||
sonuc.push(
|
||
pixelRect(x, y, {
|
||
key: `${x}-${y}`,
|
||
accent,
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
return sonuc;
|
||
}, [seed]);
|
||
|
||
return (
|
||
<svg
|
||
ref={svgRef}
|
||
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
|
||
aria-hidden="true"
|
||
focusable="false"
|
||
className={cn("h-auto w-full text-primary", className)}
|
||
>
|
||
{pixels}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/** Alt sayfa başlıklarını içerikten ayıran kompakt piksel imzası. */
|
||
export function PagePixelDivider({
|
||
seed = 5,
|
||
className,
|
||
}: {
|
||
seed?: number;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<div aria-hidden="true" className={cn("w-40", className)}>
|
||
<PixelDivider seed={seed} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Küçük sabit süslerde (mark/korner) data-px öznitelikli rect. */
|
||
function fixedPixel(x: number, y: number, opacity: number) {
|
||
return (
|
||
<rect
|
||
x={x + INSET}
|
||
y={y + INSET}
|
||
width={DOT}
|
||
height={DOT}
|
||
rx={2}
|
||
data-px=""
|
||
data-cx={x + CELL / 2}
|
||
data-cy={y + CELL / 2}
|
||
data-op={opacity}
|
||
fill="currentColor"
|
||
fillOpacity={opacity}
|
||
/>
|
||
);
|
||
}
|
||
|
||
/** PixelMark pikseli: kendi kendine, sırası geldiğinde ısınıp sönen rect. */
|
||
function markPixel(x: number, y: number, opacity: number, delay: number) {
|
||
return (
|
||
<rect
|
||
x={x + INSET}
|
||
y={y + INSET}
|
||
width={DOT}
|
||
height={DOT}
|
||
rx={2}
|
||
className="animate-pixel-isi motion-reduce:animate-none"
|
||
fill="currentColor"
|
||
fillOpacity={opacity}
|
||
style={
|
||
{ "--px-op": opacity, animationDelay: `${delay}s` } as CSSProperties
|
||
}
|
||
/>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Eyebrow rozetlerinde kullanılan 2×2'lik minik piksel imzası.
|
||
* Pikseller imleç fırçasına katılmak yerine saat yönünde sırayla
|
||
* ısınıp söner; ikisi aynı fill/fillOpacity'yi sürdüğü için çakışır.
|
||
*/
|
||
export function PixelMark({ className }: { className?: string }) {
|
||
return (
|
||
<svg
|
||
viewBox="0 0 20 20"
|
||
aria-hidden="true"
|
||
focusable="false"
|
||
className={cn("size-3.5 shrink-0", className)}
|
||
>
|
||
{markPixel(0, 0, 0.35, 0)}
|
||
<rect
|
||
x={11.3}
|
||
y={1.3}
|
||
width={7.4}
|
||
height={7.4}
|
||
rx={2}
|
||
className="animate-pixel-isi-accent fill-orange-500 motion-reduce:animate-none"
|
||
fillOpacity={0.85}
|
||
style={{ animationDelay: "0.6s" }}
|
||
/>
|
||
{markPixel(10, 10, 0.18, 1.2)}
|
||
{markPixel(0, 10, 0.6, 1.8)}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/** Kart köşelerine iliştirilen sessiz piksel kümesi. */
|
||
export function PixelCorner({ className }: { className?: string }) {
|
||
const svgRef = useRef<SVGSVGElement>(null);
|
||
usePixelHeat(svgRef, { radius: 28 });
|
||
|
||
return (
|
||
<svg
|
||
ref={svgRef}
|
||
viewBox="0 0 30 30"
|
||
aria-hidden="true"
|
||
focusable="false"
|
||
className={cn("size-7 text-primary", className)}
|
||
>
|
||
{fixedPixel(20, 0, 0.5)}
|
||
{fixedPixel(10, 0, 0.14)}
|
||
{fixedPixel(20, 10, 0.26)}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Bir sıranın yerini tek satır piksel şeridinde gösteren sessiz gösterge
|
||
* (ör. URAP'ta 198 üniversite içinde 5.). Şerit soldan (1.) sağa
|
||
* (sonuncu) dilimlere bölünür; sıranın düştüğü dilim hero'daki şehirler
|
||
* gibi turuncu yanar, geri kalanı hero dokusunda durur ve imleç fırçasına
|
||
* katılır. Dekoratiftir: anlamı yanındaki metin taşır (renk tek başına
|
||
* anlam taşımaz), bu yüzden aria-hidden.
|
||
*/
|
||
export function PixelSira({
|
||
sira,
|
||
toplam,
|
||
hucre = 8,
|
||
zemin = "acik",
|
||
className,
|
||
}: {
|
||
sira: number;
|
||
toplam: number;
|
||
hucre?: number;
|
||
/** "koyu": renkli dolu zemin üstünde (ör. turuncu gösterge karesi) —
|
||
* işaret beyaz yanar, doku zeminin üstünde beyaz kalır. */
|
||
zemin?: "acik" | "koyu";
|
||
className?: string;
|
||
}) {
|
||
const svgRef = useRef<SVGSVGElement>(null);
|
||
// Isı fırçası turuncuya ısıtır; turuncu zeminde görünmez, orada kapalı.
|
||
usePixelHeat(svgRef, { radius: zemin === "koyu" ? 0 : 28 });
|
||
const yer = Math.min(
|
||
hucre - 1,
|
||
Math.max(0, Math.floor(((sira - 1) / toplam) * hucre)),
|
||
);
|
||
|
||
return (
|
||
<svg
|
||
ref={svgRef}
|
||
viewBox={`0 0 ${hucre * CELL} ${CELL}`}
|
||
aria-hidden="true"
|
||
focusable="false"
|
||
className={cn("h-3.5 w-auto shrink-0 text-primary", className)}
|
||
>
|
||
{Array.from({ length: hucre }, (_, i) =>
|
||
i === yer ? (
|
||
<rect
|
||
key={i}
|
||
x={i * CELL + INSET}
|
||
y={INSET}
|
||
width={DOT}
|
||
height={DOT}
|
||
rx={2}
|
||
className={zemin === "koyu" ? "fill-white" : "fill-orange-500"}
|
||
fillOpacity={zemin === "koyu" ? 1 : 0.85}
|
||
/>
|
||
) : (
|
||
<g key={i}>{fixedPixel(i * CELL, 0, opacityFor(i, 0) + 0.1)}</g>
|
||
),
|
||
)}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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,
|
||
className,
|
||
}: {
|
||
children: ReactNode;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<span
|
||
className={cn(
|
||
"inline-flex items-center gap-2 rounded-full border border-slate-200 bg-card px-3.5 py-1.5 text-xs font-semibold text-primary",
|
||
className,
|
||
)}
|
||
>
|
||
<PixelMark />
|
||
{children}
|
||
</span>
|
||
);
|
||
}
|