diff --git a/.github/scripts/design-review.js b/.github/scripts/design-review.js new file mode 100644 index 000000000..584c04b99 --- /dev/null +++ b/.github/scripts/design-review.js @@ -0,0 +1,143 @@ +// get diff (pr diff or git diff depending on trigger) +// filter to design-relevant files +// build Claude prompt with guidelines + diff +// call Claude API +// post review comment to GitHub + +const Anthropic = require("@anthropic-ai/sdk"); +const { getDiff } = require("./get-diff"); +const { filterFiles } = require("./filter-files"); +const { buildPrompt } = require("./prompt"); +const { postComment } = require("./post-comment"); + +// Model is configurable via the CLAUDE_MODEL repo variable so it can be updated +// without a code change if the model is renamed or a newer version is preferred. +const CLAUDE_MODEL = process.env.CLAUDE_MODEL || "claude-sonnet-4-6"; + +if (!process.env.GITHUB_TOKEN) { + console.error( + "ERROR: GITHUB_TOKEN is not set. This is automatically provided by GitHub Actions — " + + "ensure the workflow step has not overridden it and that the job has 'issues: write' permission." + ); + process.exit(1); +} + +if (!process.env.GITHUB_REPOSITORY) { + console.error( + "ERROR: GITHUB_REPOSITORY is not set. This is automatically provided by GitHub Actions " + + "as 'owner/repo'. Ensure it is passed via the 'env:' block in the workflow step: " + + "GITHUB_REPOSITORY: ${{ github.repository }}" + ); + process.exit(1); +} + +/** + * Filter the full unified diff down to only hunks belonging to relevantFiles. + * Prevents the agent from seeing or commenting on non-design files. + * + * @param {string} fullDiff - raw unified diff + * @param {string[]} relevantFiles - file paths that passed filterFiles() + * @returns {string} filtered diff containing only relevant file sections + */ +function filterDiffToRelevantFiles(fullDiff, relevantFiles) { + const relevantSet = new Set(relevantFiles); + const sections = fullDiff.split(/^(?=diff --git )/m); + return sections + .filter((section) => { + const match = section.match(/^diff --git a\/(.+) b\/.+/); + return match && relevantSet.has(match[1]); + }) + .join(""); +} + +async function run() { + console.log("=== Keploy Design Review Agent ==="); + console.log(`Trigger: ${process.env.GITHUB_EVENT_NAME}`); + console.log(`Model: ${CLAUDE_MODEL}`); + + // Step 1: Get the diff + console.log("Fetching diff..."); + const { diff, changedFiles } = await getDiff(); + + if (!diff || diff.trim().length === 0) { + console.log("No diff found. Nothing to review."); + await postComment( + "## Keploy Design Review\n\nNo changes detected in this diff. Nothing to review." + ); + return; + } + + console.log(`Total changed files: ${changedFiles.length}`); + + // Step 2: Filter to design-relevant files + const relevantFiles = filterFiles(changedFiles); + console.log(`Design-relevant files: ${relevantFiles.length}`); + + if (relevantFiles.length === 0) { + console.log("No design-relevant files changed. Skipping review."); + await postComment( + "## Keploy Design Review\n\n✅ No design-relevant files changed in this diff. Nothing to review." + ); + return; + } + + // Step 3: Check API key — only required if we actually have files to review. + // Exit cleanly (exit 0) so the check doesn't fail and block the PR. + const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; + if (!ANTHROPIC_API_KEY) { + console.log("ANTHROPIC_API_KEY not available — skipping review."); + await postComment( + "## Keploy Design Review\n\n" + + "⚪ Design review was skipped because `ANTHROPIC_API_KEY` is not available in this workflow run.\n\n" + + "Possible causes: the secret is not configured in repo Settings → Secrets → Actions, " + + "it is restricted to a specific environment that this workflow cannot access, " + + "or the workflow trigger was changed from `pull_request_target` to `pull_request` " + + "which may not expose secrets. A maintainer can verify and rerun once resolved." + ); + return; + } + + // Step 4: Trim the diff to only the hunks for relevant files + const filteredDiff = filterDiffToRelevantFiles(diff, relevantFiles); + + // Step 5: Build the prompt + console.log("Building review prompt..."); + const { system, user } = buildPrompt(filteredDiff, relevantFiles); + + // Step 6: Call Claude API + console.log(`Calling Claude API (model: ${CLAUDE_MODEL})...`); + const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY }); + + let message; + try { + message = await client.messages.create({ + model: CLAUDE_MODEL, + max_tokens: 4096, + messages: [{ role: "user", content: user }], + system, + }); + } catch (err) { + throw new Error( + `Claude API request failed (model: ${CLAUDE_MODEL}): ${err.message}` + ); + } + + const reviewText = message.content + .filter((block) => block.type === "text") + .map((block) => block.text) + .join("\n"); + + console.log("Review generated. Posting comment..."); + console.log("--- Review Preview ---"); + console.log(reviewText.slice(0, 500) + (reviewText.length > 500 ? "..." : "")); + console.log("---------------------"); + + // Step 7: Post the comment + await postComment(reviewText); + console.log("=== Design review complete ==="); +} + +run().catch((err) => { + console.error("Design review agent failed:", err.message); + process.exit(1); +}); diff --git a/.github/scripts/filter-files.js b/.github/scripts/filter-files.js new file mode 100644 index 000000000..49d87f604 --- /dev/null +++ b/.github/scripts/filter-files.js @@ -0,0 +1,46 @@ +// filters a list of changed file paths to only those relevant for design review + +const ALLOWED_EXTENSIONS = [ + ".css", + ".scss", + ".sass", + ".mdx", + ".md", + ".tsx", + ".jsx", + ".js", + ".ts", + ".svg", // SVGs are text diffs and design-relevant (e.g. static/keploy-logo.svg) +]; + +const IGNORED_PATHS = [ + "node_modules/", + "build/", + ".docusaurus/", + ".github/", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "DESIGN_GUIDELINES.md", +]; + +// Only review actual site source directories +const ALLOWED_PATHS = ["src/", "docs/", "versioned_docs/", "blog/", "static/"]; + +/** + * @param {string[]} files array of file paths from the diff + * @returns {string[]} filtered file paths + */ +function filterFiles(files) { + return files.filter((file) => { + const isIgnored = IGNORED_PATHS.some((p) => file.includes(p)); + if (isIgnored) return false; + + const hasAllowedExt = ALLOWED_EXTENSIONS.some((ext) => file.endsWith(ext)); + if (!hasAllowedExt) return false; + + return ALLOWED_PATHS.some((p) => file.startsWith(p)); + }); +} + +module.exports = { filterFiles }; diff --git a/.github/scripts/get-diff.js b/.github/scripts/get-diff.js new file mode 100644 index 000000000..a9d4f94ac --- /dev/null +++ b/.github/scripts/get-diff.js @@ -0,0 +1,102 @@ +// returns the unified diff string and list of changed files for the current GitHub event (PR or push) + +const { execSync } = require("child_process"); +const https = require("https"); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; // "owner/repo" +const PR_NUMBER = process.env.PR_NUMBER; +const GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME; +const GITHUB_SHA = process.env.GITHUB_SHA; + +// fetch the pr diff from the GitHub API, using the "diff" media type to get a unified diff string, returns raw unified diff string +function fetchPRDiff() { + return new Promise((resolve, reject) => { + const [owner, repo] = GITHUB_REPOSITORY.split("/"); + const options = { + hostname: "api.github.com", + path: `/repos/${owner}/${repo}/pulls/${PR_NUMBER}`, + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: "application/vnd.github.v3.diff", + "User-Agent": "keploy-design-review-agent", + }, + }; + + https + .get(options, (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + reject( + new Error( + `GitHub API returned ${res.statusCode} fetching PR diff. ` + + `Check GITHUB_TOKEN permissions and that PR_NUMBER=${PR_NUMBER} is valid. ` + + `Response: ${data.trim().slice(0, 300)}` + ) + ); + return; + } + resolve(data); + }); + }) + .on("error", reject); + }); +} + +// get diff for a push event using git. Compares HEAD to its parent (HEAD~1). +function getCommitDiff() { + try { + const diff = execSync("git diff HEAD~1 HEAD", { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB + }); + return diff; + } catch { + // first commit edge case, diff against empty tree + const diff = execSync( + "git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 HEAD", + { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 } + ); + return diff; + } +} + +// for manual workflow_dispatch: diff the last commit. this is a best practice to get some diff for manual runs, but may not be perfect depending on the repo state. + +function getManualDiff() { + return getCommitDiff(); +} + +// main export function that returns the diff and list of changed files based on the GitHub event type (pull_request or push) + +// diff : raw unified diff string +// changedFiles: array of file paths that changed (extracted from diff headers) + +async function getDiff() { + let diff = ""; + + if ( + GITHUB_EVENT_NAME === "pull_request" || + GITHUB_EVENT_NAME === "pull_request_target" + ) { + // Both events carry PR_NUMBER and use the GitHub API diff endpoint. + // pull_request_target is used so secrets are available on fork PRs + // while still reviewing the actual PR changes via the API. + diff = await fetchPRDiff(); + } else if (GITHUB_EVENT_NAME === "push") { + diff = getCommitDiff(); + } else { + // workflow_dispatch or any other trigger + diff = getManualDiff(); + } + + // Extract unique file names from diff headers: "diff --git a/foo b/foo" + const fileMatches = [...diff.matchAll(/^diff --git a\/(.+) b\/.+$/gm)]; + const changedFiles = [...new Set(fileMatches.map((m) => m[1]))]; + + return { diff, changedFiles }; +} + +module.exports = { getDiff }; diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 000000000..bb4e5a909 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,9 @@ +{ + "name": "keploy-design-review-agent", + "version": "1.0.0", + "description": "AI-powered design review agent for Keploy Docs", + "private": true, + "dependencies": { + "@anthropic-ai/sdk": "0.32.1" + } +} diff --git a/.github/scripts/post-comment.js b/.github/scripts/post-comment.js new file mode 100644 index 000000000..f0b8a21a4 --- /dev/null +++ b/.github/scripts/post-comment.js @@ -0,0 +1,183 @@ +// posts the design review results as a github comment +// on a pr: posts an issue comment on the pr (via /issues/{PR_NUMBER}/comments, not the PR Reviews API) +// on a push: posts a commit comment +// on a manual workflow_dispatch: posts a commit comment (best effort to get some comment for manual runs, but may not be perfect depending on repo state) + +const https = require("https"); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; +const PR_NUMBER = process.env.PR_NUMBER; +const GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME; +const GITHUB_SHA = process.env.GITHUB_SHA; + +// Unique HTML marker embedded in every comment body. +// Used to identify and clean up previous bot comments precisely — +// won't match comments from other bots that happen to say "Keploy Design Review". +const COMMENT_MARKER = ""; + +function githubRequest(method, path, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const options = { + hostname: "api.github.com", + path, + method, + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(payload), + "User-Agent": "keploy-design-review-agent", + }, + }; + + const req = https.request(options, (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + // 204 No Content (DELETE) returns empty body — safe parse + resolve(data.length > 0 ? JSON.parse(data) : {}); + } else { + reject( + new Error(`GitHub API error ${res.statusCode}: ${data}`) + ); + } + }); + }); + + req.on("error", reject); + req.write(payload); + req.end(); + }); +} + +/** + * Parse the GitHub API `Link` header and return the path for the `next` page, + * or null if there is no next page. + * @param {string|undefined} linkHeader + * @returns {string|null} + */ +function parseNextLink(linkHeader) { + if (!linkHeader) return null; + for (const part of linkHeader.split(",")) { + const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/); + if (match && match[2] === "next") { + const u = new URL(match[1]); + return `${u.pathname}${u.search}`; + } + } + return null; +} + +/** + * Fetch ALL comments on a PR, following pagination (?per_page=100 + Link header). + * GitHub defaults to 30 per page; without pagination a previous bot comment may + * be missed on busy PRs and left as a stale duplicate. + * @param {string} owner + * @param {string} repo + * @returns {Promise} + */ +function fetchAllPRComments(owner, repo) { + return new Promise((resolve, reject) => { + const allComments = []; + + function fetchPage(path) { + const options = { + hostname: "api.github.com", + path, + method: "GET", + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "keploy-design-review-agent", + }, + }; + + https + .get(options, (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error(`GitHub API error ${res.statusCode} listing PR comments: ${data}`)); + return; + } + const page = data.length > 0 ? JSON.parse(data) : []; + allComments.push(...(Array.isArray(page) ? page : [])); + const next = parseNextLink(res.headers.link); + if (next) { + fetchPage(next); + } else { + resolve(allComments); + } + }); + }) + .on("error", reject); + } + + fetchPage(`/repos/${owner}/${repo}/issues/${PR_NUMBER}/comments?per_page=100`); + }); +} + +/** + * Find and delete any previous design review comment on the PR. + * Matches by: + * 1. author is github-actions[bot] (our specific bot, not other bots) + * 2. body contains the unique COMMENT_MARKER HTML comment + */ +async function deletePreviousReviewComment(owner, repo) { + const comments = await fetchAllPRComments(owner, repo); + + const botComments = comments.filter( + (c) => + c.user.login === "github-actions[bot]" && + c.body.includes(COMMENT_MARKER) + ); + + for (const comment of botComments) { + await githubRequest( + "DELETE", + `/repos/${owner}/${repo}/issues/comments/${comment.id}`, + {} + ).catch(() => {}); // ignore delete errors — stale comment is non-critical + } +} + +/** + * @param {string} reviewBody - the Markdown review text from Claude + */ +async function postComment(reviewBody) { + const [owner, repo] = GITHUB_REPOSITORY.split("/"); + + // Embed the unique marker so we can find and replace this comment on re-runs + const bodyWithMarker = `${COMMENT_MARKER}\n${reviewBody}`; + + if ( + (GITHUB_EVENT_NAME === "pull_request" || + GITHUB_EVENT_NAME === "pull_request_target") && + PR_NUMBER + ) { + // Clean up previous bot comment first + await deletePreviousReviewComment(owner, repo); + + // Post fresh PR comment + await githubRequest( + "POST", + `/repos/${owner}/${repo}/issues/${PR_NUMBER}/comments`, + { body: bodyWithMarker } + ); + console.log(`Design review posted to PR #${PR_NUMBER}`); + } else { + // Post as commit comment (push or manual trigger) + await githubRequest( + "POST", + `/repos/${owner}/${repo}/commits/${GITHUB_SHA}/comments`, + { body: bodyWithMarker } + ); + console.log(`Design review posted to commit ${GITHUB_SHA}`); + } +} + +module.exports = { postComment }; diff --git a/.github/scripts/prompt.js b/.github/scripts/prompt.js new file mode 100644 index 000000000..7f52f9ede --- /dev/null +++ b/.github/scripts/prompt.js @@ -0,0 +1,118 @@ +// Builds the system and user prompts for the design review agent. + +const fs = require("fs"); +const path = require("path"); + +// Max characters for the diff sent to Claude +const MAX_DIFF_CHARS = 80000; + +// Max characters for DESIGN_GUIDELINES.md. +// claude-sonnet-4-6 has a 200k token context window (~4 chars/token). +// Budget: 80k diff + 40k guidelines + ~4k system prompt + 4k response = ~128k tokens. +// 40,000 chars leaves comfortable headroom. +const MAX_GUIDELINES_CHARS = 40000; + +function loadGuidelines() { + const guidelinesPath = path.resolve(__dirname, "../../DESIGN_GUIDELINES.md"); + if (!fs.existsSync(guidelinesPath)) { + throw new Error( + "DESIGN_GUIDELINES.md not found at repo root. Cannot run design review." + ); + } + + const full = fs.readFileSync(guidelinesPath, "utf8"); + + if (full.length <= MAX_GUIDELINES_CHARS) return full; + + // Guidelines exceed the budget. Extract only the Section 11 block (PR Review + // Checklist) — stop at the next ## heading (e.g. Appendix) so the Appendix + // doesn't consume the character budget and truncate the checklist itself. + const section11Match = full.match( + /## 11\. PR Review Checklist[\s\S]*?(?=\n## |\n---\n#|$)/ + ); + if (section11Match) { + const section11 = section11Match[0].slice(0, MAX_GUIDELINES_CHARS); + return ( + "\n\n" + + section11 + ); + } + + // Fallback: hard truncate with a note + return ( + full.slice(0, MAX_GUIDELINES_CHARS) + + "\n\n[guidelines truncated — see DESIGN_GUIDELINES.md for full content]" + ); +} + +function truncateDiff(diff) { + if (diff.length <= MAX_DIFF_CHARS) return diff; + return ( + diff.slice(0, MAX_DIFF_CHARS) + + "\n\n[diff truncated, only first 80,000 characters reviewed]" + ); +} + +/** + * @param {string} diff - unified diff string (already filtered to relevant files) + * @param {string[]} changedFiles - list of changed file paths + * @returns {{ system: string, user: string }} + */ +function buildPrompt(diff, changedFiles) { + const guidelines = loadGuidelines(); + const truncatedDiff = truncateDiff(diff); + + const system = `You are a strict design review agent for the Keploy Docs website (keploy.io/docs). + +Your ONLY job is to review code diffs against the Keploy Docs Design Guidelines. + +RULES FOR YOUR REVIEW: +- Only flag issues that are present in the diff provided. Do not comment on code outside the diff. +- Cite the exact rule ID (e.g., A1, B3, C7) from Section 11 of the guidelines for every issue. +- Be concise and specific — point to the exact line or element causing the issue. +- Do not give generic advice. Every comment must reference a specific rule. +- If the diff has no design issues, say so clearly. +- Do not review logic, functionality, or non-design concerns. + +OUTPUT FORMAT (strict Markdown): + +## Keploy Design Review + +### Summary +One sentence verdict: pass / has issues. + +### ❌ Blockers +Issues that must be fixed before merge. If none, write "None". +- **[RuleID]** \`filename\`: description of violation + +### ⚠️ Major Issues +Issues that should be fixed before merge. If none, write "None". +- **[RuleID]** \`filename\`: description of violation + +### ℹ️ Minor Suggestions +Low-impact suggestions. If none, write "None". +- **[RuleID]** \`filename\`: description + +### ✅ Passed Checks +List 3 to 5 design rules that were correctly followed in this diff. + +--- +*Reviewed against DESIGN_GUIDELINES.md — Keploy Docs Design System*`; + + const user = `## Changed Files +${changedFiles.length > 0 ? changedFiles.map((f) => `- ${f}`).join("\n") : "No files listed."} + +## Diff +\`\`\`diff +${truncatedDiff} +\`\`\` + +## Design Guidelines +${guidelines} + +Review the diff above against the design guidelines. Follow the output format exactly.`; + + return { system, user }; +} + +module.exports = { buildPrompt }; diff --git a/.github/workflows/design-review.yml b/.github/workflows/design-review.yml new file mode 100644 index 000000000..285d74a5a --- /dev/null +++ b/.github/workflows/design-review.yml @@ -0,0 +1,110 @@ +name: Design Review Agent + +on: + # pull_request_target runs with repo secrets using the BASE branch's workflow code, + # so a PR cannot modify .github/scripts/* and exfiltrate ANTHROPIC_API_KEY. + # The checkout uses the PR base ref (trusted code); the PR's actual changes are + # fetched separately via the GitHub API diff endpoint in get-diff.js. + pull_request_target: + types: [opened, synchronize, reopened] + paths: + # Scoped to the same site directories filter-files.js actually reviews. + # Prevents noisy "nothing to review" comments on PRs that only touch + # .github/**, root config files, or DESIGN_GUIDELINES.md. + - "src/**" + - "docs/**" + - "versioned_docs/**" + - "blog/**" + - "static/**" + + push: + branches: + - "main" + paths: + # Scoped to the same directories filter-files.js actually reviews. + # Excludes .github/ and DESIGN_GUIDELINES.md to avoid "nothing to review" runs. + - "src/**" + - "docs/**" + - "versioned_docs/**" + - "blog/**" + - "static/**" + + workflow_dispatch: + +jobs: + # PR job: only needs issues:write to post a PR comment. contents:read is enough + # because the PR diff is fetched via the API, not by reading local git history. + design-review-pr: + name: Run Design Review (PR) + runs-on: ubuntu-latest + if: "github.event_name == 'pull_request_target' && !contains(github.event.pull_request.title || '', '[skip design-review]')" + permissions: + contents: read + pull-requests: read + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Always check out the BASE branch's trusted workflow scripts. + # The PR's actual changes are fetched from the GitHub API in get-diff.js. + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "20.0.0" + + - name: Install dependencies + # npm ci uses the committed package.json + package-lock.json for reproducible, + # supply-chain-safe installs. --ignore-scripts prevents postinstall hooks + # from running arbitrary code in CI. + run: npm ci --prefix .github/scripts --ignore-scripts --quiet + + - name: Run design review agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + CLAUDE_MODEL: ${{ vars.CLAUDE_MODEL || 'claude-sonnet-4-6' }} + run: node .github/scripts/design-review.js + + # Push/manual job: needs contents:write to post commit comments via the + # /commits/{sha}/comments API. Runs only on push to main and workflow_dispatch. + design-review-push: + name: Run Design Review (Push / Manual) + runs-on: ubuntu-latest + if: "github.event_name != 'pull_request_target' && !contains(github.event.head_commit.message || '', '[skip design-review]')" + permissions: + contents: write + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # fetch-depth 2 is needed so git diff HEAD~1 HEAD works. + fetch-depth: 2 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "20.0.0" + + - name: Install dependencies + run: npm ci --prefix .github/scripts --ignore-scripts --quiet + + - name: Run design review agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_SHA: ${{ github.sha }} + CLAUDE_MODEL: ${{ vars.CLAUDE_MODEL || 'claude-sonnet-4-6' }} + run: node .github/scripts/design-review.js diff --git a/.gitignore b/.gitignore index 39465c1a5..0c0ffd09a 100644 --- a/.gitignore +++ b/.gitignore @@ -203,4 +203,6 @@ Temporary Items # Support for Project snippet scope -# End of https://www.toptal.com/developers/gitignore/api/macos,linux,jetbrains,visualstudiocode \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/macos,linux,jetbrains,visualstudiocode +# Design review agent dependencies +.github/scripts/node_modules/ diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md new file mode 100644 index 000000000..ed33e18b8 --- /dev/null +++ b/DESIGN_GUIDELINES.md @@ -0,0 +1,1311 @@ +# Keploy Docs — Design Guidelines + +> **Version:** 1.0.0 | **Purpose:** AI-powered PR review agent reference + human contributor handbook +> +> This document is the single source of truth for all design decisions in the Keploy documentation website. It is intended to power an automated PR review agent that flags design inconsistencies, enforces standards, and ensures every contribution maintains a coherent, accessible, developer-first experience. + +--- + +## Table of Contents + +1. [Philosophy & Principles](#1-philosophy--principles) +2. [Design Tokens](#2-design-tokens) + - [Color System](#21-color-system) + - [Typography](#22-typography) + - [Spacing](#23-spacing) + - [Border Radius](#24-border-radius) + - [Shadows & Elevation](#25-shadows--elevation) + - [Transitions & Animations](#26-transitions--animations) +3. [Layout System](#3-layout-system) +4. [Component Guidelines](#4-component-guidelines) +5. [Content & Typography Rules](#5-content--typography-rules) +6. [Theming (Light / Dark Mode)](#6-theming-light--dark-mode) +7. [Interaction Design](#7-interaction-design) +8. [Accessibility Standards](#8-accessibility-standards) +9. [Performance Considerations](#9-performance-considerations) +10. [Design Best Practices & Laws of UX](#10-design-best-practices--laws-of-ux) +11. [PR Review Checklist](#11-pr-review-checklist) + +--- + +## 1. Philosophy & Principles + +### What This Site Is + +The Keploy Docs site is a **developer documentation platform** — not a marketing page. Every design decision must serve **clarity, speed, and comprehension** for developers reading technical content. + +### Core Design Principles + +| Principle | Description | +|-----------|-------------| +| **Developer-first UX** | Prioritize code blocks, information hierarchy, and navigability over visual decoration | +| **Radical clarity** | Every element must earn its place. No decoration for decoration's sake | +| **Consistent rhythm** | Predictable spacing, sizing, and colour usage throughout — no surprises | +| **Accessible by default** | Sufficient contrast, keyboard navigation, and semantic HTML are non-negotiable | +| **Content-forward** | The reading experience is the product. Typography and whitespace are primary design tools | +| **Theme parity** | Dark and light modes must be equally polished — neither is an afterthought | + +### Technology Stack + +| Layer | Technology | +|-------|-----------| +| Framework | Docusaurus v3 (Classic theme) | +| Design tokens | Infima CSS variables | +| Utility CSS | Tailwind CSS 3.0.1 | +| Custom overrides | `src/css/custom.css` (3200+ lines) | +| Fonts | `DM Sans` (Google Fonts), `Aeonik` (custom), `Roboto` (local woff2) | +| Code highlighting | Prism.js — `vsLight` (light mode), `dracula` (dark mode) | + +--- + +## 2. Design Tokens + +### 2.1 Color System + +All UI-facing colors should resolve through CSS custom properties or existing theme tokens. + +> **Token-definition exception:** Hard-coded hex/RGB values are acceptable **only when defining design tokens or CSS custom properties** — i.e. inside `:root {}` / `[data-theme]` blocks in `src/css/custom.css`, or inside `theme.extend.colors` in `tailwind.config.js`. These are the single authoritative locations where the palette is established. +> +> **Visible-style rule:** Once a token or CSS variable exists, use that variable everywhere else. Do **not** write hard-coded hex directly in `color`, `background`, `border`, `fill`, `stroke`, or `box-shadow` properties on components, pages, or MDX wrappers. Use the appropriate `var(--ifm-...)` or Tailwind token instead. +> +> **Migration note:** Existing hard-coded hex in non-component files is acceptable only if it is defining a token. If a hard-coded hex in `custom.css` is directly styling a visible element rather than defining a variable, it should be replaced on the next touch. New direct-use hex in component or page files is always a Blocker. + +#### Primary Brand Colors + +| Token | Value | Usage | +|-------|-------|-------| +| `--ifm-color-primary` | `#ff914d` | Primary accent: links, active states, CTAs, sidebar active border, TOC active | +| `--ifm-color-primary-dark` | `#e67643` | Hover state for primary | +| `--ifm-color-primary-darker` | `#c95919` | Pressed/active state | +| `--ifm-color-primary-darkest` | `#be2c1b` | Deep pressed state | +| `--ifm-color-primary-light` | `#ffd0a0` | Light tint backgrounds | +| `--ifm-color-primary-lightest` | `#ffceb1` | Lightest tint | + +> **Rule:** `#ff914d` (Keploy Orange) is the sole primary accent colour. Do not introduce new accent colours. + +#### Semantic Text Colors + +| Context | Light Mode | Dark Mode | +|---------|-----------|-----------| +| Primary body text | `#00163d` | `#f5f6f7` | +| H1 | `#00163d` | `#f9fafb` | +| H2 | `#00163d` | `#f3f4f6` | +| H3–H4 | `#0a2a5e` | `#e5e7eb` | +| H5–H6 | `#374151` | `#9ca3af` | +| Sidebar text | `#374151` | `#e5e7eb` | +| Sidebar muted | `#6b7280` | `#9ca3af` | +| Muted / secondary | `#6b7280` | `#9ca3af` | + +#### Background Colors + +| Context | Light Mode | Dark Mode | +|---------|-----------|-----------| +| Page background | `rgb(249, 250, 251)` | `#141414` | +| Navbar | `#ffffff` | `#141414` | +| Sidebar | `#ffffff` | `#18181b` | +| Card | `#ffffff` | `#1a1a1a` | +| Footer | `#ffffff` | `#000000` | +| Code block (`
`) | `#fcfcfd` | `#1e1e21` |
+| Inline code | `#fff7ed` | `rgba(251, 146, 60, 0.2)` |
+| Code title bar | `#f8fafc` | `#252528` |
+
+#### Border Colors
+
+| Context | Light Mode | Dark Mode |
+|---------|-----------|-----------|
+| Navbar border | `rgba(0, 0, 0, 0.08)` | `rgba(255, 255, 255, 0.08)` |
+| Sidebar border | `rgba(0, 0, 0, 0.08)` | `rgba(255, 255, 255, 0.08)` |
+| Code block border | `rgba(226, 232, 240, 0.8)` | `rgba(255, 255, 255, 0.08)` |
+| H2 bottom rule | `rgba(0, 0, 0, 0.08)` | — |
+| Inline code border | — | `rgba(251, 146, 60, 0.4)` |
+
+#### Semantic / Admonition Colors
+
+| Type | Icon Gradient | Border | Background (Light) |
+|------|--------------|--------|-------------------|
+| `:::note` | `#6366f1 → #4f46e5` (indigo) | `rgba(99, 102, 241, 0.2)` | `rgba(99, 102, 241, 0.08)` |
+| `:::tip` | `#10b981 → #059669` (green) | `rgba(16, 185, 129, 0.2)` | `rgba(16, 185, 129, 0.08)` |
+| `:::warning` / `:::caution` | `#f59e0b → #d97706` (amber) | `rgba(245, 158, 11, 0.2)` | `rgba(245, 158, 11, 0.08)` |
+| `:::danger` | `#ef4444 → #dc2626` (red) | `rgba(239, 68, 68, 0.2)` | `rgba(239, 68, 68, 0.08)` |
+| `:::info` | `#ff914d → #ff7a2d` (orange) | `rgba(255, 145, 77, 0.2)` | `rgba(255, 145, 77, 0.08)` |
+
+#### Tier / Feature Badge Colors
+
+| Tier | Background | Text | Border |
+|------|-----------|------|--------|
+| OSS | `rgba(34, 197, 94, 0.1)` | `#16a34a` | — |
+| Enterprise | `rgba(139, 92, 246, 0.1)` | `#7c3aed` | — |
+| Cloud | `rgba(59, 130, 246, 0.1)` | `#3b82f6` | — |
+
+#### Link Colors
+
+| State | Light Mode | Dark Mode |
+|-------|-----------|-----------|
+| Default | `#ea580c` | `#fb923c` |
+| Hover | `#ff914d` | `#ff914d` |
+| Default underline | `rgba(234, 88, 12, 0.3)` | `rgba(251, 146, 60, 0.3)` |
+| Hover underline | `#ff914d` | `#ff914d` |
+
+#### Tailwind Custom Palette (Extended Brand)
+
+These are available as Tailwind utilities and used in marketing/home components:
+
+| Name | Value | Notes |
+|------|-------|-------|
+| `offwhite` | `#F2F2F2` | Alt background |
+| `keployblue` | `#B2E7EA` | Light cyan, illustrations |
+| `keploybrightblue` | `#127AE5` | Active blue elements |
+| `keploypurple` | `#B8B4DC` | Light purple tint |
+| `keploybrightpurple` | `#8F86DA` | Vibrant purple |
+| `spaceblack` | `#141414` | Dark mode bg (matches `--ifm-background-color` dark) |
+| `green1` | `#9EE587` | Success illustrations |
+| `green2` | `#32D67B` | Success CTA |
+| `orange1` | `#FFA280` | Soft orange hover |
+| `orange2` | `#FF7065` | Red-orange accent |
+
+> **Rule:** Tailwind palette colors are for marketing/homepage components only. Do NOT use them inside documentation page content.
+
+---
+
+### 2.2 Typography
+
+#### Font Stack
+
+| Role | Font | Weights | Fallback |
+|------|------|---------|---------|
+| Headings (H1–H4) | `"Aeonik"` | 700, 800 | `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` |
+| Body text | `"DM Sans"` | 400, 700 | same system stack |
+| Code / monospace | `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New"` | — | monospace |
+
+> **Rule:** Do not import additional Google Fonts or local fonts without design review. The existing three-family stack (Aeonik + DM Sans + mono) is intentional and must be preserved.
+
+#### Type Scale
+
+| Element | Size | Weight | Line Height | Letter Spacing |
+|---------|------|--------|-------------|----------------|
+| Root/Base | `18px` | 400 | `1.6` | — |
+| Article body | `1rem` (18px) | 400 | `1.8` | — |
+| H1 | `2.5rem` (45px) | 800 | `1.5` | `-0.03em` |
+| H2 | `1.75rem` (31.5px) | 700 | `1.3` | `-0.02em` |
+| H3 | `1.375rem` (24.75px) | 600 | `1.35` | `-0.01em` |
+| H4 | `1.125rem` (20.25px) | 600 | `1.4` | — |
+| H5 | `1rem` (18px) | 600 | `1.5` | — |
+| H6 | `1rem` (18px) | 600 | `1.5` | — |
+| Inline code | `0.8125rem` (14.6px) | 600 | — | — |
+| Code block | `0.8125rem` (14.6px) | 400 | `1.6` | — |
+| Sidebar category | `0.875rem` (15.75px) | 600 | `1.5` | `0.01em` |
+| Sidebar link | `0.8125rem` (14.6px) | 400 | `1.5` | — |
+| TOC link | `0.75rem` (13.5px) | 400 | — | — |
+| TOC active | `0.75rem` (13.5px) | 500 | — | — |
+| Breadcrumb | `0.8125rem` (14.6px) | 500 | — | — |
+| Badge / tag | `0.6875rem` (12.4px) | 600 | — | `0.03em` |
+
+#### Heading Spacing
+
+| Heading | `margin-top` | `margin-bottom` |
+|---------|-------------|----------------|
+| H1 | `-0.25rem` | `0.9rem` |
+| H2 | `3.5rem` | `1.25rem` |
+| H3 | `2.5rem` | `0.75rem` |
+| H4 | `2rem` | `0.5rem` |
+| H5 | `1.5rem` | `0.5rem` |
+| H6 | `1.5rem` | `0.5rem` |
+
+> **Rule:** These spacing values create clear visual hierarchy. Do not collapse or override heading margins in component-level CSS without approval.
+
+---
+
+### 2.3 Spacing
+
+The spacing system is based on a **0.25rem (4px) base unit**. All spacing values should be multiples of this base.
+
+#### Common Spacing Values
+
+| Token | rem | px | Usage |
+|-------|-----|----|-------|
+| `xs` | `0.25rem` | 4px | Tight internal padding (badges) |
+| `sm` | `0.5rem` | 8px | Sidebar item vertical padding, list item gaps |
+| `md` | `0.75rem` | 12px | Sidebar category padding, code padding |
+| `base` | `1rem` | 16px | Default block margin, card padding |
+| `lg` | `1.25rem` | 20px | Code block padding, list padding |
+| `xl` | `1.5rem` | 24px | Paragraph margin-bottom, section spacing |
+| `2xl` | `2rem` | 32px | H4 top margin, table margin |
+| `3xl` | `2.5rem` | 40px | H3 top margin |
+| `4xl` | `3rem` | 48px | Content wrapper padding (L/R) |
+| `5xl` | `3.5rem` | 56px | H2 top margin |
+
+#### Container Widths
+
+| Context | Value |
+|---------|-------|
+| Content reading area | `max-width: 860px` |
+| Sidebar width (default) | `260px` |
+| Sidebar width (large ≥1400px) | `280px` |
+| Sidebar width (tablet 997–1200px) | `200px` |
+| Sidebar width (medium 997–1100px) | `180px` |
+| TOC width (default) | `250px` |
+| TOC width (tablet) | `140–180px` |
+| TOC width (large) | `200–240px` |
+
+> **Rule:** The `860px` content max-width is a reading-width optimization (~70–80 characters per line at 18px base). Do not widen this.
+
+---
+
+### 2.4 Border Radius
+
+| Element | Value |
+|---------|-------|
+| Admonitions / large cards | `14px` |
+| Code blocks (`
`) | `12px` |
+| Sidebar categories | `12px` |
+| Buttons, copy button, sidebar links | `8px` |
+| Search box | `8px` |
+| Blockquote | `0 8px 8px 0` (right-rounded only) |
+| Inline code | `6px` |
+| Breadcrumb links | `6px` |
+| Images | `8px` |
+| Tier badges | `4px` |
+| TOC links | `0` (flat, no radius) |
+
+> **Rule:** Radii follow a consistent scale: `4px → 6px → 8px → 12px → 14px`. Do not introduce intermediate values like `10px` or `16px`.
+
+---
+
+### 2.5 Shadows & Elevation
+
+| Element | Value |
+|---------|-------|
+| Code block (light) | `0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 16px rgba(0, 0, 0, 0.02)` |
+| Code block (dark) | `0 10px 15px -3px rgba(0, 0, 0, 0.4)` |
+| Sidebar category hover (light) | `0 2px 8px rgba(139, 92, 246, 0.08)` |
+| Sidebar category hover (dark) | `0 2px 8px rgba(139, 92, 246, 0.15)` |
+| Copy button hover | `0 4px 12px rgba(139, 92, 246, 0.3)` |
+| Tailwind `shadow-keployblue` | `0 25px 50px -12px rgba(178, 231, 234, 0.1)` |
+
+> **Rule:** Shadows must be subtle and purposeful. Avoid `box-shadow` values that create harsh depth on documentation content elements.
+
+---
+
+### 2.6 Transitions & Animations
+
+| Use case | Value |
+|----------|-------|
+| Fast (hover feedback) | `all 0.15s ease` |
+| Standard | `all 0.2s ease` |
+| Smooth eased (sidebar) | `all 0.25s cubic-bezier(0.4, 0, 0.2, 1)` |
+| Slower eased | `all 0.3s cubic-bezier(0.4, 0, 0.2, 1)` |
+| Chevron rotation | `transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)` |
+| Fade-in-down (banners) | `opacity + translateY(-10px) 0.5s ease-out` |
+| Scale hover (Tailwind util) | `hover:scale-105` + `motion-reduce:transform-none` |
+
+> **Rule:** All interactive elements must have `transition` defined. Always include `motion-reduce:transition-none` for Tailwind scale utilities to respect accessibility preferences.
+
+---
+
+## 3. Layout System
+
+### Page Structure (3-Column)
+
+```
+┌────────────────────────────────────────────────────────────┐
+│                        NAVBAR (100vw)                      │
+├──────────────┬──────────────────────────────┬──────────────┤
+│              │                              │              │
+│   SIDEBAR    │      CONTENT AREA            │     TOC      │
+│   260px      │      max-width: 860px        │    250px     │
+│   (sticky)   │      padding: 0 3rem         │   (sticky)   │
+│              │      line-height: 1.8        │              │
+│              │                              │              │
+├──────────────┴──────────────────────────────┴──────────────┤
+│                  PAGINATION  (full width)                   │
+├────────────────────────────────────────────────────────────┤
+│                        FOOTER                              │
+└────────────────────────────────────────────────────────────┘
+```
+
+### Navbar
+
+```
+height:          var(--ifm-navbar-height)  /* ~60px Docusaurus default */
+background:      #ffffff (light) / #141414 (dark)
+border-bottom:   1px solid rgba(0,0,0,0.08) / rgba(255,255,255,0.08)
+padding:         0.5rem 0
+logo height:     32px
+z-index:         sticky above content
+```
+
+**Rules:**
+- Navbar must have a visible border-bottom separator from the content
+- Logo must always be 32px tall — do not resize
+- Search is positioned in the navbar; never move it elsewhere
+- Gap between right-aligned navbar items: `12px`
+
+### Sidebar Navigation
+
+```
+width:           260px (--doc-sidebar-width)
+background:      #ffffff / #18181b
+border-right:    1px solid rgba(0,0,0,0.08)
+position:        sticky, full height
+scrollbar:       6px wide, rgba(255,145,77,0.3) thumb
+```
+
+**Rules:**
+- Category items: `padding: 0.75rem 1rem`, `border-radius: 12px`
+- Link items: `padding: 0.5rem 0.75rem`, `border-radius: 8px`
+- Active item: `3px solid #ff914d` left accent, `rgba(255,145,77,0.1)` background
+- Nesting depth ≤ 3 levels — deeper nesting requires architecture review
+- Category labels use font-size `0.875rem`, weight `600`, uppercase with `letter-spacing: 0.01em`
+- Nested items: `font-size: 0.8125rem`, deeper nesting: `0.75rem`
+
+### Content Area
+
+```
+max-width:       860px
+margin:          auto
+padding:         0 3rem (left/right wrapper)
+font-size:       1rem (18px base)
+line-height:     1.8
+```
+
+**Rules:**
+- Never exceed `860px` content width — this is an optimal reading-line-length constraint
+- Content padding must be `3rem` on desktop, `1rem` on mobile
+- Article `
` must not have added custom backgrounds or borders + +### Table of Contents (Right TOC) + +``` +width: 250px +position: sticky; top: 76px +max-height: calc(100vh - 96px) +overflow-y: auto +padding: 0 1.25rem 0 0.5rem +scrollbar: 4px, rgba(255,145,77,0.3) +``` + +**Rules:** +- TOC links: `font-size: 0.75rem`, default color `#6b7280` +- Active TOC link: `color: #ff914d`, `font-weight: 500` +- TOC border-left: default `#e5e7eb`, active `#ff914d` +- "On this page" label: `font-size: 0.95rem`, `font-weight: 600` +- TOC is hidden on screens < 996px + +### Footer + +``` +background: #ffffff / #000000 +border-top: 1px solid rgba(0,0,0,0.08) +padding-top: 2rem +social icons: gap: 5.5rem; size: 24px (desktop), 22px (mobile) +``` + +### Responsive Breakpoints + +| Name | Media Query | Behaviour | +|------|------------|-----------| +| Mobile | `max-width: 996px` | Sidebar collapses to drawer, TOC hidden, content full-width | +| Tablet | `min-width: 997px` | Sidebar `200px`, TOC `140–180px` | +| Medium | `997px – 1100px` | Sidebar `180px` | +| Desktop | `min-width: 1200px` | Default sidebar `260px`, TOC `250px` | +| Large | `min-width: 1400px` | Sidebar `280px`, TOC `200–240px` | + +**Rules:** +- Mobile: content padding reduces to `1rem`, code font to `0.75rem`, border-radius to `6px` +- Pagination stacks vertically on mobile (`flex-direction: column`) +- Never override Docusaurus responsive collapse behaviour for the sidebar + +--- + +## 4. Component Guidelines + +### 4.1 Headings (H1–H6) + +**Purpose:** Create scannable content hierarchy for long-form technical documentation. + +**Visual Rules:** + +| Level | Font | Size | Weight | Color (Light) | Top Margin | +|-------|------|------|--------|--------------|------------| +| H1 | Aeonik | 2.5rem | 800 | `#00163d` | `-0.25rem` | +| H2 | Aeonik | 1.75rem | 700 | `#00163d` | `3.5rem` | +| H3 | Aeonik | 1.375rem | 600 | `#0a2a5e` | `2.5rem` | +| H4 | Aeonik | 1.125rem | 600 | `#0a2a5e` | `2rem` | +| H5 | DM Sans | 1rem | 600 | `#374151` | `1.5rem` | +| H6 | DM Sans | 1rem | 600 | `#374151` | `1.5rem` | + +- H2 gets a `border-bottom: 1px solid rgba(0,0,0,0.08)` and `padding-bottom: 0.5rem` +- H1 uses gradient text clip in some hero contexts: `-webkit-background-clip: text` + +**Do's:** +- ✅ Use only one H1 per page +- ✅ Follow strict hierarchy: H1 → H2 → H3 (never skip levels) +- ✅ Use infinitive verb forms: "Install Keploy", not "Installing Keploy" +- ✅ Use sentence case: "Configure your environment", not "Configure Your Environment" + +**Don'ts:** +- ❌ Do not use H1 inside MDX components or admonitions +- ❌ Do not add custom color or font-size to headings inline +- ❌ Do not bold an entire heading — heading weight handles emphasis +- ❌ Do not skip heading levels (e.g., H2 → H4) + +--- + +### 4.2 Paragraphs & Body Text + +**Visual Rules:** +- Font: `DM Sans`, `1rem` (18px), weight `400` +- Line height: `1.8` +- Color: `#00163d` (light) / `#f5f6f7` (dark) +- `margin-bottom: 1.5rem` + +**Do's:** +- ✅ Keep paragraphs short — 3–5 sentences max for technical content +- ✅ Use active voice + +**Don'ts:** +- ❌ Do not set custom `color` on `

