From d587f1a821190aed84210333ef54368b8c83063e Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 18 Sep 2026 15:52:24 -0700 Subject: [PATCH 1/3] refactor(emcn): centralize overlay action buttons --- .../components/trace-view/trace-view.tsx | 13 ++-- .../components/log-details/log-details.tsx | 13 ++-- .../components/general/general.tsx | 10 ++-- .../preview-editor/preview-editor.tsx | 29 ++++----- packages/emcn/src/components/index.ts | 5 ++ .../overlay-action-button.tsx | 59 +++++++++++++++++++ 6 files changed, 92 insertions(+), 37 deletions(-) create mode 100644 packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index c75ef9c2457..a1a2acfba9c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -15,6 +15,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, Duplicate, + OverlayActionButton, Search as SearchIcon, Tooltip, useCopyToClipboard, @@ -504,39 +505,35 @@ function DetailCodeSection({
- + {copied ? 'Copied' : 'Copy'} - + Search diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 492d2fb1d5e..60dea0fb0ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -27,6 +27,7 @@ import { Duplicate, Eye, handleKeyboardActivation, + OverlayActionButton, Redo, Search as SearchIcon, Tooltip, @@ -168,39 +169,35 @@ export const WorkflowOutputSection = memo(
- + {copied ? 'Copied' : 'Copy'} - + Search diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index 231ea553c16..b38f2324a58 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -2,7 +2,6 @@ import { useId, useState } from 'react' import { - Button, ChipButtonGroup, ChipButtonGroupItem, ChipConfirmModal, @@ -12,6 +11,7 @@ import { cn, Expand, Label, + OverlayActionButton, Skeleton, Tooltip, } from '@sim/emcn' @@ -241,15 +241,15 @@ export function GeneralDeploy({
- + See preview diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index 6a1ce331611..718d10395d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -12,6 +12,7 @@ import { Input, Label, OverflowText, + OverlayActionButton, Tooltip, } from '@sim/emcn' import { @@ -1235,22 +1236,21 @@ function PreviewEditorContent({
- + {copiedSection === 'input' ? 'Copied' : 'Copy'} @@ -1258,18 +1258,17 @@ function PreviewEditorContent({ - + Search @@ -1309,22 +1308,21 @@ function PreviewEditorContent({
- + {copiedSection === 'output' ? 'Copied' : 'Copy'} @@ -1332,18 +1330,17 @@ function PreviewEditorContent({ - + Search diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index 6628b0a2d1b..8b6df9af42c 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -196,6 +196,11 @@ export { overflowTextClipClass, overflowTextFadeClass, } from './overflow-text/overflow-text' +export { + OverlayActionButton, + type OverlayActionButtonProps, + overlayActionButtonVariants, +} from './overlay-action-button/overlay-action-button' export { Popover, PopoverAnchor, diff --git a/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx b/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx new file mode 100644 index 00000000000..e502c787ca6 --- /dev/null +++ b/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx @@ -0,0 +1,59 @@ +import { forwardRef } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '../../lib/cn' +import { Button, type ButtonProps } from '../button/button' + +/** Transparent, bordered icon action over code or preview content. */ +export const overlayActionButtonVariants = cva( + 'cursor-pointer border border-[var(--border)] bg-transparent p-0 backdrop-blur-xs', + { + variants: { + size: { + sm: 'size-[20px]', + md: 'size-[28px]', + }, + surface: { + adaptive: + 'hover-hover:bg-[var(--surface-3)] dark:hover-hover:bg-[var(--surface-5)] hover-hover:border-[var(--border)]', + uniform: 'hover-hover:bg-[var(--surface-4)]', + }, + }, + defaultVariants: { size: 'sm', surface: 'adaptive' }, + } +) + +export interface OverlayActionButtonProps + extends Omit { + /** Accessible name for the icon action; tooltip content is supplied separately. */ + 'aria-label': string + /** 20px by default; `md` provides the 28px preview action. */ + size?: NonNullable['size']> + /** + * `adaptive` uses surface-3 on hover in light mode and surface-5 in dark mode. + * `uniform` uses surface-4 on hover in both themes. + * @default 'adaptive' + */ + surface?: NonNullable['surface']> +} + +/** + * Icon action floating over content. Owns geometry, border, blur and hover treatment; + * callers supply positioning, icons, labels and command behavior. + * Forwards the native button ref and props for tooltip `asChild` composition. + * Native form behavior is inherited from Button; pass `type` when it must be explicit. + * + * @example + */ +export const OverlayActionButton = forwardRef( + ({ size, surface, className, ...props }, ref) => ( + + + + + + ) + const [previous, current] = view.querySelectorAll('button') + /** The old border-1 token aliases border; class order changes when recipes are composed. */ + for (const button of [previous, current]) { + button.className = button.className + .replaceAll('--border-1', '--border') + .split(/\s+/) + .sort() + .join(' ') + } + expect(current.outerHTML).toBe(previous.outerHTML) + }) + + it('forwards refs and native props through a tooltip and suppresses disabled clicks', () => { + vi.useFakeTimers() + const ref = createRef() + const onClick = vi.fn() + const onKeyDown = vi.fn() + const action = (disabled: boolean) => ( + + + + + Copy output + + ) + const view = mount(action(false)) + const button = view.querySelector('button') + if (!button) throw new Error('Button did not render') + expect(view.querySelectorAll('button')).toHaveLength(1) + expect(ref.current).toBe(button) + expect(button.type).toBe('button') + expect(button.dataset.action).toBe('copy') + expect(button.getAttribute('aria-label')).toBe('Copy') + act(() => + button.dispatchEvent( + new MouseEvent('pointerover', { bubbles: true, clientX: 200, clientY: 200 }) + ) + ) + expect(document.querySelector('[role="tooltip"]')?.textContent).toBe('Copy output') + act(() => button.focus()) + expect(document.activeElement).toBe(button) + const keyEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + act(() => button.dispatchEvent(keyEvent)) + expect(onKeyDown).toHaveBeenCalledTimes(1) + expect(onKeyDown.mock.calls[0][0].nativeEvent).toBe(keyEvent) + act(() => button.click()) + expect(onClick).toHaveBeenCalledTimes(1) + act(() => root?.render(action(true))) + expect(button.disabled).toBe(true) + act(() => button.click()) + expect(onClick).toHaveBeenCalledTimes(1) + }) +}) From f1e45e42885fc0e6831c31de937a53bfbc95af0c Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Sat, 19 Sep 2026 09:35:41 -0700 Subject: [PATCH 3/3] refactor(emcn): unify overlay action hover styling --- .../preview-editor/preview-editor.tsx | 4 ---- .../overlay-action-button.test.tsx | 9 +------- .../overlay-action-button.tsx | 22 +++++-------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index 718d10395d9..8dd52e81c14 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -1239,7 +1239,6 @@ function PreviewEditorContent({ { e.stopPropagation() handleCopySection(formatValueAsJson(executionData.input), 'input') @@ -1261,7 +1260,6 @@ function PreviewEditorContent({ { e.stopPropagation() activateSearch() @@ -1311,7 +1309,6 @@ function PreviewEditorContent({ { e.stopPropagation() handleCopySection(formatValueAsJson(executionData.output), 'output') @@ -1333,7 +1330,6 @@ function PreviewEditorContent({ { e.stopPropagation() activateSearch() diff --git a/packages/emcn/src/components/overlay-action-button/overlay-action-button.test.tsx b/packages/emcn/src/components/overlay-action-button/overlay-action-button.test.tsx index 78b4714d7c0..cd1ecca1876 100644 --- a/packages/emcn/src/components/overlay-action-button/overlay-action-button.test.tsx +++ b/packages/emcn/src/components/overlay-action-button/overlay-action-button.test.tsx @@ -24,7 +24,7 @@ afterEach(() => { vi.useRealTimers() }) -/** Pre-migration recipes from log details, the deployment preview and workflow previews. */ +/** Pre-migration recipes from log details and the deployment preview. */ const PREVIOUS = [ { name: 'default 20px adaptive action', @@ -40,13 +40,6 @@ const PREVIOUS = [ className: 'size-[28px] cursor-pointer bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]', }, - { - name: '20px uniform action', - props: { surface: 'uniform' }, - variant: 'ghost', - className: - 'size-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-4)]', - }, ] as const describe('OverlayActionButton', () => { diff --git a/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx b/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx index e502c787ca6..6a2f52fc4f0 100644 --- a/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx +++ b/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx @@ -5,53 +5,43 @@ import { Button, type ButtonProps } from '../button/button' /** Transparent, bordered icon action over code or preview content. */ export const overlayActionButtonVariants = cva( - 'cursor-pointer border border-[var(--border)] bg-transparent p-0 backdrop-blur-xs', + 'cursor-pointer border border-[var(--border)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)] dark:hover-hover:bg-[var(--surface-5)] hover-hover:border-[var(--border)]', { variants: { size: { sm: 'size-[20px]', md: 'size-[28px]', }, - surface: { - adaptive: - 'hover-hover:bg-[var(--surface-3)] dark:hover-hover:bg-[var(--surface-5)] hover-hover:border-[var(--border)]', - uniform: 'hover-hover:bg-[var(--surface-4)]', - }, }, - defaultVariants: { size: 'sm', surface: 'adaptive' }, + defaultVariants: { size: 'sm' }, } ) export interface OverlayActionButtonProps - extends Omit { + extends Omit { /** Accessible name for the icon action; tooltip content is supplied separately. */ 'aria-label': string /** 20px by default; `md` provides the 28px preview action. */ size?: NonNullable['size']> - /** - * `adaptive` uses surface-3 on hover in light mode and surface-5 in dark mode. - * `uniform` uses surface-4 on hover in both themes. - * @default 'adaptive' - */ - surface?: NonNullable['surface']> } /** * Icon action floating over content. Owns geometry, border, blur and hover treatment; * callers supply positioning, icons, labels and command behavior. + * Hover uses surface-3 in light mode and surface-5 in dark mode. * Forwards the native button ref and props for tooltip `asChild` composition. * Native form behavior is inherited from Button; pass `type` when it must be explicit. * * @example */ export const OverlayActionButton = forwardRef( - ({ size, surface, className, ...props }, ref) => ( + ({ size, className, ...props }, ref) => (