Compare commits

...
Author SHA1 Message Date
gorkem-bwl 0f39be632f style: fix Prettier formatting in documentation content 2026-01-18 14:13:52 -05:00
gorkem-bwl 9559b40e67 docs: add Docker, Bulk Import, and Account Settings documentation
Add three new documentation articles to the user guide:
- Docker monitors: Setup and configuration for container monitoring
- Bulk import monitors: CSV import guide for creating multiple monitors
- Account settings: Profile, password, and account management

Updates article counts in collections accordingly.
2026-01-17 15:26:08 -05:00
gorkem-bwl 914d94497d docs: add Checks and Logs documentation collections 2026-01-17 14:29:17 -05:00
gorkem-bwl 8eb9b70c9b docs: add Checks and Logs to sidebar navigation list 2026-01-17 14:25:20 -05:00
gorkem-bwl bf55c21e79 fix: add right padding to content for sidebar tab bar 2026-01-17 14:24:05 -05:00
gorkem-bwl 9d44e11496 refactor: migrate HomeLayout from CSS to MUI sx styling 2026-01-17 14:22:07 -05:00
gorkem-bwl 478e9671d4 fix: revert padding approach, let sidebar overlay content 2026-01-17 13:39:07 -05:00
gorkem-bwl c44fd22031 fix: use different gaps for open and closed sidebar states 2026-01-17 13:10:57 -05:00
gorkem-bwl 62dc6453f1 fix: increase minimum gap between content and sidebar 2026-01-17 13:10:05 -05:00
gorkem-bwl 7663ad6c74 fix: add right padding to content wrapper for sidebar spacing 2026-01-17 13:09:54 -05:00
gorkem-bwl 2f7de5d103 fix: account for tab bar border and add minimum gap 2026-01-17 13:09:08 -05:00
gorkem-bwl 4e1f9bc431 fix: use margin instead of padding for sidebar spacing 2026-01-17 13:06:14 -05:00
gorkem-bwl 6b1d574698 fix: prevent user guide sidebar from overlaying main content 2026-01-17 13:04:30 -05:00
gorkem-bwl b196d0de78 style: format user guide files with prettier 2026-01-17 12:39:31 -05:00
gorkem-bwl c7107a5034 feat: add user guide documentation sidebar
- Add comprehensive user guide with 10 documentation collections
- Include 49 articles covering all Checkmate features
- Implement sidebar navigation with search functionality
- Support light/dark theme with proper color tokens
- Add article content renderer with multiple block types
- Include image lightbox for documentation screenshots
2026-01-17 10:49:16 -05:00
31 changed files with 8896 additions and 41 deletions

No files matched your search

