feat: Revamp instance page
This commit is contained in:
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { ApiError, api, type InstanceRole } from "@/lib/api";
|
||||
import { useSession } from "@/lib/session";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button, controlClass } from "@/components/Button";
|
||||
import { EmptyState, Panel } from "@/components/Panel";
|
||||
|
||||
const ROLES: InstanceRole[] = ["owner", "admin", "member"];
|
||||
@@ -23,7 +23,8 @@ const ROLE_GRANTS: Record<InstanceRole, string> = {
|
||||
const SELECT_QUIET =
|
||||
"rounded border border-transparent bg-transparent px-2 py-1 font-mono text-[0.78rem] uppercase tracking-[0.08em] text-ink-2 hover:border-rule focus:border-accent focus:text-ink focus:outline-none";
|
||||
|
||||
const SELECT = "rounded border border-rule bg-panel px-2.5 py-2 font-mono text-[0.84rem] text-ink focus:border-accent focus:outline-none";
|
||||
/* Same height as the Grant access button beside it — see controlClass. */
|
||||
const SELECT = controlClass("bg-panel");
|
||||
|
||||
/*
|
||||
* The access roster for one instance.
|
||||
@@ -110,34 +111,46 @@ export function MembersPanel({ instanceId }: { instanceId: string }) {
|
||||
/>
|
||||
) : (
|
||||
<ul className="grid border-t border-rule-soft">
|
||||
{/*
|
||||
* Two columns on a phone — monogram and address — with the
|
||||
* controls dropping to their own full-width row beneath;
|
||||
* three columns from sm up, controls right-aligned. As one
|
||||
* wrapping flex row the address competed with a select and
|
||||
* two buttons for 320px and lost, and the confirm step put
|
||||
* three more elements into the same row.
|
||||
*/}
|
||||
{rows.map((m) => (
|
||||
<li key={m.member_id} className="flex flex-wrap items-center gap-3 border-b border-rule-soft px-4 py-2.5 last:border-b-0">
|
||||
<li
|
||||
key={m.member_id}
|
||||
className="grid grid-cols-[auto_1fr] items-center gap-x-3 gap-y-2 border-b border-rule-soft px-4 py-3 last:border-b-0 sm:grid-cols-[auto_1fr_auto]"
|
||||
>
|
||||
<span aria-hidden className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-accent font-mono text-[0.62rem] font-bold text-accent-ink">
|
||||
{m.email.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-[0.84rem]">{m.email}</span>
|
||||
<span className="min-w-0 break-all font-mono text-[0.84rem] sm:truncate sm:break-normal">{m.email}</span>
|
||||
|
||||
{canManage ? (
|
||||
<label className="shrink-0">
|
||||
<span className="sr-only">Role for {m.email}</span>
|
||||
<select
|
||||
value={m.role}
|
||||
title={ROLE_GRANTS[m.role]}
|
||||
onChange={(e) => changeRole.mutate({ uid: m.customer_user_id, role: e.target.value as InstanceRole })}
|
||||
className={SELECT_QUIET}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : (
|
||||
<span className="shrink-0 font-mono text-[0.78rem] uppercase tracking-[0.08em] text-ink-3">{m.role}</span>
|
||||
)}
|
||||
<div className="col-span-2 flex flex-wrap items-center gap-2 sm:col-span-1 sm:flex-nowrap sm:justify-end">
|
||||
{canManage ? (
|
||||
<label className="shrink-0">
|
||||
<span className="sr-only">Role for {m.email}</span>
|
||||
<select
|
||||
value={m.role}
|
||||
title={ROLE_GRANTS[m.role]}
|
||||
onChange={(e) => changeRole.mutate({ uid: m.customer_user_id, role: e.target.value as InstanceRole })}
|
||||
className={SELECT_QUIET}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : (
|
||||
<span className="shrink-0 font-mono text-[0.78rem] uppercase tracking-[0.08em] text-ink-3">{m.role}</span>
|
||||
)}
|
||||
|
||||
{canManage &&
|
||||
{canManage &&
|
||||
/*
|
||||
* Confirming inline rather than through
|
||||
* window.confirm(), and in the row itself rather
|
||||
@@ -145,7 +158,7 @@ export function MembersPanel({ instanceId }: { instanceId: string }) {
|
||||
* where the eye already is.
|
||||
*/
|
||||
(confirming === m.customer_user_id ? (
|
||||
<span className="flex shrink-0 items-center gap-2.5">
|
||||
<span className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
|
||||
<span className="text-[0.8rem] text-ink-2">Revoke access?</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -173,6 +186,7 @@ export function MembersPanel({ instanceId }: { instanceId: string }) {
|
||||
Revoke<span className="sr-only"> access for {m.email}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -180,15 +194,18 @@ export function MembersPanel({ instanceId }: { instanceId: string }) {
|
||||
|
||||
{canManage && (
|
||||
<div className="grid gap-3 border-t border-rule bg-panel-2 px-4 py-3.5">
|
||||
{/* Stacked and full width on a phone; one row from sm up.
|
||||
Three controls side by side left the person select about
|
||||
90px wide, which is not enough to read an address in. */}
|
||||
<form
|
||||
className="flex flex-wrap items-end gap-3"
|
||||
className="grid gap-3 sm:flex sm:flex-wrap sm:items-end"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (selected) grant.mutate();
|
||||
}}
|
||||
>
|
||||
<label className="grid min-w-0 flex-1 gap-1.5">
|
||||
<label className="grid min-w-0 gap-1.5 sm:flex-1">
|
||||
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">Grant access to</span>
|
||||
<select value={selected} onChange={(e) => setSelected(e.target.value)} className={SELECT} disabled={candidates.length === 0}>
|
||||
<option value="">{candidates.length === 0 ? "Everyone already has access" : "Choose a person…"}</option>
|
||||
@@ -209,7 +226,7 @@ export function MembersPanel({ instanceId }: { instanceId: string }) {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<Button type="submit" disabled={!selected || grant.isPending}>
|
||||
<Button type="submit" disabled={!selected || grant.isPending} className="w-full justify-center sm:w-auto">
|
||||
{grant.isPending ? "Granting…" : "Grant access"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user