Skip to content

Commit 2cde5db

Browse files
committed
fix(chat): make attachment tiles read on every surface they render on
The sent-message tile went icon-only, which made its fill the whole affordance — and against the workflow chat panel's --surface-1 that fill is ~8/255 away in light mode. Adds the border the user message bubble already pairs with --surface-5 for the same reason. - Restore an accessible name to the sent tiles: an icon-only div with a title attribute announces as nothing. - Step the composer icon badge on hover; the chip's hover fill closed to within 7/255 of it in light mode. - Extension label moves to --text-icon/text-caption; --text-muted was 2.4:1 on this fill in dark mode, well under AA. - Tooltip.Content no longer re-declares the width and truncation it owns — it was truncating the very name it exists to reveal.
1 parent 75664b7 commit 2cde5db

2 files changed

Lines changed: 68 additions & 30 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,35 @@ import { cn } from '@sim/emcn'
22
import { getDocumentIcon } from '@/components/icons/document-icons'
33
import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types'
44

5-
function FileAttachmentPill(props: { mediaType: string; filename: string }) {
5+
/**
6+
* Tile geometry shared with the thumbnail branches so a mixed row stays uniform.
7+
*
8+
* The border is load-bearing, not decoration: this renders on both the home transcript
9+
* (`--bg`) and the workflow chat panel (`--surface-1`), and against the latter the fill
10+
* is only ~8/255 away in light mode. With an icon-only tile the surface *is* the
11+
* affordance, so it needs an edge — the same reason the user message bubble pairs
12+
* `--surface-5` with a border.
13+
*/
14+
const ATTACHMENT_TILE =
15+
'size-[56px] overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-5)]'
16+
17+
/**
18+
* A sent document shows only its type icon. The filename was already read in the
19+
* composer before sending, so repeating it here costs a wide pill in the transcript
20+
* for information the hover title still carries.
21+
*/
22+
function FileAttachmentTile(props: { mediaType: string; filename: string }) {
623
const Icon = getDocumentIcon(props.mediaType, props.filename)
724
return (
8-
<div className='flex max-w-[140px] items-center gap-[5px] rounded-[10px] bg-[var(--surface-5)] px-[6px] py-[3px]'>
9-
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
10-
<span className='truncate text-[11px] text-[var(--text-body)]'>{props.filename}</span>
25+
<div
26+
title={props.filename}
27+
// The icon carries no accessible name on its own, so without these the tile is
28+
// announced as nothing at all — the filename used to be real text.
29+
role='img'
30+
aria-label={props.filename}
31+
className={cn(ATTACHMENT_TILE, 'flex items-center justify-center text-[var(--text-icon)]')}
32+
>
33+
<Icon className='size-[18px]' />
1134
</div>
1235
)
1336
}
@@ -32,7 +55,7 @@ export function ChatMessageAttachments(props: {
3255
{attachments.map((att) => {
3356
if (!att.previewUrl) {
3457
return (
35-
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
58+
<FileAttachmentTile key={att.id} mediaType={att.media_type} filename={att.filename} />
3659
)
3760
}
3861
const isVideo = att.media_type.startsWith('video/')
@@ -41,7 +64,10 @@ export function ChatMessageAttachments(props: {
4164
return (
4265
<div
4366
key={att.id}
44-
className='relative size-[56px] overflow-hidden rounded-[8px] bg-[var(--surface-5)]'
67+
title={att.filename}
68+
role='img'
69+
aria-label={att.filename}
70+
className={cn(ATTACHMENT_TILE, 'relative')}
4571
>
4672
<div className='absolute inset-0 flex items-center justify-center text-[var(--text-icon)]'>
4773
<Icon className='size-[18px]' />
@@ -51,14 +77,14 @@ export function ChatMessageAttachments(props: {
5177
muted
5278
playsInline
5379
preload='metadata'
54-
className='relative h-full w-full object-cover'
80+
className='relative size-full object-cover'
5581
/>
5682
</div>
5783
)
5884
}
5985
return (
60-
<div key={att.id} className='size-[56px] overflow-hidden rounded-[8px]'>
61-
<img src={att.previewUrl} alt={att.filename} className='h-full w-full object-cover' />
86+
<div key={att.id} className={ATTACHMENT_TILE} title={att.filename}>
87+
<img src={att.previewUrl} alt={att.filename} className='size-full object-cover' />
6288
</div>
6389
)
6490
})}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/
1818
* hover steps further away in the direction each theme reads as "raised".
1919
*/
2020
const CHIP_SURFACE =
21-
'relative h-[48px] cursor-pointer rounded-[10px] border border-[var(--border)] bg-[var(--surface-5)] transition-colors hover-hover:bg-[var(--surface-active)] dark:hover-hover:bg-[var(--surface-6)]'
21+
'relative cursor-pointer rounded-[10px] border border-[var(--border)] bg-[var(--surface-5)] transition-colors hover-hover:bg-[var(--surface-active)] dark:hover-hover:bg-[var(--surface-6)]'
22+
23+
/** Height lives on the wrapper so both shapes are the same size by construction. */
24+
const CHIP_HEIGHT = 'h-[48px]'
2225

2326
interface AttachedFilesListProps {
2427
attachedFiles: AttachedFile[]
@@ -54,25 +57,27 @@ const AttachedFileChip = React.memo(function AttachedFileChip({
5457

5558
return (
5659
<Tooltip.Root>
57-
{/* The width cap lives here, not on the button: this wrapper anchors the remove
58-
badge, and sizing it to the button's uncapped max-content width would strand
59-
the badge far to the right of a long filename. */}
60+
{/* Both the size and the width cap live here, not on the button: this wrapper
61+
anchors the remove badge, so sizing it to the button's uncapped max-content
62+
width would strand the badge far to the right of a long filename. */}
6063
<div
6164
className={cn(
6265
'group relative',
63-
isMedia ? 'flex-shrink-0' : 'min-w-0 max-w-[min(220px,100%)]'
66+
CHIP_HEIGHT,
67+
isMedia ? 'w-[48px] shrink-0' : 'min-w-0 max-w-[min(220px,100%)]'
6468
)}
6569
>
6670
<Tooltip.Trigger asChild>
6771
<button
6872
type='button'
6973
className={cn(
7074
CHIP_SURFACE,
75+
'size-full',
7176
isMedia
72-
? 'w-[48px] overflow-hidden'
73-
: // Fills the capped wrapper, so the filename truncates rather than
74-
// widening the card past the badge it is anchored to.
75-
'flex w-full items-center gap-2 py-2 pr-3 pl-2'
77+
? 'overflow-hidden'
78+
: // `pr-5` reserves room for the remove badge so it never sits over the
79+
// filename.
80+
'flex items-center gap-2 py-2 pr-5 pl-2'
7681
)}
7782
onClick={() => onFileClick(file)}
7883
>
@@ -105,17 +110,24 @@ const AttachedFileChip = React.memo(function AttachedFileChip({
105110
</>
106111
) : (
107112
<>
108-
<span className='flex size-[32px] shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)] text-[var(--text-icon)] dark:bg-[var(--surface-3)]'>
113+
{/* Steps again on hover: the chip's own hover fill closes to within
114+
7/255 of this badge in light mode, which would erase it during the
115+
one interaction where it is being looked at. */}
116+
<span className='flex size-[32px] shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)] text-[var(--text-icon)] transition-colors group-hover:bg-[var(--surface-7)] dark:bg-[var(--surface-3)] dark:group-hover:bg-[var(--surface-3)]'>
109117
<Icon className='size-[16px]' />
110118
</span>
111119
<span className='flex min-w-0 flex-col items-start'>
112-
<span className='w-full truncate text-[var(--text-body)] text-small'>
120+
<span className='w-full truncate text-[var(--text-body)] text-small leading-tight'>
113121
{file.name}
114122
</span>
115-
{/* The name truncates, so the extension is genuinely not readable from
116-
it — this is the format, not a restatement of the label. */}
123+
{/* The name truncates from the tail, so the extension is often not
124+
readable from it — this is the format, not a restatement.
125+
`--text-icon`, not `--text-muted`: muted lands at 2.4:1 on this
126+
fill in dark mode, well under AA. */}
117127
{extension && (
118-
<span className='text-[var(--text-muted)] text-xs uppercase'>{extension}</span>
128+
<span className='text-[var(--text-icon)] text-caption uppercase leading-tight'>
129+
{extension}
130+
</span>
119131
)}
120132
</span>
121133
</>
@@ -135,18 +147,18 @@ const AttachedFileChip = React.memo(function AttachedFileChip({
135147
onRemoveFile(file.id)
136148
}}
137149
aria-label={`Remove ${file.name}`}
138-
// Overhangs the chip by 5px, which the composer's `py-2` absorbs. `--surface-6`
139-
// (not the chip's own fill) because this badge sits on the composer shell —
140-
// white in light mode, `--surface-4` in dark — and must read against both.
141-
className='-top-[5px] -right-[5px] absolute flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-6)] text-[var(--text-icon)] opacity-0 transition-opacity group-hover:opacity-100'
150+
// Sits inside the chip's top-right corner. A fixed dark scrim rather than a
151+
// surface token: it overlays arbitrary photo content on the thumbnail shape,
152+
// where no theme token can guarantee contrast.
153+
className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity group-hover:opacity-100'
142154
>
143155
<X className='size-[9px]' />
144156
</button>
145157
)}
146158
</div>
147-
<Tooltip.Content side='top'>
148-
<p className='max-w-[200px] truncate'>{file.name}</p>
149-
</Tooltip.Content>
159+
{/* No width or truncation here — Tooltip.Content already caps and wraps, and this
160+
exists precisely to reveal the name the card truncated. */}
161+
<Tooltip.Content>{file.name}</Tooltip.Content>
150162
</Tooltip.Root>
151163
)
152164
})

0 commit comments

Comments
 (0)