From 69882ecbea7ebd6d6f996f522720ca0415d64598 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Sat, 1 Aug 2026 11:34:13 +0200 Subject: [PATCH] #301: the token tables need the same scroll wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sorted report finally named it: `table.api-tokens__table` at 833px wide, with its `.visually-hidden` heading reaching right=737 — exactly the document's scrollWidth. Same mechanism as the sessions table, a second table I had not wrapped. Locally the API-tokens table was empty and therefore narrow, which is why this only ever appeared in CI. With a token present it reproduces: without the wrapper 345px of page overflow, with it none. The feed-token table gets the same treatment — it is built the same way and would fail as soon as someone holds a feed token with a long name. The "[in fitting scroller]" marker in the report is misleading for these: `main.main` is a scroller, but it is `position: static`, so it never clipped the absolutely positioned heading. Only a positioned ancestor does — which is what `.table-scroll` now is. Verified locally against a real stack, with a wide token table present: reflow guard green, whole a11y pack green in both colour schemes. --- apps/web/src/api-tokens/ApiTokensSection.tsx | 92 ++++++++++--------- apps/web/src/api-tokens/FeedTokensSection.tsx | 56 ++++++----- 2 files changed, 78 insertions(+), 70 deletions(-) diff --git a/apps/web/src/api-tokens/ApiTokensSection.tsx b/apps/web/src/api-tokens/ApiTokensSection.tsx index 22d0a9d..2b34627 100644 --- a/apps/web/src/api-tokens/ApiTokensSection.tsx +++ b/apps/web/src/api-tokens/ApiTokensSection.tsx @@ -172,52 +172,54 @@ function TokenList({ tokens }: { tokens: ApiTokenView[] }): React.JSX.Element { return ( <> - - - - - - - - - - - - - - - {tokens.map((token) => ( - - - - - - - - - +
+
{t('fields.name')}{t('fields.scope')}{t('fields.ponds')}{t('list.created')}{t('list.lastUsed')}{t('list.expires')}{t('list.status')} - {t('common:tableActions')} -
{token.name}{token.scope === 'write' ? t('fields.scopeWrite') : t('fields.scopeRead')} - {token.ponds.length === 0 - ? t('list.allPonds') - : token.ponds.map((pond) => pond.name).join(', ')} - {new Date(token.createdAt).toLocaleDateString()} - {token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : t('list.never')} - {token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : '—'}{t(`list.${statusOf(token)}`)} - {!token.revokedAt && ( - - )} -
+ + + + + + + + + + - ))} - -
{t('fields.name')}{t('fields.scope')}{t('fields.ponds')}{t('list.created')}{t('list.lastUsed')}{t('list.expires')}{t('list.status')} + {t('common:tableActions')} +
+ + + {tokens.map((token) => ( + + {token.name} + {token.scope === 'write' ? t('fields.scopeWrite') : t('fields.scopeRead')} + + {token.ponds.length === 0 + ? t('list.allPonds') + : token.ponds.map((pond) => pond.name).join(', ')} + + {new Date(token.createdAt).toLocaleDateString()} + + {token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : t('list.never')} + + {token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : '—'} + {t(`list.${statusOf(token)}`)} + + {!token.revokedAt && ( + + )} + + + ))} + + + ); } diff --git a/apps/web/src/api-tokens/FeedTokensSection.tsx b/apps/web/src/api-tokens/FeedTokensSection.tsx index 1b2e40d..bf3a1a5 100644 --- a/apps/web/src/api-tokens/FeedTokensSection.tsx +++ b/apps/web/src/api-tokens/FeedTokensSection.tsx @@ -85,32 +85,38 @@ export function FeedTokensSection(): React.JSX.Element { )} {tokens.data && tokens.data.length === 0 &&

{t('feed.empty')}

} {tokens.data && tokens.data.length > 0 && ( - - - - - - - - - - - {tokens.data.map((token) => ( - - - - - +
+
{t('fields.name')}{t('list.created')}{t('list.lastUsed')} - {t('common:tableActions')} -
{token.name}{formatTime(token.createdAt)}{token.lastUsedAt ? formatTime(token.lastUsedAt) : '—'} - -
+ + + + + + - ))} - -
{t('fields.name')}{t('list.created')}{t('list.lastUsed')} + {t('common:tableActions')} +
+ + + {tokens.data.map((token) => ( + + {token.name} + {formatTime(token.createdAt)} + {token.lastUsedAt ? formatTime(token.lastUsedAt) : '—'} + + + + + ))} + + + )} );