|
| 1 | +# React Sample App - Widget Integration Guide |
| 2 | + |
| 3 | +## Purpose |
| 4 | + |
| 5 | +Demonstrates how to integrate Contact Center widgets into a React application. Use this as a reference when adding new widgets. |
| 6 | + |
| 7 | +## Design System |
| 8 | + |
| 9 | +- **Framework:** React 18.3.1 + TypeScript |
| 10 | +- **UI Library:** Momentum Design System (`@momentum-design/components`) |
| 11 | +- **State:** MobX store (`@webex/cc-store`) |
| 12 | +- **Theme:** Dark/Light mode toggle (persisted in localStorage) |
| 13 | +- **Layout:** CSS Grid with `.box`, `.section-box`, `.fieldset` structure |
| 14 | + |
| 15 | +## Critical Integration Pattern |
| 16 | + |
| 17 | +**Follow this exact pattern for ALL new widgets:** |
| 18 | + |
| 19 | +### Step 1: Import the Widget |
| 20 | + |
| 21 | +```tsx |
| 22 | +import {NewWidget} from '@webex/cc-widgets'; |
| 23 | +``` |
| 24 | + |
| 25 | +### Step 2: Add to defaultWidgets |
| 26 | + |
| 27 | +```tsx |
| 28 | +const defaultWidgets = { |
| 29 | + stationLogin: true, |
| 30 | + userState: true, |
| 31 | + // ... existing widgets |
| 32 | + newWidget: false, // ← Add here (false by default for user opt-in) |
| 33 | +}; |
| 34 | +``` |
| 35 | + |
| 36 | +### Step 3: Add Checkbox for Widget Selection |
| 37 | + |
| 38 | +```tsx |
| 39 | +<Checkbox |
| 40 | + onChange={handleCheckboxChange} |
| 41 | + name="newWidget" |
| 42 | + checked={selectedWidgets.newWidget} |
| 43 | + htmlId="newWidget-checkbox" |
| 44 | + aria-label="new widget checkbox" |
| 45 | +> |
| 46 | + <Text>New Widget</Text> |
| 47 | +</Checkbox> |
| 48 | +``` |
| 49 | + |
| 50 | +### Step 4: Conditional Rendering with Standard Layout |
| 51 | + |
| 52 | +```tsx |
| 53 | +{ |
| 54 | + selectedWidgets.newWidget && ( |
| 55 | + <div className="box"> |
| 56 | + <section className="section-box"> |
| 57 | + <fieldset className="fieldset"> |
| 58 | + <legend className="legend-box">New Widget</legend> |
| 59 | + <NewWidget onEvent={(data) => handleNewWidgetEvent(data)} onError={(error) => onError('NewWidget', error)} /> |
| 60 | + </fieldset> |
| 61 | + </section> |
| 62 | + </div> |
| 63 | + ); |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +### Step 5: Add callbacks (if needed) |
| 68 | + |
| 69 | +```tsx |
| 70 | +const handleNewWidgetCallback = (data) => { |
| 71 | + console.log('New widget callback:', data); |
| 72 | + // Handle callback logic |
| 73 | +}; |
| 74 | +``` |
| 75 | + |
| 76 | +## Layout Structure Rules |
| 77 | + |
| 78 | +### Container Hierarchy (ALWAYS use this) |
| 79 | + |
| 80 | +```tsx |
| 81 | +<div className="box"> |
| 82 | + {' '} |
| 83 | + {/* Outer container with background */} |
| 84 | + <section className="section-box"> |
| 85 | + {' '} |
| 86 | + {/* Inner section with padding */} |
| 87 | + <fieldset className="fieldset"> |
| 88 | + {' '} |
| 89 | + {/* Fieldset for grouping */} |
| 90 | + <legend className="legend-box">Title</legend> {/* Title/legend */} |
| 91 | + <WidgetComponent /> {/* Actual widget */} |
| 92 | + </fieldset> |
| 93 | + </section> |
| 94 | +</div> |
| 95 | +``` |
| 96 | + |
| 97 | +### Why this structure? |
| 98 | + |
| 99 | +- `box` - Consistent spacing and background |
| 100 | +- `section-box` - Momentum Design padding |
| 101 | +- `fieldset` - Semantic grouping |
| 102 | +- `legend-box` - Styled title |
| 103 | +- **Result:** Visual consistency across all widgets |
| 104 | + |
| 105 | +## Styling Rules |
| 106 | + |
| 107 | +### CSS Variables (MUST USE) |
| 108 | + |
| 109 | +```scss |
| 110 | +// Colors |
| 111 | +var(--mds-color-theme-text-primary-normal) |
| 112 | +var(--mds-color-theme-background-solid-primary-normal) |
| 113 | +var(--mds-color-theme-background-primary-normal) |
| 114 | + |
| 115 | +// Spacing |
| 116 | +var(--mds-spacing-1) // 0.25rem (4px) |
| 117 | +var(--mds-spacing-2) // 0.5rem (8px) |
| 118 | +var(--mds-spacing-3) // 1rem (16px) |
| 119 | +var(--mds-spacing-4) // 1.5rem (24px) |
| 120 | + |
| 121 | +// Typography |
| 122 | +var(--mds-font-size-body-small) |
| 123 | +var(--mds-font-size-body-medium) |
| 124 | +``` |
| 125 | + |
| 126 | +### ❌ NEVER Do This |
| 127 | + |
| 128 | +```tsx |
| 129 | +<div style={{color: '#FF0000'}}> // Hardcoded color |
| 130 | +<div style={{padding: '10px'}}> // Hardcoded spacing |
| 131 | +``` |
| 132 | + |
| 133 | +### ✅ ALWAYS Do This |
| 134 | + |
| 135 | +```tsx |
| 136 | +<div style={{ |
| 137 | + color: 'var(--mds-color-theme-text-primary-normal)', |
| 138 | + padding: 'var(--mds-spacing-3)' |
| 139 | +}}> |
| 140 | +``` |
| 141 | + |
| 142 | +## Event Handling Pattern |
| 143 | + |
| 144 | +### Standard onError Handler |
| 145 | + |
| 146 | +```tsx |
| 147 | +const onError = (widgetName: string, error: Error) => { |
| 148 | + console.error(`${widgetName} error:`, error); |
| 149 | + // Optional: Show toast notification |
| 150 | + setToast({type: 'error'}); |
| 151 | +}; |
| 152 | +``` |
| 153 | + |
| 154 | +**EVERY widget MUST have onError callback.** |
| 155 | + |
| 156 | +### Widget-Specific Events |
| 157 | + |
| 158 | +```tsx |
| 159 | +// IncomingTask |
| 160 | +const onIncomingTaskCB = ({task}) => { |
| 161 | + console.log('Incoming task:', task); |
| 162 | + setIncomingTasks((prev) => [...prev, task]); |
| 163 | + playNotificationSound(); // Custom logic |
| 164 | +}; |
| 165 | + |
| 166 | +// UserState |
| 167 | +const onAgentStateChangedCB = (newState: AgentState, oldState: AgentState) => { |
| 168 | + console.log('State changed from', oldState, 'to', newState); |
| 169 | + setSelectedState(newState); |
| 170 | +}; |
| 171 | + |
| 172 | +// CallControl |
| 173 | +const onRecordingToggleCB = ({isRecording, task}) => { |
| 174 | + console.log('Recording:', isRecording, 'for task:', task.data.interactionId); |
| 175 | +}; |
| 176 | +``` |
| 177 | + |
| 178 | +## Theme Integration |
| 179 | + |
| 180 | +Theme is controlled by `@momentum-ui`'s `ThemeProvider`: |
| 181 | + |
| 182 | +```tsx |
| 183 | +import {ThemeProvider} from '@momentum-design/components/dist/react'; |
| 184 | + |
| 185 | +<ThemeProvider |
| 186 | + themeclass={store.currentTheme === 'LIGHT' ? 'mds-theme-stable-lightWebex' : 'mds-theme-stable-darkWebex'} |
| 187 | +> |
| 188 | + {/* Your widgets */} |
| 189 | +</ThemeProvider>; |
| 190 | +``` |
| 191 | + |
| 192 | +- **Theme provider**: `@momentum-ui` library manages the theme through `ThemeProvider` |
| 193 | +- **Theme storage**: `store.currentTheme` stores the theme as a string ('LIGHT' or 'DARK') |
| 194 | +- **Theme classes**: Theme class names (`mds-theme-stable-lightWebex` / `mds-theme-stable-darkWebex`) are passed to ThemeProvider |
| 195 | +- **Automatic updates**: All widgets automatically respond to theme changes through the ThemeProvider context |
| 196 | + |
| 197 | +**User can toggle theme via UI checkbox** - widgets update automatically through the provider. |
| 198 | + |
| 199 | +## State Management |
| 200 | + |
| 201 | +### When to Use store Directly |
| 202 | + |
| 203 | +```tsx |
| 204 | +// Access store for global state |
| 205 | +import {store} from '@webex/cc-widgets'; |
| 206 | + |
| 207 | +// Examples: |
| 208 | +store.currentTask; // Current active task |
| 209 | +store.taskList; // All tasks |
| 210 | +store.incomingTask; // Incoming task |
| 211 | +store.agentState; // Current agent state |
| 212 | +``` |
| 213 | + |
| 214 | +### When to Use Local State |
| 215 | + |
| 216 | +```tsx |
| 217 | +// UI-only state (no widget dependency) |
| 218 | +const [showPopup, setShowPopup] = useState(false); |
| 219 | +const [selectedOption, setSelectedOption] = useState(''); |
| 220 | +``` |
| 221 | + |
| 222 | +## Complete Example: Adding a New Widget |
| 223 | + |
| 224 | +```tsx |
| 225 | +// 1. Import |
| 226 | +import {NewAwesomeWidget} from '@webex/cc-widgets'; |
| 227 | + |
| 228 | +// 2. Add to defaultWidgets |
| 229 | +const defaultWidgets = { |
| 230 | + // ... existing |
| 231 | + newAwesomeWidget: false, |
| 232 | +}; |
| 233 | + |
| 234 | +// 3. Checkbox in widget selector |
| 235 | +<Checkbox |
| 236 | + onChange={handleCheckboxChange} |
| 237 | + name="newAwesomeWidget" |
| 238 | + checked={selectedWidgets.newAwesomeWidget} |
| 239 | + htmlId="newAwesomeWidget-checkbox" |
| 240 | +> |
| 241 | + <Text>New Awesome Widget</Text> |
| 242 | +</Checkbox>; |
| 243 | + |
| 244 | +// 4. Setup callback handler (if needed) |
| 245 | +const handleCallback = (data) => { |
| 246 | + console.log('Callback:', data); |
| 247 | +}; |
| 248 | + |
| 249 | +// 5. Render with standard layout |
| 250 | +{ |
| 251 | + selectedWidgets.newAwesomeWidget && ( |
| 252 | + <div className="box"> |
| 253 | + <section className="section-box"> |
| 254 | + <fieldset className="fieldset"> |
| 255 | + <legend className="legend-box">New Awesome Widget</legend> |
| 256 | + <NewAwesomeWidget |
| 257 | + handleCallback={handleCallback} |
| 258 | + onError={(error) => onError('NewAwesomeWidget', error)} |
| 259 | + customProp={someValue} |
| 260 | + /> |
| 261 | + </fieldset> |
| 262 | + </section> |
| 263 | + </div> |
| 264 | + ); |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +## Common Mistakes to AVOID |
| 269 | + |
| 270 | +### ❌ Breaking CSS class structure |
| 271 | + |
| 272 | +```tsx |
| 273 | +// WRONG |
| 274 | +<div> |
| 275 | + <NewWidget /> |
| 276 | +</div> |
| 277 | +``` |
| 278 | + |
| 279 | +### ✅ Correct |
| 280 | + |
| 281 | +```tsx |
| 282 | +<div className="box"> |
| 283 | + <section className="section-box"> |
| 284 | + <fieldset className="fieldset"> |
| 285 | + <legend className="legend-box">Widget Name</legend> |
| 286 | + <NewWidget /> |
| 287 | + </fieldset> |
| 288 | + </section> |
| 289 | +</div> |
| 290 | +``` |
| 291 | + |
| 292 | +### ❌ Forgetting defaultWidgets entry |
| 293 | + |
| 294 | +```tsx |
| 295 | +// WRONG - Widget renders immediately, user can't disable |
| 296 | +{ |
| 297 | + selectedWidgets.newWidget && <NewWidget />; |
| 298 | +} |
| 299 | +// But newWidget not in defaultWidgets! |
| 300 | +``` |
| 301 | + |
| 302 | +### ✅ Correct |
| 303 | + |
| 304 | +```tsx |
| 305 | +// In defaultWidgets |
| 306 | +const defaultWidgets = { |
| 307 | + newWidget: false, // ← MUST ADD HERE |
| 308 | +}; |
| 309 | + |
| 310 | +// Then render |
| 311 | +{ |
| 312 | + selectedWidgets.newWidget && <NewWidget />; |
| 313 | +} |
| 314 | +``` |
| 315 | + |
| 316 | +### ❌ Missing error handler |
| 317 | + |
| 318 | +```tsx |
| 319 | +// WRONG |
| 320 | +<NewWidget onEvent={handleEvent} /> |
| 321 | +``` |
| 322 | + |
| 323 | +### ✅ Correct |
| 324 | + |
| 325 | +```tsx |
| 326 | +<NewWidget onEvent={handleEvent} onError={(error) => onError('NewWidget', error)} /> |
| 327 | +``` |
| 328 | + |
| 329 | +### ❌ Hardcoding colors |
| 330 | + |
| 331 | +```tsx |
| 332 | +// WRONG |
| 333 | +<div style={{backgroundColor: '#1a1a1a'}}> |
| 334 | +``` |
| 335 | + |
| 336 | +### ✅ Correct |
| 337 | + |
| 338 | +```tsx |
| 339 | +<div style={{backgroundColor: 'var(--mds-color-theme-background-primary-normal)'}}> |
| 340 | +``` |
| 341 | + |
| 342 | +## Testing Checklist |
| 343 | + |
| 344 | +After adding a new widget: |
| 345 | + |
| 346 | +- [ ] Widget imports without errors |
| 347 | +- [ ] Appears in widget selector checkbox list |
| 348 | +- [ ] Can be enabled/disabled via checkbox |
| 349 | +- [ ] Selection persists in localStorage |
| 350 | +- [ ] Renders with correct layout (box > section-box > fieldset) |
| 351 | +- [ ] Has legend/title |
| 352 | +- [ ] Uses Momentum CSS variables (no hardcoded colors) |
| 353 | +- [ ] Callbacks are handled correctly |
| 354 | +- [ ] onError handler present and logs errors |
| 355 | +- [ ] Works in both light and dark themes |
| 356 | +- [ ] No console errors when enabled/disabled |
| 357 | +- [ ] No visual/layout breaking when rendered alongside other widgets |
| 358 | + |
| 359 | +## File Locations |
| 360 | + |
| 361 | +- **Main App:** `src/App.tsx` |
| 362 | +- **Styles:** `src/App.scss` |
| 363 | + |
| 364 | +## Additional Resources |
| 365 | + |
| 366 | +- [MobX Store Package](../../packages/contact-center/store/ai-docs/agent.md) |
| 367 | +- [cc-widgets Package](../../packages/contact-center/cc-widgets/ai-docs/agent.md) |
0 commit comments