Skip to content

Commit fdc1ae9

Browse files
committed
Effort picker: ghost ChipDropdown variant, bare labels
ChipDropdown gains a ghost trigger variant (bare toolbar pill, hover-only surface, keeps the owned chevron since the label changes with the value) for visual parity with neighboring Chip buttons; the effort picker uses it with start alignment, natural menu width, and effort labels trimmed to the bare levels. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent 10742d9 commit fdc1ae9

4 files changed

Lines changed: 58 additions & 16 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,9 +685,13 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
685685
<Tooltip.Content side='top'>Skills</Tooltip.Content>
686686
</Tooltip.Root>
687687
<ChipDropdown
688+
variant='ghost'
688689
options={MOTHERSHIP_EFFORT_OPTIONS}
689690
value={effort}
690691
placeholder='Effort'
692+
aria-label='Effort'
693+
align='start'
694+
matchTriggerWidth={false}
691695
onChange={(value) => setEffort(value as MothershipEffort)}
692696
/>
693697
</div>

apps/sim/stores/mothership-effort/store.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { devtools, persist } from 'zustand/middleware'
55
export type MothershipEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
66

77
export const MOTHERSHIP_EFFORT_OPTIONS: Array<{ value: MothershipEffort; label: string }> = [
8-
{ value: 'low', label: 'Low effort' },
9-
{ value: 'medium', label: 'Medium effort' },
10-
{ value: 'high', label: 'High effort' },
11-
{ value: 'xhigh', label: 'X-high effort' },
12-
{ value: 'max', label: 'Max effort' },
8+
{ value: 'low', label: 'Low' },
9+
{ value: 'medium', label: 'Medium' },
10+
{ value: 'high', label: 'High' },
11+
{ value: 'xhigh', label: 'X-high' },
12+
{ value: 'max', label: 'Max' },
1313
]
1414

