Skip to content

feat(marketing): editorial-monochrome homepage POC (/home-poc) - #246

Closed
ejay-dev wants to merge 1 commit into
mainfrom
feat/marketing-editorial-homepage-poc
Closed

feat(marketing): editorial-monochrome homepage POC (/home-poc)#246
ejay-dev wants to merge 1 commit into
mainfrom
feat/marketing-editorial-homepage-poc

Conversation

@ejay-dev

@ejay-dev ejay-dev commented Jun 4, 2026

Copy link
Copy Markdown
Owner

What this is

A proof-of-concept marketing homepage at /home-poc exploring an award-level, deliberately not-AI-vibe-coded direction that aligns with the charcoal FORMAOS wordmark rebrand.

It is isolated and safe: lives outside the (marketing) route group (so it escapes the dark mk-shell/glass header), is noindex, and does not modify the production homepage (FigmaHomepage) or any shared component.

Why it looks different from today's homepage

The current homepage leans on the exact signals we're trying to escape — Inter/Sora (default SaaS pairing), cyan glow, glass morphism, sparkles, aurora/shader heroes. This POC replaces that voice:

  • Type system: Fraunces (optical serif display) + Hanken Grotesk body + IBM Plex Mono labels — kills the Inter/Sora "AI template" tell.
  • Palette: warm paper + charcoal ink #1C1E1F + one restrained vermilion accent used sparingly. No cyan, glass, sparkles, or aurora.
  • Structure over text wall: asymmetric hero, alternating feature rows with real product-UI vignettes, oversized serif index numerals, hairline rules, faint masked grid backdrop, generous whitespace.

Two interactive moments

  1. Compliance-posture ring — counts up + fills its arc on view (hero).
  2. Compliance graph ("One graph. Every framework.") — wires draw themselves on scroll with a vermilion pulse to the Evidence node. Leans into the node-wire architecture as a brand asset.

Mobile-native

  • Horizontal snap-carousel for the feature set.
  • Sticky CTA bar that appears after the hero scrolls away.

Guardrails respected

No fake metrics or personas; Adelaide-anchored; no invented backstory.

Files

  • app/home-poc/layout.tsx — editorial fonts (next/font)
  • app/home-poc/poc.css — scoped design tokens (no leakage into app/marketing theme)
  • app/home-poc/page.tsx — the page
  • app/home-poc/_components/PostureRing, ComplianceGraph, Reveal, StickyCTA

Verification

  • Route compiles clean (HTTP 200), Fraunces variable font loads.
  • Screenshotted at 1440px and 390px (desktop + mobile) — both render faithfully.

Status

This is a direction POC for review, not a production swap. If approved, next step is porting into the real FigmaHomepage components and planning the rollout across the ~40 marketing pages.

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • Added a new demonstration landing page featuring compliance and security visualizations
  • Introduced animated compliance graph component with interactive wires and nodes
  • Added compliance posture circular progress indicator with count-up animation
  • Integrated sticky mobile call-to-action bar for improved engagement
  • Implemented in-viewport fade-in reveal animations throughout the page
  • Applied custom monochrome design system with typography and interaction styles

A proof-of-concept marketing homepage exploring an award-level,
"not-AI-vibe" direction aligned with the charcoal wordmark rebrand.
Isolated outside the (marketing) route group (noindex) so it doesn't
touch the production homepage or inherit the dark mk-shell/glass header.

What's distinct from the current FigmaHomepage:
- Type system: Fraunces (optical serif display) + Hanken Grotesk body
  + IBM Plex Mono labels — replaces the Inter/Sora default-SaaS pairing.
