Files
aragrp/components/blog-blockquote-decorations.tsx
T
alirez.brandCursor a47ec493c5 feat: add version history drawer with release notes
Introduce a footer-triggered Version History drawer with categorized release notes, hardcoded release data, and semver tracking at v0.3.0. Also ships blog blockquote/table styling, site-wide typography, home page image updates, header refinements, and related bug fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 02:46:20 +03:30

249 lines
6.1 KiB
TypeScript

import type { BlockquoteVariant } from "@/lib/blog-blocks";
import type { ReactNode } from "react";
const STROKE = 0.9;
const BASE = 12;
type ShapeType =
| "cross"
| "triangle"
| "dot9"
| "square"
| "diamond"
| "dot6"
| "circle";
type LayoutEntry = {
x: number;
y: number;
color: string;
type: ShapeType;
/** 0.2 = کوچک … 0.55 = بزرگ‌تر */
scale: number;
};
type DecorShape = {
x: number;
y: number;
color: string;
node: ReactNode;
};
function unit(scale: number) {
return BASE * scale;
}
function hollowCircle(r: number) {
return (
<circle cx="0" cy="0" r={r} fill="none" stroke="currentColor" strokeWidth={STROKE} />
);
}
function triangle(size: number) {
return (
<polygon
points={`0,${-size} ${size},${size} ${-size},${size}`}
fill="none"
stroke="currentColor"
strokeWidth={STROKE}
strokeLinejoin="round"
/>
);
}
function cross(size: number) {
return (
<>
<line
x1={-size}
y1={-size}
x2={size}
y2={size}
stroke="currentColor"
strokeWidth={STROKE}
strokeLinecap="round"
/>
<line
x1={size}
y1={-size}
x2={-size}
y2={size}
stroke="currentColor"
strokeWidth={STROKE}
strokeLinecap="round"
/>
</>
);
}
function square(size: number) {
return (
<rect
x={-size}
y={-size}
width={size * 2}
height={size * 2}
fill="none"
stroke="currentColor"
strokeWidth={STROKE}
/>
);
}
function diamond(size: number) {
return (
<rect
x={-size}
y={-size}
width={size * 2}
height={size * 2}
fill="none"
stroke="currentColor"
strokeWidth={STROKE}
transform="rotate(45)"
/>
);
}
function dotPattern(cols: number, rows: number, gap: number, dotR: number) {
const dots: ReactNode[] = [];
const offsetX = ((cols - 1) * gap) / 2;
const offsetY = ((rows - 1) * gap) / 2;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
dots.push(
<circle
key={`${row}-${col}`}
cx={col * gap - offsetX}
cy={row * gap - offsetY}
r={dotR}
fill="currentColor"
/>
);
}
}
return <>{dots}</>;
}
function buildShape(type: ShapeType, scale: number): ReactNode {
const u = unit(scale);
switch (type) {
case "circle":
return hollowCircle(u * 0.85);
case "cross":
return cross(u * 0.78);
case "triangle":
return triangle(u * 0.82);
case "square":
return square(u * 0.72);
case "diamond":
return diamond(u * 0.68);
case "dot9":
return dotPattern(3, 3, u * 0.26, u * 0.075);
case "dot6":
return dotPattern(3, 2, u * 0.3, u * 0.085);
}
}
function toDecorShapes(entries: LayoutEntry[]): DecorShape[] {
return entries.map((entry) => ({
x: entry.x,
y: entry.y,
color: entry.color,
node: buildShape(entry.type, entry.scale),
}));
}
const SHAPE_ORDER: ShapeType[] = [
"cross",
"triangle",
"dot9",
"square",
"diamond",
"dot6",
"circle",
];
function swapTypes(entries: LayoutEntry[], offset: number): LayoutEntry[] {
return entries.map((entry) => {
const index = SHAPE_ORDER.indexOf(entry.type);
return {
...entry,
type: SHAPE_ORDER[(index + offset) % SHAPE_ORDER.length],
};
});
}
/** چیدمان پایه (#2 سبز) — موقعیت، اندازه و رنگ */
const layoutGreenBase: LayoutEntry[] = [
{ x: 42, y: 11, color: "var(--primary)", type: "diamond", scale: 0.38 },
{ x: 44, y: 12, color: "var(--accent)", type: "circle", scale: 0.3 },
{ x: 72, y: 10, color: "var(--secondary)", type: "dot6", scale: 0.4 },
{ x: 104, y: 13, color: "var(--muted-foreground)", type: "cross", scale: 0.32 },
{ x: 149, y: 11, color: "var(--primary)", type: "dot9", scale: 0.36 },
{ x: 186, y: 12, color: "var(--accent)", type: "triangle", scale: 0.42 },
{ x: 198, y: 28, color: "var(--secondary)", type: "square", scale: 0.34 },
{ x: 40, y: 27, color: "var(--accent)", type: "triangle", scale: 0.36 },
{ x: 38, y: 47, color: "var(--muted-foreground)", type: "dot9", scale: 0.32 },
{ x: 41, y: 66, color: "var(--primary)", type: "circle", scale: 0.28 },
{ x: 39, y: 87, color: "var(--secondary)", type: "cross", scale: 0.38 },
{ x: 42, y: 105, color: "var(--accent)", type: "diamond", scale: 0.33 },
{ x: 200, y: 48, color: "var(--primary)", type: "square", scale: 0.36 },
{ x: 203, y: 62, color: "var(--secondary)", type: "dot6", scale: 0.42 },
{ x: 198, y: 78, color: "var(--muted-foreground)", type: "cross", scale: 0.26 },
{ x: 201, y: 92, color: "var(--accent)", type: "dot9", scale: 0.35 },
{ x: 28, y: 109, color: "var(--secondary)", type: "cross", scale: 0.3 },
{ x: 68, y: 108, color: "var(--primary)", type: "square", scale: 0.38 },
{ x: 128, y: 109, color: "var(--accent)", type: "dot6", scale: 0.34 },
{ x: 183, y: 107, color: "var(--muted-foreground)", type: "triangle", scale: 0.28 },
{ x: 205, y: 109, color: "var(--primary)", type: "circle", scale: 0.4 },
{ x: 81, y: 41, color: "var(--primary)", type: "circle", scale: 0.22 },
{ x: 167, y: 78, color: "var(--secondary)", type: "triangle", scale: 0.26 },
];
const layoutGreen = toDecorShapes(layoutGreenBase);
const layoutYellow = toDecorShapes(swapTypes(layoutGreenBase, 2));
const layoutGray = toDecorShapes(swapTypes(layoutGreenBase, 4));
const DECORATION_LAYOUTS: Record<BlockquoteVariant, DecorShape[]> = {
yellow: layoutYellow,
green: layoutGreen,
gray: layoutGray,
};
interface BlogBlockquoteDecorationsProps {
variant: BlockquoteVariant;
}
export function BlogBlockquoteDecorations({
variant,
}: BlogBlockquoteDecorationsProps) {
const shapes = DECORATION_LAYOUTS[variant];
return (
<svg
className="blog-blockquote__decorations"
viewBox="0 0 240 120"
preserveAspectRatio="xMidYMid slice"
aria-hidden
>
{shapes.map((shape, index) => (
<g
key={index}
transform={`translate(${shape.x}, ${shape.y})`}
style={{ color: shape.color }}
>
{shape.node}
</g>
))}
</svg>
);
}