From 7eb9cc4c9c745871f34ee9af07d12ccb4ba183f7 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 12:51:50 -0700 Subject: [PATCH 1/3] improvement(emcn): share larger chip and field geometry --- .claude/rules/emcn-components.md | 4 + .cursor/rules/emcn-components.mdc | 4 + apps/sim/app/(auth)/components/auth-input.tsx | 17 +--- .../landing-cta-link/landing-cta-link.tsx | 2 +- .../secret-value-field/secret-value-field.tsx | 2 +- .../components/chip-input/chip-input.test.tsx | 17 +++- .../src/components/chip-input/chip-input.tsx | 15 ++- .../emcn/src/components/chip/chip-chrome.ts | 6 +- .../emcn/src/components/chip/chip.test.tsx | 93 +++++++++++++++++++ packages/emcn/src/components/chip/chip.tsx | 36 +++++-- 10 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 packages/emcn/src/components/chip/chip.test.tsx diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index af477a7d886..8c8b468d246 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -70,3 +70,7 @@ geometry only; colour, radius and SVG stroke continue to come from the selected 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. + +### Chip sizing and centered actions + +`Chip`, `ChipLink`, and `ChipInput` retain their default 30px height. Use `size="lg"` for the existing auth-scale 36px controls; the larger size follows the root font size. `Chip` and `ChipLink` accept `align="center"` to center their icon/label group, including full-width form actions. Long labels still shrink and retain their overflow treatment. Avoid descendant flex overrides to center chip labels. The default geometry exports remain unchanged for static consumers. diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc index 6a70e29d4b3..75fe49a83e3 100644 --- a/.cursor/rules/emcn-components.mdc +++ b/.cursor/rules/emcn-components.mdc @@ -71,3 +71,7 @@ geometry only; colour, radius and SVG stroke continue to come from the selected 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. + +### Chip sizing and centered actions + +`Chip`, `ChipLink`, and `ChipInput` retain their default 30px height. Use `size="lg"` for the existing auth-scale 36px controls; the larger size follows the root font size. `Chip` and `ChipLink` accept `align="center"` to center their icon/label group, including full-width form actions. Long labels still shrink and retain their overflow treatment. Avoid descendant flex overrides to center chip labels. The default geometry exports remain unchanged for static consumers. diff --git a/apps/sim/app/(auth)/components/auth-input.tsx b/apps/sim/app/(auth)/components/auth-input.tsx index 396bae64cf8..a2c3bc12979 100644 --- a/apps/sim/app/(auth)/components/auth-input.tsx +++ b/apps/sim/app/(auth)/components/auth-input.tsx @@ -1,20 +1,11 @@ 'use client' import * as React from 'react' -import { ChipInput, type ChipInputProps, cn } from '@sim/emcn' -import { AUTH_CONTROL_HEIGHT } from '@/app/(auth)/components/constants' +import { ChipInput, type ChipInputProps } from '@sim/emcn' -/** - * The auth text field — a {@link ChipInput} raised to the auth control height - * ({@link AUTH_CONTROL_HEIGHT}) so every labeled field on the auth and invite - * surfaces shares one slightly-taller geometry. All chip props pass through - * (`error`, `endAdornment`, `icon`, …); only the height is owned here, and a - * caller's `className` (layout only) still composes on top. - */ -export const AuthInput = React.forwardRef( - ({ className, ...props }, ref) => ( - - ) +/** Auth fields use the larger shared chip size while retaining native input props and refs. */ +export const AuthInput = React.forwardRef>( + (props, ref) => ) AuthInput.displayName = 'AuthInput' diff --git a/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx b/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx index 4fbba3d3750..5787f2c8560 100644 --- a/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx +++ b/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx @@ -9,7 +9,7 @@ type LandingCtaSize = 'compact' | 'default' | 'display' export type LandingCtaSection = PostHogEventMap['landing_cta_clicked']['section'] -interface LandingCtaLinkProps extends Omit { +interface LandingCtaLinkProps extends Omit { size?: LandingCtaSize variant?: 'primary' | 'outline' /** Adds the animated chevron used by demo actions. */ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx index a843694bfe8..fa0e488c761 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx @@ -11,7 +11,7 @@ const VIEWER_MASK_LENGTH = 10 type SecretValueFieldProps = Omit< ComponentProps<'input'>, - 'type' | 'value' | 'onChange' | 'readOnly' | 'style' + 'type' | 'value' | 'onChange' | 'readOnly' | 'style' | 'size' > & { /** The chip owns field styling; callers use className for layout. */ style?: never diff --git a/packages/emcn/src/components/chip-input/chip-input.test.tsx b/packages/emcn/src/components/chip-input/chip-input.test.tsx index acbc4226246..a1d70082ff6 100644 --- a/packages/emcn/src/components/chip-input/chip-input.test.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.test.tsx @@ -30,6 +30,19 @@ afterEach(() => { }) describe('ChipInput', () => { + it.each([undefined, 'lg'] as const)( + 'emits a single height for size %s without forwarding it to the native field', + (size) => { + const input = mount() + const heights = input.parentElement?.className + .split(' ') + .filter((token) => token.startsWith('h-')) + expect(heights).toEqual([size === 'lg' ? 'h-9' : 'h-[30px]']) + expect(input.hasAttribute('size')).toBe(false) + expect(input.disabled).toBe(true) + } + ) + it('keeps the focused input mounted when custom leading content changes', () => { const input = mount() const render = (color: string) => ( @@ -76,7 +89,7 @@ describe('chip form controls', () => { error aria-invalid aria-describedby='error' - className='h-[34px]' + size='lg' />

