QA pass: layout, create-flow, and About books #68

Merged
an.di merged 38 commits from adilallo/fix/CR-129-create-rule-button into main 2026-08-26 15:46:56 +00:00
23 changed files with 67 additions and 13 deletions
Showing only changes of commit c2188015e3 - Show all commits
+3
View File
@@ -44,6 +44,9 @@ npm-cache/
!public/favicon-32x32.png
!public/apple-touch-icon.png
# Per-article body ornaments (Figma Content page Template image fills)
!public/content/blog/*-ornament-*.png
# Visual regression snapshots (allow these)
!tests/e2e/visual-regression.spec.ts-snapshots/
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
@@ -7,16 +7,17 @@
* - 19003:23576 / 19000:23009 — left
*/
import {
ASSETS,
getAssetPath,
howItWorksOrnamentLeftPath,
howItWorksOrnamentRightPath,
resolveArticleOrnamentPath,
} from "../../../lib/assetUtils";
type ContentTemplateDecorativeShapesVariant = "guide" | "article";
type ContentTemplateDecorativeShapesProps = {
variant: ContentTemplateDecorativeShapesVariant;
slug?: string;
};
const RIGHT_GUIDE_CLASS =
@@ -30,8 +31,15 @@ const LEFT_ARTICLE_CLASS =
export default function ContentTemplateDecorativeShapes({
variant,
slug,
}: ContentTemplateDecorativeShapesProps) {
const isArticle = variant === "article";
const rightSrc = isArticle
? resolveArticleOrnamentPath(slug, "right")
: getAssetPath(howItWorksOrnamentRightPath());
const leftSrc = isArticle
? resolveArticleOrnamentPath(slug, "left")
: getAssetPath(howItWorksOrnamentLeftPath());
return (
<>
@@ -42,11 +50,7 @@ export default function ContentTemplateDecorativeShapes({
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={
isArticle
? getAssetPath(ASSETS.CONTENT_SHAPE_1)
: getAssetPath(howItWorksOrnamentRightPath())
}
src={rightSrc}
alt=""
className="pointer-events-none size-full max-w-none object-contain"
/>
@@ -58,11 +62,7 @@ export default function ContentTemplateDecorativeShapes({
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={
isArticle
? getAssetPath(ASSETS.CONTENT_SHAPE_2)
: getAssetPath(howItWorksOrnamentLeftPath())
}
src={leftSrc}
alt=""
className="pointer-events-none size-full max-w-none object-contain"
/>
+4 -1
View File
@@ -208,7 +208,10 @@ export default async function BlogPostPage({ params }: PageProps) {
<ContentBanner post={post} />
<div className="relative w-full overflow-x-clip">
<ContentTemplateDecorativeShapes variant="article" />
<ContentTemplateDecorativeShapes
variant="article"
slug={post.slug}
/>
<article
data-node-id="19031:10426"
+1
View File
@@ -92,6 +92,7 @@ Each article uses SVGs in `public/content/blog/`. For a new article, duplicate a
- `{slug}-vertical.svg` — 260px × 390px
- `{slug}-horizontal.svg` — 320px × 225.5px (minimum width)
- `{slug}-section.svg` — optional md+ banner (1920×672)
- `{slug}-ornament-right.png` / `{slug}-ornament-left.png` — body gutter shapes
- `{slug}-tag.svg` — tag mark for content cards
3. **Customize** the copied SVGs for your article
4. **Add to frontmatter**:
+2 -1
View File
@@ -39,7 +39,8 @@ Do not duplicate the same glyph in both places unless migrating between systems.
`shareIconPath`, `featurePanelPath`, `ASSETS`, …) over hardcoded
`/assets/...` strings in components.
- **Blog art** stays under `public/content/blog/` with
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`.
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`,
and `-ornament-right.png` / `-ornament-left.png` (body gutter shapes).
- **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in
`app/layout.tsx` metadata) plus generated root binaries for Safari/iOS:
`public/favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, and
+23
View File
@@ -170,6 +170,14 @@ export function contentBlogTagPath(slug: string): string {
return `/content/blog/${slug}-tag.svg`;
}
/** Body gutter ornaments (Figma Content page Template 19003:23575 / 19003:23576). */
export function contentBlogOrnamentPath(
slug: string,
side: "right" | "left",
): string {
return `/content/blog/${slug}-ornament-${side}.png`;
}
/** Stable catalog slug order for blog asset fallbacks when an article has no custom SVGs. */
export const CONTENT_CATALOG_SLUG_ORDER = [
"resolving-active-conflicts",
@@ -182,6 +190,21 @@ export const CONTENT_CATALOG_SLUG_ORDER = [
"knowledge-management-and-institutional-amnesia",
] as const;
export function resolveArticleOrnamentPath(
slug: string | undefined,
side: "right" | "left",
): string {
const hasCatalogArt =
!!slug &&
(CONTENT_CATALOG_SLUG_ORDER as readonly string[]).includes(slug);
if (hasCatalogArt && slug) {
return contentBlogOrnamentPath(slug, side);
}
return getAssetPath(
side === "right" ? ASSETS.CONTENT_SHAPE_1 : ASSETS.CONTENT_SHAPE_2,
);
}
/** Pick a catalog article whose `public/content/blog/` SVGs can stand in as fallbacks. */
export function contentCatalogSlugForFallback(
slug: string,
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+23
View File
@@ -242,6 +242,29 @@ describe("BlogPostPage", () => {
expect(shapes[1]).toHaveClass("object-contain");
});
it("uses per-article body ornaments for catalog posts", async () => {
const { getBlogPostBySlug } = await import("../../lib/content");
vi.mocked(getBlogPostBySlug).mockReturnValue({
...mockPost,
slug: "resolving-active-conflicts",
});
const BlogPostPageComponent = await BlogPostPage({
params: { slug: "resolving-active-conflicts" },
});
render(BlogPostPageComponent);
const shapes = screen.getAllByAltText("");
expect(shapes[0]).toHaveAttribute(
"src",
"/content/blog/resolving-active-conflicts-ornament-right.png",
);
expect(shapes[1]).toHaveAttribute(
"src",
"/content/blog/resolving-active-conflicts-ornament-left.png",
);
});
it("applies correct styling to article content", async () => {
const BlogPostPageComponent = await BlogPostPage({
params: { slug: "test-article" },