{
const [copied, setCopied] = useState(false);
- const { theme, resolvedTheme } = useTheme();
+ const [highlightedCode, setHighlightedCode] = useState("");
+ 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(`${code} `);
+ }
+ };
+
+ 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(/]*>/);
+ const codeMatch = html.match(/]*>/);
+ const preOpenTag = preMatch ? preMatch[0] : '';
+ const codeOpenTag = codeMatch ? codeMatch[0] : '';
+
+ // Extract the content between and
+ const codeContent = html.match(/]*>([\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 `${line} `;
+ }).join('\n');
+
+ return `${preOpenTag}${codeOpenTag}${numberedLines} `;
+ };
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");
@@ -48,78 +91,99 @@ export const CodeBlock = ({
};
return (
-
- {/* macOS-style Header with Traffic Lights */}
-
+
+ {/* Header with primary color accents */}
+
- {/* macOS Traffic Light Dots */}
-
-
-
-
+ {/* VS Code style dots with primary color */}
+
- {/* Language Label */}
-
+
{language}
- {/* Copy Button */}
{copied ? (
<>
-
- Copied!
+
+ Copied!
>
) : (
<>
-
- Copy
+
+ Copy
>
)}
- {/* Code Content */}
-
-
- {code}
-
+ {/* Code content with Shiki highlighting and logical line numbers */}
+
+ {showLineNumbers && (
+
+ {Array.from({ length: lineCount }, (_, i) => (
+
+ {i + 1}
+
+ ))}
+
+ )}
+
+ {highlightedCode ? (
+
+ ) : (
+
+ {code}
+
+ )}
+
+ {/* Subtle primary color accent at bottom */}
+
);
};
\ No newline at end of file
diff --git a/src/components/ComponentCard.tsx b/src/components/ComponentCard.tsx
index b90698d..387e851 100644
--- a/src/components/ComponentCard.tsx
+++ b/src/components/ComponentCard.tsx
@@ -137,7 +137,7 @@ export const ComponentCard = ({
{/* Tabs Section - Enhanced */}
-
+
-
+
{preview}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index aa3a781..50b645a 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -22,6 +22,10 @@ const Header = () => {
const { theme, setTheme } = useTheme();
const pathname = usePathname();
+ // Prevent hydration mismatch
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+
const navigation = [
{ name: "Home", href: "/", icon: Home },
{ name: "Components", href: "#components", icon: Code2 },
@@ -47,7 +51,6 @@ const Header = () => {
return (
<>
- {/* Skip to main content - Accessibility */}
Skip to main content
@@ -56,18 +59,23 @@ const Header = () => {
{/* Logo */}
-
+
-
- DevUI
-
+
DevUI
{/* Desktop Navigation */}
-
+
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
@@ -75,8 +83,11 @@ const Header = () => {
{item.name}
@@ -89,19 +100,29 @@ const Header = () => {
{/* Actions */}
{/* Theme Toggle */}
-
setTheme(theme === "dark" ? "light" : "dark")}
- className="hover:bg-primary/10 transition-all duration-200 focus-ring"
- aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
- >
-
-
-
+ {mounted && (
+
+ setTheme(theme === "dark" ? "light" : "dark")
+ }
+ className="relative hover:bg-primary/10 transition-all duration-200 focus-ring"
+ aria-label={`Switch to ${
+ theme === "dark" ? "light" : "dark"
+ } mode`}
+ >
+
+
+
+ )}
{/* GitHub Link */}
-
+
{
{/* Star Button */}
-
+
{
href={item.href}
onClick={() => setIsMenuOpen(false)}
aria-current={isActive ? "page" : undefined}
- className={`flex items-center px-3 py-2 rounded-md text-base font-medium transition-all duration-200 focus-ring ${isActive
+ className={`flex items-center px-3 py-2 rounded-md text-base font-medium transition-all duration-200 focus-ring ${
+ isActive
? "bg-primary/10 text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-primary/10"
- }`}
+ }`}
>
{item.name}
@@ -189,4 +215,4 @@ const Header = () => {
);
};
-export default Header;
\ No newline at end of file
+export default Header;
diff --git a/src/components/ui/ThemeColorPicker.tsx b/src/components/ui/ThemeColorPicker.tsx
index aaafcda..2c76ba7 100644
--- a/src/components/ui/ThemeColorPicker.tsx
+++ b/src/components/ui/ThemeColorPicker.tsx
@@ -185,7 +185,7 @@ export default function ThemeColorPicker() {
}
`}
-