fix(web): stop long tokens and button rows overflowing on mobile
Server Deploy / deploy (push) Successful in 1m40s

Three root causes behind five reported overflows:

- Td below sm is a flex row, and a flex item's default min-width:auto is
  its min-content width. A fingerprint or target URL is one unbreakable
  token, so the value could not shrink and spilled out of the card. The
  value now sits in a min-w-0 break-words wrapper that is display:contents
  above sm, leaving the desktop table untouched. Fixes /keys, /monitors
  and the installed-keys table on /servers/[id] in one place.

- The ESO read URL and the secrets group endpoint are unbreakable tokens
  in prose with the default overflow-wrap:normal. Both paragraphs now
  break-words.

- Three page headers (servers/[id], keys/[id], monitors/[id]) held an
  unwrapped button row. They stack below sm and the clusters wrap. These
  used items-start, which is why the earlier responsive sweep — matching
  items-center — skipped all three; only one was reported.

Verified: tsc clean, next build clean, and the compiled stylesheet confirms
max-sm:min-w-0 / max-sm:break-words land in the below-640px query and
sm:contents in the above-640px one.
This commit is contained in:
mrhid6
2026-07-27 23:28:53 +01:00
parent 14a11886ae
commit 5e326335af
6 changed files with 34 additions and 18 deletions
+7 -5
View File
@@ -248,20 +248,22 @@ export default function KeyDetailPage() {
/>
)}
<div className="mb-6 flex items-start justify-between">
<div>
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<Link href="/keys" className="text-text-secondary hover:text-text-primary text-sm">
SSH Keys
</Link>
<div className="mt-2 flex items-center gap-3">
<div className="mt-2 flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-bold text-text-primary">{key.label}</h1>
<Badge variant={key.source === "generated" ? "accent" : "neutral"}>
{key.source}
</Badge>
</div>
<p className="mt-1 font-mono text-xs text-text-secondary">{key.fingerprint}</p>
{/* A fingerprint is one unbreakable token; without break-all it
overflows the column rather than wrapping. */}
<p className="mt-1 break-all font-mono text-xs text-text-secondary">{key.fingerprint}</p>
</div>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Button variant="secondary" onClick={() => setShowAssign(true)}>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
+4 -4
View File
@@ -97,12 +97,12 @@ export default function MonitorDetailPage() {
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="mb-6 flex items-start justify-between">
<div>
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<Link href="/monitors" className="text-sm text-text-secondary hover:text-text-primary">
Monitors
</Link>
<div className="mt-2 flex items-center gap-3">
<div className="mt-2 flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-bold text-text-primary">{monitor.name}</h1>
<Badge variant={statusVariant(monitor.state.status)}>{monitor.state.status}</Badge>
<Badge variant="neutral">{monitor.type}</Badge>
@@ -110,7 +110,7 @@ export default function MonitorDetailPage() {
</div>
{monitor.state.message && <p className="mt-1 text-sm text-text-secondary">{monitor.state.message}</p>}
</div>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Link href={`/monitors/${monitorId}/edit`}>
<Button variant="secondary">Edit</Button>
</Link>
+4 -4
View File
@@ -249,13 +249,13 @@ export default function SecretGroupPage() {
</Link>
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="font-mono text-2xl font-bold text-text-primary">{group}</h1>
<p className="mt-1 text-sm text-text-secondary">
<div className="min-w-0">
<h1 className="break-all font-mono text-2xl font-bold text-text-primary">{group}</h1>
<p className="mt-1 break-words text-sm text-text-secondary">
ESO reads this group at <span className="font-mono">GET /api/secrets/{group}/values</span>
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-3">
<Button variant="ghost" onClick={() => setShowYaml(true)}>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
+2 -2
View File
@@ -385,7 +385,7 @@ export default function ServerDetailPage() {
<UpdatesModal updates={server.available_updates} onClose={() => setShowUpdatesModal(false)} onApply={() => applyUpdates()} isApplying={isApplying} applySuccess={applySuccess} />
)}
<div className="mb-6 flex items-start justify-between">
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="flex items-center gap-3">
<Link href="/servers" className="text-text-secondary hover:text-text-primary text-sm">
@@ -398,7 +398,7 @@ export default function ServerDetailPage() {
</div>
<p className="mt-1 font-mono text-sm text-text-secondary">{server.ip_address}</p>
</div>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{/* Rendered disabled rather than hidden when the licence does not
include the console: a customer cannot buy what they cannot see,
and a feature that vanishes reads as a bug. */}
+3 -1
View File
@@ -86,7 +86,9 @@ function SecretsTokenCard({ tokenSet, rotatedAt }: { tokenSet: boolean; rotatedA
return (
<SectionCard title="Secrets read token (ESO)" description="Kubernetes External Secrets Operator authenticates to the read endpoint with this bearer token." icon={<KeyIcon />}>
<p className="mb-4 text-sm text-text-secondary">
{/* break-words, not the default: the read URL is one unbreakable
token, so on a narrow card it overflows rather than wrapping. */}
<p className="mb-4 break-words text-sm text-text-secondary">
Point your <span className="font-mono">ClusterSecretStore</span> at <span className="font-mono text-text-primary">{readUrl}</span>.
</p>
+14 -2
View File
@@ -106,11 +106,23 @@ export function Td({ className, label, children, ...props }: TdProps) {
{...props}
>
{label && (
<span className="hidden font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">
<span className="hidden shrink-0 font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">
{label}
</span>
)}
{children}
{/*
* Below sm the cell is a flex row, and a flex item's default
* min-width:auto resolves to its min-content width. A fingerprint or a
* target URL is a single unbreakable token, so min-content was the whole
* string: the value refused to shrink and spilled out of the card.
* min-w-0 lets it shrink, break-words gives the token somewhere to break.
*
* Above sm the wrapper is display:contents, so the desktop table renders
* exactly as it did before. Every other class here is max-sm: scoped on
* purpose — display:contents generates no box, but inherited properties
* (overflow-wrap among them) would still reach the children.
*/}
<span className="max-sm:min-w-0 max-sm:break-words sm:contents">{children}</span>
</td>
);
}