返回 Skills 目錄
getsentry/skills包含需要注意的行為

SKILL DETAIL

presentation-creator

getsentry/skills/presentation-creator

Create data-driven presentation slides using React, Vite, and Recharts with Sentry branding. Use when asked to "create a presentation", "build slides", "make a deck", "create a data presentation", "build a Sentry presentation". Scaffolds a complete slide-based app with charts, animations, and single-file HTML output.

安裝量 · 230查看來源

Installation

npx skills add https://github.com/getsentry/skills --skill presentation-creator

技能檔案

SKILL.md

最近同步 · 2026年9月29日

references/chart-patterns.md›
# Recharts Patterns for Sentry Presentations

## Color Usage Rules

**Categorical palette** — for data series, groups, categories where color is just a distinguisher:
```javascript
const CAT = [
  '#4e79a7',  // steel blue
  '#f28e2b',  // orange
  '#e15759',  // coral
  '#76b7b2',  // teal
  '#59a14f',  // green
  '#edc948',  // gold
  '#b07aa1',  // mauve
  '#ff9da7',  // pink
  '#9c755f',  // brown
  '#bab0ac',  // gray
];
```

**Semantic colors** — ONLY when the color itself carries meaning:
```javascript
const SEM_GREEN = '#2ba185';   // positive / success / good
const SEM_RED = '#f55459';     // negative / failure / bad
const SEM_AMBER = '#d4953a';   // warning / caution
```

**Rule of thumb**: If you could swap two colors without losing information, use `CAT`. If swapping would confuse the meaning (e.g., making "errors" green), use semantic.

## Shared Configuration

### Axis and Grid Defaults

```javascript
const ax = {
  axisLine: { stroke: BORDER },
  tickLine: false,
  tick: { fill: MUTED, fontSize: 11, fontFamily: 'Rubik, system-ui' }
};

const grid = {
  strokeDasharray: '3 3',
  stroke: '#f0edf3',
  vertical: false
};
```

### Tooltip Styling

```javascript
<Tooltip
  contentStyle={{
    background: '#fff',
    border: `1px solid ${BORDER}`,
    borderRadius: 6,
    fontSize: 12,
    fontFamily: 'Rubik, system-ui'
  }}
/>
```

## Common Chart Types

### 1. Stacked Area Chart (ComposedChart)

Best for showing volume breakdowns over time.

When the series have inherent good/bad semantics (accepted vs dropped), use semantic colors:

```jsx
<Area type="monotone" dataKey="accepted" stackId="1"
  fill={SEM_GREEN} stroke={SEM_GREEN} fillOpacity={0.7} />
<Area type="monotone" dataKey="dropped" stackId="1"
  fill={SEM_RED} stroke={SEM_RED} fillOpacity={0.5} />
```

When the series are neutral categories (e.g., different SDK types, regions), use categorical:

```jsx
<Area type="monotone" dataKey="javascript" stackId="1"
  fill={CAT[0]} stroke={CAT[0]} fillOpacity={0.6} />
<Area type="monotone" dataKey="python" stackId="1"
  fill={CAT[1]} stroke={CAT[1]} fillOpacity={0.6} />
<Area type="monotone" dataKey="ruby" stackId="1"
  fill={CAT[2]} stroke={CAT[2]} fillOpacity={0.6} />
```

### 2. Bar Chart (Grouped/Stacked)

For discrete comparisons. Use `CAT` colors unless the bars represent good/bad outcomes.

```jsx
<ResponsiveContainer width="100%" height={280}>
  <BarChart data={data}>
    <CartesianGrid {...grid} />
    <XAxis dataKey="name" {...ax} />
    <YAxis {...ax} />
    <Bar dataKey="seriesA" fill={CAT[0]} radius={[3, 3, 0, 0]} />
    <Bar dataKey="seriesB" fill={CAT[1]} radius={[3, 3, 0, 0]} />
  </BarChart>
</ResponsiveContainer>
```

For a single-series bar chart where all bars represent the same metric, use a single `CAT` color uniformly — do NOT alternate colors per bar unless the bars represent distinct categories.

### 3. Line/Area Curve Chart

Best for showing mathematical relationships (rate curves, thresholds).

