fix(web): admin users cards redesigned for mobile readability

The single-line card squeezed identity, created date, role select, and
two buttons into one row — on a phone the username truncated to t…,
the email to te…, and the created date wrapped vertically one fragment
per line (10- / 13- / 2026 / …). Restructured into stacked full-width
rows: identity (avatar + username + email), created on its own line
(a full date never wraps), a labeled Role row under a divider, and
Edit/Delete as side-by-side flex-1 buttons (thumb-sized on mobile,
right-aligned sm+). Last-admin protections render identically in both
controls. Verified on the emulator (Pixel 9, Brave): full names/emails
visible, no wrapping, modal + role controls functional.
This commit is contained in:
John O'Keefe committed 2026-10-13 15:00:59 -04:00
1 parent 505fc48b23
commit 2a6e4b9ae5
2 files changed
+51 -42

No files matched your search

+20 -11
View File
@@ -112,14 +112,15 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
@adminCreateUserForm()
<!-- Users: fluid card list (same pattern as library management —
no table, reflows at every width including with the nav
sidebar open). -->
<!-- Users: card list. Stacked rows on purpose — identity, created,
role, and actions each get their own full-width row so nothing
truncates into confusion on a phone. -->
<div class="space-y-4">
for _, user := range users {
<div id={ "user-" + user.ID } class="card overflow-hidden">
<div class="flex items-center justify-between gap-4 p-5">
<div class="flex items-center gap-3 min-w-0 flex-1">
<div class="p-4 sm:p-5">
<!-- Identity -->
<div class="flex items-center gap-3">
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("user", "h-5 w-5")
</span>
@@ -131,10 +132,15 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
}
</div>
<p class="text-sm truncate" style="color: var(--text-secondary)">{ user.Email }</p>
<p class="text-xs" style="color: var(--text-secondary)">Created { FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</p>
</div>
</div>
<div class="flex flex-wrap items-center justify-end gap-2 shrink-0">
<!-- Created: its own line — a full date never wraps -->
<p class="text-xs mt-2" style="color: var(--text-secondary)">
Created { FormatInTimezone(user.CreatedAt, currentUser.Timezone) }
</p>
<!-- Role -->
<div class="mt-3 pt-3 border-t flex items-center justify-between gap-3" style="border-color: var(--border)">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Role</span>
if user.Role == "admin" && adminCount == 1 {
<!-- Last admin - disabled -->
<select
@@ -157,13 +163,16 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
<option value="user" selected?={ user.Role == "user" }>User</option>
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
</select>
<div id={ "role-result-" + user.ID } class="text-xs w-full text-right"></div>
}
</div>
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
<!-- Actions -->
<div class="mt-3 flex gap-2">
<button
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
hx-target="#modal-container"
hx-swap="innerHTML"
class="btn btn-secondary text-xs px-2.5 py-1"
class="btn btn-secondary text-sm px-3 py-1.5 flex-1 sm:flex-none sm:ml-auto"
>
@Icon("edit", "h-4 w-4")
Edit
@@ -171,7 +180,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
if user.Role == "admin" && adminCount == 1 {
<button
disabled
class="btn btn-danger text-xs px-2.5 py-1"
class="btn btn-danger text-sm px-3 py-1.5 flex-1 sm:flex-none"
title="Cannot delete the last admin"
>
@Icon("trash", "h-4 w-4")
@@ -183,7 +192,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
hx-target={ "#user-" + user.ID }
hx-swap="outerHTML swap:0.5s"
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
class="btn btn-danger text-xs px-2.5 py-1"
class="btn btn-danger text-sm px-3 py-1.5 flex-1 sm:flex-none"
>
@Icon("trash", "h-4 w-4")
Delete