feat(web): mobile-responsive pass on mesh detail + invites list
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled

Mesh detail page at /dashboard/meshes/[id]:
- Header: flex-col → flex-row at sm breakpoint. Live/Invite buttons
  stretch full-width stacked on mobile (flex-1), auto-width side-by-
  side from sm up.
- "Generate invite link" truncates to "Invite" on mobile (viewport
  constrained) so the button fits next to Live.
- Members + active-invites rows: stack metadata vertically on mobile
  (flex-col → sm:flex-row), wrap badges inside with flex-wrap so the
  member display-name + role + revoked badges don't horizontal-scroll.

Invites list at /dashboard/invites:
- Wrap the table in overflow-x-auto with min-w-[560px] on the table
  itself. 5-column data-table that genuinely needs horizontal space
  — don't fake it with card stacking, let the user scroll naturally.

Quick-send composer deferred to a follow-up — writes a message to the
mesh, which requires a client-side encryption decision (ed25519
keypair in the browser? key derivation from session? plaintext-to-
broker and break E2E?). Parked as its own spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 15:13:16 +01:00
parent cdd7931837
commit 995d8a3c12
2 changed files with 22 additions and 15 deletions

View File

@@ -41,8 +41,8 @@ export default async function InvitesPage() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="rounded-lg border"> <div className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm"> <table className="w-full min-w-[560px] text-sm">
<thead className="text-muted-foreground border-b text-left text-xs uppercase"> <thead className="text-muted-foreground border-b text-left text-xs uppercase">
<tr> <tr>
<th className="px-4 py-3 font-medium">Mesh</th> <th className="px-4 py-3 font-medium">Mesh</th>

View File

@@ -40,11 +40,11 @@ export default async function MeshPage({
return ( return (
<> <>
<DashboardHeader> <DashboardHeader>
<div className="flex w-full items-start justify-between gap-4"> <div className="flex w-full flex-col items-start gap-4 sm:flex-row sm:items-start sm:justify-between">
<div> <div className="min-w-0 flex-1">
<DashboardHeaderTitle> <DashboardHeaderTitle>
<span className="flex items-center gap-3"> <span className="flex flex-wrap items-center gap-2 sm:gap-3">
{mesh.name} <span className="truncate">{mesh.name}</span>
<Badge variant="outline" className="font-mono text-xs"> <Badge variant="outline" className="font-mono text-xs">
{mesh.slug} {mesh.slug}
</Badge> </Badge>
@@ -55,19 +55,26 @@ export default async function MeshPage({
· tier {mesh.tier} · {mesh.visibility} · {mesh.transport} · tier {mesh.tier} · {mesh.visibility} · {mesh.transport}
</DashboardHeaderDescription> </DashboardHeaderDescription>
</div> </div>
<div className="flex gap-2"> <div className="flex w-full gap-2 sm:w-auto">
<Link <Link
href={pathsConfig.dashboard.user.meshes.live(mesh.id)} href={pathsConfig.dashboard.user.meshes.live(mesh.id)}
className={buttonVariants({ variant: "outline" })} className={buttonVariants({
variant: "outline",
className: "flex-1 sm:flex-initial",
})}
> >
<span className="mr-1.5 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--cm-clay)]" /> <span className="mr-1.5 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--cm-clay)]" />
Live Live
</Link> </Link>
<Link <Link
href={pathsConfig.dashboard.user.meshes.invite(mesh.id)} href={pathsConfig.dashboard.user.meshes.invite(mesh.id)}
className={buttonVariants({ variant: "default" })} className={buttonVariants({
variant: "default",
className: "flex-1 sm:flex-initial",
})}
> >
Generate invite link <span className="hidden sm:inline">Generate invite link</span>
<span className="sm:hidden">Invite</span>
</Link> </Link>
</div> </div>
</div> </div>
@@ -90,9 +97,9 @@ export default async function MeshPage({
{members.map((m) => ( {members.map((m) => (
<div <div
key={m.id} key={m.id}
className="flex items-center justify-between px-4 py-3" className="flex flex-col gap-1.5 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-3"
> >
<div className="flex items-center gap-3"> <div className="flex flex-wrap items-center gap-2 sm:gap-3">
<span className="font-medium"> <span className="font-medium">
{m.displayName} {m.displayName}
{m.isMe && ( {m.isMe && (
@@ -140,16 +147,16 @@ export default async function MeshPage({
{activeInvites.map((inv) => ( {activeInvites.map((inv) => (
<div <div
key={inv.id} key={inv.id}
className="flex items-center justify-between px-4 py-3 text-sm" className="flex flex-col gap-1.5 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between sm:gap-3"
> >
<div className="flex items-center gap-3"> <div className="flex flex-wrap items-center gap-2 sm:gap-3">
<code className="bg-muted rounded px-2 py-0.5 text-xs"> <code className="bg-muted rounded px-2 py-0.5 text-xs">
{inv.token.slice(0, 12)} {inv.token.slice(0, 12)}
</code> </code>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{inv.role} {inv.role}
</Badge> </Badge>
<span className="text-muted-foreground"> <span className="text-muted-foreground text-xs">
{inv.usedCount} / {inv.maxUses} used {inv.usedCount} / {inv.maxUses} used
</span> </span>
</div> </div>