Skip to content

Commit 491ae0f

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(emcn): complete password and searchable label consolidation
1 parent d06c4cc commit 491ae0f

9 files changed

Lines changed: 88 additions & 76 deletions

File tree

‎.claude/rules/emcn-components.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2525
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
2626
- **`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.
2727
- **`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`).
28-
- **`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.
28+
- **`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.
2929
- **`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`.
3030
- **`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 `<div>` + hand-rolled `<p>`/`<label>` title + bare `ChipInput`/`ChipTextarea`). `ChipModalBody` applies `px-2` + `gap-4`; `ChipModalField` adds another `px-2`, so each field lands at effective `px-4`, exactly matching the `px-4` header/footer — a hand-rolled row skips that gutter and sits misaligned at `px-2`. For controls the field doesn't cover (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX), use `type='custom'` with a `title` — it still applies the gutter and renders the canonical `Label`.
3131
- **`ChipButtonGroup` / `ChipSwitch`** — one Radix radio-group implementation with zero-gap segments and `default`/`compact` density. `ChipButtonGroup` accepts compound children; `ChipSwitch` adapts an options array.

‎.cursor/rules/emcn-components.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2626
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
2727
- **`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.
2828
- **`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`).
29-
- **`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.
29+
- **`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.
3030
- **`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`.
3131
- **`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 `<div>` + hand-rolled `<p>`/`<label>` title + bare `ChipInput`/`ChipTextarea`). `ChipModalBody` applies `px-2` + `gap-4`; `ChipModalField` adds another `px-2`, so each field lands at effective `px-4`, exactly matching the `px-4` header/footer — a hand-rolled row skips that gutter and sits misaligned at `px-2`. For controls the field doesn't cover (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX), use `type='custom'` with a `title` — it still applies the gutter and renders the canonical `Label`.
3232
- **`ChipButtonGroup` / `ChipSwitch`** — one Radix radio-group implementation with zero-gap segments and `default`/`compact` density. `ChipButtonGroup` accepts compound children; `ChipSwitch` adapts an options array.

‎apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx‎

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { ChipInput, cn, Label } from '@sim/emcn'
5-
import { Eye, EyeOff } from '@sim/emcn/icons'
4+
import { cn, Label } from '@sim/emcn'
65
import { createLogger } from '@sim/logger'
76
import { toError } from '@sim/utils/errors'
8-
import { AuthSubmitButton } from '@/app/(auth)/components'
7+
import { AuthSubmitButton, PasswordInput } from '@/app/(auth)/components'
98
import { useChatPasswordAuth } from '@/hooks/queries/chats'
109

