From 75664b762fcf47c300a097d4e41c0af83e169750 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 19:18:22 -0700 Subject: [PATCH 1/8] fix(chat): keep the remove badge anchored to the file card The card wrapper had no width cap, so it sized to the filename's max-content width while the card itself capped at 220px. The remove badge is positioned against that wrapper, so a long filename stranded it far to the right of the card it belongs to. Moves the cap onto the wrapper and lets the card fill it. --- .../attached-files-list.test.tsx | 10 ++++++++++ .../attached-files-list/attached-files-list.tsx | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx index d030eb456da..0f681f0a661 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx @@ -67,6 +67,16 @@ describe('AttachedFilesList', () => { expect(container.querySelector('img')).toBeNull() }) + it('caps the card wrapper so a long filename cannot strand the remove badge', () => { + // The badge is positioned against this wrapper. Without a cap here the wrapper + // stretches to the filename's max-content width while the card stays 220px, and + // the badge drifts off to the right of the card. + render([file({ name: '9bacf973-cd64-437b-be12-58be9f2c1a4d-very-long-name.pdf' })]) + + const wrapper = container.querySelector('button')?.parentElement + expect(wrapper?.className).toContain('max-w-[min(220px,100%)]') + }) + it('drops the image and reveals the type icon when the preview fails to decode', () => { render([file({ name: 'photo.heic', type: 'image/heic', previewUrl: '/api/files/serve/x' })]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index c07cf15ee28..699aec455fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -54,7 +54,15 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ return ( -
+ {/* The width cap lives here, not on the button: this wrapper anchors the remove + badge, and sizing it to the button's uncapped max-content width would strand + the badge far to the right of a long filename. */} +
)}
- -

{file.name}

