@schandlergarcia/sf-web-components 1.0.0 → 1.0.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.
Files changed (32) hide show
  1. package/.a4drules/features/command-center-dashboard-rule.md +46 -0
  2. package/.a4drules/skills/command-center-builder/SKILL.md +637 -0
  3. package/.a4drules/skills/command-center-project/SKILL.md +103 -0
  4. package/.a4drules/skills/component-library/SKILL.md +1025 -0
  5. package/.a4drules/skills/outer-app/SKILL.md +64 -0
  6. package/README.md +7 -7
  7. package/package.json +1 -1
  8. package/.a4drules/skills/building-data-visualization/SKILL.md +0 -72
  9. package/.a4drules/skills/building-data-visualization/implementation/bar-line-chart.md +0 -316
  10. package/.a4drules/skills/building-data-visualization/implementation/dashboard-layout.md +0 -189
  11. package/.a4drules/skills/building-data-visualization/implementation/donut-chart.md +0 -181
  12. package/.a4drules/skills/building-data-visualization/implementation/stat-card.md +0 -150
  13. package/.a4drules/skills/building-react-components/SKILL.md +0 -96
  14. package/.a4drules/skills/building-react-components/implementation/component.md +0 -78
  15. package/.a4drules/skills/building-react-components/implementation/header-footer.md +0 -132
  16. package/.a4drules/skills/building-react-components/implementation/page.md +0 -93
  17. package/.a4drules/skills/configuring-csp-trusted-sites/SKILL.md +0 -90
  18. package/.a4drules/skills/configuring-csp-trusted-sites/implementation/metadata-format.md +0 -281
  19. package/.a4drules/skills/configuring-webapp-metadata/SKILL.md +0 -158
  20. package/.a4drules/skills/creating-webapp/SKILL.md +0 -140
  21. package/.a4drules/skills/deploying-to-salesforce/SKILL.md +0 -226
  22. package/.a4drules/skills/implementing-file-upload/SKILL.md +0 -396
  23. package/.a4drules/skills/installing-webapp-features/SKILL.md +0 -210
  24. package/.a4drules/skills/managing-agentforce-conversation-client/SKILL.md +0 -186
  25. package/.a4drules/skills/managing-agentforce-conversation-client/references/constraints.md +0 -134
  26. package/.a4drules/skills/managing-agentforce-conversation-client/references/examples.md +0 -132
  27. package/.a4drules/skills/managing-agentforce-conversation-client/references/style-tokens.md +0 -101
  28. package/.a4drules/skills/managing-agentforce-conversation-client/references/troubleshooting.md +0 -57
  29. package/.a4drules/skills/using-salesforce-data/SKILL.md +0 -363
  30. package/.a4drules/skills/using-salesforce-data/graphql-search.sh +0 -139
  31. package/.a4drules/webapp-data.md +0 -353
  32. package/.a4drules/webapp-ui.md +0 -16
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: outer-app
3
+ description: >-
4
+ Routing, layout, and conventions for the outer app (everything outside
5
+ /command-center). Use when working with app routes, auth, navigation,
6
+ shadcn components, or outer app pages.
7
+ ---
8
+
9
+ # Outer App (Non-Command-Center)
10
+
11
+ The outer app handles navigation, global search, and standard pages. It uses an entirely different UI stack from the command center.
12
+
13
+ ## UI Stack
14
+
15
+ - **Components:** shadcn/ui (`src/components/ui/`) — Radix UI primitives styled with CVA + Tailwind
16
+ - **Icons:** Lucide React (`lucide-react`) — NOT Heroicons
17
+ - **Utility:** `cn()` from `src/lib/utils.ts` (clsx + tailwind-merge)
18
+ - **Styling:** Tailwind CSS 4 + shadcn theme variables (oklch-based in `global.css`)
19
+ - **Language:** TypeScript (`.tsx`/`.ts`)
20
+
21
+ ## Routing
22
+
23
+ Routes defined in `src/routes.tsx` as `RouteObject[]`, consumed by `createBrowserRouter` in `src/app.tsx`.
24
+
25
+ Route tree:
26
+ - `/` — `AppLayout` (wraps all routes)
27
+ - `/` (index) — `Home` (renders `CommandCenter` — command center world)
28
+ - `/search` — Search page (global search input)
29
+ - `/test-acc` — Test ACC page
30
+ - `/global-search/:query` — Search results (Suspense-wrapped)
31
+ - `/object/:objectApiName/:recordId` — Detail page (Suspense-wrapped)
32
+ - `*` — `NotFound`
33
+
34
+ ### Adding a New Outer App Route
35
+
36
+ 1. Create page component in `src/pages/` or `src/features/*/pages/`
37
+ 2. Add route object in `src/routes.tsx` under the appropriate layout
38
+ 3. Set `handle: { showInNavigation: true, label: "..." }` if it should appear in the nav bar
39
+
40
+ ### Navigation
41
+
42
+ `AppLayout` (`src/appLayout.tsx`) auto-generates nav from routes with `handle.showInNavigation === true`. The nav uses React Router `<Link>` components.
43
+
44
+ ## shadcn/ui Components
45
+
46
+ Available in `src/components/ui/`: `alert`, `button`, `card`, `dialog`, `field`, `input`, `label`, `pagination`, `select`, `separator`, `skeleton`, `spinner`, `table`, `tabs`.
47
+
48
+ Add new ones via: `npx shadcn@latest add <component>`
49
+
50
+ Configuration in `src/components.json` (New York style, neutral base color).
51
+
52
+ ## Salesforce Integration
53
+
54
+ - SDK imports (`@salesforce/*`) are stubbed in standalone mode via Vite alias → `src/stubs/`
55
+ - Stubs return sample data (accounts, filters, object metadata) for local dev
56
+ - Real SDKs are linked via `file:` references when deployed to Salesforce
57
+
58
+ ## Do Not
59
+
60
+ - Import command center library (`@/components/library`) in outer app code
61
+ - Import Heroicons in outer app code (use Lucide)
62
+ - Import HeroUI components in outer app code
63
+ - Use `brand-*` Tailwind classes in outer app code (those are command center tokens)
64
+ - Add `.heroui-scope` class outside of `CommandCenter.tsx`
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @salesforce/sf-web-components
1
+ # @schandlergarcia/sf-web-components
2
2
 