```jsx
<ResponsiveContainer width="100%" height={300}>
  <ComposedChart data={curveData}>
    <CartesianGrid {...grid} />
    <XAxis dataKey="x" {...ax} label={{ value: 'Incoming (t/s)', ... }} />
    <YAxis {...ax} domain={[0, 100]} label={{ value: 'Rate %', ... }} />
    <Area type="monotone" dataKey="rate" fill={CAT[0]} fillOpacity={0.15} stroke={CAT[0]} strokeWidth={2} />
  </ComposedChart>
</ResponsiveContainer>
```

### 4. Temporal Scenario Chart (stepAfter)

Best for showing discrete rule updates with lag. Use semantic colors when steps represent accept/reject:

```jsx
<Area type="stepAfter" dataKey="accepted" stackId="1"
  fill={SEM_GREEN} stroke={SEM_GREEN} fillOpacity={0.6} />
<Area type="stepAfter" dataKey="hardBlocked" stackId="1"
  fill={SEM_RED} stroke="none" fillOpacity={0.5} />
```

### 5. Reference Lines and Areas

```jsx
{/* Threshold line — semantic amber for "warning" boundary */}
<ReferenceLine y={300} stroke={SEM_AMBER} strokeDasharray="6 3" />

{/* Shaded zone — semantic green for "safe" region */}
<ReferenceArea x1="03:00" x2="03:10" fill={SEM_GREEN} fillOpacity={0.08}
  label={{ value: '~10 min', fill: SEM_GREEN, fontSize: 11 }} />
```

## Data Generation Patterns

### Gaussian Spike

```javascript
function gaussian(x, center, width, height) {
  return height * Math.exp(-((x - center) ** 2) / (2 * width ** 2));
}
```

### Sinusoidal Daily Pattern

```javascript
const base = 200 + 50 * Math.sin((i / 144) * Math.PI * 2 - Math.PI / 2);
```

### Exponential Adaptation Lag

```javascript
const lagFactor = Math.min(1, (i - spikeStart) / lagIntervals);
const effectiveRate = prevRate + (targetRate - prevRate) * lagFactor;
```

### useMemo for Data

Always wrap data generation in `useMemo`:

```javascript
const data = useMemo(() => {
  return Array.from({ length: 144 }, (_, i) => {
    // generate point
    return { label, incoming, accepted, sampled };
  });
}, []);
```

## Custom Diagram Components

### Zone Diagram

Horizontal bar showing zones. Use semantic colors ONLY when zones carry meaning (e.g., Normal=green, Danger=red). For neutral categories, use `CAT`:

```jsx
function ZoneDiagram({ zones }) {
  return (
    <div className="zone-diagram">
      {zones.map((z, i) => (
        <div key={i} style={{ flex: z.flex, background: z.color, padding: '12px 16px', color: '#fff' }}>
          <div className="zone-name">{z.name}</div>
          <div className="zone-desc">{z.desc}</div>
        </div>
      ))}
    </div>
  );
}
```

```css
.zone-diagram { display: flex; gap: 2px; border-radius: 8px; overflow: hidden; }
```

### Trace Diagram

Visual span representation for distributed traces. Use `CAT` for different services:

```jsx
function TraceDiagram({ rows }) {
  return (
    <div className="trace-diagram">
      {rows.map((r, i) => (
        <div key={i} className="trace-row">
          <span className="trace-label">{r.label}</span>
          <div className="trace-bar">
            {r.spans.map((s, j) => (
              <div key={j} style={{
                flex: s.w, background: s.bg || CAT[j % CAT.length],
                opacity: s.opacity ?? 1,
                borderRadius: 3
              }} />
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}
```

### Sparkline (Mini SVG Chart)

```jsx
function Sparkline({ seed = 0, bars = 14, color = CAT[0] }) {
  const h = Array.from({ length: bars }, (_, i) =>
    20 + ((seed * 17 + i * 31) % 60)
  );
  return (
    <svg width={bars * 5} height={40} style={{ verticalAlign: 'middle' }}>
      {h.map((v, i) => (
        <rect key={i} x={i * 5} y={40 - v * 0.4} width={3.5}
          height={v * 0.4} rx={1} fill={color} opacity={0.7} />
      ))}
    </svg>
  );
}
```

## Chart Container Pattern

Always wrap charts in a container div:

```jsx
<div className="chart-wrap d2">
  <ResponsiveContainer width="100%" height={320}>
    {/* chart */}
  </ResponsiveContainer>
  <p style={{ fontSize: '0.8rem', color: MUTED, textAlign: 'center', marginTop: 8 }}>
    Chart annotation or description
  </p>
</div>
```

## Responsive Sizing