Enter a work email

{ expect(input.labels?.[0].textContent).toBe('Work email') expect(input.getAttribute('aria-describedby')).toBe('error') expect(input.getAttribute('aria-invalid')).toBe('true') - expect(input.parentElement?.className).toContain('h-[34px]') + expect(input.parentElement?.className).toContain('h-9') expect(input.parentElement?.className).toContain('border-[var(--text-error)]') expect(textareaRef.current?.rows).toBe(3) expect(textareaRef.current?.className).toContain('min-h-[80px]') diff --git a/packages/emcn/src/components/chip-input/chip-input.tsx b/packages/emcn/src/components/chip-input/chip-input.tsx index 444cbb1704a..3c5d7d1f161 100644 --- a/packages/emcn/src/components/chip-input/chip-input.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.tsx @@ -28,11 +28,19 @@ */ import * as React from 'react' import { cn } from '../../lib/cn' -import { chipFieldSurfaceClass, chipFieldTextClass, chipGeometryClass } from '../chip/chip-chrome' +import { + chipContentGeometryClass, + chipFieldSurfaceClass, + chipFieldTextClass, + chipRadiusClass, + chipSizeClasses, +} from '../chip/chip-chrome' type ChipInputIcon = React.ComponentType<{ className?: string }> export interface ChipInputProps extends Omit, 'size'> { + /** Control height: 30px by default, or the larger 36px auth spacing scale. */ + size?: keyof typeof chipSizeClasses /** 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`. */ @@ -62,6 +70,7 @@ export const ChipInput = React.forwardRef( error, disabled, type = 'text', + size = 'md', ...props }, ref @@ -69,7 +78,9 @@ export const ChipInput = React.forwardRef(
{ + it.each([undefined, 'lg'] as const)( + 'uses one height for raw variants, buttons and links at size %s', + (size) => { + const height = size === 'lg' ? 'h-9' : 'h-[30px]' + expect( + chipVariants({ size }) + .split(' ') + .filter((token) => token.startsWith('h-')) + ).toEqual([height]) + for (const node of [ + + Continue + , + + Continue + , + ]) { + const markup = renderToStaticMarkup(node) + expect(markup).toContain(height) + expect(markup).not.toContain(size === 'lg' ? 'h-[30px]' : 'h-9') + expect(markup).not.toMatch(/ size=/) + } + expect(chipGeometryUnroundedClass).toContain('h-[30px]') + expect(chipGeometryClass).toContain('h-[30px]') + expect(chipGeometryClass).toContain('rounded-lg') + } + ) + + it('centers the icon and label without preventing long text from shrinking', () => { + const markup = renderToStaticMarkup( + }> + Continue with your identity provider + + ) + expect(markup).toContain('justify-center') + expect(markup).toContain('flex-initial') + expect(markup).toContain('min-w-0') + expect(markup).not.toContain('flex-none') + expect(markup).not.toMatch(/ align=/) + }) + + it('preserves native refs, focus, submission, disabled actions and link navigation', () => { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + const button = createRef() + const link = createRef() + let submissions = 0 + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const render = (disabled: boolean) => + act(() => + root.render( +
{ + event.preventDefault() + submissions++ + }} + > + + Continue + + + Workspace + +
+ ) + ) + try { + render(false) + button.current?.focus() + expect(document.activeElement).toBe(button.current) + act(() => button.current?.click()) + expect(submissions).toBe(1) + expect(link.current?.getAttribute('href')).toBe('/workspace') + render(true) + act(() => button.current?.click()) + expect(submissions).toBe(1) + expect(button.current?.disabled).toBe(true) + } finally { + act(() => root.unmount()) + container.remove() + } + }) +}) diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index 5b333032762..d8d3fbd2494 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -14,17 +14,18 @@ import { OverflowText, overflowTextClipClass } from '../overflow-text/overflow-t import { chipActiveSurfaceClass, chipBorderShadowRing, + chipContentGeometryClass, chipContentIconClass, chipContentLabelClass, chipFilledFillTokens, - chipGeometryUnroundedClass, chipHoverSurfaceClass, chipPrimaryFillTokens, chipRadiusClass, + chipSizeClasses, } from './chip-chrome' /** - * 30px pill — the platform's most common chrome pattern. + * 30px pill (36px with `size="lg"` at the default root font size) — the platform's most common chrome pattern. * * Render targets: * - {@link Chip} → ` + Configure PII Types + setOpen(false)}>Select PII Types to Detect e.stopPropagation()}>