3
3
  Reusable Salesforce web components library with Tailwind CSS v4 and shadcn/ui components.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @salesforce/sf-web-components
8
+ npm install @schandlergarcia/sf-web-components
9
9
  ```
10
10
 
11
11
  ## Peer Dependencies
@@ -20,8 +20,8 @@ This package requires:
20
20
  ### Import Components
21
21
 
22
22
  ```tsx
23
- import { Button, Card } from '@salesforce/sf-web-components';
24
- import { cn } from '@salesforce/sf-web-components/lib';
23
+ import { Button, Card } from '@schandlergarcia/sf-web-components';
24
+ import { cn } from '@schandlergarcia/sf-web-components/lib';
25
25
 
26
26
  function App() {
27
27
  return (
@@ -37,7 +37,7 @@ function App() {
37
37
  Add to your main CSS file:
38
38
 
39
39
  ```css
40
- @import '@salesforce/sf-web-components/styles';
40
+ @import '@schandlergarcia/sf-web-components/styles';
41
41
  ```
42
42
 
43
43
  ### Tailwind Configuration
@@ -50,7 +50,7 @@ import type { Config } from 'tailwindcss';
50
50
  export default {
51
51
  content: [
52
52
  './src/**/*.{js,ts,jsx,tsx}',
53
- './node_modules/@salesforce/sf-web-components/dist/**/*.{js,jsx}'
53
+ './node_modules/@schandlergarcia/sf-web-components/dist/**/*.{js,jsx}'
54
54
  ],
55
55
  theme: {
56
56
  extend: {}
@@ -71,7 +71,7 @@ This library includes:
71
71
  ## Utilities
72
72
 
73
73
  ```tsx
74
- import { cn } from '@salesforce/sf-web-components/lib';
74
+ import { cn } from '@schandlergarcia/sf-web-components/lib';
75
75
 
76
76
  // Merge Tailwind classes with conflict resolution
77
77
  const className = cn('px-2 py-1', 'px-4'); // Result: 'py-1 px-4'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schandlergarcia/sf-web-components",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Reusable Salesforce web components library with Tailwind CSS v4 and shadcn/ui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,72 +0,0 @@
1
- ---
2
- name: building-data-visualization
3
- 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.
4
- ---
5
-
6
- # Data Visualization
7
-
8
- ## When to Use
9
-
10
- Use this skill when:
11
- - Adding charts (donut, pie, bar, line, area) to a dashboard or analytics page
12
- - Displaying KPI/metric stat cards with trend indicators
13
- - Building a dashboard layout with mixed chart types and summary cards
14
-
15
- ---
16
-
17
- ## Step 1 — Determine the visualization type
18
-
19
- Identify what the user needs:
20
-
21
- - **Donut / pie chart** — categorical breakdown (e.g. issue types, status distribution)
22
- - **Bar chart** — comparison across categories or time periods
23
- - **Line / area chart** — trends over time
24
- - **Stat card** — single KPI metric with optional trend indicator
25
- - **Combined dashboard** — stat cards + one or more charts
26
-
27
- If unclear, ask:
28
-
29
- > "What data should the chart display, and would a donut chart, bar chart, line chart, or stat cards work best?"
30
-
31
- ---
32
-
33
- ## Step 2 — Install dependencies
34
-
35
- All chart types in this skill use **recharts**. Install once from the web app directory:
36
-
37
- ```bash
38
- npm install recharts
39
- ```
40
-
41
- Recharts is built on D3 and provides declarative React components. No additional CSS is needed.
42
-
43
- ---
44
-
45
- ## Step 3 — Choose implementation path
46
-
47
- Read the corresponding guide:
48
-
49
- - **Bar chart** — read `implementation/bar-line-chart.md` (categorical data)
50
- - **Line / area chart** — read `implementation/bar-line-chart.md` (time-series data)
51
- - **Donut / pie chart** — read `implementation/donut-chart.md`
52
- - **Stat card with trend** — read `implementation/stat-card.md`
53
- - **Dashboard layout** — read `implementation/dashboard-layout.md`
54
-
55
- ---
56
-
57
- ## Verification
58
-
59
- Before completing:
60
-
61
- 1. Chart renders with correct data and colors.
62
- 2. Chart is responsive (resizes with container).
63
- 3. Legend labels match the data categories.
64
- 4. Stat card trends display correct positive/negative indicators.
65
- 5. Run from the web app directory:
66
-
67
- ```bash
68
- cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
69
- ```
70
-
71
- - **Lint:** MUST result in 0 errors.
72
- - **Build:** MUST succeed.
@@ -1,316 +0,0 @@
1
- # Bar & Line / Area Chart — Implementation Guide
2
-
3
- Requires **recharts** (install from the web app directory; see SKILL.md Step 2).
4
-
5
- ---
6
-
7
- ## Data shapes
8
-
9
- ### Time-series (line / area chart)
10
-
11
- Use when data represents a trend over time or ordered sequence.
12
-
13
- ```ts
14
- interface TimeSeriesDataPoint {
15
- x: string; // date or label on the x-axis
16
- y: number; // numeric value
17
- }
18
- ```
19
-
20
- Map raw fields to this shape: e.g. `date` → `x`, `revenue` → `y`.
21
-
22
- ### Categorical (bar chart)
23
-
24
- Use when data compares discrete categories.
25
-
26
- ```ts
27
- interface CategoricalDataPoint {
28
- name: string; // category label
29
- value: number; // numeric value
30
- }
31
- ```
32
-
33
- Map raw fields to this shape: e.g. `product` → `name`, `sales` → `value`.
34
-
35
- ### How to decide
36
-
37
- | Signal | Type |
38
- |--------|------|
39
- | "over time", "trend", date-like keys | Time-series → line chart |
40
- | "by category", "by X", label-like keys | Categorical → bar chart |
41
-
42
- ---
43
-
44
- ## Theme colors
45
-
46
- Pick a theme based on the data's sentiment:
47
-
48
- | Theme | Stroke / Fill | When to use |
49
- |-------|---------------|-------------|
50
- | `green` | `#22c55e` | Growth, gain, positive trend |
51
- | `red` | `#ef4444` | Decline, loss, negative trend |
52
- | `neutral` | `#6366f1` | Default or mixed data |
53
-
54
- Define colors as constants — do not use inline hex values.
55
-
56
- ```ts
57
- const THEME_COLORS = {
58
- red: "#ef4444",
59
- green: "#22c55e",
60
- neutral: "#6366f1",
61
- } as const;
62
-
63
- type ChartTheme = keyof typeof THEME_COLORS;
64
- ```
65
-
66
- ---
67
-
68
- ## Line chart component
69
-
70
- Create at `components/LineChart.tsx` (or colocate with the page):
71
-
72
- ```tsx
73
- import React from "react";
74
- import {
75
- LineChart as RechartsLineChart,
76
- Line,
77
- XAxis,
78
- YAxis,
79
- CartesianGrid,
80
- Tooltip,
81
- Legend,
82
- ResponsiveContainer,
83
- } from "recharts";
84
-
85
- const THEME_COLORS = {
86
- red: "#ef4444",
87
- green: "#22c55e",
88
- neutral: "#6366f1",
89
- } as const;
90
-
91
- type ChartTheme = keyof typeof THEME_COLORS;
92
-
93
- interface TimeSeriesDataPoint {
94
- x: string;
95
- y: number;
96
- }
97
-
98
- interface TimeSeriesChartProps {
99
- data: TimeSeriesDataPoint[];
100
- theme?: ChartTheme;
101
- title?: string;
102
- className?: string;
103
- }
104
-
105
- export function TimeSeriesChart({
106
- data,
107
- theme = "neutral",
108
- title,
109
- className = "",
110
- }: TimeSeriesChartProps) {
111
- if (data.length === 0) {
112
- return <p className="text-muted-foreground text-center py-8">No data to display</p>;
113
- }
114
-
115
- const color = THEME_COLORS[theme];
116
-
117
- return (
118
- <div className={className}>
119
- {title && (
120
- <h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
121
- {title}
122
- </h3>
123
- )}
124
- <ResponsiveContainer width="100%" height={300}>
125
- <RechartsLineChart data={data}>
126
- <CartesianGrid strokeDasharray="3 3" />
127
- <XAxis dataKey="x" />
128
- <YAxis />
129
- <Tooltip />
130
- <Legend />
131
- <Line type="monotone" dataKey="y" stroke={color} strokeWidth={2} dot={false} />
132
- </RechartsLineChart>
133
- </ResponsiveContainer>
134
- </div>
135
- );
136
- }
137
- ```
138
-
139
- ---
140
-
141
- ## Bar chart component
142
-
143
- Create at `components/BarChart.tsx` (or colocate with the page):
144
-
145
- ```tsx
146
- import React from "react";
147
- import {
148
- BarChart as RechartsBarChart,
149
- Bar,
150
- XAxis,
151
- YAxis,
152
- CartesianGrid,
153
- Tooltip,
154
- Legend,
155
- ResponsiveContainer,
156
- } from "recharts";
157
-
158
- const THEME_COLORS = {
159
- red: "#ef4444",
160
- green: "#22c55e",
161
- neutral: "#6366f1",
162
- } as const;
163
-
164
- type ChartTheme = keyof typeof THEME_COLORS;
165
-
166
- interface CategoricalDataPoint {
167
- name: string;
168
- value: number;
169
- }
170
-
171
- interface CategoricalChartProps {
172
- data: CategoricalDataPoint[];
173
- theme?: ChartTheme;
174
- title?: string;
175
- className?: string;
176
- }
177
-
178
- export function CategoricalChart({
179
- data,
180
- theme = "neutral",
181
- title,
182
- className = "",
183
- }: CategoricalChartProps) {
184
- if (data.length === 0) {
185
- return <p className="text-muted-foreground text-center py-8">No data to display</p>;
186
- }
187
-
188
- const color = THEME_COLORS[theme];
189
-
190
- return (
191
- <div className={className}>
192
- {title && (
193
- <h3 className="text-sm font-medium text-primary mb-2 uppercase tracking-wide">
194
- {title}
195
- </h3>
196
- )}
197
- <ResponsiveContainer width="100%" height={300}>
198
- <RechartsBarChart data={data}>
199
- <CartesianGrid strokeDasharray="3 3" />
200
- <XAxis dataKey="name" />
201
- <YAxis />
202
- <Tooltip />
203
- <Legend />
204
- <Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
205
- </RechartsBarChart>
206
- </ResponsiveContainer>
207
- </div>
208
- );
209
- }
210
- ```
211
-
212
- ---
213
-
214
- ## Area chart variant
215
-
216
- For a filled area chart (useful for volume-over-time), swap `Line` for `Area`:
217
-
218
- ```tsx
219
- import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
220
-
221
- <ResponsiveContainer width="100%" height={300}>
222
- <AreaChart data={data}>
223
- <CartesianGrid strokeDasharray="3 3" />
224
- <XAxis dataKey="x" />
225
- <YAxis />
226
- <Tooltip />
227
- <Area type="monotone" dataKey="y" stroke={color} fill={color} fillOpacity={0.2} />
228
- </AreaChart>
229
- </ResponsiveContainer>
230
- ```
231
-
232
- ---
233
-
234
- ## Chart container wrapper
235
-
236
- Wrap any chart in a styled card for consistent spacing:
237
-
238
- ```tsx
239
- import { Card } from "@/components/ui/card";
240
-
241
- interface ChartContainerProps {
242
- children: React.ReactNode;
243
- className?: string;
244
- }
245
-
246
- export function ChartContainer({ children, className = "" }: ChartContainerProps) {
247
- return (
248
- <Card className={`p-4 border-gray-200 shadow-sm ${className}`}>
249
- {children}
250
- </Card>
251
- );
252
- }
253
- ```
254
-
255
- Usage:
256
-
257
- ```tsx
258
- <ChartContainer>
259
- <TimeSeriesChart data={monthlyData} theme="green" title="Monthly Revenue" />
260
- </ChartContainer>
261
- ```
262
-
263
- ---
264
-
265
- ## Preparing raw data
266
-
267
- Map API responses to the expected shape before passing to the chart:
268
-
269
- ```tsx
270
- const timeSeriesData = useMemo(
271
- () => apiRecords.map((r) => ({ x: r.date, y: r.revenue })),
272
- [apiRecords],
273
- );
274
-
275
- const categoricalData = useMemo(
276
- () => apiRecords.map((r) => ({ name: r.product, value: r.sales })),
277
- [apiRecords],
278
- );
279
- ```
280
-
281
- ---
282
-
283
- ## Key Recharts concepts
284
-
285
- | Component | Purpose |
286
- |-----------|---------|
287
- | `ResponsiveContainer` | Wraps chart to fill parent width |
288
- | `CartesianGrid` | Background grid lines |
289
- | `XAxis` / `YAxis` | Axis labels; `dataKey` maps to the data field |
290
- | `Tooltip` | Hover info |
291
- | `Legend` | Series labels |
292
- | `Line` | Line series; `type="monotone"` for smooth curves |
293
- | `Bar` | Bar series; `radius` rounds top corners |
294
- | `Area` | Filled area; `fillOpacity` controls transparency |
295
-
296
- ---
297
-
298
- ## Accessibility
299
-
300
- - Always include a text legend (not just colors).
301
- - Chart should be wrapped in a section with a visible heading.
302
- - For critical data, provide a text summary or table alternative.
303
- - Use sufficient color contrast between the chart stroke/fill and background.
304
- - Consider `prefers-reduced-motion` for chart animations.
305
-
306
- ---
307
-
308
- ## Common mistakes
309
-
310
- | Mistake | Fix |
311
- |---------|-----|
312
- | Missing `ResponsiveContainer` | Chart won't resize; always wrap |
313
- | Fixed width/height on chart | Let `ResponsiveContainer` control sizing |
314
- | No empty-data handling | Show "No data" message when `data.length === 0` |
315
- | Inline colors | Extract to `THEME_COLORS` constant |
316
- | Using raw Recharts for every chart type | Use `DonutChart` (see `donut-chart.md`) for pie/donut |
@@ -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
- ```