feat: create new style
This commit is contained in:
parent
5590787f7f
commit
6b1a8ff96f
|
|
@ -10,17 +10,18 @@
|
|||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"@banatie/database": "workspace:*",
|
||||
"lucide-react": "^0.400.0",
|
||||
"next": "15.5.4",
|
||||
"@banatie/database": "workspace:*"
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4"
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,398 @@
|
|||
# Lab Section Design System
|
||||
|
||||
The Lab section is a production-ready UI interface for interacting with the Banatie API service. It provides a clean, work-focused experience for image generation, gallery browsing, alias management, and flow control.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
apps/landing/src/
|
||||
├── app/(lab)/
|
||||
│ ├── CLAUDE.md # This design documentation
|
||||
│ ├── layout.tsx # Root lab layout with scroll context
|
||||
│ └── lab/
|
||||
│ ├── layout.tsx # Sub-layout with PageProvider
|
||||
│ ├── page.tsx # Redirect to /lab/generate
|
||||
│ ├── generate/page.tsx # Generation workbench
|
||||
│ ├── images/page.tsx # Image gallery browser
|
||||
│ ├── live/page.tsx # Live generation testing
|
||||
│ └── upload/page.tsx # File upload interface
|
||||
│
|
||||
├── components/layout/lab/
|
||||
│ ├── LabLayout.tsx # Main layout (sidebar + content + footer)
|
||||
│ ├── LabSidebar.tsx # Left filter panel
|
||||
│ └── LabFooter.tsx # Contextual footer with links
|
||||
│
|
||||
├── components/lab/
|
||||
│ ├── GenerateFormPlaceholder.tsx # Generation form component
|
||||
│ └── FilterPlaceholder.tsx # Reusable filter checkbox/radio
|
||||
│
|
||||
└── contexts/
|
||||
└── lab-scroll-context.tsx # Scroll state for header collapse
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Work-focused, not marketing** - Small typography, efficient spacing
|
||||
2. **Clean and functional** - Like Google AI Studio
|
||||
3. **Neutral colors** - Zinc palette (true gray, no blue tint)
|
||||
4. **Consistent icons** - Lucide React only, no emojis
|
||||
5. **Responsive** - Works on 768px to 1920px+ screens
|
||||
|
||||
---
|
||||
|
||||
## Color System (Zinc Palette)
|
||||
|
||||
### Backgrounds
|
||||
```css
|
||||
bg-zinc-950 /* Main app background */
|
||||
bg-zinc-900 /* Card backgrounds, elevated surfaces */
|
||||
bg-zinc-900/80 /* Card with transparency + backdrop-blur-sm */
|
||||
bg-zinc-800 /* Input backgrounds, secondary surfaces */
|
||||
bg-zinc-800/50 /* Hover states, subtle overlays */
|
||||
```
|
||||
|
||||
### Borders
|
||||
```css
|
||||
border-zinc-800 /* Standard borders */
|
||||
border-zinc-700 /* Lighter borders, dividers */
|
||||
border-white/10 /* Subtle borders (avoid in Lab) */
|
||||
```
|
||||
|
||||
### Text
|
||||
```css
|
||||
text-white /* Primary headings */
|
||||
text-zinc-100 /* Body text, primary content */
|
||||
text-zinc-300 /* Secondary text, descriptions */
|
||||
text-zinc-400 /* Muted text, placeholders */
|
||||
text-zinc-500 /* Disabled states */
|
||||
text-zinc-600 /* Count badges, metadata */
|
||||
```
|
||||
|
||||
### Accent Colors (Keep Purple/Cyan)
|
||||
```css
|
||||
/* Primary gradient */
|
||||
bg-gradient-to-r from-purple-600 to-cyan-600
|
||||
hover:from-purple-500 hover:to-cyan-500
|
||||
shadow-lg shadow-purple-900/30
|
||||
focus:ring-2 focus:ring-purple-500
|
||||
|
||||
/* Single-color accents */
|
||||
text-purple-400 /* Links, interactive text */
|
||||
bg-purple-500/10 /* Info banners */
|
||||
border-purple-500/20 /* Accent borders */
|
||||
```
|
||||
|
||||
### Status Colors
|
||||
```css
|
||||
/* Success */ bg-emerald-500/10 border-emerald-500/30 text-emerald-400
|
||||
/* Warning */ bg-amber-500/10 border-amber-500/30 text-amber-400
|
||||
/* Error */ bg-red-500/10 border-red-500/30 text-red-400
|
||||
/* Info */ bg-purple-500/10 border-purple-500/30 text-purple-400
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography Scale
|
||||
|
||||
### Headings (Practical Sizes)
|
||||
```tsx
|
||||
// Page Title (only on main pages)
|
||||
text-lg font-semibold text-white
|
||||
|
||||
// Section Title
|
||||
text-base font-semibold text-white
|
||||
|
||||
// Card Title
|
||||
text-sm font-medium text-white
|
||||
```
|
||||
|
||||
### Body Text
|
||||
```tsx
|
||||
// Primary body
|
||||
text-sm text-zinc-300
|
||||
|
||||
// Secondary/descriptions
|
||||
text-sm text-zinc-400
|
||||
|
||||
// Small text (hints, metadata)
|
||||
text-xs text-zinc-500
|
||||
|
||||
// Labels (form fields)
|
||||
text-xs font-medium text-zinc-400
|
||||
```
|
||||
|
||||
### Interactive
|
||||
```tsx
|
||||
// Button text
|
||||
text-sm font-semibold
|
||||
|
||||
// Links
|
||||
text-sm text-purple-400 hover:text-purple-300
|
||||
|
||||
// Badge/count
|
||||
text-xs text-zinc-600
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing System
|
||||
|
||||
### Padding
|
||||
```tsx
|
||||
// Cards
|
||||
p-4 md:p-5
|
||||
|
||||
// Compact cards
|
||||
p-3
|
||||
|
||||
// Form inputs
|
||||
px-3 py-2
|
||||
|
||||
// Buttons (primary)
|
||||
px-4 py-2.5
|
||||
|
||||
// Buttons (secondary)
|
||||
px-3 py-2
|
||||
```
|
||||
|
||||
### Section Spacing
|
||||
```tsx
|
||||
// Page padding
|
||||
py-6 md:py-8
|
||||
|
||||
// Between sections
|
||||
mb-4 md:mb-6
|
||||
|
||||
// Between cards in grid
|
||||
gap-3 md:gap-4
|
||||
|
||||
// Between form fields
|
||||
gap-2
|
||||
|
||||
// Label to input
|
||||
mb-1.5
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
```tsx
|
||||
rounded-lg /* Standard (inputs, small cards) */
|
||||
rounded-xl /* Medium (cards, buttons) */
|
||||
rounded-2xl /* Large - AVOID in Lab (too marketing) */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Page Header
|
||||
```tsx
|
||||
<header className="mb-4 pb-3 border-b border-zinc-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">Generate</h1>
|
||||
<p className="text-xs text-zinc-400">Create AI images from text prompts</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-zinc-300 hover:text-white hover:bg-zinc-700 transition-colors">
|
||||
<Settings className="w-4 h-4 mr-1.5 inline" />
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
### Standard Card
|
||||
```tsx
|
||||
<div className="p-4 bg-zinc-900/80 backdrop-blur-sm border border-zinc-800 rounded-xl">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Compact Card (List Items)
|
||||
```tsx
|
||||
<div className="p-3 bg-zinc-900 border border-zinc-800 rounded-lg hover:border-zinc-700 transition-colors">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form Input
|
||||
```tsx
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-zinc-400">Field Label</label>
|
||||
<input
|
||||
className="w-full px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
placeholder="Enter value..."
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
```tsx
|
||||
<textarea
|
||||
className="w-full px-3 py-2.5 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
```
|
||||
|
||||
### Primary Button
|
||||
```tsx
|
||||
<button className="px-4 py-2.5 text-sm rounded-lg bg-gradient-to-r from-purple-600 to-cyan-600 text-white font-semibold hover:from-purple-500 hover:to-cyan-500 transition-all shadow-lg shadow-purple-900/30 focus:ring-2 focus:ring-purple-500">
|
||||
Generate
|
||||
</button>
|
||||
```
|
||||
|
||||
### Secondary Button
|
||||
```tsx
|
||||
<button className="px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-zinc-300 hover:text-white hover:bg-zinc-700 transition-colors focus:ring-2 focus:ring-purple-500">
|
||||
<Settings className="w-4 h-4 mr-1.5 inline" />
|
||||
Configure
|
||||
</button>
|
||||
```
|
||||
|
||||
### Ghost Button
|
||||
```tsx
|
||||
<button className="px-3 py-2 text-sm text-zinc-400 hover:text-white hover:bg-zinc-800/50 rounded-lg transition-colors">
|
||||
Clear
|
||||
</button>
|
||||
```
|
||||
|
||||
### Icon Button
|
||||
```tsx
|
||||
<button
|
||||
className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800/50 rounded-lg transition-colors focus:ring-2 focus:ring-purple-500"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
```
|
||||
|
||||
### Empty State
|
||||
```tsx
|
||||
<div className="p-6 bg-zinc-900/50 border border-zinc-800 rounded-lg text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 flex items-center justify-center bg-zinc-800 rounded-lg">
|
||||
<ImageOff className="w-6 h-6 text-zinc-500" />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-white mb-1">No results yet</h3>
|
||||
<p className="text-xs text-zinc-400">Generated images will appear here</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Info Banner
|
||||
```tsx
|
||||
<div className="mb-4 p-3 bg-purple-500/5 border border-purple-500/20 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-purple-400 mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-zinc-300">
|
||||
<span className="font-medium text-white">Lab Mode:</span> Experimental features enabled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon System (Lucide React)
|
||||
|
||||
### Standard Sizes
|
||||
```tsx
|
||||
// Inline with text
|
||||
<Icon className="w-4 h-4" />
|
||||
|
||||
// Small (badges)
|
||||
<Icon className="w-3 h-3" />
|
||||
|
||||
// Medium (empty states)
|
||||
<Icon className="w-6 h-6" />
|
||||
|
||||
// With margin (before text)
|
||||
<Icon className="w-4 h-4 mr-1.5" />
|
||||
```
|
||||
|
||||
### Recommended Icons
|
||||
```tsx
|
||||
// Navigation
|
||||
import { Home, Image, Upload, Zap, Settings } from 'lucide-react';
|
||||
|
||||
// Actions
|
||||
import { Plus, X, Download, Share2, Copy, Trash2, Edit3, MoreVertical } from 'lucide-react';
|
||||
|
||||
// Filters
|
||||
import { Activity, Calendar, Palette, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
|
||||
// Status
|
||||
import { CheckCircle2, AlertCircle, XCircle, Info, Loader2 } from 'lucide-react';
|
||||
|
||||
// Media
|
||||
import { ImageOff, FileImage, Sparkles } from 'lucide-react';
|
||||
|
||||
// Form
|
||||
import { Search, Filter, SlidersHorizontal } from 'lucide-react';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Breakpoints
|
||||
|
||||
```tsx
|
||||
// Base: < 768px (mobile) - single column, no sidebar
|
||||
// md: >= 768px (tablet) - 2 columns, no sidebar
|
||||
// lg: >= 1024px (desktop) - sidebar visible, 2-3 columns
|
||||
// xl: >= 1280px (large desktop) - optimal spacing, 3 columns
|
||||
```
|
||||
|
||||
### Sidebar Behavior
|
||||
```tsx
|
||||
// Hidden on mobile/tablet, visible lg+
|
||||
hidden lg:block w-64
|
||||
```
|
||||
|
||||
### Content Grid
|
||||
```tsx
|
||||
// Images: 1 col mobile, 2 col tablet, 3 col desktop
|
||||
grid-cols-1 md:grid-cols-2 xl:grid-cols-3
|
||||
|
||||
// Form fields
|
||||
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### DO
|
||||
- Use zinc palette for backgrounds/borders (not slate)
|
||||
- Use text-sm/text-xs for most text (not text-lg/text-xl)
|
||||
- Use Lucide icons exclusively
|
||||
- Keep spacing tight (p-3 to p-5)
|
||||
- Add focus:ring-2 focus:ring-purple-500 to all interactive elements
|
||||
- Use transitions (transition-colors, transition-all)
|
||||
|
||||
### DON'T
|
||||
- Use emojis anywhere in the UI
|
||||
- Use marketing-size headings (text-3xl+)
|
||||
- Use generous spacing (p-8+, py-12+)
|
||||
- Use rounded-2xl (too marketing)
|
||||
- Use slate colors (too blue-tinted)
|
||||
- Forget aria-label on icon-only buttons
|
||||
|
||||
---
|
||||
|
||||
## Layout Architecture
|
||||
|
||||
The Lab uses a scroll-aware header system:
|
||||
|
||||
1. **LabScrollProvider** wraps the entire section
|
||||
2. **LabLayout** detects scroll > 50px in content area
|
||||
3. When scrolled, header collapses (h-16 → h-0)
|
||||
4. SubsectionNav becomes the top element
|
||||
5. Content height adjusts: `h-[calc(100vh-7rem)]` → `h-[calc(100vh-3rem)]`
|
||||
|
||||
```tsx
|
||||
// Layout hierarchy
|
||||
(lab)/layout.tsx → LabScrollProvider + LabHeader
|
||||
└── lab/layout.tsx → PageProvider (SubsectionNav)
|
||||
└── LabLayout.tsx → ThreeColumnLayout (sidebar + content + footer)
|
||||
└── page.tsx → Actual page content
|
||||
```
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { Section } from '@/components/shared/Section';
|
||||
import { GenerateFormPlaceholder } from '@/components/lab/GenerateFormPlaceholder';
|
||||
|
||||
const GeneratePage = () => {
|
||||
return (
|
||||
<Section className="py-12 md:py-16 min-h-screen">
|
||||
<GenerateFormPlaceholder />
|
||||
</Section>
|
||||
);
|
||||
return <GenerateFormPlaceholder />;
|
||||
};
|
||||
|
||||
export default GeneratePage;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { Section } from '@/components/shared/Section';
|
||||
import { Image, ImageOff } from 'lucide-react';
|
||||
|
||||
const ImagesPage = () => {
|
||||
return (
|
||||
<Section className="py-12 md:py-16 min-h-screen">
|
||||
<header className="mb-8 md:mb-12">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
||||
Image Library
|
||||
</h1>
|
||||
<p className="text-gray-400 text-base md:text-lg">
|
||||
Browse and manage your generated images
|
||||
</p>
|
||||
<div className="p-4 md:p-6 space-y-4">
|
||||
{/* Page Header */}
|
||||
<header className="pb-3 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">Images</h1>
|
||||
<p className="text-xs text-zinc-400">Browse and manage your generated images</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🖼️</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-2">Image Browser</h2>
|
||||
<p className="text-gray-400">Component will be implemented here</p>
|
||||
{/* Empty State Placeholder */}
|
||||
<div className="p-6 bg-zinc-900/50 border border-zinc-800 rounded-lg text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 flex items-center justify-center bg-zinc-800 rounded-lg">
|
||||
<ImageOff className="w-6 h-6 text-zinc-500" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-white mb-1">Image Browser</h2>
|
||||
<p className="text-xs text-zinc-400">Component will be implemented here</p>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { Section } from '@/components/shared/Section';
|
||||
import { Zap, Radio } from 'lucide-react';
|
||||
|
||||
const LivePage = () => {
|
||||
return (
|
||||
<Section className="py-12 md:py-16 min-h-screen">
|
||||
<header className="mb-8 md:mb-12">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
||||
Live Generation
|
||||
</h1>
|
||||
<p className="text-gray-400 text-base md:text-lg">
|
||||
Real-time testing and experimentation workspace
|
||||
</p>
|
||||
<div className="p-4 md:p-6 space-y-4">
|
||||
{/* Page Header */}
|
||||
<header className="pb-3 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">Live</h1>
|
||||
<p className="text-xs text-zinc-400">Real-time testing and experimentation</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">⚡</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-2">Live Testing Interface</h2>
|
||||
<p className="text-gray-400">Component will be implemented here</p>
|
||||
{/* Live Testing Placeholder */}
|
||||
<div className="p-6 bg-zinc-900/50 border border-zinc-800 rounded-lg text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 flex items-center justify-center bg-zinc-800 rounded-lg">
|
||||
<Radio className="w-6 h-6 text-zinc-500" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-white mb-1">Live Testing Interface</h2>
|
||||
<p className="text-xs text-zinc-400">Component will be implemented here</p>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { Section } from '@/components/shared/Section';
|
||||
import { Upload, UploadCloud } from 'lucide-react';
|
||||
|
||||
const UploadPage = () => {
|
||||
return (
|
||||
<Section className="py-12 md:py-16 min-h-screen">
|
||||
<header className="mb-8 md:mb-12">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
||||
File Upload
|
||||
</h1>
|
||||
<p className="text-gray-400 text-base md:text-lg">
|
||||
Upload and manage reference images for generation
|
||||
</p>
|
||||
<div className="p-4 md:p-6 space-y-4">
|
||||
{/* Page Header */}
|
||||
<header className="pb-3 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Upload className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">Upload</h1>
|
||||
<p className="text-xs text-zinc-400">Upload and manage reference images</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">📤</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-2">Upload Interface</h2>
|
||||
<p className="text-gray-400">Component will be implemented here</p>
|
||||
{/* Upload Dropzone Placeholder */}
|
||||
<div className="p-6 bg-zinc-900/50 border border-zinc-800 border-dashed rounded-lg text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 flex items-center justify-center bg-zinc-800 rounded-lg">
|
||||
<UploadCloud className="w-6 h-6 text-zinc-500" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-white mb-1">Upload Interface</h2>
|
||||
<p className="text-xs text-zinc-400">Drag and drop files or click to browse</p>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*
|
||||
* Checkbox/radio button group for sidebar filters.
|
||||
* Supports both single-select (radio) and multi-select (checkbox) modes.
|
||||
* Uses zinc color palette.
|
||||
*
|
||||
* Features:
|
||||
* - Radio buttons for single selection
|
||||
|
|
@ -48,7 +49,7 @@ export const FilterPlaceholder = ({
|
|||
const isSelected = (optionId: string) => selected.includes(optionId);
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-1" role="group" aria-label={`${groupId} filters`}>
|
||||
<div className="mt-2 space-y-0.5" role="group" aria-label={`${groupId} filters`}>
|
||||
{options.map((option) => {
|
||||
const checked = isSelected(option.id);
|
||||
const inputId = `${groupId}-${option.id}`;
|
||||
|
|
@ -57,7 +58,7 @@ export const FilterPlaceholder = ({
|
|||
<label
|
||||
key={option.id}
|
||||
htmlFor={inputId}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer transition-colors hover:bg-white/5 group"
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer transition-colors hover:bg-zinc-800/50 group"
|
||||
>
|
||||
<input
|
||||
type={multiSelect ? 'checkbox' : 'radio'}
|
||||
|
|
@ -65,13 +66,13 @@ export const FilterPlaceholder = ({
|
|||
name={groupId}
|
||||
checked={checked}
|
||||
onChange={() => handleSelect(option.id)}
|
||||
className="w-4 h-4 bg-slate-800 border-slate-600 text-purple-600 focus:ring-2 focus:ring-purple-500 focus:ring-offset-0 rounded cursor-pointer"
|
||||
className="w-4 h-4 bg-zinc-800 border-zinc-600 text-purple-600 focus:ring-2 focus:ring-purple-500 focus:ring-offset-0 rounded cursor-pointer"
|
||||
/>
|
||||
<span className={`flex-1 ${checked ? 'text-white font-medium' : 'text-gray-400 group-hover:text-gray-300'}`}>
|
||||
<span className={`flex-1 text-sm ${checked ? 'text-white font-medium' : 'text-zinc-400 group-hover:text-zinc-300'}`}>
|
||||
{option.label}
|
||||
</span>
|
||||
{option.count !== undefined && (
|
||||
<span className="text-xs text-gray-600">
|
||||
<span className="text-xs text-zinc-600">
|
||||
{option.count}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,25 +3,34 @@
|
|||
/**
|
||||
* Generate Form Placeholder Component
|
||||
*
|
||||
* Main content placeholder for lab generation interface.
|
||||
* Matches the /demo/tti page visual style with form card and results area.
|
||||
* Main content for lab generation interface.
|
||||
* Clean, work-focused design with compact layout.
|
||||
*
|
||||
* Features:
|
||||
* - Header with title and description
|
||||
* - Prompt textarea (similar to TTI page)
|
||||
* - Options row with selects and buttons
|
||||
* - Submit button with gradient styling
|
||||
* - Results area placeholder
|
||||
* - Keyboard shortcuts (Ctrl+Enter)
|
||||
* - Compact info banner (dismissible)
|
||||
* - Prompt textarea with keyboard shortcuts
|
||||
* - Options grid (aspect ratio, style, advanced)
|
||||
* - Results area with empty state
|
||||
* - Lucide icons throughout
|
||||
*/
|
||||
|
||||
import { useState, useRef, KeyboardEvent } from 'react';
|
||||
import {
|
||||
Info,
|
||||
X,
|
||||
Loader2,
|
||||
Settings,
|
||||
ImageOff,
|
||||
Trash2,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const GenerateFormPlaceholder = () => {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [aspectRatio, setAspectRatio] = useState('1:1');
|
||||
const [template, setTemplate] = useState('photorealistic');
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleGenerate = () => {
|
||||
|
|
@ -38,42 +47,41 @@ export const GenerateFormPlaceholder = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Page Header */}
|
||||
<header className="mb-8 md:mb-12">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
||||
Generate Workbench
|
||||
</h1>
|
||||
<p className="text-gray-400 text-base md:text-lg">
|
||||
Create and experiment with AI-generated images
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Info Banner (Placeholder) */}
|
||||
<div className="mb-6 p-5 bg-purple-900/10 border border-purple-700/50 rounded-2xl">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-1">Lab Environment</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
This is an experimental interface for testing generation features
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-400">
|
||||
<span className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></span>
|
||||
<span>Active</span>
|
||||
<div className="p-4 md:p-6 space-y-4">
|
||||
{/* Compact Info Banner */}
|
||||
{showBanner && (
|
||||
<div className="p-3 bg-purple-500/5 border border-purple-500/20 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-purple-400 mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-zinc-300">
|
||||
<span className="font-medium text-white">Lab Mode:</span>{' '}
|
||||
Experimental features enabled. API calls may be rate-limited.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowBanner(false)}
|
||||
className="p-1 text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generation Form Card */}
|
||||
<section
|
||||
className="mb-8 p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl"
|
||||
className="p-4 bg-zinc-900/80 backdrop-blur-sm border border-zinc-800 rounded-xl"
|
||||
aria-label="Image Generation Form"
|
||||
>
|
||||
{/* Prompt Textarea */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="lab-prompt-input" className="block text-lg font-semibold text-white mb-3">
|
||||
Your Prompt
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="lab-prompt-input"
|
||||
className="block text-xs font-medium text-zinc-400 mb-1.5"
|
||||
>
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="lab-prompt-input"
|
||||
|
|
@ -83,22 +91,19 @@ export const GenerateFormPlaceholder = () => {
|
|||
onKeyDown={handleKeyDown}
|
||||
placeholder="Describe the image you want to generate..."
|
||||
disabled={generating}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
||||
rows={4}
|
||||
className="w-full px-3 py-2.5 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
||||
aria-label="Image generation prompt"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Tip: Be specific and descriptive for better results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Options Row */}
|
||||
<div className="mb-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div className="mb-3 grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
{/* Aspect Ratio */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="lab-aspect-ratio"
|
||||
className="block text-xs font-medium text-gray-400 mb-1.5"
|
||||
className="block text-xs font-medium text-zinc-400 mb-1.5"
|
||||
>
|
||||
Aspect Ratio
|
||||
</label>
|
||||
|
|
@ -107,7 +112,7 @@ export const GenerateFormPlaceholder = () => {
|
|||
value={aspectRatio}
|
||||
onChange={(e) => setAspectRatio(e.target.value)}
|
||||
disabled={generating}
|
||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="1:1">Square (1:1)</option>
|
||||
<option value="3:4">Portrait (3:4)</option>
|
||||
|
|
@ -120,7 +125,10 @@ export const GenerateFormPlaceholder = () => {
|
|||
|
||||
{/* Template */}
|
||||
<div>
|
||||
<label htmlFor="lab-template" className="block text-xs font-medium text-gray-400 mb-1.5">
|
||||
<label
|
||||
htmlFor="lab-template"
|
||||
className="block text-xs font-medium text-zinc-400 mb-1.5"
|
||||
>
|
||||
Style Template
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -128,7 +136,7 @@ export const GenerateFormPlaceholder = () => {
|
|||
value={template}
|
||||
onChange={(e) => setTemplate(e.target.value)}
|
||||
disabled={generating}
|
||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="photorealistic">Photorealistic</option>
|
||||
<option value="illustration">Illustration</option>
|
||||
|
|
@ -142,66 +150,73 @@ export const GenerateFormPlaceholder = () => {
|
|||
|
||||
{/* Advanced Options Button */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-400 mb-1.5">
|
||||
Advanced Options
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1.5">
|
||||
Advanced
|
||||
</label>
|
||||
<button
|
||||
disabled={generating}
|
||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-gray-400 hover:text-white hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
className="w-full px-3 py-2 text-sm bg-zinc-800 border border-zinc-700 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1.5 focus:ring-2 focus:ring-purple-500"
|
||||
aria-label="Advanced options"
|
||||
>
|
||||
<span>⚙️</span>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Configure</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button Row */}
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap pt-2 border-t border-slate-700/50">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center justify-between gap-3 pt-3 border-t border-zinc-800">
|
||||
<div className="text-xs text-zinc-500">
|
||||
{generating ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></span>
|
||||
<span className="flex items-center gap-1.5 text-purple-400">
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
'Press Ctrl+Enter to submit'
|
||||
<span className="hidden sm:inline">Ctrl+Enter to submit</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating || !prompt.trim()}
|
||||
className="px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-600 to-cyan-600 text-white font-semibold hover:from-purple-500 hover:to-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-900/30 focus:ring-2 focus:ring-purple-500"
|
||||
className="px-4 py-2 text-sm rounded-lg bg-gradient-to-r from-purple-600 to-cyan-600 text-white font-semibold hover:from-purple-500 hover:to-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-900/30 focus:ring-2 focus:ring-purple-500 flex items-center gap-1.5"
|
||||
>
|
||||
{generating ? 'Generating...' : 'Generate Images'}
|
||||
<Sparkles className="w-4 h-4" />
|
||||
{generating ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Results Area Placeholder */}
|
||||
<section className="space-y-6" aria-label="Generated Results">
|
||||
{/* Results Area */}
|
||||
<section className="space-y-3" aria-label="Generated Results">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-white">Results</h2>
|
||||
<button className="text-sm text-gray-500 hover:text-gray-300 transition-colors">
|
||||
Clear All
|
||||
<h2 className="text-sm font-semibold text-white">Results</h2>
|
||||
<button className="flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
<div className="p-12 bg-slate-900/50 backdrop-blur-sm border border-slate-700 rounded-2xl text-center">
|
||||
<div className="mb-4 text-5xl">🎨</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">No results yet</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Generate your first image to see results here
|
||||
</p>
|
||||
</div>
|
||||
{!generating && (
|
||||
<div className="p-6 bg-zinc-900/50 border border-zinc-800 rounded-lg text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 flex items-center justify-center bg-zinc-800 rounded-lg">
|
||||
<ImageOff className="w-6 h-6 text-zinc-500" />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-white mb-1">No results yet</h3>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Generated images will appear here
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result Cards Placeholder (would appear after generation) */}
|
||||
{/* Loading State */}
|
||||
{generating && (
|
||||
<div className="p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl animate-pulse">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="aspect-square bg-slate-800 rounded-lg"></div>
|
||||
<div className="aspect-square bg-slate-800 rounded-lg"></div>
|
||||
<div className="p-4 bg-zinc-900/80 backdrop-blur-sm border border-zinc-800 rounded-lg animate-pulse">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
<div className="aspect-square bg-zinc-800 rounded-lg"></div>
|
||||
<div className="aspect-square bg-zinc-800 rounded-lg"></div>
|
||||
<div className="aspect-square bg-zinc-800 rounded-lg hidden xl:block"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
*
|
||||
* Simple 1-line footer for lab section with contextual navigation links.
|
||||
* Displays copyright on left and contextual docs/API links on right.
|
||||
* Uses zinc color palette.
|
||||
*/
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { BookOpen, Code } from 'lucide-react';
|
||||
|
||||
type LinkMapEntry = {
|
||||
docs: string;
|
||||
|
|
@ -32,27 +34,29 @@ export const LabFooter = () => {
|
|||
|
||||
return (
|
||||
<footer
|
||||
className="border-t border-white/10 bg-slate-950/50 backdrop-blur-sm"
|
||||
className="border-t border-zinc-800 bg-zinc-950/50 backdrop-blur-sm"
|
||||
role="contentinfo"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-0 px-6 py-4 md:h-14">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-0 px-4 py-3 md:h-12">
|
||||
{/* Left: Copyright */}
|
||||
<p className="text-sm text-gray-500 order-2 md:order-1">
|
||||
<p className="text-xs text-zinc-500 order-2 md:order-1">
|
||||
© 2025 Banatie. Built for builders who create.
|
||||
</p>
|
||||
|
||||
{/* Right: Contextual Links */}
|
||||
<nav aria-label="Footer navigation" className="flex items-center gap-6 order-1 md:order-2">
|
||||
<nav aria-label="Footer navigation" className="flex items-center gap-4 order-1 md:order-2">
|
||||
<Link
|
||||
href={links.docs}
|
||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
||||
className="text-xs text-zinc-500 hover:text-white transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href={links.api}
|
||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
||||
className="text-xs text-zinc-500 hover:text-white transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
API Reference
|
||||
</Link>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const LabLayout = ({ children }: LabLayoutProps) => {
|
|||
return (
|
||||
<ThreeColumnLayout
|
||||
left={
|
||||
<div className={`border-r border-white/10 bg-slate-950/50 backdrop-blur-sm ${containerHeight} overflow-y-auto transition-all duration-300`}>
|
||||
<div className={`border-r border-zinc-800 bg-zinc-950/50 backdrop-blur-sm ${containerHeight} overflow-y-auto transition-all duration-300`}>
|
||||
<LabSidebar />
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,23 @@
|
|||
* Lab Sidebar - Filter Panel
|
||||
*
|
||||
* Narrow left sidebar for filtering lab content.
|
||||
* Matches DocsSidebar visual style with collapsible filter sections.
|
||||
* Clean, work-focused design with Lucide icons and zinc palette.
|
||||
*
|
||||
* Features:
|
||||
* - Multiple filter groups (Status, Date Range, Source Type)
|
||||
* - Collapsible sections
|
||||
* - Collapsible sections with Lucide icons
|
||||
* - Checkbox/radio button controls
|
||||
* - Clean, minimal design
|
||||
* - Zinc color palette (neutral gray)
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, type ElementType } from 'react';
|
||||
import { Activity, Calendar, Palette, ChevronRight, RotateCcw } from 'lucide-react';
|
||||
import { FilterPlaceholder } from '@/components/lab/FilterPlaceholder';
|
||||
|
||||
type FilterGroupProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
icon: ElementType;
|
||||
options: Array<{ id: string; label: string; count?: number }>;
|
||||
multiSelect?: boolean;
|
||||
};
|
||||
|
|
@ -28,7 +29,7 @@ const filterGroups: FilterGroupProps[] = [
|
|||
{
|
||||
id: 'status',
|
||||
label: 'Status',
|
||||
icon: '📊',
|
||||
icon: Activity,
|
||||
options: [
|
||||
{ id: 'all', label: 'All', count: 127 },
|
||||
{ id: 'completed', label: 'Completed', count: 98 },
|
||||
|
|
@ -40,7 +41,7 @@ const filterGroups: FilterGroupProps[] = [
|
|||
{
|
||||
id: 'date',
|
||||
label: 'Date Range',
|
||||
icon: '📅',
|
||||
icon: Calendar,
|
||||
options: [
|
||||
{ id: 'today', label: 'Today', count: 12 },
|
||||
{ id: 'week', label: 'Last 7 days', count: 45 },
|
||||
|
|
@ -52,7 +53,7 @@ const filterGroups: FilterGroupProps[] = [
|
|||
{
|
||||
id: 'source',
|
||||
label: 'Source Type',
|
||||
icon: '🎨',
|
||||
icon: Palette,
|
||||
options: [
|
||||
{ id: 'text', label: 'Text-to-Image', count: 78 },
|
||||
{ id: 'upload', label: 'Uploads', count: 34 },
|
||||
|
|
@ -74,38 +75,34 @@ export const LabSidebar = () => {
|
|||
const isExpanded = (id: string) => expandedSections.includes(id);
|
||||
|
||||
return (
|
||||
<aside className="h-full bg-slate-950 border-r border-white/10" aria-label="Lab filters">
|
||||
<aside className="h-full bg-zinc-950 border-r border-zinc-800" aria-label="Lab filters">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-slate-800">
|
||||
<h2 className="text-lg font-semibold text-white">Filters</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">Refine your results</p>
|
||||
<div className="p-4 border-b border-zinc-800">
|
||||
<h2 className="text-sm font-semibold text-white">Filters</h2>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Refine your results</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Groups */}
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="p-3 space-y-3">
|
||||
{filterGroups.map((group) => {
|
||||
const expanded = isExpanded(group.id);
|
||||
const Icon = group.icon;
|
||||
|
||||
return (
|
||||
<div key={group.id} className="border-b border-slate-800 pb-4 last:border-b-0">
|
||||
<div key={group.id} className="border-b border-zinc-800 pb-3 last:border-b-0">
|
||||
{/* Section Header */}
|
||||
<button
|
||||
onClick={() => toggleSection(group.id)}
|
||||
className="w-full flex items-center justify-between px-2 py-2 rounded-lg text-sm transition-colors text-gray-400 hover:text-white hover:bg-white/5"
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 rounded-lg text-sm transition-colors text-zinc-400 hover:text-white hover:bg-zinc-800/50"
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-base">{group.icon}</span>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-medium">{group.label}</span>
|
||||
</span>
|
||||
<svg
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Filter Options */}
|
||||
|
|
@ -122,11 +119,12 @@ export const LabSidebar = () => {
|
|||
</div>
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="mt-auto p-4 border-t border-slate-800">
|
||||
<div className="mt-auto p-3 border-t border-zinc-800">
|
||||
<button
|
||||
className="w-full px-3 py-2 text-sm text-gray-500 hover:text-gray-300 transition-colors rounded-lg hover:bg-white/5"
|
||||
className="w-full flex items-center justify-center gap-1.5 px-3 py-2 text-sm text-zinc-500 hover:text-zinc-300 transition-colors rounded-lg hover:bg-zinc-800/50"
|
||||
aria-label="Reset all filters"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,9 @@ importers:
|
|||
'@banatie/database':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/database
|
||||
lucide-react:
|
||||
specifier: ^0.400.0
|
||||
version: 0.400.0(react@19.1.0)
|
||||
next:
|
||||
specifier: 15.5.4
|
||||
version: 15.5.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
|
|
@ -9192,6 +9195,10 @@ snapshots:
|
|||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
lucide-react@0.400.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
magic-string@0.30.19:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
|
|||
Loading…
Reference in New Issue