- Default chart height: 280-340px
- Side-by-side charts: 260-300px each
- Mini/sparkline charts: 80-120px
- Always use `ResponsiveContainer` with `width="100%"`
- Set explicit `margin` on the chart component for axis label space
references/design-system.md›
# Sentry Presentation Design System

## Color Palette

### Color Philosophy

Use two distinct color sets:

1. **Semantic colors** — Only when the color itself carries meaning (good/bad, success/failure, warning). Do not use these for arbitrary data grouping.
2. **Categorical colors** — For distinguishing data series, groups, or categories where color has no inherent meaning. Inspired by Tableau's palette for maximum distinguishability.

### CSS Variables

```css
:root {
  /* ── UI chrome ── */
  --dark: #1c1028;
  --purple: #6c5fc7;
  --purple-light: #b5aade;
  --purple-bg: #ede8f5;
  --bg: #faf9fb;
  --card: #f3f1f5;
  --muted: #80708f;
  --border: #dbd6e1;

  /* ── Semantic (use ONLY when meaning applies) ── */
  --semantic-green: #2ba185;   /* positive, success, good */
  --semantic-red: #f55459;     /* negative, failure, bad */
  --semantic-amber: #d4953a;   /* warning, caution, moderate */
  --semantic-green-bg: #e0f5ef;
  --semantic-red-bg: #fde8e9;
  --semantic-amber-bg: #fdf3e4;
}
```

### JS Color Constants (for Charts.jsx)

```javascript
// ── UI / chrome ──
const DARK = '#1c1028';
const MUTED = '#80708f';
const BORDER = '#dbd6e1';

// ── Categorical palette (Tableau-inspired) ──
// Use for data series, groups, and categories where color is just a label.
const CAT = [
  '#4e79a7',  // steel blue
  '#f28e2b',  // orange
  '#e15759',  // coral
  '#76b7b2',  // teal
  '#59a14f',  // green
  '#edc948',  // gold
  '#b07aa1',  // mauve
  '#ff9da7',  // pink
  '#9c755f',  // brown
  '#bab0ac',  // gray
];

// ── Semantic (use ONLY when the color conveys meaning) ──
const SEM_GREEN = '#2ba185';   // positive / success / good
const SEM_RED = '#f55459';     // negative / failure / bad
const SEM_AMBER = '#d4953a';   // warning / caution
```

### When to use which

| Scenario | Palette | Example |
|----------|---------|---------|
| Bar chart comparing 7 SKU types | `CAT[0]`–`CAT[6]` | Each SKU gets a distinct hue, none implies "good" or "bad" |
| Stacked area: accepted vs dropped | Semantic | green = accepted (good), red = dropped (bad) |
| Heatmap cells: adopted vs not | Semantic | green dot = adopted, empty = not |
| Before/after comparison | `CAT[0]` / `CAT[1]` | Two neutral colors, neither implies better |
| Priority columns: Protect / Grow / Expand | Semantic | green = protect, amber = grow, red = expand gaps |
| Multiple org lines on same chart | `CAT` | Each org gets next color in sequence |

## Typography

**Font**: Rubik (Google Fonts) with system-ui fallback.

```css
body {
  font-family: 'Rubik', system-ui, -apple-system, sans-serif;
  color: var(--dark);
  background: var(--bg);
  line-height: 1.7;
  font-size: 0.9rem;
}
```

| Element | Size | Weight | Extra |
|---------|------|--------|-------|
| h1 | 2.5rem | 700 | letter-spacing: -0.03em |
| h2 | 1.55rem | 600 | letter-spacing: -0.02em |
| h3 | 1rem | 600 | — |
| subtitle | 0.95rem | 400 | max-width: 620px, color: var(--muted) |
| body | 0.9rem | 400 | line-height: 1.7 |

## Slide System CSS

```css
.progress {
  position: fixed; top: 0; left: 0; height: 3px;
  background: var(--purple); transition: width 0.3s; z-index: 10;
}

.slide {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 0.45s ease;
}
.slide.active { opacity: 1; pointer-events: auto; }

.slide-content {
  width: 100%; max-width: 880px;
  padding: 60px 40px 100px;
}
```

## Tags

Used on every slide to label the category (Background, Problem, Proposal, etc.).

```css
.tag {
  display: inline-block; font-size: 0.66rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  padding: 4px 10px; border-radius: 4px;
  margin-bottom: 8px;
}
.tag-purple { background: var(--purple-bg); color: var(--purple); }
.tag-red { background: var(--semantic-red-bg); color: var(--semantic-red); }
.tag-green { background: var(--semantic-green-bg); color: var(--semantic-green); }
.tag-amber { background: var(--semantic-amber-bg); color: var(--semantic-amber); }
```

