Skip to content

Commit a0c5ed5

Browse files
committed
improvement(ui): standardize overflow text
1 parent b6a235c commit a0c5ed5

62 files changed

Lines changed: 600 additions & 288 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/emcn-components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +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.
3536

3637
## Modal keyboard defaults
3738

.claude/rules/sim-styling.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=
5050

5151
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
5252

53+
## Text Overflow
54+
55+
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.
56+
57+
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.
58+
5359
## Font Weight
5460

5561
Three steps, Tailwind's stock scale, nothing else: **`font-normal` (400)**, **`font-medium` (500)**, **`font-semibold` (600)**. 400 is the document default, so body text, chip labels, sidebar items, and headings carry **no weight class at all** — they inherit. Reach for a class only to step *up* from body.

.cursor/rules/sim-styling.mdc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=
4444

4545
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
4646

47+
## Text Overflow
48+
49+
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.
50+
51+
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.
52+
4753
## Color Tokens
4854

4955
Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text-icon`; borders `--border-1` (fields) / `--border` (dividers); surfaces `--surface-5` (light) / `--surface-4` (dark); active row `--surface-active`; error `--text-error`. No focus rings on chip surfaces.

apps/sim/app/f/[token]/public-file-view.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useMemo } from 'react'
4-
import { Chip } from '@sim/emcn'
4+
import { Chip, OverflowText } from '@sim/emcn'
55
import { Download } from '@sim/emcn/icons'
66
import Link from 'next/link'
77
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
@@ -85,7 +85,7 @@ export function PublicFileView({
8585
</>
8686
)}
8787
<div className='flex min-w-0 flex-col'>
88-
<span className='truncate text-[14px] text-[var(--text-body)]'>{name}</span>
88+
<OverflowText label={name} className='text-[var(--text-body)] text-sm' />
8989
{provenance ? (
9090
<span className='truncate text-[12px] text-[var(--text-muted)]'>{provenance}</span>
9191
) : null}

apps/sim/app/workspace/[workspaceId]/components/conversation-list-item/conversation-list-item.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactNode } from 'react'
2-
import { cn } from '@sim/emcn'
2+
import { cn, OverflowText } from '@sim/emcn'
33

44
interface ConversationListItemProps {
55
title: string
@@ -23,7 +23,7 @@ export function ConversationListItem({
2323
const showStatusDot = isActive || isUnread
2424
return (
2525
<div className={cn('flex w-full min-w-0 items-center gap-2', className)}>
26-
<span className={cn('min-w-0 flex-1 truncate', titleClassName)}>{title}</span>
26+
<OverflowText label={title} className={cn('flex-1', titleClassName)} />
2727
{showStatusDot && (
2828
<span
2929
aria-hidden='true'
Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,3 @@
11
'use client'
22

3-
import type React from 'react'
4-
import { memo } from 'react'
5-
import { cn, FloatingTooltip, isTextClipped, useFloatingTooltip, useIsOverflowing } from '@sim/emcn'
6-
7-
interface FloatingOverflowTextProps {
8-
/** Full text shown in the tooltip and used as the default visible content. */
9-
label: string
10-
/** Optional custom visible content (e.g. highlighted text); defaults to `label`. */
11-
children?: React.ReactNode
12-
className?: string
13-
/** Forces the tooltip even when the text is not visually clipped (e.g. content truncated upstream). */
14-
showWhen?: boolean
15-
}
16-
17-
/**
18-
* Truncating text that fades its clipped edge and reveals the full value in a
19-
* pointer-reactive floating tooltip on hover or focus.
20-
*/
21-
export const FloatingOverflowText = memo(function FloatingOverflowText({
22-
label,
23-
children,
24-
className,
25-
showWhen,
26-
}: FloatingOverflowTextProps) {
27-
const { ref: textRef, node, isOverflowing } = useIsOverflowing<HTMLSpanElement>()
28-
const { state, handlers } = useFloatingTooltip(() => {
29-
const element = node.current
30-
if (!element || label.length === 0) return false
31-
return Boolean(showWhen) || isTextClipped(element)
32-
})
33-
34-
return (
35-
<>
36-
<span
37-
ref={textRef}
38-
className={cn(
39-
'min-w-0',
40-
isOverflowing &&
41-
'[mask-image:linear-gradient(to_right,black_calc(100%-18px),transparent)] hover:[mask-image:none] focus-visible:[mask-image:none]',
42-
className
43-
)}
44-
{...handlers}
45-
>
46-
{children ?? label}
47-
</span>
48-
<FloatingTooltip label={label} state={state} />
49-
</>
50-
)
51-
})
3+
export { OverflowText as FloatingOverflowText } from '@sim/emcn'

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
DropdownMenuItem,
2525
DropdownMenuTrigger,
2626
FloatingTooltip,
27+
OverflowText,
28+
overflowTextFadeClass,
2729
POPOVER_ANIMATION_CLASSES,
2830
Popover,
2931
PopoverAnchor,
@@ -664,7 +666,7 @@ function BreadcrumbLocationItem({
664666
<span className='size-1.5 rounded-full bg-[var(--text-muted)]' />
665667
)}
666668
</span>
667-
<span className='min-w-0 flex-1 truncate text-left'>{label}</span>
669+
<OverflowText label={label} className='flex-1 text-left' />
668670
</>
669671
)
670672

@@ -702,8 +704,9 @@ const BreadcrumbLabel = memo(
702704
ref={ref}
703705
className={cn(
704706
'min-w-0 truncate text-[var(--text-body)]',
707+
isOverflowing && overflowTextFadeClass,
705708
isOverflowing &&
706-
'[mask-image:linear-gradient(to_right,black_calc(100%-18px),transparent)] group-hover:[mask-image:none] group-focus-visible:[mask-image:none]'
709+
'group-hover:[-webkit-mask-image:none] group-hover:[mask-image:none] group-focus-visible:[-webkit-mask-image:none] group-focus-visible:[mask-image:none]'
707710
)}
708711
>
709712
{label}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo
232232
active={search.highlightedTagIndex === i}
233233
className='max-w-[280px] shrink-0'
234234
>
235-
<FloatingOverflowText label={`${tag.label}: ${tag.value}`} className='block truncate'>
235+
<FloatingOverflowText label={`${tag.label}: ${tag.value}`} className='block'>
236236
{tag.label}: {tag.value}
237237
</FloatingOverflowText>
238238
</Chip>
@@ -330,7 +330,7 @@ export const SortDropdown = memo(function SortDropdown({
330330
}}
331331
>
332332
{Icon && <Icon />}
333-
<FloatingOverflowText label={option.label} className='block truncate' />
333+
<FloatingOverflowText label={option.label} className='block' />
334334
{DirectionIcon && (
335335
<DirectionIcon className='ml-auto size-[12px] text-[var(--text-tertiary)]' />
336336
)}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Folder,
1212
FolderPlus,
1313
Loader,
14+
OverflowText,
1415
Pencil,
1516
Plus,
1617
Trash,
@@ -1969,7 +1970,7 @@ export function Files() {
19691970
multiSelectValues={typeFilter}
19701971
onMultiSelectChange={setTypeFilter}
19711972
overlayContent={
1972-
<span className='truncate text-[var(--text-primary)]'>{typeDisplayLabel}</span>
1973+
<OverflowText label={typeDisplayLabel} className='text-[var(--text-primary)]' />
19731974
}
19741975
showAllOption
19751976
allOptionLabel='All'
@@ -1988,7 +1989,7 @@ export function Files() {
19881989
multiSelectValues={sizeFilter}
19891990
onMultiSelectChange={setSizeFilter}
19901991
overlayContent={
1991-
<span className='truncate text-[var(--text-primary)]'>{sizeDisplayLabel}</span>
1992+
<OverflowText label={sizeDisplayLabel} className='text-[var(--text-primary)]' />
19921993
}
19931994
showAllOption
19941995
allOptionLabel='All'

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
4-
import { ChevronDown, cn, Expandable, ExpandableContent } from '@sim/emcn'
4+
import { ChevronDown, cn, Expandable, ExpandableContent, OverflowText } from '@sim/emcn'
55
import { ShimmerText } from '@/components/ui'
66
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
77
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
@@ -209,7 +209,7 @@ export function AgentGroup({
209209
{isWorking ? (
210210
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
211211
) : (
212-
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
212+
<OverflowText label={headerText} className='text-[var(--text-body)] text-sm' />
213213
)}
214214
<ChevronDown
215215
className={cn(
@@ -226,7 +226,7 @@ export function AgentGroup({
226226
{isWorking ? (
227227
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
228228
) : (
229-
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
229+
<OverflowText label={headerText} className='text-[var(--text-body)] text-sm' />
230230
)}
231231
</div>
232232
)}

0 commit comments

Comments
 (0)