Skip to content

Commit f8f7ce4

Browse files
committed
fix(ui): preserve overflow text interactions
1 parent 1f7918f commit f8f7ce4

18 files changed

Lines changed: 145 additions & 37 deletions

File tree

.claude/rules/emcn-components.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
3232
- **`ChipDatePicker`** — chip-styled date field.
3333
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
3434
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
35-
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, single-line clipping, the conditional 18px edge fade, and the full-value floating tooltip; consumers pass only layout/typography through `className`. Never combine the fade with `truncate`, which paints an ellipsis beneath the mask. Keep ordinary `truncate` for editable or mirrored input values, code/log/path content, dense or virtualized grids, and composite rows where masking the container would also fade icons or actions. Multiline copy uses an intentional `line-clamp-*` treatment instead.
35+
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, single-line clipping, the conditional 18px edge fade, and the full-value floating tooltip; consumers pass only layout/typography through `className`. Never combine the fade with `truncate`, which paints an ellipsis beneath the mask. Keep ordinary `truncate` for editable or mirrored input values, code/log/path content, dense or virtualized grids, and composite rows where masking the container would also fade icons or actions. Multiline copy uses an intentional `line-clamp-*` treatment instead. A non-editable `Combobox` visual overlay passes its plain value through `overlayLabel`; render its visible `OverflowText` as a constrained block with `tooltipEnabled={false}` so the interactive trigger owns the single accessible tooltip.
3636

3737
## Modal keyboard defaults
3838

.claude/rules/sim-styling.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), n
5454

5555
Use `OverflowText` from `@sim/emcn` for a constrained, single-line, read-only human label or title. It owns `min-w-0`, single-line clipping, the conditional edge fade, and the full-value floating tooltip; pass only layout and typography through `className`. Never combine a fade or hand-written `mask-image` with `truncate`, which leaves an ellipsis beneath the mask. Pass the full label to this component instead of shortening it in JavaScript first.
5656

57+
For a non-editable `Combobox` visual overlay, pass the same plain value as `overlayLabel` and render the visible `OverflowText` with `block w-full` (or `block flex-1` beside an icon) plus `tooltipEnabled={false}`. The transparent interactive layer then owns the one reachable full-value tooltip while the visual layer owns the measured fade.
58+
5759
Do not apply the fade universally to editable or mirrored input values, code, logs, paths, filenames that use intentional middle truncation, dense or virtualized grids, or a composite container that also holds icons/actions. Those keep their purpose-built overflow behavior. Multiline copy uses an intentional `line-clamp-*` treatment.
5860

5961
## Font Weight

.cursor/rules/sim-styling.mdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), n
4848

4949
Use `OverflowText` from `@sim/emcn` for a constrained, single-line, read-only human label or title. It owns `min-w-0`, single-line clipping, the conditional edge fade, and the full-value floating tooltip; pass only layout and typography through `className`. Never combine a fade or hand-written `mask-image` with `truncate`, which leaves an ellipsis beneath the mask. Pass the full label to this component instead of shortening it in JavaScript first.
5050

51+
For a non-editable `Combobox` visual overlay, pass the same plain value as `overlayLabel` and render the visible `OverflowText` with `block w-full` (or `block flex-1` beside an icon) plus `tooltipEnabled={false}`. The transparent interactive layer then owns the one reachable full-value tooltip while the visual layer owns the measured fade.
52+
5153
Do not apply the fade universally to editable or mirrored input values, code, logs, paths, filenames that use intentional middle truncation, dense or virtualized grids, or a composite container that also holds icons/actions. Those keep their purpose-built overflow behavior. Multiline copy uses an intentional `line-clamp-*` treatment.
5254

