diff --git a/.claude/rules/constitution.md b/.claude/rules/constitution.md index 62dd7df3c56..565dee4b877 100644 --- a/.claude/rules/constitution.md +++ b/.claude/rules/constitution.md @@ -1,3 +1,7 @@ +--- +description: Sim product language, positioning, and tone guidelines +--- + # Sim — Language & Positioning When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules. diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index cab88b47f67..f02ff3946e5 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -1,4 +1,5 @@ --- +description: EMCN component library patterns paths: - "packages/emcn/**" --- diff --git a/.claude/rules/global.md b/.claude/rules/global.md index 8ae6e0e874c..90852e76206 100644 --- a/.claude/rules/global.md +++ b/.claude/rules/global.md @@ -1,3 +1,7 @@ +--- +description: Global coding standards that apply to all files +--- + # Global Standards ## Logging diff --git a/.claude/rules/landing-seo-geo.md b/.claude/rules/landing-seo-geo.md index 4f6ec536ad0..51c5402262a 100644 --- a/.claude/rules/landing-seo-geo.md +++ b/.claude/rules/landing-seo-geo.md @@ -1,4 +1,5 @@ --- +description: SEO and GEO guidelines for the landing page paths: - "apps/sim/app/(landing)/**/*.tsx" - "apps/sim/content/**/*.mdx" diff --git a/.claude/rules/sim-architecture.md b/.claude/rules/sim-architecture.md index d950851a3f1..95a59198851 100644 --- a/.claude/rules/sim-architecture.md +++ b/.claude/rules/sim-architecture.md @@ -1,4 +1,5 @@ --- +description: Core architecture principles for the Sim app paths: - "apps/sim/**" --- diff --git a/.claude/rules/sim-caching.md b/.claude/rules/sim-caching.md index fced410b448..28a4f884234 100644 --- a/.claude/rules/sim-caching.md +++ b/.claude/rules/sim-caching.md @@ -1,4 +1,5 @@ --- +description: In-process caching with lru-cache and a ceiling, never hand-rolled TTL maps paths: - "apps/sim/lib/**/*.ts" - "apps/sim/providers/**/*.ts" diff --git a/.claude/rules/sim-components.md b/.claude/rules/sim-components.md index 183fbaac822..f435b20c1a8 100644 --- a/.claude/rules/sim-components.md +++ b/.claude/rules/sim-components.md @@ -1,4 +1,5 @@ --- +description: Component patterns and structure for React components paths: - "apps/sim/**/*.tsx" --- diff --git a/.claude/rules/sim-hooks.md b/.claude/rules/sim-hooks.md index 36b96827c82..a4ff58fdbca 100644 --- a/.claude/rules/sim-hooks.md +++ b/.claude/rules/sim-hooks.md @@ -1,4 +1,5 @@ --- +description: Custom hook patterns and best practices paths: - "apps/sim/**/use-*.ts" - "apps/sim/**/hooks/**/*.ts" diff --git a/.claude/rules/sim-imports.md b/.claude/rules/sim-imports.md index 0449d96e11e..3aeafa0dd2e 100644 --- a/.claude/rules/sim-imports.md +++ b/.claude/rules/sim-imports.md @@ -1,4 +1,5 @@ --- +description: Import patterns for the Sim application paths: - "apps/sim/**/*.ts" - "apps/sim/**/*.tsx" diff --git a/.claude/rules/sim-integrations.md b/.claude/rules/sim-integrations.md index 34231a900b9..c4729747596 100644 --- a/.claude/rules/sim-integrations.md +++ b/.claude/rules/sim-integrations.md @@ -1,4 +1,5 @@ --- +description: Adding new integrations (tools, blocks, triggers) paths: - "apps/sim/tools/**" - "apps/sim/blocks/**" diff --git a/.claude/rules/sim-list-ordering.md b/.claude/rules/sim-list-ordering.md index cad6c2104b3..9b6d6a87147 100644 --- a/.claude/rules/sim-list-ordering.md +++ b/.claude/rules/sim-list-ordering.md @@ -1,4 +1,5 @@ --- +description: List and menu ordering that mirrors the sidebar or toolbar, with one separator before the destructive action paths: - "apps/sim/app/**/*.tsx" - "apps/sim/ee/**/*.tsx" diff --git a/.claude/rules/sim-queries.md b/.claude/rules/sim-queries.md index a71ee448dde..f2600e4e264 100644 --- a/.claude/rules/sim-queries.md +++ b/.claude/rules/sim-queries.md @@ -1,4 +1,5 @@ --- +description: React Query patterns for the Sim application paths: - "apps/sim/hooks/queries/**/*.ts" --- diff --git a/.claude/rules/sim-react-performance.md b/.claude/rules/sim-react-performance.md index a78c2e984e4..a9bfeaa7f63 100644 --- a/.claude/rules/sim-react-performance.md +++ b/.claude/rules/sim-react-performance.md @@ -1,3 +1,7 @@ +--- +description: Behavior-preserving React render-performance idioms +--- + # React & Render Performance Behavior-preserving performance idioms for components, hooks, and hot render paths. These are safe defaults — apply them freely. For the render-causing *effect/state* anti-patterns (derived state in effects, effect chains, state synced to a prop), use the dedicated skills: `/you-might-not-need-an-effect`, `/you-might-not-need-state`, `/you-might-not-need-a-memo`, `/you-might-not-need-a-callback`. Those refactors change render timing — verify them against the running UI, never mass-apply blind. diff --git a/.claude/rules/sim-settings-pages.md b/.claude/rules/sim-settings-pages.md index 9893028c352..aaf692544a1 100644 --- a/.claude/rules/sim-settings-pages.md +++ b/.claude/rules/sim-settings-pages.md @@ -1,4 +1,5 @@ --- +description: Settings page layout and SettingsPanel design-system compliance paths: - "apps/sim/app/workspace/*/settings/**" - "apps/sim/app/workspace/*/{integrations,skills,upgrade}/**" diff --git a/.claude/rules/sim-stores.md b/.claude/rules/sim-stores.md index 273c394fbce..d44078e84bc 100644 --- a/.claude/rules/sim-stores.md +++ b/.claude/rules/sim-stores.md @@ -1,4 +1,5 @@ --- +description: Zustand store patterns paths: - "apps/sim/**/store.ts" - "apps/sim/**/stores/**/*.ts" diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index 61960c9eb5c..48d9086a481 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -1,4 +1,5 @@ --- +description: Tailwind CSS and styling conventions paths: - "apps/sim/**/*.tsx" - "apps/sim/**/*.css" diff --git a/.claude/rules/sim-testing.md b/.claude/rules/sim-testing.md new file mode 100644 index 00000000000..13c9044e594 --- /dev/null +++ b/.claude/rules/sim-testing.md @@ -0,0 +1,258 @@ +--- +description: Testing patterns with Vitest and @sim/testing +paths: + - "apps/sim/**/*.test.ts" + - "apps/sim/**/*.test.tsx" +--- + +# Testing Patterns + +Use Vitest. Test files: `feature.ts` → `feature.test.ts` + +## Global Mocks (vitest.setup.ts) + +These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior: + +- `@sim/db` → `databaseMock` +- `@sim/db/schema` → `schemaMock` +- `drizzle-orm` → `drizzleOrmMock` +- `@sim/logger` → `loggerMock` +- `@/lib/auth` → `authMock` +- `@/lib/auth/hybrid` → `hybridAuthMock` (with default session-delegating behavior) +- `@/lib/core/utils/request` → `requestUtilsMock` +- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store` +- `@/blocks/registry` +- `@trigger.dev/sdk` +- `@sim/platform-authz/workflow` → `workflowAuthzMock` + +## Structure + +```typescript +/** + * @vitest-environment node + */ +import { createMockRequest } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetSession } = vi.hoisted(() => ({ + mockGetSession: vi.fn(), +})) + +vi.mock('@/lib/auth', () => ({ + auth: { api: { getSession: vi.fn() } }, + getSession: mockGetSession, +})) + +import { GET, POST } from '@/app/api/my-route/route' + +describe('my route', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) + }) + + it('returns data', async () => { + const req = createMockRequest('GET') + const res = await GET(req) + expect(res.status).toBe(200) + }) +}) +``` + +## Performance Rules (Critical) + +### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()` + +This is the #1 cause of slow tests. It forces complete module re-evaluation per test. + +```typescript +// BAD — forces module re-evaluation every test (~50-100ms each) +beforeEach(() => { + vi.resetModules() + vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() })) +}) +it('test', async () => { + const { GET } = await import('./route') // slow dynamic import +}) + +// GOOD — module loaded once, mocks reconfigured per test (~1ms each) +const { mockGetSession } = vi.hoisted(() => ({ + mockGetSession: vi.fn(), +})) +vi.mock('@/lib/auth', () => ({ getSession: mockGetSession })) +import { GET } from '@/app/api/my-route/route' + +beforeEach(() => { vi.clearAllMocks() }) +it('test', () => { + mockGetSession.mockResolvedValue({ user: { id: '1' } }) +}) +``` + +**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test. + +### NEVER use `vi.importActual()` + +This defeats the purpose of mocking by loading the real module and all its dependencies. + +```typescript +// BAD — loads real module + all transitive deps +vi.mock('@/lib/workspaces/utils', async () => { + const actual = await vi.importActual('@/lib/workspaces/utils') + return { ...actual, myFn: vi.fn() } +}) + +// GOOD — mock everything, only implement what tests need +vi.mock('@/lib/workspaces/utils', () => ({ + myFn: vi.fn(), + otherFn: vi.fn(), +})) +``` + +### Mock heavy transitive dependencies + +If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them: + +```typescript +vi.mock('@/blocks', () => ({ + getBlock: () => null, + getAllBlocks: () => ({}), + getAllBlockTypes: () => [], + registry: {}, +})) +``` + +### Use `@vitest-environment node` unless DOM is needed + +Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster. + +### Avoid real timers in tests + +```typescript +// BAD +await new Promise(r => setTimeout(r, 500)) + +// GOOD — use minimal delays or fake timers +await new Promise(r => setTimeout(r, 1)) +// or +vi.useFakeTimers() +``` + +## Centralized Mocks (prefer over local declarations) + +`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides. + +| Module mocked | Import | Factory form | +|---|---|---| +| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` | +| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` | +| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` | +| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` | +| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` | +| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` | +| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` | +| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` | +| `@/lib/core/config/env-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/env-flags', () => featureFlagsMock)` | +| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` | +| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` | +| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` | +| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` | +| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` | +| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` | +| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` | +| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` | +| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` | +| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` | +| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` | +| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` | + +### Auth mocking (API routes) + +```typescript +import { authMock, authMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/auth', () => authMock) + +import { GET } from '@/app/api/my-route/route' + +beforeEach(() => { + vi.clearAllMocks() + authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) +}) +``` + +Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`). + +### Hybrid auth mocking + +```typescript +import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing' + +vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) + +// In tests: +hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ + success: true, userId: 'user-1', authType: 'session', +}) +``` + +### Database chain mocking + +Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed. + +```typescript +import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' + +vi.mock('@sim/db', () => dbChainMock) +// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} })) + +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides +}) + +it('reads a row', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }]) + // exercise code that hits db.select().from().where().limit() + expect(dbChainMockFns.where).toHaveBeenCalled() +}) +``` + +**Default chains supported:** +- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()/for()` +- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()` +- `update() → set() → where() → limit()/orderBy()/returning()/for()` +- `delete() → where() → limit()/orderBy()/returning()/for()` +- `db.execute()` resolves `[]` +- `db.transaction(cb)` calls cb with `dbChainMock.db` + +`.for('update')` (Postgres row-level locking) is supported on `where` builders. It returns a thenable with `.limit` / `.orderBy` / `.returning` / `.groupBy` attached, so both `await .where().for('update')` (terminal) and `await .where().for('update').limit(1)` (chained) work. Override the terminal result with `dbChainMockFns.for.mockResolvedValueOnce([...])`; for the chained form, mock the downstream terminal (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`). + +All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns..mockResolvedValueOnce(...)`. Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent); tests using only `...Once` variants don't need it. + +## @sim/testing Package + +Always prefer over local test data. + +| Category | Utilities | +|----------|-----------| +| **Module mocks** | See "Centralized Mocks" table above | +| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` | +| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` | +| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` | +| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` | +| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` | +| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` | +| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` | + +## Rules Summary + +1. `@vitest-environment node` unless DOM is required +2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate +3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports +4. `vi.mock()` calls before importing mocked modules +5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach` +6. No `vi.importActual()` — mock everything explicitly +7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them +8. Use absolute imports in test files +9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()` diff --git a/.claude/rules/sim-typescript.md b/.claude/rules/sim-typescript.md index 526eefd6499..e381c7d1bbf 100644 --- a/.claude/rules/sim-typescript.md +++ b/.claude/rules/sim-typescript.md @@ -1,4 +1,5 @@ --- +description: TypeScript conventions and type safety paths: - "apps/sim/**/*.ts" - "apps/sim/**/*.tsx" diff --git a/.claude/rules/sim-ui-copy.md b/.claude/rules/sim-ui-copy.md index 951e4a15367..b676483f41f 100644 --- a/.claude/rules/sim-ui-copy.md +++ b/.claude/rules/sim-ui-copy.md @@ -1,4 +1,5 @@ --- +description: UI copy conventions — no default subtitles or helper text under headings, labels, cards, or settings paths: - "apps/sim/**/*.tsx" - "apps/sim/components/emcn/**" diff --git a/.claude/rules/sim-url-state.md b/.claude/rules/sim-url-state.md index 6790bc1b56d..69d8c3aec92 100644 --- a/.claude/rules/sim-url-state.md +++ b/.claude/rules/sim-url-state.md @@ -1,4 +1,5 @@ --- +description: Shareable client view-state lives in the URL via nuqs paths: - "apps/sim/app/**/*.tsx" - "apps/sim/app/**/*.ts" diff --git a/.cursor/rules/constitution.mdc b/.cursor/rules/constitution.mdc index dbce52e5298..9e9a50d2b2b 100644 --- a/.cursor/rules/constitution.mdc +++ b/.cursor/rules/constitution.mdc @@ -1,8 +1,10 @@ --- -description: Sim product language, positioning, and tone guidelines -globs: ["apps/sim/app/(landing)/**", "apps/docs/**", "apps/sim/app/manifest.ts", "apps/sim/app/sitemap.ts", "apps/sim/app/robots.ts", "apps/sim/app/llms.txt/**", "apps/sim/app/llms-full.txt/**", "apps/sim/app/(landing)/**/structured-data*", "apps/docs/**/structured-data*", "**/metadata*", "**/seo*"] +description: "Sim product language, positioning, and tone guidelines" +alwaysApply: true --- + + # Sim — Language & Positioning When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules. @@ -28,7 +30,8 @@ Sim is the **AI workspace** where teams build and run AI agents. Not a workflow | The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" | | Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) | | Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" | -| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" | +| The agent | "Sim" — you talk to Sim | "Mothership", "copilot", "AI assistant" | +| The chat surface | "Chat" (capitalized, the module) | "Mothership", "copilot" | | Deployment | "deploy", "ship" | "publish", "activate" | | Audience | "teams", "builders" | "users", "customers" (in marketing copy) | | What agents do | "automate real work" | "automate tasks", "automate workflows" | @@ -55,7 +58,7 @@ When describing Sim, always lead with the most differentiated claim: | Module | One-liner | |--------|-----------| -| **Mothership** | Your AI command center. Build and manage everything in natural language. | +| **Chat** | Your AI command center. Talk to Sim — build and manage everything in natural language. | | **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. | | **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. | | **Tables** | A database, built in. Store, query, and wire structured data into agent runs. | @@ -70,7 +73,8 @@ When describing Sim, always lead with the most differentiated claim: - Never promise unshipped features - Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages - Avoid "agentic workforce" as a primary term — use "AI agents" +- Never say "Mothership" or "copilot" — the agent is "Sim", the surface is "Chat" (in run logs the trigger reads "Sim agent") ## Vision -Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts. +Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Chat is another. The workspace is the constant; the interface adapts. diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc index e9307f6dccf..1ac5fc577f8 100644 --- a/.cursor/rules/emcn-components.mdc +++ b/.cursor/rules/emcn-components.mdc @@ -1,16 +1,19 @@ --- -description: EMCN component library patterns +description: "EMCN component library patterns" globs: ["packages/emcn/**"] --- + + + # EMCN Components -Import from `@sim/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). +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). ## 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`, and `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells). All are re-exported from the `@sim/emcn` barrel — no subpath import needed. +- **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. 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. @@ -19,16 +22,30 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items ## Component catalogue -- **`Chip` / `ChipLink`** — the pill button (`