---
name: 0xjitsu/proposal-builder
source: https://app.decimal.ai/s/0xjitsu-proposal-builder@1/SKILL.md
source_sha256: 5392e1c0f74f
---

# 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.

```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.

### Single-Page Scroll Architecture

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.

## Design System

### Dark Theme Foundation

```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 */
}
```

### Glass Morphism Utilities

```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;
  }
}
```

### Scrollbar Styling

```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;
}
```

### Reduced Motion

```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;
  }
}
```

## Citation System

### Cite Component

```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>
  );
}
```

### Reference List with Target Highlight

```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;
}
```

### 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

```tsx
// 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

```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>;
}
```

## Scroll Progress Bar

```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}%` }}
    />
  );
}
```

## 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`