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/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/canvas-control-button.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/canvas-control-button.tsx new file mode 100644 index 00000000000..2638999f0e9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/canvas-control-button.tsx @@ -0,0 +1,25 @@ +import { type ComponentProps, forwardRef } from 'react' +import { Button, chipHoverSurfaceClass, cn } from '@sim/emcn' + +interface CanvasControlButtonProps + extends Omit< + ComponentProps, + 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' + > { + active?: boolean + 'aria-label': string +} + +/** Fixed-size action in the canvas navigation toolbar. */ +export const CanvasControlButton = forwardRef( + ({ active = false, ...props }, ref) => ( + + + Undo @@ -177,15 +168,9 @@ export const WorkflowControls = memo(function WorkflowControls() { - + Redo @@ -196,14 +181,9 @@ export const WorkflowControls = memo(function WorkflowControls() { - + Fit to View 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 (