Add 404 design

This commit is contained in:
adilallo
2026-04-26 21:27:03 -06:00
parent 9962f44ff1
commit 252848eba9
12 changed files with 233 additions and 50 deletions
+3
View File
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import ArrowBackIcon from "./icon/arrow_back.svg";
import ContentCopyIcon from "./icon/content_copy.svg";
import EditIcon from "./icon/edit.svg";
import ExclamationIcon from "./icon/exclamation.svg";
@@ -10,6 +11,7 @@ import MailIcon from "./icon/mail.svg";
import WarningIcon from "./icon/warning.svg";
export const ICON_NAME_OPTIONS = [
"arrow_back",
"chevron_right",
"content_copy",
"edit",
@@ -27,6 +29,7 @@ type SvgComponent =
/** SVG import may be a React component or a module object { default: Component } (e.g. with Turbopack) */
const iconMap: Record<IconName, SvgComponent> = {
arrow_back: ArrowBackIcon,
chevron_right: ChevronRightIcon,
content_copy: ContentCopyIcon,
edit: EditIcon,
+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.37306 12.75L13.0692 18.4461L12 19.5L4.50003 12L12 4.5L13.0692 5.55383L7.37306 11.25H19.5V12.75H7.37306Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

@@ -149,6 +149,12 @@ function TopNavView({
);
}
/**
* Standard marketing / app top nav.
* Figma: "Navigation / Top" (Community-Rule-System, node 22078-808559) — horizontal
* padding, logo ~200px left, menu cluster centered in the bar (`left-1/2` + translate),
* log in + create rule on the right. Breakpoints and MenuBar sizes unchanged from prior map.
*/
// Render standard variant (Header style)
return (
<>
@@ -157,60 +163,79 @@ function TopNavView({
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
<header
className="sticky top-0 z-50 bg-[var(--color-surface-default-primary)] w-full border-b border-[var(--border-color-default-tertiary)]"
className="relative z-50 w-full border-b border-[var(--border-color-default-tertiary)] bg-[var(--color-surface-default-primary)]"
role="banner"
aria-label={t("ariaLabels.mainNavigationHeader")}
>
<nav
className="flex items-center gap-[var(--spacing-scale-002)] sm:justify-between mx-auto
h-[var(--spacing-scale-040)]
lg:h-auto
px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)]
sm:px-[var(--spacing-measures-spacing-016)] sm:py-[var(--spacing-measures-spacing-008)]
className="relative flex w-full items-center
px-[var(--spacing-scale-016)]
py-[var(--spacing-scale-016)]
sm:px-[var(--spacing-measures-spacing-016)]
sm:py-[var(--spacing-scale-016)]
lg:px-[var(--spacing-measures-spacing-64,64px)]
lg:py-[var(--spacing-scale-020)]
xl:py-[var(--spacing-scale-024)]
sm:gap-0"
lg:py-[var(--spacing-scale-016)]
xl:py-[var(--spacing-scale-016)]
min-h-[var(--spacing-scale-040)]"
role="navigation"
aria-label={t("ariaLabels.mainNavigation")}
>
{/* Logo - Consistent left positioning across all breakpoints */}
<Logo
size={logoSize}
wordmark
palette={folderTop ? "inverse" : "default"}
/>
<div
className="relative z-20 min-w-0 shrink-0 sm:w-[200px] sm:max-w-[200px] sm:shrink-0"
data-topnav="logo"
>
<Logo
size={logoSize}
wordmark
palette={folderTop ? "inverse" : "default"}
/>
</div>
{/* Navigation Links - Consistent center positioning */}
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
{/* XSmall breakpoint - Navigation items in Actions section (flex-1, justify-end) */}
<div className="block sm:hidden" data-testid="nav-xs">
{/* XSmall: nav + login in flow (flex-1) — same as before */}
<div
className="flex min-w-0 flex-1 items-center justify-end sm:hidden"
data-topnav="nav-xs-flow"
>
<div className="block" data-testid="nav-xs">
<MenuBar size="X Small">
{renderNavigationItems("xsmall")}
{logIn && renderLoginButton("xsmall")}
</MenuBar>
</div>
</div>
{/* 430-639px (sm: breakpoint): MenuBar X Small */}
<div className="hidden sm:block md:hidden" data-testid="nav-sm">
{/* sm+ — Figma: nav cluster centered in bar (not between logo and actions) */}
<div
className="pointer-events-none hidden sm:absolute sm:left-1/2 sm:top-1/2 sm:z-10 sm:flex sm:-translate-x-1/2 sm:-translate-y-1/2 sm:items-center sm:justify-center"
data-topnav="nav-center"
>
<div
className="pointer-events-auto hidden sm:flex md:hidden"
data-testid="nav-sm"
>
<MenuBar size="X Small">
{renderNavigationItems("xsmall")}
{logIn && renderLoginButton("xsmall")}
</MenuBar>
</div>
{/* 640-1023px (md: breakpoint): MenuBar X Small (different from folderTop=true) */}
<div className="hidden md:block lg:hidden" data-testid="nav-md">
<div
className="pointer-events-auto hidden md:flex lg:hidden"
data-testid="nav-md"
>
<MenuBar size="X Small">
{renderNavigationItems("xsmall")}
</MenuBar>
</div>
<div className="hidden lg:block xl:hidden" data-testid="nav-lg">
<div
className="pointer-events-auto hidden lg:flex xl:hidden"
data-testid="nav-lg"
>
<MenuBar size="Large">{renderNavigationItems("large")}</MenuBar>
</div>
<div className="hidden xl:block" data-testid="nav-xl">
<div
className="pointer-events-auto hidden xl:flex"
data-testid="nav-xl"
>
<MenuBar size="X Large">
{renderNavigationItems("xlarge")}
</MenuBar>
@@ -218,7 +243,7 @@ function TopNavView({
</div>
{/* Authentication Elements - Consistent right alignment across all breakpoints */}
<div className="flex items-center shrink-0">
<div className="relative z-20 ml-auto flex shrink-0 items-center">
{/* XSmall breakpoint - Only Create Rule button */}
<div className="block sm:hidden shrink-0" data-testid="auth-xs">
{renderCreateRuleButton("xsmall", "small", "small")}
+10 -11
View File
@@ -1,5 +1,5 @@
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import { AuthModalProvider } from "./contexts/AuthModalContext";
import { MessagesProvider } from "./contexts/MessagesContext";
@@ -37,6 +37,12 @@ const spaceGrotesk = Space_Grotesk({
fallback: ["system-ui", "arial"],
});
/** Viewport and favicon use the Metadata / Viewport APIs; avoid a manual `<head>` with a second viewport `meta` (duplicates Nexts head injection). */
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
};
export const metadata: Metadata = {
title: "CommunityRule - Build operating manuals for successful communities",
description:
@@ -51,6 +57,9 @@ export const metadata: Metadata = {
telephone: false,
},
metadataBase: new URL("https://communityrule.com"),
icons: {
icon: [{ url: "/favicon.ico", sizes: "16x16", type: "image/x-icon" }],
},
alternates: {
canonical: "/",
},
@@ -87,16 +96,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="font-sans">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="/favicon.ico"
type="image/x-icon"
sizes="16x16"
/>
</head>
<body
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable}`}
>
+128 -10
View File
@@ -1,14 +1,132 @@
import Link from "next/link";
import messages from "../messages/en/index";
import { getTranslation } from "../lib/i18n/getTranslation";
import { getGovernanceTemplateCatalogEntry } from "../lib/templates/governanceTemplateCatalog";
import Icon from "./components/asset/Icon";
import Button from "./components/buttons/Button";
import HeroDecor from "./components/sections/HeroBanner/HeroDecor";
const NOT_FOUND_TEMPLATE_SLUGS = [
"consensus",
"do-ocracy",
"devolution",
"quadratic-governance",
] as const;
/**
* Figma: 404 page frame 22078-808557; 480px lockup 22078-808903; title + CTA group 22078-808908
* (filled / Go home left, outline / Browse right, 16px between).
* Same [HeroDecor](app/components/sections/HeroBanner/HeroDecor.tsx) SVG as home; 404 places it only behind the title stack.
* Shell: [app/layout.tsx](app/layout.tsx) `TopNav` only — no site footer.
* Template chip row: Figma 22078-809968 — one row, 16px gaps, 20px to hint (no inner scroll; page handles overflow if needed).
* Hero pattern: behind the 404 + bar + h1; wide SVG is painted with overflow-x-clip on `main` so it does not widen the scrollport.
*/
export default function NotFound() {
const t = (key: string) => getTranslation(messages, key);
const templateEntries = NOT_FOUND_TEMPLATE_SLUGS.map((slug) =>
getGovernanceTemplateCatalogEntry(slug),
).filter(
(e): e is NonNullable<typeof e> => e != null,
);
return (
<div className="min-h-screen bg-[#F4F3F1] flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-[var(--color-content-default-primary)] mb-4">
404
</h1>
<p className="text-[var(--color-content-default-secondary)]">
Page Not Found
</p>
</div>
</div>
<main
className="relative flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col overflow-x-clip bg-[var(--color-surface-default-primary)]"
aria-labelledby="not-found-heading"
>
<div
className="relative flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col overflow-x-clip px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]"
>
<div className="relative z-10 flex min-h-0 w-full max-w-full flex-1 flex-col items-center justify-center py-[var(--spacing-scale-040)] sm:py-[var(--spacing-scale-048)]">
{/*
Vertical rhythm: 22078-808903 + 22078-808908 — 404→bar 8px, bar→h1 32px, h1→body 16px,
body→CTAs 48px, CTA→templates 40px (lockup flex gap), template→hint 20px
*/}
<div className="mx-auto flex w-full max-w-[480px] flex-col items-center gap-[var(--spacing-scale-040)] text-center">
<div className="flex w-full min-w-0 flex-col items-center">
<div className="relative flex w-full flex-col items-center">
<HeroDecor
className="pointer-events-none absolute left-1/2 top-[40%] -z-10 h-[645px] w-[1540px] max-w-none
-translate-x-1/2 -translate-y-1/2
scale-[0.41] sm:scale-[0.45] md:scale-[0.47] lg:scale-[0.5] xl:scale-[0.53]"
/>
<p
className="w-full text-center font-bricolage-grotesque font-extrabold leading-none tracking-[-0.04em] text-[clamp(5.5rem,16vw,13.75rem)] text-[var(--color-content-default-brand-primary)]"
aria-hidden="true"
>
{t("pages.notFoundPage.codeTitle")}
</p>
<div
className="mt-[var(--spacing-scale-008)] h-1.5 w-[120px] shrink-0 rounded-full bg-[var(--color-yellow-yellow200)]"
aria-hidden="true"
/>
<h1
id="not-found-heading"
className="mt-[var(--spacing-scale-032)] max-w-full text-center font-bricolage-grotesque text-[2.5rem] font-medium leading-[1.1] text-[var(--color-content-default-primary)] min-[400px]:text-[44px] min-[400px]:leading-[1.1]"
>
{t("pages.notFoundPage.heading")}
</h1>
</div>
<p className="mt-[var(--spacing-scale-016)] w-full max-w-[443px] text-center font-inter text-lg font-normal leading-[1.3] text-[var(--color-content-default-secondary)]">
{t("pages.notFoundPage.description")}
</p>
<div
dir="ltr"
className="mt-[var(--spacing-scale-048)] flex w-full min-w-0 flex-col items-center justify-center gap-[var(--spacing-scale-016)] min-[400px]:flex-nowrap min-[400px]:flex-row min-[400px]:items-center min-[400px]:justify-center"
>
<Button
href="/"
size="large"
buttonType="filled"
palette="default"
className="inline-flex w-max max-w-full shrink-0 items-center justify-center gap-[var(--spacing-scale-010)]"
>
<Icon
name="arrow_back"
size={20}
className="shrink-0"
/>
{t("pages.notFoundPage.goHomeCta")}
</Button>
<Button
href="/templates"
size="large"
buttonType="outline"
palette="default"
className="inline-flex w-max max-w-full shrink-0 items-center justify-center"
>
{t("pages.notFoundPage.browseTemplatesCta")}
</Button>
</div>
</div>
{templateEntries.length > 0 ? (
<div className="flex w-full min-w-0 max-w-[36rem] flex-col items-center gap-[var(--spacing-scale-020)] self-stretch">
<div
className="flex w-full min-w-0 max-md:flex-wrap md:flex-nowrap items-center justify-center gap-x-[var(--spacing-scale-016)] gap-y-[var(--spacing-scale-012)]"
role="list"
>
{templateEntries.map((entry) => (
<Link
key={entry.slug}
href={`/create/review-template/${entry.slug}`}
role="listitem"
className={`${entry.backgroundColor} inline-flex h-[37px] shrink-0 items-center justify-center rounded-full px-[20px] py-0 text-center font-bricolage-grotesque text-sm font-extrabold leading-[21px] text-[var(--color-content-invert-primary)] no-underline transition-opacity hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]`}
>
{entry.title}
</Link>
))}
</div>
<p className="w-full text-center font-inter text-[13px] font-normal leading-[1.2] text-[var(--color-gray-500)]">
{t("pages.notFoundPage.templateHint")}
</p>
</div>
) : null}
</div>
</div>
</div>
</main>
);
}
+21
View File
@@ -0,0 +1,21 @@
# Figma → component registry
Quick map from the Figma file **Community Rule System** (`agv0VBLiBlcnSAaiAORgPR`) to this repos [`app/components/`](/app/components/). Figma uses eleven top-level “❖” areas; `app/components` adds a few app-only buckets (not 1:1 with Figma pages).
| Figma (page) | Code | Notes |
| --- | --- | --- |
| [Utility](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20515-15809) | `utility/` | Create chrome, modals header/footer, tag, scroll, sidebar, dividers, etc. |
| [Asset](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=1240-9089) | `asset/` + `icons/` | Icons, logos; Avatar component currently under `icons/Avatar.tsx` |
| [Button](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=497-3016) | `buttons/` | Figma `Button/`; code uses plural folder name. |
| [Card](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17865-24349) | `cards/` | Step / rule / icon / selection style cards. |
| [Control](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=5944-58611) | `controls/` | Inputs, toggles, select, switch, upload, etc. |
| [Layout](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=21836-20542) | `layout/` | List / list entry / list edit. |
| [Modals](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=5944-47704) | `modals/` | Alert, create, dialog, login, tooltip, context menu, … |
| [Navigation](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=5944-69518) | `navigation/` | Top nav, footer, menu bar, link. |
| [Progress](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=21163-24443) | `progress/` | Stepper, proportion bar. |
| [Sections](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17865-24546) | `sections/` | Page-level / marketing-style compositions. |
| [Type](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=21473-29498) | `type/` | Some “section header” / lockup patterns also live in `sections/`; check both. |
| — | `content/` | Not a Figma DS page; app content shells / thumbnails. |
| — | `localization/` | Not a Figma DS page; i18n UI. |
*Update this when you add a new top-level `app/components/*` package or a new Figma canvas.*
+2
View File
@@ -18,6 +18,7 @@ import learn from "./pages/learn.json";
import monitor from "./pages/monitor.json";
import login from "./pages/login.json";
import profile from "./pages/profile.json";
import notFoundPage from "./pages/notFoundPage.json";
import navigation from "./navigation.json";
import metadata from "./metadata.json";
@@ -72,6 +73,7 @@ export default {
monitor,
login,
profile,
notFoundPage,
},
create: {
community: {
+9
View File
@@ -0,0 +1,9 @@
{
"_comment": "Global 404 — Figma 22078-808557",
"codeTitle": "404",
"heading": "Page not found",
"description": "Looks like this page didn't make it to a consensus. It may have moved, been removed, or never existed.",
"goHomeCta": "Go back home",
"browseTemplatesCta": "Browse templates",
"templateHint": "Maybe one of these templates can help?"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 780 KiB