# Gridline dashboard

A shadcn/ui block from the [@watermelon](https://ui.watermelon.sh) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/watermelon/gridline-dashboard).

- Page, with a live preview: https://shadcn.exchange/r/watermelon/gridline-dashboard
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/watermelon/gridline-dashboard-light-1a5f891f9d5234d6.png
- Registry item: https://registry.watermelon.sh/r/gridline-dashboard.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://registry.watermelon.sh/r/gridline-dashboard.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `card`, `drawer`, `dropdown-menu`, `input`, `sheet`, `switch`, `tooltip`, `utils`

## Files

### `src/components/dashboards/gridline-dashboard/assets/icons.tsx`

```tsx
import type { SVGProps } from "react";

export function WorkspacesIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M19.375 5H.625a.625.625 0 0 0 0 1.25h.625V15a.625.625 0 1 0 1.25 0v-3.75h15V15a.625.625 0 1 0 1.25 0V6.25h.625a.625.625 0 1 0 0-1.25ZM2.5 6.25h6.875V10H2.5V6.25ZM17.5 10h-6.875V6.25H17.5V10ZM7.5 8.125c0 .345-.28.625-.625.625H5a.625.625 0 1 1 0-1.25h1.875c.345 0 .625.28.625.625Zm5 0c0-.345.28-.625.625-.625H15a.625.625 0 1 1 0 1.25h-1.875a.625.625 0 0 1-.625-.625Z"
        fill="currentColor"
      />
    </svg>
  );
}

export function SidebarToggleIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      viewBox="0 0 48 48"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M41.375 4.21429C42.1209 4.21429 42.8363 4.55293 43.3637 5.15573C43.8912 5.75852 44.1875 6.57609 44.1875 7.42857V39.5714C44.1875 40.4239 43.8912 41.2415 43.3637 41.8443C42.8363 42.4471 42.1209 42.7857 41.375 42.7857H7.625C6.87908 42.7857 6.16371 42.4471 5.63626 41.8443C5.10882 41.2415 4.8125 40.4239 4.8125 39.5714V7.42857C4.8125 6.57609 5.10882 5.75852 5.63626 5.15573C6.16371 4.55293 6.87908 4.21429 7.625 4.21429H41.375ZM7.625 1C6.13316 1 4.70242 1.67729 3.64752 2.88288C2.59263 4.08848 2 5.72361 2 7.42857L2 39.5714C2 41.2764 2.59263 42.9115 3.64752 44.1171C4.70242 45.3227 6.13316 46 7.625 46H41.375C42.8668 46 44.2976 45.3227 45.3525 44.1171C46.4074 42.9115 47 41.2764 47 39.5714V7.42857C47 5.72361 46.4074 4.08848 45.3525 2.88288C44.2976 1.67729 42.8668 1 41.375 1H7.625Z"
        fill="currentColor"
      />
      <path
        d="M11.6426 11.2857C11.6426 10.4332 11.9812 9.61565 12.584 9.01285C13.1868 8.41006 14.0044 8.07141 14.8569 8.07141H21.2854C22.1379 8.07141 22.9555 8.41006 23.5583 9.01285C24.1611 9.61565 24.4997 10.4332 24.4997 11.2857V37C24.4997 37.8525 24.1611 38.67 23.5583 39.2728C22.9555 39.8756 22.1379 40.2143 21.2854 40.2143H14.8569C14.0044 40.2143 13.1868 39.8756 12.584 39.2728C11.9812 38.67 11.6426 37.8525 11.6426 37V11.2857Z"
        fill="currentColor"
      />
    </svg>
  );
}
```

### `src/components/dashboards/gridline-dashboard/assets/logo.tsx`

```tsx
import { cn } from "@/lib/utils";

export function Logo({
  showName = false,
  compact = false,
}: {
  showName?: boolean;
  compact?: boolean;
}) {
  return (
    <div className="flex items-center gap-3">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 40 40"
        fill="none"
        aria-hidden="true"
        className={cn(
          "shrink-0 rounded-lg bg-primary-foreground text-primary",
          compact ? "size-8" : "size-10",
        )}
      >
        <g fill="currentColor">
          <path d="M16.6584 40H15.3817C12.0653 40 10.07 39.9994 8.54578 39.7159L17.3668 28.0344L16.6584 40Z" />
          <path d="M31.2482 39.7514C29.7502 39.9988 27.7875 40 24.6183 40H23.109L22.397 28.0309L31.2482 39.7514Z" />
          <path d="M39.6714 31.6708C39.607 31.9773 39.5312 32.2655 39.4388 32.5426C38.8336 34.3567 37.7711 35.9522 36.3866 37.1982L26.4399 26.044L39.6714 31.6708Z" />
          <path d="M3.4854 37.0792C2.16393 35.8511 1.14735 34.3 0.561036 32.5426C0.459507 32.2382 0.375209 31.921 0.307129 31.5803L13.3239 26.0458L3.4854 37.0792Z" />
          <path d="M40 24.6183C40 25.7428 39.9968 26.7154 39.9858 27.5692L28.1729 23.006H40V24.6183Z" />
          <path d="M11.5962 23.006L0.0106534 27.4805C0.000706105 26.6481 0 25.704 0 24.6183V23.006H11.5962Z" />
          <path d="M24.6183 0C28.7807 0 30.862 0.000553668 32.5426 0.56108C35.7979 1.64713 38.3529 4.20208 39.4389 7.45739C39.9994 9.13798 40 11.2193 40 15.3817V20.016H27.5089L36.5181 16.1861L33.8459 14.4123L25.1048 17.7876L29.2081 12.358L26.2571 11.5501L21.7259 16.6282L21.3885 10.9091H18.3825L18.0398 16.6282L13.5121 11.5501L10.5629 12.358L14.6609 17.7859L5.92507 14.4123L3.25284 16.1861L12.2603 20.016H0V15.3817C0 11.2193 0.000553668 9.13798 0.56108 7.45739C1.64713 4.20208 4.20208 1.64713 7.45739 0.56108C9.13798 0.000553668 11.2193 0 15.3817 0H24.6183Z" />
        </g>
      </svg>
      {showName && <span className="text-lg font-semibold">Gridline</span>}
    </div>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/dashboard/dashboard-content.tsx`

```tsx
import { useEffect, useRef, useState } from "react";
import {
  ArrowUpRight,
  CalendarDays,
  Clock3,
  Command,
  Database,
  GitBranch,
  Radio,
  Search,
  TrendingUp,
  UsersRound,
  Zap,
  type LucideIcon,
} from "lucide-react";

import { WorkspacesIcon } from "../../assets/icons";
import { DashboardLink } from "../navigation";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
  dashboardOverview,
  workspaceCards,
  type WorkspaceCard,
} from "../../data";

const workspaceIcons: Record<WorkspaceCard["icon"], LucideIcon> = {
  database: Database,
  flexibility: Zap,
  planning: TrendingUp,
  operations: Radio,
  stakeholders: UsersRound,
  connections: GitBranch,
};

function SectionTitle({
  children,
  icon: Icon,
}: {
  children: string;
  icon?: LucideIcon;
}) {
  return (
    <h2 className="flex items-center gap-2 text-sm font-medium">
      {Icon ? (
        <Icon aria-hidden="true" className="size-5" strokeWidth={1.6} />
      ) : (
        <WorkspacesIcon aria-hidden="true" className="size-5" />
      )}
      {children}
    </h2>
  );
}

function WorkspaceCardItem({ workspace }: { workspace: WorkspaceCard }) {
  const Icon = workspaceIcons[workspace.icon];

  return (
    <DashboardLink
      href={workspace.href}
      className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
    >
      <Card className="h-full gap-11 bg-secondary shadow-none ring-0 transition-colors hover:bg-secondary/80 dark:bg-card/60 dark:hover:bg-card">
        <CardHeader className="flex flex-row items-start justify-between">
          <span className="grid size-10 place-items-center rounded-lg bg-background">
            <Icon aria-hidden="true" className="size-5" strokeWidth={1.6} />
          </span>
          <ArrowUpRight
            aria-hidden="true"
            className="size-6 text-muted-foreground"
            strokeWidth={1.5}
          />
        </CardHeader>
        <CardContent className="space-y-3">
          <CardTitle>{workspace.title}</CardTitle>
          <CardDescription className="font-mono text-xs">
            <ul className="space-y-2">
              {workspace.details.map((detail) => (
                <li key={detail} className="flex gap-2">
                  <span aria-hidden="true">·</span>
                  <span>{detail}</span>
                </li>
              ))}
            </ul>
          </CardDescription>
        </CardContent>
      </Card>
    </DashboardLink>
  );
}

export function DashboardContent() {
  const [query, setQuery] = useState("");
  const searchInputRef = useRef<HTMLInputElement>(null);
  const normalizedQuery = query.trim().toLowerCase();
  const visibleWorkspaces = workspaceCards.filter((workspace) =>
    [workspace.title, ...workspace.details]
      .join(" ")
      .toLowerCase()
      .includes(normalizedQuery),
  );

  useEffect(() => {
    function focusSearch(event: KeyboardEvent) {
      if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
        event.preventDefault();
        searchInputRef.current?.focus();
      }
    }

    window.addEventListener("keydown", focusSearch);
    return () => window.removeEventListener("keydown", focusSearch);
  }, []);

  return (
    <div className="mx-auto w-full max-w-screen-2xl px-4 py-6 sm:px-6 lg:px-8 lg:pt-12">
      <div className="flex flex-col gap-6 border-b pb-5 sm:flex-row sm:items-end sm:justify-between">
        <div className="space-y-4">
          <h1 className="text-2xl font-medium tracking-tight sm:text-3xl">
            {dashboardOverview.company}
          </h1>
          <p className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground sm:text-base">
            <CalendarDays
              aria-hidden="true"
              className="size-5"
              strokeWidth={1.5}
            />
            {dashboardOverview.date}
          </p>
        </div>

        <div className="relative w-full sm:max-w-md">
          <label htmlFor="network-search" className="sr-only">
            Search the network model
          </label>
          <Search
            aria-hidden="true"
            className="pointer-events-none absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground"
            strokeWidth={1.5}
          />
          <Input
            id="network-search"
            ref={searchInputRef}
            type="search"
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search the network model..."
            className="h-11 rounded-lg border-0 bg-muted pl-11 pr-4 font-mono shadow-none sm:pr-14 [&::-webkit-search-cancel-button]:appearance-none"
          />
          <div className="absolute right-2 top-1/2 -translate-y-1/2">
            <kbd className="pointer-events-none hidden items-center gap-1 rounded-md bg-background px-2 py-1 font-mono text-xs text-muted-foreground sm:flex">
              <Command aria-hidden="true" className="size-3" />K
            </kbd>
          </div>
        </div>
      </div>

      <section className="mt-7">
        <SectionTitle icon={Clock3}>Recents</SectionTitle>
        <div className="flex flex-col items-center gap-5 py-10 sm:py-12">
          <span className="grid size-14 place-items-center rounded-xl bg-muted">
            <Clock3 aria-hidden="true" className="size-7" strokeWidth={1.5} />
          </span>
          <p className="font-mono text-sm text-muted-foreground">
            Your recent works will appear here
          </p>
        </div>
      </section>

      <section className="space-y-6 pb-8">
        <SectionTitle>Workspaces</SectionTitle>

        {visibleWorkspaces.length > 0 ? (
          <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 xl:gap-6">
            {visibleWorkspaces.map((workspace) => (
              <WorkspaceCardItem key={workspace.title} workspace={workspace} />
            ))}
          </div>
        ) : (
          <p className="flex min-h-48 items-center justify-center rounded-xl bg-muted text-center text-sm text-muted-foreground">
            No workspaces match your search.
          </p>
        )}
      </section>
    </div>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/dashboard/flexibility-dashboard.tsx`

```tsx
import { useEffect, useMemo, useState, type ComponentType } from "react";
import {
  Building2,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  CircuitBoard,
  Database,
  FileText,
  GitCompare,
  Grid3X3,
  Map,
  MapPin,
  MessageSquare,
  Minus,
  Network,
  PanelLeft,
  Plus,
  Search,
  SlidersHorizontal,
  Table2,
  X,
  Zap,
} from "lucide-react";

import { Button, buttonVariants } from "@/components/ui/button";
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
  flexibilityDashboard,
  gridAssets,
  networkBasemapUrl,
  networkRegions,
  voltageLevelFilters,
  type GridAsset,
} from "../../data";
import { cn } from "@/lib/utils";

type ToolTab = {
  label: string;
  icon: ComponentType<{ className?: string; "aria-hidden"?: boolean }>;
};

const toolTabs: ToolTab[] = [
  { label: "Browse", icon: Database },
  { label: "Map", icon: Map },
  { label: "Graph", icon: Network },
  { label: "Circuit", icon: CircuitBoard },
  { label: "Table", icon: Table2 },
  { label: "Table++", icon: Grid3X3 },
  { label: "LTDS Table Generator", icon: FileText },
  { label: "Compare", icon: GitCompare },
];

const voltageDotStyles: Record<
  (typeof voltageLevelFilters)[number]["color"],
  string
> = {
  neutral: "bg-foreground",
  green: "bg-green-600",
  yellow: "bg-yellow-400",
  red: "bg-red-500",
  blue: "bg-blue-600",
  brown: "bg-amber-800",
};

const regionDotStyles: Record<string, string> = {
  blue: "bg-blue-500",
  purple: "bg-violet-500",
  red: "bg-red-500",
  green: "bg-emerald-500",
};

function ToolNavigation({
  activeTool,
  onToolChange,
}: {
  activeTool: string;
  onToolChange: (tool: string) => void;
}) {
  return (
    <nav
      aria-label="Asset workspace tools"
      className="flex min-w-0 flex-1 items-center justify-between overflow-x-auto px-2 sm:justify-start sm:gap-1"
    >
      {toolTabs.map(({ label, icon: Icon }) => (
        <Button
          key={label}
          type="button"
          variant="ghost"
          size="icon-sm"
          aria-label={label}
          aria-pressed={activeTool === label}
          onClick={() => onToolChange(label)}
          className={cn(
            "shrink-0 gap-2 font-mono text-xs text-muted-foreground sm:w-auto sm:px-2.5",
            activeTool === label &&
              "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary",
          )}
        >
          <Icon aria-hidden={true} className="size-4" />
          <span className="hidden sm:inline">{label}</span>
        </Button>
      ))}
    </nav>
  );
}

function AssetList({
  selectedAsset,
  onSelectAsset,
  showHeader = true,
}: {
  selectedAsset: GridAsset | null;
  onSelectAsset: (asset: GridAsset) => void;
  showHeader?: boolean;
}) {
  const [query, setQuery] = useState("");
  const [expandedAssets, setExpandedAssets] = useState<Set<string>>(
    () => new Set(selectedAsset ? [selectedAsset.id] : []),
  );
  const visibleAssets = useMemo(() => {
    const normalizedQuery = query.trim().toLowerCase();
    return gridAssets.filter((asset) =>
      `${asset.name} ${asset.voltage}`.toLowerCase().includes(normalizedQuery),
    );
  }, [query]);

  function toggleAsset(asset: GridAsset) {
    onSelectAsset(asset);
    setExpandedAssets((current) => {
      const next = new Set(current);
      if (next.has(asset.id)) next.delete(asset.id);
      else next.add(asset.id);
      return next;
    });
  }

  return (
    <div className="flex min-h-0 flex-1 flex-col bg-background">
      <div className="border-b p-3">
        {showHeader && (
          <div className="pr-10">
            <h2 className="text-sm font-medium">
              {flexibilityDashboard.regionName} Assets
            </h2>
            <p className="mt-1 font-mono text-xs text-muted-foreground">
              {flexibilityDashboard.substationCount} Substations
            </p>
          </div>
        )}
        <div className={cn("relative", showHeader && "mt-4")}>
          <Search
            aria-hidden="true"
            className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
          />
          <Input
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search"
            className="border-0 bg-muted pl-9 font-mono text-xs shadow-none"
          />
        </div>
      </div>

      <div className="min-h-0 flex-1 overflow-y-auto p-2">
        {visibleAssets.map((asset) => {
          const isExpanded = expandedAssets.has(asset.id);

          return (
            <div key={asset.id}>
              <button
                type="button"
                aria-expanded={isExpanded}
                onClick={() => toggleAsset(asset)}
                className={cn(
                  "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring",
                  selectedAsset?.id === asset.id && "bg-primary/10",
                )}
              >
                {isExpanded ? (
                  <ChevronDown
                    aria-hidden="true"
                    className="size-3.5 shrink-0 text-muted-foreground"
                  />
                ) : (
                  <ChevronRight
                    aria-hidden="true"
                    className="size-3.5 shrink-0 text-muted-foreground"
                  />
                )}
                <Zap
                  aria-hidden="true"
                  className={cn(
                    "size-3.5 shrink-0 text-muted-foreground",
                    selectedAsset?.id === asset.id && "text-primary",
                  )}
                />
                <span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
                  {asset.name} {asset.voltage}
                </span>
                <span className="shrink-0 font-mono text-xs text-muted-foreground">
                  {asset.equipmentCount}
                </span>
              </button>
              {isExpanded && (
                <div className="mx-2 mb-1 grid grid-cols-2 gap-2 px-8 py-2 font-mono text-xs text-muted-foreground">
                  <span>Voltage</span>
                  <span className="text-right">{asset.voltage}</span>
                  <span>Equipment</span>
                  <span className="text-right">
                    {asset.equipmentCount} items
                  </span>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function FilterPanel({
  className,
  onCollapse,
}: {
  className?: string;
  onCollapse?: () => void;
}) {
  const [enabledVoltages, setEnabledVoltages] = useState<Set<string>>(
    () => new Set(voltageLevelFilters.map(({ id }) => id)),
  );
  const [showSubstations, setShowSubstations] = useState(true);

  function setVoltageEnabled(id: string, enabled: boolean) {
    setEnabledVoltages((current) => {
      const next = new Set(current);
      if (enabled) next.add(id);
      else next.delete(id);
      return next;
    });
  }

  return (
    <div
      className={cn(
        "flex h-full max-h-full min-h-0 flex-col rounded-xl bg-background/95 p-3 font-mono text-xs shadow-xl backdrop-blur-xl",
        className,
      )}
    >
      <div className="flex shrink-0 items-center justify-between gap-3 pb-3">
        <h2 className="font-medium">Voltage Levels</h2>
        {onCollapse && (
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            aria-label="Collapse map filters"
            onClick={onCollapse}
          >
            <ChevronLeft aria-hidden="true" />
          </Button>
        )}
      </div>
      <div className="min-h-0 flex-1 space-y-4 overflow-x-hidden overflow-y-auto">
        <div className="space-y-1 border-b pb-3">
          {voltageLevelFilters.map((level) => (
            <div
              key={level.id}
              className="flex items-center justify-between gap-3 rounded-md bg-secondary/70 px-2 py-1.5"
            >
              <span className="flex items-center gap-2">
                <span
                  className={cn(
                    "size-2 rounded-full",
                    voltageDotStyles[level.color],
                  )}
                />
                {level.label}
              </span>
              <Switch
                size="sm"
                checked={enabledVoltages.has(level.id)}
                onCheckedChange={(checked) =>
                  setVoltageEnabled(level.id, checked)
                }
                aria-label={`Show ${level.label}`}
              />
            </div>
          ))}
        </div>

        <div className="space-y-2 border-b pb-3">
          <p className="font-medium">Option</p>
          <div className="flex items-center justify-between rounded-md bg-secondary/70 px-2 py-1.5">
            <span>Show Substations</span>
            <Switch
              size="sm"
              checked={showSubstations}
              onCheckedChange={setShowSubstations}
              aria-label="Show substations"
            />
          </div>
        </div>

        <div className="space-y-1 border-b pb-3">
          <p className="pb-1 font-medium">Grid Regions</p>
          {networkRegions.map((region) => (
            <button
              key={`${region.legendName}-${region.color}`}
              type="button"
              className={cn(
                "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-muted-foreground transition-colors hover:bg-muted",
                region.legendName === flexibilityDashboard.regionName &&
                  "bg-emerald-500/10 text-emerald-500",
              )}
            >
              <span
                className={cn(
                  "size-2 rounded-sm",
                  regionDotStyles[region.color],
                )}
              />
              <span className="flex-1">{region.legendName}</span>
              {region.legendName === flexibilityDashboard.regionName && (
                <span className="text-xs">Loaded</span>
              )}
            </button>
          ))}
          <Button
            type="button"
            variant="ghost"
            size="sm"
            className="w-full justify-start text-xs text-muted-foreground"
          >
            <MapPin aria-hidden="true" className="size-4" />
            Reset View
          </Button>
        </div>

        <div className="space-y-1">
          <p className="font-medium">Loaded</p>
          <p className="text-muted-foreground">
            {flexibilityDashboard.substationCount} Substations
          </p>
          <p className="text-emerald-500">
            {flexibilityDashboard.mappedSubstationCount} mapped on map
          </p>
          <p className="pt-2 text-muted-foreground">
            Location DB: {flexibilityDashboard.locationCount} entries
          </p>
        </div>
      </div>
    </div>
  );
}

function DetailsPanel({
  asset,
  showHeader = true,
}: {
  asset: GridAsset;
  showHeader?: boolean;
}) {
  const [overviewOpen, setOverviewOpen] = useState(true);
  const [identifierOpen, setIdentifierOpen] = useState(false);

  return (
    <div className="flex h-full flex-col bg-background">
      {showHeader && (
        <div className="border-b p-4">
          <div>
            <h2 className="text-sm font-medium">Details</h2>
            <p className="font-mono text-xs text-muted-foreground">
              {flexibilityDashboard.assetType}
            </p>
          </div>
        </div>
      )}

      <div className="space-y-6 overflow-y-auto p-4">
        <span className="grid size-10 place-items-center rounded-lg bg-secondary">
          <Building2 aria-hidden="true" className="size-5" />
        </span>
        <div>
          <h3 className="text-base font-medium">
            {asset.name} {asset.voltage}
          </h3>
          <span className="mt-2 inline-flex rounded-md bg-secondary px-2 py-1 font-mono text-xs text-muted-foreground">
            {flexibilityDashboard.assetType}
          </span>
        </div>

        <section className="border-t pt-4">
          <button
            type="button"
            aria-expanded={overviewOpen}
            onClick={() => setOverviewOpen((open) => !open)}
            className="flex w-full items-center gap-2 font-mono text-xs"
          >
            {overviewOpen ? (
              <ChevronDown aria-hidden="true" className="size-4" />
            ) : (
              <ChevronRight aria-hidden="true" className="size-4" />
            )}
            Overview
          </button>
          {overviewOpen && (
            <dl className="mt-4 space-y-4 font-mono text-xs">
              <div className="flex items-start justify-between gap-4">
                <dt className="text-muted-foreground">Name</dt>
                <dd className="text-right">
                  {asset.name} {asset.voltage}
                </dd>
              </div>
              <div className="flex items-start justify-between gap-4">
                <dt className="text-muted-foreground">Equipment</dt>
                <dd>{asset.equipmentCount} Items</dd>
              </div>
              <div className="flex items-start justify-between gap-4">
                <dt className="text-muted-foreground">Voltage Levels</dt>
                <dd>{flexibilityDashboard.voltageLevels.length}</dd>
              </div>
            </dl>
          )}
        </section>

        <section className="border-y py-4">
          <button
            type="button"
            aria-expanded={identifierOpen}
            onClick={() => setIdentifierOpen((open) => !open)}
            className="flex w-full items-center gap-2 font-mono text-xs"
          >
            {identifierOpen ? (
              <ChevronDown aria-hidden="true" className="size-4" />
            ) : (
              <ChevronRight aria-hidden="true" className="size-4" />
            )}
            Identifier
          </button>
          {identifierOpen && (
            <dl className="mt-4 space-y-3 font-mono text-xs">
              <div className="flex items-start justify-between gap-4">
                <dt className="text-muted-foreground">Asset ID</dt>
                <dd>{asset.id}</dd>
              </div>
              <div className="flex items-start justify-between gap-4">
                <dt className="text-muted-foreground">Region</dt>
                <dd>{flexibilityDashboard.regionName}</dd>
              </div>
            </dl>
          )}
        </section>
      </div>
    </div>
  );
}

function MobileUtilities({
  selectedAsset,
  onSelectAsset,
  detailsOpen,
  onDetailsOpenChange,
}: {
  selectedAsset: GridAsset | null;
  onSelectAsset: (asset: GridAsset) => void;
  detailsOpen: boolean;
  onDetailsOpenChange: (open: boolean) => void;
}) {
  return (
    <div className="pointer-events-none absolute inset-x-3 top-3 z-20 flex items-start justify-between lg:hidden">
      <div className="pointer-events-auto flex gap-2">
        <Drawer>
          <DrawerTrigger
            className={buttonVariants({ variant: "secondary", size: "sm" })}
          >
            <PanelLeft aria-hidden="true" className="size-4" />
            Assets
          </DrawerTrigger>
          <DrawerContent className="h-4/5">
            <DrawerHeader className="sr-only">
              <DrawerTitle>
                {flexibilityDashboard.regionName} Assets
              </DrawerTitle>
              <DrawerDescription>
                Browse available substations
              </DrawerDescription>
            </DrawerHeader>
            <DrawerClose
              aria-label="Close assets"
              className={cn(
                buttonVariants({ variant: "ghost", size: "icon-sm" }),
                "absolute right-4 top-4 z-10",
              )}
            >
              <X aria-hidden="true" />
            </DrawerClose>
            <AssetList
              selectedAsset={selectedAsset}
              onSelectAsset={onSelectAsset}
            />
          </DrawerContent>
        </Drawer>

        <Drawer>
          <DrawerTrigger
            className={buttonVariants({
              variant: "secondary",
              size: "icon-sm",
            })}
          >
            <SlidersHorizontal aria-hidden="true" />
            <span className="sr-only">Filters</span>
          </DrawerTrigger>
          <DrawerContent className="h-4/5">
            <DrawerHeader className="sr-only">
              <DrawerTitle>Map filters</DrawerTitle>
              <DrawerDescription>
                Filter voltage levels and grid regions
              </DrawerDescription>
            </DrawerHeader>
            <DrawerClose
              aria-label="Close filters"
              className={cn(
                buttonVariants({ variant: "ghost", size: "icon-sm" }),
                "absolute right-4 top-4 z-10",
              )}
            >
              <X aria-hidden="true" />
            </DrawerClose>
            <FilterPanel className="rounded-none bg-background p-4 shadow-none backdrop-blur-none" />
          </DrawerContent>
        </Drawer>
      </div>

      {selectedAsset && (
        <Drawer open={detailsOpen} onOpenChange={onDetailsOpenChange}>
          <DrawerTrigger
            className={cn(
              buttonVariants({ variant: "secondary", size: "sm" }),
              "pointer-events-auto",
            )}
          >
            <Building2 aria-hidden="true" className="size-4" />
            Details
          </DrawerTrigger>
          <DrawerContent className="h-4/5">
            <DrawerHeader className="sr-only">
              <DrawerTitle>Substation details</DrawerTitle>
              <DrawerDescription>
                Details for the selected substation
              </DrawerDescription>
            </DrawerHeader>
            <DrawerClose
              aria-label="Close details"
              className={cn(
                buttonVariants({ variant: "ghost", size: "icon-sm" }),
                "absolute right-4 top-4 z-10",
              )}
            >
              <X aria-hidden="true" />
            </DrawerClose>
            <DetailsPanel asset={selectedAsset} />
          </DrawerContent>
        </Drawer>
      )}
    </div>
  );
}

function AssetMapPoint({
  asset,
  selected,
  onSelect,
}: {
  asset: GridAsset;
  selected: boolean;
  onSelect: () => void;
}) {
  return (
    <button
      type="button"
      aria-label={`Load ${asset.name} ${asset.voltage}`}
      aria-pressed={selected}
      onClick={onSelect}
      className={cn(
        "group absolute z-10 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-primary shadow-lg outline-none transition-transform hover:scale-125 focus-visible:ring-2 focus-visible:ring-ring",
        selected && "scale-125 ring-4 ring-primary/25",
      )}
      style={{
        left: `${asset.mapPosition.x}%`,
        top: `${asset.mapPosition.y}%`,
      }}
    >
      <span className="pointer-events-none absolute bottom-full left-1/2 mb-2 w-max max-w-40 -translate-x-1/2 rounded-md bg-foreground px-2 py-1 font-mono text-xs text-background opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
        {asset.name} {asset.voltage}
      </span>
    </button>
  );
}

export function FlexibilityDashboard() {
  const [activeTool, setActiveTool] = useState("Browse");
  const [selectedAsset, setSelectedAsset] = useState<GridAsset | null>(null);
  const [showAssetsPanel, setShowAssetsPanel] = useState(true);
  const [showFilterPanel, setShowFilterPanel] = useState(false);
  const [mobileDetailsOpen, setMobileDetailsOpen] = useState(false);
  const [isCompact, setIsCompact] = useState(() =>
    typeof window === "undefined"
      ? false
      : !window.matchMedia("(min-width: 64rem)").matches,
  );
  const [zoom, setZoom] = useState(1);

  useEffect(() => {
    const desktopQuery = window.matchMedia("(min-width: 64rem)");
    const syncViewport = () => setIsCompact(!desktopQuery.matches);

    syncViewport();
    desktopQuery.addEventListener("change", syncViewport);
    return () => desktopQuery.removeEventListener("change", syncViewport);
  }, []);

  function selectAsset(asset: GridAsset, openMobileDetails = false) {
    setSelectedAsset(asset);
    if (openMobileDetails && isCompact) setMobileDetailsOpen(true);
  }

  function clearSelection() {
    setMobileDetailsOpen(false);
    setSelectedAsset(null);
  }

  function handleMobileDetailsChange(open: boolean) {
    if (open) {
      setMobileDetailsOpen(true);
      return;
    }

    clearSelection();
  }

  return (
    <div className="absolute inset-x-0 bottom-0 top-18 overflow-hidden bg-background md:left-17">
      <header className="absolute inset-x-0 top-0 z-20 flex h-14 items-center border-b bg-background/95 backdrop-blur">
        {showAssetsPanel ? (
          <div className="hidden w-64 shrink-0 items-center justify-between border-r px-4 lg:flex">
            <div>
              <p className="text-sm font-medium">
                {flexibilityDashboard.regionName} Assets
              </p>
              <p className="font-mono text-xs text-muted-foreground">
                {flexibilityDashboard.substationCount} Substations
              </p>
            </div>
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              aria-label="Collapse assets panel"
              onClick={() => setShowAssetsPanel(false)}
            >
              <ChevronLeft aria-hidden="true" />
            </Button>
          </div>
        ) : (
          <div className="hidden shrink-0 border-r px-2 lg:flex">
            <Button
              type="button"
              variant="ghost"
              size="sm"
              onClick={() => setShowAssetsPanel(true)}
            >
              <PanelLeft aria-hidden="true" />
              Assets
            </Button>
          </div>
        )}
        <ToolNavigation activeTool={activeTool} onToolChange={setActiveTool} />
        {selectedAsset && (
          <div className="hidden w-64 shrink-0 items-center justify-between border-l px-4 lg:flex">
            <div>
              <p className="text-sm font-medium">Details</p>
              <p className="font-mono text-xs text-muted-foreground">
                {selectedAsset.name}
              </p>
            </div>
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              aria-label="Collapse details panel"
              onClick={clearSelection}
            >
              <ChevronRight aria-hidden="true" />
            </Button>
          </div>
        )}
      </header>

      <div className="absolute inset-x-0 bottom-0 top-14 flex">
        {showAssetsPanel && (
          <aside className="hidden w-64 shrink-0 border-r lg:flex">
            <AssetList
              selectedAsset={selectedAsset}
              onSelectAsset={selectAsset}
              showHeader={false}
            />
          </aside>
        )}

        <section
          aria-label="Substation asset map"
          className="relative min-w-0 flex-1 overflow-hidden bg-black"
        >
          <img
            src={networkBasemapUrl}
            alt="Substation asset map"
            className="absolute inset-0 size-full object-cover object-center invert transition-transform duration-300 dark:invert-0"
            style={{ transform: `scale(${zoom})` }}
          />
          <div className="absolute inset-0 bg-black/10" />

          <MobileUtilities
            selectedAsset={selectedAsset}
            onSelectAsset={selectAsset}
            detailsOpen={mobileDetailsOpen && isCompact}
            onDetailsOpenChange={handleMobileDetailsChange}
          />

          {showFilterPanel ? (
            <aside className="absolute inset-y-3 left-3 z-10 hidden w-48 overflow-hidden rounded-xl xl:block">
              <FilterPanel onCollapse={() => setShowFilterPanel(false)} />
            </aside>
          ) : (
            <Button
              type="button"
              variant="secondary"
              size="sm"
              onClick={() => setShowFilterPanel(true)}
              className="absolute left-3 top-3 z-10 hidden shadow-lg xl:inline-flex"
            >
              <SlidersHorizontal aria-hidden="true" />
              Filters
            </Button>
          )}

          <div className="absolute bottom-3 right-3 z-10 flex flex-col rounded-lg bg-background p-1 shadow-lg">
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              aria-label="Zoom in"
              onClick={() => setZoom((value) => Math.min(1.4, value + 0.1))}
            >
              <Plus aria-hidden="true" />
            </Button>
            <Button
              type="button"
              variant="ghost"
              size="icon-sm"
              aria-label="Zoom out"
              onClick={() => setZoom((value) => Math.max(1, value - 0.1))}
            >
              <Minus aria-hidden="true" />
            </Button>
          </div>

          {gridAssets.map((asset) => (
            <AssetMapPoint
              key={asset.id}
              asset={asset}
              selected={selectedAsset?.id === asset.id}
              onSelect={() => selectAsset(asset, true)}
            />
          ))}

          <div className="absolute bottom-3 left-1/2 hidden -translate-x-1/2 items-center gap-2 sm:flex">
            <Button
              type="button"
              variant="secondary"
              size="sm"
              className="font-mono text-xs text-muted-foreground"
            >
              <MessageSquare aria-hidden="true" />
              Feedback
            </Button>
            <div
              className={cn(
                buttonVariants({ variant: "secondary", size: "sm" }),
                "bg-background/95 font-mono text-xs shadow-lg backdrop-blur",
              )}
            >
              <span className="text-muted-foreground">Attribution: </span>
              <span>{flexibilityDashboard.attributionCompany} </span>
              <Network
                aria-hidden="true"
                className="inline size-4 text-primary"
              />
              <span className="text-primary">
                {" "}
                {flexibilityDashboard.attributionProduct}
              </span>
            </div>
          </div>
        </section>

        {selectedAsset && (
          <aside className="hidden w-64 shrink-0 border-l lg:block">
            <DetailsPanel asset={selectedAsset} showHeader={false} />
          </aside>
        )}
      </div>
    </div>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/dashboard/modelling-dashboard.tsx`

```tsx
import { useEffect, useState } from "react";
import {
  ArrowUpRight,
  ChevronDown,
  ChevronLeft,
  Database,
  GitBranch,
  MapPin,
  Shield,
  type LucideIcon,
} from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
} from "@/components/ui/drawer";
import {
  modellingActions,
  modellingDashboard,
  modelWorkflowStatuses,
  networkBasemapUrl,
  networkRegions,
  type ModellingAction,
  type NetworkRegion,
} from "../../data";
import { cn } from "@/lib/utils";

const actionIcons: Record<ModellingAction["icon"], LucideIcon> = {
  database: Database,
  comparison: GitBranch,
  clearview: Shield,
};

const markerStyles: Record<NetworkRegion["color"], string> = {
  blue: "border-blue-700 bg-blue-500",
  purple: "border-violet-700 bg-violet-500",
  red: "border-red-700 bg-red-500",
  green: "border-emerald-700 bg-emerald-500",
};

const markerPositions: Record<NetworkRegion["position"], string> = {
  "north-east": "right-3 top-[9%] lg:left-[72%] lg:right-auto",
  central: "right-12 top-[28%] lg:left-[57%] lg:right-auto",
  west: "left-3 top-[50%] lg:left-[40%]",
  "south-east": "right-6 top-[71%] lg:left-[68%] lg:right-auto",
};

function StatusDotLabel({ children }: { children: string }) {
  return (
    <p className="flex items-center gap-2 text-xs text-muted-foreground">
      <span className="size-1.5 rounded-full bg-current" />
      {children}
    </p>
  );
}

function ModelSummary() {
  return (
    <div className="grid grid-cols-2 gap-3">
      <div className="space-y-7 rounded-xl bg-secondary/70 p-4">
        <StatusDotLabel>Current Model</StatusDotLabel>
        <div className="space-y-2">
          <p className="text-sm font-medium">
            {modellingDashboard.currentModel}
          </p>
          <p className="font-mono text-xs text-primary">
            {modellingDashboard.currentVersion}
          </p>
        </div>
      </div>

      <div className="space-y-7 rounded-xl bg-secondary/70 p-4">
        <StatusDotLabel>Latest Update</StatusDotLabel>
        <p className="text-sm font-medium leading-snug">
          {modellingDashboard.latestUpdate}
        </p>
      </div>

      <div className="col-span-2 space-y-7 rounded-xl bg-secondary/70 p-4">
        <StatusDotLabel>Status</StatusDotLabel>
        <div className="space-y-3">
          <p className="flex items-end gap-2">
            <span className="text-base font-medium">
              {modellingDashboard.coverage}
            </span>
            <span className="text-xs text-muted-foreground">Coverage</span>
          </p>
          <div className="flex flex-wrap items-center justify-between gap-2 text-xs">
            <p className="text-muted-foreground">
              Last Published:{" "}
              <span className="text-foreground/80">
                {modellingDashboard.lastPublished}
              </span>
            </p>
            <p className="text-amber-500">
              {modellingDashboard.openIssues} open issues
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

function PrimaryAction({ action }: { action: ModellingAction }) {
  const Icon = actionIcons[action.icon];

  return (
    <button
      type="button"
      className="group flex min-h-40 flex-col justify-between gap-8 rounded-xl bg-secondary/70 p-4 text-left outline-none transition-colors hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring"
    >
      <span className="flex w-full items-start justify-between">
        <span className="grid size-10 place-items-center rounded-lg bg-background">
          <Icon aria-hidden="true" className="size-5" strokeWidth={1.5} />
        </span>
        <ArrowUpRight
          aria-hidden="true"
          className="size-5 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
          strokeWidth={1.5}
        />
      </span>
      <span className="space-y-2">
        <span className="block text-sm font-medium">{action.title}</span>
        <span className="block font-mono text-xs leading-relaxed text-muted-foreground">
          {action.description}
        </span>
      </span>
    </button>
  );
}

function RegionMarker({
  region,
  selected,
  onSelect,
}: {
  region: NetworkRegion;
  selected: boolean;
  onSelect: () => void;
}) {
  return (
    <button
      type="button"
      aria-pressed={selected}
      onClick={onSelect}
      className={cn(
        "absolute w-36 rounded-xl border-2 p-2 text-left text-primary-foreground shadow-lg outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-foreground lg:w-48 lg:p-4",
        markerStyles[region.color],
        markerPositions[region.position],
        selected && "border-foreground",
      )}
    >
      <span className="block text-sm font-medium">{region.name}</span>
      <span className="mt-2 flex items-center gap-2 font-mono text-xs text-primary-foreground/80">
        Click to load
        <ArrowUpRight aria-hidden="true" className="size-4" />
      </span>
      <span
        aria-hidden="true"
        className={cn(
          "pointer-events-none absolute left-1/2 top-full size-3 -translate-x-1/2 -translate-y-1/2 rotate-45 border-b-2 border-r-2",
          markerStyles[region.color],
        )}
      />
    </button>
  );
}

function MapLegend() {
  return (
    <div className="absolute bottom-5 right-5 hidden space-y-1.5 rounded-lg bg-black/30 p-3 backdrop-blur-sm lg:block">
      {networkRegions.map((region) => (
        <div
          key={`${region.legendName}-${region.color}`}
          className="flex items-center gap-2 font-mono text-xs text-primary-foreground/80"
        >
          <span
            className={cn(
              "size-2 rounded-sm",
              markerStyles[region.color].split(" ")[1],
            )}
          />
          {region.legendName}
        </div>
      ))}
    </div>
  );
}

function Map({
  selectedRegion,
  onSelectRegion,
}: {
  selectedRegion: NetworkRegion | null;
  onSelectRegion: (region: NetworkRegion) => void;
}) {
  return (
    <div className="absolute inset-0 overflow-hidden bg-black">
      <img
        src={networkBasemapUrl}
        alt="Network regions across Maryland and Delaware"
        className="size-full object-cover object-center invert dark:invert-0"
      />
      <div className="absolute inset-0 bg-black/10" />
      {networkRegions.map((region) => {
        const key = `${region.name}-${region.color}`;
        return (
          <RegionMarker
            key={key}
            region={region}
            selected={selectedRegion?.position === region.position}
            onSelect={() => onSelectRegion(region)}
          />
        );
      })}
      <MapLegend />
    </div>
  );
}

function InformationPanelHeader({
  region,
  mobile = false,
  onCollapse,
}: {
  region: NetworkRegion;
  mobile?: boolean;
  onCollapse: () => void;
}) {
  const CollapseIcon = mobile ? ChevronDown : ChevronLeft;

  return (
    <header className="flex items-center justify-between gap-4">
      <div className="flex min-w-0 items-center gap-3">
        <span className="grid size-10 shrink-0 place-items-center rounded-lg bg-secondary">
          <Database aria-hidden="true" className="size-5" strokeWidth={1.5} />
        </span>
        <div className="min-w-0">
          <h1 className="truncate text-base font-medium">
            {modellingDashboard.title}
          </h1>
          <p className="truncate font-mono text-xs text-muted-foreground">
            {region.name}
          </p>
        </div>
      </div>
      <Button
        type="button"
        variant="ghost"
        size="icon-lg"
        aria-label="Collapse model information"
        onClick={onCollapse}
        className="shrink-0 text-muted-foreground"
      >
        <CollapseIcon aria-hidden="true" className="size-5" />
      </Button>
    </header>
  );
}

function InformationPanelBody({ region }: { region: NetworkRegion }) {
  return (
    <div className="space-y-8">
      <section className="space-y-3">
        <p className="max-w-sm font-mono text-xs leading-relaxed text-muted-foreground">
          {modellingDashboard.description}
        </p>
        <div className="flex flex-wrap items-center gap-1 font-mono text-xs">
          <span className="flex items-center gap-1.5 rounded-md bg-secondary px-2 py-1">
            <MapPin aria-hidden="true" className="size-3.5 text-primary" />
            {region.name}
          </span>
          <span className="rounded-md bg-secondary px-2 py-1 text-muted-foreground">
            {modellingDashboard.version}
          </span>
          <span className="flex items-center gap-2 rounded-md bg-secondary px-2 py-1 text-primary">
            <span className="size-1.5 rounded-full bg-primary" />
            {modelWorkflowStatuses.publication}
          </span>
          <span className="rounded-md bg-secondary px-2 py-1 text-emerald-500">
            {modelWorkflowStatuses.validation}
          </span>
        </div>
      </section>

      <ModelSummary />

      <section className="space-y-4">
        <h2 className="flex items-center gap-2 font-mono text-base text-foreground/80">
          <ArrowUpRight aria-hidden="true" className="size-5" />
          Primary Actions
        </h2>
        <div className="grid gap-3 sm:grid-cols-2">
          {modellingActions.map((action) => (
            <PrimaryAction key={action.title} action={action} />
          ))}
        </div>
      </section>
    </div>
  );
}

function DesktopInformationPanel({
  region,
  onCollapse,
}: {
  region: NetworkRegion;
  onCollapse: () => void;
}) {
  return (
    <article className="absolute inset-y-4 left-4 z-10 hidden w-96 flex-col overflow-hidden rounded-xl bg-background/95 p-6 shadow-2xl backdrop-blur-xl md:flex">
      <div className="shrink-0">
        <InformationPanelHeader region={region} onCollapse={onCollapse} />
      </div>
      <div className="mt-6 min-h-0 flex-1 overflow-y-auto">
        <InformationPanelBody region={region} />
      </div>
    </article>
  );
}

function MobileInformationDrawer({
  region,
  open,
  onCollapse,
}: {
  region: NetworkRegion;
  open: boolean;
  onCollapse: () => void;
}) {
  return (
    <Drawer
      open={open}
      onOpenChange={(nextOpen) => {
        if (!nextOpen) onCollapse();
      }}
    >
      <DrawerContent className="md:hidden">
        <DrawerHeader className="sr-only">
          <DrawerTitle>{modellingDashboard.title}</DrawerTitle>
          <DrawerDescription>
            Network model information and primary actions
          </DrawerDescription>
        </DrawerHeader>
        <article className="flex min-h-0 flex-1 flex-col px-5 pb-8 pt-3">
          <div className="shrink-0">
            <InformationPanelHeader
              region={region}
              mobile
              onCollapse={onCollapse}
            />
          </div>
          <div className="mt-6 min-h-0 flex-1 overflow-y-auto">
            <InformationPanelBody region={region} />
          </div>
        </article>
      </DrawerContent>
    </Drawer>
  );
}

export function ModellingDashboard() {
  const [selectedRegion, setSelectedRegion] = useState<NetworkRegion | null>(
    null,
  );
  const [isMobile, setIsMobile] = useState(() =>
    typeof window === "undefined"
      ? false
      : !window.matchMedia("(min-width: 48rem)").matches,
  );

  useEffect(() => {
    const desktopQuery = window.matchMedia("(min-width: 48rem)");
    const syncViewport = () => setIsMobile(!desktopQuery.matches);

    syncViewport();
    desktopQuery.addEventListener("change", syncViewport);
    return () => desktopQuery.removeEventListener("change", syncViewport);
  }, []);

  const clearSelection = () => setSelectedRegion(null);

  return (
    <div className="absolute inset-x-0 bottom-0 top-18 overflow-hidden bg-background md:left-17">
      <Map selectedRegion={selectedRegion} onSelectRegion={setSelectedRegion} />
      {selectedRegion && (
        <>
          <DesktopInformationPanel
            region={selectedRegion}
            onCollapse={clearSelection}
          />
          <MobileInformationDrawer
            region={selectedRegion}
            open={isMobile}
            onCollapse={clearSelection}
          />
        </>
      )}
    </div>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/layout/app-sidebar.tsx`

```tsx
import {
  ChevronLeft,
  Database,
  FileText,
  GitBranch,
  House,
  MoonStar,
  Radio,
  SunDim,
  TrendingUp,
  UsersRound,
  Zap,
} from "lucide-react";
import { useState } from "react";

import { buttonVariants } from "@/components/ui/button";
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";

import { SidebarToggleIcon } from "../../assets/icons";
import { Logo } from "../../assets/logo";
import { ProfileMenu } from "../shared/profile-menu";
import {
  SidebarNavigationItem,
  type NavigationItem,
} from "./sidebar-navigation-item";

export const primaryNavigation: NavigationItem[] = [
  { href: "/", label: "Dashboard", shortLabel: "Dashboard", icon: House },
  {
    href: "/modelling",
    label: "System modelling and data",
    shortLabel: "Modelling",
    icon: Database,
  },
  {
    href: "/flexibility",
    label: "Flexibility",
    shortLabel: "Flexibility",
    icon: Zap,
  },
  {
    href: "/planning",
    label: "Planning and investment",
    shortLabel: "Planning",
    icon: TrendingUp,
  },
  {
    href: "/operations",
    label: "Operations interface",
    shortLabel: "Operations",
    icon: Radio,
  },
  {
    href: "/stakeholders",
    label: "Stakeholders and public",
    shortLabel: "Stakeholders",
    icon: UsersRound,
  },
  {
    href: "/connections",
    label: "Connections and queue",
    shortLabel: "Connections",
    icon: GitBranch,
  },
];

type ThemeProps = {
  isDark: boolean;
  onThemeChange: (isDark: boolean) => void;
};

export function AppSidebar() {
  return (
    <aside
      aria-label="Primary navigation"
      className="absolute inset-y-0 left-0 z-30 hidden w-17 flex-col items-center justify-between border-r border-sidebar-border bg-sidebar px-2.5 py-4 text-sidebar-foreground md:flex"
    >
      <div className="flex w-full flex-col items-center gap-5">
        <div className="flex w-full justify-center border-b border-sidebar-border pb-4">
          <Logo />
        </div>

        <nav className="flex flex-col items-center gap-4 border-b border-sidebar-border pb-3">
          {primaryNavigation.map((item) => (
            <SidebarNavigationItem key={item.href} item={item} />
          ))}
        </nav>
      </div>

      <div className="flex flex-col items-center gap-4">
        <Tooltip>
          <TooltipTrigger
            type="button"
            aria-label="Documents"
            className={cn(
              buttonVariants({ variant: "ghost", size: "icon-lg" }),
              "rounded-lg text-muted-foreground",
            )}
          >
            <FileText aria-hidden="true" className="size-6" strokeWidth={1.6} />
          </TooltipTrigger>
          <TooltipContent side="right">Documents</TooltipContent>
        </Tooltip>
        <ProfileMenu placement="sidebar" />
      </div>
    </aside>
  );
}

export function MobileNavigation({ isDark, onThemeChange }: ThemeProps) {
  const [open, setOpen] = useState(false);

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger
        aria-label="Open navigation menu"
        className={cn(
          buttonVariants({ variant: "ghost", size: "icon-lg" }),
          "text-foreground md:hidden",
        )}
      >
        <SidebarToggleIcon aria-hidden="true" className="size-5" />
      </SheetTrigger>

      <SheetContent
        side="left"
        showCloseButton={false}
        className="w-4/5 gap-0 border-sidebar-border bg-sidebar p-0 text-sidebar-foreground sm:max-w-xs"
      >
        <SheetHeader className="flex-row items-center justify-between gap-3 border-b border-sidebar-border">
          <SheetTitle>
            <Logo showName compact />
          </SheetTitle>
          <SheetClose
            aria-label="Close navigation menu"
            className={cn(
              buttonVariants({ variant: "ghost", size: "icon-lg" }),
              "text-muted-foreground",
            )}
          >
            <ChevronLeft aria-hidden="true" className="size-6" />
          </SheetClose>
          <SheetDescription className="sr-only">
            Navigate between dashboard pages
          </SheetDescription>
        </SheetHeader>

        <nav className="flex flex-col gap-1 p-3">
          {primaryNavigation.map((item) => (
            <SidebarNavigationItem
              key={item.href}
              item={item}
              showLabel
              onNavigate={() => setOpen(false)}
            />
          ))}
        </nav>

        <SheetFooter className="border-t border-sidebar-border">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2 text-sm font-medium">
              {isDark ? (
                <MoonStar aria-hidden="true" className="size-4" />
              ) : (
                <SunDim aria-hidden="true" className="size-4.5" />
              )}
              <span>{isDark ? "Dark Mode" : "Light Mode"}</span>
            </div>
            <Switch
              checked={isDark}
              onCheckedChange={onThemeChange}
              aria-label={`Switch to ${isDark ? "light" : "dark"} mode`}
              className="data-unchecked:bg-muted"
            />
          </div>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/layout/dashboard-shell.tsx`

```tsx
import { useEffect, useState, type ReactNode } from "react";

import { TooltipProvider } from "@/components/ui/tooltip";

import { AppSidebar } from "./app-sidebar";
import { TopNavbar } from "./top-navbar";

export function DashboardShell({ children }: { children: ReactNode }) {
  const [isDark, setIsDark] = useState(() =>
    typeof document === "undefined"
      ? false
      : document.documentElement.classList.contains("dark"),
  );

  useEffect(() => {
    const root = document.documentElement;
    const syncTheme = () => setIsDark(root.classList.contains("dark"));
    const observer = new MutationObserver(syncTheme);

    syncTheme();
    observer.observe(root, {
      attributes: true,
      attributeFilter: ["class"],
    });

    return () => observer.disconnect();
  }, []);

  function handleThemeChange(dark: boolean) {
    document.documentElement.classList.toggle("dark", dark);
    localStorage.setItem("theme", dark ? "dark" : "light");
    setIsDark(dark);
  }

  return (
    <TooltipProvider>
      <div className="gridline-dashboard relative h-svh min-h-0 overflow-hidden transition-colors">
        <AppSidebar />
        <TopNavbar isDark={isDark} onThemeChange={handleThemeChange} />

        <main
          aria-label="Page content"
          className="h-full overflow-y-auto pt-18 md:pl-17"
        >
          {children}
        </main>
      </div>
    </TooltipProvider>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/layout/sidebar-navigation-item.tsx`

```tsx
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";

import { useDashboardNavigation } from "../navigation";

export type NavigationItem = {
  href: string;
  label: string;
  shortLabel: string;
  icon: LucideIcon;
};

type SidebarNavigationItemProps = {
  item: NavigationItem;
  showLabel?: boolean;
  onNavigate?: () => void;
};

export function SidebarNavigationItem({
  item,
  showLabel = false,
  onNavigate,
}: SidebarNavigationItemProps) {
  const { pathname, navigate } = useDashboardNavigation();
  const active =
    item.href === "/" ? pathname === item.href : pathname.startsWith(item.href);
  const Icon = item.icon;
  const className = cn(
    buttonVariants({
      variant: "ghost",
      size: showLabel ? "lg" : "icon-lg",
    }),
    "rounded-lg text-muted-foreground",
    showLabel && "w-full justify-start gap-3",
    active &&
      "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary",
  );

  const content = (
    <>
      <Icon
        aria-hidden="true"
        className={showLabel ? "size-5" : "size-6"}
        strokeWidth={showLabel ? 1.7 : 1.6}
      />
      {showLabel && item.shortLabel}
    </>
  );
  const handleNavigate = () => {
    navigate(item.href);
    onNavigate?.();
  };

  if (showLabel) {
    return (
      <button
        type="button"
        aria-current={active ? "page" : undefined}
        className={className}
        onClick={handleNavigate}
      >
        {content}
      </button>
    );
  }

  return (
    <Tooltip>
      <TooltipTrigger
        type="button"
        aria-label={item.label}
        aria-current={active ? "page" : undefined}
        className={className}
        onClick={handleNavigate}
      >
        {content}
      </TooltipTrigger>
      <TooltipContent side="right">{item.label}</TooltipContent>
    </Tooltip>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/layout/top-navbar.tsx`

```tsx
import { ArrowRight, MoonStar, SunDim } from "lucide-react";

import { Switch } from "@/components/ui/switch";

import { modellingDashboard, modelWorkflowStatuses } from "../../data";
import { useDashboardNavigation } from "../navigation";
import { ProfileMenu } from "../shared/profile-menu";
import { MobileNavigation, primaryNavigation } from "./app-sidebar";

type TopbarProps = {
  isDark: boolean;
  onThemeChange: (isDark: boolean) => void;
};

function StatusStrip() {
  return (
    <div className="hidden h-6.5 shrink-0 items-center overflow-hidden rounded-md bg-muted font-mono text-xs tracking-tight sm:flex">
      <span className="flex h-full items-center gap-2 border-r-2 border-sidebar px-2.5 text-primary">
        <span className="size-1.5 rounded-full bg-primary" />
        {modelWorkflowStatuses.publication}
      </span>
      <span className="flex h-full items-center border-r-2 border-sidebar px-2.5 text-emerald-500">
        {modelWorkflowStatuses.validation}
      </span>
      <span className="flex h-full items-center px-2.5 text-muted-foreground">
        {modelWorkflowStatuses.review}
      </span>
    </div>
  );
}

function DesktopThemeToggle({
  isDark,
  onThemeChange,
}: {
  isDark: boolean;
  onThemeChange: (isDark: boolean) => void;
}) {
  const ThemeIcon = isDark ? MoonStar : SunDim;

  return (
    <div className="hidden items-center gap-3 md:flex">
      <div className="flex items-center gap-1">
        <ThemeIcon
          aria-hidden="true"
          className={isDark ? "size-4" : "size-4.5"}
          strokeWidth={1.5}
        />
        <span>{isDark ? "Dark Mode" : "Light Mode"}</span>
      </div>
      <Switch
        checked={isDark}
        onCheckedChange={onThemeChange}
        aria-label={`Switch to ${isDark ? "light" : "dark"} mode`}
        className="data-unchecked:bg-muted"
      />
    </div>
  );
}

export function TopNavbar({ isDark, onThemeChange }: TopbarProps) {
  const { pathname } = useDashboardNavigation();
  const activeLabel =
    primaryNavigation.find((item) =>
      item.href === "/"
        ? pathname === item.href
        : pathname.startsWith(item.href),
    )?.shortLabel ?? "Dashboard";

  return (
    <header className="absolute left-0 right-0 top-0 z-20 flex h-18 items-center justify-between gap-3 border-b border-sidebar-border bg-sidebar px-3 text-sidebar-foreground sm:px-5 md:left-17 md:px-8">
      <div className="flex min-w-0 items-center gap-3">
        <MobileNavigation isDark={isDark} onThemeChange={onThemeChange} />
        <span className="truncate text-sm font-semibold sm:hidden">
          {activeLabel}
        </span>
        <StatusStrip />
      </div>

      <div className="flex min-w-0 items-center gap-4 font-mono text-xs text-muted-foreground lg:gap-6">
        <div className="hidden min-w-0 items-center gap-2 lg:flex">
          <span className="truncate">
            {modellingDashboard.currentModel} Model
          </span>
          <span className="size-1 rounded-full bg-muted-foreground" />
          <span>{modellingDashboard.currentVersion}</span>
          <ArrowRight aria-hidden="true" className="size-3" />
          <span className="text-primary">
            {modellingDashboard.latestVersion}
          </span>
        </div>

        <DesktopThemeToggle isDark={isDark} onThemeChange={onThemeChange} />
        <span className="text-primary md:hidden">
          {modellingDashboard.latestVersion}
        </span>
        <ProfileMenu placement="topbar" className="md:hidden" />
      </div>
    </header>
  );
}
```

### `src/components/dashboards/gridline-dashboard/components/navigation.tsx`

```tsx
import {
  createContext,
  useContext,
  useMemo,
  useState,
  type AnchorHTMLAttributes,
  type MouseEvent,
  type ReactNode,
} from "react";

type DashboardNavigation = {
  pathname: string;
  navigate: (pathname: string) => void;
};

const DashboardNavigationContext = createContext<DashboardNavigation | null>(
  null,
);

export function DashboardNavigationProvider({
  children,
}: {
  children: ReactNode;
}) {
  const [pathname, setPathname] = useState("/");
  const value = useMemo(
    () => ({ pathname, navigate: setPathname }),
    [pathname],
  );

  return (
    <DashboardNavigationContext.Provider value={value}>
      {children}
    </DashboardNavigationContext.Provider>
  );
}

export function useDashboardNavigation() {
  const context = useContext(DashboardNavigationContext);

  if (!context) {
    throw new Error(
      "useDashboardNavigation must be used within DashboardNavigationProvider",
    );
  }

  return context;
}

export function DashboardLink({
  href,
  onClick,
  ...props
}: AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
  const { navigate } = useDashboardNavigation();

  function handleClick(event: MouseEvent<HTMLAnchorElement>) {
    onClick?.(event);

    if (
      event.defaultPrevented ||
      event.button !== 0 ||
      event.metaKey ||
      event.ctrlKey ||
      event.shiftKey ||
      event.altKey
    ) {
      return;
    }

    event.preventDefault();
    navigate(href);
  }

  return <a href={href} onClick={handleClick} {...props} />;
}
```

### `src/components/dashboards/gridline-dashboard/components/shared/profile-menu.tsx`

```tsx
import { LogOut, Settings, UserRound } from "lucide-react";

import { buttonVariants } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

export function ProfileMenu({
  placement,
  className,
}: {
  placement: "sidebar" | "topbar";
  className?: string;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        aria-label="Open profile menu"
        title="Profile"
        className={cn(
          buttonVariants({
            variant: placement === "topbar" ? "secondary" : "ghost",
            size: "icon-lg",
          }),
          "rounded-lg",
          placement === "sidebar" && "text-muted-foreground",
          className,
        )}
      >
        <UserRound aria-hidden="true" className="size-5" strokeWidth={1.7} />
      </DropdownMenuTrigger>

      <DropdownMenuContent
        side={placement === "sidebar" ? "right" : "bottom"}
        align="end"
        sideOffset={8}
        className="w-48"
      >
        <DropdownMenuGroup>
          <DropdownMenuLabel>Account</DropdownMenuLabel>
          <DropdownMenuItem>
            <UserRound />
            Profile
          </DropdownMenuItem>
          <DropdownMenuItem>
            <Settings />
            Settings
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <LogOut />
          Sign out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

### `src/components/dashboards/gridline-dashboard/dashboard.css`

```css
.gridline-dashboard,
body:has(.gridline-dashboard) [data-slot="drawer-content"],
body:has(.gridline-dashboard) [data-slot="dropdown-menu-content"],
body:has(.gridline-dashboard) [data-slot="sheet-content"],
body:has(.gridline-dashboard) [data-slot="tooltip-content"] {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: #0ab1ba;
  --primary-foreground: oklch(1 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: #0ab1ba;
  --radius: 0.625rem;
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: #0ab1ba;
  --sidebar-primary-foreground: oklch(1 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: #0ab1ba;
  background-color: var(--background);
  color: var(--foreground);
}

.gridline-dashboard.dark,
.dark .gridline-dashboard,
.dark body:has(.gridline-dashboard) [data-slot="drawer-content"],
.dark body:has(.gridline-dashboard) [data-slot="dropdown-menu-content"],
.dark body:has(.gridline-dashboard) [data-slot="sheet-content"],
.dark body:has(.gridline-dashboard) [data-slot="tooltip-content"] {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: #0ab1ba;
  --primary-foreground: oklch(1 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: #0ab1ba;
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: #0ab1ba;
  --sidebar-primary-foreground: oklch(1 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: #0ab1ba;
}

body:has(.gridline-dashboard) [data-slot="tooltip-content"] {
  background-color: var(--foreground) !important;
  color: var(--background) !important;
}

body:has(.gridline-dashboard) [data-slot="tooltip-content"] > svg {
  background-color: var(--foreground) !important;
  fill: var(--foreground) !important;
}

.gridline-dashboard * {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.gridline-dashboard *::-webkit-scrollbar {
  display: none;
}
```

### `src/components/dashboards/gridline-dashboard/dashboardView.tsx`

```tsx
import { DashboardContent } from "./components/dashboard/dashboard-content";
import { FlexibilityDashboard } from "./components/dashboard/flexibility-dashboard";
import { ModellingDashboard } from "./components/dashboard/modelling-dashboard";
import { DashboardShell } from "./components/layout/dashboard-shell";
import {
  DashboardNavigationProvider,
  useDashboardNavigation,
} from "./components/navigation";

function DashboardRoute() {
  const { pathname } = useDashboardNavigation();

  return (
    <DashboardShell>
      {pathname === "/modelling" ? (
        <ModellingDashboard />
      ) : pathname === "/flexibility" ? (
        <FlexibilityDashboard />
      ) : (
        <DashboardContent />
      )}
    </DashboardShell>
  );
}

export default function DashboardView() {
  return (
    <DashboardNavigationProvider>
      <DashboardRoute />
    </DashboardNavigationProvider>
  );
}
```

### `src/components/dashboards/gridline-dashboard/data.ts`

```ts
export type WorkspaceCard = {
  title: string;
  href: string;
  icon:
    | "database"
    | "flexibility"
    | "planning"
    | "operations"
    | "stakeholders"
    | "connections";
  details: string[];
};

export const dashboardOverview = {
  company: "Electrical Grid Company",
  date: "16 March 2026",
};

export const networkBasemapUrl =
  "https://assets.watermelon.sh/components/map-sample.png";

export const workspaceCards: WorkspaceCard[] = [
  {
    title: "System Modelling and Data",
    href: "/modelling",
    icon: "database",
    details: [
      "Validate model quality and coverage",
      "Prepare publish packs and audit",
    ],
  },
  {
    title: "Flexibility",
    href: "/flexibility",
    icon: "flexibility",
    details: [
      "Build stakeholder story packs",
      "Generate briefings with traceable evidence",
    ],
  },
  {
    title: "Planning & Investment",
    href: "/planning",
    icon: "planning",
    details: [
      "Stress-test scenarios and constraints",
      "Track reinforcement options and assumptions",
    ],
  },
  {
    title: "Operations Interface",
    href: "/operations",
    icon: "operations",
    details: [
      "Operational context for switching and outage",
      "Link events back to model",
    ],
  },
  {
    title: "Stakeholder & Public",
    href: "/stakeholders",
    icon: "stakeholders",
    details: [
      "Publish model-anchored views",
      "Generate briefings with traceable evidence",
    ],
  },
  {
    title: "Connections & Queue",
    href: "/connections",
    icon: "connections",
    details: [
      "Queue context and constraints",
      "Application tracking linked to the model",
    ],
  },
];

export type ModellingAction = {
  title: string;
  description: string;
  icon: "database" | "comparison" | "clearview";
};

export type NetworkRegion = {
  name: string;
  legendName: string;
  color: "blue" | "purple" | "red" | "green";
  position: "north-east" | "central" | "west" | "south-east";
};

export const modellingDashboard = {
  title: "System Modelling and Data",
  description:
    "Validate and publish the latest network model used across DSO workflows",
  version: "Model v2.4.2",
  latestVersion: "v2.4.2",
  currentModel: "Electrical Grid Company",
  currentVersion: "v2.4.1",
  latestUpdate: "Coverage gap analysis completed",
  coverage: "94%",
  lastPublished: "3 days ago",
  openIssues: 2,
};

export const modelWorkflowStatuses = {
  publication: "Published",
  validation: "Validated",
  review: "In Review",
};

export const modellingActions: ModellingAction[] = [
  {
    title: "LTDS Table Generator",
    description: "Generate and export LTDS tables from CIM data",
    icon: "database",
  },
  {
    title: "CIM Comparison",
    description: "Compare CIM model versions and track changes",
    icon: "comparison",
  },
  {
    title: "ClearView",
    description: "Asset enrichment and reinforcement data",
    icon: "clearview",
  },
];

export const networkRegions: NetworkRegion[] = [
  {
    name: "East Midlands",
    legendName: "East Midlands",
    color: "blue",
    position: "north-east",
  },
  {
    name: "West Midlands",
    legendName: "West Midlands",
    color: "purple",
    position: "central",
  },
  {
    name: "South Wales",
    legendName: "South Wales",
    color: "red",
    position: "west",
  },
  {
    name: "South West",
    legendName: "South West",
    color: "green",
    position: "south-east",
  },
];

export type GridAsset = {
  id: string;
  name: string;
  voltage: string;
  equipmentCount: number;
  mapPosition: {
    x: number;
    y: number;
  };
};

export const flexibilityDashboard = {
  regionName: "South West",
  assetType: "Substation",
  substationCount: 621,
  mappedSubstationCount: 379,
  locationCount: 2072,
  attributionCompany: "Large Grid Company",
  attributionProduct: "Squid",
  voltageLevels: ["33kV", "11kV"],
  coordinates: "50.5544, -4.1608",
};

export const gridAssets: GridAsset[] = [
  {
    id: "tavistock",
    name: "Tavistock",
    voltage: "33/11kV",
    equipmentCount: 30,
    mapPosition: { x: 40, y: 64 },
  },
  {
    id: "st-tudy",
    name: "St Tudy",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 35, y: 52 },
  },
  {
    id: "st-tudy-bsp",
    name: "St Tudy BSP",
    voltage: "132/33kV",
    equipmentCount: 24,
    mapPosition: { x: 32, y: 46 },
  },
  {
    id: "stancombe",
    name: "Stancombe Quarry",
    voltage: "33kV",
    equipmentCount: 24,
    mapPosition: { x: 65, y: 72 },
  },
  {
    id: "staplegrove",
    name: "Staplegrove",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 55, y: 35 },
  },
  {
    id: "stentaway",
    name: "Stentaway",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 72, y: 58 },
  },
  {
    id: "stoke-bishop",
    name: "Stoke Bishop",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 61, y: 42 },
  },
  {
    id: "stokenham",
    name: "Stokenham",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 48, y: 75 },
  },
  {
    id: "street-bsp",
    name: "Street BSP",
    voltage: "132/33kV",
    equipmentCount: 24,
    mapPosition: { x: 53, y: 23 },
  },
  {
    id: "stratton",
    name: "Stratton",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 24, y: 30 },
  },
  {
    id: "tale-lane",
    name: "Tale Lane PV",
    voltage: "132/33kV",
    equipmentCount: 24,
    mapPosition: { x: 76, y: 26 },
  },
  {
    id: "st-tudy-333",
    name: "St Tudy",
    voltage: "333/11kV",
    equipmentCount: 24,
    mapPosition: { x: 30, y: 57 },
  },
  {
    id: "st-catherines",
    name: "St Catherines",
    voltage: "33/11kV",
    equipmentCount: 12,
    mapPosition: { x: 43, y: 39 },
  },
  {
    id: "south-molton",
    name: "South Molton",
    voltage: "33/11kV",
    equipmentCount: 18,
    mapPosition: { x: 58, y: 55 },
  },
  {
    id: "plymouth",
    name: "Plymouth North",
    voltage: "132/33kV",
    equipmentCount: 30,
    mapPosition: { x: 37, y: 80 },
  },
  {
    id: "exeter",
    name: "Exeter East",
    voltage: "33/11kV",
    equipmentCount: 24,
    mapPosition: { x: 68, y: 47 },
  },
];

export const voltageLevelFilters = [
  { id: "132-66", label: "132kV / 66kV", color: "neutral" },
  { id: "33", label: "33kV", color: "green" },
  { id: "22-25", label: "22kV - 25kV", color: "yellow" },
  { id: "11", label: "11kV", color: "red" },
  { id: "6", label: "6kV - 6.6kV", color: "blue" },
  { id: "unknown", label: "Unknown", color: "brown" },
] as const;
```

### `src/components/dashboards/gridline-dashboard/demo.tsx`

```tsx
import "./dashboard.css";

import DashboardView from "./dashboardView";

export default function GridlineDashboardDemo() {
  return <DashboardView />;
}
```
