Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
- **`ChipDatePicker`** — chip-styled date field.
- **`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.
- **`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.
- **`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.

## Modal keyboard defaults

Expand Down
8 changes: 8 additions & 0 deletions .claude/rules/sim-styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=

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

## Text Overflow

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.

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.

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.

## Font Weight

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.
Expand Down
8 changes: 8 additions & 0 deletions .cursor/rules/sim-styling.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=

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

## Text Overflow

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.

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.

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.

## Color Tokens

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.
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/f/[token]/public-file-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useMemo } from 'react'
import { Chip } from '@sim/emcn'
import { Chip, OverflowText } from '@sim/emcn'
import { Download } from '@sim/emcn/icons'
import Link from 'next/link'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
Expand Down Expand Up @@ -85,7 +85,7 @@ export function PublicFileView({
</>
)}
<div className='flex min-w-0 flex-col'>
<span className='truncate text-[14px] text-[var(--text-body)]'>{name}</span>
<OverflowText label={name} className='text-[var(--text-body)] text-sm' />
{provenance ? (
<span className='truncate text-[12px] text-[var(--text-muted)]'>{provenance}</span>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
import { cn } from '@sim/emcn'
import { cn, OverflowText } from '@sim/emcn'

interface ConversationListItemProps {
title: string
Expand All @@ -23,7 +23,7 @@ export function ConversationListItem({
const showStatusDot = isActive || isUnread
return (
<div className={cn('flex w-full min-w-0 items-center gap-2', className)}>
<span className={cn('min-w-0 flex-1 truncate', titleClassName)}>{title}</span>
<OverflowText label={title} className={cn('flex-1', titleClassName)} />
{showStatusDot && (
<span
aria-hidden='true'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,3 @@
'use client'

import type React from 'react'
import { memo } from 'react'
import { cn, FloatingTooltip, isTextClipped, useFloatingTooltip, useIsOverflowing } from '@sim/emcn'

interface FloatingOverflowTextProps {
/** Full text shown in the tooltip and used as the default visible content. */
label: string
/** Optional custom visible content (e.g. highlighted text); defaults to `label`. */
children?: React.ReactNode
className?: string
/** Forces the tooltip even when the text is not visually clipped (e.g. content truncated upstream). */
showWhen?: boolean
}

/**
* Truncating text that fades its clipped edge and reveals the full value in a
* pointer-reactive floating tooltip on hover or focus.
*/
export const FloatingOverflowText = memo(function FloatingOverflowText({
label,
children,
className,
showWhen,
}: FloatingOverflowTextProps) {
const { ref: textRef, node, isOverflowing } = useIsOverflowing<HTMLSpanElement>()
const { state, handlers } = useFloatingTooltip(() => {
const element = node.current
if (!element || label.length === 0) return false
return Boolean(showWhen) || isTextClipped(element)
})

return (
<>
<span
ref={textRef}
className={cn(
'min-w-0',
isOverflowing &&
'[mask-image:linear-gradient(to_right,black_calc(100%-18px),transparent)] hover:[mask-image:none] focus-visible:[mask-image:none]',
className
)}
{...handlers}
>
{children ?? label}
</span>
<FloatingTooltip label={label} state={state} />
</>
)
})
export { OverflowText as FloatingOverflowText } from '@sim/emcn'
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
FloatingTooltip,
OverflowText,
overflowTextFadeClass,
POPOVER_ANIMATION_CLASSES,
Popover,
PopoverAnchor,
Expand Down Expand Up @@ -664,7 +666,7 @@ function BreadcrumbLocationItem({
<span className='size-1.5 rounded-full bg-[var(--text-muted)]' />
)}
</span>
<span className='min-w-0 flex-1 truncate text-left'>{label}</span>
<OverflowText label={label} className='flex-1 text-left' />
</>
)

Expand Down Expand Up @@ -702,8 +704,9 @@ const BreadcrumbLabel = memo(
ref={ref}
className={cn(
'min-w-0 truncate text-[var(--text-body)]',
isOverflowing && overflowTextFadeClass,
isOverflowing &&
'[mask-image:linear-gradient(to_right,black_calc(100%-18px),transparent)] group-hover:[mask-image:none] group-focus-visible:[mask-image:none]'
'group-hover:[-webkit-mask-image:none] group-hover:[mask-image:none] group-focus-visible:[-webkit-mask-image:none] group-focus-visible:[mask-image:none]'
)}
>
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ afterEach(() => {
})

describe('SortDropdown', () => {
it('renders long option labels in the truncatable span between both icons', () => {
it('renders long option labels in the overflow span between both icons', () => {
act(() => {
root.render(
<SortDropdown
Expand Down Expand Up @@ -63,7 +63,8 @@ describe('SortDropdown', () => {
const label = item?.querySelector('span')
expect(label).not.toBeNull()
expect(label).toHaveTextContent(LONG_COLUMN_LABEL)
expect(label).toHaveClass('min-w-0', 'block', 'truncate')
expect(label).toHaveClass('min-w-0', 'block', 'overflow-hidden', 'text-clip')
expect(label).not.toHaveClass('truncate')
expect(item?.querySelector('[data-testid="column-icon"]')).not.toBeNull()
expect(item?.querySelectorAll('svg')).toHaveLength(2)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo
active={search.highlightedTagIndex === i}
className='max-w-[280px] shrink-0'
>
<FloatingOverflowText label={`${tag.label}: ${tag.value}`} className='block truncate'>
<FloatingOverflowText label={`${tag.label}: ${tag.value}`} className='block'>
{tag.label}: {tag.value}
</FloatingOverflowText>
</Chip>
Expand Down Expand Up @@ -330,7 +330,7 @@ export const SortDropdown = memo(function SortDropdown({
}}
>
{Icon && <Icon />}
<FloatingOverflowText label={option.label} className='block truncate' />
<FloatingOverflowText label={option.label} className='block' />
{DirectionIcon && (
<DirectionIcon className='ml-auto size-[12px] text-[var(--text-tertiary)]' />
)}
Expand Down
15 changes: 13 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Folder,
FolderPlus,
Loader,
OverflowText,
Pencil,
Plus,
Trash,
Expand Down Expand Up @@ -1968,8 +1969,13 @@ export function Files() {
multiSelect
multiSelectValues={typeFilter}
onMultiSelectChange={setTypeFilter}
overlayLabel={typeDisplayLabel}
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{typeDisplayLabel}</span>
<OverflowText
label={typeDisplayLabel}
className='block w-full text-[var(--text-primary)]'
tooltipEnabled={false}
/>
}
showAllOption
allOptionLabel='All'
Expand All @@ -1987,8 +1993,13 @@ export function Files() {
multiSelect
multiSelectValues={sizeFilter}
onMultiSelectChange={setSizeFilter}
overlayLabel={sizeDisplayLabel}
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{sizeDisplayLabel}</span>
<OverflowText
label={sizeDisplayLabel}
className='block w-full text-[var(--text-primary)]'
tooltipEnabled={false}
/>
}
showAllOption
allOptionLabel='All'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, cn, Expandable, ExpandableContent } from '@sim/emcn'
import { ChevronDown, cn, Expandable, ExpandableContent, OverflowText } from '@sim/emcn'
import { ShimmerText } from '@/components/ui'
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
Expand Down Expand Up @@ -209,7 +209,7 @@ export function AgentGroup({
{isWorking ? (
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
) : (
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
<OverflowText label={headerText} className='text-[var(--text-body)] text-sm' />
)}
<ChevronDown
className={cn(
Expand All @@ -226,7 +226,7 @@ export function AgentGroup({
{isWorking ? (
<ShimmerText className='min-w-0 truncate text-sm'>{headerText}</ShimmerText>
) : (
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{headerText}</span>
<OverflowText label={headerText} className='text-[var(--text-body)] text-sm' />
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Button, PlayOutline, Skeleton, Tooltip, toast } from '@sim/emcn'
import { Button, OverflowText, PlayOutline, Skeleton, Tooltip, toast } from '@sim/emcn'
import {
Download,
FileX,
Expand Down Expand Up @@ -782,7 +782,7 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
className='flex items-center gap-2 rounded-[6px] px-3 py-2 text-left transition-colors hover:bg-[var(--surface-4)]'
>
<WorkflowIcon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate text-[13px] text-[var(--text-primary)]'>{w.name}</span>
<OverflowText label={w.name} className='text-[var(--text-primary)] text-small' />
</button>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import type { ElementType, ReactNode } from 'react'
import { cn } from '@sim/emcn'
import { cn, OverflowText } from '@sim/emcn'
import {
Connections,
Database,
Expand Down Expand Up @@ -54,21 +54,21 @@ function WorkflowDropdownItem({ item }: DropdownItemRenderProps) {
return (
<>
<Workflow className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{item.name}</span>
<OverflowText label={item.name} />
</>
)
}

function DefaultDropdownItem({ item }: DropdownItemRenderProps) {
return <span className='truncate'>{item.name}</span>
return <OverflowText label={item.name} />
}

function FileDropdownItem({ item }: DropdownItemRenderProps) {
const DocIcon = getDocumentIcon('', item.name)
return (
<>
<DocIcon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{item.name}</span>
<OverflowText label={item.name} />
</>
)
}
Expand All @@ -77,7 +77,7 @@ function IconDropdownItem({ item, icon: Icon }: DropdownItemRenderProps & { icon
return (
<>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{item.name}</span>
<OverflowText label={item.name} />
</>
)
}
Expand All @@ -90,11 +90,11 @@ function IconDropdownItem({ item, icon: Icon }: DropdownItemRenderProps & { icon
*/
function IntegrationDropdownItem({ item }: DropdownItemRenderProps) {
const Icon = item.iconComponent as StyleableIcon | undefined
if (!Icon) return <span className='truncate'>{item.name}</span>
if (!Icon) return <OverflowText label={item.name} />
return (
<>
<BrandIcon icon={Icon} className='size-[14px] flex-shrink-0' />
<span className='truncate'>{item.name}</span>
<OverflowText label={item.name} />
</>
)
}
Expand All @@ -115,7 +115,7 @@ function LogDropdownItem({ item }: DropdownItemRenderProps) {
return (
<>
<Library className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{workflowName}</span>
<OverflowText label={workflowName} />
{statusColor && (
<div
aria-hidden
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'use client'

import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
import { Badge, ChipCombobox, ChipConfirmModal, chipContentLabelClass, cn } from '@sim/emcn'
import {
Badge,
ChipCombobox,
ChipConfirmModal,
chipContentLabelClass,
cn,
OverflowText,
} from '@sim/emcn'
import {
ChevronDown,
ChevronUp,
Expand Down Expand Up @@ -746,8 +753,13 @@ export function Document({
setEnabledFilter(values)
setSelectedChunks(new Set())
}}
overlayLabel={enabledDisplayLabel}
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{enabledDisplayLabel}</span>
<OverflowText
label={enabledDisplayLabel}
className='block w-full text-[var(--text-primary)]'
tooltipEnabled={false}
/>
}
showAllOption
allOptionLabel='All'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type ComboboxOption,
cn,
handleKeyboardActivation,
OverflowText,
Search,
} from '@sim/emcn'
import { ArrowLeft, Plus } from '@sim/emcn/icons'
Expand Down Expand Up @@ -508,8 +509,11 @@ function ConnectorTypeCard({ type, config, onClick }: ConnectorTypeCardProps) {
</div>
</div>
<div className='flex min-w-0 flex-1 flex-col'>
<span className='truncate text-[var(--text-body)] text-sm'>{config.name}</span>
<span className='truncate text-[var(--text-muted)] text-caption'>{config.description}</span>
<OverflowText label={config.name} className='text-[var(--text-body)] text-sm' />
<OverflowText
label={config.description}
className='text-[var(--text-muted)] text-caption'
/>
</div>
<ArrowRight className='size-4 flex-shrink-0 text-[var(--text-icon)]' />
</button>
Expand Down
Loading
Loading