From 168685d98818f2174f5951235475f7cdcbffa664 Mon Sep 17 00:00:00 2001 From: "kilo-code-bot[bot]" <240665456+kilo-code-bot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 10:28:06 -0700 Subject: [PATCH] docs: convert Copy page button into dropdown menu with Open markdown and Edit page options (#6763) * docs: convert Copy page button to dropdown menu with Open markdown and Edit page options * docs: fix button border highlight and add page footer with actions - Fix split-button hover: highlight entire button group (both main button and chevron) when hovering any part of the group, using .page-actions:hover parent selector - Add PageFooter component with horizontal layout showing Copy page, Open markdown, and Edit page actions at the bottom of every docs page - Footer uses same API endpoints (raw-markdown, resolve-path) and GitHub URL construction as the dropdown button * fix: remove flex class from article-content to fix PageFooter layout The article-content div had Tailwind's 'flex' class which set display:flex with default row direction. The 'column' class was not a valid Tailwind utility (should be 'flex-col'), so the PageFooter rendered to the right of page content instead of below it. Removing 'flex column' restores block layout so children stack vertically. --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../kilo-docs/components/CopyPageButton.tsx | 256 +++++++++++++++--- packages/kilo-docs/components/PageFooter.tsx | 230 ++++++++++++++++ packages/kilo-docs/components/index.js | 1 + packages/kilo-docs/pages/_app.tsx | 5 +- packages/kilo-docs/pages/api/resolve-path.ts | 36 +++ 5 files changed, 487 insertions(+), 41 deletions(-) create mode 100644 packages/kilo-docs/components/PageFooter.tsx create mode 100644 packages/kilo-docs/pages/api/resolve-path.ts diff --git a/packages/kilo-docs/components/CopyPageButton.tsx b/packages/kilo-docs/components/CopyPageButton.tsx index df670b8ae4..525431336e 100644 --- a/packages/kilo-docs/components/CopyPageButton.tsx +++ b/packages/kilo-docs/components/CopyPageButton.tsx @@ -1,17 +1,26 @@ -import React, { useState, useEffect } from "react" +import React, { useState, useEffect, useRef } from "react" import { useRouter } from "next/router" +const GITHUB_REPO = "Kilo-Org/kilocode" +const GITHUB_BRANCH = "main" + interface CopyPageButtonProps { className?: string } +function getRoutePath(asPath: string) { + const path = asPath.split("#")[0].split("?")[0] + return path === "/" ? "/index" : path +} + export function CopyPageButton({ className }: CopyPageButtonProps) { const router = useRouter() + const [open, setOpen] = useState(false) const [copied, setCopied] = useState(false) const [error, setError] = useState(false) const [isLoading, setIsLoading] = useState(false) + const ref = useRef(null) - // Reset copied/error state after 3 seconds useEffect(() => { if (copied || error) { const timer = setTimeout(() => { @@ -22,18 +31,30 @@ export function CopyPageButton({ className }: CopyPageButtonProps) { } }, [copied, error]) + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpen(false) + } + } + + if (open) { + document.addEventListener("mousedown", handleClickOutside) + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, [open]) + const handleCopy = async () => { if (copied || error || isLoading) return setIsLoading(true) + setOpen(false) try { - // Fetch the raw markdown file based on current route - // The route path maps to pages/.md - const path = router.asPath.split("#")[0].split("?")[0] // Remove hash and query params - const mdPath = path === "/" ? "/index" : path - - // Fetch the raw markdown content from the API route + const mdPath = getRoutePath(router.asPath) const response = await fetch(`/docs/api/raw-markdown?path=${encodeURIComponent(mdPath)}`) if (!response.ok) { @@ -41,7 +62,6 @@ export function CopyPageButton({ className }: CopyPageButtonProps) { } const markdown = await response.text() - await navigator.clipboard.writeText(markdown) setCopied(true) } catch (err) { @@ -52,33 +72,79 @@ export function CopyPageButton({ className }: CopyPageButtonProps) { } } + const openGitHubUrl = async (mode: "raw" | "edit") => { + setOpen(false) + + try { + const mdPath = getRoutePath(router.asPath) + const response = await fetch(`/docs/api/resolve-path?path=${encodeURIComponent(mdPath)}`) + + if (!response.ok) { + throw new Error("Failed to resolve file path") + } + + const { filePath } = await response.json() + + const url = + mode === "raw" + ? `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/${filePath}` + : `https://github.com/${GITHUB_REPO}/edit/${GITHUB_BRANCH}/${filePath}` + + window.open(url, "_blank", "noopener,noreferrer") + } catch (err) { + console.error(`Failed to open ${mode} URL:`, err) + } + } + + const label = copied ? "Copied" : error ? "Copy failed" : "Copy page" + return ( <> - + + + {open && ( +
+ + + +
)} - + ) } -// Copy icon (two overlapping rectangles) function CopyIcon() { return ( ) } + +function ChevronDownIcon() { + return ( + + + + ) +} + +function FileIcon() { + return ( + + + + + ) +} + +function EditIcon() { + return ( + + + + ) +} diff --git a/packages/kilo-docs/components/PageFooter.tsx b/packages/kilo-docs/components/PageFooter.tsx new file mode 100644 index 0000000000..23b75e2874 --- /dev/null +++ b/packages/kilo-docs/components/PageFooter.tsx @@ -0,0 +1,230 @@ +import React, { useState, useEffect } from "react" +import { useRouter } from "next/router" + +const GITHUB_REPO = "Kilo-Org/kilocode" +const GITHUB_BRANCH = "main" + +function getRoutePath(asPath: string) { + const path = asPath.split("#")[0].split("?")[0] + return path === "/" ? "/index" : path +} + +export function PageFooter() { + const router = useRouter() + const [copied, setCopied] = useState(false) + const [error, setError] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + if (copied || error) { + const timer = setTimeout(() => { + setCopied(false) + setError(false) + }, 3000) + return () => clearTimeout(timer) + } + }, [copied, error]) + + const handleCopy = async () => { + if (copied || error || isLoading) return + + setIsLoading(true) + + try { + const mdPath = getRoutePath(router.asPath) + const response = await fetch(`/docs/api/raw-markdown?path=${encodeURIComponent(mdPath)}`) + + if (!response.ok) { + throw new Error("Failed to fetch markdown") + } + + const markdown = await response.text() + await navigator.clipboard.writeText(markdown) + setCopied(true) + } catch (err) { + console.error("Failed to copy page:", err) + setError(true) + } finally { + setIsLoading(false) + } + } + + const openGitHubUrl = async (mode: "raw" | "edit") => { + try { + const mdPath = getRoutePath(router.asPath) + const response = await fetch(`/docs/api/resolve-path?path=${encodeURIComponent(mdPath)}`) + + if (!response.ok) { + throw new Error("Failed to resolve file path") + } + + const { filePath } = await response.json() + + const url = + mode === "raw" + ? `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/${filePath}` + : `https://github.com/${GITHUB_REPO}/edit/${GITHUB_BRANCH}/${filePath}` + + window.open(url, "_blank", "noopener,noreferrer") + } catch (err) { + console.error(`Failed to open ${mode} URL:`, err) + } + } + + const copyLabel = copied ? "Copied" : error ? "Copy failed" : "Copy page" + + return ( + <> +
+
+
+ + + +
+ + + + ) +} + +function CopyIcon() { + return ( + + + + + ) +} + +function CheckIcon() { + return ( + + + + ) +} + +function FileIcon() { + return ( + + + + + ) +} + +function EditIcon() { + return ( + + + + ) +} diff --git a/packages/kilo-docs/components/index.js b/packages/kilo-docs/components/index.js index eaf57b2378..9bb2cc21b2 100644 --- a/packages/kilo-docs/components/index.js +++ b/packages/kilo-docs/components/index.js @@ -6,6 +6,7 @@ export * from "./Heading" export * from "./Icon" export * from "./Image" export * from "./KiloCodeIcon" +export * from "./PageFooter" export * from "./SideNav" export * from "./Table" export * from "./TableOfContents" diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index f3836c97c5..6d45a81782 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -3,7 +3,7 @@ import Head from "next/head" import { useRouter } from "next/router" import posthog from "posthog-js" -import { CopyPageButton, SideNav, TableOfContents, TopNav } from "../components" +import { CopyPageButton, PageFooter, SideNav, TableOfContents, TopNav } from "../components" import "prismjs" import "prismjs/components/prism-bash.min" @@ -178,8 +178,9 @@ export default function MyApp({ Component, pageProps }: AppProps) {
-
+
+ {markdoc && }
{markdoc && } diff --git a/packages/kilo-docs/pages/api/resolve-path.ts b/packages/kilo-docs/pages/api/resolve-path.ts new file mode 100644 index 0000000000..287f3da61e --- /dev/null +++ b/packages/kilo-docs/pages/api/resolve-path.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import fs from "fs" +import path from "path" + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }) + } + + const { path: mdPath } = req.query + + if (!mdPath || typeof mdPath !== "string") { + return res.status(400).json({ error: "Missing path parameter" }) + } + + const sanitizedPath = path.normalize(mdPath).replace(/^\/+/, "") + const pagesDir = path.join(process.cwd(), "pages") + const resolvedPagesDir = path.resolve(pagesDir) + + const candidatePath = path.resolve(pagesDir, `${sanitizedPath}.md`) + const candidateIndexPath = path.resolve(pagesDir, sanitizedPath, "index.md") + + if (!candidatePath.startsWith(resolvedPagesDir) || !candidateIndexPath.startsWith(resolvedPagesDir)) { + return res.status(403).json({ error: "Access denied" }) + } + + if (fs.existsSync(candidatePath)) { + return res.status(200).json({ filePath: `packages/kilo-docs/pages/${sanitizedPath}.md` }) + } + + if (fs.existsSync(candidateIndexPath)) { + return res.status(200).json({ filePath: `packages/kilo-docs/pages/${sanitizedPath}/index.md` }) + } + + return res.status(404).json({ error: "File not found" }) +}