feat(ui): improve mobile browser support
This commit is contained in:
@@ -20,12 +20,12 @@ export function AdminPage() {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="Admin" subtitle="Platform administration." />
|
||||
<div className="mb-6 flex gap-1 border-b border-border">
|
||||
<div className="mb-6 flex gap-1 overflow-x-auto border-b border-border">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`px-4 py-2 text-sm font-medium transition ${
|
||||
className={`shrink-0 px-4 py-2 text-sm font-medium transition ${
|
||||
tab === t.id ? "border-b-2 border-accent text-text" : "text-muted hover:text-text"
|
||||
}`}
|
||||
>
|
||||
@@ -110,7 +110,7 @@ function UsersTab() {
|
||||
</p>
|
||||
</div>
|
||||
{u.id !== me?.id && (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex w-full flex-wrap gap-2 sm:w-auto sm:flex-nowrap">
|
||||
<Button variant="ghost" onClick={() => setRole(u, u.role === "ADMIN" ? "USER" : "ADMIN")}>
|
||||
{u.role === "ADMIN" ? "Demote" : "Promote"}
|
||||
</Button>
|
||||
@@ -178,7 +178,7 @@ function AgentsTab() {
|
||||
</div>
|
||||
{a.description && <p className="text-xs text-muted">{a.description}</p>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex w-full flex-wrap gap-2 sm:w-auto sm:flex-nowrap">
|
||||
<Button variant="ghost" onClick={() => toggle(a)}>
|
||||
{a.disabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
@@ -211,14 +211,14 @@ function SettingsTab() {
|
||||
if (!settings) return <Loader />;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Card className="flex items-center justify-between p-5">
|
||||
<Card className="flex flex-wrap items-center justify-between gap-4 p-4 sm:p-5">
|
||||
<div>
|
||||
<p className="font-medium text-text">Enable registration</p>
|
||||
<p className="text-sm text-muted">Allow new humans to create accounts.</p>
|
||||
</div>
|
||||
<Toggle checked={settings.registration_enabled} onChange={(v) => update({ registration_enabled: v })} />
|
||||
</Card>
|
||||
<Card className="flex items-center justify-between p-5">
|
||||
<Card className="flex flex-wrap items-center justify-between gap-4 p-4 sm:p-5">
|
||||
<div>
|
||||
<p className="font-medium text-text">Enable requests</p>
|
||||
<p className="text-sm text-muted">Allow agents to submit new change requests platform-wide.</p>
|
||||
@@ -251,14 +251,14 @@ function AuditTab() {
|
||||
<div>
|
||||
<div className="space-y-1.5">
|
||||
{logs.map((l) => (
|
||||
<Card key={l.id} className="flex items-center gap-3 p-3 text-sm">
|
||||
<Card key={l.id} className="flex flex-wrap items-center gap-2 p-3 text-sm sm:flex-nowrap sm:gap-3">
|
||||
<code className="rounded bg-surface-raised px-2 py-0.5 text-xs text-accent">{l.action}</code>
|
||||
<span className="min-w-0 flex-1 truncate text-muted">
|
||||
{l.actor ? `@${l.actor.username}` : "system"}
|
||||
{l.target_type && ` → ${l.target_type}:${l.target_id}`}
|
||||
{l.detail && ` · ${l.detail}`}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-faint">{relativeTime(l.created_at)}</span>
|
||||
<span className="w-full text-xs text-faint sm:w-auto sm:shrink-0">{relativeTime(l.created_at)}</span>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function DashboardPage() {
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h2 className="text-lg font-semibold">Awaiting review</h2>
|
||||
<Link to="/requests" className="text-sm text-primary hover:underline">
|
||||
View all →
|
||||
@@ -89,7 +89,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h2 className="text-lg font-semibold">Agents</h2>
|
||||
<Link to="/agents" className="text-sm text-primary hover:underline">
|
||||
Manage →
|
||||
@@ -128,7 +128,7 @@ function PendingRow({ request }: { request: ChangeRequest }) {
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<AgentAvatar name={request.agent?.name ?? "?"} iconUrl={request.agent?.icon_url} size={32} />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="truncate font-medium text-text">{request.title}</p>
|
||||
{request.resubmitted && (
|
||||
<span className="animate-pulse-ring rounded-full bg-changes/20 px-2 py-0.5 text-[10px] font-semibold text-changes">
|
||||
|
||||
@@ -90,7 +90,7 @@ export function AuthShell({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="flex min-h-[100dvh] items-center justify-center p-3 sm:p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mb-3 text-4xl">✅</div>
|
||||
@@ -99,7 +99,7 @@ export function AuthShell({
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted">{subtitle}</p>
|
||||
</div>
|
||||
<Card className="p-6">
|
||||
<Card className="p-4 sm:p-6">
|
||||
<h2 className="mb-5 text-lg font-semibold">{title}</h2>
|
||||
{children}
|
||||
</Card>
|
||||
|
||||
@@ -76,11 +76,11 @@ export function RequestDetailPage() {
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="order-2 lg:order-1 lg:col-span-2">
|
||||
<div className="mb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">{request.title}</h1>
|
||||
<h1 className="break-words text-xl font-bold tracking-tight sm:text-2xl">{request.title}</h1>
|
||||
<StateBadge state={request.state} />
|
||||
</div>
|
||||
{request.description && <p className="mt-2 text-muted">{request.description}</p>}
|
||||
@@ -102,7 +102,7 @@ export function RequestDetailPage() {
|
||||
<ChangeList changes={request.changes} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="order-1 space-y-4 lg:order-2">
|
||||
{canDecide && (
|
||||
<Card className="space-y-2 p-4">
|
||||
<p className="mb-1 text-sm font-semibold">Your decision</p>
|
||||
@@ -197,9 +197,9 @@ export function RequestDetailPage() {
|
||||
|
||||
function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
|
||||
<span className="shrink-0 text-faint">{label}</span>
|
||||
<span className="text-right text-text">{value}</span>
|
||||
<span className="break-all text-text sm:text-right">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,13 +60,13 @@ export function RequestsPage() {
|
||||
<div>
|
||||
<PageHeader title="Requests" subtitle="Full history of change requests submitted by your agents." />
|
||||
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||
<div className="mb-4 flex flex-nowrap items-center gap-2 overflow-x-auto pb-1 sm:flex-wrap sm:pb-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
setStateFilter("");
|
||||
setPage(1);
|
||||
}}
|
||||
className={`rounded-lg px-3 py-1.5 text-sm ${stateFilter === "" ? "bg-surface-raised text-text" : "text-muted hover:text-text"}`}
|
||||
className={`shrink-0 rounded-lg px-3 py-2 text-sm ${stateFilter === "" ? "bg-surface-raised text-text" : "text-muted hover:text-text"}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
@@ -77,7 +77,7 @@ export function RequestsPage() {
|
||||
setStateFilter(s);
|
||||
setPage(1);
|
||||
}}
|
||||
className={`rounded-lg px-3 py-1.5 text-sm ${stateFilter === s ? "bg-surface-raised text-text" : "text-muted hover:text-text"}`}
|
||||
className={`shrink-0 rounded-lg px-3 py-2 text-sm ${stateFilter === s ? "bg-surface-raised text-text" : "text-muted hover:text-text"}`}
|
||||
>
|
||||
{s.replace("_", " ").toLowerCase()}
|
||||
</button>
|
||||
@@ -88,7 +88,7 @@ export function RequestsPage() {
|
||||
setAgentFilter(e.target.value ? Number(e.target.value) : "");
|
||||
setPage(1);
|
||||
}}
|
||||
className="ml-auto rounded-lg border border-border-strong bg-bg px-3 py-1.5 text-sm text-text outline-none"
|
||||
className="sticky right-0 ml-auto shrink-0 rounded-lg border border-border-strong bg-bg px-3 py-2 text-sm text-text outline-none sm:static"
|
||||
>
|
||||
<option value="">All agents</option>
|
||||
{agents.map((a) => (
|
||||
@@ -125,7 +125,7 @@ export function RequestsPage() {
|
||||
{relativeTime(r.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<StateBadge state={r.state} />
|
||||
<div className="shrink-0"><StateBadge state={r.state} /></div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<Card className="p-4 sm:p-5">
|
||||
<h2 className="text-base font-semibold text-text">{title}</h2>
|
||||
{description && <p className="mb-4 mt-0.5 text-sm text-muted">{description}</p>}
|
||||
<div className={description ? "" : "mt-4"}>{children}</div>
|
||||
@@ -147,7 +147,7 @@ export function SettingsPage() {
|
||||
|
||||
<Section title="Two-factor authentication" description="Add a TOTP authenticator app for extra security.">
|
||||
{user?.totp_enabled ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<span className="text-sm text-approved">✓ 2FA is enabled</span>
|
||||
<Button variant="danger" onClick={() => setDisable2faOpen(true)}>
|
||||
Disable
|
||||
|
||||
Reference in New Issue
Block a user