## Layout Utilities

```css
.cols { display: flex; gap: 40px; max-width: 1060px; }
.col { flex: 1; }

.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.card {
  background: #fff; border: 1px solid var(--border);
  border-radius: 8px; padding: 17px;
}

.chart-wrap { max-width: 920px; margin: 0 auto; }
```

## Animations

```css
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

.anim h2, .anim .subtitle, .anim .cols,
.anim .cards, .anim .chart-wrap, .anim table,
.anim .zone-diagram, .anim ul {
  opacity: 0; animation: fadeUp 0.5s ease both;
}

.anim .d1 { animation-delay: 0.1s; }
.anim .d2 { animation-delay: 0.2s; }
.anim .d3 { animation-delay: 0.3s; }
```

Add the `.anim` class to `.slide-content` only when the slide is active. Use `.d1`, `.d2`, `.d3` on child elements for staggered entrance.

## Persistent Glyph Watermark

Every slide shows a small, low-contrast Sentry glyph in the top-left corner. Use the exact path from `references/sentry-glyph.svg`.

```css
.glyph-watermark {
  position: fixed; top: 20px; left: 24px; z-index: 8;
  opacity: 0.12; pointer-events: none;
  display: flex; align-items: center; gap: 10px;
}
.watermark-title {
  font-size: 1.1rem; font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
}
```

Hidden on the title slide (slide 0), shown on all others:

```jsx
{cur > 0 && (
  <div className="glyph-watermark">
    <SentryGlyph size={50} />
    <span className="watermark-title">Presentation Title</span>
  </div>
)}
```

## Navigation

Navigation sits at the bottom of the viewport with **no border, no background separation** — it floats transparently over the slide. Always show the slide number as `{current} / {total}`.

```css
nav {
  position: fixed; bottom: 0; left: 0; right: 0;
  display: flex; align-items: center; justify-content: center;
  gap: 16px; padding: 14px;
  z-index: 5;
}
nav button {
  background: none; border: none;
  cursor: pointer; font-family: inherit;
  font-size: 0.8rem; color: var(--muted);
  padding: 4px 8px;
}
nav button:hover { color: var(--dark); }
nav button:disabled { opacity: 0.2; cursor: default; }
.slide-number {
  font-size: 0.75rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
```

### Dot Indicators

```css
.dots { display: flex; gap: 6px; }
.dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--border); cursor: pointer;
  transition: background 0.2s;
}
.dot.on { background: var(--purple); }
```

### Nav Component

```jsx
function Nav({ cur, total, go, setCur }) {
  return (
    <nav>
      <button onClick={() => go(-1)} disabled={cur === 0}>←</button>
      <div className="dots">
        {Array.from({ length: total }, (_, i) => (
          <div key={i} className={`dot${i === cur ? ' on' : ''}`} onClick={() => setCur(i)} />
        ))}
      </div>
      <button onClick={() => go(1)} disabled={cur === total - 1}>→</button>
      <span className="slide-number">{cur + 1} / {total}</span>
    </nav>
  );
}
```

## Icons

Use Material Symbols Outlined for icons:

```html
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=swap" rel="stylesheet" />
```

```jsx
<span className="material-symbols-outlined">chevron_right</span>
```

## Comparison Tables

```css
.compare { width: 100%; border-collapse: collapse; }
.compare th {
  text-align: left; font-weight: 600;
  padding: 10px 14px; border-bottom: 2px solid var(--border);
}
.compare td { padding: 10px 14px; border-bottom: 1px solid #f0edf3; }
```

## Sentry Logo

**Do NOT hardcode the Sentry logo as inline SVG.** Read the official SVGs from the skill references directory:

- **Full wordmark**: `references/sentry-logo.svg` — the "Sentry" logotype with glyph. Use on title slides.
- **Glyph only**: `references/sentry-glyph.svg` — the standalone glyph mark. Use for compact branding.

Read the SVG file contents and embed them as a React component using `dangerouslySetInnerHTML` or by extracting the `<path>` data:

