From 5b60a9ef71b7b528ed71d87fd5bfa4b87b600714 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Sat, 19 Sep 2026 13:37:45 -0700 Subject: [PATCH 1/2] improvement(ui): reuse chip inputs for remaining settings fields --- .../components/chip-field.ts | 7 --- .../components/credential-detail/index.ts | 1 - .../components/byok/byok-key-manager.tsx | 63 +++++++++---------- .../whitelabeling-settings.test.tsx | 4 -- .../components/whitelabeling-settings.tsx | 53 ++++++++-------- .../components/chip-input/chip-input.test.tsx | 21 +++++++ .../src/components/chip-input/chip-input.tsx | 6 +- 7 files changed, 82 insertions(+), 73 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts deleted file mode 100644 index d416af43b67..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { chipFieldSurfaceClass, chipFieldTextClass, cn } from '@sim/emcn' - -/** Pill wrapper. Override height/alignment (e.g. a textarea) via `cn`. */ -export const CHIP_FIELD_SHELL = cn('flex h-[30px] items-center gap-1.5 px-2', chipFieldSurfaceClass) - -/** Borderless input/textarea hosted inside {@link CHIP_FIELD_SHELL}. */ -export const CHIP_FIELD_INPUT = cn('h-full w-full bg-transparent', chipFieldTextClass) diff --git a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts index 3a4d337771d..89e6b5508fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts @@ -1,5 +1,4 @@ export { AddPeopleModal } from './components/add-people-modal' -export { CHIP_FIELD_INPUT, CHIP_FIELD_SHELL } from './components/chip-field' export { CredentialDetailHeading } from './components/credential-detail-heading' export { CredentialDetailLayout } from './components/credential-detail-layout' export { CredentialMembersSection } from './components/credential-members-section' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx index 88bd457cb0d..c321b25c1cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx @@ -16,10 +16,6 @@ import { import { Eye, EyeOff, Search } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { - CHIP_FIELD_INPUT, - CHIP_FIELD_SHELL, -} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field' import { BYOKProviderKeysModal } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { @@ -432,34 +428,37 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) { tabIndex={-1} readOnly /> -
- { - setApiKeyInput(e.target.value) - if (error) setError(null) - }} - placeholder={editingMeta?.placeholder} - className={CHIP_FIELD_INPUT} - name='byok_api_key' - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - data-lpignore='true' - data-form-type='other' - /> - -
+ { + setApiKeyInput(e.target.value) + if (error) setError(null) + }} + placeholder={editingMeta?.placeholder} + name='byok_api_key' + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + data-lpignore='true' + data-form-type='other' + endAdornment={ + + } + /> {props.multiKey && ( ({ saveDiscardActions: () => [], })) -vi.mock('@/app/workspace/[workspaceId]/components/credential-detail', () => ({ - CHIP_FIELD_INPUT: '', - CHIP_FIELD_SHELL: '', -})) vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({ SettingsEmptyState: ({ children }: { children?: ReactNode }) =>
{children}
, })) diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index 933a8d56731..128fc5aafbe 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -11,10 +11,6 @@ import { isEnterprise } from '@/lib/billing/plan-helpers' import { HEX_COLOR_REGEX } from '@/lib/branding' import type { OrganizationWhitelabelSettings } from '@/lib/branding/types' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' -import { - CHIP_FIELD_INPUT, - CHIP_FIELD_SHELL, -} from '@/app/workspace/[workspaceId]/components/credential-detail' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' @@ -81,30 +77,31 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn return (
-
-
- { - let v = e.target.value.trim() - if (v && !v.startsWith('#')) { - v = `#${v}` - } - v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') - onChange(v.slice(0, 7)) - }} - onFocus={(e) => e.target.select()} - placeholder={placeholder} - maxLength={7} - className={cn(CHIP_FIELD_INPUT, 'font-mono')} - /> -
+ + } + value={value} + onChange={(e) => { + let v = e.target.value.trim() + if (v && !v.startsWith('#')) { + v = `#${v}` + } + v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') + onChange(v.slice(0, 7)) + }} + onFocus={(e) => e.target.select()} + placeholder={placeholder} + maxLength={7} + inputClassName='font-mono' + /> {!isValidHex && (

Must be a valid hex color (e.g. #33c482) diff --git a/packages/emcn/src/components/chip-input/chip-input.test.tsx b/packages/emcn/src/components/chip-input/chip-input.test.tsx index ed7c1a54f9f..d3fc1d0223a 100644 --- a/packages/emcn/src/components/chip-input/chip-input.test.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.test.tsx @@ -29,6 +29,27 @@ afterEach(() => { }) describe('ChipInput', () => { + it('keeps the focused input mounted when custom leading content changes', () => { + const input = mount() + const render = (color: string) => ( + } + endAdornment={} + /> + ) + act(() => root?.render(render('#123456'))) + input.focus() + input.value = '#123456' + act(() => root?.render(render('#abcdef'))) + + expect(container?.querySelector('input')).toBe(input) + expect(document.activeElement).toBe(input) + expect(input.value).toBe('#123456') + expect(input.previousElementSibling?.getAttribute('style')).toContain('rgb(171, 205, 239)') + expect(input.nextElementSibling?.textContent).toBe('Reset') + }) + it('reserves paintable clearance for a leading glyph without shifting its alignment', () => { const input = mount() diff --git a/packages/emcn/src/components/chip-input/chip-input.tsx b/packages/emcn/src/components/chip-input/chip-input.tsx index e1dca7bc55e..444cbb1704a 100644 --- a/packages/emcn/src/components/chip-input/chip-input.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.tsx @@ -35,6 +35,8 @@ type ChipInputIcon = React.ComponentType<{ className?: string }> export interface ChipInputProps extends Omit, 'size'> { /** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */ icon?: ChipInputIcon + /** Custom leading content, such as a color swatch. Takes precedence over `icon`. */ + startAdornment?: React.ReactNode /** Trailing content rendered after the input (e.g. reveal / copy buttons). */ endAdornment?: React.ReactNode /** Marks the field invalid; swaps the border to the error token. */ @@ -55,6 +57,7 @@ export const ChipInput = React.forwardRef( className, inputClassName, icon: Icon, + startAdornment, endAdornment, error, disabled, @@ -73,7 +76,8 @@ export const ChipInput = React.forwardRef( className )} > - {Icon ? : null} + {startAdornment ?? + (Icon ? : null)} Date: Sat, 19 Sep 2026 13:58:51 -0700 Subject: [PATCH 2/2] improvement(ui): reuse the shared branding upload drop zone (#8031) Co-authored-by: Bill Leoutsakos --- .../components/whitelabeling-settings.tsx | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index 128fc5aafbe..163375d336a 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -11,6 +11,7 @@ import { isEnterprise } from '@/lib/billing/plan-helpers' import { HEX_COLOR_REGEX } from '@/lib/branding' import type { OrganizationWhitelabelSettings } from '@/lib/branding/types' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' +import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' @@ -27,42 +28,6 @@ import { useWorkspacesQuery } from '@/hooks/queries/workspace' const logger = createLogger('WhitelabelingSettings') -interface DropZoneProps { - onDrop: (e: React.DragEvent) => void - children: React.ReactNode - className?: string -} - -function DropZone({ onDrop, children, className }: DropZoneProps) { - const [isDragging, setIsDragging] = useState(false) - - return ( -

{ - if (e.dataTransfer.types.includes('Files')) { - e.preventDefault() - setIsDragging(true) - } - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setIsDragging(false) - } - }} - onDrop={(e) => { - setIsDragging(false) - onDrop(e) - }} - > - {children} - {isDragging && ( -
- )} -
- ) -} - interface ColorInputProps { label: string value: string