This commit is contained in:
sean-eskerium
2025-10-08 23:43:43 -04:00
parent 5b839a1465
commit 17ca62ceb4
12 changed files with 1167 additions and 450 deletions

View File

@@ -283,20 +283,90 @@ Based on the argument:
Use grep/glob to find:
```bash
# Hardcoded edge-lit implementations
# Hardcoded edge-lit implementations (should use Card primitive)
grep -r "absolute inset-x-0 top-0" [path]
# Native HTML form elements
# Native HTML form elements (should use Radix)
grep -r "<select>\|<option>\|<input type=\"checkbox\"" [path]
# Hardcoded pill navigation
# Hardcoded pill navigation (should use PillNavigation component)
grep -r "backdrop-blur-sm bg-white/40.*rounded-full" [path]
# Manual glassmorphism
# Manual glassmorphism (should use Card primitive or styles.ts)
grep -r "bg-gradient-to-b from-white/\|from-purple-100/" [path]
# Hardcoded colors instead of semantic tokens
grep -r "#[0-9a-fA-F]{6}" [path]
# CRITICAL: Dynamic Tailwind class construction (WILL NOT WORK)
grep -r "bg-\${.*}\|text-\${.*}\|border-\${.*}\|shadow-\${.*}" [path]
grep -r "\.replace.*rgba\|\.replace.*VAR" [path]
# CRITICAL: Template literal Tailwind classes (WILL NOT WORK)
grep -r "\`bg-.*-.*\`\|\`text-.*-.*\`\|\`border-.*-.*\`" [path]
# Not using pre-defined classes from styles.ts
grep -r "glassCard\.variants\|glassmorphism\." [path] --files-without-match
```
## Critical Anti-Patterns
### 🔴 **BREAKING: Dynamic Tailwind Class Construction**
**Problem:**
```tsx
// ❌ BROKEN - Tailwind processes at BUILD time, not runtime
const color = "cyan";
className={`bg-${color}-500`} // CSS won't be generated!
// ❌ BROKEN - String replacement at runtime
const glow = `shadow-[0_0_30px_rgba(${rgba},0.4)]`;
// ❌ BROKEN - Template literals with variables
<div className={`text-${textColor}-700`} />
```
**Why it fails:**
- Tailwind scans code at BUILD time to generate CSS
- Dynamic strings aren't scanned - no CSS generated
- Results in missing styles at runtime
**Solution:**
```tsx
// ✅ CORRECT - Static class lookup
const colorClasses = {
cyan: "bg-cyan-500 text-cyan-700",
purple: "bg-purple-500 text-purple-700",
};
className={colorClasses[color]}
// ✅ CORRECT - Use pre-defined classes from styles.ts
const glowVariant = glassCard.variants[glowColor];
className={cn(glowVariant.glow, glowVariant.border)}
// ✅ CORRECT - Inline arbitrary values (scanned by Tailwind)
className="shadow-[0_0_30px_rgba(34,211,238,0.4)]"
```
### 🔴 **Not Using styles.ts Pre-Defined Classes**
**Problem:**
```tsx
// ❌ WRONG - Hardcoding glassmorphism
<div className="backdrop-blur-md bg-white/10 border border-gray-200 rounded-lg">
// ❌ WRONG - Not using existing glassCard.variants
const myCustomGlow = "shadow-[0_0_40px_rgba(34,211,238,0.4)]";
```
**Solution:**
```tsx
// ✅ CORRECT - Use glassCard from styles.ts
import { glassCard } from '@/features/ui/primitives/styles';
className={cn(glassCard.base, glassCard.variants.cyan.glow)}
// ✅ CORRECT - Use Card primitive with props
<Card glowColor="cyan" edgePosition="top" edgeColor="purple" />
```
---

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Search, ChevronDown, ChevronRight, Code, FileText } from "lucide-react";
import { Search, Code, FileText, Globe } from "lucide-react";
import { Button } from "@/features/ui/primitives/button";
import {
Dialog,
@@ -9,28 +9,34 @@ import {
} from "@/features/ui/primitives/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/features/ui/primitives/tabs";
import { Input } from "@/features/ui/primitives/input";
import { cn } from "@/features/ui/primitives/styles";
const MOCK_DOCUMENTS = [
{
id: "1",
title: "Getting Started with React",
content:
"React is a JavaScript library for building user interfaces. It lets you compose complex UIs from small and isolated pieces of code called components. React components are JavaScript functions that return markup. Components can be as simple as a function that returns JSX, or they can have state and lifecycle methods...",
tags: ["guide", "intro", "react"],
title: "[Radix Homepage](https://www.radix-ui.com/)[Made by WorkOS](https://workos.com)",
preview: "[Radix Homepage](https://www.radix-ui.com/)[Made by WorkOS]...",
content: "[Radix Homepage](https://www.radix-ui.com/)[Made by WorkOS](https://workos.com)\n\n[ThemesThemes](https://www.radix-ui.com/)[PrimitivesPrimitives](https://www.radix-ui.com/primitives)[IconsIcons](https://www.radix-ui.com/icons)[ColorsColors](https://www.radix-ui.com/colors)\n\n[Documentation](https://www.radix-ui.com/themes/docs/overview/getting-started)[Playground](https://www.radix-ui.com/themes/playground)[Blog](https://www.radix-ui.com/blog)[](https://github.com/radix-ui/themes)",
sourceType: "Web" as const,
category: "Technical" as const,
url: "https://www.radix-ui.com/primitives/docs/guides/styling",
},
{
id: "2",
title: "API Reference - useState Hook",
content:
"useState is a React Hook that lets you add a state variable to your component. Call useState at the top level of your component to declare a state variable. The convention is to name state variables like [something, setSomething] using array destructuring. useState returns an array with exactly two values: the current state and the set function that lets you update it...",
tags: ["api", "hooks", "reference"],
title: "Deleted report #34",
preview: "7-4d586f394674?&w=64&h=64&dpr=2&q=70&crop=faces...",
content: "Detailed report content...",
sourceType: "Document" as const,
category: "Technical" as const,
},
{
id: "3",
title: "Performance Optimization Guide",
content:
"Before you start optimizing, make sure you're actually measuring performance. React DevTools Profiler can help you identify components that are re-rendering unnecessarily. Common optimization techniques include: using React.memo for expensive components, using useMemo and useCallback hooks to memoize values and functions, code splitting with React.lazy and Suspense...",
tags: ["performance", "optimization", "guide"],
title: "Latest updates",
preview: "[Radix Homepage](https://www.radix-ui.com/)[Made by WorkOS]...",
content: "Latest updates and changes...",
sourceType: "Web" as const,
category: "Technical" as const,
url: "https://www.radix-ui.com",
},
];
@@ -38,56 +44,14 @@ const MOCK_CODE = [
{
id: "1",
language: "typescript",
summary: "React functional component with useState",
code: `const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
};`,
file_path: "src/components/Counter.tsx",
summary: "React component example",
code: `const Example = () => {\n return <div>Hello</div>;\n};`,
},
{
id: "2",
language: "python",
summary: "FastAPI endpoint with dependency injection",
code: `@app.get("/api/items/{item_id}")
async def get_item(
item_id: str,
db: Session = Depends(get_db)
):
item = db.query(Item).filter(Item.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item`,
file_path: "src/api/routes/items.py",
},
{
id: "3",
language: "typescript",
summary: "Custom React hook for data fetching",
code: `const useData = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
};`,
file_path: "src/hooks/useData.ts",
summary: "FastAPI endpoint",
code: `@app.get("/api/test")\nasync def test():\n return {"status": "ok"}`,
},
];
@@ -96,17 +60,13 @@ export const DocumentBrowserExample = () => {
return (
<div className="space-y-4">
{/* Explanation Text */}
<p className="text-sm text-gray-600 dark:text-gray-400">
<strong>Use this pattern for:</strong> Displaying structured information in modals
(documents, logs, code, API responses). Tabs organize different data types, search filters
content, items expand/collapse for details.
<strong>Use this pattern for:</strong> Browsing documents and code with sidebar selection,
tabs, and search filtering.
</p>
{/* Button to Open Modal */}
<Button onClick={() => setOpen(true)}>Open Document Browser Example</Button>
<Button onClick={() => setOpen(true)}>Open Document Browser</Button>
{/* Document Browser Modal */}
<DocumentBrowserModal open={open} onOpenChange={setOpen} />
</div>
);
@@ -121,173 +81,148 @@ const DocumentBrowserModal = ({
}) => {
const [activeTab, setActiveTab] = useState<"documents" | "code">("documents");
const [searchQuery, setSearchQuery] = useState("");
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [selectedDoc, setSelectedDoc] = useState(MOCK_DOCUMENTS[0]);
const [selectedCode, setSelectedCode] = useState(MOCK_CODE[0]);
const toggleExpanded = (id: string) => {
setExpandedItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
// Filter based on search
const filteredDocuments = MOCK_DOCUMENTS.filter((doc) =>
doc.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
doc.content.toLowerCase().includes(searchQuery.toLowerCase())
doc.title.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredCode = MOCK_CODE.filter((example) =>
example.summary.toLowerCase().includes(searchQuery.toLowerCase()) ||
example.code.toLowerCase().includes(searchQuery.toLowerCase())
example.summary.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
<DialogHeader>
<DialogContent className="max-w-6xl h-[80vh] flex flex-col p-0">
{/* Header outside tabs */}
<div className="p-6 pb-4">
<DialogTitle>Document Browser</DialogTitle>
<div className="flex items-center gap-2 mt-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
type="text"
placeholder="Search documents and code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-black/30 border-white/10 focus:border-cyan-500/50"
/>
</div>
</div>
{/* Tabs and Content */}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as typeof activeTab)} className="flex-1 flex flex-col px-6">
<div className="flex justify-start mb-4">
<TabsList>
<TabsTrigger value="documents" color="cyan">
<FileText className="w-4 h-4" />
Documents ({filteredDocuments.length})
</TabsTrigger>
<TabsTrigger value="code" color="cyan">
<Code className="w-4 h-4" />
Code Examples ({filteredCode.length})
</TabsTrigger>
</TabsList>
</div>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "documents" | "code")}
className="flex-1 flex flex-col"
>
<TabsList>
<TabsTrigger value="documents" className="data-[state=active]:bg-cyan-500/20">
<FileText className="w-4 h-4 mr-2" />
Documents ({filteredDocuments.length})
</TabsTrigger>
<TabsTrigger value="code" className="data-[state=active]:bg-cyan-500/20">
<Code className="w-4 h-4 mr-2" />
Code Examples ({filteredCode.length})
</TabsTrigger>
</TabsList>
{/* Documents Tab - Left Sidebar + Right Content */}
<TabsContent value="documents" className="flex-1 flex">
{/* Left Sidebar */}
<div className="w-80 flex flex-col pr-4 border-r border-gray-700">
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
placeholder="Search documents..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<TabsContent value="documents" className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
{filteredDocuments.length === 0 ? (
<div className="text-center py-8 text-gray-400">
{searchQuery ? "No documents match your search" : "No documents available"}
<div className="flex-1 overflow-y-auto space-y-2">
{filteredDocuments.map((doc) => (
<button
key={doc.id}
type="button"
onClick={() => setSelectedDoc(doc)}
className={cn(
"w-full text-left p-3 rounded-lg transition-colors",
selectedDoc.id === doc.id
? "bg-cyan-500/10 border border-cyan-500/30"
: "hover:bg-white/5"
)}
>
<div className="flex items-center gap-2 mb-1">
<FileText className="w-4 h-4 text-cyan-400" />
<span className="font-medium text-sm text-white line-clamp-1">{doc.title}</span>
</div>
<p className="text-xs text-gray-400 line-clamp-2">{doc.preview}</p>
</button>
))}
</div>
</div>
{/* Right Content */}
<div className="flex-1 overflow-y-auto pl-6">
{/* Header with badges and URL */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<span className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-cyan-100 text-cyan-700 dark:bg-cyan-500/10 dark:text-cyan-400">
<Globe className="w-3.5 h-3.5" />
{selectedDoc.sourceType}
</span>
<span className="px-2 py-1 text-xs rounded-md font-medium bg-cyan-500/10 text-cyan-600 dark:text-cyan-400">
{selectedDoc.category}
</span>
</div>
) : (
<div className="space-y-3 p-4">
{filteredDocuments.map((doc) => {
const isExpanded = expandedItems.has(doc.id);
const preview = doc.content.substring(0, 200);
const needsExpansion = doc.content.length > 200;
{selectedDoc.url && (
<a
href={selectedDoc.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-cyan-400 hover:text-cyan-300 inline-block mb-4"
>
{selectedDoc.url}
</a>
)}
</div>
return (
<div
key={doc.id}
className="bg-black/30 rounded-lg border border-white/10 p-4 hover:border-cyan-500/30 transition-colors"
>
{doc.title && (
<h4 className="font-medium text-white/90 mb-2 flex items-center gap-2">
{needsExpansion && (
<button
type="button"
onClick={() => toggleExpanded(doc.id)}
className="text-gray-400 hover:text-white transition-colors"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
)}
{doc.title}
</h4>
)}
<div className="text-sm text-gray-300 whitespace-pre-wrap">
{isExpanded || !needsExpansion ? (
doc.content
) : (
<>
{preview}...
<button
type="button"
onClick={() => toggleExpanded(doc.id)}
className="ml-2 text-cyan-400 hover:text-cyan-300"
>
Show more
</button>
</>
)}
</div>
{doc.tags && doc.tags.length > 0 && (
<div className="flex items-center gap-2 mt-3 flex-wrap">
{doc.tags.map((tag) => (
<span key={tag} className="px-2 py-1 text-xs border border-white/20 rounded bg-black/20">
{tag}
</span>
))}
</div>
)}
</div>
);
})}
</div>
)}
<div className="prose prose-invert max-w-none">
<p className="text-gray-300 whitespace-pre-wrap">{selectedDoc.content}</p>
</div>
</div>
</TabsContent>
<TabsContent value="code" className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
{filteredCode.length === 0 ? (
<div className="text-center py-8 text-gray-400">
{searchQuery ? "No code examples match your search" : "No code examples available"}
</div>
) : (
<div className="space-y-3 p-4">
{filteredCode.map((example) => (
<div
key={example.id}
className="bg-black/30 rounded-lg border border-white/10 overflow-hidden hover:border-cyan-500/30 transition-colors"
>
<div className="flex items-center justify-between p-3 border-b border-white/10 bg-black/20">
<div className="flex items-center gap-2">
<Code className="w-4 h-4 text-cyan-400" />
{example.language && (
<span className="px-2 py-1 text-xs bg-cyan-500/20 text-cyan-400 rounded">
{example.language}
</span>
)}
</div>
{example.file_path && <span className="text-xs text-gray-400">{example.file_path}</span>}
</div>
{example.summary && (
<div className="p-3 text-sm text-gray-300 border-b border-white/10">{example.summary}</div>
)}
<pre className="p-4 text-sm overflow-x-auto">
<code className="text-gray-300">{example.code}</code>
</pre>
{/* Code Tab - Left Sidebar + Right Content */}
<TabsContent value="code" className="flex-1 flex">
{/* Left Sidebar */}
<div className="w-80 flex flex-col pr-4 border-r border-gray-700">
<div className="flex-1 overflow-y-auto space-y-2">
{filteredCode.map((code) => (
<button
key={code.id}
type="button"
onClick={() => setSelectedCode(code)}
className={cn(
"w-full text-left p-3 rounded-lg transition-colors",
selectedCode.id === code.id
? "bg-cyan-500/10 border border-cyan-500/30"
: "hover:bg-white/5"
)}
>
<div className="flex items-center gap-2 mb-1">
<Code className="w-4 h-4 text-cyan-400" />
<span className="px-2 py-0.5 text-xs bg-cyan-500/20 text-cyan-400 rounded">
{code.language}
</span>
</div>
))}
</div>
)}
<p className="text-xs text-gray-400 line-clamp-2">{code.summary}</p>
</button>
))}
</div>
</div>
{/* Right Content */}
<div className="flex-1 overflow-y-auto pl-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">{selectedCode.summary}</h3>
<span className="px-2 py-1 text-xs bg-cyan-500/20 text-cyan-400 rounded">
{selectedCode.language}
</span>
</div>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto">
<code className="text-gray-300 text-sm">{selectedCode.code}</code>
</pre>
</div>
</TabsContent>
</Tabs>

View File

@@ -1,7 +1,8 @@
import { useState } from "react";
import { Grid, List, Asterisk, Terminal, FileCode, Globe, FileText, Calendar } from "lucide-react";
import { Button } from "@/features/ui/primitives/button";
import { Card } from "@/features/ui/primitives/card";
import { DataCard, DataCardHeader, DataCardContent, DataCardFooter } from "@/features/ui/primitives/data-card";
import { StatPill } from "@/features/ui/primitives/pill";
import { Input } from "@/features/ui/primitives/input";
import { ToggleGroup, ToggleGroupItem } from "@/features/ui/primitives/toggle-group";
import { cn } from "@/features/ui/primitives/styles";
@@ -158,114 +159,71 @@ export const KnowledgeLayoutExample = () => {
);
};
// Grid Card Component - matches real KnowledgeCard structure
// Grid Card Component - using DataCard primitive
const KnowledgeCard = ({ item }: { item: typeof MOCK_KNOWLEDGE_ITEMS[0] }) => {
const isUrl = !!item.url;
const isTechnical = item.type === "technical";
const getCardGradient = () => {
if (isTechnical) {
return isUrl
? "from-cyan-100/50 via-cyan-50/25 to-white/60 dark:from-cyan-900/20 dark:via-cyan-900/10 dark:to-black/30"
: "from-purple-100/50 via-purple-50/25 to-white/60 dark:from-purple-900/20 dark:via-purple-900/10 dark:to-black/30";
}
return isUrl
? "from-blue-100/50 via-blue-50/25 to-white/60 dark:from-blue-900/20 dark:via-blue-900/10 dark:to-black/30"
: "from-pink-100/50 via-pink-50/25 to-white/60 dark:from-pink-900/20 dark:via-pink-900/10 dark:to-black/30";
const getEdgeColor = (): "cyan" | "purple" | "blue" | "pink" => {
if (isTechnical) return isUrl ? "cyan" : "purple";
return isUrl ? "blue" : "pink";
};
const getBorderColor = () => {
if (isTechnical) {
return isUrl
? "border-cyan-600/30 dark:border-cyan-500/30"
: "border-purple-600/30 dark:border-purple-500/30";
}
return isUrl
? "border-blue-600/30 dark:border-blue-500/30"
: "border-pink-600/30 dark:border-pink-500/30";
};
const getAccent = () => {
if (isTechnical) {
return isUrl
? { bar: "bg-cyan-500", smear: "from-cyan-500/25" }
: { bar: "bg-purple-500", smear: "from-purple-500/25" };
}
return isUrl
? { bar: "bg-blue-500", smear: "from-blue-500/25" }
: { bar: "bg-pink-500", smear: "from-pink-500/25" };
};
const accent = getAccent();
return (
<div
className={cn(
"relative overflow-hidden transition-all duration-300 rounded-xl cursor-pointer",
"bg-gradient-to-b backdrop-blur-md border",
getCardGradient(),
getBorderColor(),
"hover:shadow-[0_0_30px_rgba(6,182,212,0.2)]",
"min-h-[240px] flex flex-col",
)}
<DataCard
edgePosition="top"
edgeColor={getEdgeColor()}
className="cursor-pointer hover:shadow-[0_0_30px_rgba(6,182,212,0.2)] transition-shadow"
>
{/* Top accent glow */}
<div className="pointer-events-none absolute inset-x-0 top-0">
<div className={cn("mx-1 mt-0.5 h-[2px] rounded-full", accent.bar)} />
<div className={cn("-mt-1 h-8 w-full bg-gradient-to-b to-transparent blur-md", accent.smear)} />
</div>
{/* Content */}
<div className="relative p-4">
<div className="flex items-start justify-between gap-2 mb-3">
<div className="flex items-center gap-2">
<div
className={cn(
"flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium",
isUrl
? "bg-cyan-100 text-cyan-700 dark:bg-cyan-500/10 dark:text-cyan-400"
: "bg-purple-100 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400",
)}
>
{isUrl ? <Globe className="w-3.5 h-3.5" /> : <FileText className="w-3.5 h-3.5" />}
<span>{isUrl ? "Web Page" : "Document"}</span>
</div>
<span
className={cn(
"px-2 py-1 text-xs rounded-md font-medium",
item.type === "technical"
? "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400"
: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
)}
>
{item.type}
</span>
<DataCardHeader>
<div className="flex items-center gap-2 mb-3">
<div
className={cn(
"flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium",
isUrl
? "bg-cyan-100 text-cyan-700 dark:bg-cyan-500/10 dark:text-cyan-400"
: "bg-purple-100 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400",
)}
>
{isUrl ? <Globe className="w-3.5 h-3.5" /> : <FileText className="w-3.5 h-3.5" />}
<span>{isUrl ? "Web Page" : "Document"}</span>
</div>
<span
className={cn(
"px-2 py-1 text-xs rounded-md font-medium",
item.type === "technical"
? "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400"
: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
)}
>
{item.type}
</span>
</div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 line-clamp-2">{item.title}</h4>
{item.url && (
<div className="text-xs text-gray-600 dark:text-gray-400 truncate">{item.url}</div>
)}
</div>
{item.url && <div className="text-xs text-gray-600 dark:text-gray-400 truncate">{item.url}</div>}
</DataCardHeader>
{/* Footer with stats */}
<div className="mt-auto px-4 py-3 bg-gray-100/50 dark:bg-black/30 border-t border-gray-200/50 dark:border-white/10">
<DataCardContent />
<DataCardFooter>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
<Calendar className="w-3 h-3" />
<span>{item.date}</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-orange-500/10 text-orange-600 dark:text-orange-400">
<FileText className="w-3.5 h-3.5" />
<span>{item.chunks}</span>
</div>
</div>
<StatPill
color="orange"
value={item.chunks}
icon={<FileText className="w-3.5 h-3.5" />}
size="sm"
onClick={() => console.log('View documents')}
className="cursor-pointer hover:scale-105 transition-transform"
/>
</div>
</div>
</div>
</DataCardFooter>
</DataCard>
);
};

View File

@@ -1,8 +1,11 @@
import { useState } from "react";
import { LayoutGrid, List, ListTodo, Activity, CheckCircle2, FileText, Search, Table as TableIcon, Tag, User, Trash2, Pin, Copy } from "lucide-react";
import { motion } from "framer-motion";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Button } from "@/features/ui/primitives/button";
import { Card } from "@/features/ui/primitives/card";
import { DraggableCard } from "@/features/ui/primitives/draggable-card";
import { SelectableCard } from "@/features/ui/primitives/selectable-card";
import { Input } from "@/features/ui/primitives/input";
import { PillNavigation, type PillNavigationItem } from "../shared/PillNavigation";
import { cn } from "@/features/ui/primitives/styles";
@@ -273,7 +276,7 @@ const SidebarProjectCard = ({
);
};
// Project Card matching REAL ProjectCard exactly
// Project Card using SelectableCard primitive
const ProjectCardExample = ({
project,
isSelected,
@@ -283,36 +286,25 @@ const ProjectCardExample = ({
isSelected: boolean;
onSelect: () => void;
}) => {
// Custom gradients for pinned vs selected vs default
const getBackgroundClass = () => {
if (project.pinned) return "bg-gradient-to-b from-purple-100/80 via-purple-50/30 to-purple-100/50 dark:from-purple-900/30 dark:via-purple-900/20 dark:to-purple-900/10";
if (isSelected) return "bg-gradient-to-b from-white/70 via-purple-50/20 to-white/50 dark:from-white/5 dark:via-purple-900/5 dark:to-black/20";
return "bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30";
};
return (
<motion.div
onClick={onSelect}
<SelectableCard
isSelected={isSelected}
isPinned={project.pinned}
showAuroraGlow={isSelected}
onSelect={onSelect}
size="none"
className={cn(
"relative rounded-xl backdrop-blur-md w-72 min-h-[180px] cursor-pointer overflow-visible group flex flex-col",
"transition-all duration-300",
project.pinned
? "bg-gradient-to-b from-purple-100/80 via-purple-50/30 to-purple-100/50 dark:from-purple-900/30 dark:via-purple-900/20 dark:to-purple-900/10"
: isSelected
? "bg-gradient-to-b from-white/70 via-purple-50/20 to-white/50 dark:from-white/5 dark:via-purple-900/5 dark:to-black/20"
: "bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30",
"border",
project.pinned
? "border-purple-500/80 dark:border-purple-500/80 shadow-[0_0_15px_rgba(168,85,247,0.3)]"
: isSelected
? "border-purple-400/60 dark:border-purple-500/60"
: "border-gray-200 dark:border-zinc-800/50",
isSelected
? "shadow-[0_0_15px_rgba(168,85,247,0.4),0_0_10px_rgba(147,51,234,0.3)] dark:shadow-[0_0_20px_rgba(168,85,247,0.5),0_0_15px_rgba(147,51,234,0.4)]"
: "shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
isSelected ? "scale-[1.02]" : "hover:scale-[1.01]",
"w-72 min-h-[180px] flex flex-col",
getBackgroundClass(),
)}
>
{/* Aurora glow effect for selected card */}
{isSelected && (
<div className="absolute inset-0 rounded-xl overflow-hidden opacity-30 dark:opacity-40 pointer-events-none">
<div className="absolute -inset-[100px] bg-[radial-gradient(circle,rgba(168,85,247,0.8)_0%,rgba(147,51,234,0.6)_40%,transparent_70%)] blur-3xl animate-[pulse_8s_ease-in-out_infinite]" />
</div>
)}
{/* Main content */}
<div className="flex-1 p-4 pb-2">
{/* Title */}
@@ -450,11 +442,11 @@ const ProjectCardExample = ({
</button>
</div>
</div>
</motion.div>
</SelectableCard>
);
};
// Kanban Board - NO BACKGROUNDS
// Kanban Board - NO BACKGROUNDS, wrapped in DndProvider
const KanbanBoardView = () => {
const columns = [
{ status: "todo" as const, title: "Todo", color: "text-pink-500", glow: "bg-pink-500" },
@@ -468,30 +460,32 @@ const KanbanBoardView = () => {
};
return (
<div className="grid grid-cols-4 gap-2 min-h-[500px]">
{columns.map(({ status, title, color, glow }) => (
<div key={status} className="flex flex-col">
{/* Column Header - transparent */}
<div className="text-center py-3 relative">
<h3 className={cn("font-mono text-sm font-medium", color)}>{title}</h3>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{getTasksByStatus(status).length}</div>
<div className={cn("absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px]", glow, "shadow-md")} />
</div>
<DndProvider backend={HTML5Backend}>
<div className="grid grid-cols-4 gap-2 min-h-[500px]">
{columns.map(({ status, title, color, glow }) => (
<div key={status} className="flex flex-col">
{/* Column Header - transparent */}
<div className="text-center py-3 relative">
<h3 className={cn("font-mono text-sm font-medium", color)}>{title}</h3>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{getTasksByStatus(status).length}</div>
<div className={cn("absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px]", glow, "shadow-md")} />
</div>
{/* Tasks */}
<div className="flex-1 p-2 space-y-2">
{getTasksByStatus(status).map((task) => (
<TaskCardExample key={task.id} task={task} />
))}
{/* Tasks */}
<div className="flex-1 p-2 space-y-2">
{getTasksByStatus(status).map((task, idx) => (
<TaskCardExample key={task.id} task={task} index={idx} />
))}
</div>
</div>
</div>
))}
</div>
))}
</div>
</DndProvider>
);
};
// Task Card matching REAL TaskCard exactly
const TaskCardExample = ({ task }: { task: typeof MOCK_TASKS[0] }) => {
// Task Card using DraggableCard primitive
const TaskCardExample = ({ task, index }: { task: typeof MOCK_TASKS[0]; index: number }) => {
const getPriorityColor = (priority: string) => {
if (priority === "high") return { color: "bg-red-500", glow: "shadow-[0_0_10px_rgba(239,68,68,0.3)]" };
if (priority === "medium") return { color: "bg-yellow-500", glow: "shadow-[0_0_10px_rgba(234,179,8,0.3)]" };
@@ -501,9 +495,15 @@ const TaskCardExample = ({ task }: { task: typeof MOCK_TASKS[0] }) => {
const priorityStyle = getPriorityColor(task.priority);
return (
<div className="w-full min-h-[140px] group cursor-move relative">
<div className="bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border border-gray-200 dark:border-gray-700 rounded-lg backdrop-blur-md transition-all duration-200 group-hover:border-cyan-400/70 dark:group-hover:border-cyan-500/50 group-hover:shadow-[0_0_15px_rgba(34,211,238,0.4)] dark:group-hover:shadow-[0_0_15px_rgba(34,211,238,0.6)] w-full min-h-[140px] h-full relative">
{/* Priority indicator glow on left */}
<div className="relative group">
<DraggableCard
itemType="task"
itemId={task.id}
index={index}
size="none"
className="min-h-[140px]"
>
{/* Priority indicator on left edge */}
<div className={cn("absolute left-0 top-0 bottom-0 w-[3px] rounded-l-lg opacity-80 group-hover:w-[4px] group-hover:opacity-100 transition-all duration-300", priorityStyle.color, priorityStyle.glow)} />
{/* Content */}
@@ -538,7 +538,7 @@ const TaskCardExample = ({ task }: { task: typeof MOCK_TASKS[0] }) => {
<div className={cn("w-2 h-2 rounded-full", priorityStyle.color)} />
</div>
</div>
</div>
</DraggableCard>
</div>
);
};

View File

@@ -1,77 +1,292 @@
import { useState } from "react";
import { Card } from "@/features/ui/primitives/card";
import { DraggableCard } from "@/features/ui/primitives/draggable-card";
import { SelectableCard } from "@/features/ui/primitives/selectable-card";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { cn } from "@/features/ui/primitives/styles";
export const StaticCards = () => {
// Base Glass Card with transparency tabs
const BaseGlassCardShowcase = () => {
const [activeTab, setActiveTab] = useState<"light" | "frosted" | "solid">("light");
return (
<div className="space-y-6">
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Base Glass Card</h4>
{/* Tabs */}
<div className="flex gap-2 mb-3">
{(["light", "frosted", "solid"] as const).map((tab) => (
<button
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
className={cn(
"px-3 py-1 text-xs rounded-md transition-colors",
activeTab === tab
? "bg-blue-500/20 text-blue-700 dark:text-blue-300 border border-blue-500/50"
: "bg-gray-200/50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400 hover:bg-gray-300/50 dark:hover:bg-gray-600/50"
)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
{/* Card Display */}
<Card
size="md"
transparency={activeTab}
blur="md"
className={activeTab === "solid" ? "border-2 border-gray-400 dark:border-gray-600" : ""}
>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Card Title</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
{activeTab === "light" && "Light glass - low opacity (8%), see grid through"}
{activeTab === "frosted" && "Frosted glass - white frosted in light mode, black frosted in dark mode"}
{activeTab === "solid" && "Solid - high opacity (90%), opaque background"}
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
{`<Card transparency="${activeTab}" />`}
</p>
</div>
);
};
// Outer Glow Card with size tabs
const OuterGlowCardShowcase = () => {
const [activeSize, setActiveSize] = useState<"sm" | "md" | "lg" | "xl">("md");
return (
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Outer Glow Card</h4>
{/* Size Tabs */}
<div className="flex gap-2 mb-3">
{(["sm", "md", "lg", "xl"] as const).map((size) => (
<button
key={size}
type="button"
onClick={() => setActiveSize(size)}
className={cn(
"px-3 py-1 text-xs rounded-md transition-colors",
activeSize === size
? "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border border-cyan-500/50"
: "bg-gray-200/50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400 hover:bg-gray-300/50 dark:hover:bg-gray-600/50"
)}
>
{size.toUpperCase()}
</button>
))}
</div>
{/* Card Display */}
<Card glowColor="cyan" glowType="outer" glowSize={activeSize}>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Active Card</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Outer glow - {activeSize.toUpperCase()} (hover for brighter, same size)
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
{`<Card glowColor="cyan" glowType="outer" glowSize="${activeSize}" />`}
</p>
</div>
);
};
// Inner Glow Card with size tabs
const InnerGlowCardShowcase = () => {
const [activeSize, setActiveSize] = useState<"sm" | "md" | "lg" | "xl">("md");
return (
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Inner Glow Card</h4>
{/* Size Tabs */}
<div className="flex gap-2 mb-3">
{(["sm", "md", "lg", "xl"] as const).map((size) => (
<button
key={size}
type="button"
onClick={() => setActiveSize(size)}
className={cn(
"px-3 py-1 text-xs rounded-md transition-colors",
activeSize === size
? "bg-blue-500/20 text-blue-700 dark:text-blue-300 border border-blue-500/50"
: "bg-gray-200/50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400 hover:bg-gray-300/50 dark:hover:bg-gray-600/50"
)}
>
{size.toUpperCase()}
</button>
))}
</div>
{/* Card Display */}
<Card glowColor="blue" glowType="inner" glowSize={activeSize}>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Featured Card</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Inner glow - {activeSize.toUpperCase()} (hover for brighter, same size)
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
{`<Card glowColor="blue" glowType="inner" glowSize="${activeSize}" />`}
</p>
</div>
);
};
// Edge-Lit Card with color tabs
const EdgeLitCardShowcase = () => {
const [activeColor, setActiveColor] = useState<"cyan" | "purple" | "pink" | "blue">("cyan");
const colorDescriptions = {
cyan: "Technical web pages",
purple: "Uploaded documents",
pink: "Business content",
blue: "Information pages",
};
// Static color classes (NOT dynamic) - Tailwind requirement
const tabColorClasses = {
cyan: "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border border-cyan-500/50",
purple: "bg-purple-500/20 text-purple-700 dark:text-purple-300 border border-purple-500/50",
pink: "bg-pink-500/20 text-pink-700 dark:text-pink-300 border border-pink-500/50",
blue: "bg-blue-500/20 text-blue-700 dark:text-blue-300 border border-blue-500/50",
};
return (
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Top Edge Glow Card</h4>
{/* Color Tabs */}
<div className="flex gap-2 mb-3">
{(["cyan", "purple", "pink", "blue"] as const).map((color) => (
<button
key={color}
type="button"
onClick={() => setActiveColor(color)}
className={cn(
"px-3 py-1 text-xs rounded-md transition-colors",
activeColor === color
? tabColorClasses[color]
: "bg-gray-200/50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400 hover:bg-gray-300/50 dark:hover:bg-gray-600/50"
)}
>
{color.charAt(0).toUpperCase() + color.slice(1)}
</button>
))}
</div>
{/* Card Display */}
<Card edgePosition="top" edgeColor={activeColor}>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
{activeColor.charAt(0).toUpperCase() + activeColor.slice(1)} Edge Light
</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
{colorDescriptions[activeColor]}
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
{`<Card edgePosition="top" edgeColor="${activeColor}" />`}
</p>
</div>
);
};
export const StaticCards = () => {
const [selectedCardId, setSelectedCardId] = useState("card-2");
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Cards</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Glass card variants used in the application
Glass card variants and advanced card components
</p>
</div>
<div className="space-y-6">
{/* Base Glass Card */}
{/* Responsive Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Base Glass Card - Transparency Variants */}
<BaseGlassCardShowcase />
{/* Outer Glow Card - Size Variants */}
<OuterGlowCardShowcase />
{/* Inner Glow Card - Size Variants */}
<InnerGlowCardShowcase />
{/* Top Edge Glow Card - Color Variants */}
<EdgeLitCardShowcase />
</div>
{/* Advanced Card Components */}
<div className="space-y-6 mt-8">
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Base Glass Card</h4>
<Card className="p-6 max-w-md">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Card Title</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Default glass card with backdrop blur and semi-transparent background. Used for general containers, settings panels, and content wrappers.
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
{"<Card />"}
<h3 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Advanced Card Components</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
Specialized cards that extend the base Card primitive with additional behaviors
</p>
</div>
{/* Outer Glow Card */}
{/* Selectable Cards */}
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Outer Glow Card</h4>
<div className="relative max-w-md">
<div className="absolute inset-0 bg-cyan-500/20 blur-xl rounded-xl" />
<Card className="p-6 relative border-cyan-500/30 hover:shadow-[0_0_30px_rgba(6,182,212,0.4)] transition-shadow">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Active Card</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Card with external glow. Used for selected or active states. Hover to see enhanced glow.
</p>
</Card>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">SelectableCard</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-4">
Card with selection states, hover effects, and optional aurora glow. Click cards to select.
</p>
<div className="grid grid-cols-3 gap-4">
{["card-1", "card-2", "card-3"].map((id) => (
<SelectableCard
key={id}
isSelected={selectedCardId === id}
showAuroraGlow={selectedCardId === id}
onSelect={() => setSelectedCardId(id)}
size="sm"
className="min-h-[120px]"
>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
{id === selectedCardId ? "Selected" : "Click to Select"}
</h5>
<p className="text-xs text-gray-600 dark:text-gray-400">
Card {id.split("-")[1]}
</p>
</SelectableCard>
))}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
shadow-[0_0_30px_rgba(6,182,212,0.3)] + hover effect
<p className="text-xs text-gray-500 dark:text-gray-400 mt-3 font-mono">
{'<SelectableCard isSelected={...} showAuroraGlow onSelect={...} />'}
</p>
</div>
{/* Inner Glow Card */}
{/* Draggable Cards */}
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Inner Glow Card</h4>
<Card className="p-6 max-w-md shadow-[inset_0_0_20px_rgba(59,130,246,0.2)] border-blue-500/30">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Featured Card</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Card with internal glow effect. Used for special containers, featured sections, and highlighted content areas.
</p>
</Card>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
shadow-[inset_0_0_20px_rgba(59,130,246,0.2)]
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">DraggableCard</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-4">
Card with drag-and-drop functionality. Try dragging cards to reorder.
</p>
</div>
{/* Top Edge Glow Card */}
<div>
<h4 className="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">Top Edge Glow Card</h4>
<div className="relative overflow-hidden rounded-xl max-w-md">
<div className="absolute inset-x-0 top-0 h-[2px] bg-cyan-500 mx-1 mt-0.5 rounded-full pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-cyan-500/25 to-transparent blur-md -mt-1 pointer-events-none" />
<Card className="p-6 relative border-cyan-500/30 bg-gradient-to-b from-cyan-100/50 via-cyan-50/25 to-white/60 dark:from-cyan-900/20 dark:via-cyan-900/10 dark:to-black/30">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Knowledge Item</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">
Card with colored top edge glow. Used for knowledge cards - cyan for technical web pages, purple for uploaded docs, blue for business content.
</p>
</Card>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
Top hairline (2px) + blur smear (8px)
<DndProvider backend={HTML5Backend}>
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map((num) => (
<DraggableCard
key={num}
itemType="example-card"
itemId={`drag-${num}`}
index={num}
size="sm"
className="min-h-[120px] cursor-move"
>
<h5 className="font-medium text-gray-900 dark:text-white mb-2">Draggable {num}</h5>
<p className="text-xs text-gray-600 dark:text-gray-400">
Drag me to reorder
</p>
</DraggableCard>
))}
</div>
</DndProvider>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-3 font-mono">
{'<DraggableCard itemType="..." itemId="..." index={...} onDrop={...} />'}
</p>
</div>
</div>

View File

@@ -7,32 +7,67 @@ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
transparency?: 'clear' | 'light' | 'medium' | 'frosted' | 'solid';
glassTint?: 'none' | 'purple' | 'blue' | 'cyan' | 'green' | 'orange' | 'pink' | 'red';
// Glow properties
// Glow properties (uses pre-defined static classes from styles.ts)
glowColor?: 'none' | 'purple' | 'blue' | 'cyan' | 'green' | 'orange' | 'pink' | 'red';
glowType?: 'outer' | 'inner';
glowSize?: 'sm' | 'md' | 'lg' | 'xl';
// Edge-lit properties
edgePosition?: 'none' | 'top' | 'left' | 'right' | 'bottom';
edgeColor?: 'purple' | 'blue' | 'cyan' | 'green' | 'orange' | 'pink' | 'red';
// Size
size?: 'sm' | 'md' | 'lg' | 'xl';
// Size (padding)
size?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
}
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
({
className,
blur = 'xl',
blur = 'md',
transparency = 'light',
glassTint = 'none',
glowColor = 'none',
glowType = 'outer',
glowSize = 'md',
edgePosition = 'none',
edgeColor = 'cyan',
size = 'md',
children,
...props
}, ref) => {
const glowVariant = glassCard.variants[glowColor] || glassCard.variants.none;
const hasEdge = edgePosition !== 'none';
const hasGlow = glowColor !== 'none';
// Use pre-defined static classes from styles.ts
const glowVariant = glassCard.variants[glowColor] || glassCard.variants.none;
// Get glow class from static lookups in styles.ts
const getGlowClass = () => {
if (!hasGlow || hasEdge) return '';
if (glowType === 'inner') {
// @ts-ignore - accessing dynamic object safely
return glassCard.innerGlowSizes?.[glowColor]?.[glowSize] || '';
}
// Outer glow
// @ts-ignore - accessing dynamic object safely
return glassCard.outerGlowSizes?.[glowColor]?.[glowSize] || glowVariant.glow;
};
// Get size-matched hover glow class
const getHoverGlowClass = () => {
if (!hasGlow || hasEdge) return '';
if (glowType === 'inner') {
// @ts-ignore - accessing dynamic object safely
return glassCard.innerGlowHover?.[glowColor]?.[glowSize] || '';
}
// Outer glow hover
// @ts-ignore - accessing dynamic object safely
return glassCard.outerGlowHover?.[glowColor]?.[glowSize] || glowVariant.hover;
};
// Edge color mappings
const edgeColors = {
@@ -60,29 +95,33 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
if (hasEdge && edgePosition === 'top') {
// Edge-lit card with actual div elements (not pseudo-elements)
// Extract flex/layout classes from className to apply to inner content div
const flexClasses = className?.match(/(flex|flex-col|flex-row|flex-1|items-\S+|justify-\S+|gap-\S+)/g)?.join(' ') || '';
const otherClasses = className?.replace(/(flex|flex-col|flex-row|flex-1|items-\S+|justify-\S+|gap-\S+)/g, '').trim() || '';
return (
<div
ref={ref}
className={cn(
"relative rounded-xl overflow-hidden",
edgeStyle.border,
className
otherClasses
)}
{...props}
>
{/* Top edge light bar */}
<div className={cn("absolute inset-x-0 top-0 h-[3px] pointer-events-none z-10", edgeStyle.solid)} />
{/* Top edge light bar - thinner */}
<div className={cn("absolute inset-x-0 top-0 h-[2px] pointer-events-none z-10", edgeStyle.solid)} />
{/* Glow bleeding into card */}
<div className={cn("absolute inset-x-0 top-0 h-16 bg-gradient-to-b to-transparent blur-lg pointer-events-none z-10", edgeStyle.gradient)} />
{/* Content with tinted background */}
<div className={cn("backdrop-blur-sm", tintBackgrounds[edgeColor], glassCard.sizes[size])}>
{/* Content with tinted background - INHERIT flex classes */}
<div className={cn("backdrop-blur-sm", tintBackgrounds[edgeColor], glassCard.sizes[size], flexClasses)}>
{children}
</div>
</div>
);
}
// Standard card (no edge-lit)
// Standard card (no edge-lit) - use static classes from styles.ts
return (
<div
ref={ref}
@@ -93,9 +132,10 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
? glassCard.tints[glassTint][transparency]
: glassCard.transparency[transparency],
glassCard.sizes[size],
// Border and glow classes from static lookups
!hasEdge && glowVariant.border,
!hasEdge && glowVariant.glow,
!hasEdge && glowVariant.hover,
!hasEdge && getGlowClass(),
!hasEdge && getHoverGlowClass(), // Size-matched hover
className
)}
{...props}

View File

@@ -0,0 +1,155 @@
import React from "react";
import { cn } from "./styles";
interface DataCardProps extends React.HTMLAttributes<HTMLDivElement> {
// Edge-lit properties
edgePosition?: 'none' | 'top' | 'left' | 'right' | 'bottom';
edgeColor?: 'purple' | 'blue' | 'cyan' | 'green' | 'orange' | 'pink' | 'red';
// Glow properties
glowColor?: 'none' | 'purple' | 'blue' | 'cyan' | 'green' | 'orange' | 'pink' | 'red';
// Glass properties
blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
transparency?: 'clear' | 'light' | 'medium' | 'frosted' | 'solid';
}
interface DataCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
interface DataCardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
interface DataCardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
// Edge color mappings for edge-lit cards
const edgeColors = {
purple: { solid: 'bg-purple-500', gradient: 'from-purple-500/40', border: 'border-purple-500/30', bg: 'bg-gradient-to-br from-purple-500/15 to-purple-600/5' },
blue: { solid: 'bg-blue-500', gradient: 'from-blue-500/40', border: 'border-blue-500/30', bg: 'bg-gradient-to-br from-blue-500/15 to-blue-600/5' },
cyan: { solid: 'bg-cyan-500', gradient: 'from-cyan-500/40', border: 'border-cyan-500/30', bg: 'bg-gradient-to-br from-cyan-500/15 to-cyan-600/5' },
green: { solid: 'bg-green-500', gradient: 'from-green-500/40', border: 'border-green-500/30', bg: 'bg-gradient-to-br from-green-500/15 to-green-600/5' },
orange: { solid: 'bg-orange-500', gradient: 'from-orange-500/40', border: 'border-orange-500/30', bg: 'bg-gradient-to-br from-orange-500/15 to-orange-600/5' },
pink: { solid: 'bg-pink-500', gradient: 'from-pink-500/40', border: 'border-pink-500/30', bg: 'bg-gradient-to-br from-pink-500/15 to-pink-600/5' },
red: { solid: 'bg-red-500', gradient: 'from-red-500/40', border: 'border-red-500/30', bg: 'bg-gradient-to-br from-red-500/15 to-red-600/5' },
};
const blurClasses = {
none: "backdrop-blur-none",
sm: "backdrop-blur-sm",
md: "backdrop-blur-md",
lg: "backdrop-blur-lg",
xl: "backdrop-blur-xl",
};
const transparencyClasses = {
clear: "bg-white/[0.02] dark:bg-white/[0.01]",
light: "bg-white/[0.08] dark:bg-white/[0.05]",
medium: "bg-white/[0.15] dark:bg-white/[0.08]",
frosted: "bg-white/[0.40] dark:bg-black/[0.40]",
solid: "bg-white/[0.90] dark:bg-black/[0.95]",
};
export const DataCard = React.forwardRef<HTMLDivElement, DataCardProps>(
({
className,
edgePosition = 'none',
edgeColor = 'cyan',
glowColor = 'none',
blur = 'md',
transparency = 'light',
children,
...props
}, ref) => {
const hasEdge = edgePosition !== 'none';
const edgeStyle = hasEdge ? edgeColors[edgeColor] : null;
if (hasEdge && edgePosition === 'top') {
return (
<div
ref={ref}
className={cn(
"relative rounded-xl overflow-hidden min-h-[240px]",
edgeStyle?.border,
className
)}
{...props}
>
{/* Top edge light */}
<div className={cn("absolute inset-x-0 top-0 h-[2px] pointer-events-none z-10", edgeStyle?.solid)} />
{/* Glow bleeding down */}
<div className={cn("absolute inset-x-0 top-0 h-16 bg-gradient-to-b to-transparent blur-lg pointer-events-none z-10", edgeStyle?.gradient)} />
{/* Content wrapper with flex layout */}
<div className={cn(
"flex flex-col min-h-[240px]",
blurClasses[blur],
edgeStyle?.bg
)}>
{children}
</div>
</div>
);
}
// Standard card (no edge-lit)
return (
<div
ref={ref}
className={cn(
"relative rounded-xl overflow-hidden border border-gray-300/20 dark:border-white/10 min-h-[240px]",
blurClasses[blur],
transparencyClasses[transparency],
"flex flex-col",
className
)}
{...props}
>
{children}
</div>
);
}
);
DataCard.displayName = "DataCard";
// Header component
export const DataCardHeader = React.forwardRef<HTMLDivElement, DataCardHeaderProps>(
({ className, children, ...props }, ref) => {
return (
<div ref={ref} className={cn("relative p-4 pb-2", className)} {...props}>
{children}
</div>
);
}
);
DataCardHeader.displayName = "DataCardHeader";
// Content component (flexible - grows to fill space)
export const DataCardContent = React.forwardRef<HTMLDivElement, DataCardContentProps>(
({ className, children, ...props }, ref) => {
return (
<div ref={ref} className={cn("flex-1 px-4", className)} {...props}>
{children}
</div>
);
}
);
DataCardContent.displayName = "DataCardContent";
// Footer component (anchored to bottom)
export const DataCardFooter = React.forwardRef<HTMLDivElement, DataCardFooterProps>(
({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"px-4 py-3 bg-gray-100/50 dark:bg-black/30 border-t border-gray-200/50 dark:border-white/10",
className
)}
{...props}
>
{children}
</div>
);
}
);
DataCardFooter.displayName = "DataCardFooter";

View File

@@ -0,0 +1,79 @@
import React from "react";
import { useDrag, useDrop } from "react-dnd";
import { Card, type CardProps } from "./card";
interface DraggableCardProps extends Omit<CardProps, 'ref'> {
// Drag and drop
itemType: string;
itemId: string;
index: number;
onDrop?: (draggedId: string, targetIndex: number) => void;
// Visual states
isDragging?: boolean;
onDragStart?: () => void;
onDragEnd?: () => void;
}
export const DraggableCard = React.forwardRef<HTMLDivElement, DraggableCardProps>(
({
itemType,
itemId,
index,
onDrop,
onDragStart,
onDragEnd,
children,
className,
...cardProps
}, ref) => {
const [{ isDragging }, drag] = useDrag({
type: itemType,
item: { id: itemId, index },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
end: () => {
onDragEnd?.();
},
});
const [{ isOver }, drop] = useDrop({
accept: itemType,
hover: (draggedItem: { id: string; index: number }) => {
if (draggedItem.id === itemId) return;
if (draggedItem.index === index) return;
if (onDrop) {
onDrop(draggedItem.id, index);
draggedItem.index = index;
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
const combinedRef = (node: HTMLDivElement | null) => {
drag(drop(node));
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
};
return (
<div ref={combinedRef} className={isDragging ? "opacity-50 scale-95 transition-all" : "transition-all"}>
<Card
{...cardProps}
className={className}
>
{children}
</Card>
</div>
);
}
);
DraggableCard.displayName = "DraggableCard";

View File

@@ -14,6 +14,10 @@ export * from "./alert-dialog";
// Export all primitives
export * from "./button";
export * from "./card";
export * from "./data-card";
export * from "./draggable-card";
export * from "./selectable-card";
export * from "./combobox";
export * from "./dialog";
export * from "./dropdown-menu";

View File

@@ -0,0 +1,72 @@
import React from "react";
import { motion } from "framer-motion";
import { Card, type CardProps } from "./card";
import { cn } from "./styles";
interface SelectableCardProps extends Omit<CardProps, 'ref'> {
// Selection state
isSelected?: boolean;
onSelect?: () => void;
// Visual states
isPinned?: boolean;
showAuroraGlow?: boolean; // Aurora effect for selected state
// Selection colors
selectedBorderColor?: string;
selectedShadow?: string;
pinnedBorderColor?: string;
pinnedShadow?: string;
}
export const SelectableCard = React.forwardRef<HTMLDivElement, SelectableCardProps>(
({
isSelected = false,
isPinned = false,
showAuroraGlow = false,
onSelect,
selectedBorderColor = "border-purple-400/60 dark:border-purple-500/60",
selectedShadow = "shadow-[0_0_15px_rgba(168,85,247,0.4),0_0_10px_rgba(147,51,234,0.3)] dark:shadow-[0_0_20px_rgba(168,85,247,0.5),0_0_15px_rgba(147,51,234,0.4)]",
pinnedBorderColor = "border-purple-500/80 dark:border-purple-500/80",
pinnedShadow = "shadow-[0_0_15px_rgba(168,85,247,0.3)]",
children,
className,
...cardProps
}, ref) => {
return (
<motion.div
onClick={onSelect}
className={cn(
"cursor-pointer transition-all duration-300 overflow-visible",
isSelected ? "scale-[1.02]" : "hover:scale-[1.01]",
)}
whileHover={{ scale: isSelected ? 1.02 : 1.01 }}
>
<div className="relative">
{/* Aurora glow effect for selected state */}
{isSelected && showAuroraGlow && (
<div className="absolute inset-0 rounded-xl overflow-hidden opacity-30 dark:opacity-40 pointer-events-none">
<div className="absolute -inset-[100px] bg-[radial-gradient(circle,rgba(168,85,247,0.8)_0%,rgba(147,51,234,0.6)_40%,transparent_70%)] blur-3xl animate-[pulse_8s_ease-in-out_infinite]" />
</div>
)}
<Card
ref={ref}
{...cardProps}
className={cn(
isPinned && pinnedBorderColor,
isPinned && pinnedShadow,
isSelected && !isPinned && selectedBorderColor,
isSelected && !isPinned && selectedShadow,
className
)}
>
{children}
</Card>
</div>
</motion.div>
);
}
);
SelectableCard.displayName = "SelectableCard";

View File

@@ -136,11 +136,11 @@ export const glassCard = {
// Glass transparency levels - Theme-aware for better color visibility
transparency: {
clear: "bg-white/[0.01] dark:bg-black/[0.01]", // 1% - almost invisible
light: "bg-white/[0.03] dark:bg-black/[0.05]", // 3-5% - very subtle glass
medium: "bg-white/[0.05] dark:bg-black/[0.08]", // 5-8% - standard glass
frosted: "bg-white/[0.08] dark:bg-black/[0.12]", // 8-12% - frosted glass
solid: "bg-white/[0.12] dark:bg-black/[0.20]" // 12-20% - more visible
clear: "bg-white/[0.02] dark:bg-white/[0.01]", // Very transparent - see through
light: "bg-white/[0.08] dark:bg-white/[0.05]", // Light glass - see through clearly
medium: "bg-white/[0.15] dark:bg-white/[0.08]", // Medium glass - lighter in dark mode
frosted: "bg-white/[0.40] dark:bg-black/[0.40]", // Frosted - white in light, black in dark
solid: "bg-white/[0.90] dark:bg-black/[0.95]" // Solid - opaque
},
// Colored glass tints - BRIGHT NEON COLORS with higher opacity
@@ -197,7 +197,7 @@ export const glassCard = {
}
},
// Neon glow effects - BRIGHTER & MORE INTENSE
// Neon glow effects - BRIGHTER & MORE INTENSE (default = md size)
variants: {
none: {
border: "border-gray-300/20 dark:border-white/10",
@@ -241,8 +241,192 @@ export const glassCard = {
}
},
// Outer glow size variants (static classes for each color)
outerGlowSizes: {
cyan: {
sm: "shadow-[0_0_20px_rgba(34,211,238,0.3)]",
md: "shadow-[0_0_40px_rgba(34,211,238,0.4)]",
lg: "shadow-[0_0_70px_rgba(34,211,238,0.5)]",
xl: "shadow-[0_0_100px_rgba(34,211,238,0.6)]"
},
purple: {
sm: "shadow-[0_0_20px_rgba(168,85,247,0.3)]",
md: "shadow-[0_0_40px_rgba(168,85,247,0.4)]",
lg: "shadow-[0_0_70px_rgba(168,85,247,0.5)]",
xl: "shadow-[0_0_100px_rgba(168,85,247,0.6)]"
},
blue: {
sm: "shadow-[0_0_20px_rgba(59,130,246,0.3)]",
md: "shadow-[0_0_40px_rgba(59,130,246,0.4)]",
lg: "shadow-[0_0_70px_rgba(59,130,246,0.5)]",
xl: "shadow-[0_0_100px_rgba(59,130,246,0.6)]"
},
pink: {
sm: "shadow-[0_0_20px_rgba(236,72,153,0.3)]",
md: "shadow-[0_0_40px_rgba(236,72,153,0.4)]",
lg: "shadow-[0_0_70px_rgba(236,72,153,0.5)]",
xl: "shadow-[0_0_100px_rgba(236,72,153,0.6)]"
},
green: {
sm: "shadow-[0_0_20px_rgba(16,185,129,0.3)]",
md: "shadow-[0_0_40px_rgba(16,185,129,0.4)]",
lg: "shadow-[0_0_70px_rgba(16,185,129,0.5)]",
xl: "shadow-[0_0_100px_rgba(16,185,129,0.6)]"
},
orange: {
sm: "shadow-[0_0_20px_rgba(251,146,60,0.3)]",
md: "shadow-[0_0_40px_rgba(251,146,60,0.4)]",
lg: "shadow-[0_0_70px_rgba(251,146,60,0.5)]",
xl: "shadow-[0_0_100px_rgba(251,146,60,0.6)]"
},
red: {
sm: "shadow-[0_0_20px_rgba(239,68,68,0.3)]",
md: "shadow-[0_0_40px_rgba(239,68,68,0.4)]",
lg: "shadow-[0_0_70px_rgba(239,68,68,0.5)]",
xl: "shadow-[0_0_100px_rgba(239,68,68,0.6)]"
}
},
// Inner glow variants (static classes for each color) - WIDER range than outer
innerGlowSizes: {
cyan: {
sm: "shadow-[inset_0_0_15px_rgba(34,211,238,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(34,211,238,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(34,211,238,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(34,211,238,0.5)]"
},
purple: {
sm: "shadow-[inset_0_0_15px_rgba(168,85,247,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(168,85,247,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(168,85,247,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(168,85,247,0.5)]"
},
blue: {
sm: "shadow-[inset_0_0_15px_rgba(59,130,246,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(59,130,246,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(59,130,246,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(59,130,246,0.5)]"
},
pink: {
sm: "shadow-[inset_0_0_15px_rgba(236,72,153,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(236,72,153,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(236,72,153,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(236,72,153,0.5)]"
},
green: {
sm: "shadow-[inset_0_0_15px_rgba(16,185,129,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(16,185,129,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(16,185,129,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(16,185,129,0.5)]"
},
orange: {
sm: "shadow-[inset_0_0_15px_rgba(251,146,60,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(251,146,60,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(251,146,60,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(251,146,60,0.5)]"
},
red: {
sm: "shadow-[inset_0_0_15px_rgba(239,68,68,0.2)]",
md: "shadow-[inset_0_0_40px_rgba(239,68,68,0.3)]",
lg: "shadow-[inset_0_0_80px_rgba(239,68,68,0.4)]",
xl: "shadow-[inset_0_0_120px_rgba(239,68,68,0.5)]"
}
},
// Hover glow variants - size-matched (brighter, same size)
outerGlowHover: {
cyan: {
sm: "hover:shadow-[0_0_20px_rgba(34,211,238,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(34,211,238,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(34,211,238,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(34,211,238,0.8)]"
},
purple: {
sm: "hover:shadow-[0_0_20px_rgba(168,85,247,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(168,85,247,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(168,85,247,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(168,85,247,0.8)]"
},
blue: {
sm: "hover:shadow-[0_0_20px_rgba(59,130,246,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(59,130,246,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(59,130,246,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(59,130,246,0.8)]"
},
pink: {
sm: "hover:shadow-[0_0_20px_rgba(236,72,153,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(236,72,153,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(236,72,153,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(236,72,153,0.8)]"
},
green: {
sm: "hover:shadow-[0_0_20px_rgba(16,185,129,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(16,185,129,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(16,185,129,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(16,185,129,0.8)]"
},
orange: {
sm: "hover:shadow-[0_0_20px_rgba(251,146,60,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(251,146,60,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(251,146,60,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(251,146,60,0.8)]"
},
red: {
sm: "hover:shadow-[0_0_20px_rgba(239,68,68,0.5)]",
md: "hover:shadow-[0_0_40px_rgba(239,68,68,0.6)]",
lg: "hover:shadow-[0_0_70px_rgba(239,68,68,0.7)]",
xl: "hover:shadow-[0_0_100px_rgba(239,68,68,0.8)]"
}
},
innerGlowHover: {
cyan: {
sm: "hover:shadow-[inset_0_0_15px_rgba(34,211,238,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(34,211,238,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(34,211,238,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(34,211,238,0.7)]"
},
purple: {
sm: "hover:shadow-[inset_0_0_15px_rgba(168,85,247,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(168,85,247,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(168,85,247,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(168,85,247,0.7)]"
},
blue: {
sm: "hover:shadow-[inset_0_0_15px_rgba(59,130,246,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(59,130,246,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(59,130,246,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(59,130,246,0.7)]"
},
pink: {
sm: "hover:shadow-[inset_0_0_15px_rgba(236,72,153,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(236,72,153,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(236,72,153,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(236,72,153,0.7)]"
},
green: {
sm: "hover:shadow-[inset_0_0_15px_rgba(16,185,129,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(16,185,129,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(16,185,129,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(16,185,129,0.7)]"
},
orange: {
sm: "hover:shadow-[inset_0_0_15px_rgba(251,146,60,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(251,146,60,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(251,146,60,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(251,146,60,0.7)]"
},
red: {
sm: "hover:shadow-[inset_0_0_15px_rgba(239,68,68,0.4)]",
md: "hover:shadow-[inset_0_0_40px_rgba(239,68,68,0.5)]",
lg: "hover:shadow-[inset_0_0_80px_rgba(239,68,68,0.6)]",
xl: "hover:shadow-[inset_0_0_120px_rgba(239,68,68,0.7)]"
}
},
// Size variants
sizes: {
none: "p-0",
sm: "p-4",
md: "p-6",
lg: "p-8",

View File

@@ -5,16 +5,20 @@ import { cn } from "./styles";
// Root
export const Tabs = TabsPrimitive.Root;
// List
// List - styled like pill navigation
export const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List ref={ref} className={cn("relative", className)} role="tablist" {...props}>
{/* Subtle neon glow effect */}
<div className="absolute inset-0 rounded-lg opacity-30 blur-[1px] bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 pointer-events-none" />
{props.children}
</TabsPrimitive.List>
<TabsPrimitive.List
ref={ref}
className={cn(
"backdrop-blur-sm bg-white/40 dark:bg-white/5 border border-white/30 dark:border-white/15 rounded-full p-1 shadow-lg inline-flex gap-1",
className
)}
role="tablist"
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
@@ -58,29 +62,30 @@ export const TabsTrigger = React.forwardRef<
},
};
const activeClasses = {
blue: "data-[state=active]:bg-blue-500/20 dark:data-[state=active]:bg-blue-400/20 data-[state=active]:text-blue-700 dark:data-[state=active]:text-blue-300 data-[state=active]:border data-[state=active]:border-blue-400/50 data-[state=active]:shadow-[0_0_10px_rgba(59,130,246,0.5)]",
purple: "data-[state=active]:bg-purple-500/20 dark:data-[state=active]:bg-purple-400/20 data-[state=active]:text-purple-700 dark:data-[state=active]:text-purple-300 data-[state=active]:border data-[state=active]:border-purple-400/50 data-[state=active]:shadow-[0_0_10px_rgba(168,85,247,0.5)]",
pink: "data-[state=active]:bg-pink-500/20 dark:data-[state=active]:bg-pink-400/20 data-[state=active]:text-pink-700 dark:data-[state=active]:text-pink-300 data-[state=active]:border data-[state=active]:border-pink-400/50 data-[state=active]:shadow-[0_0_10px_rgba(236,72,153,0.5)]",
orange: "data-[state=active]:bg-orange-500/20 dark:data-[state=active]:bg-orange-400/20 data-[state=active]:text-orange-700 dark:data-[state=active]:text-orange-300 data-[state=active]:border data-[state=active]:border-orange-400/50 data-[state=active]:shadow-[0_0_10px_rgba(251,146,60,0.5)]",
cyan: "data-[state=active]:bg-cyan-500/20 dark:data-[state=active]:bg-cyan-400/20 data-[state=active]:text-cyan-700 dark:data-[state=active]:text-cyan-300 data-[state=active]:border data-[state=active]:border-cyan-400/50 data-[state=active]:shadow-[0_0_10px_rgba(34,211,238,0.5)]",
green: "data-[state=active]:bg-green-500/20 dark:data-[state=active]:bg-green-400/20 data-[state=active]:text-green-700 dark:data-[state=active]:text-green-300 data-[state=active]:border data-[state=active]:border-green-400/50 data-[state=active]:shadow-[0_0_10px_rgba(16,185,129,0.5)]",
};
return (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"relative px-24 py-10 font-mono transition-all duration-300 z-10",
"text-gray-600 dark:text-gray-400",
"flex items-center gap-2 px-6 py-2.5 rounded-full transition-all duration-200",
"text-sm font-medium whitespace-nowrap",
"text-gray-700 dark:text-gray-300 hover:bg-white/10 dark:hover:bg-white/5",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50",
colorMap[color].text,
colorMap[color].hover,
activeClasses[color],
className,
)}
{...props}
>
{props.children}
{/* Active state neon indicator - only show when active */}
<span
className={cn(
"absolute bottom-0 left-0 right-0 w-full h-[2px]",
"data-[state=active]:block hidden",
colorMap[color].glow,
)}
/>
</TabsPrimitive.Trigger>
);
});