+3
View File
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import AppLayout from "@/Components/v1/Layouts/AppLayout";
import { UserGuideSidebarProvider } from "@/Components/v1/UserGuide";
function App() {
const mode = useSelector((state) => state.ui.mode);
@@ -25,9 +26,11 @@ function App() {
return (
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<CssBaseline />
<UserGuideSidebarProvider>
<AppLayout>
<Routes />
</AppLayout>
</UserGuideSidebarProvider>
<ToastContainer />
</ThemeProvider>
);
@@ -1,37 +0,0 @@
.home-layout {
position: relative;
min-height: 100vh;
margin: 0 auto;
padding: 0;
overflow-x: hidden;
width: 100%;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
/* @media (max-width: 1000px) {
.home-layout {
flex-direction: column !important;
}
} */
/* .home-layout aside {
position: sticky;
top: 0;
left: 0;
height: 100vh;
max-width: var(--env-var-side-bar-width);
} */
.home-layout > div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
flex: 1;
}
.home-content-wrapper {
padding: var(--env-var-spacing-2);
max-width: 1400px;
margin: 0 auto;
flex: 1;
min-width: 0;
overflow-x: hidden;
}
@@ -1,20 +1,44 @@
import Sidebar from "../../Sidebar/index.jsx";
import { Outlet } from "react-router";
import { Stack } from "@mui/material";
import "./index.css";
import { useTheme } from "@mui/material/styles";
import { UserGuideSidebar, TAB_BAR_WIDTH } from "../../UserGuide";
const HomeLayout = () => {
const theme = useTheme();
return (
<Stack
className="home-layout"
flexDirection="row"
gap={14}
sx={{
position: "relative",
minHeight: "100vh",
margin: "0 auto",
padding: 0,
overflowX: "hidden",
width: "100%",
"& > div": {
minHeight: `calc(100vh - ${theme.spacing(4)} * 2)`,
flex: 1,
},
}}
>
<Sidebar />
<Stack className="home-content-wrapper">
<Stack
sx={{
padding: theme.spacing(4),
paddingRight: `${TAB_BAR_WIDTH + 16}px`,
maxWidth: 1400,
margin: "0 auto",
flex: 1,
minWidth: 0,
overflowX: "hidden",
}}
>
<Outlet />
</Stack>
<UserGuideSidebar />
</Stack>
);
};
@@ -89,6 +89,8 @@ const Sidebar = () => {
gap={theme.spacing(6)}
sx={{
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
backgroundColor: theme.palette.primaryBackground.main,
zIndex: 1000,
}}
>
<CollapseButton collapsed={collapsed} />
@@ -0,0 +1,157 @@
/* ArticlePage styles - hover states and nested selectors (Dark theme) */
/* Breadcrumb hover */
.user-guide-breadcrumb-link {
transition: color 150ms ease;
}
.user-guide-breadcrumb-link:hover {
color: #1570ef;
}
/* ToC link hover */
.user-guide-toc-link {
transition: all 150ms ease;
}
.user-guide-toc-link:hover {
color: #1570ef;
text-decoration: underline !important;
}
/* Navigation button hover */
.user-guide-nav-button .nav-text {
transition: color 150ms ease;
}
.user-guide-nav-button:hover .nav-text {
color: #1570ef;
}
/* Article body typography */
.user-guide-article-body h2 {
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin-top: 32px;
margin-bottom: 16px;
line-height: 1.4;
}
.user-guide-article-body h3 {
font-size: 14px;
font-weight: 600;
color: #ffffff;
margin-top: 24px;
margin-bottom: 12px;
line-height: 1.4;
}
.user-guide-article-body p {
font-size: 13px;
color: #d0d5dd;
line-height: 1.5;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body ul,
.user-guide-article-body ol {
padding-left: 24px;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body li {
font-size: 13px;
color: #d0d5dd;
line-height: 1.5;
margin-bottom: 8px;
}
.user-guide-article-body a {
color: #1570ef;
text-decoration: none;
font-weight: 500;
}
.user-guide-article-body a:hover {
text-decoration: underline;
}
.user-guide-article-body code {
background-color: #2d2d2d;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-family: "Geist Mono", "Fira Code", "Consolas", monospace;
color: #d0d5dd;
}
.user-guide-article-body pre {
background-color: #1c2130;
border-radius: 4px;
padding: 16px;
overflow: auto;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body pre code {
background-color: transparent;
padding: 0;
color: #e5e7eb;
}
.user-guide-article-body blockquote {
border-left: 3px solid #1570ef;
background-color: rgba(255, 255, 255, 0.05);
padding: 12px 16px;
border-radius: 0 4px 4px 0;
margin-bottom: 16px;
margin-top: 0;
margin-left: 0;
margin-right: 0;
}
.user-guide-article-body blockquote p {
margin-bottom: 0;
color: #d0d5dd;
}
.user-guide-article-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.user-guide-article-body th,
.user-guide-article-body td {
border: 1px solid #344054;
padding: 12px;
text-align: left;
font-size: 10px;
}
.user-guide-article-body th {
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
color: #ffffff;
}
.user-guide-article-body td {
color: #d0d5dd;
}
.user-guide-article-body img {
max-width: 100%;
border-radius: 4px;
border: 1px solid #344054;
margin-bottom: 16px;
}
.user-guide-article-body hr {
border: none;
border-top: 1px solid #344054;
margin: 32px 0;
}
@@ -0,0 +1,333 @@
import { useState, useEffect } from "react";
import {
ChevronLeft,
ChevronRight,
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./ArticlePage.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
};
const ArticlePage = ({
collection,
article,
onBack,
onBackToHome,
onNextArticle,
onPrevArticle,
nextArticle,
prevArticle,
tocItems,
children,
mode = "in-app",
}) => {
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
const IconComponent = iconMap[collection.icon] || Rocket;
const isInApp = mode === "in-app";
const [activeSection, setActiveSection] = useState("");
// Track active section on scroll
useEffect(() => {
if (!tocItems || tocItems.length === 0) return;
const handleScroll = () => {
const sections = tocItems
.map((item) => {
const id = item.href.replace("#", "");
const element = document.getElementById(id);
return { id: item.href, element };
})
.filter((s) => s.element);
if (sections.length === 0) return;
let currentSection = sections[0].id;
for (const section of sections) {
if (section.element) {
const rect = section.element.getBoundingClientRect();
if (rect.top <= 150) {
currentSection = section.id;
}
}
}
setActiveSection(currentSection);
};
window.addEventListener("scroll", handleScroll, { passive: true });
document.addEventListener("scroll", handleScroll, { passive: true, capture: true });
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
document.removeEventListener("scroll", handleScroll, { capture: true });
};
}, [tocItems]);
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Main content area */}
<div
style={{
maxWidth: isInApp ? "100%" : 1100,
margin: "0 auto",
padding: isInApp ? spacing.lg : spacing["3xl"],
}}
>
{/* Article Content */}
<div style={{ maxWidth: isInApp ? "100%" : 680, minWidth: 0 }}>
{/* Title */}
<h1
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["3xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.lg,
lineHeight: typography.lineHeight.snug,
marginTop: 0,
}}
>
{article.title}
</h1>
{/* Description */}
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.md,
color: colors.text.secondary,
marginBottom: spacing.lg,
lineHeight: typography.lineHeight.normal,
marginTop: 0,
}}
>
{article.description}
</p>
{/* Table of Contents - at top */}
{tocItems && tocItems.length > 0 && (
<div
style={{
marginBottom: spacing["2xl"],
paddingBottom: isInApp ? 0 : spacing.lg,
borderBottom: isInApp ? "none" : border.default,
}}
>
<div
style={{
backgroundColor: isInApp
? isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.02)"
: "transparent",
borderRadius: isInApp ? border.radius : 0,
padding: isInApp ? spacing.md : 0,
border: isInApp ? border.default : "none",
}}
>
<span
style={{
display: "block",
fontSize: 12,
fontWeight: typography.fontWeight.semibold,
color: colors.text.secondary,
marginBottom: spacing.sm,
textTransform: "uppercase",
letterSpacing: "0.5px",
}}
>
In this article
</span>
<div
style={{
display: "flex",
flexDirection: isInApp ? "column" : "row",
flexWrap: isInApp ? "nowrap" : "wrap",
gap: isInApp ? 0 : spacing.xs,
columnGap: isInApp ? 0 : spacing.lg,
}}
>
{tocItems.map((item) => {
const isActive = activeSection === item.href;
return (
<a
key={item.href}
href={item.href}
className="user-guide-toc-link"
onClick={(e) => {
if (isInApp) {
e.preventDefault();
const id = item.href.replace("#", "");
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}}
style={{
fontFamily: typography.fontFamily.sans,
fontSize: isInApp ? 13 : 12,
color: isActive ? colors.brand.primary : colors.text.secondary,
fontWeight: isActive
? typography.fontWeight.medium
: typography.fontWeight.normal,
textDecoration: "none",
padding: isInApp ? "2px 0" : `${spacing.xs} 0`,
}}
>
{item.label}
</a>
);
})}
</div>
</div>
</div>
)}
{/* Article Body */}
<div className="user-guide-article-body">
{children || (
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.xl,
textAlign: "center",
}}
>
<span
style={{ fontSize: typography.fontSize.sm, color: colors.text.muted }}
>
Article content coming soon.
</span>
</div>
)}
</div>
{/* Navigation */}
{(prevArticle || nextArticle) && (
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: spacing["2xl"],
paddingTop: spacing.xl,
borderTop: border.default,
}}
>
{prevArticle ? (
<div
onClick={onPrevArticle}
className="user-guide-nav-button"
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
cursor: "pointer",
}}
>
<ChevronLeft
size={16}
strokeWidth={1.5}
color={colors.text.muted}
/>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.muted,
}}
>
Previous
</span>
<span
className="nav-text"
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.primary,
fontWeight: typography.fontWeight.medium,
}}
>
{prevArticle.title}
</span>
</div>
</div>
) : (
<div />
)}
{nextArticle && (
<div
onClick={onNextArticle}
className="user-guide-nav-button"
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
textAlign: "right",
cursor: "pointer",
}}
>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.muted,
}}
>
Next
</span>
<span
className="nav-text"
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.primary,
fontWeight: typography.fontWeight.medium,
}}
>
{nextArticle.title}
</span>
</div>
<ChevronRight
size={16}
strokeWidth={1.5}
color={colors.text.muted}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default ArticlePage;
@@ -0,0 +1,31 @@
/* CollectionPage styles - hover states */
/* Back link hover */
.user-guide-back-link {
transition: color 150ms ease;
}
.user-guide-back-link:hover {
color: #1570ef;
}
/* Article row hover */
.user-guide-article-row {
transition: all 150ms ease;
}
.user-guide-article-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-article-row:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-article-row .arrow-icon {
transition: all 150ms ease;
}
.user-guide-article-row:hover .arrow-icon {
color: #1570ef;
}
@@ -0,0 +1,171 @@
import {
ArrowRight,
FileText,
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./CollectionPage.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
};
const CollectionPage = ({ collection, onBack, onArticleClick }) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const IconComponent = iconMap[collection.icon] || Rocket;
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Header */}
<div
style={{
backgroundColor: colors.background.white,
borderBottom: border.default,
padding: `${spacing.lg} ${spacing.lg}`,
}}
>
<div style={{ maxWidth: 900, margin: "0 auto" }}>
{/* Collection Header */}
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
<IconComponent
size={24}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ marginTop: 4 }}
/>
<div>
<h1
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
marginTop: 0,
}}
>
{collection.title}
</h1>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
}}
>
{collection.description}
</p>
<span
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.muted,
marginTop: spacing.sm,
}}
>
{collection.articleCount} articles in this collection
</span>
</div>
</div>
</div>
</div>
{/* Articles List */}
<div
style={{
maxWidth: 900,
margin: "0 auto",
padding: `${spacing.lg} ${spacing.lg}`,
}}
>
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
overflow: "hidden",
}}
>
{collection.articles.map((article, index) => (
<div
key={article.id}
onClick={() => onArticleClick(article.id)}
className="user-guide-article-row"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: spacing.lg,
borderBottom:
index < collection.articles.length - 1 ? border.default : "none",
cursor: "pointer",
}}
>
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
<FileText
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ marginTop: 2, flexShrink: 0 }}
/>
<div>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
marginBottom: spacing.xs,
}}
>
{article.title}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{article.description}
</span>
</div>
</div>
<ArrowRight
className="arrow-icon"
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{
flexShrink: 0,
marginLeft: 16,
}}
/>
</div>
))}
</div>
</div>
</div>
);
};
export default CollectionPage;
@@ -0,0 +1,45 @@
/* ContentRenderer styles - hover states and list styling */
/* List styling - colors handled by inline styles for theme support */
.user-guide-content-list li {
/* Font styles handled inline for theme-aware colors */
}
/* Table row hover */
.user-guide-table-row {
transition: background-color 150ms ease;
}
.user-guide-table-row:hover {
background-color: rgba(0, 0, 0, 0.04) !important;
}
[data-mui-color-scheme="dark"] .user-guide-table-row:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
}
/* Article links hover */
.user-guide-article-link-item {
transition: all 150ms ease;
}
.user-guide-article-link-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-article-link-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-article-link-item:hover .link-title {
color: #1570ef;
}
.user-guide-article-link-item .arrow-icon {
transition: all 150ms ease;
}
.user-guide-article-link-item:hover .arrow-icon {
transform: translateX(4px);
color: #1570ef;
}
@@ -0,0 +1,925 @@
import { useState } from "react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import ImageLightbox from "./ImageLightbox";
import {
CheckCircle,
Clock,
Rocket,
Shield,
Brain,
AlertTriangle,
Server,
Database,
Terminal,
FileText,
FolderKanban,
Gauge,
LayoutDashboard,
Menu,
Settings,
Info,
Lightbulb,
ArrowRight,
Globe,
Bell,
Link,
} from "lucide-react";
import "./ContentRenderer.css";
// Icon mapping
const iconMap = {
CheckCircle,
Clock,
Rocket,
Shield,
Brain,
AlertTriangle,
Server,
Database,
Terminal,
FileText,
FolderKanban,
Gauge,
LayoutDashboard,
Menu,
Settings,
Info,
Lightbulb,
Globe,
Bell,
Link,
};
const getIcon = (name) => iconMap[name] || Info;
const ContentRenderer = ({ content, onNavigate }) => {
const { colors, typography, spacing, border, chipStyles, imageStyles, isDark } =
useUserGuideTheme();
const [lightboxImage, setLightboxImage] = useState(null);
// Parse basic markdown in text (inside component to access onNavigate)
const parseMarkdown = (text) => {
const parts = [];
let key = 0;
// Supports: **bold**, `code`, [[link text]](collection/article)
const combinedRegex = /\*\*([^*]+)\*\*|`([^`]+)`|\[\[([^\]]+)\]\]\(([^)]+)\)/g;
let lastIndex = 0;
let match;
const getChipStyle = (codeContent) => {
const isUrl = /^https?:\/\//.test(codeContent);
return isUrl ? chipStyles.url : chipStyles.code;
};
while ((match = combinedRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
if (match[1]) {
// Bold: **text**
parts.push(<strong key={key++}>{match[1]}</strong>);
} else if (match[2]) {
// Code: `text`
const style = getChipStyle(match[2]);
parts.push(
<span
key={key++}
style={style}
>
{match[2]}
</span>
);
} else if (match[3] && match[4]) {
// Article link: [[text]](collection/article)
const linkText = match[3];
const linkPath = match[4];
const [collectionId, articleId] = linkPath.split("/");
parts.push(
<span
key={key++}
onClick={() => onNavigate?.(collectionId, articleId)}
style={{
color: colors.brand.primary,
cursor: "pointer",
textDecoration: "underline",
textDecorationColor: "transparent",
transition: "text-decoration-color 150ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecorationColor = colors.brand.primary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecorationColor = "transparent";
}}
>
{linkText}
</span>
);
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts.length > 0 ? parts : text;
};
const renderBlock = (block, index) => {
switch (block.type) {
case "heading":
return renderHeading(block, index);
case "paragraph":
return renderParagraph(block, index);
case "callout":
return renderCallout(block, index);
case "bullet-list":
return renderBulletList(block, index);
case "ordered-list":
return renderOrderedList(block, index);
case "checklist":
return renderChecklist(block, index);
case "icon-cards":
return renderIconCards(block, index);
case "grid-cards":
return renderGridCards(block, index);
case "time-estimate":
return renderTimeEstimate(block, index);
case "info-box":
return renderInfoBox(block, index);
case "code":
return renderCode(block, index);
case "requirements":
return renderRequirements(block, index);
case "table":
return renderTable(block, index);
case "image":
return renderImage(block, index);
case "article-links":
return renderArticleLinks(block, index);
default:
return null;
}
};
const renderHeading = (block, index) => {
const Tag = block.level === 2 ? "h2" : "h3";
return (
<Tag
key={index}
id={block.id}
style={{
fontFamily: typography.fontFamily.sans,
fontSize:
block.level === 2 ? typography.fontSize["2xl"] : typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.lg,
marginTop: index > 0 ? spacing["2xl"] : 0,
lineHeight: typography.lineHeight.normal,
}}
>
{block.text}
</Tag>
);
};
const renderParagraph = (block, index) => (
<p
key={index}
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.relaxed,
marginBottom: spacing.lg,
marginTop: 0,
}}
>
{parseMarkdown(block.text)}
</p>
);
const renderCallout = (block, index) => {
const calloutColors = {
info: colors.brand.primary,
tip: colors.brand.primary,
warning: colors.brand.warning,
success: colors.brand.success,
};
const accentColor = calloutColors[block.variant || "info"];
return (
<div
key={index}
style={{
border: border.default,
borderLeftWidth: "3px",
borderLeftColor: accentColor,
backgroundColor: isDark ? "rgba(255, 255, 255, 0.03)" : "rgba(0, 0, 0, 0.02)",
paddingLeft: spacing.lg,
paddingTop: spacing.md,
paddingBottom: spacing.md,
paddingRight: spacing.lg,
borderRadius: border.radius,
marginBottom: spacing["2xl"],
}}
>
{block.title && (
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
}}
>
{block.title}
</span>
)}
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{parseMarkdown(block.text)}
</span>
</div>
);
};
const renderBulletList = (block, index) => (
<ul
key={index}
className="user-guide-content-list"
style={{
paddingLeft: spacing.xl,
marginBottom: spacing["2xl"],
marginTop: 0,
}}
>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.sm,
}}
>
{item.bold && (
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
)}
{item.bold && <span style={{ color: colors.text.secondary }}> — </span>}
<span style={{ color: colors.text.secondary }}>
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
</span>
</li>
))}
</ul>
);
const renderOrderedList = (block, index) => (
<ol
key={index}
className="user-guide-content-list"
style={{
paddingLeft: spacing.xl,
marginBottom: spacing.lg,
marginTop: 0,
}}
>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.sm,
}}
>
{item.bold && (
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
)}
{item.bold && " "}
<span style={{ color: colors.text.secondary }}>
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
</span>
</li>
))}
</ol>
);
const renderChecklist = (block, index) => (
<div
key={index}
style={{ marginBottom: spacing["2xl"], paddingLeft: spacing.sm }}
>
{block.items.map((item, i) => (
<div
key={i}
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-start",
gap: "10px",
marginBottom: spacing.sm,
}}
>
<CheckCircle
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0, marginTop: 3 }}
/>
<span
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.tight,
}}
>
{parseMarkdown(item)}
</span>
</div>
))}
</div>
);
const renderIconCards = (block, index) => (
<div
key={index}
style={{
display: "flex",
flexDirection: "column",
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((item, i) => {
const IconComponent = getIcon(item.icon);
return (
<div
key={i}
style={{
display: "flex",
gap: spacing.md,
padding: spacing.sm,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
width: "20px",
height: "22px",
}}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</div>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: "22px",
marginBottom: "2px",
}}
>
{item.title}
</span>
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{parseMarkdown(item.description)}
</span>
</div>
</div>
);
})}
</div>
);
const renderGridCards = (block, index) => (
<div
key={index}
style={{
display: "grid",
gridTemplateColumns: `repeat(auto-fit, minmax(${block.columns === 2 ? "250px" : "200px"}, 1fr))`,
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((item, i) => {
const IconComponent = item.icon ? getIcon(item.icon) : null;
return (
<div
key={i}
style={{
padding: "14px",
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: spacing.sm,
marginBottom: "6px",
}}
>
{IconComponent && (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
width: "16px",
height: "18px",
}}
>
<IconComponent
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</span>
)}
<span
style={{
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: "18px",
}}
>
{item.title}
</span>
</div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.secondary,
lineHeight: typography.lineHeight.snug,
}}
>
{parseMarkdown(item.description)}
</span>
</div>
);
})}
</div>
);
const renderTimeEstimate = (block, index) => (
<div
key={index}
style={{
display: "inline-flex",
alignItems: "center",
gap: spacing.sm,
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
border: border.default,
borderRadius: border.radius,
marginBottom: spacing.xl,
}}
>
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
height: "20px",
}}
>
<Clock
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</span>
<span
style={{
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: "20px",
display: "inline-flex",
alignItems: "center",
}}
>
{parseMarkdown(block.text)}
</span>
</div>
);
const renderInfoBox = (block, index) => {
const IconComponent = block.icon ? getIcon(block.icon) : FolderKanban;
return (
<div
key={index}
style={{
display: "flex",
gap: spacing.md,
padding: spacing.lg,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
marginBottom: spacing.lg,
}}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
}}
>
{block.title}
</span>
<ul style={{ margin: 0, paddingLeft: "20px" }}>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.xs,
}}
>
{parseMarkdown(item)}
</li>
))}
</ul>
</div>
</div>
);
};
const renderCode = (block, index) => (
<div
key={index}
style={{
backgroundColor: colors.background.code,
borderRadius: border.radius,
padding: "14px 16px",
overflow: "auto",
marginBottom: spacing.lg,
}}
>
<pre
style={{
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.mono,
color: "#E5E7EB",
margin: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>
{block.code}
</pre>
</div>
);
const renderRequirements = (block, index) => (
<div
key={index}
style={{
display: "flex",
flexDirection: "column",
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((req, i) => {
const IconComponent = getIcon(req.icon);
return (
<div
key={i}
style={{
padding: spacing.lg,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
marginBottom: spacing.md,
}}
>
<IconComponent
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0 }}
/>
<span
style={{
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
}}
>
{req.title}
</span>
</div>
<ul style={{ margin: 0, paddingLeft: "20px" }}>
{req.items.map((item, j) => (
<li
key={j}
style={{
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.xs,
}}
>
{item}
</li>
))}
</ul>
</div>
);
})}
</div>
);
const renderTableCell = (value) => {
if (value === "✓") {
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: "rgba(19, 113, 91, 0.2)",
color: colors.brand.primary,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
}}
>
✓
</span>
);
}
if (value === "✗" || value === "—") {
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: "rgba(102, 112, 133, 0.2)",
color: colors.text.muted,
fontSize: typography.fontSize.sm,
}}
>
—
</span>
);
}
return parseMarkdown(value);
};
const renderTable = (block, index) => (
<div
key={index}
style={{
border: border.default,
borderRadius: border.radius,
overflow: "hidden",
marginBottom: spacing["2xl"],
}}
>
{/* Header */}
<div
style={{
display: "grid",
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
borderBottom: border.default,
padding: `${spacing.md} ${spacing.lg}`,
gap: spacing.md,
}}
>
{block.columns.map((col) => (
<span
key={col.key}
style={{
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.tight,
}}
>
{col.label}
</span>
))}
</div>
{/* Rows */}
{block.rows.map((row, i) => (
<div
key={i}
className="user-guide-table-row"
style={{
display: "grid",
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
borderBottom: i < block.rows.length - 1 ? border.default : "none",
backgroundColor:
i % 2 === 1
? isDark
? "rgba(255, 255, 255, 0.02)"
: "rgba(0, 0, 0, 0.02)"
: "transparent",
padding: `10px ${spacing.lg}`,
gap: spacing.md,
alignItems: "center",
}}
>
{block.columns.map((col, j) => (
<div
key={col.key}
style={{
fontSize: typography.fontSize.sm,
fontWeight:
j === 0 ? typography.fontWeight.medium : typography.fontWeight.normal,
color: j === 0 ? colors.text.primary : colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{renderTableCell(row[col.key])}
</div>
))}
</div>
))}
</div>
);
const renderImage = (block, index) => (
<div
key={index}
style={{
marginBottom: spacing["2xl"],
}}
>
<img
src={block.src}
alt={block.alt}
onClick={() =>
setLightboxImage({ src: block.src, alt: block.alt, caption: block.caption })
}
style={{
...imageStyles.image,
cursor: "zoom-in",
}}
/>
{block.caption && (
<p
style={{
...imageStyles.caption,
marginTop: spacing.sm,
marginBottom: 0,
}}
>
{block.caption}
</p>
)}
</div>
);
const renderArticleLinks = (block, index) => (
<div
key={index}
style={{ marginBottom: spacing["2xl"] }}
>
<div
style={{
height: "1px",
backgroundColor: colors.border.default,
marginBottom: spacing.xl,
}}
/>
{block.title && (
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.md,
}}
>
{block.title}
</span>
)}
<div style={{ display: "flex", flexDirection: "column" }}>
{block.items.map((item, i) => (
<div
key={i}
onClick={() => onNavigate?.(item.collectionId, item.articleId)}
className="user-guide-article-link-item"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: spacing.sm,
margin: `0 -${spacing.sm}`,
borderRadius: "4px",
cursor: "pointer",
}}
>
<span
className="link-title"
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
{item.title}
</span>
<ArrowRight
className="arrow-icon"
size={18}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginLeft: 16 }}
/>
</div>
))}
</div>
</div>
);
return (
<>
<div>{content.blocks.map((block, index) => renderBlock(block, index))}</div>
{/* Image Lightbox */}
{lightboxImage && (
<ImageLightbox
src={lightboxImage.src}
alt={lightboxImage.alt}
caption={lightboxImage.caption}
onClose={() => setLightboxImage(null)}
/>
)}
</>
);
};
export default ContentRenderer;
@@ -0,0 +1,14 @@
/* HelpSection styles */
.user-guide-help-button {
transition: all 150ms ease;
}
.user-guide-help-button:hover {
border-color: #1570ef;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
[data-mui-color-scheme="dark"] .user-guide-help-button:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
@@ -0,0 +1,134 @@
import { ExternalLink } from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./HelpSection.css";
const HelpSection = () => {
const { colors, typography, spacing, border } = useUserGuideTheme();
return (
<div
style={{
padding: spacing.xl,
minHeight: "100%",
}}
>
{/* Contact us */}
<div style={{ marginBottom: spacing["2xl"] }}>
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
marginTop: 0,
}}
>
Contact us
</h2>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
Can't find what you need? Our support team is here to help.
</p>
<a
href="https://discord.com/invite/NAb6H3UTjK"
target="_blank"
rel="noopener noreferrer"
className="user-guide-help-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: spacing.sm,
width: "100%",
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
textDecoration: "none",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
Join our Discord
<ExternalLink
size={14}
strokeWidth={1.5}
color={colors.text.muted}
/>
</a>
</div>
{/* Ask the community */}
<div>
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
marginTop: 0,
}}
>
GitHub discussions
</h2>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
Questions about features, how-tos, or use cases? Join the community discussion.
</p>
<a
href="https://github.com/bluewave-labs/checkmate/discussions"
target="_blank"
rel="noopener noreferrer"
className="user-guide-help-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: spacing.sm,
width: "100%",
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
textDecoration: "none",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
GitHub discussions
<ExternalLink
size={14}
strokeWidth={1.5}
color={colors.text.muted}
/>
</a>
</div>
</div>
);
};
export default HelpSection;
@@ -0,0 +1,125 @@
import { useEffect, useCallback } from "react";
import { X } from "lucide-react";
import { typography, spacing } from "./styles/theme";
const ImageLightbox = ({ src, alt, caption, onClose }) => {
// Close on escape key
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Escape") {
onClose();
}
},
[onClose]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
};
}, [handleKeyDown]);
// Close when clicking backdrop
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div
onClick={handleBackdropClick}
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.9)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 2000,
padding: spacing.xl,
cursor: "zoom-out",
}}
>
{/* Close button */}
<button
onClick={onClose}
style={{
position: "absolute",
top: 16,
right: 16,
width: 40,
height: 40,
borderRadius: "50%",
backgroundColor: "rgba(255, 255, 255, 0.1)",
border: "none",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "background-color 150ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
}}
title="Close (Esc)"
>
<X
size={24}
strokeWidth={1.5}
color="#fff"
/>
</button>
{/* Image container */}
<div
style={{
maxWidth: "90vw",
maxHeight: "90vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: caption ? "calc(90vh - 40px)" : "90vh",
objectFit: "contain",
borderRadius: "4px",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
cursor: "default",
}}
onClick={(e) => e.stopPropagation()}
/>
{caption && (
<p
style={{
marginTop: spacing.md,
fontSize: typography.fontSize.sm,
color: "rgba(255, 255, 255, 0.8)",
textAlign: "center",
fontStyle: "italic",
}}
>
{caption}
</p>
)}
</div>
</div>
);
};
export default ImageLightbox;
@@ -0,0 +1,638 @@
# Checkmate User Guide Style Guide
This document defines the writing standards, formatting rules, and content patterns for all Checkmate user documentation.
---
## Voice & Tone
### Core Principles
1. **Second person, active voice** - Address the reader directly
- ✅ "You can configure monitors from the dashboard"
- ❌ "Monitors can be configured from the dashboard"
2. **Professional but approachable** - Clear and helpful, not stiff or overly casual
- ✅ "Select the notification channel you want to use"
- ❌ "Go ahead and pick whatever notification channel works for you!"
- ❌ "The user shall select the appropriate notification channel"
3. **Direct and instructional** - Tell users what to do, not what the system does
- ✅ "Click Save to apply your changes"
- ❌ "The Save button will apply your changes"
4. **User-focused** - Lead with what users can accomplish
- ✅ "You can track server performance with infrastructure monitors"
- ❌ "Infrastructure monitors use the Capture agent to collect metrics"
### Language Patterns
**Opening phrases:**
- "The [Feature] allows you to..."
- "[Feature] is..."
- "You can..."
- "To [action]:"
**Navigation instructions:**
- "Navigate to [location]"
- "Click [element]"
- "Select [option]"
- "From the [page], click..."
**Avoid:**
- Passive voice where active is clearer
- Jargon without explanation
- Assuming prior knowledge
- Marketing language ("powerful", "seamless", "robust")
---
## Content Structure
### Standard Article Pattern
Every article should follow this flow:
```
1. OVERVIEW (H2)
- 1-2 paragraphs explaining what and why
- Set context for the topic
2. VISUAL (Optional)
- Screenshot showing the feature
- Include after intro, not before
3. MAIN CONTENT (H2/H3 sections)
- Procedures with ordered lists
- Features with bullet lists
- Reference info in tables
4. TIPS & WARNINGS (Callouts)
- Best practices
- Common pitfalls
- Important restrictions
5. RELATED ARTICLES (Links)
- 2-4 related topics
- Logical next steps
```
### Heading Hierarchy
Use only two heading levels within articles:
- **H2** - Major sections (Overview, Configuration, Settings)
- **H3** - Subsections within H2s (Creating a monitor, Editing settings)
Never use H4 or deeper. If content requires more nesting, restructure into separate sections.
### Section Length
| Element | Target Length |
| ------------- | -------------- |
| Paragraphs | 2-4 sentences |
| H2 sections | 100-300 words |
| H3 sections | 50-150 words |
| Full articles | 800-1500 words |
---
## Formatting Rules
### Lists
**Bullet lists** - Use for features, options, characteristics (unordered items)
```javascript
{
type: 'bullet-list',
items: [
{ text: 'HTTP monitors check website availability' },
{ text: 'Ping monitors verify server reachability' },
{ text: 'Port monitors test specific service ports' }
]
}
```
**Ordered lists** - Use for step-by-step procedures (sequential actions)
```javascript
{
type: 'ordered-list',
items: [
{ text: 'Navigate to **Uptime** in the sidebar' },
{ text: 'Click **Create monitor**' },
{ text: 'Enter the URL you want to monitor' },
{ text: 'Click **Save** to create the monitor' }
]
}
```
**List guidelines:**
- Keep procedures to 4-6 steps maximum
- If longer, break into multiple procedures
- Start each item with an action verb for procedures
- Use parallel structure (all items same grammatical form)
### Callouts
Use callouts sparingly for important information that shouldn't be missed.
**Types:**
| Variant | Use For | Example |
| --------- | --------------------------------- | -------------------------------------------------------------------------- |
| `info` | Additional context, clarification | "Monitors check at the interval you specify, starting from creation time." |
| `tip` | Best practices, recommendations | "Use descriptive names to easily identify monitors in alerts." |
| `warning` | Restrictions, potential issues | "Deleting a monitor removes all associated check history." |
| `success` | Confirmations, positive outcomes | "Your monitor is now active and checking every 5 minutes." |
```javascript
{
type: 'callout',
variant: 'tip',
title: 'Best practice',
text: 'Group related monitors together for easier management.'
}
```
**Callout guidelines:**
- Maximum 2-3 callouts per article
- Keep callout text concise (1-2 sentences)
- Title is optional but helps scannability
- Don't use callouts for routine information
### Inline Formatting
**Bold** - Use for:
- UI element names: "Click **Save**"
- Key terms on first use: "**Uptime** measures availability over time"
- Emphasis in lists: "**Name** — The display name for your monitor"
**Code/monospace** - Use for:
- URLs: `https://example.com`
- File paths: `/api/v1/monitors`
- Technical values: `200`, `timeout`
- Commands: `npm run dev`
```javascript
// In paragraph text, use backticks in markdown:
{
text: "Enter the URL (e.g., `https://example.com`) in the field";
}
```
### Tables
Use tables for:
- Feature comparisons
- Configuration options
- Reference data with multiple attributes
```javascript
{
type: 'table',
columns: [
{ key: 'type', label: 'Monitor Type', width: '1fr' },
{ key: 'use', label: 'Best For', width: '2fr' }
],
rows: [
{ type: 'HTTP', use: 'Websites and APIs' },
{ type: 'Ping', use: 'Server availability' },
{ type: 'Port', use: 'Specific services (databases, mail servers)' }
]
}
```
---
## Content Types
### Block Types Reference
| Type | Purpose | When to Use |
| --------------- | ----------------- | ------------------------- |
| `heading` | Section titles | Start each major topic |
| `paragraph` | Body text | Explanations, context |
| `bullet-list` | Unordered items | Features, options |
| `ordered-list` | Sequential steps | Procedures |
| `checklist` | Checkable items | Requirements, checklists |
| `callout` | Important notices | Tips, warnings |
| `code` | Code blocks | Commands, config examples |
| `table` | Structured data | Comparisons, reference |
| `image` | Screenshots | UI demonstration |
| `icon-cards` | Feature grids | Capability overviews |
| `article-links` | Related content | End-of-article navigation |
### Icon Cards
Use for presenting 4-8 related features or concepts:
```javascript
{
type: 'icon-cards',
items: [
{
icon: 'Globe',
title: 'HTTP monitoring',
description: 'Check website availability and response times'
},
{
icon: 'Server',
title: 'Infrastructure',
description: 'Monitor CPU, memory, and disk usage'
}
]
}
```
### Article Links
Always end articles with related content:
```javascript
{
type: 'article-links',
title: 'Related articles',
items: [
{
collectionId: 'uptime-monitoring',
articleId: 'http-monitors',
title: 'Creating HTTP monitors',
description: 'Set up your first website monitor'
}
]
}
```
---
## Images & Screenshots
### When to Include Images
- After introducing a feature (not before)
- For complex UI that benefits from visual reference
- For multi-step workflows
- Skip for simple, text-describable actions
### Image Requirements
```javascript
{
type: 'image',
src: '/images/user-guide/monitor-create.png',
alt: 'Monitor creation form showing URL field, name field, and interval selector',
caption: 'The monitor creation form with required fields highlighted'
}
```
**Alt text guidelines:**
- Describe what the image shows (15-30 words)
- Include key UI elements visible
- Make it useful for screen readers
**Caption guidelines:**
- Explain the context or what to notice
- Keep to one sentence
- Optional but recommended
### Screenshot Standards
- Capture at consistent browser width (1200px recommended)
- Use light theme for screenshots
- Highlight relevant areas if needed
- Avoid personal data in screenshots
- Crop to show relevant UI only
---
## Writing Procedures
### Step-by-Step Instructions
**Format:**
```javascript
{
type: 'ordered-list',
items: [
{ text: 'Navigate to **Uptime** in the sidebar' },
{ text: 'Click **Create monitor** in the top right' },
{ text: 'Enter the URL you want to monitor' },
{ text: 'Set the check interval (default: 5 minutes)' },
{ text: 'Click **Save** to create the monitor' }
]
}
```
**Guidelines:**
- Start each step with an action verb (Click, Enter, Select, Navigate)
- One action per step
- Include the location of UI elements
- Bold all clickable elements
- Add context in parentheses when helpful
### Describing Features
**Format:**
```javascript
{
type: 'bullet-list',
items: [
{ bold: 'Name', text: 'A descriptive label for your monitor' },
{ bold: 'URL', text: 'The address to check (must include protocol)' },
{ bold: 'Interval', text: 'How often to perform checks (1-60 minutes)' }
]
}
```
---
## Terminology
### Consistent Terms
Use these terms consistently throughout documentation:
| Term | Use | Don't Use |
| ------------ | ------------------------- | --------------------- |
| Monitor | The configured check | Watcher, probe, check |
| Check | A single monitoring event | Ping, request, poll |
| Incident | A detected problem | Outage, alert, issue |
| Notification | Alert sent to user | Message, email, ping |
| Dashboard | Main overview page | Home, overview |
| Sidebar | Left navigation | Menu, nav |
### Feature Names
Always match UI exactly:
- **Uptime** (not "Uptime Monitoring")
- **PageSpeed** (not "Page Speed" or "Pagespeed")
- **Infrastructure** (not "Server Monitoring")
- **Status pages** (not "Status Pages" - lowercase 'p')
---
## Article Templates
### Getting Started Article
```javascript
{
blocks: [
{
type: "heading",
level: 2,
text: "Overview",
id: "overview",
},
{
type: "paragraph",
text: "[1-2 sentences: What this feature/concept is]",
},
{
type: "paragraph",
text: "[1-2 sentences: Why it matters to users]",
},
{
type: "heading",
level: 2,
text: "[Main topic]",
id: "main-topic",
},
// ... content blocks
{
type: "article-links",
title: "Next steps",
items: [
// 2-3 logical next articles
],
},
];
}
```
### How-To Article
```javascript
{
blocks: [
{
type: "heading",
level: 2,
text: "Overview",
id: "overview",
},
{
type: "paragraph",
text: "[Brief description of what user will accomplish]",
},
{
type: "heading",
level: 2,
text: "[Action: Creating/Configuring/Setting up...]",
id: "action",
},
{
type: "ordered-list",
items: [
// 4-6 steps
],
},
{
type: "callout",
variant: "tip",
text: "[Best practice or helpful hint]",
},
{
type: "article-links",
title: "Related articles",
items: [],
},
];
}
```
### Reference Article
```javascript
{
blocks: [
{
type: 'heading',
level: 2,
text: 'Overview',
id: 'overview'
},
{
type: 'paragraph',
text: '[What this reference covers]'
},
{
type: 'heading',
level: 2,
text: '[Category 1]',
id: 'category-1'
},
{
type: 'table',
columns: [...],
rows: [...]
},
// More categories as needed
{
type: 'article-links',
title: 'Related articles',
items: []
}
]
}
```
---
## Quality Checklist
Before publishing any article, verify:
### Content
- [ ] Overview explains what AND why
- [ ] Procedures are tested and accurate
- [ ] All UI element names match exactly
- [ ] No assumptions about user knowledge
- [ ] Related articles are linked
### Formatting
- [ ] Only H2 and H3 headings used
- [ ] All headings have unique IDs
- [ ] Lists use correct type (bullet vs ordered)
- [ ] Callouts are used sparingly (max 3)
- [ ] Bold used for UI elements
### Style
- [ ] Active voice throughout
- [ ] Second person ("you")
- [ ] Action verbs start procedure steps
- [ ] Consistent terminology
- [ ] No marketing language
### Technical
- [ ] All links work
- [ ] Images have alt text
- [ ] Code examples are correct
- [ ] No placeholder content
---
## Available Icons
For icon-cards and other icon references, use these Lucide icon names:
**Navigation:**
- `Globe` - Uptime/HTTP
- `Gauge` - PageSpeed
- `Server` - Infrastructure
- `Bell` - Notifications
- `AlertTriangle` - Incidents
- `Wifi` - Status pages
- `Wrench` - Maintenance
- `Settings` - Settings
- `Users` - Team
**Actions:**
- `CheckCircle` - Success/complete
- `Clock` - Time/scheduling
- `Info` - Information
- `Lightbulb` - Tips
- `ArrowRight` - Navigation
**Features:**
- `Database` - Data/storage
- `Terminal` - Commands
- `FileText` - Documentation
- `Shield` - Security
- `Rocket` - Getting started
---
## File Structure
Articles are stored in `/content/index.js`:
```javascript
export const articleContents = {
"collection-id": {
"article-id": {
blocks: [
// Content blocks
],
},
},
};
```
Article metadata is in `/content/userGuideConfig.js`:
```javascript
{
id: 'article-id',
title: 'Article Title',
description: 'Brief description for search and previews',
keywords: ['search', 'terms', 'for', 'this', 'article']
}
```
---
## Examples
### Good Paragraph
> "HTTP monitors check your websites and APIs at regular intervals. When a check fails—due to a timeout, error response, or connection issue—Checkmate creates an incident and sends notifications to your configured channels."
### Good Procedure
> 1. Navigate to **Uptime** in the sidebar
> 2. Click **Create monitor** in the top right
> 3. Enter the URL you want to monitor (include `https://`)
> 4. Set your preferred check interval
> 5. Click **Save** to create the monitor
### Good Callout
> **Tip:** Use descriptive monitor names like "Production API" or "Marketing Website" to quickly identify issues in notifications.
### Good Feature List
> - **Name** — A descriptive label for easy identification
> - **URL** — The address to monitor (must include protocol)
> - **Interval** — How often to check (1-60 minutes)
> - **Timeout** — How long to wait before marking as failed
---
_Last updated: January 2025_
@@ -0,0 +1,12 @@
/* SearchResults styles (Dark theme) */
.user-guide-search-result-item {
transition:
background-color 150ms ease,
border-color 150ms ease;
}
.user-guide-search-result-item:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
border-color: #4a5568 !important;
}
@@ -0,0 +1,173 @@
import { FileText, ArrowRight } from "lucide-react";
import { searchArticles } from "./content/userGuideConfig";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./SearchResults.css";
const SearchResults = ({ query, onNavigate, onClearSearch }) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const results = searchArticles(query);
const handleResultClick = (result) => {
onNavigate(result.collectionId, result.articleId);
onClearSearch();
};
if (query.trim().length < 2) {
return (
<div style={{ padding: spacing.lg }}>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
textAlign: "center",
margin: 0,
}}
>
Type at least 2 characters to search...
</p>
</div>
);
}
if (results.length === 0) {
return (
<div style={{ padding: spacing.xl }}>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
textAlign: "center",
marginBottom: spacing.sm,
marginTop: 0,
}}
>
No results found for "{query}"
</p>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
textAlign: "center",
margin: 0,
}}
>
Try different keywords or browse by topic
</p>
</div>
);
}
return (
<div style={{ padding: spacing.lg }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.md,
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
}}
>
{results.length} result{results.length !== 1 ? "s" : ""} for "{query}"
</span>
<button
onClick={onClearSearch}
style={{
background: "none",
border: "none",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.brand.primary,
cursor: "pointer",
padding: 0,
}}
>
Clear search
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: spacing.sm }}>
{results.map((result) => (
<div
key={`${result.collectionId}-${result.articleId}`}
onClick={() => handleResultClick(result)}
className="user-guide-search-result-item"
style={{
display: "flex",
alignItems: "flex-start",
gap: spacing.md,
padding: spacing.md,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
cursor: "pointer",
}}
>
<FileText
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
marginBottom: 2,
}}
>
{result.articleTitle}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{result.articleDescription}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.xs,
color: colors.text.muted,
marginTop: spacing.xs,
}}
>
in {result.collectionTitle}
</span>
</div>
<ArrowRight
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginTop: 2 }}
/>
</div>
))}
</div>
</div>
);
};
export default SearchResults;
@@ -0,0 +1,40 @@
/* SidebarHeader styles - Dark theme */
.user-guide-header-icon-button {
transition: all 150ms ease;
}
.user-guide-header-icon-button:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: #1570ef !important;
}
.user-guide-header-icon-button.active {
background-color: #0d0d0d;
color: #1570ef;
}
.user-guide-breadcrumb-dropdown-trigger {
transition: background-color 150ms ease;
}
.user-guide-breadcrumb-dropdown-trigger:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-breadcrumb-item {
transition: background-color 150ms ease;
}
.user-guide-breadcrumb-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-search-input:focus {
outline: none;
border-color: #1570ef;
}
.user-guide-search-input::placeholder {
color: #667085;
}
@@ -0,0 +1,399 @@
import { useState, useRef, useEffect } from "react";
import {
Home,
ChevronDown,
ChevronLeft,
ChevronRight,
Search,
ExternalLink,
X,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./SidebarHeader.css";
const SidebarHeader = ({
breadcrumbs,
onHomeClick,
onBack,
onForward,
canGoBack,
canGoForward,
onClose,
onOpenInNewTab,
isSearchOpen,
onSearchToggle,
searchQuery,
onSearchChange,
showOpenInNewTab = true,
showNavigation = true,
}) => {
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
const searchInputRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Focus search input when opened
useEffect(() => {
if (isSearchOpen && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isSearchOpen]);
// Get current breadcrumb (last item)
const currentBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
return (
<div
style={{
backgroundColor: colors.background.white,
borderBottom: border.default,
padding: `${spacing.sm} ${spacing.md}`,
}}
>
{/* Main header row */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: spacing.sm,
}}
>
{/* Left side: Navigation + Breadcrumb dropdown */}
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.xs,
flex: 1,
minWidth: 0,
}}
>
{/* Navigation buttons - only shown when showNavigation is true */}
{showNavigation && (
<>
{/* Back button */}
<button
onClick={onBack}
disabled={!canGoBack}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: canGoBack ? "pointer" : "default",
color: canGoBack ? colors.text.secondary : colors.border.default,
flexShrink: 0,
opacity: canGoBack ? 1 : 0.5,
}}
title="Back"
>
<ChevronLeft
size={16}
strokeWidth={1.5}
/>
</button>
{/* Forward button */}
<button
onClick={onForward}
disabled={!canGoForward}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: canGoForward ? "pointer" : "default",
color: canGoForward ? colors.text.secondary : colors.border.default,
flexShrink: 0,
opacity: canGoForward ? 1 : 0.5,
}}
title="Forward"
>
<ChevronRight
size={16}
strokeWidth={1.5}
/>
</button>
{/* Home button */}
<button
onClick={breadcrumbs.length > 1 ? onHomeClick : undefined}
disabled={breadcrumbs.length <= 1}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
color:
breadcrumbs.length > 1
? colors.text.secondary
: colors.border.default,
flexShrink: 0,
opacity: breadcrumbs.length > 1 ? 1 : 0.5,
}}
title="Home"
>
<Home
size={16}
strokeWidth={1.5}
/>
</button>
</>
)}
{/* Breadcrumb dropdown */}
<div
ref={dropdownRef}
style={{ position: "relative", flex: 1, minWidth: 0 }}
>
<button
onClick={() => breadcrumbs.length > 1 && setIsDropdownOpen(!isDropdownOpen)}
className="user-guide-breadcrumb-dropdown-trigger"
style={{
display: "flex",
alignItems: "center",
gap: 4,
padding: "4px 8px",
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
maxWidth: "100%",
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{currentBreadcrumb?.label || "User guide"}
</span>
{breadcrumbs.length > 1 && (
<ChevronDown
size={14}
strokeWidth={1.5}
color={colors.text.muted}
style={{
flexShrink: 0,
transform: isDropdownOpen ? "rotate(180deg)" : "rotate(0)",
transition: "transform 150ms ease",
}}
/>
)}
</button>
{/* Dropdown menu */}
{isDropdownOpen && breadcrumbs.length > 1 && (
<div
className="user-guide-breadcrumb-dropdown"
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: 4,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
boxShadow: isDark
? "0 4px 12px rgba(0, 0, 0, 0.3)"
: "0 4px 12px rgba(0, 0, 0, 0.1)",
minWidth: 180,
maxWidth: 280,
zIndex: 100,
overflow: "hidden",
}}
>
{breadcrumbs.slice(0, -1).map((item, index) => (
<button
key={index}
onClick={() => {
item.onClick();
setIsDropdownOpen(false);
}}
className="user-guide-breadcrumb-item"
style={{
display: "block",
width: "100%",
padding: "10px 14px",
backgroundColor: "transparent",
border: "none",
textAlign: "left",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.primary,
}}
>
{item.label}
</button>
))}
</div>
)}
</div>
</div>
{/* Right side: Actions */}
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.xs,
flexShrink: 0,
}}
>
{/* Open in new tab - only shown when showOpenInNewTab is true */}
{showOpenInNewTab && (
<button
onClick={onOpenInNewTab}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: colors.text.secondary,
}}
title="Open in new tab"
>
<ExternalLink
size={16}
strokeWidth={1.5}
/>
</button>
)}
{/* Search toggle */}
<button
onClick={onSearchToggle}
className={`user-guide-header-icon-button ${isSearchOpen ? "active" : ""}`}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: isSearchOpen ? colors.background.alt : "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: isSearchOpen ? colors.brand.primary : colors.text.secondary,
}}
title="Search"
>
<Search
size={16}
strokeWidth={1.5}
/>
</button>
{/* Close button */}
<button
onClick={onClose}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: colors.text.secondary,
}}
title="Close"
>
<X
size={16}
strokeWidth={1.5}
/>
</button>
</div>
</div>
{/* Search input (collapsible) */}
{isSearchOpen && (
<div style={{ marginTop: spacing.sm }}>
<div style={{ position: "relative" }}>
<Search
size={14}
strokeWidth={1.5}
color={colors.text.muted}
style={{
position: "absolute",
left: 10,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
}}
/>
<input
ref={searchInputRef}
type="text"
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="user-guide-search-input"
style={{
width: "100%",
height: 32,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.sans,
backgroundColor: colors.background.alt,
borderRadius: border.radius,
border: border.default,
paddingLeft: 32,
paddingRight: 12,
boxSizing: "border-box",
color: colors.text.primary,
}}
/>
</div>
</div>
)}
</div>
);
};
export default SidebarHeader;
@@ -0,0 +1,11 @@
/* SidebarWrapper styles (Dark theme) */
.user-guide-sidebar-container {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
}
/* Push main content when sidebar is open */
body.user-guide-sidebar-open .home-content-wrapper {
margin-right: var(--help-sidebar-padding, 440px);
transition: margin-right 300ms ease-in-out;
}
@@ -0,0 +1,485 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import TabBar from "./TabBar";
import SidebarHeader from "./SidebarHeader";
import UserGuideLanding from "./UserGuideLanding";
import CollectionPage from "./CollectionPage";
import ArticlePage from "./ArticlePage";
import ContentRenderer from "./ContentRenderer";
import HelpSection from "./HelpSection";
import SearchResults from "./SearchResults";
import { getCollection, getArticle } from "./content/userGuideConfig";
import { getArticleContent } from "./content";
import { extractToc } from "./content/contentTypes";
import {
useUserGuideSidebarContext,
DEFAULT_CONTENT_WIDTH,
} from "./UserGuideSidebarContext";
import "./SidebarWrapper.css";
const STORAGE_KEY = "checkmate-sidebar-state";
const MIN_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH;
const MAX_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH * 2; // 100% wider
const SidebarWrapper = ({ isOpen, onClose, onOpen, initialPath, onOpenInNewTab }) => {
const { setContentWidth: setContextContentWidth } = useUserGuideSidebarContext();
const { colors, border } = useUserGuideTheme();
const [activeTab, setActiveTab] = useState("user-guide");
const [collectionId, setCollectionId] = useState(undefined);
const [articleId, setArticleId] = useState(undefined);
const [searchQuery, setSearchQuery] = useState("");
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [contentWidth, setContentWidthLocal] = useState(DEFAULT_CONTENT_WIDTH);
const [isResizing, setIsResizing] = useState(false);
const [isHoveringHandle, setIsHoveringHandle] = useState(false);
const [mouseY, setMouseY] = useState(0);
const resizeRef = useRef(null);
const handleRef = useRef(null);
// Sync content width with context whenever it changes
const setContentWidth = useCallback(
(width) => {
setContentWidthLocal(width);
setContextContentWidth(width);
},
[setContextContentWidth]
);
// Initialize context with current width on mount
useEffect(() => {
setContextContentWidth(DEFAULT_CONTENT_WIDTH);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run once on mount
// Navigation history
const [history, setHistory] = useState([
{ collectionId: undefined, articleId: undefined },
]);
const [historyIndex, setHistoryIndex] = useState(0);
const isNavigatingRef = useRef(false);
// Parse initial path on mount
useEffect(() => {
if (initialPath) {
const parts = initialPath.split("/");
if (parts.length >= 1 && parts[0]) {
setCollectionId(parts[0]);
if (parts.length >= 2 && parts[1]) {
setArticleId(parts[1]);
}
}
}
}, [initialPath]);
// Persist sidebar state to localStorage
useEffect(() => {
if (isOpen) {
const state = { activeTab, collectionId, articleId };
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
}, [isOpen, activeTab, collectionId, articleId]);
// Restore state from localStorage on mount
useEffect(() => {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState && !initialPath) {
try {
const {
activeTab: savedTab,
collectionId: savedCollection,
articleId: savedArticle,
} = JSON.parse(savedState);
if (savedTab) setActiveTab(savedTab);
if (savedCollection) setCollectionId(savedCollection);
if (savedArticle) setArticleId(savedArticle);
} catch (e) {
// Ignore parse errors
}
}
}, [initialPath]);
// Track navigation changes in history
useEffect(() => {
if (isNavigatingRef.current) {
isNavigatingRef.current = false;
return;
}
// Only track if this is a new navigation (not from back/forward)
const currentEntry = history[historyIndex];
if (
currentEntry?.collectionId !== collectionId ||
currentEntry?.articleId !== articleId
) {
// Truncate forward history and add new entry
const newHistory = [
...history.slice(0, historyIndex + 1),
{ collectionId, articleId },
];
setHistory(newHistory);
setHistoryIndex(newHistory.length - 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionId, articleId]);
// Navigation handlers
const handleNavigate = useCallback((newCollectionId, newArticleId) => {
setCollectionId(newCollectionId);
setArticleId(newArticleId);
}, []);
const handleBackToHome = useCallback(() => {
setCollectionId(undefined);
setArticleId(undefined);
}, []);
// Back/Forward navigation
const canGoBack = historyIndex > 0;
const canGoForward = historyIndex < history.length - 1;
const handleBack = useCallback(() => {
if (historyIndex > 0) {
isNavigatingRef.current = true;
const prevIndex = historyIndex - 1;
const prevEntry = history[prevIndex];
setHistoryIndex(prevIndex);
setCollectionId(prevEntry.collectionId);
setArticleId(prevEntry.articleId);
}
}, [historyIndex, history]);
const handleForward = useCallback(() => {
if (historyIndex < history.length - 1) {
isNavigatingRef.current = true;
const nextIndex = historyIndex + 1;
const nextEntry = history[nextIndex];
setHistoryIndex(nextIndex);
setCollectionId(nextEntry.collectionId);
setArticleId(nextEntry.articleId);
}
}, [historyIndex, history]);
const handleBackToCollection = useCallback(() => {
setArticleId(undefined);
}, []);
const handleHomeClick = useCallback(() => {
setCollectionId(undefined);
setArticleId(undefined);
}, []);
// Get current collection and article
const collection = collectionId ? getCollection(collectionId) : undefined;
const article =
collectionId && articleId ? getArticle(collectionId, articleId) : undefined;
// Get prev/next articles for navigation
const getAdjacentArticles = () => {
if (!collection || !articleId) return { prev: undefined, next: undefined };
const currentIndex = collection.articles.findIndex((a) => a.id === articleId);
return {
prev: currentIndex > 0 ? collection.articles[currentIndex - 1] : undefined,
next:
currentIndex < collection.articles.length - 1
? collection.articles[currentIndex + 1]
: undefined,
};
};
const { prev: prevArticle, next: nextArticle } = getAdjacentArticles();
// Get article content
const articleContent =
collectionId && articleId ? getArticleContent(collectionId, articleId) : undefined;
// Auto-generate TOC from content headings
const tocItems = articleContent ? extractToc(articleContent.blocks) : undefined;
// Build breadcrumb items for dropdown
const buildBreadcrumbs = () => {
let items = [];
switch (activeTab) {
case "user-guide":
items = [{ label: "User guide", onClick: handleHomeClick }];
if (collection) {
items.push({ label: collection.title, onClick: () => setArticleId(undefined) });
}
if (article) {
items.push({ label: article.title, onClick: () => {} });
}
return items;
default:
return [{ label: "Help", onClick: () => {} }];
}
};
// Handle "Open in new tab"
const handleOpenInNewTab = () => {
let path = "https://bluewavelabs.gitbook.io/checkmate";
if (onOpenInNewTab) {
onOpenInNewTab();
} else {
window.open(path, "_blank", "noopener,noreferrer");
}
};
// Clear search and close search bar
const handleClearSearch = useCallback(() => {
setSearchQuery("");
setIsSearchOpen(false);
}, []);
// Render User Guide content
const renderUserGuideContent = () => {
// Show search results when search is active
if (isSearchOpen && searchQuery.trim().length > 0) {
return (
<SearchResults
query={searchQuery}
onNavigate={(colId, artId) => {
handleNavigate(colId, artId);
handleClearSearch();
}}
onClearSearch={handleClearSearch}
/>
);
}
if (collection && article) {
return (
<ArticlePage
collection={collection}
article={article}
onBack={handleBackToCollection}
onBackToHome={handleBackToHome}
prevArticle={prevArticle}
nextArticle={nextArticle}
onPrevArticle={
prevArticle ? () => handleNavigate(collectionId, prevArticle.id) : undefined
}
onNextArticle={
nextArticle ? () => handleNavigate(collectionId, nextArticle.id) : undefined
}
tocItems={tocItems}
mode="in-app"
>
{articleContent && (
<ContentRenderer
content={articleContent}
onNavigate={handleNavigate}
/>
)}
</ArticlePage>
);
}
if (collection) {
return (
<CollectionPage
collection={collection}
onArticleClick={(id) => handleNavigate(collectionId, id)}
/>
);
}
return (
<UserGuideLanding
onNavigate={handleNavigate}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
mode="in-app"
/>
);
};
const contentArea = (tabValue) => {
switch (tabValue) {
case "user-guide":
return renderUserGuideContent();
default:
return <HelpSection />;
}
};
// Handle tab click - open sidebar if closed, close if clicking active tab, or switch tab
const handleTabClick = (tab) => {
if (!isOpen) {
onOpen();
setActiveTab(tab);
} else if (activeTab === tab) {
// Clicking the already-active tab closes the sidebar
onClose();
} else {
// Clicking a different tab switches to it
setActiveTab(tab);
}
};
// Resize handlers
const handleResizeStart = useCallback(
(e) => {
e.preventDefault();
setIsResizing(true);
resizeRef.current = { startX: e.clientX, startWidth: contentWidth };
},
[contentWidth]
);
const handleResizeMove = useCallback(
(e) => {
if (!isResizing || !resizeRef.current) return;
const deltaX = resizeRef.current.startX - e.clientX;
const newWidth = Math.min(
MAX_CONTENT_WIDTH,
Math.max(MIN_CONTENT_WIDTH, resizeRef.current.startWidth + deltaX)
);
setContentWidth(newWidth);
},
[isResizing, setContentWidth]
);
const handleResizeEnd = useCallback(() => {
setIsResizing(false);
setIsHoveringHandle(false);
resizeRef.current = null;
}, []);
// Add/remove resize event listeners
useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleResizeMove, handleResizeEnd]);
return (
<div
className="user-guide-sidebar-container"
style={{
position: "fixed",
top: 0,
right: 0,
height: "100vh",
display: "flex",
zIndex: 1100,
}}
>
{/* Resize Handle - positioned absolutely on the left edge */}
<div
ref={handleRef}
onMouseDown={handleResizeStart}
onMouseEnter={() => setIsHoveringHandle(true)}
onMouseLeave={() => !isResizing && setIsHoveringHandle(false)}
onMouseMove={(e) => {
if (handleRef.current) {
const rect = handleRef.current.getBoundingClientRect();
setMouseY(e.clientY - rect.top);
}
}}
style={{
position: "absolute",
left: 0,
top: 0,
width: 8,
height: "100%",
cursor: "ew-resize",
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
{/* Visual indicator line */}
<div
style={{
width: isResizing ? 2 : 1,
height: "100%",
backgroundColor:
isResizing || isHoveringHandle ? "#4CAF93" : colors.border.default,
transition: isResizing ? "none" : "all 150ms ease",
position: "relative",
}}
>
{/* Grab indicator follows mouse */}
{(isHoveringHandle || isResizing) && (
<div
style={{
position: "absolute",
left: -1,
top: mouseY - 10,
width: 3,
height: 20,
backgroundColor: "#555",
borderRadius: 1,
}}
/>
)}
</div>
</div>
{/* Tab Bar - Always visible */}
<TabBar
activeTab={isOpen ? activeTab : undefined}
onTabChange={handleTabClick}
/>
{/* Main Sidebar Content - Slides in/out */}
<div
style={{
width: isOpen ? contentWidth : 0,
height: "100%",
backgroundColor: colors.background.white,
borderLeft: isOpen ? border.default : "none",
display: "flex",
flexDirection: "column",
overflow: "hidden",
transition: isResizing ? "none" : "width 300ms ease-in-out",
position: "relative",
}}
>
{isOpen && (
<>
{/* Header */}
<SidebarHeader
showOpenInNewTab={activeTab === "user-guide"}
showNavigation={activeTab === "user-guide"}
breadcrumbs={buildBreadcrumbs()}
onHomeClick={handleHomeClick}
onBack={handleBack}
onForward={handleForward}
canGoBack={canGoBack}
canGoForward={canGoForward}
onClose={onClose}
onOpenInNewTab={handleOpenInNewTab}
isSearchOpen={isSearchOpen}
onSearchToggle={() => setIsSearchOpen(!isSearchOpen)}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
{/* Content Area */}
<div
style={{
flex: 1,
overflowY: "auto",
backgroundColor: colors.background.alt,
}}
>
{contentArea(activeTab)}
</div>
</>
)}
</div>
</div>
);
};
export default SidebarWrapper;
@@ -0,0 +1,13 @@
/* TabBar styles */
.user-guide-tab-item {
transition: all 150ms ease;
}
.user-guide-tab-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-tab-item.active:hover {
background-color: #1a1a1a;
}
@@ -0,0 +1,96 @@
import { BookOpen, HelpCircle } from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./TabBar.css";
const TabItem = ({ label, icon, isActive, onClick, colors, typography }) => {
return (
<div
onClick={onClick}
className={`user-guide-tab-item ${isActive ? "active" : ""}`}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "16px 8px",
cursor: "pointer",
backgroundColor: isActive ? colors.background.white : "transparent",
borderLeft: isActive
? `2px solid ${colors.brand.primary}`
: "2px solid transparent",
borderBottom: `1px solid ${colors.border.default}`,
}}
>
<div
style={{
color: isActive ? colors.brand.primary : colors.text.muted,
marginBottom: 8,
}}
>
{icon}
</div>
<span
style={{
writingMode: "vertical-rl",
textOrientation: "mixed",
fontFamily: typography.fontFamily.sans,
fontSize: 12,
fontWeight: typography.fontWeight.normal,
color: isActive ? colors.brand.primary : colors.text.muted,
letterSpacing: "0.5px",
}}
>
{label}
</span>
</div>
);
};
const TabBar = ({ activeTab, onTabChange }) => {
const { colors, typography } = useUserGuideTheme();
return (
<div
style={{
width: 40,
height: "100%",
backgroundColor: colors.background.sidebar,
borderLeft: `1px solid ${colors.border.default}`,
display: "flex",
flexDirection: "column",
alignItems: "stretch",
}}
>
<TabItem
id="user-guide"
label="User guide"
icon={
<BookOpen
size={18}
strokeWidth={1.5}
/>
}
isActive={activeTab === "user-guide"}
onClick={() => onTabChange("user-guide")}
colors={colors}
typography={typography}
/>
<TabItem
id="help"
label="Help"
icon={
<HelpCircle
size={18}
strokeWidth={1.5}
/>
}
isActive={activeTab === "help"}
onClick={() => onTabChange("help")}
colors={colors}
typography={typography}
/>
</div>
);
};
export default TabBar;
@@ -0,0 +1,39 @@
/* UserGuideLanding styles - hover states */
/* Collection card hover */
.user-guide-collection-card {
transition: all 150ms ease;
}
.user-guide-collection-card:hover {
border-color: rgba(21, 112, 239, 0.3) !important;
background-color: rgba(21, 112, 239, 0.02) !important;
box-shadow: 0px 2px 8px -2px rgba(0, 0, 0, 0.08);
}
[data-mui-color-scheme="dark"] .user-guide-collection-card:hover {
border-color: rgba(21, 112, 239, 0.5) !important;
background-color: rgba(21, 112, 239, 0.05) !important;
box-shadow: 0px 4px 16px -4px rgba(0, 0, 0, 0.4);
}
/* Fast find item hover */
.user-guide-fast-find-item {
transition: all 150ms ease;
}
.user-guide-fast-find-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-fast-find-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-fast-find-item .arrow-icon {
transition: all 150ms ease;
}
.user-guide-fast-find-item:hover .arrow-icon {
color: #1570ef;
}
@@ -0,0 +1,214 @@
import {
ArrowRight,
Rocket,
Globe,
Gauge,
Server,
Bell,
AlertTriangle,
Settings,
Wifi,
Wrench,
Users,
} from "lucide-react";
import { collections, fastFinds, getCollection } from "./content/userGuideConfig";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./UserGuideLanding.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Server,
Bell,
AlertTriangle,
Settings,
Wifi,
Wrench,
Users,
};
const UserGuideLanding = ({
onNavigate,
searchQuery,
onSearchChange,
mode = "in-app",
}) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const isInApp = mode === "in-app";
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Main Content */}
<div
style={{
maxWidth: isInApp ? "100%" : 1200,
margin: "0 auto",
padding: isInApp ? spacing.lg : `${spacing["3xl"]} ${spacing["3xl"]}`,
}}
>
{/* Collections Grid */}
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: isInApp ? typography.fontSize.lg : typography.fontSize.xl,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: isInApp ? spacing.md : spacing.xl,
marginTop: 0,
}}
>
Browse by topic
</h2>
<div
style={{
display: "grid",
gridTemplateColumns: isInApp
? "1fr"
: "repeat(auto-fill, minmax(340px, 1fr))",
gap: isInApp ? spacing.md : spacing.lg,
marginBottom: isInApp ? spacing.xl : spacing["4xl"],
}}
>
{collections.map((collection) => {
const IconComponent = iconMap[collection.icon] || Rocket;
return (
<div
key={collection.id}
onClick={() => onNavigate(collection.id)}
className="user-guide-collection-card"
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.lg,
cursor: "pointer",
}}
>
<div
style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
/>
<div style={{ flex: 1 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: spacing.xs,
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
}}
>
{collection.title}
</span>
<span
style={{
fontSize: typography.fontSize.xs,
color: colors.text.secondary,
backgroundColor: colors.background.alt,
padding: "2px 6px",
borderRadius: border.radius,
}}
>
{collection.articleCount} articles
</span>
</div>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
display: "block",
}}
>
{collection.description}
</span>
</div>
</div>
</div>
);
})}
</div>
{/* About our docs - Only shown in in-app mode */}
{isInApp && (
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.lg,
}}
>
<h3
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.md,
marginTop: 0,
}}
>
About our docs
</h3>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
There are a few ways to explore our docs:
</p>
<div style={{ marginBottom: spacing.md }}>
<h4
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
marginTop: 0,
}}
>
In the product
</h4>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
}}
>
Look for help icons and tooltips throughout the interface.
</p>
</div>
</div>
)}
</div>
</div>
);
};
export default UserGuideLanding;
@@ -0,0 +1,102 @@
import { createContext, useContext, useState, useCallback, useEffect } from "react";
const SIDEBAR_STATE_KEY = "checkmate-sidebar-open";
// Constants for sidebar dimensions
export const TAB_BAR_WIDTH = 41; // 40px width + 1px border
export const DEFAULT_CONTENT_WIDTH = 400;
// Gap when sidebar is closed (need more space since tab bar is narrow)
export const CLOSED_GAP = 24;
// Gap when sidebar is open (less needed since content edge is clear)
export const OPEN_GAP = 8;
const UserGuideSidebarContext = createContext(null);
export const UserGuideSidebarProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(() => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem(SIDEBAR_STATE_KEY);
return saved === "true";
});
const [currentPath, setCurrentPath] = useState(undefined);
const [contentWidth, setContentWidth] = useState(DEFAULT_CONTENT_WIDTH);
// Calculate total sidebar width and required padding
const totalSidebarWidth = isOpen ? TAB_BAR_WIDTH + contentWidth : TAB_BAR_WIDTH;
const gap = isOpen ? OPEN_GAP : CLOSED_GAP;
const requiredPaddingRight = totalSidebarWidth + gap;
// Set CSS custom properties on document root for global access
useEffect(() => {
if (typeof document === "undefined") return;
const root = document.documentElement;
root.style.setProperty("--help-sidebar-width", `${totalSidebarWidth}px`);
root.style.setProperty("--help-sidebar-padding", `${requiredPaddingRight}px`);
}, [totalSidebarWidth, requiredPaddingRight]);
// Update body class for content push effect
useEffect(() => {
if (typeof document === "undefined") return;
if (isOpen) {
document.body.classList.add("user-guide-sidebar-open");
} else {
document.body.classList.remove("user-guide-sidebar-open");
}
return () => {
document.body.classList.remove("user-guide-sidebar-open");
};
}, [isOpen]);
// Persist state to localStorage
useEffect(() => {
if (typeof window === "undefined") return;
localStorage.setItem(SIDEBAR_STATE_KEY, String(isOpen));
}, [isOpen]);
const open = useCallback((path) => {
setCurrentPath(path);
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
}, []);
const toggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
return (
<UserGuideSidebarContext.Provider
value={{
isOpen,
open,
close,
toggle,
currentPath,
contentWidth,
setContentWidth,
totalSidebarWidth,
requiredPaddingRight,
}}
>
{children}
</UserGuideSidebarContext.Provider>
);
};
export const useUserGuideSidebarContext = () => {
const context = useContext(UserGuideSidebarContext);
if (!context) {
throw new Error(
"useUserGuideSidebarContext must be used within a UserGuideSidebarProvider"
);
}
return context;
};
export default UserGuideSidebarContext;
@@ -0,0 +1,176 @@
// Content block types for the templatic article system
/**
* @typedef {'heading' | 'paragraph' | 'callout' | 'bullet-list' | 'ordered-list' | 'checklist' | 'icon-cards' | 'grid-cards' | 'time-estimate' | 'info-box' | 'code' | 'requirements' | 'table' | 'image' | 'article-links'} ContentBlockType
*/
/**
* @typedef {Object} HeadingBlock
* @property {'heading'} type
* @property {string} id - Used for TOC anchors
* @property {2 | 3} level - h2 or h3
* @property {string} text
*/
/**
* @typedef {Object} ParagraphBlock
* @property {'paragraph'} type
* @property {string} text - Supports basic markdown: **bold**, *italic*, `code`
*/
/**
* @typedef {Object} CalloutBlock
* @property {'callout'} type
* @property {'info' | 'tip' | 'warning' | 'success'} [variant]
* @property {string} [title]
* @property {string} text
*/
/**
* @typedef {Object} ListItem
* @property {string} text
* @property {string} [bold] - Optional bold prefix
*/
/**
* @typedef {Object} BulletListBlock
* @property {'bullet-list'} type
* @property {ListItem[]} items
*/
/**
* @typedef {Object} OrderedListBlock
* @property {'ordered-list'} type
* @property {ListItem[]} items
*/
/**
* @typedef {Object} ChecklistBlock
* @property {'checklist'} type
* @property {string[]} items
*/
/**
* @typedef {Object} IconCardItem
* @property {string} icon - Icon name from lucide-react
* @property {string} title
* @property {string} description
*/
/**
* @typedef {Object} IconCardsBlock
* @property {'icon-cards'} type
* @property {IconCardItem[]} items
*/
/**
* @typedef {Object} GridCardItem
* @property {string} title
* @property {string} description
* @property {string} [icon]
*/
/**
* @typedef {Object} GridCardsBlock
* @property {'grid-cards'} type
* @property {GridCardItem[]} items
* @property {number} [columns]
*/
/**
* @typedef {Object} TimeEstimateBlock
* @property {'time-estimate'} type
* @property {string} text
*/
/**
* @typedef {Object} InfoBoxBlock
* @property {'info-box'} type
* @property {string} [icon]
* @property {string} title
* @property {string[]} items
*/
/**
* @typedef {Object} CodeBlock
* @property {'code'} type
* @property {string} code
* @property {string} [language]
*/
/**
* @typedef {Object} RequirementItem
* @property {string} icon
* @property {string} title
* @property {string[]} items
*/
/**
* @typedef {Object} RequirementsBlock
* @property {'requirements'} type
* @property {RequirementItem[]} items
*/
/**
* @typedef {Object} TableColumn
* @property {string} key
* @property {string} label
* @property {string} [width]
*/
/**
* @typedef {Object} TableBlock
* @property {'table'} type
* @property {TableColumn[]} columns
* @property {Object[]} rows
*/
/**
* @typedef {Object} ImageBlock
* @property {'image'} type
* @property {string} src
* @property {string} alt
* @property {string} [caption]
*/
/**
* @typedef {Object} ArticleLink
* @property {string} collectionId
* @property {string} articleId
* @property {string} title
* @property {string} [description]
*/
/**
* @typedef {Object} ArticleLinksBlock
* @property {'article-links'} type
* @property {string} [title] - e.g., "Next steps", "Related articles"
* @property {ArticleLink[]} items
*/
/**
* @typedef {Object} ArticleContent
* @property {ContentBlock[]} blocks
*/
/**
* @typedef {Object} TocItem
* @property {string} label
* @property {string} href
* @property {number} level
*/
/**
* Helper to extract TOC from content blocks
* @param {ContentBlock[]} blocks
* @returns {TocItem[]}
*/
export const extractToc = (blocks) => {
return blocks
.filter((block) => block.type === "heading")
.map((block) => ({
label: block.text,
href: `#${block.id}`,
level: block.level,
}));
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,604 @@
// Icon names as strings - resolved to actual components in UserGuideLanding.jsx
// Available icons: Rocket, Globe, Gauge, Server, Bell, AlertTriangle, Settings, Wifi, Wrench, Users
/**
* @typedef {Object} Article
* @property {string} id
* @property {string} title
* @property {string} description
* @property {string[]} keywords
*/
/**
* @typedef {Object} Collection
* @property {string} id
* @property {string} title
* @property {string} description
* @property {string} icon
* @property {number} articleCount
* @property {Article[]} articles
*/
/**
* @typedef {Object} FastFind
* @property {string} id
* @property {string} title
* @property {string} collectionId
* @property {string} articleId
*/
// Collections configuration for Checkmate
export const collections = [
{
id: "getting-started",
title: "Getting started",
description: "Essential first steps for new users.",
icon: "Rocket",
articleCount: 4,
articles: [
{
id: "what-is-checkmate",
title: "What is Checkmate",
description: "Overview of monitoring capabilities and core features.",
keywords: ["introduction", "overview", "platform", "monitoring", "uptime"],
},
{
id: "quick-start",
title: "Quick start guide",
description: "Create your first monitor in 5 minutes.",
keywords: ["quick", "start", "first", "monitor", "setup", "tutorial"],
},
{
id: "dashboard",
title: "Understanding the dashboard",
description: "Navigation and key metrics explained.",
keywords: ["dashboard", "navigation", "interface", "home", "metrics"],
},
{
id: "roles-permissions",
title: "User roles and permissions",
description: "User, Admin, and Superadmin roles explained.",
keywords: ["role", "permission", "admin", "superadmin", "user", "access"],
},
],
},
{
id: "uptime-monitoring",
title: "Uptime monitoring",
description: "Monitor website and service availability.",
icon: "Globe",
articleCount: 9,
articles: [
{
id: "http-monitors",
title: "Creating HTTP monitors",
description: "URL monitoring with response validation.",
keywords: ["http", "https", "url", "website", "monitor", "create"],
},
{
id: "ping-monitors",
title: "Ping monitors",
description: "ICMP-based availability checks.",
keywords: ["ping", "icmp", "availability", "monitor"],
},
{
id: "port-monitors",
title: "Port monitors",
description: "TCP port availability monitoring.",
keywords: ["port", "tcp", "availability", "monitor", "service"],
},
{
id: "docker-monitors",
title: "Docker monitors",
description: "Monitor Docker container status and health.",
keywords: ["docker", "container", "monitoring", "health", "status"],
},
{
id: "bulk-import",
title: "Bulk import monitors",
description: "Import multiple monitors from a CSV file.",
keywords: ["bulk", "import", "csv", "multiple", "batch", "upload"],
},
{
id: "intervals-timing",
title: "Monitor intervals and timing",
description: "Configure how often to check your services.",
keywords: ["interval", "timing", "frequency", "check", "schedule"],
},
{
id: "json-path-matching",
title: "JSON path matching",
description: "Validate API response content.",
keywords: ["json", "api", "response", "validation", "match", "path"],
},
{
id: "ssl-tls-monitoring",
title: "SSL/TLS monitoring",
description: "Certificate tracking and error handling.",
keywords: ["ssl", "tls", "certificate", "https", "security", "expiry"],
},
{
id: "uptime-metrics",
title: "Understanding uptime metrics",
description: "Response time, status codes, and uptime percentage.",
keywords: [
"metrics",
"response",
"time",
"status",
"code",
"uptime",
"percentage",
],
},
],
},
{
id: "pagespeed",
title: "PageSpeed monitoring",
description: "Track website performance and Core Web Vitals.",
icon: "Gauge",
articleCount: 5,
articles: [
{
id: "creating-pagespeed",
title: "Creating PageSpeed monitors",
description: "Setup and configuration for performance monitoring.",
keywords: ["pagespeed", "create", "setup", "performance", "lighthouse"],
},
{
id: "understanding-scores",
title: "Understanding scores",
description: "Performance, Accessibility, SEO, and Best Practices explained.",
keywords: ["score", "performance", "accessibility", "seo", "best", "practices"],
},
{
id: "core-web-vitals",
title: "Core Web Vitals explained",
description: "LCP, FCP, CLS, TBT, and Speed Index.",
keywords: ["core", "web", "vitals", "lcp", "fcp", "cls", "tbt", "speed", "index"],
},
{
id: "pagespeed-api",
title: "PageSpeed API configuration",
description: "Setting up your Google PageSpeed API key.",
keywords: ["api", "key", "google", "pagespeed", "configure", "setup"],
},
{
id: "performance-trends",
title: "Performance trends",
description: "Tracking improvements over time.",
keywords: ["trend", "history", "improvement", "tracking", "chart"],
},
],
},
{
id: "infrastructure",
title: "Infrastructure monitoring",
description: "Monitor server hardware with Capture agent.",
icon: "Server",
articleCount: 6,
articles: [
{
id: "installing-capture",
title: "Installing Capture agent",
description: "Setup the monitoring agent on your servers.",
keywords: ["install", "capture", "agent", "setup", "server"],
},
{
id: "cpu-monitoring",
title: "CPU monitoring",
description: "Usage, temperature, and core metrics.",
keywords: ["cpu", "processor", "usage", "temperature", "cores"],
},
{
id: "memory-monitoring",
title: "Memory monitoring",
description: "RAM usage and availability tracking.",
keywords: ["memory", "ram", "usage", "available", "free"],
},
{
id: "disk-monitoring",
title: "Disk monitoring",
description: "Storage space and read/write speeds.",
keywords: ["disk", "storage", "space", "read", "write", "speed"],
},
{
id: "network-monitoring",
title: "Network monitoring",
description: "Bandwidth and packet statistics.",
keywords: ["network", "bandwidth", "packet", "interface", "traffic"],
},
{
id: "hardware-thresholds",
title: "Setting hardware thresholds",
description: "Alert when resources become critical.",
keywords: ["threshold", "alert", "critical", "warning", "limit"],
},
],
},
{
id: "incidents",
title: "Incidents & alerts",
description: "Respond to downtime and issues.",
icon: "AlertTriangle",
articleCount: 4,
articles: [
{
id: "understanding-incidents",
title: "Understanding incidents",
description: "How incidents are detected and created.",
keywords: ["incident", "detection", "downtime", "outage", "alert"],
},
{
id: "incident-timeline",
title: "Incident timeline",
description: "Tracking start, duration, and resolution.",
keywords: ["timeline", "duration", "start", "end", "resolution"],
},
{
id: "resolving-incidents",
title: "Resolving incidents",
description: "Manual vs automatic resolution.",
keywords: ["resolve", "manual", "automatic", "acknowledge", "fix"],
},
{
id: "incident-history",
title: "Incident history and analysis",
description: "Learn from past outages.",
keywords: ["history", "analysis", "report", "past", "outage"],
},
],
},
{
id: "checks",
title: "Checks",
description: "View monitoring check results and history.",
icon: "FileText",
articleCount: 3,
articles: [
{
id: "understanding-checks",
title: "Understanding checks",
description: "What checks are and how they work.",
keywords: ["check", "result", "monitoring", "status", "response"],
},
{
id: "check-details",
title: "Check details and timing",
description: "Response times, status codes, and timing breakdown.",
keywords: ["response", "time", "timing", "breakdown", "dns", "tls"],
},
{
id: "filtering-checks",
title: "Filtering and date ranges",
description: "Filter checks by date range and status.",
keywords: ["filter", "date", "range", "pagination", "sort"],
},
],
},
{
id: "logs",
title: "Logs & diagnostics",
description: "System logs, queue monitoring, and diagnostics.",
icon: "Database",
articleCount: 3,
articles: [
{
id: "application-logs",
title: "Application logs",
description: "View and filter system logs by level.",
keywords: ["log", "error", "warning", "info", "debug", "filter"],
},
{
id: "queue-monitoring",
title: "Queue monitoring",
description: "Monitor background jobs and queue status.",
keywords: ["queue", "job", "background", "bullmq", "worker"],
},
{
id: "system-diagnostics",
title: "System diagnostics",
description: "CPU, memory, and heap usage metrics.",
keywords: ["diagnostics", "cpu", "memory", "heap", "performance"],
},
],
},
{
id: "notifications",
title: "Notifications",
description: "Get alerted when things go wrong.",
icon: "Bell",
articleCount: 7,
articles: [
{
id: "email-notifications",
title: "Email notifications",
description: "SMTP setup and configuration.",
keywords: ["email", "smtp", "mail", "setup", "configure"],
},
{
id: "slack-integration",
title: "Slack integration",
description: "Webhook setup for Slack channels.",
keywords: ["slack", "webhook", "integration", "channel"],
},
{
id: "discord-integration",
title: "Discord integration",
description: "Channel notifications via webhooks.",
keywords: ["discord", "webhook", "integration", "channel"],
},
{
id: "pagerduty-integration",
title: "PagerDuty integration",
description: "On-call management integration.",
keywords: ["pagerduty", "oncall", "integration", "escalation"],
},
{
id: "webhook-notifications",
title: "Webhook notifications",
description: "Custom HTTP callbacks for alerts.",
keywords: ["webhook", "http", "callback", "custom", "api"],
},
{
id: "matrix-integration",
title: "Matrix integration",
description: "Decentralized messaging notifications.",
keywords: ["matrix", "messaging", "integration", "homeserver"],
},
{
id: "testing-notifications",
title: "Testing notifications",
description: "Verify your notification setup works.",
keywords: ["test", "verify", "notification", "check"],
},
],
},
{
id: "status-pages",
title: "Status pages",
description: "Share uptime with your users.",
icon: "Wifi",
articleCount: 5,
articles: [
{
id: "creating-status-page",
title: "Creating a status page",
description: "Public-facing uptime dashboard setup.",
keywords: ["status", "page", "create", "public", "dashboard"],
},
{
id: "customizing-appearance",
title: "Customizing appearance",
description: "Logo, colors, and branding options.",
keywords: ["customize", "logo", "color", "brand", "style"],
},
{
id: "adding-monitors",
title: "Adding monitors to status page",
description: "Main monitors and sub-components.",
keywords: ["add", "monitor", "component", "group"],
},
{
id: "display-options",
title: "Display options",
description: "Charts, uptime percentage, and login links.",
keywords: ["display", "chart", "uptime", "percentage", "option"],
},
{
id: "sharing",
title: "Sharing your status page",
description: "Share your status page URL with users.",
keywords: ["share", "url", "public", "link", "access"],
},
],
},
{
id: "maintenance",
title: "Maintenance windows",
description: "Schedule planned downtime.",
icon: "Wrench",
articleCount: 3,
articles: [
{
id: "creating-maintenance",
title: "Creating maintenance windows",
description: "Suppress alerts during planned maintenance.",
keywords: ["maintenance", "create", "schedule", "suppress", "alert"],
},
{
id: "recurring-maintenance",
title: "One-time vs recurring",
description: "Different scheduling options.",
keywords: ["recurring", "one-time", "schedule", "repeat", "frequency"],
},
{
id: "managing-maintenance",
title: "Managing active maintenance",
description: "Edit, extend, or cancel maintenance windows.",
keywords: ["manage", "edit", "cancel", "extend", "active"],
},
],
},
{
id: "team-management",
title: "Team management",
description: "Collaborate with your team.",
icon: "Users",
articleCount: 3,
articles: [
{
id: "inviting-members",
title: "Inviting team members",
description: "Send email invitations to your team.",
keywords: ["invite", "team", "member", "email", "add"],
},
{
id: "user-roles",
title: "User roles",
description: "Admin vs User permissions explained.",
keywords: ["role", "admin", "user", "permission", "access"],
},
{
id: "managing-access",
title: "Managing team access",
description: "Add and remove team members.",
keywords: ["manage", "access", "remove", "team", "member"],
},
],
},
{
id: "settings",
title: "Settings & configuration",
description: "Customize your Checkmate instance.",
icon: "Settings",
articleCount: 6,
articles: [
{
id: "account-settings",
title: "Account settings",
description: "Manage your profile, password, and account.",
keywords: ["account", "profile", "password", "avatar", "photo", "delete"],
},
{
id: "email-configuration",
title: "Email configuration",
description: "SMTP server setup for notifications.",
keywords: ["email", "smtp", "configure", "server", "mail"],
},
{
id: "global-thresholds",
title: "Global thresholds",
description: "Default alert levels for all monitors.",
keywords: ["threshold", "global", "default", "alert", "level"],
},
{
id: "appearance",
title: "Appearance settings",
description: "Theme and display preferences.",
keywords: ["appearance", "theme", "dark", "light", "display", "mode"],
},
{
id: "timezone-settings",
title: "Timezone settings",
description: "Display preferences for dates and times.",
keywords: ["timezone", "time", "date", "display", "preference"],
},
{
id: "export-monitors",
title: "Export monitors",
description: "Download your monitors as JSON.",
keywords: ["export", "download", "backup", "json", "monitors"],
},
],
},
];
// Fast Finds - Popular/quick access articles
export const fastFinds = [
{
id: "ff-1",
title: "Creating your first monitor",
collectionId: "getting-started",
articleId: "quick-start",
},
{
id: "ff-2",
title: "Setting up email notifications",
collectionId: "notifications",
articleId: "email-notifications",
},
{
id: "ff-3",
title: "Installing Capture agent",
collectionId: "infrastructure",
articleId: "installing-capture",
},
{
id: "ff-4",
title: "Creating a status page",
collectionId: "status-pages",
articleId: "creating-status-page",
},
{
id: "ff-5",
title: "Understanding Core Web Vitals",
collectionId: "pagespeed",
articleId: "core-web-vitals",
},
];
// Helper functions
export const getCollection = (collectionId) => {
return collections.find((c) => c.id === collectionId);
};
export const getArticle = (collectionId, articleId) => {
const collection = getCollection(collectionId);
return collection?.articles.find((a) => a.id === articleId);
};
export const getTotalArticleCount = () => {
return collections.reduce((sum, collection) => sum + collection.articleCount, 0);
};
/**
* @typedef {Object} SearchResult
* @property {string} collectionId
* @property {string} collectionTitle
* @property {string} articleId
* @property {string} articleTitle
* @property {string} articleDescription
* @property {'title' | 'description' | 'keyword'} matchType
*/
/**
* Search articles by query
* @param {string} query
* @returns {SearchResult[]}
*/
export const searchArticles = (query) => {
if (!query || query.trim().length < 2) return [];
const normalizedQuery = query.toLowerCase().trim();
const results = [];
for (const collection of collections) {
for (const article of collection.articles) {
let matchType = null;
// Check title match (highest priority)
if (article.title.toLowerCase().includes(normalizedQuery)) {
matchType = "title";
}
// Check description match
else if (article.description.toLowerCase().includes(normalizedQuery)) {
matchType = "description";
}
// Check keywords match
else if (
article.keywords.some((kw) => kw.toLowerCase().includes(normalizedQuery))
) {
matchType = "keyword";
}
if (matchType) {
results.push({
collectionId: collection.id,
collectionTitle: collection.title,
articleId: article.id,
articleTitle: article.title,
articleDescription: article.description,
matchType,
});
}
}
}
// Sort by match type priority: title > description > keyword
return results.sort((a, b) => {
const priority = { title: 0, description: 1, keyword: 2 };
return priority[a.matchType] - priority[b.matchType];
});
};
@@ -0,0 +1,30 @@
import SidebarWrapper from "./SidebarWrapper";
import {
UserGuideSidebarProvider,
useUserGuideSidebarContext,
TAB_BAR_WIDTH,
DEFAULT_CONTENT_WIDTH,
} from "./UserGuideSidebarContext";
// Main component that wraps SidebarWrapper with context consumer
const UserGuideSidebar = () => {
const { isOpen, open, close } = useUserGuideSidebarContext();
return (
<SidebarWrapper
isOpen={isOpen}
onOpen={open}
onClose={close}
/>
);
};
export {
UserGuideSidebar,
UserGuideSidebarProvider,
useUserGuideSidebarContext,
TAB_BAR_WIDTH,
DEFAULT_CONTENT_WIDTH,
};
export default UserGuideSidebar;
@@ -0,0 +1,222 @@
/**
* Design Tokens for UserGuide
* Supports both light and dark themes via MUI theme integration
*/
// Typography (theme-independent)
export const typography = {
fontFamily: {
sans: "'Inter', system-ui, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif",
mono: "'Geist Mono', 'Fira Code', 'Consolas', monospace",
},
fontSize: {
xs: 12,
sm: 12,
base: 13,
md: 14,
lg: 15,
xl: 16,
"2xl": 20,
"3xl": 22,
"4xl": 26,
"5xl": 30,
},
lineHeight: {
tight: 1.2,
snug: 1.3,
normal: 1.4,
relaxed: 1.5,
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
};
// Spacing (theme-independent)
export const spacing = {
xs: "4px",
sm: "8px",
md: "12px",
lg: "16px",
xl: "24px",
"2xl": "32px",
"3xl": "40px",
"4xl": "48px",
};
// Brand colors (theme-independent) - Checkmate blue
export const brandColors = {
primary: "#1570EF",
primaryDark: "#1054B0",
primaryLight: "#5BA3F5",
success: "#17B26A",
warning: "#F79009",
error: "#F04438",
};
/**
* Get theme-aware colors based on MUI theme mode
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Theme-aware color tokens
*/
export const getColors = (mode) => {
const isDark = mode === "dark";
return {
text: {
primary: isDark ? "#ffffff" : "#1C2130",
secondary: isDark ? "#d0d5dd" : "#475467",
muted: isDark ? "#98a2b3" : "#667085",
white: "#ffffff",
whiteMuted: "rgba(255, 255, 255, 0.8)",
},
brand: brandColors,
border: {
default: isDark ? "#344054" : "#d0d5dd",
},
background: {
white: isDark ? "#1a1a1a" : "#ffffff",
alt: isDark ? "#0d0d0d" : "#f9fafb",
code: isDark ? "#1C2130" : "#f3f4f6",
sidebar: isDark ? "#101010" : "#ffffff",
hover: isDark ? "#2a2a2a" : "#f3f4f6",
},
};
};
/**
* Get theme-aware border styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Border styles
*/
export const getBorder = (mode) => {
const colors = getColors(mode);
return {
radius: "4px",
default: `1px solid ${colors.border.default}`,
};
};
/**
* Get theme-aware image styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Image styles
*/
export const getImageStyles = (mode) => {
const colors = getColors(mode);
const border = getBorder(mode);
const isDark = mode === "dark";
return {
image: {
width: "100%",
borderRadius: border.radius,
border: border.default,
display: "block",
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
},
caption: {
fontSize: 10,
color: colors.text.muted,
textAlign: "center",
fontStyle: "italic",
},
};
};
/**
* Get theme-aware chip styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Chip styles
*/
export const getChipStyles = (mode) => {
const isDark = mode === "dark";
return {
url: {
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
color: isDark ? "#5BA3F5" : "#1570EF",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
code: {
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
color: isDark ? "#d0d5dd" : "#1C2130",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
};
};
/**
* Get theme-aware component styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Component styles
*/
export const getComponentStyles = (mode) => {
const colors = getColors(mode);
const border = getBorder(mode);
return {
card: {
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
},
heading: {
h2: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
h3: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
},
paragraph: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.relaxed,
},
listItem: {
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.snug,
},
};
};
// Legacy exports for backwards compatibility (dark mode defaults)
// These will be replaced by useUserGuideTheme hook usage
export const colors = getColors("dark");
export const border = getBorder("dark");
export const imageStyles = getImageStyles("dark");
export const chipStyles = getChipStyles("dark");
export const componentStyles = getComponentStyles("dark");
// Export everything as a single theme object (dark mode default)
export const theme = {
colors,
typography,
spacing,
border,
imageStyles,
chipStyles,
componentStyles,
};
export default theme;
@@ -0,0 +1,123 @@
/**
* Hook to get theme-aware styles for UserGuide components
* Uses MUI's theme system from Checkmate
*/
import { useTheme } from "@mui/material/styles";
import { typography, spacing, brandColors } from "./theme";
/**
* Returns theme-aware styles for UserGuide components
*/
export const useUserGuideTheme = () => {
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const colors = {
text: {
primary: theme.palette.primary.contrastText,
secondary: theme.palette.primary.contrastTextSecondary,
muted: theme.palette.primary.contrastTextTertiary,
white: "#ffffff",
},
brand: brandColors,
border: {
default: theme.palette.primary.lowContrast,
},
background: {
white: isDark ? "#1a1a1a" : "#ffffff",
alt: theme.palette.primaryBackground.main,
code: isDark ? "#1C2130" : "#f3f4f6",
sidebar: theme.palette.primaryBackground.main,
hover: isDark ? "#2a2a2a" : "#f3f4f6",
},
};
const border = {
radius: "4px",
default: `1px solid ${colors.border.default}`,
};
const imageStyles = {
image: {
width: "100%",
borderRadius: border.radius,
border: border.default,
display: "block",
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
},
caption: {
fontSize: 10,
color: colors.text.muted,
textAlign: "center",
fontStyle: "italic",
},
};
const chipStyles = {
url: {
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
color: isDark ? "#5BA3F5" : "#1570EF",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
code: {
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
color: isDark ? "#d0d5dd" : "#1C2130",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
};
const componentStyles = {
card: {
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
},
heading: {
h2: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
h3: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
},
paragraph: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.relaxed,
},
listItem: {
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.snug,
},
};
return {
colors,
typography,
spacing,
border,
imageStyles,
chipStyles,
componentStyles,
isDark,
theme,
};
};
export default useUserGuideTheme;