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>
249 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|