— no
+ // whitespace-nowrap so a long German date+time can flow onto a
+ // second line within the card. The shadcn `` primitive
+ // does carry `whitespace-nowrap`, which is fine for the short
+ // permission strings we render — those are scoped to a flex-wrap
+ // container that line-breaks at the badge boundary.
+ expect(mobileMatch![1]).toMatch(
+ /]*text-\[11px\][^>]*>(?![^<]*whitespace-nowrap)/,
+ );
+ });
});
diff --git a/src/components/admin/api-token-overview-section.tsx b/src/components/admin/api-token-overview-section.tsx
index 0cf5b9ce0..26bb17d82 100644
--- a/src/components/admin/api-token-overview-section.tsx
+++ b/src/components/admin/api-token-overview-section.tsx
@@ -28,7 +28,7 @@ export function ApiTokenOverviewSection() {
});
return (
-
+
@@ -63,101 +63,166 @@ export function ApiTokenOverviewSection() {
description={t("admin.tokensEmptyDescription")}
/>
) : (
- // Mobile fits within the card by hiding lower-priority
- // columns (user, last-used, created) until `sm`. The
- // remaining `overflow-x-auto` is a belt-and-suspenders
- // guard for ultra-narrow viewports — the table still
- // scrolls within its container instead of forcing the
- // page itself to scroll horizontally.
-
-
-
-
- |
- {t("admin.tokenUser")}
- |
-
- {t("admin.tokenName")}
- |
-
- {t("admin.tokenPermissions")}
- |
-
- {t("admin.tokenStatus")}
- |
-
- {t("admin.tokenLastUsed")}
- |
-
- {t("admin.tokenCreated")}
- |
-
-
-
- {tokens.map((token, i) => {
- const isExpired =
- token.expiresAt && new Date(token.expiresAt) < new Date();
- return (
-
- |
- {token.user.username}
- |
-
-
- {token.name}
- {/* On mobile the user column is hidden, so
- surface the username inline under the
- token name instead of dropping it. */}
-
- {token.user.username}
-
-
- |
-
-
- {token.permissions.map((p) => (
-
- {p}
+ <>
+ {/* Desktop table — verbatim layout for md+. The wrapping
+ `hidden md:block` swaps it out for the card-list below
+ on mobile, mirroring the pattern used in
+ ``. The inner `overflow-x-auto`
+ is a belt-and-suspenders guard for medium viewports
+ with extra-long permission lists. */}
+
+
+
+
+ |
+ {t("admin.tokenUser")}
+ |
+
+ {t("admin.tokenName")}
+ |
+
+ {t("admin.tokenPermissions")}
+ |
+
+ {t("admin.tokenStatus")}
+ |
+
+ {t("admin.tokenLastUsed")}
+ |
+
+ {t("admin.tokenCreated")}
+ |
+
+
+
+ {tokens.map((token, i) => {
+ const isExpired =
+ token.expiresAt &&
+ new Date(token.expiresAt) < new Date();
+ return (
+
+ |
+ {token.user.username}
+ |
+ {token.name} |
+
+
+ {token.permissions.map((p) => (
+
+ {p}
+
+ ))}
+
+ |
+
+ {token.revoked ? (
+
+ {t("settings.tokenRevoked")}
- ))}
+ ) : isExpired ? (
+
+ {t("settings.tokenExpired")}
+
+ ) : (
+
+ {t("common.active")}
+
+ )}
+ |
+
+ {token.lastUsedAt
+ ? formatDateTime(token.lastUsedAt)
+ : t("admin.tokenNeverUsed")}
+ |
+
+ {formatDate(token.createdAt)}
+ |
+
+ );
+ })}
+
+
+
+
+ {/* Mobile card list — each token renders as a self-
+ contained card with a stacked meta layout. No
+ horizontal scroll: every cell wraps within the card
+ width. Mirrors ``. */}
+
+ {tokens.map((token) => {
+ const isExpired =
+ token.expiresAt && new Date(token.expiresAt) < new Date();
+ return (
+ -
+
+
+
+
+ {token.name}
+
+ {token.revoked ? (
+
+ {t("settings.tokenRevoked")}
+
+ ) : isExpired ? (
+
+ {t("settings.tokenExpired")}
+
+ ) : (
+
+ {t("common.active")}
+
+ )}
- |
-
- {token.revoked ? (
-
- {t("settings.tokenRevoked")}
-
- ) : isExpired ? (
-
- {t("settings.tokenExpired")}
-
- ) : (
-
- {t("common.active")}
-
- )}
- |
-
- {token.lastUsedAt
- ? formatDateTime(token.lastUsedAt)
- : t("admin.tokenNeverUsed")}
- |
-
- {formatDate(token.createdAt)}
- |
-
- );
- })}
-
-
-
+
+ {token.user.username}
+
+
+
+
+ {token.permissions.map((p) => (
+
+ {p}
+
+ ))}
+
+
+ {t("admin.tokenLastUsed")}:{" "}
+ {token.lastUsedAt
+ ? formatDateTime(token.lastUsedAt)
+ : t("admin.tokenNeverUsed")}
+
+
+ {t("admin.tokenCreated")}: {formatDate(token.createdAt)}
+
+
+ );
+ })}
+
+ >
)}
)}