Skip to content

Commit 192f77b

Browse files
authored
improvement(emcn): consolidate chip chrome, enforce ChipModalField, paint real chrome in loading fallbacks (#4935)
* improvement(emcn): consolidate chip chrome, enforce ChipModalField, paint real chrome in loading fallbacks - Move chip chrome single-source to chip/chip-chrome.ts (surface, typography, and new content tokens); delete chip-input/chip-field-chrome.ts - Rework Chip variants: implicit default replaces ghost, filled reserved for chip fields/triggers and removed from Chip's public API; add ChipChevron - Migrate every labeled modal body field to ChipModalField across knowledge, settings, tables, files, deploy, sidebar, and ee modals - Replace skeleton loading.tsx files with ResourceChromeFallback that paints the page's real header, actions, search/filter/sort chips, and column headers - Rename resource-options-bar to resource-options; simplify resource.tsx and resource-header - Delete legacy Breadcrumb, Callout, and FormField components - Add shared connector-config-fields (knowledge) and ee InfoNote; add useTagUsageQuery; make useInlineRename save async with isSaving - Update AGENTS.md/CLAUDE.md and emcn/styling rules (with .cursor/.agents mirrors) * fix(rename): make inline rename await mutateAsync so isSaving disables the field; recover edits on failure - useInlineRename: catch onSave rejection — log, restore the original name, keep the edit session open, and re-arm doneRef (mirrors useItemRename) - switch rename call sites (files, tables, table header, knowledge base, document) from mutate() to mutateAsync() so isSaving spans the request - table-grid column rename stays fire-and-forget by design (optimistic local update + undo entry) * fix(rename): type onSave as void | Promise<unknown> so fire-and-forget call sites pass the build table-grid's optimistic column rename is a block-bodied callback returning void, which is only assignable when the declared return type is literally void — 'undefined | Promise<unknown>' rejected it and failed the Next.js type check in CI.
1 parent 3bf7104 commit 192f77b

113 files changed

Lines changed: 3092 additions & 4078 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/emcn-design-review/SKILL.md

Lines changed: 42 additions & 296 deletions
Large diffs are not rendered by default.

.claude/commands/emcn-design-review.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Intent-to-variant mapping (read the actual `buttonVariants` in `apps/sim/compone
5757

5858
## Delete/Remove Confirmations
5959

60-
Modal `size="sm"`, title "Delete/Remove {ItemType}", `variant="destructive"` action button, `variant="default"` cancel. Cancel left, action right (100% compliance). Use `text-[var(--text-error)]` for irreversible warnings.
60+
`ChipModal` `size='sm'`, title "Delete/Remove {ItemType}", destructive confirm button, plain Cancel (follow the chip footer layout in `.claude/rules/emcn-components.md`). Use `text-[var(--text-error)]` for irreversible warnings.
6161

6262
## Toast
6363

@@ -73,7 +73,8 @@ Default: `size-[14px]`. Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 1
7373

7474
## Anti-patterns to flag
7575

76-
- Raw `<button>`/`<input>` instead of emcn components
76+
- Raw `<button>`/`<input>`, or legacy `Input`/`Textarea`/`Modal`, instead of the canonical chip components (`ChipInput`/`ChipTextarea`/`ChipModal`)
77+
- Hand-rolled field rows inside a `ChipModalBody` instead of `ChipModalField`
7778
- Hardcoded colors (`text-gray-*`, `#hex`, `rgb()`)
7879
- Tailwind semantics (`text-muted-foreground`) instead of CSS variables
7980
- Template literal className instead of `cn()`

.claude/rules/emcn-components.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ paths:
55

66
# EMCN Components
77

8-
Import from `@/components/emcn`, never from subpaths (except CSS files). The **chip family** is the platform's primary chrome — prefer it over the legacy primitives (`Input`, `Textarea`, etc.), which it is progressively replacing.
8+
Import from `@/components/emcn`, never from subpaths (except CSS files). 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).
99

1010
## Chip chrome — single source of truth
1111

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

14-
- **Surface + typography tokens:** `chip-input/chip-field-chrome.ts``chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass`. Text fields and the dropdown search box build on these.
14+
- **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`, and `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells). All are re-exported from the `@/components/emcn` barrel — no subpath import needed.
1515
- **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.
1616

