diff --git a/CHANGELOG.md b/CHANGELOG.md index 15895d3..f0e42fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,47 @@ skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. +## 0.31.1 — 2026-05-29 + +**Patch — admin Users tab + header UX polish. Visual only: CSS plus +markup/structure in `Admin.jsx` (no API, schema, config, overlay, or +secret change). A plain frontend rebuild applies it.** + +Two latent CSS defects fixed: + +- **`.invite-badge` had no rule.** The "(pending invite)" marker on + admin-created-but-unclaimed user rows rendered as bare parenthetical + text. It's now a quiet amber pill, consistent with the other status + badges. +- **`.btn-link-quiet` never reset native button chrome.** Used as a + bare link-style ` + +

The pending bucket is the beta-access review queue (§6.1 / v0.8.0). Grant or revoke writes to permission_events @@ -190,17 +203,6 @@ function UsersTab() { retain their v0.7.0 semantics — promote to admin to remove a user's ability to write without silencing them.

- {/* v0.17.0 — roadmap item #16. The "Create user + invite" - affordance opens a modal that provisions a fresh users row - with the chosen role and sends an invite email with a - single-use claim link. */} -
- -
{error &&

{error}

} {inviteModalOpen && ( @@ -270,21 +272,27 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission } // the admin sees at a glance which rows are real users vs. unclaimed // invites. const pendingInvite = u.pending_invite + // When there's no gitea_login the handle already IS the email, so the + // subline would otherwise repeat it. Only append the email when it adds + // something the handle doesn't already show. + const showEmail = u.email && u.email !== handle return ( <>
- {handle} - {pendingInvite && ( - (pending invite) - )} +
+ {handle} + {pendingInvite && ( + pending invite + )} +
{fullName || u.display_name} - {u.email ? ` · ${u.email}` : ''} + {showEmail ? ` · ${u.email}` : ''}
@@ -317,8 +325,8 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission } N/A )} - {u.created_at || '—'} - {u.last_seen_at || '—'} + + {state === 'pending' && u.beta_request_reason ? ( @@ -334,6 +342,21 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission } ) } +// Render a "YYYY-MM-DD HH:MM:SS" timestamp as an intentional date-over-time +// stack (date prominent, time quiet below) rather than letting a narrow +// column wrap the value mid-string. Falls back to an em-dash when absent. +function TimeCell({ value }) { + if (!value) return — + const [date, ...rest] = String(value).split(' ') + const time = rest.join(' ') + return ( + + {date} + {time && {time}} + + ) +} + function PermissionCell({ user: u, busy, onFlipPermission }) { const state = u.permission_state || 'granted' const decidedSuffix = u.permission_decided_at