Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/docs/app/examples/ExamplesLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { Header } from '@/components/Header';
import { type ReactNode, useEffect, useState } from 'react';

import { ExamplesSidebar } from './ExamplesSidebar';

export interface ExamplesLayoutProps {
children: ReactNode;
}

export function ExamplesLayout({ children }: ExamplesLayoutProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

const handleMobileMenuToggle = () => {
setIsMobileMenuOpen((prev) => !prev);
};

const handleMobileMenuClose = () => {
setIsMobileMenuOpen(false);
};

// Prevent body scroll when mobile menu is open
useEffect(() => {
if (isMobileMenuOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}

return () => {
document.body.classList.remove('overflow-hidden');
};
}, [isMobileMenuOpen]);

return (
<>
<Header
onMobileMenuToggle={handleMobileMenuToggle}
className="-mb-[1px]"
/>
<div className="relative gap-6 pt-6 md:grid md:grid-cols-[180px_1fr] md:gap-10">
<ExamplesSidebar
isMobileOpen={isMobileMenuOpen}
onMobileClose={handleMobileMenuClose}
/>
{children}
</div>
</>
);
}
87 changes: 87 additions & 0 deletions apps/docs/app/examples/ExamplesSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import NavLink from '@/components/NavLink';
import { useEffect, useState } from 'react';

interface ExamplesSidebarProps {
isMobileOpen?: boolean;
onMobileClose?: () => void;
}

const EXAMPLES = [
{ id: 'theme-carousel', label: 'Theme Carousel' },
{ id: 'custom-chrome', label: 'Custom Chrome' },
{ id: 'hover-actions', label: 'Hover Actions' },
{ id: 'ai-code-review', label: 'AI Code Review' },
{ id: 'pr-review', label: 'PR Review' },
{ id: 'git-blame', label: 'Git Blame' },
] as const;

export function ExamplesSidebar({
isMobileOpen = false,
onMobileClose,
}: ExamplesSidebarProps) {
const [activeSection, setActiveSection] = useState<string>(EXAMPLES[0].id);

useEffect(() => {
const handleScroll = () => {
// Find the section that's currently in view
for (let i = EXAMPLES.length - 1; i >= 0; i--) {
const example = EXAMPLES[i];
const element = document.getElementById(example.id);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= 120) {
setActiveSection(example.id);
return;
}
}
}
// Default to first section
setActiveSection(EXAMPLES[0].id);
};

window.addEventListener('scroll', handleScroll);
handleScroll();

return () => window.removeEventListener('scroll', handleScroll);
}, []);

// Handle initial hash
useEffect(() => {
if (window.location.hash) {
const id = window.location.hash.slice(1);
const element = document.getElementById(id);
if (element) {
setActiveSection(id);
element.scrollIntoView({ behavior: 'instant', block: 'start' });
}
}
}, []);

return (
<>
{isMobileOpen && (
<div
className="bg-background/50 fixed inset-0 z-[50] backdrop-blur-sm transition-opacity duration-200 md:hidden"
onClick={onMobileClose}
/>
)}

<nav
className={`docs-sidebar ${isMobileOpen ? 'is-open' : ''}`}
onClick={onMobileClose}
>
{EXAMPLES.map((example) => (
<NavLink
key={example.id}
href={`#${example.id}`}
active={activeSection === example.id}
>
{example.label}
</NavLink>
))}
</nav>
</>
);
}
140 changes: 140 additions & 0 deletions apps/docs/app/examples/ai-code-review/AICodeReview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client';

import { IconCheck, IconSparkle, IconX } from '@/components/icons';
import { FileDiff } from '@pierre/diffs/react';
import type { PreloadFileDiffResult } from '@pierre/diffs/ssr';
import { useState } from 'react';

import type { AIAnnotation } from './constants';

// =============================================================================
// Main Component
// =============================================================================

interface AICodeReviewProps {
prerenderedDiff: PreloadFileDiffResult<AIAnnotation>;
}

export function AICodeReview({ prerenderedDiff }: AICodeReviewProps) {
const [annotations, setAnnotations] = useState(
prerenderedDiff.annotations ?? []
);
const [resolvedCount, setResolvedCount] = useState(0);

const handleResolve = (lineNumber: number) => {
setAnnotations((prev) => prev.filter((a) => a.lineNumber !== lineNumber));
setResolvedCount((c) => c + 1);
};

const handleDismiss = (lineNumber: number) => {
setAnnotations((prev) => prev.filter((a) => a.lineNumber !== lineNumber));
};

return (
<div className="space-y-5">
<div>
<div className="flex items-center gap-2">
<IconSparkle size={20} className="text-purple-400" />
<h2 className="text-2xl font-medium">AI Code Review</h2>
</div>
<p className="text-muted-foreground">
Use annotations to display inline AI suggestions, warnings, and code
improvements. Each annotation can have custom actions like accept,
dismiss, or apply fix.
</p>
</div>

{annotations.length > 0 && (
<div className="flex items-center gap-2 rounded-lg bg-purple-950/50 px-4 py-2 text-sm text-purple-300">
<IconSparkle size={16} />
<span>
{annotations.length} suggestion{annotations.length !== 1 && 's'}{' '}
remaining
{resolvedCount > 0 && ` · ${resolvedCount} resolved`}
</span>
</div>
)}

<div className="overflow-hidden rounded-lg border border-neutral-800">
<FileDiff
{...prerenderedDiff}
lineAnnotations={annotations}
renderAnnotation={(annotation) => (
<AIAnnotationCard
annotation={annotation.metadata}
onResolve={() => handleResolve(annotation.lineNumber)}
onDismiss={() => handleDismiss(annotation.lineNumber)}
/>
)}
/>
</div>
</div>
);
}