1717
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-1` border.
@@ -25,14 +25,15 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2525
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default).
2626
- **`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`.
2727
- **`ChipSelect` / `ChipCombobox`**`Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`.
28-
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `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.
28+
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `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`.
2929
- **`ChipSwitch`** — segmented pill control (built from `chipVariants`).
3030
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
3131
- **`ChipDatePicker`** — chip-styled date field.
32+
- **`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.
3233

3334
## Authoring principles
3435

35-
- **One source of truth for shared chrome.** Compose from `chip-field-chrome.ts` / `chipVariants`; never duplicate the chrome string.
36+
- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.
3637
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
3738
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
3839
- **Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).

.claude/rules/sim-styling.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text
5252

5353
## Chip Components (consumer usage)
5454

55-
`ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `apps/sim/components/emcn/components/chip-input/chip-field-chrome.ts` — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings.
55+
`ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `apps/sim/components/emcn/components/chip/chip-chrome.ts` (all tokens are re-exported from the `@/components/emcn` barrel — no subpath import needed) — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings.
5656

5757
### Props over className
5858

@@ -71,6 +71,8 @@ Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins
7171
- **Field row** = `ChipModalField`: label↔control `gap-[9px]`, field gutter `px-2` (`px-0` when `flush`). Title = `Label` at `text-small` (13px), muted, normal weight; hint/error at `text-caption` (12px).
7272
- **Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`.
7373
- **Header/footer**: horizontal gutter `px-4` (header `pt-3`; footer `px-4 pt-2 pb-2`, tinted bar).
74+
- **Every body field MUST be a `ChipModalField`** — NEVER hand-roll a field row (raw `<div>` + hand-rolled `<p>`/`<label>` title + bare `ChipInput`/`ChipTextarea`). WHY: body `px-2` + field `px-2` = effective `px-4`, exactly matching the `px-4` header/footer. A hand-rolled row skips the field gutter, sits at `px-2`, and is visibly misaligned (this bug shipped in the scheduled-tasks "Create new scheduled task" modal). Inline errors go through the `error` prop, not a hand-rolled `<p>`.
75+
- **Uncovered controls** (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `<div>`, and never add a body-level wrapper `<div>` with a custom `gap-*` that fights `gap-4`.
7476
- **Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it.
7577

7678
When a standalone labeled field outside a `ChipModal` needs the same look (e.g. `SkillImport`), match the field rhythm by hand: `flex flex-col gap-[9px]`, muted label, `ChipInput`/`ChipTextarea` control, `text-caption` error below.

.cursor/commands/emcn-design-review.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit
1212

1313
## Steps
1414

15-
1. Read the emcn barrel export at `apps/sim/components/emcn/components/index.ts` to know what's available
15+
1. Read the emcn public barrel at `apps/sim/components/emcn/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `apps/sim/components/emcn/icons/index.ts`
1616
2. Read `apps/sim/app/_styles/globals.css` for CSS variable tokens
1717
3. Analyze the specified scope against every rule below
1818
4. If fix=true, apply the fixes. If fix=false, propose the fixes without applying.
@@ -29,26 +29,30 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit
2929

3030
Use CSS variable pattern (`text-[var(--text-primary)]`), never Tailwind semantics (`text-muted-foreground`) or hardcoded colors (`text-gray-500`, `#333`).
3131

32-
**Text**: `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-muted`, `--text-icon`, `--text-inverse`, `--text-error`
33-
**Surfaces**: `--bg`, `--surface-2` through `--surface-7`, `--surface-hover`, `--surface-active`
32+
**Text**: `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-muted`, `--text-body` (canonical value text), `--text-icon`, `--text-placeholder`, `--text-subtle`, `--text-inverse`, `--text-error`
33+
**Surfaces**: `--bg`, `--surface-1` through `--surface-7`, `--surface-hover`, `--surface-active`
3434
**Borders**: `--border`, `--border-1`, `--border-muted`
35+
**Brand/accent**: `--brand-secondary`, `--brand-accent`
3536
**Z-Index**: `--z-dropdown` (100), `--z-modal` (200), `--z-popover` (300), `--z-tooltip` (400), `--z-toast` (500)
3637
**Shadows**: `shadow-subtle`, `shadow-medium`, `shadow-overlay`, `shadow-card`
38+
**Badges**: `--badge-*` semantic families (success/error/gray/blue/purple/orange/amber/teal/cyan/pink, each with `-bg`/`-text`)
3739

3840
## Buttons
3941

42+
Intent-to-variant mapping (read the actual `buttonVariants` in `apps/sim/components/emcn/components/button/button.tsx` for the full variant set — it exposes more than listed here):
43+
4044
| Action | Variant |
4145
|--------|---------|
42-
| Toolbar, icon-only | `ghost` (most common, 28%) |
43-
| Create, save, submit | `primary` (24%) |
46+
| Toolbar, icon-only | `ghost` |
47+
| Create, save, submit | `primary` |
4448
| Cancel, close | `default` |
4549
| Delete, remove | `destructive` |
4650
| Selected state | `active` |
4751
| Toggle | `outline` |
4852

4953
## Delete/Remove Confirmations
5054

51-
Modal `size="sm"`, title "Delete/Remove {ItemType}", `variant="destructive"` action button, `variant="default"` cancel. Cancel left, action right (100% compliance). Use `text-[var(--text-error)]` for irreversible warnings.
55+
`ChipModal` `size='sm'`, title "Delete/Remove {ItemType}", destructive confirm button, plain Cancel (follow the chip footer layout in `.claude/rules/emcn-components.md`). Use `text-[var(--text-error)]` for irreversible warnings.
5256

5357
## Toast
5458

@@ -64,7 +68,8 @@ Default: `size-[14px]`. Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 1
6468

6569
## Anti-patterns to flag
6670

67-
- Raw `<button>`/`<input>` instead of emcn components
71+
- Raw `<button>`/`<input>`, or legacy `Input`/`Textarea`/`Modal`, instead of the canonical chip components (`ChipInput`/`ChipTextarea`/`ChipModal`)
72+
- Hand-rolled field rows inside a `ChipModalBody` instead of `ChipModalField`
6873
- Hardcoded colors (`text-gray-*`, `#hex`, `rgb()`)
6974
- Tailwind semantics (`text-muted-foreground`) instead of CSS variables
7075
- Template literal className instead of `cn()`

.cursor/rules/emcn-components.mdc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ globs: ["apps/sim/components/emcn/**"]
44
---
55
# EMCN Components
66

7-
Import from `@/components/emcn`, never from subpaths (except CSS files). The **chip family** is the platform's primary chrome — prefer it over the legacy primitives (`Input`, `Textarea`, etc.), which it is progressively replacing.
7+
Import from `@/components/emcn`, never from subpaths (except CSS files). 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).
88

