@salesforce/afv-skills 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -415
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +399 -0
- package/skills/generating-apex/assets/abstract.cls +132 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +199 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
- package/skills/generating-apex-test/references/async-testing.md +193 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
- package/skills/generating-apex-test/references/test-data-factory.md +75 -0
- package/skills/generating-experience-react-site/SKILL.md +20 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +58 -60
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +342 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -140
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
# Dashboard Layout — Implementation Guide
|
|
2
|
-
|
|
3
|
-
## Anatomy of a dashboard page
|
|
4
|
-
|
|
5
|
-
A typical dashboard combines stat cards, charts, and data tables:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
┌────────────────────────────────────────────────────┐
|
|
9
|
-
│ Search / global action bar │
|
|
10
|
-
├──────────┬──────────┬──────────────────────────────┤
|
|
11
|
-
│ Stat 1 │ Stat 2 │ Stat 3 │
|
|
12
|
-
├──────────┴──────────┴──────┬───────────────────────┤
|
|
13
|
-
│ │ │
|
|
14
|
-
│ Data table / list │ Donut chart │
|
|
15
|
-
│ (70% width) │ (30% width) │
|
|
16
|
-
│ │ │
|
|
17
|
-
└────────────────────────────┴───────────────────────┘
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Layout implementation
|
|
23
|
-
|
|
24
|
-
```tsx
|
|
25
|
-
import { PageContainer } from "@/components/layout/PageContainer";
|
|
26
|
-
import { StatCard } from "@/components/StatCard";
|
|
27
|
-
import { DonutChart } from "@/components/DonutChart";
|
|
28
|
-
|
|
29
|
-
export default function Dashboard() {
|
|
30
|
-
return (
|
|
31
|
-
<PageContainer>
|
|
32
|
-
<div className="max-w-7xl mx-auto space-y-6">
|
|
33
|
-
{/* Search bar */}
|
|
34
|
-
<div>{/* global search component */}</div>
|
|
35
|
-
|
|
36
|
-
{/* Main content: 70/30 split */}
|
|
37
|
-
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
|
38
|
-
<div className="space-y-6">
|
|
39
|
-
{/* Stat cards row */}
|
|
40
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
41
|
-
<StatCard title="Metric A" value={42} />
|
|
42
|
-
<StatCard title="Metric B" value={18} />
|
|
43
|
-
<StatCard title="Metric C" value={7} />
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
{/* Data table */}
|
|
47
|
-
<div>{/* table component */}</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
{/* Sidebar chart */}
|
|
51
|
-
<div>
|
|
52
|
-
<DonutChart title="Distribution" data={chartData} />
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</PageContainer>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Responsive behavior
|
|
64
|
-
|
|
65
|
-
| Breakpoint | Layout |
|
|
66
|
-
|------------|--------|
|
|
67
|
-
| Mobile (`< 768px`) | Single column, everything stacked |
|
|
68
|
-
| Tablet (`md`) | Stat cards in 3-col grid, rest stacked |
|
|
69
|
-
| Desktop (`lg`) | 70/30 split for table + chart |
|
|
70
|
-
|
|
71
|
-
Key Tailwind classes:
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6
|
|
75
|
-
grid grid-cols-1 md:grid-cols-3 gap-6
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Loading state
|
|
81
|
-
|
|
82
|
-
Show a full-page loading state while dashboard data is being fetched:
|
|
83
|
-
|
|
84
|
-
```tsx
|
|
85
|
-
if (loading) {
|
|
86
|
-
return (
|
|
87
|
-
<PageContainer>
|
|
88
|
-
<div className="flex items-center justify-center min-h-[400px]">
|
|
89
|
-
<p className="text-muted-foreground">Loading dashboard…</p>
|
|
90
|
-
</div>
|
|
91
|
-
</PageContainer>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Or use a skeleton layout:
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
if (loading) {
|
|
100
|
-
return (
|
|
101
|
-
<PageContainer>
|
|
102
|
-
<div className="max-w-7xl mx-auto space-y-6">
|
|
103
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
104
|
-
{[1, 2, 3].map((i) => (
|
|
105
|
-
<div key={i} className="h-28 animate-pulse rounded-xl bg-muted" />
|
|
106
|
-
))}
|
|
107
|
-
</div>
|
|
108
|
-
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
|
|
109
|
-
<div className="h-64 animate-pulse rounded-xl bg-muted" />
|
|
110
|
-
<div className="h-64 animate-pulse rounded-xl bg-muted" />
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</PageContainer>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Data fetching pattern
|
|
121
|
-
|
|
122
|
-
Use `useEffect` with cancellation for dashboard metrics:
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
|
126
|
-
const [loading, setLoading] = useState(true);
|
|
127
|
-
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
let cancelled = false;
|
|
130
|
-
(async () => {
|
|
131
|
-
try {
|
|
132
|
-
setLoading(true);
|
|
133
|
-
const data = await fetchDashboardMetrics();
|
|
134
|
-
if (!cancelled) setMetrics(data);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
if (!cancelled) console.error("Error loading metrics:", error);
|
|
137
|
-
} finally {
|
|
138
|
-
if (!cancelled) setLoading(false);
|
|
139
|
-
}
|
|
140
|
-
})();
|
|
141
|
-
return () => { cancelled = true; };
|
|
142
|
-
}, []);
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Combining multiple data sources
|
|
148
|
-
|
|
149
|
-
Dashboards often aggregate data from several APIs. Load them in parallel:
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
|
153
|
-
const [requests, setRequests] = useState<Request[]>([]);
|
|
154
|
-
const [loading, setLoading] = useState(true);
|
|
155
|
-
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
let cancelled = false;
|
|
158
|
-
Promise.all([fetchMetrics(), fetchRecentRequests()])
|
|
159
|
-
.then(([metricsData, requestsData]) => {
|
|
160
|
-
if (!cancelled) {
|
|
161
|
-
setMetrics(metricsData);
|
|
162
|
-
setRequests(requestsData);
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
.catch((err) => {
|
|
166
|
-
if (!cancelled) console.error(err);
|
|
167
|
-
})
|
|
168
|
-
.finally(() => {
|
|
169
|
-
if (!cancelled) setLoading(false);
|
|
170
|
-
});
|
|
171
|
-
return () => { cancelled = true; };
|
|
172
|
-
}, []);
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## PageContainer wrapper
|
|
178
|
-
|
|
179
|
-
A simple wrapper for consistent page padding:
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
interface PageContainerProps {
|
|
183
|
-
children: React.ReactNode;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function PageContainer({ children }: PageContainerProps) {
|
|
187
|
-
return <div className="p-6">{children}</div>;
|
|
188
|
-
}
|
|
189
|
-
```
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
# Donut / Pie Chart — Implementation Guide
|
|
2
|
-
|
|
3
|
-
Requires **recharts** (install from the web app directory; see SKILL.md Step 2).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Data structure
|
|
8
|
-
|
|
9
|
-
Charts expect an array of objects with `name`, `value`, and `color`:
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
interface ChartData {
|
|
13
|
-
name: string;
|
|
14
|
-
value: number;
|
|
15
|
-
color: string;
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Donut chart component
|
|
22
|
-
|
|
23
|
-
Create at `components/DonutChart.tsx`:
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
import React from "react";
|
|
27
|
-
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
|
28
|
-
import { Card } from "@/components/ui/card";
|
|
29
|
-
|
|
30
|
-
interface ChartData {
|
|
31
|
-
name: string;
|
|
32
|
-
value: number;
|
|
33
|
-
color: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DonutChartProps {
|
|
37
|
-
title: string;
|
|
38
|
-
data: ChartData[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const DonutChart: React.FC<DonutChartProps> = ({ title, data }) => {
|
|
42
|
-
const total = data.reduce((sum, item) => sum + item.value, 0);
|
|
43
|
-
const mainPercentage = total > 0 ? Math.round((data[0]?.value / total) * 100) : 0;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Card className="p-4 border-gray-200 shadow-sm flex flex-col">
|
|
47
|
-
<h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
|
|
48
|
-
{title}
|
|
49
|
-
</h3>
|
|
50
|
-
|
|
51
|
-
<div className="relative flex items-center justify-center">
|
|
52
|
-
<ResponsiveContainer width="100%" height={300}>
|
|
53
|
-
<PieChart>
|
|
54
|
-
<Pie
|
|
55
|
-
data={data}
|
|
56
|
-
cx="50%"
|
|
57
|
-
cy="50%"
|
|
58
|
-
innerRadius={70}
|
|
59
|
-
outerRadius={110}
|
|
60
|
-
paddingAngle={2}
|
|
61
|
-
dataKey="value"
|
|
62
|
-
>
|
|
63
|
-
{data.map((entry, index) => (
|
|
64
|
-
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
65
|
-
))}
|
|
66
|
-
</Pie>
|
|
67
|
-
</PieChart>
|
|
68
|
-
</ResponsiveContainer>
|
|
69
|
-
|
|
70
|
-
{/* Center label */}
|
|
71
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
72
|
-
<div className="text-center">
|
|
73
|
-
<div className="text-5xl font-bold text-primary">{mainPercentage}%</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
{/* Legend */}
|
|
79
|
-
<div className="mt-6 grid grid-cols-2 gap-3">
|
|
80
|
-
{data.map((item, index) => (
|
|
81
|
-
<div key={index} className="flex items-center gap-2">
|
|
82
|
-
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.color }} />
|
|
83
|
-
<span className="text-sm text-gray-700">{item.name}</span>
|
|
84
|
-
</div>
|
|
85
|
-
))}
|
|
86
|
-
</div>
|
|
87
|
-
</Card>
|
|
88
|
-
);
|
|
89
|
-
};
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Key Recharts concepts
|
|
95
|
-
|
|
96
|
-
| Component | Purpose |
|
|
97
|
-
|-----------|---------|
|
|
98
|
-
| `ResponsiveContainer` | Wraps chart to make it fill its parent's width |
|
|
99
|
-
| `PieChart` | Chart container for pie/donut |
|
|
100
|
-
| `Pie` | The data ring; `innerRadius` > 0 makes it a donut |
|
|
101
|
-
| `Cell` | Individual segment; accepts `fill` color |
|
|
102
|
-
| `paddingAngle` | Gap between segments (degrees) |
|
|
103
|
-
|
|
104
|
-
### Donut vs Pie
|
|
105
|
-
|
|
106
|
-
| Property | Donut | Pie |
|
|
107
|
-
|----------|-------|-----|
|
|
108
|
-
| `innerRadius` | `> 0` (e.g. `70`) | `0` |
|
|
109
|
-
| Center label | Yes, positioned absolutely | Not typical |
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## Preparing chart data from raw records
|
|
114
|
-
|
|
115
|
-
Transform API data into the `ChartData[]` format before passing to the chart:
|
|
116
|
-
|
|
117
|
-
```tsx
|
|
118
|
-
const CATEGORIES = ["Plumbing", "HVAC", "Electrical"] as const;
|
|
119
|
-
const OTHER_LABEL = "Other";
|
|
120
|
-
const COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"];
|
|
121
|
-
|
|
122
|
-
const chartData = useMemo(() => {
|
|
123
|
-
const counts: Record<string, number> = {};
|
|
124
|
-
CATEGORIES.forEach((c) => (counts[c] = 0));
|
|
125
|
-
counts[OTHER_LABEL] = 0;
|
|
126
|
-
|
|
127
|
-
records.forEach((record) => {
|
|
128
|
-
const type = record.category;
|
|
129
|
-
if (CATEGORIES.includes(type as (typeof CATEGORIES)[number])) {
|
|
130
|
-
counts[type]++;
|
|
131
|
-
} else {
|
|
132
|
-
counts[OTHER_LABEL]++;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
return [
|
|
137
|
-
...CATEGORIES.map((name, i) => ({ name, value: counts[name], color: COLORS[i] })),
|
|
138
|
-
{ name: OTHER_LABEL, value: counts[OTHER_LABEL], color: COLORS[CATEGORIES.length] },
|
|
139
|
-
];
|
|
140
|
-
}, [records]);
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## Color palette recommendations
|
|
146
|
-
|
|
147
|
-
| Use case | Colors |
|
|
148
|
-
|----------|--------|
|
|
149
|
-
| Categorical (4 items) | `#7C3AED` `#EC4899` `#14B8A6` `#06B6D4` |
|
|
150
|
-
| Status (3 items) | `#22C55E` `#F59E0B` `#EF4444` (green/amber/red) |
|
|
151
|
-
| Sequential | Use opacity variants of one hue: `#7C3AED` at 100%, 75%, 50%, 25% |
|
|
152
|
-
|
|
153
|
-
Keep chart colors consistent with the app's design system. Define them as constants, not inline values.
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## Other chart types
|
|
158
|
-
|
|
159
|
-
For **bar charts** and **line / area charts**, see `bar-line-chart.md` in this directory.
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Accessibility
|
|
164
|
-
|
|
165
|
-
- Always include a text legend (not just colors).
|
|
166
|
-
- Chart should be wrapped in a section with a visible heading.
|
|
167
|
-
- For critical data, provide a text summary or table alternative.
|
|
168
|
-
- Use sufficient color contrast between segments.
|
|
169
|
-
- Consider `prefers-reduced-motion` for chart animations.
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## Common mistakes
|
|
174
|
-
|
|
175
|
-
| Mistake | Fix |
|
|
176
|
-
|---------|-----|
|
|
177
|
-
| Missing `ResponsiveContainer` | Chart won't resize; always wrap in `ResponsiveContainer` |
|
|
178
|
-
| Fixed width/height on `PieChart` | Let `ResponsiveContainer` control sizing |
|
|
179
|
-
| No legend | Add a grid legend below the chart |
|
|
180
|
-
| Inline colors | Extract to constants for consistency |
|
|
181
|
-
| No fallback for empty data | Show "No data" message when `data` is empty |
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# Stat Card — Implementation Guide
|
|
2
|
-
|
|
3
|
-
## What is a stat card
|
|
4
|
-
|
|
5
|
-
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%)".
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Component interface
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
interface StatCardProps {
|
|
13
|
-
title: string;
|
|
14
|
-
value: number | string;
|
|
15
|
-
trend?: {
|
|
16
|
-
value: number;
|
|
17
|
-
isPositive: boolean;
|
|
18
|
-
};
|
|
19
|
-
subtitle?: string;
|
|
20
|
-
onClick?: () => void;
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## StatCard component
|
|
27
|
-
|
|
28
|
-
Create at `components/StatCard.tsx`:
|
|
29
|
-
|
|
30
|
-
```tsx
|
|
31
|
-
import React from "react";
|
|
32
|
-
import { Card } from "@/components/ui/card";
|
|
33
|
-
import { TrendingUp, TrendingDown } from "lucide-react";
|
|
34
|
-
|
|
35
|
-
interface StatCardProps {
|
|
36
|
-
title: string;
|
|
37
|
-
value: number | string;
|
|
38
|
-
trend?: {
|
|
39
|
-
value: number;
|
|
40
|
-
isPositive: boolean;
|
|
41
|
-
};
|
|
42
|
-
subtitle?: string;
|
|
43
|
-
onClick?: () => void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const StatCard: React.FC<StatCardProps> = ({ title, value, trend, subtitle, onClick }) => {
|
|
47
|
-
return (
|
|
48
|
-
<Card
|
|
49
|
-
className={`p-4 border-gray-200 shadow-sm relative ${
|
|
50
|
-
onClick ? "cursor-pointer hover:shadow-lg transition-shadow" : ""
|
|
51
|
-
}`}
|
|
52
|
-
onClick={onClick}
|
|
53
|
-
>
|
|
54
|
-
<div className="space-y-1">
|
|
55
|
-
<p className="text-sm font-medium text-muted-foreground uppercase tracking-wide">{title}</p>
|
|
56
|
-
<div className="flex items-baseline gap-3">
|
|
57
|
-
<p className="text-4xl font-bold text-primary">{value}</p>
|
|
58
|
-
{trend && (
|
|
59
|
-
<span
|
|
60
|
-
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-sm font-medium ${
|
|
61
|
-
trend.isPositive
|
|
62
|
-
? "bg-emerald-100 text-emerald-800"
|
|
63
|
-
: "bg-pink-100 text-pink-800"
|
|
64
|
-
}`}
|
|
65
|
-
>
|
|
66
|
-
{trend.isPositive ? (
|
|
67
|
-
<TrendingUp className="w-4 h-4" />
|
|
68
|
-
) : (
|
|
69
|
-
<TrendingDown className="w-4 h-4" />
|
|
70
|
-
)}
|
|
71
|
-
{Math.abs(trend.value)}%
|
|
72
|
-
</span>
|
|
73
|
-
)}
|
|
74
|
-
</div>
|
|
75
|
-
{subtitle && <p className="text-sm text-muted-foreground mt-1">{subtitle}</p>}
|
|
76
|
-
</div>
|
|
77
|
-
</Card>
|
|
78
|
-
);
|
|
79
|
-
};
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
This version uses Lucide icons (`TrendingUp`/`TrendingDown`) instead of custom SVGs for portability across projects.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## Layout: stat card grid
|
|
87
|
-
|
|
88
|
-
Display stat cards in a responsive grid:
|
|
89
|
-
|
|
90
|
-
```tsx
|
|
91
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
92
|
-
<StatCard
|
|
93
|
-
title="Total Properties"
|
|
94
|
-
value={metrics.totalProperties}
|
|
95
|
-
trend={{ value: 10, isPositive: true }}
|
|
96
|
-
subtitle="Last month total 38"
|
|
97
|
-
/>
|
|
98
|
-
<StatCard
|
|
99
|
-
title="Units Available"
|
|
100
|
-
value={metrics.unitsAvailable}
|
|
101
|
-
trend={{ value: 5, isPositive: false }}
|
|
102
|
-
subtitle="Last month total 12/42"
|
|
103
|
-
/>
|
|
104
|
-
<StatCard
|
|
105
|
-
title="Occupied Units"
|
|
106
|
-
value={metrics.occupiedUnits}
|
|
107
|
-
trend={{ value: 8, isPositive: true }}
|
|
108
|
-
subtitle="Last month total 27"
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Computing trend values
|
|
116
|
-
|
|
117
|
-
Calculate trends from current vs previous period:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
const trends = useMemo(() => {
|
|
121
|
-
const previousTotal = metrics.totalProperties - Math.round(metrics.totalProperties * 0.1);
|
|
122
|
-
const trendPercent = previousTotal > 0
|
|
123
|
-
? Math.round(((metrics.totalProperties - previousTotal) / previousTotal) * 100)
|
|
124
|
-
: 0;
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
value: Math.abs(trendPercent),
|
|
128
|
-
isPositive: trendPercent >= 0,
|
|
129
|
-
};
|
|
130
|
-
}, [metrics]);
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Trend badge color conventions
|
|
136
|
-
|
|
137
|
-
| Trend | Background | Text | Meaning |
|
|
138
|
-
|-------|------------|------|---------|
|
|
139
|
-
| Positive (up) | `bg-emerald-100` | `text-emerald-800` | Growth, improvement |
|
|
140
|
-
| Negative (down) | `bg-pink-100` | `text-pink-800` | Decline, concern |
|
|
141
|
-
| Neutral | `bg-gray-100` | `text-gray-600` | No change |
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## Accessibility
|
|
146
|
-
|
|
147
|
-
- Card uses `cursor-pointer` and `hover:shadow-lg` only when `onClick` is provided.
|
|
148
|
-
- Trend icons have implicit meaning from color + direction icon.
|
|
149
|
-
- Stat values use large, bold text for visibility.
|
|
150
|
-
- Title uses `uppercase tracking-wide` for visual hierarchy without heading tags (appropriate in a card grid).
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: building-webapp-react-components
|
|
3
|
-
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)."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# React Web App (Components, Pages, Layout)
|
|
7
|
-
|
|
8
|
-
Use this skill whenever you are editing React/TSX code in the web app (creating or modifying components, pages, header/footer, or layout).
|
|
9
|
-
|
|
10
|
-
## Step 1 — Identify the type of component
|
|
11
|
-
|
|
12
|
-
Determine which of these three categories the request falls into, then follow the corresponding section below:
|
|
13
|
-
|
|
14
|
-
- **Page** — user wants a new routed page (e.g. "add a contacts page", "create a dashboard page", "add a settings section")
|
|
15
|
-
- **Header / Footer** — user wants a site-wide header, footer, nav bar, or page footer that appears on every page
|
|
16
|
-
- **Component** — everything else: a widget, card, table, form, dialog, or other UI element placed within an existing page
|
|
17
|
-
|
|
18
|
-
If it is not immediately clear from the user's message, ask:
|
|
19
|
-
|
|
20
|
-
> "Are you looking to add a new page, a site-wide header or footer, or a component within an existing page?"
|
|
21
|
-
|
|
22
|
-
Then follow the matching section.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Clarifying Questions
|
|
27
|
-
|
|
28
|
-
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.
|
|
29
|
-
|
|
30
|
-
### For a Page
|
|
31
|
-
|
|
32
|
-
1. **What is the name and purpose of the page?** (e.g., Contacts, Dashboard, Settings)
|
|
33
|
-
2. **What URL path should it use?** (e.g., `/contacts`, `/dashboard`) — or derive from the page name?
|
|
34
|
-
3. **Should the page appear in the navigation menu?**
|
|
35
|
-
4. **Who can access it?** Public, authenticated users only (`PrivateRoute`), or unauthenticated only (e.g., login — `AuthenticationRoute`)?
|
|
36
|
-
5. **What content or sections should the page include?** (list, form, table, detail view, etc.)
|
|
37
|
-
6. **Does it need to fetch any data?** If so, from where?
|
|
38
|
-
|
|
39
|
-
### For a Header / Footer
|
|
40
|
-
|
|
41
|
-
1. **Header, footer, or both?**
|
|
42
|
-
2. **What should the header contain?** (logo/app name, nav links, user avatar, CTA button, etc.)
|
|
43
|
-
3. **What should the footer contain?** (copyright text, links, social icons, etc.)
|
|
44
|
-
4. **Should the header be sticky (fixed to top while scrolling)?**
|
|
45
|
-
5. **Is there a logo or brand name to display?** (or placeholder?)
|
|
46
|
-
6. **Any specific color scheme or style direction?** (dark background, branded primary color, minimal, etc.)
|
|
47
|
-
7. **Should navigation links appear in the header?** If so, which pages?
|
|
48
|
-
|
|
49
|
-
### For a Component
|
|
50
|
-
|
|
51
|
-
1. **What should the component do?** (display data, accept input, trigger an action, etc.)
|
|
52
|
-
2. **What page or location should it appear on?**
|
|
53
|
-
3. **Is this shared/reusable across pages, or specific to one feature?** (determines file location)
|
|
54
|
-
4. **What data or props does it need?** (static content, props, fetched data)
|
|
55
|
-
5. **Does it need internal state?** (loading, toggle, form state, etc.)
|
|
56
|
-
6. **Are there any specific shadcn components to use?** (Card, Table, Dialog, Form, etc.)
|
|
57
|
-
7. **Should it appear in a specific layout position?** (full-width, sidebar, inline, etc.)
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Implementation
|
|
62
|
-
|
|
63
|
-
Once you have identified the type and gathered answers to the clarifying questions, read and follow the corresponding implementation guide:
|
|
64
|
-
|
|
65
|
-
- **Page** — read `implementation/page.md` and follow the instructions there.
|
|
66
|
-
- **Header / Footer** — read `implementation/header-footer.md` and follow the instructions there.
|
|
67
|
-
- **Component** — read `implementation/component.md` and follow the instructions there.
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## TypeScript Standards
|
|
72
|
-
|
|
73
|
-
- **Never use `any`** — use proper types, generics, or `unknown` with type guards.
|
|
74
|
-
- **Event handlers:** `(event: React.FormEvent<HTMLFormElement>): void`
|
|
75
|
-
- **State:** `useState<User | null>(null)` — always provide the type parameter.
|
|
76
|
-
- **No unsafe assertions** (`obj as User`). Use type guards:
|
|
77
|
-
```typescript
|
|
78
|
-
function isUser(obj: unknown): obj is User {
|
|
79
|
-
return typeof obj === 'object' && obj !== null && typeof (obj as User).id === 'string';
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Verification (MANDATORY)
|
|
86
|
-
|
|
87
|
-
Before completing, run from the web app directory `force-app/main/default/webapplications/<appName>/`:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- **Lint:** MUST result in 0 errors.
|
|
94
|
-
- **Build:** MUST succeed (includes TypeScript check).
|
|
95
|
-
|
|
96
|
-
If either fails, fix the errors and re-run. Do not leave the session with failing quality gates.
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: configuring-webapp-csp-trusted-sites
|
|
3
|
-
description: "Creates Salesforce CSP Trusted Site metadata when adding external domains. Use when the user adds an external API, CDN, image host, font provider, map tile server, or any third-party URL that the web application needs to load resources from — or when a browser console shows a CSP violation error."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# CSP Trusted Sites
|
|
7
|
-
|
|
8
|
-
## When to Use
|
|
9
|
-
|
|
10
|
-
Use this skill whenever the application references a new external domain that is not already registered as a CSP Trusted Site. This includes:
|
|
11
|
-
|
|
12
|
-
- Adding images from a new CDN (Unsplash, Pexels, Cloudinary, etc.)
|
|
13
|
-
- Loading fonts from an external provider (Google Fonts, Adobe Fonts)
|
|
14
|
-
- Calling a third-party API (Open-Meteo, Nominatim, Mapbox, etc.)
|
|
15
|
-
- Loading map tiles from a tile server (OpenStreetMap, Mapbox)
|
|
16
|
-
- Embedding iframes from external services (YouTube, Vimeo)
|
|
17
|
-
- Loading external stylesheets or scripts
|
|
18
|
-
|
|
19
|
-
Salesforce enforces Content Security Policy (CSP) headers on all web applications. Any external domain not registered as a CSP Trusted Site will be blocked by the browser, causing images to not load, API calls to fail, or fonts to be missing.
|
|
20
|
-
|
|
21
|
-
**Reference:** [Salesforce CspTrustedSite Object Reference](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_csptrustedsite.htm)
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Step 1 — Identify external domains
|
|
26
|
-
|
|
27
|
-
Scan the code for any URLs pointing to external domains. Common patterns:
|
|
28
|
-
|
|
29
|
-
- `fetch("https://api.example.com/...")` — API calls
|
|
30
|
-
- `<img src="https://images.example.com/..." />` — images
|
|
31
|
-
- `<link href="https://fonts.example.com/..." />` — stylesheets
|
|
32
|
-
- `url="https://tiles.example.com/{z}/{x}/{y}.png"` — map tiles
|
|
33
|
-
- `@import url("https://cdn.example.com/...")` — CSS imports
|
|
34
|
-
|
|
35
|
-
Extract the **origin** (scheme + host) from each URL. For example:
|
|
36
|
-
- `https://api.open-meteo.com/v1/forecast?lat=...` → `https://api.open-meteo.com`
|
|
37
|
-
- `https://images.unsplash.com/photo-123?w=800` → `https://images.unsplash.com`
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Step 2 — Check existing CSP Trusted Sites
|
|
42
|
-
|
|
43
|
-
Before creating a new file, check if the domain already has a CSP Trusted Site:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
ls force-app/main/default/cspTrustedSites/
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
If the domain is already registered, no action is needed.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Step 3 — Determine the CSP directive(s)
|
|
54
|
-
|
|
55
|
-
Map the resource type to the correct CSP `isApplicableTo*Src` fields. Read `implementation/metadata-format.md` for the full reference.
|
|
56
|
-
|
|
57
|
-
Quick reference:
|
|
58
|
-
|
|
59
|
-
| Resource type | CSP directive field(s) to set `true` |
|
|
60
|
-
|--------------|--------------------------------------|
|
|
61
|
-
| Images (img, background-image) | `isApplicableToImgSrc` |
|
|
62
|
-
| API calls (fetch, XMLHttpRequest) | `isApplicableToConnectSrc` |
|
|
63
|
-
| Fonts (.woff, .woff2, .ttf) | `isApplicableToFontSrc` |
|
|
64
|
-
| Stylesheets (CSS) | `isApplicableToStyleSrc` |
|
|
65
|
-
| Video / audio | `isApplicableToMediaSrc` |
|
|
66
|
-
| Iframes | `isApplicableToFrameSrc` |
|
|
67
|
-
|
|
68
|
-
**Always also set `isApplicableToConnectSrc` to `true`** — most resources also require connect-src for preflight/redirect handling.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Step 4 — Create the metadata file
|
|
73
|
-
|
|
74
|
-
Read `implementation/metadata-format.md` and follow the instructions to create the `.cspTrustedSite-meta.xml` file.
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Step 5 — Verify
|
|
79
|
-
|
|
80
|
-
1. Confirm the file is valid XML and matches the expected schema.
|
|
81
|
-
2. Confirm the file is placed in `force-app/main/default/cspTrustedSites/`.
|
|
82
|
-
3. Confirm only the necessary `isApplicableTo*Src` fields are set to `true`.
|
|
83
|
-
4. Run from the web app directory:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
- **Lint:** MUST result in 0 errors.
|
|
90
|
-
- **Build:** MUST succeed.
|