Add 404 design
@@ -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,
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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 Next’s 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}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Figma → component registry
|
||||
|
||||
Quick map from the Figma file **Community Rule System** (`agv0VBLiBlcnSAaiAORgPR`) to this repo’s [`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.*
|
||||
@@ -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: {
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 780 KiB |