- Palette: warm paper + charcoal ink (#1C1E1F) + one restrained vermilion
  accent used sparingly. No cyan glow, glass, sparkles or aurora.
- Editorial structure: asymmetric hero, alternating feature rows with
  real product-UI vignettes, oversized serif index numerals, hairline
  rules, faint masked grid backdrop.
- Two interactive moments: a counting compliance-posture ring, and a
  node-wire compliance graph whose wires draw on scroll.
- Mobile-native: horizontal snap-carousel for features + sticky CTA bar.

Copy follows brand guardrails: no fake metrics/personas, Adelaide-anchored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
forma-os Ready Ready Preview, Comment Jun 4, 2026 1:34pm

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete proof-of-concept landing page at /home-poc for FormaOS compliance marketing. It includes reusable animation components, specialized data visualizations, a full design system, and a composed marketing page orchestrating all elements into an interactive experience.

Changes

Home POC Marketing Experience

Layer / File(s) Summary
Layout foundation & typography configuration
app/home-poc/layout.tsx
Configures Next.js layout with three Google font families as CSS variables, metadata with noindex robots directive, and establishes root div with font classes.
Reusable reveal animation component
app/home-poc/_components/Reveal.tsx
Implements fade-in and upward rise animation primitive with configurable delay/offset, triggered on viewport entry, respecting reduced-motion preferences.
Data visualization components: graph & progress ring
app/home-poc/_components/ComplianceGraph.tsx, app/home-poc/_components/PostureRing.tsx
Adds ComplianceGraph rendering pure-SVG compliance diagram with animated quadratic wire paths and node animations; PostureRing displays circular progress with requestAnimationFrame count-up to target percentage.
Mobile scroll-triggered CTA bar
app/home-poc/_components/StickyCTA.tsx
Implements fixed mobile-only CTA bar that appears when scrollY exceeds 620px threshold, with passive scroll listener and cleanup.
Complete scoped design system & component styles
app/home-poc/poc.css
Defines .poc-root scoped stylesheet with theme variables, editorial typography classes, accent underline animations, layout primitives, button variants, vignette card UI, carousel styles, sticky CTA bar styling, and reduced-motion compliance.
Landing page composition with vignettes & sections
app/home-poc/page.tsx
Implements static HomePocPage with three vignette sub-components (ControlVignette, EvidenceVignette, AuditVignette) and composed sections: sticky header, hero with Reveal animations, PostureRing preview, trust strip, ComplianceGraph, data-driven desktop/mobile features, outcomes band, dark closing CTA, footer, and StickyCTA.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A hop through compliance so fair and so fine,
With graphs that animate, rings that align,
From layout to page, each section in place,
A proof of concept with editorial grace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding an editorial-monochrome homepage proof-of-concept at the /home-poc route, using marketing-relevant terminology that aligns with the changeset scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/marketing-editorial-homepage-poc

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

♿ Accessibility Test Results

PASSED - No critical accessibility issues found

Tests Performed:

  • WCAG 2.1 AA compliance validation
  • Cross-browser accessibility testing
  • Keyboard navigation testing
  • Screen reader compatibility
  • Color contrast validation

Artifacts: Download the accessibility reports from the "Artifacts" section for detailed results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/home-poc/page.tsx (1)

10-31: ⚡ Quick win

Make vignette strongly typed to prevent silent wrong renders.

Line 131 accepts any string and Line 134 falls back silently. A typo in FEATURES will render the wrong card without failing fast.

Proposed refactor
+type VignetteKind = 'control' | 'evidence' | 'audit';
+
 const FEATURES = [
@@
-    vignette: 'control',
+    vignette: 'control' as VignetteKind,
@@
-    vignette: 'evidence',
+    vignette: 'evidence' as VignetteKind,
@@
-    vignette: 'audit',
+    vignette: 'audit' as VignetteKind,
   },
 ];