5355
## Color Tokens

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,8 +1969,13 @@ export function Files() {
19691969
multiSelect
19701970
multiSelectValues={typeFilter}
19711971
onMultiSelectChange={setTypeFilter}
1972+
overlayLabel={typeDisplayLabel}
19721973
overlayContent={
1973-
<OverflowText label={typeDisplayLabel} className='text-[var(--text-primary)]' />
1974+
<OverflowText
1975+
label={typeDisplayLabel}
1976+
className='block w-full text-[var(--text-primary)]'
1977+
tooltipEnabled={false}
1978+
/>
19741979
}
19751980
showAllOption
19761981
allOptionLabel='All'
@@ -1988,8 +1993,13 @@ export function Files() {
19881993
multiSelect
19891994
multiSelectValues={sizeFilter}
19901995
onMultiSelectChange={setSizeFilter}
1996+
overlayLabel={sizeDisplayLabel}
19911997
overlayContent={
1992-
<OverflowText label={sizeDisplayLabel} className='text-[var(--text-primary)]' />
1998+
<OverflowText
1999+
label={sizeDisplayLabel}
2000+
className='block w-full text-[var(--text-primary)]'
2001+
tooltipEnabled={false}
2002+
/>
19932003
}
19942004
showAllOption
19952005
allOptionLabel='All'

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,8 +753,13 @@ export function Document({
753753
setEnabledFilter(values)
754754
setSelectedChunks(new Set())
755755
}}
756+
overlayLabel={enabledDisplayLabel}
756757
overlayContent={
757-
<OverflowText label={enabledDisplayLabel} className='text-[var(--text-primary)]' />
758+
<OverflowText
759+
label={enabledDisplayLabel}
760+
className='block w-full text-[var(--text-primary)]'
761+
tooltipEnabled={false}
762+
/>
758763
}
759764
showAllOption
760765
allOptionLabel='All'

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,15 +1464,20 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
14641464
multiSelectValues={selectedStatuses}
14651465
onMultiSelectChange={handleStatusChange}
14661466
placeholder='All statuses'
1467+
overlayLabel={statusDisplayLabel}
14671468
overlayContent={
1468-
<span className='flex items-center gap-1.5 truncate text-[var(--text-primary)]'>
1469+
<span className='flex w-full min-w-0 items-center gap-1.5 text-[var(--text-primary)]'>
14691470
{selectedStatusColor && (
14701471
<div
14711472
className='flex-shrink-0 rounded-[3px]'
14721473
style={{ backgroundColor: selectedStatusColor, width: 8, height: 8 }}
14731474
/>
14741475
)}
1475-
<OverflowText label={statusDisplayLabel} />
1476+
<OverflowText
1477+
label={statusDisplayLabel}
1478+
className='block flex-1'
1479+
tooltipEnabled={false}
1480+
/>
14761481
</span>
14771482
}
14781483
showAllOption
@@ -1489,12 +1494,17 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
14891494
multiSelectValues={workflowIds}
14901495
onMultiSelectChange={setWorkflowIds}
14911496
placeholder='All workflows'
1497+
overlayLabel={workflowDisplayLabel}
14921498
overlayContent={
1493-
<span className='flex items-center gap-1.5 truncate text-[var(--text-primary)]'>
1499+
<span className='flex w-full min-w-0 items-center gap-1.5 text-[var(--text-primary)]'>
14941500
{selectedWorkflow && (
14951501
<Workflow className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
14961502
)}
1497-
<OverflowText label={workflowDisplayLabel} />
1503+
<OverflowText
1504+
label={workflowDisplayLabel}
1505+
className='block flex-1'
1506+
tooltipEnabled={false}
1507+
/>
14981508
</span>
14991509
}
15001510
searchable
@@ -1513,8 +1523,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
15131523
multiSelectValues={folderIds}
15141524
onMultiSelectChange={setFolderIds}
15151525
placeholder='All folders'
1526+
overlayLabel={folderDisplayLabel}
15161527
overlayContent={
1517-
<OverflowText label={folderDisplayLabel} className='text-[var(--text-primary)]' />
1528+
<OverflowText
1529+
label={folderDisplayLabel}
1530+
className='block w-full text-[var(--text-primary)]'
1531+
tooltipEnabled={false}
1532+
/>
15181533
}
15191534
searchable
15201535
searchPlaceholder='Search folders...'
@@ -1532,8 +1547,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
15321547
multiSelectValues={triggers}
15331548
onMultiSelectChange={setTriggers}
15341549
placeholder='All triggers'
1550+
overlayLabel={triggerDisplayLabel}
15351551
overlayContent={
1536-
<OverflowText label={triggerDisplayLabel} className='text-[var(--text-primary)]' />
1552+
<OverflowText
1553+
label={triggerDisplayLabel}
1554+
className='block w-full text-[var(--text-primary)]'
1555+
tooltipEnabled={false}
1556+
/>
15371557
}
15381558
searchable
15391559
searchPlaceholder='Search triggers...'
@@ -1551,8 +1571,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
15511571
value={timeRange}
15521572
onChange={handleTimeRangeChange}
15531573
placeholder='All time'
1574+
overlayLabel={timeDisplayLabel}
15541575
overlayContent={
1555-
<OverflowText label={timeDisplayLabel} className='text-[var(--text-primary)]' />
1576+
<OverflowText
1577+
label={timeDisplayLabel}
1578+
className='block w-full text-[var(--text-primary)]'
1579+
tooltipEnabled={false}
1580+
/>
15561581
}
15571582
className='w-full'
15581583
maxHeight={320}

apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ interface SettingsResourceRowProps {
6464
*/
6565
badge?: ReactNode
6666
/**
67-
* Makes the whole row activatable via a stretched overlay button. `trailing`
67+
* Makes the whole row activatable via a control with a stretched hit area. `trailing`
6868
* stacks above it, so interactive trailing controls (menus, chips) keep
6969
* working — never nest an interactive `trailing` inside a caller-supplied
7070
* wrapper `<button>`, which is invalid HTML.
@@ -156,7 +156,7 @@ export function SettingsResourceRow({
156156
{icon}
157157
</div>
158158
)}
159-
<div className='flex min-w-0 flex-col justify-center gap-[1px] text-left'>
159+
<div className='relative z-10 flex min-w-0 flex-col justify-center gap-[1px] text-left'>
160160
{typeof title === 'string' ? (
161161
<OverflowText label={title} className='text-[var(--text-body)] text-sm' />
162162
) : (
@@ -186,7 +186,7 @@ export function SettingsResourceRow({
186186
// Decoration and the chevron stay click-through so the row's right edge never
187187
// becomes a dead zone; only `trailing` takes pointer events back.
188188
const end = hasEnd ? (
189-
<div className='pointer-events-none relative flex flex-shrink-0 items-center gap-2'>
189+
<div className='pointer-events-none relative z-20 flex flex-shrink-0 items-center gap-2'>
190190
{badge}
191191
{trailing != null && <div className='pointer-events-auto flex items-center'>{trailing}</div>}
192192
{navigable && <ArrowRight className={RESOURCE_ROW_ARROW_CLASSES} />}
@@ -210,14 +210,13 @@ export function SettingsResourceRow({
210210
)
211211
}
212212

213-
// The ring renders on the stretched overlay, which is inset-0 over the row — so a
214-
// keyboard focus outline traces the visible row even though the control is empty.
215-
const overlayClass =
216-
'absolute inset-0 cursor-pointer rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]'
213+
const controlClass = cn(
214+
clusterClass,
215+
'min-w-0 flex-1 cursor-pointer focus-visible:outline-none',
216+
'after:absolute after:inset-0 after:rounded-lg after:content-[""]',
217+
'focus-visible:after:ring-2 focus-visible:after:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]'
218+
)
217219

218-
// The hit area is a stretched overlay rather than a wrapper around the cluster:
219-
// it lets the hover band span the full row (matching every hand-rolled settings
220-
// list) while `trailing` — which may hold its own buttons — stacks above it.
221220
return (
222221
<div
223222
className={cn(
@@ -230,18 +229,21 @@ export function SettingsResourceRow({
230229
href={href}
231230
aria-label={clickLabel}
232231
aria-describedby={description != null ? describedById : undefined}
233-
className={overlayClass}
234-
/>
232+
className={controlClass}
233+
>
234+
{cluster}
235+
</Link>
235236
) : (
236237
<button
237238
type='button'
238239
onClick={onClick}
239240
aria-label={clickLabel}
240241
aria-describedby={description != null ? describedById : undefined}
241-
className={overlayClass}
242-
/>
242+
className={controlClass}
243+
>
244+
{cluster}
245+
</button>
243246
)}
244-
<div className={cn(clusterClass, 'pointer-events-none')}>{cluster}</div>
245247
{end}
246248
</div>
247249
)

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,9 @@ export function WorkflowSidebarBody({
764764
<ArrowLeft className='size-[14px]' />
765765
</Button>
766766
)}
767-
<OverflowText label={title} className='text-[var(--text-primary)] text-small' />
767+
<h2 className='flex min-w-0'>
768+
<OverflowText label={title} className='text-[var(--text-primary)] text-small' />
769+
</h2>
768770
</div>
769771
<Button
770772
variant='ghost'

apps/sim/app/workspace/[workspaceId]/tables/tables.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,8 +692,13 @@ export function Tables() {
692692
multiSelect
693693
multiSelectValues={rowCountFilter}
694694
onMultiSelectChange={setRowCountFilter}
695+
overlayLabel={rowCountDisplayLabel}
695696
overlayContent={
696-
<OverflowText label={rowCountDisplayLabel} className='text-[var(--text-primary)]' />
697+
<OverflowText
698+
label={rowCountDisplayLabel}
699+
className='block w-full text-[var(--text-primary)]'
700+
tooltipEnabled={false}
701+
/>
697702
}
698703
showAllOption
699704
allOptionLabel='All'
@@ -708,8 +713,13 @@ export function Tables() {
708713
multiSelect
709714
multiSelectValues={ownerFilter}
710715
onMultiSelectChange={setOwnerFilter}
716+
overlayLabel={ownerDisplayLabel}
711717
overlayContent={
712-
<OverflowText label={ownerDisplayLabel} className='text-[var(--text-primary)]' />
718+
<OverflowText
719+
label={ownerDisplayLabel}
720+
className='block w-full text-[var(--text-primary)]'
721+
tooltipEnabled={false}
722+
/>
713723
}
714724
searchable
715725
searchPlaceholder='Search members...'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,13 @@ export function OutputSelect({
269269
multiSelectValues={normalizedSelectedValues}
270270
onMultiSelectChange={onOutputSelect}
271271
placeholder={selectedDisplayText}
272+
overlayLabel={selectedDisplayText}
272273
overlayContent={
273-
<OverflowText label={selectedDisplayText} className='text-[var(--text-primary)]' />
274+
<OverflowText
275+
label={selectedDisplayText}
276+
className='block w-full text-[var(--text-primary)]'
277+
tooltipEnabled={false}
278+
/>
274279
}
275280
disabled={disabled || workflowOutputs.length === 0}
276281
align={align}

0 commit comments

Comments
 (0)