From 91bf6ae3c7ccac69b22045c4442833384e1ad320 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 18 Sep 2026 16:25:31 -0700 Subject: [PATCH 1/6] refactor(emcn): centralize compact icon button sizing --- .../components/find-bar/find-bar.tsx | 12 ++-- .../browser-session/browser-find-bar.tsx | 9 ++- .../workflow-sidebar/workflow-sidebar.tsx | 3 +- .../components/action-bar/action-bar.tsx | 9 ++- .../components/file-upload/file-upload.tsx | 6 +- .../messages-input/messages-input.tsx | 12 ++-- .../selector-combobox/selector-combobox.tsx | 3 +- .../panel/components/editor/editor.tsx | 3 +- .../preview-editor/preview-editor.tsx | 3 +- .../src/components/button/button.test.tsx | 66 +++++++++++++++++++ .../emcn/src/components/button/button.tsx | 17 ++++- .../src/note/note-block-view.tsx | 3 +- 12 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 packages/emcn/src/components/button/button.test.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx index ceb2b0b709b..6ec4de4377e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx @@ -129,7 +129,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label={showReplace ? 'Hide replace' : 'Show replace'} aria-expanded={showReplace} onClick={() => setShowReplace((visible) => !visible)} @@ -179,7 +180,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Previous match' title='Previous match (Shift+Enter)' disabled={!navEnabled} @@ -191,7 +193,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Next match' title='Next match (Enter)' disabled={!navEnabled} @@ -203,7 +206,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Close find' title='Close (Esc)' onClick={onClose} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx index a3f9e23198d..8313c2c549e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx @@ -106,7 +106,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro size='sm' aria-label='Previous match' disabled={!result?.matches} - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={() => step('back')} > @@ -117,7 +118,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro size='sm' aria-label='Next match' disabled={!result?.matches} - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={() => step('forward')} > @@ -127,7 +129,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro variant='ghost-secondary' size='sm' aria-label='Close find bar' - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={dismiss} > diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index 18b04cfd5b2..2c53382bf54 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -730,7 +730,8 @@ export function WorkflowSidebarBody({ 'noopener,noreferrer' ) } - className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]' + iconSize='compact-fixed' + className='absolute right-[6px] bottom-1.5 z-10 cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] hover-hover:bg-[var(--surface-4)]' > diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index c42c236d1c3..4bd3f1c6c00 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -36,7 +36,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const DEFAULT_DUPLICATE_OFFSET = { x: 50, y: 50 } const ACTION_BUTTON_STYLES = [ - 'size-[24px] rounded-md p-0', + 'rounded-md', 'border-none bg-transparent text-[var(--text-icon)]', 'hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-primary)]!', 'dark:hover-hover:bg-[var(--surface-4)]', @@ -472,6 +472,7 @@ export const ActionBar = memo( } if (canRunBlock) handleRunFromBlockClick() }} + iconSize='compact-fixed' className={cn(getActionButtonStyles('run'), isWorkflowRunning && 'group/run')} disabled={!canStopWorkflow && !canRunBlock} > @@ -517,6 +518,7 @@ export const ActionBar = memo( collaborativeBatchToggleBlockEnabled([blockId]) } }} + iconSize='compact-fixed' className={getActionButtonStyles('enabled')} disabled={ isWorkflowRunning || @@ -553,6 +555,7 @@ export const ActionBar = memo( 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 4adfb95edd8..c7c6880bf90 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 @@ -1398,7 +1398,8 @@ function PreviewEditorContent({ type='button' variant='ghost' onClick={handleExpandChildWorkflow} - className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]' + iconSize='compact-fixed' + className='absolute right-[6px] bottom-1.5 z-10 cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] hover-hover:bg-[var(--surface-4)]' > {isExecutionMode && onDrillDown ? ( diff --git a/packages/emcn/src/components/button/button.test.tsx b/packages/emcn/src/components/button/button.test.tsx new file mode 100644 index 00000000000..154820a5b0b --- /dev/null +++ b/packages/emcn/src/components/button/button.test.tsx @@ -0,0 +1,66 @@ +/** @vitest-environment node */ +import { Button } from '@sim/emcn' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' + +function normalizeClasses(markup: string) { + return markup.replace( + /class="([^"]+)"/g, + (_, classes: string) => `class="${classes.split(/\s+/).sort().join(' ')}"` + ) +} + +/** The three existing Button treatments used by compact product actions. */ +const TREATMENTS = [ + { size: undefined, variant: 'ghost' }, + { size: 'sm', variant: 'ghost-secondary' }, + { size: 'icon', variant: 'ghost' }, +] as const + +describe('Button iconSize', () => { + for (const [iconSize, previousClass] of [ + ['compact', 'size-6 p-0'], + ['compact-fixed', 'size-[24px] p-0'], + ] as const) { + it.each(TREATMENTS)( + `preserves the ${iconSize} treatment with size=$size and variant=$variant`, + (treatment) => { + const icon = + const before = renderToStaticMarkup( + + ) + const after = renderToStaticMarkup( + + ) + expect(normalizeClasses(after)).toBe(normalizeClasses(before)) + } + ) + } + + it('allows explicit padding and consumer width to take precedence', () => { + const markup = renderToStaticMarkup( + + */ + iconSize?: VariantProps['iconSize'] /** * Symmetric padding for icon actions whose content or layout determines their size. * Preserves the selected size's typography, corner radius and icon stroke. @@ -85,11 +98,11 @@ export interface ButtonProps } const Button = forwardRef( - ({ className, variant, size, iconPadding, ...props }, ref) => { + ({ className, variant, size, iconSize, iconPadding, ...props }, ref) => { return ( + {copied ? 'Copied message' : 'Copy message'} @@ -180,27 +180,29 @@ export const MessageActions = memo(function MessageActions({ <> - + Good response - + Bad response @@ -209,15 +211,16 @@ export const MessageActions = memo(function MessageActions({ {canFork && ( - + Fork in new chat From 1d252e5ca01f07229c1462e2bf40575c0b814aca Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos <157128530+BillLeoutsakosvl346@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:12:55 -0700 Subject: [PATCH 6/6] improvement(emcn): share ordinary action geometry in Button (#8149) * improvement(emcn): share ordinary action geometry in Button * improvement(ui): use inline Button for small text actions (#8177) Co-authored-by: Bill Leoutsakos * fix(emcn): assert responsive icon size map as const --------- Co-authored-by: Bill Leoutsakos --- .claude/rules/emcn-components.md | 14 +++ .cursor/rules/emcn-components.mdc | 14 +++ .../file-download/file-download.tsx | 6 +- .../select-field/select-options-editor.tsx | 3 +- .../components/table-filter/table-filter.tsx | 3 +- .../table-grid/table-primitives.tsx | 7 +- .../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 +- 10 files changed, 159 insertions(+), 20 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]/logs/components/log-details/components/file-download/file-download.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx index 8d310f38b29..064baa45326 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx @@ -106,11 +106,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) {file.type} {file.version === undefined ? '' : ` · v${file.version}`} - 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/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-primitives.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-primitives.tsx index fab52045ad8..477ab19c88b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-primitives.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-primitives.tsx @@ -72,11 +72,8 @@ export const AddRowButton = React.memo(function AddRowButton({ + ) + 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 (