An interactive demonstration of sitemap and navigation layouts. Explore grouped grids, sorted Indexes, active trees, and column directories.
CRM Sitemap Laboratory
Navigate the CRM using search-enabled grouped card grids, alphabetical listings, interactive folder tree explorers, or column-based department directories.
16 sections — 96 pages total
Accounts Receivable
Fleet
Menus
Scheduling
Administration
Inspection Points
Period End
Security
CSR
Inventory
Reports & Analysis
Support
Doc Tracs
Log Book
Requests
Validation
Variant1_GroupedCardGrid.tsx (Widget Implementation)
import { useState, useMemo } from "react";
import {
Box,
TextInput,
SimpleGrid,
Paper,
Text,
Stack,
Group,
Badge,
UnstyledButton,
Highlight,
} from "@mantine/core";
import {
HiOutlineMagnifyingGlass,
HiOutlineCurrencyDollar,
HiOutlineTruck,
HiOutlineListBullet,
HiOutlineCalendarDays,
HiOutlineCog6Tooth,
HiOutlineClipboardDocumentCheck,
HiOutlineCalendar,
HiOutlineShieldCheck,
HiOutlinePhoneArrowDownLeft,
HiOutlineArchiveBox,
HiOutlineChartBar,
HiOutlineLifebuoy,
HiOutlineDocumentText,
HiOutlineBookOpen,
HiOutlineInboxArrowDown,
HiOutlineCheckBadge,
} from "react-icons/hi2";
import type { CrmSitemapProps, SitemapLink, SitemapSection } from "./types";
const iconMap: Record<string, React.ComponentType<{ size?: number; color?: string }>> = {
HiOutlineCurrencyDollar,
HiOutlineTruck,
HiOutlineListBullet,
HiOutlineCalendarDays,
HiOutlineCog6Tooth,
HiOutlineClipboardDocumentCheck,
HiOutlineCalendar,
HiOutlineShieldCheck,
HiOutlinePhoneArrowDownLeft,
HiOutlineArchiveBox,
HiOutlineChartBar,
HiOutlineLifebuoy,
HiOutlineDocumentText,
HiOutlineBookOpen,
HiOutlineInboxArrowDown,
HiOutlineCheckBadge,
};
function SectionCard({
section,
searchQuery,
onNavigate,
}: {
section: SitemapSection;
searchQuery: string;
onNavigate?: (link: SitemapLink) => void;
}) {
const Icon = iconMap[section.icon] ?? HiOutlineListBullet;
const query = searchQuery.toLowerCase().trim();
const visibleLinks = useMemo(() => {
if (!query) return section.links;
return section.links.filter(
(l) =>
l.label.toLowerCase().includes(query) ||
(l.description ?? "").toLowerCase().includes(query),
);
}, [section.links, query]);
if (query && visibleLinks.length === 0) return null;
return (
<Paper
shadow="xs"
radius="md"
withBorder
style={{
overflow: "hidden",
borderLeft: `4px solid var(--mantine-color-${section.color}-6)`,
}}
>
{/* Card Header */}
<Group
px="md"
py="sm"
gap="sm"
style={{
backgroundColor: `var(--mantine-color-${section.color}-0)`,
borderBottom: "1px solid var(--mantine-color-gray-2)",
}}
>
<Box
style={{
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: `var(--mantine-color-${section.color}-1)`,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<Icon size={17} color={`var(--mantine-color-${section.color}-7)`} />
</Box>
<div style={{ flex: 1, minWidth: 0 }}>
<Group gap="xs" align="center">
<Text fw={600} size="sm" style={{ lineHeight: 1.2 }}>
{section.label}
</Text>
{query && (
<Badge size="xs" color={section.color} variant="light">
{visibleLinks.length} match{visibleLinks.length !== 1 ? "es" : ""}
</Badge>
)}
</Group>
</div>
{!query && (
<Badge size="xs" color="gray" variant="light">
{section.links.length}
</Badge>
)}
</Group>
{/* Link List */}
<Stack gap={0} p="sm">
{visibleLinks.map((link) => (
<UnstyledButton
key={link.id}
onClick={() => onNavigate?.(link)}
style={{
borderRadius: 6,
padding: "6px 8px",
transition: "background-color 0.12s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.backgroundColor =
`var(--mantine-color-${section.color}-0)`;
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.backgroundColor = "transparent";
}}
>
<Text size="sm" fw={500} c={`${section.color}.7`} style={{ lineHeight: 1.3 }}>
{query ? (
<Highlight highlight={searchQuery} color={section.color}>
{link.label}
</Highlight>
) : (
link.label
)}
</Text>
{link.description && (
<Text size="xs" c="dimmed" mt={1} style={{ lineHeight: 1.4 }}>
{query ? (
<Highlight highlight={searchQuery} color={section.color}>
{link.description}
</Highlight>
) : (
link.description
)}
</Text>
)}
</UnstyledButton>
))}
</Stack>
</Paper>
);
}
export function Variant1_GroupedCardGrid({ sections, onNavigate }: CrmSitemapProps) {
const [searchQuery, setSearchQuery] = useState("");
const query = searchQuery.toLowerCase().trim();
const totalMatches = useMemo(() => {
if (!query) return 0;
return sections.reduce((sum, section) => {
return (
sum +
section.links.filter(
(l) =>
l.label.toLowerCase().includes(query) ||
(l.description ?? "").toLowerCase().includes(query),
).length
);
}, 0);
}, [sections, query]);
const visibleSectionCount = useMemo(() => {
if (!query) return sections.length;
return sections.filter((s) =>
s.links.some(
(l) =>
l.label.toLowerCase().includes(query) ||
(l.description ?? "").toLowerCase().includes(query),
),
).length;
}, [sections, query]);
return (
<Box style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* Search Bar */}
<Box
px="lg"
py="md"
style={{
borderBottom: "1px solid var(--mantine-color-gray-3)",
backgroundColor: "#fff",
}}
>
<Group align="center" gap="md">
<TextInput
placeholder="Search all sections and pages..."
leftSection={<HiOutlineMagnifyingGlass size={16} />}
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
style={{ flex: 1, maxWidth: 520 }}
size="md"
/>
{query && (
<Text size="sm" c="dimmed">
{totalMatches} result{totalMatches !== 1 ? "s" : ""} in {visibleSectionCount} section
{visibleSectionCount !== 1 ? "s" : ""}
</Text>
)}
{!query && (
<Text size="sm" c="dimmed">
{sections.length} sections —{" "}
{sections.reduce((s, sec) => s + sec.links.length, 0)} pages total
</Text>
)}
</Group>
</Box>
{/* Card Grid */}
<Box style={{ flex: 1, overflowY: "auto" }} p="lg">
{visibleSectionCount === 0 ? (
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: 200,
}}
>
<Stack align="center" gap="xs">
<HiOutlineMagnifyingGlass size={32} color="var(--mantine-color-gray-5)" />
<Text c="dimmed" size="lg" fw={500}>
No results for “{searchQuery}”
</Text>
<Text c="dimmed" size="sm">
Try a different search term
</Text>
</Stack>
</Box>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="md">
{sections.map((section) => (
<SectionCard
key={section.id}
section={section}
searchQuery={searchQuery}
onNavigate={onNavigate}
/>
))}
</SimpleGrid>
)}
</Box>
</Box>
);
}