Skip to content
Merged

Main #131

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
610 changes: 610 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"react-day-picker": "^9.11.0",
"react-dom": "19.1.0",
"react-syntax-highlighter": "^15.6.6",
"shiki": "^3.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1"
},
Expand Down
15 changes: 10 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import BackToTopButton from '@/components/ui/BackToTopButton';
import "./globals.css";
import ThemeColorPicker from "@/components/ui/ThemeColorPicker";
import { ThemeProvider } from "next-themes"; // ⬅️ import

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -17,7 +18,8 @@ const geistMono = Geist_Mono({

export const metadata: Metadata = {
title: "DevUI",
description: "a modern, open-source component library showcase built with shadcn/ui components",
description:
"a modern, open-source component library showcase built with shadcn/ui components",
};

export default function RootLayout({
Expand All @@ -26,11 +28,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ThemeColorPicker />
{children}
<BackToTopButton />
{/* ✅ Wrap everything in ThemeProvider */}
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<ThemeColorPicker />
{children}
<BackToTopButton />
</ThemeProvider>
</body>
</html>
);
Expand Down
3 changes: 3 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import Header from "@/components/Header";

const Index = () => {
const [searchQuery, setSearchQuery] = useState("");
Expand Down Expand Up @@ -67,6 +68,8 @@ const Index = () => {

return (
<div className="min-h-screen bg-background">

<Header />
{/* Hero Section - Modern & Clean */}
<section
id="main-content"
Expand Down
188 changes: 126 additions & 62 deletions src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import {
vscDarkPlus,
vs,
} from "react-syntax-highlighter/dist/esm/styles/prism";
import { codeToHtml } from "shiki";
import { Check, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
Expand All @@ -23,103 +19,171 @@ export const CodeBlock = ({
showLineNumbers = true,
}: CodeBlockProps) => {
const [copied, setCopied] = useState(false);
const { theme, resolvedTheme } = useTheme();
const [highlightedCode, setHighlightedCode] = useState<string>("");
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);

// Prevent hydration mismatch
// Calculate line count and create line numbers array
const lines = code.split('\n');
const lineCount = lines.length;
const maxLineNumberWidth = lineCount.toString().length;

useEffect(() => {
setMounted(true);
}, []);

const currentTheme = mounted ? resolvedTheme || theme : "dark";
const syntaxTheme = currentTheme === "dark" ? vscDarkPlus : vs;
useEffect(() => {
const generateHighlight = async () => {
try {
const theme = mounted && resolvedTheme === "dark" ? "github-dark" : "github-light";
const html = await codeToHtml(code, {
lang: language,
theme,
});

// If line numbers are enabled, process the HTML to add line number structure
if (showLineNumbers) {
const processedHtml = addLineNumbersToHtml(html, lineCount);
setHighlightedCode(processedHtml);
} else {
setHighlightedCode(html);
}
} catch (error) {
console.error("Highlighting error:", error);
setHighlightedCode(`<pre><code>${code}</code></pre>`);
}
};

generateHighlight();
}, [code, language, mounted, resolvedTheme, showLineNumbers, lineCount]);

// Function to add line numbers to the highlighted HTML
const addLineNumbersToHtml = (html: string, totalLines: number) => {
// Split HTML by newlines and add line number structure
const htmlLines = html.split('\n');
const preMatch = html.match(/<pre[^>]*>/);
const codeMatch = html.match(/<code[^>]*>/);
const preOpenTag = preMatch ? preMatch[0] : '<pre>';
const codeOpenTag = codeMatch ? codeMatch[0] : '<code>';

// Extract the content between <code> and </code>
const codeContent = html.match(/<code[^>]*>([\s\S]*?)<\/code>/)?.[1] || '';
const lines = codeContent.split('\n');

// Wrap each line with line number data
const numberedLines = lines.map((line, index) => {
const lineNumber = index + 1;
return `<span class="code-line" data-line-number="${lineNumber}">${line}</span>`;
}).join('\n');

return `${preOpenTag}${codeOpenTag}${numberedLines}</code></pre>`;
};

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
toast.success("Code copied to clipboard!", {
duration: 2000,
});
toast.success("Code copied to clipboard!");
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error("Failed to copy code");
}
};

return (
<div className="relative rounded-xl overflow-hidden border border-border bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md transition-shadow">
{/* macOS-style Header with Traffic Lights */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-secondary/80 dark:bg-secondary/50">
<div className="group relative overflow-hidden rounded-xl border border-primary/20 bg-card/50 shadow-lg transition-all duration-300 hover:shadow-xl hover:border-primary/30">
{/* Header with primary color accents */}
<div className="flex items-center justify-between px-4 py-3 border-b border-primary/20 bg-gradient-to-r from-primary/5 to-primary/10">
<div className="flex items-center gap-3">
{/* macOS Traffic Light Dots */}
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-colors" />
<div className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-colors" />
<div className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-colors" />
{/* VS Code style dots with primary color */}
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-primary/60" />
<div className="w-3 h-3 rounded-full bg-primary/40" />
<div className="w-3 h-3 rounded-full bg-primary/20" />
</div>
{/* Language Label */}
<span className="text-xs sm:text-sm font-mono text-muted-foreground uppercase tracking-wider font-semibold">
<span className="text-xs font-mono text-primary font-semibold uppercase tracking-wider">
{language}
</span>
</div>

{/* Copy Button */}
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="h-7 sm:h-8 px-2 sm:px-3 hover:bg-accent/50 transition-all"
aria-label={copied ? "Code copied" : "Copy code"}
className="h-8 px-3 text-primary hover:bg-primary/10 hover:text-primary transition-all"
>
{copied ? (
<>
<Check className="h-3 w-3 sm:h-4 sm:w-4 mr-1 text-green-500" />
<span className="text-xs hidden sm:inline">Copied!</span>
<Check className="w-4 h-4 mr-2" />
<span className="text-xs">Copied!</span>
</>
) : (
<>
<Copy className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
<span className="text-xs hidden sm:inline">Copy</span>
<Copy className="w-4 h-4 mr-2" />
<span className="text-xs">Copy</span>
</>
)}
</Button>
</div>

{/* Code Content */}
<div className="overflow-x-auto">
<SyntaxHighlighter
language={language}
style={{
...syntaxTheme,
'code[class*="language-"]': {
...syntaxTheme['code[class*="language-"]'],
background: "transparent",
backgroundColor: "transparent",
},
}}
showLineNumbers={showLineNumbers}
customStyle={{
margin: 0,
padding: "0.875rem 1rem",
background: "transparent",
backgroundColor: "transparent",
fontSize: "0.8125rem",
lineHeight: "1.6",
}}
codeTagProps={{
style: {
fontSize: "0.8125rem",
fontFamily:
"'Fira Code', 'JetBrains Mono', 'Courier New', monospace",
},
}}
wrapLongLines={false}
className="scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent"
>
{code}
</SyntaxHighlighter>
{/* Code content with Shiki highlighting and logical line numbers */}
<div
className="overflow-x-auto text-sm leading-relaxed relative"
style={{
fontFamily: "'Fira Code', 'JetBrains Mono', Consolas, monospace",
}}
>
{showLineNumbers && (
<div
className="absolute left-0 top-0 flex flex-col py-4 px-2 text-right select-none pointer-events-none z-10"
style={{
width: `${Math.max(2.5, maxLineNumberWidth * 0.6 + 1)}rem`,
backgroundColor: 'transparent',
borderRight: '1px solid hsl(var(--primary) / 0.2)',
}}
>
{Array.from({ length: lineCount }, (_, i) => (
<span
key={i + 1}
className="block text-xs leading-relaxed opacity-70 hover:opacity-100 transition-opacity"
style={{
color: 'hsl(var(--primary) / 0.8)',
lineHeight: '1.5',
height: '1.5em',
}}
>
{i + 1}
</span>
))}
</div>
)}

{highlightedCode ? (
<div
dangerouslySetInnerHTML={{ __html: highlightedCode }}
className={`
[&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:py-4 [&_code]:!bg-transparent
${showLineNumbers ? `[&_pre]:pl-[${Math.max(2.5, maxLineNumberWidth * 0.6 + 1) + 1}rem]` : '[&_pre]:px-4'}
`}
style={{
marginLeft: showLineNumbers ? `${Math.max(2.5, maxLineNumberWidth * 0.6 + 1)}rem` : '0',
paddingLeft: showLineNumbers ? '1rem' : '1rem',
}}
/>
) : (
<pre
className={`py-4 text-muted-foreground ${showLineNumbers ? 'pl-16' : 'px-4'}`}
style={{
marginLeft: showLineNumbers ? `${Math.max(2.5, maxLineNumberWidth * 0.6 + 1)}rem` : '0',
}}
>
<code>{code}</code>
</pre>
)}
</div>

{/* Subtle primary color accent at bottom */}
<div className="h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
</div>
);
};
4 changes: 2 additions & 2 deletions src/components/ComponentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const ComponentCard = ({
{/* Tabs Section - Enhanced */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="px-4 sm:px-5 lg:px-6 py-3 border-b border-border ">
<TabsList className="bg-secondary/70 h-10 sm:h-11 p-1">
<TabsList className="bg-gray-200 h-10 sm:h-11 p-1">
<TabsTrigger
value="preview"
className="flex items-center gap-2 text-sm px-4 data-[state=active]:bg-card data-[state=active]:shadow-sm"
Expand All @@ -159,7 +159,7 @@ export const ComponentCard = ({
value="preview"
className="p-4 sm:p-6 lg:p-8 min-h-[200px] sm:min-h-[240px]"
>
<div className="w-full flex items-center justify-center p-8 rounded-xl border-2 border-dashed border-border/50 bg-secondary/10 hover:border-border transition-colors">
<div className="w-full flex items-center justify-center p-8 rounded-xl border-2 border-border/50 bg-hover:border-border transition-colors">
<div className="scale-90 sm:scale-95 lg:scale-100 origin-center">
{preview}
</div>
Expand Down
Loading