afv-library/skills/building-webapp-data-visualization/implementation/dashboard-layout.md
k-j-kim 9ba064174a
feat: syncing webapp skills sync to afv @W-21338965@ (#57)
* 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 <hsinghbisht@salesforce.com>
2026-03-19 22:46:27 +05:30

5.1 KiB

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

import { PageContainer } from "@/components/layout/PageContainer";
import { StatCard } from "@/components/StatCard";
import { DonutChart } from "@/components/DonutChart";

export default function Dashboard() {
  return (
    <PageContainer>
      <div className="max-w-7xl mx-auto space-y-6">
        {/* Search bar */}
        <div>{/* global search component */}</div>

        {/* Main content: 70/30 split */}
        <div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
          <div className="space-y-6">
            {/* Stat cards row */}
            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
              <StatCard title="Metric A" value={42} />
              <StatCard title="Metric B" value={18} />
              <StatCard title="Metric C" value={7} />
            </div>

            {/* Data table */}
            <div>{/* table component */}</div>
          </div>

          {/* Sidebar chart */}
          <div>
            <DonutChart title="Distribution" data={chartData} />
          </div>
        </div>
      </div>
    </PageContainer>
  );
}

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:

if (loading) {
  return (
    <PageContainer>
      <div className="flex items-center justify-center min-h-[400px]">
        <p className="text-muted-foreground">Loading dashboard</p>
      </div>
    </PageContainer>
  );
}

Or use a skeleton layout:

if (loading) {
  return (
    <PageContainer>
      <div className="max-w-7xl mx-auto space-y-6">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          {[1, 2, 3].map((i) => (
            <div key={i} className="h-28 animate-pulse rounded-xl bg-muted" />
          ))}
        </div>
        <div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
          <div className="h-64 animate-pulse rounded-xl bg-muted" />
          <div className="h-64 animate-pulse rounded-xl bg-muted" />
        </div>
      </div>
    </PageContainer>
  );
}

Data fetching pattern

Use useEffect with cancellation for dashboard metrics:

const [metrics, setMetrics] = useState<Metrics | null>(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:

const [metrics, setMetrics] = useState<Metrics | null>(null);
const [requests, setRequests] = useState<Request[]>([]);
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:

interface PageContainerProps {
  children: React.ReactNode;
}

export function PageContainer({ children }: PageContainerProps) {
  return <div className="p-6">{children}</div>;
}