From d583476eed126d98c52b9d55038b8f0dcabb8952 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 22 Sep 2026 12:17:25 -0700 Subject: [PATCH 1/4] 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 ( - +
0 && (
{uploadErrors.map((error, idx) => ( - + {error} ))} diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx index 5d187bb7345..0d3ca4fb9d1 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx @@ -1,4 +1,4 @@ -import { Chip, ChipDropdown, cn } from '@sim/emcn' +import { Chip, ChipSelect, cn } from '@sim/emcn' import { Building, MoreHorizontal, Plus } from '@sim/emcn/icons' import { MenuPreviewFrame } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-frame' import { MenuPreviewHeader } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-header/menu-preview-header' @@ -47,11 +47,14 @@ export function EnterpriseMenuPreview({ layout = 'menu' }: EnterpriseMenuPreview image={null} status='' roleControl={ - } menu={} diff --git a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx index 534fa557cdc..787a852a5e1 100644 --- a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx +++ b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx @@ -2,7 +2,7 @@ import { type ReactNode, useId, useRef, useState } from 'react' import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile' -import { Chip, ChipDropdown, ChipInput, ChipTextarea, Label } from '@sim/emcn' +import { Chip, ChipInput, ChipSelect, ChipTextarea, Label } from '@sim/emcn' import { Check } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' import { @@ -286,7 +286,10 @@ export function ContactForm() { /> - - - option.value === (organizationRoles[member.email] ?? member.role))?.label ?? 'Owner'}`} value={organizationRoles[member.email] ?? member.role} options={ @@ -76,7 +79,7 @@ export function EnterpriseMembersPreview() { setOrganizationRoles((roles) => ({ ...roles, [member.email]: role })) } disabled={member.role === 'owner'} - matchTriggerWidth={false} + dropdownWidth='content' /> } /> @@ -95,7 +98,10 @@ export function EnterpriseMembersPreview() { image={null} status='' roleControl={ - option.value === (workspaceRoles[member.email] ?? (member.role === 'owner' ? 'admin' : 'write')))?.label}`} value={ workspaceRoles[member.email] ?? (member.role === 'owner' ? 'admin' : 'write') @@ -105,7 +111,7 @@ export function EnterpriseMembersPreview() { setWorkspaceRoles((roles) => ({ ...roles, [member.email]: role })) } disabled={member.role === 'owner'} - matchTriggerWidth={false} + dropdownWidth='content' /> } /> diff --git a/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx b/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx index 2e1dba49f5f..af9574dcb6f 100644 --- a/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx +++ b/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useId, useRef, useState } from 'react' -import { Badge, Chip, ChipDropdown, ChipInput } from '@sim/emcn' +import { Badge, Chip, ChipInput, ChipSelect } from '@sim/emcn' import { Library, ListFilter, Search, X } from '@sim/emcn/icons' import { MenuPreviewHeader, @@ -109,13 +109,16 @@ export function LogHistoryPreview() { onChange={(event) => setQuery(event.target.value)} className='mr-auto min-w-0 flex-1' /> - option.value === filter)?.label ?? filter}`} value={filter} options={FILTERS} onChange={setFilter} - matchTriggerWidth={false} + dropdownWidth='content' />
diff --git a/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx b/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx index 9eb6378eb52..7236265ea4b 100644 --- a/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx +++ b/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx @@ -1,11 +1,11 @@ import { useState } from 'react' import { - ChipDropdown, ChipModal, ChipModalBody, ChipModalField, ChipModalFooter, ChipModalHeader, + ChipSelect, } from '@sim/emcn' import { TagIcon, TypeNumber, TypeText } from '@sim/emcn/icons' import type { LeadRecord } from '@/app/(landing)/tables/components/tables-records-preview/data' @@ -65,7 +65,11 @@ export function LeadRecordDetail({ record, onClose, onSave }: LeadRecordDetailPr } > - (null) const handleAuthenticate = async () => { @@ -49,35 +47,22 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) { >
-
- { - setPassword(e.target.value) - setError(null) - }} - className={cn( - 'pr-10', - error && 'border-[var(--text-error)] focus:border-[var(--text-error)]' - )} - /> - -
+ { + setPassword(e.target.value) + setError(null) + }} + className='h-[34px]' + error={Boolean(error)} + /> {error ?

{error}

: null}
diff --git a/apps/sim/app/f/[token]/public-file-email-auth.tsx b/apps/sim/app/f/[token]/public-file-email-auth.tsx index 71a5c564492..a401c748e3f 100644 --- a/apps/sim/app/f/[token]/public-file-email-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-email-auth.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { cn, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' +import { ChipInput, cn, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { normalizeEmail } from '@sim/utils/string' import { useRouter } from 'next/navigation' @@ -90,7 +90,7 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { >
- {error ?

{error}

: null}
diff --git a/apps/sim/app/f/[token]/public-file-sso-auth.tsx b/apps/sim/app/f/[token]/public-file-sso-auth.tsx index 3e73581f715..26ca1961483 100644 --- a/apps/sim/app/f/[token]/public-file-sso-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-sso-auth.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { cn, Input, Label } from '@sim/emcn' +import { ChipInput, Label } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { normalizeEmail } from '@sim/utils/string' import { useRouter } from 'next/navigation' @@ -68,7 +68,7 @@ export function PublicFileSSOAuth({ token }: PublicFileSSOAuthProps) { >
- {error ?

{error}

: null}
diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index fa5a295eacd..b014041848c 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -307,12 +307,6 @@ export default function PlaygroundPage() { - - - - - - @@ -344,14 +338,8 @@ export default function PlaygroundPage() { JavaScript - - - Option 1 - Option 2 - - - - + + Option 1 Option 2 @@ -410,9 +398,6 @@ export default function PlaygroundPage() { Amber - - Teal - Cyan @@ -425,7 +410,6 @@ export default function PlaygroundPage() { Small Medium - Large @@ -525,10 +509,7 @@ export default function PlaygroundPage() { Medium (16px) - - - Large (20px) - + @@ -879,7 +860,7 @@ export default function PlaygroundPage() { - + Item 1 diff --git a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx index f27e865a6a1..ac53da16d2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx @@ -21,7 +21,7 @@ const { hostContext, mockUseOrganizationBilling, mockUseAdminWorkspaces, mockMut ) vi.mock('@sim/emcn', () => ({ - ChipDropdown: () =>
, + ChipSelect: () =>
, ChipModal: ({ children }: { children: ReactNode }) =>
{children}
, ChipModalBody: ({ children }: { children: ReactNode }) =>
{children}
, ChipModalError: ({ children }: { children: ReactNode }) =>
{children}
, diff --git a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx index 090a9096847..2347899cd2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx @@ -2,14 +2,14 @@ import { useCallback, useMemo, useState } from 'react' import { - ChipDropdown, - type ChipDropdownOption, ChipModal, ChipModalBody, ChipModalError, ChipModalField, ChipModalFooter, ChipModalHeader, + ChipSelect, + type ChipSelectOption, toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' @@ -161,7 +161,7 @@ export function InviteModal({ { enabled: open && isOrganizationInvite && !organizationOnly } ) - const workspaceOptions = useMemo(() => { + const workspaceOptions = useMemo(() => { if (!isOrganizationInvite) { return workspaceId ? [{ value: workspaceId, label: workspaceName ?? 'This workspace' }] : [] } @@ -317,12 +317,15 @@ export function InviteModal({ {!organizationOnly && ( <> - -
- - {exceedsLimit - ? `${selectedCount} selected · select ${maxSelectable} or fewer` - : `${selectedCount} selected`} - -
- {onDownload && ( - - )} - {onMove && moveOptions && ( - - - - - - - - - - Move - - - {renderMoveOptions(moveOptions, onMove)} - - - )} - {onDelete && ( - - )} -
-
+ + {exceedsLimit + ? `${selectedCount} selected · select ${maxSelectable} or fewer` + : `${selectedCount} selected`} + + } + > + {onDownload && ( + + )} + {onMove && moveOptions && ( + + + + + + + + + + Move + + + {renderMoveOptions(moveOptions, onMove)} + + + )} + {onDelete && ( + + )} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index af31043f573..aa14ffd07ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useMemo, useRef, useState } from 'react' -import { Chip, ChipDropdown, ChipLink, cn } from '@sim/emcn' +import { Chip, ChipLink, ChipSelect, cn } from '@sim/emcn' import { ArrowLeft, Plus } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' @@ -193,14 +193,16 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration ) : oauthService ? ( connectOptions.length > 1 ? ( - ) : oauthAvailable ? ( setOAuthOpen(true)}> diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx index 68ab142e2d8..627d13edf1a 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx @@ -4,10 +4,10 @@ import { type ReactNode, useEffect, useMemo, useState } from 'react' import { Button, Chip, - ChipDropdown, - type ChipDropdownOption, ChipInput, ChipModalField, + ChipSelect, + type ChipSelectOption, SecretInput, Wizard, } from '@sim/emcn' @@ -50,7 +50,7 @@ const CUSTOM_BOT_CAPABILITIES = [ const ALL_CAPABILITIES = new Set(CUSTOM_BOT_CAPABILITIES.map((capability) => capability.id)) -const CAPABILITY_OPTIONS: ChipDropdownOption[] = CUSTOM_BOT_CAPABILITIES.map((capability) => ({ +const CAPABILITY_OPTIONS: ChipSelectOption[] = CUSTOM_BOT_CAPABILITIES.map((capability) => ({ value: capability.id, label: capability.label, })) @@ -404,13 +404,16 @@ function StepConfigure({ : undefined } > - diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx index 13819242fb2..3f8f9bec003 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx @@ -701,7 +701,7 @@ export function DocumentTagsModal({ !kbTagDefinitions.find( (def) => def.displayName.toLowerCase() === editTagForm.displayName.toLowerCase() ) && ( - + Maximum tag definitions reached. You can still use existing tag definitions, but cannot create new ones. diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 9330027ea58..683c692cb15 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -7,12 +7,12 @@ import { ChipConfirmModal, type ChipConfirmTextSegment, ChipDatePicker, - ChipDropdown, - type ChipDropdownOption, ChipInput, ChipModal, ChipModalBody, ChipModalHeader, + ChipSelect, + type ChipSelectOption, cellIconNodeClass, chipContentGap, chipContentLabelClass, @@ -162,7 +162,7 @@ const PROCESSING_POLL_INTERVAL_MS = 3000 /** Slower cadence while only a connector sync is running: rows arrive in batches. */ const CONNECTOR_SYNC_DOCUMENT_POLL_INTERVAL_MS = 5000 -const STATUS_FILTER_OPTIONS: ChipDropdownOption[] = [ +const STATUS_FILTER_OPTIONS: ChipSelectOption[] = [ { value: 'all', label: 'All' }, { value: 'enabled', label: 'Enabled' }, { value: 'disabled', label: 'Disabled' }, @@ -1096,7 +1096,10 @@ export function KnowledgeBase({ )}
- { @@ -1731,7 +1734,7 @@ function TagFilterValueControl({ entry, onChange }: TagFilterValueControlProps) function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectionProps) { const activeCount = entries.filter((f) => f.tagSlot && f.value.trim()).length - const tagOptions: ChipDropdownOption[] = tagDefinitions.map((t) => ({ + const tagOptions: ChipSelectOption[] = tagDefinitions.map((t) => ({ value: t.displayName, label: t.displayName, })) @@ -1801,7 +1804,7 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio > {filtersToShow.map((entry, index) => { const operators = getOperatorsForFieldType(entry.fieldType) - const operatorOptions: ChipDropdownOption[] = operators.map((op) => ({ + const operatorOptions: ChipSelectOption[] = operators.map((op) => ({ value: op.value, label: op.label, })) @@ -1818,24 +1821,29 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio )}
- handleTagChange(entry.id, value)} placeholder='Select tag' align='start' - matchTriggerWidth={false} + dropdownWidth='content' contentClassName='max-h-[240px] overflow-y-auto' - className='max-w-[150px]' + className='w-auto max-w-[150px]' /> {entry.tagSlot && ( - updateEntry(entry.id, { operator: value, valueTo: '' })} placeholder='Operator' align='start' - matchTriggerWidth={false} + dropdownWidth='content' /> )}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx index 01dbfaead48..3bd0cab96d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx @@ -1,4 +1,4 @@ -import { BulkActionButton, cn, Tooltip } from '@sim/emcn' +import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn' import { Ban, Circle, Trash } from '@sim/emcn/icons' import { domAnimation, LazyMotion, m } from 'framer-motion' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -53,70 +53,70 @@ export function ActionBar({ transition={{ duration: 0.2 }} className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-[var(--z-dropdown)]', className)} > -
- - {isAllSelected ? totalCount : selectedCount} selected - {showSelectAllOption && ( - <> - {' · '} - - - )} - {isAllSelected && onClearSelectAll && ( - <> - {' · '} - - - )} - + + {isAllSelected ? totalCount : selectedCount} selected + {showSelectAllOption && ( + <> + {' · '} + + + )} + {isAllSelected && onClearSelectAll && ( + <> + {' · '} + + + )} + + } + > + {showEnableButton && ( + + + + + + + Enable + + )} -
- {showEnableButton && ( - - - - - - - Enable - - )} + {showDisableButton && ( + + + + + + + Disable + + )} - {showDisableButton && ( - - - - - - - Disable - - )} - - {onDelete && canEdit && ( - - - - - - - Delete - - )} -
-
+ {onDelete && canEdit && ( + + + + + + + Delete + + )} + ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx index bd2cc556aa0..cefe21c6f32 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx @@ -5,9 +5,9 @@ import { ChipButtonGroup, ChipButtonGroupItem, ChipCombobox, - ChipDropdown, ChipLink, ChipModalField, + ChipSelect, type ComboboxOption, Tooltip, } from '@sim/emcn' @@ -170,12 +170,15 @@ export function ConnectorAccessField({ - ({ value: mode, label }))} disabled - className='pointer-events-none w-fit' + className='pointer-events-none w-fit max-w-none' /> diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx index 749b08d852c..6a5843c27f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx @@ -1,6 +1,6 @@ 'use client' -import { ChipDropdown } from '@sim/emcn' +import { ChipSelect } from '@sim/emcn' import type { ConnectorDocumentFilter, ConnectorDocumentsData, @@ -20,14 +20,17 @@ export function ConnectorDocumentStatusFilter({ isLoading, }: ConnectorDocumentStatusFilterProps) { return ( - { if (value === 'active' || value === 'excluded' || value === 'failed' || value === 'skipped') onFilterChange(value) }} - matchTriggerWidth={false} + dropdownWidth='content' options={[ { value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` }, { value: 'excluded', label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})` }, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index ccda6b899a9..994b3d820d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -1,8 +1,8 @@ 'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import type { ChipDropdownOption } from '@sim/emcn' -import { Button, ChipConfirmModal, ChipDropdown, Tooltip, toast } from '@sim/emcn' +import type { ChipSelectOption } from '@sim/emcn' +import { Button, ChipConfirmModal, ChipSelect, Tooltip, toast } from '@sim/emcn' import { Database, FolderPlus, Pencil, Plus, Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -127,13 +127,13 @@ const SEARCH_COLUMNS: ResourceColumn[] = [...COLUMNS, FOLDER_LOCATION_COLUMN] const KNOWLEDGE_BASE_ICON = -const CONNECTOR_FILTER_OPTIONS: ChipDropdownOption[] = [ +const CONNECTOR_FILTER_OPTIONS: ChipSelectOption[] = [ { value: 'all', label: 'All' }, { value: 'connected', label: 'With connectors' }, { value: 'unconnected', label: 'Without connectors' }, ] -const CONTENT_FILTER_OPTIONS: ChipDropdownOption[] = [ +const CONTENT_FILTER_OPTIONS: ChipSelectOption[] = [ { value: 'all', label: 'All' }, { value: 'has-docs', label: 'Has documents' }, { value: 'empty', label: 'Empty' }, @@ -1320,7 +1320,7 @@ function KnowledgeContent() { [activeSort, setListSort, clearListSort] ) - const memberOptions: ChipDropdownOption[] = useMemo( + const memberOptions: ChipSelectOption[] = useMemo( () => (members ?? []).map((m) => ({ value: m.userId, @@ -1346,7 +1346,10 @@ function KnowledgeContent() { )}
- setConnectorFilter(value === 'all' ? [] : [value])} @@ -1367,7 +1370,10 @@ function KnowledgeContent() { )}
- setContentFilter(value === 'all' ? [] : [value])} @@ -1389,12 +1395,16 @@ function KnowledgeContent() { )}
- - setDraft((prev) => ({ ...prev, language: language as SandboxLanguage })) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx index dd3249fab67..0fc1ce4fa3e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx @@ -33,7 +33,6 @@ const { recordChipSelectProps } = vi.hoisted(() => ({ vi.mock('@sim/emcn', () => ({ Chip: ({ children }: { children: ReactNode }) => , - ChipDropdown: () => null, ChipInput: () => null, ChipSelect: (props: MockChipSelectProps) => { recordChipSelectProps(props) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx index 51e6ff33c52..595ca1e45c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { Chip, ChipDropdown, ChipInput, ChipTextarea, cn } from '@sim/emcn' +import { Chip, ChipInput, ChipSelect, ChipTextarea, cn } from '@sim/emcn' import type { SandboxDependencyIssue } from '@/lib/api/contracts/sandboxes' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { ManagedCliSelect } from '@/app/workspace/[workspaceId]/settings/components/sandboxes/components/managed-cli-select' @@ -67,7 +67,11 @@ export function SandboxEditor({ /> - onChange({ ...draft, language: language as SandboxLanguage })} options={LANGUAGE_OPTIONS.map((option) => ({ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx index 42ca096a093..43e5f9d25a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { ChipDropdown, ChipTag, toast } from '@sim/emcn' +import { ChipSelect, ChipTag, toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' @@ -138,7 +138,10 @@ export function OrganizationMemberLists({ } roleControl={ editable ? ( - updateMemberRole @@ -150,7 +153,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update member role', { error })) } options={ORG_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={updateMemberRole.isPending} /> ) : ( @@ -263,7 +266,10 @@ export function OrganizationMemberLists({ {isExternal ? 'External' : invitation.role === 'admin' ? 'Admin' : 'Member'} ) : ( - updateInvitation @@ -275,7 +281,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update invitation role', { error })) } options={ORG_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={updateInvitation.isPending} /> ) @@ -315,7 +321,10 @@ export function OrganizationMemberLists({ } roleControl={ - updatePermissions @@ -329,7 +338,7 @@ export function OrganizationMemberLists({ ) } options={WORKSPACE_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={disabled} /> @@ -364,7 +373,10 @@ export function OrganizationMemberLists({ access: RosterWorkspaceAccess ) => { const roleControl = ( - updateInvitation @@ -376,7 +388,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update invitation grant', { error })) } options={WORKSPACE_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={!canManage || updateInvitation.isPending} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx index de8294ee6c5..133c90c68b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useMemo, useState } from 'react' -import { ChipDropdown, Plus, toast } from '@sim/emcn' +import { ChipSelect, Plus, toast } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { formatDate } from '@sim/utils/formatting' import { useQueryClient } from '@tanstack/react-query' @@ -238,11 +238,14 @@ export function Teammates() { status={teammate.status} roleControl={ - handleRoleChange(teammate, role as WorkspacePermission)} options={ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={ teammate.isPending || !canManage || diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx index cdd89e8e821..44cf3ae11bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo } from 'react' -import { ChipDropdown } from '@sim/emcn' +import { ChipSelect } from '@sim/emcn' import type { ColumnDefinition } from '@/lib/table' import { SelectPill, selectedOptionIds } from './select-pill' @@ -18,7 +18,7 @@ const CLEAR_VALUE = '' /** * Option picker for `select`/`multiselect` cells in a form context (the row - * modal) — a `ChipDropdown` pill that lists each option as its colored pill and + * modal) — a `ChipSelect` pill that lists each option as its colored pill and * writes option ids back through `onChange`. Inline grid editing uses a bare * `DropdownMenu` instead (see `InlineSelectEditor`). */ @@ -35,31 +35,30 @@ export function SelectValueEditor({ (column.options ?? []).map((option) => ({ value: option.id, label: , + searchTerms: [option.name] as const, })), [column.options] ) if (isMulti) { return ( - { + onMultiSelectChange={(ids) => { if (column.required && ids.length === 0) return onChange(ids) }} options={options} showAllOption={false} - // In multiple mode ChipDropdown ignores `placeholder` and renders - // `allLabel` when nothing is selected — which would read as if every - // option were chosen. There is no "All" entry here, so this is the - // empty label. - allLabel='Select options' align={align} fullWidth={fullWidth} - matchTriggerWidth={false} + dropdownWidth='content' /> ) } @@ -69,19 +68,26 @@ export function SelectValueEditor({ const singleOptions = column.required ? options : [ - { value: CLEAR_VALUE, label: None }, + { + value: CLEAR_VALUE, + label: None, + searchTerms: ['None'] as const, + }, ...options, ] return ( - onChange(id === CLEAR_VALUE ? null : id)} options={singleOptions} placeholder='Select an option' align={align} fullWidth={fullWidth} - matchTriggerWidth={false} + dropdownWidth='content' /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx index b04257ae755..9922997c840 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx @@ -1,7 +1,7 @@ 'use client' import type React from 'react' -import { BulkActionButton, cn, Tooltip } from '@sim/emcn' +import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn' import { Eye, PlayOutline, RefreshCw, Square } from '@sim/emcn/icons' import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' @@ -89,47 +89,42 @@ export function TableActionBar({ className )} > -
- - {selectedCellCount === 1 + + : `Selected ${selectedCellCount} workflow cells` + } + > + {showPlay && ( + + + + )} -
- {showPlay && ( - - - - )} + {showRefresh && ( + + + + )} - {showRefresh && ( - - - - )} + {runningCount > 0 && ( + + + + )} - {runningCount > 0 && ( - - - - )} - - {onViewExecution && ( - - - - )} -
-
+ {onViewExecution && ( + + + + )} + )} @@ -152,12 +147,7 @@ function ActionIconButton({ label, onClick, disabled, children }: ActionIconButt return ( - + {children} 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 c005de9d781..53bd21ddf25 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 @@ -1,7 +1,7 @@ 'use client' import { memo, useCallback, useMemo, useRef, useState } from 'react' -import { Button, ChipDropdown, ChipInput, cn } from '@sim/emcn' +import { Button, Chip, ChipInput, ChipSelect, cn } from '@sim/emcn' import { Plus, X } from '@sim/emcn/icons' import { generateShortId } from '@sim/utils/id' import type { ColumnDefinition, FilterRule, TablePredicate } from '@/lib/table' @@ -243,20 +243,19 @@ export function TableFilter({ ))}
- + {!autoApply && (
{filter !== null && ( - + )} - +
)}
@@ -329,37 +328,43 @@ const FilterRuleRow = memo(function FilterRuleRow({ )} - onColumnChange(rule.id, value)} placeholder='Column' align='start' - matchTriggerWidth={false} - className='min-w-[100px]' + dropdownWidth='content' + className='w-auto min-w-[100px] max-w-none' /> - onUpdate(rule.id, 'operator', value)} placeholder='Operator' align='start' - matchTriggerWidth={false} - className='min-w-[90px]' + dropdownWidth='content' + className='w-auto min-w-[90px] max-w-none' /> {VALUELESS_OPS.has(rule.operator) ? (
) : isSelect ? ( - onUpdate(rule.id, 'value', value)} placeholder='Select a value' align='start' - matchTriggerWidth={false} - className='min-w-[100px] flex-1' + dropdownWidth='content' + className='w-auto min-w-[100px] max-w-none flex-1' /> ) : autoApply ? ( Welcome message -