diff --git a/ai-docs/patterns/mobx-patterns.md b/ai-docs/patterns/mobx-patterns.md new file mode 100644 index 000000000..1999297ae --- /dev/null +++ b/ai-docs/patterns/mobx-patterns.md @@ -0,0 +1,308 @@ +# MobX Patterns + +> Quick reference for LLMs working with MobX state management in this repository. + +--- + +## Rules + +- **MUST** use the singleton store pattern via `Store.getInstance()` +- **MUST** wrap widgets with `observer` HOC from `mobx-react-lite` +- **MUST** use `runInAction` for all state mutations +- **MUST** access store via `import store from '@webex/cc-store'` +- **MUST** mark state properties as `observable` +- **MUST** use `makeObservable` in store constructor +- **NEVER** mutate state outside of `runInAction` +- **NEVER** access store directly in presentational components +- **NEVER** create multiple store instances + +--- + +## Store Singleton Pattern + +```typescript +// store.ts +import { makeObservable, observable, action, runInAction } from 'mobx'; + +class Store { + private static instance: Store; + + // Observable state + @observable agentId: string = ''; + @observable currentState: string = ''; + @observable idleCodes: IdleCode[] = []; + @observable isLoggedIn: boolean = false; + + private constructor() { + makeObservable(this); + } + + static getInstance(): Store { + if (!Store.instance) { + Store.instance = new Store(); + } + return Store.instance; + } +} + +export default Store.getInstance(); +``` + +--- + +## Observable Decorator Pattern + +```typescript +import { observable, makeObservable } from 'mobx'; + +class Store { + @observable agentId: string = ''; + @observable teams: Team[] = []; + @observable currentState: string = 'Available'; + @observable tasks: ITask[] = []; + + constructor() { + makeObservable(this); + } +} +``` + +--- + +## runInAction Pattern + +**ALWAYS use runInAction for state mutations:** + +```typescript +import { runInAction } from 'mobx'; + +// ✅ CORRECT +const handleLogin = async () => { + const result = await cc.login(); + runInAction(() => { + store.agentId = result.agentId; + store.isLoggedIn = true; + store.teams = result.teams; + }); +}; + +// ❌ WRONG - Direct mutation +const handleLogin = async () => { + const result = await cc.login(); + store.agentId = result.agentId; // NOT ALLOWED +}; +``` + +--- + +## Observer HOC Pattern + +**ALWAYS wrap widgets that access store with observer:** + +```typescript +import { observer } from 'mobx-react-lite'; +import store from '@webex/cc-store'; + +const UserStateInternal: React.FC = observer((props) => { + // Access store - component will re-render when these change + const { currentState, idleCodes, agentId } = store; + + return ( + + ); +}); +``` + +--- + +## Store Import Pattern + +```typescript +// ✅ CORRECT - Import singleton +import store from '@webex/cc-store'; + +const MyWidget = observer(() => { + const { agentId, teams } = store; + // ... +}); + +// ❌ WRONG - Creating new instance +import { Store } from '@webex/cc-store'; +const store = new Store(); // NOT ALLOWED +``` + +--- + +## Action Pattern + +```typescript +import { action, makeObservable } from 'mobx'; + +class Store { + @observable currentState: string = ''; + + constructor() { + makeObservable(this); + } + + @action + setCurrentState(state: string) { + this.currentState = state; + } + + @action + reset() { + this.currentState = ''; + this.agentId = ''; + this.isLoggedIn = false; + } +} +``` + +--- + +## Computed Pattern + +```typescript +import { observable, computed, makeObservable } from 'mobx'; + +class Store { + @observable tasks: ITask[] = []; + + constructor() { + makeObservable(this); + } + + @computed + get activeTasks(): ITask[] { + return this.tasks.filter(task => task.status === 'active'); + } + + @computed + get taskCount(): number { + return this.tasks.length; + } +} +``` + +--- + +## Event Handling with Store Pattern + +```typescript +import { runInAction } from 'mobx'; +import store from '@webex/cc-store'; + +// In helper.ts or hook +useEffect(() => { + const handleTaskIncoming = (task: ITask) => { + runInAction(() => { + store.incomingTask = task; + }); + }; + + store.cc.on(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming); + + return () => { + store.cc.off(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming); + }; +}, []); +``` + +--- + +## Store Wrapper Pattern + +```typescript +// storeEventsWrapper.ts +import { runInAction } from 'mobx'; +import store from './store'; + +export const initStoreEventListeners = () => { + store.cc.on(CC_EVENTS.AGENT_STATE_CHANGED, (data) => { + runInAction(() => { + store.currentState = data.state; + store.lastStateChangeTimestamp = Date.now(); + }); + }); + + store.cc.on(CC_EVENTS.AGENT_LOGOUT_SUCCESS, () => { + runInAction(() => { + store.reset(); + }); + }); +}; +``` + +--- + +## Store Access in Widgets + +```typescript +// Widget file +import { observer } from 'mobx-react-lite'; +import store from '@webex/cc-store'; + +const StationLoginInternal = observer(() => { + // Destructure what you need from store + const { + cc, + teams, + dialNumbers, + isAgentLoggedIn, + loginConfig, + } = store; + + // Use in component + return ( + + ); +}); +``` + +--- + +## Async Action Pattern + +```typescript +import { runInAction } from 'mobx'; + +const fetchData = async () => { + // Set loading state + runInAction(() => { + store.isLoading = true; + store.error = null; + }); + + try { + const result = await store.cc.fetchTeams(); + + // Update with result + runInAction(() => { + store.teams = result.teams; + store.isLoading = false; + }); + } catch (error) { + // Handle error + runInAction(() => { + store.error = error; + store.isLoading = false; + }); + } +}; +``` + +--- + +## Related + +- [React Patterns](./react-patterns.md) +- [TypeScript Patterns](./typescript-patterns.md) +- [Testing Patterns](./testing-patterns.md) diff --git a/ai-docs/patterns/react-patterns.md b/ai-docs/patterns/react-patterns.md new file mode 100644 index 000000000..c55382d31 --- /dev/null +++ b/ai-docs/patterns/react-patterns.md @@ -0,0 +1,283 @@ +# React Patterns + +> Quick reference for LLMs working with React in this repository. + +--- + +## Rules + +- **MUST** use functional components with hooks (no class components) +- **MUST** wrap every widget with `ErrorBoundary` from `react-error-boundary` +- **MUST** use the three-layer pattern: Widget → Hook → Component +- **MUST** use `observer` HOC from `mobx-react-lite` for widgets that access store +- **MUST** keep presentational components in `cc-components` package +- **MUST** encapsulate business logic in custom hooks (`helper.ts`) +- **NEVER** access store directly in presentational components +- **NEVER** call SDK methods directly in components (use hooks) +- **NEVER** use class components + +--- + +## Three-Layer Architecture + +``` +┌─────────────────────────────────────┐ +│ Widget (observer) │ ← MobX observer, ErrorBoundary wrapper +│ packages/*/src/{widget}/index.tsx │ +├─────────────────────────────────────┤ +│ Custom Hook │ ← Business logic, SDK calls, events +│ packages/*/src/helper.ts │ +├─────────────────────────────────────┤ +│ Presentational Component │ ← Pure UI, props only +│ packages/cc-components/src/... │ +└─────────────────────────────────────┘ +``` + +--- + +## Error Boundary Pattern + +**ALWAYS wrap widgets with this pattern:** + +```typescript +import { ErrorBoundary } from 'react-error-boundary'; +import { observer } from 'mobx-react-lite'; +import store from '@webex/cc-store'; + +// Internal observer component +const UserStateInternal: React.FC = observer((props) => { + // Widget logic here + return ; +}); + +// External wrapper with ErrorBoundary +const UserState: React.FC = (props) => { + return ( + <>} + onError={(error: Error) => { + if (store.onErrorCallback) { + store.onErrorCallback('UserState', error); + } + }} + > + + + ); +}; + +export { UserState }; +``` + +--- + +## Custom Hook Pattern + +**ALWAYS encapsulate business logic in hooks:** + +```typescript +// helper.ts +export const useUserState = (props: UseUserStateProps) => { + const { cc, idleCodes, currentState, onStateChange } = props; + + const [selectedState, setSelectedState] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Event listener setup + useEffect(() => { + const handleStateChange = (data: StateChangeEvent) => { + setSelectedState(data.state); + onStateChange?.(data.state); + }; + + cc.on(CC_EVENTS.AGENT_STATE_CHANGED, handleStateChange); + + return () => { + cc.off(CC_EVENTS.AGENT_STATE_CHANGED, handleStateChange); + }; + }, [cc, onStateChange]); + + // Action handler + const handleSetState = useCallback(async (state: IdleCode) => { + setIsLoading(true); + try { + await cc.setAgentState(state); + } catch (error) { + console.error('Failed to set state:', error); + } finally { + setIsLoading(false); + } + }, [cc]); + + return { + selectedState, + isLoading, + handleSetState, + }; +}; +``` + +--- + +## Widget Pattern + +```typescript +// index.tsx +import { observer } from 'mobx-react-lite'; +import { ErrorBoundary } from 'react-error-boundary'; +import store from '@webex/cc-store'; +import { UserStateComponent } from '@webex/cc-components'; +import { useUserState } from '../helper'; +import { IUserStateProps } from './user-state.types'; + +const UserStateInternal: React.FC = observer((props) => { + const { onStateChange } = props; + + // Get data from store + const { cc, idleCodes, currentState, agentId } = store; + + // Use custom hook for logic + const { selectedState, isLoading, handleSetState } = useUserState({ + cc, + idleCodes, + currentState, + onStateChange, + }); + + // Render presentational component + return ( + + ); +}); + +const UserState: React.FC = (props) => ( + <>} + onError={(error) => store.onErrorCallback?.('UserState', error)} + > + + +); + +export { UserState }; +``` + +--- + +## Presentational Component Pattern + +```typescript +// cc-components/src/components/UserState/UserState.tsx +import React from 'react'; +import { IUserStateComponentProps } from './user-state.types'; + +export const UserStateComponent: React.FC = ({ + idleCodes, + currentState, + selectedState, + isLoading, + onStateSelect, +}) => { + return ( +
+ {idleCodes.map((code) => ( + + ))} +
+ ); +}; +``` + +--- + +## useEffect Cleanup Pattern + +**ALWAYS clean up event listeners and subscriptions:** + +```typescript +useEffect(() => { + const handler = (data: EventData) => { + // Handle event + }; + + cc.on(CC_EVENTS.SOME_EVENT, handler); + + // Cleanup function + return () => { + cc.off(CC_EVENTS.SOME_EVENT, handler); + }; +}, [cc]); +``` + +--- + +## useCallback Pattern + +**ALWAYS use useCallback for handlers passed to child components:** + +```typescript +const handleClick = useCallback((id: string) => { + // Handle click +}, [dependency1, dependency2]); +``` + +--- + +## Conditional Rendering Pattern + +```typescript +// Loading state +if (isLoading) { + return ; +} + +// Error state +if (error) { + return ; +} + +// Empty state +if (!data || data.length === 0) { + return ; +} + +// Normal render +return ; +``` + +--- + +## Props Destructuring Pattern + +```typescript +const Component: React.FC = ({ + prop1, + prop2, + optionalProp = 'default', + onCallback, +}) => { + // Component logic +}; +``` + +--- + +## Related + +- [TypeScript Patterns](./typescript-patterns.md) +- [MobX Patterns](./mobx-patterns.md) +- [Web Component Patterns](./web-component-patterns.md) +- [Testing Patterns](./testing-patterns.md) diff --git a/ai-docs/patterns/testing-patterns.md b/ai-docs/patterns/testing-patterns.md new file mode 100644 index 000000000..3b493d2df --- /dev/null +++ b/ai-docs/patterns/testing-patterns.md @@ -0,0 +1,319 @@ +# Testing Patterns + +> Quick reference for LLMs working with tests in this repository. + +--- + +## Rules + +- **MUST** use Jest for unit tests +- **MUST** use React Testing Library for component tests +- **MUST** use Playwright for E2E tests +- **MUST** mock the store using `@webex/cc-test-fixtures` +- **MUST** use `data-testid` attributes for test selectors +- **MUST** place unit tests in `tests/` folder within each package +- **MUST** place E2E tests in `playwright/` folder at repo root +- **NEVER** test implementation details - test behavior +- **NEVER** use CSS selectors in tests - use `data-testid` + +--- + +## Test File Structure + +``` +packages/contact-center/{package}/ +├── src/ +│ └── {widget}/ +│ └── index.tsx +└── tests/ + └── {widget}/ + └── index.test.tsx + +playwright/ +├── tests/ +│ ├── station-login-test.spec.ts +│ ├── user-state-test.spec.ts +│ └── tasklist-test.spec.ts +└── Utils/ + ├── stationLoginUtils.ts + └── userStateUtils.ts +``` + +--- + +## Jest Unit Test Pattern + +```typescript +// tests/{widget}/index.test.tsx +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { UserState } from '../../src/user-state'; +import { mockStore } from '@webex/cc-test-fixtures'; + +// Mock the store +jest.mock('@webex/cc-store', () => ({ + __esModule: true, + default: mockStore, +})); + +describe('UserState', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render idle codes', () => { + mockStore.idleCodes = [ + { id: '1', name: 'Available' }, + { id: '2', name: 'Break' }, + ]; + + render(); + + expect(screen.getByText('Available')).toBeInTheDocument(); + expect(screen.getByText('Break')).toBeInTheDocument(); + }); + + it('should call onStateChange when state is selected', async () => { + const onStateChange = jest.fn(); + mockStore.idleCodes = [{ id: '1', name: 'Available' }]; + + render(); + + fireEvent.click(screen.getByText('Available')); + + await waitFor(() => { + expect(onStateChange).toHaveBeenCalled(); + }); + }); +}); +``` + +--- + +## Mock Store Pattern + +```typescript +// test-fixtures/src/mockStore.ts +export const mockStore = { + cc: { + on: jest.fn(), + off: jest.fn(), + login: jest.fn(), + logout: jest.fn(), + setAgentState: jest.fn(), + }, + agentId: 'test-agent-123', + isAgentLoggedIn: false, + teams: [], + idleCodes: [], + currentState: 'Available', + onErrorCallback: jest.fn(), +}; + +// Usage in test +jest.mock('@webex/cc-store', () => ({ + __esModule: true, + default: mockStore, +})); +``` + +--- + +## Hook Testing Pattern + +```typescript +import { renderHook, act } from '@testing-library/react'; +import { useUserState } from '../../src/helper'; +import { mockStore } from '@webex/cc-test-fixtures'; + +describe('useUserState', () => { + it('should handle state change', async () => { + const onStateChange = jest.fn(); + + const { result } = renderHook(() => + useUserState({ + cc: mockStore.cc, + idleCodes: mockStore.idleCodes, + currentState: 'Available', + onStateChange, + }) + ); + + await act(async () => { + await result.current.handleSetState({ id: '1', name: 'Break' }); + }); + + expect(mockStore.cc.setAgentState).toHaveBeenCalled(); + }); +}); +``` + +--- + +## data-testid Pattern + +```typescript +// In component + + +
+ {/* content */} +
+ +// In test +const loginButton = screen.getByTestId('login-button'); +const dropdown = screen.getByTestId('user-state-dropdown'); +``` + +--- + +## Playwright E2E Test Pattern + +```typescript +// playwright/tests/station-login-test.spec.ts +import { test, expect } from '@playwright/test'; +import { StationLoginUtils } from '../Utils/stationLoginUtils'; + +test.describe('Station Login', () => { + let utils: StationLoginUtils; + + test.beforeEach(async ({ page }) => { + utils = new StationLoginUtils(page); + await utils.navigateToApp(); + }); + + test('should login successfully', async ({ page }) => { + await utils.selectTeam('Team A'); + await utils.selectDialNumber('+1234567890'); + await utils.clickLogin(); + + await expect(page.getByTestId('login-success')).toBeVisible(); + }); + + test('should show error on invalid credentials', async ({ page }) => { + await utils.clickLogin(); + + await expect(page.getByTestId('error-message')).toBeVisible(); + }); +}); +``` + +--- + +## Playwright Utils Pattern + +```typescript +// playwright/Utils/stationLoginUtils.ts +import { Page, Locator } from '@playwright/test'; + +export class StationLoginUtils { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async navigateToApp(): Promise { + await this.page.goto('/'); + } + + async selectTeam(teamName: string): Promise { + await this.page.getByTestId('team-dropdown').click(); + await this.page.getByText(teamName).click(); + } + + async selectDialNumber(number: string): Promise { + await this.page.getByTestId('dial-number-input').fill(number); + } + + async clickLogin(): Promise { + await this.page.getByTestId('login-button').click(); + } + + async waitForLoginSuccess(): Promise { + await this.page.waitForSelector('[data-testid="login-success"]'); + } +} +``` + +--- + +## Async Testing Pattern + +```typescript +// Using waitFor +await waitFor(() => { + expect(screen.getByText('Success')).toBeInTheDocument(); +}); + +// Using findBy (auto-waits) +const successMessage = await screen.findByText('Success'); +expect(successMessage).toBeInTheDocument(); + +// Using act for state updates +await act(async () => { + fireEvent.click(button); +}); +``` + +--- + +## Mock Event Pattern + +```typescript +// Mock event listener +const mockOn = jest.fn(); +const mockOff = jest.fn(); + +mockStore.cc = { + on: mockOn, + off: mockOff, +}; + +// Simulate event +const eventHandler = mockOn.mock.calls[0][1]; +act(() => { + eventHandler({ state: 'Break' }); +}); +``` + +--- + +## Snapshot Testing Pattern + +```typescript +it('should match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); +``` + +--- + +## Test Commands + +```bash +# Run all unit tests +yarn test + +# Run specific package tests +yarn workspace @webex/cc-station-login test + +# Run with coverage +yarn test --coverage + +# Run E2E tests +yarn test:e2e + +# Run specific E2E test +npx playwright test tests/station-login-test.spec.ts +``` + +--- + +## Related + +- [React Patterns](./react-patterns.md) +- [TypeScript Patterns](./typescript-patterns.md) +- [MobX Patterns](./mobx-patterns.md) diff --git a/ai-docs/patterns/typescript-patterns.md b/ai-docs/patterns/typescript-patterns.md new file mode 100644 index 000000000..c8dfd1890 --- /dev/null +++ b/ai-docs/patterns/typescript-patterns.md @@ -0,0 +1,216 @@ +# TypeScript Patterns + +> Quick reference for LLMs working with TypeScript in this repository. + +--- + +## Rules + +- **MUST** prefix all interfaces with `I` (e.g., `IUserState`, `IStationLoginProps`) +- **MUST** use PascalCase for components and interfaces +- **MUST** use camelCase for hooks with `use` prefix (e.g., `useUserState`) +- **MUST** use `.tsx` extension for components, `.ts` for hooks and utilities +- **MUST** co-locate types in `{component}.types.ts` files +- **MUST** use `Pick` and `Partial` to derive types from parent interfaces +- **MUST** document every interface property with JSDoc comments +- **MUST** use enums for event names and constants +- **NEVER** use `any` without ESLint disable comment and explanation +- **NEVER** duplicate type definitions - derive from source with `Pick` + +--- + +## Naming Conventions + +### Components +```typescript +// PascalCase, .tsx extension +UserState.tsx +StationLogin.tsx +CallControl.tsx +``` + +### Hooks +```typescript +// camelCase with 'use' prefix, .ts extension +useUserState.ts +useStationLogin.ts +useCallControl.ts +``` + +### Interfaces & Types +```typescript +// PascalCase with 'I' prefix +interface IUserState { ... } +interface IStationLoginProps { ... } +interface IContactCenter { ... } +``` + +### Constants +```typescript +// SCREAMING_SNAKE_CASE +const MAX_RETRY_COUNT = 3; +const DEFAULT_TIMEOUT = 5000; +``` + +### File Structure +``` +packages/*/src/{widget}/index.tsx # Widget entry +packages/*/src/helper.ts # Hooks/helpers +packages/*/src/{widget}/{widget}.types.ts # Types +``` + +--- + +## Import Patterns + +### Store Import +```typescript +import store from '@webex/cc-store'; +``` + +### Components Import +```typescript +import { Component } from '@webex/cc-components'; +``` + +### Types Import +```typescript +import { IUserState } from './user-state.types'; +``` + +### MobX Import +```typescript +import { observer } from 'mobx-react-lite'; +import { runInAction } from 'mobx'; +``` + +--- + +## Interface Patterns + +### Pattern 1: Interface with I Prefix +```typescript +interface IUserState { + idleCodes: IdleCode[]; + agentId: string; + cc: IContactCenter; + currentState: string; + onStateChange?: (arg: IdleCode | ICustomState) => void; +} +``` + +### Pattern 2: Use Pick to Derive Types +```typescript +// Widget picks only what it needs from component interface +export type IUserStateProps = Pick; + +// Hook picks different subset +export type UseUserStateProps = Pick< + IUserState, + 'idleCodes' | 'agentId' | 'cc' | 'currentState' | 'logger' +>; +``` + +### Pattern 3: Combine Pick with Partial +```typescript +// Required props + optional callback props +export type StationLoginProps = + Pick & + Partial>; +``` + +### Pattern 4: Union Types +```typescript +type ICustomState = ICustomStateSet | ICustomStateReset; +``` + +--- + +## Enum Patterns + +### Event Enums +```typescript +export enum TASK_EVENTS { + TASK_INCOMING = 'task:incoming', + TASK_ASSIGNED = 'task:assigned', + TASK_HOLD = 'task:hold', +} + +export enum CC_EVENTS { + AGENT_DN_REGISTERED = 'agent:dnRegistered', + AGENT_LOGOUT_SUCCESS = 'agent:logoutSuccess', +} +``` + +### State Enums +```typescript +export enum AgentUserState { + Available = 'Available', + RONA = 'RONA', + Engaged = 'ENGAGED', +} +``` + +--- + +## JSDoc Pattern + +```typescript +/** + * Interface representing the properties for the Station Login component. + */ +export interface IStationLoginProps { + /** + * Webex Contact Center instance. + */ + cc: IContactCenter; + + /** + * Array of teams the agent belongs to. + */ + teams: Team[]; + + /** + * Handler called when login completes. + */ + onLogin?: () => void; +} +``` + +--- + +## Type Export Pattern + +```typescript +// Central export from *.types.ts +export type { + IContactCenter, + ITask, + Profile, + Team, +}; + +export { + CC_EVENTS, + TASK_EVENTS, +}; +``` + +--- + +## Callback Type Pattern + +```typescript +// Optional callback with specific signature +onStateChange?: (arg: IdleCode | ICustomState) => void; +onLogin?: () => void; +onSaveEnd?: (isComplete: boolean) => void; +``` + +--- + +## Related + +- [React Patterns](./react-patterns.md) +- [MobX Patterns](./mobx-patterns.md) +- [Testing Patterns](./testing-patterns.md) diff --git a/ai-docs/patterns/web-component-patterns.md b/ai-docs/patterns/web-component-patterns.md new file mode 100644 index 000000000..6c8b84257 --- /dev/null +++ b/ai-docs/patterns/web-component-patterns.md @@ -0,0 +1,227 @@ +# Web Component Patterns + +> Quick reference for LLMs working with Web Components in this repository. + +--- + +## Rules + +- **MUST** use `r2wc` (React to Web Component) to wrap React widgets +- **MUST** register custom elements with `customElements.define()` +- **MUST** use kebab-case for custom element names (e.g., `cc-station-login`) +- **MUST** prefix all custom elements with `cc-` +- **MUST** define prop types in r2wc options +- **MUST** export Web Components from `cc-widgets` package only +- **NEVER** create Web Components directly - always wrap React components +- **NEVER** use camelCase for custom element names + +--- + +## r2wc Wrapper Pattern + +```typescript +// wc.ts in widget package +import r2wc from '@r2wc/react-to-web-component'; +import { StationLogin } from './station-login'; + +export const StationLoginWC = r2wc(StationLogin, { + props: { + profileMode: 'string', + onLogin: 'function', + onLogout: 'function', + onCCSignOut: 'function', + }, +}); +``` + +--- + +## Custom Element Registration Pattern + +```typescript +// cc-widgets/src/index.ts +import { StationLoginWC } from '@webex/cc-station-login/wc'; +import { UserStateWC } from '@webex/cc-user-state/wc'; + +// Register custom elements +customElements.define('cc-station-login', StationLoginWC); +customElements.define('cc-user-state', UserStateWC); +customElements.define('cc-incoming-task', IncomingTaskWC); +customElements.define('cc-task-list', TaskListWC); +customElements.define('cc-call-control', CallControlWC); +``` + +--- + +## Prop Type Mapping + +```typescript +// Map React prop types to r2wc types +const WidgetWC = r2wc(Widget, { + props: { + // String props + profileMode: 'string', + agentId: 'string', + + // Boolean props + isEnabled: 'boolean', + showHeader: 'boolean', + + // Number props + timeout: 'number', + maxRetries: 'number', + + // Function props (callbacks) + onLogin: 'function', + onLogout: 'function', + onError: 'function', + + // Object/Array props (passed as JSON string) + config: 'json', + teams: 'json', + }, +}); +``` + +--- + +## Widget Package wc.ts Structure + +```typescript +// packages/contact-center/{widget}/src/wc.ts +import r2wc from '@r2wc/react-to-web-component'; +import { WidgetName } from './{widget}'; + +export const WidgetNameWC = r2wc(WidgetName, { + props: { + // Define all props that should be exposed to Web Component + prop1: 'string', + prop2: 'boolean', + onCallback: 'function', + }, +}); +``` + +--- + +## HTML Usage Pattern + +```html + + + + + + + +``` + +--- + +## JavaScript Usage Pattern + +```javascript +// Create element programmatically +const stationLogin = document.createElement('cc-station-login'); +stationLogin.setAttribute('profile-mode', 'desktop'); + +// Set callback props +stationLogin.onLogin = () => { + console.log('Login successful'); +}; + +stationLogin.onLogout = () => { + console.log('Logged out'); +}; + +// Append to DOM +document.getElementById('container').appendChild(stationLogin); +``` + +--- + +## Attribute Naming Convention + +```typescript +// React prop → HTML attribute +profileMode → profile-mode +onStateChange → on-state-change (or handled via JS property) +isEnabled → is-enabled +maxRetries → max-retries +``` + +--- + +## cc-widgets Package Structure + +``` +packages/contact-center/cc-widgets/ +├── src/ +│ ├── index.ts # Custom element registration +│ └── wc.ts # Aggregated exports +├── package.json +└── tsconfig.json +``` + +--- + +## Full Widget to Web Component Flow + +``` +React Widget (packages/{widget}/src/{widget}/index.tsx) + ↓ +r2wc Wrapper (packages/{widget}/src/wc.ts) + ↓ +Custom Element Registration (packages/cc-widgets/src/index.ts) + ↓ +HTML Usage () +``` + +--- + +## Example: Complete Web Component Setup + +### 1. Widget Package (station-login/src/wc.ts) +```typescript +import r2wc from '@r2wc/react-to-web-component'; +import { StationLogin } from './station-login'; + +export const StationLoginWC = r2wc(StationLogin, { + props: { + profileMode: 'string', + teamId: 'string', + onLogin: 'function', + onLogout: 'function', + onCCSignOut: 'function', + onSaveStart: 'function', + onSaveEnd: 'function', + }, +}); +``` + +### 2. CC-Widgets Registration (cc-widgets/src/index.ts) +```typescript +import { StationLoginWC } from '@webex/cc-station-login/wc'; + +customElements.define('cc-station-login', StationLoginWC); +``` + +### 3. HTML Usage +```html + + +``` + +--- + +## Related + +- [React Patterns](./react-patterns.md) +- [TypeScript Patterns](./typescript-patterns.md)