// =============================================================================
// Annotation Card
// =============================================================================

function AIAnnotationCard({
annotation,
onResolve,
onDismiss,
}: {
annotation: AIAnnotation;
onResolve: () => void;
onDismiss: () => void;
}) {
const bgColor =
annotation.type === 'warning'
? 'bg-amber-950/80 border-amber-800/50'
: annotation.type === 'suggestion'
? 'bg-purple-950/80 border-purple-800/50'
: 'bg-blue-950/80 border-blue-800/50';

const iconColor =
annotation.type === 'warning'
? 'text-amber-400'
: annotation.type === 'suggestion'
? 'text-purple-400'
: 'text-blue-400';

return (
<div className="p-4" style={{ fontFamily: 'system-ui, sans-serif' }}>
<div className={`rounded-lg border p-4 ${bgColor}`}>
<div className="flex items-start gap-3">
<IconSparkle
size={16}
className={`mt-0.5 flex-shrink-0 ${iconColor}`}
/>
<div className="min-w-0 flex-1">
<p className="text-sm text-neutral-200">{annotation.message}</p>

{annotation.suggestion && (
<pre className="mt-3 overflow-x-auto rounded bg-black/30 p-3 text-xs text-neutral-300">
<code>{annotation.suggestion}</code>
</pre>
)}

<div className="mt-3 flex items-center gap-2">
<button
onClick={onResolve}
className="flex cursor-pointer items-center gap-1.5 rounded-md bg-purple-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-purple-500"
>
<IconCheck size={12} />
{annotation.suggestion ? 'Apply Fix' : 'Resolve'}
</button>
<button
onClick={onDismiss}
className="flex cursor-pointer items-center gap-1.5 rounded-md bg-neutral-700 px-3 py-1.5 text-xs font-medium text-neutral-300 transition-colors hover:bg-neutral-600"
>
<IconX size={12} />
Dismiss
</button>
</div>
</div>
</div>
</div>
</div>
);
}
113 changes: 113 additions & 0 deletions apps/docs/app/examples/ai-code-review/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CustomScrollbarCSS } from '@/components/CustomScrollbarCSS';
import {
type DiffLineAnnotation,
type FileContents,
parseDiffFromFile,
} from '@pierre/diffs';
import type { PreloadFileDiffOptions } from '@pierre/diffs/ssr';

export interface AIAnnotation {
type: 'suggestion' | 'warning' | 'info';
message: string;
suggestion?: string;
}

const OLD_FILE: FileContents = {
name: 'auth.ts',
contents: `import { hash, compare } from 'bcrypt';
import { sign, verify } from 'jsonwebtoken';

const SECRET = 'my-secret-key';
const SALT_ROUNDS = 10;

export async function hashPassword(password: string) {
return hash(password, SALT_ROUNDS);
}

export async function verifyPassword(password: string, hashed: string) {
return compare(password, hashed);
}

export function createToken(userId: string) {
return sign({ userId }, SECRET, { expiresIn: '24h' });
}

export function verifyToken(token: string) {
try {
return verify(token, SECRET);
} catch {
return null;
}
}
`,
};

const NEW_FILE: FileContents = {
name: 'auth.ts',
contents: `import { hash, compare } from 'bcrypt';
import { sign, verify } from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;
const SALT_ROUNDS = 12;

export async function hashPassword(password: string): Promise<string> {
return hash(password, SALT_ROUNDS);
}

export async function verifyPassword(
password: string,
hashed: string
): Promise<boolean> {
return compare(password, hashed);
}

export function createToken(userId: string, role: string = 'user'): string {
return sign({ userId, role }, SECRET, { expiresIn: '1h' });
}

export function verifyToken(token: string): TokenPayload | null {
try {
return verify(token, SECRET) as TokenPayload;
} catch {
return null;
}
}

interface TokenPayload {
userId: string;
role: string;
}
`,
};

const ANNOTATIONS: DiffLineAnnotation<AIAnnotation>[] = [
{
side: 'additions',
lineNumber: 4,
metadata: {
type: 'warning',
message:
'Using non-null assertion on environment variable. Consider adding runtime validation.',
suggestion: `const SECRET = process.env.JWT_SECRET;
if (!SECRET) throw new Error('JWT_SECRET not configured');`,
},
},
{
side: 'additions',
lineNumber: 18,
metadata: {
type: 'suggestion',
message: 'Consider using a shorter token expiry for better security.',
},
},
];

export const AI_CODE_REVIEW_EXAMPLE: PreloadFileDiffOptions<AIAnnotation> = {
fileDiff: parseDiffFromFile(OLD_FILE, NEW_FILE),
options: {
theme: 'pierre-dark',
diffStyle: 'unified',
unsafeCSS: CustomScrollbarCSS,
},
annotations: ANNOTATIONS,
};
Loading
Loading