frontend-ui-dark-ts
Build dark-themed React applications using Tailwind CSS with custom theming, glassmorphism effects, and Framer Motion animations. Use when creating dashboards, admin panels, or data-rich interfaces with a refined dark aesthetic.
Install
npx skills add https://github.com/microsoft/skills/tree/main/.github/plugins/azure-sdk-typescript/skills/frontend-ui-dark-ts
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart
git clone https://github.com/microsoft/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole microsoft/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Frontend UI Dark Theme (TypeScript)
A modern dark-themed React UI system using Tailwind CSS and Framer Motion. Designed for dashboards, admin panels, and data-rich applications with glassmorphism effects and tasteful animations.
Stack
| Package | Version | Purpose |
|---|---|---|
react |
^18.x | UI framework |
react-dom |
^18.x | DOM rendering |
react-router-dom |
^6.x | Routing |
framer-motion |
^11.x | Animations |
clsx |
^2.x | Class merging |
tailwindcss |
^3.x | Styling |
vite |
^5.x | Build tool |
typescript |
^5.x | Type safety |
Quick Start
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install framer-motion clsx react-router-dom
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Project Structure
public/
├── favicon.ico # Classic favicon (32x32)
├── favicon.svg # Modern SVG favicon
├── apple-touch-icon.png # iOS home screen (180x180)
├── og-image.png # Social sharing image (1200x630)
└── site.webmanifest # PWA manifest
src/
├── assets/
│ └── fonts/
│ ├── Segoe UI.ttf
│ ├── Segoe UI Bold.ttf
│ ├── Segoe UI Italic.ttf
│ └── Segoe UI Bold Italic.ttf
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Input.tsx
│ │ ├── Badge.tsx
│ │ ├── Dialog.tsx
│ │ ├── Tabs.tsx
│ │ └── index.ts
│ └── layout/
│ ├── AppShell.tsx
│ ├── Sidebar.tsx
│ └── PageHeader.tsx
├── styles/
│ └── globals.css
├── App.tsx
└── main.tsx
Configuration
index.html
The HTML entry point with mobile viewport, favicons, and social meta tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Theme color for mobile browser chrome -->
<meta name="theme-color" content="#18181B" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="App Name" />
<meta property="og:description" content="App description" />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:url" content="https://example.com" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="App Name" />
<meta name="twitter:description" content="App description" />
<meta name="twitter:image" content="https://example.com/og-image.png" />
<title>App Name</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
public/site.webmanifest
PWA manifest for installable web apps:
{
"name": "App Name",
"short_name": "App",
"icons": [
{ "src": "/favicon.ico", "sizes": "32x32", "type": "image/x-icon" },
{ "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
],
"theme_color": "#18181B",
"background_color": "#18181B",
"display": "standalone"
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Segoe UI', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
DEFAULT: '#8251EE',
hover: '#9366F5',
light: '#A37EF5',
subtle: 'rgba(130, 81, 238, 0.15)',
},
neutral: {
bg1: 'hsl(240, 6%, 10%)',
bg2: 'hsl(240, 5%, 12%)',
bg3: 'hsl(240, 5%, 14%)',
bg4: 'hsl(240, 4%, 18%)',
bg5: 'hsl(240, 4%, 22%)',
bg6: 'hsl(240, 4%, 26%)',
},
text: {
primary: '#FFFFFF',
secondary: '#A1A1AA',
muted: '#71717A',
},
border: {
subtle: 'hsla(0, 0%, 100%, 0.08)',
DEFAULT: 'hsla(0, 0%, 100%, 0.12)',
strong: 'hsla(0, 0%, 100%, 0.20)',
},
status: {
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
info: '#3B82F6',
},
dataviz: {
purple: '#8251EE',
blue: '#3B82F6',
green: '#10B981',
yellow: '#F59E0B',
red: '#EF4444',
pink: '#EC4899',
cyan: '#06B6D4',
},
},
borderRadius: {
DEFAULT: '0.5rem',
lg: '0.75rem',
xl: '1rem',
},
boxShadow: {
glow: '0 0 20px rgba(130, 81, 238, 0.3)',
'glow-lg': '0 0 40px rgba(130, 81, 238, 0.4)',
},
backdropBlur: {
xs: '2px',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
// Mobile: safe area insets for notched devices
spacing: {
'safe-top': 'env(safe-area-inset-top)',
'safe-bottom': 'env(safe-area-inset-bottom)',
'safe-left': 'env(safe-area-inset-left)',
'safe-right': 'env(safe-area-inset-right)',
},
// Mobile: minimum touch target sizes (44px per Apple/Google guidelines)
minHeight: {
'touch': '44px',
},
minWidth: {
'touch': '44px',
},
},
},
plugins: [],
};
postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Font faces */
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Bold Italic.ttf') format('truetype');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* CSS Custom Properties */
:root {
/* Brand colors */
--color-brand: #8251EE;
--color-brand-hover: #9366F5;
--color-brand-light: #A37EF5;
--color-brand-subtle: rgba(130, 81, 238, 0.15);
/* Neutral backgrounds */
--color-bg-1: hsl(240, 6%, 10%);
--color-bg-2: hsl(240, 5%, 12%);
--color-bg-3: hsl(240, 5%, 14%);
--color-bg-4: hsl(240, 4%, 18%);
--color-bg-5: hsl(240, 4%, 22%);
--color-bg-6: hsl(240, 4%, 26%);
/* Text colors */
--color-text-primary: #FFFFFF;
--color-text-secondary: #A1A1AA;
--color-text-muted: #71717A;
/* Border colors */
--color-border-subtle: hsla(0, 0%, 100%, 0.08);
--color-border-default: hsla(0, 0%, 100%, 0.12);
--color-border-strong: hsla(0, 0%, 100%, 0.20);
/* Status colors */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease;
}
/* Base styles */
html {
color-scheme: dark;
}
body {
@apply bg-neutral-bg1 text-text-primary font-sans antialiased;
min-height: 100vh;
}
/* Focus styles */
*:focus-visible {
@apply outline-none ring-2 ring-brand ring-offset-2 ring-offset-neutral-bg1;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-bg2;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-bg5 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-bg6;
}
/* Glass utility classes */
@layer components {
.glass {
@apply backdrop-blur-md bg-white/5 border border-white/10;
}
.glass-card {
@apply backdrop-blur-md bg-white/5 border border-white/10 rounded-xl;
}
.glass-panel {
@apply backdrop-blur-lg bg-black/40 border border-white/5;
}
.glass-overlay {
@apply backdrop-blur-sm bg-black/60;
}
.glass-input {
@apply backdrop-blur-sm bg-white/5 border border-white/10 focus:border-brand focus:bg-white/10;
}
}
/* Animation utilities */
@layer utilities {
.animate-in {
animation: fadeIn 0.3s ease-out, slideUp 0.3s ease-out;
}
}
src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/globals.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
src/App.tsx
import { Routes, Route } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import { AppShell } from './components/layout/AppShell';
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
export default function App() {
return (
<AppShell>
<AnimatePresence mode="wait">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</AnimatePresence>
</AppShell>
);
}
Animation Patterns
Framer Motion Variants
// Fade in on mount
export const fadeIn = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: 0.2 },
};
// Slide up on mount
export const slideUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
transition: { duration: 0.3, ease: 'easeOut' },
};
// Scale on hover (for buttons/cards)
export const scaleOnHover = {
whileHover: { scale: 1.02 },
whileTap: { scale: 0.98 },
transition: { type: 'spring', stiffness: 400, damping: 17 },
};
// Stagger children
export const staggerContainer = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: 0.1,
},
},
};
export const staggerItem = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.2, ease: 'easeOut' },
},
};
Page Transition Wrapper
import { motion } from 'framer-motion';
import { ReactNode } from 'react';
interface PageTransitionProps {
children: ReactNode;
}
export function PageTransition({ children }: PageTransitionProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
Glass Effect Patterns
Glass Card
<div className="glass-card p-6">
<h2 className="text-lg font-semibold text-text-primary">Card Title</h2>
<p className="text-text-secondary mt-2">Card content goes here.</p>
</div>
Glass Panel (Sidebar)
<aside className="glass-panel w-64 h-screen p-4">
<nav className="space-y-2">
{/* Navigation items */}
</nav>
</aside>
Glass Modal Overlay
<motion.div
className="fixed inset-0 glass-overlay flex items-center justify-center z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="glass-card p-6 max-w-md w-full mx-4"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
>
{/* Modal content */}
</motion.div>
</motion.div>
Typography
| Element | Classes |
|---|---|
| Page title | text-2xl font-semibold text-text-primary |
| Section title | text-lg font-semibold text-text-primary |
| Card title | text-base font-medium text-text-primary |
| Body text | text-sm text-text-secondary |
| Caption | text-xs text-text-muted |
| Label | text-sm font-medium text-text-secondary |
Color Usage
| Use Case | Color | Class |
|---|---|---|
| Primary action | Brand purple | bg-brand text-white |
| Primary hover | Brand hover | hover:bg-brand-hover |
| Page background | Neutral bg1 | bg-neutral-bg1 |
| Card background | Neutral bg2 | bg-neutral-bg2 |
| Elevated surface | Neutral bg3 | bg-neutral-bg3 |
| Input background | Neutral bg2 | bg-neutral-bg2 |
| Input focus | Neutral bg3 | focus:bg-neutral-bg3 |
| Border default | Border default | border-border |
| Border subtle | Border subtle | border-border-subtle |
| Success | Status success | text-status-success |
| Warning | Status warning | text-status-warning |
| Error | Status error | text-status-error |
Related Files
- Design Tokens — Complete color system, spacing, typography scales
- Components — Button, Card, Input, Dialog, Tabs, and more
- Patterns — Page layouts, navigation, lists, forms
Files (skills)
-
assets
-
foundry-logo-dark.png 29.3 KB · in bundle
-
foundry-logo-light.png 24.9 KB · in bundle
-
Segoe UI Bold Italic.ttf 41.6 KB · in bundle
-
Segoe UI Bold.ttf 35.2 KB · in bundle
-
Segoe UI Italic.ttf 40.4 KB · in bundle
-
Segoe UI.ttf 33.4 KB · in bundle
-
-
references
-
components.md 26.1 KB
# Components Reusable UI components built with React, Tailwind CSS, and Framer Motion. ## Button ```tsx import { motion, type HTMLMotionProps } from 'framer-motion'; import { clsx } from 'clsx'; import { forwardRef, type ReactNode } from 'react'; type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'; type ButtonSize = 'sm' | 'md' | 'lg'; interface ButtonProps extends Omit<HTMLMotionProps<'button'>, 'children'> { variant?: ButtonVariant; size?: ButtonSize; children: ReactNode; isLoading?: boolean; leftIcon?: ReactNode; rightIcon?: ReactNode; } const variants: Record<ButtonVariant, string> = { primary: 'bg-brand text-white hover:bg-brand-hover active:bg-brand-light', secondary: 'bg-neutral-bg3 text-text-primary border border-border hover:bg-neutral-bg4 hover:border-border-strong', ghost: 'text-text-secondary hover:text-text-primary hover:bg-neutral-bg3', danger: 'bg-status-error text-white hover:bg-red-600 active:bg-red-700', }; // Size variants - all sizes meet 44px minimum height on mobile for touch targets const sizes: Record<ButtonSize, string> = { sm: 'h-8 min-h-touch px-3 text-xs gap-1.5 rounded', // 32px desktop, 44px mobile minimum md: 'h-10 min-h-touch px-4 text-sm gap-2 rounded-lg', // 40px desktop, 44px mobile minimum lg: 'h-12 px-6 text-base gap-2.5 rounded-lg', // 48px already exceeds 44px }; export const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ variant = 'primary', size = 'md', children, isLoading, leftIcon, rightIcon, className, disabled, ...props }, ref) => { return ( <motion.button ref={ref} whileHover={{ scale: disabled || isLoading ? 1 : 1.02 }} whileTap={{ scale: disabled || isLoading ? 1 : 0.98 }} transition={{ type: 'spring', stiffness: 400, damping: 17 }} className={clsx( 'inline-flex items-center justify-center font-medium transition-colors duration-150', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-bg1', 'disabled:opacity-50 disabled:cursor-not-allowed', variants[variant], sizes[size], className )} disabled={disabled || isLoading} {...props} > {isLoading ? ( <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> </svg> ) : ( <> {leftIcon && <span className="flex-shrink-0">{leftIcon}</span>} {children} {rightIcon && <span className="flex-shrink-0">{rightIcon}</span>} </> )} </motion.button> ); } ); Button.displayName = 'Button'; ``` ### Button Usage ```tsx // Primary (default) <Button>Save Changes</Button> // Secondary <Button variant="secondary">Cancel</Button> // Ghost <Button variant="ghost">Learn More</Button> // Danger <Button variant="danger">Delete</Button> // With icons <Button leftIcon={<PlusIcon />}>Add Item</Button> <Button rightIcon={<ArrowRightIcon />}>Continue</Button> // Loading state <Button isLoading>Saving...</Button> // Sizes <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> ``` --- ## Input ```tsx import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react'; import { clsx } from 'clsx'; interface InputProps extends InputHTMLAttributes<HTMLInputElement> { label?: string; error?: string; hint?: string; leftIcon?: ReactNode; rightIcon?: ReactNode; } export const Input = forwardRef<HTMLInputElement, InputProps>( ({ label, error, hint, leftIcon, rightIcon, className, id, ...props }, ref) => { const inputId = id || label?.toLowerCase().replace(/\s+/g, '-'); return ( <div className="flex flex-col gap-1.5"> {label && ( <label htmlFor={inputId} className="text-sm font-medium text-text-secondary"> {label} </label> )} <div className="relative"> {leftIcon && ( <span className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted"> {leftIcon} </span> )} <input ref={ref} id={inputId} className={clsx( 'w-full h-10 min-h-touch px-3 rounded-lg text-sm text-text-primary', // min-h-touch for 44px mobile touch target 'bg-neutral-bg2 border border-border', 'placeholder:text-text-muted', 'focus:outline-none focus:border-brand focus:bg-neutral-bg3', 'disabled:opacity-50 disabled:cursor-not-allowed', 'transition-colors duration-150', leftIcon && 'pl-10', rightIcon && 'pr-10', error && 'border-status-error focus:border-status-error', className )} {...props} /> {rightIcon && ( <span className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted"> {rightIcon} </span> )} </div> {error && <p className="text-xs text-status-error">{error}</p>} {hint && !error && <p className="text-xs text-text-muted">{hint}</p>} </div> ); } ); Input.displayName = 'Input'; ``` ### Search Input Variant ```tsx import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/20/solid'; interface SearchInputProps { value: string; onChange: (value: string) => void; placeholder?: string; } export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) { return ( <Input type="search" value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} leftIcon={<MagnifyingGlassIcon className="w-4 h-4" />} rightIcon={ value && ( <button onClick={() => onChange('')} className="hover:text-text-primary transition-colors"> <XMarkIcon className="w-4 h-4" /> </button> ) } /> ); } ``` --- ## Card ```tsx import { motion, type HTMLMotionProps } from 'framer-motion'; import { clsx } from 'clsx'; import { forwardRef, type ReactNode } from 'react'; type CardVariant = 'default' | 'glass' | 'elevated'; interface CardProps extends HTMLMotionProps<'div'> { variant?: CardVariant; children: ReactNode; interactive?: boolean; padding?: 'none' | 'sm' | 'md' | 'lg'; } const variants: Record<CardVariant, string> = { default: 'bg-neutral-bg2 border border-border', glass: 'backdrop-blur-md bg-white/5 border border-white/10', elevated: 'bg-neutral-bg3 border border-border shadow-lg', }; const paddings = { none: '', sm: 'p-3', md: 'p-4', lg: 'p-6', }; export const Card = forwardRef<HTMLDivElement, CardProps>( ({ variant = 'default', children, interactive = false, padding = 'md', className, ...props }, ref) => { const baseClass = clsx( 'rounded-xl', variants[variant], paddings[padding], interactive && 'cursor-pointer hover:border-border-strong transition-colors duration-150', className ); if (interactive) { return ( <motion.div ref={ref} className={baseClass} whileHover={{ scale: 1.01, y: -2 }} whileTap={{ scale: 0.99 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }} {...props} > {children} </motion.div> ); } return ( <motion.div ref={ref} className={baseClass} {...props}> {children} </motion.div> ); } ); Card.displayName = 'Card'; // Subcomponents export function CardHeader({ children, className }: { children: ReactNode; className?: string }) { return <div className={clsx('mb-4', className)}>{children}</div>; } export function CardTitle({ children, className }: { children: ReactNode; className?: string }) { return <h3 className={clsx('text-lg font-semibold text-text-primary', className)}>{children}</h3>; } export function CardDescription({ children, className }: { children: ReactNode; className?: string }) { return <p className={clsx('text-sm text-text-secondary mt-1', className)}>{children}</p>; } export function CardContent({ children, className }: { children: ReactNode; className?: string }) { return <div className={className}>{children}</div>; } export function CardFooter({ children, className }: { children: ReactNode; className?: string }) { return <div className={clsx('mt-4 flex items-center gap-2', className)}>{children}</div>; } ``` ### Card Usage ```tsx // Default card <Card> <CardHeader> <CardTitle>Card Title</CardTitle> <CardDescription>Card description goes here</CardDescription> </CardHeader> <CardContent> <p>Card content</p> </CardContent> </Card> // Glass card <Card variant="glass"> <p>Glass card with blur effect</p> </Card> // Interactive card <Card interactive onClick={() => console.log('clicked')}> <p>Click me!</p> </Card> ``` --- ## Badge ```tsx import { clsx } from 'clsx'; import { type ReactNode } from 'react'; type BadgeVariant = 'default' | 'brand' | 'success' | 'warning' | 'error' | 'info'; type BadgeSize = 'sm' | 'md'; interface BadgeProps { variant?: BadgeVariant; size?: BadgeSize; children: ReactNode; className?: string; } const variants: Record<BadgeVariant, string> = { default: 'bg-neutral-bg4 text-text-secondary', brand: 'bg-brand-subtle text-brand-light', success: 'bg-status-success-subtle text-status-success', warning: 'bg-status-warning-subtle text-status-warning', error: 'bg-status-error-subtle text-status-error', info: 'bg-status-info-subtle text-status-info', }; const sizes: Record<BadgeSize, string> = { sm: 'text-xs px-1.5 py-0.5', md: 'text-xs px-2 py-1', }; export function Badge({ variant = 'default', size = 'md', children, className }: BadgeProps) { return ( <span className={clsx( 'inline-flex items-center font-medium rounded-md', variants[variant], sizes[size], className )} > {children} </span> ); } ``` ### Badge Usage ```tsx <Badge>Default</Badge> <Badge variant="brand">Brand</Badge> <Badge variant="success">Success</Badge> <Badge variant="warning">Warning</Badge> <Badge variant="error">Error</Badge> <Badge variant="info">Info</Badge> // With dot indicator <Badge variant="success"> <span className="w-1.5 h-1.5 rounded-full bg-current mr-1.5" /> Active </Badge> ``` --- ## Dialog ```tsx import { motion, AnimatePresence } from 'framer-motion'; import { clsx } from 'clsx'; import { type ReactNode, useEffect, useRef } from 'react'; import { XMarkIcon } from '@heroicons/react/24/outline'; interface DialogProps { open: boolean; onClose: () => void; children: ReactNode; title?: string; description?: string; size?: 'sm' | 'md' | 'lg' | 'xl'; } const sizes = { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-xl', }; export function Dialog({ open, onClose, children, title, description, size = 'md' }: DialogProps) { const overlayRef = useRef<HTMLDivElement>(null); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; if (open) { document.addEventListener('keydown', handleEscape); document.body.style.overflow = 'hidden'; } return () => { document.removeEventListener('keydown', handleEscape); document.body.style.overflow = ''; }; }, [open, onClose]); return ( <AnimatePresence> {open && ( <div className="fixed inset-0 z-50 flex items-center justify-center"> {/* Backdrop */} <motion.div ref={overlayRef} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} className="absolute inset-0 glass-overlay" onClick={(e) => e.target === overlayRef.current && onClose()} /> {/* Dialog */} <motion.div initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }} transition={{ duration: 0.2, ease: [0, 0, 0.2, 1] }} className={clsx( 'relative w-full mx-4 glass-card p-6', sizes[size] )} > {/* Close button */} <button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary transition-colors" > <XMarkIcon className="w-5 h-5" /> </button> {/* Header */} {(title || description) && ( <div className="mb-4 pr-8"> {title && <h2 className="text-lg font-semibold text-text-primary">{title}</h2>} {description && <p className="text-sm text-text-secondary mt-1">{description}</p>} </div> )} {/* Content */} {children} </motion.div> </div> )} </AnimatePresence> ); } // Subcomponents export function DialogFooter({ children, className }: { children: ReactNode; className?: string }) { return ( <div className={clsx('mt-6 flex items-center justify-end gap-3', className)}> {children} </div> ); } ``` ### Dialog Usage ```tsx const [open, setOpen] = useState(false); <Button onClick={() => setOpen(true)}>Open Dialog</Button> <Dialog open={open} onClose={() => setOpen(false)} title="Confirm Action" description="Are you sure you want to continue?" > <p className="text-sm text-text-secondary"> This action cannot be undone. </p> <DialogFooter> <Button variant="secondary" onClick={() => setOpen(false)}> Cancel </Button> <Button variant="danger" onClick={handleConfirm}> Delete </Button> </DialogFooter> </Dialog> ``` --- ## Tabs ```tsx import { motion } from 'framer-motion'; import { clsx } from 'clsx'; import { createContext, useContext, useState, type ReactNode } from 'react'; interface TabsContextValue { activeTab: string; setActiveTab: (tab: string) => void; } const TabsContext = createContext<TabsContextValue | null>(null); interface TabsProps { defaultValue: string; children: ReactNode; className?: string; } export function Tabs({ defaultValue, children, className }: TabsProps) { const [activeTab, setActiveTab] = useState(defaultValue); return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className={className}>{children}</div> </TabsContext.Provider> ); } export function TabsList({ children, className }: { children: ReactNode; className?: string }) { return ( <div className={clsx( 'flex items-center gap-1 p-1 bg-neutral-bg2 rounded-lg', className )} > {children} </div> ); } interface TabsTriggerProps { value: string; children: ReactNode; className?: string; } export function TabsTrigger({ value, children, className }: TabsTriggerProps) { const context = useContext(TabsContext); if (!context) throw new Error('TabsTrigger must be used within Tabs'); const { activeTab, setActiveTab } = context; const isActive = activeTab === value; return ( <button onClick={() => setActiveTab(value)} className={clsx( 'relative px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150', isActive ? 'text-text-primary' : 'text-text-muted hover:text-text-secondary', className )} > {isActive && ( <motion.div layoutId="activeTab" className="absolute inset-0 bg-neutral-bg4 rounded-md" transition={{ type: 'spring', stiffness: 500, damping: 30 }} /> )} <span className="relative z-10">{children}</span> </button> ); } interface TabsContentProps { value: string; children: ReactNode; className?: string; } export function TabsContent({ value, children, className }: TabsContentProps) { const context = useContext(TabsContext); if (!context) throw new Error('TabsContent must be used within Tabs'); if (context.activeTab !== value) return null; return ( <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2 }} className={clsx('mt-4', className)} > {children} </motion.div> ); } ``` ### Tabs Usage ```tsx <Tabs defaultValue="overview"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="analytics">Analytics</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContent value="overview"> <p>Overview content</p> </TabsContent> <TabsContent value="analytics"> <p>Analytics content</p> </TabsContent> <TabsContent value="settings"> <p>Settings content</p> </TabsContent> </Tabs> ``` --- ## Avatar ```tsx import { clsx } from 'clsx'; import { useState, type ImgHTMLAttributes } from 'react'; type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; interface AvatarProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'size'> { size?: AvatarSize; fallback?: string; } const sizes: Record<AvatarSize, string> = { xs: 'w-6 h-6 text-xs', sm: 'w-8 h-8 text-sm', md: 'w-10 h-10 text-base', lg: 'w-12 h-12 text-lg', xl: 'w-16 h-16 text-xl', }; export function Avatar({ size = 'md', fallback, src, alt, className, ...props }: AvatarProps) { const [hasError, setHasError] = useState(false); const initials = fallback || alt?.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase() || '?'; if (!src || hasError) { return ( <div className={clsx( 'flex items-center justify-center rounded-full bg-brand-subtle text-brand font-medium', sizes[size], className )} > {initials} </div> ); } return ( <img src={src} alt={alt} onError={() => setHasError(true)} className={clsx('rounded-full object-cover', sizes[size], className)} {...props} /> ); } ``` --- ## Checkbox ```tsx import { motion } from 'framer-motion'; import { clsx } from 'clsx'; import { forwardRef, type InputHTMLAttributes } from 'react'; import { CheckIcon } from '@heroicons/react/20/solid'; interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> { label?: string; } export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( ({ label, className, id, ...props }, ref) => { const checkboxId = id || label?.toLowerCase().replace(/\s+/g, '-'); return ( <label className={clsx('flex items-center gap-2 cursor-pointer', className)}> <div className="relative"> <input ref={ref} type="checkbox" id={checkboxId} className="peer sr-only" {...props} /> <div className={clsx( 'w-5 h-5 rounded border-2 transition-colors duration-150', 'border-border bg-transparent', 'peer-checked:border-brand peer-checked:bg-brand', 'peer-focus-visible:ring-2 peer-focus-visible:ring-brand peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-neutral-bg1', 'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed' )} /> <motion.div initial={false} animate={{ scale: props.checked ? 1 : 0 }} className="absolute inset-0 flex items-center justify-center text-white pointer-events-none" > <CheckIcon className="w-3.5 h-3.5" /> </motion.div> </div> {label && <span className="text-sm text-text-secondary">{label}</span>} </label> ); } ); Checkbox.displayName = 'Checkbox'; ``` --- ## Select ```tsx import { clsx } from 'clsx'; import { forwardRef, type SelectHTMLAttributes } from 'react'; import { ChevronDownIcon } from '@heroicons/react/20/solid'; interface SelectOption { value: string; label: string; } interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> { label?: string; options: SelectOption[]; error?: string; placeholder?: string; } export const Select = forwardRef<HTMLSelectElement, SelectProps>( ({ label, options, error, placeholder, className, id, ...props }, ref) => { const selectId = id || label?.toLowerCase().replace(/\s+/g, '-'); return ( <div className="flex flex-col gap-1.5"> {label && ( <label htmlFor={selectId} className="text-sm font-medium text-text-secondary"> {label} </label> )} <div className="relative"> <select ref={ref} id={selectId} className={clsx( 'w-full h-10 px-3 pr-10 rounded-lg text-sm appearance-none', 'bg-neutral-bg2 border border-border text-text-primary', 'focus:outline-none focus:border-brand focus:bg-neutral-bg3', 'disabled:opacity-50 disabled:cursor-not-allowed', 'transition-colors duration-150', error && 'border-status-error focus:border-status-error', className )} {...props} > {placeholder && ( <option value="" disabled> {placeholder} </option> )} {options.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> <ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" /> </div> {error && <p className="text-xs text-status-error">{error}</p>} </div> ); } ); Select.displayName = 'Select'; ``` --- ## Toast ```tsx import { motion, AnimatePresence } from 'framer-motion'; import { clsx } from 'clsx'; import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; import { CheckCircleIcon, ExclamationCircleIcon, InformationCircleIcon, XCircleIcon, XMarkIcon } from '@heroicons/react/24/outline'; type ToastVariant = 'success' | 'error' | 'warning' | 'info'; interface Toast { id: string; variant: ToastVariant; title: string; description?: string; } interface ToastContextValue { toasts: Toast[]; addToast: (toast: Omit<Toast, 'id'>) => void; removeToast: (id: string) => void; } const ToastContext = createContext<ToastContextValue | null>(null); export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState<Toast[]>([]); const addToast = useCallback((toast: Omit<Toast, 'id'>) => { const id = Math.random().toString(36).slice(2); setToasts((prev) => [...prev, { ...toast, id }]); // Auto-remove after 5 seconds setTimeout(() => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, 5000); }, []); const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); return ( <ToastContext.Provider value={{ toasts, addToast, removeToast }}> {children} <ToastContainer /> </ToastContext.Provider> ); } export function useToast() { const context = useContext(ToastContext); if (!context) throw new Error('useToast must be used within ToastProvider'); return context; } const icons: Record<ToastVariant, typeof CheckCircleIcon> = { success: CheckCircleIcon, error: XCircleIcon, warning: ExclamationCircleIcon, info: InformationCircleIcon, }; const variants: Record<ToastVariant, string> = { success: 'text-status-success', error: 'text-status-error', warning: 'text-status-warning', info: 'text-status-info', }; function ToastContainer() { const { toasts, removeToast } = useToast(); return ( <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2"> <AnimatePresence> {toasts.map((toast) => { const Icon = icons[toast.variant]; return ( <motion.div key={toast.id} initial={{ opacity: 0, y: 20, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, x: 100, scale: 0.95 }} transition={{ duration: 0.2 }} className="glass-card p-4 min-w-[300px] max-w-[400px] flex items-start gap-3" > <Icon className={clsx('w-5 h-5 flex-shrink-0 mt-0.5', variants[toast.variant])} /> <div className="flex-1 min-w-0"> <p className="text-sm font-medium text-text-primary">{toast.title}</p> {toast.description && ( <p className="text-sm text-text-secondary mt-1">{toast.description}</p> )} </div> <button onClick={() => removeToast(toast.id)} className="text-text-muted hover:text-text-primary transition-colors" > <XMarkIcon className="w-4 h-4" /> </button> </motion.div> ); })} </AnimatePresence> </div> ); } ``` ### Toast Usage ```tsx // Wrap app with provider <ToastProvider> <App /> </ToastProvider> // Use in components const { addToast } = useToast(); addToast({ variant: 'success', title: 'Changes saved', description: 'Your settings have been updated.', }); addToast({ variant: 'error', title: 'Error', description: 'Something went wrong.', }); ``` --- ## Component Index Export all components from a central file: ```tsx // src/components/ui/index.ts export { Button } from './Button'; export { Input, SearchInput } from './Input'; export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; export { Badge } from './Badge'; export { Dialog, DialogFooter } from './Dialog'; export { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs'; export { Avatar } from './Avatar'; export { Checkbox } from './Checkbox'; export { Select } from './Select'; export { ToastProvider, useToast } from './Toast'; ``` -
design-tokens.md 13.9 KB
# Design Tokens Complete design token reference for the frontend-ui-dark-ts theme system. ## CSS Custom Properties Define these in your `globals.css` or a dedicated `tokens.css` file: ```css :root { /* ======================================== * BRAND COLORS * ======================================== */ --color-brand: #8251EE; --color-brand-hover: #9366F5; --color-brand-light: #A37EF5; --color-brand-subtle: rgba(130, 81, 238, 0.15); --color-brand-muted: rgba(130, 81, 238, 0.4); /* ======================================== * NEUTRAL BACKGROUNDS (HSL-based) * ======================================== */ --color-bg-1: hsl(240, 6%, 10%); /* #18181B - Base/page background */ --color-bg-2: hsl(240, 5%, 12%); /* #1F1F23 - Card background */ --color-bg-3: hsl(240, 5%, 14%); /* #242428 - Elevated surface */ --color-bg-4: hsl(240, 4%, 18%); /* #2D2D32 - Higher elevation */ --color-bg-5: hsl(240, 4%, 22%); /* #38383E - Hover states */ --color-bg-6: hsl(240, 4%, 26%); /* #43434A - Active states */ /* ======================================== * TEXT COLORS * ======================================== */ --color-text-primary: #FFFFFF; --color-text-secondary: #A1A1AA; --color-text-muted: #71717A; --color-text-disabled: #52525B; --color-text-inverse: #18181B; /* ======================================== * BORDER COLORS * ======================================== */ --color-border-subtle: hsla(0, 0%, 100%, 0.08); --color-border-default: hsla(0, 0%, 100%, 0.12); --color-border-strong: hsla(0, 0%, 100%, 0.20); --color-border-focus: var(--color-brand); /* ======================================== * STATUS COLORS * ======================================== */ --color-success: #10B981; --color-success-subtle: rgba(16, 185, 129, 0.15); --color-warning: #F59E0B; --color-warning-subtle: rgba(245, 158, 11, 0.15); --color-error: #EF4444; --color-error-subtle: rgba(239, 68, 68, 0.15); --color-info: #3B82F6; --color-info-subtle: rgba(59, 130, 246, 0.15); /* ======================================== * DATA VISUALIZATION PALETTE * ======================================== */ --color-dataviz-1: #8251EE; /* Purple (brand) */ --color-dataviz-2: #3B82F6; /* Blue */ --color-dataviz-3: #10B981; /* Green */ --color-dataviz-4: #F59E0B; /* Yellow */ --color-dataviz-5: #EF4444; /* Red */ --color-dataviz-6: #EC4899; /* Pink */ --color-dataviz-7: #06B6D4; /* Cyan */ /* ======================================== * GLASS EFFECT COLORS * ======================================== */ --glass-bg: rgba(255, 255, 255, 0.05); --glass-bg-hover: rgba(255, 255, 255, 0.08); --glass-border: rgba(255, 255, 255, 0.10); --glass-border-strong: rgba(255, 255, 255, 0.15); --glass-overlay: rgba(0, 0, 0, 0.60); --glass-panel: rgba(0, 0, 0, 0.40); /* ======================================== * SHADOWS * ======================================== */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3); --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.3); --shadow-glow: 0 0 20px rgba(130, 81, 238, 0.3); --shadow-glow-lg: 0 0 40px rgba(130, 81, 238, 0.4); /* ======================================== * SPACING SCALE * ======================================== */ --spacing-0: 0; --spacing-px: 1px; --spacing-0-5: 0.125rem; /* 2px */ --spacing-1: 0.25rem; /* 4px */ --spacing-1-5: 0.375rem; /* 6px */ --spacing-2: 0.5rem; /* 8px */ --spacing-2-5: 0.625rem; /* 10px */ --spacing-3: 0.75rem; /* 12px */ --spacing-3-5: 0.875rem; /* 14px */ --spacing-4: 1rem; /* 16px */ --spacing-5: 1.25rem; /* 20px */ --spacing-6: 1.5rem; /* 24px */ --spacing-7: 1.75rem; /* 28px */ --spacing-8: 2rem; /* 32px */ --spacing-9: 2.25rem; /* 36px */ --spacing-10: 2.5rem; /* 40px */ --spacing-12: 3rem; /* 48px */ --spacing-14: 3.5rem; /* 56px */ --spacing-16: 4rem; /* 64px */ --spacing-20: 5rem; /* 80px */ --spacing-24: 6rem; /* 96px */ /* ======================================== * BORDER RADIUS * ======================================== */ --radius-none: 0; --radius-sm: 0.25rem; /* 4px */ --radius-default: 0.375rem; /* 6px */ --radius-md: 0.5rem; /* 8px */ --radius-lg: 0.75rem; /* 12px */ --radius-xl: 1rem; /* 16px */ --radius-2xl: 1.5rem; /* 24px */ --radius-full: 9999px; /* ======================================== * TYPOGRAPHY * ======================================== */ --font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; --font-mono: 'Cascadia Code', 'Fira Code', monospace; /* Font sizes */ --text-xs: 0.75rem; /* 12px */ --text-sm: 0.875rem; /* 14px */ --text-base: 1rem; /* 16px */ --text-lg: 1.125rem; /* 18px */ --text-xl: 1.25rem; /* 20px */ --text-2xl: 1.5rem; /* 24px */ --text-3xl: 1.875rem; /* 30px */ --text-4xl: 2.25rem; /* 36px */ /* Line heights */ --leading-none: 1; --leading-tight: 1.25; --leading-snug: 1.375; --leading-normal: 1.5; --leading-relaxed: 1.625; --leading-loose: 2; /* Font weights */ --font-normal: 400; --font-medium: 500; --font-semibold: 600; --font-bold: 700; /* ======================================== * TRANSITIONS * ======================================== */ --transition-fast: 150ms ease; --transition-normal: 200ms ease; --transition-slow: 300ms ease; --transition-spring: 200ms cubic-bezier(0.34, 1.56, 0.64, 1); /* ======================================== * Z-INDEX SCALE * ======================================== */ --z-dropdown: 50; --z-sticky: 100; --z-modal: 200; --z-popover: 300; --z-tooltip: 400; --z-toast: 500; } ``` ## Tailwind Configuration Extend these tokens in `tailwind.config.js`: ```js /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: { fontFamily: { sans: ['Segoe UI', 'system-ui', '-apple-system', 'sans-serif'], mono: ['Cascadia Code', 'Fira Code', 'monospace'], }, colors: { brand: { DEFAULT: '#8251EE', hover: '#9366F5', light: '#A37EF5', subtle: 'rgba(130, 81, 238, 0.15)', muted: 'rgba(130, 81, 238, 0.4)', }, neutral: { bg1: 'hsl(240, 6%, 10%)', bg2: 'hsl(240, 5%, 12%)', bg3: 'hsl(240, 5%, 14%)', bg4: 'hsl(240, 4%, 18%)', bg5: 'hsl(240, 4%, 22%)', bg6: 'hsl(240, 4%, 26%)', }, text: { primary: '#FFFFFF', secondary: '#A1A1AA', muted: '#71717A', disabled: '#52525B', inverse: '#18181B', }, border: { subtle: 'hsla(0, 0%, 100%, 0.08)', DEFAULT: 'hsla(0, 0%, 100%, 0.12)', strong: 'hsla(0, 0%, 100%, 0.20)', }, status: { success: '#10B981', 'success-subtle': 'rgba(16, 185, 129, 0.15)', warning: '#F59E0B', 'warning-subtle': 'rgba(245, 158, 11, 0.15)', error: '#EF4444', 'error-subtle': 'rgba(239, 68, 68, 0.15)', info: '#3B82F6', 'info-subtle': 'rgba(59, 130, 246, 0.15)', }, dataviz: { purple: '#8251EE', blue: '#3B82F6', green: '#10B981', yellow: '#F59E0B', red: '#EF4444', pink: '#EC4899', cyan: '#06B6D4', }, glass: { bg: 'rgba(255, 255, 255, 0.05)', 'bg-hover': 'rgba(255, 255, 255, 0.08)', border: 'rgba(255, 255, 255, 0.10)', 'border-strong': 'rgba(255, 255, 255, 0.15)', overlay: 'rgba(0, 0, 0, 0.60)', panel: 'rgba(0, 0, 0, 0.40)', }, }, borderRadius: { DEFAULT: '0.375rem', lg: '0.75rem', xl: '1rem', '2xl': '1.5rem', }, boxShadow: { glow: '0 0 20px rgba(130, 81, 238, 0.3)', 'glow-lg': '0 0 40px rgba(130, 81, 238, 0.4)', }, backdropBlur: { xs: '2px', }, animation: { 'fade-in': 'fadeIn 0.3s ease-out', 'slide-up': 'slideUp 0.3s ease-out', 'slide-down': 'slideDown 0.3s ease-out', 'scale-in': 'scaleIn 0.2s ease-out', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideUp: { '0%': { opacity: '0', transform: 'translateY(10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' }, }, slideDown: { '0%': { opacity: '0', transform: 'translateY(-10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' }, }, scaleIn: { '0%': { opacity: '0', transform: 'scale(0.95)' }, '100%': { opacity: '1', transform: 'scale(1)' }, }, }, transitionTimingFunction: { spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, }, }, plugins: [], }; ``` ## Glass Effect Utilities Add these to your `globals.css` under `@layer components`: ```css @layer components { /* Basic glass effect */ .glass { @apply backdrop-blur-md bg-white/5 border border-white/10; } /* Glass card with rounded corners */ .glass-card { @apply backdrop-blur-md bg-white/5 border border-white/10 rounded-xl; } /* Glass card on hover */ .glass-card-hover { @apply backdrop-blur-md bg-white/5 border border-white/10 rounded-xl hover:bg-white/[0.08] hover:border-white/[0.15] transition-colors duration-200; } /* Darker glass panel (for sidebars, navigation) */ .glass-panel { @apply backdrop-blur-lg bg-black/40 border border-white/5; } /* Glass overlay for modals */ .glass-overlay { @apply backdrop-blur-sm bg-black/60; } /* Glass input field */ .glass-input { @apply backdrop-blur-sm bg-white/5 border border-white/10 placeholder:text-text-muted focus:border-brand focus:bg-white/[0.08] focus:outline-none transition-colors duration-200; } /* Glass button */ .glass-button { @apply backdrop-blur-sm bg-white/5 border border-white/10 hover:bg-white/[0.08] hover:border-white/[0.15] active:bg-white/[0.12] transition-colors duration-150; } /* Elevated glass (stronger blur, more prominent) */ .glass-elevated { @apply backdrop-blur-xl bg-white/[0.08] border border-white/[0.15] shadow-lg; } } ``` ## Typography Classes Add these utility classes for consistent typography: ```css @layer components { /* Headings */ .heading-1 { @apply text-3xl font-bold text-text-primary leading-tight; } .heading-2 { @apply text-2xl font-semibold text-text-primary leading-tight; } .heading-3 { @apply text-xl font-semibold text-text-primary leading-snug; } .heading-4 { @apply text-lg font-semibold text-text-primary leading-snug; } /* Body text */ .body-large { @apply text-base text-text-secondary leading-relaxed; } .body { @apply text-sm text-text-secondary leading-normal; } .body-small { @apply text-xs text-text-muted leading-normal; } /* Labels */ .label { @apply text-sm font-medium text-text-secondary; } .label-small { @apply text-xs font-medium text-text-muted uppercase tracking-wide; } /* Captions */ .caption { @apply text-xs text-text-muted; } } ``` ## Framer Motion Timing Recommended easing functions for Framer Motion: ```tsx // Timing presets export const timing = { fast: 0.15, normal: 0.2, slow: 0.3, verySlow: 0.5, }; // Easing presets export const easing = { // Standard ease for most animations default: [0.25, 0.1, 0.25, 1], // Ease out for entering elements easeOut: [0, 0, 0.2, 1], // Ease in for exiting elements easeIn: [0.4, 0, 1, 1], // Spring-like bounce spring: [0.34, 1.56, 0.64, 1], // Smooth deceleration decelerate: [0, 0.7, 0.3, 1], }; // Transition presets export const transitions = { fast: { duration: timing.fast, ease: easing.default }, normal: { duration: timing.normal, ease: easing.default }, slow: { duration: timing.slow, ease: easing.easeOut }, spring: { type: 'spring', stiffness: 400, damping: 17 }, springGentle: { type: 'spring', stiffness: 200, damping: 20 }, }; ``` ## Color Palette Summary ### Brand Colors | Name | Hex | Usage | |------|-----|-------| | Brand | `#8251EE` | Primary actions, links, focus rings | | Brand Hover | `#9366F5` | Hover state for brand elements | | Brand Light | `#A37EF5` | Lighter accent, selected states | | Brand Subtle | `rgba(130,81,238,0.15)` | Background for brand badges, tags | ### Neutral Backgrounds | Name | HSL | Hex | Usage | |------|-----|-----|-------| | bg1 | `hsl(240,6%,10%)` | `#18181B` | Page background | | bg2 | `hsl(240,5%,12%)` | `#1F1F23` | Card background | | bg3 | `hsl(240,5%,14%)` | `#242428` | Elevated surface, input bg | | bg4 | `hsl(240,4%,18%)` | `#2D2D32` | Higher elevation | | bg5 | `hsl(240,4%,22%)` | `#38383E` | Hover states | | bg6 | `hsl(240,4%,26%)` | `#43434A` | Active/pressed states | ### Text Colors | Name | Hex | Usage | |------|-----|-------| | Primary | `#FFFFFF` | Headings, important text | | Secondary | `#A1A1AA` | Body text, descriptions | | Muted | `#71717A` | Captions, placeholders | | Disabled | `#52525B` | Disabled text | ### Status Colors | Status | Color | Subtle | |--------|-------|--------| | Success | `#10B981` | `rgba(16,185,129,0.15)` | | Warning | `#F59E0B` | `rgba(245,158,11,0.15)` | | Error | `#EF4444` | `rgba(239,68,68,0.15)` | | Info | `#3B82F6` | `rgba(59,130,246,0.15)` | ### Data Visualization | Index | Color | Name | |-------|-------|------| | 1 | `#8251EE` | Purple | | 2 | `#3B82F6` | Blue | | 3 | `#10B981` | Green | | 4 | `#F59E0B` | Yellow | | 5 | `#EF4444` | Red | | 6 | `#EC4899` | Pink | | 7 | `#06B6D4` | Cyan | -
patterns.md 30.1 KB
# Patterns Page layouts, navigation patterns, and application templates for dark-themed React applications. ## App Shell The main application layout with sidebar navigation: ```tsx import { motion } from 'framer-motion'; import { type ReactNode } from 'react'; import { Sidebar } from './Sidebar'; interface AppShellProps { children: ReactNode; } export function AppShell({ children }: AppShellProps) { return ( <div className="flex min-h-screen bg-neutral-bg1"> <Sidebar /> <main className="flex-1 ml-64"> {children} </main> </div> ); } ``` --- ## Responsive App Shell Mobile-responsive layout: desktop shows fixed sidebar, mobile shows hamburger menu with slide-in drawer. **Breakpoint:** `lg` (1024px) — sidebar visible on desktop, hidden on mobile. ```tsx import { motion, AnimatePresence } from 'framer-motion'; import { type ReactNode, useState } from 'react'; import { Sidebar } from './Sidebar'; import { MobileHeader } from './MobileHeader'; import { MobileDrawer } from './MobileDrawer'; interface ResponsiveAppShellProps { children: ReactNode; } export function ResponsiveAppShell({ children }: ResponsiveAppShellProps) { const [drawerOpen, setDrawerOpen] = useState(false); return ( <div className="flex min-h-screen bg-neutral-bg1"> {/* Desktop sidebar - hidden on mobile */} <div className="hidden lg:block"> <Sidebar /> </div> {/* Mobile header - visible only on mobile */} <MobileHeader onMenuClick={() => setDrawerOpen(true)} /> {/* Mobile drawer */} <MobileDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} /> {/* Main content - adjusted margins for desktop sidebar and mobile header */} <main className="flex-1 lg:ml-64 pt-14 lg:pt-0 pb-safe-bottom"> {children} </main> </div> ); } ``` --- ## Mobile Header Fixed top header for mobile with hamburger menu toggle. Uses 44px minimum touch target. ```tsx import { Bars3Icon } from '@heroicons/react/24/outline'; interface MobileHeaderProps { onMenuClick: () => void; } export function MobileHeader({ onMenuClick }: MobileHeaderProps) { return ( <header className="fixed top-0 left-0 right-0 h-14 glass-panel flex items-center justify-between px-4 lg:hidden z-40"> {/* Logo */} <div className="flex items-center gap-2"> <img src="/logo.svg" alt="Logo" className="w-6 h-6" /> <span className="text-base font-semibold text-text-primary">AppName</span> </div> {/* Hamburger menu - 44px touch target */} <button onClick={onMenuClick} className="min-w-touch min-h-touch flex items-center justify-center rounded-lg hover:bg-white/5 active:bg-white/10 transition-colors" aria-label="Open menu" > <Bars3Icon className="w-6 h-6 text-text-primary" /> </button> </header> ); } ``` --- ## Mobile Drawer Animated slide-in navigation drawer for mobile. Includes backdrop and close button. ```tsx import { motion, AnimatePresence } from 'framer-motion'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { NavLink, useLocation } from 'react-router-dom'; import { clsx } from 'clsx'; import { HomeIcon, ChartBarIcon, Cog6ToothIcon, UsersIcon, FolderIcon, } from '@heroicons/react/24/outline'; const navItems = [ { path: '/', label: 'Dashboard', icon: HomeIcon }, { path: '/analytics', label: 'Analytics', icon: ChartBarIcon }, { path: '/projects', label: 'Projects', icon: FolderIcon }, { path: '/team', label: 'Team', icon: UsersIcon }, { path: '/settings', label: 'Settings', icon: Cog6ToothIcon }, ]; interface MobileDrawerProps { open: boolean; onClose: () => void; } export function MobileDrawer({ open, onClose }: MobileDrawerProps) { const location = useLocation(); return ( <AnimatePresence> {open && ( <> {/* Backdrop */} <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 lg:hidden" onClick={onClose} /> {/* Drawer */} <motion.aside initial={{ x: '-100%' }} animate={{ x: 0 }} exit={{ x: '-100%' }} transition={{ type: 'spring', stiffness: 400, damping: 30 }} className="fixed left-0 top-0 bottom-0 w-72 glass-panel p-4 flex flex-col z-50 lg:hidden pl-safe-left" > {/* Header with close button */} <div className="flex items-center justify-between mb-6"> <div className="flex items-center gap-2"> <img src="/logo.svg" alt="Logo" className="w-8 h-8" /> <span className="text-lg font-semibold text-text-primary">AppName</span> </div> <button onClick={onClose} className="min-w-touch min-h-touch flex items-center justify-center rounded-lg hover:bg-white/5 active:bg-white/10 transition-colors" aria-label="Close menu" > <XMarkIcon className="w-6 h-6 text-text-primary" /> </button> </div> {/* Navigation */} <nav className="flex-1 space-y-1"> {navItems.map((item) => { const isActive = location.pathname === item.path; const Icon = item.icon; return ( <NavLink key={item.path} to={item.path} onClick={onClose}> <motion.div className={clsx( 'relative flex items-center gap-3 px-3 py-3 rounded-lg', 'text-base font-medium transition-colors duration-150', 'min-h-touch', // 44px touch target isActive ? 'text-text-primary bg-brand/20 border border-brand/30' : 'text-text-muted hover:text-text-secondary hover:bg-white/5 active:bg-white/10' )} whileTap={{ scale: 0.98 }} > <Icon className={clsx('w-5 h-5', isActive && 'text-brand')} /> <span>{item.label}</span> </motion.div> </NavLink> ); })} </nav> {/* User profile at bottom */} <div className="mt-auto pt-4 border-t border-white/10 pb-safe-bottom"> <div className="flex items-center gap-3 px-3 py-2"> <div className="w-10 h-10 rounded-full bg-brand-subtle flex items-center justify-center"> <span className="text-sm font-medium text-brand">JD</span> </div> <div className="flex-1 min-w-0"> <p className="text-sm font-medium text-text-primary truncate">John Doe</p> <p className="text-xs text-text-muted truncate">john@example.com</p> </div> </div> </div> </motion.aside> </> )} </AnimatePresence> ); } ``` --- ## Sidebar Navigation Glass-effect sidebar with animated navigation items: ```tsx import { motion } from 'framer-motion'; import { NavLink, useLocation } from 'react-router-dom'; import { clsx } from 'clsx'; import { HomeIcon, ChartBarIcon, Cog6ToothIcon, UsersIcon, FolderIcon, } from '@heroicons/react/24/outline'; const navItems = [ { path: '/', label: 'Dashboard', icon: HomeIcon }, { path: '/analytics', label: 'Analytics', icon: ChartBarIcon }, { path: '/projects', label: 'Projects', icon: FolderIcon }, { path: '/team', label: 'Team', icon: UsersIcon }, { path: '/settings', label: 'Settings', icon: Cog6ToothIcon }, ]; export function Sidebar() { const location = useLocation(); return ( <aside className="fixed left-0 top-0 h-screen w-64 glass-panel p-4 flex flex-col"> {/* Logo */} <div className="flex items-center gap-3 px-3 py-4 mb-6"> <img src="/logo.svg" alt="Logo" className="w-8 h-8" /> <span className="text-lg font-semibold text-text-primary">AppName</span> </div> {/* Navigation */} <nav className="flex-1 space-y-1"> {navItems.map((item) => { const isActive = location.pathname === item.path; const Icon = item.icon; return ( <NavLink key={item.path} to={item.path}> <motion.div className={clsx( 'relative flex items-center gap-3 px-3 py-2.5 rounded-lg', 'text-sm font-medium transition-colors duration-150', isActive ? 'text-text-primary' : 'text-text-muted hover:text-text-secondary hover:bg-white/5' )} whileHover={{ x: 4 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }} > {isActive && ( <motion.div layoutId="activeNavItem" className="absolute inset-0 bg-brand/20 border border-brand/30 rounded-lg" transition={{ type: 'spring', stiffness: 500, damping: 30 }} /> )} <Icon className={clsx('w-5 h-5 relative z-10', isActive && 'text-brand')} /> <span className="relative z-10">{item.label}</span> </motion.div> </NavLink> ); })} </nav> {/* User profile at bottom */} <div className="mt-auto pt-4 border-t border-white/10"> <div className="flex items-center gap-3 px-3 py-2"> <div className="w-8 h-8 rounded-full bg-brand-subtle flex items-center justify-center"> <span className="text-sm font-medium text-brand">JD</span> </div> <div className="flex-1 min-w-0"> <p className="text-sm font-medium text-text-primary truncate">John Doe</p> <p className="text-xs text-text-muted truncate">john@example.com</p> </div> </div> </div> </aside> ); } ``` --- ## Page Header Reusable header component for pages: ```tsx import { motion } from 'framer-motion'; import { type ReactNode } from 'react'; interface PageHeaderProps { title: string; description?: string; actions?: ReactNode; } export function PageHeader({ title, description, actions }: PageHeaderProps) { return ( <motion.header initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} className="flex items-center justify-between mb-8" > <div> <h1 className="text-2xl font-semibold text-text-primary">{title}</h1> {description && ( <p className="text-sm text-text-secondary mt-1">{description}</p> )} </div> {actions && <div className="flex items-center gap-3">{actions}</div>} </motion.header> ); } ``` --- ## Page Transition Wrapper Animate page transitions with route changes: ```tsx import { motion } from 'framer-motion'; import { type ReactNode } from 'react'; interface PageTransitionProps { children: ReactNode; } const pageVariants = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, }; export function PageTransition({ children }: PageTransitionProps) { return ( <motion.div variants={pageVariants} initial="initial" animate="animate" exit="exit" transition={{ duration: 0.3, ease: 'easeOut' }} > {children} </motion.div> ); } ``` ### Usage with React Router ```tsx import { Routes, Route, useLocation } from 'react-router-dom'; import { AnimatePresence } from 'framer-motion'; function App() { const location = useLocation(); return ( <AppShell> <AnimatePresence mode="wait"> <Routes location={location} key={location.pathname}> <Route path="/" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </AnimatePresence> </AppShell> ); } ``` --- ## Basic Page Template Standard page layout with header and content area: ```tsx import { PageTransition } from '../components/layout/PageTransition'; import { PageHeader } from '../components/layout/PageHeader'; import { Button } from '../components/ui'; export function BasicPage() { return ( <PageTransition> <div className="p-8"> <PageHeader title="Page Title" description="A brief description of this page" actions={ <Button>Primary Action</Button> } /> <div className="space-y-6"> {/* Page content */} </div> </div> </PageTransition> ); } ``` --- ## List Page Template Page with search, filters, and data table: ```tsx import { useState } from 'react'; import { motion } from 'framer-motion'; import { PageTransition } from '../components/layout/PageTransition'; import { PageHeader } from '../components/layout/PageHeader'; import { Button, SearchInput, Card, Badge } from '../components/ui'; import { PlusIcon } from '@heroicons/react/20/solid'; interface Item { id: string; name: string; status: 'active' | 'inactive' | 'pending'; createdAt: string; } const staggerContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05 }, }, }; const staggerItem = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 }, }; export function ListPage() { const [search, setSearch] = useState(''); const [items] = useState<Item[]>([ { id: '1', name: 'Project Alpha', status: 'active', createdAt: '2024-01-15' }, { id: '2', name: 'Project Beta', status: 'pending', createdAt: '2024-01-14' }, { id: '3', name: 'Project Gamma', status: 'inactive', createdAt: '2024-01-13' }, ]); const filteredItems = items.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()) ); return ( <PageTransition> <div className="p-8"> <PageHeader title="Projects" description="Manage your projects and their settings" actions={ <Button leftIcon={<PlusIcon className="w-4 h-4" />}> New Project </Button> } /> {/* Filters */} <div className="flex items-center gap-4 mb-6"> <div className="w-72"> <SearchInput value={search} onChange={setSearch} placeholder="Search projects..." /> </div> </div> {/* Table */} <Card padding="none"> <table className="w-full"> <thead> <tr className="border-b border-border"> <th className="text-left text-xs font-medium text-text-muted uppercase tracking-wide px-4 py-3"> Name </th> <th className="text-left text-xs font-medium text-text-muted uppercase tracking-wide px-4 py-3"> Status </th> <th className="text-left text-xs font-medium text-text-muted uppercase tracking-wide px-4 py-3"> Created </th> </tr> </thead> <motion.tbody variants={staggerContainer} initial="hidden" animate="visible" > {filteredItems.map((item) => ( <motion.tr key={item.id} variants={staggerItem} className="border-b border-border last:border-0 hover:bg-neutral-bg3 transition-colors cursor-pointer" > <td className="px-4 py-3"> <span className="text-sm font-medium text-text-primary"> {item.name} </span> </td> <td className="px-4 py-3"> <Badge variant={ item.status === 'active' ? 'success' : item.status === 'pending' ? 'warning' : 'default' } > {item.status} </Badge> </td> <td className="px-4 py-3"> <span className="text-sm text-text-secondary"> {item.createdAt} </span> </td> </motion.tr> ))} </motion.tbody> </table> </Card> </div> </PageTransition> ); } ``` --- ## Tabs Page Template Page with tab navigation: ```tsx import { PageTransition } from '../components/layout/PageTransition'; import { PageHeader } from '../components/layout/PageHeader'; import { Tabs, TabsList, TabsTrigger, TabsContent, Card } from '../components/ui'; export function TabsPage() { return ( <PageTransition> <div className="p-8"> <PageHeader title="Settings" description="Manage your account and preferences" /> <Tabs defaultValue="general"> <TabsList> <TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="security">Security</TabsTrigger> <TabsTrigger value="notifications">Notifications</TabsTrigger> <TabsTrigger value="integrations">Integrations</TabsTrigger> </TabsList> <TabsContent value="general"> <Card> <h3 className="text-lg font-semibold text-text-primary mb-4"> General Settings </h3> {/* General settings form */} </Card> </TabsContent> <TabsContent value="security"> <Card> <h3 className="text-lg font-semibold text-text-primary mb-4"> Security Settings </h3> {/* Security settings form */} </Card> </TabsContent> <TabsContent value="notifications"> <Card> <h3 className="text-lg font-semibold text-text-primary mb-4"> Notification Preferences </h3> {/* Notification settings */} </Card> </TabsContent> <TabsContent value="integrations"> <Card> <h3 className="text-lg font-semibold text-text-primary mb-4"> Connected Integrations </h3> {/* Integrations list */} </Card> </TabsContent> </Tabs> </div> </PageTransition> ); } ``` --- ## Dashboard Template Dashboard with stat cards and data visualization: ```tsx import { motion } from 'framer-motion'; import { PageTransition } from '../components/layout/PageTransition'; import { PageHeader } from '../components/layout/PageHeader'; import { Card, CardTitle, CardContent } from '../components/ui'; import { ArrowUpIcon, ArrowDownIcon, UsersIcon, CurrencyDollarIcon, ChartBarIcon, ClockIcon, } from '@heroicons/react/24/outline'; const stats = [ { label: 'Total Revenue', value: '$45,231', change: '+12.5%', trend: 'up', icon: CurrencyDollarIcon }, { label: 'Active Users', value: '2,345', change: '+8.2%', trend: 'up', icon: UsersIcon }, { label: 'Conversion Rate', value: '3.2%', change: '-2.1%', trend: 'down', icon: ChartBarIcon }, { label: 'Avg. Session', value: '4m 32s', change: '+18.7%', trend: 'up', icon: ClockIcon }, ]; const staggerContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1 }, }, }; const staggerItem = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 }, }; export function Dashboard() { return ( <PageTransition> <div className="p-8"> <PageHeader title="Dashboard" description="Welcome back! Here's what's happening." /> {/* Stat Cards */} <motion.div variants={staggerContainer} initial="hidden" animate="visible" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8" > {stats.map((stat) => { const Icon = stat.icon; const TrendIcon = stat.trend === 'up' ? ArrowUpIcon : ArrowDownIcon; return ( <motion.div key={stat.label} variants={staggerItem}> <Card variant="glass" className="relative overflow-hidden"> <div className="flex items-start justify-between"> <div> <p className="text-sm text-text-muted">{stat.label}</p> <p className="text-2xl font-semibold text-text-primary mt-1"> {stat.value} </p> <div className="flex items-center gap-1 mt-2"> <TrendIcon className={`w-4 h-4 ${ stat.trend === 'up' ? 'text-status-success' : 'text-status-error' }`} /> <span className={`text-sm ${ stat.trend === 'up' ? 'text-status-success' : 'text-status-error' }`} > {stat.change} </span> <span className="text-xs text-text-muted">vs last month</span> </div> </div> <div className="p-2 rounded-lg bg-brand-subtle"> <Icon className="w-5 h-5 text-brand" /> </div> </div> </Card> </motion.div> ); })} </motion.div> {/* Main Content Grid */} <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> {/* Chart Card */} <Card variant="glass" className="lg:col-span-2"> <CardTitle>Revenue Overview</CardTitle> <CardContent className="h-80"> {/* Chart component would go here */} <div className="w-full h-full flex items-center justify-center text-text-muted"> Chart placeholder </div> </CardContent> </Card> {/* Recent Activity */} <Card variant="glass"> <CardTitle>Recent Activity</CardTitle> <CardContent> <ul className="space-y-4"> {[1, 2, 3, 4].map((i) => ( <li key={i} className="flex items-start gap-3"> <div className="w-8 h-8 rounded-full bg-brand-subtle flex items-center justify-center flex-shrink-0"> <UsersIcon className="w-4 h-4 text-brand" /> </div> <div> <p className="text-sm text-text-primary">New user signed up</p> <p className="text-xs text-text-muted">2 minutes ago</p> </div> </li> ))} </ul> </CardContent> </Card> </div> </div> </PageTransition> ); } ``` --- ## Stagger Animation Patterns Reusable animation variants for list items: ```tsx // variants.ts export const staggerContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05, delayChildren: 0.1, }, }, }; export const staggerItem = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0, transition: { duration: 0.2, ease: 'easeOut' }, }, }; export const fadeInScale = { hidden: { opacity: 0, scale: 0.95 }, visible: { opacity: 1, scale: 1, transition: { duration: 0.2, ease: 'easeOut' }, }, }; export const slideInFromRight = { hidden: { opacity: 0, x: 20 }, visible: { opacity: 1, x: 0, transition: { duration: 0.3, ease: 'easeOut' }, }, }; export const slideInFromLeft = { hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0, transition: { duration: 0.3, ease: 'easeOut' }, }, }; ``` ### Using Stagger Animations ```tsx import { motion } from 'framer-motion'; import { staggerContainer, staggerItem } from './variants'; function ItemList({ items }) { return ( <motion.ul variants={staggerContainer} initial="hidden" animate="visible" className="space-y-2" > {items.map((item) => ( <motion.li key={item.id} variants={staggerItem}> {item.name} </motion.li> ))} </motion.ul> ); } ``` --- ## Glass Panel Layouts ### Two-Column Layout with Glass Panels ```tsx export function TwoColumnLayout() { return ( <div className="flex gap-6 p-8"> {/* Main content */} <div className="flex-1"> <Card variant="glass"> <h2 className="text-lg font-semibold text-text-primary mb-4"> Main Content </h2> {/* Content */} </Card> </div> {/* Sidebar */} <aside className="w-80"> <Card variant="glass" className="sticky top-8"> <h3 className="text-sm font-medium text-text-muted uppercase tracking-wide mb-4"> Quick Actions </h3> {/* Sidebar content */} </Card> </aside> </div> ); } ``` ### Grid of Glass Cards ```tsx export function CardGrid({ items }) { return ( <motion.div variants={staggerContainer} initial="hidden" animate="visible" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" > {items.map((item) => ( <motion.div key={item.id} variants={staggerItem}> <Card variant="glass" interactive> <h3 className="text-base font-medium text-text-primary"> {item.title} </h3> <p className="text-sm text-text-secondary mt-2"> {item.description} </p> </Card> </motion.div> ))} </motion.div> ); } ``` --- ## Form Patterns ### Settings Form ```tsx import { Button, Input, Select, Checkbox, Card } from '../components/ui'; export function SettingsForm() { return ( <Card> <h2 className="text-lg font-semibold text-text-primary mb-6"> Account Settings </h2> <form className="space-y-6"> {/* Two-column grid for larger screens */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <Input label="First Name" placeholder="Enter your first name" /> <Input label="Last Name" placeholder="Enter your last name" /> </div> <Input label="Email Address" type="email" placeholder="you@example.com" /> <Select label="Timezone" placeholder="Select timezone" options={[ { value: 'utc', label: 'UTC' }, { value: 'pst', label: 'Pacific Time (PST)' }, { value: 'est', label: 'Eastern Time (EST)' }, ]} /> <div className="border-t border-border pt-6"> <h3 className="text-sm font-medium text-text-primary mb-4"> Notification Preferences </h3> <div className="space-y-3"> <Checkbox label="Email notifications" defaultChecked /> <Checkbox label="Push notifications" /> <Checkbox label="Weekly digest" defaultChecked /> </div> </div> <div className="flex justify-end gap-3 pt-4"> <Button variant="secondary">Cancel</Button> <Button>Save Changes</Button> </div> </form> </Card> ); } ``` --- ## Empty States ```tsx import { motion } from 'framer-motion'; import { FolderIcon } from '@heroicons/react/24/outline'; import { Button } from '../components/ui'; interface EmptyStateProps { icon?: typeof FolderIcon; title: string; description: string; action?: { label: string; onClick: () => void; }; } export function EmptyState({ icon: Icon = FolderIcon, title, description, action }: EmptyStateProps) { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="flex flex-col items-center justify-center py-16 px-4" > <div className="w-16 h-16 rounded-full bg-neutral-bg3 flex items-center justify-center mb-4"> <Icon className="w-8 h-8 text-text-muted" /> </div> <h3 className="text-lg font-medium text-text-primary mb-2">{title}</h3> <p className="text-sm text-text-secondary text-center max-w-sm mb-6"> {description} </p> {action && ( <Button onClick={action.onClick}>{action.label}</Button> )} </motion.div> ); } ``` ### Usage ```tsx <EmptyState title="No projects yet" description="Get started by creating your first project." action={{ label: 'Create Project', onClick: () => setShowCreateModal(true), }} /> ``` --- ## Loading States ### Skeleton Loader ```tsx import { clsx } from 'clsx'; interface SkeletonProps { className?: string; } export function Skeleton({ className }: SkeletonProps) { return ( <div className={clsx( 'animate-pulse bg-neutral-bg4 rounded', className )} /> ); } // Usage <Skeleton className="h-4 w-32" /> <Skeleton className="h-10 w-full" /> <Skeleton className="h-40 w-full rounded-xl" /> ``` ### Card Skeleton ```tsx export function CardSkeleton() { return ( <Card> <div className="space-y-4"> <Skeleton className="h-5 w-1/3" /> <Skeleton className="h-4 w-full" /> <Skeleton className="h-4 w-2/3" /> </div> </Card> ); } ``` ### Table Skeleton ```tsx export function TableSkeleton({ rows = 5 }: { rows?: number }) { return ( <Card padding="none"> <div className="divide-y divide-border"> {Array.from({ length: rows }).map((_, i) => ( <div key={i} className="flex items-center gap-4 p-4"> <Skeleton className="h-8 w-8 rounded-full" /> <Skeleton className="h-4 flex-1" /> <Skeleton className="h-4 w-20" /> <Skeleton className="h-4 w-24" /> </div> ))} </div> </Card> ); } ```
-
-
SKILL.md 14.4 KB
--- name: frontend-ui-dark-ts description: Build dark-themed React applications using Tailwind CSS with custom theming, glassmorphism effects, and Framer Motion animations. Use when creating dashboards, admin panels, or data-rich interfaces with a refined dark aesthetic. license: MIT metadata: author: Microsoft version: "1.0.0" --- # Frontend UI Dark Theme (TypeScript) A modern dark-themed React UI system using **Tailwind CSS** and **Framer Motion**. Designed for dashboards, admin panels, and data-rich applications with glassmorphism effects and tasteful animations. ## Stack | Package | Version | Purpose | |---------|---------|---------| | `react` | ^18.x | UI framework | | `react-dom` | ^18.x | DOM rendering | | `react-router-dom` | ^6.x | Routing | | `framer-motion` | ^11.x | Animations | | `clsx` | ^2.x | Class merging | | `tailwindcss` | ^3.x | Styling | | `vite` | ^5.x | Build tool | | `typescript` | ^5.x | Type safety | ## Quick Start ```bash npm create vite@latest my-app -- --template react-ts cd my-app npm install framer-motion clsx react-router-dom npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ## Project Structure ``` public/ ├── favicon.ico # Classic favicon (32x32) ├── favicon.svg # Modern SVG favicon ├── apple-touch-icon.png # iOS home screen (180x180) ├── og-image.png # Social sharing image (1200x630) └── site.webmanifest # PWA manifest src/ ├── assets/ │ └── fonts/ │ ├── Segoe UI.ttf │ ├── Segoe UI Bold.ttf │ ├── Segoe UI Italic.ttf │ └── Segoe UI Bold Italic.ttf ├── components/ │ ├── ui/ │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── Input.tsx │ │ ├── Badge.tsx │ │ ├── Dialog.tsx │ │ ├── Tabs.tsx │ │ └── index.ts │ └── layout/ │ ├── AppShell.tsx │ ├── Sidebar.tsx │ └── PageHeader.tsx ├── styles/ │ └── globals.css ├── App.tsx └── main.tsx ``` ## Configuration ### index.html The HTML entry point with mobile viewport, favicons, and social meta tags: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <!-- Favicons --> <link rel="icon" href="/favicon.ico" sizes="32x32" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="manifest" href="/site.webmanifest" /> <!-- Theme color for mobile browser chrome --> <meta name="theme-color" content="#18181B" /> <!-- Open Graph --> <meta property="og:type" content="website" /> <meta property="og:title" content="App Name" /> <meta property="og:description" content="App description" /> <meta property="og:image" content="https://example.com/og-image.png" /> <meta property="og:url" content="https://example.com" /> <!-- Twitter Card --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="App Name" /> <meta name="twitter:description" content="App description" /> <meta name="twitter:image" content="https://example.com/og-image.png" /> <title>App Name</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html> ``` ### public/site.webmanifest PWA manifest for installable web apps: ```json { "name": "App Name", "short_name": "App", "icons": [ { "src": "/favicon.ico", "sizes": "32x32", "type": "image/x-icon" }, { "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" } ], "theme_color": "#18181B", "background_color": "#18181B", "display": "standalone" } ``` ### tailwind.config.js ```js /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: { fontFamily: { sans: ['Segoe UI', 'system-ui', 'sans-serif'], }, colors: { brand: { DEFAULT: '#8251EE', hover: '#9366F5', light: '#A37EF5', subtle: 'rgba(130, 81, 238, 0.15)', }, neutral: { bg1: 'hsl(240, 6%, 10%)', bg2: 'hsl(240, 5%, 12%)', bg3: 'hsl(240, 5%, 14%)', bg4: 'hsl(240, 4%, 18%)', bg5: 'hsl(240, 4%, 22%)', bg6: 'hsl(240, 4%, 26%)', }, text: { primary: '#FFFFFF', secondary: '#A1A1AA', muted: '#71717A', }, border: { subtle: 'hsla(0, 0%, 100%, 0.08)', DEFAULT: 'hsla(0, 0%, 100%, 0.12)', strong: 'hsla(0, 0%, 100%, 0.20)', }, status: { success: '#10B981', warning: '#F59E0B', error: '#EF4444', info: '#3B82F6', }, dataviz: { purple: '#8251EE', blue: '#3B82F6', green: '#10B981', yellow: '#F59E0B', red: '#EF4444', pink: '#EC4899', cyan: '#06B6D4', }, }, borderRadius: { DEFAULT: '0.5rem', lg: '0.75rem', xl: '1rem', }, boxShadow: { glow: '0 0 20px rgba(130, 81, 238, 0.3)', 'glow-lg': '0 0 40px rgba(130, 81, 238, 0.4)', }, backdropBlur: { xs: '2px', }, animation: { 'fade-in': 'fadeIn 0.3s ease-out', 'slide-up': 'slideUp 0.3s ease-out', 'slide-down': 'slideDown 0.3s ease-out', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideUp: { '0%': { opacity: '0', transform: 'translateY(10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' }, }, slideDown: { '0%': { opacity: '0', transform: 'translateY(-10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' }, }, }, // Mobile: safe area insets for notched devices spacing: { 'safe-top': 'env(safe-area-inset-top)', 'safe-bottom': 'env(safe-area-inset-bottom)', 'safe-left': 'env(safe-area-inset-left)', 'safe-right': 'env(safe-area-inset-right)', }, // Mobile: minimum touch target sizes (44px per Apple/Google guidelines) minHeight: { 'touch': '44px', }, minWidth: { 'touch': '44px', }, }, }, plugins: [], }; ``` ### postcss.config.js ```js export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` ### src/styles/globals.css ```css @tailwind base; @tailwind components; @tailwind utilities; /* Font faces */ @font-face { font-family: 'Segoe UI'; src: url('../assets/fonts/Segoe UI.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'Segoe UI'; src: url('../assets/fonts/Segoe UI Bold.ttf') format('truetype'); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: 'Segoe UI'; src: url('../assets/fonts/Segoe UI Italic.ttf') format('truetype'); font-weight: 400; font-style: italic; font-display: swap; } @font-face { font-family: 'Segoe UI'; src: url('../assets/fonts/Segoe UI Bold Italic.ttf') format('truetype'); font-weight: 700; font-style: italic; font-display: swap; } /* CSS Custom Properties */ :root { /* Brand colors */ --color-brand: #8251EE; --color-brand-hover: #9366F5; --color-brand-light: #A37EF5; --color-brand-subtle: rgba(130, 81, 238, 0.15); /* Neutral backgrounds */ --color-bg-1: hsl(240, 6%, 10%); --color-bg-2: hsl(240, 5%, 12%); --color-bg-3: hsl(240, 5%, 14%); --color-bg-4: hsl(240, 4%, 18%); --color-bg-5: hsl(240, 4%, 22%); --color-bg-6: hsl(240, 4%, 26%); /* Text colors */ --color-text-primary: #FFFFFF; --color-text-secondary: #A1A1AA; --color-text-muted: #71717A; /* Border colors */ --color-border-subtle: hsla(0, 0%, 100%, 0.08); --color-border-default: hsla(0, 0%, 100%, 0.12); --color-border-strong: hsla(0, 0%, 100%, 0.20); /* Status colors */ --color-success: #10B981; --color-warning: #F59E0B; --color-error: #EF4444; --color-info: #3B82F6; /* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; --spacing-2xl: 3rem; /* Border radius */ --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; /* Transitions */ --transition-fast: 150ms ease; --transition-normal: 200ms ease; --transition-slow: 300ms ease; } /* Base styles */ html { color-scheme: dark; } body { @apply bg-neutral-bg1 text-text-primary font-sans antialiased; min-height: 100vh; } /* Focus styles */ *:focus-visible { @apply outline-none ring-2 ring-brand ring-offset-2 ring-offset-neutral-bg1; } /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { @apply bg-neutral-bg2; } ::-webkit-scrollbar-thumb { @apply bg-neutral-bg5 rounded-full; } ::-webkit-scrollbar-thumb:hover { @apply bg-neutral-bg6; } /* Glass utility classes */ @layer components { .glass { @apply backdrop-blur-md bg-white/5 border border-white/10; } .glass-card { @apply backdrop-blur-md bg-white/5 border border-white/10 rounded-xl; } .glass-panel { @apply backdrop-blur-lg bg-black/40 border border-white/5; } .glass-overlay { @apply backdrop-blur-sm bg-black/60; } .glass-input { @apply backdrop-blur-sm bg-white/5 border border-white/10 focus:border-brand focus:bg-white/10; } } /* Animation utilities */ @layer utilities { .animate-in { animation: fadeIn 0.3s ease-out, slideUp 0.3s ease-out; } } ``` ### src/main.tsx ```tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './styles/globals.css'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> ); ``` ### src/App.tsx ```tsx import { Routes, Route } from 'react-router-dom'; import { AnimatePresence } from 'framer-motion'; import { AppShell } from './components/layout/AppShell'; import { Dashboard } from './pages/Dashboard'; import { Settings } from './pages/Settings'; export default function App() { return ( <AppShell> <AnimatePresence mode="wait"> <Routes> <Route path="/" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </AnimatePresence> </AppShell> ); } ``` ## Animation Patterns ### Framer Motion Variants ```tsx // Fade in on mount export const fadeIn = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, }; // Slide up on mount export const slideUp = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 }, transition: { duration: 0.3, ease: 'easeOut' }, }; // Scale on hover (for buttons/cards) export const scaleOnHover = { whileHover: { scale: 1.02 }, whileTap: { scale: 0.98 }, transition: { type: 'spring', stiffness: 400, damping: 17 }, }; // Stagger children export const staggerContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05, delayChildren: 0.1, }, }, }; export const staggerItem = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0, transition: { duration: 0.2, ease: 'easeOut' }, }, }; ``` ### Page Transition Wrapper ```tsx import { motion } from 'framer-motion'; import { ReactNode } from 'react'; interface PageTransitionProps { children: ReactNode; } export function PageTransition({ children }: PageTransitionProps) { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3, ease: 'easeOut' }} > {children} </motion.div> ); } ``` ## Glass Effect Patterns ### Glass Card ```tsx <div className="glass-card p-6"> <h2 className="text-lg font-semibold text-text-primary">Card Title</h2> <p className="text-text-secondary mt-2">Card content goes here.</p> </div> ``` ### Glass Panel (Sidebar) ```tsx <aside className="glass-panel w-64 h-screen p-4"> <nav className="space-y-2"> {/* Navigation items */} </nav> </aside> ``` ### Glass Modal Overlay ```tsx <motion.div className="fixed inset-0 glass-overlay flex items-center justify-center z-50" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > <motion.div className="glass-card p-6 max-w-md w-full mx-4" initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} > {/* Modal content */} </motion.div> </motion.div> ``` ## Typography | Element | Classes | |---------|---------| | Page title | `text-2xl font-semibold text-text-primary` | | Section title | `text-lg font-semibold text-text-primary` | | Card title | `text-base font-medium text-text-primary` | | Body text | `text-sm text-text-secondary` | | Caption | `text-xs text-text-muted` | | Label | `text-sm font-medium text-text-secondary` | ## Color Usage | Use Case | Color | Class | |----------|-------|-------| | Primary action | Brand purple | `bg-brand text-white` | | Primary hover | Brand hover | `hover:bg-brand-hover` | | Page background | Neutral bg1 | `bg-neutral-bg1` | | Card background | Neutral bg2 | `bg-neutral-bg2` | | Elevated surface | Neutral bg3 | `bg-neutral-bg3` | | Input background | Neutral bg2 | `bg-neutral-bg2` | | Input focus | Neutral bg3 | `focus:bg-neutral-bg3` | | Border default | Border default | `border-border` | | Border subtle | Border subtle | `border-border-subtle` | | Success | Status success | `text-status-success` | | Warning | Status warning | `text-status-warning` | | Error | Status error | `text-status-error` | ## Related Files - [Design Tokens](./references/design-tokens.md) — Complete color system, spacing, typography scales - [Components](./references/components.md) — Button, Card, Input, Dialog, Tabs, and more - [Patterns](./references/patterns.md) — Page layouts, navigation, lists, forms
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.