From 9ba064174a714e487c683b986c3c6e08d478b784 Mon Sep 17 00:00:00 2001 From: k-j-kim <17989954+k-j-kim@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:16:27 -0700 Subject: [PATCH 1/2] feat: syncing webapp skills sync to afv @W-21338965@ (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: removing old webapp skills * feat: adding sync of skills from webapps to afv * feat: adding the first iteration of skills * feat: pin template deps to latest npm versions and flatten skill folders - Add pin-template-deps.js to resolve "*" deps to exact npm versions - Integrate pinning into sync-template-skills npm script - Remove check-template-skills-versions.js (no longer needed) - Simplify workflow to single sync step - Flatten skill output: one folder per skill with cleaned names Made-with: Cursor * fix: resolve skill validation errors - Move .template-versions.json from skills/ to root - Shorten skill names to meet 64-char limit: - salesforce-webapp-feature-micro-frontend-generating-micro-frontend-lwc → salesforce-webapp-micro-frontend-lwc - salesforce-webapp-feature-react-agentforce-conversation-client-integrating-agentforce-conversation-client → salesforce-webapp-agentforce-conversation-client - salesforce-webapp-feature-react-file-upload-implementing-file-upload → salesforce-webapp-react-file-upload - Expand descriptions to meet 20-word minimum with trigger context * Add webapp skills from template, sync script updates - Rename skill folders from salesforce-webapp-* to *-webapp-* convention - Update sync-template-skills.js: set SKILL.md front matter name to dest folder - Remove sync-template-skills workflow and pin-template-deps script - Add .synced-template-skills.json manifest, deploying-webapp-to-salesforce skill - Replace salesforce-webapp-designing-webapp-ui-ux with designing-webapp-ui-ux Made-with: Cursor * Align SKILL.md front matter name with folder for all webapp skills Made-with: Cursor * Fix skill validation: description length and trigger context for configuring-webapp-metadata, creating-webapp Made-with: Cursor * Rename sync script to sync-webapp-skills, drop manifest file - Rename sync-template-skills.js to sync-webapp-skills.js - Update package.json script to sync-webapp-skills - Remove .synced-template-skills.json creation and add to .gitignore Made-with: Cursor * Revert sync-react-b2e-sample and sync-react-b2x-sample to upstream version Made-with: Cursor * Sync script: pin b2e and b2x to latest, sync skills from template - Pin both template packages to latest in sync-webapp-skills.js - Update package.json / package-lock.json (b2x 1.109.0) - Sync skills: managing-webapp-agentforce-conversation-client, bar-line-chart, remove building-webapp-analytics-charts and integrating-webapp-agentforce-conversation-client - Minor skill content updates Made-with: Cursor * Remove interactive map, weather widget, and Unsplash skills (no longer in template) Made-with: Cursor --------- Co-authored-by: Hemant Singh Bisht --- .gitignore | 3 + .template-versions.json | 10 + package-lock.json | 48 +- package.json | 7 +- scripts/sync-webapp-skills.js | 118 ++ skills/accessing-webapp-data/SKILL.md | 178 +++ .../SKILL.md | 72 ++ .../implementation/bar-line-chart.md | 316 +++++ .../implementation/dashboard-layout.md | 189 +++ .../implementation/donut-chart.md | 181 +++ .../implementation/stat-card.md | 150 +++ .../building-webapp-react-components/SKILL.md | 96 ++ .../implementation/component.md | 78 ++ .../implementation/header-footer.md | 132 ++ .../implementation/page.md | 93 ++ .../SKILL.md | 90 ++ .../implementation/metadata-format.md | 281 ++++ skills/configuring-webapp-metadata/SKILL.md | 158 +++ skills/creating-webapp/SKILL.md | 141 ++ .../deploying-webapp-to-salesforce/SKILL.md | 229 ++++ .../exploring-webapp-graphql-schema/SKILL.md | 149 +++ skills/fetching-webapp-rest-api/SKILL.md | 167 +++ .../SKILL.md | 258 ++++ .../SKILL.md | 253 ++++ .../implementing-webapp-file-upload/SKILL.md | 396 ++++++ skills/installing-webapp-features/SKILL.md | 210 +++ .../SKILL.md | 186 +++ .../references/constraints.md | 134 ++ .../references/examples.md | 132 ++ .../references/style-tokens.md | 101 ++ .../references/troubleshooting.md | 57 + .../SKILL.md | 84 -- skills/salesforce-web-app-feature/SKILL.md | 70 - .../SKILL.md | 36 - skills/salesforce-web-application/SKILL.md | 34 - skills/using-webapp-graphql/SKILL.md | 324 +++++ .../shared-schema.graphqls | 1150 +++++++++++++++++ 37 files changed, 6060 insertions(+), 251 deletions(-) create mode 100644 .template-versions.json create mode 100644 scripts/sync-webapp-skills.js create mode 100644 skills/accessing-webapp-data/SKILL.md create mode 100644 skills/building-webapp-data-visualization/SKILL.md create mode 100644 skills/building-webapp-data-visualization/implementation/bar-line-chart.md create mode 100644 skills/building-webapp-data-visualization/implementation/dashboard-layout.md create mode 100644 skills/building-webapp-data-visualization/implementation/donut-chart.md create mode 100644 skills/building-webapp-data-visualization/implementation/stat-card.md create mode 100644 skills/building-webapp-react-components/SKILL.md create mode 100644 skills/building-webapp-react-components/implementation/component.md create mode 100644 skills/building-webapp-react-components/implementation/header-footer.md create mode 100644 skills/building-webapp-react-components/implementation/page.md create mode 100644 skills/configuring-webapp-csp-trusted-sites/SKILL.md create mode 100644 skills/configuring-webapp-csp-trusted-sites/implementation/metadata-format.md create mode 100644 skills/configuring-webapp-metadata/SKILL.md create mode 100644 skills/creating-webapp/SKILL.md create mode 100644 skills/deploying-webapp-to-salesforce/SKILL.md create mode 100644 skills/exploring-webapp-graphql-schema/SKILL.md create mode 100644 skills/fetching-webapp-rest-api/SKILL.md create mode 100644 skills/generating-webapp-graphql-mutation-query/SKILL.md create mode 100644 skills/generating-webapp-graphql-read-query/SKILL.md create mode 100644 skills/implementing-webapp-file-upload/SKILL.md create mode 100644 skills/installing-webapp-features/SKILL.md create mode 100644 skills/managing-webapp-agentforce-conversation-client/SKILL.md create mode 100644 skills/managing-webapp-agentforce-conversation-client/references/constraints.md create mode 100644 skills/managing-webapp-agentforce-conversation-client/references/examples.md create mode 100644 skills/managing-webapp-agentforce-conversation-client/references/style-tokens.md create mode 100644 skills/managing-webapp-agentforce-conversation-client/references/troubleshooting.md delete mode 100644 skills/salesforce-web-app-creating-records/SKILL.md delete mode 100644 skills/salesforce-web-app-feature/SKILL.md delete mode 100644 skills/salesforce-web-app-list-and-create-records/SKILL.md delete mode 100644 skills/salesforce-web-application/SKILL.md create mode 100644 skills/using-webapp-graphql/SKILL.md create mode 100644 skills/using-webapp-graphql/shared-schema.graphqls diff --git a/.gitignore b/.gitignore index 78e46837..6bea64e4 100644 --- a/.gitignore +++ b/.gitignore @@ -219,6 +219,9 @@ target/ bin/ obj/ +# Sync script manifest (not created by script) +skills/.synced-template-skills.json + # Local configuration config.local.* *.local.json diff --git a/.template-versions.json b/.template-versions.json new file mode 100644 index 00000000..1ba6cf10 --- /dev/null +++ b/.template-versions.json @@ -0,0 +1,10 @@ +{ + "@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.107.0", + "@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.107.0", + "@salesforce/webapp-template-base-sfdx-project-experimental": "1.107.0", + "@salesforce/webapp-template-feature-react-file-upload-experimental": "1.107.0", + "@salesforce/webapp-template-feature-react-chart-experimental": "1.107.0", + "@salesforce/webapp-template-feature-react-agentforce-conversation-client-experimental": "1.107.0", + "@salesforce/webapp-template-feature-micro-frontend": "1.107.0", + "@salesforce/webapps-features-experimental": "1.107.0" +} diff --git a/package-lock.json b/package-lock.json index 0983dbe6..c393ea50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "1.1.0", "license": "CC-BY-NC-4.0", "devDependencies": { - "@salesforce/webapp-template-app-react-sample-b2e-experimental": "^1.107.0", - "@salesforce/webapp-template-app-react-sample-b2x-experimental": "^1.107.0", + "@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.109.1", + "@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.109.1", "tsx": "^4.21.0" } }, @@ -686,22 +686,22 @@ } }, "node_modules/@salesforce/sdk-core": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/sdk-core/-/sdk-core-1.107.0.tgz", - "integrity": "sha512-OV4NSD5WaDFSFVuiTtuDSxVAG5Z9qMFuUpgYfhmzdkK1GRFJERpa8LNqRqzI7GjrkR7z41yFB7ed0lRzYL8BAQ==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/sdk-core/-/sdk-core-1.109.1.tgz", + "integrity": "sha512-czBnRRuNXfVN/3SADZs9xA+iBAA+l6W6ZrNFw7nw7D1dOfzFKoPI4oUtCa4ahe2ToSwpFlGD+iyngq2HaYCh2Q==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt" }, "node_modules/@salesforce/sdk-data": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/sdk-data/-/sdk-data-1.107.0.tgz", - "integrity": "sha512-C8J6lBKGL1gJBhFqkwoT9o1CxalhKDMkgkCt4UqV/hmd07DLdaXIxoUmtolPdkClv0f8LjijLOlUFXk6SvYJ3w==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/sdk-data/-/sdk-data-1.109.1.tgz", + "integrity": "sha512-ep66OvR4Azi1+4as5+FeZFQB2ism1tX1bp6WQAElAJOu1UTqstsUFDGgdHaoXa2BwkER3kzuB/RWQzG7BueXsg==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@conduit-client/service-fetch-network": "3.17.0", "@conduit-client/utils": "3.17.0", - "@salesforce/sdk-core": "^1.107.0" + "@salesforce/sdk-core": "^1.109.1" } }, "node_modules/@salesforce/ts-types": { @@ -715,14 +715,14 @@ } }, "node_modules/@salesforce/webapp-experimental": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/webapp-experimental/-/webapp-experimental-1.107.0.tgz", - "integrity": "sha512-7TTUTyDQ2kjF7gIVNmcIDF1ZbDPWR1xRIMIpaxoENJPpjMMTFy1+SOsRm1KF28/Lh6wP1FbuI5qXvcwM5zZyNA==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/webapp-experimental/-/webapp-experimental-1.109.1.tgz", + "integrity": "sha512-x9o1xvep+zXOQZeGNo13M106yfXWWsYMfrBS8nGuH8NcWFQlss23vnvflVZnCKeG6Je/ZCZp/sHiOdSY4KGEhw==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@salesforce/core": "^8.23.4", - "@salesforce/sdk-data": "^1.107.0", + "@salesforce/sdk-data": "^1.109.1", "axios": "^1.7.7", "micromatch": "^4.0.8", "path-to-regexp": "^8.3.0" @@ -732,27 +732,27 @@ } }, "node_modules/@salesforce/webapp-template-app-react-sample-b2e-experimental": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2e-experimental/-/webapp-template-app-react-sample-b2e-experimental-1.107.0.tgz", - "integrity": "sha512-G34763Qan/uJx1rDeVi2ss3+KQSzA7ybypUpwog7gKFQ/EmtC//wkbo2/E4YzSFGhm4nZqjGeEADaTa+R3pZOA==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2e-experimental/-/webapp-template-app-react-sample-b2e-experimental-1.109.1.tgz", + "integrity": "sha512-ju77WQp0dsIDroito/xgZrLI7DuIQfo8NgMLB8qVgnhE7Tssma8KAtqDHjK9HsmL/HTPlMXZUEhEqh8nJNq5Sw==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "@salesforce/webapp-experimental": "^1.107.0", - "@salesforce/webapp-template-feature-react-global-search-experimental": "^1.107.0" + "@salesforce/webapp-experimental": "^1.109.1", + "@salesforce/webapp-template-feature-react-global-search-experimental": "^1.109.1" } }, "node_modules/@salesforce/webapp-template-app-react-sample-b2x-experimental": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2x-experimental/-/webapp-template-app-react-sample-b2x-experimental-1.107.0.tgz", - "integrity": "sha512-ZNwqUI/l8llEtLR/gDiZ5Ijci5wJvnF72GxzddBKiyk3sIzTPMSXT4JsLpX4mHrOi/mvTKV20kE2qR/0H/4l2A==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-app-react-sample-b2x-experimental/-/webapp-template-app-react-sample-b2x-experimental-1.109.1.tgz", + "integrity": "sha512-Pf4cC1fbDJ5o7gJlztDCArqimQN60+iCAxNpHHDsz8hHUBO2lteD5jK3I9WSPeoBDNm7VMOgLFIssWiGPvRlVg==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt" }, "node_modules/@salesforce/webapp-template-feature-react-global-search-experimental": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-feature-react-global-search-experimental/-/webapp-template-feature-react-global-search-experimental-1.107.0.tgz", - "integrity": "sha512-Kxo2+6hzie3JiDi/DPRqzQ4i2A6obYy1fJJS3mBn+P5EuAl3X8qH9yAY/B/haO6HU6PQHbvsqm/rnLkh3AJHXQ==", + "version": "1.109.1", + "resolved": "https://registry.npmjs.org/@salesforce/webapp-template-feature-react-global-search-experimental/-/webapp-template-feature-react-global-search-experimental-1.109.1.tgz", + "integrity": "sha512-w7qVrI3d7oMQKtUG982wgQpz+ZmlynxbYKIC3cJSnNVAAelOlRp77X1sq4tqu+nJ/HRsH7mx9blGXw6L903kvw==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt" }, diff --git a/package.json b/package.json index 2171f0df..1ae5e1a7 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "registry": "https://registry.npmjs.org" }, "devDependencies": { - "@salesforce/webapp-template-app-react-sample-b2e-experimental": "^1.107.0", - "@salesforce/webapp-template-app-react-sample-b2x-experimental": "^1.107.0", + "@salesforce/webapp-template-app-react-sample-b2e-experimental": "1.109.1", + "@salesforce/webapp-template-app-react-sample-b2x-experimental": "1.109.1", "tsx": "^4.21.0" }, "scripts": { "validate:skills": "tsx scripts/validate-skills.ts", "sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js", - "sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js" + "sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js", + "sync-webapp-skills": "node scripts/sync-webapp-skills.js" } } diff --git a/scripts/sync-webapp-skills.js b/scripts/sync-webapp-skills.js new file mode 100644 index 00000000..67334dad --- /dev/null +++ b/scripts/sync-webapp-skills.js @@ -0,0 +1,118 @@ +#!/usr/bin/env node +/** + * Sync webapp skills: pins b2e and b2x template packages to latest npm versions, + * runs npm install, then copies skills from dist/.a4drules/skills/ into skills/. + * Run from repo root. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { copyRecursive } = require('./lib/copy-recursive'); + +const TEMPLATE_PACKAGES = [ + '@salesforce/webapp-template-app-react-sample-b2e-experimental', + '@salesforce/webapp-template-app-react-sample-b2x-experimental', +]; +const PACKAGE_NAME = TEMPLATE_PACKAGES[0]; // used for syncing skills +const SKILLS_SRC = 'dist/.a4drules/skills'; + +const repoRoot = process.cwd(); +const pkgPath = path.join(repoRoot, 'package.json'); +const skillsDir = path.join(repoRoot, 'skills'); + +// ── Pin template packages to latest npm versions ──────────────────── +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); +let pkgChanged = false; +for (const name of TEMPLATE_PACKAGES) { + const current = (pkg.devDependencies || {})[name]; + if (!current || current.startsWith('file:')) continue; + let latest; + try { + latest = execSync(`npm view ${name} version`, { encoding: 'utf8' }).trim(); + } catch (_) { + console.warn(`Could not resolve ${name} on npm, using current version.`); + continue; + } + if (current !== latest) { + console.log(`${name}: ${current} -> ${latest}`); + pkg.devDependencies[name] = latest; + pkgChanged = true; + } +} +if (pkgChanged) { + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); +} + +// ── Install ────────────────────────────────────────────────────────── +console.log('Installing dependencies...'); +execSync('npm install', { cwd: repoRoot, stdio: 'inherit' }); + +// ── Sync skills ────────────────────────────────────────────────────── +const pkgRoot = path.join(repoRoot, 'node_modules', PACKAGE_NAME.replace('/', path.sep)); +if (!fs.existsSync(pkgRoot)) { + console.error(`Package not found at ${pkgRoot}.`); + process.exit(1); +} + +const srcDir = path.join(pkgRoot, SKILLS_SRC); +if (!fs.existsSync(srcDir)) { + console.error(`Skills not found at ${srcDir}.`); + process.exit(1); +} + +if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true }); + +function addWebappPrefix(name) { + const parts = name.split('-'); + if (parts.length < 2) return name; + if (parts[1] === 'webapp') return name; + return parts[0] + '-webapp-' + parts.slice(1).join('-'); +} + +/** Dirs in skills/ that look like synced webapp skills (e.g. *-webapp-*, creating-webapp). */ +function isSyncedWebappSkillDir(name) { + return name.includes('webapp'); +} + +/** Set front matter `name` in SKILL.md to match the destination folder name. */ +function setSkillFrontMatterName(skillDir, destName) { + const skillPath = path.join(skillDir, 'SKILL.md'); + if (!fs.existsSync(skillPath)) return; + let content = fs.readFileSync(skillPath, 'utf8'); + content = content.replace(/^name:\s*.+$/m, `name: ${destName}`); + fs.writeFileSync(skillPath, content, 'utf8'); +} + +// ── Clean up: remove skills no longer in the package ─────────────────── +const srcNames = fs.readdirSync(srcDir).filter((name) => + fs.statSync(path.join(srcDir, name)).isDirectory() +); +const currentDestNames = new Set(srcNames.map(addWebappPrefix)); + +for (const name of fs.readdirSync(skillsDir)) { + const dirPath = path.join(skillsDir, name); + if (!fs.statSync(dirPath).isDirectory()) continue; + if (!isSyncedWebappSkillDir(name)) continue; + if (currentDestNames.has(name)) continue; + fs.rmSync(dirPath, { recursive: true }); + console.log(`Removed skills/${name}/ (no longer in package)`); +} + +// ── Copy each skill from package ────────────────────────────────────── +const syncedDirs = []; +for (const srcName of srcNames) { + const src = path.join(srcDir, srcName); + const destName = addWebappPrefix(srcName); + const dest = path.join(skillsDir, destName); + if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true }); + copyRecursive(src, dest); + setSkillFrontMatterName(dest, destName); + syncedDirs.push(destName); + console.log(`Synced skills/${destName}/`); +} + +const version = JSON.parse( + fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8') +).version; +console.log(`Done — synced ${syncedDirs.length} skills from ${PACKAGE_NAME}@${version}.`); diff --git a/skills/accessing-webapp-data/SKILL.md b/skills/accessing-webapp-data/SKILL.md new file mode 100644 index 00000000..e8627450 --- /dev/null +++ b/skills/accessing-webapp-data/SKILL.md @@ -0,0 +1,178 @@ +--- +name: accessing-webapp-data +description: Salesforce data access patterns. Use when adding or modifying any code that fetches data from Salesforce (records, Chatter, Connect API, etc.). +paths: + - "**/*.ts" + - "**/*.tsx" + - "**/*.graphql" +--- + +# Salesforce Data Access + +Guidance for accessing Salesforce data from web apps. **All Salesforce data fetches MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK provides authentication, CSRF handling, and correct base URL resolution — direct `fetch` or `axios` calls bypass these and are not allowed. + +## Mandatory: Use the Data SDK + +> **Every Salesforce data fetch must go through the Data SDK.** Obtain it via `createDataSDK()`, then use `sdk.graphql?.()` or `sdk.fetch?.()`. Never call `fetch()` or `axios` directly for Salesforce endpoints. + +## Optional Chaining and Graceful Handling + +**Always use optional chaining** when calling `sdk.graphql` or `sdk.fetch` — these methods may be undefined in some surfaces (e.g., Salesforce ACC, MCP Apps). Handle the case where they are not available gracefully: + +```typescript +const sdk = await createDataSDK(); + +// ✅ Use optional chaining +const response = await sdk.graphql?.(query); + +// ✅ Check before using fetch +if (!sdk.fetch) { + throw new Error("Data SDK fetch is not available in this context"); +} +const res = await sdk.fetch(url); +``` + +For GraphQL, if `sdk.graphql` is undefined, the call returns `undefined` — handle that in your logic (e.g., throw a clear error or return a fallback). For `sdk.fetch`, check availability before calling when the operation is required. + +## Preference: GraphQL First + +**GraphQL is the preferred method** for querying and mutating Salesforce records. Use it when: + +- Querying records (Account, Contact, Opportunity, custom objects) +- Creating, updating, or deleting records (when GraphQL supports the operation) +- Fetching related data, filters, sorting, pagination + +**Use `sdk.fetch` only when GraphQL is not sufficient.** For REST API usage, invoke the `fetching-rest-api` skill, which documents: + +- Chatter API (e.g., `/services/data/v65.0/chatter/users/me`) +- Connect REST API (e.g., `/services/data/v65.0/connect/file/upload/config`) +- Apex REST (e.g., `/services/apexrest/auth/login`) +- UI API REST (e.g., `/services/data/v65.0/ui-api/records/{recordId}`) +- Einstein LLM Gateway + +--- + +## Getting the SDK + +```typescript +import { createDataSDK } from "@salesforce/sdk-data"; + +const sdk = await createDataSDK(); +``` + +--- + +## Example 1: GraphQL (Preferred) + +For record queries and mutations, use GraphQL via the Data SDK. Invoke the `using-graphql` skill for the full workflow (schema exploration, query authoring, codegen, lint validate). + +```typescript +import { createDataSDK, gql } from "@salesforce/sdk-data"; +import type { GetAccountsQuery } from "../graphql-operations-types"; + +const GET_ACCOUNTS = gql` + query GetAccounts { + uiapi { + query { + Account(first: 10) { + edges { + node { + Id + Name { value } + } + } + } + } + } + } +`; + +export async function getAccounts() { + const sdk = await createDataSDK(); + const response = await sdk.graphql?.(GET_ACCOUNTS); + + if (response?.errors?.length) { + throw new Error(response.errors.map((e) => e.message).join("; ")); + } + + return response?.data?.uiapi?.query?.Account?.edges?.map((e) => e?.node) ?? []; +} +``` + +--- + +## Example 2: Fetch (When GraphQL Is Not Sufficient) + +For REST endpoints that have no GraphQL equivalent, use `sdk.fetch`. **Invoke the `fetching-rest-api` skill** for full documentation of Chatter, Connect REST, Apex REST, UI API REST, and Einstein LLM endpoints. + +```typescript +import { createDataSDK } from "@salesforce/sdk-data"; + +declare const __SF_API_VERSION__: string; +const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0"; + +export async function getCurrentUser() { + const sdk = await createDataSDK(); + const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/chatter/users/me`); + + if (!response?.ok) throw new Error(`HTTP ${response?.status}`); + const data = await response.json(); + return { id: data.id, name: data.name }; +} +``` + +--- + +## Anti-Patterns (Forbidden) + +### Direct fetch to Salesforce + +```typescript +// ❌ FORBIDDEN — bypasses Data SDK auth and CSRF +const res = await fetch("/services/data/v65.0/chatter/users/me"); +``` + +### Direct axios to Salesforce + +```typescript +// ❌ FORBIDDEN — bypasses Data SDK +const res = await axios.get("/services/data/v65.0/chatter/users/me"); +``` + +### Correct approach + +```typescript +// ✅ CORRECT — use Data SDK +const sdk = await createDataSDK(); +const res = await sdk.fetch?.("/services/data/v65.0/chatter/users/me"); +``` + +--- + +## Clarifying Vague Data Requests + +When a user asks about data and the request is vague, **clarify before implementing**. Ask which of the following they want: + +- **Application code** — Add or modify code in a specific web app so the app performs the data interaction at runtime (e.g., GraphQL query in the React app) +- **Local SF CLI** — Run Salesforce CLI commands locally (e.g., `sf data query`, `sf data import tree`) to interact with the org from the terminal +- **Local example data** — Update or add local fixture/example data files (e.g., JSON in `data/`) for development or testing +- **Other** — Data export, report generation, setup script, etc. + +Do not assume. A request like "fetch accounts" could mean: (1) add a GraphQL query to the app, (2) run `sf data query` in the terminal, or (3) update sample data files. Confirm the intent before proceeding. + +--- + +## Decision Flow + +1. **Need to query or mutate Salesforce records?** → Use GraphQL via the Data SDK. Invoke the `using-graphql` skill. +2. **Need Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM?** → Use `sdk.fetch`. Invoke the `fetching-rest-api` skill. +3. **Never** use `fetch`, `axios`, or similar directly for Salesforce API calls. + +--- + +## Reference + +- GraphQL workflow: invoke the `using-graphql` skill (`.a4drules/skills/using-graphql/`) +- REST API via fetch: invoke the `fetching-rest-api` skill (`.a4drules/skills/fetching-rest-api/`) +- Data SDK package: `@salesforce/sdk-data` (`createDataSDK`, `gql`, `NodeOfConnection`) +- `createRecord` for UI API record creation: `@salesforce/webapp-experimental/api` (uses Data SDK internally) diff --git a/skills/building-webapp-data-visualization/SKILL.md b/skills/building-webapp-data-visualization/SKILL.md new file mode 100644 index 00000000..59978d10 --- /dev/null +++ b/skills/building-webapp-data-visualization/SKILL.md @@ -0,0 +1,72 @@ +--- +name: building-webapp-data-visualization +description: Adds data visualization components (charts, stat cards, KPI metrics) to React pages using Recharts. Use when the user asks to add a chart, graph, donut chart, pie chart, bar chart, stat card, KPI metric, dashboard visualization, or analytics component to the web application. +--- + +# Data Visualization + +## When to Use + +Use this skill when: +- Adding charts (donut, pie, bar, line, area) to a dashboard or analytics page +- Displaying KPI/metric stat cards with trend indicators +- Building a dashboard layout with mixed chart types and summary cards + +--- + +## Step 1 — Determine the visualization type + +Identify what the user needs: + +- **Donut / pie chart** — categorical breakdown (e.g. issue types, status distribution) +- **Bar chart** — comparison across categories or time periods +- **Line / area chart** — trends over time +- **Stat card** — single KPI metric with optional trend indicator +- **Combined dashboard** — stat cards + one or more charts + +If unclear, ask: + +> "What data should the chart display, and would a donut chart, bar chart, line chart, or stat cards work best?" + +--- + +## Step 2 — Install dependencies + +All chart types in this skill use **recharts**. Install once from the web app directory: + +```bash +npm install recharts +``` + +Recharts is built on D3 and provides declarative React components. No additional CSS is needed. + +--- + +## Step 3 — Choose implementation path + +Read the corresponding guide: + +- **Bar chart** — read `implementation/bar-line-chart.md` (categorical data) +- **Line / area chart** — read `implementation/bar-line-chart.md` (time-series data) +- **Donut / pie chart** — read `implementation/donut-chart.md` +- **Stat card with trend** — read `implementation/stat-card.md` +- **Dashboard layout** — read `implementation/dashboard-layout.md` + +--- + +## Verification + +Before completing: + +1. Chart renders with correct data and colors. +2. Chart is responsive (resizes with container). +3. Legend labels match the data categories. +4. Stat card trends display correct positive/negative indicators. +5. Run from the web app directory: + +```bash +cd force-app/main/default/webapplications/ && npm run lint && npm run build +``` + +- **Lint:** MUST result in 0 errors. +- **Build:** MUST succeed. diff --git a/skills/building-webapp-data-visualization/implementation/bar-line-chart.md b/skills/building-webapp-data-visualization/implementation/bar-line-chart.md new file mode 100644 index 00000000..c094c8b2 --- /dev/null +++ b/skills/building-webapp-data-visualization/implementation/bar-line-chart.md @@ -0,0 +1,316 @@ +# Bar & Line / Area Chart — Implementation Guide + +Requires **recharts** (install from the web app directory; see SKILL.md Step 2). + +--- + +## Data shapes + +### Time-series (line / area chart) + +Use when data represents a trend over time or ordered sequence. + +```ts +interface TimeSeriesDataPoint { + x: string; // date or label on the x-axis + y: number; // numeric value +} +``` + +Map raw fields to this shape: e.g. `date` → `x`, `revenue` → `y`. + +### Categorical (bar chart) + +Use when data compares discrete categories. + +```ts +interface CategoricalDataPoint { + name: string; // category label + value: number; // numeric value +} +``` + +Map raw fields to this shape: e.g. `product` → `name`, `sales` → `value`. + +### How to decide + +| Signal | Type | +|--------|------| +| "over time", "trend", date-like keys | Time-series → line chart | +| "by category", "by X", label-like keys | Categorical → bar chart | + +--- + +## Theme colors + +Pick a theme based on the data's sentiment: + +| Theme | Stroke / Fill | When to use | +|-------|---------------|-------------| +| `green` | `#22c55e` | Growth, gain, positive trend | +| `red` | `#ef4444` | Decline, loss, negative trend | +| `neutral` | `#6366f1` | Default or mixed data | + +Define colors as constants — do not use inline hex values. + +```ts +const THEME_COLORS = { + red: "#ef4444", + green: "#22c55e", + neutral: "#6366f1", +} as const; + +type ChartTheme = keyof typeof THEME_COLORS; +``` + +--- + +## Line chart component + +Create at `components/LineChart.tsx` (or colocate with the page): + +```tsx +import React from "react"; +import { + LineChart as RechartsLineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; + +const THEME_COLORS = { + red: "#ef4444", + green: "#22c55e", + neutral: "#6366f1", +} as const; + +type ChartTheme = keyof typeof THEME_COLORS; + +interface TimeSeriesDataPoint { + x: string; + y: number; +} + +interface TimeSeriesChartProps { + data: TimeSeriesDataPoint[]; + theme?: ChartTheme; + title?: string; + className?: string; +} + +export function TimeSeriesChart({ + data, + theme = "neutral", + title, + className = "", +}: TimeSeriesChartProps) { + if (data.length === 0) { + return

No data to display

; + } + + const color = THEME_COLORS[theme]; + + return ( +
+ {title && ( +

+ {title} +

+ )} + + + + + + + + + + +
+ ); +} +``` + +--- + +## Bar chart component + +Create at `components/BarChart.tsx` (or colocate with the page): + +```tsx +import React from "react"; +import { + BarChart as RechartsBarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; + +const THEME_COLORS = { + red: "#ef4444", + green: "#22c55e", + neutral: "#6366f1", +} as const; + +type ChartTheme = keyof typeof THEME_COLORS; + +interface CategoricalDataPoint { + name: string; + value: number; +} + +interface CategoricalChartProps { + data: CategoricalDataPoint[]; + theme?: ChartTheme; + title?: string; + className?: string; +} + +export function CategoricalChart({ + data, + theme = "neutral", + title, + className = "", +}: CategoricalChartProps) { + if (data.length === 0) { + return

No data to display

; + } + + const color = THEME_COLORS[theme]; + + return ( +
+ {title && ( +

+ {title} +

+ )} + + + + + + + + + + +
+ ); +} +``` + +--- + +## Area chart variant + +For a filled area chart (useful for volume-over-time), swap `Line` for `Area`: + +```tsx +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; + + + + + + + + + + +``` + +--- + +## Chart container wrapper + +Wrap any chart in a styled card for consistent spacing: + +```tsx +import { Card } from "@/components/ui/card"; + +interface ChartContainerProps { + children: React.ReactNode; + className?: string; +} + +export function ChartContainer({ children, className = "" }: ChartContainerProps) { + return ( + + {children} + + ); +} +``` + +Usage: + +```tsx + + + +``` + +--- + +## Preparing raw data + +Map API responses to the expected shape before passing to the chart: + +```tsx +const timeSeriesData = useMemo( + () => apiRecords.map((r) => ({ x: r.date, y: r.revenue })), + [apiRecords], +); + +const categoricalData = useMemo( + () => apiRecords.map((r) => ({ name: r.product, value: r.sales })), + [apiRecords], +); +``` + +--- + +## Key Recharts concepts + +| Component | Purpose | +|-----------|---------| +| `ResponsiveContainer` | Wraps chart to fill parent width | +| `CartesianGrid` | Background grid lines | +| `XAxis` / `YAxis` | Axis labels; `dataKey` maps to the data field | +| `Tooltip` | Hover info | +| `Legend` | Series labels | +| `Line` | Line series; `type="monotone"` for smooth curves | +| `Bar` | Bar series; `radius` rounds top corners | +| `Area` | Filled area; `fillOpacity` controls transparency | + +--- + +## Accessibility + +- Always include a text legend (not just colors). +- Chart should be wrapped in a section with a visible heading. +- For critical data, provide a text summary or table alternative. +- Use sufficient color contrast between the chart stroke/fill and background. +- Consider `prefers-reduced-motion` for chart animations. + +--- + +## Common mistakes + +| Mistake | Fix | +|---------|-----| +| Missing `ResponsiveContainer` | Chart won't resize; always wrap | +| Fixed width/height on chart | Let `ResponsiveContainer` control sizing | +| No empty-data handling | Show "No data" message when `data.length === 0` | +| Inline colors | Extract to `THEME_COLORS` constant | +| Using raw Recharts for every chart type | Use `DonutChart` (see `donut-chart.md`) for pie/donut | diff --git a/skills/building-webapp-data-visualization/implementation/dashboard-layout.md b/skills/building-webapp-data-visualization/implementation/dashboard-layout.md new file mode 100644 index 00000000..c701012c --- /dev/null +++ b/skills/building-webapp-data-visualization/implementation/dashboard-layout.md @@ -0,0 +1,189 @@ +# Dashboard Layout — Implementation Guide + +## Anatomy of a dashboard page + +A typical dashboard combines stat cards, charts, and data tables: + +``` +┌────────────────────────────────────────────────────┐ +│ Search / global action bar │ +├──────────┬──────────┬──────────────────────────────┤ +│ Stat 1 │ Stat 2 │ Stat 3 │ +├──────────┴──────────┴──────┬───────────────────────┤ +│ │ │ +│ Data table / list │ Donut chart │ +│ (70% width) │ (30% width) │ +│ │ │ +└────────────────────────────┴───────────────────────┘ +``` + +--- + +## Layout implementation + +```tsx +import { PageContainer } from "@/components/layout/PageContainer"; +import { StatCard } from "@/components/StatCard"; +import { DonutChart } from "@/components/DonutChart"; + +export default function Dashboard() { + return ( + +
+ {/* Search bar */} +
{/* global search component */}
+ + {/* Main content: 70/30 split */} +
+
+ {/* Stat cards row */} +
+ + + +
+ + {/* Data table */} +
{/* table component */}
+
+ + {/* Sidebar chart */} +
+ +
+
+
+
+ ); +} +``` + +--- + +## Responsive behavior + +| Breakpoint | Layout | +|------------|--------| +| Mobile (`< 768px`) | Single column, everything stacked | +| Tablet (`md`) | Stat cards in 3-col grid, rest stacked | +| Desktop (`lg`) | 70/30 split for table + chart | + +Key Tailwind classes: + +``` +grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6 +grid grid-cols-1 md:grid-cols-3 gap-6 +``` + +--- + +## Loading state + +Show a full-page loading state while dashboard data is being fetched: + +```tsx +if (loading) { + return ( + +
+

Loading dashboard…

+
+
+ ); +} +``` + +Or use a skeleton layout: + +```tsx +if (loading) { + return ( + +
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+
+
+
+ + ); +} +``` + +--- + +## Data fetching pattern + +Use `useEffect` with cancellation for dashboard metrics: + +```ts +const [metrics, setMetrics] = useState(null); +const [loading, setLoading] = useState(true); + +useEffect(() => { + let cancelled = false; + (async () => { + try { + setLoading(true); + const data = await fetchDashboardMetrics(); + if (!cancelled) setMetrics(data); + } catch (error) { + if (!cancelled) console.error("Error loading metrics:", error); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; }; +}, []); +``` + +--- + +## Combining multiple data sources + +Dashboards often aggregate data from several APIs. Load them in parallel: + +```ts +const [metrics, setMetrics] = useState(null); +const [requests, setRequests] = useState([]); +const [loading, setLoading] = useState(true); + +useEffect(() => { + let cancelled = false; + Promise.all([fetchMetrics(), fetchRecentRequests()]) + .then(([metricsData, requestsData]) => { + if (!cancelled) { + setMetrics(metricsData); + setRequests(requestsData); + } + }) + .catch((err) => { + if (!cancelled) console.error(err); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { cancelled = true; }; +}, []); +``` + +--- + +## PageContainer wrapper + +A simple wrapper for consistent page padding: + +```tsx +interface PageContainerProps { + children: React.ReactNode; +} + +export function PageContainer({ children }: PageContainerProps) { + return
{children}
; +} +``` diff --git a/skills/building-webapp-data-visualization/implementation/donut-chart.md b/skills/building-webapp-data-visualization/implementation/donut-chart.md new file mode 100644 index 00000000..1f20bdc5 --- /dev/null +++ b/skills/building-webapp-data-visualization/implementation/donut-chart.md @@ -0,0 +1,181 @@ +# Donut / Pie Chart — Implementation Guide + +Requires **recharts** (install from the web app directory; see SKILL.md Step 2). + +--- + +## Data structure + +Charts expect an array of objects with `name`, `value`, and `color`: + +```ts +interface ChartData { + name: string; + value: number; + color: string; +} +``` + +--- + +## Donut chart component + +Create at `components/DonutChart.tsx`: + +```tsx +import React from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { Card } from "@/components/ui/card"; + +interface ChartData { + name: string; + value: number; + color: string; +} + +interface DonutChartProps { + title: string; + data: ChartData[]; +} + +export const DonutChart: React.FC = ({ title, data }) => { + const total = data.reduce((sum, item) => sum + item.value, 0); + const mainPercentage = total > 0 ? Math.round((data[0]?.value / total) * 100) : 0; + + return ( + +

+ {title} +

+ +
+ + + + {data.map((entry, index) => ( + + ))} + + + + + {/* Center label */} +
+
+
{mainPercentage}%
+
+
+
+ + {/* Legend */} +
+ {data.map((item, index) => ( +
+
+ {item.name} +
+ ))} +
+ + ); +}; +``` + +--- + +## Key Recharts concepts + +| Component | Purpose | +|-----------|---------| +| `ResponsiveContainer` | Wraps chart to make it fill its parent's width | +| `PieChart` | Chart container for pie/donut | +| `Pie` | The data ring; `innerRadius` > 0 makes it a donut | +| `Cell` | Individual segment; accepts `fill` color | +| `paddingAngle` | Gap between segments (degrees) | + +### Donut vs Pie + +| Property | Donut | Pie | +|----------|-------|-----| +| `innerRadius` | `> 0` (e.g. `70`) | `0` | +| Center label | Yes, positioned absolutely | Not typical | + +--- + +## Preparing chart data from raw records + +Transform API data into the `ChartData[]` format before passing to the chart: + +```tsx +const CATEGORIES = ["Plumbing", "HVAC", "Electrical"] as const; +const OTHER_LABEL = "Other"; +const COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"]; + +const chartData = useMemo(() => { + const counts: Record = {}; + CATEGORIES.forEach((c) => (counts[c] = 0)); + counts[OTHER_LABEL] = 0; + + records.forEach((record) => { + const type = record.category; + if (CATEGORIES.includes(type as (typeof CATEGORIES)[number])) { + counts[type]++; + } else { + counts[OTHER_LABEL]++; + } + }); + + return [ + ...CATEGORIES.map((name, i) => ({ name, value: counts[name], color: COLORS[i] })), + { name: OTHER_LABEL, value: counts[OTHER_LABEL], color: COLORS[CATEGORIES.length] }, + ]; +}, [records]); +``` + +--- + +## Color palette recommendations + +| Use case | Colors | +|----------|--------| +| Categorical (4 items) | `#7C3AED` `#EC4899` `#14B8A6` `#06B6D4` | +| Status (3 items) | `#22C55E` `#F59E0B` `#EF4444` (green/amber/red) | +| Sequential | Use opacity variants of one hue: `#7C3AED` at 100%, 75%, 50%, 25% | + +Keep chart colors consistent with the app's design system. Define them as constants, not inline values. + +--- + +## Other chart types + +For **bar charts** and **line / area charts**, see `bar-line-chart.md` in this directory. + +--- + +## Accessibility + +- Always include a text legend (not just colors). +- Chart should be wrapped in a section with a visible heading. +- For critical data, provide a text summary or table alternative. +- Use sufficient color contrast between segments. +- Consider `prefers-reduced-motion` for chart animations. + +--- + +## Common mistakes + +| Mistake | Fix | +|---------|-----| +| Missing `ResponsiveContainer` | Chart won't resize; always wrap in `ResponsiveContainer` | +| Fixed width/height on `PieChart` | Let `ResponsiveContainer` control sizing | +| No legend | Add a grid legend below the chart | +| Inline colors | Extract to constants for consistency | +| No fallback for empty data | Show "No data" message when `data` is empty | diff --git a/skills/building-webapp-data-visualization/implementation/stat-card.md b/skills/building-webapp-data-visualization/implementation/stat-card.md new file mode 100644 index 00000000..11720ed3 --- /dev/null +++ b/skills/building-webapp-data-visualization/implementation/stat-card.md @@ -0,0 +1,150 @@ +# Stat Card — Implementation Guide + +## What is a stat card + +A stat card displays a single KPI metric with an optional trend indicator. Used on dashboards to show at-a-glance numbers like "Total Properties: 42 (+10%)". + +--- + +## Component interface + +```ts +interface StatCardProps { + title: string; + value: number | string; + trend?: { + value: number; + isPositive: boolean; + }; + subtitle?: string; + onClick?: () => void; +} +``` + +--- + +## StatCard component + +Create at `components/StatCard.tsx`: + +```tsx +import React from "react"; +import { Card } from "@/components/ui/card"; +import { TrendingUp, TrendingDown } from "lucide-react"; + +interface StatCardProps { + title: string; + value: number | string; + trend?: { + value: number; + isPositive: boolean; + }; + subtitle?: string; + onClick?: () => void; +} + +export const StatCard: React.FC = ({ title, value, trend, subtitle, onClick }) => { + return ( + +
+

{title}

+
+

{value}

+ {trend && ( + + {trend.isPositive ? ( + + ) : ( + + )} + {Math.abs(trend.value)}% + + )} +
+ {subtitle &&

{subtitle}

} +
+
+ ); +}; +``` + +This version uses Lucide icons (`TrendingUp`/`TrendingDown`) instead of custom SVGs for portability across projects. + +--- + +## Layout: stat card grid + +Display stat cards in a responsive grid: + +```tsx +
+ + + +
+``` + +--- + +## Computing trend values + +Calculate trends from current vs previous period: + +```ts +const trends = useMemo(() => { + const previousTotal = metrics.totalProperties - Math.round(metrics.totalProperties * 0.1); + const trendPercent = previousTotal > 0 + ? Math.round(((metrics.totalProperties - previousTotal) / previousTotal) * 100) + : 0; + + return { + value: Math.abs(trendPercent), + isPositive: trendPercent >= 0, + }; +}, [metrics]); +``` + +--- + +## Trend badge color conventions + +| Trend | Background | Text | Meaning | +|-------|------------|------|---------| +| Positive (up) | `bg-emerald-100` | `text-emerald-800` | Growth, improvement | +| Negative (down) | `bg-pink-100` | `text-pink-800` | Decline, concern | +| Neutral | `bg-gray-100` | `text-gray-600` | No change | + +--- + +## Accessibility + +- Card uses `cursor-pointer` and `hover:shadow-lg` only when `onClick` is provided. +- Trend icons have implicit meaning from color + direction icon. +- Stat values use large, bold text for visibility. +- Title uses `uppercase tracking-wide` for visual hierarchy without heading tags (appropriate in a card grid). diff --git a/skills/building-webapp-react-components/SKILL.md b/skills/building-webapp-react-components/SKILL.md new file mode 100644 index 00000000..8448424f --- /dev/null +++ b/skills/building-webapp-react-components/SKILL.md @@ -0,0 +1,96 @@ +--- +name: building-webapp-react-components +description: Use when editing any React code in the web application — creating or modifying components, pages, layout, headers, footers, or any TSX/JSX files. Follow this skill for add component, add page, header/footer, and general React UI implementation patterns (shadcn UI and Tailwind CSS). +--- + +# React Web App (Components, Pages, Layout) + +Use this skill whenever you are editing React/TSX code in the web app (creating or modifying components, pages, header/footer, or layout). + +## Step 1 — Identify the type of component + +Determine which of these three categories the request falls into, then follow the corresponding section below: + +- **Page** — user wants a new routed page (e.g. "add a contacts page", "create a dashboard page", "add a settings section") +- **Header / Footer** — user wants a site-wide header, footer, nav bar, or page footer that appears on every page +- **Component** — everything else: a widget, card, table, form, dialog, or other UI element placed within an existing page + +If it is not immediately clear from the user's message, ask: + +> "Are you looking to add a new page, a site-wide header or footer, or a component within an existing page?" + +Then follow the matching section. + +--- + +## Clarifying Questions + +Ask **one question at a time** and wait for the response before asking the next. Stop when you have enough to build accurately — do not guess or assume. + +### For a Page + +1. **What is the name and purpose of the page?** (e.g., Contacts, Dashboard, Settings) +2. **What URL path should it use?** (e.g., `/contacts`, `/dashboard`) — or derive from the page name? +3. **Should the page appear in the navigation menu?** +4. **Who can access it?** Public, authenticated users only (`PrivateRoute`), or unauthenticated only (e.g., login — `AuthenticationRoute`)? +5. **What content or sections should the page include?** (list, form, table, detail view, etc.) +6. **Does it need to fetch any data?** If so, from where? + +### For a Header / Footer + +1. **Header, footer, or both?** +2. **What should the header contain?** (logo/app name, nav links, user avatar, CTA button, etc.) +3. **What should the footer contain?** (copyright text, links, social icons, etc.) +4. **Should the header be sticky (fixed to top while scrolling)?** +5. **Is there a logo or brand name to display?** (or placeholder?) +6. **Any specific color scheme or style direction?** (dark background, branded primary color, minimal, etc.) +7. **Should navigation links appear in the header?** If so, which pages? + +### For a Component + +1. **What should the component do?** (display data, accept input, trigger an action, etc.) +2. **What page or location should it appear on?** +3. **Is this shared/reusable across pages, or specific to one feature?** (determines file location) +4. **What data or props does it need?** (static content, props, fetched data) +5. **Does it need internal state?** (loading, toggle, form state, etc.) +6. **Are there any specific shadcn components to use?** (Card, Table, Dialog, Form, etc.) +7. **Should it appear in a specific layout position?** (full-width, sidebar, inline, etc.) + +--- + +## Implementation + +Once you have identified the type and gathered answers to the clarifying questions, read and follow the corresponding implementation guide: + +- **Page** — read `implementation/page.md` and follow the instructions there. +- **Header / Footer** — read `implementation/header-footer.md` and follow the instructions there. +- **Component** — read `implementation/component.md` and follow the instructions there. + +--- + +## TypeScript Standards + +- **Never use `any`** — use proper types, generics, or `unknown` with type guards. +- **Event handlers:** `(event: React.FormEvent): void` +- **State:** `useState(null)` — always provide the type parameter. +- **No unsafe assertions** (`obj as User`). Use type guards: + ```typescript + function isUser(obj: unknown): obj is User { + return typeof obj === 'object' && obj !== null && typeof (obj as User).id === 'string'; + } + ``` + +--- + +## Verification (MANDATORY) + +Before completing, run from the web app directory `force-app/main/default/webapplications//`: + +```bash +cd force-app/main/default/webapplications/ && npm run lint && npm run build +``` + +- **Lint:** MUST result in 0 errors. +- **Build:** MUST succeed (includes TypeScript check). + +If either fails, fix the errors and re-run. Do not leave the session with failing quality gates. diff --git a/skills/building-webapp-react-components/implementation/component.md b/skills/building-webapp-react-components/implementation/component.md new file mode 100644 index 00000000..b3f3b4bb --- /dev/null +++ b/skills/building-webapp-react-components/implementation/component.md @@ -0,0 +1,78 @@ +# Implementation — Component + +### Rules + +1. **Always use shadcn components** from `@/components/ui` — never build raw HTML equivalents for buttons, inputs, cards, alerts, tabs, tables, or labels. +2. **All styling via Tailwind** — utility classes only. No inline `style={{}}`, CSS Modules, or other styling systems. +3. **Use design tokens** — prefer `bg-background`, `text-foreground`, `text-muted-foreground`, `border`, `bg-primary`, `text-destructive`, `rounded-lg` over hardcoded colors. +4. **Use `cn()`** from `@/lib/utils` for conditional or composable class names. +5. **TypeScript** — functional components with typed props interface; always accept `className?: string`. + +### File Location — Component + +| Component type | Location | Export | +| ---------------------------------------------- | ---------------------------------------- | ---------------------------------------- | +| Shared UI primitive (reusable across features) | `src/components/ui/` — add to `index.ts` | Named export | +| Feature-specific (e.g., dashboard widget) | `src/components//` | Named export, import directly where used | +| Page-level layout element | `src/components/layout/` | Named export | + +### Component Structure + +```tsx +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui"; +import { cn } from "@/lib/utils"; + +interface MyComponentProps { + title: string; + value: string; + className?: string; +} + +export function MyComponent({ title, value, className }: MyComponentProps) { + return ( + + + {title} + + +

{value}

+
+
+ ); +} +``` + +### State and Hooks + +- **Local state only:** keep `useState`, `useReducer`, `useRef` inside the component. +- **Shared or complex state:** extract to a custom hook in `src/hooks/` (prefix with `use`, e.g. `useFormData`). Do this when more than one component needs the state, or when multiple hooks are composed together. + +### Adding the Component to a Page + +```tsx +// In the target page file, e.g. src/pages/HomePage.tsx +import { MyComponent } from "@/components//MyComponent"; + +export default function HomePage() { + return ( +
+ +
+ ); +} +``` + +### Useful Patterns — Component + +- **Programmatic navigation:** use `useNavigate` from `react-router`; call `navigate(path)` — consistent with GlobalSearchInput, SearchResultCard, MaintenanceTable, and other components in the web application. +- **Page container:** `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12` +- **Icons:** `lucide-react`; add `aria-hidden="true"` on decorative icons +- **Focus styles:** use `focus-visible:` variants +- **Multiple visual variants:** use CVA (`cva`) and `VariantProps` +- **shadcn import barrel:** `import { Button, Card, Input } from "@/components/ui"` + +### Confirm — Component + +- Imports use path aliases (`@/`, not deep relative paths) +- No raw `