1515
interface MothershipEffortState {

packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import { act } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { afterEach, describe, expect, it } from 'vitest'
7-
import { ChipDropdown } from './chip-dropdown'
7+
import { ChipDropdown, type ChipDropdownProps } from './chip-dropdown'
88

99
let root: Root | null = null
1010
let container: HTMLDivElement | null = null
1111

12-
function mount(fullWidth: boolean): HTMLButtonElement {
12+
function mount(props: Pick<ChipDropdownProps, 'fullWidth' | 'variant'> = {}): HTMLButtonElement {
1313
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
1414
container = document.createElement('div')
1515
document.body.appendChild(container)
@@ -19,8 +19,8 @@ function mount(fullWidth: boolean): HTMLButtonElement {
1919
<ChipDropdown
2020
value='workflow'
2121
options={[{ value: 'workflow', label: 'Workflow' }]}
22-
fullWidth={fullWidth}
2322
aria-label='Principal type'
23+
{...props}
2424
/>
2525
)
2626
)
@@ -39,18 +39,35 @@ afterEach(() => {
3939

4040
describe('ChipDropdown', () => {
4141
it('fills its container when fullWidth is enabled', () => {
42-
expect(mount(true).className).toContain('w-full')
42+
expect(mount({ fullWidth: true }).className).toContain('w-full')
4343
})
4444

4545
it('keeps its intrinsic width by default', () => {
46-
expect(mount(false).className).not.toContain('w-full')
46+
expect(mount({ fullWidth: false }).className).not.toContain('w-full')
4747
})
4848

4949
it('renders its text trigger through the fade-only overflow primitive', () => {
50-
const label = mount(true).querySelector<HTMLElement>('[data-overflow-text]')
50+
const label = mount({ fullWidth: true }).querySelector<HTMLElement>('[data-overflow-text]')
5151

5252
expect(label?.textContent).toBe('Workflow')
5353
expect(label?.className).toContain('text-clip')
5454
expect(label?.className).not.toContain('truncate')
5555
})
56+
57+
it('renders the filled trigger with the border by default', () => {
58+
const trigger = mount()
59+
expect(trigger.className).toContain('border')
60+
expect(trigger.className).toContain('bg-[var(--surface-5)]')
61+
})
62+
63+
it('renders the ghost trigger as the bare pill — no border, no fill, icon-tinted label', () => {
64+
const trigger = mount({ variant: 'ghost' })
65+
expect(trigger.className).not.toContain('border')
66+
expect(trigger.className).not.toContain('bg-[var(--surface-5)]')
67+
expect(trigger.className).toContain('hover-hover:bg-[var(--surface-hover)]')
68+
69+
const label = trigger.querySelector<HTMLElement>('[data-overflow-text]')
70+
expect(label?.className).toContain('text-[var(--text-icon)]')
71+
expect(label?.className).not.toContain('text-[var(--text-body)]')
72+
})
5673
})

packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,18 @@ interface ChipDropdownOption {
4747
/**
4848
* Trigger + menu chrome props shared by both selection modes.
4949
*/
50-
interface ChipDropdownBaseProps extends VariantProps<typeof chipVariants> {
50+
interface ChipDropdownBaseProps extends Omit<VariantProps<typeof chipVariants>, 'variant'> {
51+
/**
52+
* Trigger chrome. `filled` (default) is the bordered field chip; `ghost` is
53+
* the bare toolbar pill — no border, hover-only surface, label and chevron
54+
* both `--text-icon` — for visual parity with neighboring icon-toolbar
55+
* buttons (mirrors {@link ChipDatePicker}'s `ghost`).
56+
* Unlike the date picker's, a ghost dropdown keeps the owned chevron: its
57+
* label changes with the selected value, so the chevron is the one stable
58+
* cue that this is a picker. Other `chipVariants` values pass through
59+
* (e.g. `primary` for an inverse call-to-action trigger).
60+
*/
61+
variant?: VariantProps<typeof chipVariants>['variant'] | 'ghost'
5162
/** Options to render in the menu. */
5263
options: ReadonlyArray<ChipDropdownOption>
5364
/** Shown in the trigger when nothing is selected. */
@@ -140,7 +151,9 @@ type ChipDropdownProps = ChipDropdownSingleProps | ChipDropdownMultiProps
140151
* `multiple` mode it toggles values, keeps the menu open across selections,
141152
* and optionally renders an "all" reset row and a search field.
142153
*
143-
* The trigger reuses `chipVariants` for visual parity with `Chip`. The label
154+
* The trigger reuses `chipVariants` for visual parity with `Chip` — the
155+
* default `filled` variant with the trigger border, or the bare toolbar pill
156+
* via `variant='ghost'` (see the prop doc). The label
144157
* is `flex-1`, so the trailing chevron is pushed flush right. The chevron is
145158
* owned by the component and rendered at `size-[14px]` (matching the
146159
* workspace-header chevron) — there is intentionally no `rightIcon` prop. The
@@ -219,8 +232,9 @@ const ChipDropdown = forwardRef<HTMLButtonElement, ChipDropdownProps>(
219232
)
220233
}, [options, searchable, search])
221234

235+
const isGhost = variant === 'ghost'
222236
const isInverse = variant === 'primary' || variant === 'destructive'
223-
const hasTriggerBorder = variant !== 'primary' && variant !== 'destructive'
237+
const hasTriggerBorder = !isGhost && !isInverse
224238

225239
let displayLabel: ReactNode
226240
if (isMultiple) {
@@ -252,8 +266,15 @@ const ChipDropdown = forwardRef<HTMLButtonElement, ChipDropdownProps>(
252266
* On intrinsic-width triggers (`inline-flex` with no parent constraint) the
253267
* container is sized to max-content, so `flex-grow` has no leftover space to
254268
* consume and the layout collapses to the natural `gap-2` between items.
269+
*
270+
* The ghost pill's label is `--text-icon` (matching its chevron), not
271+
* `--text-body`: it sits in toolbars beside icon-only buttons, and a
272+
* body-colored label would read louder than every control around it.
255273
*/
256-
const labelClass = cn('flex-1 text-sm', !isInverse && 'text-[var(--text-body)]')
274+
const labelClass = cn(
275+
'flex-1 text-sm',
276+
!isInverse && (isGhost ? 'text-[var(--text-icon)]' : 'text-[var(--text-body)]')
277+
)
257278

258279
const triggerLabelClass = cn(
259280
labelClass,
@@ -325,7 +346,7 @@ const ChipDropdown = forwardRef<HTMLButtonElement, ChipDropdownProps>(
325346
aria-label={ariaLabel}
326347
aria-labelledby={ariaLabelledBy}
327348
className={cn(
328-
chipVariants({ variant, active, fullWidth }),
349+
chipVariants({ variant: isGhost ? 'default' : variant, active, fullWidth }),
329350
hasTriggerBorder && TRIGGER_BORDER_CLASS,
330351
className
331352
)}

0 commit comments

Comments
 (0)