Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
91bf6ae
refactor(emcn): centralize compact icon button sizing
Sep 18, 2026
8012ece
test(emcn): follow documented button test import order
Sep 18, 2026
8618d27
fix(emcn): restrict specialized action button sizing props
Sep 19, 2026
18c007a
style(test): use TSDoc for the import-order directive
Sep 19, 2026
13f85d8
improvement(ui): reuse compact buttons for message actions (#8087)
BillLeoutsakosvl346 Sep 21, 2026
1d252e5
improvement(emcn): share ordinary action geometry in Button (#8149)
BillLeoutsakosvl346 Sep 23, 2026
b182120
improvement(ui): share canvas toolbar controls (#8066)
BillLeoutsakosvl346 Sep 24, 2026
5c042a0
improvement(ui): share browser toolbar actions (#8068)
BillLeoutsakosvl346 Sep 24, 2026
801329c
refactor(ui): share field disclosures and icon action geometry (#8038)
BillLeoutsakosvl346 Sep 24, 2026
455191b
Consolidate EMCN controls and redundant styling options (#7998)
BillLeoutsakosvl346 Sep 24, 2026
7819fea
Merge staging into icon button sizing and reconcile EMCN APIs
Sep 25, 2026
342f944
refactor(emcn): preserve workflow action chrome after staging merge
Sep 25, 2026
35f4389
Merge remote-tracking branch 'origin/staging' into codex/control-fix-…
Sep 25, 2026
ef993ec
fix(ui): preserve icon action parity and address review feedback
Sep 25, 2026
342c17a
fix(knowledge): keep resource imports direct on list routes
Sep 25, 2026
47a18f4
fix(workflow): preserve panel tab geometry
Sep 25, 2026
a83149f
Merge staging into compact icon button consolidation
Oct 3, 2026
729e80a
fix(ci): align control regression tests with repository audits
Oct 3, 2026
d775632
fix(ci): sort deployment regression mock imports
Oct 3, 2026
81fbe50
fix(test): provide auth text actions in OTP regression fixture
Oct 3, 2026
8c4adec
fix(test): settle OTP timers before DOM teardown
Oct 3, 2026
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
36 changes: 27 additions & 9 deletions .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ paths:

# EMCN Components

Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover).
Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover).

## Chip chrome — single source of truth

Never hand-roll the chip pill from raw class strings (they go stale). Compose from the canonical sources:

- **Surface, typography + content tokens:** `chip/chip-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass` (text fields and the dropdown search box build on these), plus the chip-content chrome `chipContentGap`, `chipGeometryClass`, `chipContentIconClass`, `chipContentLabelClass`, `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells), and the row-state pair `chipHoverSurfaceClass` / `chipActiveSurfaceClass` (hover vs. selected — mutually exclusive, so a selected row holds its surface through hover; every hand-rolled row imports these rather than restating the literals). All are re-exported from the `@sim/emcn` barrel — no subpath import needed.
- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipDropdown`, `ChipSelect`, `ChipSwitch`) reuses it for visual parity.
- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipSelect`, `ChipDatePicker`) reuses it for visual parity.

Canonical look: normal font-weight (never `font-medium`/`font-semibold`), value text `--text-body`, icons `--text-icon` at `size-[14px]`, placeholder `--text-muted`, `transition-colors`, **no focus ring** (the caret marks focus). Filled surface is `--surface-5` light / `--surface-4` dark with a `--border` border (`chip-chrome.ts` still spells it through the legacy alias `--border-1`; new code writes `--border`).

Expand All @@ -24,15 +24,15 @@ 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`, so never give a chip a margin, by default or 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`).
- **`ChipDropdown`** — pill that opens a menu. Single OR multi-select via the discriminated `multiple` prop (one component, not two). Owns its trailing chevron — no `rightIcon`.
- **`ChipSelect` / `ChipCombobox`** — `Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`.
- **`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` (with a `title`) is the escape hatch for controls the field doesn't cover. Every body field is a `ChipModalField`; the gutter rhythm that makes this matter is in `sim-styling.md` → "Form / chip-modal layout rhythm".
- **`ChipSwitch`** — segmented pill control (built from `chipVariants`).
- **`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. `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` (with a `title`) is the escape hatch for controls the field doesn't cover. Every body field is a `ChipModalField`; the gutter rhythm that makes this matter is in `sim-styling.md` → "Form / chip-modal layout rhythm".
- **`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.
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipSelect` instead.
- **`useScrollEdges` + `scrollFadeClass` / `scrollFadeAttributes`** — the canonical scroll-region edge treatment. The hook reports which edges hide content (tracking scroll and resizes; pass the element itself, held in state, when the region mounts after its owner, e.g. inside a Radix portal); the class and attributes fade a fixed 12px band at an active edge only, so a list that fits or sits at its top is never fogged. A floating control over the top edge sets `--scroll-fade-inset` to its height. A region that scrolls sideways (a tab row, a chip strip) uses `useScrollEdges(ref, { axis: 'x' })` with `scrollFadeXClass`; the attributes helper is shared. Any divider beside the region belongs to the neighboring block (`border-b` above, `border-t` below), never to the masked element, and shows only while that edge is active. Never hand-roll a `mask-image` gradient for a scroll region.
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, fade-only clipping (never an ellipsis), the conditional 18px edge mask, and the full-value floating tooltip; consumers pass only layout/typography through `className`. `overflowTextClipClass` and `overflowTextFadeClass` are the complete base/faded treatments for the rare component that must own measurement itself; never pair either with `truncate`, `text-ellipsis`, or hover-time mask removal. Use `DropdownMenuItemLabel` for a menu label beside icons, checks, or actions. A non-editable `Combobox` passes the full visual value through `overlayLabel`; the combobox owns the visual overlay's fade and keeps its one accessible tooltip on the interactive layer. Keep ordinary `truncate` only for editable values, code/log/path content, dense or virtualized grids, and rich composite content that cannot supply a plain tooltip label. Multiline copy uses an intentional `line-clamp-*` treatment instead.

Expand All @@ -51,9 +51,27 @@ Declare keyboard intent on the action-owning primitive; never add document-level
- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.
- **Props over `className` overrides.** When a consumer needs to change chrome, expose a prop (`error`, `icon`, `endAdornment`, `inputClassName`); reaching for `className` to restyle chrome is the smell.
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
- **Discriminated-union props for modes** (e.g. `multiSelect`, the modal field `type`) instead of near-duplicate components.
- **Delete legacy variants after migration** — leave no dead paths.
- **Verify CSS vars exist.** An undefined var resolves to `currentColor`, which draws black borders. Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
- Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example.

Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.


## Ordinary Button action geometry

`Button` retains its existing appearance variants. For square actions use `iconSize`:
`compact` (24px on the spacing scale), `compact-fixed` (24px), `regular` (28px),
`roomy` (32px), or `touch` (40px). These values follow the root spacing scale;
only `compact-fixed` stays fixed when root text is enlarged.
Use `{ base: 'touch', sm: 'regular' }` for mobile/desktop targets. These props own
geometry only; colour, radius and SVG stroke continue to come from the selected
`variant` and `size`. `iconPadding` explicitly overrides the zero-padding geometry.
Use `shape='round'` for circular actions, or omit it to retain the current radius.
`size='inline'` is a 20px-high action with caption typography and compact horizontal
padding. Prefer these supported props to size, padding and radius overrides.

### Chip sizing and centered actions

`Chip`, `ChipLink`, and `ChipInput` retain their default 30px height. Use `size="lg"` for the existing auth-scale 36px controls; the larger size follows the root font size. `Chip` and `ChipLink` accept `align="center"` to center their icon/label group, including full-width form actions. Long labels still shrink and retain their overflow treatment. Avoid descendant flex overrides to center chip labels. The default geometry exports remain unchanged for static consumers.
8 changes: 6 additions & 2 deletions .claude/rules/sim-styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ setWidth: (width) => {

## Text Scale

The `@theme` block in `apps/sim/app/_styles/globals.css`: `text-micro` 10px, `text-xs` 11px, `text-caption` 12px, `text-small` 13px, `text-sm` 14px (Tailwind default), `text-base` 15px, `text-md` 16px, `text-lg` 18px (Tailwind default). `text-xs` is 11px here, so a caption written as `text-xs` is a pixel short. Field titles use `text-small`; hints/errors use `text-caption`. A literal `text-[Npx]` is either a rename to the matching token or an unmigrated straggler.
Use the named scale in `apps/sim/app/_styles/globals.css`: `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px, `text-md`=16px, `text-title`=28px, `text-title-lg`=32px, and `text-display`=40px. The explicitly adopted stock entries retain their rem units and default leading: `text-sm`=0.875rem, `text-lg`=1.125rem, `text-xl`=1.25rem, and `text-2xl`=1.5rem (14/18/20/24px at the default root size). Field titles use `text-small`; hints/errors use `text-caption`; workflow titles use `text-md`.

Preserve explicit line heights when replacing arbitrary sizes. Where the old size inherited its line height, use `leading-[inherit]` if the named utility would otherwise add a different default. Keep document-relative heading/code recipes and the thinking loader's proportional label sizing distinct from ordinary UI text.

Use `font-inter` within the existing scoped Inter loading boundary; the utility does not load the font globally. Keep monospace for code, JSON, and credentials.

Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N` or `h-[Npx] w-[Npx]`.

Expand All @@ -68,7 +72,7 @@ A scroll region that can hide rows past an edge uses `useScrollEdges` with `scro

## Font Weight

Three steps, Tailwind's stock scale, nothing else: **`font-normal` (400)**, **`font-medium` (500)**, **`font-semibold` (600)**. 400 is the document default, so body text, chip labels, sidebar items, and headings carry **no weight class at all** — they inherit. Reach for a class only to step *up* from body.
Three steps, explicitly adopted from Tailwind's stock scale in globals.css, nothing else: **`font-normal` (400)**, **`font-medium` (500)**, **`font-semibold` (600)**. 400 is the document default, so body text, chip labels, sidebar items, and headings carry **no weight class at all** — they inherit. Reach for a class to step *up* from body or to reset meaningful inherited weight. Raw CSS references the same `--font-weight-normal`, `--font-weight-medium`, and `--font-weight-semibold` definitions; their values remain 400/500/600.

Never write an arbitrary weight (`font-[380]`, `font-[450]`, …) or a `--font-weight-*` variable, and never set `fontWeight` in an inline `style`: values clustered between the three steps do not read as hierarchy. Off-scale values are acceptable only where the design system cannot reach — react-email templates and the static `apps/sim/emails/broadcasts/*.html`, which email clients render without CSS variables.

Expand Down
Loading
Loading