mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-06 21:54:01 -05:00
feat(cursorrules): updated cursorrules and claude md file (#2640)
* feat(cursorrules): updated cursorrules and claude md file * added rules for adding new integrations
This commit is contained in:
@@ -1,45 +1,35 @@
|
|||||||
---
|
---
|
||||||
description: EMCN component library patterns with CVA
|
description: EMCN component library patterns
|
||||||
globs: ["apps/sim/components/emcn/**"]
|
globs: ["apps/sim/components/emcn/**"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# EMCN Component Guidelines
|
# EMCN Components
|
||||||
|
|
||||||
## When to Use CVA vs Direct Styles
|
Import from `@/components/emcn`, never from subpaths (except CSS files).
|
||||||
|
|
||||||
**Use CVA (class-variance-authority) when:**
|
## CVA vs Direct Styles
|
||||||
- 2+ visual variants (primary, secondary, outline)
|
|
||||||
- Multiple sizes or state variations
|
|
||||||
- Example: Button with variants
|
|
||||||
|
|
||||||
**Use direct className when:**
|
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
|
||||||
- Single consistent style
|
|
||||||
- No variations needed
|
|
||||||
- Example: Label with one style
|
|
||||||
|
|
||||||
## Patterns
|
|
||||||
|
|
||||||
**With CVA:**
|
|
||||||
```tsx
|
```tsx
|
||||||
const buttonVariants = cva('base-classes', {
|
const buttonVariants = cva('base-classes', {
|
||||||
variants: {
|
variants: { variant: { default: '...', primary: '...' } }
|
||||||
variant: { default: '...', primary: '...' },
|
|
||||||
size: { sm: '...', md: '...' }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Without CVA:**
|
**Use direct className when:** Single consistent style, no variations
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Label({ className, ...props }) {
|
function Label({ className, ...props }) {
|
||||||
return <Primitive className={cn('single-style-classes', className)} {...props} />
|
return <Primitive className={cn('style-classes', className)} {...props} />
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Use Radix UI primitives for accessibility
|
- Use Radix UI primitives for accessibility
|
||||||
- Export component and variants (if using CVA)
|
- Export component and variants (if using CVA)
|
||||||
- TSDoc with usage examples
|
- TSDoc with usage examples
|
||||||
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
||||||
- Always use `transition-colors` for hover states
|
- `transition-colors` for hover states
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ alwaysApply: true
|
|||||||
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
|
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
|
||||||
|
|
||||||
## Comments
|
## Comments
|
||||||
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
||||||
|
|||||||
@@ -10,58 +10,47 @@ globs: ["apps/sim/**"]
|
|||||||
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
|
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
|
||||||
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
|
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
|
||||||
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
|
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
|
||||||
5. **Performance by Default**: useMemo, useCallback, refs appropriately
|
|
||||||
|
|
||||||
## File Organization
|
## Root-Level Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/sim/
|
||||||
|
├── app/ # Next.js app router (pages, API routes)
|
||||||
|
├── blocks/ # Block definitions and registry
|
||||||
|
├── components/ # Shared UI (emcn/, ui/)
|
||||||
|
├── executor/ # Workflow execution engine
|
||||||
|
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||||
|
├── lib/ # App-wide utilities
|
||||||
|
├── providers/ # LLM provider integrations
|
||||||
|
├── stores/ # Zustand stores
|
||||||
|
├── tools/ # Tool definitions
|
||||||
|
└── triggers/ # Trigger definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Organization
|
||||||
|
|
||||||
|
Features live under `app/workspace/[workspaceId]/`:
|
||||||
|
|
||||||
```
|
```
|
||||||
feature/
|
feature/
|
||||||
├── components/ # Feature components
|
├── components/ # Feature components
|
||||||
│ └── sub-feature/ # Sub-feature with own components
|
├── hooks/ # Feature-scoped hooks
|
||||||
├── hooks/ # Custom hooks
|
├── utils/ # Feature-scoped utilities (2+ consumers)
|
||||||
└── feature.tsx # Main component
|
├── feature.tsx # Main component
|
||||||
|
└── page.tsx # Next.js page entry
|
||||||
```
|
```
|
||||||
|
|
||||||
## Naming Conventions
|
## Naming Conventions
|
||||||
- **Components**: PascalCase (`WorkflowList`, `TriggerPanel`)
|
- **Components**: PascalCase (`WorkflowList`)
|
||||||
- **Hooks**: camelCase with `use` prefix (`useWorkflowOperations`)
|
- **Hooks**: `use` prefix (`useWorkflowOperations`)
|
||||||
- **Files**: kebab-case matching export (`workflow-list.tsx`)
|
- **Files**: kebab-case (`workflow-list.tsx`)
|
||||||
- **Stores**: kebab-case in stores/ (`sidebar/store.ts`)
|
- **Stores**: `stores/feature/store.ts`
|
||||||
- **Constants**: SCREAMING_SNAKE_CASE
|
- **Constants**: SCREAMING_SNAKE_CASE
|
||||||
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
|
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
|
||||||
|
|
||||||
## State Management
|
## Utils Rules
|
||||||
|
|
||||||
**useState**: UI-only concerns (dropdown open, hover, form inputs)
|
- **Never create `utils.ts` for single consumer** - inline it
|
||||||
**Zustand**: Shared state, persistence, global app state
|
- **Create `utils.ts` when** 2+ files need the same helper
|
||||||
**useRef**: DOM refs, avoiding dependency issues, mutable non-reactive values
|
- **Check existing sources** before duplicating (`lib/` has many utilities)
|
||||||
|
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)
|
||||||
## Component Extraction
|
|
||||||
|
|
||||||
**Extract to separate file when:**
|
|
||||||
- Complex (50+ lines)
|
|
||||||
- Used across 2+ files
|
|
||||||
- Has own state/logic
|
|
||||||
|
|
||||||
**Keep inline when:**
|
|
||||||
- Simple (< 10 lines)
|
|
||||||
- Used in only 1 file
|
|
||||||
- Purely presentational
|
|
||||||
|
|
||||||
**Never import utilities from another component file.** Extract shared helpers to `lib/` or `utils/`.
|
|
||||||
|
|
||||||
## Utils Files
|
|
||||||
|
|
||||||
**Never create a `utils.ts` file for a single consumer.** Inline the logic directly in the consuming component.
|
|
||||||
|
|
||||||
**Create `utils.ts` when:**
|
|
||||||
- 2+ files import the same helper
|
|
||||||
|
|
||||||
**Prefer existing sources of truth:**
|
|
||||||
- Before duplicating logic, check if a centralized helper already exists (e.g., `lib/logs/get-trigger-options.ts`)
|
|
||||||
- Import from the source of truth rather than creating wrapper functions
|
|
||||||
|
|
||||||
**Location hierarchy:**
|
|
||||||
- `lib/` — App-wide utilities (auth, billing, core)
|
|
||||||
- `feature/utils.ts` — Feature-scoped utilities (used by 2+ components in the feature)
|
|
||||||
- Inline — Single-use helpers (define directly in the component)
|
|
||||||
|
|||||||
@@ -6,59 +6,43 @@ globs: ["apps/sim/**/*.tsx"]
|
|||||||
# Component Patterns
|
# Component Patterns
|
||||||
|
|
||||||
## Structure Order
|
## Structure Order
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'use client' // Only if using hooks
|
'use client' // Only if using hooks
|
||||||
|
|
||||||
// 1. Imports (external → internal → relative)
|
// Imports (external → internal)
|
||||||
// 2. Constants at module level
|
// Constants at module level
|
||||||
const CONFIG = { SPACING: 8 } as const
|
const CONFIG = { SPACING: 8 } as const
|
||||||
|
|
||||||
// 3. Props interface with TSDoc
|
// Props interface
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
/** Description */
|
|
||||||
requiredProp: string
|
requiredProp: string
|
||||||
optionalProp?: boolean
|
optionalProp?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Component with TSDoc
|
|
||||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||||
// a. Refs
|
// a. Refs
|
||||||
// b. External hooks (useParams, useRouter)
|
// b. External hooks (useParams, useRouter)
|
||||||
// c. Store hooks
|
// c. Store hooks
|
||||||
// d. Custom hooks
|
// d. Custom hooks
|
||||||
// e. Local state
|
// e. Local state
|
||||||
// f. useMemo computations
|
// f. useMemo
|
||||||
// g. useCallback handlers
|
// g. useCallback
|
||||||
// h. useEffect
|
// h. useEffect
|
||||||
// i. Return JSX
|
// i. Return JSX
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
1. Add `'use client'` when using React hooks
|
|
||||||
|
1. `'use client'` only when using React hooks
|
||||||
2. Always define props interface
|
2. Always define props interface
|
||||||
3. TSDoc on component: description, @param, @returns
|
3. Extract constants with `as const`
|
||||||
4. Extract constants with `as const`
|
4. Semantic HTML (`aside`, `nav`, `article`)
|
||||||
5. Use Tailwind only, no inline styles
|
5. Optional chain callbacks: `onAction?.(id)`
|
||||||
6. Semantic HTML (`aside`, `nav`, `article`)
|
|
||||||
7. Include ARIA attributes where appropriate
|
|
||||||
8. Optional chain callbacks: `onAction?.(id)`
|
|
||||||
|
|
||||||
## Factory Pattern with Caching
|
## Component Extraction
|
||||||
|
|
||||||
When generating components for a specific signature (e.g., icons):
|
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
|
||||||
|
|
||||||
```typescript
|
**Keep inline when:** < 10 lines, single use, purely presentational
|
||||||
const cache = new Map<string, React.ComponentType<{ className?: string }>>()
|
|
||||||
|
|
||||||
function getColorIcon(color: string) {
|
|
||||||
if (cache.has(color)) return cache.get(color)!
|
|
||||||
|
|
||||||
const Icon = ({ className }: { className?: string }) => (
|
|
||||||
<div className={cn(className, 'rounded-[3px]')} style={{ backgroundColor: color, width: 10, height: 10 }} />
|
|
||||||
)
|
|
||||||
Icon.displayName = `ColorIcon(${color})`
|
|
||||||
cache.set(color, Icon)
|
|
||||||
return Icon
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -6,21 +6,13 @@ globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"]
|
|||||||
# Hook Patterns
|
# Hook Patterns
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
|
||||||
|
|
||||||
const logger = createLogger('useFeatureName')
|
|
||||||
|
|
||||||
interface UseFeatureProps {
|
interface UseFeatureProps {
|
||||||
id: string
|
id: string
|
||||||
onSuccess?: (result: Result) => void
|
onSuccess?: (result: Result) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook description.
|
|
||||||
* @param props - Configuration
|
|
||||||
* @returns State and operations
|
|
||||||
*/
|
|
||||||
export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
||||||
// 1. Refs for stable dependencies
|
// 1. Refs for stable dependencies
|
||||||
const idRef = useRef(id)
|
const idRef = useRef(id)
|
||||||
@@ -29,7 +21,6 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
|||||||
// 2. State
|
// 2. State
|
||||||
const [data, setData] = useState<Data | null>(null)
|
const [data, setData] = useState<Data | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [error, setError] = useState<Error | null>(null)
|
|
||||||
|
|
||||||
// 3. Sync refs
|
// 3. Sync refs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,32 +28,27 @@ export function useFeature({ id, onSuccess }: UseFeatureProps) {
|
|||||||
onSuccessRef.current = onSuccess
|
onSuccessRef.current = onSuccess
|
||||||
}, [id, onSuccess])
|
}, [id, onSuccess])
|
||||||
|
|
||||||
// 4. Operations with useCallback
|
// 4. Operations (useCallback with empty deps when using refs)
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
|
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
|
||||||
setData(result)
|
setData(result)
|
||||||
onSuccessRef.current?.(result)
|
onSuccessRef.current?.(result)
|
||||||
} catch (err) {
|
|
||||||
setError(err as Error)
|
|
||||||
logger.error('Failed', { error: err })
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, []) // Empty deps - using refs
|
}, [])
|
||||||
|
|
||||||
// 5. Return grouped by state/operations
|
return { data, isLoading, fetchData }
|
||||||
return { data, isLoading, error, fetchData }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. Single responsibility per hook
|
1. Single responsibility per hook
|
||||||
2. Props interface required
|
2. Props interface required
|
||||||
3. TSDoc required
|
3. Refs for stable callback dependencies
|
||||||
4. Use logger, not console.log
|
4. Wrap returned functions in useCallback
|
||||||
5. Refs for stable callback dependencies
|
5. Always try/catch async operations
|
||||||
6. Wrap returned functions in useCallback
|
6. Track loading/error states
|
||||||
7. Always try/catch async operations
|
|
||||||
8. Track loading/error states
|
|
||||||
|
|||||||
@@ -5,33 +5,45 @@ globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
|
|||||||
|
|
||||||
# Import Patterns
|
# Import Patterns
|
||||||
|
|
||||||
## EMCN Components
|
## Absolute Imports
|
||||||
Import from `@/components/emcn`, never from subpaths like `@/components/emcn/components/modal/modal`.
|
|
||||||
|
|
||||||
**Exception**: CSS imports use actual file paths: `import '@/components/emcn/components/code/code.css'`
|
**Always use absolute imports.** Never use relative imports.
|
||||||
|
|
||||||
## Feature Components
|
|
||||||
Import from central folder indexes, not specific subfolders:
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Correct
|
// ✓ Good
|
||||||
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
|
import { useWorkflowStore } from '@/stores/workflows/store'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
// ❌ Wrong
|
// ✗ Bad
|
||||||
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard'
|
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Internal vs External
|
## Barrel Exports
|
||||||
- **Cross-feature**: Absolute paths through central index
|
|
||||||
- **Within feature**: Relative paths (`./components/...`, `../utils`)
|
Use barrel exports (`index.ts`) when a folder has 3+ exports. Import from barrel, not individual files.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✓ Good
|
||||||
|
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
|
||||||
|
|
||||||
|
// ✗ Bad
|
||||||
|
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
|
||||||
|
```
|
||||||
|
|
||||||
## Import Order
|
## Import Order
|
||||||
|
|
||||||
1. React/core libraries
|
1. React/core libraries
|
||||||
2. External libraries
|
2. External libraries
|
||||||
3. UI components (`@/components/emcn`, `@/components/ui`)
|
3. UI components (`@/components/emcn`, `@/components/ui`)
|
||||||
4. Utilities (`@/lib/...`)
|
4. Utilities (`@/lib/...`)
|
||||||
5. Feature imports from indexes
|
5. Stores (`@/stores/...`)
|
||||||
6. Relative imports
|
6. Feature imports
|
||||||
7. CSS imports
|
7. CSS imports
|
||||||
|
|
||||||
## Types
|
## Type Imports
|
||||||
Use `type` keyword: `import type { WorkflowLog } from '...'`
|
|
||||||
|
Use `type` keyword for type-only imports:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { WorkflowLog } from '@/stores/logs/types'
|
||||||
|
```
|
||||||
|
|||||||
207
.cursor/rules/sim-integrations.mdc
Normal file
207
.cursor/rules/sim-integrations.mdc
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
description: Adding new integrations (tools, blocks, triggers)
|
||||||
|
globs: ["apps/sim/tools/**", "apps/sim/blocks/**", "apps/sim/triggers/**"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Adding Integrations
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Adding a new integration typically requires:
|
||||||
|
1. **Tools** - API operations (`tools/{service}/`)
|
||||||
|
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
|
||||||
|
3. **Icon** - SVG icon (`components/icons.tsx`)
|
||||||
|
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
|
||||||
|
|
||||||
|
Always look up the service's API docs first.
|
||||||
|
|
||||||
|
## 1. Tools (`tools/{service}/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/{service}/
|
||||||
|
├── index.ts # Export all tools
|
||||||
|
├── types.ts # Params/response types
|
||||||
|
├── {action}.ts # Individual tool (e.g., send_message.ts)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool file structure:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tools/{service}/{action}.ts
|
||||||
|
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
|
||||||
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
|
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
|
||||||
|
id: '{service}_{action}',
|
||||||
|
name: '{Service} {Action}',
|
||||||
|
description: 'What this tool does',
|
||||||
|
version: '1.0.0',
|
||||||
|
oauth: { required: true, provider: '{service}' }, // if OAuth
|
||||||
|
params: { /* param definitions */ },
|
||||||
|
request: {
|
||||||
|
url: '/api/tools/{service}/{action}',
|
||||||
|
method: 'POST',
|
||||||
|
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||||
|
body: (params) => ({ ...params }),
|
||||||
|
},
|
||||||
|
transformResponse: async (response) => {
|
||||||
|
const data = await response.json()
|
||||||
|
if (!data.success) throw new Error(data.error)
|
||||||
|
return { success: true, output: data.output }
|
||||||
|
},
|
||||||
|
outputs: { /* output definitions */ },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Register in `tools/registry.ts`:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { {service}{Action}Tool } from '@/tools/{service}'
|
||||||
|
// Add to registry object
|
||||||
|
{service}_{action}: {service}{Action}Tool,
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Block (`blocks/blocks/{service}.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { {Service}Icon } from '@/components/icons'
|
||||||
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import type { {Service}Response } from '@/tools/{service}/types'
|
||||||
|
|
||||||
|
export const {Service}Block: BlockConfig<{Service}Response> = {
|
||||||
|
type: '{service}',
|
||||||
|
name: '{Service}',
|
||||||
|
description: 'Short description',
|
||||||
|
longDescription: 'Detailed description',
|
||||||
|
category: 'tools',
|
||||||
|
bgColor: '#hexcolor',
|
||||||
|
icon: {Service}Icon,
|
||||||
|
subBlocks: [ /* see SubBlock Properties below */ ],
|
||||||
|
tools: {
|
||||||
|
access: ['{service}_{action}', ...],
|
||||||
|
config: {
|
||||||
|
tool: (params) => `{service}_${params.operation}`,
|
||||||
|
params: (params) => ({ ...params }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: { /* input definitions */ },
|
||||||
|
outputs: { /* output definitions */ },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SubBlock Properties
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'fieldName', // Unique identifier
|
||||||
|
title: 'Field Label', // UI label
|
||||||
|
type: 'short-input', // See SubBlock Types below
|
||||||
|
placeholder: 'Hint text',
|
||||||
|
required: true, // See Required below
|
||||||
|
condition: { ... }, // See Condition below
|
||||||
|
dependsOn: ['otherField'], // See DependsOn below
|
||||||
|
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
|
||||||
|
|
||||||
|
### `condition` - Show/hide based on another field
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Show when operation === 'send'
|
||||||
|
condition: { field: 'operation', value: 'send' }
|
||||||
|
|
||||||
|
// Show when operation is 'send' OR 'read'
|
||||||
|
condition: { field: 'operation', value: ['send', 'read'] }
|
||||||
|
|
||||||
|
// Show when operation !== 'send'
|
||||||
|
condition: { field: 'operation', value: 'send', not: true }
|
||||||
|
|
||||||
|
// Complex: NOT in list AND another condition
|
||||||
|
condition: {
|
||||||
|
field: 'operation',
|
||||||
|
value: ['list_channels', 'list_users'],
|
||||||
|
not: true,
|
||||||
|
and: { field: 'destinationType', value: 'dm', not: true }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `required` - Field validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Always required
|
||||||
|
required: true
|
||||||
|
|
||||||
|
// Conditionally required (same syntax as condition)
|
||||||
|
required: { field: 'operation', value: 'send' }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `dependsOn` - Clear field when dependencies change
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Clear when credential changes
|
||||||
|
dependsOn: ['credential']
|
||||||
|
|
||||||
|
// Clear when authMethod changes AND (credential OR botToken) changes
|
||||||
|
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mode` - When to show field
|
||||||
|
|
||||||
|
- `'basic'` - Only in basic mode (default UI)
|
||||||
|
- `'advanced'` - Only in advanced mode (manual input)
|
||||||
|
- `'both'` - Show in both modes (default)
|
||||||
|
- `'trigger'` - Only when block is used as trigger
|
||||||
|
|
||||||
|
**Register in `blocks/registry.ts`:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||||
|
// Add to registry object (alphabetically)
|
||||||
|
{service}: {Service}Block,
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Icon (`components/icons.tsx`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
{/* SVG path from service's brand assets */}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Trigger (`triggers/{service}/`) - Optional
|
||||||
|
|
||||||
|
```
|
||||||
|
triggers/{service}/
|
||||||
|
├── index.ts # Export all triggers
|
||||||
|
├── webhook.ts # Webhook handler
|
||||||
|
├── utils.ts # Shared utilities
|
||||||
|
└── {event}.ts # Specific event handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Register in `triggers/registry.ts`:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { {service}WebhookTrigger } from '@/triggers/{service}'
|
||||||
|
// Add to TRIGGER_REGISTRY
|
||||||
|
{service}_webhook: {service}WebhookTrigger,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Look up API docs for the service
|
||||||
|
- [ ] Create `tools/{service}/types.ts` with proper types
|
||||||
|
- [ ] Create tool files for each operation
|
||||||
|
- [ ] Create `tools/{service}/index.ts` barrel export
|
||||||
|
- [ ] Register tools in `tools/registry.ts`
|
||||||
|
- [ ] Add icon to `components/icons.tsx`
|
||||||
|
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||||
|
- [ ] Register block in `blocks/registry.ts`
|
||||||
|
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||||
|
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||||
66
.cursor/rules/sim-queries.mdc
Normal file
66
.cursor/rules/sim-queries.mdc
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
description: React Query patterns for the Sim application
|
||||||
|
globs: ["apps/sim/hooks/queries/**/*.ts"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# React Query Patterns
|
||||||
|
|
||||||
|
All React Query hooks live in `hooks/queries/`.
|
||||||
|
|
||||||
|
## Query Key Factory
|
||||||
|
|
||||||
|
Every query file defines a keys factory:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const entityKeys = {
|
||||||
|
all: ['entity'] as const,
|
||||||
|
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
|
||||||
|
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] as const,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Query keys factory
|
||||||
|
// 2. Types (if needed)
|
||||||
|
// 3. Private fetch functions
|
||||||
|
// 4. Exported hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: entityKeys.list(workspaceId),
|
||||||
|
queryFn: () => fetchEntities(workspaceId as string),
|
||||||
|
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mutation Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useCreateEntity() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (variables) => { /* fetch POST */ },
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimistic Updates
|
||||||
|
|
||||||
|
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- **Keys**: `entityKeys`
|
||||||
|
- **Query hooks**: `useEntity`, `useEntityList`
|
||||||
|
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
|
||||||
|
- **Fetch functions**: `fetchEntity` (private)
|
||||||
@@ -5,53 +5,66 @@ globs: ["apps/sim/**/store.ts", "apps/sim/**/stores/**/*.ts"]
|
|||||||
|
|
||||||
# Zustand Store Patterns
|
# Zustand Store Patterns
|
||||||
|
|
||||||
## Structure
|
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
|
||||||
|
|
||||||
|
## Basic Store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
import type { FeatureState } from '@/stores/feature/types'
|
||||||
|
|
||||||
|
const initialState = { items: [] as Item[], activeId: null as string | null }
|
||||||
|
|
||||||
|
export const useFeatureStore = create<FeatureState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
setItems: (items) => set({ items }),
|
||||||
|
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
|
||||||
|
reset: () => set(initialState),
|
||||||
|
}),
|
||||||
|
{ name: 'feature-store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persisted Store
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
interface FeatureState {
|
|
||||||
// State
|
|
||||||
items: Item[]
|
|
||||||
activeId: string | null
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
setItems: (items: Item[]) => void
|
|
||||||
addItem: (item: Item) => void
|
|
||||||
clearState: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInitialState = () => ({
|
|
||||||
items: [],
|
|
||||||
activeId: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useFeatureStore = create<FeatureState>()(
|
export const useFeatureStore = create<FeatureState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
...createInitialState(),
|
width: 300,
|
||||||
|
setWidth: (width) => set({ width }),
|
||||||
setItems: (items) => set({ items }),
|
_hasHydrated: false,
|
||||||
|
setHasHydrated: (v) => set({ _hasHydrated: v }),
|
||||||
addItem: (item) => set((state) => ({
|
|
||||||
items: [...state.items, item],
|
|
||||||
})),
|
|
||||||
|
|
||||||
clearState: () => set(createInitialState()),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'feature-state',
|
name: 'feature-state',
|
||||||
partialize: (state) => ({ items: state.items }),
|
partialize: (state) => ({ width: state.width }),
|
||||||
|
onRehydrateStorage: () => (state) => state?.setHasHydrated(true),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
1. Interface includes state and actions
|
|
||||||
2. Extract config to module constants
|
1. Use `devtools` middleware (named stores)
|
||||||
3. TSDoc on store
|
2. Use `persist` only when data should survive reload
|
||||||
4. Only persist what's needed
|
3. `partialize` to persist only necessary state
|
||||||
5. Immutable updates only - never mutate
|
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
|
||||||
6. Use `set((state) => ...)` when depending on previous state
|
5. Immutable updates only
|
||||||
7. Provide clear/reset actions
|
6. `set((state) => ...)` when depending on previous state
|
||||||
|
7. Provide `reset()` action
|
||||||
|
|
||||||
|
## Outside React
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const items = useFeatureStore.getState().items
|
||||||
|
useFeatureStore.setState({ items: newItems })
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
|
|||||||
# Styling Rules
|
# Styling Rules
|
||||||
|
|
||||||
## Tailwind
|
## Tailwind
|
||||||
1. **No inline styles** - Use Tailwind classes exclusively
|
|
||||||
2. **No duplicate dark classes** - Don't add `dark:` when value matches light mode
|
1. **No inline styles** - Use Tailwind classes
|
||||||
3. **Exact values** - Use design system values (`text-[14px]`, `h-[25px]`)
|
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
|
||||||
4. **Prefer px** - Use `px-[4px]` over `px-1`
|
3. **Exact values** - `text-[14px]`, `h-[25px]`
|
||||||
5. **Transitions** - Add `transition-colors` for interactive states
|
4. **Transitions** - `transition-colors` for interactive states
|
||||||
|
|
||||||
## Conditional Classes
|
## Conditional Classes
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -23,25 +24,17 @@ import { cn } from '@/lib/utils'
|
|||||||
)} />
|
)} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## CSS Variables for Dynamic Styles
|
## CSS Variables
|
||||||
|
|
||||||
|
For dynamic values (widths, heights) synced with stores:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// In store setter
|
// In store
|
||||||
setSidebarWidth: (width) => {
|
setWidth: (width) => {
|
||||||
set({ sidebarWidth: width })
|
set({ width })
|
||||||
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
|
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In component
|
// In component
|
||||||
<aside style={{ width: 'var(--sidebar-width)' }} />
|
<aside style={{ width: 'var(--sidebar-width)' }} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
```typescript
|
|
||||||
// ❌ Bad
|
|
||||||
<div style={{ width: 200 }}>
|
|
||||||
<div className='text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
|
||||||
|
|
||||||
// ✅ Good
|
|
||||||
<div className='w-[200px]'>
|
|
||||||
<div className='text-[var(--text-primary)]'>
|
|
||||||
```
|
|
||||||
|
|||||||
60
.cursor/rules/sim-testing.mdc
Normal file
60
.cursor/rules/sim-testing.mdc
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
description: Testing patterns with Vitest
|
||||||
|
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing Patterns
|
||||||
|
|
||||||
|
Use Vitest. Test files live next to source: `feature.ts` → `feature.test.ts`
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Tests for [feature name]
|
||||||
|
*
|
||||||
|
* @vitest-environment node
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1. Mocks BEFORE imports
|
||||||
|
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
|
||||||
|
vi.mock('@sim/logger', () => loggerMock)
|
||||||
|
|
||||||
|
// 2. Imports AFTER mocks
|
||||||
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { createSession, loggerMock } from '@sim/testing'
|
||||||
|
import { myFunction } from '@/lib/feature'
|
||||||
|
|
||||||
|
describe('myFunction', () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks())
|
||||||
|
|
||||||
|
it('should do something', () => {
|
||||||
|
expect(myFunction()).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('runs in parallel', () => { ... })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## @sim/testing Package
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Factories - create test data
|
||||||
|
import { createBlock, createWorkflow, createSession } from '@sim/testing'
|
||||||
|
|
||||||
|
// Mocks - pre-configured mocks
|
||||||
|
import { loggerMock, databaseMock, fetchMock } from '@sim/testing'
|
||||||
|
|
||||||
|
// Builders - fluent API for complex objects
|
||||||
|
import { ExecutionBuilder, WorkflowBuilder } from '@sim/testing'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. `@vitest-environment node` directive at file top
|
||||||
|
2. **Mocks before imports** - `vi.mock()` calls must come first
|
||||||
|
3. Use `@sim/testing` factories over manual test data
|
||||||
|
4. `it.concurrent` for independent tests (faster)
|
||||||
|
5. `beforeEach(() => vi.clearAllMocks())` to reset state
|
||||||
|
6. Group related tests with nested `describe` blocks
|
||||||
|
7. Test file naming: `*.test.ts` (not `*.spec.ts`)
|
||||||
@@ -6,19 +6,15 @@ globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
|
|||||||
# TypeScript Rules
|
# TypeScript Rules
|
||||||
|
|
||||||
1. **No `any`** - Use proper types or `unknown` with type guards
|
1. **No `any`** - Use proper types or `unknown` with type guards
|
||||||
2. **Props interface** - Always define, even for simple components
|
2. **Props interface** - Always define for components
|
||||||
3. **Callback types** - Full signature with params and return type
|
3. **Const assertions** - `as const` for constant objects/arrays
|
||||||
4. **Generics** - Use for reusable components/hooks
|
4. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
|
||||||
5. **Const assertions** - `as const` for constant objects/arrays
|
5. **Type imports** - `import type { X }` for type-only imports
|
||||||
6. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Bad
|
// ✗ Bad
|
||||||
const handleClick = (e: any) => {}
|
const handleClick = (e: any) => {}
|
||||||
useEffect(() => { doSomething(prop) }, []) // Missing dep
|
|
||||||
|
|
||||||
// ✅ Good
|
// ✓ Good
|
||||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
|
||||||
useEffect(() => { doSomething(prop) }, [prop])
|
|
||||||
```
|
```
|
||||||
|
|||||||
304
CLAUDE.md
304
CLAUDE.md
@@ -1,47 +1,295 @@
|
|||||||
# Expert Programming Standards
|
# Sim Studio Development Guidelines
|
||||||
|
|
||||||
**You are tasked with implementing solutions that follow best practices. You MUST be accurate, elegant, and efficient as an expert programmer.**
|
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||||
|
|
||||||
---
|
## Global Standards
|
||||||
|
|
||||||
# Role
|
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||||
|
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||||
|
- **Styling**: Never update global styles. Keep all styling local to components
|
||||||
|
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||||
|
|
||||||
You are a professional software engineer. All code you write MUST follow best practices, ensuring accuracy, quality, readability, and cleanliness. You MUST make FOCUSED EDITS that are EFFICIENT and ELEGANT.
|
## Architecture
|
||||||
|
|
||||||
## Logs
|
### Core Principles
|
||||||
|
1. Single Responsibility: Each component, hook, store has one clear purpose
|
||||||
|
2. Composition Over Complexity: Break down complex logic into smaller pieces
|
||||||
|
3. Type Safety First: TypeScript interfaces for all props, state, return types
|
||||||
|
4. Predictable State: Zustand for global state, useState for UI-only concerns
|
||||||
|
|
||||||
ENSURE that you use the logger.info and logger.warn and logger.error instead of the console.log whenever you want to display logs.
|
### Root Structure
|
||||||
|
```
|
||||||
|
apps/sim/
|
||||||
|
├── app/ # Next.js app router (pages, API routes)
|
||||||
|
├── blocks/ # Block definitions and registry
|
||||||
|
├── components/ # Shared UI (emcn/, ui/)
|
||||||
|
├── executor/ # Workflow execution engine
|
||||||
|
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||||
|
├── lib/ # App-wide utilities
|
||||||
|
├── providers/ # LLM provider integrations
|
||||||
|
├── stores/ # Zustand stores
|
||||||
|
├── tools/ # Tool definitions
|
||||||
|
└── triggers/ # Trigger definitions
|
||||||
|
```
|
||||||
|
|
||||||
## Comments
|
### Naming Conventions
|
||||||
|
- Components: PascalCase (`WorkflowList`)
|
||||||
|
- Hooks: `use` prefix (`useWorkflowOperations`)
|
||||||
|
- Files: kebab-case (`workflow-list.tsx`)
|
||||||
|
- Stores: `stores/feature/store.ts`
|
||||||
|
- Constants: SCREAMING_SNAKE_CASE
|
||||||
|
- Interfaces: PascalCase with suffix (`WorkflowListProps`)
|
||||||
|
|
||||||
You must use TSDOC for comments. Do not use ==== for comments to separate sections. Do not leave any comments that are not TSDOC.
|
## Imports
|
||||||
|
|
||||||
## Global Styles
|
**Always use absolute imports.** Never use relative imports.
|
||||||
|
|
||||||
You should not update the global styles unless it is absolutely necessary. Keep all styling local to components and files.
|
```typescript
|
||||||
|
// ✓ Good
|
||||||
|
import { useWorkflowStore } from '@/stores/workflows/store'
|
||||||
|
|
||||||
## Bun
|
// ✗ Bad
|
||||||
|
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||||
|
```
|
||||||
|
|
||||||
Use bun and bunx not npm and npx.
|
Use barrel exports (`index.ts`) when a folder has 3+ exports.
|
||||||
|
|
||||||
## Code Quality
|
### Import Order
|
||||||
|
1. React/core libraries
|
||||||
|
2. External libraries
|
||||||
|
3. UI components (`@/components/emcn`, `@/components/ui`)
|
||||||
|
4. Utilities (`@/lib/...`)
|
||||||
|
5. Stores (`@/stores/...`)
|
||||||
|
6. Feature imports
|
||||||
|
7. CSS imports
|
||||||
|
|
||||||
- Write clean, maintainable code that follows the project's existing patterns
|
Use `import type { X }` for type-only imports.
|
||||||
- Prefer composition over inheritance
|
|
||||||
- Keep functions small and focused on a single responsibility
|
## TypeScript
|
||||||
- Use meaningful variable and function names
|
|
||||||
- Handle errors gracefully and provide useful error messages
|
1. No `any` - Use proper types or `unknown` with type guards
|
||||||
- Write type-safe code with proper TypeScript types
|
2. Always define props interface for components
|
||||||
|
3. `as const` for constant objects/arrays
|
||||||
|
4. Explicit ref types: `useRef<HTMLDivElement>(null)`
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client' // Only if using hooks
|
||||||
|
|
||||||
|
const CONFIG = { SPACING: 8 } as const
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
requiredProp: string
|
||||||
|
optionalProp?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||||
|
// Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UseFeatureProps { id: string }
|
||||||
|
|
||||||
|
export function useFeature({ id }: UseFeatureProps) {
|
||||||
|
const idRef = useRef(id)
|
||||||
|
const [data, setData] = useState<Data | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => { idRef.current = id }, [id])
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
|
||||||
|
|
||||||
|
return { data, fetchData }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zustand Stores
|
||||||
|
|
||||||
|
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
const initialState = { items: [] as Item[] }
|
||||||
|
|
||||||
|
export const useFeatureStore = create<FeatureState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
setItems: (items) => set({ items }),
|
||||||
|
reset: () => set(initialState),
|
||||||
|
}),
|
||||||
|
{ name: 'feature-store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.
|
||||||
|
|
||||||
|
## React Query
|
||||||
|
|
||||||
|
All React Query hooks live in `hooks/queries/`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const entityKeys = {
|
||||||
|
all: ['entity'] as const,
|
||||||
|
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityList(workspaceId?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: entityKeys.list(workspaceId),
|
||||||
|
queryFn: () => fetchEntities(workspaceId as string),
|
||||||
|
enabled: Boolean(workspaceId),
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## EMCN Components
|
||||||
|
|
||||||
|
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Write tests for new functionality when appropriate
|
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||||
- Ensure existing tests pass before completing work
|
|
||||||
- Follow the project's testing conventions
|
|
||||||
|
|
||||||
## Performance
|
```typescript
|
||||||
|
/**
|
||||||
|
* @vitest-environment node
|
||||||
|
*/
|
||||||
|
|
||||||
- Consider performance implications of your code
|
// Mocks BEFORE imports
|
||||||
- Avoid unnecessary re-renders in React components
|
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
|
||||||
- Use appropriate data structures and algorithms
|
|
||||||
- Profile and optimize when necessary
|
// Imports AFTER mocks
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSession, loggerMock } from '@sim/testing'
|
||||||
|
|
||||||
|
describe('feature', () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks())
|
||||||
|
it.concurrent('runs in parallel', () => { ... })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `@sim/testing` factories over manual test data.
|
||||||
|
|
||||||
|
## Utils Rules
|
||||||
|
|
||||||
|
- Never create `utils.ts` for single consumer - inline it
|
||||||
|
- Create `utils.ts` when 2+ files need the same helper
|
||||||
|
- Check existing sources in `lib/` before duplicating
|
||||||
|
|
||||||
|
## Adding Integrations
|
||||||
|
|
||||||
|
New integrations require: **Tools** → **Block** → **Icon** → (optional) **Trigger**
|
||||||
|
|
||||||
|
Always look up the service's API docs first.
|
||||||
|
|
||||||
|
### 1. Tools (`tools/{service}/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/{service}/
|
||||||
|
├── index.ts # Barrel export
|
||||||
|
├── types.ts # Params/response types
|
||||||
|
└── {action}.ts # Tool implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool structure:**
|
||||||
|
```typescript
|
||||||
|
export const serviceTool: ToolConfig<Params, Response> = {
|
||||||
|
id: 'service_action',
|
||||||
|
name: 'Service Action',
|
||||||
|
description: '...',
|
||||||
|
version: '1.0.0',
|
||||||
|
oauth: { required: true, provider: 'service' },
|
||||||
|
params: { /* ... */ },
|
||||||
|
request: { url: '/api/tools/service/action', method: 'POST', ... },
|
||||||
|
transformResponse: async (response) => { /* ... */ },
|
||||||
|
outputs: { /* ... */ },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register in `tools/registry.ts`.
|
||||||
|
|
||||||
|
### 2. Block (`blocks/blocks/{service}.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const ServiceBlock: BlockConfig = {
|
||||||
|
type: 'service',
|
||||||
|
name: 'Service',
|
||||||
|
description: '...',
|
||||||
|
category: 'tools',
|
||||||
|
bgColor: '#hexcolor',
|
||||||
|
icon: ServiceIcon,
|
||||||
|
subBlocks: [ /* see SubBlock Properties */ ],
|
||||||
|
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
|
||||||
|
inputs: { /* ... */ },
|
||||||
|
outputs: { /* ... */ },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register in `blocks/registry.ts` (alphabetically).
|
||||||
|
|
||||||
|
**SubBlock Properties:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
|
||||||
|
required: true, // or condition object
|
||||||
|
condition: { field: 'op', value: 'send' }, // show/hide
|
||||||
|
dependsOn: ['credential'], // clear when dep changes
|
||||||
|
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**condition examples:**
|
||||||
|
- `{ field: 'op', value: 'send' }` - show when op === 'send'
|
||||||
|
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
|
||||||
|
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
|
||||||
|
- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex
|
||||||
|
|
||||||
|
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
||||||
|
|
||||||
|
### 3. Icon (`components/icons.tsx`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function ServiceIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return <svg {...props}>/* SVG from brand assets */</svg>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Trigger (`triggers/{service}/`) - Optional
|
||||||
|
|
||||||
|
```
|
||||||
|
triggers/{service}/
|
||||||
|
├── index.ts # Barrel export
|
||||||
|
├── webhook.ts # Webhook handler
|
||||||
|
└── {event}.ts # Event-specific handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
Register in `triggers/registry.ts`.
|
||||||
|
|
||||||
|
### Integration Checklist
|
||||||
|
|
||||||
|
- [ ] Look up API docs
|
||||||
|
- [ ] Create `tools/{service}/` with types and tools
|
||||||
|
- [ ] Register tools in `tools/registry.ts`
|
||||||
|
- [ ] Add icon to `components/icons.tsx`
|
||||||
|
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||||
|
- [ ] Register block in `blocks/registry.ts`
|
||||||
|
- [ ] (Optional) Create and register triggers
|
||||||
|
|||||||
Reference in New Issue
Block a user