` tags +- ❌ Do not reduce `line-height` below `1.6` + +--- + +### 4.3 Code Blocks (Fenced) + +**Purpose:** Display multi-line commands, configuration snippets, and code samples. + +**Visual Rules:** +``` +background: #fcfcfd (light) / #1e1e21 (dark) +border: 1px solid rgba(226,232,240,0.8) / rgba(255,255,255,0.08) +border-radius: 12px +padding: 1rem 1.25rem +font-size: 0.8125rem (14.6px) +line-height: 1.6 +font-family: monospace stack +box-shadow: (see §2.5) +margin: 2rem 0 +Prism (light): vsLight theme +Prism (dark): dracula theme +``` + +**Title bar (filename label):** +``` +background: #f8fafc / #252528 +border-bottom: 1px solid #e2e8f0 / rgba(255,255,255,0.08) +color: #475569 / #94a3b8 +font-size: 0.75rem +padding: 0.5rem 1rem +``` + +**Copy button:** +``` +position: absolute top: 0.75rem; right: 0.75rem +opacity: 0 by default, 1 on code block hover/focus +border-radius: 8px +transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1) +hover: translateY(-2px), purple shadow +``` + +**Do's:** +- ✅ Always specify the language identifier after triple backticks: ` ```bash `, ` ```javascript ` +- ✅ Use filename labels for config file examples: ` ```yaml title="keploy.yaml" ` +- ✅ Highlight relevant lines using Docusaurus `{1,3-5}` syntax when needed + +**Don'ts:** +- ❌ Do not use code blocks for single tokens — use inline code instead +- ❌ Do not add inline styles to `

