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.
.claude/skills/0xjitsu-proposal-builder/SKILL.md| 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 build| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 31,068 | 28,855 | -7% | 1 | 1 | 0% | 6,218 | 9,756 | +57% | 0 | 0 | — |
case-02 | fail→fail | 30,389 | 26,506 | -13% | 1 | 1 | 0% | 6,212 | 9,750 | +57% | 0 | 0 | — |
case-03 | fail→pass | 31,924 | 29,647 | -7% | 1 | 1 | 0% | 6,206 | 9,744 | +57% | 0 | 0 | — |
case-04 | pass→pass | 11,556 | 19,171 | +66% | 1 | 1 | 0% | 2,233 | 7,129 | +219% | 0 | 0 | — |
case-05 | pass→pass | 9,815 | 6,164 | -37% | 1 | 1 | 0% | 1,952 | 4,861 | +149% | 0 | 0 | — |
case-06 | pass→pass | 18,105 | 19,528 | +8% | 1 | 1 | 0% | 3,476 | 7,839 | +126% | 0 | 0 | — |
case-07 | fail→pass | 16,560 | 15,010 | -9% | 1 | 1 | 0% | 3,306 | 6,850 | +107% | 0 | 0 | — |
case-08 | fail→pass | 11,265 | 6,666 | -41% | 1 | 1 | 0% | 2,043 | 4,797 | +135% | 0 | 0 | — |
case-09 | fail→pass | 19,625 | 16,350 | -17% | 1 | 1 | 0% | 4,137 | 6,706 | +62% | 0 | 0 | — |
case-10 | fail→fail | 14,265 | 14,182 | -1% | 1 | 1 | 0% | 3,070 | 6,697 | +118% | 0 | 0 | — |
case-11 | pass→pass | 12,399 | 9,889 | -20% | 1 | 1 | 0% | 2,619 | 5,613 | +114% | 0 | 0 | — |
case-12 | pass→pass | 12,984 | 10,171 | -22% | 1 | 1 | 0% | 2,604 | 5,797 | +123% | 0 | 0 | — |
case-13 | pass→fail | 18,880 | 17,917 | -5% | 1 | 1 | 0% | 3,702 | 7,734 | +109% | 0 | 0 | — |
case-14 | pass→pass | 10,780 | 7,674 | -29% | 1 | 1 | 0% | 1,754 | 4,985 | +184% | 0 | 0 | — |
case-15 | fail→pass | 12,578 | 26,137 | +108% | 1 | 1 | 0% | 2,644 | 9,698 | +267% | 0 | 0 | — |
case-16 | fail→pass | 7,741 | 3,501 | -55% | 1 | 1 | 0% | 1,327 | 4,149 | +213% | 0 | 0 | — |
case-22 | pass→pass | 13,356 | 15,809 | +18% | 1 | 1 | 0% | 2,570 | 6,592 | +156% | 0 | 0 | — |
case-17 | fail→pass | 15,782 | 10,448 | -34% | 1 | 1 | 0% | 2,631 | 5,322 | +102% | 0 | 0 | — |
case-18 | fail→pass | 13,264 | 8,085 | -39% | 1 | 1 | 0% | 2,740 | 5,194 | +90% | 0 | 0 | — |
case-19 | pass→pass | 15,343 | 11,419 | -26% | 1 | 1 | 0% | 2,903 | 5,704 | +96% | 0 | 0 | — |
case-20 | fail→fail | 11,051 | 11,877 | +7% | 1 | 1 | 0% | 2,347 | 6,028 | +157% | 0 | 0 | — |
case-21 | fail→pass | 27,410 | 26,090 | -5% | 1 | 1 | 0% | 6,174 | 9,712 | +57% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.