```jsx
// Read the SVG file, extract the content, and embed it:
function SentryLogo({ width = 120 }) {
  // The SVG content should be read from references/sentry-logo.svg
  // and embedded directly — never use a hardcoded approximation.
  return (
    <svg viewBox="0 0 200 44" width={width} fill="none" aria-hidden="true">
      {/* paste exact path data from sentry-logo.svg */}
    </svg>
  );
}

function SentryGlyph({ size = 32 }) {
  return (
    <svg viewBox="0 0 72 66" width={size} height={size} aria-hidden="true">
      {/* paste exact path data from sentry-glyph.svg */}
    </svg>
  );
}
```

## Wrapup Column Variants

For summary/decision slides with multi-column layouts:

```css
.wrapup-col--muted { border-top: 3px solid #80708f; }
.wrapup-col--muted h3 { color: #3e3450; }
.wrapup-col--muted li::before {
  content: 'chevron_right';
  font-family: 'Material Symbols Outlined';
  color: #80708f;
}
```
references/sentry-glyph.svg›
<svg class="css-lfbo6j e1igk8x04" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 66" width="400" height="367" aria-hidden="true"><path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z" transform="translate(11, 11)" fill="#181225"></path></svg>
references/sentry-logo.svg›
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 44" aria-hidden="true" class="css-4zleql e6sdxp70"><path fill="currentColor" d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"></path></svg>
SKILL.md›
---
name: presentation-creator
description: Create data-driven presentation slides using React, Vite, and Recharts with Sentry branding. Use when asked to "create a presentation", "build slides", "make a deck", "create a data presentation", "build a Sentry presentation". Scaffolds a complete slide-based app with charts, animations, and single-file HTML output.
---

# Sentry Presentation Builder

Create interactive, data-driven presentation slides using React + Vite + Recharts, styled with the Sentry design system and built as a single distributable HTML file.

## Step 1: Gather Requirements

Ask the user:
1. What is the presentation topic?
2. How many slides (typically 5-8)?
3. What data/charts are needed? (time series, comparisons, diagrams, zone charts)
4. What is the narrative arc? (problem → solution, before → after, technical deep-dive)

### Data Assessment (CRITICAL)

Before designing any slides, assess whether the source content contains **real quantitative data** (numbers, percentages, measurements, time series, costs, metrics). Only create Recharts visualizations for slides where real data exists. Do NOT fabricate, estimate, or invent data to fill charts.

- **Has real data** → use a Recharts chart (bar, area, line, etc.)
- **Has no data** → use text-based layouts: cards, tables, bullet columns, diagrams, or quote blocks. Do NOT create a chart with made-up numbers.

If the source content is purely qualitative (narrative, opinions, strategy, process descriptions), the presentation should use zero charts. Recharts and `Charts.jsx` should only be included in the project if at least one slide has real data to visualize.

## Step 2: Scaffold the Project

Create the project structure:

```
<project-name>/
├── index.html
├── package.json
├── vite.config.js
└── src/
    ├── main.jsx
    ├── App.jsx
    ├── App.css
    └── Charts.jsx
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
    <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=swap" rel="stylesheet" />
    <title>TITLE</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
```

### package.json

```json
{
  "name": "PROJECT_NAME",
  "private": true,
  "type": "module",
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", "recharts": "^2.15.3" },
  "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.0", "vite-plugin-singlefile": "^2.3.0" }
}
```

### vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteSingleFile } from 'vite-plugin-singlefile'

