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

LayerTechnology
FrameworkNext.js App Router (latest stable)
StylingTailwind CSS v4+
FontsGeist Sans + Geist Mono
DeploymentVercel
Package managerBun (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)

  • metadataBase set in layout.tsx (resolves canonical URLs + OG image paths)
  • Separate export const viewport: Viewport (not inside metadata object)
  • themeColor matching primary background (#09090B)
  • Meta description: 150-160 characters
  • robots.ts — allow all, link to sitemap
  • sitemap.ts — root URL with changeFrequency: "daily"
  • JSON-LD structured data (WebPage or Report type) in page.tsx
  • Preconnect/dns-prefetch for external domains (fonts, images, APIs)

Accessibility (Target: 90+)

  • Skip navigation link as first focusable element
  • Global focus-visible ring: outline: 2px solid #38BDF8; outline-offset: 2px
  • prefers-reduced-motion disables ALL animations
  • color-scheme: dark on <html>
  • WCAG AA color contrast (4.5:1 minimum)
  • aria-hidden="true" on decorative SVGs/icons
  • aria-label on <nav>, chart wrappers
  • Touch targets: min-h-[44px] on buttons, nav links
  • ::selection styling 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 with animate-pulse and fixed min-h
  • optimizePackageImports in next.config.ts for 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:

  1. Initialize with bunx create-next-app@latest --app --tailwind --ts --src-dir
  2. Install Geist fonts: bun add geist
  3. Create the directory structure above
  4. Generate data.js with placeholder content matching the user's topic
  5. Generate all section components with glass morphism styling
  6. Generate all UI components (Cite, ScrollProgress, ScrollSpy, AnimatedCounter, SkipNav)
  7. Set up globals.css with full design system
  8. Configure layout.tsx with metadata, viewport, fonts
  9. Configure page.tsx with JSON-LD and section composition
  10. Add robots.ts and sitemap.ts
  11. Verify build passes: bun run build