99
## Chip chrome — single source of truth
1010

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

13-
- **Surface + typography tokens:** `chip-input/chip-field-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass`. Text fields and the dropdown search box build on these.
13+
- **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`, and `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells). All are re-exported from the `@/components/emcn` barrel — no subpath import needed.
1414
- **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.
1515

1616
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-1` border.
@@ -24,14 +24,15 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2424
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default).
2525
- **`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`.
2626
- **`ChipSelect` / `ChipCombobox`** — `Combobox`-backed pickers with search, groups, multi-select; for richer lists than `ChipDropdown`.
27-
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `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.
27+
- **`ChipModal` + `ChipModalField`** — declarative compact modal. The field's `type` (`input` | `email` | `textarea` | `dropdown` | `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`.
2828
- **`ChipSwitch`** — segmented pill control (built from `chipVariants`).
2929
- **`ChipTag`** — 20px inline tag/badge (`mono`/`gray`/`invite`), not a pill trigger.
3030
- **`ChipDatePicker`** — chip-styled date field.
31+
- **`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.
3132

3233
## Authoring principles
3334

34-
- **One source of truth for shared chrome.** Compose from `chip-field-chrome.ts` / `chipVariants`; never duplicate the chrome string.
35+
- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.
3536
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
3637
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
3738
- **Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).

0 commit comments

Comments
 (0)