repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Projectflow
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/projectflowgit clone gitlawb://did:key:z6Mkfh4Y.../projectflowb3cded1async from playground23h ago| #1 | import { useEffect, useRef, type ReactNode } from "react"; |
| #2 | import { motion, AnimatePresence } from "framer-motion"; |
| #3 | import { X } from "lucide-react"; |
| #4 | import clsx from "clsx"; |
| #5 | |
| #6 | // Avatar |
| #7 | export function Avatar({ |
| #8 | name, |
| #9 | size = "md", |
| #10 | className, |
| #11 | }: { |
| #12 | name: string; |
| #13 | size?: "xs" | "sm" | "md" | "lg"; |
| #14 | className?: string; |
| #15 | }) { |
| #16 | const initials = name |
| #17 | .split(" ") |
| #18 | .map((n) => n[0]) |
| #19 | .join("") |
| #20 | .toUpperCase() |
| #21 | .slice(0, 2); |
| #22 | |
| #23 | const colors = [ |
| #24 | "bg-indigo-500", |
| #25 | "bg-emerald-500", |
| #26 | "bg-amber-500", |
| #27 | "bg-rose-500", |
| #28 | "bg-cyan-500", |
| #29 | "bg-violet-500", |
| #30 | "bg-pink-500", |
| #31 | "bg-teal-500", |
| #32 | ]; |
| #33 | |
| #34 | const colorIndex = |
| #35 | name.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) % |
| #36 | colors.length; |
| #37 | |
| #38 | const sizes = { |
| #39 | xs: "w-5 h-5 text-[9px]", |
| #40 | sm: "w-6 h-6 text-[10px]", |
| #41 | md: "w-8 h-8 text-xs", |
| #42 | lg: "w-10 h-10 text-sm", |
| #43 | }; |
| #44 | |
| #45 | return ( |
| #46 | <div |
| #47 | className={clsx( |
| #48 | "rounded-full flex items-center justify-center font-medium text-white shrink-0", |
| #49 | colors[colorIndex], |
| #50 | sizes[size], |
| #51 | className |
| #52 | )} |
| #53 | title={name} |
| #54 | > |
| #55 | {initials} |
| #56 | </div> |
| #57 | ); |
| #58 | } |
| #59 | |
| #60 | // Modal |
| #61 | export function Modal({ |
| #62 | open, |
| #63 | onClose, |
| #64 | title, |
| #65 | children, |
| #66 | size = "md", |
| #67 | }: { |
| #68 | open: boolean; |
| #69 | onClose: () => void; |
| #70 | title: string; |
| #71 | children: ReactNode; |
| #72 | size?: "sm" | "md" | "lg" | "xl"; |
| #73 | }) { |
| #74 | const ref = useRef<HTMLDivElement>(null); |
| #75 | |
| #76 | useEffect(() => { |
| #77 | if (!open) return; |
| #78 | const handleKey = (e: KeyboardEvent) => { |
| #79 | if (e.key === "Escape") onClose(); |
| #80 | }; |
| #81 | window.addEventListener("keydown", handleKey); |
| #82 | return () => window.removeEventListener("keydown", handleKey); |
| #83 | }, [open, onClose]); |
| #84 | |
| #85 | useEffect(() => { |
| #86 | if (open) { |
| #87 | document.body.style.overflow = "hidden"; |
| #88 | } else { |
| #89 | document.body.style.overflow = ""; |
| #90 | } |
| #91 | return () => { |
| #92 | document.body.style.overflow = ""; |
| #93 | }; |
| #94 | }, [open]); |
| #95 | |
| #96 | const sizes = { |
| #97 | sm: "max-w-sm", |
| #98 | md: "max-w-lg", |
| #99 | lg: "max-w-2xl", |
| #100 | xl: "max-w-4xl", |
| #101 | }; |
| #102 | |
| #103 | return ( |
| #104 | <AnimatePresence> |
| #105 | {open && ( |
| #106 | <div className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"> |
| #107 | <motion.div |
| #108 | initial={{ opacity: 0 }} |
| #109 | animate={{ opacity: 1 }} |
| #110 | exit={{ opacity: 0 }} |
| #111 | transition={{ duration: 0.15 }} |
| #112 | className="absolute inset-0 bg-black/60 backdrop-blur-sm" |
| #113 | onClick={onClose} |
| #114 | /> |
| #115 | <motion.div |
| #116 | ref={ref} |
| #117 | initial={{ opacity: 0, scale: 0.95, y: -10 }} |
| #118 | animate={{ opacity: 1, scale: 1, y: 0 }} |
| #119 | exit={{ opacity: 0, scale: 0.95, y: -10 }} |
| #120 | transition={{ duration: 0.15 }} |
| #121 | className={clsx( |
| #122 | "relative w-full rounded-xl border border-border bg-surface-1 shadow-2xl shadow-black/50", |
| #123 | sizes[size] |
| #124 | )} |
| #125 | > |
| #126 | <div className="flex items-center justify-between px-5 py-4 border-b border-border"> |
| #127 | <h2 className="text-sm font-semibold text-zinc-100"> |
| #128 | {title} |
| #129 | </h2> |
| #130 | <button |
| #131 | onClick={onClose} |
| #132 | className="p-1 rounded-md text-zinc-400 hover:text-zinc-200 hover:bg-surface-3 transition-colors" |
| #133 | > |
| #134 | <X size={16} /> |
| #135 | </button> |
| #136 | </div> |
| #137 | <div className="p-5">{children}</div> |
| #138 | </motion.div> |
| #139 | </div> |
| #140 | )} |
| #141 | </AnimatePresence> |
| #142 | ); |
| #143 | } |
| #144 | |
| #145 | // Badge variants |
| #146 | export function StatusBadge({ status }: { status: string }) { |
| #147 | const map: Record<string, string> = { |
| #148 | backlog: "status-backlog", |
| #149 | todo: "status-todo", |
| #150 | "in-progress": "status-in-progress", |
| #151 | "in-review": "status-in-review", |
| #152 | done: "status-done", |
| #153 | cancelled: "status-cancelled", |
| #154 | }; |
| #155 | |
| #156 | const labelMap: Record<string, string> = { |
| #157 | backlog: "Backlog", |
| #158 | todo: "Todo", |
| #159 | "in-progress": "In Progress", |
| #160 | "in-review": "In Review", |
| #161 | done: "Done", |
| #162 | cancelled: "Cancelled", |
| #163 | }; |
| #164 | |
| #165 | return ( |
| #166 | <span className={clsx("badge", map[status] || "status-backlog")}> |
| #167 | {labelMap[status] || status} |
| #168 | </span> |
| #169 | ); |
| #170 | } |
| #171 | |
| #172 | export function PriorityBadge({ priority }: { priority: string }) { |
| #173 | const map: Record<string, string> = { |
| #174 | urgent: "priority-urgent", |
| #175 | high: "priority-high", |
| #176 | medium: "priority-medium", |
| #177 | low: "priority-low", |
| #178 | none: "priority-none", |
| #179 | }; |
| #180 | |
| #181 | return ( |
| #182 | <span className={clsx("badge capitalize", map[priority] || "priority-none")}> |
| #183 | {priority} |
| #184 | </span> |
| #185 | ); |
| #186 | } |
| #187 | |
| #188 | export function IssueTypeBadge({ type }: { type: string }) { |
| #189 | const icons: Record<string, string> = { |
| #190 | task: "○", |
| #191 | bug: "●", |
| #192 | feature: "△", |
| #193 | }; |
| #194 | |
| #195 | const colors: Record<string, string> = { |
| #196 | task: "text-zinc-400", |
| #197 | bug: "text-red-400", |
| #198 | feature: "text-indigo-400", |
| #199 | }; |
| #200 | |
| #201 | return ( |
| #202 | <span className={clsx("text-sm", colors[type] || "text-zinc-400")}> |
| #203 | {icons[type] || "○"} |
| #204 | </span> |
| #205 | ); |
| #206 | } |
| #207 | |
| #208 | // Dropdown Menu |
| #209 | export function Dropdown({ |
| #210 | trigger, |
| #211 | children, |
| #212 | align = "left", |
| #213 | }: { |
| #214 | trigger: ReactNode; |
| #215 | children: ReactNode; |
| #216 | align?: "left" | "right"; |
| #217 | }) { |
| #218 | return ( |
| #219 | <div className="relative group"> |
| #220 | {trigger} |
| #221 | <div |
| #222 | className={clsx( |
| #223 | "absolute top-full mt-1 z-40 min-w-[180px] rounded-lg border border-border bg-surface-2 shadow-xl shadow-black/30 py-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-100", |
| #224 | align === "right" ? "right-0" : "left-0" |
| #225 | )} |
| #226 | > |
| #227 | {children} |
| #228 | </div> |
| #229 | </div> |
| #230 | ); |
| #231 | } |
| #232 | |
| #233 | export function DropdownItem({ |
| #234 | onClick, |
| #235 | children, |
| #236 | danger, |
| #237 | }: { |
| #238 | onClick: () => void; |
| #239 | children: ReactNode; |
| #240 | danger?: boolean; |
| #241 | }) { |
| #242 | return ( |
| #243 | <button |
| #244 | onClick={onClick} |
| #245 | className={clsx( |
| #246 | "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2", |
| #247 | danger |
| #248 | ? "text-red-400 hover:bg-red-500/10" |
| #249 | : "text-zinc-300 hover:bg-surface-3 hover:text-zinc-100" |
| #250 | )} |
| #251 | > |
| #252 | {children} |
| #253 | </button> |
| #254 | ); |
| #255 | } |
| #256 | |
| #257 | // Empty State |
| #258 | export function EmptyState({ |
| #259 | icon, |
| #260 | title, |
| #261 | description, |
| #262 | action, |
| #263 | }: { |
| #264 | icon?: ReactNode; |
| #265 | title: string; |
| #266 | description?: string; |
| #267 | action?: ReactNode; |
| #268 | }) { |
| #269 | return ( |
| #270 | <div className="flex flex-col items-center justify-center py-16 text-center"> |
| #271 | {icon && ( |
| #272 | <div className="mb-4 p-3 rounded-xl bg-surface-3 text-zinc-500"> |
| #273 | {icon} |
| #274 | </div> |
| #275 | )} |
| #276 | <h3 className="text-sm font-medium text-zinc-300 mb-1">{title}</h3> |
| #277 | {description && ( |
| #278 | <p className="text-sm text-zinc-500 max-w-sm mb-4">{description}</p> |
| #279 | )} |
| #280 | {action} |
| #281 | </div> |
| #282 | ); |
| #283 | } |
| #284 | |
| #285 | // Tooltip |
| #286 | export function Tooltip({ |
| #287 | content, |
| #288 | children, |
| #289 | }: { |
| #290 | content: string; |
| #291 | children: ReactNode; |
| #292 | }) { |
| #293 | return ( |
| #294 | <div className="relative group/tooltip inline-flex"> |
| #295 | {children} |
| #296 | <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1.5 rounded-md bg-zinc-800 border border-zinc-700 text-xs text-zinc-200 whitespace-nowrap opacity-0 invisible group-hover/tooltip:opacity-100 group-hover/tooltip:visible transition-all duration-100 pointer-events-none z-50"> |
| #297 | {content} |
| #298 | <div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" /> |
| #299 | </div> |
| #300 | </div> |
| #301 | ); |
| #302 | } |
| #303 | |
| #304 | // Progress Bar |
| #305 | export function ProgressBar({ |
| #306 | value, |
| #307 | max, |
| #308 | color = "bg-accent", |
| #309 | size = "md", |
| #310 | }: { |
| #311 | value: number; |
| #312 | max: number; |
| #313 | color?: string; |
| #314 | size?: "sm" | "md"; |
| #315 | }) { |
| #316 | const pct = max > 0 ? (value / max) * 100 : 0; |
| #317 | return ( |
| #318 | <div |
| #319 | className={clsx( |
| #320 | "w-full rounded-full bg-surface-3 overflow-hidden", |
| #321 | size === "sm" ? "h-1.5" : "h-2" |
| #322 | )} |
| #323 | > |
| #324 | <motion.div |
| #325 | initial={{ width: 0 }} |
| #326 | animate={{ width: `${pct}%` }} |
| #327 | transition={{ duration: 0.5, ease: "easeOut" }} |
| #328 | className={clsx("h-full rounded-full", color)} |
| #329 | /> |
| #330 | </div> |
| #331 | ); |
| #332 | } |
| #333 | |
| #334 | // Tabs |
| #335 | export function Tabs({ |
| #336 | tabs, |
| #337 | active, |
| #338 | onChange, |
| #339 | }: { |
| #340 | tabs: { id: string; label: string; count?: number }[]; |
| #341 | active: string; |
| #342 | onChange: (id: string) => void; |
| #343 | }) { |
| #344 | return ( |
| #345 | <div className="flex items-center gap-1 border-b border-border"> |
| #346 | {tabs.map((tab) => ( |
| #347 | <button |
| #348 | key={tab.id} |
| #349 | onClick={() => onChange(tab.id)} |
| #350 | className={clsx( |
| #351 | "px-3 py-2.5 text-sm font-medium transition-colors relative", |
| #352 | active === tab.id |
| #353 | ? "text-zinc-100" |
| #354 | : "text-zinc-500 hover:text-zinc-300" |
| #355 | )} |
| #356 | > |
| #357 | <span className="flex items-center gap-2"> |
| #358 | {tab.label} |
| #359 | {tab.count !== undefined && ( |
| #360 | <span className="text-2xs text-zinc-500">{tab.count}</span> |
| #361 | )} |
| #362 | </span> |
| #363 | {active === tab.id && ( |
| #364 | <motion.div |
| #365 | layoutId="tab-indicator" |
| #366 | className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent rounded-full" |
| #367 | /> |
| #368 | )} |
| #369 | </button> |
| #370 | ))} |
| #371 | </div> |
| #372 | ); |
| #373 | } |
| #374 | |
| #375 | // Select |
| #376 | export function Select({ |
| #377 | value, |
| #378 | onChange, |
| #379 | options, |
| #380 | placeholder, |
| #381 | className, |
| #382 | }: { |
| #383 | value: string; |
| #384 | onChange: (value: string) => void; |
| #385 | options: { value: string; label: string }[]; |
| #386 | placeholder?: string; |
| #387 | className?: string; |
| #388 | }) { |
| #389 | return ( |
| #390 | <select |
| #391 | value={value} |
| #392 | onChange={(e) => onChange(e.target.value)} |
| #393 | className={clsx("input appearance-none cursor-pointer", className)} |
| #394 | > |
| #395 | {placeholder && ( |
| #396 | <option value="" className="bg-surface-2"> |
| #397 | {placeholder} |
| #398 | </option> |
| #399 | )} |
| #400 | {options.map((opt) => ( |
| #401 | <option key={opt.value} value={opt.value} className="bg-surface-2"> |
| #402 | {opt.label} |
| #403 | </option> |
| #404 | ))} |
| #405 | </select> |
| #406 | ); |
| #407 | } |
| #408 |