Skip to content

Instantly share code, notes, and snippets.

@celsowm
Created December 14, 2025 13:31
Show Gist options
  • Select an option

  • Save celsowm/65f2e55a0386519224c3aecedbb0df29 to your computer and use it in GitHub Desktop.

Select an option

Save celsowm/65f2e55a0386519224c3aecedbb0df29 to your computer and use it in GitHub Desktop.
glassmorph.tsx
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">
Email
</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