mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
refining
This commit is contained in:
@@ -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" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
155
archon-ui-main/src/features/ui/primitives/data-card.tsx
Normal file
155
archon-ui-main/src/features/ui/primitives/data-card.tsx
Normal 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";
|
||||
79
archon-ui-main/src/features/ui/primitives/draggable-card.tsx
Normal file
79
archon-ui-main/src/features/ui/primitives/draggable-card.tsx
Normal 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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user