🎨
UI Knihovna
Kolekce nádherných, znovupoužitelných React komponent a CSS fragmentů vytvořená pomocí Tailwind CSS a Framer Motion.
Místní nabídky
Kontextová menu pro zobrazení seznamů akcí nebo odkazů.
Basic Action Menu
DropdownBasic.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export function BasicDropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-4 py-2 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none"
>
<span className="font-medium text-sm">Options</span>
<svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-2 w-48 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20"
>
<div className="py-1">
<a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Edit Item</a>
<a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Duplicate</a>
<a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Archive</a>
</div>
<div className="py-1">
<a href="#" className="block px-4 py-2.5 text-sm font-medium text-red-500 hover:bg-red-500/10 transition-colors">Delete Permanently</a>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}Grouped Options
DropdownGrouped.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export function GroupedDropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// ... same useEffect for click outside ...
return (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-4 py-2 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none"
>
<span className="font-medium text-sm">Create New</span>
<svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-2 w-56 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20"
>
<div className="py-2">
<span className="block px-4 py-1 text-xs font-semibold text-muted uppercase tracking-wider">Document</span>
<a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Blank Document</a>
<a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">From Template</a>
</div>
<div className="py-2">
<span className="block px-4 py-1 text-xs font-semibold text-muted uppercase tracking-wider">Project</span>
<a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">New Workspace</a>
<a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Import Repository</a>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}Multi-Select Menus
DropdownMultiSelect.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export function MultiSelectDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>(['Option 1']);
const dropdownRef = useRef<HTMLDivElement>(null);
// ... same useEffect for click outside ...
const toggleSelection = (option: string) => {
if (selectedItems.includes(option)) {
setSelectedItems(selectedItems.filter(item => item !== option));
} else {
setSelectedItems([...selectedItems, option]);
}
};
const options = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];
return (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between min-w-[200px] gap-2 px-4 py-2.5 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none"
>
<span className="font-medium text-sm text-muted">
{selectedItems.length ? `${selectedItems.length} Selected` : 'Select items...'}
</span>
<svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute left-0 mt-2 w-full min-w-[200px] origin-top-left bg-popover border border-border shadow-2xl rounded-xl overflow-hidden z-20 py-1"
>
{options.map((option) => (
<label key={option} className="flex items-center gap-3 px-4 py-2.5 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<div className={`flex items-center justify-center w-4 h-4 rounded border transition-colors ${selectedItems.includes(option) ? 'bg-accent border-accent text-white' : 'border-border bg-transparent'}`}>
{selectedItems.includes(option) && (
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm font-medium text-foreground">{option}</span>
</label>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}User Profile Menu
DropdownProfile.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export function UserProfileDropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// ... same useEffect for click outside ...
return (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-3 px-2 py-2 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none"
>
<div className="w-9 h-9 rounded-full bg-gradient-to-tr from-accent to-purple-500 flex items-center justify-center text-white font-bold text-sm">
DW
</div>
<svg className={`w-4 h-4 text-muted transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-2 w-64 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20"
>
<div className="px-4 py-3 flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-tr from-accent to-purple-500 flex items-center justify-center text-white font-bold text-sm shrink-0">
DW
</div>
<div className="overflow-hidden">
<p className="text-sm font-semibold text-foreground truncate">Dowy Designer</p>
<p className="text-xs text-muted truncate">[email protected]</p>
</div>
</div>
<div className="py-1">
<a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<svg className="w-4 h-4 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
View Profile
</a>
<a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<svg className="w-4 h-4 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37... (truncated visually)" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
Settings
</a>
</div>
<div className="py-1">
<a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-red-500 hover:bg-red-500/10 transition-colors">
<svg className="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
Sign out
</a>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}