diff --git a/app/globals.css b/app/globals.css
index 0e0228e..a678eff 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -237,6 +237,21 @@ kbd {
color: inherit;
}
+/* Search Highlight - Discreet styling */
+mark {
+ background: color-mix(in oklab, var(--primary) 15%, transparent);
+ color: var(--primary);
+ font-weight: 600;
+ padding: 0.125rem 0.25rem;
+ border-radius: 0.25rem;
+ border-bottom: 2px solid color-mix(in oklab, var(--primary) 40%, transparent);
+}
+
+.dark mark {
+ background: color-mix(in oklab, var(--primary) 20%, transparent);
+ color: var(--primary-foreground);
+}
+
/* Loading Spinner */
@keyframes spin-awesome {
from {
diff --git a/app/layout.tsx b/app/layout.tsx
index abeabbb..ff02cf0 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -6,6 +6,7 @@ import { Toaster } from '@/components/ui/sonner'
import { WorkerProvider } from '@/components/providers/worker-provider'
import { CommandProvider } from '@/components/providers/command-provider'
import { AppHeader } from '@/components/layout/app-header'
+import { AppFooter } from '@/components/layout/app-footer'
import { ThemeProvider } from 'next-themes'
const inter = Inter({ subsets: ['latin'] })
@@ -203,8 +204,11 @@ export default function RootLayout({
>
-
- {children}
+
diff --git a/app/page.tsx b/app/page.tsx
index 532c759..5217e4e 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -237,39 +237,6 @@ export default function Home() {
-
- {/* Footer */}
-
)
}
diff --git a/app/readme/[owner]/[repo]/page.tsx b/app/readme/[owner]/[repo]/page.tsx
index b3ee514..fefb4e3 100644
--- a/app/readme/[owner]/[repo]/page.tsx
+++ b/app/readme/[owner]/[repo]/page.tsx
@@ -92,7 +92,7 @@ export default async function ReadmePage({ params }: PageProps) {
}
>
-
+
diff --git a/app/repository/[id]/page.tsx b/app/repository/[id]/page.tsx
index 1eedbfc..d6cce82 100644
--- a/app/repository/[id]/page.tsx
+++ b/app/repository/[id]/page.tsx
@@ -192,7 +192,7 @@ export default function RepositoryDetailPage() {
{data.readme?.content ? (
-
+
) : (
diff --git a/app/search/page.tsx b/app/search/page.tsx
index 5099f9b..5c4ae5d 100644
--- a/app/search/page.tsx
+++ b/app/search/page.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Suspense } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
-import { Search, Star, Filter, SlidersHorizontal, ExternalLink } from 'lucide-react'
+import { Search, Star, Filter, SlidersHorizontal, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -129,6 +129,48 @@ function SearchPageContent() {
setFilters(prev => ({ ...prev, [key]: value }))
}
+ // Strip markdown and HTML from descriptions
+ const stripMarkdown = (text: string | null): string => {
+ if (!text) return ''
+ return text
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert [text](url) to text
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') // Convert  to alt text
+ .replace(/[*_~`#]/g, '') // Remove markdown formatting chars
+ .replace(/\s+/g, ' ') // Normalize whitespace
+ .trim()
+ }
+
+ // Generate page numbers for pagination
+ const getPageNumbers = (current: number, total: number): (number | string)[] => {
+ const delta = 2
+ const range: (number | string)[] = []
+ const rangeWithDots: (number | string)[] = []
+ let l: number | undefined
+
+ for (let i = 1; i <= total; i++) {
+ if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
+ range.push(i)
+ }
+ }
+
+ for (const i of range) {
+ if (l !== undefined && typeof i === 'number') {
+ if (i - l === 2) {
+ rangeWithDots.push(l + 1)
+ } else if (i - l !== 1) {
+ rangeWithDots.push('...')
+ }
+ }
+ rangeWithDots.push(i)
+ if (typeof i === 'number') {
+ l = i
+ }
+ }
+
+ return rangeWithDots
+ }
+
React.useEffect(() => {
if (query) {
performSearch(query)
@@ -314,7 +356,7 @@ function SearchPageContent() {
{result.description && (
-
{result.description}
+
{stripMarkdown(result.description)}
)}
{result.snippet && (
@@ -343,23 +385,43 @@ function SearchPageContent() {
{/* Pagination */}
{results.totalPages > 1 && (
-
+
-
- Page {results.page} of {results.totalPages}
-
+
+ {getPageNumbers(results.page, results.totalPages).map((pageNum, idx) =>
+ pageNum === '...' ? (
+
+ ...
+
+ ) : (
+
+ )
+ )}
+
)}
diff --git a/components/layout/app-footer.tsx b/components/layout/app-footer.tsx
new file mode 100644
index 0000000..a6f386c
--- /dev/null
+++ b/components/layout/app-footer.tsx
@@ -0,0 +1,37 @@
+import Link from 'next/link'
+
+export function AppFooter() {
+ return (
+
+ )
+}
diff --git a/components/readme/readme-viewer.tsx b/components/readme/readme-viewer.tsx
index c73457c..4075b2a 100644
--- a/components/readme/readme-viewer.tsx
+++ b/components/readme/readme-viewer.tsx
@@ -8,9 +8,10 @@ import 'highlight.js/styles/github-dark.css'
interface ReadmeViewerProps {
content: string
+ repositoryUrl?: string
}
-export function ReadmeViewer({ content }: ReadmeViewerProps) {
+export function ReadmeViewer({ content, repositoryUrl }: ReadmeViewerProps) {
const [html, setHtml] = React.useState('')
React.useEffect(() => {
@@ -30,13 +31,59 @@ export function ReadmeViewer({ content }: ReadmeViewerProps) {
breaks: true,
})
+ // Custom renderer to fix relative image URLs
+ if (repositoryUrl) {
+ const renderer = {
+ image(href: string, title: string | null, text: string) {
+ let imgSrc = href
+
+ // Convert GitHub URLs to raw content URLs
+ if (repositoryUrl.includes('github.com')) {
+ const match = repositoryUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/)
+ if (match) {
+ const [, owner, repo] = match
+ const cleanRepo = repo.replace(/\.git$/, '')
+
+ // Handle relative URLs
+ if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://') && !imgSrc.startsWith('//')) {
+ // Remove leading ./
+ imgSrc = imgSrc.replace(/^\.\//, '')
+ // Build raw GitHub URL (main/master branch assumed)
+ imgSrc = `https://raw.githubusercontent.com/${owner}/${cleanRepo}/master/${imgSrc}`
+ }
+ // Handle GitHub blob URLs - convert to raw
+ else if (imgSrc.includes('github.com') && imgSrc.includes('/blob/')) {
+ imgSrc = imgSrc.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
+ }
+ }
+ }
+
+ const titleAttr = title ? ` title="${title}"` : ''
+
+ // Check if it's a badge (shields.io, badgen, etc.)
+ const isBadge = imgSrc.includes('shields.io') ||
+ imgSrc.includes('badgen.net') ||
+ imgSrc.includes('badge') ||
+ imgSrc.includes('img.shields') ||
+ imgSrc.match(/\/badges?\//)
+
+ if (isBadge) {
+ return `

`
+ }
+
+ return `

`
+ }
+ }
+ marked.use({ renderer })
+ }
+
// Parse markdown
const parseMarkdown = async () => {
const result = await marked.parse(content)
setHtml(result)
}
parseMarkdown()
- }, [content])
+ }, [content, repositoryUrl])
return (