Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `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`).
- **`ChipTextarea`** — multi-line sibling. `monospace` for structured text, `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`).
- **`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.
- **`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 `<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`.
- **`ChipModal` + `ChipModalField`** — declarative compact modal. `orientation='horizontal'` places a field title beside its control, wraps on narrow surfaces, and keeps error/hint text below the row; vertical is the default. 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`.
- **`ChipButtonGroup` / `ChipSwitch`** — one Radix radio-group implementation with zero-gap segments and `default`/`compact` density. `ChipButtonGroup` accepts compound children; `ChipSwitch` adapts an options array.
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
- **`ChipDatePicker`** — chip-styled date field.
Expand Down
4 changes: 2 additions & 2 deletions .cursor/rules/emcn-components.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `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`).
- **`ChipTextarea`** — multi-line sibling. `monospace` for structured text, `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`).
- **`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.
- **`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 `<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`.
- **`ChipModal` + `ChipModalField`** — declarative compact modal. `orientation='horizontal'` places a field title beside its control, wraps on narrow surfaces, and keeps error/hint text below the row; vertical is the default. 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`.
- **`ChipButtonGroup` / `ChipSwitch`** — one Radix radio-group implementation with zero-gap segments and `default`/`compact` density. `ChipButtonGroup` accepts compound children; `ChipSwitch` adapts an options array.
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
- **`ChipDatePicker`** — chip-styled date field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,8 @@ export default function ResumeExecutionPage({
placeholder='{"example": "value"}'
rows={6}
spellCheck={false}
className='min-h-[180px] font-mono'
monospace
className='min-h-[180px]'
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ function ClientCredentialAccountModalForm({
value={value}
onChange={(event) => setField(field.id, event.target.value)}
placeholder={field.placeholder}
className='min-h-[120px] font-mono'
monospace
className='min-h-[120px]'
// Browser spell-check and autofill ship textarea contents to
// third-party services — an exfiltration route for a pasted
// private key. `ChipModalField type='textarea'` exposes none
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,8 @@ export function McpServerFormModal({
type='button'
variant='ghost'
onClick={() => setShowAdvanced((v) => !v)}
className='gap-1 self-start py-0 text-small'
size='inline'
className='gap-1 self-start'
>
{showAdvanced ? (
<ChevronDown className='size-[14px]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,16 @@ export function NoOrganizationView({

<div>
<Label htmlFor='orgSlug'>Team URL</Label>
<div className='mt-1 flex items-center'>
<div className='rounded-l-[6px] border border-[var(--border-1)] border-r-0 bg-[var(--surface-4)] px-3 py-1.5 text-[var(--text-muted)] text-small'>
sim.ai/team/
</div>
<ChipInput
id='orgSlug'
value={orgSlug}
onChange={(e) => setOrgSlug(e.target.value)}
placeholder='my-team'
className='rounded-l-none'
/>
</div>
<ChipInput
id='orgSlug'
value={orgSlug}
onChange={(e) => setOrgSlug(e.target.value)}
placeholder='my-team'
className='mt-1'
startAdornment={
<span className='shrink-0 text-[var(--text-muted)] text-small'>sim.ai/team/</span>
}
/>
</div>

<div className='flex flex-col gap-2'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function LockSettingsModal({
<ChipModalField
key={field.key}
type='custom'
className='flex-row items-center justify-between'
orientation='horizontal'
title={
<span className='inline-flex items-center gap-1.5'>
{field.label}
Expand Down
2 changes: 2 additions & 0 deletions packages/emcn/src/components/chip-input/chip-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe('chip form controls', () => {
<ChipTextarea
ref={textareaRef}
name='description'
monospace
rows={3}
className='min-h-[80px]'
defaultValue='Description'
Expand All @@ -109,6 +110,7 @@ describe('chip form controls', () => {
expect(input.parentElement?.className).toContain('h-9')
expect(input.parentElement?.className).toContain('border-[var(--text-error)]')
expect(textareaRef.current?.rows).toBe(3)
expect(textareaRef.current?.className).toContain('font-mono')
expect(textareaRef.current?.className).toContain('min-h-[80px]')
act(() => container?.querySelector('button')?.click())
expect(submit).not.toHaveBeenCalled()
Expand Down
63 changes: 62 additions & 1 deletion packages/emcn/src/components/chip-modal/chip-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,61 @@ describe('ChipModalBody', () => {
})
})

describe('ChipModalField composition', () => {
it.each(['vertical', 'horizontal'] as const)(
'retains labels and feedback in %s fields',
(orientation) => {
mount(
<ChipModalField
type='input'
title='Name'
titleActions={<button type='button'>Help</button>}
orientation={orientation}
value='Review'
onChange={() => {}}
required
error='Enter a valid name'
hint='Hidden while invalid'
/>
)
const input = container!.querySelector('input')!
expect(input.labels?.[0].textContent).toBe('Name*')
expect(input.getAttribute('aria-required')).toBe('true')
expect(input.getAttribute('aria-invalid')).toBe('true')
const error = document.getElementById(input.getAttribute('aria-describedby')!)!
expect(error.textContent).toBe('Enter a valid name')
expect(error.getAttribute('role')).toBe('alert')
expect(container!.textContent).not.toContain('Hidden while invalid')
expect(input.labels?.[0].contains(buttonByText('Help'))).toBe(false)
act(() => input.focus())
expect(document.activeElement).toBe(input)
}
)

it('forwards monospace and height settings to a labeled textarea', () => {
mount(
<ChipModalField
type='textarea'
title='JSON'
value='{}'
onChange={() => {}}
mono
minHeight={120}
rows={4}
hint='Enter JSON'
/>
)
const textarea = container!.querySelector('textarea')!
expect(textarea.labels?.[0].textContent).toBe('JSON')
expect(textarea.className).toContain('font-mono')
expect(textarea.style.minHeight).toBe('120px')
expect(textarea.rows).toBe(4)
expect(document.getElementById(textarea.getAttribute('aria-describedby')!)?.textContent).toBe(
'Enter JSON'
)
})
})

describe('ChipModalField file actions', () => {
it('names each upload action with its field title', () => {
mount(
Expand Down Expand Up @@ -501,7 +556,13 @@ describe('ChipModal default actions', () => {
<ChipModal open onOpenChange={() => {}} srTitle='Visible action'>
<ChipModalHeader onClose={() => {}}>Visible action</ChipModalHeader>
<ChipModalBody>
<ChipModalField type='input' title='Name' value='Canary' onChange={() => {}} />
<ChipModalField
type='input'
title='Name'
orientation='horizontal'
value='Canary'
onChange={() => {}}
/>
</ChipModalBody>
<div aria-hidden='true'>
<ChipModalFooter
Expand Down
Loading
Loading