Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 107% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 135% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 62% | 0% |
R&D proposal app scaffolder with dark theme, citations, and Lighthouse compliance.
Activate this skill when the user:
| 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 |
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 truthALL text, numbers, and citations live in src/data/data.js. Section components are pure render logic.
js// 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.
The page is a single vertical scroll with scroll-spy navigation:
tsx// 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.
css/* 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 */ }
css.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; } }
css::-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; }
css@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; } }
tsx// 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> ); }
css/* 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; }
references[]id="ref-{n}" anchorstsx// 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.
tsx// 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>; }
tsx// 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}%` }} /> ); }
metadataBase set in layout.tsx (resolves canonical URLs + OG image paths)export const viewport: Viewport (not inside metadata object)themeColor matching primary background (#09090B)robots.ts — allow all, link to sitemapsitemap.ts — root URL with changeFrequency: "daily"WebPage or Report type) in page.tsxfocus-visible ring: outline: 2px solid #38BDF8; outline-offset: 2pxprefers-reduced-motion disables ALL animationscolor-scheme: dark on <html>aria-hidden="true" on decorative SVGs/iconsaria-label on <nav>, chart wrappersmin-h-[44px] on buttons, nav links::selection styling for dark themescope="col" on all <th> elementsdynamic()dynamic() imports with animate-pulse and fixed min-hoptimizePackageImports in next.config.ts for heavy librarieserror.tsx) with themed retry UIWhen triggered, generate the full project:
bunx create-next-app@latest --app --tailwind --ts --src-dirbun add geistdata.js with placeholder content matching the user's topicglobals.css with full design systemlayout.tsx with metadata, viewport, fontspage.tsx with JSON-LD and section compositionrobots.ts and sitemap.tsbun run buildOther measured skills in the registry, with their headline benchmark lift.