From d583476eed126d98c52b9d55038b8f0dcabb8952 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 12:17:25 -0700 Subject: [PATCH 01/10] improvement(emcn): share ordinary action geometry in Button --- .claude/rules/emcn-components.md | 14 +++ .cursor/rules/emcn-components.mdc | 14 +++ .../select-field/select-options-editor.tsx | 3 +- .../components/table-filter/table-filter.tsx | 3 +- .../bulk-action-button/bulk-action-button.tsx | 2 +- .../src/components/button/button.test.tsx | 86 ++++++++++++++++++- .../emcn/src/components/button/button.tsx | 42 +++++++-- .../composer-action-button.tsx | 2 +- 8 files changed, 156 insertions(+), 10 deletions(-) diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index f02ff3946e5..fd57ee5c00c 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -56,3 +56,17 @@ Declare keyboard intent on the action-owning primitive; never add document-level - Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example. Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating. + + +## Ordinary Button action geometry + +`Button` retains its existing appearance variants. For square actions use `iconSize`: +`compact` (24px on the spacing scale), `compact-fixed` (24px), `regular` (28px), +`roomy` (32px), or `touch` (40px). These values follow the root spacing scale; +only `compact-fixed` stays fixed when root text is enlarged. +Use `{ base: 'touch', sm: 'regular' }` for mobile/desktop targets. These props own +geometry only; colour, radius and SVG stroke continue to come from the selected +`variant` and `size`. `iconPadding` explicitly overrides the zero-padding geometry. +Use `shape='round'` for circular actions, or omit it to retain the current radius. +`size='inline'` is a 20px-high action with caption typography and compact horizontal +padding. Prefer these supported props to size, padding and radius overrides. diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc index 1ac5fc577f8..3f5cd1bf90e 100644 --- a/.cursor/rules/emcn-components.mdc +++ b/.cursor/rules/emcn-components.mdc @@ -57,3 +57,17 @@ Declare keyboard intent on the action-owning primitive; never add document-level - Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example. Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating. + + +## Ordinary Button action geometry + +`Button` retains its existing appearance variants. For square actions use `iconSize`: +`compact` (24px on the spacing scale), `compact-fixed` (24px), `regular` (28px), +`roomy` (32px), or `touch` (40px). These values follow the root spacing scale; +only `compact-fixed` stays fixed when root text is enlarged. +Use `{ base: 'touch', sm: 'regular' }` for mobile/desktop targets. These props own +geometry only; colour, radius and SVG stroke continue to come from the selected +`variant` and `size`. `iconPadding` explicitly overrides the zero-padding geometry. +Use `shape='round'` for circular actions, or omit it to retain the current radius. +`size='inline'` is a 20px-high action with caption typography and compact horizontal +padding. Prefer these supported props to size, padding and radius overrides. diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx index f15e2c82ef6..93f746d7c44 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx @@ -79,7 +79,8 @@ export function SelectOptionsEditor({ options, onChange }: SelectOptionsEditorPr size='sm' onClick={() => remove(option.id)} iconPadding='sm' - className='size-7 shrink-0' + iconSize='regular' + className='shrink-0' aria-label={`Remove ${option.name || 'option'}`} > diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx index 8fe52025569..c005de9d781 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx @@ -383,7 +383,8 @@ const FilterRuleRow = memo(function FilterRuleRow({ size='sm' onClick={() => onRemove(rule.id)} iconPadding='sm' - className='size-7 shrink-0' + iconSize='regular' + className='shrink-0' aria-label='Remove filter' > diff --git a/packages/emcn/src/components/bulk-action-button/bulk-action-button.tsx b/packages/emcn/src/components/bulk-action-button/bulk-action-button.tsx index 8088e98f093..e9717c2f7b9 100644 --- a/packages/emcn/src/components/bulk-action-button/bulk-action-button.tsx +++ b/packages/emcn/src/components/bulk-action-button/bulk-action-button.tsx @@ -19,7 +19,7 @@ export const bulkActionButtonVariants = cva( ) export interface BulkActionButtonProps - extends Omit { + extends Omit { /** Accessible name for the icon action; tooltip content is supplied separately. */ 'aria-label': string /** diff --git a/packages/emcn/src/components/button/button.test.tsx b/packages/emcn/src/components/button/button.test.tsx index c0b4fc35df3..4c8cae79dc0 100644 --- a/packages/emcn/src/components/button/button.test.tsx +++ b/packages/emcn/src/components/button/button.test.tsx @@ -1,5 +1,7 @@ -/** @vitest-environment node */ +/** @vitest-environment jsdom */ /** biome-ignore assist/source/organizeImports: Preserve the documented core/external/UI import order. */ +import { act, createRef } from 'react' +import { createRoot } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it } from 'vitest' import { Button } from '@sim/emcn' @@ -22,6 +24,9 @@ describe('Button iconSize', () => { for (const [iconSize, previousClass] of [ ['compact', 'size-6 p-0'], ['compact-fixed', 'size-[24px] p-0'], + ['regular', 'size-7 p-0'], + ['roomy', 'size-8 p-0'], + ['touch', 'size-10 p-0'], ] as const) { it.each(TREATMENTS)( `preserves the ${iconSize} treatment with size=$size and variant=$variant`, @@ -65,3 +70,82 @@ describe('Button iconSize', () => { expect(markup).not.toContain('iconPadding') }) }) + +describe('Button shared action geometry', () => { + it('composes responsive geometry, explicit padding and round shape without changing icon treatment', () => { + const markup = renderToStaticMarkup( + + ) + expect(markup).toContain('size-10') + expect(markup).toContain('sm:size-7') + expect(markup).not.toContain('size-[20px]') + expect(markup).toContain('p-1') + expect(markup).not.toContain('p-0') + expect(markup).toContain('rounded-full') + expect(markup).not.toContain('rounded-sm') + expect(markup).toContain('[stroke-width:1.25]') + expect(markup).toContain('text-[var(--text-icon-muted)]') + expect(markup).not.toMatch(/(?:iconSize|iconPadding|shape)=/) + }) + + it('retains an inline caption size and supports a base-only responsive value', () => { + const inline = renderToStaticMarkup() + expect(inline).toContain('h-[20px]') + expect(inline).toContain('text-caption') + expect(inline).toContain('px-1.5 py-0') + const baseOnly = renderToStaticMarkup( */ - iconSize?: VariantProps['iconSize'] + iconSize?: ButtonIconSize | { base: ButtonIconSize; sm?: ButtonIconSize } | null /** * Symmetric padding for icon actions whose content or layout determines their size. * Preserves the selected size's typography, corner radius and icon stroke. @@ -98,11 +118,23 @@ export interface ButtonProps } const Button = forwardRef( - ({ className, variant, size, iconSize, iconPadding, ...props }, ref) => { + ({ className, variant, size, iconSize, iconPadding, shape, ...props }, ref) => { + const baseIconSize = typeof iconSize === 'object' ? iconSize?.base : iconSize + const smIconSize = typeof iconSize === 'object' ? iconSize?.sm : undefined return ( - + +
{/* `disabled` on the fieldset reaches every native control inside, 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 f411281d21e..e158e4ab159 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 @@ -21,6 +21,10 @@ import type { ColumnDefinition, WorkflowGroup, WorkflowGroupOutput } from '@/lib import { columnMatchesRef, getColumnId } from '@/lib/table/column-keys' import { deriveOutputColumnName } from '@/lib/table/column-naming' import { FieldError } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/sidebar-fields' +import { + TableSidebarHeader, + TableSidebarHeaderAction, +} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header' import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types' import { useAddWorkflowGroup, @@ -232,31 +236,21 @@ export function EnrichmentConfig({ return (
-
+
- +
- -
+ +
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx index 0aebd34cb4c..421055477fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx @@ -1,9 +1,13 @@ 'use client' import { useState } from 'react' -import { Button, ChipInput, cn } from '@sim/emcn' +import { ChipInput, cn } from '@sim/emcn' import { Search, X } from '@sim/emcn/icons' import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' +import { + TableSidebarHeader, + TableSidebarHeaderAction, +} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header' import { ALL_ENRICHMENTS } from '@/enrichments' import { getEnrichment } from '@/enrichments/registry' import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types' @@ -71,19 +75,12 @@ function EnrichmentsSidebarBody({ if (editGroup && !editEnrichment) { return (
-
+

Enrichment

- -
+ +

This enrichment ("{editGroup.enrichmentId}") is no longer available. Delete the column @@ -118,19 +115,12 @@ function EnrichmentsSidebarBody({ return (

-
+

Enrichments

- -
+ +
+ {children} +
+ ) +} + +interface TableSidebarHeaderActionProps extends ButtonHTMLAttributes { + 'aria-label': string +} + +export const TableSidebarHeaderAction = forwardRef< + HTMLButtonElement, + TableSidebarHeaderActionProps +>(({ className, ...props }, ref) => ( + + )}

- -
+ +
{/* Single-output mode renames this column directly. */} @@ -838,23 +832,9 @@ export function WorkflowSidebarBody({ )} {selectedWorkflowId && ( <> -
- - - -
+ setShowAdvanced((v) => !v)}> + {showAdvanced ? 'Hide additional fields' : 'Show additional fields'} + {showAdvanced && ( <> collaborativeBatchToggleLocked([currentBlockId!])} aria-label='Unlock block' > - + ) : (
@@ -499,7 +505,7 @@ export function Editor() { - -
+ + {displayAdvancedOptions + ? 'Hide additional fields' + : 'Show additional fields'} + )} {hasAdditionalFields && !canEditBlock && displayAdvancedOptions && (
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 09ede00b538..27f4ee1ed93 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 @@ -875,12 +875,7 @@ export const Toolbar = memo(

Toolbar

{!isSearchActive ? ( - ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index b26c5ac8ccd..72d27a82b7e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -915,7 +915,7 @@ export const Panel = memo(function Panel() { diff --git a/packages/emcn/src/components/field-divider/field-divider.dom.test.tsx b/packages/emcn/src/components/field-divider/field-divider.dom.test.tsx new file mode 100644 index 00000000000..ed3a624673d --- /dev/null +++ b/packages/emcn/src/components/field-divider/field-divider.dom.test.tsx @@ -0,0 +1,64 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { FieldDisclosure } from './field-divider' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +afterEach(() => { + act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('FieldDisclosure', () => { + it('leaves expansion to the caller and does not submit its surrounding form', () => { + const onClick = vi.fn() + const onSubmit = vi.fn((event) => event.preventDefault()) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + const render = (expanded: boolean, disabled = false) => ( +
+ + {expanded ? 'Hide additional fields' : 'Show additional fields'} + + +
+ ) + act(() => root?.render(render(false))) + const button = container.querySelector('button')! + expect(button.getAttribute('aria-expanded')).toBe('false') + expect(button.getAttribute('aria-controls')).toBe('additional-fields') + act(() => { + button.focus() + button.click() + }) + expect(document.activeElement).toBe(button) + expect(onClick).toHaveBeenCalledTimes(1) + expect(onSubmit).not.toHaveBeenCalled() + expect(button.getAttribute('aria-expanded')).toBe('false') + + act(() => root?.render(render(true))) + expect(button.getAttribute('aria-expanded')).toBe('true') + expect(button.textContent).toBe('Hide additional fields') + expect(container.querySelector('#additional-fields')?.hidden).toBe(false) + + act(() => root?.render(render(true, true))) + act(() => button.click()) + expect(onClick).toHaveBeenCalledTimes(1) + expect(onSubmit).not.toHaveBeenCalled() + }) +}) diff --git a/packages/emcn/src/components/field-divider/field-divider.tsx b/packages/emcn/src/components/field-divider/field-divider.tsx index 7408a21360f..4b15d867f80 100644 --- a/packages/emcn/src/components/field-divider/field-divider.tsx +++ b/packages/emcn/src/components/field-divider/field-divider.tsx @@ -1,4 +1,6 @@ +import { ChevronDown } from '../../icons' import { cn } from '../../lib/cn' +import { Button, type ButtonProps } from '../button/button' const DASHED_DIVIDER_STYLE = { backgroundImage: @@ -54,4 +56,40 @@ function FieldDivider({ className, subblockMarker = false, ...props }: FieldDivi ) } -export { DashedDividerLine, FieldDivider } +export interface FieldDisclosureProps + extends Pick { + expanded: boolean +} + +/** + * Controlled disclosure between field groups. The caller owns the label, + * expanded state and fields; this supplies the divider lines and rotating chevron. + * + * @example + * + * {expanded ? 'Hide additional fields' : 'Show additional fields'} + * + */ +function FieldDisclosure({ expanded, children, ...props }: FieldDisclosureProps) { + return ( +
+ + + +
+ ) +} + +export { DashedDividerLine, FieldDisclosure, FieldDivider } diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index 8a6c0391020..aa3313646f2 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -154,7 +154,12 @@ export { dropdownMenuRowClass, } from './dropdown-menu/dropdown-menu' export { Expandable, ExpandableContent } from './expandable/expandable' -export { DashedDividerLine, FieldDivider } from './field-divider/field-divider' +export { + DashedDividerLine, + FieldDisclosure, + type FieldDisclosureProps, + FieldDivider, +} from './field-divider/field-divider' export { Info } from './info/info' export { InfoCard, From 2365885c5258986910d7a1692b248e5f8cc8cd83 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 13:12:49 -0700 Subject: [PATCH 03/10] fix(ui): preserve tiny actions and use shared sidebar geometry --- .../chat/components/message/components/file-download.tsx | 2 +- .../app/(interfaces)/chat/components/message/message.tsx | 2 +- .../table-sidebar-header/table-sidebar-header.tsx | 3 ++- .../components/panel/components/editor/editor.tsx | 8 ++++---- .../components/panel/components/toolbar/toolbar.tsx | 7 ++++++- .../w/[workflowId]/components/panel/panel.tsx | 4 ++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx index 0e3c40e1f15..938ea3f2b3c 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx @@ -254,7 +254,7 @@ export function ChatFileDownloadAll({ files }: ChatFileDownloadAllProps) { variant='ghost-secondary' onClick={handleDownloadAll} disabled={isDownloading} - size='icon' + className='p-0' > {isDownloading ? ( diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.tsx index 1b99ccf96cc..9458ed16be7 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.tsx @@ -274,7 +274,7 @@ export const ClientChatMessage = memo(function ClientChatMessage({ ) : (
@@ -505,7 +505,7 @@ export function Editor() { ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 72d27a82b7e..b26c5ac8ccd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -915,7 +915,7 @@ export const Panel = memo(function Panel() { From 7bf8fe33556e9113df43be870e3618250ab6bf27 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos <157128530+BillLeoutsakosvl346@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:27:38 -0700 Subject: [PATCH 04/10] improvement(ui): use shared geometry for standard icon actions (#8153) Co-authored-by: Bill Leoutsakos --- .../components/input/public-chat-action-button.tsx | 4 ++-- apps/sim/app/playground/page.tsx | 4 ++-- .../file-viewer/rich-markdown-editor/image.tsx | 4 +++- .../rich-markdown-editor/menus/toolbar-button.test.tsx | 3 ++- .../rich-markdown-editor/menus/toolbar-button.tsx | 10 ++++------ .../components/mic-button/mic-button.test.tsx | 2 +- .../user-input/components/mic-button/mic-button.tsx | 10 ++++------ 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx b/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx index 838051a1817..0ac57babb30 100644 --- a/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx +++ b/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx @@ -4,7 +4,7 @@ import { Button } from '@sim/emcn' interface PublicChatActionButtonProps extends Omit< ComponentProps, - 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' + 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' | 'shape' > { variant: 'primary' | 'quiet' 'aria-label': string @@ -12,6 +12,6 @@ interface PublicChatActionButtonProps /** Public chat's circular composer action, retaining its primary and quiet palettes. */ export const PublicChatActionButton = forwardRef( - (props, ref) => @@ -187,7 +187,7 @@ export default function PlaygroundPage() { aria-label={isDarkMode ? 'Light mode' : 'Dark mode'} variant='default' onClick={toggleDarkMode} - className='size-8 p-0' + iconSize='roomy' > {isDarkMode ? : } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx index d6623e648d6..24897d1edfa 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx @@ -282,9 +282,11 @@ export function ResizableImageView({ node, selected, editor, getPos }: ReactNode type='button' variant='ghost' size='icon' + iconSize={{ base: 'touch', sm: 'roomy' }} + iconPadding='sm' aria-label='Resize image' onPointerDown={startResize} - className='absolute right-0 bottom-0 flex size-10 cursor-nwse-resize touch-none items-end justify-end p-1 sm:size-8' + className='absolute right-0 bottom-0 cursor-nwse-resize touch-none items-end justify-end' > diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.test.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.test.tsx index 340a43c1e50..4ce6c90a039 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.test.tsx @@ -41,7 +41,8 @@ describe('ToolbarButton', () => { ) const button = host.querySelector('button[aria-label="Add to Chat"]') - expect(button?.className).toContain('size-[28px]') + expect(button?.classList.contains('size-10')).toBe(true) + expect(button?.classList.contains('sm:size-7')).toBe(true) expect(button?.querySelector('svg')?.className.baseVal).toContain('size-[12px]') }) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx index 4bdcb5b19d8..7d2aa9f6a58 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx @@ -1,5 +1,5 @@ import type { ComponentType, SVGProps } from 'react' -import { Button, cn, Tooltip } from '@sim/emcn' +import { Button, Tooltip } from '@sim/emcn' interface ToolbarButtonProps { /** Any SVG icon component, e.g. from `@sim/emcn/icons`. */ @@ -28,17 +28,15 @@ export function ToolbarButton({ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.test.tsx index 8364c6d1fc1..96094c31efb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.test.tsx @@ -72,7 +72,7 @@ describe('MicButton', () => { const waveform = container.querySelector('svg[viewBox="0 0 18 18"]') const bars = waveform?.querySelectorAll('line') - expect(button?.className).toContain('size-[28px]') + expect(button?.classList.contains('size-7')).toBe(true) expect(button?.className).toContain('overflow-hidden') expect(button?.className).toContain('rounded-full') expect(waveform?.classList.contains('size-[18px]')).toBe(true) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.tsx index 2e2b6a51cae..452d8d26f8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.tsx @@ -89,15 +89,13 @@ export const MicButton = memo(function MicButton({ + {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 b3a28c53f71..59164f6d861 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 @@ -28,6 +28,7 @@ import { Duplicate, Eye, handleKeyboardActivation, + OverlayActionButton, Redo, Search as SearchIcon, Tooltip, @@ -158,7 +159,8 @@ export const WorkflowOutputSection = memo( - + {copied ? 'Copied' : 'Copy'} - + Search diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx index c0bbd87f022..2c498f71f6b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -607,7 +607,7 @@ function ServerDetailView({ : 'json' } wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' /> {activeConfigTab === 'cursor' && ( + {props.config && ( )} - + ) } @@ -274,7 +270,7 @@ function ColumnConfigBody({ -
+ {/* `disabled` on the fieldset reaches every native control inside, including the comboboxes' trigger buttons; `contents` keeps the existing layout. Values stay readable and selectable. */} @@ -383,7 +379,7 @@ function ColumnConfigBody({ )} -
+
- - Open workflow - + + window.open( + `/workspace/${workspaceId}/w/${selectedWorkflowId}`, + '_blank', + 'noopener,noreferrer' + ) + } + > + + )} ) : ( @@ -849,7 +836,7 @@ export function WorkflowSidebarBody({ )} )} -
+
@@ -533,7 +533,7 @@ console.log(limits);` code={getStreamCommand()} language={LANGUAGE_SYNTAX[language]} wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' />
@@ -579,7 +579,7 @@ console.log(limits);` code={getAsyncCommand()} language={LANGUAGE_SYNTAX[language]} wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' />
)} 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 2245b6a3647..8c2f754da9f 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 @@ -231,7 +231,7 @@ export function Versions({ className={cn( 'flex h-[36px] cursor-pointer items-center px-4 transition-colors duration-100', isSelected - ? 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover-hover:bg-[color-mix(in_srgb,var(--accent)_15%,transparent)]' + ? 'bg-[color-mix(in_srgb,hsl(var(--accent))_10%,transparent)] hover-hover:bg-[color-mix(in_srgb,hsl(var(--accent))_15%,transparent)]' : 'hover-hover:bg-[var(--surface-6)] dark:hover-hover:bg-[var(--border)]' )} onClick={() => handleRowClick(v.version)} @@ -329,12 +329,8 @@ export function Versions({
- + See preview diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx index 5169f4697fd..c71ae2bc46f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx @@ -1114,6 +1114,7 @@ export function ConditionInput({ overlayRefs.current.delete(block.id) } }} + data-preview-full-opacity={isPreview || undefined} className={cn( 'pointer-events-none absolute inset-0 box-border overflow-auto whitespace-pre-wrap break-words border border-transparent bg-transparent px-3 py-2 font-sans text-sm', (isPreview || disabled) && 'opacity-50' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx index 6c659aff3dc..5fd799386d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx @@ -197,6 +197,7 @@ export function EvalInput({ className='text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50' />
{isDeleting ? (
) : ( - + )}
@@ -770,12 +771,13 @@ export function FileUpload({ iconSize='compact' className='-translate-y-1/2 absolute top-1/2 right-[4px]' onClick={(e) => handleRemoveFile(file, e)} - disabled={isDeleting} + disabled={disabled || isPreview || isDeleting} + data-preview-full-opacity={isPreview || undefined} > {isDeleting ? (
) : ( - + )}
@@ -972,7 +974,7 @@ export function FileUpload({ onOpenChange={(open) => { if (open) void refetchWorkspaceFiles() }} - disabled={disabled} + disabled={disabled || isPreview} isLoading={loadingWorkspaceFiles} formatFileSize={formatFileSize} truncateMiddle={truncateMiddle} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx index d385a77ab71..b503433c4af 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx @@ -123,7 +123,7 @@ export function GroupedCheckboxList({ )} > - + Configure PII Types
(
+
{ if (el) descriptionOverlayRefs.current[field.id] = el }} + data-preview-full-opacity={isPreview || undefined} style={{ scrollbarWidth: 'none' }} className={cn( 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 54b7d751123..adecacf7d6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1634,6 +1634,7 @@ export const ToolInput = memo(function ToolInput({ onDrop={(e) => handleDrop(e, toolIndex)} >
Generate @@ -1244,6 +1245,7 @@ function SubBlockComponent({ onMouseDown={handleMouseDown} data-workflow-search-subblock-id={config.id} data-workflow-search-canonical-id={config.canonicalParamId ?? config.id} + data-preview-readonly={(isPreview && !disabled) || undefined} className='subblock-content flex flex-col gap-2.5' > {renderLabel( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 61bd02da17b..9222584ba9e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -24,6 +24,7 @@ import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { useStoreWithEqualityFn } from 'zustand/traditional' +import { WorkflowPreviewAction } from '@/components/workflow/workflow-preview-action' import { isMcpRuntimeReference } from '@/lib/mcp/operation-policy' import { resolveMcpBlockConfig } from '@/lib/mcp/workflow-config' import { captureEvent } from '@/lib/posthog/client' @@ -610,21 +611,12 @@ export function Editor() { lightweight />
- - - - - Open workflow - + + + ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx index 7eb787be542..daa24590d8e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx @@ -66,7 +66,8 @@ const OutputCodeContent = React.memo(function OutputCodeContent({ code={code} showGutter language={language} - className='m-0 min-h-full rounded-none border-0 bg-[var(--bg)] dark:bg-[var(--bg)]' + appearance='flat' + className='m-0 min-h-full' paddingLeft={8} gutterStyle={{ backgroundColor: 'transparent' }} wrapText={wrapText} 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 c7c6880bf90..bba86b895e8 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 { @@ -29,6 +30,7 @@ import { import { formatDuration } from '@sim/utils/formatting' import { ReactFlowProvider } from '@xyflow/react' import { useParams } from 'next/navigation' +import { WorkflowPreviewAction } from '@/components/workflow/workflow-preview-action' import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references' import { buildCanonicalIndexForSurface, @@ -41,6 +43,7 @@ import { import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu' +import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles' import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow' import { getBlock } from '@/blocks' import { BlockTile } from '@/blocks/block-tile' @@ -52,31 +55,6 @@ import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { useContextMenu } from '@/hooks/use-context-menu' import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types' -/** - * CSS override to show full opacity and prevent interaction in readonly preview mode. - * Extracted to avoid duplicating the style block in multiple places. - */ -const READONLY_PREVIEW_STYLES = ` - .readonly-preview, - .readonly-preview * { - cursor: default !important; - } - .readonly-preview [disabled], - .readonly-preview [data-disabled], - .readonly-preview input, - .readonly-preview textarea, - .readonly-preview [role="combobox"], - .readonly-preview [role="slider"], - .readonly-preview [role="switch"], - .readonly-preview [role="checkbox"] { - opacity: 1 !important; - pointer-events: none; - } - .readonly-preview .opacity-50 { - opacity: 1 !important; - } -` - /** * Format a value for display as JSON string */ @@ -604,7 +582,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro return (
{/* Type Selection - matches SubflowEditor */} -
+
@@ -627,7 +605,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro {isCountMode ? ( -
+
- + {copiedSection === 'input' ? 'Copied' : 'Copy'} @@ -1258,18 +1235,16 @@ function PreviewEditorContent({ - + Search @@ -1295,8 +1270,9 @@ function PreviewEditorContent({ - + {copiedSection === 'output' ? 'Copied' : 'Copy'} @@ -1332,18 +1306,16 @@ function PreviewEditorContent({ - + Search @@ -1387,31 +1359,18 @@ function PreviewEditorContent({ cursorStyle='grab' />
- - - - - - {isExecutionMode && onDrillDown ? 'Expand workflow' : 'Open in new tab'} - - + + {isExecutionMode && onDrillDown ? ( + + ) : ( + + )} + ) : (
@@ -1450,7 +1409,6 @@ function PreviewEditorContent({ ...subBlockValues, __canonicalModes: canonicalModeOverrides, }} - disabled={true} /> {index < visibleSubBlocks.length - 1 && ( ({ useParams: () => ({ workspaceId: 'workspace-1' }) })) +vi.mock('@/hooks/use-webhook-management', () => ({ + useWebhookManagement: () => ({ webhookUrl: null }), +})) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value', + () => ({ useSubBlockValue: () => [undefined, vi.fn()] }) +) + +import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' +import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles' +import type { SubBlockConfig } from '@/blocks/types' + +const config: SubBlockConfig = { id: 'enabled', type: 'switch', title: 'Enabled' } + +let container: HTMLDivElement +let root: Root + +beforeEach(() => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() +}) + +describe('workflow preview read-only appearance', () => { + it('shows a preview value at full opacity while retaining the disabled interaction state', () => { + act(() => + root.render( + <> + +
+
+ +
+ +
+
+
+ +
+
+ + ) + ) + + const preview = container.querySelector('[data-testid="preview"]')! + const previewSwitch = preview.querySelector('[role="switch"]') as HTMLButtonElement + const disabled = container.querySelector('[data-testid="disabled"]')! + const disabledSwitch = disabled.querySelector('[role="switch"]') as HTMLButtonElement + + expect(preview.querySelector('[data-preview-readonly]')).not.toBeNull() + expect(previewSwitch.hasAttribute('disabled')).toBe(true) + expect(previewSwitch.getAttribute('aria-checked')).toBe('true') + expect(getComputedStyle(previewSwitch).opacity).toBe('1') + expect(getComputedStyle(previewSwitch).pointerEvents).toBe('none') + const removeButton = preview.querySelector('button:not([role="switch"])') as HTMLButtonElement + expect(getComputedStyle(removeButton).pointerEvents).toBe('none') + expect(getComputedStyle(removeButton).opacity).toBe('0.5') + + act(() => { + previewSwitch.click() + previewSwitch.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })) + }) + expect(previewSwitch.getAttribute('aria-checked')).toBe('true') + + expect(disabled.querySelector('[data-preview-readonly]')).toBeNull() + expect(disabledSwitch.hasAttribute('disabled')).toBe(true) + expect(getComputedStyle(disabledSwitch).opacity).toBe('0.5') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts new file mode 100644 index 00000000000..3fe8107450f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts @@ -0,0 +1,27 @@ +/** + * Keep preview fields noninteractive while showing intentionally read-only controls + * and their marked value overlays at full opacity in both preview sections. + */ +export const READONLY_PREVIEW_STYLES = ` + .readonly-preview, + .readonly-preview * { + cursor: default !important; + } + .readonly-preview [data-preview-readonly] :is( + input, + textarea, + [role="combobox"], + [role="slider"], + [role="switch"], + [role="checkbox"] + ) { + opacity: 1 !important; + pointer-events: none; + } + .readonly-preview [data-preview-readonly] :is(button, [role="button"]) { + pointer-events: none; + } + .readonly-preview [data-preview-readonly] [data-preview-full-opacity] { + opacity: 1 !important; + } +` diff --git a/apps/sim/components/workflow/workflow-preview-action.tsx b/apps/sim/components/workflow/workflow-preview-action.tsx new file mode 100644 index 00000000000..96ee8275afd --- /dev/null +++ b/apps/sim/components/workflow/workflow-preview-action.tsx @@ -0,0 +1,33 @@ +'use client' + +import { type ComponentProps, forwardRef } from 'react' +import { OverlayActionButton, Tooltip } from '@sim/emcn' + +interface WorkflowPreviewActionProps + extends Omit< + ComponentProps, + 'size' | 'type' | 'className' | 'shape' + > { + 'aria-label': string +} + +/** Overlay corner action shared by embedded workflow previews. */ +export const WorkflowPreviewAction = forwardRef( + ({ 'aria-label': label, ...props }, ref) => ( + + + + + {label} + + ) +) + +WorkflowPreviewAction.displayName = 'WorkflowPreviewAction' diff --git a/packages/emcn/src/components/code/code.test.tsx b/packages/emcn/src/components/code/code.test.tsx index ebe7d5b1ea6..f9b249ab21c 100644 --- a/packages/emcn/src/components/code/code.test.tsx +++ b/packages/emcn/src/components/code/code.test.tsx @@ -1,10 +1,10 @@ /** * @vitest-environment jsdom */ -import { act } from 'react' +import { act, createRef } from 'react' import { sleep } from '@sim/utils/helpers' import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Code } from './code' let root: Root | null = null @@ -12,6 +12,14 @@ let host: HTMLDivElement | null = null beforeEach(() => { ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + vi.stubGlobal( + 'ResizeObserver', + class { + observe() {} + unobserve() {} + disconnect() {} + } + ) host = document.createElement('div') document.body.appendChild(host) root = createRoot(host) @@ -20,6 +28,7 @@ beforeEach(() => { afterEach(() => { if (root) act(() => root?.unmount()) host?.remove() + vi.unstubAllGlobals() root = null host = null }) @@ -60,3 +69,79 @@ describe('Code.Viewer workflow references', () => { expect(host?.querySelector('[data-search-match]')?.textContent).toBe('result') }) }) + +describe('Code.Viewer appearances', () => { + for (const virtualized of [false, true]) { + it(`applies the inspection surface to ${virtualized ? 'virtualized' : 'standard'} output`, async () => { + await act(async () => { + root?.render( + + ) + await sleep(1) + }) + + const viewer = host?.firstElementChild + expect(viewer?.classList.contains('rounded-md')).toBe(true) + expect(viewer?.classList.contains('border-0')).toBe(true) + expect(viewer?.classList.contains('bg-[var(--surface-4)]!')).toBe(true) + expect(viewer?.classList.contains('dark:bg-[var(--surface-3)]!')).toBe(true) + expect(viewer?.classList.contains('max-h-[300px]')).toBe(true) + }) + } + + it('keeps the flat viewer separate from the default code container', async () => { + await act(async () => { + root?.render( + <> + + + + ) + await sleep(1) + }) + + const [defaultViewer, flatViewer] = Array.from(host?.children ?? []) + expect(defaultViewer.classList.contains('rounded-sm')).toBe(true) + expect(flatViewer.classList.contains('rounded-none')).toBe(true) + expect(flatViewer.classList.contains('bg-[var(--bg)]')).toBe(true) + expect(flatViewer.classList.contains('dark:bg-[var(--bg)]')).toBe(true) + expect(flatViewer.textContent).toContain('flat') + }) + + it('applies flat chrome on the virtualized gutter path used by the terminal', async () => { + const contentRef = createRef() + await act(async () => { + root?.render( + + ) + await sleep(1) + }) + + const viewer = host?.firstElementChild + expect(contentRef.current).toBe(viewer) + expect(viewer?.classList.contains('rounded-none')).toBe(true) + expect(viewer?.classList.contains('border-0')).toBe(true) + expect(viewer?.classList.contains('bg-[var(--bg)]')).toBe(true) + expect(viewer?.classList.contains('dark:bg-[var(--bg)]')).toBe(true) + expect(viewer?.classList.contains('overflow-x-hidden')).toBe(true) + expect(viewer?.classList.contains('min-h-full')).toBe(true) + expect(viewer?.classList.contains('rounded-sm')).toBe(false) + }) +}) diff --git a/packages/emcn/src/components/code/code.tsx b/packages/emcn/src/components/code/code.tsx index 2e540301caa..2f0a42a5781 100644 --- a/packages/emcn/src/components/code/code.tsx +++ b/packages/emcn/src/components/code/code.tsx @@ -13,6 +13,7 @@ import { import { escapeRegExp } from '@sim/utils/string' import { findWorkflowReferenceTokens } from '@sim/utils/workflow-references' import { useVirtualizer } from '@tanstack/react-virtual' +import { cva, type VariantProps } from 'class-variance-authority' import { ChevronRight } from '../../icons' import { cn } from '../../lib/cn' import './code.css' @@ -856,6 +857,18 @@ function applySearchHighlightingToLine( */ type CodeViewerDensity = 'default' | 'compact' +/** Container appearances shared by the standard and virtualized viewers. */ +export const codeViewerAppearanceVariants = cva('', { + variants: { + appearance: { + default: '', + inspection: 'rounded-md border-0 bg-[var(--surface-4)]! dark:bg-[var(--surface-3)]!', + flat: 'rounded-none border-0 bg-[var(--bg)] dark:bg-[var(--bg)]', + }, + }, + defaultVariants: { appearance: 'default' }, +}) + interface CodeViewerProps { /** Code content to display */ code: string @@ -865,6 +878,8 @@ interface CodeViewerProps { language?: 'javascript' | 'json' | 'python' | 'bash' | 'toml' /** Additional CSS classes for the container */ className?: string + /** Container appearance for code inspected in logs/previews or on flat surfaces. */ + appearance?: NonNullable['appearance']> /** Visual density for read-only code. */ density?: CodeViewerDensity /** Highlight Sim `{{ENV}}` and `` references with the platform accent. */ @@ -948,6 +963,7 @@ type ViewerInnerProps = { language: 'javascript' | 'json' | 'python' | 'bash' | 'toml' /** Additional CSS classes for the container */ className?: string + appearance: NonNullable /** Visual density for read-only code. */ density: CodeViewerDensity highlightWorkflowReferences: boolean @@ -978,6 +994,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ showGutter, language, className, + appearance, density, highlightWorkflowReferences, paddingLeft, @@ -1147,6 +1164,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ wrapText ? 'overflow-x-hidden' : 'overflow-x-auto', 'overflow-y-auto', 'dark:bg-[var(--code-bg)]', + codeViewerAppearanceVariants({ appearance }), className )} style={{ height: containerHeight }} @@ -1196,6 +1214,7 @@ const ViewerInner = memo(function ViewerInner({ showGutter, language, className, + appearance, density, highlightWorkflowReferences, paddingLeft, @@ -1309,7 +1328,7 @@ const ViewerInner = memo(function ViewerInner({ // Grid-based rendering for gutter alignment (works with wrap) if (showGutter) { return ( - +
+
 root?.render(children))
+  return container
+}
+
+afterEach(() => {
+  if (root) act(() => root?.unmount())
+  container?.remove()
+  root = null
+  container = null
+  vi.useRealTimers()
+})
+
+/** Pre-migration recipes from log details and the deployment preview. */
+const PREVIOUS = [
+  {
+    name: 'default 20px adaptive action',
+    props: {},
+    variant: 'default',
+    className:
+      'size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]',
+  },
+  {
+    name: '28px adaptive action',
+    props: { size: 'md' },
+    variant: 'default',
+    className:
+      'size-[28px] cursor-pointer bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]',
+  },
+] as const
+
+describe('OverlayActionButton', () => {
+  it.each(PREVIOUS)('preserves the previous $name markup', ({ props, variant, className }) => {
+    const view = mount(
+      <>
+        
+        
+          
+        
+      
+    )
+    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)
+  })
+})
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..6a2f52fc4f0
--- /dev/null
+++ b/packages/emcn/src/components/overlay-action-button/overlay-action-button.tsx
@@ -0,0 +1,49 @@
+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 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]',
+      },
+    },
+    defaultVariants: { size: 'sm' },
+  }
+)
+
+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']>
+}
+
+/**
+ * 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, className, ...props }, ref) => (
+    
+    )
+    const after = renderToStaticMarkup(
+      
+    )
+    expect(normalizeClasses(after)).toBe(normalizeClasses(before))
+    expect(after).not.toContain('focusRing=')
+    expect(renderToStaticMarkup()).not.toContain('focus-visible:ring-2')
+  })
+
   it('forwards refs and native focus, submission and disabled behavior with responsive sizing', () => {
     const container = document.createElement('div')
     document.body.appendChild(container)
diff --git a/packages/emcn/src/components/button/button.tsx b/packages/emcn/src/components/button/button.tsx
index cc93c956b25..cb919931304 100644
--- a/packages/emcn/src/components/button/button.tsx
+++ b/packages/emcn/src/components/button/button.tsx
@@ -1,6 +1,7 @@
 import { type ButtonHTMLAttributes, forwardRef } from 'react'
 import { cva, type VariantProps } from 'class-variance-authority'
 import { cn } from '../../lib/cn'
+import { mutedFocusRingClass } from '../../lib/focus-ring'
 
 /**
  * `size='icon'` is the square 20px icon-only button — a chip field's trailing
@@ -61,6 +62,9 @@ const buttonVariants = cva(
       shape: {
         round: 'rounded-full',
       },
+      focusRing: {
+        muted: mutedFocusRingClass,
+      },
       iconPadding: {
         sm: 'p-1',
         md: 'p-1.5',
@@ -118,7 +122,7 @@ export interface ButtonProps
 }
 
 const Button = forwardRef(
-  ({ className, variant, size, iconSize, iconPadding, shape, ...props }, ref) => {
+  ({ className, variant, size, iconSize, iconPadding, shape, focusRing, ...props }, ref) => {
     const baseIconSize = typeof iconSize === 'object' ? iconSize?.base : iconSize
     const smIconSize = typeof iconSize === 'object' ? iconSize?.sm : undefined
     return (
@@ -131,6 +135,7 @@ const Button = forwardRef(
             iconSize: baseIconSize,
             iconPadding,
             shape,
+            focusRing,
           }),
           smIconSize && responsiveIconSizes[smIconSize],
           className
diff --git a/packages/emcn/src/components/field-divider/field-divider.tsx b/packages/emcn/src/components/field-divider/field-divider.tsx
index 4b15d867f80..9e11f4b5a10 100644
--- a/packages/emcn/src/components/field-divider/field-divider.tsx
+++ b/packages/emcn/src/components/field-divider/field-divider.tsx
@@ -79,8 +79,9 @@ function FieldDisclosure({ expanded, children, ...props }: FieldDisclosureProps)
         type='button'
         variant='ghost'
         size={null}
+        focusRing='muted'
         aria-expanded={expanded}
-        className='gap-1.5 whitespace-nowrap p-0 text-small focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-2)]'
+        className='gap-1.5 whitespace-nowrap p-0 text-small'
       >
         {children}
          {}
 
@@ -30,7 +31,12 @@ const Slider = React.forwardRef, S
       
         
       
-      
+      
     
   )
 )
diff --git a/packages/emcn/src/components/switch/switch.tsx b/packages/emcn/src/components/switch/switch.tsx
index 1d5b0c8be9c..ed8e455e9f4 100644
--- a/packages/emcn/src/components/switch/switch.tsx
+++ b/packages/emcn/src/components/switch/switch.tsx
@@ -3,6 +3,7 @@
 import * as React from 'react'
 import * as SwitchPrimitives from '@radix-ui/react-switch'
 import { cn } from '../../lib/cn'
+import { mutedFocusRingClass } from '../../lib/focus-ring'
 
 /**
  * Switch component styled to match Sim's design system.
@@ -16,7 +17,8 @@ const Switch = React.memo(
     
Date: Thu, 24 Sep 2026 15:55:34 -0700
Subject: [PATCH 07/10] fix(ui): make closed table sidebars inert and
 strengthen focus ring

---
 .../components/table-sidebar-layout.test.tsx  |  3 ++
 .../components/table-sidebar-layout.tsx       |  1 +
 .../src/components/button/button.test.tsx     |  4 +-
 .../overlay-action-button.test.tsx            | 46 ++++++++++---------
 .../overlay-action-button.tsx                 |  2 +-
 packages/emcn/src/lib/focus-ring.ts           |  2 +-
 6 files changed, 33 insertions(+), 25 deletions(-)

diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.test.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.test.tsx
index a9b03496744..b1f0a09b495 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.test.tsx
@@ -45,6 +45,7 @@ describe('table sidebar layout', () => {
     expect(dialog.getAttribute('aria-label')).toBe('Configure workflow')
     expect(dialog.classList.contains('translate-x-0')).toBe(true)
     expect(dialog.classList.contains('shadow-overlay')).toBe(true)
+    expect(dialog.hasAttribute('inert')).toBe(false)
 
     input.value = 'Edited workflow'
     scrollBody.scrollTop = 64
@@ -56,9 +57,11 @@ describe('table sidebar layout', () => {
     expect(scrollBody.scrollTop).toBe(64)
     expect(dialog.classList.contains('translate-x-full')).toBe(true)
     expect(dialog.classList.contains('shadow-overlay')).toBe(false)
+    expect(dialog.hasAttribute('inert')).toBe(true)
 
     render(true)
     expect(dialog.classList.contains('translate-x-0')).toBe(true)
+    expect(dialog.hasAttribute('inert')).toBe(false)
     expect(input.value).toBe('Edited workflow')
     expect(scrollBody.scrollTop).toBe(64)
   })
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.tsx
index 7315df41c52..728d6e4c410 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-layout.tsx
@@ -17,6 +17,7 @@ export function TableSidebarShell({
     
{isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 w-[94px]' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - -
+ )} {typeof document !== 'undefined' && createPortal( 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 59164f6d861..dc169dd44f5 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 @@ -15,7 +15,6 @@ import { Badge, Button, Chip, - ChipInput, ChipModalTabs, Code, cn, @@ -35,8 +34,6 @@ import { useCopyToClipboard, } from '@sim/emcn' import { - ArrowDown, - ArrowUp, Check, ChevronUp, Clipboard, @@ -60,6 +57,7 @@ import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-s import type { TraceSpan } from '@/lib/logs/types' import { sendMothershipMessage } from '@/lib/mothership/events' import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' +import { CodeSearchOverlay } from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' /** * Deep imports on purpose: importing these back through the parent `logs/components` * barrel forms a parent->child cycle that would keep the barrel edge to the snapshot @@ -209,54 +207,18 @@ export const WorkflowOutputSection = memo( {/* Search Overlay */} {isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 w-[94px]' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - -
+ )} {/* Context Menu - rendered in portal to avoid transform/overflow clipping */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx index daa24590d8e..743a822da77 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx @@ -5,7 +5,6 @@ import { Button, Code, cn, - Input, Popover, PopoverContent, PopoverItem, @@ -13,8 +12,6 @@ import { Tooltip, } from '@sim/emcn' import { - ArrowDown, - ArrowUp, Check, Clipboard, Download, @@ -26,6 +23,7 @@ import { } from '@sim/emcn/icons' import Link from 'next/link' import { AgentStreamThinkingChrome } from '@/components/agent-stream/agent-stream-chrome' +import { CodeSearchOverlay } from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' import { OutputContextMenu, StructuredOutput, @@ -493,58 +491,19 @@ export const OutputPanel = React.memo(function OutputPanel({ {/* Search Overlay */} {isOutputSearchActive && ( -
e.stopPropagation()} - data-toolbar-root - data-search-active='true' - > - setOutputSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 h-[23px] w-[94px] text-caption' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : 'No results'} - - - - -
+ )} {/* Content */} 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 bba86b895e8..debd78b8257 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 @@ -16,8 +16,6 @@ import { Tooltip, } from '@sim/emcn' import { - ArrowDown, - ArrowUp, Check, ChevronDown, ChevronUp, @@ -41,6 +39,7 @@ import { isToolInputOnlySubBlock, } from '@/lib/workflows/subblocks/visibility' import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' +import { CodeSearchOverlay } from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu' import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles' @@ -1454,49 +1453,18 @@ function PreviewEditorContent({ {/* Search Overlay */} {isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 h-[23px] w-[94px] text-caption' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - -
+ )} {/* Context Menu */} diff --git a/packages/emcn/src/components/chip-input/chip-input.tsx b/packages/emcn/src/components/chip-input/chip-input.tsx index 444cbb1704a..78aef8658b9 100644 --- a/packages/emcn/src/components/chip-input/chip-input.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.tsx @@ -14,6 +14,8 @@ * same 1.5 gap as `Chip`. It shares the chip-field chrome with * {@link ChipTextarea}, shows no focus ring — keep the surface calm and rely on * the caret for focus. Pass `error` to swap the border to the error token. + * `appearance='compactSearch'` owns the existing 23px code-search field + * treatment without changing the 30px chip default. * * @example * ```tsx @@ -27,12 +29,28 @@ * ``` */ import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '../../lib/cn' import { chipFieldSurfaceClass, chipFieldTextClass, chipGeometryClass } from '../chip/chip-chrome' type ChipInputIcon = React.ComponentType<{ className?: string }> -export interface ChipInputProps extends Omit, 'size'> { +/** The compact search field keeps the existing code-viewer input geometry. */ +export const chipInputVariants = cva('', { + variants: { + appearance: { + chip: '', + compactSearch: `${chipFieldSurfaceClass} h-[23px] items-center rounded-sm px-2 dark:bg-[var(--surface-5)]`, + }, + }, + defaultVariants: { appearance: 'chip' }, +}) + +export interface ChipInputProps + extends Omit, 'size'>, + VariantProps { + /** The default chip field or the compact code-search field. */ + appearance?: VariantProps['appearance'] /** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */ icon?: ChipInputIcon /** Custom leading content, such as a color swatch. Takes precedence over `icon`. */ @@ -54,6 +72,7 @@ export interface ChipInputProps extends Omit( ( { + appearance = 'chip', className, inputClassName, icon: Icon, @@ -69,8 +88,9 @@ export const ChipInput = React.forwardRef(
( type={type} disabled={disabled} className={cn( - '-ml-1 h-full w-full bg-transparent indent-1 disabled:cursor-not-allowed', - chipFieldTextClass, + appearance === 'compactSearch' + ? 'h-full w-full touch-manipulation scroll-pr-1 bg-transparent font-sans text-[var(--text-primary)] text-caption outline-hidden [letter-spacing:inherit] placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed' + : cn( + '-ml-1 h-full w-full bg-transparent indent-1 disabled:cursor-not-allowed', + chipFieldTextClass + ), inputClassName )} {...props} diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index 2436b32f65d..28da4499541 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -64,7 +64,7 @@ export { ChipEmailsInput, type ChipEmailsInputProps, } from './chip-emails-input/chip-emails-input' -export { ChipInput, type ChipInputProps } from './chip-input/chip-input' +export { ChipInput, type ChipInputProps, chipInputVariants } from './chip-input/chip-input' export { type ChipConfirmAction, type ChipConfirmDefaultAction, From cc2699ef523d5dcb863dd0857e05f741c6663418 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 16:20:35 -0700 Subject: [PATCH 09/10] fix(ui): announce code search result changes --- .../code-search-overlay/code-search-overlay.test.tsx | 5 +++++ .../components/code-search-overlay/code-search-overlay.tsx | 2 ++ 2 files changed, 7 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx index 49505d3a9d5..a3b16c21c7f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx @@ -67,6 +67,8 @@ describe('CodeSearchOverlay', () => { expect(input.getAttribute('aria-label')).toBe('Search code') expect(input.value).toBe('error') expect(overlay.textContent).toContain('2/3') + const tally = overlay.querySelector('[aria-live="polite"][aria-atomic="true"]') + expect(tally?.textContent).toBe('2/3') const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set act(() => { @@ -83,6 +85,9 @@ describe('CodeSearchOverlay', () => { expect(callbacks.onNext).toHaveBeenCalledTimes(1) expect(callbacks.onClose).toHaveBeenCalledTimes(1) expect(parentClick).not.toHaveBeenCalled() + + renderOverlay({ matchCount: 0, currentMatchIndex: 0 }) + expect(tally?.textContent).toBe('0/0') }) it('keeps preview search compact in a floating overlay with a usable input ref', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx index 5c741d1b300..19247f1dfc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx @@ -68,6 +68,8 @@ export function CodeSearchOverlay({ )} 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' From 9c29e806e2899c053008c5c4968954c8cb4f4070 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 16:25:20 -0700 Subject: [PATCH 10/10] docs(ui): describe shared table sidebar exports --- .../components/table-sidebar-header/table-sidebar-header.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx index f51e9298a44..99892eb15fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx @@ -5,6 +5,7 @@ interface TableSidebarHeaderProps { children: ReactNode } +/** Shared header row for table configuration sidebars. */ export function TableSidebarHeader({ children }: TableSidebarHeaderProps) { return (
@@ -17,6 +18,7 @@ interface TableSidebarHeaderActionProps extends ButtonHTMLAttributes