` or `` elements
+- ❌ Do not override code block background colors in page-level CSS
+
+---
+
+### 4.4 Inline Code
+
+**Purpose:** Mark file names, commands, variables, and technical terms within prose.
+
+**Visual Rules:**
+```
+background:     #fff7ed (light) / rgba(251,146,60,0.2) (dark)
+color:          #c2410c (light) / #fb923c (dark)
+border:         (dark only) 1px solid rgba(251,146,60,0.4)
+padding:        0.2rem 0.4rem
+border-radius:  6px
+font-size:      0.8125rem
+font-weight:    600
+```
+
+**Do's:**
+- ✅ Use for: CLI commands, file paths, env variable names, function/method names, flag names
+- ✅ Consistent: wrap all code-like tokens, not just some
+
+**Don'ts:**
+- ❌ Do not use for full sentences or descriptions
+- ❌ Do not override inline code color — it uses semantic orange to distinguish from link orange
+
+---
+
+### 4.5 Admonitions / Callout Boxes
+
+**Purpose:** Surface important notes, tips, warnings, dangers, and info contextually.
+
+**5 Types and When to Use:**
+
+| Type | Keyword | Use For |
+|------|---------|---------|
+| `:::note` | Note | Supplementary context, caveats |
+| `:::tip` | Tip | Best practices, helpful shortcuts |
+| `:::warning` / `:::caution` | Warning | Potential issues, gotchas |
+| `:::danger` | Danger | Destructive actions, data loss risks |
+| `:::info` | Info | Keploy-specific callouts, feature notes |
+
+> **Admonition syntax note:** Use the Docusaurus `:::type` syntax for all **new** documentation. Legacy files (primarily under `versioned_docs/`) may contain GitHub-style admonitions (`> [!NOTE]`, `> [!TIP]`) — these are tolerated in versioned content but should not be used in new pages under `docs/`. Migrate to `:::type` syntax when touching those files.
+
+**Visual Rules:**
+```
+border-radius:  14px
+border:         1px solid [type-color at 20% opacity]
+background:     gradient 135deg, [type-color at 8% → 2%]
+left-accent:    4px vertical bar with type gradient
+padding:        1rem 1.25rem
+margin:         1.5rem 0
+heading size:   0.875rem, weight 700, uppercase, letter-spacing 0.04em
+content size:   0.9375rem, line-height 1.7
+```
+
+**Do's:**
+- ✅ Use the correct semantic type — don't use `:::warning` as a general note
+- ✅ Keep admonition content concise (2–4 lines)
+- ✅ Use the built-in Docusaurus `:::type` syntax — never a custom `
` for callouts + +**Don'ts:** +- ❌ Do not nest admonitions +- ❌ Do not create custom callout divs with inline styles +- ❌ Do not use `:::danger` for non-destructive warnings + +--- + +### 4.6 Links + +**Visual Rules:** +``` +color: #ea580c (light) / #fb923c (dark) +border-bottom: 1px solid rgba(234,88,12,0.3) +text-decoration: none +hover color: #ff914d +hover border: 1px solid #ff914d +transition: all 0.15s ease +``` + +**Do's:** +- ✅ Use descriptive anchor text: "See the [configuration guide](...)" not "click [here](...)" +- ✅ Distinguish internal links (relative) from external links + +**Don'ts:** +- ❌ Do not use raw URLs as link text +- ❌ Do not add `color` or `text-decoration` overrides to anchor tags +- ❌ Do not style links as buttons unless they are truly CTAs + +--- + +### 4.7 Blockquotes + +**Purpose:** Highlight important quotes, key statements, or referenced excerpts. + +``` +border-left: 4px solid #8b5cf6 +background: rgba(139, 92, 246, 0.05) / rgba(139,92,246,0.1) dark +border-radius: 0 8px 8px 0 +padding: 1rem 1.5rem +margin: 1.5rem 0 +color: #000000 (light) / #eeeeee (dark) +``` + +> **Rule:** Blockquotes use **purple** (`#8b5cf6`) as their accent — this is intentional and distinct from orange (interactive) and semantic (admonition) colors. + +--- + +### 4.8 Tables + +``` +width: 100% +cell padding: 12px 18px +line-height: 1.6 +margin-bottom: 2rem +``` + +**Do's:** +- ✅ Always include a header row +- ✅ Keep column counts to ≤ 6 for readability on mobile +- ✅ Use tables for comparison/reference data only + +**Don'ts:** +- ❌ Do not use tables for layout purposes +- ❌ Do not add inline `width` attributes to `` or `` + +--- + +### 4.9 Sidebar Navigation + +(See Layout §3 for dimensions. This section covers interaction states.) + +**States:** + +| State | Background | Left Border | Text Color | +|-------|-----------|-------------|------------| +| Default | transparent | none | `#374151` / `#e5e7eb` | +| Hover | `rgba(255,145,77,0.06)` | none | `#374151` | +| Active/Current | `rgba(255,145,77,0.1)` | `3px solid #ff914d` | `#ff914d`, weight 600 | +| Category | transparent | none | `#ff914d`, weight 600 | + +**Do's:** +- ✅ Active page must always be visually distinguished with the orange left border +- ✅ Categories must be visually heavier than items (larger font, weight 600) + +**Don'ts:** +- ❌ Do not add icons to sidebar items unless following existing badge patterns +- ❌ Do not change the sidebar background — it intentionally contrasts with the page background + +--- + +### 4.10 Table of Contents (TOC) + +**States:** + +| State | Color | Weight | +|-------|-------|--------| +| Default link | `#6b7280` | 400 | +| Hover | `#ff914d` | 400 | +| Active (in-viewport heading) | `#ff914d` | 500 | + +- Border-left: default `#e5e7eb`, active `#ff914d` +- "On this page" label: `0.95rem`, weight `600` +- TOC hides at `< 996px` + +--- + +### 4.11 Tier / Feature Badges (OSS / Enterprise / Cloud) + +**Purpose:** Indicate feature availability by tier in an inline context. + +``` +display: inline-flex +padding: 0.125rem 0.5rem +font-size: 0.6875rem +font-weight: 600 +border-radius: 4px +text-transform: uppercase +letter-spacing: 0.03em +vertical-align: middle +``` + +**Callout box variants** (block-level, not inline): +``` +padding: 1rem 1.25rem +margin: 1.5rem 0 +border-radius: 8px +border: 1px solid [tier-color at 20%] +``` + +**Do's:** +- ✅ Use inline badge in headings/sentences to mark feature availability +- ✅ Use block callout at the top of a page/section when an entire page is tier-gated + +**Don'ts:** +- ❌ Do not use custom tier colors — only the three defined (green/purple/blue) +- ❌ Do not create custom badge components + +--- + +### 4.12 Breadcrumbs + +``` +position: sticky; top: var(--ifm-navbar-height) +background: #ffffff (light), z-index: 10 +font-size: 0.8125rem +font-weight: 500 +separator: › (CSS content), color #9ca3af +link color: #6b7280 +link padding: 0.375rem 0.5rem +link radius: 6px +``` + +--- + +### 4.13 Pagination (Prev / Next) + +``` +border-top: 2.5px solid var(--ifm-color-emphasis-200) +padding-top: 2rem +margin: 2rem 0 3rem +card border: 1.5px solid +card radius: 6px +card padding: 1rem +label size: 0.9rem, weight 700 +title size: 0.85rem, weight 600 +``` + +**Do's:** +- ✅ Always include both previous and next on interior pages +- ✅ Pagination card labels must be "Previous" and "Next" + +--- + +### 4.14 Search UI + +- Positioned in the navbar +- Uses Docusaurus Algolia DocSearch or built-in search +- Search input: `rounded-lg px-3 py-2` (Tailwind) + +> **Rule:** Do not replace or restyle the search component. + +--- + +### 4.15 Announcement Bar + +``` +background: repeating-linear-gradient with rgba(255,145,77,0.15) at 10px steps +text: #00163d (light) / #f3f4f6 (dark) +link: #c45a1a (light) / #ff914d (dark) +border-bottom: 1px solid rgba(255,145,77,0.2) +``` + +--- + +## 5. Content & Typography Rules + +### Heading Hierarchy Consistency + +1. Every page **must** begin with exactly one `H1` (the page title) +2. Sections use `H2` +3. Sub-sections use `H3` +4. Paragraphs within sub-sections may use `H4` for granular topics +5. `H5`/`H6` should be used sparingly — consider restructuring if needed +6. **Never skip heading levels** (e.g., H2 → H4) + +### Writing Style (from `STYLE.md`) + +- **Primary guide:** Google Developer Documentation Style Guide +- **Secondary guide:** Microsoft Writing Style Guide +- **Voice:** Active voice preferred +- **Capitalization:** Sentence case for headings, capitalize Keploy-specific proper nouns +- **Verb form for tasks:** Infinitive ("Install", "Configure"), not gerund ("Installing", "Configuring") +- **Numeric ranges:** En-dash (–), not hyphen (-) +- **Code in prose:** Always wrap in backticks — filenames, commands, flags, paths, variable names + +### Paragraph & Content Rules + +- Paragraph `margin-bottom: 1.5rem` — never collapse this +- Max line length enforced by `860px` content width — do not add inline `max-width` to paragraphs +- `line-height: 1.8` for article body — minimum `1.6` anywhere +- List `margin-bottom: 0.5rem` per item, `1.5rem` for the list block +- List `padding-left: 1.5rem` + +### Emphasis Rules + +| Emphasis | Markdown | Use For | +|----------|----------|---------| +| **Bold** | `**text**` | Key terms, critical warnings | +| *Italic* | `*text*` | Titles, technical terms being introduced | +| `inline code` | `` `text` `` | All code-like tokens | +| ~~strikethrough~~ | `~~text~~` | Deprecated items only | + +> **Rule:** Do not bold entire sentences. Bold is for **keywords**, not phrases. + +### Scannability + +- Use bullet lists for 3+ parallel items +- Use numbered lists for sequential steps only +- Use `:::tip` admonitions for pro-tips that can be skipped +- Use `:::note` for important caveats at the end of a section +- Avoid walls of prose — break long explanations with sub-headings + +### Code vs Text Balance + +- Code samples should appear **within 2 paragraphs** of their introduction +- Never show code without a sentence explaining what it does +- Keep code blocks to ≤ 30 lines unless showing a full file — use `// ...` to truncate + +--- + +## 6. Theming (Light / Dark Mode) + +### Theme Architecture + +- Base: Docusaurus `html[data-theme="light"]` / `html[data-theme="dark"]` selectors +- User toggle: Enabled (navbar theme switch) +- Default: Light mode + +### Key Theme Variable Pairs + +| Variable | Light | Dark | +|----------|-------|------| +| `--ifm-background-color` | `rgb(249, 250, 251)` | `#141414` | +| `--ifm-color` | `#00163d` | `#f5f6f7` | +| `--sidebar-bg` | `#ffffff` | `#18181b` | +| `--ifm-card-background-color` | `#ffffff` | `#1a1a1a` | +| `--ifm-footer-background-color` | `#ffffff` | `#000000` | +| `--ifm-code-background` | `#fff7ed` | `rgba(251, 146, 60, 0.2)` | +| `--ifm-code-color` | `#c2410c` | `#fb923c` | + +### Theme Rules + +1. **Every new CSS rule that sets color, background, or border must have a dark mode counterpart** inside `html[data-theme="dark"]` +2. Hard-coded hex/RGB values are allowed **only when defining design tokens** (e.g. inside `:root {}` / `[data-theme]` blocks in `custom.css`, or in `tailwind.config.js` theme tokens). When styling visible elements in components or pages, always use CSS variables — never literal color values. +3. Contrast ratios must meet WCAG AA minimum (4.5:1 for body text, 3:1 for large text) in both modes +4. The primary orange `#ff914d` is the **same in both modes** — it has sufficient contrast in both + +### Contrast Reference + +| Pair | Ratio | Standard | +|------|-------|---------| +| `#00163d` on `rgb(249,250,251)` (light body) | ~14:1 | AAA ✅ | +| `#f5f6f7` on `#141414` (dark body) | ~13:1 | AAA ✅ | +| `#ff914d` on `#141414` (orange on dark bg) | ~5.6:1 | AA ✅ | +| `#ea580c` on `rgb(249,250,251)` (links light) | ~4.8:1 | AA ✅ | +| `#fb923c` on `#141414` (links dark) | ~5.2:1 | AA ✅ | +| `#c2410c` on `#fff7ed` (inline code) | ~5.1:1 | AA ✅ | + +--- + +## 7. Interaction Design + +### Hover States + +| Element | Hover Behaviour | +|---------|----------------| +| Links | Color → `#ff914d`, border-bottom → `#ff914d` | +| Sidebar items | Background → `rgba(255,145,77,0.06)` | +| Sidebar categories | Background → `rgba(255,145,77,0.08)` + subtle shadow | +| Code block | Copy button fades in (opacity 0 → 1) | +| Copy button | `translateY(-2px)` + purple glow shadow | +| Buttons | `opacity-90` | +| TOC links | Color → `#ff914d` | + +### Active / Selected States + +| Element | Active Behaviour | +|---------|-----------------| +| Sidebar current page | `3px solid #ff914d` left border, bg tint, weight 600 | +| TOC current section | `color: #ff914d`, `font-weight: 500` | +| Navbar link | Underline or color indicator via Docusaurus default | + +### Focus States (Accessibility) + +- All interactive elements must have visible `:focus-visible` outlines +- Focus ring: `ring-2 ring-[--ifm-color-primary] ring-offset-2` (Tailwind pattern) +- Do not use `outline: none` without providing a custom focus indicator +- Focus ring color: `#ff914d` (matches primary) + +### Transition Principles + +1. All interactive state changes must use CSS transitions — no instant jumps +2. Fast feedback (`0.15s`) for hover on links/buttons +3. Smooth easing (`cubic-bezier(0.4, 0, 0.2, 1)`) for structural changes like sidebar expand +4. Respect `prefers-reduced-motion` — always include `motion-reduce:transition-none` + +--- + +## 8. Accessibility Standards + +### Color Contrast + +- **Body text:** WCAG AAA (≥ 7:1) in both modes +- **Link text:** WCAG AA (≥ 4.5:1) in both modes +- **Interactive components:** WCAG AA minimum (3:1 for large text) +- **Never** rely on color alone to convey information — admonitions use icons + color + +### Keyboard Navigation + +- All sidebar items, TOC links, navbar items must be reachable via `Tab` +- Sidebar collapse/expand must work via `Enter`/`Space` +- Code copy button must be keyboard-accessible (it becomes visible on focus) +- Modal/drawer elements must implement focus trapping + +### Semantic HTML Rules + +1. Use proper heading levels — never use heading tags for visual sizing +2. Use `