Skip to content

Commit 35dfd54

Browse files
authored
fix(ui): recover the text that truncate hides (#288)
Follow-up sweep after the landing hero. Grepped all 25 `truncate` uses in `.tsx`, then measured each in a browser rather than trusting the class list — `truncate` is usually correct, and the landing bug was specific: the hidden text was needed verbatim and nothing could recover it. Measured, not assumed. The worst case is not mobile: settings store, 1440px value 267px shown, 519px hidden (66%) description 267px shown, 226px hidden (46%) doctor, 375px migration msg up to 26px hidden (15%) The settings row hides two thirds of a realistic connection string on a desktop monitor, with no `title` and no way to read it without opening the editor — so the list cannot be scanned for the value you are looking for. Adds `title` to the cases where the value is arbitrary-length and the row is the only place it appears: settings value + description, migration messages, user names + addresses, in-flight upload filenames, and the admin card's url (its only disambiguator when two tools share a label). `title` is a partial remedy — it does nothing on touch. It is the right one here because these are scanning surfaces where the layout should stay a fixed grid; the full value stays reachable through the row's own detail route, and the tooltip removes the need to travel for a glance. Deliberately unchanged: - `CopyableId` — already correct, and the model for the rest: working copy handler, plus `title` and `aria-label` carrying the full value. The landing hero had hand-rolled a worse version of this component. - Permission keys in the role editor — measured 0px clipped across all 19 at 375px; the longest is 23 characters and the column fits it. - Sidebar identity, module names, status labels, the branding preview, the vendored shadcn sidebar, and the dev-only placeholder fixture: short, fixed-vocabulary, decorative, or your own identity. - Doctor's "run command" block truncates four hardcoded short commands that do not clip at any width tested. It has no copy affordance, which is a missing feature rather than hidden text, so it is left for its own change. `Doctor.tsx` is now exactly at the 300-line cap. The next edit there needs a split, not a shorter comment. Verified: make lint clean, 2152 passed / 2 skipped, 127 JS passed. The settings fix re-measured against the same connection string that exposed it — still clipped by design, now carrying the full text in `title`. Claude-Session: https://claude.ai/code/session_01JJtbN97VhtDr28Fuy5JKEF
1 parent 4b09a0a commit 35dfd54

5 files changed

Lines changed: 95 additions & 52 deletions

File tree

‎host/client_app/pages/Admin.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ function ToolCard({ item }: { item: MenuItem }) {
4040
<span className="block truncate font-medium text-foreground group-hover:text-primary">
4141
{item.label}
4242
</span>
43-
<span className="block truncate text-sm text-muted-foreground">{item.url}</span>
43+
{/* The url is the card's only disambiguator when two tools share a
44+
label, so clipping it silently makes them indistinguishable. */}
45+
<span title={item.url} className="block truncate text-sm text-muted-foreground">
46+
{item.url}
47+
</span>
4448
</span>
4549
</Link>
4650
);

‎modules/dashboard/dashboard/pages/components/doctor/MigrationsCard.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ export function MigrationsCard({ migrations, commands, onCopyCommand }: Props) {
7272
{row.module}
7373
</code>
7474
)}
75-
<span className="min-w-0 flex-1 truncate">{row.message}</span>
75+
{/* Clips at narrow widths, and the row offers no other way to
76+
read it — the same remedy the short id above already uses. */}
77+
<span title={row.message} className="min-w-0 flex-1 truncate">
78+
{row.message}
79+
</span>
7680
<span
7781
className={`shrink-0 text-[12.5px] ${
7882
row.applied ? 'text-primary-700' : 'text-amber-700'

‎modules/file_storage/file_storage/pages/components/UploadsCard.tsx‎

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,47 +34,55 @@ export function UploadsCard({ jobs, onCancel, onRetry, onDismiss }: Props) {
3434
{t(keys.file_storage.upload.card_subtitle)}
3535
</span>
3636
</div>
37-
{jobs.map((job) => (
38-
<div key={job.id} className="flex items-center gap-3 text-sm">
39-
<span className="w-40 shrink-0 truncate sm:w-[190px]">{job.name}</span>
40-
{job.status === 'error' ? (
41-
<>
42-
<span className="min-w-0 flex-1 truncate text-destructive">
43-
{job.reason
44-
? t(keys.file_storage.upload.failed_reason, { reason: job.reason })
45-
: t(keys.file_storage.toasts.upload_failed)}
46-
</span>
47-
<Button
48-
variant="link"
49-
size="sm"
50-
className="h-auto p-0 text-primary-700"
51-
onClick={() => onRetry(job.id)}
52-
>
53-
{t(keys.file_storage.upload.retry)}
54-
</Button>
55-
<DismissButton
56-
label={t(keys.file_storage.upload.dismiss)}
57-
onClick={() => onDismiss(job.id)}
58-
/>
59-
</>
60-
) : (
61-
<>
62-
<Progress
63-
value={job.percent}
64-
className="h-1.5 flex-1"
65-
aria-label={t(keys.file_storage.upload.in_progress, { name: job.name })}
66-
/>
67-
<span className="w-11 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
68-
{job.percent}%
69-
</span>
70-
<DismissButton
71-
label={t(keys.file_storage.upload.cancel)}
72-
onClick={() => onCancel(job.id)}
73-
/>
74-
</>
75-
)}
76-
</div>
77-
))}
37+
{jobs.map((job) => {
38+
// Filenames are arbitrary-length and the fixed 160px column clips them,
39+
// while this row is the only place an in-flight upload is named at all.
40+
// The failure text is the server's own reason, equally unbounded.
41+
const failure = job.reason
42+
? t(keys.file_storage.upload.failed_reason, { reason: job.reason })
43+
: t(keys.file_storage.toasts.upload_failed);
44+
return (
45+
<div key={job.id} className="flex items-center gap-3 text-sm">
46+
<span title={job.name} className="w-40 shrink-0 truncate sm:w-[190px]">
47+
{job.name}
48+
</span>
49+
{job.status === 'error' ? (
50+
<>
51+
<span title={failure} className="min-w-0 flex-1 truncate text-destructive">
52+
{failure}
53+
</span>
54+
<Button
55+
variant="link"
56+
size="sm"
57+
className="h-auto p-0 text-primary-700"
58+
onClick={() => onRetry(job.id)}
59+
>
60+
{t(keys.file_storage.upload.retry)}
61+
</Button>
62+
<DismissButton
63+
label={t(keys.file_storage.upload.dismiss)}
64+
onClick={() => onDismiss(job.id)}
65+
/>
66+
</>
67+
) : (
68+
<>
69+
<Progress
70+
value={job.percent}
71+
className="h-1.5 flex-1"
72+
aria-label={t(keys.file_storage.upload.in_progress, { name: job.name })}
73+
/>
74+
<span className="w-11 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
75+
{job.percent}%
76+
</span>
77+
<DismissButton
78+
label={t(keys.file_storage.upload.cancel)}
79+
onClick={() => onCancel(job.id)}
80+
/>
81+
</>
82+
)}
83+
</div>
84+
);
85+
})}
7886
</Card>
7987
);
8088
}

‎modules/settings/settings/pages/components/StoreTable.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,14 @@ export function StoreTable({ settings, onDelete }: Props) {
8585
style={{ background: setting.value }}
8686
/>
8787
)}
88-
<code className="font-mono text-[13px] truncate">{setting.value}</code>
88+
{/* `title` carries the full text: the cell clips most of a
89+
realistic value (a connection string measured 66% hidden at
90+
1440px, not just on phones) and the row's only other route
91+
to it is opening the editor. It repeats what is rendered,
92+
so a masked secret stays masked here too. */}
93+
<code title={setting.value} className="font-mono text-[13px] truncate">
94+
{setting.value}
95+
</code>
8996
</div>
9097
</TableCell>
9198
<TableCell>

‎modules/users/users/admin/components/UserRow.tsx‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ export function UserRow({ user, actions }: Props) {
5555
const { ago, until } = useRelativeTime();
5656
const emptyValue = t(keys.users.common.empty_value);
5757
const invited = user.state === 'invited';
58+
// Both member lines truncate, and this row is the only place a name or an
59+
// address is shown, so each carries its own rendered text in `title`:
60+
// arbitrary-length user data clips on real accounts long before seeded ones.
61+
// Each `title` sits on an element lifted above the link's row overlay (see
62+
// below) — under it, the overlay would answer the hover instead.
63+
const primary = invited ? user.email : user.full_name || user.email.split('@')[0];
64+
const secondary = invited
65+
? t(keys.users.user_row.invited_meta, {
66+
ago: ago(user.invited_at),
67+
until: until(user.invite_expires_at),
68+
})
69+
: user.email;
5870

5971
return (
6072
<TableRow
@@ -77,15 +89,23 @@ export function UserRow({ user, actions }: Props) {
7789
href={`/admin/users/${user.id}`}
7890
className="truncate text-sm font-medium text-foreground before:absolute before:inset-0 before:content-[''] hover:underline focus-visible:underline focus-visible:outline-none"
7991
>
80-
{invited ? user.email : user.full_name || user.email.split('@')[0]}
92+
{/* `relative` puts the text back on top of the link's own overlay
93+
so it answers the hover: on the overlay the tooltip would
94+
belong to the whole row and follow the pointer across every
95+
other cell. Wrapping the text does not stop it truncating —
96+
the anchor is still the element that clips. */}
97+
<span className="relative" title={primary}>
98+
{primary}
99+
</span>
81100
</Link>
82-
<div className="truncate text-[12.5px] text-muted-foreground">
83-
{invited
84-
? t(keys.users.user_row.invited_meta, {
85-
ago: ago(user.invited_at),
86-
until: until(user.invite_expires_at),
87-
})
88-
: user.email}
101+
{/* Lifted for the same reason. The cost is that this one line stops
102+
being part of the row-wide click target; the rest of the row,
103+
including the name above it, still opens the user. */}
104+
<div
105+
title={secondary}
106+
className="relative truncate text-[12.5px] text-muted-foreground"
107+
>
108+
{secondary}
89109
</div>
90110
</div>
91111
</div>

0 commit comments

Comments
 (0)