From 3556d0cd3013c0e52cc86bac931774f17321a497 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 18 Sep 2026 17:51:39 -0700 Subject: [PATCH 1/7] Consolidate EMCN selection controls and redundant styling options --- .claude/rules/emcn-components.md | 14 +- .cursor/rules/emcn-components.mdc | 14 +- CLAUDE.md | 4 +- .../chat/components/auth/email/email-auth.tsx | 9 +- .../auth/password/password-auth.tsx | 11 +- .../chat/components/input/input.tsx | 2 +- .../enterprise-menu-preview.tsx | 9 +- .../components/contact-form/contact-form.tsx | 7 +- .../demo/components/demo-form/demo-form.tsx | 12 +- .../enterprise-members-preview.tsx | 16 +- .../log-history-preview.tsx | 9 +- .../lead-record-detail/lead-record-detail.tsx | 8 +- apps/sim/app/f/[token]/public-file-auth.tsx | 10 +- .../app/f/[token]/public-file-email-auth.tsx | 7 +- .../app/f/[token]/public-file-sso-auth.tsx | 7 +- apps/sim/app/playground/page.tsx | 27 +- .../invite-modal/invite-modal.test.tsx | 2 +- .../components/invite-modal/invite-modal.tsx | 19 +- .../[block]/integration-block-detail.tsx | 8 +- .../connect-slack-bot-modal.tsx | 19 +- .../document-tags-modal.tsx | 2 +- .../[workspaceId]/knowledge/[id]/base.tsx | 30 +- .../connector-access-field.tsx | 9 +- .../connector-document-status-filter.tsx | 9 +- .../[workspaceId]/knowledge/knowledge.tsx | 34 +- .../components/member-list/member-list.tsx | 2 +- .../components/sandbox-create-modal.tsx | 8 +- .../components/sandbox-editor.test.tsx | 1 - .../sandboxes/components/sandbox-editor.tsx | 8 +- .../organization-member-lists.tsx | 30 +- .../components/teammates/teammates.tsx | 9 +- .../select-field/select-value-editor.tsx | 28 +- .../table-action-bar/table-action-bar.tsx | 7 +- .../components/table-filter/table-filter.tsx | 26 +- .../deploy-modal/components/chat/chat.tsx | 6 +- .../general/components/api-info-modal.tsx | 4 +- .../deploy-modal/components/mcp/mcp.tsx | 6 +- .../components/deploy-modal/deploy-modal.tsx | 8 +- .../model-fallback-list.test.tsx | 2 +- .../model-fallback-list.tsx | 9 +- .../slack-setup-wizard/slack-setup-wizard.tsx | 8 +- .../mcp/operation-policy-editor.test.tsx | 6 +- .../mcp/operation-policy-editor.tsx | 8 +- .../sim/components/permissions/member-row.tsx | 8 +- .../components/group-detail.tsx | 26 +- .../components/workspace-select.tsx | 26 +- .../organization-access-requests.tsx | 8 +- ...edential-group-add-resource-modal.test.tsx | 49 +-- .../credential-group-add-resource-modal.tsx | 15 +- .../organization-workspace-grant-modal.tsx | 17 +- .../components/data-retention-settings.tsx | 16 +- .../components/activity-panel.tsx | 18 +- apps/sim/ee/sso/components/sso-auth.tsx | 11 +- apps/sim/ee/sso/components/sso-form.test.tsx | 5 +- apps/sim/ee/sso/components/sso-form.tsx | 9 +- .../emcn/src/components/badge/badge.test.tsx | 30 ++ packages/emcn/src/components/badge/badge.tsx | 7 +- .../bulk-action-button.test.tsx | 49 +-- .../bulk-action-button/bulk-action-button.tsx | 27 +- .../emcn/src/components/button/button.tsx | 3 - .../emcn/src/components/checkbox/checkbox.tsx | 6 +- .../chip-button-group/chip-button-group.tsx | 11 +- .../chip-combobox/chip-combobox.tsx | 10 +- .../chip-date-picker/chip-date-picker.tsx | 2 +- .../chip-dropdown/chip-dropdown.test.tsx | 101 ----- .../chip-dropdown/chip-dropdown.tsx | 407 ------------------ .../components/chip-input/chip-input.test.tsx | 58 ++- .../src/components/chip-modal/chip-modal.tsx | 15 +- .../chip-select/chip-select.dom.test.tsx | 289 +++++++++++-- .../components/chip-select/chip-select.tsx | 217 +++++++--- .../components/chip-switch/chip-switch.tsx | 34 +- .../emcn/src/components/chip/chip-chevron.tsx | 2 +- .../emcn/src/components/chip/chip-chrome.ts | 8 +- packages/emcn/src/components/chip/chip.tsx | 8 +- .../src/components/chip/segmented-control.ts | 3 +- .../emcn/src/components/combobox/combobox.tsx | 7 +- .../composer-action-button.tsx | 3 +- .../dropdown-menu/dropdown-menu.tsx | 8 +- packages/emcn/src/components/index.ts | 7 +- .../src/components/info-card/info-card.tsx | 10 +- .../modal/modal-interactions.test.tsx | 11 +- packages/emcn/src/components/modal/modal.tsx | 4 +- .../progress-item/progress-item.tsx | 26 +- .../emcn/src/components/textarea/textarea.tsx | 30 +- .../components/time-picker/time-picker.tsx | 7 +- 85 files changed, 984 insertions(+), 1098 deletions(-) create mode 100644 packages/emcn/src/components/badge/badge.test.tsx delete mode 100644 packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx delete mode 100644 packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index f02ff3946e5..6ef894f2f2f 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. `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`. @@ -421,7 +421,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..cd1435c6f34 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,7 +1,7 @@ 'use client' import { useState } from 'react' -import { cn, Input, Label } from '@sim/emcn' +import { ChipInput, cn, Label } from '@sim/emcn' import { Eye, EyeOff } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' @@ -68,7 +68,7 @@ export default function PasswordAuth({ identifier }: PasswordAuthProps) {
- - - - - - - @@ -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 && ( <> - ) : 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/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..5a41ea4d385 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`). */ @@ -41,25 +41,28 @@ export function SelectValueEditor({ 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 + // In multiple mode ChipSelect 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' + allOptionLabel='Select options' align={align} fullWidth={fullWidth} - matchTriggerWidth={false} + dropdownWidth='content' /> ) } @@ -74,14 +77,17 @@ export function SelectValueEditor({ ] 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..a3152bc2c7a 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 @@ -152,12 +152,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 8fe52025569..22496741add 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, 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' @@ -329,37 +329,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 -