-
+ {/* No width or truncation here — Tooltip.Content already caps and wraps, and this + exists precisely to reveal the name the card truncated. */} + {file.name} ) }) From 14f6225836166b9c5983a3532f16e4b17db300bf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 19:36:46 -0700 Subject: [PATCH 3/8] improvement(chat): restore sent-attachment filenames and align chip tokens - Revert the sent-message attachments to main's styling: the icon-only tile dropped the filename, leaving no way to tell what was sent. - Radii onto the scale: --radius is 8px, so rounded-[10px] was off-system in both files. Outer surfaces use rounded-lg, the nested icon badge rounded-md. - Pill filename uses the named text-xs rather than an arbitrary text-[11px]. - The remove badge is opaque instead of a translucent scrim, so it reads the same over a light card and over a photo rather than compositing with each. --- .../chat-message-attachments.tsx | 40 ++++--------------- .../attached-files-list.tsx | 13 +++--- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx index 9d082014bc3..f540835726c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx @@ -2,35 +2,12 @@ import { cn } from '@sim/emcn' import { getDocumentIcon } from '@/components/icons/document-icons' import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types' -/** - * Tile geometry shared with the thumbnail branches so a mixed row stays uniform. - * - * The border is load-bearing, not decoration: this renders on both the home transcript - * (`--bg`) and the workflow chat panel (`--surface-1`), and against the latter the fill - * is only ~8/255 away in light mode. With an icon-only tile the surface *is* the - * affordance, so it needs an edge — the same reason the user message bubble pairs - * `--surface-5` with a border. - */ -const ATTACHMENT_TILE = - 'size-[56px] overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-5)]' - -/** - * A sent document shows only its type icon. The filename was already read in the - * composer before sending, so repeating it here costs a wide pill in the transcript - * for information the hover title still carries. - */ -function FileAttachmentTile(props: { mediaType: string; filename: string }) { +function FileAttachmentPill(props: { mediaType: string; filename: string }) { const Icon = getDocumentIcon(props.mediaType, props.filename) return ( -
- +
+ + {props.filename}
) } @@ -55,7 +32,7 @@ export function ChatMessageAttachments(props: { {attachments.map((att) => { if (!att.previewUrl) { return ( - + ) } const isVideo = att.media_type.startsWith('video/') @@ -64,10 +41,7 @@ export function ChatMessageAttachments(props: { return (
@@ -83,7 +57,7 @@ export function ChatMessageAttachments(props: { ) } return ( -
+
{att.filename}
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index 937083ea18f..61ce63985c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -18,7 +18,7 @@ import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/ * hover steps further away in the direction each theme reads as "raised". */ const CHIP_SURFACE = - '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)]' + 'relative cursor-pointer rounded-lg border border-[var(--border)] bg-[var(--surface-5)] transition-colors hover-hover:bg-[var(--surface-active)] dark:hover-hover:bg-[var(--surface-6)]' /** Height lives on the wrapper so both shapes are the same size by construction. */ const CHIP_HEIGHT = 'h-[48px]' @@ -113,7 +113,7 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ {/* Steps again on hover: the chip's own hover fill closes to within 7/255 of this badge in light mode, which would erase it during the one interaction where it is being looked at. */} - + @@ -147,10 +147,11 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ onRemoveFile(file.id) }} aria-label={`Remove ${file.name}`} - // Sits inside the chip's top-right corner. A fixed dark scrim rather than a - // surface token: it overlays arbitrary photo content on the thumbnail shape, - // where no theme token can guarantee contrast. - 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' + // Opaque, not a translucent scrim: a semi-transparent fill composites with + // whatever sits under it, so the same badge reads differently over a light + // card than over a photo. An opaque surface plus a border keeps the glyph + // contrast fixed and gives the badge an edge against any thumbnail. + className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-1)] text-[var(--text-body)] opacity-0 transition-opacity group-hover:opacity-100' > From 43c399691a096134b7155d75d5cccf3fb0c15beb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 19:50:03 -0700 Subject: [PATCH 4/8] improvement(chat): tighten composer chip markup and tokens - Remove the chip tooltips: the document card already shows its filename, so the tooltip mostly restated it. - Collapse the single-use height constant and use size-[48px] on the media branch, which was h-[48px] + w-[48px] split across two class strings. - Remove badge moves to --surface-2; --surface-1 sat 8/255 from the chip fill in light mode, reachable on a coarse pointer where the chip's hover-hover fill never applies. Its hover gating now matches the chip's. - py-[7px] so the 32px icon badge fits the 48px box instead of overflowing it. - Trim comments to TSDoc or one-line rationale per the repo rule. --- .../chat-message-attachments.tsx | 2 +- .../attached-files-list.test.tsx | 10 +- .../attached-files-list.tsx | 198 +++++++----------- .../user-input/hooks/use-file-attachments.ts | 18 +- 4 files changed, 91 insertions(+), 137 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx index f540835726c..ff13612456f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-message-attachments/chat-message-attachments.tsx @@ -6,7 +6,7 @@ function FileAttachmentPill(props: { mediaType: string; filename: string }) { const Icon = getDocumentIcon(props.mediaType, props.filename) return (
- + {props.filename}
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx index 0f681f0a661..50d17a5b375 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx @@ -58,9 +58,6 @@ describe('AttachedFilesList', () => { }) it('keeps a HEIC on the thumbnail shape while it has no preview yet', () => { - // The shape is keyed off the type, not the preview: a HEIC gets its preview only - // once the server derivative exists, and switching shape mid-upload would jump the - // layout. It must not fall back to the document card. render([file({ name: 'photo.heic', type: 'image/heic' })]) expect(container.textContent).not.toContain('photo.heic') @@ -68,13 +65,12 @@ describe('AttachedFilesList', () => { }) it('caps the card wrapper so a long filename cannot strand the remove badge', () => { - // The badge is positioned against this wrapper. Without a cap here the wrapper - // stretches to the filename's max-content width while the card stays 220px, and - // the badge drifts off to the right of the card. render([file({ name: '9bacf973-cd64-437b-be12-58be9f2c1a4d-very-long-name.pdf' })]) + // jsdom does no layout, so the cap can only be asserted structurally: it has to sit + // on the wrapper the badge is positioned against, not on the button. const wrapper = container.querySelector('button')?.parentElement - expect(wrapper?.className).toContain('max-w-[min(220px,100%)]') + expect(wrapper?.className).toMatch(/max-w-/) }) it('drops the image and reveals the type icon when the preview fails to decode', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index 61ce63985c2..56b692a5c54 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -1,28 +1,19 @@ 'use client' import React, { useState } from 'react' -import { cn, Loader, Tooltip } from '@sim/emcn' +import { cn, Loader } from '@sim/emcn' import { X } from '@sim/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' /** - * Chrome shared by both chip shapes. Both stand 48px tall so a row mixing thumbnails - * and documents sits on one baseline. - * - * Deliberately NOT `chipFilledFillTokens` (`--surface-5` / `dark:--surface-4`): that - * pair assumes a page background, but this chip sits inside the composer, which is - * already `--surface-4` in dark mode — reusing it would make the chip invisible against - * its own container. `--surface-5` steps away from the composer in both themes, and - * hover steps further away in the direction each theme reads as "raised". + * Chrome shared by both chip shapes. Not `chipFilledFillTokens` — its dark fill is + * `--surface-4`, which is the composer's own background. */ const CHIP_SURFACE = 'relative cursor-pointer rounded-lg border border-[var(--border)] bg-[var(--surface-5)] transition-colors hover-hover:bg-[var(--surface-active)] dark:hover-hover:bg-[var(--surface-6)]' -/** Height lives on the wrapper so both shapes are the same size by construction. */ -const CHIP_HEIGHT = 'h-[48px]' - interface AttachedFilesListProps { attachedFiles: AttachedFile[] onFileClick: (file: AttachedFile) => void @@ -36,11 +27,9 @@ interface AttachedFileChipProps { } /** - * One attachment. - * - * Media renders as a thumbnail; everything else renders as a labelled card — icon - * badge, filename, file type. A document has no thumbnail worth showing, and the - * filename is the thing worth reading. + * One attachment: media renders as a thumbnail, anything else as a labelled card. + * Shape keys off the media type, not preview presence — a HEIC has no preview until its + * server derivative lands, and flipping shape mid-upload would jump the layout. */ const AttachedFileChip = React.memo(function AttachedFileChip({ file, @@ -49,118 +38,89 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ }: AttachedFileChipProps) { const Icon = getDocumentIcon(file.type, file.name) const isVideo = file.type.startsWith('video/') - // Keyed off the type, not the presence of a preview: a HEIC has no preview until its - // upload finishes, and flipping shape mid-upload would jump the layout. const isMedia = isVideo || file.type.startsWith('image/') const extension = getFileExtension(file.name) const [previewFailed, setPreviewFailed] = useState(false) return ( - - {/* Both the size and the width cap live here, not on the button: this wrapper - anchors the remove badge, so sizing it to the button's uncapped max-content - width would strand the badge far to the right of a long filename. */} -
+ - - {!file.uploading && ( - + {extension && ( + + {extension} + + )} + + + )} + {file.uploading && ( + + + )} -
- {/* No width or truncation here — Tooltip.Content already caps and wraps, and this - exists precisely to reveal the name the card truncated. */} - {file.name} -
+ + {!file.uploading && ( + + )} +
) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts index 52e7064cdc8..d8b68c21e4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts @@ -159,8 +159,8 @@ export function useFileAttachments(props: UseFileAttachmentsProps) { if (files.length === 0) return const placeholders: AttachedFile[] = files.map((file) => { - // Resolve once: the browser reports `application/octet-stream` (or nothing) for - // plenty of files, and both the chip and the preview decision key off the type. + /** Resolved once: browsers report `application/octet-stream` (or nothing) for + * plenty of files, and both the chip and the preview decision key off it. */ const type = resolveFileType(file) return { id: generateId(), @@ -211,10 +211,9 @@ export function useFileAttachments(props: UseFileAttachmentsProps) { path: result.path, key: result.key, uploading: false, - // A format the browser cannot decode has no local preview; now that - // the bytes are stored, the serve route can hand back a renderable - // derivative. Anything already previewing keeps its blob URL rather - // than paying a round trip for a thumbnail it can draw locally. + /** A format the browser cannot decode has no local preview; the + * stored bytes now have a renderable derivative. Anything already + * previewing keeps its blob URL. */ previewUrl: f.previewUrl ?? getMothershipAttachmentPreviewUrl({ @@ -352,12 +351,11 @@ export function useFileAttachments(props: UseFileAttachmentsProps) { }, []) /** - * Replaces the current attached files with a given set. - * Cleans up preview URLs from the prior set before replacing. + * Replaces the current attached files with a given set, revoking the prior set's + * preview URLs first. Revoked outside the updater, which must stay pure — React + * double-invokes updaters in StrictMode and may replay them. */ const restoreAttachedFiles = useCallback((files: AttachedFile[]) => { - // Revoked outside the updater: React double-invokes updaters in StrictMode and may - // replay them, so they have to stay pure. attachedFilesRef.current.forEach((f) => revokePreviewUrl(f.previewUrl)) setAttachedFiles(files) }, []) From 839d8ebfa2de057ad54064d0bead2a034820afdc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 19:55:26 -0700 Subject: [PATCH 5/8] fix(chat): keep the remove badge reachable on coarse pointers Gating the reveal on hover-hover alone would hide it from touch entirely, since that variant is fine-pointer only. Instead it is visible by default and only fine pointers get reveal-on-hover, so the badge never depends on an emulated hover. --- .../components/attached-files-list/attached-files-list.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index 56b692a5c54..aa308ca3064 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -115,7 +115,9 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ onRemoveFile(file.id) }} aria-label={`Remove ${file.name}`} - className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)] opacity-0 transition-opacity group-hover:opacity-100' + /* Visible by default so a coarse pointer never has to discover it through an + emulated hover; fine pointers get the reveal-on-hover treatment. */ + className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)] transition-opacity hover-hover:opacity-0 hover-hover:group-hover:opacity-100' > From 159a7063c3a606567a99d280f4ce4052eccaa98c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 20:02:55 -0700 Subject: [PATCH 6/8] fix(chat): reveal the remove control on keyboard focus On a fine pointer the badge is transparent until hover, so tabbing to it left a sighted keyboard user unable to see which attachment Enter would remove. The focus-visible chain carries higher specificity than the hide rule, so it wins regardless of source order. --- .../attached-files-list/attached-files-list.test.tsx | 10 ++++++++++ .../attached-files-list/attached-files-list.tsx | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx index 50d17a5b375..178867c6d58 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx @@ -73,6 +73,16 @@ describe('AttachedFilesList', () => { expect(wrapper?.className).toMatch(/max-w-/) }) + it('reveals the remove control on keyboard focus, not only on hover', () => { + render([file({})]) + + const remove = container.querySelector('button[aria-label^="Remove"]') + // Hidden only where a pointer can reveal it, and focus has to reveal it there too — + // otherwise a keyboard user tabs onto a fully transparent control. + expect(remove?.className).toContain('hover-hover:opacity-0') + expect(remove?.className).toContain('hover-hover:focus-visible:opacity-100') + }) + it('drops the image and reveals the type icon when the preview fails to decode', () => { render([file({ name: 'photo.heic', type: 'image/heic', previewUrl: '/api/files/serve/x' })]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index aa308ca3064..e44d0f5efac 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -116,8 +116,9 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ }} aria-label={`Remove ${file.name}`} /* Visible by default so a coarse pointer never has to discover it through an - emulated hover; fine pointers get the reveal-on-hover treatment. */ - className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)] transition-opacity hover-hover:opacity-0 hover-hover:group-hover:opacity-100' + emulated hover; fine pointers get reveal-on-hover, plus reveal-on-focus so + it is never transparent while it holds the keyboard focus. */ + className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)] transition-opacity hover-hover:opacity-0 hover-hover:focus-visible:opacity-100 hover-hover:group-hover:opacity-100' > From 0a4ac3c6ca06b5c7f15da0f5c9328a04fa34f697 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 20:07:00 -0700 Subject: [PATCH 7/8] improvement(chat): drop the icon badge's own hover step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chip's hover is the only hover affordance needed. The badge fill is now constant, sitting one step below --surface-6 in light mode so the chip's hover fill cannot close on it — which is what the per-badge step was compensating for. --- .../components/attached-files-list/attached-files-list.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index e44d0f5efac..80ab7548f92 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -85,8 +85,10 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ ) : ( <> - {/* Hover steps too: --surface-active is within 7/255 of --surface-6 in light mode. */} - + {/* Fill is constant — the chip's own hover is the only hover affordance. It + sits a step below `--surface-6` in light mode so the chip's hover fill + (`--surface-active`) cannot close on it. */} + From eef32b198453940d14c4566bf773017d9fb4c62d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 6 Aug 2026 20:13:51 -0700 Subject: [PATCH 8/8] fix(chat): stop gating the remove badge on a variant that cannot express it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hover-hover expands to '@media (hover:hover) and (pointer:fine) { &:hover }', so it binds to the element carrying the class. On the badge that meant every rule required hovering the badge itself, making the whole chain dead CSS — the badge was simply always visible. Rather than rebuild the gating, drop it: an always-visible control is reachable on touch and stays visible while holding keyboard focus, which the reveal-on-hover form could not manage without special cases for both. --- .../attached-files-list/attached-files-list.test.tsx | 10 +++++----- .../attached-files-list/attached-files-list.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx index 178867c6d58..2fe1d9c1e7b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.test.tsx @@ -73,14 +73,14 @@ describe('AttachedFilesList', () => { expect(wrapper?.className).toMatch(/max-w-/) }) - it('reveals the remove control on keyboard focus, not only on hover', () => { + it('keeps the remove control visible rather than gating it on hover', () => { render([file({})]) + // A reveal-on-hover badge is unreachable on touch and invisible while it holds + // keyboard focus, so it must not be opacity-gated at all. const remove = container.querySelector('button[aria-label^="Remove"]') - // Hidden only where a pointer can reveal it, and focus has to reveal it there too — - // otherwise a keyboard user tabs onto a fully transparent control. - expect(remove?.className).toContain('hover-hover:opacity-0') - expect(remove?.className).toContain('hover-hover:focus-visible:opacity-100') + expect(remove).not.toBeNull() + expect(remove?.className).not.toMatch(/opacity-0/) }) it('drops the image and reveals the type icon when the preview fails to decode', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index 80ab7548f92..db3f8c36ecd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -117,10 +117,10 @@ const AttachedFileChip = React.memo(function AttachedFileChip({ onRemoveFile(file.id) }} aria-label={`Remove ${file.name}`} - /* Visible by default so a coarse pointer never has to discover it through an - emulated hover; fine pointers get reveal-on-hover, plus reveal-on-focus so - it is never transparent while it holds the keyboard focus. */ - className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)] transition-opacity hover-hover:opacity-0 hover-hover:focus-visible:opacity-100 hover-hover:group-hover:opacity-100' + /* Always visible: reveal-on-hover would hide it from touch and from keyboard + focus, and `hover-hover` cannot express "while the chip is hovered" from + here anyway — it carries its own `&:hover`, so it binds to this element. */ + className='absolute top-[2px] right-[2px] flex size-[16px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-body)]' >