Skip to content

Commit be663f4

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
improvement(ui): compose horizontal and credential fields in EMCN
1 parent cfa8a5c commit be663f4

12 files changed

Lines changed: 162 additions & 35 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2424
- **`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`.
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.
27-
- **`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`).
27+
- **`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`).
2828
- **`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`.
30-
- **`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`.
30+
- **`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`.
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.
3232
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
3333
- **`ChipDatePicker`** — chip-styled date field.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2525
- **`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`.
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.
28-
- **`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+
- **`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`).
2929
- **`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`.
31-
- **`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`.
31+
- **`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`.
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.
3333
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
3434
- **`ChipDatePicker`** — chip-styled date field.

‎apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,8 @@ export default function ResumeExecutionPage({
10201020
placeholder='{"example": "value"}'
10211021
rows={6}
10221022
spellCheck={false}
1023-
className='min-h-[180px] font-mono'
1023+
monospace
1024+
className='min-h-[180px]'
10241025
/>
10251026
</div>
10261027
</div>

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ function ClientCredentialAccountModalForm({
275275
value={value}
276276
onChange={(event) => setField(field.id, event.target.value)}
277277
placeholder={field.placeholder}
278-
className='min-h-[120px] font-mono'
278+
monospace
279+
className='min-h-[120px]'
279280
// Browser spell-check and autofill ship textarea contents to
280281
// third-party services — an exfiltration route for a pasted
281282
// private key. `ChipModalField type='textarea'` exposes none

‎apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,8 @@ export function McpServerFormModal({
763763
type='button'
764764
variant='ghost'
765765
onClick={() => setShowAdvanced((v) => !v)}
766-
className='gap-1 self-start py-0 text-small'
766+
size='inline'
767+
className='gap-1 self-start'
767768
>
768769
{showAdvanced ? (
769770
<ChevronDown className='size-[14px]' />

‎apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,16 @@ export function NoOrganizationView({
8383

8484
<div>
8585
<Label htmlFor='orgSlug'>Team URL</Label>
86-
<div className='mt-1 flex items-center'>
87-
<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'>
88-
sim.ai/team/
89-
</div>
90-
<ChipInput
91-
id='orgSlug'
92-
value={orgSlug}
93-
onChange={(e) => setOrgSlug(e.target.value)}
94-
placeholder='my-team'
95-
className='rounded-l-none'
96-
/>
97-
</div>
86+
<ChipInput
87+
id='orgSlug'
88+
value={orgSlug}
89+
onChange={(e) => setOrgSlug(e.target.value)}
90+
placeholder='my-team'
91+
className='mt-1'
92+
startAdornment={
93+
<span className='shrink-0 text-[var(--text-muted)] text-small'>sim.ai/team/</span>
94+
}
95+
/>
9896
</div>
9997

10098
<div className='flex flex-col gap-2'>

‎apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function LockSettingsModal({
9595
<ChipModalField
9696
key={field.key}
9797
type='custom'
98-
className='flex-row items-center justify-between'
98+
orientation='horizontal'
9999
title={
100100
<span className='inline-flex items-center gap-1.5'>
101101
{field.label}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ describe('chip form controls', () => {
9595
<ChipTextarea
9696
ref={textareaRef}
9797
name='description'
98+
monospace
9899
rows={3}
99100
className='min-h-[80px]'
100101
defaultValue='Description'
@@ -109,6 +110,7 @@ describe('chip form controls', () => {
109110
expect(input.parentElement?.className).toContain('h-9')
110111
expect(input.parentElement?.className).toContain('border-[var(--text-error)]')
111112
expect(textareaRef.current?.rows).toBe(3)
113+
expect(textareaRef.current?.className).toContain('font-mono')
112114
expect(textareaRef.current?.className).toContain('min-h-[80px]')
113115
act(() => container?.querySelector('button')?.click())
114116
expect(submit).not.toHaveBeenCalled()

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,61 @@ describe('ChipModalBody', () => {
280280
})
281281
})
282282

283+
describe('ChipModalField composition', () => {
284+
it.each(['vertical', 'horizontal'] as const)(
285+
'retains labels and feedback in %s fields',
286+
(orientation) => {
287+
mount(
288+
<ChipModalField
289+
type='input'
290+
title='Name'
291+
titleActions={<button type='button'>Help</button>}
292+
orientation={orientation}
293+
value='Review'
294+
onChange={() => {}}
295+
required
296+
error='Enter a valid name'
297+
hint='Hidden while invalid'
298+
/>
299+
)
300+
const input = container!.querySelector('input')!
301+
expect(input.labels?.[0].textContent).toBe('Name*')
302+
expect(input.getAttribute('aria-required')).toBe('true')
303+
expect(input.getAttribute('aria-invalid')).toBe('true')
304+
const error = document.getElementById(input.getAttribute('aria-describedby')!)!
305+
expect(error.textContent).toBe('Enter a valid name')
306+
expect(error.getAttribute('role')).toBe('alert')
307+
expect(container!.textContent).not.toContain('Hidden while invalid')
308+
expect(input.labels?.[0].contains(buttonByText('Help'))).toBe(false)
309+
act(() => input.focus())
310+
expect(document.activeElement).toBe(input)
311+
}
312+
)
313+
314+
it('forwards monospace and height settings to a labeled textarea', () => {
315+
mount(
316+
<ChipModalField
317+
type='textarea'
318+
title='JSON'
319+
value='{}'
320+
onChange={() => {}}
321+
mono
322+
minHeight={120}
323+
rows={4}
324+
hint='Enter JSON'
325+
/>
326+
)
327+
const textarea = container!.querySelector('textarea')!
328+
expect(textarea.labels?.[0].textContent).toBe('JSON')
329+
expect(textarea.className).toContain('font-mono')
330+
expect(textarea.style.minHeight).toBe('120px')
331+
expect(textarea.rows).toBe(4)
332+
expect(document.getElementById(textarea.getAttribute('aria-describedby')!)?.textContent).toBe(
333+
'Enter JSON'
334+
)
335+
})
336+
})
337+
283338
describe('ChipModalField file actions', () => {
284339
it('names each upload action with its field title', () => {
285340
mount(
@@ -501,7 +556,13 @@ describe('ChipModal default actions', () => {
501556
<ChipModal open onOpenChange={() => {}} srTitle='Visible action'>
502557
<ChipModalHeader onClose={() => {}}>Visible action</ChipModalHeader>
503558
<ChipModalBody>
504-
<ChipModalField type='input' title='Name' value='Canary' onChange={() => {}} />
559+
<ChipModalField
560+
type='input'
561+
title='Name'
562+
orientation='horizontal'
563+
value='Canary'
564+
onChange={() => {}}
565+
/>
505566
</ChipModalBody>
506567
<div aria-hidden='true'>
507568
<ChipModalFooter

0 commit comments

Comments
 (0)