export default defineConfig({ plugins: [react(), viteSingleFile()] })
```

### main.jsx

```jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './App.css'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)
```

## Step 3: Build the Slide System

Read `references/design-system.md` for the complete Sentry color palette, typography, CSS variables, layout utilities, and animation system.

### App.jsx Structure

Define slides as an array of functions returning JSX:

```jsx
const SLIDES = [
  () => ( /* Slide 0: Title */ ),
  () => ( /* Slide 1: Context */ ),
  // ...
];
```

Each slide function returns a `<div className="slide-content">` with:
1. An `<h2>` heading
2. Optional subtitle paragraph
3. Main content (charts, cards, diagrams, tables)
4. Animation classes: `.anim`, `.d1`, `.d2`, `.d3` for staggered fade-in

Do NOT add category tag pills/badges above headings (e.g., "BACKGROUND", "EXPERIMENTS"). They look generic and add no value. Let the heading speak for itself.

### Navigation

Implement keyboard navigation (ArrowRight/Space = next, ArrowLeft = prev) and a bottom nav overlay with prev/next buttons, dot indicators, and slide number. The nav has **no border or background** — it floats transparently. A small low-contrast Sentry glyph watermark sits fixed in the top-left corner of every slide.

```jsx
function App() {
  const [cur, setCur] = useState(0);
  const go = useCallback((d) => setCur(c => Math.max(0, Math.min(SLIDES.length - 1, c + d))), []);

  useEffect(() => {
    const h = (e) => {
      if (e.target.tagName === 'INPUT') return;
      if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); go(1); }
      if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [go]);

  return (
    <>
      {cur > 0 && <div className="glyph-watermark"><SentryGlyph size={50} /><span className="watermark-title">TITLE</span></div>}
      <div className="progress" style={{ width: `${((cur + 1) / SLIDES.length) * 100}%` }} />
      {SLIDES.map((S, i) => (
        <div key={i} className={`slide ${i === cur ? 'active' : ''}`}>
          <div className={`slide-content${i === cur ? ' anim' : ''}`}>
            <S />
          </div>
        </div>
      ))}
      <Nav cur={cur} total={SLIDES.length} go={go} setCur={setCur} />
    </>
  );
}
```

## Step 4: Create Charts (Only When Data Exists)

**IMPORTANT: Only create charts for slides backed by real, concrete data from the source content.** If a slide's content is qualitative (strategies, learnings, process descriptions, opinions), use text-based layouts instead (cards, tables, bullet lists, columns). Never invent numbers, fabricate percentages, or generate synthetic data to populate a chart. If you are unsure whether data is real or inferred, do NOT create a chart.

If NO slides require charts, skip this step entirely — do not create `Charts.jsx` or import Recharts.

When real data IS available, read `references/chart-patterns.md` for Recharts component patterns including axis configuration, color constants, chart types, and data generation techniques.

Put all chart components in `Charts.jsx`. Key patterns:

- Use `ResponsiveContainer` with explicit height
- Wrap in `.chart-wrap` div with max-width 920px
- Use `useMemo` for data generation
- **Color rule**: Use the Tableau-inspired categorical palette (`CAT[]`) for distinguishing data series and groups. Only use semantic colors (`SEM_GREEN`, `SEM_RED`, `SEM_AMBER`) when the color itself carries meaning (good/bad, success/failure, warning).
- Common charts: `ComposedChart` with stacked `Area`/`Line`, `BarChart`, custom SVG diagrams
- **Every data point in a chart must come from the source content.** Do not interpolate, extrapolate, or round numbers to make charts look better.

## Step 5: Style with Sentry Design System

Apply the complete CSS from the design system reference. Key elements:

- **Font**: Rubik from Google Fonts
- **Colors**: CSS variables for UI chrome (`--purple`, `--dark`, `--muted`). Semantic CSS variables (`--semantic-green`, `--semantic-red`, `--semantic-amber`) only where color conveys meaning. Categorical palette (`CAT[]`) for all other data visualization.
- **Slides**: Absolute positioned, opacity transitions
- **Animations**: `fadeUp` keyframe with staggered delays
- **Layout**: `.cols` flex rows, `.cards` grid, `.chart-wrap` containers
- **Tags**: `.tag-purple`, `.tag-red`, `.tag-green`, `.tag-amber` for slide labels
- **Logo**: Read the official SVG from `references/sentry-logo.svg` (full wordmark) or `references/sentry-glyph.svg` (glyph only). Do NOT hardcode an approximation — always use the exact SVG paths from these files.

## Step 6: Common Slide Patterns

### Title Slide
Logo (from `references/sentry-logo.svg` or `references/sentry-glyph.svg`) + h1 + subtitle + author/date info.

### Problem/Context Slide
Tag + heading + 2-column card grid with icon headers.

### Data Comparison Slide
Tag + heading + side-by-side charts or before/after comparison table.

### Technical Deep-Dive Slide
Tag + heading + full-width chart + annotation bullets below.

### Summary/Decision Slide
Tag + heading + 3-column layout with category headers and bullet lists.

## Step 7: Iterate and Refine

After initial scaffolding:
1. Run `npm install && npm run dev` to start the dev server
2. Iterate on chart data models and visual design
3. Adjust animations, colors, and layout spacing
4. Build final output: `npm run build` produces a single HTML file in `dist/`

## Output Expectations

A working React + Vite project that:
- Renders as a keyboard-navigable slide deck
- Uses Sentry branding (colors, fonts, icons)
- Contains Recharts visualizations **only for slides with real quantitative data** from the source content — no fabricated data
- Omits `Charts.jsx` and the Recharts dependency entirely if no slides have real data
- Builds to a single distributable HTML file
- Has smooth fade-in animations on slide transitions