feat(marketing): editorial-monochrome homepage POC (/home-poc) - #246
feat(marketing): editorial-monochrome homepage POC (/home-poc)#246ejay-dev wants to merge 1 commit into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces a complete proof-of-concept landing page at ChangesHome POC Marketing Experience
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
♿ Accessibility Test Results✅ PASSED - No critical accessibility issues found Tests Performed:
Artifacts: Download the accessibility reports from the "Artifacts" section for detailed results. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/home-poc/page.tsx (1)
10-31: ⚡ Quick winMake
vignettestrongly typed to prevent silent wrong renders.Line 131 accepts any string and Line 134 falls back silently. A typo in
FEATURESwill 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 winAvoid index-coupling for the evidence pulse path.
Line 74 hard-codes
NODES[3], which can silently drift if node order changes. Resolve byidinstead.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
📒 Files selected for processing (7)
app/home-poc/_components/ComplianceGraph.tsxapp/home-poc/_components/PostureRing.tsxapp/home-poc/_components/Reveal.tsxapp/home-poc/_components/StickyCTA.tsxapp/home-poc/layout.tsxapp/home-poc/page.tsxapp/home-poc/poc.css
| 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); |
There was a problem hiding this comment.
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.
| 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.
What this is
A proof-of-concept marketing homepage at
/home-pocexploring an award-level, deliberately not-AI-vibe-coded direction that aligns with the charcoalFORMAOSwordmark rebrand.It is isolated and safe: lives outside the
(marketing)route group (so it escapes the darkmk-shell/glass header), isnoindex, 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:
#1C1E1F+ one restrained vermilion accent used sparingly. No cyan, glass, sparkles, or aurora.Two interactive moments
Mobile-native
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 pageapp/home-poc/_components/—PostureRing,ComplianceGraph,Reveal,StickyCTAVerification
Status
This is a direction POC for review, not a production swap. If approved, next step is porting into the real
FigmaHomepagecomponents and planning the rollout across the ~40 marketing pages.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features