diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md new file mode 100644 index 0000000000..011a3280f4 --- /dev/null +++ b/.claude/rules/emcn-components.md @@ -0,0 +1,35 @@ +--- +paths: + - "apps/sim/components/emcn/**" +--- + +# EMCN Components + +Import from `@/components/emcn`, never from subpaths (except CSS files). + +## CVA vs Direct Styles + +**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg) + +```tsx +const buttonVariants = cva('base-classes', { + variants: { variant: { default: '...', primary: '...' } } +}) +export { Button, buttonVariants } +``` + +**Use direct className when:** Single consistent style, no variations + +```tsx +function Label({ className, ...props }) { + return +} +``` + +## Rules + +- Use Radix UI primitives for accessibility +- Export component and variants (if using CVA) +- TSDoc with usage examples +- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]` +- `transition-colors` for hover states diff --git a/.claude/rules/global.md b/.claude/rules/global.md new file mode 100644 index 0000000000..e749b67b28 --- /dev/null +++ b/.claude/rules/global.md @@ -0,0 +1,13 @@ +# Global Standards + +## 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`. diff --git a/.claude/rules/sim-architecture.md b/.claude/rules/sim-architecture.md new file mode 100644 index 0000000000..d6d7197972 --- /dev/null +++ b/.claude/rules/sim-architecture.md @@ -0,0 +1,56 @@ +--- +paths: + - "apps/sim/**" +--- + +# Sim App Architecture + +## 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 + +## 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/ +├── components/ # Feature components +├── hooks/ # Feature-scoped hooks +├── utils/ # Feature-scoped utilities (2+ consumers) +├── feature.tsx # Main component +└── page.tsx # Next.js page entry +``` + +## 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`) + +## Utils Rules + +- **Never create `utils.ts` for single consumer** - inline it +- **Create `utils.ts` when** 2+ files need the same helper +- **Check existing sources** before duplicating (`lib/` has many utilities) +- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use) diff --git a/.claude/rules/sim-components.md b/.claude/rules/sim-components.md new file mode 100644 index 0000000000..23799bcda0 --- /dev/null +++ b/.claude/rules/sim-components.md @@ -0,0 +1,48 @@ +--- +paths: + - "apps/sim/**/*.tsx" +--- + +# Component Patterns + +## Structure Order + +```typescript +'use client' // Only if using hooks + +// Imports (external → internal) +// Constants at module level +const CONFIG = { SPACING: 8 } as const + +// Props interface +interface ComponentProps { + requiredProp: string + optionalProp?: boolean +} + +export function Component({ requiredProp, optionalProp = false }: ComponentProps) { + // a. Refs + // b. External hooks (useParams, useRouter) + // c. Store hooks + // d. Custom hooks + // e. Local state + // f. useMemo + // g. useCallback + // h. useEffect + // i. Return JSX +} +``` + +## Rules + +1. `'use client'` only when using React hooks +2. Always define props interface +3. Extract constants with `as const` +4. Semantic HTML (`aside`, `nav`, `article`) +5. Optional chain callbacks: `onAction?.(id)` + +## Component Extraction + +**Extract when:** 50+ lines, used in 2+ files, or has own state/logic + +**Keep inline when:** < 10 lines, single use, purely presentational diff --git a/.claude/rules/sim-hooks.md b/.claude/rules/sim-hooks.md new file mode 100644 index 0000000000..3c06a4a310 --- /dev/null +++ b/.claude/rules/sim-hooks.md @@ -0,0 +1,55 @@ +--- +paths: + - "apps/sim/**/use-*.ts" + - "apps/sim/**/hooks/**/*.ts" +--- + +# Hook Patterns + +## Structure + +```typescript +interface UseFeatureProps { + id: string + onSuccess?: (result: Result) => void +} + +export function useFeature({ id, onSuccess }: UseFeatureProps) { + // 1. Refs for stable dependencies + const idRef = useRef(id) + const onSuccessRef = useRef(onSuccess) + + // 2. State + const [data, setData] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + // 3. Sync refs + useEffect(() => { + idRef.current = id + onSuccessRef.current = onSuccess + }, [id, onSuccess]) + + // 4. Operations (useCallback with empty deps when using refs) + const fetchData = useCallback(async () => { + setIsLoading(true) + try { + const result = await fetch(`/api/${idRef.current}`).then(r => r.json()) + setData(result) + onSuccessRef.current?.(result) + } finally { + setIsLoading(false) + } + }, []) + + return { data, isLoading, fetchData } +} +``` + +## Rules + +1. Single responsibility per hook +2. Props interface required +3. Refs for stable callback dependencies +4. Wrap returned functions in useCallback +5. Always try/catch async operations +6. Track loading/error states diff --git a/.claude/rules/sim-imports.md b/.claude/rules/sim-imports.md new file mode 100644 index 0000000000..b1f1926cd9 --- /dev/null +++ b/.claude/rules/sim-imports.md @@ -0,0 +1,62 @@ +--- +paths: + - "apps/sim/**/*.ts" + - "apps/sim/**/*.tsx" +--- + +# Import Patterns + +## Absolute Imports + +**Always use absolute imports.** Never use relative imports. + +```typescript +// ✓ Good +import { useWorkflowStore } from '@/stores/workflows/store' +import { Button } from '@/components/ui/button' + +// ✗ Bad +import { useWorkflowStore } from '../../../stores/workflows/store' +``` + +## Barrel Exports + +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' +``` + +## No Re-exports + +Do not re-export from non-barrel files. Import directly from the source. + +```typescript +// ✓ Good - import from where it's declared +import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' + +// ✗ Bad - re-exporting in utils.ts then importing from there +import { CORE_TRIGGER_TYPES } from '@/app/workspace/.../utils' +``` + +## 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 + +## Type Imports + +Use `type` keyword for type-only imports: + +```typescript +import type { WorkflowLog } from '@/stores/logs/types' +``` diff --git a/.claude/rules/sim-integrations.md b/.claude/rules/sim-integrations.md new file mode 100644 index 0000000000..cef0c895bd --- /dev/null +++ b/.claude/rules/sim-integrations.md @@ -0,0 +1,209 @@ +--- +paths: + - "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) { + return ( + + {/* SVG path from service's brand assets */} + + ) +} +``` + +## 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` diff --git a/.claude/rules/sim-queries.md b/.claude/rules/sim-queries.md new file mode 100644 index 0000000000..0ca91ac263 --- /dev/null +++ b/.claude/rules/sim-queries.md @@ -0,0 +1,66 @@ +--- +paths: + - "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) diff --git a/.claude/rules/sim-stores.md b/.claude/rules/sim-stores.md new file mode 100644 index 0000000000..333ff9fd91 --- /dev/null +++ b/.claude/rules/sim-stores.md @@ -0,0 +1,71 @@ +--- +paths: + - "apps/sim/**/store.ts" + - "apps/sim/**/stores/**/*.ts" +--- + +# Zustand Store Patterns + +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()( + 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 +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export const useFeatureStore = create()( + persist( + (set) => ({ + width: 300, + setWidth: (width) => set({ width }), + _hasHydrated: false, + setHasHydrated: (v) => set({ _hasHydrated: v }), + }), + { + name: 'feature-state', + partialize: (state) => ({ width: state.width }), + onRehydrateStorage: () => (state) => state?.setHasHydrated(true), + } + ) +) +``` + +## Rules + +1. Use `devtools` middleware (named stores) +2. Use `persist` only when data should survive reload +3. `partialize` to persist only necessary state +4. `_hasHydrated` pattern for persisted stores needing hydration tracking +5. Immutable updates only +6. `set((state) => ...)` when depending on previous state +7. Provide `reset()` action + +## Outside React + +```typescript +const items = useFeatureStore.getState().items +useFeatureStore.setState({ items: newItems }) +``` diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md new file mode 100644 index 0000000000..1b8c384a70 --- /dev/null +++ b/.claude/rules/sim-styling.md @@ -0,0 +1,41 @@ +--- +paths: + - "apps/sim/**/*.tsx" + - "apps/sim/**/*.css" +--- + +# Styling Rules + +## Tailwind + +1. **No inline styles** - Use Tailwind classes +2. **No duplicate dark classes** - Skip `dark:` when value matches light mode +3. **Exact values** - `text-[14px]`, `h-[26px]` +4. **Transitions** - `transition-colors` for interactive states + +## Conditional Classes + +```typescript +import { cn } from '@/lib/utils' + +
+``` + +## CSS Variables + +For dynamic values (widths, heights) synced with stores: + +```typescript +// In store +setWidth: (width) => { + set({ width }) + document.documentElement.style.setProperty('--sidebar-width', `${width}px`) +} + +// In component +