Clip content-template ornaments at the frame so they do not overlap copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-18 22:11:02 -06:00
co-authored by Cursor
parent 63842b31c8
commit 6820315aaa
6 changed files with 143 additions and 112 deletions
@@ -0,0 +1,72 @@
/**
* Figma: Content page Template body ornaments (19003:23305).
* Hidden below md (640). From md, shapes sit in the side gutters and the
* page frame clips anything that bleeds past the viewport.
*
* - 19003:23575 / 19000:23008 — right
* - 19003:23576 / 19000:23009 — left
*/
import {
ASSETS,
getAssetPath,
howItWorksOrnamentLeftPath,
howItWorksOrnamentRightPath,
} from "../../../lib/assetUtils";
type ContentTemplateDecorativeShapesVariant = "guide" | "article";
type ContentTemplateDecorativeShapesProps = {
variant: ContentTemplateDecorativeShapesVariant;
};
const RIGHT_GUIDE_CLASS =
"pointer-events-none absolute z-0 hidden aspect-[155/250] md:block left-[87.19%] right-0 top-[266px] lg:left-[84.86%] lg:top-[255px]";
const LEFT_GUIDE_CLASS =
"pointer-events-none absolute z-0 hidden aspect-[119/136] md:block left-0 right-[88.38%] top-[811px]";
const RIGHT_ARTICLE_CLASS =
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[87.19%] right-[-8.13%] top-[266px] lg:left-[84.86%] lg:right-[-9.28%] lg:top-[255px]";
const LEFT_ARTICLE_CLASS =
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[811px]";
export default function ContentTemplateDecorativeShapes({
variant,
}: ContentTemplateDecorativeShapesProps) {
const isArticle = variant === "article";
return (
<>
<div
aria-hidden
className={isArticle ? RIGHT_ARTICLE_CLASS : RIGHT_GUIDE_CLASS}
data-node-id="19003:23575"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={
isArticle
? getAssetPath(ASSETS.CONTENT_SHAPE_1)
: getAssetPath(howItWorksOrnamentRightPath())
}
alt=""
className="pointer-events-none size-full max-w-none object-contain"
/>
</div>
<div
aria-hidden
className={isArticle ? LEFT_ARTICLE_CLASS : LEFT_GUIDE_CLASS}
data-node-id="19003:23576"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={
isArticle
? getAssetPath(ASSETS.CONTENT_SHAPE_2)
: getAssetPath(howItWorksOrnamentLeftPath())
}
alt=""
className="pointer-events-none size-full max-w-none object-contain"
/>
</div>
</>
);
}
+15 -48
View File
@@ -10,7 +10,7 @@ import {
import { logger } from "../../../../lib/logger"; import { logger } from "../../../../lib/logger";
import ContentBanner from "../../../components/sections/ContentBanner"; import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer"; import AskOrganizer from "../../../components/sections/AskOrganizer";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils"; import ContentTemplateDecorativeShapes from "../../_components/ContentTemplateDecorativeShapes";
import "../blog.css"; import "../blog.css";
// Code split RelatedArticles - blog-specific, below the fold // Code split RelatedArticles - blog-specific, below the fold
@@ -205,58 +205,25 @@ export default async function BlogPostPage({ params }: PageProps) {
className="relative min-h-screen overflow-x-clip" className="relative min-h-screen overflow-x-clip"
style={{ backgroundColor }} style={{ backgroundColor }}
> >
{/* Content Banner */}
<ContentBanner post={post} /> <ContentBanner post={post} />
{/* Decorative Shapes */} <div className="relative w-full overflow-x-clip">
{/* Right Side Shape (3/4 up the page) */} <ContentTemplateDecorativeShapes variant="article" />
<div
className="hidden md:block absolute top-1/4 right-0 pointer-events-none z-10"
style={{ transform: "translateX(40%)" }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(ASSETS.CONTENT_SHAPE_1)}
alt=""
className="w-auto h-auto max-w-none"
style={{
width: "clamp(120px, 15vw, 200px)",
height: "auto",
}}
/>
</div>
{/* Left Side Shape (3/4 down the page) */} <article
<div data-node-id="19031:10426"
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10" className="
style={{ transform: "translateX(-10%)" }} relative z-10 flex w-full justify-center
> p-[var(--spacing-scale-024)]
{/* eslint-disable-next-line @next/next/no-img-element */} sm:px-0 sm:py-[var(--spacing-scale-032)]
<img "
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)} >
alt="" <div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
className="w-auto h-auto max-w-none" <div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
style={{ </div>
width: "clamp(100px, 12vw, 180px)", </article>
height: "auto",
}}
/>
</div> </div>
{/* Main Content — Figma Content page Template (19003:23305) article body instances */}
<article
data-node-id="19031:10426"
className="
relative z-[2] flex w-full justify-center
p-[var(--spacing-scale-024)]
sm:px-0 sm:py-[var(--spacing-scale-032)]
"
>
<div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
</div>
</article>
{/* Related Articles Section */} {/* Related Articles Section */}
<RelatedArticles <RelatedArticles
relatedPosts={relatedArticles} relatedPosts={relatedArticles}
@@ -1,43 +0,0 @@
/**
* Figma: "A Guide to CommunityRule" body ornaments (22078:791901)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22078-791901
*
* - 19003:23575 — concentric circles, right (`how-shape-2.svg`)
* - 19003:23576 — loop mark, left (`how-shape-1.svg`)
*/
import {
getAssetPath,
howItWorksOrnamentLeftPath,
howItWorksOrnamentRightPath,
} from "../../../../lib/assetUtils";
export default function HowItWorksDecorativeShapes() {
return (
<>
<div
aria-hidden
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[84.86%] right-[-9.28%] top-[clamp(200px,20vw,255px)]"
data-node-id="19003:23575"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(howItWorksOrnamentRightPath())}
alt=""
className="pointer-events-none size-full max-w-none object-cover"
/>
</div>
<div
aria-hidden
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[clamp(520px,55vw,811px)]"
data-node-id="19003:23576"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(howItWorksOrnamentLeftPath())}
alt=""
className="pointer-events-none size-full max-w-none object-cover"
/>
</div>
</>
);
}
+4 -4
View File
@@ -11,7 +11,7 @@ import {
HOW_IT_WORKS_SENTINEL_SLUG, HOW_IT_WORKS_SENTINEL_SLUG,
} from "../../../lib/howItWorksSyntheticPost"; } from "../../../lib/howItWorksSyntheticPost";
import ContentBanner from "../../components/sections/ContentBanner"; import ContentBanner from "../../components/sections/ContentBanner";
import HowItWorksDecorativeShapes from "./_components/HowItWorksDecorativeShapes"; import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
import AskOrganizer from "../../components/sections/AskOrganizer"; import AskOrganizer from "../../components/sections/AskOrganizer";
import "../blog/blog.css"; import "../blog/blog.css";
@@ -103,11 +103,11 @@ export default function HowItWorksPage() {
}} }}
/> />
<div className="relative min-h-screen overflow-x-hidden bg-transparent"> <div className="relative min-h-screen overflow-x-clip bg-transparent">
<ContentBanner post={syntheticPost} variant="guide" /> <ContentBanner post={syntheticPost} variant="guide" />
<div className="relative w-full"> <div className="relative w-full overflow-x-clip">
<HowItWorksDecorativeShapes /> <ContentTemplateDecorativeShapes variant="guide" />
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]"> <article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
<div <div
+23 -17
View File
@@ -88,15 +88,15 @@ vi.mock("../../app/components/sections/AskOrganizer", () => {
}); });
// Mock asset utils // Mock asset utils
vi.mock("../../lib/assetUtils", () => ({ vi.mock("../../lib/assetUtils", async (importOriginal) => {
getAssetPath: vi.fn((asset) => const actual = await importOriginal();
asset.startsWith("/") ? asset : `/${asset}`, return {
), ...actual,
ASSETS: { getAssetPath: vi.fn((asset) =>
CONTENT_SHAPE_1: "assets/shapes/content-shape-1.svg", asset.startsWith("/") ? asset : `/${asset}`,
CONTENT_SHAPE_2: "assets/shapes/content-shape-2.svg", ),
}, };
})); });
// Mock blog post data // Mock blog post data
const mockPost = { const mockPost = {
@@ -238,6 +238,8 @@ describe("BlogPostPage", () => {
// Check shape sources // Check shape sources
expect(shapes[0]).toHaveAttribute("src", "/assets/shapes/content-shape-1.svg"); expect(shapes[0]).toHaveAttribute("src", "/assets/shapes/content-shape-1.svg");
expect(shapes[1]).toHaveAttribute("src", "/assets/shapes/content-shape-2.svg"); expect(shapes[1]).toHaveAttribute("src", "/assets/shapes/content-shape-2.svg");
expect(shapes[0]).toHaveClass("object-contain");
expect(shapes[1]).toHaveClass("object-contain");
}); });
it("applies correct styling to article content", async () => { it("applies correct styling to article content", async () => {
@@ -349,29 +351,33 @@ describe("BlogPostPage", () => {
const shapes = screen.getAllByAltText(""); const shapes = screen.getAllByAltText("");
// First shape (right side)
const rightShape = shapes[0].closest("div"); const rightShape = shapes[0].closest("div");
expect(rightShape).toHaveClass( expect(rightShape).toHaveClass(
"hidden", "hidden",
"md:block", "md:block",
"absolute", "absolute",
"top-1/4",
"right-0",
"pointer-events-none", "pointer-events-none",
"z-10", "z-0",
"top-[266px]",
"lg:top-[255px]",
"left-[87.19%]",
"lg:left-[84.86%]",
); );
expect(rightShape).not.toHaveClass("z-10");
expect(rightShape).not.toHaveClass("top-1/4");
// Second shape (left side)
const leftShape = shapes[1].closest("div"); const leftShape = shapes[1].closest("div");
expect(leftShape).toHaveClass( expect(leftShape).toHaveClass(
"hidden", "hidden",
"md:block", "md:block",
"absolute", "absolute",
"top-1/2",
"left-0",
"pointer-events-none", "pointer-events-none",
"z-10", "z-0",
"top-[811px]",
"left-[-1.66%]",
); );
expect(leftShape).not.toHaveClass("z-10");
expect(leftShape).not.toHaveClass("top-1/2");
}); });
it("handles malformed post data gracefully", async () => { it("handles malformed post data gracefully", async () => {
+29
View File
@@ -63,4 +63,33 @@ describe("HowItWorksPage", () => {
screen.getByText(/mutual aid network, manage an open-source project/), screen.getByText(/mutual aid network, manage an open-source project/),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test("clips body ornaments at the page frame and keeps them behind copy", () => {
const { container } = render(<HowItWorksPage />);
const page = container.querySelector("div.min-h-screen");
expect(page).toHaveClass("overflow-x-clip");
const right = container.querySelector('[data-node-id="19003:23575"]');
const left = container.querySelector('[data-node-id="19003:23576"]');
expect(right).toHaveClass(
"z-0",
"left-[87.19%]",
"right-0",
"top-[266px]",
"lg:left-[84.86%]",
"lg:top-[255px]",
);
expect(left).toHaveClass("z-0", "left-0", "right-[88.38%]", "top-[811px]");
const rightImg = right?.querySelector("img");
const leftImg = left?.querySelector("img");
expect(rightImg).toHaveAttribute(
"src",
"/assets/shapes/how-shape-2.svg",
);
expect(leftImg).toHaveAttribute("src", "/assets/shapes/how-shape-1.svg");
expect(rightImg).toHaveClass("object-contain");
expect(leftImg).toHaveClass("object-contain");
});
}); });