proposal-builder
Scaffolds production-ready R&D proposal and research pitch deck apps using Next.js App Router, Tailwind CSS, and Geist fonts. Generates the full project structure including dark-themed glass morphism UI, citation system, scroll-spy navigation, animated counters, and Lighthouse-ready metadata. Triggered when the user asks to build a research proposal, R&D app, pitch deck, or single-page research site.
Proposal Builder Skill
R&D proposal app scaffolder with dark theme, citations, and Lighthouse compliance.
When to Trigger
Activate this skill when the user:
- Asks to build a research proposal or R&D pitch
- Wants to create a single-page research site or pitch deck app
- Requests a scaffold for a data-driven narrative page
- Needs a presentation-quality web app for a grant, investor deck, or technical report
Stack
| Layer | Technology |
|---|---|
| Framework | Next.js App Router (latest stable) |
| Styling | Tailwind CSS v4+ |
| Fonts | Geist Sans + Geist Mono |
| Deployment | Vercel |
| Package manager | Bun (preferred) or npm |
Project Structure
src/
app/
page.tsx # Main page — metadata, JSON-LD, section composition
layout.tsx # Root layout — metadataBase, viewport, themeColor, fonts
globals.css # Design tokens, glass utilities, scrollbar, reduced-motion
robots.ts # Allow all, link to sitemap
sitemap.ts # Root URL with changeFrequency: "daily"
error.tsx # Error boundary with themed retry UI
components/
sections/ # One component per content section
Hero.tsx # Title, subtitle, key stats, scroll CTA
Problem.tsx # Problem statement with data points
Solution.tsx # Proposed solution with architecture
Methodology.tsx # Research methodology / approach
Timeline.tsx # Project timeline / milestones
Team.tsx # Team credentials
Budget.tsx # Budget breakdown (table or chart)
References.tsx # Full reference list with DOI links
ui/
Cite.tsx # Citation component (superscript + inline)
ScrollProgress.tsx # Fixed top gradient progress bar
ScrollSpy.tsx # Sidebar nav with active section highlight
AnimatedCounter.tsx # Number counter with requestAnimationFrame
SkipNav.tsx # Skip navigation link (accessibility)
SectionWrapper.tsx # IntersectionObserver fade-in wrapper
data/
data.js # ALL text, numbers, citations — single source of truth
Architecture Rules
Content Separation
ALL text, numbers, and citations live in src/data/data.js. Section components are pure render logic.
// src/data/data.js
export const hero = {
title: "Project Title",
subtitle: "A one-line elevator pitch",
stats: [
{ value: 2.4, suffix: "B", label: "Market size (USD)" },
{ value: 79, suffix: "%", label: "Efficiency improvement" },
],
};
export const references = [
{
id: 1,
authors: "Smith, J., & Doe, A.",
year: 2024,
title: "Research paper title",
journal: "Nature Biotechnology",
doi: "10.1038/s41587-024-00000-0",
},
// ...
];
Critical: Never use \uXXXX escape sequences in data files. Use actual Unicode characters: –, —, ², °, ±, >=, etc.
Single-Page Scroll Architecture
The page is a single vertical scroll with scroll-spy navigation:
// src/app/page.tsx
import Hero from '@/components/sections/Hero';
import Problem from '@/components/sections/Problem';
// ... static imports for ALL sections (LCP requirement)
export default function Page() {
return (
<main>
<Hero />
<Problem />
<Solution />
<Methodology />
<Timeline />
<Team />
<Budget />
<References />
</main>
);
}
LCP rule: Every section is a static import. Never use dynamic() for above-the-fold content.
Design System
Dark Theme Foundation
/* globals.css */
:root {
color-scheme: dark;
/* Opacity scale */
--white-05: rgba(255,255,255,0.05);
--white-10: rgba(255,255,255,0.10);
--white-20: rgba(255,255,255,0.20);
--white-40: rgba(255,255,255,0.40);
--white-60: rgba(255,255,255,0.60);
--white-80: rgba(255,255,255,0.80);
--white-90: rgba(255,255,255,0.90);
}
html {
background: #09090B; /* zinc-950 */
color: #F4F4F5; /* zinc-100 */
}
Glass Morphism Utilities
.glass {
background: var(--white-05);
backdrop-filter: blur(16px);
border: 1px solid var(--white-10);
border-radius: 1rem;
}
.glass-hover {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.glass-hover:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.glass-static {
/* Full-width containers that should NOT lift on hover */
transform: none !important;
box-shadow: none !important;
}
@media (hover: none) {
.glass-hover:hover {
transform: none;
box-shadow: none;
}
}
Scrollbar Styling
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--white-20); border-radius: 4px; }
/* Firefox */
html {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
Reduced Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Citation System
Cite Component
// src/components/ui/Cite.tsx
interface CiteProps {
n: number;
inline?: boolean;
}
export function Cite({ n, inline }: CiteProps) {
const ref = references.find(r => r.id === n);
if (!ref) return null;
if (inline) {
return (
<a href={`#ref-${n}`} className="text-blue-400 hover:text-blue-300 transition-colors">
{ref.authors.split(',')[0]} et al. ({ref.year})
</a>
);
}
return (
<sup>
<a href={`#ref-${n}`} className="text-blue-400 hover:text-blue-300 text-xs ml-0.5">
[{n}]
</a>
</sup>
);
}
Reference List with Target Highlight
/* Highlight scrolled-to reference */
[id^="ref-"]:target {
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid #3B82F6;
padding-left: 0.75rem;
transition: background 0.3s ease;
}
Reference Rules
- Every citation must have a corresponding entry in
references[] - Every reference must include a DOI or institutional URL — no dead-end citations
- Format: Author(s), Year, Title, Journal/Source, DOI link
- References section renders at the bottom with
id="ref-{n}"anchors
Scroll Animations
IntersectionObserver Wrapper
// src/components/ui/SectionWrapper.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
export function SectionWrapper({ children, id }: { children: React.ReactNode; id: string }) {
const ref = useRef<HTMLElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Check if already in viewport on mount (SSR safety)
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
setIsVisible(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setIsVisible(true); },
{ threshold: 0.1 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<section
ref={ref}
id={id}
className={`scroll-mt-20 transition-all duration-700 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
}`}
>
{children}
</section>
);
}
Critical: Always check viewport on mount. Never default to opacity: 0 without an immediate viewport check — this breaks SSR and causes content to be invisible if IntersectionObserver fires late.
Animated Number Counter
// src/components/ui/AnimatedCounter.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
export function AnimatedCounter({ target, duration = 2000 }: { target: number; duration?: number }) {
const [count, setCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
observer.disconnect();
const start = performance.now();
const step = (now: number) => {
const progress = Math.min((now - start) / duration, 1);
// Ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
setCount(Math.floor(eased * target));
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}, { threshold: 0.5 });
observer.observe(el);
return () => observer.disconnect();
}, [target, duration]);
return <span ref={ref}>{count.toLocaleString()}</span>;
}
Scroll Progress Bar
// src/components/ui/ScrollProgress.tsx
'use client';
import { useEffect, useState } from 'react';
export function ScrollProgress() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const onScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
setProgress(scrollTop / (scrollHeight - clientHeight));
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div
className="fixed top-0 left-0 h-1 z-50 bg-gradient-to-r from-blue-500 via-purple-500 to-emerald-500"
style={{ width: `${progress * 100}%` }}
/>
);
}
Lighthouse Compliance Checklist
SEO (Target: 100)
-
metadataBaseset inlayout.tsx(resolves canonical URLs + OG image paths) - Separate
export const viewport: Viewport(not inside metadata object) -
themeColormatching primary background (#09090B) - Meta description: 150-160 characters
-
robots.ts— allow all, link to sitemap -
sitemap.ts— root URL withchangeFrequency: "daily" - JSON-LD structured data (
WebPageorReporttype) inpage.tsx - Preconnect/dns-prefetch for external domains (fonts, images, APIs)
Accessibility (Target: 90+)
- Skip navigation link as first focusable element
- Global
focus-visiblering:outline: 2px solid #38BDF8; outline-offset: 2px -
prefers-reduced-motiondisables ALL animations -
color-scheme: darkon<html> - WCAG AA color contrast (4.5:1 minimum)
-
aria-hidden="true"on decorative SVGs/icons -
aria-labelon<nav>, chart wrappers - Touch targets:
min-h-[44px]on buttons, nav links -
::selectionstyling for dark theme -
scope="col"on all<th>elements
Performance (Target: 90+)
- LCP section (Hero): static import, never
dynamic() - Loading skeletons on all
dynamic()imports withanimate-pulseand fixedmin-h -
optimizePackageImportsinnext.config.tsfor heavy libraries - Error boundary (
error.tsx) with themed retry UI - All design token colors defined upfront — no undefined Tailwind tokens
Scaffold Command
When triggered, generate the full project:
- Initialize with
bunx create-next-app@latest --app --tailwind --ts --src-dir - Install Geist fonts:
bun add geist - Create the directory structure above
- Generate
data.jswith placeholder content matching the user's topic - Generate all section components with glass morphism styling
- Generate all UI components (Cite, ScrollProgress, ScrollSpy, AnimatedCounter, SkipNav)
- Set up
globals.csswith full design system - Configure
layout.tsxwith metadata, viewport, fonts - Configure
page.tsxwith JSON-LD and section composition - Add
robots.tsandsitemap.ts - Verify build passes:
bun run build