generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

,\n # Responsive sizing: slds-small-size_, slds-medium-size_, slds-large-size_\n r'^slds-(small|medium|large|max-small|max-medium|max-large)-size_\\d+-of-\\d+

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

,\n r'^slds-text-(heading|body|color|align)_',\n r'^slds-grid(_|$)',\n r'^slds-col(_|$)',\n r'^slds-button(_|$)',\n r'^slds-input(_|$)',\n r'^slds-form(_|$)',\n r'^slds-card(_|$)',\n r'^slds-modal(_|$)',\n r'^slds-notify(_|$)',\n r'^slds-illustration(_|$)',\n r'^slds-table(_|$)',\n r'^slds-box(_|$)',\n r'^slds-badge(_|$)',\n r'^slds-spinner(_|$)',\n r'^slds-alert(_|$)',\n # Utility patterns\n r'^slds-has-',\n r'^slds-no-',\n r'^slds-var-',\n r'^slds-is-',\n r'^slds-theme_',\n r'^slds-icon(_|$)',\n r'^slds-media(_|$)',\n r'^slds-list(_|$)',\n r'^slds-tile(_|$)',\n r'^slds-popover(_|$)',\n r'^slds-dropdown(_|$)',\n r'^slds-tabs_',\n r'^slds-path(_|$)',\n r'^slds-progress(_|$)',\n ]\n return any(re.match(p, cls) for p in patterns)\n\n def _check_accessibility(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check accessibility requirements in HTML.\"\"\"\n content = self.content\n\n # Check lightning-icon without alternative-text\n icon_pattern = r'\u003clightning-icon[^>]*>'\n for i, line in enumerate(self.lines, 1):\n for match in re.finditer(icon_pattern, line):\n icon_tag = match.group(0)\n if 'alternative-text' not in icon_tag:\n scores['accessibility'] = max(0, scores['accessibility'] - 3)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'accessibility',\n 'message': 'lightning-icon missing alternative-text attribute',\n 'line': i,\n 'fix': 'Add alternative-text=\"description\" for screen readers'\n })\n\n # Check lightning-button-icon without label\n button_icon_pattern = r'\u003clightning-button-icon[^>]*>'\n for i, line in enumerate(self.lines, 1):\n for match in re.finditer(button_icon_pattern, line):\n tag = match.group(0)\n if 'aria-label' not in tag and 'alternative-text' not in tag:\n scores['accessibility'] = max(0, scores['accessibility'] - 3)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'accessibility',\n 'message': 'lightning-button-icon missing aria-label or alternative-text',\n 'line': i,\n 'fix': 'Add aria-label=\"action description\" for accessibility'\n })\n\n # Check for slds-assistive-text usage (good practice)\n if 'slds-assistive-text' not in content and 'aria-live' not in content:\n # Only deduct if there's dynamic content indicators\n if '{' in content and '}' in content:\n scores['accessibility'] = max(0, scores['accessibility'] - 2)\n issues.append({\n 'severity': 'INFO',\n 'category': 'accessibility',\n 'message': 'Consider adding aria-live for dynamic content updates',\n 'line': 0,\n 'fix': 'Use aria-live=\"polite\" for status updates'\n })\n\n def _check_component_structure(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check component structure for SLDS compliance.\"\"\"\n # Check for lightning-* base components (good)\n lightning_components = re.findall(r'\u003clightning-[a-z-]+', self.content)\n if not lightning_components:\n scores['component_structure'] = max(0, scores['component_structure'] - 5)\n issues.append({\n 'severity': 'INFO',\n 'category': 'component_structure',\n 'message': 'No lightning-* base components found',\n 'line': 0,\n 'fix': 'Consider using lightning-* components for SLDS consistency'\n })\n\n # ═══════════════════════════════════════════════════════════════════════\n # CSS VALIDATION\n # ═══════════════════════════════════════════════════════════════════════\n\n def _validate_css(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Validate CSS file.\"\"\"\n self._check_dark_mode(scores, issues)\n self._check_styling_hooks(scores, issues)\n self._check_slds_migration(scores, issues)\n self._check_css_performance(scores, issues)\n\n def _check_dark_mode(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for dark mode compatibility (no hardcoded colors).\"\"\"\n # Patterns for hardcoded colors\n color_patterns = [\n (r'#[0-9A-Fa-f]{3,8}(?![0-9A-Fa-f])', 'hex color'),\n (r'rgb\\s*\\([^)]+\\)', 'RGB color'),\n (r'rgba\\s*\\([^)]+\\)', 'RGBA color'),\n (r'hsl\\s*\\([^)]+\\)', 'HSL color'),\n (r'hsla\\s*\\([^)]+\\)', 'HSLA color'),\n ]\n\n for i, line in enumerate(self.lines, 1):\n # Skip comments\n if line.strip().startswith('/*') or line.strip().startswith('//'):\n continue\n\n # Skip if it's inside a var() - that's allowed\n line_without_vars = re.sub(r'var\\s*\\([^)]+\\)', '', line)\n\n for pattern, color_type in color_patterns:\n matches = re.findall(pattern, line_without_vars)\n for match in matches:\n # Skip transparent and common exceptions\n if match.lower() in ['#fff', '#ffffff', '#000', '#000000']:\n scores['dark_mode'] = max(0, scores['dark_mode'] - 5)\n issues.append({\n 'severity': 'HIGH',\n 'category': 'dark_mode',\n 'message': f'Hardcoded {color_type} ({match}) breaks dark mode',\n 'line': i,\n 'fix': f'Use var(--slds-g-color-*) instead of {match}'\n })\n elif match.lower() not in ['transparent', 'inherit', 'currentcolor']:\n scores['dark_mode'] = max(0, scores['dark_mode'] - 3)\n issues.append({\n 'severity': 'MODERATE',\n 'category': 'dark_mode',\n 'message': f'Hardcoded {color_type} ({match}) may break dark mode',\n 'line': i,\n 'fix': f'Consider using var(--slds-g-color-*) instead'\n })\n\n def _check_styling_hooks(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for proper SLDS 2 styling hooks usage.\"\"\"\n # Find all CSS variable references\n var_pattern = r'var\\s*\\(\\s*(--[a-zA-Z0-9-]+)'\n\n for i, line in enumerate(self.lines, 1):\n matches = re.findall(var_pattern, line)\n for var_name in matches:\n # Check if it's an SLDS variable\n if var_name.startswith('--slds-'):\n # SLDS 2 global hooks use --slds-g-\n if var_name.startswith('--slds-c-'):\n scores['styling_hooks'] = max(0, scores['styling_hooks'] - 3)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'styling_hooks',\n 'message': f'Component hooks ({var_name}) not yet supported in SLDS 2',\n 'line': i,\n 'fix': 'Use --slds-g-* global hooks or wait for SLDS 2 component hook support'\n })\n elif not var_name.startswith('--slds-g-'):\n scores['styling_hooks'] = max(0, scores['styling_hooks'] - 2)\n issues.append({\n 'severity': 'INFO',\n 'category': 'styling_hooks',\n 'message': f'Non-standard SLDS variable: {var_name}',\n 'line': i,\n 'fix': 'Use --slds-g-* for SLDS 2 compatibility'\n })\n\n def _check_slds_migration(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for deprecated SLDS 1 patterns.\"\"\"\n deprecated_tokens = self.deprecated_patterns.get('tokens', {})\n deprecated_classes = self.deprecated_patterns.get('classes', {})\n\n for i, line in enumerate(self.lines, 1):\n # Check deprecated Sass tokens\n for old_token, replacement in deprecated_tokens.items():\n if old_token in line:\n scores['slds_migration'] = max(0, scores['slds_migration'] - 5)\n issues.append({\n 'severity': 'HIGH',\n 'category': 'slds_migration',\n 'message': f'Deprecated SLDS 1 token: {old_token}',\n 'line': i,\n 'fix': f'Replace with {replacement}'\n })\n\n # Check --lwc- prefix (old format)\n if '--lwc-' in line:\n scores['slds_migration'] = max(0, scores['slds_migration'] - 3)\n issues.append({\n 'severity': 'MODERATE',\n 'category': 'slds_migration',\n 'message': 'Old --lwc-* token format detected',\n 'line': i,\n 'fix': 'Migrate to --slds-g-* styling hooks'\n })\n\n def _check_css_performance(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for CSS performance issues.\"\"\"\n for i, line in enumerate(self.lines, 1):\n # Check for !important\n if '!important' in line:\n scores['performance'] = max(0, scores['performance'] - 3)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'performance',\n 'message': '!important override detected',\n 'line': i,\n 'fix': 'Avoid !important; use more specific selectors or SLDS utilities'\n })\n\n # Check for overly deep selectors (> 3 levels)\n selector_pattern = r'^[^{]+{'\n for i, line in enumerate(self.lines, 1):\n if '{' in line:\n # Count selector depth\n selector = line.split('{')[0]\n depth = len(re.findall(r'\\s+', selector.strip()))\n if depth > 3:\n scores['performance'] = max(0, scores['performance'] - 2)\n issues.append({\n 'severity': 'INFO',\n 'category': 'performance',\n 'message': 'Deep CSS selector detected (>3 levels)',\n 'line': i,\n 'fix': 'Simplify selector for better performance'\n })\n\n # ═══════════════════════════════════════════════════════════════════════\n # JAVASCRIPT VALIDATION\n # ═══════════════════════════════════════════════════════════════════════\n\n def _validate_js(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Validate JavaScript controller file.\"\"\"\n # JS files have limited SLDS-specific validation\n # Focus on inline styles, classList manipulation, GraphQL, and focus management\n\n for i, line in enumerate(self.lines, 1):\n # Check for inline style manipulation with colors\n if '.style.' in line and any(c in line.lower() for c in ['color', 'background', 'border']):\n if re.search(r'#[0-9A-Fa-f]{3,8}|rgb\\s*\\(', line):\n scores['dark_mode'] = max(0, scores['dark_mode'] - 5)\n issues.append({\n 'severity': 'HIGH',\n 'category': 'dark_mode',\n 'message': 'Inline style with hardcoded color detected',\n 'line': i,\n 'fix': 'Use CSS classes or CSS variables instead of inline styles'\n })\n\n # Check for classList with invalid SLDS classes\n if 'classList' in line and 'slds-' in line:\n # Extract class names from string literals\n classes = re.findall(r'[\"\\']([slds-][^\"\\']+)[\"\\']', line)\n for cls in classes:\n if cls.startswith('slds-') and self.valid_slds_classes and cls not in self.valid_slds_classes:\n if not self._is_valid_slds_pattern(cls):\n scores['slds_class_usage'] = max(0, scores['slds_class_usage'] - 2)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'slds_class_usage',\n 'message': f\"Unknown SLDS class in JS: {cls}\",\n 'line': i,\n 'fix': f\"Verify '{cls}' is a valid SLDS 2 class\"\n })\n\n # Check GraphQL patterns\n self._check_graphql_patterns(scores, issues)\n\n # Check focus management patterns\n self._check_focus_management(scores, issues)\n\n def _check_graphql_patterns(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for proper GraphQL wire adapter usage.\"\"\"\n content = self.content\n\n # Check if using GraphQL wire adapter\n has_graphql_import = 'lightning/uiGraphQLApi' in content\n has_gql_import = \"import { gql\" in content or \"import { graphql\" in content\n\n if has_graphql_import or has_gql_import:\n # Verify proper patterns\n\n # Check for storing wire result for refresh\n if '@wire(graphql' in content or '@wire(gql' in content:\n # Check if result is stored for refreshGraphQL\n if 'refreshGraphQL' in content:\n # Good pattern - using refreshGraphQL\n pass\n else:\n # Check if the wire is used with a function that stores result\n wire_pattern = r'@wire\\s*\\(\\s*graphql[^)]*\\)\\s*(\\w+)'\n matches = re.findall(wire_pattern, content)\n for match in matches:\n # If it's a function pattern, check if it stores result\n if f'wired{match[0].upper()}' not in content and 'Result' not in match:\n scores['graphql_patterns'] = max(0, scores['graphql_patterns'] - 5)\n issues.append({\n 'severity': 'INFO',\n 'category': 'graphql_patterns',\n 'message': 'GraphQL wire result not stored for potential refresh',\n 'line': 0,\n 'fix': 'Store wire result in a property for use with refreshGraphQL()'\n })\n\n # Check for cursor-based pagination pattern\n if 'first:' in content and 'after:' not in content and 'pageInfo' not in content:\n scores['graphql_patterns'] = max(0, scores['graphql_patterns'] - 3)\n issues.append({\n 'severity': 'INFO',\n 'category': 'graphql_patterns',\n 'message': 'GraphQL query uses first: but missing pagination',\n 'line': 0,\n 'fix': 'Add pageInfo { hasNextPage endCursor } for cursor pagination'\n })\n\n # Check for proper error handling\n if 'graphQLErrors' not in content and '.errors' not in content:\n scores['graphql_patterns'] = max(0, scores['graphql_patterns'] - 2)\n issues.append({\n 'severity': 'INFO',\n 'category': 'graphql_patterns',\n 'message': 'GraphQL error handling not detected',\n 'line': 0,\n 'fix': 'Handle graphQLErrors in wire result or catch block'\n })\n\n def _check_focus_management(self, scores: Dict[str, int], issues: List[Dict]):\n \"\"\"Check for proper focus management patterns in modals/dialogs.\"\"\"\n content = self.content\n\n # Check if this appears to be a modal component\n is_modal = any(indicator in content.lower() for indicator in [\n 'modal', 'dialog', 'overlay', 'popup', 'backdrop'\n ])\n\n if is_modal:\n # Check for ESC key handler\n has_esc_handler = any(pattern in content for pattern in [\n \"'Escape'\", '\"Escape\"', 'key === 27', 'keyCode === 27',\n \"code === 'Escape'\", 'code === \"Escape\"'\n ])\n\n if not has_esc_handler:\n scores['focus_management'] = max(0, scores['focus_management'] - 3)\n issues.append({\n 'severity': 'WARNING',\n 'category': 'focus_management',\n 'message': 'Modal component missing ESC key handler',\n 'line': 0,\n 'fix': \"Add window.addEventListener('keyup', handler) to close on Escape\"\n })\n\n # Check for focus trap pattern\n has_focus_trap = any(pattern in content for pattern in [\n 'focusable', 'tabbable', '.focus()', 'tabindex',\n 'querySelectorAll', 'firstElementChild'\n ])\n\n if not has_focus_trap:\n scores['focus_management'] = max(0, scores['focus_management'] - 3)\n issues.append({\n 'severity': 'INFO',\n 'category': 'focus_management',\n 'message': 'Modal may need focus trap for accessibility',\n 'line': 0,\n 'fix': 'Implement focus trap to keep focus within modal'\n })\n\n # Check for cleanup in disconnectedCallback\n if 'addEventListener' in content:\n if 'removeEventListener' not in content and 'disconnectedCallback' not in content:\n scores['focus_management'] = max(0, scores['focus_management'] - 4)\n issues.append({\n 'severity': 'HIGH',\n 'category': 'focus_management',\n 'message': 'Event listener added but not cleaned up',\n 'line': 0,\n 'fix': 'Add disconnectedCallback to remove event listeners'\n })\n\n\nif __name__ == \"__main__\":\n import sys\n if len(sys.argv) \u003c 2:\n print(\"Usage: python validate_slds.py \u003cfile.html|file.css|file.js>\")\n sys.exit(1)\n\n validator = SLDSValidator(sys.argv[1])\n results = validator.validate()\n print(json.dumps(results, indent=2))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27160,"content_sha256":"2271558829ccde522615475ef37d52e9d4e2647db06aa103a8c089fc34e98c3e"},{"filename":"README.md","content":"# generating-lwc-components\n\nLightning Web Components development skill with PICKLES architecture methodology, 165-point scoring, SLDS 2 compliance, and dark mode support. Build modern Salesforce UIs.\n\n## Features\n\n- **Component Scaffolding**: Generate complete LWC bundles (JS, HTML, CSS, meta.xml)\n- **PICKLES Architecture**: Structured methodology for robust components\n- **165-Point Scoring**: Validation across 8 categories (SLDS 2 + Dark Mode)\n- **Wire Service Patterns**: @wire decorators for Apex & GraphQL\n- **Jest Testing**: Comprehensive unit test generation\n- **Spring '26 Features**: TypeScript, lwc:on, Complex Expressions\n\n## Quick Start\n\n### 1. Invoke the skill\n\n```\nSkill: generating-lwc-components\nRequest: \"Create a data table component for Account records\"\n```\n\n### 2. Answer requirements questions\n\nThe skill will ask about:\n- Component purpose\n- Data source (LDS, Apex, GraphQL)\n- Target (App Page, Record Page, Flow Screen)\n- Accessibility requirements\n\n### 3. Review generated component\n\nThe skill generates:\n- JavaScript controller with decorators\n- HTML template with SLDS styling\n- CSS with styling hooks (dark mode ready)\n- meta.xml configuration\n- Jest test file\n\n## PICKLES Framework\n\n```\nP → Prototype │ Validate ideas with wireframes & mock data\nI → Integrate │ Choose data source (LDS, Apex, GraphQL, API)\nC → Composition │ Structure component hierarchy & communication\nK → Kinetics │ Handle user interactions & event flow\nL → Libraries │ Leverage platform APIs & base components\nE → Execution │ Optimize performance & lifecycle hooks\nS → Security │ Enforce permissions, FLS, and data protection\n```\n\n## Scoring System (165 Points)\n\n| Category | Points | Focus |\n|----------|--------|-------|\n| Component Structure | 25 | File organization, naming |\n| Data Layer | 25 | Wire service, error handling |\n| UI/UX | 25 | SLDS 2, responsiveness, dark mode |\n| Accessibility | 20 | WCAG, ARIA, keyboard navigation |\n| Testing | 20 | Jest coverage, async patterns |\n| Performance | 20 | Lazy loading, debouncing |\n| Events | 15 | Component communication |\n| Security | 15 | FLS, permissions |\n\n## Templates\n\n| Template | Use Case |\n|----------|----------|\n| `basic-component/` | Simple component starter |\n| `graphql-component/` | GraphQL data binding |\n| `flow-screen-component/` | Flow screen integration |\n| `typescript-component/` | TypeScript support (Spring '26) |\n\n## Cross-Skill Integration\n\n| Related Skill | When to Use |\n|---------------|-------------|\n| generating-apex | Create @AuraEnabled controllers |\n| generating-flow | Embed in Flow screens |\n| generating-metadata | Create Lightning Message Channels |\n| deploying-metadata | Deploy component to org |\n\n## Spring '26 Features (API 66.0)\n\n- **lwc:on directive**: Dynamic event binding from JavaScript\n- **GraphQL Mutations**: executeMutation for create/update/delete\n- **Complex Expressions**: JS expressions in templates (Beta)\n- **TypeScript Support**: @salesforce/lightning-types package\n- **Agentforce Discovery**: lightning__agentforce capability\n\n## Documentation\n\n- [Best Practices](references/lwc-best-practices.md)\n- [Flow Integration](references/flow-integration-guide.md)\n- [Accessibility Guide](references/accessibility-guide.md)\n- [Jest Testing](references/jest-testing.md)\n\n## Jest Configuration for SFDX Projects\n\nIf your project uses Jest, scope discovery to your Salesforce source so example assets in agent folders are not picked up during `npm test`.\n\n```javascript\nconst { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');\n\nmodule.exports = {\n ...jestConfig,\n roots: ['\u003crootDir>/force-app'],\n testPathIgnorePatterns: [\n '\u003crootDir>/node_modules/',\n '\u003crootDir>/.sfdx/',\n '\u003crootDir>/.agents/',\n '\u003crootDir>/.cursor/',\n '\u003crootDir>/.claude/',\n '\u003crootDir>/.pi/'\n ]\n};\n```\n\nExample test assets in this skill now ship with a `.example` suffix. Copy them into your project and rename them before running Jest.\n\n## Requirements\n\n- sf CLI v2\n- Node.js 18+ (for Jest tests)\n- Target Salesforce org\n- API Version 66.0+ (Spring '26)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4144,"content_sha256":"e1e9622952ed62193d98ead33b96989e639e7bc0a11f6c8295e1a6783dba15f7"},{"filename":"references/accessibility-guide.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Accessibility Guide for LWC\n\nComprehensive guide to building WCAG 2.1 AA compliant Lightning Web Components.\n\n---\n\n## Table of Contents\n\n1. [Accessibility Standards](#accessibility-standards)\n2. [Semantic HTML](#semantic-html)\n3. [ARIA Attributes](#aria-attributes)\n4. [Keyboard Navigation](#keyboard-navigation)\n5. [Focus Management](#focus-management)\n6. [Color and Contrast](#color-and-contrast)\n7. [Screen Reader Support](#screen-reader-support)\n8. [Live Regions](#live-regions)\n9. [Form Accessibility](#form-accessibility)\n10. [Common Patterns](#common-patterns)\n11. [Testing](#testing)\n12. [Tools and Resources](#tools-and-resources)\n\n---\n\n## Accessibility Standards\n\n### WCAG 2.1 AA Compliance\n\nAll Lightning Web Components should meet WCAG 2.1 Level AA standards.\n\n| Principle | Description |\n|-----------|-------------|\n| **Perceivable** | Information must be presentable to users in ways they can perceive |\n| **Operable** | UI components must be operable (keyboard, mouse, voice) |\n| **Understandable** | Information and UI must be understandable |\n| **Robust** | Content must work with assistive technologies |\n\n### Key Requirements\n\n| Requirement | Standard | Implementation |\n|-------------|----------|----------------|\n| **Color contrast** | 4.5:1 for normal text, 3:1 for large text | Use SLDS color tokens |\n| **Keyboard navigation** | All interactive elements accessible via keyboard | Tab order, Enter/Space triggers |\n| **Screen reader support** | ARIA labels, roles, live regions | Proper semantic HTML + ARIA |\n| **Focus indicators** | Visible focus state | Use SLDS focus utilities |\n| **Alternative text** | All images have alt text | `alt` attribute on images |\n\n---\n\n## Semantic HTML\n\n### Use Proper HTML Elements\n\n```html\n\u003c!-- BAD: Non-semantic markup -->\n\u003cdiv onclick={handleClick}>Click me\u003c/div>\n\n\u003c!-- GOOD: Semantic button -->\n\u003cbutton onclick={handleClick}>Click me\u003c/button>\n```\n\n### Headings Hierarchy\n\n```html\n\u003c!-- BAD: Skipping heading levels -->\n\u003ch1>Page Title\u003c/h1>\n\u003ch3>Subsection\u003c/h3> \u003c!-- Skipped h2 -->\n\n\u003c!-- GOOD: Logical heading structure -->\n\u003ch1>Page Title\u003c/h1>\n\u003ch2>Main Section\u003c/h2>\n\u003ch3>Subsection\u003c/h3>\n```\n\n### Landmarks\n\n```html\n\u003ctemplate>\n \u003cheader class=\"slds-page-header\">\n \u003ch1>Dashboard\u003c/h1>\n \u003c/header>\n\n \u003cnav aria-label=\"Primary navigation\">\n \u003cul>\n \u003cli>\u003ca href=\"#home\">Home\u003c/a>\u003c/li>\n \u003cli>\u003ca href=\"#accounts\">Accounts\u003c/a>\u003c/li>\n \u003c/ul>\n \u003c/nav>\n\n \u003cmain>\n \u003carticle>\n \u003ch2>Account Details\u003c/h2>\n \u003c!-- Content -->\n \u003c/article>\n \u003c/main>\n\n \u003caside aria-label=\"Related information\">\n \u003c!-- Sidebar content -->\n \u003c/aside>\n\n \u003cfooter>\n \u003cp>Copyright 2025\u003c/p>\n \u003c/footer>\n\u003c/template>\n```\n\n---\n\n## ARIA Attributes\n\n### ARIA Labels\n\n```html\n\u003c!-- Icon button without visible text -->\n\u003cbutton aria-label=\"Delete record\" onclick={handleDelete}>\n \u003clightning-icon icon-name=\"utility:delete\" size=\"small\">\u003c/lightning-icon>\n\u003c/button>\n\n\u003c!-- Form field with additional context -->\n\u003clightning-input\n label=\"Phone\"\n aria-describedby=\"phone-help\"\n value={phone}\n onchange={handlePhoneChange}>\n\u003c/lightning-input>\n\u003cdiv id=\"phone-help\" class=\"slds-text-color_weak\">\n Enter phone number with country code\n\u003c/div>\n```\n\n### ARIA Roles\n\n```html\n\u003c!-- Custom list -->\n\u003cdiv role=\"list\">\n \u003cdiv role=\"listitem\">Item 1\u003c/div>\n \u003cdiv role=\"listitem\">Item 2\u003c/div>\n\u003c/div>\n\n\u003c!-- Alert message -->\n\u003cdiv role=\"alert\" class=\"slds-notify slds-notify_alert\">\n \u003cspan class=\"slds-assistive-text\">Error\u003c/span>\n \u003cp>Form validation failed\u003c/p>\n\u003c/div>\n\n\u003c!-- Dialog/Modal -->\n\u003cdiv role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"modal-heading\"\n aria-describedby=\"modal-description\">\n \u003ch2 id=\"modal-heading\">Confirm Action\u003c/h2>\n \u003cp id=\"modal-description\">Are you sure you want to delete this record?\u003c/p>\n\u003c/div>\n```\n\n### ARIA States\n\n```html\n\u003c!-- Expandable section -->\n\u003cbutton\n aria-expanded={isExpanded}\n aria-controls=\"details-section\"\n onclick={toggleExpanded}>\n Show Details\n\u003c/button>\n\u003cdiv id=\"details-section\" class={sectionClass}>\n \u003c!-- Details content -->\n\u003c/div>\n\n\u003c!-- Loading state -->\n\u003cdiv aria-busy={isLoading}>\n \u003ctemplate lwc:if={isLoading}>\n \u003clightning-spinner alternative-text=\"Loading data\">\u003c/lightning-spinner>\n \u003c/template>\n \u003ctemplate lwc:else>\n \u003c!-- Content -->\n \u003c/template>\n\u003c/div>\n\n\u003c!-- Required field -->\n\u003clightning-input\n label=\"Name\"\n required\n aria-required=\"true\"\n value={name}>\n\u003c/lightning-input>\n```\n\n---\n\n## Keyboard Navigation\n\n### Tab Order\n\n```html\n\u003c!-- Natural tab order -->\n\u003cform>\n \u003clightning-input label=\"First Name\" tabindex=\"0\">\u003c/lightning-input>\n \u003clightning-input label=\"Last Name\" tabindex=\"0\">\u003c/lightning-input>\n \u003clightning-button label=\"Submit\" tabindex=\"0\">\u003c/lightning-button>\n\u003c/form>\n\n\u003c!-- Skip to main content link -->\n\u003ca href=\"#main-content\" class=\"slds-assistive-text slds-assistive-text_focus\">\n Skip to main content\n\u003c/a>\n\u003cmain id=\"main-content\">\n \u003c!-- Content -->\n\u003c/main>\n```\n\n### Keyboard Event Handlers\n\n```javascript\n// accountCard.js\nexport default class AccountCard extends LightningElement {\n handleKeyDown(event) {\n // Enter or Space activates\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.handleSelect();\n }\n\n // Arrow navigation\n if (event.key === 'ArrowDown') {\n event.preventDefault();\n this.focusNextItem();\n } else if (event.key === 'ArrowUp') {\n event.preventDefault();\n this.focusPreviousItem();\n }\n\n // Escape closes\n if (event.key === 'Escape') {\n this.handleClose();\n }\n }\n\n focusNextItem() {\n const items = this.template.querySelectorAll('[role=\"listitem\"]');\n const currentIndex = Array.from(items).indexOf(document.activeElement);\n const nextIndex = (currentIndex + 1) % items.length;\n items[nextIndex].focus();\n }\n\n focusPreviousItem() {\n const items = this.template.querySelectorAll('[role=\"listitem\"]');\n const currentIndex = Array.from(items).indexOf(document.activeElement);\n const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;\n items[prevIndex].focus();\n }\n}\n```\n\n```html\n\u003c!-- accountCard.html -->\n\u003ctemplate>\n \u003cdiv role=\"list\">\n \u003ctemplate for:each={accounts} for:item=\"account\">\n \u003cdiv key={account.Id}\n role=\"listitem\"\n tabindex=\"0\"\n onkeydown={handleKeyDown}\n onclick={handleSelect}\n data-id={account.Id}>\n {account.Name}\n \u003c/div>\n \u003c/template>\n \u003c/div>\n\u003c/template>\n```\n\n---\n\n## Focus Management\n\n### Focus Trap in Modals\n\n```javascript\n// composableModal.js\nexport default class ComposableModal extends LightningElement {\n _focusableElements = [];\n _isOpen = false;\n\n @api\n toggleModal() {\n this._isOpen = !this._isOpen;\n\n if (this._isOpen) {\n // Capture focusable elements\n this._focusableElements = this.getFocusableElements();\n\n // Focus first element\n requestAnimationFrame(() => {\n this._focusableElements[0]?.focus();\n });\n\n // Add keyboard listener\n window.addEventListener('keydown', this._handleKeyDown);\n\n // Store previous focus\n this._previousFocus = document.activeElement;\n } else {\n // Remove keyboard listener\n window.removeEventListener('keydown', this._handleKeyDown);\n\n // Restore focus\n this._previousFocus?.focus();\n }\n }\n\n getFocusableElements() {\n const selector = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])'\n ].join(',');\n\n return Array.from(this.template.querySelectorAll(selector));\n }\n\n _handleKeyDown = (event) => {\n if (event.key === 'Tab') {\n this.trapFocus(event);\n } else if (event.key === 'Escape') {\n this.toggleModal();\n }\n }\n\n trapFocus(event) {\n const firstElement = this._focusableElements[0];\n const lastElement = this._focusableElements[this._focusableElements.length - 1];\n const activeElement = this.template.activeElement;\n\n if (event.shiftKey) {\n // Shift+Tab: Moving backward\n if (activeElement === firstElement) {\n event.preventDefault();\n lastElement.focus();\n }\n } else {\n // Tab: Moving forward\n if (activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n }\n }\n\n disconnectedCallback() {\n window.removeEventListener('keydown', this._handleKeyDown);\n }\n}\n```\n\n### Managing Focus After Actions\n\n```javascript\nhandleDelete(event) {\n const itemId = event.target.dataset.id;\n const itemIndex = this.items.findIndex(item => item.Id === itemId);\n\n // Delete item\n this.items = this.items.filter(item => item.Id !== itemId);\n\n // Focus next item or previous if last item deleted\n requestAnimationFrame(() => {\n const focusIndex = itemIndex \u003c this.items.length ? itemIndex : itemIndex - 1;\n if (focusIndex >= 0) {\n const nextItem = this.template.querySelector(`[data-id=\"${this.items[focusIndex].Id}\"]`);\n nextItem?.focus();\n }\n });\n}\n```\n\n---\n\n## Color and Contrast\n\n### Use SLDS Color Tokens\n\n```css\n/* BAD: Hardcoded colors */\n.my-component {\n color: #333333;\n background-color: #ffffff;\n border-color: #dddddd;\n}\n\n/* GOOD: SLDS tokens with proper contrast */\n.my-component {\n color: var(--slds-g-color-on-surface, #181818);\n background-color: var(--slds-g-color-surface-container-1, #ffffff);\n border-color: var(--slds-g-color-border-1, #c9c9c9);\n}\n```\n\n### Testing Contrast\n\n```html\n\u003c!-- Text: 4.5:1 minimum contrast ratio -->\n\u003cp class=\"slds-text-body_regular\">Regular body text\u003c/p>\n\n\u003c!-- Large text (18pt+): 3:1 minimum -->\n\u003ch1 class=\"slds-text-heading_large\">Large heading\u003c/h1>\n\n\u003c!-- Links: Must be distinguishable from surrounding text -->\n\u003cp>\n Visit our \u003ca href=\"/help\" class=\"slds-text-link\">help center\u003c/a> for support.\n\u003c/p>\n```\n\n### Color Independence\n\n```html\n\u003c!-- BAD: Relies only on color to convey status -->\n\u003cspan class=\"text-red\">Error\u003c/span>\n\n\u003c!-- GOOD: Uses icon + text + color -->\n\u003cspan class=\"slds-text-color_error\">\n \u003clightning-icon icon-name=\"utility:error\" size=\"x-small\">\u003c/lightning-icon>\n Error\n\u003c/span>\n\n\u003c!-- GOOD: Status indicators with patterns -->\n\u003cdiv class=\"slds-badge slds-theme_error\">\n \u003clightning-icon icon-name=\"utility:close\" size=\"xx-small\">\u003c/lightning-icon>\n Failed\n\u003c/div>\n```\n\n---\n\n## Screen Reader Support\n\n### Assistive Text\n\n```html\n\u003c!-- Hidden text for screen readers -->\n\u003cspan class=\"slds-assistive-text\">Required field\u003c/span>\n\u003clightning-input label=\"Email\" required value={email}>\u003c/lightning-input>\n\n\u003c!-- Button with icon only -->\n\u003cbutton aria-label=\"Edit record\">\n \u003clightning-icon icon-name=\"utility:edit\" size=\"small\">\u003c/lightning-icon>\n \u003cspan class=\"slds-assistive-text\">Edit\u003c/span>\n\u003c/button>\n\n\u003c!-- Loading state announcement -->\n\u003ctemplate lwc:if={isLoading}>\n \u003cspan class=\"slds-assistive-text\">Loading data, please wait\u003c/span>\n \u003clightning-spinner size=\"small\">\u003c/lightning-spinner>\n\u003c/template>\n```\n\n### Image Alternative Text\n\n```html\n\u003c!-- Decorative images (no alt needed, hide from screen readers) -->\n\u003cimg src=\"decorative-icon.png\" alt=\"\" role=\"presentation\">\n\n\u003c!-- Informative images (descriptive alt text) -->\n\u003cimg src=\"chart.png\" alt=\"Sales trend showing 15% increase over last quarter\">\n\n\u003c!-- Functional images (describe action) -->\n\u003ca href=\"/profile\">\n \u003cimg src=\"user-avatar.png\" alt=\"View your profile\">\n\u003c/a>\n```\n\n---\n\n## Live Regions\n\n### ARIA Live Regions\n\n```javascript\n// notificationComponent.js\nexport default class NotificationComponent extends LightningElement {\n @track messages = [];\n\n addMessage(message, type = 'info') {\n const id = Date.now();\n this.messages = [...this.messages, { id, message, type }];\n\n // Auto-remove after 5 seconds\n setTimeout(() => {\n this.messages = this.messages.filter(m => m.id !== id);\n }, 5000);\n }\n}\n```\n\n```html\n\u003c!-- notificationComponent.html -->\n\u003ctemplate>\n \u003c!-- Polite: Announced after current speech -->\n \u003cdiv aria-live=\"polite\" aria-atomic=\"true\" class=\"slds-assistive-text\">\n \u003ctemplate for:each={messages} for:item=\"msg\">\n \u003cp key={msg.id}>{msg.message}\u003c/p>\n \u003c/template>\n \u003c/div>\n\n \u003c!-- Visual notifications -->\n \u003cdiv class=\"slds-notify-container\">\n \u003ctemplate for:each={messages} for:item=\"msg\">\n \u003cdiv key={msg.id} class={msg.type} role=\"status\">\n \u003cp>{msg.message}\u003c/p>\n \u003c/div>\n \u003c/template>\n \u003c/div>\n\u003c/template>\n```\n\n### Assertive vs Polite\n\n```html\n\u003c!-- Polite: Non-urgent updates (search results, status changes) -->\n\u003cdiv aria-live=\"polite\" class=\"slds-assistive-text\">\n {searchResultsCount} results found\n\u003c/div>\n\n\u003c!-- Assertive: Urgent messages (errors, warnings) -->\n\u003cdiv aria-live=\"assertive\" role=\"alert\" class=\"slds-notify slds-notify_alert\">\n \u003cspan class=\"slds-assistive-text\">Error\u003c/span>\n Form submission failed. Please correct the errors and try again.\n\u003c/div>\n```\n\n---\n\n## Form Accessibility\n\n### Accessible Form Fields\n\n```html\n\u003ctemplate>\n \u003cform onsubmit={handleSubmit}>\n \u003c!-- Required field with validation -->\n \u003clightning-input\n label=\"Email\"\n type=\"email\"\n name=\"email\"\n required\n value={email}\n onchange={handleEmailChange}\n message-when-value-missing=\"Email is required\"\n message-when-bad-input=\"Please enter a valid email\">\n \u003c/lightning-input>\n\n \u003c!-- Field with help text -->\n \u003clightning-input\n label=\"Phone\"\n type=\"tel\"\n value={phone}\n field-level-help=\"Enter phone number with country code\"\n aria-describedby=\"phone-help\"\n onchange={handlePhoneChange}>\n \u003c/lightning-input>\n \u003cdiv id=\"phone-help\" class=\"slds-text-color_weak slds-m-top_xx-small\">\n Format: +1 (555) 555-5555\n \u003c/div>\n\n \u003c!-- Error state -->\n \u003ctemplate lwc:if={errors.industry}>\n \u003clightning-input\n label=\"Industry\"\n value={industry}\n variant=\"label-hidden\"\n aria-invalid=\"true\"\n aria-describedby=\"industry-error\"\n class=\"slds-has-error\">\n \u003c/lightning-input>\n \u003cdiv id=\"industry-error\" class=\"slds-form-element__help\" role=\"alert\">\n {errors.industry}\n \u003c/div>\n \u003c/template>\n\n \u003c!-- Submit button -->\n \u003clightning-button\n type=\"submit\"\n label=\"Save\"\n variant=\"brand\"\n disabled={isSubmitting}>\n \u003c/lightning-button>\n \u003c/form>\n\u003c/template>\n```\n\n### Fieldset and Legend\n\n```html\n\u003c!-- Radio button group -->\n\u003cfieldset class=\"slds-form-element\">\n \u003clegend class=\"slds-form-element__legend slds-form-element__label\">\n Contact Method \u003cabbr class=\"slds-required\" title=\"required\">*\u003c/abbr>\n \u003c/legend>\n \u003cdiv class=\"slds-form-element__control\">\n \u003clightning-radio-group\n name=\"contactMethod\"\n label=\"Contact Method\"\n options={contactOptions}\n value={selectedMethod}\n onchange={handleMethodChange}\n variant=\"label-hidden\"\n required>\n \u003c/lightning-radio-group>\n \u003c/div>\n\u003c/fieldset>\n```\n\n---\n\n## Common Patterns\n\n### Accessible Tabs\n\n```javascript\n// tabsComponent.js\nexport default class TabsComponent extends LightningElement {\n @track activeTab = 'tab1';\n\n handleTabKeyDown(event) {\n const tabs = Array.from(this.template.querySelectorAll('[role=\"tab\"]'));\n const currentIndex = tabs.indexOf(event.target);\n\n let nextIndex;\n if (event.key === 'ArrowRight') {\n nextIndex = (currentIndex + 1) % tabs.length;\n } else if (event.key === 'ArrowLeft') {\n nextIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;\n } else if (event.key === 'Home') {\n nextIndex = 0;\n } else if (event.key === 'End') {\n nextIndex = tabs.length - 1;\n }\n\n if (nextIndex !== undefined) {\n event.preventDefault();\n tabs[nextIndex].focus();\n this.activeTab = tabs[nextIndex].dataset.tab;\n }\n }\n\n handleTabClick(event) {\n this.activeTab = event.currentTarget.dataset.tab;\n }\n}\n```\n\n```html\n\u003c!-- tabsComponent.html -->\n\u003ctemplate>\n \u003cdiv class=\"slds-tabs_default\">\n \u003cul role=\"tablist\" class=\"slds-tabs_default__nav\">\n \u003cli class=\"slds-tabs_default__item\" role=\"presentation\">\n \u003ca role=\"tab\"\n tabindex={tab1Tabindex}\n aria-selected={isTab1Active}\n aria-controls=\"tab1-panel\"\n data-tab=\"tab1\"\n onclick={handleTabClick}\n onkeydown={handleTabKeyDown}>\n Tab 1\n \u003c/a>\n \u003c/li>\n \u003cli class=\"slds-tabs_default__item\" role=\"presentation\">\n \u003ca role=\"tab\"\n tabindex={tab2Tabindex}\n aria-selected={isTab2Active}\n aria-controls=\"tab2-panel\"\n data-tab=\"tab2\"\n onclick={handleTabClick}\n onkeydown={handleTabKeyDown}>\n Tab 2\n \u003c/a>\n \u003c/li>\n \u003c/ul>\n\n \u003cdiv id=\"tab1-panel\"\n role=\"tabpanel\"\n aria-labelledby=\"tab1\"\n class={tab1PanelClass}>\n \u003c!-- Tab 1 content -->\n \u003c/div>\n\n \u003cdiv id=\"tab2-panel\"\n role=\"tabpanel\"\n aria-labelledby=\"tab2\"\n class={tab2PanelClass}>\n \u003c!-- Tab 2 content -->\n \u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n### Accessible Data Table\n\n```html\n\u003ctemplate>\n \u003ctable class=\"slds-table slds-table_bordered\" role=\"grid\">\n \u003cthead>\n \u003ctr>\n \u003cth scope=\"col\" role=\"columnheader\">\n \u003cspan class=\"slds-truncate\">Account Name\u003c/span>\n \u003c/th>\n \u003cth scope=\"col\" role=\"columnheader\">\n \u003cspan class=\"slds-truncate\">Industry\u003c/span>\n \u003c/th>\n \u003cth scope=\"col\" role=\"columnheader\">\n \u003cspan class=\"slds-truncate\">Actions\u003c/span>\n \u003c/th>\n \u003c/tr>\n \u003c/thead>\n \u003ctbody>\n \u003ctemplate for:each={accounts} for:item=\"account\">\n \u003ctr key={account.Id} role=\"row\">\n \u003cth scope=\"row\" role=\"gridcell\">\n \u003ca href={account.link}>{account.Name}\u003c/a>\n \u003c/th>\n \u003ctd role=\"gridcell\">\n {account.Industry}\n \u003c/td>\n \u003ctd role=\"gridcell\">\n \u003cbutton aria-label={account.editLabel}\n data-id={account.Id}\n onclick={handleEdit}>\n \u003clightning-icon icon-name=\"utility:edit\" size=\"x-small\">\u003c/lightning-icon>\n \u003c/button>\n \u003c/td>\n \u003c/tr>\n \u003c/template>\n \u003c/tbody>\n \u003c/table>\n\u003c/template>\n```\n\n---\n\n## Testing\n\n### Automated Testing\n\n```javascript\n// Jest accessibility tests\nit('has proper ARIA labels', () => {\n const element = createElement('c-my-component', {\n is: MyComponent\n });\n document.body.appendChild(element);\n\n const button = element.shadowRoot.querySelector('button');\n expect(button.getAttribute('aria-label')).toBeTruthy();\n});\n\nit('manages focus when modal opens', async () => {\n const element = createElement('c-modal', { is: Modal });\n document.body.appendChild(element);\n\n element.openModal();\n await flushPromises();\n\n const firstFocusable = element.shadowRoot.querySelector('.focusable');\n expect(document.activeElement).toBe(firstFocusable);\n});\n\nit('announces status changes to screen readers', async () => {\n const element = createElement('c-notification', {\n is: Notification\n });\n document.body.appendChild(element);\n\n element.showMessage('Success');\n await flushPromises();\n\n const liveRegion = element.shadowRoot.querySelector('[aria-live]');\n expect(liveRegion.textContent).toContain('Success');\n});\n```\n\n### Manual Testing Checklist\n\n- [ ] Navigate entire component using only keyboard (Tab, Shift+Tab, Enter, Space, Arrows)\n- [ ] Test with screen reader (NVDA, JAWS, VoiceOver)\n- [ ] Verify color contrast ratios (4.5:1 minimum for text)\n- [ ] Test at 200% zoom\n- [ ] Verify focus indicators are visible\n- [ ] Test with high contrast mode\n- [ ] Verify all interactive elements have accessible names\n- [ ] Test form validation announcements\n\n---\n\n## Tools and Resources\n\n### Browser Extensions\n\n| Tool | Purpose |\n|------|---------|\n| **axe DevTools** | Automated accessibility testing |\n| **Lighthouse** | Built into Chrome DevTools, accessibility audit |\n| **WAVE** | Visual accessibility evaluation |\n| **Color Contrast Analyzer** | Check WCAG contrast compliance |\n\n### Screen Readers\n\n| Platform | Screen Reader |\n|----------|---------------|\n| Windows | NVDA (free), JAWS |\n| macOS | VoiceOver (built-in) |\n| iOS | VoiceOver (built-in) |\n| Android | TalkBack (built-in) |\n\n### Testing Commands\n\n```bash\n# Run axe accessibility tests\nnpm install --save-dev @axe-core/cli\naxe https://your-app.lightning.force.com\n\n# Lighthouse CLI\nnpm install -g lighthouse\nlighthouse https://your-app.lightning.force.com --only-categories=accessibility\n```\n\n### Resources\n\n- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)\n- [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/)\n- [Salesforce Lightning Design System Accessibility](https://www.lightningdesignsystem.com/accessibility/overview/)\n- [WebAIM Resources](https://webaim.org/resources/)\n\n---\n\n## Related Resources\n\n- [component-patterns.md](component-patterns.md) - Implementation patterns\n- [jest-testing.md](jest-testing.md) - Testing strategies\n- [performance-guide.md](performance-guide.md) - Performance optimization\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22860,"content_sha256":"ede514e7146c11afb07e5aad27381133c1844ec11ba56e930b08232311f0f4fe"},{"filename":"references/advanced-features.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n\n# Flow Screen Integration & Advanced Features\n\n## Flow Screen Integration\n\nLWC components can be embedded in Flow Screens for custom UI experiences within guided processes.\n\n### Key Concepts\n\n| Mechanism | Direction | Purpose |\n|-----------|-----------|---------|\n| `@api` with `role=\"inputOnly\"` | Flow → LWC | Pass context data |\n| `FlowAttributeChangeEvent` | LWC → Flow | Return user selections |\n| `FlowNavigationFinishEvent` | LWC → Flow | Programmatic Next/Back/Finish |\n| `availableActions` | Flow → LWC | Check available navigation |\n\n### Quick Example\n\n```javascript\nimport { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';\n\n@api recordId; // Input from Flow\n@api selectedRecordId; // Output to Flow\n@api availableActions = [];\n\nhandleSelect(event) {\n this.selectedRecordId = event.detail.id;\n // CRITICAL: Notify Flow of the change\n this.dispatchEvent(new FlowAttributeChangeEvent(\n 'selectedRecordId',\n this.selectedRecordId\n ));\n}\n\nhandleNext() {\n if (this.availableActions.includes('NEXT')) {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n}\n```\n\n**For complete Flow integration patterns, see:**\n- [references/flow-integration-guide.md](../references/flow-integration-guide.md)\n- [references/triangle-pattern.md](../references/triangle-pattern.md)\n\n---\n\n## TypeScript Support (Spring '26 - GA in API 66.0)\n\nLightning Web Components now support TypeScript with the `@salesforce/lightning-types` package.\n\n```typescript\ninterface AccountRecord {\n Id: string;\n Name: string;\n Industry?: string;\n}\n\nexport default class AccountList extends LightningElement {\n @api recordId: string | undefined;\n @track private _accounts: AccountRecord[] = [];\n\n @wire(getAccounts, { maxRecords: '$maxRecords' })\n wiredAccounts(result: WireResult\u003cAccountRecord[]>): void {\n // Typed wire handling...\n }\n}\n```\n\n**Requirements**: TypeScript 5.4.5+, `@salesforce/lightning-types` package\n\n---\n\n## LWC in Dashboards (Beta - Spring '26)\n\nComponents can be embedded as custom dashboard widgets.\n\n```xml\n\u003ctargets>\n \u003ctarget>lightning__Dashboard\u003c/target>\n\u003c/targets>\n\u003ctargetConfigs>\n \u003ctargetConfig targets=\"lightning__Dashboard\">\n \u003cproperty name=\"metricType\" type=\"String\" label=\"Metric Type\"/>\n \u003cproperty name=\"refreshInterval\" type=\"Integer\" default=\"30\"/>\n \u003c/targetConfig>\n\u003c/targetConfigs>\n```\n\n**Note**: Requires enablement via Salesforce Customer Support\n\n---\n\n## Agentforce Discoverability (Spring '26 - GA in API 66.0)\n\nMake components discoverable by Agentforce agents:\n\n```xml\n\u003ccapabilities>\n \u003ccapability>lightning__agentforce\u003c/capability>\n\u003c/capabilities>\n```\n\n**Best Practices**:\n- Clear `masterLabel` and `description`\n- Detailed property descriptions\n- Semantic naming conventions\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2897,"content_sha256":"8a48632b59b55c5ed5bdc329806b9acdc053ea51bc5a5d09c112a2049ebfa616"},{"filename":"references/async-notification-patterns.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Async Notification Patterns in LWC\n\nThis guide covers real-time notification patterns for Lightning Web Components using Platform Events, Change Data Capture, and the Emp API.\n\n---\n\n## Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ ASYNC NOTIFICATION ARCHITECTURE │\n├─────────────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────┐ Platform Events ┌──────────────┐ │\n│ │ Apex │ ─────────────────────────────▶│ LWC │ │\n│ │ Queueable │ /event/My_Event__e │ empApi │ │\n│ └──────────────┘ └──────────────┘ │\n│ │\n│ ┌──────────────┐ Change Data Capture ┌──────────────┐ │\n│ │ DML │ ─────────────────────────────▶│ LWC │ │\n│ │ Operation │ /data/AccountChangeEvent │ empApi │ │\n│ └──────────────┘ └──────────────┘ │\n│ │\n│ Use Cases: │\n│ • Queueable/Batch job completion notification │\n│ • Real-time record updates across users │\n│ • External system webhook processing notification │\n│ • Background AI generation completion │\n│ │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Emp API Basics\n\nThe `lightning/empApi` module enables LWC to subscribe to Streaming API channels.\n\n### Import and Setup\n\n```javascript\nimport { LightningElement } from 'lwc';\nimport { subscribe, unsubscribe, onError, setDebugFlag } from 'lightning/empApi';\n\nexport default class NotificationListener extends LightningElement {\n subscription = null;\n channelName = '/event/Job_Complete__e';\n\n connectedCallback() {\n // Enable debug logging (development only)\n setDebugFlag(true);\n\n // Register global error handler\n this.registerErrorListener();\n\n // Subscribe to channel\n this.handleSubscribe();\n }\n\n disconnectedCallback() {\n // CRITICAL: Always unsubscribe to prevent memory leaks\n this.handleUnsubscribe();\n }\n}\n```\n\n### Subscribe to Platform Events\n\n```javascript\nasync handleSubscribe() {\n // Callback invoked when event received\n const messageCallback = (response) => {\n console.log('Event received:', JSON.stringify(response));\n this.handleEvent(response.data.payload);\n };\n\n try {\n // Subscribe and store reference\n this.subscription = await subscribe(\n this.channelName,\n -1, // -1 = all new events, -2 = replay last 24h\n messageCallback\n );\n console.log('Subscribed to:', JSON.stringify(this.subscription));\n } catch (error) {\n console.error('Subscribe error:', error);\n }\n}\n\nhandleUnsubscribe() {\n if (this.subscription) {\n unsubscribe(this.subscription, (response) => {\n console.log('Unsubscribed:', JSON.stringify(response));\n });\n }\n}\n\nregisterErrorListener() {\n onError((error) => {\n console.error('Streaming API error:', JSON.stringify(error));\n // Attempt reconnection after delay\n setTimeout(() => this.handleSubscribe(), 5000);\n });\n}\n```\n\n---\n\n## Pattern 1: Queueable Job Completion\n\nNotify users when async Apex processing completes.\n\n### Platform Event Definition\n\n```xml\n\u003c!-- force-app/main/default/objects/Job_Complete__e/Job_Complete__e.object-meta.xml -->\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdeploymentStatus>Deployed\u003c/deploymentStatus>\n \u003ceventType>StandardVolume\u003c/eventType>\n \u003clabel>Job Complete\u003c/label>\n \u003cpluralLabel>Job Complete Events\u003c/pluralLabel>\n \u003cpublishBehavior>PublishAfterCommit\u003c/publishBehavior>\n \u003cfields>\n \u003cfullName>Job_Id__c\u003c/fullName>\n \u003clabel>Job ID\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>50\u003c/length>\n \u003c/fields>\n \u003cfields>\n \u003cfullName>User_Id__c\u003c/fullName>\n \u003clabel>User ID\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>18\u003c/length>\n \u003c/fields>\n \u003cfields>\n \u003cfullName>Status__c\u003c/fullName>\n \u003clabel>Status\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>20\u003c/length>\n \u003c/fields>\n \u003cfields>\n \u003cfullName>Message__c\u003c/fullName>\n \u003clabel>Message\u003c/label>\n \u003ctype>TextArea\u003c/type>\n \u003c/fields>\n \u003cfields>\n \u003cfullName>Record_Id__c\u003c/fullName>\n \u003clabel>Record ID\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>18\u003c/length>\n \u003c/fields>\n\u003c/CustomObject>\n```\n\n### Apex Queueable with Event Publishing\n\n```apex\npublic class AIGenerationQueueable implements Queueable, Database.AllowsCallouts {\n\n private Id recordId;\n private Id userId;\n\n public AIGenerationQueueable(Id recordId, Id userId) {\n this.recordId = recordId;\n this.userId = userId;\n }\n\n public void execute(QueueableContext context) {\n String status;\n String message;\n\n try {\n // Perform async work (AI generation, callout, etc.)\n performGeneration();\n status = 'SUCCESS';\n message = 'AI content generated successfully';\n } catch (Exception e) {\n status = 'ERROR';\n message = e.getMessage();\n }\n\n // Publish completion event\n publishCompletionEvent(status, message);\n }\n\n private void publishCompletionEvent(String status, String message) {\n Job_Complete__e event = new Job_Complete__e();\n event.Job_Id__c = 'AI_GEN_' + recordId;\n event.User_Id__c = userId;\n event.Status__c = status;\n event.Message__c = message;\n event.Record_Id__c = recordId;\n\n Database.SaveResult result = EventBus.publish(event);\n if (!result.isSuccess()) {\n System.debug('Event publish failed: ' + result.getErrors());\n }\n }\n\n private void performGeneration() {\n // AI generation logic...\n }\n}\n```\n\n### LWC Listener Component\n\n```javascript\n// jobCompletionListener.js\nimport { LightningElement, api } from 'lwc';\nimport { subscribe, unsubscribe, onError } from 'lightning/empApi';\nimport { ShowToastEvent } from 'lightning/platformShowToastEvent';\nimport { refreshApex } from '@salesforce/apex';\nimport userId from '@salesforce/user/Id';\n\nexport default class JobCompletionListener extends LightningElement {\n @api recordId;\n\n subscription = null;\n channelName = '/event/Job_Complete__e';\n\n // Reference to wired data for refresh\n wiredResult;\n\n connectedCallback() {\n this.registerErrorListener();\n this.subscribeToChannel();\n }\n\n disconnectedCallback() {\n this.unsubscribeFromChannel();\n }\n\n async subscribeToChannel() {\n if (this.subscription) return;\n\n try {\n this.subscription = await subscribe(\n this.channelName,\n -1,\n (message) => this.handleMessage(message)\n );\n } catch (error) {\n console.error('Subscription error:', error);\n }\n }\n\n unsubscribeFromChannel() {\n if (this.subscription) {\n unsubscribe(this.subscription);\n this.subscription = null;\n }\n }\n\n handleMessage(message) {\n const payload = message.data.payload;\n\n // Filter: Only process events for current user and record\n if (payload.User_Id__c !== userId) return;\n if (payload.Record_Id__c !== this.recordId) return;\n\n // Show toast notification\n const variant = payload.Status__c === 'SUCCESS' ? 'success' : 'error';\n this.dispatchEvent(new ShowToastEvent({\n title: payload.Status__c === 'SUCCESS' ? 'Complete' : 'Error',\n message: payload.Message__c,\n variant: variant\n }));\n\n // Refresh data if successful\n if (payload.Status__c === 'SUCCESS' && this.wiredResult) {\n refreshApex(this.wiredResult);\n }\n\n // Dispatch custom event for parent components\n this.dispatchEvent(new CustomEvent('jobcomplete', {\n detail: {\n jobId: payload.Job_Id__c,\n status: payload.Status__c,\n message: payload.Message__c\n }\n }));\n }\n\n registerErrorListener() {\n onError((error) => {\n console.error('EmpApi error:', JSON.stringify(error));\n // Reconnect after delay\n this.subscription = null;\n setTimeout(() => this.subscribeToChannel(), 3000);\n });\n }\n}\n```\n\n---\n\n## Pattern 2: Real-Time Record Updates (CDC)\n\nUse Change Data Capture to sync UI when records change.\n\n### Enable CDC for Object\n\n1. Setup → Integrations → Change Data Capture\n2. Select objects to track\n3. Deploy (or enable via metadata)\n\n### CDC Channel Names\n\n| Object Type | Channel Pattern |\n|-------------|-----------------|\n| Standard Object | `/data/AccountChangeEvent` |\n| Custom Object | `/data/My_Object__ChangeEvent` |\n\n### LWC CDC Listener\n\n```javascript\n// recordChangeListener.js\nimport { LightningElement, api, wire } from 'lwc';\nimport { subscribe, unsubscribe, onError } from 'lightning/empApi';\nimport { getRecord, getFieldValue } from 'lightning/uiRecordApi';\nimport { refreshApex } from '@salesforce/apex';\n\nconst FIELDS = ['Account.Name', 'Account.Industry', 'Account.AnnualRevenue'];\n\nexport default class RecordChangeListener extends LightningElement {\n @api recordId;\n\n subscription = null;\n channelName = '/data/AccountChangeEvent';\n\n @wire(getRecord, { recordId: '$recordId', fields: FIELDS })\n wiredRecord;\n\n get accountName() {\n return getFieldValue(this.wiredRecord.data, 'Account.Name');\n }\n\n connectedCallback() {\n this.registerErrorListener();\n this.subscribeToChanges();\n }\n\n disconnectedCallback() {\n if (this.subscription) {\n unsubscribe(this.subscription);\n }\n }\n\n async subscribeToChanges() {\n try {\n this.subscription = await subscribe(\n this.channelName,\n -1,\n (message) => this.handleChange(message)\n );\n } catch (error) {\n console.error('CDC subscription error:', error);\n }\n }\n\n handleChange(message) {\n const payload = message.data.payload;\n const changeType = payload.ChangeEventHeader.changeType;\n const recordIds = payload.ChangeEventHeader.recordIds;\n\n // Only process changes for our record\n if (!recordIds.includes(this.recordId)) return;\n\n console.log(`Record ${changeType}:`, payload);\n\n // Refresh the wire\n if (this.wiredRecord) {\n refreshApex(this.wiredRecord);\n }\n\n // Notify parent\n this.dispatchEvent(new CustomEvent('recordchange', {\n detail: {\n changeType,\n changedFields: payload.ChangeEventHeader.changedFields,\n payload\n }\n }));\n }\n\n registerErrorListener() {\n onError((error) => {\n console.error('CDC error:', error);\n this.subscription = null;\n setTimeout(() => this.subscribeToChanges(), 5000);\n });\n }\n}\n```\n\n### CDC Payload Structure\n\n```javascript\n{\n \"data\": {\n \"schema\": \"...\",\n \"payload\": {\n \"ChangeEventHeader\": {\n \"entityName\": \"Account\",\n \"recordIds\": [\"001xx000003ABCDE\"],\n \"changeType\": \"UPDATE\", // CREATE, UPDATE, DELETE, UNDELETE\n \"changedFields\": [\"Name\", \"Industry\"],\n \"changeOrigin\": \"com/salesforce/api/soap/58.0\",\n \"transactionKey\": \"...\",\n \"sequenceNumber\": 1,\n \"commitTimestamp\": 1705612800000,\n \"commitUser\": \"005xx000001AAAAA\",\n \"commitNumber\": 123456789\n },\n // Changed field values\n \"Name\": \"Updated Account Name\",\n \"Industry\": \"Technology\"\n }\n }\n}\n```\n\n---\n\n## Pattern 3: Multi-User Collaboration\n\nNotify all users viewing the same record.\n\n```javascript\n// collaborativeEditor.js\nimport { LightningElement, api } from 'lwc';\nimport { subscribe, unsubscribe, onError } from 'lightning/empApi';\nimport userId from '@salesforce/user/Id';\n\nexport default class CollaborativeEditor extends LightningElement {\n @api recordId;\n\n subscription = null;\n channelName = '/event/Edit_Activity__e';\n activeEditors = [];\n\n connectedCallback() {\n this.registerErrorListener();\n this.subscribeToActivity();\n this.announcePresence();\n }\n\n disconnectedCallback() {\n this.announceExit();\n this.unsubscribe();\n }\n\n async subscribeToActivity() {\n this.subscription = await subscribe(\n this.channelName,\n -1,\n (message) => this.handleActivity(message)\n );\n }\n\n handleActivity(message) {\n const { Record_Id__c, User_Id__c, User_Name__c, Action__c } = message.data.payload;\n\n // Ignore our own events, only track this record\n if (User_Id__c === userId || Record_Id__c !== this.recordId) return;\n\n if (Action__c === 'JOINED') {\n this.addEditor(User_Id__c, User_Name__c);\n } else if (Action__c === 'LEFT') {\n this.removeEditor(User_Id__c);\n } else if (Action__c === 'EDITING') {\n this.showEditingIndicator(User_Name__c, message.data.payload.Field__c);\n }\n }\n\n addEditor(userId, userName) {\n if (!this.activeEditors.find(e => e.id === userId)) {\n this.activeEditors = [...this.activeEditors, { id: userId, name: userName }];\n }\n }\n\n removeEditor(userId) {\n this.activeEditors = this.activeEditors.filter(e => e.id !== userId);\n }\n\n // Call Apex to publish presence event\n announcePresence() {\n // publishEditActivity({ recordId: this.recordId, action: 'JOINED' });\n }\n\n announceExit() {\n // publishEditActivity({ recordId: this.recordId, action: 'LEFT' });\n }\n\n unsubscribe() {\n if (this.subscription) {\n unsubscribe(this.subscription);\n }\n }\n\n registerErrorListener() {\n onError((error) => {\n console.error('Collaboration error:', error);\n });\n }\n}\n```\n\n---\n\n## Pattern 4: Polling Fallback\n\nWhen empApi isn't available (Communities, some mobile contexts), use polling.\n\n```javascript\n// pollingFallback.js\nimport { LightningElement, api } from 'lwc';\nimport checkJobStatus from '@salesforce/apex/JobStatusController.checkJobStatus';\n\nexport default class PollingFallback extends LightningElement {\n @api jobId;\n\n pollingInterval = null;\n pollFrequencyMs = 3000;\n maxAttempts = 60;\n attemptCount = 0;\n\n connectedCallback() {\n this.startPolling();\n }\n\n disconnectedCallback() {\n this.stopPolling();\n }\n\n startPolling() {\n this.pollingInterval = setInterval(() => {\n this.checkStatus();\n }, this.pollFrequencyMs);\n }\n\n stopPolling() {\n if (this.pollingInterval) {\n clearInterval(this.pollingInterval);\n this.pollingInterval = null;\n }\n }\n\n async checkStatus() {\n this.attemptCount++;\n\n if (this.attemptCount >= this.maxAttempts) {\n this.stopPolling();\n this.handleTimeout();\n return;\n }\n\n try {\n const result = await checkJobStatus({ jobId: this.jobId });\n\n if (result.status === 'COMPLETE' || result.status === 'ERROR') {\n this.stopPolling();\n this.handleCompletion(result);\n }\n } catch (error) {\n console.error('Polling error:', error);\n }\n }\n\n handleCompletion(result) {\n this.dispatchEvent(new CustomEvent('complete', { detail: result }));\n }\n\n handleTimeout() {\n this.dispatchEvent(new CustomEvent('timeout'));\n }\n}\n```\n\n---\n\n## Replay Options\n\n| Replay ID | Behavior |\n|-----------|----------|\n| `-1` | Only new events (after subscription) |\n| `-2` | All stored events (last 24 hours) + new |\n| Specific ID | Events after that replay ID |\n\n### Storing Replay Position\n\n```javascript\n// For durable subscriptions, store last processed replay ID\nhandleMessage(message) {\n const replayId = message.data.event.replayId;\n\n // Process message...\n\n // Store replay ID for recovery\n this.lastReplayId = replayId;\n localStorage.setItem('myapp_replay_id', replayId);\n}\n\nconnectedCallback() {\n // Recover from stored position\n const storedReplayId = localStorage.getItem('myapp_replay_id');\n const replayFrom = storedReplayId ? parseInt(storedReplayId, 10) : -1;\n\n this.subscribeFromReplayId(replayFrom);\n}\n```\n\n---\n\n## Best Practices\n\n### DO ✅\n\n| Practice | Reason |\n|----------|--------|\n| Always unsubscribe in `disconnectedCallback` | Prevents memory leaks |\n| Filter events by user/record ID | Reduces unnecessary processing |\n| Register error handler | Enables reconnection on failure |\n| Use `refreshApex` after events | Keeps wire data in sync |\n| Set reasonable replay ID | `-1` for real-time, `-2` for recovery |\n\n### DON'T ❌\n\n| Anti-Pattern | Problem |\n|--------------|---------|\n| Subscribe without unsubscribing | Memory leak, duplicate handlers |\n| Process all events without filtering | Performance impact |\n| Ignore error handler | Silent failures, no reconnection |\n| Mutate wire data directly | Breaks reactivity |\n| Use CDC for high-frequency updates | Rate limits, performance |\n\n---\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| No events received | Subscription failed silently | Check error handler, verify channel name |\n| Duplicate processing | Component re-mounted | Track subscription state, dedupe by replay ID |\n| Events delayed | High server load | Normal behavior, events are async |\n| \"Handshake denied\" | Missing permissions | Verify Streaming API access in profile |\n| Events stop after time | Session expired | Handle error event, resubscribe |\n\n---\n\n## Cross-Skill References\n\n| Topic | Resource |\n|-------|----------|\n| Platform Event definition | [building-sf-integrations/references/platform-events-guide.md](../../building-sf-integrations/references/platform-events-guide.md) |\n| Publishing from Apex | [sf-apex/references/best-practices.md](../../sf-apex/references/best-practices.md) |\n| State management | [state-management.md](state-management.md) |\n| Agentforce Models API | [developing-agentforce/references/models-api.md](../../developing-agentforce/references/models-api.md) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20146,"content_sha256":"9d970333a0198685912064821f2175771b3cc714871efbeebdfc46e43f407a86"},{"filename":"references/cli-commands.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Salesforce CLI Commands for LWC Development\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Create component | `sf template generate lightning component --name myComp --type lwc` |\n| Create FlexiPage | `sf template generate flexipage --name MyPage --template DefaultAppPage` |\n| Run all tests | `sf force lightning lwc test run` |\n| Preview a component locally | `sf lightning dev component --target-org my-sandbox` |\n| Preview an app locally | `sf lightning dev app --target-org my-sandbox` |\n| Preview an Experience Cloud site locally | `sf lightning dev site --target-org my-sandbox` |\n| Deploy component | `sf project deploy start --source-dir force-app/.../lwc/myComp` |\n| Create message channel | Manual XML: `force-app/.../messageChannels/MyChannel.messageChannel-meta.xml` |\n\n---\n\n## Component Generation\n\n### Create New LWC\n\n```bash\n# Basic component\nsf template generate lightning component \\\n --name accountList \\\n --type lwc \\\n --output-dir force-app/main/default/lwc\n\n# Creates:\n# force-app/main/default/lwc/accountList/\n# ├── accountList.js\n# ├── accountList.html\n# └── accountList.js-meta.xml\n```\n\n### Generate with Jest Test\n\n```bash\n# The test file must be created manually in __tests__ folder\nmkdir -p force-app/main/default/lwc/accountList/__tests__\ntouch force-app/main/default/lwc/accountList/__tests__/accountList.test.js\n```\n\n---\n\n## Testing\n\n### Run All Jest Tests\n\n```bash\nsf force lightning lwc test run\n```\n\n### Run Specific Test File\n\n```bash\nsf force lightning lwc test run \\\n --spec force-app/main/default/lwc/accountList/__tests__/accountList.test.js\n```\n\n### Watch Mode (Development)\n\n```bash\n# Re-runs tests when files change\nsf force lightning lwc test run --watch\n```\n\n### Coverage Report\n\n```bash\n# Generate HTML coverage report\nsf force lightning lwc test run --coverage\n# Report at: coverage/lcov-report/index.html\n```\n\n### Debug Tests\n\n```bash\n# Run with Node debugger\nsf force lightning lwc test run --debug\n\n# Then in Chrome: chrome://inspect\n```\n\n### Update Snapshots\n\n```bash\nsf force lightning lwc test run --update-snapshot\n```\n\n---\n\n## Linting\n\n### Run ESLint\n\n> **Note**: `sf lightning lint` does not exist. Use `npx eslint` directly or `sf code-analyzer run`.\n\n```bash\n# Lint LWC files with ESLint (requires @salesforce/eslint-config-lwc)\nnpx eslint force-app/main/default/lwc\n\n# Lint specific component\nnpx eslint force-app/main/default/lwc/accountList\n\n# Auto-fix issues\nnpx eslint force-app/main/default/lwc --fix\n\n# Or use Code Analyzer (includes ESLint + PMD + RetireJS)\nsf code-analyzer run --workspace force-app/main/default/lwc\n```\n\n---\n\n## Deployment\n\n### Deploy Single Component\n\n```bash\nsf project deploy start \\\n --source-dir force-app/main/default/lwc/accountList \\\n --target-org my-sandbox\n```\n\n### Deploy Multiple Components\n\n```bash\nsf project deploy start \\\n --source-dir force-app/main/default/lwc/accountList \\\n --source-dir force-app/main/default/lwc/accountForm \\\n --target-org my-sandbox\n```\n\n### Deploy with Related Apex\n\n```bash\nsf project deploy start \\\n --source-dir force-app/main/default/lwc/accountList \\\n --source-dir force-app/main/default/classes/AccountController.cls \\\n --target-org my-sandbox\n```\n\n### Validate Without Deploying\n\n```bash\nsf project deploy start \\\n --source-dir force-app/main/default/lwc \\\n --target-org my-sandbox \\\n --dry-run\n```\n\n---\n\n## Retrieval\n\n### Retrieve Component from Org\n\n```bash\nsf project retrieve start \\\n --metadata LightningComponentBundle:accountList \\\n --target-org my-sandbox \\\n --output-dir force-app/main/default\n```\n\n### Retrieve All LWC\n\n```bash\nsf project retrieve start \\\n --metadata LightningComponentBundle \\\n --target-org my-sandbox\n```\n\n---\n\n## Local Development\n\n### Preview Components Locally\n\n> **Note**: `sf lightning dev-server` was deprecated. In current SF CLI releases, Local Dev commands are installed just-in-time on first use via the Local Dev plugin.\n\n```bash\n# Preview an LWC component in isolation\nsf lightning dev component --name myComp --target-org my-sandbox\n\n# Launch component preview and choose the component interactively\nsf lightning dev component --target-org my-sandbox\n\n# Preview a Lightning Experience app locally\nsf lightning dev app --target-org my-sandbox\n\n# Preview an Experience Cloud site locally\nsf lightning dev site --name \"Partner Central\" --target-org my-sandbox\n```\n\nThese commands start long-running local preview sessions with hot reload. Use `sf project deploy start` for changes that Local Dev can't reflect automatically.\n\n---\n\n## Message Channels\n\n### Create Message Channel\n\n> **Note**: There is no `sf lightning generate messageChannel` command. Message Channels are created as metadata XML files manually.\n\n```bash\n# Create the directory if it doesn't exist\nmkdir -p force-app/main/default/messageChannels\n\n# Create the XML file manually:\n# force-app/main/default/messageChannels/RecordSelected.messageChannel-meta.xml\n```\n\n### Deploy Message Channel\n\n```bash\nsf project deploy start \\\n --metadata LightningMessageChannel:RecordSelected__c \\\n --target-org my-sandbox\n```\n\n---\n\n## Debugging\n\n### Open Component in Browser\n\n```bash\n# Open Lightning App Builder\nsf org open --target-org my-sandbox --path /lightning/setup/FlexiPageList/home\n```\n\n### View Debug Logs\n\n```bash\n# Tail logs while testing\nsf apex tail log --target-org my-sandbox --color\n```\n\n### Check Deployment Errors\n\n```bash\n# If deployment fails, check status\nsf project deploy report --job-id \u003cjob-id>\n```\n\n---\n\n## Package Development\n\n### Create Unlocked Package\n\n```bash\n# Create package\nsf package create \\\n --name \"My LWC Package\" \\\n --package-type Unlocked \\\n --path force-app\n\n# Create version\nsf package version create \\\n --package \"My LWC Package\" \\\n --installation-key test1234 \\\n --wait 10\n```\n\n---\n\n## Jest Configuration\n\n### Setup Jest (if not already configured)\n\n```bash\n# Install Jest dependencies\nnpm install @salesforce/sfdx-lwc-jest --save-dev\n\n# Add to package.json scripts\n{\n \"scripts\": {\n \"test:unit\": \"sfdx-lwc-jest\",\n \"test:unit:watch\": \"sfdx-lwc-jest --watch\",\n \"test:unit:debug\": \"sfdx-lwc-jest --debug\",\n \"test:unit:coverage\": \"sfdx-lwc-jest --coverage\"\n }\n}\n```\n\n### Jest Config (jest.config.js)\n\n```javascript\nconst { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');\n\nmodule.exports = {\n ...jestConfig,\n roots: ['\u003crootDir>/force-app'],\n modulePathIgnorePatterns: ['\u003crootDir>/.localdevserver'],\n testPathIgnorePatterns: [\n '\u003crootDir>/node_modules/',\n '\u003crootDir>/.sfdx/',\n '\u003crootDir>/.agents/',\n '\u003crootDir>/.cursor/',\n '\u003crootDir>/.claude/',\n '\u003crootDir>/.pi/'\n ],\n testTimeout: 10000\n};\n```\n\nIf you copy example tests from `generating-lwc-components/assets/`, rename the `.example` file after placing it in your component's `__tests__` folder.\n\n---\n\n## Useful Patterns\n\n### Deploy and Test Flow\n\n```bash\n# 1. Run local tests\nsf force lightning lwc test run\n\n# 2. Deploy to sandbox\nsf project deploy start \\\n --source-dir force-app/main/default/lwc/myComponent \\\n --target-org my-sandbox\n\n# 3. Open org to test\nsf org open --target-org my-sandbox\n```\n\n### CI/CD Pipeline Pattern\n\n```bash\n#!/bin/bash\n\n# Lint\nnpx eslint ./force-app/main/default/lwc || exit 1\n\n# Test\nsf force lightning lwc test run --coverage || exit 1\n\n# Validate deployment\nsf project deploy start \\\n --source-dir force-app/main/default/lwc \\\n --target-org ci-sandbox \\\n --dry-run \\\n --json || exit 1\n\n# Deploy if validation passes\nsf project deploy start \\\n --source-dir force-app/main/default/lwc \\\n --target-org ci-sandbox \\\n --json\n```\n\n### Watch and Auto-Deploy\n\n```bash\n# Using nodemon or similar\nnpx nodemon \\\n --watch \"force-app/main/default/lwc/**/*\" \\\n --exec \"sf project deploy start --source-dir force-app/main/default/lwc --target-org my-sandbox\"\n```\n\n---\n\n## Troubleshooting\n\n### Component Not Visible in App Builder\n\n1. Check `isExposed` is `true` in meta.xml\n2. Check `targets` include the desired location\n3. Verify deployment was successful\n\n```bash\n# Re-deploy with verbose output\nsf project deploy start \\\n --source-dir force-app/main/default/lwc/myComponent \\\n --target-org my-sandbox \\\n --verbose\n```\n\n### Jest Tests Not Finding Component\n\nIf Jest reports errors from `.agents/`, `.cursor/`, `.claude/`, or `.pi/`, it is discovering files outside your real Salesforce source tree. Scope Jest to `force-app` and ignore agent folders.\n\n```bash\n# Clear Jest cache\nnpx jest --clearCache\n\n# Re-run tests\nsf force lightning lwc test run\n```\n\n### Wire Service Not Working\n\n1. Verify `cacheable=true` on Apex method\n2. Check reactive parameter has ` generating-lwc-components — Skillopedia prefix\n3. Verify Apex method is accessible\n\n```bash\n# Test Apex method directly\nsf apex run --target-org my-sandbox \u003c\u003c\u003c \"System.debug(MyController.getRecords());\"\n```\n\n### Deployment Conflicts\n\n```bash\n# Check what's different\nsf project retrieve start \\\n --metadata LightningComponentBundle:myComponent \\\n --target-org my-sandbox \\\n --output-dir temp-retrieve\n\n# Compare and resolve\ndiff -r force-app/main/default/lwc/myComponent temp-retrieve/force-app/.../myComponent\n```\n\n---\n\n## Static Analysis (Code Analyzer v5)\n\n### Salesforce Code Analyzer\n\nCode Analyzer v5 (`@salesforce/plugin-code-analyzer`) validates LWC files for SLDS 2 compliance, accessibility, and security.\n\n```bash\n# Install Code Analyzer v5 plugin\nsf plugins install @salesforce/plugin-code-analyzer\n\n# Run scan on LWC components\nsf code-analyzer run \\\n --workspace force-app/main/default/lwc \\\n --output-file lwc-scan-results.html\n\n# Run with specific rules\nsf code-analyzer run \\\n --workspace force-app/main/default/lwc \\\n --rule-selector \"Category:Best Practices,Security\"\n```\n\n> **Migration from sfdx-scanner**: v5 uses `--workspace` instead of `--target`, `--output-file` instead of `--outfile` (format inferred from extension), `--view` for table/detail display, and `--rule-selector` instead of `--engine`/`--category`.\n\n### SLDS 2 Compliance Checks\n\n```bash\n# Check for hardcoded colors (breaks dark mode)\nrg -n '#[0-9A-Fa-f]{3,8}' force-app/main/default/lwc/**/*.css\n\n# Find deprecated SLDS 1 tokens\nrg -n '\\-\\-lwc\\-' force-app/main/default/lwc/**/*.css\n\n# Find missing alternative-text on icons\nrg -n '\u003clightning-icon' force-app/main/default/lwc/**/*.html | \\\n rg -v 'alternative-text'\n\n# Check for !important overrides\nrg -n '!important' force-app/main/default/lwc/**/*.css\n```\n\n### Dark Mode Validation\n\n```bash\n# Find all hardcoded colors that may break dark mode\nrg -n 'rgb\\(|rgba\\(|#[0-9A-Fa-f]{3,8}' \\\n force-app/main/default/lwc/**/*.css \\\n --glob '!**/node_modules/**'\n\n# Verify CSS variables usage (SLDS 2 global hooks)\nrg -n '\\-\\-slds-g-color' force-app/main/default/lwc/**/*.css\n```\n\n---\n\n## GraphQL Debugging\n\n### GraphQL Wire Service\n\n```bash\n# View GraphQL queries in debug mode (enable in Setup → Debug Logs)\nsf apex tail log --target-org my-sandbox --color\n# Note: Debug levels are configured via TraceFlag records in Setup, not CLI flags\n\n# Test GraphQL query via Anonymous Apex (for syntax validation)\nsf apex run --target-org my-sandbox \u003c\u003c'EOF'\n// GraphQL syntax can't be tested directly in Apex\n// But you can verify field access:\nSystem.debug([SELECT Id, Name FROM Account LIMIT 1]);\nEOF\n```\n\n### GraphQL Troubleshooting\n\n| Issue | Possible Cause | Solution |\n|-------|---------------|----------|\n| \"Field not found\" | FLS restriction | Check user has read access |\n| \"Object not supported\" | GraphQL scope | Not all objects support GraphQL |\n| Cursor pagination fails | Invalid cursor | Use exact cursor from `pageInfo.endCursor` |\n| Null data | Query error | Check `errors` array in wire result |\n\n### Monitor GraphQL Performance\n\n```bash\n# Open Developer Console for network inspection\nsf org open --target-org my-sandbox \\\n --path /lightning/setup/ApexDebugLogDetail/home\n\n# View Event Monitoring logs (if enabled)\nsf data query \\\n --query \"SELECT EventType, LogDate FROM EventLogFile WHERE EventType='LightningPageView' ORDER BY LogDate DESC LIMIT 5\" \\\n --target-org my-sandbox\n```\n\n---\n\n## Workspace API (Console Apps)\n\n### Console Detection\n\n```bash\n# Check if an app is a Console app\nsf data query \\\n --query \"SELECT DeveloperName, NavType FROM AppDefinition WHERE NavType='Console'\" \\\n --target-org my-sandbox\n\n# List all Lightning Apps\nsf data query \\\n --query \"SELECT DeveloperName, NavType, Label FROM AppDefinition ORDER BY Label\" \\\n --target-org my-sandbox\n```\n\n### Console App Testing\n\n```bash\n# Open Service Console\nsf org open --target-org my-sandbox \\\n --path /lightning/app/standard__ServiceConsole\n\n# Open Sales Console\nsf org open --target-org my-sandbox \\\n --path /lightning/app/standard__SalesConsole\n\n# Open custom console app\nsf org open --target-org my-sandbox \\\n --path /lightning/app/c__MyConsoleApp\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12871,"content_sha256":"4c83ad8a78783350d435f53ad6bc286759a5592bfa66f4e14a4bb210712755c2"},{"filename":"references/component-patterns.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# LWC Component Patterns\n\nComprehensive code examples for common Lightning Web Component patterns.\n\n---\n\n## Table of Contents\n\n1. [PICKLES Framework Details](#pickles-framework-details)\n2. [Wire Service Patterns](#wire-service-patterns)\n - [Wire vs Imperative Apex Calls](#wire-vs-imperative-apex-calls)\n3. [GraphQL Patterns](#graphql-patterns)\n4. [Modal Component Pattern](#modal-component-pattern)\n5. [Record Picker Pattern](#record-picker-pattern)\n6. [Workspace API Pattern](#workspace-api-pattern)\n7. [Parent-Child Communication](#parent-child-communication)\n8. [Sibling Communication (via Parent)](#sibling-communication-via-parent)\n9. [Navigation Patterns](#navigation-patterns)\n10. [TypeScript Patterns](#typescript-patterns)\n11. [Apex Controller Patterns](#apex-controller-patterns)\n\n---\n\n## PICKLES Framework Details\n\n### P - Prototype\n\n**Purpose**: Validate ideas early before full implementation.\n\n| Action | Description |\n|--------|-------------|\n| Wireframe | Create high-level component sketches |\n| Mock Data | Use sample data to test functionality |\n| Stakeholder Review | Gather feedback before development |\n| Separation of Concerns | Break into smaller functional pieces |\n\n```javascript\n// Mock data pattern for prototyping\nconst MOCK_ACCOUNTS = [\n { Id: '001MOCK001', Name: 'Acme Corp', Industry: 'Technology' },\n { Id: '001MOCK002', Name: 'Global Inc', Industry: 'Finance' }\n];\n\nexport default class AccountPrototype extends LightningElement {\n accounts = MOCK_ACCOUNTS; // Replace with wire/Apex later\n}\n```\n\n### I - Integrate\n\n**Purpose**: Determine how components interact with data systems.\n\n**Integration Checklist**:\n- [ ] Implement error handling with clear user notifications\n- [ ] Add loading spinners to prevent duplicate requests\n- [ ] Use LDS for single-object operations (minimizes DML)\n- [ ] Respect FLS and CRUD in Apex implementations\n- [ ] Store `wiredResult` for `refreshApex()` support\n\n### C - Composition\n\n**Purpose**: Structure how LWCs nest and communicate.\n\n**Best Practices**:\n- Maintain shallow component hierarchies (max 3-4 levels)\n- Single responsibility per component\n- Clean up subscriptions in `disconnectedCallback()`\n- Use custom events purposefully, not for every interaction\n\n```javascript\n// Parent-managed composition pattern\n// parent.js\nhandleChildEvent(event) {\n this.selectedId = event.detail.id;\n // Update child via @api\n this.template.querySelector('c-child').selectedId = this.selectedId;\n}\n```\n\n### K - Kinetics\n\n**Purpose**: Manage user interaction and event responsiveness.\n\n```javascript\n// Debounce pattern for search\ndelayTimeout;\n\nhandleSearchChange(event) {\n const searchTerm = event.target.value;\n clearTimeout(this.delayTimeout);\n this.delayTimeout = setTimeout(() => {\n this.dispatchEvent(new CustomEvent('search', {\n detail: { searchTerm }\n }));\n }, 300);\n}\n```\n\n### L - Libraries\n\n**Purpose**: Leverage Salesforce-provided and platform tools.\n\n**Recommended Platform Features**:\n\n| API/Module | Use Case |\n|------------|----------|\n| `lightning/navigation` | Page/record navigation |\n| `lightning/uiRecordApi` | LDS operations (getRecord, updateRecord) |\n| `lightning/platformShowToastEvent` | User notifications |\n| `lightning/modal` | Native modal dialogs |\n| Base Components | Pre-built UI (button, input, datatable) |\n| `lightning/refresh` | Dispatch refresh events |\n\n**Avoid reinventing** what base components already provide!\n\n### E - Execution\n\n**Purpose**: Optimize performance and resource efficiency.\n\n**Performance Checklist**:\n- [ ] Lazy load with `if:true` / `lwc:if`\n- [ ] Use `key` directive in iterations\n- [ ] Cache computed values in getters\n- [ ] Avoid property updates that trigger re-renders\n- [ ] Use browser DevTools Performance tab\n\n### S - Security\n\n**Purpose**: Enforce access control and data protection.\n\n```apex\n// Secure Apex pattern\n@AuraEnabled(cacheable=true)\npublic static List\u003cAccount> getAccounts(String searchTerm) {\n String searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%';\n return [\n SELECT Id, Name, Industry\n FROM Account\n WHERE Name LIKE :searchKey\n WITH SECURITY_ENFORCED\n LIMIT 50\n ];\n}\n```\n\n---\n\n## Wire Service Patterns\n\n### Wire vs Imperative Apex Calls\n\nLWC can interact with Apex in two ways: **@wire** (reactive/declarative) and **imperative calls** (manual/programmatic). Understanding when to use each is critical for building performant, maintainable components.\n\n#### Quick Comparison\n\n```\n┌─────────────────────────────────────────────────────────────────────────────────────┐\n│ WIRE vs IMPERATIVE APEX CALLS │\n├──────────────────┬──────────────────────────────┬────────────────────────────────────┤\n│ Aspect │ Wire (@wire) │ Imperative Calls │\n├──────────────────┼──────────────────────────────┼────────────────────────────────────┤\n│ Execution │ Automatic / Reactive │ Manual / Programmatic │\n│ DML Operations │ ❌ Read-Only │ ✅ Insert / Update / Delete │\n│ Data Updates │ ✅ Auto on Parameter Change │ ❌ Manual Refresh Required │\n│ Control │ ⚠️ Low (framework decides) │ ✅ Full (you decide when/how) │\n│ Error Handling │ ✅ Framework Managed │ ⚠️ Developer Managed │\n│ Supported Objects│ ⚠️ UI API Only │ ✅ All Objects │\n│ Caching │ ✅ Built-in (cacheable=true) │ ❌ No automatic caching │\n└──────────────────┴──────────────────────────────┴────────────────────────────────────┘\n```\n\n#### Pros & Cons\n\n| Wire (@wire) | Imperative Calls |\n|--------------|------------------|\n| ✅ Auto UI sync & caching | ✅ Supports DML & all objects |\n| ✅ Less boilerplate code | ✅ Full control over timing |\n| ✅ Reactive to parameter changes | ✅ Can handle complex logic |\n| ❌ Read-only, limited objects | ❌ Manual handling, no auto refresh |\n| ❌ Can't control execution timing | ❌ More error handling code needed |\n\n#### When to Use Each\n\n**Use Wire (@wire) when:**\n- 📌 Read-only data display\n- 📌 Auto-refresh UI when parameters change\n- 📌 Stable parameters (recordId, filter values)\n- 📌 Working with UI API supported objects\n\n**Use Imperative Calls when:**\n- 📌 User actions (clicks, form submissions)\n- 📌 DML operations (Insert, Update, Delete)\n- 📌 Dynamic parameters determined at runtime\n- 📌 Custom objects or complex queries\n- 📌 Need control over execution timing\n\n#### Side-by-Side Code Examples\n\n**Wire Example** - Data loads automatically when `selectedIndustry` changes:\n\n```javascript\nimport { LightningElement, wire } from 'lwc';\nimport fetchAccounts from '@salesforce/apex/AccountController.fetchAccounts';\n\nexport default class WireExample extends LightningElement {\n selectedIndustry = 'Technology';\n accounts;\n error;\n\n // Automatically re-fetches when selectedIndustry changes\n @wire(fetchAccounts, { industry: '$selectedIndustry' })\n wiredAccounts({ data, error }) {\n if (data) {\n this.accounts = data;\n this.error = undefined;\n } else if (error) {\n this.error = error;\n this.accounts = undefined;\n }\n }\n}\n```\n\n**Imperative Example** - Data loads only when user triggers action:\n\n```javascript\nimport { LightningElement } from 'lwc';\nimport fetchAccounts from '@salesforce/apex/AccountController.fetchAccounts';\n\nexport default class ImperativeExample extends LightningElement {\n selectedIndustry = 'Technology';\n accounts;\n error;\n isLoading = false;\n\n // Called explicitly when user clicks button or submits form\n async fetchAccounts() {\n this.isLoading = true;\n try {\n this.accounts = await fetchAccounts({\n industry: this.selectedIndustry\n });\n this.error = undefined;\n } catch (error) {\n this.error = error;\n this.accounts = undefined;\n } finally {\n this.isLoading = false;\n }\n }\n}\n```\n\n#### Decision Tree\n\n```\n ┌─────────────────────────────┐\n │ Need to modify data? │\n │ (Insert/Update/Delete) │\n └─────────────┬───────────────┘\n │\n ┌─────────────┴───────────────┐\n │ │\n YES NO\n │ │\n ▼ ▼\n ┌─────────────────┐ ┌─────────────────────────┐\n │ IMPERATIVE │ │ Should data auto- │\n │ (Use await) │ │ refresh on param │\n └─────────────────┘ │ change? │\n └───────────┬─────────────┘\n │\n ┌───────────┴───────────┐\n │ │\n YES NO\n │ │\n ▼ ▼\n ┌─────────────────┐ ┌─────────────────┐\n │ @WIRE │ │ IMPERATIVE │\n │ (Reactive) │ │ (On-demand) │\n └─────────────────┘ └─────────────────┘\n```\n\n---\n\n### 1. Basic Data Display (Wire Service)\n\n```javascript\n// accountCard.js\nimport { LightningElement, api, wire } from 'lwc';\nimport { getRecord, getFieldValue } from 'lightning/uiRecordApi';\nimport NAME_FIELD from '@salesforce/schema/Account.Name';\nimport INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';\n\nconst FIELDS = [NAME_FIELD, INDUSTRY_FIELD];\n\nexport default class AccountCard extends LightningElement {\n @api recordId;\n\n @wire(getRecord, { recordId: '$recordId', fields: FIELDS })\n account;\n\n get name() {\n return getFieldValue(this.account.data, NAME_FIELD);\n }\n\n get industry() {\n return getFieldValue(this.account.data, INDUSTRY_FIELD);\n }\n\n get isLoading() {\n return !this.account.data && !this.account.error;\n }\n}\n```\n\n```html\n\u003c!-- accountCard.html -->\n\u003ctemplate>\n \u003ctemplate lwc:if={isLoading}>\n \u003clightning-spinner alternative-text=\"Loading\">\u003c/lightning-spinner>\n \u003c/template>\n \u003ctemplate lwc:if={account.data}>\n \u003cdiv class=\"slds-box slds-theme_default\">\n \u003ch2 class=\"slds-text-heading_medium\">{name}\u003c/h2>\n \u003cp class=\"slds-text-color_weak\">{industry}\u003c/p>\n \u003c/div>\n \u003c/template>\n \u003ctemplate lwc:if={account.error}>\n \u003cp class=\"slds-text-color_error\">{account.error.body.message}\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### 2. Wire Service with Apex\n\n```javascript\n// contactList.js\nimport { LightningElement, api, wire } from 'lwc';\nimport getContacts from '@salesforce/apex/ContactController.getContacts';\nimport { refreshApex } from '@salesforce/apex';\n\nexport default class ContactList extends LightningElement {\n @api recordId;\n contacts;\n error;\n wiredContactsResult;\n\n @wire(getContacts, { accountId: '$recordId' })\n wiredContacts(result) {\n this.wiredContactsResult = result; // Store for refreshApex\n const { error, data } = result;\n if (data) {\n this.contacts = data;\n this.error = undefined;\n } else if (error) {\n this.error = error;\n this.contacts = undefined;\n }\n }\n\n async handleRefresh() {\n await refreshApex(this.wiredContactsResult);\n }\n}\n```\n\n---\n\n## GraphQL Patterns\n\n> **Module Note**: `lightning/graphql` supersedes `lightning/uiGraphQLApi` and provides newer features like mutations, optional fields, and dynamic query construction.\n\n### GraphQL Query (Wire Adapter)\n\n```javascript\n// graphqlContacts.js\nimport { LightningElement, wire } from 'lwc';\nimport { gql, graphql } from 'lightning/graphql';\n\nconst CONTACTS_QUERY = gql`\n query ContactsQuery($first: Int, $after: String) {\n uiapi {\n query {\n Contact(first: $first, after: $after) {\n edges {\n node {\n Id\n Name { value }\n Email { value }\n Account {\n Name { value }\n }\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n }\n`;\n\nexport default class GraphqlContacts extends LightningElement {\n contacts;\n pageInfo;\n error;\n _cursor;\n\n @wire(graphql, {\n query: CONTACTS_QUERY,\n variables: '$queryVariables'\n })\n wiredContacts({ data, error }) {\n if (data) {\n const result = data.uiapi.query.Contact;\n this.contacts = result.edges.map(edge => ({\n id: edge.node.Id,\n name: edge.node.Name.value,\n email: edge.node.Email?.value,\n accountName: edge.node.Account?.Name?.value\n }));\n this.pageInfo = result.pageInfo;\n } else if (error) {\n this.error = error;\n }\n }\n\n get queryVariables() {\n return { first: 10, after: this._cursor };\n }\n\n loadMore() {\n if (this.pageInfo?.hasNextPage) {\n this._cursor = this.pageInfo.endCursor;\n }\n }\n}\n```\n\n### GraphQL Mutations (Spring '26 - GA in API 66.0)\n\nMutations allow create, update, and delete operations via GraphQL. Use `executeMutation` for imperative operations.\n\n```javascript\n// graphqlAccountMutation.js\nimport { LightningElement, track } from 'lwc';\nimport { gql, executeMutation } from 'lightning/graphql';\nimport { ShowToastEvent } from 'lightning/platformShowToastEvent';\n\n// Create mutation\nconst CREATE_ACCOUNT = gql`\n mutation CreateAccount($name: String!, $industry: String) {\n uiapi {\n AccountCreate(input: {\n Account: {\n Name: $name\n Industry: $industry\n }\n }) {\n Record {\n Id\n Name { value }\n Industry { value }\n }\n }\n }\n }\n`;\n\n// Update mutation\nconst UPDATE_ACCOUNT = gql`\n mutation UpdateAccount($id: ID!, $name: String!) {\n uiapi {\n AccountUpdate(input: {\n Account: {\n Id: $id\n Name: $name\n }\n }) {\n Record {\n Id\n Name { value }\n }\n }\n }\n }\n`;\n\n// Delete mutation\nconst DELETE_ACCOUNT = gql`\n mutation DeleteAccount($id: ID!) {\n uiapi {\n AccountDelete(input: { Account: { Id: $id } }) {\n Id\n }\n }\n }\n`;\n\nexport default class GraphqlAccountMutation extends LightningElement {\n @track accountName = '';\n @track industry = '';\n isLoading = false;\n\n handleNameChange(event) {\n this.accountName = event.target.value;\n }\n\n handleIndustryChange(event) {\n this.industry = event.target.value;\n }\n\n async handleCreate() {\n if (!this.accountName) return;\n\n this.isLoading = true;\n try {\n const result = await executeMutation(CREATE_ACCOUNT, {\n variables: {\n name: this.accountName,\n industry: this.industry || null\n }\n });\n\n const newRecord = result.data.uiapi.AccountCreate.Record;\n this.showToast('Success', `Account \"${newRecord.Name.value}\" created`, 'success');\n this.resetForm();\n } catch (error) {\n this.handleError(error);\n } finally {\n this.isLoading = false;\n }\n }\n\n async handleUpdate(accountId, newName) {\n try {\n const result = await executeMutation(UPDATE_ACCOUNT, {\n variables: { id: accountId, name: newName }\n });\n this.showToast('Success', 'Account updated', 'success');\n return result.data.uiapi.AccountUpdate.Record;\n } catch (error) {\n this.handleError(error);\n }\n }\n\n async handleDelete(accountId) {\n try {\n await executeMutation(DELETE_ACCOUNT, {\n variables: { id: accountId }\n });\n this.showToast('Success', 'Account deleted', 'success');\n } catch (error) {\n this.handleError(error);\n }\n }\n\n handleError(error) {\n const message = error.graphQLErrors\n ? error.graphQLErrors.map(e => e.message).join(', ')\n : error.message || 'Unknown error';\n this.showToast('Error', message, 'error');\n }\n\n showToast(title, message, variant) {\n this.dispatchEvent(new ShowToastEvent({ title, message, variant }));\n }\n\n resetForm() {\n this.accountName = '';\n this.industry = '';\n }\n}\n```\n\n### GraphQL Mutation Operations\n\n| Operation | Mutation Type | Notes |\n|-----------|---------------|-------|\n| **Create** | `{Object}Create` | Can request fields from newly created record |\n| **Update** | `{Object}Update` | Cannot query fields in same request |\n| **Delete** | `{Object}Delete` | Cannot query fields in same request |\n\n### allOrNone Parameter\n\nControl transaction behavior with `allOrNone` (default: `true`):\n\n```javascript\nconst BATCH_CREATE = gql`\n mutation BatchCreate($allOrNone: Boolean = true) {\n uiapi(allOrNone: $allOrNone) {\n acc1: AccountCreate(input: { Account: { Name: \"Account 1\" } }) {\n Record { Id }\n }\n acc2: AccountCreate(input: { Account: { Name: \"Account 2\" } }) {\n Record { Id }\n }\n }\n }\n`;\n\n// If allOrNone=true: All rollback if any fails\n// If allOrNone=false: Only failed operations rollback\n```\n\n---\n\n## Modal Component Pattern\n\nBased on [James Simone's composable modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/).\n\n```javascript\n// composableModal.js\nimport { LightningElement, api } from 'lwc';\n\nconst OUTER_MODAL_CLASS = 'outerModalContent';\n\nexport default class ComposableModal extends LightningElement {\n @api modalHeader;\n @api modalTagline;\n @api modalSaveHandler;\n\n _isOpen = false;\n _focusableElements = [];\n\n @api\n toggleModal() {\n this._isOpen = !this._isOpen;\n if (this._isOpen) {\n this._focusableElements = [...this.querySelectorAll('.focusable')];\n this._focusFirstElement();\n window.addEventListener('keyup', this._handleKeyUp);\n } else {\n window.removeEventListener('keyup', this._handleKeyUp);\n }\n }\n\n get modalAriaHidden() {\n return !this._isOpen;\n }\n\n get modalClass() {\n return this._isOpen\n ? 'slds-modal slds-visible slds-fade-in-open'\n : 'slds-modal slds-hidden';\n }\n\n get backdropClass() {\n return this._isOpen ? 'slds-backdrop slds-backdrop_open' : 'slds-backdrop';\n }\n\n _handleKeyUp = (event) => {\n if (event.code === 'Escape') {\n this.toggleModal();\n } else if (event.code === 'Tab') {\n this._handleTabNavigation(event);\n }\n }\n\n _handleTabNavigation(event) {\n // Focus trap logic - keep focus within modal\n const activeEl = this.template.activeElement;\n const lastIndex = this._focusableElements.length - 1;\n const currentIndex = this._focusableElements.indexOf(activeEl);\n\n if (event.shiftKey && currentIndex === 0) {\n this._focusableElements[lastIndex]?.focus();\n } else if (!event.shiftKey && currentIndex === lastIndex) {\n this._focusFirstElement();\n }\n }\n\n _focusFirstElement() {\n if (this._focusableElements.length > 0) {\n this._focusableElements[0].focus();\n }\n }\n\n handleBackdropClick(event) {\n if (event.target.classList.contains(OUTER_MODAL_CLASS)) {\n this.toggleModal();\n }\n }\n\n handleSave() {\n if (this.modalSaveHandler) {\n this.modalSaveHandler();\n }\n this.toggleModal();\n }\n\n disconnectedCallback() {\n window.removeEventListener('keyup', this._handleKeyUp);\n }\n}\n```\n\n```html\n\u003c!-- composableModal.html -->\n\u003ctemplate>\n \u003c!-- Backdrop -->\n \u003cdiv class={backdropClass}>\u003c/div>\n\n \u003c!-- Modal -->\n \u003cdiv class={modalClass}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-hidden={modalAriaHidden}\n aria-labelledby=\"modal-heading\">\n\n \u003cdiv class=\"slds-modal__container outerModalContent\"\n onclick={handleBackdropClick}>\n\n \u003cdiv class=\"slds-modal__content slds-p-around_medium\">\n \u003c!-- Header -->\n \u003ctemplate lwc:if={modalHeader}>\n \u003ch2 id=\"modal-heading\" class=\"slds-text-heading_medium\">\n {modalHeader}\n \u003c/h2>\n \u003c/template>\n \u003ctemplate lwc:if={modalTagline}>\n \u003cp class=\"slds-m-top_x-small slds-text-color_weak\">\n {modalTagline}\n \u003c/p>\n \u003c/template>\n\n \u003c!-- Slotted Content -->\n \u003cdiv class=\"slds-m-top_medium\">\n \u003cslot name=\"modalContent\">\u003c/slot>\n \u003c/div>\n\n \u003c!-- Footer -->\n \u003cdiv class=\"slds-m-top_medium slds-text-align_right\">\n \u003clightning-button\n label=\"Cancel\"\n onclick={toggleModal}\n class=\"slds-m-right_x-small focusable\">\n \u003c/lightning-button>\n \u003clightning-button\n variant=\"brand\"\n label=\"Save\"\n onclick={handleSave}\n class=\"focusable\">\n \u003c/lightning-button>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- Hidden background content -->\n \u003cdiv aria-hidden={_isOpen}>\n \u003cslot name=\"body\">\u003c/slot>\n \u003c/div>\n\u003c/template>\n```\n\n---\n\n## Record Picker Pattern\n\n```javascript\n// recordPicker.js\nimport { LightningElement, api } from 'lwc';\n\nexport default class RecordPicker extends LightningElement {\n @api label = 'Select Record';\n @api objectApiName = 'Account';\n @api placeholder = 'Search...';\n @api required = false;\n @api multiSelect = false;\n\n _selectedIds = [];\n\n @api\n get value() {\n return this.multiSelect ? this._selectedIds : this._selectedIds[0];\n }\n\n set value(val) {\n this._selectedIds = Array.isArray(val) ? val : val ? [val] : [];\n }\n\n handleChange(event) {\n const recordId = event.detail.recordId;\n if (this.multiSelect) {\n if (!this._selectedIds.includes(recordId)) {\n this._selectedIds = [...this._selectedIds, recordId];\n }\n } else {\n this._selectedIds = recordId ? [recordId] : [];\n }\n\n this.dispatchEvent(new CustomEvent('select', {\n detail: {\n recordId: this.value,\n recordIds: this._selectedIds\n }\n }));\n }\n\n handleRemove(event) {\n const idToRemove = event.target.dataset.id;\n this._selectedIds = this._selectedIds.filter(id => id !== idToRemove);\n this.dispatchEvent(new CustomEvent('select', {\n detail: { recordIds: this._selectedIds }\n }));\n }\n}\n```\n\n```html\n\u003c!-- recordPicker.html -->\n\u003ctemplate>\n \u003clightning-record-picker\n label={label}\n placeholder={placeholder}\n object-api-name={objectApiName}\n onchange={handleChange}\n required={required}>\n \u003c/lightning-record-picker>\n\n \u003ctemplate lwc:if={multiSelect}>\n \u003cdiv class=\"slds-m-top_x-small\">\n \u003ctemplate for:each={_selectedIds} for:item=\"id\">\n \u003clightning-pill\n key={id}\n label={id}\n data-id={id}\n onremove={handleRemove}>\n \u003c/lightning-pill>\n \u003c/template>\n \u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n---\n\n## Workspace API Pattern\n\n```javascript\n// workspaceTabManager.js\nimport { LightningElement, wire } from 'lwc';\nimport { IsConsoleNavigation, getFocusedTabInfo, openTab, closeTab,\n setTabLabel, setTabIcon, refreshTab } from 'lightning/platformWorkspaceApi';\n\nexport default class WorkspaceTabManager extends LightningElement {\n @wire(IsConsoleNavigation) isConsole;\n\n async openRecordTab(recordId, objectApiName) {\n if (!this.isConsole) return;\n\n await openTab({\n recordId,\n focus: true,\n icon: `standard:${objectApiName.toLowerCase()}`,\n label: 'Loading...'\n });\n }\n\n async openSubtab(parentTabId, recordId) {\n if (!this.isConsole) return;\n\n await openTab({\n parentTabId,\n recordId,\n focus: true\n });\n }\n\n async getCurrentTabInfo() {\n if (!this.isConsole) return null;\n return await getFocusedTabInfo();\n }\n\n async updateTabLabel(tabId, label) {\n if (!this.isConsole) return;\n await setTabLabel(tabId, label);\n }\n\n async updateTabIcon(tabId, iconName) {\n if (!this.isConsole) return;\n await setTabIcon(tabId, iconName);\n }\n\n async refreshCurrentTab() {\n if (!this.isConsole) return;\n const tabInfo = await getFocusedTabInfo();\n await refreshTab(tabInfo.tabId);\n }\n\n async closeCurrentTab() {\n if (!this.isConsole) return;\n const tabInfo = await getFocusedTabInfo();\n await closeTab(tabInfo.tabId);\n }\n}\n```\n\n---\n\n\u003ca id=\"parent-child-communication\">\u003c/a>\n\n## Parent-Child Communication\n\n```javascript\n// parent.js\nimport { LightningElement } from 'lwc';\n\nexport default class Parent extends LightningElement {\n selectedAccountId;\n\n handleAccountSelected(event) {\n this.selectedAccountId = event.detail.accountId;\n }\n}\n```\n\n```html\n\u003c!-- parent.html -->\n\u003ctemplate>\n \u003cc-account-list onaccountselected={handleAccountSelected}>\u003c/c-account-list>\n \u003ctemplate lwc:if={selectedAccountId}>\n \u003cc-account-detail account-id={selectedAccountId}>\u003c/c-account-detail>\n \u003c/template>\n\u003c/template>\n```\n\n```javascript\n// child.js (accountList)\nimport { LightningElement } from 'lwc';\n\nexport default class AccountList extends LightningElement {\n handleRowAction(event) {\n const accountId = event.detail.row.Id;\n\n // Dispatch event to parent\n this.dispatchEvent(new CustomEvent('accountselected', {\n detail: { accountId },\n bubbles: true,\n composed: false // Don't cross shadow DOM boundaries\n }));\n }\n}\n```\n\n---\n\n## Sibling Communication (via Parent)\n\nWhen two child components need to communicate but share the same parent, use the **parent as middleware**. This is the recommended pattern for master-detail UIs.\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ SIBLING COMMUNICATION FLOW │\n├─────────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────┐ │\n│ │ Parent │ ← Manages state │\n│ └────┬─────┘ │\n│ ┌─────────┴─────────┐ │\n│ │ │ │\n│ CustomEvent @api property │\n│ (up) (down) │\n│ │ │ │\n│ ┌─────┴─────┐ ┌─────┴─────┐ │\n│ │ Child A │ │ Child B │ │\n│ │ (List) │ │ (Detail) │ │\n│ └───────────┘ └───────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n**The flow**:\n1. **Child A** dispatches a custom event (e.g., user selects an account)\n2. **Parent** catches the event and updates its state\n3. **Parent** passes data to **Child B** via `@api` property\n\n### Complete Example: Account List → Account Detail\n\n```javascript\n// accountContainer.js - Parent orchestrates communication between siblings\nimport { LightningElement } from 'lwc';\n\nexport default class AccountContainer extends LightningElement {\n // State managed at parent level\n selectedAccountId;\n selectedAccountName;\n\n // Child A (accountList) fires this event\n handleAccountSelect(event) {\n this.selectedAccountId = event.detail.accountId;\n this.selectedAccountName = event.detail.accountName;\n }\n\n // Clear selection (triggered by Child B)\n handleClearSelection() {\n this.selectedAccountId = null;\n this.selectedAccountName = null;\n }\n\n get hasSelection() {\n return !!this.selectedAccountId;\n }\n}\n```\n\n```html\n\u003c!-- accountContainer.html -->\n\u003ctemplate>\n \u003cdiv class=\"slds-grid slds-gutters\">\n \u003c!-- Child A: Account List -->\n \u003cdiv class=\"slds-col slds-size_1-of-2\">\n \u003cc-account-list\n onaccountselect={handleAccountSelect}\n selected-id={selectedAccountId}>\n \u003c/c-account-list>\n \u003c/div>\n\n \u003c!-- Child B: Account Detail (receives data via @api) -->\n \u003cdiv class=\"slds-col slds-size_1-of-2\">\n \u003ctemplate lwc:if={hasSelection}>\n \u003cc-account-detail\n account-id={selectedAccountId}\n account-name={selectedAccountName}\n onclearselection={handleClearSelection}>\n \u003c/c-account-detail>\n \u003c/template>\n \u003ctemplate lwc:else>\n \u003cdiv class=\"slds-box slds-theme_shade\">\n Select an account to view details\n \u003c/div>\n \u003c/template>\n \u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n```javascript\n// accountList.js - Child A: Dispatches events UP to parent\nimport { LightningElement, api, wire } from 'lwc';\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\n\nexport default class AccountList extends LightningElement {\n @api selectedId; // Highlight selected row (from parent)\n accounts;\n error;\n\n @wire(getAccounts)\n wiredAccounts({ data, error }) {\n if (data) {\n this.accounts = data;\n this.error = undefined;\n } else if (error) {\n this.error = error;\n this.accounts = undefined;\n }\n }\n\n handleRowClick(event) {\n const accountId = event.currentTarget.dataset.id;\n const accountName = event.currentTarget.dataset.name;\n\n // Dispatch event to parent (not bubbles - parent listens directly)\n this.dispatchEvent(new CustomEvent('accountselect', {\n detail: { accountId, accountName }\n }));\n }\n\n // Computed: Check if row should be highlighted\n getRowClass(accountId) {\n return accountId === this.selectedId\n ? 'slds-item slds-is-selected'\n : 'slds-item';\n }\n}\n```\n\n```javascript\n// accountDetail.js - Child B: Receives data via @api from parent\nimport { LightningElement, api, wire } from 'lwc';\nimport { getRecord, getFieldValue } from 'lightning/uiRecordApi';\nimport INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';\nimport REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';\n\nconst FIELDS = [INDUSTRY_FIELD, REVENUE_FIELD];\n\nexport default class AccountDetail extends LightningElement {\n @api accountId; // Received from parent\n @api accountName; // Received from parent\n\n @wire(getRecord, { recordId: '$accountId', fields: FIELDS })\n account;\n\n get industry() {\n return getFieldValue(this.account.data, INDUSTRY_FIELD);\n }\n\n get revenue() {\n return getFieldValue(this.account.data, REVENUE_FIELD);\n }\n\n get isLoading() {\n return !this.account.data && !this.account.error;\n }\n\n handleClose() {\n // Dispatch event back to parent to clear selection\n this.dispatchEvent(new CustomEvent('clearselection'));\n }\n}\n```\n\n### When to Use Sibling Pattern vs LMS\n\n| Scenario | Sibling Pattern | LMS |\n|----------|-----------------|-----|\n| Components share same parent | ✅ Recommended | ❌ Overkill |\n| State is simple (1-2 values) | ✅ | ❌ |\n| Need bidirectional updates | ✅ | ✅ |\n| Components in different DOM trees | ❌ | ✅ Required |\n| Cross-framework (LWC ↔ Aura) | ❌ | ✅ Required |\n| Many consumers need same data | ❌ Consider LMS | ✅ |\n| Component hierarchy is deep (4+ levels) | ❌ Consider LMS | ✅ |\n\n**Rule of thumb**: If components share a parent and data flow is simple, use sibling pattern. If components are \"far apart\" in the DOM or you need pub/sub semantics, use LMS.\n\n---\n\n## Navigation Patterns\n\n```javascript\n// navigator.js\nimport { LightningElement } from 'lwc';\nimport { NavigationMixin } from 'lightning/navigation';\n\nexport default class Navigator extends NavigationMixin(LightningElement) {\n\n navigateToRecord(recordId, objectApiName = 'Account') {\n this[NavigationMixin.Navigate]({\n type: 'standard__recordPage',\n attributes: {\n recordId,\n objectApiName,\n actionName: 'view'\n }\n });\n }\n\n navigateToList(objectApiName, filterName = 'Recent') {\n this[NavigationMixin.Navigate]({\n type: 'standard__objectPage',\n attributes: {\n objectApiName,\n actionName: 'list'\n },\n state: { filterName }\n });\n }\n\n navigateToNewRecord(objectApiName, defaultValues = {}) {\n this[NavigationMixin.Navigate]({\n type: 'standard__objectPage',\n attributes: {\n objectApiName,\n actionName: 'new'\n },\n state: {\n defaultFieldValues: Object.entries(defaultValues)\n .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)\n .join(',')\n }\n });\n }\n\n navigateToRelatedList(recordId, relationshipApiName) {\n this[NavigationMixin.Navigate]({\n type: 'standard__recordRelationshipPage',\n attributes: {\n recordId,\n relationshipApiName,\n actionName: 'view'\n }\n });\n }\n\n navigateToNamedPage(pageName, params = {}) {\n this[NavigationMixin.Navigate]({\n type: 'standard__namedPage',\n attributes: {\n pageName\n },\n state: params\n });\n }\n}\n```\n\n---\n\n## TypeScript Patterns\n\n### TypeScript Component Pattern\n\n```typescript\n// accountList.ts\nimport { LightningElement, api, wire, track } from 'lwc';\nimport { getRecord, getFieldValue } from 'lightning/uiRecordApi';\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\nimport ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';\n\n// Define interfaces for type safety\ninterface AccountRecord {\n Id: string;\n Name: string;\n Industry?: string;\n AnnualRevenue?: number;\n}\n\ninterface WireResult\u003cT> {\n data?: T;\n error?: Error;\n}\n\nexport default class AccountList extends LightningElement {\n // Typed @api properties\n @api recordId: string | undefined;\n\n @api\n get maxRecords(): number {\n return this._maxRecords;\n }\n set maxRecords(value: number) {\n this._maxRecords = value;\n }\n\n // Typed @track properties\n @track private _accounts: AccountRecord[] = [];\n @track private _error: string | null = null;\n\n private _maxRecords: number = 10;\n private _wiredResult: WireResult\u003cAccountRecord[]> | undefined;\n\n // Typed wire service\n @wire(getAccounts, { maxRecords: '$maxRecords' })\n wiredAccounts(result: WireResult\u003cAccountRecord[]>): void {\n this._wiredResult = result;\n const { data, error } = result;\n\n if (data) {\n this._accounts = data;\n this._error = null;\n } else if (error) {\n this._error = this.reduceErrors(error);\n this._accounts = [];\n }\n }\n\n // Typed getters\n get accounts(): AccountRecord[] {\n return this._accounts;\n }\n\n get hasAccounts(): boolean {\n return this._accounts.length > 0;\n }\n\n // Typed event handlers\n handleSelect(event: CustomEvent\u003c{ accountId: string }>): void {\n const { accountId } = event.detail;\n this.dispatchEvent(new CustomEvent('accountselected', {\n detail: { accountId },\n bubbles: true,\n composed: true\n }));\n }\n\n // Typed utility methods\n private reduceErrors(error: Error | Error[]): string {\n const errors = Array.isArray(error) ? error : [error];\n return errors\n .filter((e): e is Error => e !== null)\n .map(e => e.message || 'Unknown error')\n .join('; ');\n }\n}\n```\n\n### TypeScript Jest Test Pattern\n\n```typescript\n// accountList.test.ts\nimport { createElement, LightningElement } from 'lwc';\nimport AccountList from 'c/accountList';\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\n\n// Type definitions for tests\ninterface AccountRecord {\n Id: string;\n Name: string;\n Industry?: string;\n}\n\n// Mock Apex\njest.mock(\n '@salesforce/apex/AccountController.getAccounts',\n () => ({ default: jest.fn() }),\n { virtual: true }\n);\n\nconst MOCK_ACCOUNTS: AccountRecord[] = [\n { Id: '001xx000003DGQ', Name: 'Acme Corp', Industry: 'Technology' }\n];\n\ndescribe('c-account-list', () => {\n let element: LightningElement & { maxRecords?: number };\n\n afterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n it('displays accounts after data loads', async () => {\n (getAccounts as jest.Mock).mockResolvedValue(MOCK_ACCOUNTS);\n\n element = createElement('c-account-list', { is: AccountList });\n document.body.appendChild(element);\n\n await Promise.resolve();\n\n const items = element.shadowRoot?.querySelectorAll('.slds-item');\n expect(items?.length).toBe(MOCK_ACCOUNTS.length);\n });\n});\n```\n\n### TypeScript Features for LWC\n\n| Feature | LWC Support | Notes |\n|---------|-------------|-------|\n| **Interface definitions** | ✅ | Define shapes for records, events, props |\n| **Typed @api properties** | ✅ | Getter/setter patterns with types |\n| **Typed @wire results** | ✅ | Generic `WireResult\u003cT>` pattern |\n| **Typed event handlers** | ✅ | `CustomEvent\u003cT>` for event detail typing |\n| **Private class fields** | ✅ | Use `private` keyword |\n| **Strict null checking** | ✅ | Optional chaining `?.` and nullish coalescing `??` |\n\n---\n\n## Apex Controller Patterns\n\n### Cacheable Methods (for @wire)\n\n```apex\npublic with sharing class LwcController {\n\n @AuraEnabled(cacheable=true)\n public static List\u003cAccount> getAccounts(String searchTerm) {\n String searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%';\n return [\n SELECT Id, Name, Industry, AnnualRevenue\n FROM Account\n WHERE Name LIKE :searchKey\n WITH SECURITY_ENFORCED\n ORDER BY Name\n LIMIT 50\n ];\n }\n\n @AuraEnabled(cacheable=true)\n public static List\u003cPicklistOption> getIndustryOptions() {\n List\u003cPicklistOption> options = new List\u003cPicklistOption>();\n Schema.DescribeFieldResult fieldResult =\n Account.Industry.getDescribe();\n for (Schema.PicklistEntry entry : fieldResult.getPicklistValues()) {\n if (entry.isActive()) {\n options.add(new PicklistOption(entry.getLabel(), entry.getValue()));\n }\n }\n return options;\n }\n\n public class PicklistOption {\n @AuraEnabled public String label;\n @AuraEnabled public String value;\n\n public PicklistOption(String label, String value) {\n this.label = label;\n this.value = value;\n }\n }\n}\n```\n\n### Non-Cacheable Methods (for DML)\n\n```apex\n@AuraEnabled\npublic static Account createAccount(String accountJson) {\n Account acc = (Account) JSON.deserialize(accountJson, Account.class);\n\n // FLS check\n SObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.CREATABLE,\n new List\u003cAccount>{ acc }\n );\n\n insert decision.getRecords();\n return (Account) decision.getRecords()[0];\n}\n\n@AuraEnabled\npublic static void deleteAccounts(List\u003cId> accountIds) {\n if (accountIds == null || accountIds.isEmpty()) {\n throw new AuraHandledException('No accounts to delete');\n }\n\n List\u003cAccount> toDelete = [\n SELECT Id FROM Account\n WHERE Id IN :accountIds\n WITH SECURITY_ENFORCED\n ];\n\n delete toDelete;\n}\n```\n\n### Error Handling Pattern\n\n```apex\n@AuraEnabled\npublic static List\u003cContact> getContactsWithErrorHandling(Id accountId) {\n try {\n if (accountId == null) {\n throw new AuraHandledException('Account ID is required');\n }\n\n List\u003cContact> contacts = [\n SELECT Id, Name, Email, Phone\n FROM Contact\n WHERE AccountId = :accountId\n WITH SECURITY_ENFORCED\n ORDER BY Name\n LIMIT 100\n ];\n\n return contacts;\n } catch (Exception e) {\n throw new AuraHandledException('Error fetching contacts: ' + e.getMessage());\n }\n}\n```\n\n---\n\n## Related Resources\n\n- [lms-guide.md](lms-guide.md) - Lightning Message Service deep dive\n- [jest-testing.md](jest-testing.md) - Advanced testing patterns\n- [accessibility-guide.md](accessibility-guide.md) - WCAG compliance\n- [performance-guide.md](performance-guide.md) - Optimization techniques\n\n---\n\n## External References\n\n- [PICKLES Framework](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/) — David Picksley, Third Eye Consulting\n- [LWC Recipes (GitHub)](https://github.com/trailheadapps/lwc-recipes)\n- [James Simone - Composable Modal](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":45558,"content_sha256":"ed18e31b9570058f46d2b08e7a2c3a8bf0c39bde87da08b31dd6d97d980bfc95"},{"filename":"references/flow-integration-guide.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# LWC Flow Integration Guide\n\nThis guide covers building Lightning Web Components for use in Salesforce Flow Screens.\n\n---\n\n## Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ FLOW ↔ LWC COMMUNICATION │\n├─────────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────────┐ ┌─────────────────┐ │\n│ │ FLOW │ │ LWC │ │\n│ │ Variables │ │ Component │ │\n│ └────────┬────────┘ └────────┬────────┘ │\n│ │ │ │\n│ │ @api (inputOnly) │ │\n│ ├─────────────────────────────────────▶│ │\n│ │ │ │\n│ │ FlowAttributeChangeEvent (outputOnly)│ │\n│ │◀─────────────────────────────────────┤ │\n│ │ │ │\n│ │ FlowNavigationFinishEvent │ │\n│ │◀─────────────────────────────────────┤ │\n│ │ (NEXT, BACK, FINISH, PAUSE) │ │\n│ │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Quick Reference\n\n| Direction | Mechanism | Use Case |\n|-----------|-----------|----------|\n| Flow → LWC | `@api` with `role=\"inputOnly\"` | Pass context data to component |\n| LWC → Flow | `FlowAttributeChangeEvent` | Return user selections/data |\n| LWC → Navigation | `FlowNavigationFinishEvent` | Trigger Next/Back/Finish |\n\n---\n\n## Meta.xml Configuration\n\n### Target Configuration\n\n```xml\n\u003ctargets>\n \u003ctarget>lightning__FlowScreen\u003c/target>\n\u003c/targets>\n```\n\n### Property Roles\n\n```xml\n\u003ctargetConfig targets=\"lightning__FlowScreen\">\n \u003c!-- INPUT: Flow → Component -->\n \u003cproperty\n name=\"recordId\"\n type=\"String\"\n label=\"Record ID\"\n description=\"ID from Flow\"\n role=\"inputOnly\"/>\n\n \u003c!-- OUTPUT: Component → Flow -->\n \u003cproperty\n name=\"selectedValue\"\n type=\"String\"\n label=\"Selected Value\"\n description=\"User's selection\"\n role=\"outputOnly\"/>\n\u003c/targetConfig>\n```\n\n### Supported Property Types\n\n| Type | Description | Example |\n|------|-------------|---------|\n| `String` | Text values | Record IDs, names |\n| `Boolean` | True/false | Flags, completion status |\n| `Integer` | Whole numbers | Counts, indexes |\n| `Date` | Date values | Due dates |\n| `DateTime` | Date and time | Timestamps |\n| `@salesforce/schema/*` | SObject references | Record types |\n\n---\n\n## FlowAttributeChangeEvent\n\nThis is the **critical** mechanism for sending data back to Flow.\n\n### Import\n\n```javascript\nimport { FlowAttributeChangeEvent } from 'lightning/flowSupport';\n```\n\n### Usage\n\n```javascript\n// Dispatch event to update Flow variable\n// First param: @api property name (must match meta.xml exactly)\n// Second param: new value\nthis.dispatchEvent(new FlowAttributeChangeEvent(\n 'selectedRecordId', // Property name\n this.recordId // Value\n));\n```\n\n### Example: Selection Handler\n\n```javascript\n@api selectedRecordId;\n@api selectedRecordName;\n\nhandleSelect(event) {\n const id = event.target.dataset.id;\n const name = event.target.dataset.name;\n\n // Update local properties\n this.selectedRecordId = id;\n this.selectedRecordName = name;\n\n // Notify Flow of BOTH changes\n this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordId', id));\n this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordName', name));\n}\n```\n\n### Common Mistake\n\n```javascript\n// ❌ WRONG: Only updating local property\nthis.selectedRecordId = id;\n\n// ✅ CORRECT: Update AND dispatch event\nthis.selectedRecordId = id;\nthis.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordId', id));\n```\n\n---\n\n## FlowNavigationFinishEvent\n\nProgrammatically trigger Flow navigation from your component.\n\n### Import\n\n```javascript\nimport { FlowNavigationFinishEvent } from 'lightning/flowSupport';\n```\n\n### Navigation Actions\n\n| Action | Description | When Available |\n|--------|-------------|----------------|\n| `'NEXT'` | Go to next screen | Mid-flow screens |\n| `'BACK'` | Go to previous screen | After first screen |\n| `'FINISH'` | Complete the flow | Final screens |\n| `'PAUSE'` | Pause flow (if enabled) | Pausable flows |\n\n### Usage\n\n```javascript\n// Navigate to next screen\nthis.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n\n// Navigate back\nthis.dispatchEvent(new FlowNavigationFinishEvent('BACK'));\n\n// Finish the flow\nthis.dispatchEvent(new FlowNavigationFinishEvent('FINISH'));\n```\n\n### Check Available Actions\n\nFlow provides available actions via a special `@api` property:\n\n```javascript\n// Automatically populated by Flow runtime\n@api availableActions = [];\n\nget canGoNext() {\n return this.availableActions.includes('NEXT');\n}\n\nget canGoBack() {\n return this.availableActions.includes('BACK');\n}\n\nhandleNext() {\n if (this.canGoNext) {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n}\n```\n\n### Conditional Navigation Buttons\n\n```html\n\u003ctemplate lwc:if={canGoBack}>\n \u003clightning-button label=\"Back\" onclick={handleBack}>\u003c/lightning-button>\n\u003c/template>\n\n\u003ctemplate lwc:if={canGoNext}>\n \u003clightning-button label=\"Next\" variant=\"brand\" onclick={handleNext}>\u003c/lightning-button>\n\u003c/template>\n```\n\n---\n\n## Validation Before Navigation\n\nAlways validate before allowing navigation:\n\n```javascript\nhandleNext() {\n // Validate\n if (!this.selectedRecordId) {\n this.errorMessage = 'Please select a record.';\n this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', this.errorMessage));\n return;\n }\n\n // Clear error\n this.errorMessage = null;\n this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', null));\n\n // Mark complete and navigate\n this.isComplete = true;\n this.dispatchEvent(new FlowAttributeChangeEvent('isComplete', true));\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n}\n```\n\n---\n\n## Apex Integration in Flow Context\n\n### Wire Service\n\n```javascript\nimport { wire } from 'lwc';\nimport getRecords from '@salesforce/apex/MyController.getRecords';\n\n@api recordId; // From Flow\n\n@wire(getRecords, { parentId: '$recordId' })\nwiredRecords({ error, data }) {\n if (data) {\n this.records = data;\n } else if (error) {\n this.error = this.reduceErrors(error);\n }\n}\n```\n\n### Imperative Calls\n\n```javascript\nimport processRecord from '@salesforce/apex/MyController.processRecord';\n\nasync handleProcess() {\n this.isLoading = true;\n try {\n const result = await processRecord({\n recordId: this.selectedRecordId\n });\n\n if (result.success) {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n } else {\n this.errorMessage = result.message;\n this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', result.message));\n }\n } catch (error) {\n this.errorMessage = this.reduceErrors(error);\n } finally {\n this.isLoading = false;\n }\n}\n```\n\n---\n\n## Flow Context Variables\n\nFlow provides special context via reserved variable names:\n\n```xml\n\u003c!-- In Flow Builder, map these to your component -->\n\u003cproperty name=\"recordId\" value=\"{!$Record.Id}\"/>\n\u003cproperty name=\"objectApiName\" value=\"{!$Record.Object}\"/>\n```\n\n---\n\n## Testing LWC in Flows\n\n### Jest Testing\n\n```javascript\nimport { createElement } from 'lwc';\nimport FlowScreenComponent from 'c/flowScreenComponent';\nimport { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';\n\n// Mock the flow support module\njest.mock('lightning/flowSupport', () => ({\n FlowAttributeChangeEvent: jest.fn(),\n FlowNavigationFinishEvent: jest.fn()\n}), { virtual: true });\n\ndescribe('c-flow-screen-component', () => {\n afterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n it('dispatches FlowAttributeChangeEvent on selection', async () => {\n const element = createElement('c-flow-screen-component', {\n is: FlowScreenComponent\n });\n element.availableActions = ['NEXT', 'BACK'];\n document.body.appendChild(element);\n\n // Simulate selection\n const tile = element.shadowRoot.querySelector('.record-tile');\n tile.click();\n\n // Verify event dispatched\n expect(FlowAttributeChangeEvent).toHaveBeenCalled();\n });\n\n it('dispatches FlowNavigationFinishEvent on next', async () => {\n const element = createElement('c-flow-screen-component', {\n is: FlowScreenComponent\n });\n element.availableActions = ['NEXT'];\n element.selectedRecordId = '001xx000000001';\n document.body.appendChild(element);\n\n // Click next button\n const nextButton = element.shadowRoot.querySelector('lightning-button[label=\"Next\"]');\n nextButton.click();\n\n // Verify navigation event\n expect(FlowNavigationFinishEvent).toHaveBeenCalledWith('NEXT');\n });\n});\n```\n\n### Manual Testing\n\n1. Create a Screen Flow in Setup\n2. Add your LWC component to a screen\n3. Map input/output variables\n4. Test in Flow debug mode\n5. Verify variable values in debug panel\n\n---\n\n## Common Patterns\n\n### Selection with Confirmation\n\n```javascript\nhandleSelect(event) {\n this.selectedId = event.target.dataset.id;\n // Don't navigate yet - wait for explicit confirmation\n}\n\nhandleConfirm() {\n if (!this.selectedId) {\n this.showError('Please select an item');\n return;\n }\n\n this.dispatchEvent(new FlowAttributeChangeEvent('selectedId', this.selectedId));\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n}\n```\n\n### Multi-Select to Collection\n\n```javascript\n@api selectedIds = [];\n\nhandleToggle(event) {\n const id = event.target.dataset.id;\n\n if (this.selectedIds.includes(id)) {\n this.selectedIds = this.selectedIds.filter(i => i !== id);\n } else {\n this.selectedIds = [...this.selectedIds, id];\n }\n\n // Send collection back to Flow\n this.dispatchEvent(new FlowAttributeChangeEvent('selectedIds', this.selectedIds));\n}\n```\n\n### Conditional Screen (Skip Logic)\n\n```javascript\nconnectedCallback() {\n // Auto-skip if condition met\n if (this.shouldSkip) {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n}\n```\n\n---\n\n## Troubleshooting\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| Output not updating in Flow | Missing FlowAttributeChangeEvent | Always dispatch event after updating @api property |\n| Navigation buttons not showing | Wrong availableActions | Check Flow provides availableActions correctly |\n| Component not appearing | Missing `isExposed: true` | Set in meta.xml |\n| Properties not mapping | Role mismatch | Use `inputOnly` for inputs, `outputOnly` for outputs |\n| Values reset on navigation | Local state not persisted | Use @api properties for all persisted data |\n\n---\n\n## Template\n\nUse the template at `assets/flow-screen-component/` as a starting point.\n\n---\n\n## Passing sObjects and Wrapper Classes to Flow\n\n### Overview\n\nFlow can receive complex Apex types through `apex://` type bindings. This enables:\n- Passing sObjects directly (not just IDs)\n- Passing wrapper/DTO classes with multiple fields\n- Two-way data binding for record editing\n\n### apex:// Type Syntax\n\nIn your meta.xml, reference Apex classes using the `apex://` prefix:\n\n```xml\n\u003ctargetConfig targets=\"lightning__FlowScreen\">\n \u003c!-- Pass entire Account record -->\n \u003cproperty\n name=\"accountRecord\"\n type=\"apex://Account\"\n label=\"Account Record\"\n role=\"inputOnly\"/>\n\n \u003c!-- Pass custom wrapper class -->\n \u003cproperty\n name=\"orderSummary\"\n type=\"apex://OrderController.OrderSummaryWrapper\"\n label=\"Order Summary\"\n role=\"inputOnly\"/>\n\n \u003c!-- Output a modified record -->\n \u003cproperty\n name=\"updatedAccount\"\n type=\"apex://Account\"\n label=\"Updated Account\"\n role=\"outputOnly\"/>\n\u003c/targetConfig>\n```\n\n### Wrapper Class Requirements\n\nApex wrapper classes must be **public** and have **public properties**:\n\n```apex\npublic class OrderController {\n\n // Wrapper class for Flow\n public class OrderSummaryWrapper {\n @AuraEnabled public String orderId;\n @AuraEnabled public String orderName;\n @AuraEnabled public Decimal totalAmount;\n @AuraEnabled public List\u003cLineItemWrapper> lineItems;\n @AuraEnabled public Account customer;\n }\n\n public class LineItemWrapper {\n @AuraEnabled public String productName;\n @AuraEnabled public Integer quantity;\n @AuraEnabled public Decimal unitPrice;\n }\n\n // Invocable method to create the wrapper\n @InvocableMethod(label='Get Order Summary')\n public static List\u003cOrderSummaryWrapper> getOrderSummary(List\u003cId> orderIds) {\n // Query and build wrapper...\n }\n}\n```\n\n### Using sObjects in LWC\n\n```javascript\nimport { api, LightningElement } from 'lwc';\nimport { FlowAttributeChangeEvent } from 'lightning/flowSupport';\n\nexport default class AccountEditor extends LightningElement {\n // Receive sObject from Flow\n @api accountRecord;\n\n // Track local modifications\n _modifiedAccount;\n\n connectedCallback() {\n // Create a working copy\n this._modifiedAccount = { ...this.accountRecord };\n }\n\n handleNameChange(event) {\n this._modifiedAccount.Name = event.target.value;\n }\n\n handleSave() {\n // Send modified record back to Flow\n this.dispatchEvent(\n new FlowAttributeChangeEvent('updatedAccount', this._modifiedAccount)\n );\n }\n}\n```\n\n### Using Wrapper Classes in LWC\n\n```javascript\nimport { api, LightningElement } from 'lwc';\n\nexport default class OrderSummaryViewer extends LightningElement {\n @api orderSummary; // apex://OrderController.OrderSummaryWrapper\n\n get formattedTotal() {\n return this.orderSummary?.totalAmount?.toLocaleString('en-US', {\n style: 'currency',\n currency: 'USD'\n });\n }\n\n get lineItems() {\n return this.orderSummary?.lineItems || [];\n }\n\n get customerName() {\n // Access nested sObject\n return this.orderSummary?.customer?.Name || 'Unknown';\n }\n}\n```\n\n### Flow Configuration for apex:// Types\n\n1. **Create an Invocable Action** that returns your wrapper:\n ```apex\n @InvocableMethod\n public static List\u003cMyWrapper> getData(List\u003cString> inputs) { ... }\n ```\n\n2. **In Flow Builder**, call the Invocable Action before the screen\n\n3. **Store result** in an Apex-Defined Variable\n\n4. **Pass to LWC** via the screen component input mapping\n\n### Common Patterns\n\n#### Pattern 1: Record Edit with Validation\n\n```javascript\n// LWC that receives, edits, and returns an sObject\n@api inputRecord; // apex://Contact (inputOnly)\n@api outputRecord; // apex://Contact (outputOnly)\n@api isValid = false; // Boolean (outputOnly)\n\nhandleFieldChange(event) {\n const field = event.target.dataset.field;\n this.workingRecord[field] = event.target.value;\n\n // Validate and update outputs\n this.isValid = this.validateRecord();\n this.dispatchEvent(new FlowAttributeChangeEvent('outputRecord', this.workingRecord));\n this.dispatchEvent(new FlowAttributeChangeEvent('isValid', this.isValid));\n}\n```\n\n#### Pattern 2: Multi-Record Selection\n\n```javascript\n// Select from a list, output selected items\n@api availableRecords; // apex://Account[] (inputOnly)\n@api selectedRecords = []; // apex://Account[] (outputOnly)\n\nhandleSelect(event) {\n const id = event.target.dataset.id;\n const record = this.availableRecords.find(r => r.Id === id);\n\n if (record && !this.selectedRecords.find(r => r.Id === id)) {\n this.selectedRecords = [...this.selectedRecords, record];\n this.dispatchEvent(\n new FlowAttributeChangeEvent('selectedRecords', this.selectedRecords)\n );\n }\n}\n```\n\n#### Pattern 3: Master-Detail Editing\n\n```javascript\n// Edit parent with nested child records\n@api orderWrapper; // apex://OrderController.OrderWithLines (inputOnly)\n@api updatedOrder; // apex://OrderController.OrderWithLines (outputOnly)\n\nhandleLineItemChange(event) {\n const index = event.target.dataset.index;\n const field = event.target.dataset.field;\n const value = event.target.value;\n\n // Update nested structure\n const updated = JSON.parse(JSON.stringify(this.orderWrapper));\n updated.lineItems[index][field] = value;\n\n // Recalculate totals\n updated.totalAmount = updated.lineItems.reduce(\n (sum, item) => sum + (item.quantity * item.unitPrice), 0\n );\n\n this.updatedOrder = updated;\n this.dispatchEvent(new FlowAttributeChangeEvent('updatedOrder', updated));\n}\n```\n\n### Limitations\n\n| Limitation | Workaround |\n|------------|------------|\n| No `@JsonAccess` support | Ensure wrapper classes don't require JSON annotation |\n| 1000 record limit per collection | Paginate or filter in Apex before passing |\n| No generic types | Create specific wrapper classes |\n| Complex nesting depth | Flatten deep hierarchies |\n\n### Debugging Tips\n\n1. **Console log received data** to verify structure:\n ```javascript\n connectedCallback() {\n console.log('Received from Flow:', JSON.stringify(this.inputWrapper));\n }\n ```\n\n2. **Check Apex class visibility** - inner classes need `public` modifier\n\n3. **Verify @AuraEnabled** on all properties you need to access\n\n---\n\n## Cross-Skill Integration\n\n| Integration | See Also |\n|-------------|----------|\n| Flow → Apex → LWC | [triangle-pattern.md](triangle-pattern.md) |\n| Apex @AuraEnabled | [sf-apex/references/best-practices.md](../../sf-apex/references/best-practices.md) |\n| Flow Templates | [sf-flow/assets/](../../sf-flow/assets/) |\n| Async Notifications | [async-notification-patterns.md](async-notification-patterns.md) |\n| State Management | [state-management.md](state-management.md) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19426,"content_sha256":"074b802f7178787d04f728b69c6a1c96be02d40212a7f9b8b2a12a439fa6b70a"},{"filename":"references/jest-testing.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Advanced Jest Testing for LWC\n\nComprehensive guide to testing Lightning Web Components using Jest, based on [James Simone's advanced testing patterns](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/).\n\n---\n\n## Table of Contents\n\n1. [Setup](#setup)\n2. [Core Testing Patterns](#core-testing-patterns)\n3. [Render Cycle Management](#render-cycle-management)\n4. [Mocking Strategies](#mocking-strategies)\n5. [Testing Wire Services](#testing-wire-services)\n6. [Testing Events](#testing-events)\n7. [Testing Navigation](#testing-navigation)\n8. [Testing LMS](#testing-lms)\n9. [Testing GraphQL](#testing-graphql)\n10. [Polyfills and Utilities](#polyfills-and-utilities)\n11. [Best Practices](#best-practices)\n\n---\n\n## Setup\n\n### Install Dependencies\n\n```bash\n# Install Jest and LWC testing tools\nnpm install --save-dev @salesforce/sfdx-lwc-jest jest\n\n# Install additional utilities\nnpm install --save-dev @testing-library/jest-dom\n```\n\n### Jest Configuration\n\n**File**: `jest.config.js`\n\n```javascript\nconst { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');\n\nmodule.exports = {\n ...jestConfig,\n roots: ['\u003crootDir>/force-app'],\n moduleNameMapper: {\n '^@salesforce/apex

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

: '\u003crootDir>/force-app/test/jest-mocks/apex',\n '^@salesforce/schema

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

: '\u003crootDir>/force-app/test/jest-mocks/schema',\n '^lightning/navigation

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

: '\u003crootDir>/force-app/test/jest-mocks/lightning/navigation',\n '^lightning/messageService

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…

: '\u003crootDir>/force-app/test/jest-mocks/lightning/messageService'\n },\n setupFilesAfterEnv: ['\u003crootDir>/jest.setup.js'],\n testPathIgnorePatterns: [\n '\u003crootDir>/node_modules/',\n '\u003crootDir>/.sfdx/',\n '\u003crootDir>/.agents/',\n '\u003crootDir>/.cursor/',\n '\u003crootDir>/.claude/',\n '\u003crootDir>/.pi/'\n ],\n coverageThreshold: {\n global: {\n branches: 80,\n functions: 80,\n lines: 80,\n statements: 80\n }\n }\n};\n```\n\n`roots: ['\u003crootDir>/force-app']` keeps Jest focused on real Salesforce source. The ignore list adds defense-in-depth for agent/tool folders that may contain copied examples or generated artifacts.\n\nExample test assets in `generating-lwc-components/assets/` ship with a `.example` suffix. Copy them into `force-app/main/default/lwc/\u003ccomponent>/__tests__/` and rename them to `*.test.js` or `*.test.ts` before running Jest.\n\n### Setup File\n\n**File**: `jest.setup.js`\n\n```javascript\n// ResizeObserver polyfill\nif (!window.ResizeObserver) {\n window.ResizeObserver = class ResizeObserver {\n constructor(callback) {\n this.callback = callback;\n }\n observe() {}\n unobserve() {}\n disconnect() {}\n };\n}\n\n// IntersectionObserver polyfill\nif (!window.IntersectionObserver) {\n window.IntersectionObserver = class IntersectionObserver {\n constructor(callback) {\n this.callback = callback;\n }\n observe() {}\n unobserve() {}\n disconnect() {}\n };\n}\n\n// Custom matchers\nexpect.extend({\n toHaveClass(element, className) {\n const pass = element.classList.contains(className);\n return {\n pass,\n message: () => pass\n ? `Expected element NOT to have class \"${className}\"`\n : `Expected element to have class \"${className}\"`\n };\n }\n});\n```\n\n---\n\n## Core Testing Patterns\n\n### Basic Test Structure\n\n```javascript\nimport { createElement } from 'lwc';\nimport MyComponent from 'c/myComponent';\n\ndescribe('c-my-component', () => {\n afterEach(() => {\n // Clean up DOM\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n // Clear all mocks\n jest.clearAllMocks();\n });\n\n it('renders correctly', () => {\n const element = createElement('c-my-component', {\n is: MyComponent\n });\n document.body.appendChild(element);\n\n expect(element.shadowRoot.querySelector('h1')).not.toBeNull();\n });\n});\n```\n\n### DOM Cleanup Pattern\n\n```javascript\ndescribe('c-my-component', () => {\n let element;\n\n beforeEach(() => {\n element = createElement('c-my-component', { is: MyComponent });\n document.body.appendChild(element);\n });\n\n afterEach(() => {\n // CRITICAL: Prevent state bleed between tests\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n it('test case 1', () => {\n // Element is fresh for each test\n });\n\n it('test case 2', () => {\n // Element is fresh for each test\n });\n});\n```\n\n---\n\n## Render Cycle Management\n\n### Render Cycle Helper\n\nBased on [James Simone's pattern](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/).\n\n```javascript\n// testUtils.js\nexport const runRenderingLifecycle = async (reasons = ['render']) => {\n while (reasons.length > 0) {\n await Promise.resolve(reasons.pop());\n }\n};\n\n// Alias for brevity\nexport const flushPromises = () => runRenderingLifecycle();\n```\n\n### Usage in Tests\n\n```javascript\nimport { runRenderingLifecycle, flushPromises } from './testUtils';\n\nit('updates after property change', async () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n // Change property\n element.greeting = 'new value';\n\n // Wait for render cycle\n await runRenderingLifecycle(['property change', 'render']);\n\n // Assert\n const div = element.shadowRoot.querySelector('div');\n expect(div.textContent).toBe('new value');\n});\n\nit('handles async operation', async () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n // Trigger async action\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n // Flush all promises\n await flushPromises();\n\n // Assert\n expect(element.shadowRoot.querySelector('.result')).not.toBeNull();\n});\n```\n\n---\n\n## Mocking Strategies\n\n### Mock Apex Imports\n\n```javascript\n// __mocks__/apex.js\nexport default function createApexTestWireAdapter() {\n return jest.fn();\n}\n\n// Component test\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\n\njest.mock(\n '@salesforce/apex/AccountController.getAccounts',\n () => ({ default: jest.fn() }),\n { virtual: true }\n);\n\ndescribe('c-account-list', () => {\n it('displays accounts', async () => {\n const MOCK_DATA = [\n { Id: '001xxx', Name: 'Acme' }\n ];\n\n getAccounts.mockResolvedValue(MOCK_DATA);\n\n const element = createElement('c-account-list', {\n is: AccountList\n });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const items = element.shadowRoot.querySelectorAll('.account-item');\n expect(items.length).toBe(1);\n });\n});\n```\n\n### Mock Schema Imports\n\n```javascript\n// __mocks__/schema.js\nexport default {\n 'Account.Name': 'Name',\n 'Account.Industry': 'Industry',\n 'Contact.FirstName': 'FirstName'\n};\n\n// Component test\njest.mock('@salesforce/schema/Account.Name', () => 'Name', { virtual: true });\njest.mock('@salesforce/schema/Account.Industry', () => 'Industry', { virtual: true });\n```\n\n### Mock Platform Events\n\n```javascript\n// Mock ShowToastEvent\nimport { ShowToastEvent } from 'lightning/platformShowToastEvent';\n\njest.mock('lightning/platformShowToastEvent', () => ({\n ShowToastEvent: jest.fn()\n}), { virtual: true });\n\nit('shows toast on success', () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n const handler = jest.fn();\n element.addEventListener('showtoast', handler);\n\n // Trigger action\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n // Assert toast was dispatched\n expect(handler).toHaveBeenCalled();\n expect(handler.mock.calls[0][0].detail.title).toBe('Success');\n});\n```\n\n---\n\n## Testing Wire Services\n\n### Test Wire Adapter\n\n```javascript\nimport { createElement } from 'lwc';\nimport AccountCard from 'c/accountCard';\nimport { getRecord } from 'lightning/uiRecordApi';\n\n// Emit data from wire\nconst mockGetRecord = require('lightning/uiRecordApi');\n\ndescribe('c-account-card', () => {\n afterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n it('displays account data', async () => {\n const element = createElement('c-account-card', {\n is: AccountCard\n });\n element.recordId = '001xxx000003DGQ';\n document.body.appendChild(element);\n\n // Emit mock data to wire\n mockGetRecord.emit({\n Id: '001xxx000003DGQ',\n fields: {\n Name: { value: 'Acme Corp' },\n Industry: { value: 'Technology' }\n }\n });\n\n await flushPromises();\n\n const name = element.shadowRoot.querySelector('.account-name');\n expect(name.textContent).toBe('Acme Corp');\n });\n\n it('displays error', async () => {\n const element = createElement('c-account-card', {\n is: AccountCard\n });\n element.recordId = '001xxx000003DGQ';\n document.body.appendChild(element);\n\n // Emit error to wire\n mockGetRecord.error();\n\n await flushPromises();\n\n const error = element.shadowRoot.querySelector('.error-message');\n expect(error).not.toBeNull();\n });\n});\n```\n\n### Test Imperative Apex\n\n```javascript\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\n\njest.mock(\n '@salesforce/apex/AccountController.getAccounts',\n () => ({ default: jest.fn() }),\n { virtual: true }\n);\n\nit('loads accounts imperatively', async () => {\n const MOCK_ACCOUNTS = [\n { Id: '001xxx', Name: 'Acme' }\n ];\n\n getAccounts.mockResolvedValue(MOCK_ACCOUNTS);\n\n const element = createElement('c-account-search', {\n is: AccountSearch\n });\n document.body.appendChild(element);\n\n // Trigger search\n const input = element.shadowRoot.querySelector('input');\n input.value = 'Acme';\n input.dispatchEvent(new Event('change'));\n\n await flushPromises();\n\n expect(getAccounts).toHaveBeenCalledWith({ searchTerm: 'Acme' });\n\n const results = element.shadowRoot.querySelectorAll('.account-item');\n expect(results.length).toBe(1);\n});\n\nit('handles apex error', async () => {\n getAccounts.mockRejectedValue(new Error('Network error'));\n\n const element = createElement('c-account-search', {\n is: AccountSearch\n });\n document.body.appendChild(element);\n\n const input = element.shadowRoot.querySelector('input');\n input.value = 'Test';\n input.dispatchEvent(new Event('change'));\n\n await flushPromises();\n\n const errorMsg = element.shadowRoot.querySelector('.error');\n expect(errorMsg.textContent).toContain('Network error');\n});\n```\n\n---\n\n## Testing Events\n\n### Test Custom Events\n\n```javascript\nit('dispatches custom event', () => {\n const element = createElement('c-event-emitter', {\n is: EventEmitter\n });\n document.body.appendChild(element);\n\n const handler = jest.fn();\n element.addEventListener('itemselected', handler);\n\n // Trigger event\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n // Assert\n expect(handler).toHaveBeenCalled();\n expect(handler.mock.calls[0][0].detail.id).toBe('001xxx');\n});\n```\n\n### Test Event Bubbling\n\n```javascript\nit('bubbles event to parent', () => {\n const parent = createElement('c-parent', { is: Parent });\n document.body.appendChild(parent);\n\n const handler = jest.fn();\n parent.addEventListener('itemselected', handler);\n\n // Get child component\n const child = parent.shadowRoot.querySelector('c-child');\n const button = child.shadowRoot.querySelector('button');\n button.click();\n\n expect(handler).toHaveBeenCalled();\n});\n```\n\n### Test Event Composition\n\n```javascript\nit('composes event across shadow DOM', () => {\n const element = createElement('c-composer', {\n is: Composer\n });\n document.body.appendChild(element);\n\n const handler = jest.fn();\n document.addEventListener('customevent', handler);\n\n // Trigger composed event\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n expect(handler).toHaveBeenCalled();\n});\n```\n\n---\n\n## Testing Navigation\n\n### Mock Navigation Mixin\n\n```javascript\n// __mocks__/lightning/navigation.js\nexport const NavigationMixin = (Base) => {\n return class extends Base {\n [NavigationMixin.Navigate](pageReference, replace) {\n this._navigate = { pageReference, replace };\n }\n };\n};\n\nNavigationMixin.Navigate = Symbol('Navigate');\n\n// Component test\nimport { NavigationMixin } from 'lightning/navigation';\n\nit('navigates to record page', async () => {\n const element = createElement('c-navigator', {\n is: Navigator\n });\n document.body.appendChild(element);\n\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n await flushPromises();\n\n expect(element._navigate.pageReference.type).toBe('standard__recordPage');\n expect(element._navigate.pageReference.attributes.recordId).toBe('001xxx');\n});\n```\n\n---\n\n## Testing LMS\n\n### Mock Message Service\n\n```javascript\n// __mocks__/lightning/messageService.js\nexport const publish = jest.fn();\nexport const subscribe = jest.fn();\nexport const unsubscribe = jest.fn();\nexport const MessageContext = Symbol('MessageContext');\nexport const APPLICATION_SCOPE = Symbol('APPLICATION_SCOPE');\n```\n\n### Test Publisher\n\n```javascript\nimport { publish, MessageContext } from 'lightning/messageService';\nimport ACCOUNT_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n\njest.mock('lightning/messageService');\n\nit('publishes message on selection', () => {\n const element = createElement('c-publisher', {\n is: Publisher\n });\n document.body.appendChild(element);\n\n const button = element.shadowRoot.querySelector('button');\n button.click();\n\n expect(publish).toHaveBeenCalledWith(\n expect.anything(),\n ACCOUNT_CHANNEL,\n expect.objectContaining({ accountId: '001xxx' })\n );\n});\n```\n\n### Test Subscriber\n\n```javascript\nimport { subscribe, unsubscribe } from 'lightning/messageService';\n\njest.mock('lightning/messageService');\n\nit('subscribes on connected', () => {\n const element = createElement('c-subscriber', {\n is: Subscriber\n });\n document.body.appendChild(element);\n\n expect(subscribe).toHaveBeenCalled();\n});\n\nit('unsubscribes on disconnected', () => {\n const element = createElement('c-subscriber', {\n is: Subscriber\n });\n document.body.appendChild(element);\n document.body.removeChild(element);\n\n expect(unsubscribe).toHaveBeenCalled();\n});\n\nit('handles incoming message', async () => {\n let messageHandler;\n subscribe.mockImplementation((context, channel, handler) => {\n messageHandler = handler;\n return { subscription: 'mock' };\n });\n\n const element = createElement('c-subscriber', {\n is: Subscriber\n });\n document.body.appendChild(element);\n\n // Simulate message\n messageHandler({ accountId: '001xxx', accountName: 'Acme' });\n\n await flushPromises();\n\n const name = element.shadowRoot.querySelector('.account-name');\n expect(name.textContent).toBe('Acme');\n});\n```\n\n---\n\n## Testing GraphQL\n\n### Mock GraphQL Adapter\n\n```javascript\nimport { graphql } from 'lightning/graphql';\n\n// Mock graphql wire adapter\njest.mock('lightning/graphql', () => ({\n gql: jest.fn(query => query),\n graphql: jest.fn()\n}), { virtual: true });\n\nit('displays graphql query results', async () => {\n const element = createElement('c-graphql-component', {\n is: GraphqlComponent\n });\n document.body.appendChild(element);\n\n // Emit mock data\n const mockData = {\n uiapi: {\n query: {\n Contact: {\n edges: [\n { node: { Id: '003xxx', Name: { value: 'John Doe' } } }\n ]\n }\n }\n }\n };\n\n graphql.emit({ data: mockData });\n\n await flushPromises();\n\n const contacts = element.shadowRoot.querySelectorAll('.contact-item');\n expect(contacts.length).toBe(1);\n});\n```\n\n---\n\n## Polyfills and Utilities\n\n### ResizeObserver Polyfill\n\n```javascript\nif (!window.ResizeObserver) {\n window.ResizeObserver = class ResizeObserver {\n constructor(callback) {\n this.callback = callback;\n }\n observe() {}\n unobserve() {}\n disconnect() {}\n };\n}\n```\n\n### Proxy Unboxing (LWS Compatibility)\n\n```javascript\n// Lightning Web Security proxifies objects\n// Unbox them for deep equality assertions\n\nit('compares complex objects', () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n // Unbox proxied data\n const unboxedData = JSON.parse(JSON.stringify(element.data));\n\n expect(unboxedData).toEqual({\n accounts: [\n { Id: '001xxx', Name: 'Acme' }\n ]\n });\n});\n```\n\n### Test Utilities Module\n\n```javascript\n// testUtils.js\nexport const runRenderingLifecycle = async (reasons = ['render']) => {\n while (reasons.length > 0) {\n await Promise.resolve(reasons.pop());\n }\n};\n\nexport const flushPromises = () => runRenderingLifecycle();\n\nexport const queryAll = (element, selector) => {\n return Array.from(element.shadowRoot.querySelectorAll(selector));\n};\n\nexport const query = (element, selector) => {\n return element.shadowRoot.querySelector(selector);\n};\n\nexport const waitFor = async (condition, timeout = 3000) => {\n const start = Date.now();\n while (!condition()) {\n if (Date.now() - start > timeout) {\n throw new Error('waitFor timeout');\n }\n await flushPromises();\n }\n};\n\n// Usage\nimport { query, queryAll, waitFor } from './testUtils';\n\nit('uses test utils', async () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n const button = query(element, 'button');\n button.click();\n\n await waitFor(() => query(element, '.result') !== null);\n\n const items = queryAll(element, '.item');\n expect(items.length).toBeGreaterThan(0);\n});\n```\n\n---\n\n## Best Practices\n\n### 1. Always Clean Up DOM\n\n```javascript\nafterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n});\n```\n\n### 2. Use Descriptive Test Names\n\n```javascript\n// BAD\nit('works', () => { /* ... */ });\n\n// GOOD\nit('displays error message when apex call fails', () => { /* ... */ });\n```\n\n### 3. Test User Interactions\n\n```javascript\nit('filters list when search input changes', async () => {\n const element = createElement('c-searchable-list', {\n is: SearchableList\n });\n document.body.appendChild(element);\n\n const input = element.shadowRoot.querySelector('input');\n input.value = 'test';\n input.dispatchEvent(new Event('input'));\n\n await flushPromises();\n\n const items = element.shadowRoot.querySelectorAll('.list-item');\n expect(items.length).toBeLessThan(10); // Filtered results\n});\n```\n\n### 4. Test Error States\n\n```javascript\nit('displays error when wire service fails', async () => {\n mockGetRecord.error({ message: 'Network error' });\n\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const error = element.shadowRoot.querySelector('.error-message');\n expect(error.textContent).toContain('Network error');\n});\n```\n\n### 5. Test Loading States\n\n```javascript\nit('shows spinner during data load', async () => {\n const element = createElement('c-async-component', {\n is: AsyncComponent\n });\n document.body.appendChild(element);\n\n // Before data loads\n let spinner = element.shadowRoot.querySelector('lightning-spinner');\n expect(spinner).not.toBeNull();\n\n // Emit data\n mockGetData.emit([{ Id: '001xxx' }]);\n await flushPromises();\n\n // After data loads\n spinner = element.shadowRoot.querySelector('lightning-spinner');\n expect(spinner).toBeNull();\n});\n```\n\n### 6. Test Accessibility\n\n```javascript\nit('has proper ARIA labels', () => {\n const element = createElement('c-accessible', {\n is: Accessible\n });\n document.body.appendChild(element);\n\n const button = element.shadowRoot.querySelector('button');\n expect(button.getAttribute('aria-label')).toBe('Delete record');\n});\n\nit('manages focus correctly', async () => {\n const element = createElement('c-modal', { is: Modal });\n document.body.appendChild(element);\n\n element.openModal();\n await flushPromises();\n\n const firstFocusable = element.shadowRoot.querySelector('.focusable');\n expect(document.activeElement).toBe(firstFocusable);\n});\n```\n\n### 7. Organize Tests by Feature\n\n```javascript\ndescribe('c-account-list', () => {\n describe('data loading', () => {\n it('displays accounts when data loads successfully');\n it('shows error when data load fails');\n it('shows spinner during loading');\n });\n\n describe('filtering', () => {\n it('filters by search term');\n it('filters by industry');\n it('clears filters');\n });\n\n describe('selection', () => {\n it('selects account on click');\n it('dispatches selection event');\n it('highlights selected account');\n });\n});\n```\n\n---\n\n## Complete Test Example\n\n```javascript\nimport { createElement } from 'lwc';\nimport AccountList from 'c/accountList';\nimport getAccounts from '@salesforce/apex/AccountController.getAccounts';\nimport { publish } from 'lightning/messageService';\nimport ACCOUNT_SELECTED from '@salesforce/messageChannel/AccountSelected__c';\n\n// Mocks\njest.mock('@salesforce/apex/AccountController.getAccounts', () => ({\n default: jest.fn()\n}), { virtual: true });\n\njest.mock('lightning/messageService');\n\nconst MOCK_ACCOUNTS = [\n { Id: '001xxx001', Name: 'Acme Corp', Industry: 'Technology' },\n { Id: '001xxx002', Name: 'Global Inc', Industry: 'Finance' }\n];\n\nconst flushPromises = () => new Promise(resolve => setImmediate(resolve));\n\ndescribe('c-account-list', () => {\n afterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n describe('data loading', () => {\n it('displays accounts when loaded successfully', async () => {\n getAccounts.mockResolvedValue(MOCK_ACCOUNTS);\n\n const element = createElement('c-account-list', {\n is: AccountList\n });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const items = element.shadowRoot.querySelectorAll('.account-item');\n expect(items.length).toBe(2);\n expect(items[0].textContent).toContain('Acme Corp');\n });\n\n it('displays error when fetch fails', async () => {\n getAccounts.mockRejectedValue(new Error('Network error'));\n\n const element = createElement('c-account-list', {\n is: AccountList\n });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const error = element.shadowRoot.querySelector('.error-message');\n expect(error).not.toBeNull();\n expect(error.textContent).toContain('Network error');\n });\n });\n\n describe('selection', () => {\n it('publishes message when account selected', async () => {\n getAccounts.mockResolvedValue(MOCK_ACCOUNTS);\n\n const element = createElement('c-account-list', {\n is: AccountList\n });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const firstAccount = element.shadowRoot.querySelector('.account-item');\n firstAccount.click();\n\n expect(publish).toHaveBeenCalledWith(\n expect.anything(),\n ACCOUNT_SELECTED,\n expect.objectContaining({\n accountId: '001xxx001',\n accountName: 'Acme Corp'\n })\n );\n });\n });\n\n describe('filtering', () => {\n it('filters accounts by search term', async () => {\n getAccounts.mockResolvedValue(MOCK_ACCOUNTS);\n\n const element = createElement('c-account-list', {\n is: AccountList\n });\n document.body.appendChild(element);\n\n await flushPromises();\n\n const searchInput = element.shadowRoot.querySelector('input');\n searchInput.value = 'Acme';\n searchInput.dispatchEvent(new Event('input'));\n\n await flushPromises();\n\n const visibleItems = element.shadowRoot.querySelectorAll('.account-item:not(.hidden)');\n expect(visibleItems.length).toBe(1);\n expect(visibleItems[0].textContent).toContain('Acme Corp');\n });\n });\n});\n```\n\n---\n\n## Related Resources\n\n- [component-patterns.md](component-patterns.md) - Component implementation patterns\n- [lms-guide.md](lms-guide.md) - Lightning Message Service\n- [James Simone - Advanced LWC Jest Testing](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/)\n- [LWC Recipes](https://github.com/trailheadapps/lwc-recipes) - See component-level `__tests__` folders for Jest examples\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":25948,"content_sha256":"1a1a64beb6d453004640172f597b163ec0934b3c8f6e7a4052d7d688af25e48a"},{"filename":"references/lms-guide.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Lightning Message Service (LMS) Guide\n\nComplete guide to cross-DOM component communication using Lightning Message Service.\n\n---\n\n## Table of Contents\n\n1. [Overview](#overview)\n2. [When to Use LMS](#when-to-use-lms)\n3. [Message Channel Setup](#message-channel-setup)\n4. [Publishing Messages](#publishing-messages)\n5. [Subscribing to Messages](#subscribing-to-messages)\n6. [Scopes](#scopes)\n7. [Advanced Patterns](#advanced-patterns)\n8. [Best Practices](#best-practices)\n9. [Troubleshooting](#troubleshooting)\n\n---\n\n## Overview\n\nLightning Message Service (LMS) enables communication between components across different DOM contexts:\n- Lightning Web Components (LWC)\n- Aura Components\n- Visualforce pages (in Lightning Experience)\n\n**Key Benefits**:\n- Cross-DOM communication (Shadow DOM boundaries)\n- Loosely coupled components\n- Publish-subscribe pattern\n- Type-safe messaging with message channels\n\n---\n\n## When to Use LMS\n\n| Use Case | Recommended Pattern |\n|----------|---------------------|\n| Parent → Child | `@api` properties (simple, direct) |\n| Child → Parent | Custom Events (simple, direct) |\n| Sibling → Sibling (same hierarchy) | Parent mediator + Custom Events |\n| **Cross-DOM communication** | **Lightning Message Service** |\n| **App Builder page components** | **Lightning Message Service** |\n| **Aura ↔ LWC communication** | **Lightning Message Service** |\n| **Visualforce ↔ LWC (in LEX)** | **Lightning Message Service** |\n\n**Rule of Thumb**: Use LMS when components cannot directly reference each other or cross DOM boundaries.\n\n---\n\n## Message Channel Setup\n\n### 1. Create Message Channel Metadata\n\nLightning Message Channels are metadata files that define the message schema.\n\n**File**: `force-app/main/default/messageChannels/AccountSelected__c.messageChannel-meta.xml`\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cLightningMessageChannel xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdescription>Message channel for account selection events\u003c/description>\n \u003cisExposed>true\u003c/isExposed>\n \u003clightningMessageFields>\n \u003cdescription>Account ID\u003c/description>\n \u003cfieldName>accountId\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003clightningMessageFields>\n \u003cdescription>Account Name\u003c/description>\n \u003cfieldName>accountName\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003clightningMessageFields>\n \u003cdescription>Source component identifier\u003c/description>\n \u003cfieldName>source\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003cmasterLabel>Account Selected\u003c/masterLabel>\n\u003c/LightningMessageChannel>\n```\n\n### 2. Deploy Message Channel\n\n```bash\nsf project deploy start -m LightningMessageChannel:AccountSelected__c\n```\n\n### 3. Import Message Channel in Component\n\n```javascript\nimport ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n```\n\n---\n\n## Publishing Messages\n\n### Basic Publisher Pattern\n\n```javascript\n// accountPublisher.js\nimport { LightningElement, wire } from 'lwc';\nimport { publish, MessageContext } from 'lightning/messageService';\nimport ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n\nexport default class AccountPublisher extends LightningElement {\n @wire(MessageContext)\n messageContext;\n\n handleAccountClick(event) {\n const accountId = event.target.dataset.id;\n const accountName = event.target.dataset.name;\n\n // Create payload\n const payload = {\n accountId: accountId,\n accountName: accountName,\n source: 'accountPublisher'\n };\n\n // Publish message\n publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);\n }\n}\n```\n\n```html\n\u003c!-- accountPublisher.html -->\n\u003ctemplate>\n \u003cdiv class=\"slds-card\">\n \u003cdiv class=\"slds-card__header\">\n \u003ch2 class=\"slds-text-heading_medium\">Account List\u003c/h2>\n \u003c/div>\n \u003cdiv class=\"slds-card__body\">\n \u003ctemplate for:each={accounts} for:item=\"account\">\n \u003cdiv key={account.Id}\n class=\"slds-box slds-m-around_small\"\n data-id={account.Id}\n data-name={account.Name}\n onclick={handleAccountClick}>\n {account.Name}\n \u003c/div>\n \u003c/template>\n \u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n### Publisher with Conditional Logic\n\n```javascript\nhandlePublish(accountData) {\n // Validate before publishing\n if (!this.messageContext) {\n console.error('MessageContext not initialized');\n return;\n }\n\n if (!accountData || !accountData.Id) {\n console.error('Invalid account data');\n return;\n }\n\n const payload = {\n accountId: accountData.Id,\n accountName: accountData.Name,\n source: this.componentName,\n timestamp: Date.now()\n };\n\n publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);\n\n // Optional: Show toast confirmation\n this.dispatchEvent(new ShowToastEvent({\n title: 'Selection Published',\n message: `Account \"${accountData.Name}\" selected`,\n variant: 'success'\n }));\n}\n```\n\n---\n\n## Subscribing to Messages\n\n### Basic Subscriber Pattern\n\n```javascript\n// accountSubscriber.js\nimport { LightningElement, wire } from 'lwc';\nimport { subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';\nimport ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n\nexport default class AccountSubscriber extends LightningElement {\n subscription = null;\n selectedAccountId;\n selectedAccountName;\n\n @wire(MessageContext)\n messageContext;\n\n connectedCallback() {\n this.subscribeToChannel();\n }\n\n disconnectedCallback() {\n this.unsubscribeFromChannel();\n }\n\n subscribeToChannel() {\n if (!this.subscription) {\n this.subscription = subscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleMessage(message),\n { scope: APPLICATION_SCOPE }\n );\n }\n }\n\n unsubscribeFromChannel() {\n unsubscribe(this.subscription);\n this.subscription = null;\n }\n\n handleMessage(message) {\n this.selectedAccountId = message.accountId;\n this.selectedAccountName = message.accountName;\n\n console.log('Message received from:', message.source);\n console.log('Account ID:', message.accountId);\n }\n}\n```\n\n```html\n\u003c!-- accountSubscriber.html -->\n\u003ctemplate>\n \u003cdiv class=\"slds-card\">\n \u003cdiv class=\"slds-card__header\">\n \u003ch2 class=\"slds-text-heading_medium\">Selected Account\u003c/h2>\n \u003c/div>\n \u003cdiv class=\"slds-card__body\">\n \u003ctemplate lwc:if={selectedAccountId}>\n \u003cdl class=\"slds-dl_horizontal\">\n \u003cdt class=\"slds-dl_horizontal__label\">Account ID:\u003c/dt>\n \u003cdd class=\"slds-dl_horizontal__detail\">{selectedAccountId}\u003c/dd>\n \u003cdt class=\"slds-dl_horizontal__label\">Account Name:\u003c/dt>\n \u003cdd class=\"slds-dl_horizontal__detail\">{selectedAccountName}\u003c/dd>\n \u003c/dl>\n \u003c/template>\n \u003ctemplate lwc:else>\n \u003cp class=\"slds-text-color_weak\">No account selected\u003c/p>\n \u003c/template>\n \u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n### Subscriber with Filtering\n\n```javascript\nhandleMessage(message) {\n // Ignore messages from this component (avoid self-updates)\n if (message.source === this.componentName) {\n return;\n }\n\n // Filter by specific conditions\n if (message.accountId && message.accountId.startsWith('001')) {\n this.selectedAccountId = message.accountId;\n this.selectedAccountName = message.accountName;\n\n // Fetch additional data if needed\n this.loadAccountDetails(message.accountId);\n }\n}\n\nasync loadAccountDetails(accountId) {\n try {\n const data = await getAccountDetails({ accountId });\n this.accountDetails = data;\n } catch (error) {\n this.handleError(error);\n }\n}\n```\n\n---\n\n## Scopes\n\nLMS supports two subscription scopes:\n\n| Scope | Behavior | Use Case |\n|-------|----------|----------|\n| `APPLICATION_SCOPE` | Receive messages from entire app | Cross-page communication, global state |\n| `undefined` (default) | Receive messages only within active tab | Tab-specific communication |\n\n### Application Scope Example\n\n```javascript\nimport { APPLICATION_SCOPE } from 'lightning/messageService';\n\nsubscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleMessage(message),\n { scope: APPLICATION_SCOPE }\n);\n```\n\n### Tab Scope Example\n\n```javascript\n// No scope specified = tab scope only\nsubscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleMessage(message)\n);\n```\n\n---\n\n## Advanced Patterns\n\n### 1. Multiple Subscriptions\n\n```javascript\nexport default class MultiSubscriber extends LightningElement {\n accountSubscription = null;\n contactSubscription = null;\n\n @wire(MessageContext) messageContext;\n\n connectedCallback() {\n // Subscribe to account channel\n this.accountSubscription = subscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleAccountMessage(message),\n { scope: APPLICATION_SCOPE }\n );\n\n // Subscribe to contact channel\n this.contactSubscription = subscribe(\n this.messageContext,\n CONTACT_SELECTED_CHANNEL,\n (message) => this.handleContactMessage(message),\n { scope: APPLICATION_SCOPE }\n );\n }\n\n disconnectedCallback() {\n unsubscribe(this.accountSubscription);\n unsubscribe(this.contactSubscription);\n this.accountSubscription = null;\n this.contactSubscription = null;\n }\n\n handleAccountMessage(message) {\n // Handle account-specific logic\n }\n\n handleContactMessage(message) {\n // Handle contact-specific logic\n }\n}\n```\n\n### 2. Publish-Subscribe in Same Component\n\n```javascript\nexport default class PublisherSubscriber extends LightningElement {\n subscription = null;\n @wire(MessageContext) messageContext;\n\n connectedCallback() {\n // Subscribe to messages from OTHER components\n this.subscription = subscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleMessage(message),\n { scope: APPLICATION_SCOPE }\n );\n }\n\n disconnectedCallback() {\n unsubscribe(this.subscription);\n }\n\n handleMessage(message) {\n // Filter out own messages\n if (message.source === 'myComponent') {\n return;\n }\n // Process external messages\n this.selectedAccountId = message.accountId;\n }\n\n handleLocalSelection(event) {\n const accountId = event.detail.id;\n\n // Publish for other components\n publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, {\n accountId,\n source: 'myComponent'\n });\n\n // Update own state directly (don't rely on subscription)\n this.selectedAccountId = accountId;\n }\n}\n```\n\n### 3. Conditional Subscription\n\n```javascript\nexport default class ConditionalSubscriber extends LightningElement {\n @api enableLiveUpdates = false;\n subscription = null;\n @wire(MessageContext) messageContext;\n\n connectedCallback() {\n if (this.enableLiveUpdates) {\n this.subscribeToChannel();\n }\n }\n\n @api\n toggleLiveUpdates(enabled) {\n this.enableLiveUpdates = enabled;\n if (enabled) {\n this.subscribeToChannel();\n } else {\n this.unsubscribeFromChannel();\n }\n }\n\n subscribeToChannel() {\n if (!this.subscription) {\n this.subscription = subscribe(\n this.messageContext,\n ACCOUNT_SELECTED_CHANNEL,\n (message) => this.handleMessage(message),\n { scope: APPLICATION_SCOPE }\n );\n }\n }\n\n unsubscribeFromChannel() {\n if (this.subscription) {\n unsubscribe(this.subscription);\n this.subscription = null;\n }\n }\n}\n```\n\n### 4. Message Buffering\n\n```javascript\nexport default class MessageBuffer extends LightningElement {\n messageQueue = [];\n isProcessing = false;\n\n handleMessage(message) {\n this.messageQueue.push(message);\n this.processQueue();\n }\n\n async processQueue() {\n if (this.isProcessing || this.messageQueue.length === 0) {\n return;\n }\n\n this.isProcessing = true;\n\n while (this.messageQueue.length > 0) {\n const message = this.messageQueue.shift();\n await this.processMessage(message);\n }\n\n this.isProcessing = false;\n }\n\n async processMessage(message) {\n // Simulate async processing\n return new Promise(resolve => {\n setTimeout(() => {\n this.selectedAccountId = message.accountId;\n resolve();\n }, 100);\n });\n }\n}\n```\n\n---\n\n## Best Practices\n\n### 1. Always Unsubscribe\n\n```javascript\ndisconnectedCallback() {\n // CRITICAL: Prevent memory leaks\n if (this.subscription) {\n unsubscribe(this.subscription);\n this.subscription = null;\n }\n}\n```\n\n### 2. Validate MessageContext\n\n```javascript\nhandlePublish(data) {\n if (!this.messageContext) {\n console.warn('MessageContext not available');\n return;\n }\n publish(this.messageContext, CHANNEL, data);\n}\n```\n\n### 3. Use Descriptive Payloads\n\n```javascript\n// BAD - Unclear payload\npublish(this.messageContext, CHANNEL, { id: '001xxx' });\n\n// GOOD - Clear, descriptive payload\npublish(this.messageContext, CHANNEL, {\n accountId: '001xxx000003DGQ',\n accountName: 'Acme Corp',\n source: 'accountList',\n timestamp: Date.now(),\n metadata: {\n action: 'selected',\n view: 'list'\n }\n});\n```\n\n### 4. Document Message Channels\n\n```javascript\n/**\n * Publishes account selection event to AccountSelected__c channel\n * @param {Object} payload\n * @param {String} payload.accountId - Salesforce Account ID\n * @param {String} payload.accountName - Account Name\n * @param {String} payload.source - Component identifier\n */\npublishAccountSelection(payload) {\n publish(this.messageContext, ACCOUNT_SELECTED_CHANNEL, payload);\n}\n```\n\n### 5. Error Handling\n\n```javascript\nhandleMessage(message) {\n try {\n if (!message || !message.accountId) {\n throw new Error('Invalid message payload');\n }\n\n this.selectedAccountId = message.accountId;\n this.loadAccountDetails(message.accountId);\n } catch (error) {\n console.error('Error processing message:', error);\n this.dispatchEvent(new ShowToastEvent({\n title: 'Error',\n message: 'Failed to process message',\n variant: 'error'\n }));\n }\n}\n```\n\n---\n\n## Troubleshooting\n\n### Issue: Messages Not Received\n\n**Checklist**:\n1. Is `MessageContext` wired correctly?\n ```javascript\n @wire(MessageContext) messageContext;\n ```\n\n2. Is subscription active?\n ```javascript\n console.log('Subscription:', this.subscription); // Should not be null\n ```\n\n3. Is the message channel deployed?\n ```bash\n sf project deploy start -m LightningMessageChannel\n ```\n\n4. Are publisher and subscriber using the same channel?\n ```javascript\n // Both should import the same channel\n import CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n ```\n\n5. Is the scope correct?\n ```javascript\n // For cross-page: APPLICATION_SCOPE\n // For same page: no scope (default)\n ```\n\n### Issue: Memory Leaks\n\n**Cause**: Not unsubscribing in `disconnectedCallback()`\n\n**Fix**:\n```javascript\ndisconnectedCallback() {\n unsubscribe(this.subscription);\n this.subscription = null;\n}\n```\n\n### Issue: Self-Updates\n\n**Cause**: Component receives its own published messages\n\n**Fix**: Filter by source\n```javascript\nhandleMessage(message) {\n if (message.source === this.componentName) {\n return; // Ignore own messages\n }\n // Process message\n}\n```\n\n---\n\n## Testing LMS Components\n\n### Mock MessageContext\n\n```javascript\n// testUtils.js\nexport const createMessageContextMock = () => {\n return jest.fn();\n};\n\nexport const mockPublish = jest.fn();\nexport const mockSubscribe = jest.fn();\nexport const mockUnsubscribe = jest.fn();\n\njest.mock('lightning/messageService', () => ({\n publish: mockPublish,\n subscribe: mockSubscribe,\n unsubscribe: mockUnsubscribe,\n MessageContext: Symbol('MessageContext'),\n APPLICATION_SCOPE: Symbol('APPLICATION_SCOPE')\n}), { virtual: true });\n```\n\n### Test Publisher\n\n```javascript\nimport { createElement } from 'lwc';\nimport AccountPublisher from 'c/accountPublisher';\nimport { publish } from 'lightning/messageService';\nimport ACCOUNT_SELECTED_CHANNEL from '@salesforce/messageChannel/AccountSelected__c';\n\njest.mock('lightning/messageService');\n\ndescribe('c-account-publisher', () => {\n afterEach(() => {\n jest.clearAllMocks();\n });\n\n it('publishes account selection', () => {\n const element = createElement('c-account-publisher', {\n is: AccountPublisher\n });\n document.body.appendChild(element);\n\n // Trigger selection\n const accountCard = element.shadowRoot.querySelector('[data-id=\"001xxx\"]');\n accountCard.click();\n\n // Assert publish was called\n expect(publish).toHaveBeenCalledWith(\n expect.anything(),\n ACCOUNT_SELECTED_CHANNEL,\n expect.objectContaining({\n accountId: '001xxx'\n })\n );\n });\n});\n```\n\n### Test Subscriber\n\n```javascript\nimport { createElement } from 'lwc';\nimport AccountSubscriber from 'c/accountSubscriber';\nimport { subscribe } from 'lightning/messageService';\n\njest.mock('lightning/messageService');\n\ndescribe('c-account-subscriber', () => {\n let messageHandler;\n\n beforeEach(() => {\n subscribe.mockImplementation((context, channel, handler, options) => {\n messageHandler = handler;\n return { subscription: 'mock-subscription' };\n });\n });\n\n it('subscribes on connected', () => {\n const element = createElement('c-account-subscriber', {\n is: AccountSubscriber\n });\n document.body.appendChild(element);\n\n expect(subscribe).toHaveBeenCalled();\n });\n\n it('handles incoming message', async () => {\n const element = createElement('c-account-subscriber', {\n is: AccountSubscriber\n });\n document.body.appendChild(element);\n\n // Simulate message\n messageHandler({\n accountId: '001xxx',\n accountName: 'Acme Corp'\n });\n\n await Promise.resolve();\n\n const accountName = element.shadowRoot.querySelector('.account-name');\n expect(accountName.textContent).toBe('Acme Corp');\n });\n});\n```\n\n---\n\n## Complete Example: Account-Contact Sync\n\n### Message Channel\n\n```xml\n\u003c!-- ContactSelected__c.messageChannel-meta.xml -->\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cLightningMessageChannel xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdescription>Contact selection messaging\u003c/description>\n \u003cisExposed>true\u003c/isExposed>\n \u003clightningMessageFields>\n \u003cfieldName>contactId\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003clightningMessageFields>\n \u003cfieldName>contactName\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003clightningMessageFields>\n \u003cfieldName>accountId\u003c/fieldName>\n \u003c/lightningMessageFields>\n \u003cmasterLabel>Contact Selected\u003c/masterLabel>\n\u003c/LightningMessageChannel>\n```\n\n### Publisher Component\n\n```javascript\n// contactList.js\nimport { LightningElement, api, wire } from 'lwc';\nimport { publish, MessageContext } from 'lightning/messageService';\nimport CONTACT_SELECTED from '@salesforce/messageChannel/ContactSelected__c';\nimport getContacts from '@salesforce/apex/ContactController.getContacts';\n\nexport default class ContactList extends LightningElement {\n @api accountId;\n contacts;\n @wire(MessageContext) messageContext;\n\n @wire(getContacts, { accountId: '$accountId' })\n wiredContacts({ data, error }) {\n if (data) {\n this.contacts = data;\n }\n }\n\n handleContactSelect(event) {\n const contactId = event.currentTarget.dataset.id;\n const contact = this.contacts.find(c => c.Id === contactId);\n\n publish(this.messageContext, CONTACT_SELECTED, {\n contactId: contact.Id,\n contactName: contact.Name,\n accountId: this.accountId\n });\n }\n}\n```\n\n### Subscriber Component\n\n```javascript\n// contactDetails.js\nimport { LightningElement, wire } from 'lwc';\nimport { subscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';\nimport CONTACT_SELECTED from '@salesforce/messageChannel/ContactSelected__c';\nimport getContactDetails from '@salesforce/apex/ContactController.getContactDetails';\n\nexport default class ContactDetails extends LightningElement {\n subscription = null;\n contactId;\n contactDetails;\n @wire(MessageContext) messageContext;\n\n connectedCallback() {\n this.subscription = subscribe(\n this.messageContext,\n CONTACT_SELECTED,\n (message) => this.handleContactSelected(message),\n { scope: APPLICATION_SCOPE }\n );\n }\n\n disconnectedCallback() {\n unsubscribe(this.subscription);\n }\n\n async handleContactSelected(message) {\n this.contactId = message.contactId;\n try {\n this.contactDetails = await getContactDetails({\n contactId: message.contactId\n });\n } catch (error) {\n console.error('Error loading contact details:', error);\n }\n }\n}\n```\n\n---\n\n## Related Resources\n\n- [component-patterns.md](component-patterns.md) - Parent-child communication\n- [jest-testing.md](jest-testing.md) - Testing LMS components\n- [Official LMS Documentation](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_message_channel)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22390,"content_sha256":"385c3a5dc87261f5beeed49dd268a2a37f5ae1651c9fe5cb26705df3efdb6477"},{"filename":"references/lwc-best-practices.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Lightning Web Components Best Practices\n\nThis guide provides comprehensive best practices for building production-ready LWC components, organized around the **PICKLES Framework** and incorporating advanced patterns from industry experts.\n\n---\n\n## PICKLES Framework Overview\n\nThe PICKLES Framework provides a structured approach to LWC architecture. Use it as a checklist during component design and implementation.\n\n```\n🥒 P - Prototype → Validate ideas with wireframes & mock data\n🥒 I - Integrate → Choose data source (LDS, Apex, GraphQL)\n🥒 C - Composition → Structure component hierarchy & communication\n🥒 K - Kinetics → Handle user interactions & event flow\n🥒 L - Libraries → Leverage platform APIs & base components\n🥒 E - Execution → Optimize performance & lifecycle hooks\n🥒 S - Security → Enforce permissions & data protection\n```\n\n**Reference**: [PICKLES Framework](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/) — David Picksley, Third Eye Consulting\n\n---\n\n## Component Design Principles\n\n### Single Responsibility (PICKLES: Composition)\n\nEach component should do one thing well.\n\n```\n✅ GOOD: accountCard, accountList, accountForm (separate components)\n❌ BAD: accountManager (does display, list, and form in one)\n```\n\n### Composition Over Inheritance\n\nBuild complex UIs by composing simple components.\n\n```html\n\u003c!-- Compose components -->\n\u003ctemplate>\n \u003cc-page-header title=\"Accounts\">\u003c/c-page-header>\n \u003cc-account-filters onfilter={handleFilter}>\u003c/c-account-filters>\n \u003cc-account-list accounts={filteredAccounts}>\u003c/c-account-list>\n \u003cc-pagination total={totalCount} onpage={handlePage}>\u003c/c-pagination>\n\u003c/template>\n```\n\n### Unidirectional Data Flow\n\nData flows down (props), events bubble up.\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ DATA FLOW PATTERN │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ Parent Component │\n│ ┌─────────────────────────────────────────────────────────┐ │\n│ │ state: accounts = [...] │ │\n│ │ │ │\n│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │\n│ │ │ Child A │ ←── │ Child B │ ←── │ Child C │ │ │\n│ │ │ │ │ │ │ │ │ │\n│ │ │ @api │ │ @api │ │ @api │ │ │\n│ │ │ accounts │ │ selected │ │ details │ │ │\n│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │\n│ │ │ │ │ │ │\n│ │ │ Events │ Events │ Events │ │\n│ │ └────────────────┴────────────────┘ │ │\n│ │ ↑ bubbles to parent │ │\n│ └─────────────────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Naming & Decorator Conventions\n\n### Property & Attribute Naming\n\n| Context | Convention | Example |\n|---------|-----------|---------|\n| JavaScript properties | camelCase | `itemName`, `maxValue` |\n| HTML attributes | kebab-case, lowercase | `item-name`, `max-value` |\n| Dispatched event names | Lowercase, no `on` prefix | `'recordchange'`, `'save'` |\n| HTML event listeners | `on` + event name | `onrecordchange`, `onsave` |\n\nReserved prefixes in JS property names: `on`, `aria`, `data`. Reserved words: `slot`, `part`, `is`.\n\n### @api Decorator Rules\n\n- Only one decorator per field/method — do not combine `@api` with `@track` or `@wire`\n- For getter/setter pairs: decorate only the getter, and always define both getter and setter\n- Never mutate `@api` properties internally — use a private reactive copy instead\n- Only use `@api` on properties/methods that are part of the component's public API\n\n```javascript\n// ✅ GOOD: getter/setter with @api on getter only\n_recordId;\n\n@api\nget recordId() { return this._recordId; }\nset recordId(value) {\n this._recordId = value;\n this.loadRecord();\n}\n```\n\n### @track Decorator Rules\n\nSince Spring '20, primitive properties are reactive by default. `@track` is only needed when **mutating nested properties** of objects or arrays.\n\n| Scenario | @track Needed? |\n|----------|----------------|\n| Primitive value (`string`, `number`, `boolean`) | No |\n| Object/array that is **reassigned** entirely | No |\n| Object with **nested property mutation** (`this.obj.nested.value++`) | Yes |\n\n```javascript\n// ❌ Unnecessary: primitives are reactive by default\n@track searchTerm = '';\n\n// ✅ Correct: remove @track for primitives\nsearchTerm = '';\n\n// ✅ Correct: @track needed for nested mutation\n@track formData = { billing: { city: '' } };\n// later: this.formData.billing.city = 'San Francisco';\n```\n\n---\n\n## Data Integration (PICKLES: Integrate)\n\n### Data Source Decision Tree\n\n| Scenario | Recommended Approach |\n|----------|---------------------|\n| Single record by ID | Lightning Data Service (`getRecord`) |\n| Simple record CRUD | `lightning-record-form` / `lightning-record-edit-form` |\n| Complex queries | Apex with `@AuraEnabled(cacheable=true)` |\n| Related records, filtering | GraphQL wire adapter |\n| Real-time updates | Platform Events / Streaming API |\n| External data | Named Credentials + Apex callout |\n\n### GraphQL vs Apex Decision\n\n| Use GraphQL When | Use Apex When |\n|------------------|---------------|\n| Fetching related objects | Complex business logic |\n| Client-side filtering | Aggregate queries (COUNT, SUM) |\n| Cursor-based pagination | Bulk DML operations |\n| Reducing over-fetching | Callouts to external systems |\n\n### Wire Service Best Practices\n\n```javascript\n// Store wire result for refreshApex\nwiredAccountsResult;\n\n@wire(getAccounts, { searchTerm: '$searchTerm' })\nwiredAccounts(result) {\n this.wiredAccountsResult = result; // Store for refresh\n const { data, error } = result;\n if (data) {\n this.accounts = data;\n this.error = undefined;\n } else if (error) {\n this.error = this.reduceErrors(error);\n this.accounts = [];\n }\n}\n\n// Refresh when needed\nasync handleRefresh() {\n await refreshApex(this.wiredAccountsResult);\n}\n```\n\n### Error Handling Pattern\n\n```javascript\n// Centralized error reducer\nreduceErrors(errors) {\n if (!Array.isArray(errors)) {\n errors = [errors];\n }\n\n return errors\n .filter(error => !!error)\n .map(error => {\n // UI API errors\n if (error.body?.message) return error.body.message;\n // JS errors\n if (error.message) return error.message;\n // GraphQL errors\n if (error.graphQLErrors) {\n return error.graphQLErrors.map(e => e.message).join(', ');\n }\n return JSON.stringify(error);\n })\n .join('; ');\n}\n```\n---\n\n## Event Patterns (PICKLES: Kinetics)\n\n### Custom Events\n\n```javascript\n// Child dispatches event\nthis.dispatchEvent(new CustomEvent('select', {\n detail: { recordId: this.recordId },\n bubbles: true, // Bubbles through DOM\n composed: true // Crosses shadow boundary\n}));\n\n// Parent handles event\nhandleSelect(event) {\n const recordId = event.detail.recordId;\n}\n```\n\n### Event Bubbling Configuration\n\nChoose the minimum propagation scope needed:\n\n| Configuration | Encapsulation | Use Case |\n|---------------|---------------|----------|\n| `{ bubbles: false, composed: false }` | **Maximum** (Preferred) | Direct parent-child communication |\n| `{ bubbles: true, composed: false }` | Acceptable | Internal shadow DOM communication |\n| `{ bubbles: false, composed: true }` | Acceptable | Cross shadow boundary without full bubbling |\n| `{ bubbles: true, composed: true }` | **Discouraged** | Only when grandparent+ must handle event |\n\n```javascript\n// ✅ Preferred: Maximum encapsulation\nthis.dispatchEvent(new CustomEvent('select', {\n detail: { recordId: this.recordId }\n // bubbles and composed default to false\n}));\n\n// ⚠️ Use only when necessary\nthis.dispatchEvent(new CustomEvent('globalnotify', {\n detail: { message: 'Record saved' },\n bubbles: true,\n composed: true\n}));\n```\n\n### Event Data Passing\n\n```javascript\n// Primitives: pass directly in detail\nthis.dispatchEvent(new CustomEvent('update', {\n detail: this.recordId // string — no wrapping needed\n}));\n\n// Non-primitives: always pass a copy to prevent mutation\nthis.dispatchEvent(new CustomEvent('change', {\n detail: { ...this.formData } // shallow copy\n}));\n```\n\n### Event Naming Conventions\n\n```\n✅ GOOD ❌ BAD\n──────────────────────── ────────────────────────\nonselect onSelectItem\nonrecordchange on-record-change\nonsave onSaveClicked\nonerror onErrorOccurred\n```\n\n### When to Use LMS vs Events\n\n| Scenario | Use |\n|----------|-----|\n| Parent-child communication | Custom events |\n| Sibling components (same parent) | Events via parent |\n| Components on different parts of page | Lightning Message Service |\n| LWC to Aura communication | LMS |\n| LWC to Visualforce | LMS |\n\n### Debouncing Pattern\n\n```javascript\ndelayTimeout;\n\nhandleSearch(event) {\n const searchTerm = event.target.value;\n clearTimeout(this.delayTimeout);\n\n this.delayTimeout = setTimeout(() => {\n this.searchTerm = searchTerm;\n }, 300); // 300ms debounce\n}\n```\n\n---\n\n## Spread Patterns & Destructuring\n\n### lwc:spread Directive\n\nThe `lwc:spread` directive dynamically spreads object properties as component attributes. Useful for reducing boilerplate and enabling dynamic attribute binding.\n\n**Reference**: [Saurabh Samir - lwc:spread Directive](https://medium.com/@saurabh.samirs)\n\n#### Basic Usage\n\n```html\n\u003c!-- Without lwc:spread (verbose) -->\n\u003clightning-button\n label={buttonLabel}\n variant={buttonVariant}\n disabled={isDisabled}\n onclick={handleClick}>\n\u003c/lightning-button>\n\n\u003c!-- With lwc:spread (dynamic) -->\n\u003clightning-button lwc:spread={buttonAttributes} onclick={handleClick}>\u003c/lightning-button>\n```\n\n```javascript\nget buttonAttributes() {\n return {\n label: this.buttonLabel,\n variant: this.isImportant ? 'brand' : 'neutral',\n disabled: this.isProcessing\n };\n}\n```\n\n#### lwc:spread vs @api Object Binding\n\n| Approach | Use When | Reactivity |\n|----------|----------|------------|\n| `lwc:spread={obj}` | Passing multiple attributes dynamically | Re-renders on object change |\n| `@api config` | Passing structured data to custom component | Must spread in child |\n| Individual `@api` props | Simple, known properties | Each prop triggers render |\n\n#### Conditional Attribute Spreading\n\n```javascript\nget inputAttributes() {\n const attrs = {\n label: 'Search',\n type: 'text',\n value: this.searchTerm\n };\n\n // Conditionally add attributes\n if (this.isRequired) {\n attrs.required = true;\n }\n\n if (this.maxLength) {\n attrs['max-length'] = this.maxLength;\n }\n\n return attrs;\n}\n```\n\n```html\n\u003clightning-input lwc:spread={inputAttributes} onchange={handleChange}>\u003c/lightning-input>\n```\n\n#### Event Handlers with lwc:spread\n\n**Important**: Event handlers must be bound separately, not spread:\n\n```html\n\u003c!-- ✅ CORRECT: Event handler separate from spread -->\n\u003clightning-button lwc:spread={buttonProps} onclick={handleClick}>\u003c/lightning-button>\n\n\u003c!-- ❌ INCORRECT: onclick in spread object won't work -->\n\u003c!-- buttonProps = { label: 'Save', onclick: this.handleClick } -->\n```\n\n### lwc:on Directive (Spring '26 - API 66.0)\n\nThe `lwc:on` directive solves the limitation above by enabling **dynamic event binding** directly from JavaScript. It allows you to bind multiple event handlers at runtime.\n\n**Requires**: API 66.0+ (Spring '26)\n\n#### Basic Usage\n\n```javascript\n// component.js\nexport default class DynamicEventComponent extends LightningElement {\n // Define event handlers as object properties\n eventHandlers = {\n click: this.handleClick.bind(this),\n mouseover: this.handleMouseOver.bind(this),\n focus: this.handleFocus.bind(this)\n };\n\n handleClick() {\n console.log('Element clicked!');\n }\n\n handleMouseOver() {\n console.log('Mouse over!');\n }\n\n handleFocus() {\n console.log('Element focused!');\n }\n}\n```\n\n```html\n\u003c!-- template.html -->\n\u003ctemplate>\n \u003c!-- Bind multiple event handlers dynamically -->\n \u003cbutton lwc:on={eventHandlers}>Click Me\u003c/button>\n\u003c/template>\n```\n\n#### Combining lwc:spread and lwc:on\n\nFor fully dynamic components, combine both directives:\n\n```javascript\n// component.js\nexport default class FullyDynamicButton extends LightningElement {\n // Properties via lwc:spread\n buttonAttributes = {\n label: 'Save',\n variant: 'brand',\n disabled: false\n };\n\n // Events via lwc:on\n buttonEvents = {\n click: this.handleClick.bind(this),\n focus: this.handleFocus.bind(this)\n };\n\n handleClick() {\n this.dispatchEvent(new CustomEvent('save'));\n }\n\n handleFocus() {\n console.log('Button focused');\n }\n}\n```\n\n```html\n\u003c!-- template.html -->\n\u003ctemplate>\n \u003c!-- Best of both worlds: dynamic props AND dynamic events -->\n \u003clightning-button\n lwc:spread={buttonAttributes}\n lwc:on={buttonEvents}>\n \u003c/lightning-button>\n\u003c/template>\n```\n\n#### Dynamic Event Handlers from @api\n\nPass event handler configurations from parent components:\n\n```javascript\n// childComponent.js\nexport default class ChildComponent extends LightningElement {\n @api eventConfig; // { click: handler, change: handler }\n\n get resolvedHandlers() {\n // Ensure handlers are properly bound\n const handlers = {};\n if (this.eventConfig) {\n Object.entries(this.eventConfig).forEach(([event, handler]) => {\n handlers[event] = typeof handler === 'function' ? handler : () => {};\n });\n }\n return handlers;\n }\n}\n```\n\n```html\n\u003c!-- childComponent.html -->\n\u003ctemplate>\n \u003cdiv lwc:on={resolvedHandlers}>\n \u003cslot>\u003c/slot>\n \u003c/div>\n\u003c/template>\n```\n\n#### Removing Event Listeners\n\nRemove specific event listeners by omitting them from the object:\n\n```javascript\n// Toggle mouseover handler on/off\ntoggleHoverHandler() {\n if (this._hoverEnabled) {\n // Remove mouseover by omitting it\n this.eventHandlers = {\n click: this.handleClick.bind(this)\n };\n } else {\n // Add mouseover back\n this.eventHandlers = {\n click: this.handleClick.bind(this),\n mouseover: this.handleMouseOver.bind(this)\n };\n }\n this._hoverEnabled = !this._hoverEnabled;\n}\n```\n\n#### lwc:spread vs lwc:on Comparison\n\n| Directive | Purpose | Use Case |\n|-----------|---------|----------|\n| `lwc:spread` | Dynamic **properties/attributes** | Pass label, variant, disabled dynamically |\n| `lwc:on` | Dynamic **event handlers** | Bind click, change, custom events dynamically |\n| Both together | Fully dynamic configuration | Reusable wrapper components, dynamic UIs |\n\n**Important Notes**:\n- Do NOT mutate the object passed to `lwc:on` - create a new object to update handlers\n- Event type names should be lowercase without the `on` prefix (use `click` not `onclick`)\n- Always use `.bind(this)` or arrow functions to preserve context\n\n### Object Spread & Destructuring\n\nModern JavaScript patterns for cleaner data handling in LWC.\n\n#### Object Spread for Config Merging\n\n```javascript\n// Default + user config pattern\nconst defaultConfig = {\n pageSize: 10,\n sortField: 'Name',\n sortDirection: 'ASC'\n};\n\nget tableConfig() {\n return {\n ...defaultConfig,\n ...this.userConfig // User config overrides defaults\n };\n}\n```\n\n#### Destructuring with Defaults\n\n```javascript\n// Extract values with fallbacks\nhandleRecordLoad(record) {\n const {\n Name = 'Unknown',\n Industry = 'Not Specified',\n AnnualRevenue = 0\n } = record.fields;\n\n this.accountName = Name.value;\n this.industry = Industry.value;\n this.revenue = AnnualRevenue.value;\n}\n```\n\n#### Nested Destructuring\n\n```javascript\n// Deep extraction in single statement\nprocessResult(result) {\n const {\n data: {\n record: {\n fields: { Name, BillingCity }\n }\n },\n error\n } = result;\n\n if (error) {\n this.handleError(error);\n return;\n }\n\n this.name = Name.value;\n this.city = BillingCity.value;\n}\n```\n\n#### Array Spread Patterns\n\n```javascript\n// Immutable array updates (required for LWC reactivity)\naddItem(newItem) {\n this.items = [...this.items, newItem]; // Append\n}\n\nremoveItem(index) {\n this.items = [\n ...this.items.slice(0, index),\n ...this.items.slice(index + 1)\n ]; // Remove at index\n}\n\nupdateItem(index, updates) {\n this.items = this.items.map((item, i) =>\n i === index ? { ...item, ...updates } : item\n ); // Update at index\n}\n```\n\n#### Parameter Spreading in Apex Calls\n\n```javascript\nasync handleSubmit() {\n const result = await createRecord({\n ...this.recordData,\n CreatedBy__c: this.currentUserId,\n Status__c: 'Pending'\n });\n}\n```\n\n### When to Use Each Pattern\n\n| Pattern | Best For | Avoid When |\n|---------|----------|------------|\n| `lwc:spread` | Many dynamic attributes, base component wrappers | Need event binding, simple static props |\n| Object spread | Config merging, immutable updates | Deep objects (consider structuredClone) |\n| Destructuring | Extracting multiple values, API responses | Simple single-property access |\n| Array spread | Adding/removing items immutably | Large arrays (performance concern) |\n\n---\n\n## Complex Template Expressions (Spring '26 Beta - API 66.0)\n\nSpring '26 introduces **complex template expressions**, enabling JavaScript expressions directly in templates. This was previously limited to simple property and getter bindings.\n\n> ⚠️ **Beta Feature**: Use getters in production until this becomes GA. Document any complex expressions for future migration.\n\n### Before vs After\n\n```html\n\u003c!-- BEFORE Spring '26: Required getters for any logic -->\n\u003ctemplate>\n \u003c!-- Simple property binding only -->\n \u003ctemplate lwc:if={isValid}>...\u003c/template>\n\n \u003c!-- Complex conditions needed a getter -->\n \u003ctemplate lwc:if={showLoadingState}>...\u003c/template>\n\u003c/template>\n```\n\n```javascript\n// Required getter in JS\nget showLoadingState() {\n return this.isLoading && this.items.length === 0;\n}\n```\n\n```html\n\u003c!-- AFTER Spring '26 (Beta): Complex expressions in template -->\n\u003ctemplate>\n \u003c!-- Logical operators -->\n \u003ctemplate lwc:if={!isLoading && items.length > 0}>\n \u003cc-item-list items={items}>\u003c/c-item-list>\n \u003c/template>\n\n \u003c!-- Optional chaining -->\n \u003ctemplate lwc:if={user?.permissions?.canEdit}>\n \u003clightning-button label=\"Edit\">\u003c/lightning-button>\n \u003c/template>\n\n \u003c!-- Arithmetic expressions -->\n \u003cspan class=\"slds-text-body_small\">\n Total: ${total * taxRate}\n \u003c/span>\n\n \u003c!-- Comparison operators -->\n \u003ctemplate lwc:if={items.length >= minItems}>\n \u003cc-pagination>\u003c/c-pagination>\n \u003c/template>\n\u003c/template>\n```\n\n### Supported Expression Types\n\n| Expression Type | Example | Notes |\n|-----------------|---------|-------|\n| **Logical NOT** | `{!isLoading}` | Negation |\n| **Logical AND** | `{a && b}` | Short-circuit evaluation |\n| **Logical OR** | `{a \\|\\| b}` | Short-circuit evaluation |\n| **Comparison** | `{count > 0}`, `{status === 'active'}` | `==`, `===`, `!=`, `!==`, `\u003c`, `>`, `\u003c=`, `>=` |\n| **Arithmetic** | `{price * quantity}` | `+`, `-`, `*`, `/`, `%` |\n| **Optional Chaining** | `{user?.profile?.name}` | Safe property access |\n| **Nullish Coalescing** | `{value ?? 'default'}` | Default for null/undefined |\n| **Ternary** | `{isActive ? 'Yes' : 'No'}` | Conditional value |\n| **Array Access** | `{items[0]}` | Index-based access |\n| **String Concatenation** | `{firstName + ' ' + lastName}` | String joining |\n\n### Best Practices for Complex Expressions\n\n```html\n\u003c!-- ✅ GOOD: Simple inline logic -->\n\u003ctemplate lwc:if={!isLoading && hasData}>\n ...\n\u003c/template>\n\n\u003c!-- ✅ GOOD: Optional chaining for safety -->\n\u003cspan>{account?.Owner?.Name}\u003c/span>\n\n\u003c!-- ⚠️ CAUTION: Keep expressions readable -->\n\u003c!-- If expression is long, consider a getter for maintainability -->\n\u003ctemplate lwc:if={isEditable && hasPermission && !isLocked && status === 'draft'}>\n \u003c!-- Consider: get canEdit() { return ...; } -->\n\u003c/template>\n\n\u003c!-- ❌ AVOID: Side effects in expressions -->\n\u003c!-- Don't call methods that modify state -->\n```\n\n### Migration Strategy\n\n1. **New code**: Use complex expressions for simple conditions\n2. **Existing code**: Keep getters that have unit tests\n3. **Complex logic**: Continue using getters for maintainability\n4. **Document**: Mark complex expressions in templates for review when GA\n\n### Limitations (Beta)\n\n- No function calls in expressions (use getters)\n- No template literals with `${}` interpolation\n- Cannot reference `this` directly\n- No destructuring in expressions\n\n---\n\n## Template Directives\n\n### Conditional Rendering: Legacy → Modern\n\nAlways use `lwc:if`, `lwc:elseif`, `lwc:else` instead of the deprecated `if:true` / `if:false` directives.\n\n```html\n\u003c!-- ❌ Legacy (deprecated) -->\n\u003ctemplate if:true={isLoading}>\n \u003clightning-spinner>\u003c/lightning-spinner>\n\u003c/template>\n\u003ctemplate if:false={isLoading}>\n \u003cc-data-view data={records}>\u003c/c-data-view>\n\u003c/template>\n\n\u003c!-- ✅ Modern -->\n\u003ctemplate lwc:if={isLoading}>\n \u003clightning-spinner>\u003c/lightning-spinner>\n\u003c/template>\n\u003ctemplate lwc:elseif={error}>\n \u003cc-error-panel errors={error}>\u003c/c-error-panel>\n\u003c/template>\n\u003ctemplate lwc:else>\n \u003cc-data-view data={records}>\u003c/c-data-view>\n\u003c/template>\n```\n\n**Rules**: Conditional directives are valid on `\u003ctemplate>`, standard HTML tags, custom components, and base components. All elements in a conditional group must be siblings at the same DOM level.\n\n### List Rendering\n\n#### for:each\n\nEvery `for:each` must be paired with `for:item`. The `key` attribute must use a stable unique identifier — always `key={item.id}`, never an index.\n\n```html\n\u003c!-- ✅ GOOD -->\n\u003ctemplate for:each={accounts} for:item=\"account\">\n \u003cc-account-card key={account.Id} account={account}>\u003c/c-account-card>\n\u003c/template>\n\n\u003c!-- ❌ BAD: key={index} or key={account.Name} -->\n```\n\n#### iterator Directive\n\nUse `iterator` when you need access to `.first` or `.last` metadata for conditional styling.\n\n```html\n\u003ctemplate iterator:it={contacts}>\n \u003cdiv key={it.value.Id}>\n {it.value.Name}\n \u003c/div>\n\u003c/template>\n```\n\n**Iterator name must be lowercase**. Access item data via `{iteratorname}.value.property` and metadata via `.index`, `.first`, `.last`.\n\n#### Nested Loop Rules\n\nUse distinct `for:item` or `iterator` names in nested loops to avoid variable shadowing:\n\n```html\n\u003c!-- ✅ Distinct names -->\n\u003ctemplate for:each={departments} for:item=\"dept\">\n \u003cdiv key={dept.Id}>\n \u003ctemplate for:each={dept.Employees} for:item=\"emp\">\n \u003cspan key={emp.Id}>{emp.Name}\u003c/span>\n \u003c/template>\n \u003c/div>\n\u003c/template>\n```\n\n### Multiple Template Rendering\n\nWhen a component needs to switch between entirely different layouts, import multiple HTML templates and return the appropriate one from `render()`.\n\n```javascript\nimport defaultTemplate from './myComponent.html';\nimport editTemplate from './myComponentEdit.html';\n\nexport default class MyComponent extends LightningElement {\n isEditing = false;\n\n render() {\n return this.isEditing ? editTemplate : defaultTemplate;\n }\n}\n```\n\n---\n\n## Performance Optimization (PICKLES: Execution)\n\n### Lifecycle Hook Guidance\n\n| Hook | When to Use | Avoid |\n|------|-------------|-------|\n| `constructor()` | Initialize properties | DOM access (not ready) |\n| `connectedCallback()` | Subscribe to events, fetch data | Heavy processing |\n| `renderedCallback()` | DOM-dependent logic | Infinite loops, property changes |\n| `disconnectedCallback()` | Cleanup subscriptions/listeners | Async operations |\n\n### Lazy Loading\n\n```html\n\u003c!-- Only render when needed -->\n\u003ctemplate lwc:if={showDetails}>\n \u003cc-expensive-component record-id={recordId}>\u003c/c-expensive-component>\n\u003c/template>\n```\n\n### Efficient Rendering\n\n```javascript\n// Bad: Creates new array every render\nget filteredItems() {\n return this.items.filter(item => item.active);\n}\n\n// Good: Cache the result\n_filteredItems;\n_itemsHash;\n\nget filteredItems() {\n const currentHash = JSON.stringify(this.items);\n if (currentHash !== this._itemsHash) {\n this._filteredItems = this.items.filter(item => item.active);\n this._itemsHash = currentHash;\n }\n return this._filteredItems;\n}\n```\n\n### Virtual Scrolling\n\nUse `lightning-datatable` with `enable-infinite-loading` for large datasets instead of rendering all items.\n\n**For comprehensive performance patterns** (DOM optimization, event delegation, memory management, bundle size): see `references/performance-guide.md`\n\n---\n\n## Advanced Jest Testing Patterns\n\nBased on [James Simone's advanced testing patterns](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/).\n\n### Render Cycle Helper\n\nLWC re-rendering is asynchronous. Use this helper to document and await render cycles:\n\n```javascript\n// testUtils.js\nexport const runRenderingLifecycle = async (reasons = ['render']) => {\n while (reasons.length > 0) {\n await Promise.resolve(reasons.pop());\n }\n};\n\n// Usage in tests\nit('updates after property change', async () => {\n const element = createElement('c-example', { is: Example });\n document.body.appendChild(element);\n\n element.greeting = 'new value';\n await runRenderingLifecycle(['property change', 'render']);\n\n expect(element.shadowRoot.querySelector('div').textContent).toBe('new value');\n});\n```\n\n### Proxy Unboxing (Lightning Web Security)\n\nLightning Web Security proxifies objects. Unbox them for assertions:\n\n```javascript\n// LWS proxifies complex objects - unbox for comparison\nconst unboxedData = JSON.parse(JSON.stringify(component.data));\nexpect(unboxedData).toEqual(expectedData);\n```\n\n### DOM Cleanup Pattern\n\nClean up after each test to prevent state bleed:\n\n```javascript\ndescribe('c-my-component', () => {\n afterEach(() => {\n // Clean up DOM\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n});\n```\n\n### ResizeObserver Polyfill\n\nSome components use ResizeObserver. Add polyfill in jest.setup.js:\n\n```javascript\n// jest.setup.js\nif (!window.ResizeObserver) {\n window.ResizeObserver = class ResizeObserver {\n constructor(callback) {\n this.callback = callback;\n }\n observe() {}\n unobserve() {}\n disconnect() {}\n };\n}\n```\n\n### Mocking Apex Methods\n\n```javascript\njest.mock('@salesforce/apex/MyController.getData', () => ({\n default: jest.fn()\n}), { virtual: true });\n\n// In test\nimport getData from '@salesforce/apex/MyController.getData';\n\nit('displays data', async () => {\n getData.mockResolvedValue(MOCK_DATA);\n // ... test code\n});\n```\n\n---\n\n## Security Best Practices (PICKLES: Security)\n\n### FLS Enforcement\n\n```apex\n// Always use SECURITY_ENFORCED or stripInaccessible\n@AuraEnabled(cacheable=true)\npublic static List\u003cAccount> getAccounts() {\n return [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED];\n}\n\n// For DML operations\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.CREATABLE,\n records\n);\ninsert decision.getRecords();\n```\n\n### Input Sanitization\n\n```apex\n// Apex should escape user input\nString searchKey = '%' + String.escapeSingleQuotes(searchTerm) + '%';\n```\n\n### XSS Prevention\n\nLWC automatically escapes content in templates. Never bypass this.\n\n```html\n\u003c!-- Safe: LWC auto-escapes -->\n\u003cp>{userInput}\u003c/p>\n```\n\n### Input Validation Patterns\n\n#### Lightning Base Component Validation Attributes\n\nUse built-in validation attributes on Lightning input components to enforce constraints declaratively:\n\n```html\n\u003clightning-input\n label=\"Email\"\n type=\"email\"\n required\n max-length=\"255\"\n message-when-value-missing=\"Email is required\"\n message-when-pattern-mismatch=\"Enter a valid email address\"\n onchange={handleEmailChange}>\n\u003c/lightning-input>\n```\n\n#### Form Submission Validation\n\nAlways validate all inputs before processing a form submission:\n\n```javascript\nhandleSubmit() {\n const allValid = [...this.template.querySelectorAll('lightning-input')]\n .reduce((valid, input) => {\n input.reportValidity();\n return valid && input.checkValidity();\n }, true);\n\n if (!allValid) {\n return; // Stop — validation errors displayed to user\n }\n\n this.saveRecord();\n}\n```\n\n#### Custom Validation with setCustomValidity\n\n```javascript\nhandleBlur(event) {\n const input = event.target;\n if (input.value && !this.isUniqueName(input.value)) {\n input.setCustomValidity('This name is already in use');\n } else {\n input.setCustomValidity(''); // Always clear when valid\n }\n input.reportValidity();\n}\n```\n\n### Scoped Module Imports\n\nAlways use static `@salesforce/` scoped imports instead of legacy Global Value Providers (`$Label`, `$Resource`, etc.)\n\n---\n\n## Accessibility (a11y)\n\n### Required Practices\n\n| Element | Requirement |\n|---------|-------------|\n| Buttons | `label` or `aria-label` |\n| Icons | `alternative-text` |\n| Form inputs | Associated `\u003clabel>` |\n| Dynamic content | `aria-live` region |\n| Loading states | `aria-busy=\"true\"` |\n\n### Keyboard Navigation\n\n```javascript\nhandleKeyDown(event) {\n switch (event.key) {\n case 'Enter':\n case ' ':\n this.handleSelect(event);\n break;\n case 'Escape':\n this.handleClose();\n break;\n case 'ArrowDown':\n this.focusNext();\n event.preventDefault();\n break;\n }\n}\n```\n\n### Focus Trap Pattern (for Modals)\n\nBased on [James Simone's modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/):\n\n```javascript\n_focusableElements = [];\n\n_onOpen() {\n // Collect focusable elements\n this._focusableElements = [\n ...this.querySelectorAll('.focusable'),\n ...this.template.querySelectorAll('lightning-button, button, [tabindex=\"0\"]')\n ].filter(el => !el.disabled);\n\n // Focus first element\n this._focusableElements[0]?.focus();\n\n // Add ESC handler\n window.addEventListener('keyup', this._handleKeyUp);\n}\n\n_handleKeyUp = (event) => {\n if (event.code === 'Escape') {\n this.close();\n }\n}\n\ndisconnectedCallback() {\n window.removeEventListener('keyup', this._handleKeyUp);\n}\n```\n\n---\n\n## SLDS 2 & Dark Mode\n\n### Dark Mode Checklist\n\n- [ ] No hardcoded hex colors (`#FFFFFF`, `#333333`)\n- [ ] No hardcoded RGB/RGBA values\n- [ ] All colors use CSS variables (`var(--slds-g-color-*)`)\n- [ ] Fallback values provided for SLDS 1 compatibility\n- [ ] Icons use SLDS utility icons (auto-adjust for dark mode)\n\n### SLDS 1 → SLDS 2 Migration\n\n```css\n/* BEFORE (SLDS 1 - Deprecated) */\n.my-card {\n background-color: #ffffff;\n color: #333333;\n}\n\n/* AFTER (SLDS 2 - Dark Mode Ready) */\n.my-card {\n background-color: var(--slds-g-color-surface-container-1, #ffffff);\n color: var(--slds-g-color-on-surface, #181818);\n}\n```\n\n### Key Global Styling Hooks\n\n| Category | SLDS 2 Variable |\n|----------|-----------------|\n| Surface | `--slds-g-color-surface-1` to `-4` |\n| Text | `--slds-g-color-on-surface` |\n| Border | `--slds-g-color-border-1`, `-2` |\n| Spacing | `--slds-g-spacing-0` to `-12` |\n\n**Important**: `--slds-c-*` (component-level hooks) are NOT supported in SLDS 2 yet.\n\n---\n\n## CSS Isolation & Scoping\n\nLWC uses Shadow DOM for style encapsulation. Follow these rules to prevent style leakage and collisions.\n\n### CSS Selector Rules\n\n| Pattern | Status | Reason |\n|---------|--------|--------|\n| `:host` | ✅ Use | Targets the component's root element |\n| `:host(.modifier)` | ✅ Use | Conditional styling based on host class |\n| `.my-class` | ✅ Use | Class selectors scoped automatically |\n| `*` (universal) | ❌ Avoid | Can leak outside component scope |\n| `#my-id` | ❌ Avoid | LWC transforms IDs to globally unique values |\n| `c-my-component` | ❌ Avoid | Use `:host` instead of component name |\n| `:host-context()` | ❌ Not supported | Use `:host` instead |\n| `lightning-button` | ❌ Avoid | Cannot override base component internals |\n| `.slds-button` | ❌ Avoid | Cannot replace or override SLDS classes |\n\n```css\n/* ❌ BAD: Universal selector leaks */\n* { font-family: Arial, sans-serif; }\n\n/* ✅ GOOD: Scoped universal */\n:host * { font-family: Arial, sans-serif; }\n\n/* ❌ BAD: Component name as selector */\nc-my-component { display: flex; }\n\n/* ✅ GOOD: :host for component-level styles */\n:host { display: flex; }\n\n/* ❌ BAD: Overriding base component or SLDS */\nlightning-button { background-color: red; }\n.slds-button { background-color: purple; }\n\n/* ✅ GOOD: Use styling hooks */\n:host {\n --slds-c-button-brand-color-background: red;\n}\n```\n\n### Avoid `!important` Overuse\n\nRely on proper specificity rather than `!important` declarations. Excessive `!important` interferes with parent component styling and makes future maintenance difficult.\n\n### Never Rely on Compiler-Generated Scope Tokens\n\n```css\n/* ❌ BAD: Brittle — token changes across builds */\nc-child[lwc-2j48dfhd928c-host] { padding: 1rem; }\n\n/* ✅ GOOD */\n:host { padding: 1rem; }\n```\n\n---\n\n## Testing Checklist\n\n### Unit Test Coverage\n\n- [ ] Component renders without errors\n- [ ] Data displays correctly when loaded\n- [ ] Error state displays when fetch fails\n- [ ] Empty state displays when no data\n- [ ] Events dispatch with correct payload\n- [ ] User interactions work correctly\n- [ ] Loading states are shown/hidden appropriately\n\n### Manual Testing\n\n- [ ] Works in Lightning Experience\n- [ ] Works in Salesforce Mobile\n- [ ] Works in Experience Cloud (if targeted)\n- [ ] Works in Dark Mode (SLDS 2)\n- [ ] Keyboard navigation works\n- [ ] Screen reader announces properly\n- [ ] No console errors\n- [ ] Performance acceptable with real data\n\n---\n\n## Common Mistakes\n\n### 1. Modifying @api Properties\n\n```javascript\n// ❌ BAD\n@api items;\nhandleClick() {\n this.items.push(newItem); // Mutation!\n}\n\n// ✅ GOOD\nhandleClick() {\n this.items = [...this.items, newItem];\n}\n```\n\n### 2. Forgetting to Clean Up\n\n```javascript\n// ❌ BAD: Memory leak\nconnectedCallback() {\n this.subscription = subscribe(...);\n}\n\n// ✅ GOOD\ndisconnectedCallback() {\n unsubscribe(this.subscription);\n}\n```\n\n### 3. Wire with Non-Reactive Parameters\n\n```javascript\n// ❌ BAD\nlet recordId = '001xxx';\n@wire(getRecord, { recordId: recordId })\n\n// ✅ GOOD\n@api recordId;\n@wire(getRecord, { recordId: '$recordId' })\n```\n\n---\n\n## Resources\n\n- [PICKLES Framework](https://www.salesforceben.com/the-ideal-framework-for-architecting-salesforce-lightning-web-components/) — David Picksley, Third Eye Consulting\n- [LWC Recipes (GitHub)](https://github.com/trailheadapps/lwc-recipes)\n- [SLDS 2 Transition Guide](https://www.lightningdesignsystem.com/2e1ef8501/p/8184ad-transition-to-slds-2)\n- [James Simone - Advanced Jest Testing](https://www.jamessimone.net/blog/joys-of-apex/advanced-lwc-jest-testing/)\n- [James Simone - Composable Modal](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/)\n- [SLDS Styling Hooks](https://developer.salesforce.com/docs/platform/lwc/guide/create-components-css-custom-properties.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":37153,"content_sha256":"278199aca2f4d093c1ed00777c2fba6b11ea83cf1353337188f72a36f5dadb79"},{"filename":"references/performance-guide.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Performance Optimization Guide for LWC\n\nComprehensive guide to optimizing Lightning Web Component performance, including dark mode implementation, lazy loading, and rendering optimization.\n\n---\n\n## Table of Contents\n\n1. [Dark Mode Implementation](#dark-mode-implementation)\n2. [Rendering Performance](#rendering-performance)\n3. [Lazy Loading](#lazy-loading)\n4. [Data Management](#data-management)\n5. [Event Optimization](#event-optimization)\n6. [Memory Management](#memory-management)\n7. [Bundle Size Optimization](#bundle-size-optimization)\n8. [Performance Testing](#performance-testing)\n9. [Common Anti-Patterns](#common-anti-patterns)\n\n---\n\n## Dark Mode Implementation\n\nDark mode is exclusive to SLDS 2 themes. Components must use global styling hooks to support light/dark theme switching.\n\n### Complete SLDS 2 Color Token Reference\n\n#### Surface Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-surface-1` | `#FFFFFF` | `#0B0B0B` | Primary surface (body background) |\n| `--slds-g-color-surface-2` | `#F3F3F3` | `#181818` | Secondary surface |\n| `--slds-g-color-surface-3` | `#E5E5E5` | `#2B2B2B` | Tertiary surface |\n| `--slds-g-color-surface-4` | `#C9C9C9` | `#3E3E3E` | Quaternary surface |\n\n#### Container Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-surface-container-1` | `#FAFAFA` | `#1A1A1A` | Card backgrounds, panels |\n| `--slds-g-color-surface-container-2` | `#F7F7F7` | `#232323` | Nested containers |\n| `--slds-g-color-surface-container-3` | `#F3F3F3` | `#2E2E2E` | Deep nesting |\n\n#### Text Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-on-surface` | `#181818` | `#FAFAFA` | Primary text |\n| `--slds-g-color-on-surface-1` | `#444444` | `#C9C9C9` | Secondary text |\n| `--slds-g-color-on-surface-2` | `#706E6B` | `#A0A0A0` | Muted/disabled text |\n| `--slds-g-color-on-surface-inverse` | `#FFFFFF` | `#181818` | Inverse text (buttons, badges) |\n\n#### Border Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-border-1` | `#C9C9C9` | `#444444` | Primary borders |\n| `--slds-g-color-border-2` | `#E5E5E5` | `#3E3E3E` | Secondary borders (dividers) |\n\n#### Brand Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-brand-1` | `#0176D3` | `#1B96FF` | Primary brand (buttons, links) |\n| `--slds-g-color-brand-2` | `#014486` | `#0B5CAB` | Brand hover/active states |\n\n#### Status Colors\n\n| Token | Light Mode | Dark Mode | Purpose |\n|-------|------------|-----------|---------|\n| `--slds-g-color-success-1` | `#2E844A` | `#45C65A` | Success states |\n| `--slds-g-color-error-1` | `#EA001E` | `#FE5C4C` | Error states |\n| `--slds-g-color-warning-1` | `#FFB75D` | `#FFB75D` | Warning states |\n| `--slds-g-color-info-1` | `#0176D3` | `#1B96FF` | Info states |\n\n#### Spacing Tokens\n\n| Token | Value (rem) | Value (px) |\n|-------|-------------|------------|\n| `--slds-g-spacing-0` | 0 | 0 |\n| `--slds-g-spacing-1` | 0.125 | 2 |\n| `--slds-g-spacing-2` | 0.25 | 4 |\n| `--slds-g-spacing-3` | 0.5 | 8 |\n| `--slds-g-spacing-4` | 0.75 | 12 |\n| `--slds-g-spacing-5` | 1 | 16 |\n| `--slds-g-spacing-6` | 1.5 | 24 |\n| `--slds-g-spacing-7` | 2 | 32 |\n| `--slds-g-spacing-8` | 3 | 48 |\n\n### Migration Examples\n\n#### Before: SLDS 1 (Hardcoded Colors)\n\n```css\n/* accountCard.css - SLDS 1 (Deprecated) */\n.card {\n background-color: #ffffff;\n color: #333333;\n border: 1px solid #dddddd;\n border-radius: 4px;\n padding: 16px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.card-header {\n color: #000000;\n font-size: 16px;\n font-weight: 700;\n margin-bottom: 8px;\n}\n\n.card-text {\n color: #666666;\n font-size: 14px;\n}\n\n.card-link {\n color: #0176d3;\n}\n\n.card-link:hover {\n color: #014486;\n text-decoration: underline;\n}\n```\n\n#### After: SLDS 2 (Dark Mode Ready)\n\n```css\n/* accountCard.css - SLDS 2 */\n.card {\n background-color: var(--slds-g-color-surface-container-1, #ffffff);\n color: var(--slds-g-color-on-surface, #181818);\n border: 1px solid var(--slds-g-color-border-2, #e5e5e5);\n border-radius: var(--slds-g-radius-border-2, 0.25rem);\n padding: var(--slds-g-spacing-5, 1rem);\n box-shadow: 0 2px 4px var(--slds-g-color-border-1, rgba(0, 0, 0, 0.1));\n}\n\n.card-header {\n color: var(--slds-g-color-on-surface, #181818);\n font-size: var(--slds-g-font-size-5, 1rem);\n font-weight: var(--slds-g-font-weight-bold, 700);\n margin-bottom: var(--slds-g-spacing-3, 0.5rem);\n}\n\n.card-text {\n color: var(--slds-g-color-on-surface-1, #444444);\n font-size: var(--slds-g-font-size-3, 0.875rem);\n}\n\n.card-link {\n color: var(--slds-g-color-brand-1, #0176d3);\n}\n\n.card-link:hover {\n color: var(--slds-g-color-brand-2, #014486);\n text-decoration: underline;\n}\n```\n\n### Component-Level Example\n\n```javascript\n// darkModeCard.js\nimport { LightningElement } from 'lwc';\n\nexport default class DarkModeCard extends LightningElement {\n // No JavaScript changes needed for dark mode!\n // All theming is handled via CSS variables\n}\n```\n\n```html\n\u003c!-- darkModeCard.html -->\n\u003ctemplate>\n \u003cdiv class=\"card\">\n \u003cdiv class=\"card-header\">\n \u003ch2 class=\"card-title\">Account Details\u003c/h2>\n \u003c/div>\n \u003cdiv class=\"card-body\">\n \u003cp class=\"card-text\">This card automatically adapts to light/dark mode\u003c/p>\n \u003ca href=\"#\" class=\"card-link\">Learn more\u003c/a>\n \u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n```css\n/* darkModeCard.css */\n.card {\n background-color: var(--slds-g-color-surface-container-1, #ffffff);\n border: 1px solid var(--slds-g-color-border-2, #e5e5e5);\n border-radius: var(--slds-g-radius-border-2, 0.25rem);\n padding: var(--slds-g-spacing-5, 1rem);\n}\n\n.card-header {\n border-bottom: 1px solid var(--slds-g-color-border-2, #e5e5e5);\n margin-bottom: var(--slds-g-spacing-4, 0.75rem);\n padding-bottom: var(--slds-g-spacing-3, 0.5rem);\n}\n\n.card-title {\n color: var(--slds-g-color-on-surface, #181818);\n font-size: var(--slds-g-font-size-5, 1rem);\n font-weight: var(--slds-g-font-weight-bold, 700);\n margin: 0;\n}\n\n.card-body {\n color: var(--slds-g-color-on-surface-1, #444444);\n}\n\n.card-text {\n margin-bottom: var(--slds-g-spacing-4, 0.75rem);\n}\n\n.card-link {\n color: var(--slds-g-color-brand-1, #0176d3);\n text-decoration: none;\n}\n\n.card-link:hover {\n color: var(--slds-g-color-brand-2, #014486);\n text-decoration: underline;\n}\n```\n\n### Validation Script\n\n```bash\n# Check for hardcoded colors in CSS\ngrep -r \"#[0-9A-Fa-f]\\{3,6\\}\" force-app/main/default/lwc/ --include=\"*.css\"\n\n# Check for rgb/rgba values\ngrep -r \"rgb\\|rgba\" force-app/main/default/lwc/ --include=\"*.css\"\n\n# Install SLDS Linter\nnpm install -g @salesforce-ux/slds-linter\n\n# Run validation\nslds-linter lint force-app/main/default/lwc/\n```\n\n---\n\n## Rendering Performance\n\n### Conditional Rendering\n\n```javascript\n// BAD: Re-renders entire list\n\u003ctemplate for:each={allItems} for:item=\"item\">\n \u003cdiv if:true={item.visible} key={item.id}>\n {item.name}\n \u003c/div>\n\u003c/template>\n\n// GOOD: Filter before rendering\nget visibleItems() {\n return this.allItems.filter(item => item.visible);\n}\n\n\u003ctemplate for:each={visibleItems} for:item=\"item\">\n \u003cdiv key={item.id}>{item.name}\u003c/div>\n\u003c/template>\n```\n\n### Use `lwc:if` for Large Blocks\n\n```html\n\u003c!-- lwc:if removes from DOM (better for large blocks) -->\n\u003ctemplate lwc:if={showDashboard}>\n \u003cc-dashboard data={dashboardData}>\u003c/c-dashboard>\n\u003c/template>\n\n\u003c!-- if:true hides with CSS (better for frequent toggling) -->\n\u003cdiv if:true={showMessage} class=\"message\">\n {message}\n\u003c/div>\n```\n\n### Key Directive for Lists\n\n```html\n\u003c!-- CRITICAL: Use unique, stable keys -->\n\u003ctemplate for:each={accounts} for:item=\"account\">\n \u003cdiv key={account.Id}> \u003c!-- Use record ID, not index -->\n {account.Name}\n \u003c/div>\n\u003c/template>\n```\n\n### Getter Caching\n\n```javascript\n// BAD: Recalculates on every render\nget formattedValue() {\n return this.expensiveCalculation(this.data);\n}\n\n// GOOD: Cache the result\n@track _cachedValue;\n_cacheKey;\n\nget formattedValue() {\n const currentKey = JSON.stringify(this.data);\n if (this._cacheKey !== currentKey) {\n this._cacheKey = currentKey;\n this._cachedValue = this.expensiveCalculation(this.data);\n }\n return this._cachedValue;\n}\n```\n\n### Avoid renderedCallback Loops\n\n```javascript\n// BAD: Infinite loop\nrenderedCallback() {\n this.count++; // Triggers re-render\n}\n\n// GOOD: Guard against loops\nrenderedCallback() {\n if (!this._rendered) {\n this._rendered = true;\n this.initializeChart();\n }\n}\n```\n\n---\n\n## Lazy Loading\n\n### Dynamic Imports\n\n```javascript\n// accountManager.js\nexport default class AccountManager extends LightningElement {\n @track showCharts = false;\n chartModule;\n\n async handleShowCharts() {\n if (!this.chartModule) {\n // Lazy load chart component\n this.chartModule = await import('c/accountChart');\n }\n this.showCharts = true;\n }\n}\n```\n\n### Intersection Observer for Lazy Loading\n\n```javascript\n// lazyImageLoader.js\nexport default class LazyImageLoader extends LightningElement {\n @api src;\n @api alt;\n\n isVisible = false;\n observer;\n\n renderedCallback() {\n if (!this.observer) {\n this.observer = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n this.isVisible = true;\n this.observer.disconnect();\n }\n });\n }, { rootMargin: '50px' });\n\n const img = this.template.querySelector('img');\n if (img) {\n this.observer.observe(img);\n }\n }\n }\n\n get imageSrc() {\n return this.isVisible ? this.src : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';\n }\n\n disconnectedCallback() {\n if (this.observer) {\n this.observer.disconnect();\n }\n }\n}\n```\n\n```html\n\u003c!-- lazyImageLoader.html -->\n\u003ctemplate>\n \u003cimg src={imageSrc} alt={alt} loading=\"lazy\">\n\u003c/template>\n```\n\n### Virtual Scrolling\n\n```javascript\n// virtualList.js\nexport default class VirtualList extends LightningElement {\n @api items = [];\n @track visibleItems = [];\n\n itemHeight = 50;\n containerHeight = 500;\n scrollTop = 0;\n\n get visibleCount() {\n return Math.ceil(this.containerHeight / this.itemHeight);\n }\n\n get startIndex() {\n return Math.floor(this.scrollTop / this.itemHeight);\n }\n\n get endIndex() {\n return Math.min(\n this.startIndex + this.visibleCount + 1,\n this.items.length\n );\n }\n\n get paddingTop() {\n return this.startIndex * this.itemHeight;\n }\n\n get paddingBottom() {\n return (this.items.length - this.endIndex) * this.itemHeight;\n }\n\n connectedCallback() {\n this.updateVisibleItems();\n }\n\n handleScroll(event) {\n this.scrollTop = event.target.scrollTop;\n this.updateVisibleItems();\n }\n\n updateVisibleItems() {\n this.visibleItems = this.items.slice(\n this.startIndex,\n this.endIndex\n );\n }\n}\n```\n\n```html\n\u003c!-- virtualList.html -->\n\u003ctemplate>\n \u003cdiv class=\"container\"\n style={containerStyle}\n onscroll={handleScroll}>\n \u003cdiv style={paddingTopStyle}>\u003c/div>\n \u003ctemplate for:each={visibleItems} for:item=\"item\">\n \u003cdiv key={item.id} class=\"item\">\n {item.name}\n \u003c/div>\n \u003c/template>\n \u003cdiv style={paddingBottomStyle}>\u003c/div>\n \u003c/div>\n\u003c/template>\n```\n\n---\n\n## Data Management\n\n### Debouncing\n\n```javascript\n// searchComponent.js\nexport default class SearchComponent extends LightningElement {\n searchTerm = '';\n delayTimeout;\n\n handleSearchChange(event) {\n const searchTerm = event.target.value;\n\n // Clear previous timeout\n clearTimeout(this.delayTimeout);\n\n // Set new timeout (300ms debounce)\n this.delayTimeout = setTimeout(() => {\n this.performSearch(searchTerm);\n }, 300);\n }\n\n async performSearch(term) {\n try {\n const results = await searchAccounts({ searchTerm: term });\n this.results = results;\n } catch (error) {\n this.handleError(error);\n }\n }\n\n disconnectedCallback() {\n clearTimeout(this.delayTimeout);\n }\n}\n```\n\n### Throttling\n\n```javascript\n// scrollTracker.js\nexport default class ScrollTracker extends LightningElement {\n lastScrollTime = 0;\n throttleDelay = 100;\n\n handleScroll(event) {\n const now = Date.now();\n\n if (now - this.lastScrollTime >= this.throttleDelay) {\n this.lastScrollTime = now;\n this.processScroll(event);\n }\n }\n\n processScroll(event) {\n // Handle scroll logic\n console.log('Scroll position:', event.target.scrollTop);\n }\n}\n```\n\n### Caching Wire Results\n\n```javascript\n// accountList.js\nexport default class AccountList extends LightningElement {\n @api recordId;\n wiredAccountsResult;\n\n @wire(getAccounts, { accountId: '$recordId' })\n wiredAccounts(result) {\n this.wiredAccountsResult = result; // Cache for refreshApex\n if (result.data) {\n this.accounts = result.data;\n } else if (result.error) {\n this.error = result.error;\n }\n }\n\n async handleRefresh() {\n // Refresh cached wire result\n await refreshApex(this.wiredAccountsResult);\n }\n}\n```\n\n---\n\n## Event Optimization\n\n### Event Delegation\n\n```javascript\n// BAD: Multiple event listeners\n\u003ctemplate for:each={items} for:item=\"item\">\n \u003cbutton key={item.id} onclick={handleClick} data-id={item.id}>\n {item.name}\n \u003c/button>\n\u003c/template>\n\n// GOOD: Single delegated listener\n\u003cdiv onclick={handleContainerClick}>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cbutton key={item.id} data-id={item.id}>\n {item.name}\n \u003c/button>\n \u003c/template>\n\u003c/div>\n\nhandleContainerClick(event) {\n if (event.target.tagName === 'BUTTON') {\n const itemId = event.target.dataset.id;\n this.processClick(itemId);\n }\n}\n```\n\n### Prevent Event Bubbling\n\n```javascript\nhandleClick(event) {\n event.stopPropagation(); // Stop bubbling\n event.preventDefault(); // Prevent default action\n\n // Process event\n}\n```\n\n---\n\n## Memory Management\n\n### Cleanup in disconnectedCallback\n\n```javascript\nexport default class ResourceManager extends LightningElement {\n subscription;\n intervalId;\n observer;\n\n connectedCallback() {\n // Subscribe to events\n this.subscription = subscribe(\n this.messageContext,\n CHANNEL,\n this.handleMessage\n );\n\n // Set interval\n this.intervalId = setInterval(() => {\n this.updateData();\n }, 5000);\n\n // Create observer\n this.observer = new IntersectionObserver(\n this.handleIntersection\n );\n }\n\n disconnectedCallback() {\n // CRITICAL: Clean up all resources\n if (this.subscription) {\n unsubscribe(this.subscription);\n this.subscription = null;\n }\n\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n\n if (this.observer) {\n this.observer.disconnect();\n this.observer = null;\n }\n }\n}\n```\n\n### Remove Event Listeners\n\n```javascript\nexport default class EventManager extends LightningElement {\n boundHandler;\n\n connectedCallback() {\n this.boundHandler = this.handleResize.bind(this);\n window.addEventListener('resize', this.boundHandler);\n }\n\n disconnectedCallback() {\n window.removeEventListener('resize', this.boundHandler);\n }\n\n handleResize() {\n // Handle resize\n }\n}\n```\n\n---\n\n## Bundle Size Optimization\n\n### Code Splitting\n\n```javascript\n// Import only what you need\nimport { getRecord } from 'lightning/uiRecordApi';\nimport NAME_FIELD from '@salesforce/schema/Account.Name';\n\n// Don't import entire modules\n// BAD: import * as uiRecordApi from 'lightning/uiRecordApi';\n```\n\n### Minimize Dependencies\n\n```javascript\n// BAD: Import heavy library for simple task\nimport moment from 'moment';\n\nget formattedDate() {\n return moment(this.date).format('MM/DD/YYYY');\n}\n\n// GOOD: Use native APIs\nget formattedDate() {\n return new Intl.DateTimeFormat('en-US').format(new Date(this.date));\n}\n```\n\n---\n\n## Performance Testing\n\n### Chrome DevTools Performance Tab\n\n```javascript\n// Add performance marks\nexport default class PerformanceTracked extends LightningElement {\n connectedCallback() {\n performance.mark('component-start');\n this.initializeComponent();\n performance.mark('component-end');\n performance.measure(\n 'component-initialization',\n 'component-start',\n 'component-end'\n );\n\n const measure = performance.getEntriesByName('component-initialization')[0];\n console.log('Initialization took:', measure.duration, 'ms');\n }\n}\n```\n\n### Lighthouse Audit\n\n```bash\nlighthouse https://your-org.lightning.force.com --only-categories=performance\n```\n\n### Custom Performance Metrics\n\n```javascript\nexport default class MetricsTracker extends LightningElement {\n connectedCallback() {\n // Track time to interactive\n const startTime = performance.now();\n\n this.loadData().then(() => {\n const endTime = performance.now();\n console.log('Time to interactive:', endTime - startTime, 'ms');\n });\n }\n}\n```\n\n---\n\n\u003ca id=\"common-anti-patterns\">\u003c/a>\n\n## Common Anti-Patterns\n\n### 1. Excessive Wire Calls\n\n```javascript\n// BAD: Multiple wire calls for related data\n@wire(getAccount, { accountId: '$recordId' }) account;\n@wire(getContacts, { accountId: '$recordId' }) contacts;\n@wire(getOpportunities, { accountId: '$recordId' }) opportunities;\n\n// GOOD: Single wire call with joined data\n@wire(getAccountWithRelated, { accountId: '$recordId' })\nwiredData({ data, error }) {\n if (data) {\n this.account = data.account;\n this.contacts = data.contacts;\n this.opportunities = data.opportunities;\n }\n}\n```\n\n### 2. Updating Tracked Properties in Getters\n\n```javascript\n// BAD: Side effects in getter\n@track count = 0;\n\nget message() {\n this.count++; // Causes infinite re-render!\n return `Count: ${this.count}`;\n}\n\n// GOOD: Pure getter\nget message() {\n return `Count: ${this.count}`;\n}\n```\n\n### 3. Not Using @track Wisely\n\n```javascript\n// BAD: Over-using @track\n@track simpleValue = 'hello';\n@track anotherValue = 42;\n\n// GOOD: Only track complex objects\nsimpleValue = 'hello'; // Primitives don't need @track\n@track complexObject = { nested: { value: 42 } };\n```\n\n### 4. Heavy Operations in renderedCallback\n\n```javascript\n// BAD: Heavy calculation every render\nrenderedCallback() {\n this.calculateComplexMetrics(); // Expensive!\n}\n\n// GOOD: Calculate only when data changes\n@track _dataVersion = 0;\n_renderedVersion = -1;\n\nrenderedCallback() {\n if (this._renderedVersion !== this._dataVersion) {\n this._renderedVersion = this._dataVersion;\n this.calculateComplexMetrics();\n }\n}\n\nhandleDataChange() {\n this._dataVersion++;\n}\n```\n\n---\n\n## Performance Checklist\n\n- [ ] Use SLDS 2 color tokens (dark mode ready)\n- [ ] Lazy load components with dynamic imports\n- [ ] Implement virtual scrolling for long lists (100+ items)\n- [ ] Debounce search inputs (300ms)\n- [ ] Throttle scroll/resize handlers (100ms)\n- [ ] Cache expensive getter calculations\n- [ ] Use `lwc:if` for large conditional blocks\n- [ ] Provide stable keys in `for:each` loops\n- [ ] Clean up resources in `disconnectedCallback()`\n- [ ] Avoid heavy operations in `renderedCallback()`\n- [ ] Use event delegation for list items\n- [ ] Minimize wire service calls\n- [ ] Remove unused imports\n- [ ] Test with Chrome DevTools Performance tab\n- [ ] Run Lighthouse performance audit\n\n---\n\n## Related Resources\n\n- [component-patterns.md](component-patterns.md) - Implementation patterns\n- [accessibility-guide.md](accessibility-guide.md) - A11y compliance\n- [jest-testing.md](jest-testing.md) - Testing strategies\n- [SLDS 2 Transition Guide](https://www.lightningdesignsystem.com/2e1ef8501/p/8184ad-transition-to-slds-2)\n- [LWC Developer Guide](https://developer.salesforce.com/docs/platform/lwc/guide)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20786,"content_sha256":"b0c71edfaf3cd643fb6e9f5d319d4fdc3c3235194727fc0a24f14105882d1cc6"},{"filename":"references/scoring-and-testing.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n\n# SLDS 2 Validation (165-Point Scoring) & Jest Testing\n\n## SLDS 2 Scoring Categories\n\n| Category | Points | Key Checks |\n|----------|--------|------------|\n| **SLDS Class Usage** | 25 | Valid class names, proper `slds-*` utilities |\n| **Accessibility** | 25 | ARIA labels, roles, alt-text, keyboard navigation |\n| **Dark Mode Readiness** | 25 | No hardcoded colors, CSS variables only |\n| **SLDS Migration** | 20 | No deprecated SLDS 1 patterns/tokens |\n| **Styling Hooks** | 20 | Proper `--slds-g-*` variable usage |\n| **Component Structure** | 15 | Uses `lightning-*` base components |\n| **Performance** | 10 | Efficient selectors, no `!important` |\n| **PICKLES Compliance** | 25 | Architecture methodology adherence (optional) |\n\n**Scoring Thresholds**:\n```\n⭐⭐⭐⭐⭐ 150-165 pts → Production-ready, full SLDS 2 + Dark Mode\n⭐⭐⭐⭐ 125-149 pts → Good component, minor styling issues\n⭐⭐⭐ 100-124 pts → Functional, needs SLDS cleanup\n⭐⭐ 75-99 pts → Basic functionality, SLDS issues\n⭐ \u003c75 pts → Needs significant work\n```\n\n---\n\n## Dark Mode Readiness\n\nDark mode is exclusive to SLDS 2 themes. Components must use global styling hooks to support light/dark theme switching.\n\n### Dark Mode Checklist\n\n- [ ] **No hardcoded hex colors** (`#FFFFFF`, `#333333`)\n- [ ] **No hardcoded RGB/RGBA values**\n- [ ] **All colors use CSS variables** (`var(--slds-g-color-*)`)\n- [ ] **Fallback values provided** for SLDS 1 compatibility\n- [ ] **No inline color styles** in HTML templates\n- [ ] **Icons use SLDS utility icons** (auto-adjust for dark mode)\n\n### Global Styling Hooks (Common)\n\n| Category | SLDS 2 Variable | Purpose |\n|----------|-----------------|---------|\n| **Surface** | `--slds-g-color-surface-1` to `-4` | Background colors |\n| **Container** | `--slds-g-color-surface-container-1` to `-3` | Card/section backgrounds |\n| **Text** | `--slds-g-color-on-surface` | Primary text |\n| **Border** | `--slds-g-color-border-1`, `-2` | Borders |\n| **Brand** | `--slds-g-color-brand-1`, `-2` | Brand accent |\n| **Spacing** | `--slds-g-spacing-0` to `-12` | Margins/padding |\n\n**Example Migration**:\n```css\n/* SLDS 1 (Deprecated) */\n.my-card { background-color: #ffffff; color: #333333; }\n\n/* SLDS 2 (Dark Mode Ready) */\n.my-card {\n background-color: var(--slds-g-color-surface-container-1, #ffffff);\n color: var(--slds-g-color-on-surface, #181818);\n}\n```\n\n---\n\n## Jest Testing Patterns\n\n### Essential Patterns\n\n```javascript\n// Render cycle helper\nconst runRenderingLifecycle = async (reasons = ['render']) => {\n while (reasons.length > 0) {\n await Promise.resolve(reasons.pop());\n }\n};\n\n// DOM cleanup\nafterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n});\n\n// Proxy unboxing (LWS compatibility)\nconst unboxedData = JSON.parse(JSON.stringify(component.data));\nexpect(unboxedData).toEqual(expectedData);\n```\n\n### Test Template Structure\n\n```javascript\nimport { createElement } from 'lwc';\nimport MyComponent from 'c/myComponent';\nimport getData from '@salesforce/apex/MyController.getData';\n\njest.mock('@salesforce/apex/MyController.getData', () => ({\n default: jest.fn()\n}), { virtual: true });\n\ndescribe('c-my-component', () => {\n afterEach(() => { /* DOM cleanup */ });\n\n it('displays data when loaded successfully', async () => {\n getData.mockResolvedValue(MOCK_DATA);\n const element = createElement('c-my-component', { is: MyComponent });\n document.body.appendChild(element);\n await runRenderingLifecycle();\n // Assertions...\n });\n});\n```\n\n**For complete testing patterns (ResizeObserver polyfill, advanced mocks, event testing), see [references/jest-testing.md](../references/jest-testing.md)**\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3866,"content_sha256":"75d4eddffd6a7eb502df2bd8ae7c5ce7943f3175e9373fbaaf007da131069e85"},{"filename":"references/slds-design-guide.md","content":"# SLDS Design\n\n## Overview\n\n**Use blueprints** when building custom HTML/CSS, targeting non-Salesforce platforms, or when no Lightning Base Component exists.\n\nRefer to the SLDS blueprints in [slds-blueprints.json](slds-blueprints.json)\n\n**Use Lightning Base Components** when working within Salesforce (LWC, Aura, Visualforce) — they handle accessibility, events, and framework integration automatically.\n\n---\n\n## Blueprints by Category\n\n### Actions (4 components)\n\n| Blueprint | Description | Lightning Component |\n| ---------------------- | -------------------------------- | ------------------------ |\n| **Buttons** | Action triggers with text labels | `lightning-button` |\n| **Button Icons** | Icon-only action triggers | `lightning-button-icon` |\n| **Button Groups** | Related buttons in a row | `lightning-button-group` |\n\n---\n\n### Input (23 components)\n\n| Blueprint | Description | Lightning Component |\n| ------------------------- | ---------------------------------------------- | ----------------------------------- |\n| **Checkbox** | Binary selection control | `lightning-input` (type=\"checkbox\") |\n| **Checkbox Button** | Checkbox styled as a button | `lightning-checkbox-button` |\n| **Checkbox Button Group** | Set of checkbox buttons in a fieldset | N/A |\n| **Checkbox Toggle** | Toggle-style checkbox | N/A |\n| **Combobox** | Text input with dropdown list | `lightning-combobox` |\n| **Counter** | Number input with increment/decrement controls | `lightning-input` |\n| **Datepickers** | Calendar date selection | `lightning-input` (type=\"date\") |\n| **Datetime Picker** | Combined date and time selection | N/A |\n| **Dueling Picklist** | Multi-select with two lists | N/A |\n| **Expression** | Formula builder interface | N/A |\n| **File Selector** | File upload with drag-and-drop | `lightning-file-upload` |\n| **Form Element** | Wrapper for form inputs | N/A |\n| **Input** | Text/number input | `lightning-input` |\n| **Lookups** | Search and select from a dataset | N/A |\n| **Picklist** | Dropdown from predefined options | N/A |\n| **Radio Group** | Single selection from exclusive options | `lightning-radio-group` |\n| **Radio Button Group** | Radio buttons in a styled group | `lightning-radio-group` |\n| **Rich Text Editor** | Text editor with formatting toolbar | `lightning-input-rich-text` |\n| **Select** | Native dropdown selection | `lightning-select` |\n| **Slider** | Numeric range input via draggable handle | `lightning-slider` |\n| **Textarea** | Multi-line text input | `lightning-textarea` |\n| **Timepicker** | Time selection interface | `lightning-input` (type=\"time\") |\n| **Visual Picker** | Visual tile-based selection | N/A |\n\n---\n\n### Layout (12 components)\n\n| Blueprint | Description | Lightning Component |\n| ---------------------- | ------------------------------------ | ---------------------- |\n| **Accordion** | Collapsible stacked sections | `lightning-accordion` |\n| **Brand Band** | Visual header with branding | `lightning-brand-band` |\n| **Builder Header** | Header for app builder interfaces | N/A |\n| **Cards** | Container with header/body/footer | `lightning-card` |\n| **Carousel** | Slideshow with navigation controls | `lightning-carousel` |\n| **Docked Form Footer** | Fixed bottom footer for form actions | N/A |\n| **Expandable Section** | Collapsible content block | N/A |\n| **Page Headers** | Page title, metadata, and actions | N/A |\n| **Panels** | Structured side or overlay container | `lightning-panel` |\n| **Split View** | Resizable two-pane layout | N/A |\n| **Summary Detail** | Collapsible key-value layout | N/A |\n| **Tiles** | Card-like grid content items | `lightning-tile` |\n\n---\n\n### Navigation (12 components)\n\n| Blueprint | Description | Lightning Component |\n| ----------------------- | ------------------------------ | ------------------------------- |\n| **App Launcher** | Grid for app discovery | N/A |\n| **Breadcrumbs** | Hierarchical location trail | `lightning-breadcrumbs` |\n| **Dynamic Menu** | Contextual menu in popover | `lightning-menu` |\n| **Global Header** | Primary application header | N/A |\n| **Global Navigation** | Main navigation bar | N/A |\n| **Menus** | Contextual action lists | `lightning-menu-item` |\n| **Path** | Linear process stage indicator | N/A |\n| **Scoped Tabs** | Tabs scoped to a context | `lightning-tabset` |\n| **Tabs** | Switchable content panels | `lightning-tabset` |\n| **Trees** | Hierarchical list | `lightning-tree` |\n| **Vertical Navigation** | Vertical nav menu | `lightning-vertical-navigation` |\n| **Vertical Tabs** | Vertical tab interface | `lightning-tabset` |\n\n---\n\n### Display (9 components)\n\n| Blueprint | Description | Lightning Component |\n| --------------------- | -------------------------------- | ------------------- |\n| **Activity Timeline** | Chronological event list | N/A |\n| **Avatar** | User or entity image placeholder | `lightning-avatar` |\n| **Avatar Group** | Stacked avatar collection | N/A |\n| **Badges** | Small status label | `lightning-badge` |\n| **Dynamic Icons** | Animated contextual icons | N/A |\n| **Files** | File attachment card | N/A |\n| **Icons** | SVG icons from SLDS sprite | `lightning-icon` |\n| **Illustration** | Empty/error state graphic | N/A |\n| **Pills** | Removable tag or filter token | `lightning-pill` |\n\n---\n\n### Data (2 components)\n\n| Blueprint | Description | Lightning Component |\n| --------------- | ----------------------- | --------------------- |\n| **Data Tables** | Sortable tabular data | `lightning-datatable` |\n| **Tree Grid** | Hierarchical data table | `lightning-tree-grid` |\n\n---\n\n### Feedback (9 components)\n\n| Blueprint | Description | Lightning Component |\n| ------------------------ | ----------------------------------- | ------------------------------ |\n| **Alert** | Page-level status banner | N/A |\n| **Notifications** | System notification messages | N/A |\n| **Progress Bar** | Linear completion indicator | `lightning-progress-bar` |\n| **Progress Indicator** | Multi-step process tracker | `lightning-progress-indicator` |\n| **Progress Ring** | Circular progress indicator | `lightning-progress-ring` |\n| **Scoped Notifications** | Notification scoped to a container | N/A |\n| **Spinners** | Loading state indicator | `lightning-spinner` |\n| **Toast** | Temporary notification message | N/A |\n| **Trial Bar** | Trial status and call-to-action bar | N/A |\n\n---\n\n### Overlay (6 components)\n\n| Blueprint | Description | Lightning Component |\n| ------------------- | -------------------------------------- | ------------------- |\n| **Docked Composer** | Bottom-docked content creation panel | N/A |\n| **Modals** | Blocking dialog overlay | N/A |\n| **Popovers** | Contextual overlay anchored to trigger | N/A |\n| **Prompt** | Confirmation or input dialog | N/A |\n| **Tooltips** | Hover-triggered label | N/A |\n| **Welcome Mat** | Onboarding introduction overlay | N/A |\n\n---\n\n### Complex Components (8 components)\n\n| Blueprint | Description | Lightning Component |\n| ------------------- | ------------------------------------- | ------------------------ |\n| **Chat** | Chronological chat message display | N/A |\n| **Color Picker** | Color selection with hex/swatch input | N/A |\n| **Drop Zone** | Drag-and-drop target area | N/A |\n| **Feeds** | Chronological activity feed | N/A |\n| **List Builder** | Drag-and-drop list ordering | `lightning-dual-listbox` |\n| **Map** | Interactive map display | `lightning-map` |\n| **Publishers** | Content creation panel | N/A |\n| **Setup Assistant** | Guided onboarding checklist | N/A |\n\n---\n\n## Framework Notes\n\n- **LWC / Aura**: Prefer the mapped Lightning component when listed — it handles accessibility and events.\n- **React / Vue / Angular / plain HTML**: Use the blueprint HTML structure and CSS classes directly.\n- **Customization**: Apply styling hooks (CSS custom properties) to theme components without overriding base styles.","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10977,"content_sha256":"5eb40ddd258be473227cb3e70e9fff9bbb0fe46d6660bcb55ac07234afd9c80c"},{"filename":"references/state-management.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n\u003c!-- TIER: 3 | DETAILED REFERENCE -->\n\u003c!-- Read after: SKILL.md -->\n\u003c!-- Purpose: Modern state management patterns using @lwc/state -->\n\n# LWC State Management\n\n> Modern state management patterns for Lightning Web Components using @lwc/state and Platform State Managers\n\n## Overview\n\nLWC state management has evolved beyond simple reactive properties. This guide covers modern patterns for managing complex state across components, including the `@lwc/state` library and Salesforce Platform State Managers.\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ STATE MANAGEMENT SPECTRUM │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ │\n│ SIMPLE ◄────────────────────────────────────────────────────────► COMPLEX │\n│ │\n│ @track/@api Singleton Store @lwc/state Platform State │\n│ (Component) (Cross-Component) (Full Library) (Record/Layout) │\n│ │\n│ ┌─────────┐ ┌─────────────┐ ┌────────────┐ ┌───────────────┐ │\n│ │ Single │ │ Shared │ │ Atoms + │ │ Record Data + │ │\n│ │ Component│ │ Across │ │ Computed + │ │ Layout State │ │\n│ │ State │ │ Components │ │ Actions │ │ (Platform) │ │\n│ └─────────┘ └─────────────┘ └────────────┘ └───────────────┘ │\n│ │\n│ Use when: Use when: Use when: Use when: │\n│ - Local state - Shared cart - Complex UI - Record pages │\n│ - Form fields - User prefs - Async state - Flexipages │\n│ - UI toggles - Cached data - Derived data - Tab persistence │\n│ │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## When to Use Each Pattern\n\n| Pattern | Complexity | Scope | Use Case |\n|---------|------------|-------|----------|\n| **@track / reactive properties** | Low | Component | Form inputs, toggles, local UI state |\n| **Singleton Store** | Medium | Cross-component | Shopping cart, filters, user preferences |\n| **@lwc/state** | Medium-High | Cross-component | Complex forms, async state, computed values |\n| **Platform State Managers** | High | Page/Record | Record pages, layout-aware components |\n\n---\n\n## Pattern 1: Reactive Properties (Component-Level)\n\n### Standard Reactivity\n\n```javascript\nimport { LightningElement } from 'lwc';\n\nexport default class SimpleState extends LightningElement {\n // Reactive by default (primitive types and objects)\n counter = 0;\n isActive = false;\n user = { name: 'John', email: '[email protected]' };\n\n // Object reassignment triggers reactivity\n updateUser() {\n // ✅ Works - new object reference\n this.user = { ...this.user, name: 'Jane' };\n\n // ❌ Won't trigger rerender - same reference\n // this.user.name = 'Jane';\n }\n\n increment() {\n this.counter++; // ✅ Primitive assignment is reactive\n }\n}\n```\n\n### Getters (Computed Properties)\n\n```javascript\nexport default class ComputedExample extends LightningElement {\n firstName = '';\n lastName = '';\n items = [];\n\n // Computed property - recalculates when dependencies change\n get fullName() {\n return `${this.firstName} ${this.lastName}`.trim();\n }\n\n get hasItems() {\n return this.items.length > 0;\n }\n\n get totalPrice() {\n return this.items.reduce((sum, item) => sum + item.price, 0);\n }\n\n get formattedPrice() {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD'\n }).format(this.totalPrice);\n }\n}\n```\n\n---\n\n## Pattern 2: Singleton Store (Cross-Component State)\n\nFor sharing state across components without platform dependencies.\n\n### Store Module (`store.js`)\n\n```javascript\n/**\n * Singleton store for cross-component state management.\n *\n * Usage:\n * import store from 'c/store';\n *\n * // Read state\n * const cart = store.getState('cart');\n *\n * // Update state\n * store.setState('cart', { items: [...cart.items, newItem] });\n *\n * // Subscribe to changes\n * store.subscribe('cart', (newCart) => { this.cart = newCart; });\n */\n\n// Private state container\nconst state = new Map();\n\n// Subscribers by key\nconst subscribers = new Map();\n\n// Get current state for a key\nfunction getState(key) {\n return state.get(key);\n}\n\n// Set state and notify subscribers\nfunction setState(key, value) {\n const oldValue = state.get(key);\n state.set(key, value);\n\n // Notify all subscribers for this key\n const keySubscribers = subscribers.get(key) || [];\n keySubscribers.forEach(callback => {\n try {\n callback(value, oldValue);\n } catch (e) {\n console.error('Store subscriber error:', e);\n }\n });\n}\n\n// Subscribe to state changes\nfunction subscribe(key, callback) {\n if (!subscribers.has(key)) {\n subscribers.set(key, []);\n }\n subscribers.get(key).push(callback);\n\n // Return unsubscribe function\n return () => {\n const keySubscribers = subscribers.get(key) || [];\n const index = keySubscribers.indexOf(callback);\n if (index > -1) {\n keySubscribers.splice(index, 1);\n }\n };\n}\n\n// Initialize state with default values\nfunction initState(key, defaultValue) {\n if (!state.has(key)) {\n state.set(key, defaultValue);\n }\n return state.get(key);\n}\n\n// Clear state (useful for testing)\nfunction clearState() {\n state.clear();\n subscribers.clear();\n}\n\nexport default {\n getState,\n setState,\n subscribe,\n initState,\n clearState\n};\n```\n\n### Using the Store\n\n```javascript\n// cartManager.js\nimport { LightningElement } from 'lwc';\nimport store from 'c/store';\n\nexport default class CartManager extends LightningElement {\n cart = { items: [], total: 0 };\n unsubscribe;\n\n connectedCallback() {\n // Initialize cart state\n this.cart = store.initState('cart', { items: [], total: 0 });\n\n // Subscribe to cart changes from other components\n this.unsubscribe = store.subscribe('cart', (newCart) => {\n this.cart = newCart;\n });\n }\n\n disconnectedCallback() {\n // Clean up subscription\n if (this.unsubscribe) {\n this.unsubscribe();\n }\n }\n\n addItem(event) {\n const item = event.detail;\n const currentCart = store.getState('cart');\n\n const newCart = {\n items: [...currentCart.items, item],\n total: currentCart.total + item.price\n };\n\n store.setState('cart', newCart);\n }\n}\n```\n\n---\n\n## Pattern 3: @lwc/state Library\n\nThe `@lwc/state` library provides reactive state primitives with automatic dependency tracking.\n\n### Installation\n\n```bash\n# If using npm in LWC project\nnpm install @lwc/state\n```\n\n### Core Concepts\n\n#### Atoms (Primitive State)\n\n```javascript\nimport { atom, computed } from '@lwc/state';\n\n// Create atoms for primitive state\nconst countAtom = atom(0);\nconst nameAtom = atom('');\nconst itemsAtom = atom([]);\n\n// Read value\nconsole.log(countAtom.value); // 0\n\n// Write value - triggers reactivity\ncountAtom.value = 5;\n\n// Reset to initial value\ncountAtom.reset();\n```\n\n#### Computed (Derived State)\n\n```javascript\nimport { atom, computed } from '@lwc/state';\n\nconst priceAtom = atom(100);\nconst quantityAtom = atom(2);\nconst taxRateAtom = atom(0.08);\n\n// Computed automatically recalculates when dependencies change\nconst subtotal = computed(() => priceAtom.value * quantityAtom.value);\nconst tax = computed(() => subtotal.value * taxRateAtom.value);\nconst total = computed(() => subtotal.value + tax.value);\n\nconsole.log(total.value); // 216 (100 * 2 * 1.08)\n\n// Update a dependency - all computed values update\npriceAtom.value = 150;\nconsole.log(total.value); // 324 (150 * 2 * 1.08)\n```\n\n#### Actions (State Mutations)\n\n```javascript\nimport { atom, action } from '@lwc/state';\n\nconst cartAtom = atom({ items: [], total: 0 });\n\n// Actions encapsulate state mutations\nconst addToCart = action((item) => {\n const cart = cartAtom.value;\n cartAtom.value = {\n items: [...cart.items, item],\n total: cart.total + item.price\n };\n});\n\nconst removeFromCart = action((itemId) => {\n const cart = cartAtom.value;\n const item = cart.items.find(i => i.id === itemId);\n cartAtom.value = {\n items: cart.items.filter(i => i.id !== itemId),\n total: cart.total - (item?.price || 0)\n };\n});\n\nconst clearCart = action(() => {\n cartAtom.reset();\n});\n```\n\n### LWC Integration\n\n```javascript\nimport { LightningElement } from 'lwc';\nimport { atom, computed } from '@lwc/state';\n\n// Define atoms outside component (singleton)\nconst searchTermAtom = atom('');\nconst resultsAtom = atom([]);\nconst isLoadingAtom = atom(false);\n\n// Computed values\nconst hasResults = computed(() => resultsAtom.value.length > 0);\nconst resultCount = computed(() => resultsAtom.value.length);\n\nexport default class SearchComponent extends LightningElement {\n // Bind atoms to component for reactivity\n get searchTerm() {\n return searchTermAtom.value;\n }\n\n get results() {\n return resultsAtom.value;\n }\n\n get isLoading() {\n return isLoadingAtom.value;\n }\n\n get hasResults() {\n return hasResults.value;\n }\n\n handleSearchChange(event) {\n searchTermAtom.value = event.target.value;\n this.performSearch();\n }\n\n async performSearch() {\n if (searchTermAtom.value.length \u003c 2) {\n resultsAtom.value = [];\n return;\n }\n\n isLoadingAtom.value = true;\n try {\n const results = await searchRecords({ term: searchTermAtom.value });\n resultsAtom.value = results;\n } finally {\n isLoadingAtom.value = false;\n }\n }\n}\n```\n\n---\n\n## Pattern 4: Platform State Managers\n\nSalesforce provides built-in state managers for record pages and layouts.\n\n### stateManagerRecord\n\nManages record data with automatic refresh and caching.\n\n```javascript\nimport { LightningElement, wire } from 'lwc';\nimport { stateManagerRecord } from 'lightning/stateManagerRecord';\nimport { getRecord } from 'lightning/uiRecordApi';\n\nexport default class RecordStateExample extends LightningElement {\n @api recordId;\n\n // Wire with state manager for enhanced caching\n @wire(stateManagerRecord, {\n recordId: '$recordId',\n fields: ['Account.Name', 'Account.Industry']\n })\n recordState;\n\n get accountName() {\n return this.recordState?.data?.fields?.Name?.value;\n }\n\n get industry() {\n return this.recordState?.data?.fields?.Industry?.value;\n }\n\n // State manager provides loading/error states\n get isLoading() {\n return this.recordState?.loading;\n }\n\n get hasError() {\n return !!this.recordState?.error;\n }\n}\n```\n\n### stateManagerLayout\n\nManages layout-aware state for flexipage components.\n\n```javascript\nimport { LightningElement, wire } from 'lwc';\nimport { stateManagerLayout } from 'lightning/stateManagerLayout';\n\nexport default class LayoutAwareComponent extends LightningElement {\n @api recordId;\n @api objectApiName;\n\n // Wire layout state manager\n @wire(stateManagerLayout, {\n recordId: '$recordId',\n objectApiName: '$objectApiName'\n })\n layoutState;\n\n get isCompact() {\n return this.layoutState?.density === 'compact';\n }\n\n get visibleFields() {\n return this.layoutState?.fields || [];\n }\n}\n```\n\n### Composing State Managers\n\n```javascript\nimport { LightningElement, wire } from 'lwc';\nimport { stateManagerRecord } from 'lightning/stateManagerRecord';\nimport { atom, computed } from '@lwc/state';\n\n// Custom state atoms\nconst selectedTabAtom = atom('details');\nconst expandedSectionsAtom = atom(new Set(['overview']));\n\nexport default class ComposedStateComponent extends LightningElement {\n @api recordId;\n\n // Platform state for record data\n @wire(stateManagerRecord, {\n recordId: '$recordId',\n fields: ['Account.Name', 'Account.Type', 'Account.Industry']\n })\n recordState;\n\n // Custom state for UI\n get selectedTab() {\n return selectedTabAtom.value;\n }\n\n get expandedSections() {\n return expandedSectionsAtom.value;\n }\n\n isSectionExpanded(sectionId) {\n return expandedSectionsAtom.value.has(sectionId);\n }\n\n handleTabChange(event) {\n selectedTabAtom.value = event.detail.value;\n }\n\n handleSectionToggle(event) {\n const sectionId = event.target.dataset.section;\n const sections = new Set(expandedSectionsAtom.value);\n\n if (sections.has(sectionId)) {\n sections.delete(sectionId);\n } else {\n sections.add(sectionId);\n }\n\n expandedSectionsAtom.value = sections;\n }\n}\n```\n\n---\n\n## Anti-Patterns to Avoid\n\n### ❌ BAD: Mutating Objects In Place\n\n```javascript\n// DON'T - won't trigger reactivity\nthis.user.name = 'New Name';\nthis.items.push(newItem);\n```\n\n### ✅ GOOD: Create New References\n\n```javascript\n// DO - triggers reactivity\nthis.user = { ...this.user, name: 'New Name' };\nthis.items = [...this.items, newItem];\n```\n\n### ❌ BAD: Heavy Computation in Getters\n\n```javascript\n// DON'T - runs every render cycle\nget expensiveComputation() {\n return this.items\n .map(item => complexTransform(item))\n .filter(item => complexFilter(item))\n .sort((a, b) => complexSort(a, b));\n}\n```\n\n### ✅ GOOD: Cache Computed Values\n\n```javascript\n_cachedResult;\n_lastItemsHash;\n\nget optimizedComputation() {\n const currentHash = JSON.stringify(this.items);\n if (this._lastItemsHash !== currentHash) {\n this._cachedResult = this.items\n .map(item => complexTransform(item))\n .filter(item => complexFilter(item))\n .sort((a, b) => complexSort(a, b));\n this._lastItemsHash = currentHash;\n }\n return this._cachedResult;\n}\n```\n\n### ❌ BAD: Forgetting to Unsubscribe\n\n```javascript\n// DON'T - memory leak\nconnectedCallback() {\n store.subscribe('data', (data) => this.data = data);\n}\n```\n\n### ✅ GOOD: Clean Up Subscriptions\n\n```javascript\n// DO - proper cleanup\n_unsubscribe;\n\nconnectedCallback() {\n this._unsubscribe = store.subscribe('data', (data) => this.data = data);\n}\n\ndisconnectedCallback() {\n if (this._unsubscribe) {\n this._unsubscribe();\n }\n}\n```\n\n---\n\n## Best Practices Summary\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ STATE MANAGEMENT BEST PRACTICES │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ │\n│ CHOOSE THE RIGHT PATTERN │\n│ ───────────────────────────────────────────────────────────────────────── │\n│ ✅ Start with simple reactive properties │\n│ ✅ Use singleton store for shared non-record state │\n│ ✅ Use @lwc/state for complex derived state │\n│ ✅ Use Platform State Managers on record pages │\n│ ❌ Don't over-engineer simple components │\n│ │\n│ REACTIVITY │\n│ ───────────────────────────────────────────────────────────────────────── │\n│ ✅ Create new object/array references for updates │\n│ ✅ Use spread operator: { ...obj, newProp } │\n│ ✅ Use getters for computed values │\n│ ❌ Don't mutate objects/arrays in place │\n│ │\n│ PERFORMANCE │\n│ ───────────────────────────────────────────────────────────────────────── │\n│ ✅ Cache expensive computations │\n│ ✅ Debounce rapid state updates │\n│ ✅ Clean up subscriptions in disconnectedCallback │\n│ ❌ Don't do heavy computation in getters │\n│ │\n│ TESTING │\n│ ───────────────────────────────────────────────────────────────────────── │\n│ ✅ Test state transitions explicitly │\n│ ✅ Verify subscription cleanup │\n│ ✅ Mock store for unit tests │\n│ ❌ Don't test implementation details │\n│ │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Related Documentation\n\n- [SKILL.md](../SKILL.md) - PICKLES Framework overview\n- [lwc-best-practices.md](lwc-best-practices.md) - General LWC patterns\n- [triangle-pattern.md](triangle-pattern.md) - Component composition\n\n---\n\n## Source\n\n> **References**:\n> - [Mastering State Management in LWC using @lwc/state](https://salesforcediaries.com/2025/11/26/mastering-state-management-in-lwc-using-lwc-state/) - Salesforce Diaries\n> - [Platform State Managers in LWC](https://salesforcediaries.com/2025/11/26/platform-state-managers-in-lwc/) - Salesforce Diaries\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20357,"content_sha256":"2e9e9f6c02f3f9ba37a5cbdb86bd7496f09f7673247a6294f7b09d17436c5607"},{"filename":"references/template-anti-patterns.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# LWC Template Anti-Patterns\n\nThis guide documents systematic errors in Lightning Web Component templates, with special focus on patterns that LLMs commonly generate incorrectly. LWC templates have strict limitations compared to frameworks like React or Vue.\n\n> **Source**: [LLM Mistakes in Apex & LWC - Salesforce Diaries](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n\n---\n\n## Table of Contents\n\n1. [Inline JavaScript Expressions](#1-inline-javascript-expressions)\n2. [Ternary Operators in Templates](#2-ternary-operators-in-templates)\n3. [Object Literals in Attributes](#3-object-literals-in-attributes)\n4. [Complex Expressions](#4-complex-expressions)\n5. [Event Handler Mistakes](#5-event-handler-mistakes)\n6. [Iteration Anti-Patterns](#6-iteration-anti-patterns)\n7. [Conditional Rendering Issues](#7-conditional-rendering-issues)\n8. [Slot and Composition Errors](#8-slot-and-composition-errors)\n9. [Data Binding Mistakes](#9-data-binding-mistakes)\n10. [Style and Class Binding](#10-style-and-class-binding)\n\n---\n\n## 1. Inline JavaScript Expressions\n\n**Critical Rule**: LWC templates do NOT support JavaScript expressions. Only property references are allowed.\n\n### ❌ BAD: Arithmetic in Template\n\n```html\n\u003c!-- LLM generates this - DOES NOT WORK -->\n\u003ctemplate>\n \u003cp>Total: {price * quantity}\u003c/p>\n \u003cp>Tax: {price * 0.1}\u003c/p>\n \u003cp>Discount: {price - discount}\u003c/p>\n\u003c/template>\n```\n\n### ✅ GOOD: Use Getters\n\n```javascript\n// component.js\nexport default class PriceCalculator extends LightningElement {\n price = 100;\n quantity = 2;\n discount = 10;\n\n get total() {\n return this.price * this.quantity;\n }\n\n get tax() {\n return this.price * 0.1;\n }\n\n get discountedPrice() {\n return this.price - this.discount;\n }\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cp>Total: {total}\u003c/p>\n \u003cp>Tax: {tax}\u003c/p>\n \u003cp>Discount: {discountedPrice}\u003c/p>\n\u003c/template>\n```\n\n### ❌ BAD: String Concatenation in Template\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003cp>Hello, {firstName + ' ' + lastName}!\u003c/p>\n \u003ca href={'/account/' + accountId}>View Account\u003c/a>\n\u003c/template>\n```\n\n### ✅ GOOD: Computed Properties\n\n```javascript\n// component.js\nexport default class Greeting extends LightningElement {\n firstName = 'John';\n lastName = 'Doe';\n accountId = '001xx000003DGbY';\n\n get fullName() {\n return `${this.firstName} ${this.lastName}`;\n }\n\n get accountUrl() {\n return `/account/${this.accountId}`;\n }\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cp>Hello, {fullName}!\u003c/p>\n \u003ca href={accountUrl}>View Account\u003c/a>\n\u003c/template>\n```\n\n---\n\n## 2. Ternary Operators in Templates\n\n**Critical Rule**: Ternary operators (`condition ? a : b`) are NOT allowed in LWC templates.\n\n### ❌ BAD: Ternary in Template\n\n```html\n\u003c!-- LLM generates this - DOES NOT WORK -->\n\u003ctemplate>\n \u003cp class={isActive ? 'active' : 'inactive'}>Status\u003c/p>\n \u003cspan>{count > 0 ? count : 'None'}\u003c/span>\n \u003cbutton disabled={isLoading ? true : false}>Submit\u003c/button>\n\u003c/template>\n```\n\n### ✅ GOOD: Use Getters for Conditional Values\n\n```javascript\n// component.js\nexport default class StatusDisplay extends LightningElement {\n isActive = true;\n count = 0;\n isLoading = false;\n\n get statusClass() {\n return this.isActive ? 'active' : 'inactive';\n }\n\n get displayCount() {\n return this.count > 0 ? this.count : 'None';\n }\n\n get isButtonDisabled() {\n return this.isLoading;\n }\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cp class={statusClass}>Status\u003c/p>\n \u003cspan>{displayCount}\u003c/span>\n \u003cbutton disabled={isButtonDisabled}>Submit\u003c/button>\n\u003c/template>\n```\n\n### ✅ GOOD: Use if:true/if:false for Conditional Rendering\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate if:true={isActive}>\n \u003cp class=\"active\">Active\u003c/p>\n \u003c/template>\n \u003ctemplate if:false={isActive}>\n \u003cp class=\"inactive\">Inactive\u003c/p>\n \u003c/template>\n\n \u003ctemplate if:true={hasCount}>\n \u003cspan>{count}\u003c/span>\n \u003c/template>\n \u003ctemplate if:false={hasCount}>\n \u003cspan>None\u003c/span>\n \u003c/template>\n\u003c/template>\n```\n\n```javascript\n// component.js\nget hasCount() {\n return this.count > 0;\n}\n```\n\n---\n\n## 3. Object Literals in Attributes\n\n**Critical Rule**: Object literals (`{}`) cannot be passed directly as attribute values.\n\n### ❌ BAD: Inline Object Literals\n\n```html\n\u003c!-- LLM generates this - DOES NOT WORK -->\n\u003ctemplate>\n \u003cc-child-component\n config={{ showHeader: true, theme: 'dark' }}\n style={{ color: 'red', fontSize: '14px' }}>\n \u003c/c-child-component>\n\n \u003clightning-datatable\n columns={[{ label: 'Name', fieldName: 'name' }]}\n data={records}>\n \u003c/lightning-datatable>\n\u003c/template>\n```\n\n### ✅ GOOD: Define Objects in JavaScript\n\n```javascript\n// component.js\nexport default class ParentComponent extends LightningElement {\n config = {\n showHeader: true,\n theme: 'dark'\n };\n\n columns = [\n { label: 'Name', fieldName: 'name' },\n { label: 'Email', fieldName: 'email' }\n ];\n\n records = [];\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cc-child-component config={config}>\u003c/c-child-component>\n\n \u003clightning-datatable\n columns={columns}\n data={records}>\n \u003c/lightning-datatable>\n\u003c/template>\n```\n\n### ❌ BAD: Inline Array Literals\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003cc-multi-select options={['Red', 'Green', 'Blue']}>\u003c/c-multi-select>\n\u003c/template>\n```\n\n### ✅ GOOD: Define Arrays in JavaScript\n\n```javascript\n// component.js\nexport default class ColorPicker extends LightningElement {\n colorOptions = [\n { label: 'Red', value: 'red' },\n { label: 'Green', value: 'green' },\n { label: 'Blue', value: 'blue' }\n ];\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cc-multi-select options={colorOptions}>\u003c/c-multi-select>\n\u003c/template>\n```\n\n---\n\n## 4. Complex Expressions\n\n**Critical Rule**: No method calls, comparisons, or logical operators in templates.\n\n### ❌ BAD: Method Calls in Template\n\n```html\n\u003c!-- LLM generates this - DOES NOT WORK -->\n\u003ctemplate>\n \u003cp>{name.toUpperCase()}\u003c/p>\n \u003cp>{items.length}\u003c/p>\n \u003cp>{formatDate(createdDate)}\u003c/p>\n \u003cp>{JSON.stringify(data)}\u003c/p>\n\u003c/template>\n```\n\n### ✅ GOOD: Use Getters for Transformations\n\n```javascript\n// component.js\nexport default class DataDisplay extends LightningElement {\n name = 'john doe';\n items = ['a', 'b', 'c'];\n createdDate = new Date();\n data = { key: 'value' };\n\n get upperName() {\n return this.name.toUpperCase();\n }\n\n get itemCount() {\n return this.items.length;\n }\n\n get formattedDate() {\n return new Intl.DateTimeFormat('en-US').format(this.createdDate);\n }\n\n get dataJson() {\n return JSON.stringify(this.data);\n }\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cp>{upperName}\u003c/p>\n \u003cp>{itemCount}\u003c/p>\n \u003cp>{formattedDate}\u003c/p>\n \u003cp>{dataJson}\u003c/p>\n\u003c/template>\n```\n\n### ❌ BAD: Comparisons in Template\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003ctemplate if:true={count > 5}>\n \u003cp>Many items\u003c/p>\n \u003c/template>\n\n \u003ctemplate if:true={status === 'active'}>\n \u003cp>Active\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Getter-Based Comparisons\n\n```javascript\n// component.js\nget hasManyItems() {\n return this.count > 5;\n}\n\nget isActive() {\n return this.status === 'active';\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate if:true={hasManyItems}>\n \u003cp>Many items\u003c/p>\n \u003c/template>\n\n \u003ctemplate if:true={isActive}>\n \u003cp>Active\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ❌ BAD: Logical Operators in Template\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003ctemplate if:true={isAdmin && hasPermission}>\n \u003cbutton>Delete\u003c/button>\n \u003c/template>\n\n \u003ctemplate if:true={!isLoading}>\n \u003cp>Content\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Computed Boolean Properties\n\n```javascript\n// component.js\nget canDelete() {\n return this.isAdmin && this.hasPermission;\n}\n\nget isNotLoading() {\n return !this.isLoading;\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate if:true={canDelete}>\n \u003cbutton>Delete\u003c/button>\n \u003c/template>\n\n \u003ctemplate if:true={isNotLoading}>\n \u003cp>Content\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n---\n\n## 5. Event Handler Mistakes\n\n### ❌ BAD: Inline Event Handlers with Arguments\n\n```html\n\u003c!-- LLM generates this - DOES NOT WORK -->\n\u003ctemplate>\n \u003cbutton onclick={handleClick(item.id)}>Click\u003c/button>\n \u003cbutton onclick={() => this.handleDelete(record)}>Delete\u003c/button>\n \u003cinput onchange={e => this.handleChange(e.target.value)}>\n\u003c/template>\n```\n\n### ✅ GOOD: Handler Functions with Data Attributes\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cbutton\n key={item.id}\n data-id={item.id}\n onclick={handleClick}>\n Click {item.name}\n \u003c/button>\n \u003c/template>\n\u003c/template>\n```\n\n```javascript\n// component.js\nhandleClick(event) {\n const itemId = event.target.dataset.id;\n // or use event.currentTarget.dataset.id for delegated events\n console.log('Clicked item:', itemId);\n}\n\nhandleChange(event) {\n const value = event.target.value;\n this.inputValue = value;\n}\n```\n\n### ❌ BAD: Event Binding with bind()\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003cbutton onclick={handleClick.bind(this, item)}>Click\u003c/button>\n\u003c/template>\n```\n\n### ✅ GOOD: Use Data Attributes for Context\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cbutton\n key={item.id}\n data-id={item.id}\n data-name={item.name}\n data-index={item.index}\n onclick={handleItemClick}>\n {item.name}\n \u003c/button>\n \u003c/template>\n\u003c/template>\n```\n\n```javascript\n// component.js\nhandleItemClick(event) {\n const { id, name, index } = event.currentTarget.dataset;\n // dataset values are always strings\n const indexNum = parseInt(index, 10);\n}\n```\n\n---\n\n\u003ca id=\"6-iteration-anti-patterns\">\u003c/a>\n\n## 6. Iteration Anti-Patterns\n\n### ❌ BAD: Missing Key in Iteration\n\n```html\n\u003c!-- LLM forgets key - causes rendering issues -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cdiv>{item.name}\u003c/div> \u003c!-- Missing key! -->\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Always Include Key\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cdiv key={item.id}>{item.name}\u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n### ❌ BAD: Using Index as Key\n\n```html\n\u003c!-- Anti-pattern: index can cause issues with reordering -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\" for:index=\"index\">\n \u003cdiv key={index}>{item.name}\u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Use Unique Identifier as Key\n\n```javascript\n// If items don't have unique IDs, generate them\nconnectedCallback() {\n this.items = this.rawItems.map((item, index) => ({\n ...item,\n uniqueKey: `item-${item.name}-${index}`\n }));\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate for:each={items} for:item=\"item\">\n \u003cdiv key={item.uniqueKey}>{item.name}\u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n### ❌ BAD: Nested Iteration Without Proper Keys\n\n```html\n\u003c!-- PROBLEMATIC -->\n\u003ctemplate>\n \u003ctemplate for:each={categories} for:item=\"category\">\n \u003cdiv key={category.id}>\n \u003ch3>{category.name}\u003c/h3>\n \u003ctemplate for:each={category.items} for:item=\"item\">\n \u003c!-- Key might conflict with other categories -->\n \u003cp key={item.id}>{item.name}\u003c/p>\n \u003c/template>\n \u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Compound Keys for Nested Iteration\n\n```javascript\n// component.js\nget processedCategories() {\n return this.categories.map(category => ({\n ...category,\n items: category.items.map(item => ({\n ...item,\n compositeKey: `${category.id}-${item.id}`\n }))\n }));\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate for:each={processedCategories} for:item=\"category\">\n \u003cdiv key={category.id}>\n \u003ch3>{category.name}\u003c/h3>\n \u003ctemplate for:each={category.items} for:item=\"item\">\n \u003cp key={item.compositeKey}>{item.name}\u003c/p>\n \u003c/template>\n \u003c/div>\n \u003c/template>\n\u003c/template>\n```\n\n---\n\n## 7. Conditional Rendering Issues\n\n### ❌ BAD: if:true on Non-Boolean Values\n\n```html\n\u003c!-- LLM assumes truthy/falsy works like JS - it doesn't always -->\n\u003ctemplate>\n \u003c!-- String 'false' is truthy! -->\n \u003ctemplate if:true={stringValue}>\n \u003cp>Shown even for 'false' string!\u003c/p>\n \u003c/template>\n\n \u003c!-- 0 might not behave as expected -->\n \u003ctemplate if:true={count}>\n \u003cp>Count: {count}\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Explicit Boolean Conversion\n\n```javascript\n// component.js\nget hasStringValue() {\n return Boolean(this.stringValue) && this.stringValue !== 'false';\n}\n\nget hasCount() {\n return this.count !== null && this.count !== undefined && this.count !== 0;\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate if:true={hasStringValue}>\n \u003cp>Has value\u003c/p>\n \u003c/template>\n\n \u003ctemplate if:true={hasCount}>\n \u003cp>Count: {count}\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ❌ BAD: Multiple Conditions Without Else\n\n```html\n\u003c!-- Verbose and error-prone -->\n\u003ctemplate>\n \u003ctemplate if:true={isLoading}>\n \u003clightning-spinner>\u003c/lightning-spinner>\n \u003c/template>\n \u003ctemplate if:true={hasError}>\n \u003cp>Error occurred\u003c/p>\n \u003c/template>\n \u003ctemplate if:true={hasData}>\n \u003cp>Data loaded\u003c/p>\n \u003c/template>\n\u003c/template>\n```\n\n### ✅ GOOD: Use a State Getter\n\n```javascript\n// component.js\nget viewState() {\n if (this.isLoading) return 'loading';\n if (this.error) return 'error';\n if (this.data) return 'success';\n return 'empty';\n}\n\nget isLoadingState() { return this.viewState === 'loading'; }\nget isErrorState() { return this.viewState === 'error'; }\nget isSuccessState() { return this.viewState === 'success'; }\nget isEmptyState() { return this.viewState === 'empty'; }\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003ctemplate if:true={isLoadingState}>\n \u003clightning-spinner>\u003c/lightning-spinner>\n \u003c/template>\n \u003ctemplate if:true={isErrorState}>\n \u003cc-error-display error={error}>\u003c/c-error-display>\n \u003c/template>\n \u003ctemplate if:true={isSuccessState}>\n \u003cc-data-display data={data}>\u003c/c-data-display>\n \u003c/template>\n \u003ctemplate if:true={isEmptyState}>\n \u003cc-empty-state>\u003c/c-empty-state>\n \u003c/template>\n\u003c/template>\n```\n\n---\n\n## 8. Slot and Composition Errors\n\n### ❌ BAD: Named Slot with Wrong Syntax\n\n```html\n\u003c!-- LLM uses Vue/React slot syntax -->\n\u003ctemplate>\n \u003c!-- Wrong: Vue syntax -->\n \u003cc-card>\n \u003ctemplate v-slot:header>Header\u003c/template>\n \u003c/c-card>\n\n \u003c!-- Wrong: React children syntax -->\n \u003cc-card header=\"Header Content\">\n Body Content\n \u003c/c-card>\n\u003c/template>\n```\n\n### ✅ GOOD: LWC Slot Syntax\n\n```html\n\u003c!-- Parent component using slots -->\n\u003ctemplate>\n \u003cc-card>\n \u003cspan slot=\"header\">Card Header\u003c/span>\n \u003cp slot=\"body\">Card body content\u003c/p>\n \u003cbutton slot=\"footer\">Action\u003c/button>\n \u003c/c-card>\n\u003c/template>\n```\n\n```html\n\u003c!-- c-card component template -->\n\u003ctemplate>\n \u003carticle class=\"slds-card\">\n \u003cheader class=\"slds-card__header\">\n \u003cslot name=\"header\">\u003c/slot>\n \u003c/header>\n \u003cdiv class=\"slds-card__body\">\n \u003cslot name=\"body\">\u003c/slot>\n \u003cslot>\u003c/slot> \u003c!-- Default slot -->\n \u003c/div>\n \u003cfooter class=\"slds-card__footer\">\n \u003cslot name=\"footer\">\u003c/slot>\n \u003c/footer>\n \u003c/article>\n\u003c/template>\n```\n\n---\n\n## 9. Data Binding Mistakes\n\n### ❌ BAD: Two-Way Binding Syntax\n\n```html\n\u003c!-- LLM uses Angular/Vue two-way binding - doesn't exist in LWC -->\n\u003ctemplate>\n \u003cinput [(ngModel)]=\"name\"> \u003c!-- Angular -->\n \u003cinput v-model=\"name\"> \u003c!-- Vue -->\n \u003cinput bind:value={name}> \u003c!-- Svelte-ish -->\n\u003c/template>\n```\n\n### ✅ GOOD: One-Way Binding with Event Handler\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003clightning-input\n label=\"Name\"\n value={name}\n onchange={handleNameChange}>\n \u003c/lightning-input>\n\n \u003c!-- Or standard HTML input -->\n \u003cinput\n type=\"text\"\n value={name}\n onchange={handleInputChange}\n oninput={handleInputChange}>\n\u003c/template>\n```\n\n```javascript\n// component.js\nname = '';\n\nhandleNameChange(event) {\n this.name = event.detail.value; // lightning-input uses detail.value\n}\n\nhandleInputChange(event) {\n this.name = event.target.value; // standard input uses target.value\n}\n```\n\n### ❌ BAD: Direct Property Mutation in Template\n\n```html\n\u003c!-- Cannot mutate in template -->\n\u003ctemplate>\n \u003cbutton onclick={count++}>Increment\u003c/button>\n\u003c/template>\n```\n\n### ✅ GOOD: Mutate in Handler\n\n```javascript\n// component.js\ncount = 0;\n\nhandleIncrement() {\n this.count++;\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cbutton onclick={handleIncrement}>Increment ({count})\u003c/button>\n\u003c/template>\n```\n\n---\n\n## 10. Style and Class Binding\n\n### ❌ BAD: Dynamic Styles in Template\n\n```html\n\u003c!-- LLM uses React/Vue style binding - doesn't work in LWC -->\n\u003ctemplate>\n \u003cdiv style=\"color: {textColor}; font-size: {fontSize}px\">\n Content\n \u003c/div>\n\n \u003cdiv style={{ color: textColor, fontSize: fontSize + 'px' }}>\n Content\n \u003c/div>\n\u003c/template>\n```\n\n### ✅ GOOD: CSS Custom Properties (Recommended)\n\n```javascript\n// component.js\n@api textColor = 'blue';\n@api fontSize = 14;\n\nrenderedCallback() {\n this.template.host.style.setProperty('--text-color', this.textColor);\n this.template.host.style.setProperty('--font-size', `${this.fontSize}px`);\n}\n```\n\n```css\n/* component.css */\n.dynamic-text {\n color: var(--text-color, black);\n font-size: var(--font-size, 14px);\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cdiv class=\"dynamic-text\">Content\u003c/div>\n\u003c/template>\n```\n\n### ✅ GOOD: Computed Style String (When Necessary)\n\n```javascript\n// component.js\nget dynamicStyle() {\n return `color: ${this.textColor}; font-size: ${this.fontSize}px;`;\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cdiv style={dynamicStyle}>Content\u003c/div>\n\u003c/template>\n```\n\n### ❌ BAD: Dynamic Class with Expression\n\n```html\n\u003c!-- DOES NOT WORK -->\n\u003ctemplate>\n \u003cdiv class=\"base {isActive ? 'active' : ''}\">Content\u003c/div>\n \u003cdiv class={`base ${isActive ? 'active' : ''}`}>Content\u003c/div>\n\u003c/template>\n```\n\n### ✅ GOOD: Computed Class String\n\n```javascript\n// component.js\nget containerClass() {\n return `base ${this.isActive ? 'active' : ''} ${this.isHighlighted ? 'highlighted' : ''}`.trim();\n}\n```\n\n```html\n\u003c!-- component.html -->\n\u003ctemplate>\n \u003cdiv class={containerClass}>Content\u003c/div>\n\u003c/template>\n```\n\n---\n\n## Quick Reference: Template Rules\n\n| What You Want | Wrong (Other Frameworks) | Right (LWC) |\n|---------------|-------------------------|-------------|\n| Arithmetic | `{a + b}` | Getter: `get sum() { return a + b; }` |\n| String concat | `{a + ' ' + b}` | Getter with template literal |\n| Ternary | `{x ? a : b}` | Getter or if:true/if:false |\n| Method call | `{items.length}` | Getter: `get count() { return items.length; }` |\n| Comparison | `if:true={x > 5}` | Getter: `get isBig() { return x > 5; }` |\n| Logical AND | `if:true={a && b}` | Getter: `get both() { return a && b; }` |\n| Negation | `if:true={!x}` | `if:false={x}` or getter |\n| Object literal | `config={{ key: val }}` | Class property |\n| Event args | `onclick={fn(x)}` | Use data attributes |\n| Two-way bind | `value={name}` auto-update | `value={name}` + `onchange` |\n\n---\n\n## Validation Checklist\n\nBefore deploying LWC templates, verify:\n\n- [ ] No arithmetic operations (`+`, `-`, `*`, `/`)\n- [ ] No ternary operators (`? :`)\n- [ ] No object/array literals (`{}`, `[]`)\n- [ ] No method calls (`.length`, `.toUpperCase()`)\n- [ ] No comparisons (`>`, `\u003c`, `===`, `!==`)\n- [ ] No logical operators (`&&`, `||`, `!`)\n- [ ] All iterations have unique `key` attributes\n- [ ] Event handlers don't have inline arguments\n- [ ] Dynamic styles use CSS custom properties or computed strings\n\n---\n\n## Reference\n\n- **LWC Best Practices**: See `references/lwc-best-practices.md`\n- **Component Patterns**: See `references/component-patterns.md`\n- **Source**: [Salesforce Diaries - LLM Mistakes](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20729,"content_sha256":"57a1b859de78b60498bfd911f3d903578fc5632338b18cfcd5eb052dc1f5de52"},{"filename":"references/triangle-pattern.md","content":"\u003c!-- Parent: generating-lwc-components/SKILL.md -->\n# Flow-LWC-Apex Triangle: LWC Perspective\n\nThe **Triangle Architecture** is a foundational Salesforce pattern where Flow, LWC, and Apex work together. This guide focuses on the **LWC role** in this architecture.\n\n---\n\n## Architecture Overview\n\n```\n ┌─────────────────────────────────────┐\n │ FLOW │\n │ (Orchestrator) │\n └───────────────┬─────────────────────┘\n │\n ┌──────────────────────────┼──────────────────────────┐\n │ │ │\n │ screens │ actionCalls │\n │ \u003ccomponentInstance> │ actionType=\"apex\" │\n │ │ │\n ▼ ▼ ▲\n┌─────────────────────────┐ ┌─────────────────────────┐ │\n│ LWC │ │ APEX │ │\n│ (UI Component) │───▶│ (Business Logic) │─────────┘\n│ │ │ │\n│ • Rich UI/UX ◀── YOU ARE HERE │\n│ • User Interaction │ │ • @InvocableMethod │\n│ • FlowAttribute │ │ • @AuraEnabled │\n│ ChangeEvent │ │ • Complex Logic │\n│ • FlowNavigation │ │ • DML Operations │\n│ FinishEvent │ │ │\n└─────────────────────────┘ └─────────────────────────┘\n │ ▲\n │ @AuraEnabled │\n │ wire / imperative │\n └──────────────────────────┘\n```\n\n---\n\n## LWC's Role in the Triangle\n\n| Communication Path | Mechanism | Direction |\n|-------------------|-----------|-----------|\n| Flow → LWC | `inputParameters` → `@api` | One-way input |\n| LWC → Flow | `FlowAttributeChangeEvent` | Event-based output |\n| LWC → Flow Nav | `FlowNavigationFinishEvent` | Navigation command |\n| LWC → Apex | `@wire` or `imperative` | Async call |\n| Apex → LWC | Return value / wire refresh | Response |\n\n---\n\n## Pattern 1: Flow Screen Component\n\n**Use Case**: Custom UI component for user selection within a guided Flow.\n\n```\n┌─────────┐ @api (in) ┌─────────┐\n│ FLOW │ ────────────────▶ │ LWC │\n│ Screen │ │ Screen │\n│ │ ◀──────────────── │Component│\n│ │ FlowAttribute │ │\n└─────────┘ ChangeEvent └─────────┘\n```\n\n### LWC JavaScript\n\n```javascript\nimport { LightningElement, api } from 'lwc';\nimport { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';\n\nexport default class RecordSelector extends LightningElement {\n // Input from Flow (read-only in component)\n @api recordId;\n @api availableActions = [];\n\n // Output to Flow (must dispatch event to update)\n @api selectedRecordId;\n\n handleSelect(event) {\n // Update local property\n this.selectedRecordId = event.detail.id;\n\n // CRITICAL: Dispatch event to update Flow variable\n this.dispatchEvent(new FlowAttributeChangeEvent(\n 'selectedRecordId', // Must match @api property name\n this.selectedRecordId\n ));\n }\n\n handleFinish() {\n // Navigate Flow to next screen\n if (this.availableActions.find(action => action === 'NEXT')) {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n }\n}\n```\n\n### LWC meta.xml (Flow Target)\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cLightningComponentBundle xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003capiVersion>66.0\u003c/apiVersion>\n \u003cisExposed>true\u003c/isExposed>\n \u003cmasterLabel>Record Selector\u003c/masterLabel>\n \u003cdescription>Custom record selection component for Flow screens\u003c/description>\n \u003ctargets>\n \u003ctarget>lightning__FlowScreen\u003c/target>\n \u003c/targets>\n \u003ctargetConfigs>\n \u003ctargetConfig targets=\"lightning__FlowScreen\">\n \u003cproperty name=\"recordId\" type=\"String\" label=\"Record ID\" description=\"Parent record ID\"/>\n \u003cproperty name=\"selectedRecordId\" type=\"String\" label=\"Selected Record ID\" role=\"outputOnly\"/>\n \u003c/targetConfig>\n \u003c/targetConfigs>\n\u003c/LightningComponentBundle>\n```\n\n### Property Roles\n\n| Role | Description | Flow Behavior |\n|------|-------------|---------------|\n| (none) | Input/Output | Editable in Flow builder |\n| `role=\"inputOnly\"` | Input only | Cannot be used as output |\n| `role=\"outputOnly\"` | Output only | Read-only in Flow builder |\n\n---\n\n## Pattern 2: LWC Calling Apex\n\n**Use Case**: LWC needs data or operations beyond Flow context.\n\n### Wire Pattern (Reactive, Cached)\n\n```javascript\nimport { LightningElement, api, wire } from 'lwc';\nimport getRecords from '@salesforce/apex/RecordController.getRecords';\n\nexport default class RecordList extends LightningElement {\n @api recordId;\n records;\n error;\n\n @wire(getRecords, { parentId: '$recordId' })\n wiredRecords({ error, data }) {\n if (data) {\n this.records = data;\n this.error = undefined;\n } else if (error) {\n this.error = error;\n this.records = undefined;\n }\n }\n}\n```\n\n### Imperative Pattern (On-Demand, Fresh)\n\n```javascript\nimport { LightningElement, api } from 'lwc';\nimport { FlowNavigationFinishEvent } from 'lightning/flowSupport';\nimport processRecord from '@salesforce/apex/RecordController.processRecord';\n\nexport default class RecordProcessor extends LightningElement {\n @api recordId;\n @api availableActions = [];\n isProcessing = false;\n\n async handleSubmit() {\n this.isProcessing = true;\n try {\n const result = await processRecord({ recordId: this.recordId });\n if (result.isSuccess) {\n // Navigate Flow forward\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n } catch (error) {\n // Handle error (show toast, etc.)\n console.error('Processing failed:', error.body.message);\n } finally {\n this.isProcessing = false;\n }\n }\n}\n```\n\n### When to Use Each Pattern\n\n| Pattern | Use When | Caching |\n|---------|----------|---------|\n| `@wire` | Data should refresh when inputs change | Yes (cacheable=true) |\n| Imperative | User-triggered action, DML needed | No |\n\n---\n\n## Pattern 3: Full Triangle Integration\n\n**Use Case**: LWC in Flow screen that calls Apex for complex operations.\n\n```javascript\nimport { LightningElement, api, wire } from 'lwc';\nimport { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';\nimport getProducts from '@salesforce/apex/ProductController.getProducts';\nimport calculatePricing from '@salesforce/apex/PricingService.calculate';\n\nexport default class ProductSelector extends LightningElement {\n @api accountId; // Input from Flow\n @api selectedProducts; // Output to Flow\n @api totalPrice; // Output to Flow\n @api availableActions = [];\n\n products;\n selectedIds = new Set();\n\n // Wire: Fetch products reactively\n @wire(getProducts, { accountId: '$accountId' })\n wiredProducts({ data, error }) {\n if (data) this.products = data;\n }\n\n handleProductSelect(event) {\n const productId = event.target.dataset.id;\n if (this.selectedIds.has(productId)) {\n this.selectedIds.delete(productId);\n } else {\n this.selectedIds.add(productId);\n }\n this.updateFlowOutputs();\n }\n\n async updateFlowOutputs() {\n // Update selected products output\n this.selectedProducts = Array.from(this.selectedIds);\n this.dispatchEvent(new FlowAttributeChangeEvent(\n 'selectedProducts',\n this.selectedProducts\n ));\n\n // Imperative: Calculate pricing\n const result = await calculatePricing({\n productIds: this.selectedProducts\n });\n this.totalPrice = result.total;\n this.dispatchEvent(new FlowAttributeChangeEvent(\n 'totalPrice',\n this.totalPrice\n ));\n }\n\n handleNext() {\n this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));\n }\n}\n```\n\n---\n\n## Jest Testing (Flow Integration)\n\n```javascript\nimport { createElement } from 'lwc';\nimport RecordSelector from 'c/recordSelector';\nimport { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';\n\n// Mock lightning/flowSupport\njest.mock('lightning/flowSupport', () => ({\n FlowAttributeChangeEvent: jest.fn(),\n FlowNavigationFinishEvent: jest.fn()\n}), { virtual: true });\n\ndescribe('c-record-selector', () => {\n afterEach(() => {\n while (document.body.firstChild) {\n document.body.removeChild(document.body.firstChild);\n }\n jest.clearAllMocks();\n });\n\n it('dispatches FlowAttributeChangeEvent on selection', async () => {\n const element = createElement('c-record-selector', {\n is: RecordSelector\n });\n element.recordId = '001xx000003GYHAA2';\n document.body.appendChild(element);\n\n // Simulate user selection\n const handler = jest.fn();\n element.addEventListener('flowattributechange', handler);\n\n const tile = element.shadowRoot.querySelector('[data-id]');\n tile.click();\n\n // Verify FlowAttributeChangeEvent was constructed\n expect(FlowAttributeChangeEvent).toHaveBeenCalledWith(\n 'selectedRecordId',\n expect.any(String)\n );\n });\n\n it('dispatches FlowNavigationFinishEvent on finish', async () => {\n const element = createElement('c-record-selector', {\n is: RecordSelector\n });\n element.availableActions = ['NEXT', 'FINISH'];\n document.body.appendChild(element);\n\n const finishButton = element.shadowRoot.querySelector('.finish-button');\n finishButton.click();\n\n expect(FlowNavigationFinishEvent).toHaveBeenCalledWith('NEXT');\n });\n});\n```\n\n---\n\n## Deployment Order\n\nWhen deploying integrated triangle solutions:\n\n```\n1. APEX CLASSES\n └── @AuraEnabled controllers (LWC depends on these)\n\n2. LWC COMPONENTS ← Deploy SECOND\n └── meta.xml with lightning__FlowScreen target\n └── JavaScript with Flow imports\n\n3. FLOWS\n └── Reference deployed LWC components\n```\n\n---\n\n## Common Anti-Patterns\n\n| Anti-Pattern | Problem | Solution |\n|--------------|---------|----------|\n| Missing FlowAttributeChangeEvent | Output never updates in Flow | Always dispatch event when output changes |\n| Direct @api property mutation for outputs | Flow doesn't see changes | Use FlowAttributeChangeEvent |\n| Mixing @wire and imperative for same data | Cache/freshness conflicts | Choose one pattern per data need |\n| Calling Apex for Flow-available data | Unnecessary callouts | Pass via inputParameters |\n| Hardcoded navigation actions | Breaks in different contexts | Check availableActions first |\n\n---\n\n## Flow Events Reference\n\n| Event | Purpose | Parameters |\n|-------|---------|------------|\n| `FlowAttributeChangeEvent` | Update output property | (propertyName, value) |\n| `FlowNavigationFinishEvent` | Navigate Flow | 'NEXT', 'BACK', 'PAUSE', 'FINISH' |\n\n---\n\n## Related Documentation\n\n| Topic | Location |\n|-------|----------|\n| Flow screen component template | `generating-lwc-components/assets/flow-screen-component/` |\n| LWC best practices | `generating-lwc-components/references/lwc-best-practices.md` |\n| Apex triangle perspective | `sf-apex/references/triangle-pattern.md` |\n| Flow triangle perspective | `sf-flow/references/triangle-pattern.md` |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12817,"content_sha256":"5f2544438431c915e27c86b2050ea959d6849f36ffbf90d228cef6c6a1b095ba"},{"filename":"scripts/local-dev-preview.sh","content":"#!/usr/bin/env bash\n# Local Development Server — preview LWC components with hot reload (no deployment needed).\n# Requires an active org connection. Commands install just-in-time on first run and open\n# a browser with live preview. Changes to .js, .html, and .css files auto-reload instantly.\n#\n# Usage: Pass --target-org \u003calias> and choose the mode that matches your use case.\n\nTARGET_ORG=\"${1:-}\"\n\nif [ -z \"$TARGET_ORG\" ]; then\n echo \"Usage: $0 \u003corg-alias> [component|app|site]\"\n echo \" component Preview a single LWC component in isolation (default)\"\n echo \" app Preview a Lightning Experience app locally\"\n echo \" site Preview an Experience Cloud site locally\"\n exit 1\nfi\n\nMODE=\"${2:-component}\"\n\ncase \"$MODE\" in\n component)\n sf lightning dev component --target-org \"$TARGET_ORG\"\n ;;\n app)\n sf lightning dev app --target-org \"$TARGET_ORG\"\n ;;\n site)\n sf lightning dev site --target-org \"$TARGET_ORG\"\n ;;\n *)\n echo \"Unknown mode: $MODE. Choose one of: component, app, site\"\n exit 1\n ;;\nesac\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1103,"content_sha256":"5f0e268e81b58f58fe70c7bf45f502c98ed140e9085d52799c27cbd6f8599ea9"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"generating-lwc-components: Lightning Web Components Development","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when the user needs ","type":"text"},{"text":"Lightning Web Components","type":"text","marks":[{"type":"strong"}]},{"text":": LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When This Skill Owns the Task","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"generating-lwc-components","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the work involves:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"lwc/**/*.js","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".html","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".css","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".js-meta.xml","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"component scaffolding and bundle design","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"wire service, Apex integration, GraphQL integration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SLDS 2, dark mode, and accessibility work","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Jest unit tests for LWC","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Delegate elsewhere when the user is:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing Apex controllers or business logic first → ","type":"text"},{"text":"generating-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../generating-apex/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"building Flow XML rather than an LWC screen component → ","type":"text"},{"text":"generating-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../generating-flow/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deploying metadata → ","type":"text"},{"text":"deploying-metadata","type":"text","marks":[{"type":"link","attrs":{"href":"../deploying-metadata/SKILL.md","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Required Context to Gather First","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask for or infer:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"component purpose and target surface","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data source: LDS, Apex, GraphQL, LMS, or external system via Apex","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether the user needs tests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether the component must run in Flow, App Builder, Experience Cloud, or dashboard contexts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"accessibility and styling expectations","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Recommended Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Choose the right architecture","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the ","type":"text"},{"text":"PICKLES","type":"text","marks":[{"type":"strong"}]},{"text":" mindset:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prototype","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"integrate the right data source","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"compose component boundaries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"define interaction model","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use platform libraries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"optimize execution","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"enforce security","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Choose the right data access pattern","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"single-record UI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LDS / ","type":"text"},{"text":"getRecord","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"simple CRUD form","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"base record form components","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"complex server query","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Apex ","type":"text"},{"text":"@AuraEnabled(cacheable=true)","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"related graph data","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GraphQL wire adapter","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cross-DOM communication","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lightning Message Service","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Start from an asset when useful","type":"text"}]},{"type":"paragraph","content":[{"text":"Use provided assets for:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"basic component bundles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"datatables","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"modal patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Flow screen components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GraphQL components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LMS message channels","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Jest tests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TypeScript-enabled components","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Validate for frontend quality","type":"text"}]},{"type":"paragraph","content":[{"text":"Check:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"accessibility","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SLDS 2 / dark mode compliance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"event contracts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"performance / rerender safety","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Jest coverage when required","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Hand off supporting backend or deploy work","type":"text"}]},{"type":"paragraph","content":[{"text":"Use:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"generating-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../generating-apex/SKILL.md","title":null}}]},{"text":" for controllers / services","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deploying-metadata","type":"text","marks":[{"type":"link","attrs":{"href":"../deploying-metadata/SKILL.md","title":null}}]},{"text":" for deployment","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"running-apex-tests","type":"text","marks":[{"type":"link","attrs":{"href":"../running-apex-tests/SKILL.md","title":null}}]},{"text":" only for Apex-side test loops, not Jest","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"High-Signal Rules","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prefer platform base components over reinventing controls","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use ","type":"text"},{"text":"@wire","type":"text","marks":[{"type":"code_inline"}]},{"text":" for reactive read-only use cases; imperative calls for explicit actions and DML paths","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"do not introduce inaccessible custom UI","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"avoid hardcoded colors; use SLDS 2-compatible styling hooks / variables","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"avoid rerender loops in ","type":"text"},{"text":"renderedCallback()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"keep component communication patterns explicit and minimal","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"When finishing, report in this order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Component(s) created or updated","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data access pattern chosen","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Files changed","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Accessibility / styling / testing notes","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Next implementation or deploy step","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"paragraph","content":[{"text":"Suggested shape:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"LWC work: \u003csummary>\nPattern: \u003cwire / apex / graphql / lms / flow-screen>\nFiles: \u003cpaths>\nQuality: \u003ca11y, SLDS2, dark mode, Jest>\nNext step: \u003cdeploy, add controller, or run tests>","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Local Development Server","type":"text"}]},{"type":"paragraph","content":[{"text":"Preview LWC components locally with hot reload — no deployment needed. Run the commands in ","type":"text"},{"text":"scripts/local-dev-preview.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" to start a local dev session for a component, app, or Experience Cloud site.","type":"text"}]},{"type":"paragraph","content":[{"text":"Local Dev commands install just-in-time on first run. They are long-running processes that open a browser with live preview. Changes to ","type":"text"},{"text":".js","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".html","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":".css","type":"text","marks":[{"type":"code_inline"}]},{"text":" files auto-reload instantly. Requires an active org connection for data and Apex callouts.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Skill Integration","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delegate to","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reason","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Apex controller or service","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generating-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../generating-apex/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"backend logic","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"embed in Flow screens","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generating-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../generating-flow/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"declarative orchestration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy component bundle","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploying-metadata","type":"text","marks":[{"type":"link","attrs":{"href":"../deploying-metadata/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"org rollout","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create supporting metadata (message channels, objects)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploying-metadata","type":"text","marks":[{"type":"link","attrs":{"href":"../deploying-metadata/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata deployment","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference File Index","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Start here","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/component-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/component-patterns.md","title":null}}]},{"text":" — component architecture patterns and bundle design","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/slds-design-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/slds-design-guide.md","title":null}}]},{"text":" — SLDS 2 styling, dark mode, CSS hooks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/lwc-best-practices.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/lwc-best-practices.md","title":null}}]},{"text":" — high-signal rules and anti-patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/scoring-and-testing.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/scoring-and-testing.md","title":null}}]},{"text":" — 165-point scoring rubric across 8 categories","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/jest-testing.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/jest-testing.md","title":null}}]},{"text":" — Jest unit test patterns and async rendering helpers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/slds-blueprints.json","type":"text","marks":[{"type":"link","attrs":{"href":"references/slds-blueprints.json","title":null}}]},{"text":" — machine-readable SLDS component blueprints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/cli-commands.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cli-commands.md","title":null}}]},{"text":" — SF CLI commands for LWC development","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Accessibility / performance / state","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/accessibility-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/accessibility-guide.md","title":null}}]},{"text":" — WCAG, ARIA, keyboard navigation patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/performance-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/performance-guide.md","title":null}}]},{"text":" — lazy loading, debouncing, rerender safety","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/state-management.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/state-management.md","title":null}}]},{"text":" — reactive state patterns and LMS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/template-anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/template-anti-patterns.md","title":null}}]},{"text":" — common HTML template mistakes to avoid","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Integration / advanced features","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/lms-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/lms-guide.md","title":null}}]},{"text":" — Lightning Message Service patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/flow-integration-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/flow-integration-guide.md","title":null}}]},{"text":" — Flow screen component design","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/advanced-features.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/advanced-features.md","title":null}}]},{"text":" — Spring '26 features: TypeScript, lwc:on, GraphQL mutations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/async-notification-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/async-notification-patterns.md","title":null}}]},{"text":" — toast, notifications, async flows","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/triangle-pattern.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/triangle-pattern.md","title":null}}]},{"text":" — parent-child-sibling communication triangle","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Asset templates","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/basic-component/basicComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/basic-component/basicComponent.js","title":null}}]},{"text":" — wire service, error/loading states, event dispatching","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/datatable-component/datatableComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/datatable-component/datatableComponent.js","title":null}}]},{"text":" — datatable with inline editing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/flow-screen-component/flowScreenComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/flow-screen-component/flowScreenComponent.js","title":null}}]},{"text":" — Flow screen with input/output properties","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/form-component/formComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/form-component/formComponent.js","title":null}}]},{"text":" — form validation and DML patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/graphql-component/graphqlComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/graphql-component/graphqlComponent.js","title":null}}]},{"text":" — GraphQL wire adapter with cursor-based pagination","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/jest-test/componentName.test.js.example","type":"text","marks":[{"type":"link","attrs":{"href":"assets/jest-test/componentName.test.js.example","title":null}}]},{"text":" — Jest test template (copy and rename, remove ","type":"text"},{"text":".example","type":"text","marks":[{"type":"code_inline"}]},{"text":" suffix)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/message-channel/lmsPublisher.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/message-channel/lmsPublisher.js","title":null}}]},{"text":" — LMS publisher pattern","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/message-channel/lmsSubscriber.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/message-channel/lmsSubscriber.js","title":null}}]},{"text":" — LMS subscriber pattern","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/modal-component/modalComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/modal-component/modalComponent.js","title":null}}]},{"text":" — modal with focus trap and ESC handling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/record-picker/recordPicker.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/record-picker/recordPicker.js","title":null}}]},{"text":" — record picker with search","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/state-store/store.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/state-store/store.js","title":null}}]},{"text":" — reactive state store for cross-component state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/typescript-component/typescriptComponent.ts","type":"text","marks":[{"type":"link","attrs":{"href":"assets/typescript-component/typescriptComponent.ts","title":null}}]},{"text":" — TypeScript-enabled component (Spring '26)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/workspace-api/workspaceComponent.js","type":"text","marks":[{"type":"link","attrs":{"href":"assets/workspace-api/workspaceComponent.js","title":null}}]},{"text":" — workspace API for tab and focus management","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/apex-controller/LwcController.cls","type":"text","marks":[{"type":"link","attrs":{"href":"assets/apex-controller/LwcController.cls","title":null}}]},{"text":" — Apex controller with ","type":"text"},{"text":"@AuraEnabled(cacheable=true)","type":"text","marks":[{"type":"code_inline"}]},{"text":" patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Scripts","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/local-dev-preview.sh","type":"text","marks":[{"type":"link","attrs":{"href":"scripts/local-dev-preview.sh","title":null}}]},{"text":" — local dev server commands for component, app, and site preview","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Score Guide","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Score","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"150+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"production-ready LWC bundle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"125–149","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"strong component with minor polish left","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"100–124","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"functional but review recommended","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\u003c 100","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"needs significant improvement","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"generating-lwc-components","author":"@skillopedia","source":{"stars":438,"repo_name":"sf-skills","origin_url":"https://github.com/forcedotcom/sf-skills/blob/HEAD/skills/generating-lwc-components/SKILL.md","repo_owner":"forcedotcom","body_sha256":"f29f5d34d823fd3100de42c4623447dfe5a852969c96c2a103f9822145f64d70","cluster_key":"99ac26dfd8d50b78eeee42adad7b92a31a09835ab19c47a3daea9fa381dcb311","clean_bundle":{"format":"clean-skill-bundle-v1","source":"forcedotcom/sf-skills/skills/generating-lwc-components/SKILL.md","attachments":[{"id":"ae41ffa5-6668-5426-a7a5-6d340e60b0b8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ae41ffa5-6668-5426-a7a5-6d340e60b0b8/attachment.md","path":"CREDITS.md","size":148,"sha256":"7d5a7ebd7bfe550af7e3c755082fd75625d7cc138a00861313ce70b6d2e074a6","contentType":"text/markdown; charset=utf-8"},{"id":"6e2c631d-4fea-5b74-992a-d9fe065c0b9a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6e2c631d-4fea-5b74-992a-d9fe065c0b9a/attachment.md","path":"README.md","size":4144,"sha256":"e1e9622952ed62193d98ead33b96989e639e7bc0a11f6c8295e1a6783dba15f7","contentType":"text/markdown; charset=utf-8"},{"id":"5c64e655-fad5-5836-8936-417d3dbb7c96","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5c64e655-fad5-5836-8936-417d3dbb7c96/attachment.cls","path":"assets/apex-controller/LwcController.cls","size":12272,"sha256":"a400c11ec219e5214b455797b7198d9e2f85bf0c7153b0f74abdc52c59d7d0cc","contentType":"text/x-tex"},{"id":"20432c86-9335-59f1-970c-5dbb1a0621c2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20432c86-9335-59f1-970c-5dbb1a0621c2/attachment.css","path":"assets/basic-component/basicComponent.css","size":1486,"sha256":"a6b169693899cd87c3dac6262f669a8ef17774ec2c2e0d6fd4fafb9cd6a18fdf","contentType":"text/css; charset=utf-8"},{"id":"25b3573c-717a-5adc-b21c-2d050f03e434","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/25b3573c-717a-5adc-b21c-2d050f03e434/attachment.html","path":"assets/basic-component/basicComponent.html","size":4601,"sha256":"f2024bee0526bf137da8c75216f0ac7df6251cbf4cd6e9eda6f00571b12d3923","contentType":"text/html; charset=utf-8"},{"id":"b565e0c9-7408-50b1-9304-d7191e220a77","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b565e0c9-7408-50b1-9304-d7191e220a77/attachment.js","path":"assets/basic-component/basicComponent.js","size":7447,"sha256":"16224c38d8d29ee058fe37cf57211e07a069e4133a79e85841e2e6ad4d9f0c4e","contentType":"application/javascript; charset=utf-8"},{"id":"60169937-6fc6-5c88-9cac-fdadd6720a27","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/60169937-6fc6-5c88-9cac-fdadd6720a27/attachment.xml","path":"assets/basic-component/basicComponent.js-meta.xml","size":4650,"sha256":"bc605729fa4839a5fa589d8232a6b0f93844d4dbf09efe9e51af92ad1a63a683","contentType":"application/xml"},{"id":"befb9591-360d-58f3-8953-814f9e7524b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/befb9591-360d-58f3-8953-814f9e7524b9/attachment.html","path":"assets/datatable-component/datatableComponent.html","size":4229,"sha256":"2f714384a928a9077b3fc8606bc2567adb18245d11fac98c82905df350ce9049","contentType":"text/html; charset=utf-8"},{"id":"9bc55f38-bb2c-54c0-925c-e461ade5cb57","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9bc55f38-bb2c-54c0-925c-e461ade5cb57/attachment.js","path":"assets/datatable-component/datatableComponent.js","size":13414,"sha256":"86e587f6a3fd99d921ee04104850d07db05ae6822d313da6bf5908856bbf7cb8","contentType":"application/javascript; charset=utf-8"},{"id":"91b41fc7-78aa-5777-b8df-e6241ee6b8d6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91b41fc7-78aa-5777-b8df-e6241ee6b8d6/attachment.css","path":"assets/flow-screen-component/flowScreenComponent.css","size":1723,"sha256":"fe68da4276e9173869ed5d89517696e949eeabfc852c09c38d1ea18c435d1e64","contentType":"text/css; charset=utf-8"},{"id":"351edd19-1012-52ca-b4c1-e1c43bda682b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/351edd19-1012-52ca-b4c1-e1c43bda682b/attachment.html","path":"assets/flow-screen-component/flowScreenComponent.html","size":7268,"sha256":"20cd8871425dca3f876de92f21ac4addb6a32556184d96305785e838d40890df","contentType":"text/html; charset=utf-8"},{"id":"810ec1ec-da63-5759-bd4c-609905e15ca4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/810ec1ec-da63-5759-bd4c-609905e15ca4/attachment.js","path":"assets/flow-screen-component/flowScreenComponent.js","size":14199,"sha256":"f5d53c63cae0a37d3b311854e116f47a6c25d0d6418dc01a1c04ef0c8cd9d89d","contentType":"application/javascript; charset=utf-8"},{"id":"4159e025-eb9b-5e61-94a8-42b2e72e963a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4159e025-eb9b-5e61-94a8-42b2e72e963a/attachment.xml","path":"assets/flow-screen-component/flowScreenComponent.js-meta.xml","size":3988,"sha256":"7a1fee08ed7db778a2edd010783ab6dbb5bb54aced784e63ad6efa7c3d01b19c","contentType":"application/xml"},{"id":"80a0331a-e58d-5495-95b6-3974d968d00e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/80a0331a-e58d-5495-95b6-3974d968d00e/attachment.html","path":"assets/form-component/formComponent.html","size":6538,"sha256":"7382fe4c65ecadb563c54bc6d42557cb9cefa6be033bf0d777684da984444d66","contentType":"text/html; charset=utf-8"},{"id":"dc7c2718-53ee-538c-9d71-881fb557620f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc7c2718-53ee-538c-9d71-881fb557620f/attachment.js","path":"assets/form-component/formComponent.js","size":11617,"sha256":"72de0a1b9b0530effd75240cd8e0a7aab555c75a60b0a9d6c641f7f7cd4a7a5f","contentType":"application/javascript; charset=utf-8"},{"id":"3091785e-6b67-553c-ab26-325e4258132c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3091785e-6b67-553c-ab26-325e4258132c/attachment.html","path":"assets/graphql-component/graphqlComponent.html","size":4906,"sha256":"bafe48b03a9643425ea42943219513951aa23ee8d84940750bd4a209a0430b33","contentType":"text/html; charset=utf-8"},{"id":"be4ddd73-9efd-5189-a1f9-b0ba378b6318","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/be4ddd73-9efd-5189-a1f9-b0ba378b6318/attachment.js","path":"assets/graphql-component/graphqlComponent.js","size":10965,"sha256":"7064567838426f9c494523932ea9b6db7306eb5d6a1be2ade181e9dced0c6496","contentType":"application/javascript; charset=utf-8"},{"id":"709ae824-5647-556c-84dd-cfbb72632e92","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/709ae824-5647-556c-84dd-cfbb72632e92/attachment.example","path":"assets/jest-test/componentName.test.js.example","size":15983,"sha256":"e5df691dc65b135ed6b9a300ee3ae15d44fd35c82537f0ce8ee86b444c8429c9","contentType":"application/octet-stream"},{"id":"14dadb44-b278-56b9-99a7-20a3ceb8bde0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/14dadb44-b278-56b9-99a7-20a3ceb8bde0/attachment.xml","path":"assets/message-channel/RecordSelected.messageChannel-meta.xml","size":2465,"sha256":"46adc20f5f1c01b711151e1cc4d822de3a409acf21411f1b2ba4a4128ba4d662","contentType":"application/xml"},{"id":"0b641679-c400-52c6-a6e9-d0a624da9a33","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0b641679-c400-52c6-a6e9-d0a624da9a33/attachment.js","path":"assets/message-channel/lmsPublisher.js","size":3934,"sha256":"8b80f3abefee07e592ed6af478540b4011f00f92fa7ea96fbff03469a1792216","contentType":"application/javascript; charset=utf-8"},{"id":"687354f5-efd5-5e06-94ce-6c77d9d50c44","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/687354f5-efd5-5e06-94ce-6c77d9d50c44/attachment.js","path":"assets/message-channel/lmsSubscriber.js","size":7132,"sha256":"9581e594aa5bea0410b39890e5ffe758d9d825eebf406b635d559c3198220b7c","contentType":"application/javascript; charset=utf-8"},{"id":"867d7251-1b6d-500c-96fe-b30c09dc05dc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/867d7251-1b6d-500c-96fe-b30c09dc05dc/attachment.html","path":"assets/modal-component/modalComponent.html","size":3299,"sha256":"ab4e585ac8273416241751a4e975b2b61a89af5d183446690b6b8c4acd1b8737","contentType":"text/html; charset=utf-8"},{"id":"aafa1ff7-e9bf-53eb-9428-2453c780d7cf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aafa1ff7-e9bf-53eb-9428-2453c780d7cf/attachment.js","path":"assets/modal-component/modalComponent.js","size":5581,"sha256":"8cab9bc744858b43537f12fa46bcb5ff96dbb205b517306d3a8c666a4db2b60b","contentType":"application/javascript; charset=utf-8"},{"id":"25f19945-d94e-55f8-b42b-04b766302751","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/25f19945-d94e-55f8-b42b-04b766302751/attachment.html","path":"assets/record-picker/recordPicker.html","size":2044,"sha256":"4221f6e525d245e751e17c9c681cabc63f111cd6ac7c08688b95de95d76e0a2d","contentType":"text/html; charset=utf-8"},{"id":"6bbe9310-b975-55a9-97da-2b207f13b93c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6bbe9310-b975-55a9-97da-2b207f13b93c/attachment.js","path":"assets/record-picker/recordPicker.js","size":5576,"sha256":"94266042a61cc385000261f677510dcb50ac55037c3404ef9b188103dd1ff43d","contentType":"application/javascript; charset=utf-8"},{"id":"faf1eb78-cf8b-53b7-ad36-f98c9bba5e8f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/faf1eb78-cf8b-53b7-ad36-f98c9bba5e8f/attachment.js","path":"assets/state-store/store.js","size":9350,"sha256":"2ed39c3b5227a1f3c7e6d20e2fe4f67422973b0ec9d3c777a5df79d44afe3446","contentType":"application/javascript; charset=utf-8"},{"id":"49c819a5-9bb5-5725-96ba-fb6a1e738efa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/49c819a5-9bb5-5725-96ba-fb6a1e738efa/attachment.css","path":"assets/typescript-component/typescriptComponent.css","size":1358,"sha256":"12495eee93c2accaf8183edc3b88706f990c5d64d9ec44efe4af8e386b09855d","contentType":"text/css; charset=utf-8"},{"id":"30ae7d6b-ee21-5358-9caf-7529211fcf7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/30ae7d6b-ee21-5358-9caf-7529211fcf7f/attachment.html","path":"assets/typescript-component/typescriptComponent.html","size":4453,"sha256":"f25191ac99b62ab414a926dc39c79d721857e5adc14ba86029bbadce37026177","contentType":"text/html; charset=utf-8"},{"id":"d8c65a20-edb6-5dad-a7b3-95f62cf9a717","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d8c65a20-edb6-5dad-a7b3-95f62cf9a717/attachment.xml","path":"assets/typescript-component/typescriptComponent.js-meta.xml","size":2410,"sha256":"26f03953c5d27688d8f0cb6f256a9804033bb4b6d897f7dce865391756ad762f","contentType":"application/xml"},{"id":"46723d62-e1a7-5d1c-b4d6-03aff46233ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/46723d62-e1a7-5d1c-b4d6-03aff46233ec/attachment.example","path":"assets/typescript-component/typescriptComponent.test.ts.example","size":13963,"sha256":"0891524bb9023e27fc100d0b86f829ae0a7372da1a48940dc65d2cc60552e3aa","contentType":"application/octet-stream"},{"id":"9d5c019f-def2-52af-bcca-c1755116b599","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9d5c019f-def2-52af-bcca-c1755116b599/attachment.ts","path":"assets/typescript-component/typescriptComponent.ts","size":12023,"sha256":"8218fb0577a608311f54bb10feb3c1c16f3ee55c90b8871b8bc4ed624fc4f00f","contentType":"text/typescript; charset=utf-8"},{"id":"224eb73a-a631-5034-92f6-b98a2a44c8be","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/224eb73a-a631-5034-92f6-b98a2a44c8be/attachment.html","path":"assets/workspace-api/workspaceComponent.html","size":3442,"sha256":"aad7a947c9107a1cd1b72d780d9e04c92e923a31549448f0ebe89d6cb5d31af8","contentType":"text/html; charset=utf-8"},{"id":"48e63e99-9374-5d84-ad7e-bdf5f88ae2b4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/48e63e99-9374-5d84-ad7e-bdf5f88ae2b4/attachment.js","path":"assets/workspace-api/workspaceComponent.js","size":8207,"sha256":"efc59d71fc824185af55a7c545e1dff1a7b1e5a4925bd3fe84127ddd567a7678","contentType":"application/javascript; charset=utf-8"},{"id":"e11bb410-7a91-5116-b4b4-9fdead105088","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e11bb410-7a91-5116-b4b4-9fdead105088/attachment.py","path":"hooks/scripts/lwc-lsp-validate.py","size":8802,"sha256":"5732110827600e7fdd7eef29485360f39d80b46342fac381af6186b89c636b09","contentType":"text/x-python; charset=utf-8"},{"id":"019f394f-0849-5235-917a-f2756704325d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/019f394f-0849-5235-917a-f2756704325d/attachment.py","path":"hooks/scripts/post-tool-validate.py","size":15086,"sha256":"c0808a1902a363702b0468241c653eac234998756546fe866cc9740f53e28061","contentType":"text/x-python; charset=utf-8"},{"id":"e78343d1-ea9f-518a-a9d1-441bb241db2b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e78343d1-ea9f-518a-a9d1-441bb241db2b/attachment.json","path":"hooks/scripts/slds_data/deprecated_patterns.json","size":3440,"sha256":"af1b834ed2b941670b8cac7f8c0e46745ce282bc5756812028b6539609951c81","contentType":"application/json; charset=utf-8"},{"id":"db7d54f6-8694-59b6-b9c1-45236fda2633","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/db7d54f6-8694-59b6-b9c1-45236fda2633/attachment.json","path":"hooks/scripts/slds_data/styling_hooks.json","size":2936,"sha256":"fff56d0a2a67496476a1338355fc38500c854cc185d1987867b0dc514dbc6432","contentType":"application/json; charset=utf-8"},{"id":"3cce4d79-fbc2-57b3-a3b3-e82b657348b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3cce4d79-fbc2-57b3-a3b3-e82b657348b9/attachment.json","path":"hooks/scripts/slds_data/valid_slds_classes.json","size":7627,"sha256":"f5ea252c453f53483df2212fe6c7240c18e4c8fcd4ec424b6a13d33fd2696581","contentType":"application/json; charset=utf-8"},{"id":"2ec29a0f-e546-56e7-892d-9ddbfe82373d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2ec29a0f-e546-56e7-892d-9ddbfe82373d/attachment.py","path":"hooks/scripts/slds_linter_wrapper.py","size":9137,"sha256":"08bb8c67064b862a8a8157dbbf26de831917fcdefe81d6fa257c5d61d0c6e3db","contentType":"text/x-python; charset=utf-8"},{"id":"38b45ed9-7a05-579c-9e1b-c72e364500d5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/38b45ed9-7a05-579c-9e1b-c72e364500d5/attachment.py","path":"hooks/scripts/slds_rules/__init__.py","size":593,"sha256":"d617d085d78dda8df914a7eedb80c7ba12204ba95b168c62906ccaa533fc6abb","contentType":"text/x-python; charset=utf-8"},{"id":"d5f2f95d-4b9d-5ca7-8fed-9164a819c644","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5f2f95d-4b9d-5ca7-8fed-9164a819c644/attachment.py","path":"hooks/scripts/template_validator.py","size":13831,"sha256":"4b17de2017878da80ba44c0e2c1e493758e6ecfbabdcfcc2826eea3435297bc1","contentType":"text/x-python; charset=utf-8"},{"id":"6610d88c-71d3-53a7-84dc-1f18ed1acc31","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6610d88c-71d3-53a7-84dc-1f18ed1acc31/attachment.py","path":"hooks/scripts/validate_slds.py","size":27160,"sha256":"2271558829ccde522615475ef37d52e9d4e2647db06aa103a8c089fc34e98c3e","contentType":"text/x-python; charset=utf-8"},{"id":"52f19cf3-9fb5-5b33-8035-4afc685f0705","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/52f19cf3-9fb5-5b33-8035-4afc685f0705/attachment.md","path":"references/accessibility-guide.md","size":22860,"sha256":"ede514e7146c11afb07e5aad27381133c1844ec11ba56e930b08232311f0f4fe","contentType":"text/markdown; charset=utf-8"},{"id":"4aef4e16-be2d-5c8f-ba8c-5ab815084993","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4aef4e16-be2d-5c8f-ba8c-5ab815084993/attachment.md","path":"references/advanced-features.md","size":2897,"sha256":"8a48632b59b55c5ed5bdc329806b9acdc053ea51bc5a5d09c112a2049ebfa616","contentType":"text/markdown; charset=utf-8"},{"id":"cd1a215a-c34e-59a2-9634-1b5ecb182075","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd1a215a-c34e-59a2-9634-1b5ecb182075/attachment.md","path":"references/async-notification-patterns.md","size":20146,"sha256":"9d970333a0198685912064821f2175771b3cc714871efbeebdfc46e43f407a86","contentType":"text/markdown; charset=utf-8"},{"id":"dec15be6-0be3-5ccb-bcfa-94bd13c0409d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dec15be6-0be3-5ccb-bcfa-94bd13c0409d/attachment.md","path":"references/cli-commands.md","size":12871,"sha256":"4c83ad8a78783350d435f53ad6bc286759a5592bfa66f4e14a4bb210712755c2","contentType":"text/markdown; charset=utf-8"},{"id":"5bb39b7e-6408-5eb3-b831-b9b939636d74","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5bb39b7e-6408-5eb3-b831-b9b939636d74/attachment.md","path":"references/component-patterns.md","size":45558,"sha256":"ed18e31b9570058f46d2b08e7a2c3a8bf0c39bde87da08b31dd6d97d980bfc95","contentType":"text/markdown; charset=utf-8"},{"id":"f9c5e88a-5bc3-5bdb-b73f-33bfc46f62b2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f9c5e88a-5bc3-5bdb-b73f-33bfc46f62b2/attachment.md","path":"references/flow-integration-guide.md","size":19426,"sha256":"074b802f7178787d04f728b69c6a1c96be02d40212a7f9b8b2a12a439fa6b70a","contentType":"text/markdown; charset=utf-8"},{"id":"83e03e45-b9ab-5b45-93ef-ce1eab76623a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/83e03e45-b9ab-5b45-93ef-ce1eab76623a/attachment.md","path":"references/jest-testing.md","size":25948,"sha256":"1a1a64beb6d453004640172f597b163ec0934b3c8f6e7a4052d7d688af25e48a","contentType":"text/markdown; charset=utf-8"},{"id":"7aae52c7-3d87-5589-a4ae-903285fda481","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7aae52c7-3d87-5589-a4ae-903285fda481/attachment.md","path":"references/lms-guide.md","size":22390,"sha256":"385c3a5dc87261f5beeed49dd268a2a37f5ae1651c9fe5cb26705df3efdb6477","contentType":"text/markdown; charset=utf-8"},{"id":"df9f124e-e4a4-5236-a476-cbcb9ebdec9f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/df9f124e-e4a4-5236-a476-cbcb9ebdec9f/attachment.md","path":"references/lwc-best-practices.md","size":37153,"sha256":"278199aca2f4d093c1ed00777c2fba6b11ea83cf1353337188f72a36f5dadb79","contentType":"text/markdown; charset=utf-8"},{"id":"a727b40d-0ad3-5207-99b1-776e07849693","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a727b40d-0ad3-5207-99b1-776e07849693/attachment.md","path":"references/performance-guide.md","size":20786,"sha256":"b0c71edfaf3cd643fb6e9f5d319d4fdc3c3235194727fc0a24f14105882d1cc6","contentType":"text/markdown; charset=utf-8"},{"id":"ba1d8379-4c70-59f1-8369-4d1a1dbbc14b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ba1d8379-4c70-59f1-8369-4d1a1dbbc14b/attachment.md","path":"references/scoring-and-testing.md","size":3866,"sha256":"75d4eddffd6a7eb502df2bd8ae7c5ce7943f3175e9373fbaaf007da131069e85","contentType":"text/markdown; charset=utf-8"},{"id":"08e22892-1364-51e2-9b0d-807b5cbd0664","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/08e22892-1364-51e2-9b0d-807b5cbd0664/attachment.json","path":"references/slds-blueprints.json","size":800013,"sha256":"0ae59172c9885f936994109edb51cf7315ab3c37df3b368383d9a0561c12e4f8","contentType":"application/json; charset=utf-8"},{"id":"6629560c-1908-5cc7-8436-d0f42bb42fb8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6629560c-1908-5cc7-8436-d0f42bb42fb8/attachment.md","path":"references/slds-design-guide.md","size":10977,"sha256":"5eb40ddd258be473227cb3e70e9fff9bbb0fe46d6660bcb55ac07234afd9c80c","contentType":"text/markdown; charset=utf-8"},{"id":"bc451e04-6114-5119-a824-28844b3693e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bc451e04-6114-5119-a824-28844b3693e2/attachment.md","path":"references/state-management.md","size":20357,"sha256":"2e9e9f6c02f3f9ba37a5cbdb86bd7496f09f7673247a6294f7b09d17436c5607","contentType":"text/markdown; charset=utf-8"},{"id":"abc045fc-a5b0-53f8-935d-f1b02ee2ce4a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/abc045fc-a5b0-53f8-935d-f1b02ee2ce4a/attachment.md","path":"references/template-anti-patterns.md","size":20729,"sha256":"57a1b859de78b60498bfd911f3d903578fc5632338b18cfcd5eb052dc1f5de52","contentType":"text/markdown; charset=utf-8"},{"id":"f8239daf-008a-58e2-99eb-c13c12ba3cdc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f8239daf-008a-58e2-99eb-c13c12ba3cdc/attachment.md","path":"references/triangle-pattern.md","size":12817,"sha256":"5f2544438431c915e27c86b2050ea959d6849f36ffbf90d228cef6c6a1b095ba","contentType":"text/markdown; charset=utf-8"},{"id":"ea6b6085-d5c6-5f0a-92a5-73460c43b71a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ea6b6085-d5c6-5f0a-92a5-73460c43b71a/attachment.sh","path":"scripts/local-dev-preview.sh","size":1103,"sha256":"5f0e268e81b58f58fe70c7bf45f502c98ed140e9085d52799c27cbd6f8599ea9","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"7f60b1630ea24efdade7876cd1a7730fcdb54b9e835ac9c15501ee7c1a48b786","attachment_count":59,"text_attachments":57,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":2,"excluded_attachments":[]},"cluster_size":2,"skill_md_path":"skills/generating-lwc-components/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":1},"license":"MIT","version":"v1","category":"testing-qa","metadata":{"version":"1.1"},"import_tag":"clean-skills-v1","description":"Lightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce."}},"renderedAt":1782987268102}

generating-lwc-components: Lightning Web Components Development Use this skill when the user needs Lightning Web Components : LWC bundles, wire patterns, Apex/GraphQL integration, SLDS 2 styling, accessibility, performance work, or Jest unit tests. When This Skill Owns the Task Use when the work involves: - , , , - component scaffolding and bundle design - wire service, Apex integration, GraphQL integration - SLDS 2, dark mode, and accessibility work - Jest unit tests for LWC Delegate elsewhere when the user is: - writing Apex controllers or business logic first → generating-apex - building F…