Created
December 14, 2025 13:31
-
-
Save celsowm/65f2e55a0386519224c3aecedbb0df29 to your computer and use it in GitHub Desktop.
glassmorph.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState } from "react"; | |
| import { ChevronLeft, ChevronRight, Pencil, Trash2 } from "lucide-react"; | |
| import { AnimatePresence, motion } from "framer-motion"; | |
| type Status = "Online" | "Ausente" | "Ocupado" | "Offline"; | |
| type Person = { | |
| name: string; | |
| email: string; | |
| role: string; | |
| status: Status; | |
| lastActive: string; | |
| }; | |
| const PEOPLE: Person[] = [ | |
| { name: "Ana Souza", email: "ana.souza@exemplo.com", role: "Product Designer", status: "Online", lastActive: "Agora" }, | |
| { name: "Bruno Almeida", email: "bruno.almeida@exemplo.com", role: "Frontend Engineer", status: "Online", lastActive: "Há 5 min" }, | |
| { name: "Carla Mendes", email: "carla.mendes@exemplo.com", role: "Data Analyst", status: "Ausente", lastActive: "Há 22 min" }, | |
| { name: "Diego Lima", email: "diego.lima@exemplo.com", role: "Backend Engineer", status: "Ocupado", lastActive: "Há 1h" }, | |
| { name: "Eduarda Ramos", email: "eduarda.ramos@exemplo.com", role: "QA Engineer", status: "Offline", lastActive: "Hoje" }, | |
| { name: "Felipe Costa", email: "felipe.costa@exemplo.com", role: "DevOps", status: "Online", lastActive: "Hoje" }, | |
| { name: "Gabriela Nunes", email: "gabriela.nunes@exemplo.com", role: "Product Manager", status: "Ausente", lastActive: "Ontem" }, | |
| { name: "Henrique Silva", email: "henrique.silva@exemplo.com", role: "Mobile Engineer", status: "Ocupado", lastActive: "Há 2 dias" }, | |
| { name: "Isabela Ferreira", email: "isabela.ferreira@exemplo.com", role: "Support", status: "Offline", lastActive: "Há 1 semana" }, | |
| { name: "João Pereira", email: "joao.pereira@exemplo.com", role: "Tech Lead", status: "Online", lastActive: "Há 3 semanas" }, | |
| { name: "Karina Rocha", email: "karina.rocha@exemplo.com", role: "UX Researcher", status: "Ausente", lastActive: "Há 3h" }, | |
| { name: "Lucas Martins", email: "lucas.martins@exemplo.com", role: "Fullstack Engineer", status: "Online", lastActive: "Há 12 min" }, | |
| { name: "Mariana Azevedo", email: "mariana.azevedo@exemplo.com", role: "Data Engineer", status: "Ocupado", lastActive: "Hoje" }, | |
| { name: "Nicolas Barros", email: "nicolas.barros@exemplo.com", role: "Security", status: "Offline", lastActive: "Ontem" }, | |
| { name: "Olívia Santos", email: "olivia.santos@exemplo.com", role: "Content", status: "Online", lastActive: "Agora" }, | |
| { name: "Pedro Henrique", email: "pedro.henrique@exemplo.com", role: "SRE", status: "Ocupado", lastActive: "Há 40 min" }, | |
| { name: "Quézia Duarte", email: "quezia.duarte@exemplo.com", role: "HR", status: "Ausente", lastActive: "Há 6h" }, | |
| { name: "Renato Carvalho", email: "renato.carvalho@exemplo.com", role: "Backend Engineer", status: "Online", lastActive: "Hoje" }, | |
| { name: "Sabrina Oliveira", email: "sabrina.oliveira@exemplo.com", role: "QA Lead", status: "Offline", lastActive: "Há 4 dias" }, | |
| { name: "Thiago Ribeiro", email: "thiago.ribeiro@exemplo.com", role: "Frontend Engineer", status: "Online", lastActive: "Há 1 dia" }, | |
| ]; | |
| const STATUS_UI: Record<Status, { dot: string; pill: string }> = { | |
| Online: { dot: "bg-emerald-600/80", pill: "text-slate-900/85" }, | |
| Ausente: { dot: "bg-amber-600/80", pill: "text-slate-900/85" }, | |
| Ocupado: { dot: "bg-rose-600/80", pill: "text-slate-900/85" }, | |
| Offline: { dot: "bg-slate-900/35", pill: "text-slate-900/80" }, | |
| }; | |
| export default function App() { | |
| const bgStyle = { | |
| backgroundColor: "#ffffff", | |
| backgroundAttachment: "fixed", | |
| backgroundRepeat: "no-repeat", | |
| backgroundSize: "cover", | |
| // tweak this to match the real site columns | |
| ["--col" as any]: "90px", | |
| backgroundImage: ` | |
| /* DIVISÓRIA com “shadow” no lado esquerdo + linha clara na borda */ | |
| repeating-linear-gradient( | |
| 90deg, | |
| rgba(255, 255, 255, 0) 0px, | |
| rgba(255, 255, 255, 0) calc(var(--col) - 6px), | |
| rgba(0, 0, 0, 0.020) calc(var(--col) - 6px), | |
| rgba(0, 0, 0, 0.040) calc(var(--col) - 2px), | |
| rgba(0, 0, 0, 0.060) calc(var(--col) - 1px), | |
| rgba(255, 255, 255, 0.18) calc(var(--col) - 1px), | |
| rgba(255, 255, 255, 0.18) var(--col) | |
| ), | |
| /* brilho sutil do lado DIREITO da mesma borda (1px) */ | |
| repeating-linear-gradient( | |
| 90deg, | |
| rgba(255, 255, 255, 0.06) 0px, | |
| rgba(255, 255, 255, 0.06) 1px, | |
| rgba(255, 255, 255, 0) 1px, | |
| rgba(255, 255, 255, 0) var(--col) | |
| ), | |
| /* COLUNAS ALTERNADAS (bem sutil) */ | |
| repeating-linear-gradient( | |
| 90deg, | |
| rgba(255, 255, 255, 0.05) 0px, | |
| rgba(255, 255, 255, 0.05) var(--col), | |
| rgba(255, 255, 255, 0.015) var(--col), | |
| rgba(255, 255, 255, 0.015) calc(var(--col) * 2) | |
| ), | |
| /* AZUL/CYAN NO TOPO-DIREITA */ | |
| radial-gradient( | |
| 900px 600px at 88% 12%, | |
| rgba(180, 250, 255, 0.42) 0%, | |
| rgba(180, 250, 255, 0) 68% | |
| ), | |
| /* GLOW QUENTE (meio/baixo-esquerda) */ | |
| radial-gradient( | |
| 1200px 820px at 18% 56%, | |
| rgba(255, 160, 120, 0.42) 0%, | |
| rgba(255, 160, 120, 0) 64% | |
| ), | |
| /* GLOW ROSA */ | |
| radial-gradient( | |
| 1050px 820px at 22% 78%, | |
| rgba(255, 120, 210, 0.40) 0%, | |
| rgba(255, 120, 210, 0) 66% | |
| ), | |
| /* GLOW MENTA */ | |
| radial-gradient( | |
| 1200px 860px at 82% 72%, | |
| rgba(110, 255, 215, 0.38) 0%, | |
| rgba(110, 255, 215, 0) 66% | |
| ), | |
| /* WASH DO TOPO */ | |
| linear-gradient( | |
| 180deg, | |
| rgba(255, 255, 255, 0.45) 0%, | |
| rgba(255, 255, 255, 0.16) 30%, | |
| rgba(255, 255, 255, 0) 62% | |
| ), | |
| /* BASE (L→R) */ | |
| linear-gradient( | |
| 90deg, | |
| #ffc4b2 0%, | |
| #ffbfd2 18%, | |
| #ffe1ec 30%, | |
| #f1f7f9 50%, | |
| #d7fbf6 70%, | |
| #bdf7ea 85%, | |
| #a7f5e4 100% | |
| ) | |
| `, | |
| } as React.CSSProperties & Record<string, any>; | |
| const PAGE_SIZE = 10; | |
| const [page, setPage] = useState(1); | |
| const total = PEOPLE.length; | |
| const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE)); | |
| const safePage = Math.min(Math.max(page, 1), pageCount); | |
| const start = (safePage - 1) * PAGE_SIZE; | |
| const end = Math.min(start + PAGE_SIZE, total); | |
| const pageItems = PEOPLE.slice(start, end); | |
| return ( | |
| <div | |
| style={bgStyle} | |
| className="min-h-screen w-full grid place-items-center px-6 py-14 text-slate-900" | |
| > | |
| <div | |
| className={ | |
| "relative w-full max-w-5xl overflow-hidden rounded-[22px] border border-white/45 bg-white/18 p-6 " + | |
| "shadow-[0_18px_60px_rgba(0,0,0,0.12)] backdrop-blur-xl backdrop-saturate-150 " + | |
| "before:pointer-events-none before:absolute before:inset-0 before:rounded-[22px] before:content-[''] " + | |
| "before:bg-[linear-gradient(135deg,_rgba(255,255,255,0.65),_rgba(255,255,255,0.12),_rgba(255,255,255,0))] before:opacity-70 " + | |
| "after:pointer-events-none after:absolute after:inset-0 after:rounded-[22px] after:content-[''] " + | |
| "after:bg-[radial-gradient(900px_500px_at_20%_0%,_rgba(255,255,255,0.35),_rgba(255,255,255,0)_60%)] after:opacity-60" | |
| } | |
| > | |
| <div className="relative z-10"> | |
| <div className="mb-4 flex items-end justify-between gap-4"> | |
| <div> | |
| <div className="text-lg font-semibold tracking-tight text-slate-900">Pessoas</div> | |
| <div className="text-sm text-slate-900/70"> | |
| Tabela mockada com 10 pessoas (glassmorphism usando o fundo) | |
| </div> | |
| </div> | |
| <button className="rounded-full border border-white/45 bg-white/18 px-3 py-2 text-sm text-slate-900/85 backdrop-blur-md hover:bg-white/28 cursor-pointer"> | |
| + Adicionar | |
| </button> | |
| </div> | |
| <div className="overflow-hidden rounded-2xl border border-white/30 bg-white/14"> | |
| <table className="w-full border-separate border-spacing-0 text-sm"> | |
| <thead> | |
| <tr className="bg-white/22"> | |
| <th className="border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70"> | |
| Nome | |
| </th> | |
| <th className="hidden sm:table-cell border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70"> | |
| </th> | |
| <th className="border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70"> | |
| Cargo | |
| </th> | |
| <th className="border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70"> | |
| Status | |
| </th> | |
| <th className="hidden sm:table-cell border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70"> | |
| Última atividade | |
| </th> | |
| <th | |
| className="border-b border-white/30 px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-900/70" | |
| style={{ width: 160 }} | |
| > | |
| Ações | |
| </th> | |
| </tr> | |
| </thead> | |
| <AnimatePresence mode="wait" initial={false}> | |
| <motion.tbody | |
| key={safePage} | |
| initial={{ opacity: 0, y: 8 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -8 }} | |
| transition={{ duration: 0.18, ease: "easeOut" }} | |
| > | |
| {pageItems.map((p, idx) => { | |
| const last = idx === pageItems.length - 1; | |
| const ui = STATUS_UI[p.status]; | |
| return ( | |
| <tr | |
| key={p.email} | |
| className="odd:bg-white/8 hover:bg-white/18 transition-colors" | |
| > | |
| <td className={(last ? "" : "border-b border-white/20 ") + "px-4 py-3"}> | |
| {p.name} | |
| </td> | |
| <td | |
| className={ | |
| (last ? "" : "border-b border-white/20 ") + | |
| "hidden sm:table-cell px-4 py-3 text-slate-900/80" | |
| } | |
| > | |
| {p.email} | |
| </td> | |
| <td | |
| className={ | |
| (last ? "" : "border-b border-white/20 ") + | |
| "px-4 py-3 text-slate-900/90" | |
| } | |
| > | |
| {p.role} | |
| </td> | |
| <td className={(last ? "" : "border-b border-white/20 ") + "px-4 py-3"}> | |
| <span | |
| className={ | |
| ui.pill + | |
| " inline-flex items-center gap-2 rounded-full border border-white/35 bg-white/18 px-3 py-1.5 text-xs" | |
| } | |
| > | |
| <span className={ui.dot + " h-2 w-2 rounded-full"} /> | |
| {p.status} | |
| </span> | |
| </td> | |
| <td | |
| className={ | |
| (last ? "" : "border-b border-white/20 ") + | |
| "hidden sm:table-cell px-4 py-3 text-slate-900/80" | |
| } | |
| > | |
| {p.lastActive} | |
| </td> | |
| <td className={(last ? "" : "border-b border-white/20 ") + "px-4 py-3"}> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| type="button" | |
| aria-label="Editar" | |
| className="grid h-9 w-9 place-items-center rounded-full border border-white/45 bg-white/18 text-slate-900/80 backdrop-blur-md transition hover:bg-white/28 hover:text-sky-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/20 cursor-pointer" | |
| > | |
| <Pencil className="h-4 w-4" /> | |
| </button> | |
| <button | |
| type="button" | |
| aria-label="Remover" | |
| className="grid h-9 w-9 place-items-center rounded-full border border-white/45 bg-white/18 text-slate-900/80 backdrop-blur-md transition hover:bg-white/28 hover:text-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/20 cursor-pointer" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </motion.tbody> | |
| </AnimatePresence> | |
| </table> | |
| {/* Pagination */} | |
| <div className="flex flex-col gap-3 border-t border-white/25 bg-white/10 px-4 py-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <div className="text-xs text-slate-900/70"> | |
| Mostrando{" "} | |
| <span className="font-medium text-slate-900/85">{start + 1}</span>– | |
| <span className="font-medium text-slate-900/85">{end}</span> de{" "} | |
| <span className="font-medium text-slate-900/85">{total}</span> | |
| </div> | |
| <div className="flex items-center justify-end gap-2"> | |
| <button | |
| type="button" | |
| aria-label="Página anterior" | |
| disabled={safePage === 1} | |
| onClick={() => setPage((p) => Math.max(1, p - 1))} | |
| className="grid h-9 w-9 place-items-center rounded-full border border-white/45 bg-white/18 text-slate-900/80 backdrop-blur-md transition hover:bg-white/28 disabled:opacity-45 disabled:cursor-not-allowed cursor-pointer" | |
| > | |
| <ChevronLeft className="h-4 w-4" /> | |
| </button> | |
| {Array.from({ length: pageCount }, (_, i) => i + 1).map((n) => { | |
| const active = n === safePage; | |
| return ( | |
| <button | |
| key={n} | |
| type="button" | |
| aria-label={`Ir para página ${n}`} | |
| aria-current={active ? "page" : undefined} | |
| onClick={() => setPage(n)} | |
| className={ | |
| "grid h-9 w-9 place-items-center rounded-full border text-xs font-semibold backdrop-blur-md transition cursor-pointer " + | |
| (active | |
| ? "border-white/55 bg-white/35 text-slate-900" | |
| : "border-white/45 bg-white/18 text-slate-900/80 hover:bg-white/28") | |
| } | |
| > | |
| {n} | |
| </button> | |
| ); | |
| })} | |
| <button | |
| type="button" | |
| aria-label="Próxima página" | |
| disabled={safePage === pageCount} | |
| onClick={() => setPage((p) => Math.min(pageCount, p + 1))} | |
| className="grid h-9 w-9 place-items-center rounded-full border border-white/45 bg-white/18 text-slate-900/80 backdrop-blur-md transition hover:bg-white/28 disabled:opacity-45 disabled:cursor-not-allowed cursor-pointer" | |
| > | |
| <ChevronRight className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment