diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md
index a1ab8b5b93a..2d5babc15df 100644
--- a/.claude/rules/emcn-components.md
+++ b/.claude/rules/emcn-components.md
@@ -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
diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md
index 1670b0bd513..188fc2b1810 100644
--- a/.claude/rules/sim-styling.md
+++ b/.claude/rules/sim-styling.md
@@ -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.
diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc
index 7479c5676e0..d79cc9fd04e 100644
--- a/.cursor/rules/sim-styling.mdc
+++ b/.cursor/rules/sim-styling.mdc
@@ -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.
diff --git a/apps/sim/app/f/[token]/public-file-view.tsx b/apps/sim/app/f/[token]/public-file-view.tsx
index 9f44f508e2e..8247cfe24d7 100644
--- a/apps/sim/app/f/[token]/public-file-view.tsx
+++ b/apps/sim/app/f/[token]/public-file-view.tsx
@@ -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'
@@ -85,7 +85,7 @@ export function PublicFileView({
>
)}
-
{name}
+
{provenance ? (
{provenance}
) : null}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item/conversation-list-item.tsx b/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item/conversation-list-item.tsx
index 0cc2baa0d99..57f5346360c 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item/conversation-list-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item/conversation-list-item.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
-import { cn } from '@sim/emcn'
+import { cn, OverflowText } from '@sim/emcn'
interface ConversationListItemProps {
title: string
@@ -23,7 +23,7 @@ export function ConversationListItem({
const showStatusDot = isActive || isUnread
return (
- {title}
+
{showStatusDot && (
()
- const { state, handlers } = useFloatingTooltip(() => {
- const element = node.current
- if (!element || label.length === 0) return false
- return Boolean(showWhen) || isTextClipped(element)
- })
-
- return (
- <>
-
- {children ?? label}
-
-
- >
- )
-})
+export { OverflowText as FloatingOverflowText } from '@sim/emcn'
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
index 4231b7ad534..5c94470b581 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx
@@ -24,6 +24,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
FloatingTooltip,
+ OverflowText,
+ overflowTextFadeClass,
POPOVER_ANIMATION_CLASSES,
Popover,
PopoverAnchor,
@@ -664,7 +666,7 @@ function BreadcrumbLocationItem({
)}
- {label}
+
>
)
@@ -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}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.test.tsx
index db10cad80a4..079c0677838 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.test.tsx
@@ -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(
{
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)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx
index 6f30372b129..53f0ea69706 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx
@@ -232,7 +232,7 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo
active={search.highlightedTagIndex === i}
className='max-w-[280px] shrink-0'
>
-
+
{tag.label}: {tag.value}
@@ -330,7 +330,7 @@ export const SortDropdown = memo(function SortDropdown({
}}
>
{Icon && }
-
+
{DirectionIcon && (
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index bef21a3c6d8..f81334b9512 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -11,6 +11,7 @@ import {
Folder,
FolderPlus,
Loader,
+ OverflowText,
Pencil,
Plus,
Trash,
@@ -1968,8 +1969,13 @@ export function Files() {
multiSelect
multiSelectValues={typeFilter}
onMultiSelectChange={setTypeFilter}
+ overlayLabel={typeDisplayLabel}
overlayContent={
- {typeDisplayLabel}
+
}
showAllOption
allOptionLabel='All'
@@ -1987,8 +1993,13 @@ export function Files() {
multiSelect
multiSelectValues={sizeFilter}
onMultiSelectChange={setSizeFilter}
+ overlayLabel={sizeDisplayLabel}
overlayContent={
- {sizeDisplayLabel}
+
}
showAllOption
allOptionLabel='All'
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx
index f454093ca96..39f3eb1e3eb 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx
@@ -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'
@@ -209,7 +209,7 @@ export function AgentGroup({
{isWorking ? (
{headerText}
) : (
- {headerText}
+
)}
{headerText}
) : (
- {headerText}
+
)}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
index b1fb3d08812..25286e6e8c3 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
@@ -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,
@@ -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)]'
>
-
{w.name}
+
))}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
index 029c33f90f7..527c6d4cf3a 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
@@ -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,
@@ -54,13 +54,13 @@ function WorkflowDropdownItem({ item }: DropdownItemRenderProps) {
return (
<>
- {item.name}
+
>
)
}
function DefaultDropdownItem({ item }: DropdownItemRenderProps) {
- return {item.name}
+ return
}
function FileDropdownItem({ item }: DropdownItemRenderProps) {
@@ -68,7 +68,7 @@ function FileDropdownItem({ item }: DropdownItemRenderProps) {
return (
<>
- {item.name}
+
>
)
}
@@ -77,7 +77,7 @@ function IconDropdownItem({ item, icon: Icon }: DropdownItemRenderProps & { icon
return (
<>
- {item.name}
+
>
)
}
@@ -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 {item.name}
+ if (!Icon) return
return (
<>
- {item.name}
+
>
)
}
@@ -115,7 +115,7 @@ function LogDropdownItem({ item }: DropdownItemRenderProps) {
return (
<>
- {workflowName}
+
{statusColor && (
{enabledDisplayLabel}
+
}
showAllOption
allOptionLabel='All'
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
index 7d6566de35d..faa56a106b9 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
@@ -18,6 +18,7 @@ import {
type ComboboxOption,
cn,
handleKeyboardActivation,
+ OverflowText,
Search,
} from '@sim/emcn'
import { ArrowLeft, Plus } from '@sim/emcn/icons'
@@ -508,8 +509,11 @@ function ConnectorTypeCard({ type, config, onClick }: ConnectorTypeCardProps) {
- {config.name}
- {config.description}
+
+
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
index bbca18b7547..2121d040e41 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.test.tsx
@@ -60,6 +60,9 @@ vi.mock('@sim/emcn', () => ({
DropdownMenuContent: ({ children }: { children?: ReactNode }) => {children}
,
DropdownMenuItem: ({ children }: { children?: ReactNode }) => {children}
,
DropdownMenuTrigger: ({ children }: { children?: ReactNode }) => {children}
,
+ OverflowText: ({ label, children }: { label: string; children?: ReactNode }) => (
+ {children ?? label}
+ ),
Tooltip: {
Root: ({ children }: { children?: ReactNode }) => {children}
,
Trigger: ({ children }: { children?: ReactNode }) => {children}
,
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
index e5efda502a3..443a7cc4b20 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
@@ -11,6 +11,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
+ OverflowText,
Tooltip,
} from '@sim/emcn'
import {
@@ -362,7 +363,7 @@ function ConnectorCard({
- {connectorDef?.name || connector.connectorType}
+
{syncInFlight && }
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
index 62b0934018f..83a53d724c0 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
@@ -401,7 +401,7 @@ function DashboardInner({ stats, isLoading, error, searchQuery }: DashboardProps
-
Runs
+
Runs
{globalDetails && (
{globalDetails.totalRuns}
@@ -426,7 +426,7 @@ function DashboardInner({ stats, isLoading, error, searchQuery }: DashboardProps
-
Errors
+
Errors
{globalDetails && (
{globalDetails.totalErrors}
@@ -451,7 +451,7 @@ function DashboardInner({ stats, isLoading, error, searchQuery }: DashboardProps
-
Latency
+
Latency
{globalDetails && (
{formatLatency(globalDetails.avgLatency)}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
index 6f78db43a6d..8be63a40c5c 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@@ -18,6 +18,7 @@ import {
type ComboboxOption,
cn,
Library,
+ OverflowText,
Popover,
PopoverAnchor,
PopoverContent,
@@ -1463,15 +1464,20 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
multiSelectValues={selectedStatuses}
onMultiSelectChange={handleStatusChange}
placeholder='All statuses'
+ overlayLabel={statusDisplayLabel}
overlayContent={
-
+
{selectedStatusColor && (
)}
- {statusDisplayLabel}
+
}
showAllOption
@@ -1488,12 +1494,17 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
multiSelectValues={workflowIds}
onMultiSelectChange={setWorkflowIds}
placeholder='All workflows'
+ overlayLabel={workflowDisplayLabel}
overlayContent={
-
+
{selectedWorkflow && (
)}
- {workflowDisplayLabel}
+
}
searchable
@@ -1512,8 +1523,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
multiSelectValues={folderIds}
onMultiSelectChange={setFolderIds}
placeholder='All folders'
+ overlayLabel={folderDisplayLabel}
overlayContent={
- {folderDisplayLabel}
+
}
searchable
searchPlaceholder='Search folders...'
@@ -1531,8 +1547,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
multiSelectValues={triggers}
onMultiSelectChange={setTriggers}
placeholder='All triggers'
+ overlayLabel={triggerDisplayLabel}
overlayContent={
- {triggerDisplayLabel}
+
}
searchable
searchPlaceholder='Search triggers...'
@@ -1550,8 +1571,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='All time'
+ overlayLabel={timeDisplayLabel}
overlayContent={
- {timeDisplayLabel}
+
}
className='w-full'
maxHeight={320}
@@ -1627,7 +1653,7 @@ function SuggestionButton({
}}
>
-
{suggestion.label}
+
{showCategory && suggestion.value !== suggestion.label && (
{suggestion.category === 'workflow' || suggestion.category === 'folder'
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
index cf39c45cd98..860cbb3c53f 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
@@ -65,14 +65,14 @@ function ActivityLogRow({
{typeof entry.description === 'string' ? (
-
+
) : (
entry.description
)}
{typeof entry.actor === 'string' ? (
-
+
) : (
{entry.actor}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
index 98652eff6ab..4cbe2f03ed2 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
@@ -11,6 +11,7 @@ import {
ChipModalField,
ChipSelect,
Label,
+ OverflowText,
Search,
Switch,
toast,
@@ -219,8 +220,8 @@ export function Admin() {
const renderUserRow = (u: AdminUser) => (
- {u.name || '—'}
- {u.email}
+
+
{u.role || 'user'}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.test.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.test.tsx
index 4b75e1440d0..e6a32a8d2f4 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.test.tsx
@@ -48,6 +48,9 @@ vi.mock('@sim/emcn', () => ({
Label: ({ children, htmlFor }: { children: ReactNode; htmlFor?: string }) => (
{children}
),
+ OverflowText: ({ label, children }: { label: string; children?: ReactNode }) => (
+ {children ?? label}
+ ),
Switch: ({
checked,
disabled,
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
index b0fc26e75d9..a930f107b05 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
@@ -8,6 +8,7 @@ import {
chipVariants,
cn,
Label,
+ OverflowText,
Switch,
Tooltip,
toast,
@@ -480,8 +481,8 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
- {planTitle}
- {priceText}
+
+
{!subscription.isEnterprise &&
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/passwords-view/passwords-view.test.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/passwords-view/passwords-view.test.tsx
index b334e010c14..88c7238866b 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/passwords-view/passwords-view.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/passwords-view/passwords-view.test.tsx
@@ -34,6 +34,9 @@ vi.mock('@sim/emcn', () => ({
) : null,
Key: () =>
,
+ OverflowText: ({ label, children }: { label: string; children?: ReactNode }) => (
+
{children ?? label}
+ ),
Plus: () =>
,
toast: mockToast,
}))
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal.tsx
index b3fc1d0e577..e29cee913d8 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal.tsx
@@ -1,6 +1,13 @@
'use client'
-import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@sim/emcn'
+import {
+ Chip,
+ ChipModal,
+ ChipModalBody,
+ ChipModalFooter,
+ ChipModalHeader,
+ OverflowText,
+} from '@sim/emcn'
import type {
BYOKManagerCapabilities,
BYOKManagerKey,
@@ -53,12 +60,14 @@ export function BYOKProviderKeysModal({
{keys.map((key) => (
-
- {key.name ?? 'Unnamed key'}
-
-
- {key.maskedKey}
-
+
+
{(capabilities.update || capabilities.delete) && (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx
index 8966401f6b5..718e3342866 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx
@@ -1,11 +1,11 @@
'use client'
import type { ReactNode } from 'react'
+import { OverflowText } from '@sim/emcn'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
const ROW_CLASSES = 'flex items-center gap-2.5 p-2'
-const ROW_EMAIL_CLASSES = 'min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'
const ROW_STATUS_CLASSES = 'flex-shrink-0 text-[var(--text-muted)] text-caption'
interface MemberAvatarProps {
@@ -57,7 +57,7 @@ export function MemberRow({ name, email, image, status, roleControl, menu }: Mem
return (
- {email}
+
{status}
{roleControl}
{menu}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx
index f05e8b3c73e..3753caed70f 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx
@@ -1,5 +1,5 @@
import { type ReactNode, useId } from 'react'
-import { cn } from '@sim/emcn'
+import { cn, OverflowText } from '@sim/emcn'
import { ArrowRight } from '@sim/emcn/icons'
import Link from 'next/link'
import {
@@ -64,7 +64,7 @@ interface SettingsResourceRowProps {
*/
badge?: ReactNode
/**
- * Makes the whole row activatable via a stretched overlay button. `trailing`
+ * Makes the whole row activatable via a control with a stretched hit area. `trailing`
* stacks above it, so interactive trailing controls (menus, chips) keep
* working — never nest an interactive `trailing` inside a caller-supplied
* wrapper ``, which is invalid HTML.
@@ -137,6 +137,7 @@ export function SettingsResourceRow({
}: SettingsResourceRowProps) {
const describedById = useId()
const isTile = iconVariant === 'tile'
+ const isActivatable = !disabled && Boolean(onClick || href)
const cluster = (
<>
{icon == null ? null : iconVariant === 'custom' ? (
@@ -156,13 +157,29 @@ export function SettingsResourceRow({
{icon}
)}
-
-
{title}
- {description != null && (
-
- {description}
-
+
+ {typeof title === 'string' ? (
+
+ ) : (
+ {title}
)}
+ {description != null &&
+ (typeof description === 'string' ? (
+
+
+
+ ) : (
+
+ {description}
+
+ ))}
>
)
@@ -174,7 +191,7 @@ export function SettingsResourceRow({
// Decoration and the chevron stay click-through so the row's right edge never
// becomes a dead zone; only `trailing` takes pointer events back.
const end = hasEnd ? (
-
+
{badge}
{trailing != null &&
{trailing}
}
{navigable &&
}
@@ -198,14 +215,13 @@ export function SettingsResourceRow({
)
}
- // The ring renders on the stretched overlay, which is inset-0 over the row — so a
- // keyboard focus outline traces the visible row even though the control is empty.
- const overlayClass =
- '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)]'
+ const controlClass = cn(
+ clusterClass,
+ 'min-w-0 flex-1 cursor-pointer focus-visible:outline-none',
+ 'after:absolute after:inset-0 after:rounded-lg after:content-[""]',
+ 'focus-visible:after:ring-2 focus-visible:after:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]'
+ )
- // The hit area is a stretched overlay rather than a wrapper around the cluster:
- // it lets the hover band span the full row (matching every hand-rolled settings
- // list) while `trailing` — which may hold its own buttons — stacks above it.
return (
+ className={controlClass}
+ >
+ {cluster}
+
) : (
+ className={controlClass}
+ >
+ {cluster}
+
)}
-
{cluster}
{end}
)
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/transfer-ownership-dialog/transfer-ownership-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/transfer-ownership-dialog/transfer-ownership-dialog.tsx
index 2c2d91637aa..5f2b20d33c4 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/transfer-ownership-dialog/transfer-ownership-dialog.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/transfer-ownership-dialog/transfer-ownership-dialog.tsx
@@ -10,6 +10,7 @@ import {
ChipConfirmModal,
ChipInput,
cn,
+ OverflowText,
Search,
Skeleton,
} from '@sim/emcn'
@@ -182,18 +183,20 @@ export function TransferOwnershipDialog({
-
- {m.name}
-
+
{m.role === 'admin' && (
Admin
)}
-
- {m.email}
-
+
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/secrets/[credentialId]/components/secret-usage-panel/secret-usage-panel.tsx b/apps/sim/app/workspace/[workspaceId]/settings/secrets/[credentialId]/components/secret-usage-panel/secret-usage-panel.tsx
index 1e59583d091..03fd1f8bf56 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/secrets/[credentialId]/components/secret-usage-panel/secret-usage-panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/secrets/[credentialId]/components/secret-usage-panel/secret-usage-panel.tsx
@@ -63,7 +63,7 @@ export function SecretUsagePanel({ workspaceId, secretName, scope }: SecretUsage
*/
description: (
-
+
{entry.useCount > 1 && (
{entry.useCount} runs
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx
index 42b51b0db3a..b142da251b4 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx
@@ -9,6 +9,7 @@ import {
CollapsibleCard,
FieldDivider,
Label,
+ OverflowText,
Switch,
toast,
} from '@sim/emcn'
@@ -242,7 +243,7 @@ export function EnrichmentConfig({
>
- {enrichment.name}
+
- {option.name}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx
index cffa4a09d1f..8bb600b66d4 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx
@@ -8,6 +8,7 @@ import {
chipContentLabelClass,
chipVariants,
cn,
+ OverflowText,
POPOVER_ANIMATION_CLASSES,
Popover,
PopoverAnchor,
@@ -197,7 +198,7 @@ export const ViewsMenu = memo(function ViewsMenu({
- New view
+
>
)}
@@ -268,7 +269,7 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
{isActive && }
- {label}
+
{actionCount > 0 && (
)}
-
{title}
+
+
+
{rowCountDisplayLabel}
+
}
showAllOption
allOptionLabel='All'
@@ -708,8 +713,13 @@ export function Tables() {
multiSelect
multiSelectValues={ownerFilter}
onMultiSelectChange={setOwnerFilter}
+ overlayLabel={ownerDisplayLabel}
overlayContent={
- {ownerDisplayLabel}
+
}
searchable
searchPlaceholder='Search members...'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
index f9fb46787dc..ab8d0f9f7d1 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
@@ -1,7 +1,7 @@
'use client'
import { useMemo } from 'react'
-import { ChipCombobox, Combobox, type ComboboxOptionGroup, cn } from '@sim/emcn'
+import { ChipCombobox, Combobox, type ComboboxOptionGroup, cn, OverflowText } from '@sim/emcn'
import { useShallow } from 'zustand/react/shallow'
import {
type FlattenOutputsBlockInput,
@@ -269,8 +269,13 @@ export function OutputSelect({
multiSelectValues={normalizedSelectedValues}
onMultiSelectChange={onOutputSelect}
placeholder={selectedDisplayText}
+ overlayLabel={selectedDisplayText}
overlayContent={
- {selectedDisplayText}
+
}
disabled={disabled || workflowOutputs.length === 0}
align={align}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx
index d3ca82aac63..b3e98aed297 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx
@@ -5,6 +5,7 @@ import {
Button,
cn,
Input,
+ OverflowText,
Popover,
PopoverContent,
PopoverItem,
@@ -296,7 +297,7 @@ export function Versions({
v{v.version}
- {v.name && {v.name} }
+ {v.name && }
{rowLabel && (
({rowLabel})
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
index 97321b5b4a4..a3cdb695183 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
import {
cn,
+ OverflowText,
Popover,
PopoverAnchor,
PopoverContent,
@@ -442,7 +443,7 @@ const FolderContentsInner: React.FC = ({
}
}}
>
- {currentNestedTag.display}
+
)}
@@ -482,7 +483,7 @@ const FolderContentsInner: React.FC = ({
}
}}
>
- {child.display}
+
{childType && childType !== 'any' && (
{childType}
@@ -518,7 +519,7 @@ const FolderContentsInner: React.FC = ({
}
}}
>
- {nestedChild.display}
+
{'>'}
)
@@ -673,7 +674,7 @@ const NestedTagRenderer: React.FC = ({
}
}}
>
- {nestedTag.display}
+
{tagDescription && tagDescription !== 'any' && (
{tagDescription}
@@ -741,9 +742,12 @@ const VariableTagItem: React.FC<{
}
}}
>
-
- {tag.startsWith(TAG_PREFIXES.VARIABLE) ? tag.substring(TAG_PREFIXES.VARIABLE.length) : tag}
-
+
{variableInfo && (
{variableInfo.type}
@@ -800,7 +804,7 @@ const BlockRootTagItem: React.FC<{
fallbackLabel={blockName.charAt(0).toUpperCase()}
size='sm'
/>
- {blockName}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx
index 05bf4b46d31..2e588c683b6 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx
@@ -19,6 +19,7 @@ import {
ExpandableContent,
handleKeyboardActivation,
Info,
+ OverflowText,
} from '@sim/emcn'
import { ChevronDown, Search } from '@sim/emcn/icons'
import clsx from 'clsx'
@@ -134,7 +135,7 @@ const ToolbarItem = memo(function ToolbarItem({
bgColor={item.bgColor}
data-toolbar-item-icon=''
/>
- {item.name}
+
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
index ae2ce8f6fa0..e543ac8e94c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
@@ -13,6 +13,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
Loader,
+ OverflowText,
} from '@sim/emcn'
import { Folder, MoreHorizontal, Pencil, Pin, Plus, SquareArrowUpRight } from '@sim/emcn/icons'
import Link from 'next/link'
@@ -95,7 +96,7 @@ function CollapsedFlyoutRows({
- {entry.name}
+
{entry.pinned && }
@@ -106,7 +107,7 @@ function CollapsedFlyoutRows({
return (
- {entry.name}
+
{entry.pinned && }
)
@@ -116,7 +117,7 @@ function CollapsedFlyoutRows({
- {entry.name}
+
{entry.pinned && }
@@ -463,7 +464,7 @@ export function CollapsedWorkflowFlyoutItem({
: undefined
}
>
- {workflow.name}
+
)
@@ -520,7 +521,7 @@ export function CollapsedFolderItems(props: CollapsedFolderItemsProps) {
return (
- {folder.name}
+
)
}
@@ -529,7 +530,7 @@ export function CollapsedFolderItems(props: CollapsedFolderItemsProps) {
- {folder.name}
+
{interleaveSiblings(folder.children, folderWorkflows).map((child) =>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
index c67b2089917..4b19b943f38 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx
@@ -2,6 +2,7 @@
import type { ComponentType } from 'react'
import { memo } from 'react'
+import { OverflowText } from '@sim/emcn'
import { File, Workflow } from '@sim/emcn/icons'
import { Command } from 'cmdk'
import { HEX_COLOR_REGEX } from '@/lib/branding'
@@ -33,13 +34,14 @@ function ItemFolderPath({ folderPath }: ItemFolderPathProps) {
{folderPath.length > 1 && (
<>
-
- {folderPath.slice(0, -1).join(' / ')}
-
+
/
>
)}
- {folderPath[folderPath.length - 1]}
+
)
}
@@ -91,10 +93,13 @@ export const MemoizedCommandItem = memo(
return (
-
+
{labelPrefix && {labelPrefix} }
{label}
-
+
{meta ? : null}
)
@@ -127,7 +132,7 @@ export const MemoizedActionItem = memo(
return (
- {name}
+
{meta ? : shortcut ? : null}
)
@@ -161,7 +166,7 @@ export const MemoizedWorkflowItem = memo(
- {name}
+
{isCurrent && (current) }
{meta ? (
@@ -199,7 +204,7 @@ export const MemoizedFileItem = memo(
- {name}
+
{meta ? (
@@ -229,7 +234,7 @@ export const MemoizedTaskItem = memo(
} & ResultMetaProps) {
return (
- {name}
+
{meta && }
)
@@ -278,7 +283,7 @@ export const MemoizedWorkspaceItem = memo(
)}
- {name}
+
{isCurrent && (current) }
{meta &&
}
@@ -312,7 +317,7 @@ export const MemoizedPageItem = memo(
return (
- {name}
+
{meta ? : shortcut ? : null}
)
@@ -344,7 +349,7 @@ export const MemoizedIconItem = memo(
- {name}
+
{meta ? (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
index 5bebaba2fb6..60c26715a5b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
@@ -7,6 +7,7 @@ import {
chipIconSlotClass,
chipVariants,
cn,
+ OverflowText,
} from '@sim/emcn'
import { ChevronLeft } from '@sim/emcn/icons'
import { type QueryClient, useQueryClient } from '@tanstack/react-query'
@@ -345,7 +346,7 @@ export function SettingsSidebar({
- Back
+ Back
@@ -396,9 +397,11 @@ export function SettingsSidebar({
const content = (
<>
-
- {item.label}
-
+
{isLocked && (
Max
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx
index 41fa904a83e..3f32ddbbaae 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx
@@ -1,7 +1,7 @@
'use client'
import { type ReactNode, useState } from 'react'
-import { cn, disclosureChevronClass, Expandable, ExpandableContent } from '@sim/emcn'
+import { cn, disclosureChevronClass, Expandable, ExpandableContent, OverflowText } from '@sim/emcn'
import { ChevronDown } from '@sim/emcn/icons'
/**
@@ -60,9 +60,10 @@ export function SidebarSection({
}
const label = (
-
- {title}
-
+
)
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
index 4877ba28400..78b0987f7f7 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
@@ -1,7 +1,14 @@
'use client'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
-import { chipContentIconClass, chipVariants, cn, disclosureChevronClass, toast } from '@sim/emcn'
+import {
+ chipContentIconClass,
+ chipVariants,
+ cn,
+ disclosureChevronClass,
+ OverflowText,
+ toast,
+} from '@sim/emcn'
import { ChevronRight, Folder, FolderOpen, Lock, MoreHorizontal } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
@@ -535,13 +542,11 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold
/>
) : (
-
-
- {folder.name}
-
+
+
{folder.locked && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
index 009052d2d44..8992c70e3fc 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
@@ -1,7 +1,7 @@
'use client'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
-import { chipVariants, cn } from '@sim/emcn'
+import { chipVariants, cn, OverflowText } from '@sim/emcn'
import { Lock, MoreHorizontal } from '@sim/emcn/icons'
import clsx from 'clsx'
import Link from 'next/link'
@@ -441,11 +441,8 @@ export const WorkflowItem = memo(function WorkflowItem({
spellCheck='false'
/>
) : (
-
- {workflow.name}
+
+
)}
{!isEditing &&
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal.tsx
index 541db1b29e5..2a63f4f848d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal.tsx
@@ -1,6 +1,14 @@
'use client'
-import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader, toast } from '@sim/emcn'
+import {
+ Chip,
+ ChipModal,
+ ChipModalBody,
+ ChipModalFooter,
+ ChipModalHeader,
+ OverflowText,
+ toast,
+} from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { useRouter } from 'next/navigation'
@@ -100,10 +108,14 @@ export function ViewInvitationsModal({ open, onOpenChange }: ViewInvitationsModa
invitations.map((inv) => (
-
{invitationLabel(inv)}
-
- {invitationSubLabel(inv)}
-
+
+
)}
-
- {workspace.name}
-
+
{/* Pin and options share one fixed slot, as the chat rows do:
the trailing width never changes, so pinning cannot re-truncate
the name under the user's cursor. */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index 4d06bf87c44..d8c707f0e81 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -14,6 +14,7 @@ import {
Home,
Library,
Loader,
+ OverflowText,
Skeleton,
Tooltip,
Upload,
@@ -275,7 +276,7 @@ const SidebarChatItem = memo(function SidebarChatItem({
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
- {chat.name}
+
{chat.id !== 'new' && (
{showStatusDot && (
diff --git a/apps/sim/components/permissions/member-row.tsx b/apps/sim/components/permissions/member-row.tsx
index 8e184e5633a..50308f7b05b 100644
--- a/apps/sim/components/permissions/member-row.tsx
+++ b/apps/sim/components/permissions/member-row.tsx
@@ -1,6 +1,6 @@
'use client'
-import { Avatar, AvatarFallback, Chip, ChipDropdown, cn } from '@sim/emcn'
+import { Avatar, AvatarFallback, Chip, ChipDropdown, cn, OverflowText } from '@sim/emcn'
import { getUserColor } from '@/lib/workspaces/colors'
import type { MemberRole } from './member-role-options'
import { RoleLockTooltip } from './role-lock'
@@ -63,12 +63,14 @@ export function MemberRow
({
-
- {member.userName || member.userEmail || member.userId}
-
-
- {member.userEmail || member.userId}
-
+
+
diff --git a/apps/sim/components/settings/settings-sidebar.tsx b/apps/sim/components/settings/settings-sidebar.tsx
index b64bf54a1b2..b51c8399594 100644
--- a/apps/sim/components/settings/settings-sidebar.tsx
+++ b/apps/sim/components/settings/settings-sidebar.tsx
@@ -1,7 +1,14 @@
'use client'
import { useEffect, useRef, useState } from 'react'
-import { ChipConfirmModal, chipIconSlotClass, chipVariants, cn, Tooltip } from '@sim/emcn'
+import {
+ ChipConfirmModal,
+ chipIconSlotClass,
+ chipVariants,
+ cn,
+ OverflowText,
+ Tooltip,
+} from '@sim/emcn'
import { ChevronLeft } from '@sim/emcn/icons'
import { useRouter } from 'next/navigation'
import {
@@ -119,7 +126,7 @@ export function SettingsSidebar({
- Back
+ Back
)}
@@ -175,9 +182,10 @@ export function SettingsSidebar({
}}
>
-
- {item.label}
-
+
{item.locked && (
Plan
diff --git a/apps/sim/ee/access-control/components/group-detail.tsx b/apps/sim/ee/access-control/components/group-detail.tsx
index 8e599fa4de6..93352103f8d 100644
--- a/apps/sim/ee/access-control/components/group-detail.tsx
+++ b/apps/sim/ee/access-control/components/group-detail.tsx
@@ -17,6 +17,7 @@ import {
ChipTag,
cn,
Info,
+ OverflowText,
Search,
Skeleton,
Switch,
@@ -305,10 +306,14 @@ function AddMembersModal({
-
{name}
-
- {email}
-
+
+
)
@@ -428,7 +433,7 @@ function CheckboxGrid({
checked={isAllowed(item.id)}
onCheckedChange={() => onToggle(item.id)}
/>
- {item.label}
+
)
})}
@@ -524,7 +529,7 @@ function ProviderRow({
isProviderAllowed ? 'cursor-pointer' : 'cursor-default opacity-60'
)}
>
- {providerName}
+
{isProviderAllowed && deniedCount > 0 && (
{deniedCount} blocked
@@ -611,7 +616,7 @@ function BlockToolRow({
!isBlockAllowed && 'opacity-60'
)}
>
- {block.name}
+
{/* An org running one custom block per environment has prod/uat/sandbox copies
sharing a name and differing only by an opaque type slug. The source workspace
is the only thing that tells them apart, so an allowlist decision made without
@@ -1651,7 +1656,7 @@ export function GroupDetail({
>
{BlockIcon && }
-
{block.name}
+
{block.sourceWorkspaceName && (
{block.sourceWorkspaceName}
diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
index a7942f2a4bf..62f41b4c16d 100644
--- a/apps/sim/ee/audit-logs/components/audit-logs.tsx
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -10,6 +10,7 @@ import {
ChipSelect,
type ComboboxOption,
Download,
+ OverflowText,
Popover,
PopoverAnchor,
PopoverContent,
@@ -419,8 +420,13 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='All time'
+ overlayLabel={timeDisplayLabel}
overlayContent={
- {timeDisplayLabel}
+
}
maxHeight={320}
align='start'
diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx
index 10b69a4daad..15637672d8c 100644
--- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx
+++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx
@@ -11,6 +11,7 @@ import {
ChipSwitch,
ChipTag,
Info,
+ OverflowText,
Search,
toast,
} from '@sim/emcn'
@@ -336,7 +337,7 @@ function EntityCheckboxGrid({
checked={selected.includes(entity.value)}
onCheckedChange={() => toggle(entity.value)}
/>
- {entity.label}
+
)
})}
diff --git a/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx b/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx
index 8ced29f7330..29f5d073344 100644
--- a/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx
+++ b/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx
@@ -1,7 +1,7 @@
'use client'
import { useId, useMemo, useState } from 'react'
-import { Checkbox, ChevronDown, cn, toast } from '@sim/emcn'
+import { Checkbox, ChevronDown, cn, OverflowText, toast } from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { useUpdateForkExcludedWorkflows } from '@/ee/workspace-forking/hooks/workspace-fork'
@@ -205,12 +205,14 @@ function ExcludedFolderRow({
className='flex min-w-0 items-center gap-1.5 text-left hover:text-[var(--text-primary)]'
onClick={() => setExpanded((value) => !value)}
>
-
+ 0 ? `${selectedCount}/${total}` : total})`}
+ >
{folder.name}{' '}
({selectedCount > 0 ? `${selectedCount}/${total}` : total})
-
+
onToggle([workflow.id], value === true)}
disabled={disabled}
/>
- {workflow.name}
+
)
}
diff --git a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx
index 695edefa6c9..2cc7f60bda6 100644
--- a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx
+++ b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx
@@ -1,7 +1,7 @@
'use client'
import { useId, useState } from 'react'
-import { Checkbox, ChevronDown, cn } from '@sim/emcn'
+import { Checkbox, ChevronDown, cn, OverflowText } from '@sim/emcn'
export interface ForkFileTreeItem {
id: string
@@ -137,9 +137,10 @@ function ForkFileFolderRow({
className='flex min-w-0 flex-1 items-center gap-1 text-left hover:text-[var(--text-primary)]'
onClick={() => setExpanded((value) => !value)}
>
-
- {folder.name} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
-
+ 0 ? `${selectedCount}/${total}` : total})`}
+ className='flex-1'
+ />
onToggle(file.id, value === true)}
disabled={disabled}
/>
- {file.label}
+
)
}
diff --git a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx
index 3a6ddd0f7cc..890cb0c78ff 100644
--- a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx
+++ b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx
@@ -1,7 +1,7 @@
'use client'
import { useId, useMemo, useState } from 'react'
-import { Checkbox, ChevronDown, cn } from '@sim/emcn'
+import { Checkbox, ChevronDown, cn, OverflowText } from '@sim/emcn'
import {
ForkFileTree,
type ForkFlatFile,
@@ -69,9 +69,10 @@ export function ResourceKindRow({
className='flex min-w-0 flex-1 items-center gap-1 text-left hover:text-[var(--text-primary)]'
onClick={() => setExpanded((value) => !value)}
>
-
- {label} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
-
+ 0 ? `${selectedCount}/${total}` : total})`}
+ className='flex-1'
+ />
onToggleItem(item.id, checked === true)}
disabled={disabled}
/>
- {item.label}
+
)
})}
@@ -164,9 +165,10 @@ export function FileKindRow({
className='flex min-w-0 flex-1 items-center gap-1 text-left hover:text-[var(--text-primary)]'
onClick={() => setExpanded((value) => !value)}
>
-
- {label} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
-
+ 0 ? `${selectedCount}/${total}` : total})`}
+ className='flex-1'
+ />
-
{entry.sourceLabel}
+
+
+
setOpen((value) => !value)}
className='flex w-full items-center gap-2 text-left text-[var(--text-body)] text-sm transition-colors hover:text-[var(--text-primary)]'
>
- {group.label}
+
{badge.label}
@@ -733,14 +736,11 @@ function TriggerMappingRow({ controller, mapping }: TriggerMappingRowProps) {
return (
- {/* One inner span, so the name and its "in
" suffix share a normal inline flow:
- `Label` is inline-flex, and a flex container DISCARDS whitespace-only children, which
- eats the separating space (and leaves `truncate` with no text run to clip). */}
-
+
{mapping.blockName}{' '}
in {mapping.workflowName}
-
+
{decidable ? (
diff --git a/packages/emcn/src/components/combobox/combobox.dom.test.tsx b/packages/emcn/src/components/combobox/combobox.dom.test.tsx
index c67131c91ab..ac9d87b9114 100644
--- a/packages/emcn/src/components/combobox/combobox.dom.test.tsx
+++ b/packages/emcn/src/components/combobox/combobox.dom.test.tsx
@@ -57,6 +57,19 @@ afterEach(() => {
})
describe('Combobox onOpenChange', () => {
+ it('uses the overlay label for the interactive overflow layer', () => {
+ render(
+ 2 selected}
+ overlayLabel='2 selected'
+ />
+ )
+
+ const overflowLabel = trigger().querySelector('[data-overflow-text]')
+ expect(overflowLabel?.textContent).toBe('2 selected')
+ })
+
it('reports the open a trigger click causes', () => {
const onOpenChange = vi.fn()
render( )
diff --git a/packages/emcn/src/components/combobox/combobox.tsx b/packages/emcn/src/components/combobox/combobox.tsx
index 68e33d12136..a2298f187b9 100644
--- a/packages/emcn/src/components/combobox/combobox.tsx
+++ b/packages/emcn/src/components/combobox/combobox.tsx
@@ -19,6 +19,7 @@ import { Check, ChevronDown, Loader, Search } from '../../icons'
import { cn } from '../../lib/cn'
import { chipActiveSurfaceClass, chipHoverSurfaceClass } from '../chip/chip-chrome'
import { Input } from '../input/input'
+import { OverflowText } from '../overflow-text/overflow-text'
import { Popover, PopoverAnchor, PopoverContent, PopoverScrollArea } from '../popover/popover'
const comboboxVariants = cva(
@@ -93,8 +94,10 @@ export interface ComboboxProps
disabled?: boolean
/** Enable free-text input mode (default: false) */
editable?: boolean
- /** Custom overlay content for editable mode */
+ /** Visual content rendered over the selected value. */
overlayContent?: ReactNode
+ /** Plain-text value represented by a visual overlay in non-editable mode. */
+ overlayLabel?: string
/** Additional input props for editable mode */
inputProps?: Omit<
React.InputHTMLAttributes,
@@ -172,6 +175,7 @@ const Combobox = memo(
disabled,
editable = false,
overlayContent,
+ overlayLabel,
inputProps = {},
inputRef: externalInputRef,
filterOptions = editable,
@@ -677,9 +681,10 @@ const Combobox = memo(
) : (
<>
{SelectedIcon && }
-
- {selectedOption?.label}
-
+
>
)}
@@ -716,15 +721,18 @@ const Combobox = memo(
onClick={handleToggle}
onKeyDown={handleKeyDown}
>
-
- {multiSelectLabel ?? (selectedOption ? selectedOption.label : placeholder)}
-
+ />
)}
-
- {option.label}
-
+
{option.suffixElement}
{multiSelect && isSelected && (
@@ -935,9 +944,10 @@ const Combobox = memo(
: chipHoverSurfaceClass
)}
>
-
- {allOptionLabel}
-
+
)}
{filteredOptions.map((option, index) => {
@@ -973,9 +983,10 @@ const Combobox = memo(
{option.iconElement
? option.iconElement
: OptionIcon &&
}
-
- {option.label}
-
+
{option.suffixElement}
{multiSelect && isSelected && (
diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
index 1d4b9cf85eb..f683ddf5141 100644
--- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
+++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
@@ -2,8 +2,8 @@
* @vitest-environment jsdom
*
* Menu rows are a fixed height, so a label that wraps overflows its row and paints over its
- * neighbours. Rows are held to one line and their bare text is wrapped in a truncating box so an
- * over-long label ellipsizes instead of being cut mid-word. These tests cover that wrapping:
+ * neighbours. Rows are held to one line and their bare text is wrapped in the shared overflow
+ * treatment so an over-long label fades instead of being cut mid-word. These tests cover that wrapping:
* that it happens, that adjacent text stays in ONE box (two boxes would be two flex items, and
* the row's `gap` would open between the words), and that it steps aside for `asChild`, where
* Radix's `Slot` requires exactly one element child.
@@ -51,13 +51,15 @@ afterEach(() => {
})
describe('menu row labels', () => {
- it('wraps a bare text label in a truncating box', () => {
+ it('wraps a bare text label in the shared overflow treatment', () => {
openMenu(
Run empty or failed cells on 2 rows )
const labels = row().querySelectorAll('span')
expect(labels).toHaveLength(1)
expect(labels[0].textContent).toBe('Run empty or failed cells on 2 rows')
- expect(labels[0].className).toContain('truncate')
+ expect(labels[0].className).toContain('overflow-hidden')
+ expect(labels[0].className).toContain('text-clip')
+ expect(labels[0].className).not.toContain('truncate')
})
it('keeps the row on one line', () => {
@@ -84,7 +86,7 @@ describe('menu row labels', () => {
openMenu(
Show archived workflows )
const labels = row('[role="menuitemcheckbox"]').querySelectorAll('span')
- const label = Array.from(labels).find((node) => node.className.includes('truncate'))
+ const label = Array.from(labels).find((node) => node.className.includes('text-clip'))
expect(label?.textContent).toBe('Show archived workflows')
})
@@ -108,5 +110,6 @@ describe('menu row labels', () => {
)
expect(row().querySelectorAll('span')).toHaveLength(1)
+ expect(row().className).toContain('[&>span:not([data-overflow-text])]:truncate')
})
})
diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
index 9dbdfeda815..0198bda51c5 100644
--- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
+++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
@@ -26,6 +26,7 @@ import { Check, ChevronRight, Circle, Search } from '../../icons'
import { cn } from '../../lib/cn'
import { chipContentGap, chipFieldSurfaceClass } from '../chip/chip-chrome'
import { InsideModalContext } from '../modal/modal'
+import { OverflowText } from '../overflow-text/overflow-text'
const ANIMATION_CLASSES =
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in motion-reduce:animate-none'
@@ -76,30 +77,32 @@ const MENU_ROW_SELECTED_CLASS =
/**
* Rows are a fixed height, so a label that wraps overflows its row and paints
* over its neighbours instead of growing the row. Every row is therefore held
- * to one line, and its label ellipsizes — see {@link withEllipsizedLabel}.
+ * to one line, and its label uses the shared overflow treatment — see
+ * {@link withOverflowLabel}.
*/
-const MENU_ROW_SINGLE_LINE_CLASS = 'whitespace-nowrap [&>span]:min-w-0 [&>span]:truncate'
+const MENU_ROW_SINGLE_LINE_CLASS =
+ 'whitespace-nowrap [&>span]:min-w-0 [&>span:not([data-overflow-text])]:truncate'
/**
* Wraps a row's bare text children in a truncating box so a label wider than
- * the menu ends in an ellipsis rather than being cut mid-word at the surface
- * edge. Consumers that already wrap their label in a `
` are unaffected —
- * the row's `[&>span]` rule truncates those in place.
+ * the menu uses the platform overflow treatment rather than being cut mid-word
+ * at the surface edge. Consumer-provided direct `` labels retain an
+ * ellipsis fallback; a canonical {@link OverflowText} owns its fade and tooltip.
*
* Adjacent text is coalesced into a single box: a row is a flex container, so
* wrapping `Insert row {n}` as two boxes would make them two flex items and
* open the row's `gap` between the words. `React.Children.toArray` keys the
* element children it returns, so the rebuilt array needs no keys of its own.
*/
-function withEllipsizedLabel(children: React.ReactNode): React.ReactNode {
+function withOverflowLabel(children: React.ReactNode): React.ReactNode {
const rebuilt: React.ReactNode[] = []
- let text: React.ReactNode[] = []
+ let text: Array = []
const flushText = () => {
if (text.length === 0) return
rebuilt.push(
-
+
{text}
-
+
)
text = []
}
@@ -212,7 +215,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
)}
{...props}
>
- {withEllipsizedLabel(children)}
+ {withOverflowLabel(children)}
)
@@ -303,7 +306,7 @@ const DropdownMenuItem = React.forwardRef<
action?: React.ReactNode
}
>(({ className, inset, active, action, asChild, children, ...props }, ref) => {
- const content = asChild ? children : withEllipsizedLabel(children)
+ const content = asChild ? children : withOverflowLabel(children)
const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS
if (action) {
return (
@@ -389,7 +392,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
- {withEllipsizedLabel(children)}
+ {withOverflowLabel(children)}
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
@@ -411,7 +414,7 @@ const DropdownMenuRadioItem = React.forwardRef<
- {withEllipsizedLabel(children)}
+ {withOverflowLabel(children)}
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts
index 7acf22d12ce..f0830e272ad 100644
--- a/packages/emcn/src/components/index.ts
+++ b/packages/emcn/src/components/index.ts
@@ -163,6 +163,11 @@ export {
useModalDismissDisabled,
useNativeSurfaceOcclusionReady,
} from './modal/modal'
+export {
+ OverflowText,
+ type OverflowTextProps,
+ overflowTextFadeClass,
+} from './overflow-text/overflow-text'
export {
Popover,
PopoverAnchor,
diff --git a/packages/emcn/src/components/overflow-text/overflow-text.test.tsx b/packages/emcn/src/components/overflow-text/overflow-text.test.tsx
new file mode 100644
index 00000000000..d057c1019ab
--- /dev/null
+++ b/packages/emcn/src/components/overflow-text/overflow-text.test.tsx
@@ -0,0 +1,273 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act, StrictMode } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { OverflowText } from './overflow-text'
+
+let host: HTMLDivElement
+let root: Root
+let resizeObserverCallback: ResizeObserverCallback
+let resizeObserverCount: number
+let originalFontsDescriptor: PropertyDescriptor | undefined
+
+beforeEach(() => {
+ resizeObserverCount = 0
+ originalFontsDescriptor = Object.getOwnPropertyDescriptor(document, 'fonts')
+ const fontEvents = new EventTarget()
+ Object.defineProperty(fontEvents, 'ready', {
+ configurable: true,
+ value: new Promise(() => {}),
+ })
+ Object.defineProperty(document, 'fonts', {
+ configurable: true,
+ value: fontEvents,
+ })
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class ResizeObserver {
+ constructor(callback: ResizeObserverCallback) {
+ resizeObserverCount += 1
+ resizeObserverCallback = callback
+ }
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ )
+ host = document.createElement('div')
+ document.body.appendChild(host)
+ root = createRoot(host)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ host.remove()
+ if (originalFontsDescriptor) {
+ Object.defineProperty(document, 'fonts', originalFontsDescriptor)
+ } else {
+ Reflect.deleteProperty(document, 'fonts')
+ }
+ vi.unstubAllGlobals()
+})
+
+function setWidths(element: HTMLElement, clientWidth: number, scrollWidth: number) {
+ Object.defineProperties(element, {
+ clientWidth: { configurable: true, value: clientWidth },
+ scrollWidth: { configurable: true, value: scrollWidth },
+ })
+ act(() =>
+ resizeObserverCallback(
+ [
+ {
+ target: element,
+ contentRect: element.getBoundingClientRect(),
+ borderBoxSize: [],
+ contentBoxSize: [],
+ devicePixelContentBoxSize: [],
+ },
+ ],
+ {} as ResizeObserver
+ )
+ )
+}
+
+describe('OverflowText', () => {
+ it('fades and reveals the full value only when clipped', () => {
+ act(() =>
+ root.render(
)
+ )
+ const label = host.querySelector
('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 80, 180)
+
+ expect(label.classList.contains('overflow-hidden')).toBe(true)
+ expect(label.classList.contains('block')).toBe(true)
+ expect(label.classList.contains('text-clip')).toBe(true)
+ expect(label.classList.contains('whitespace-nowrap')).toBe(true)
+ expect(label.classList.contains('truncate')).toBe(false)
+ expect(label.classList.contains('text-sm')).toBe(true)
+ expect(label.className).toContain('-webkit-mask-image:linear-gradient')
+ expect(label.className).toContain('mask-image:linear-gradient')
+
+ act(() => {
+ label.dispatchEvent(
+ new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 })
+ )
+ })
+ expect(document.querySelector('[data-native-surface-overlay]')?.textContent).toBe(
+ 'A long workflow name'
+ )
+
+ setWidths(label, 200, 180)
+ expect(document.querySelector('[data-native-surface-overlay]')).toBeNull()
+ })
+
+ it('leaves a fitting label unmasked and does not open a tooltip', () => {
+ act(() => root.render( ))
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 100, 60)
+ expect(label.className).not.toContain('mask-image:linear-gradient')
+
+ act(() => {
+ label.dispatchEvent(
+ new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 })
+ )
+ })
+ expect(document.querySelector('[data-native-surface-overlay]')).toBeNull()
+ })
+
+ it('can disable the tooltip for a visual mirror layer', () => {
+ act(() => root.render( ))
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 80, 180)
+ expect(label.className).toContain('mask-image:linear-gradient')
+
+ act(() => {
+ label.dispatchEvent(
+ new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 })
+ )
+ })
+ expect(document.querySelector('[data-native-surface-overlay]')).toBeNull()
+ })
+
+ it('opens from an external composite control keyboard focus', () => {
+ act(() =>
+ root.render(
+
+
+
+ )
+ )
+ const button = host.querySelector('button')
+ const label = host.querySelector('[data-overflow-text]')
+ if (!button || !label) throw new Error('Composite label did not render')
+
+ setWidths(label, 80, 180)
+ vi.spyOn(button, 'matches').mockReturnValue(true)
+ act(() => button.focus())
+
+ expect(document.querySelector('[data-native-surface-overlay]')?.textContent).toBe(
+ 'A long workflow name'
+ )
+ })
+
+ it('keeps decorated visible content out of the plain tooltip label', () => {
+ act(() =>
+ root.render(
+
+ Workflow production
+
+ )
+ )
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+ setWidths(label, 80, 180)
+
+ act(() => {
+ label.dispatchEvent(
+ new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 })
+ )
+ })
+
+ const tooltip = document.querySelector('[data-native-surface-overlay]')
+ expect(tooltip?.textContent).toBe('Workflow production')
+ expect(tooltip?.querySelector('mark')).toBeNull()
+ })
+
+ it('shares one resize observer across overflow labels', () => {
+ act(() =>
+ root.render(
+ <>
+
+
+ >
+ )
+ )
+
+ expect(resizeObserverCount).toBe(1)
+ })
+
+ it('remeasures when an existing label changes', () => {
+ act(() => root.render( ))
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 100, 60)
+ expect(label.className).not.toContain('mask-image:linear-gradient')
+
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 180 })
+ act(() => root.render( ))
+ expect(label.className).toContain('mask-image:linear-gradient')
+
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 60 })
+ act(() => root.render( ))
+ expect(label.className).not.toContain('mask-image:linear-gradient')
+ })
+
+ it('remeasures when decorated content changes without changing the label', () => {
+ act(() =>
+ root.render(
+
+ Workflow production
+
+ )
+ )
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 100, 60)
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 180 })
+ act(() =>
+ root.render(
+
+ Workflow production
+
+ )
+ )
+
+ expect(label.className).toContain('mask-image:linear-gradient')
+ })
+
+ it('remeasures when loaded fonts change text width', () => {
+ act(() => root.render( ))
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 100, 60)
+ expect(label.className).not.toContain('mask-image:linear-gradient')
+
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 180 })
+ act(() => document.fonts.dispatchEvent(new Event('loadingdone')))
+ expect(label.className).toContain('mask-image:linear-gradient')
+
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 60 })
+ act(() => document.fonts.dispatchEvent(new Event('loadingerror')))
+ expect(label.className).not.toContain('mask-image:linear-gradient')
+ })
+
+ it('keeps observing mounted labels through Strict Mode effect replay', () => {
+ act(() =>
+ root.render(
+
+
+
+ )
+ )
+ const label = host.querySelector('span')
+ if (!label) throw new Error('Overflow label did not render')
+
+ setWidths(label, 100, 60)
+ Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 180 })
+ act(() => document.fonts.dispatchEvent(new Event('loadingdone')))
+
+ expect(label.className).toContain('mask-image:linear-gradient')
+ })
+})
diff --git a/packages/emcn/src/components/overflow-text/overflow-text.tsx b/packages/emcn/src/components/overflow-text/overflow-text.tsx
new file mode 100644
index 00000000000..f845d49dc23
--- /dev/null
+++ b/packages/emcn/src/components/overflow-text/overflow-text.tsx
@@ -0,0 +1,87 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import { memo, useCallback } from 'react'
+import { cn } from '../../lib/cn'
+import {
+ FloatingTooltip,
+ isTextClipped,
+ useFloatingTooltip,
+ useIsOverflowing,
+} from '../tooltip/tooltip'
+
+/** Shared 18px trailing fade for measured special cases such as breadcrumb groups. */
+export const overflowTextFadeClass =
+ '[-webkit-mask-image:linear-gradient(to_right,black_calc(100%_-_18px),transparent)] [mask-image:linear-gradient(to_right,black_calc(100%_-_18px),transparent)]'
+
+export interface OverflowTextProps {
+ /** Full text shown in the tooltip and used as the default visible content. */
+ label: string
+ /** Decorated rendering of `label`; the tooltip always keeps the plain label. */
+ children?: ReactNode
+ /** Layout and typography only; truncation and fade chrome are owned here. */
+ className?: string
+ /** Forces the tooltip when the visible label was shortened before rendering. */
+ showWhen?: boolean
+ /** Whether the full-value tooltip may open. Disable for visual mirror layers. */
+ tooltipEnabled?: boolean
+ /** Lets the nearest interactive ancestor own keyboard focus for this label. */
+ focusTarget?: 'nearest-interactive'
+}
+
+/**
+ * A single-line, read-only label that fades only when clipped and exposes its
+ * complete value in the platform floating tooltip.
+ *
+ * Use this for human-readable names and titles in constrained chrome. Keep
+ * editable values, code, logs, paths, dense grids, and multiline copy on their
+ * purpose-built overflow behavior.
+ */
+export const OverflowText = memo(function OverflowText({
+ label,
+ children,
+ className,
+ showWhen,
+ tooltipEnabled = true,
+ focusTarget,
+}: OverflowTextProps) {
+ const { ref: textRef, node, isOverflowing } = useIsOverflowing(children ?? label)
+ const tooltipEligible = tooltipEnabled && label.length > 0 && (Boolean(showWhen) || isOverflowing)
+ const getFocusTarget = useCallback(() => {
+ if (focusTarget !== 'nearest-interactive') return null
+ return (
+ node.current?.closest(
+ 'a[href], button, [role="button"], [tabindex]:not([tabindex="-1"])'
+ ) ?? null
+ )
+ }, [focusTarget, node])
+ const { state, handlers } = useFloatingTooltip(
+ () => {
+ const element = node.current
+ if (!tooltipEnabled || !element || label.length === 0) return false
+ return Boolean(showWhen) || isTextClipped(element)
+ },
+ {
+ getFocusTarget: focusTarget === 'nearest-interactive' ? getFocusTarget : undefined,
+ revalidateKey: tooltipEligible,
+ }
+ )
+
+ return (
+ <>
+
+ {children ?? label}
+
+
+ >
+ )
+})
diff --git a/packages/emcn/src/components/tab-strip/tab-strip.tsx b/packages/emcn/src/components/tab-strip/tab-strip.tsx
index 2c727bdfa20..8ede2a176f6 100644
--- a/packages/emcn/src/components/tab-strip/tab-strip.tsx
+++ b/packages/emcn/src/components/tab-strip/tab-strip.tsx
@@ -17,6 +17,7 @@ import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import { Plus, X } from '../../icons'
import { cn } from '../../lib/cn'
import { Button } from '../button/button'
+import { overflowTextFadeClass } from '../overflow-text/overflow-text'
import { Tooltip } from '../tooltip/tooltip'
const DRAG_EDGE_ZONE = 40
@@ -441,7 +442,14 @@ const Tab = forwardRef(function Tab(
>
{tab.icon}
{!tab.pinned && (
-
+
{tab.title}
)}
diff --git a/packages/emcn/src/components/tooltip/tooltip.tsx b/packages/emcn/src/components/tooltip/tooltip.tsx
index 601ec570d67..321f912caeb 100644
--- a/packages/emcn/src/components/tooltip/tooltip.tsx
+++ b/packages/emcn/src/components/tooltip/tooltip.tsx
@@ -83,11 +83,10 @@ const HIDDEN_STATE: FloatingTooltipState = {
}
/**
- * Drives a pointer-reactive floating tooltip. `canShow` is queried on every
- * gesture with the event target, letting the caller gate the tooltip on its own
- * overflow measurement. Returns the current {@link FloatingTooltipState} to feed
- * a {@link FloatingTooltip} and a stable set of {@link FloatingTooltipHandlers}
- * to spread onto the trigger element.
+ * Drives a pointer-reactive floating tooltip. `canShow` is checked on each
+ * gesture and while visible, allowing callers to dismiss the tooltip when its
+ * eligibility changes. Returns the current state and stable pointer/focus
+ * handlers; `getFocusTarget` may supply a separate keyboard-focus trigger.
*/
export interface UseFloatingTooltipOptions {
/**
@@ -95,6 +94,10 @@ export interface UseFloatingTooltipOptions {
* pointer is too close to the top of the viewport.
*/
preferAbove?: boolean
+ /** Resolves an external control whose keyboard focus should reveal this tooltip. */
+ getFocusTarget?: () => HTMLElement | null
+ /** Semantic value whose changes should revalidate a visible tooltip. */
+ revalidateKey?: unknown
}
export function useFloatingTooltip(
@@ -126,41 +129,49 @@ export function useFloatingTooltip(
setState((current) => (current.visible ? HIDDEN_STATE : current))
}, [reset])
- const handlers = React.useMemo(() => {
- const apply = (clientX: number, clientY: number, motion: TooltipMotion) => {
- const next = { ...getTooltipPosition(clientX, clientY, preferAboveRef.current), ...motion }
- setState((current) =>
- current.visible &&
- current.x === next.x &&
- current.y === next.y &&
- current.alignX === next.alignX &&
- current.alignY === next.alignY &&
- current.skew === next.skew &&
- current.scaleX === next.scaleX &&
- current.scaleY === next.scaleY
- ? current
- : { visible: true, ...next }
- )
- }
+ const apply = React.useCallback((clientX: number, clientY: number, motion: TooltipMotion) => {
+ const next = { ...getTooltipPosition(clientX, clientY, preferAboveRef.current), ...motion }
+ setState((current) =>
+ current.visible &&
+ current.x === next.x &&
+ current.y === next.y &&
+ current.alignX === next.alignX &&
+ current.alignY === next.alignY &&
+ current.skew === next.skew &&
+ current.scaleX === next.scaleX &&
+ current.scaleY === next.scaleY
+ ? current
+ : { visible: true, ...next }
+ )
+ }, [])
- /** Reveals the tooltip at the pointer, seeding velocity tracking from it. */
- const showFromPointer = (clientX: number, clientY: number) => {
+ const showFromPointer = React.useCallback(
+ (clientX: number, clientY: number) => {
reset()
lastPointerRef.current = { x: clientX, y: clientY, time: performance.now() }
apply(clientX, clientY, NEUTRAL_MOTION)
- }
+ },
+ [apply, reset]
+ )
- /**
- * Reveals the tooltip anchored to an element's box rather than the pointer.
- * Velocity tracking stays cleared: seeding it from the box would make the next
- * `pointermove` read the box-to-cursor delta as velocity and spike the flourish
- * when the pointer already happens to be over the trigger.
- */
- const showFromElement = (clientX: number, clientY: number) => {
+ /**
+ * Anchors the tooltip to an element without seeding pointer velocity; using the
+ * box position would make the next pointer move produce a false motion spike.
+ */
+ const showFromElement = React.useCallback(
+ (target: HTMLElement) => {
reset()
- apply(clientX, clientY, NEUTRAL_MOTION)
- }
+ const rect = target.getBoundingClientRect()
+ apply(
+ rect.left + rect.width / 2,
+ preferAboveRef.current ? rect.top : rect.bottom,
+ NEUTRAL_MOTION
+ )
+ },
+ [apply, reset]
+ )
+ const handlers = React.useMemo(() => {
return {
onPointerEnter: (event) => {
if (!canShowRef.current(event.currentTarget)) return
@@ -200,14 +211,32 @@ export function useFloatingTooltip(
if (!canShowRef.current(target)) return
if (!isFocusVisible(target)) return
triggerRef.current = target
- const rect = target.getBoundingClientRect()
- /* Anchor on the edge the bubble grows away from, so a `preferAbove`
- tooltip measures from the trigger's top rather than its bottom. */
- showFromElement(rect.left + rect.width / 2, preferAboveRef.current ? rect.top : rect.bottom)
+ showFromElement(target)
},
onBlur: hide,
}
- }, [hide, reset])
+ }, [apply, hide, showFromElement, showFromPointer])
+
+ React.useEffect(() => {
+ const target = options.getFocusTarget?.()
+ if (!target) return undefined
+ const show = () => {
+ if (!canShowRef.current(target) || !isFocusVisible(target)) return
+ triggerRef.current = target
+ showFromElement(target)
+ }
+ target.addEventListener('focus', show)
+ target.addEventListener('blur', hide)
+ return () => {
+ target.removeEventListener('focus', show)
+ target.removeEventListener('blur', hide)
+ }
+ }, [hide, options.getFocusTarget, showFromElement])
+
+ React.useEffect(() => {
+ const trigger = triggerRef.current
+ if (state.visible && (!trigger || !canShowRef.current(trigger))) hide()
+ }, [hide, options.revalidateKey, state.visible])
/**
* A keyboard- or script-driven UI change can hide the trigger with no pointer or focus event —
@@ -227,23 +256,77 @@ export function useFloatingTooltip(
return { state, handlers }
}
+const overflowMeasureByElement = new WeakMap void>()
+const observedOverflowElements = new Set()
+let sharedOverflowObserver: ResizeObserver | null = null
+let sharedOverflowFontSet: FontFaceSet | null = null
+
+function measureObservedOverflow() {
+ for (const element of observedOverflowElements) overflowMeasureByElement.get(element)?.()
+}
+
+function observeOverflowFontChanges() {
+ if (typeof document === 'undefined' || !document.fonts || sharedOverflowFontSet) return
+ const fontSet = document.fonts
+ sharedOverflowFontSet = fontSet
+ fontSet.addEventListener('loadingdone', measureObservedOverflow)
+ fontSet.addEventListener('loadingerror', measureObservedOverflow)
+ void fontSet.ready.then(() => {
+ if (sharedOverflowFontSet === fontSet) measureObservedOverflow()
+ })
+}
+
+function unobserveOverflowFontChanges() {
+ sharedOverflowFontSet?.removeEventListener('loadingdone', measureObservedOverflow)
+ sharedOverflowFontSet?.removeEventListener('loadingerror', measureObservedOverflow)
+ sharedOverflowFontSet = null
+}
+
+function observeOverflow(element: Element, measure: () => void): boolean {
+ overflowMeasureByElement.set(element, measure)
+ observedOverflowElements.add(element)
+ observeOverflowFontChanges()
+ if (typeof ResizeObserver === 'undefined') return false
+ sharedOverflowObserver ??= new ResizeObserver((entries) => {
+ for (const entry of entries) overflowMeasureByElement.get(entry.target)?.()
+ })
+ sharedOverflowObserver.observe(element)
+ return true
+}
+
+function unobserveOverflow(element: Element | null) {
+ if (!element) return
+ sharedOverflowObserver?.unobserve(element)
+ overflowMeasureByElement.delete(element)
+ observedOverflowElements.delete(element)
+ if (observedOverflowElements.size === 0) {
+ sharedOverflowObserver?.disconnect()
+ sharedOverflowObserver = null
+ unobserveOverflowFontChanges()
+ }
+}
+
/**
- * Tracks whether an element's text is horizontally clipped, re-measuring via a
- * `ResizeObserver` and window resizes.
+ * Tracks whether an element's text is horizontally clipped, re-measuring when
+ * `measurementKey` or loaded fonts change and via a shared `ResizeObserver` (or
+ * window resizes when the API is unavailable).
*
* Returns a callback `ref` to attach to the element — the observer follows the
* element across mount, unmount, and reassignment, so it is safe to use on
* conditionally rendered children. `node` is a stable ref for reading the
* current element (e.g. for live measurements in event handlers).
+ *
+ * @param measurementKey - Value whose changes may alter the element's rendered width.
*/
-export function useIsOverflowing(): {
+export function useIsOverflowing(
+ measurementKey?: unknown
+): {
ref: (node: T | null) => void
node: React.RefObject
isOverflowing: boolean
} {
const [isOverflowing, setIsOverflowing] = React.useState(false)
const nodeRef = React.useRef(null)
- const observerRef = React.useRef(null)
const measure = React.useCallback(() => {
const element = nodeRef.current
@@ -252,27 +335,31 @@ export function useIsOverflowing(): {
const ref = React.useCallback(
(node: T | null) => {
- observerRef.current?.disconnect()
- observerRef.current = null
+ unobserveOverflow(nodeRef.current)
nodeRef.current = node
if (!node) return
measure()
- const observer = new ResizeObserver(measure)
- observer.observe(node)
- observerRef.current = observer
+ observeOverflow(node, measure)
},
[measure]
)
React.useEffect(() => {
- window.addEventListener('resize', measure)
+ const element = nodeRef.current
+ if (!element) return undefined
+ const usesResizeObserver = observeOverflow(element, measure)
+ if (!usesResizeObserver) window.addEventListener('resize', measure)
return () => {
- window.removeEventListener('resize', measure)
- observerRef.current?.disconnect()
+ if (!usesResizeObserver) window.removeEventListener('resize', measure)
+ unobserveOverflow(element)
}
}, [measure])
+ React.useLayoutEffect(() => {
+ measure()
+ }, [measure, measurementKey])
+
return { ref, node: nodeRef, isOverflowing }
}
diff --git a/packages/workflow-renderer/src/lib/overflow-span.tsx b/packages/workflow-renderer/src/lib/overflow-span.tsx
index 9ddfdc11d0e..608b10b3434 100644
--- a/packages/workflow-renderer/src/lib/overflow-span.tsx
+++ b/packages/workflow-renderer/src/lib/overflow-span.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
-import { FloatingTooltip, isTextClipped, useFloatingTooltip } from '@sim/emcn'
+import { OverflowText } from '@sim/emcn'
import type { CodePreview } from '../types'
import { CodeHoverCard } from './code-hover-card'
@@ -41,14 +41,9 @@ export function OverflowSpan({ value, className, codePreview, children }: Overfl
/** Plain clipped text keeps the platform tooltip behavior unchanged. */
function TextOverflowSpan({ value, className, children }: Omit) {
- const { state, handlers } = useFloatingTooltip(isTextClipped)
-
return (
- <>
-
- {children ?? value}
-
-
- >
+
+ {children}
+
)
}
diff --git a/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx
index 2f7eec00e64..112fd8eccae 100644
--- a/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx
+++ b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx
@@ -112,14 +112,13 @@ export function SubBlockRowView({
className='min-w-0 truncate text-[var(--text-tertiary)] text-sm capitalize'
/>
{displayValue !== undefined && (
-
- {displayValue}
-
+ />
)}
)