1110
const logger = createLogger('PasswordAuth')
@@ -16,7 +15,6 @@ interface PasswordAuthProps {
1615

1716
export default function PasswordAuth({ identifier }: PasswordAuthProps) {
1817
const [password, setPassword] = useState('')
19-
const [showPassword, setShowPassword] = useState(false)
2018
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
2119
const hasPasswordError = passwordErrors.length > 0
2220
const authenticate = useChatPasswordAuth(identifier)
@@ -67,34 +65,19 @@ export default function PasswordAuth({ identifier }: PasswordAuthProps) {
6765
<Label htmlFor='password'>Password</Label>
6866
</div>
6967
<div className='relative'>
70-
<div className='relative'>
71-
<ChipInput
72-
id='password'
73-
name='password'
74-
required
75-
type={showPassword ? 'text' : 'password'}
76-
autoCapitalize='none'
77-
autoComplete='new-password'
78-
autoCorrect='off'
79-
placeholder='Enter password'
80-
value={password}
81-
onChange={handlePasswordChange}
82-
className='h-[34px] pr-10'
83-
error={Boolean(hasPasswordError)}
84-
/>
85-
<button
86-
type='button'
87-
onClick={() => setShowPassword(!showPassword)}
88-
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]'
89-
aria-label={showPassword ? 'Hide password' : 'Show password'}
90-
>
91-
{showPassword ? (
92-
<EyeOff className='size-[18px]' />
93-
) : (
94-
<Eye className='size-[18px]' />
95-
)}
96-
</button>
97-
</div>
68+
<PasswordInput
69+
id='password'
70+
name='password'
71+
required
72+
autoCapitalize='none'
73+
autoComplete='new-password'
74+
autoCorrect='off'
75+
placeholder='Enter password'
76+
value={password}
77+
onChange={handlePasswordChange}
78+
className='h-[34px]'
79+
error={hasPasswordError}
80+
/>
9881
<div
9982
className={cn(
10083
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',

‎apps/sim/app/f/[token]/public-file-auth.tsx‎

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { ChipInput, Label } from '@sim/emcn'
5-
import { Eye, EyeOff } from '@sim/emcn/icons'
4+
import { Label } from '@sim/emcn'
65
import { getErrorMessage } from '@sim/utils/errors'
76
import { useRouter } from 'next/navigation'
8-
import { AuthSubmitButton } from '@/app/(auth)/components'
7+
import { AuthSubmitButton, PasswordInput } from '@/app/(auth)/components'
98
import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell'
109
import { usePublicFileAuth } from '@/hooks/queries/public-shares'
1110

@@ -21,7 +20,6 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) {
2120
const router = useRouter()
2221
const authenticate = usePublicFileAuth(token)
2322
const [password, setPassword] = useState('')
24-
const [showPassword, setShowPassword] = useState(false)
2523
const [error, setError] = useState<string | null>(null)
2624

2725
const handleAuthenticate = async () => {
@@ -49,33 +47,22 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) {
4947
>
5048
<div className='space-y-2'>
5149
<Label htmlFor='password'>Password</Label>
52-
<div className='relative'>
53-
<ChipInput
54-
id='password'
55-
name='password'
56-
required
57-
type={showPassword ? 'text' : 'password'}
58-
autoCapitalize='none'
59-
autoComplete='current-password'
60-
autoCorrect='off'
61-
placeholder='Enter password'
62-
value={password}
63-
onChange={(e) => {
64-
setPassword(e.target.value)
65-
setError(null)
66-
}}
67-
className='h-[34px] pr-10'
68-
error={Boolean(error)}
69-
/>
70-
<button
71-
type='button'
72-
onClick={() => setShowPassword(!showPassword)}
73-
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
74-
aria-label={showPassword ? 'Hide password' : 'Show password'}
75-
>
76-
{showPassword ? <EyeOff className='size-[18px]' /> : <Eye className='size-[18px]' />}
77-
</button>
78-
</div>
50+
<PasswordInput
51+
id='password'
52+
name='password'
53+
required
54+
autoCapitalize='none'
55+
autoComplete='current-password'
56+
autoCorrect='off'
57+
placeholder='Enter password'
58+
value={password}
59+
onChange={(e) => {
60+
setPassword(e.target.value)
61+
setError(null)
62+
}}
63+
className='h-[34px]'
64+
error={Boolean(error)}
65+
/>
7966
{error ? <p className='text-[var(--text-error)] text-xs'>{error}</p> : null}
8067
</div>
8168

‎apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function SelectValueEditor({
3535
(column.options ?? []).map((option) => ({
3636
value: option.id,
3737
label: <SelectPill option={option} />,
38+
searchTerms: [option.name] as const,
3839
})),
3940
[column.options]
4041
)
@@ -67,7 +68,11 @@ export function SelectValueEditor({
6768
const singleOptions = column.required
6869
? options
6970
: [
70-
{ value: CLEAR_VALUE, label: <span className='text-[var(--text-muted)]'>None</span> },
71+
{
72+
value: CLEAR_VALUE,
73+
label: <span className='text-[var(--text-muted)]'>None</span>,
74+
searchTerms: ['None'] as const,
75+
},
7176
...options,
7277
]
7378

‎packages/emcn/src/components/chip-select/chip-select.dom.test.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,20 @@ describe('ChipSelect menu interactions', () => {
208208
{
209209
section: 'Cloud',
210210
items: [
211-
{ value: 'gcp', label: <strong>Google Cloud</strong>, searchTerms: ['gcloud'] },
211+
{
212+
value: 'gcp',
213+
label: <strong>Google Cloud</strong>,
214+
searchTerms: ['Google Cloud', 'gcloud'],
215+
},
212216
],
213217
},
214218
{ section: 'Source', items: [{ value: 'gh', label: 'GitHub' }] },
215219
]}
216220
/>
217221
)
218222
await key(trigger, 'ArrowDown')
223+
changeSearch('cloud')
224+
expect(menuItem('Google Cloud').querySelector('strong')).not.toBeNull()
219225
changeSearch('gcloud')
220226
expect(menuItem('Google Cloud').querySelector('strong')).not.toBeNull()
221227
expect(document.querySelector('[role="menu"]')?.textContent).not.toContain('Source')
@@ -241,7 +247,12 @@ describe('ChipSelect menu interactions', () => {
241247
dropdownWidth={280}
242248
value=''
243249
options={[
244-
{ value: '', label: <span>None</span>, iconElement: <span data-testid='avatar' /> },
250+
{
251+
value: '',
252+
label: <span>None</span>,
253+
searchTerms: ['None'],
254+
iconElement: <span data-testid='avatar' />,
255+
},
245256
]}
246257
/>
247258
)

‎packages/emcn/src/components/chip-select/chip-select.test.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { describe, expect, it } from 'vitest'
1+
import type { ReactElement } from 'react'
2+
import { describe, expect, expectTypeOf, it } from 'vitest'
23
import { type ChipSelectOption, chipSelectOptionMatchesSearch } from './chip-select'
34

45
const GOOGLE_CLOUD_OPTION: ChipSelectOption = {
@@ -8,6 +9,20 @@ const GOOGLE_CLOUD_OPTION: ChipSelectOption = {
89
}
910

1011
describe('chipSelectOptionMatchesSearch', () => {
12+
it('requires search text for rich labels', () => {
13+
expectTypeOf<{ label: ReactElement; value: string }>().not.toExtend<ChipSelectOption>()
14+
expectTypeOf<{
15+
label: ReactElement
16+
value: string
17+
searchTerms: readonly []
18+
}>().not.toExtend<ChipSelectOption>()
19+
expectTypeOf<{
20+
label: ReactElement
21+
value: string
22+
searchTerms: readonly [string]
23+
}>().toExtend<ChipSelectOption>()
24+
})
25+
1126
it('matches labels with normalized casing and whitespace', () => {
1227
expect(chipSelectOptionMatchesSearch(GOOGLE_CLOUD_OPTION, ' CLOUD ')).toBe(true)
1328
})

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,8 @@ import {
1818
import { InsideModalContext } from '../modal/modal'
1919
import { OverflowText, overflowTextClipClass } from '../overflow-text/overflow-text'
2020

21-
/** A selectable option in a {@link ChipSelect}. */
22-
export interface ChipSelectOption {
23-
label: React.ReactNode
21+
interface ChipSelectOptionBase {
2422
value: string
25-
/** Additional search-only terms. These are never rendered in the option label. */
26-
searchTerms?: readonly string[]
2723
/** Optional leading icon. */
2824
icon?: React.ComponentType<{ className?: string }>
2925
/** Pre-rendered leading element, such as an avatar; takes precedence over `icon`. */
@@ -32,6 +28,20 @@ export interface ChipSelectOption {
3228
disabled?: boolean
3329
}
3430

31+
/**
32+
* A selectable option in a {@link ChipSelect}. Rich labels must supply their
33+
* visible text in `searchTerms`, followed by any aliases, so they remain searchable.
34+
* Search terms are never rendered. Plain text labels are matched automatically.
35+
*/
36+
export type ChipSelectOption = ChipSelectOptionBase &
37+
(
38+
| { label: string | number; searchTerms?: readonly string[] }
39+
| {
40+
label: React.ReactNode
41+
searchTerms: readonly [string, ...string[]]
42+
}
43+
)
44+
3545
/** A labeled group of options. When `groups` is set, `options` is ignored. */
3646
export interface ChipSelectOptionGroup {
3747
/** Optional section header rendered above the group. */

‎packages/emcn/src/components/chip/chip-chrome.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ export const chipFieldTextClass =
4949
*/
5050
export const chipContentGap = 'gap-1.5'
5151

52+
/** Standard chip height, also shared by combobox fields. */
53+
export const chipHeightClass = 'h-[30px]'
54+
5255
/**
5356
* Chip pill geometry minus its corner radius — height, centering, gap, padding,
5457
* text size. `chipVariants` composes this with its `shape` variant so a raw
5558
* (non-`cn`) consumer never emits two competing radii; everything else reads
5659
* {@link chipGeometryClass}, which adds the default radius back.
5760
*/
58-
/** Standard chip height, also shared by combobox fields. */
59-
export const chipHeightClass = 'h-[30px]'
6061
export const chipGeometryUnroundedClass = `${chipHeightClass} items-center ${chipContentGap} px-2 text-left text-sm`
6162
/**
6263
* Chip pill geometry — height, centering, gap, radius, padding, text size — with

0 commit comments

Comments
 (0)