diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index fd57ee5c00c..af477a7d886 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -6,14 +6,14 @@ paths: # EMCN Components -Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). +Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth Never hand-roll the chip pill from raw class strings (they go stale). Compose from the canonical sources: - **Surface, typography + content tokens:** `chip/chip-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass` (text fields and the dropdown search box build on these), plus the chip-content chrome `chipContentGap`, `chipGeometryClass`, `chipContentIconClass`, `chipContentLabelClass`, `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells), and the row-state pair `chipHoverSurfaceClass` / `chipActiveSurfaceClass` (hover vs. selected — mutually exclusive, so a selected row holds its surface through hover; every hand-rolled row imports these rather than restating the literals). All are re-exported from the `@sim/emcn` barrel — no subpath import needed. -- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipDropdown`, `ChipSelect`, `ChipSwitch`) reuses it for visual parity. +- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipSelect`, `ChipDatePicker`) reuses it for visual parity. Canonical look: normal font-weight (never `font-medium`/`font-semibold`), value text `--text-body`, icons `--text-icon` at `size-[14px]`, placeholder `--text-muted`, `transition-colors`, **no focus ring** (the caret marks focus). Filled surface is `--surface-5` light / `--surface-4` dark with a `--border-1` border. @@ -25,14 +25,14 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items - **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner ``); `className` styles the chrome wrapper. - **`ChipCopyInput`** — the canonical view-only field: a read-only `ChipInput` at full opacity with a trailing copy-to-clipboard button. View-only is a display mode, not a disabled state — reach for it (or `ChipModalField type='copy'`) over a `disabled` (greyed) input for values the user cannot edit. - **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`). -- **`ChipDropdown`** — pill that opens a menu. Single OR multi-select via the discriminated `multiple` prop (one component, not two). Owns its trailing chevron — no `rightIcon`. -- **`ChipSelect` / `ChipCombobox`** — `Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`. +- **`ChipSelect`** — canonical menu selection. Single selection uses `value`/`onChange`; `multiSelect` uses `multiSelectValues`/`onMultiSelectChange`, checkbox rows, and stays open. Supports groups, search aliases, rich labels/icons, disabled options, optional single-selection checks, and trigger refs. Rich labels require a nonempty `searchTerms` array containing their visible text, followed by any aliases; plain string/number labels are searchable automatically. `showAllOption` clears to an empty selection; without it, use `placeholder` for the empty state. Owns its chevron. `dropdownWidth` accepts `'trigger'`, `'content'`, or pixels; `modal={false}` keeps outside-dialog filters non-modal, while nested dialogs retain their focus lock. +- **`ChipCombobox`** — shared chip styling over the separate `Combobox` engine for editable values, asynchronous results, and virtualized lists. Plain menu selection, including searchable menus, uses `ChipSelect`. - **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `copy` | `file` | `emails` | `custom`) picks the control and **owns all chrome** — consumers describe intent, never pass `variant`/`className`/`id` to the inner control. `custom` is the escape hatch. **Every body field MUST be a `ChipModalField`** — never hand-roll a field row (raw `
` + hand-rolled `

`/`

` + hand-rolled `

`/`

` + a hand-rolled `

`/`

` with a custom `gap-*` that fights `ChipModalBody`'s `gap-4`. @@ -426,7 +426,7 @@ Principles when building or migrating shared UI: - One canonical source of truth for shared chrome — compose it, never re-derive it per consumer. - Props-driven API over `className` overrides — reaching for `className` to change chrome is a smell; expose a prop instead. -- Discriminated-union props for modes (e.g. `ChipDropdown multiple`) over near-duplicate components. +- Discriminated-union props for modes (e.g. `ChipSelect multiSelect`) over near-duplicate components. - Delete legacy variants/components after migration — no parallel paths left behind. - Plain `cn()` for a single error/state toggle; CVA only for genuinely multiple variants. - Align consumers to the canonical defaults — normal weight, `--text-body` text, `--text-icon` icons. diff --git a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx b/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx index 8710f405311..248d24644ee 100644 --- a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx +++ b/apps/sim/app/(interfaces)/chat/components/auth/email/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 { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { quickValidateEmail } from '@/lib/messaging/email/validation' @@ -134,7 +134,7 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
- {hasEmailError && (
diff --git a/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx b/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx index 378fba15cdc..df723907a00 100644 --- a/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx +++ b/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx @@ -1,11 +1,10 @@ 'use client' import { useState } from 'react' -import { cn, Input, Label } from '@sim/emcn' -import { Eye, EyeOff } from '@sim/emcn/icons' +import { cn, Label } from '@sim/emcn' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { AuthSubmitButton } from '@/app/(auth)/components' +import { AuthSubmitButton, PasswordInput } from '@/app/(auth)/components' import { useChatPasswordAuth } from '@/hooks/queries/chats' const logger = createLogger('PasswordAuth') @@ -16,7 +15,6 @@ interface PasswordAuthProps { export default function PasswordAuth({ identifier }: PasswordAuthProps) { const [password, setPassword] = useState('') - const [showPassword, setShowPassword] = useState(false) const [passwordErrors, setPasswordErrors] = useState([]) const hasPasswordError = passwordErrors.length > 0 const authenticate = useChatPasswordAuth(identifier) @@ -67,37 +65,19 @@ export default function PasswordAuth({ identifier }: PasswordAuthProps) {
-
- - -
+
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 -