@@
-function Vignette({ kind }: { kind: string }) {
+function Vignette({ kind }: { kind: VignetteKind }) {
   if (kind === 'control') return <ControlVignette />;
   if (kind === 'evidence') return <EvidenceVignette />;
   return <AuditVignette />;
 }

Also applies to: 131-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/home-poc/page.tsx` around lines 10 - 31, Add a dedicated string union or
enum for allowed vignette values (e.g., type Vignette = 'control' | 'evidence' |
'audit') and annotate the FEATURES constant so each item is typed with that
Vignette for the vignette property; then update the consumer that reads
feature.vignette (the component rendering the cards) to accept only Vignette and
replace any silent fallback logic with an exhaustive switch or a default that
throws an error so typos fail at compile or runtime rather than rendering the
wrong card. Ensure the union/enum is exported/used where FEATURES and the
renderer (card component) are declared so TypeScript enforces allowed vignette
values.
app/home-poc/_components/ComplianceGraph.tsx (1)

65-75: ⚡ Quick win

Avoid index-coupling for the evidence pulse path.

Line 74 hard-codes NODES[3], which can silently drift if node order changes. Resolve by id instead.

Proposed refactor
 export function ComplianceGraph() {
   const reduce = useReducedMotion();
+  const evidenceNode = NODES.find((n) => n.id === 'evidence');

   return (
@@
-      {!reduce && (
+      {!reduce && evidenceNode && (
         <motion.circle
@@
-          <animateMotion dur="1.4s" begin="1.1s" fill="freeze" path={curve(CENTER, NODES[3])} />
+          <animateMotion
+            dur="1.4s"
+            begin="1.1s"
+            fill="freeze"
+            path={curve(CENTER, evidenceNode)}
+          />
         </motion.circle>
       )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/home-poc/_components/ComplianceGraph.tsx` around lines 65 - 75, The pulse
path is index-coupled to NODES[3], which can break if node ordering changes;
replace the hard-coded index by finding the node by a stable identifier (e.g.,
id === 'evidence' or a semantic property) before rendering: locate the target
node with something like const target = NODES.find(n => n.id === 'evidence'),
use curve(CENTER, target) for the animateMotion path, and add a safe guard
(render nothing or a fallback) if target is undefined so you don't pass
undefined into curve/animateMotion; update the animateMotion path prop and any
related logic in the component (e.g., where motion.circle / animateMotion are
used) to reference the found node instead of NODES[3].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/home-poc/_components/PostureRing.tsx`:
- Around line 11-39: PostureRing currently accepts a target that can be <0 or
>100, which breaks the ring rendering; normalize/clamp target to 0–100 at the
top of the component (e.g., compute a const clampedTarget = Math.min(100,
Math.max(0, target))) and then use clampedTarget everywhere instead of raw
target (use in initial state useState(reduce ? clampedTarget : 0), in the
animation setValue(Math.round(eased * clampedTarget)), and for any calculations
of dash/circ/value) so the displayed number and SVG arc always stay within valid
percentage bounds.

---

Nitpick comments:
In `@app/home-poc/_components/ComplianceGraph.tsx`:
- Around line 65-75: The pulse path is index-coupled to NODES[3], which can
break if node ordering changes; replace the hard-coded index by finding the node
by a stable identifier (e.g., id === 'evidence' or a semantic property) before
rendering: locate the target node with something like const target =
NODES.find(n => n.id === 'evidence'), use curve(CENTER, target) for the
animateMotion path, and add a safe guard (render nothing or a fallback) if
target is undefined so you don't pass undefined into curve/animateMotion; update
the animateMotion path prop and any related logic in the component (e.g., where
motion.circle / animateMotion are used) to reference the found node instead of
NODES[3].

In `@app/home-poc/page.tsx`:
- Around line 10-31: Add a dedicated string union or enum for allowed vignette
values (e.g., type Vignette = 'control' | 'evidence' | 'audit') and annotate the
FEATURES constant so each item is typed with that Vignette for the vignette
property; then update the consumer that reads feature.vignette (the component
rendering the cards) to accept only Vignette and replace any silent fallback
logic with an exhaustive switch or a default that throws an error so typos fail
at compile or runtime rather than rendering the wrong card. Ensure the
union/enum is exported/used where FEATURES and the renderer (card component) are
declared so TypeScript enforces allowed vignette values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03ff17d6-9d2e-45fd-8831-3e37263db42e

📥 Commits

Reviewing files that changed from the base of the PR and between f702fa0 and 40db046.

📒 Files selected for processing (7)
  • app/home-poc/_components/ComplianceGraph.tsx
  • app/home-poc/_components/PostureRing.tsx
  • app/home-poc/_components/Reveal.tsx
  • app/home-poc/_components/StickyCTA.tsx
  • app/home-poc/layout.tsx
  • app/home-poc/page.tsx
  • app/home-poc/poc.css

Comment on lines +11 to +39
export function PostureRing({ target = 98 }: { target?: number }) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const reduce = useReducedMotion();
const [value, setValue] = useState(reduce ? target : 0);

useEffect(() => {
if (!inView || reduce) {
if (reduce) setValue(target);
return;
}
let raf = 0;
const duration = 1400;
let start: number | null = null;
const tick = (t: number) => {
if (start === null) start = t;
const p = Math.min((t - start) / duration, 1);
// easeOutCubic
const eased = 1 - Math.pow(1 - p, 3);
setValue(Math.round(eased * target));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, reduce, target]);

const r = 52;
const circ = 2 * Math.PI * r;
const dash = circ * (value / 100);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp target to a valid percentage range.

Without normalization, values <0 or >100 produce invalid UI state (number + ring arc).

Proposed fix
 export function PostureRing({ target = 98 }: { target?: number }) {
+  const normalizedTarget = Math.max(0, Math.min(100, target));
   const ref = useRef<HTMLDivElement>(null);
   const inView = useInView(ref, { once: true, amount: 0.6 });
   const reduce = useReducedMotion();
-  const [value, setValue] = useState(reduce ? target : 0);
+  const [value, setValue] = useState(reduce ? normalizedTarget : 0);

   useEffect(() => {
     if (!inView || reduce) {
-      if (reduce) setValue(target);
+      if (reduce) setValue(normalizedTarget);
       return;
     }
@@
-      setValue(Math.round(eased * target));
+      setValue(Math.round(eased * normalizedTarget));
       if (p < 1) raf = requestAnimationFrame(tick);
     };
@@
-  }, [inView, reduce, target]);
+  }, [inView, reduce, normalizedTarget]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function PostureRing({ target = 98 }: { target?: number }) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const reduce = useReducedMotion();
const [value, setValue] = useState(reduce ? target : 0);
useEffect(() => {
if (!inView || reduce) {
if (reduce) setValue(target);
return;
}
let raf = 0;
const duration = 1400;
let start: number | null = null;
const tick = (t: number) => {
if (start === null) start = t;
const p = Math.min((t - start) / duration, 1);
// easeOutCubic
const eased = 1 - Math.pow(1 - p, 3);
setValue(Math.round(eased * target));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, reduce, target]);
const r = 52;
const circ = 2 * Math.PI * r;
const dash = circ * (value / 100);
export function PostureRing({ target = 98 }: { target?: number }) {
const normalizedTarget = Math.max(0, Math.min(100, target));
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const reduce = useReducedMotion();
const [value, setValue] = useState(reduce ? normalizedTarget : 0);
useEffect(() => {
if (!inView || reduce) {
if (reduce) setValue(normalizedTarget);
return;
}
let raf = 0;
const duration = 1400;
let start: number | null = null;
const tick = (t: number) => {
if (start === null) start = t;
const p = Math.min((t - start) / duration, 1);
// easeOutCubic
const eased = 1 - Math.pow(1 - p, 3);
setValue(Math.round(eased * normalizedTarget));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, reduce, normalizedTarget]);
const r = 52;
const circ = 2 * Math.PI * r;
const dash = circ * (value / 100);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/home-poc/_components/PostureRing.tsx` around lines 11 - 39, PostureRing
currently accepts a target that can be <0 or >100, which breaks the ring
rendering; normalize/clamp target to 0–100 at the top of the component (e.g.,
compute a const clampedTarget = Math.min(100, Math.max(0, target))) and then use
clampedTarget everywhere instead of raw target (use in initial state
useState(reduce ? clampedTarget : 0), in the animation setValue(Math.round(eased
* clampedTarget)), and for any calculations of dash/circ/value) so the displayed
number and SVG arc always stay within valid percentage bounds.

@ejay-dev

ejay-dev commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #247 — the editorial-monochrome direction read as 'AI-vibe' (serif-on-paper + canonical landing skeleton). #247 takes a brutalist-editorial direction (dark, monumental expanded-black type, terminal ledger, engineering-schematic graph) per design feedback.

@ejay-dev ejay-dev closed this Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant