Skip to content

Commit 5b60a9e

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
improvement(ui): reuse chip inputs for remaining settings fields
1 parent d5df6f6 commit 5b60a9e

7 files changed

Lines changed: 82 additions & 73 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { AddPeopleModal } from './components/add-people-modal'
2-
export { CHIP_FIELD_INPUT, CHIP_FIELD_SHELL } from './components/chip-field'
32
export { CredentialDetailHeading } from './components/credential-detail-heading'
43
export { CredentialDetailLayout } from './components/credential-detail-layout'
54
export { CredentialMembersSection } from './components/credential-members-section'

‎apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx‎

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import {
1616
import { Eye, EyeOff, Search } from '@sim/emcn/icons'
1717
import { createLogger } from '@sim/logger'
1818
import { getErrorMessage } from '@sim/utils/errors'
19-
import {
20-
CHIP_FIELD_INPUT,
21-
CHIP_FIELD_SHELL,
22-
} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field'
2319
import { BYOKProviderKeysModal } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal'
2420
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2521
import {
@@ -432,34 +428,37 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
432428
tabIndex={-1}
433429
readOnly
434430
/>
435-
<div className={CHIP_FIELD_SHELL}>
436-
<input
437-
aria-label='API Key'
438-
type={showApiKey ? 'text' : 'password'}
439-
value={apiKeyInput}
440-
onChange={(e) => {
441-
setApiKeyInput(e.target.value)
442-
if (error) setError(null)
443-
}}
444-
placeholder={editingMeta?.placeholder}
445-
className={CHIP_FIELD_INPUT}
446-
name='byok_api_key'
447-
autoComplete='off'
448-
autoCorrect='off'
449-
autoCapitalize='off'
450-
data-lpignore='true'
451-
data-form-type='other'
452-
/>
453-
<Button
454-
variant='quiet'
455-
size='icon'
456-
className='shrink-0'
457-
onClick={() => setShowApiKey(!showApiKey)}
458-
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
459-
>
460-
{showApiKey ? <EyeOff className='size-[13px]' /> : <Eye className='size-[13px]' />}
461-
</Button>
462-
</div>
431+
<ChipInput
432+
aria-label='API Key'
433+
type={showApiKey ? 'text' : 'password'}
434+
value={apiKeyInput}
435+
onChange={(e) => {
436+
setApiKeyInput(e.target.value)
437+
if (error) setError(null)
438+
}}
439+
placeholder={editingMeta?.placeholder}
440+
name='byok_api_key'
441+
autoComplete='off'
442+
autoCorrect='off'
443+
autoCapitalize='off'
444+
data-lpignore='true'
445+
data-form-type='other'
446+
endAdornment={
447+
<Button
448+
variant='quiet'
449+
size='icon'
450+
className='shrink-0'
451+
onClick={() => setShowApiKey(!showApiKey)}
452+
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
453+
>
454+
{showApiKey ? (
455+
<EyeOff className='size-[13px]' />
456+
) : (
457+
<Eye className='size-[13px]' />
458+
)}
459+
</Button>
460+
}
461+
/>
463462
</ChipModalField>
464463
{props.multiKey && (
465464
<ChipModalField

‎apps/sim/ee/whitelabeling/components/whitelabeling-settings.test.tsx‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ afterAll(resetEnvFlagsMock)
1616
vi.mock('@/components/settings/save-discard-actions', () => ({
1717
saveDiscardActions: () => [],
1818
}))
19-
vi.mock('@/app/workspace/[workspaceId]/components/credential-detail', () => ({
20-
CHIP_FIELD_INPUT: '',
21-
CHIP_FIELD_SHELL: '',
22-
}))
2319
vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({
2420
SettingsEmptyState: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
2521
}))

‎apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx‎

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import { isEnterprise } from '@/lib/billing/plan-helpers'
1111
import { HEX_COLOR_REGEX } from '@/lib/branding'
1212
import type { OrganizationWhitelabelSettings } from '@/lib/branding/types'
1313
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
14-
import {
15-
CHIP_FIELD_INPUT,
16-
CHIP_FIELD_SHELL,
17-
} from '@/app/workspace/[workspaceId]/components/credential-detail'
1814
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1915
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
2016
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
@@ -81,30 +77,31 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
8177
return (
8278
<div className='flex flex-col gap-1.5'>
8379
<Label>{label}</Label>
84-
<div className={cn(CHIP_FIELD_SHELL, !isValidHex && 'border-[var(--text-error)]')}>
85-
<div
86-
className={cn(
87-
'size-[16px] shrink-0 rounded-sm border border-[var(--border-1)]',
88-
!showColor && 'bg-[var(--surface-3)]'
89-
)}
90-
style={showColor ? { backgroundColor: value } : undefined}
91-
/>
92-
<input
93-
value={value}
94-
onChange={(e) => {
95-
let v = e.target.value.trim()
96-
if (v && !v.startsWith('#')) {
97-
v = `#${v}`
98-
}
99-
v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '')
100-
onChange(v.slice(0, 7))
101-
}}
102-
onFocus={(e) => e.target.select()}
103-
placeholder={placeholder}
104-
maxLength={7}
105-
className={cn(CHIP_FIELD_INPUT, 'font-mono')}
106-
/>
107-
</div>
80+
<ChipInput
81+
error={!isValidHex}
82+
startAdornment={
83+
<div
84+
className={cn(
85+
'size-[16px] shrink-0 rounded-sm border border-[var(--border-1)]',
86+
!showColor && 'bg-[var(--surface-3)]'
87+
)}
88+
style={showColor ? { backgroundColor: value } : undefined}
89+
/>
90+
}
91+
value={value}
92+
onChange={(e) => {
93+
let v = e.target.value.trim()
94+
if (v && !v.startsWith('#')) {
95+
v = `#${v}`
96+
}
97+
v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '')
98+
onChange(v.slice(0, 7))
99+
}}
100+
onFocus={(e) => e.target.select()}
101+
placeholder={placeholder}
102+
maxLength={7}
103+
inputClassName='font-mono'
104+
/>
108105
{!isValidHex && (
109106
<p className='text-[var(--text-error)] text-caption'>
110107
Must be a valid hex color (e.g. #33c482)

‎packages/emcn/src/components/chip-input/chip-input.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ afterEach(() => {
2929
})
3030

3131
describe('ChipInput', () => {
32+
it('keeps the focused input mounted when custom leading content changes', () => {
33+
const input = mount()
34+
const render = (color: string) => (
35+
<ChipInput
36+
aria-label='Color'
37+
startAdornment={<span aria-hidden style={{ backgroundColor: color }} />}
38+
endAdornment={<button type='button'>Reset</button>}
39+
/>
40+
)
41+
act(() => root?.render(render('#123456')))
42+
input.focus()
43+
input.value = '#123456'
44+
act(() => root?.render(render('#abcdef')))
45+
46+
expect(container?.querySelector('input')).toBe(input)
47+
expect(document.activeElement).toBe(input)
48+
expect(input.value).toBe('#123456')
49+
expect(input.previousElementSibling?.getAttribute('style')).toContain('rgb(171, 205, 239)')
50+
expect(input.nextElementSibling?.textContent).toBe('Reset')
51+
})
52+
3253
it('reserves paintable clearance for a leading glyph without shifting its alignment', () => {
3354
const input = mount()
3455

‎packages/emcn/src/components/chip-input/chip-input.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type ChipInputIcon = React.ComponentType<{ className?: string }>
3535
export interface ChipInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
3636
/** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */
3737
icon?: ChipInputIcon
38+
/** Custom leading content, such as a color swatch. Takes precedence over `icon`. */
39+
startAdornment?: React.ReactNode
3840
/** Trailing content rendered after the input (e.g. reveal / copy buttons). */
3941
endAdornment?: React.ReactNode
4042
/** Marks the field invalid; swaps the border to the error token. */
@@ -55,6 +57,7 @@ export const ChipInput = React.forwardRef<HTMLInputElement, ChipInputProps>(
5557
className,
5658
inputClassName,
5759
icon: Icon,
60+
startAdornment,
5861
endAdornment,
5962
error,
6063
disabled,
@@ -73,7 +76,8 @@ export const ChipInput = React.forwardRef<HTMLInputElement, ChipInputProps>(
7376
className
7477
)}
7578
>
76-
{Icon ? <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' /> : null}
79+
{startAdornment ??
80+
(Icon ? <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' /> : null)}
7781
<input
7882
ref={ref}
7983
type={type}

0 commit comments

Comments
 (0)