Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions app/components/copy-page-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useState } from "react";
import { DetailsMenu } from "~/modules/details-menu";
import iconsHref from "~/icons.svg";
import { useHydrated } from "~/ui/utils";

export function CopyPageDropdown({
githubPath,
githubEditPath,
}: {
githubPath: string;
githubEditPath?: string;
}) {
return (
<DetailsMenu className="relative inline-block group select-none w-full">
<summary className="_no-triangle cursor-pointer flex text-sm text-gray-300 group hover:text-gray-700 focus-visible:text-gray-700 dark:text-gray-400 dark:hover:text-gray-50 dark:focus-visible:text-gray-50 w-fit">
<div className="flex py-2 pl-4 pr-3 items-center gap-2 border border-gray-100 dark:border-gray-700 rounded-tl-full rounded-bl-full">
<svg className="size-5">
<use href={`${iconsHref}#copy`} />
</svg>
<span>Copy Page</span>
</div>
<div className="flex py-2 pr-4 pl-2 items-center gap-2 border border-gray-100 dark:border-gray-700 rounded-tr-full rounded-br-full border-l-0 text-gray-300 group-open:text-gray-700 dark:text-gray-400 dark:group-open:text-gray-50 dark:focus-visible:text-gray-50">
<svg className="size-5">
<use href={`${iconsHref}#dropdown-arrows`} />
</svg>
</div>
</summary>

<div className="absolute right-0 z-10 mt-1 rounded-xl border border-gray-100 bg-white shadow-lg p-4 flex flex-col gap-4 w-full min-w-fit dark:border-gray-700 dark:bg-gray-900">
<CopyButton
githubPath={githubPath}
className="flex w-full items-start gap-3 text-gray-400 hover:text-gray-700 focus-visible:text-gray-700 dark:text-gray-400 dark:hover:text-gray-50 dark:focus-visible:text-gray-50"
/>

{githubEditPath && (
<a
href={githubEditPath}
className="flex w-full items-start gap-3 text-gray-400 hover:text-gray-700 focus-visible:text-gray-700 dark:text-gray-400 dark:hover:text-gray-50 dark:focus-visible:text-gray-50"
>
<MenuItem
icon="edit"
title="Edit Page"
description="Edit this page on Github"
/>
</a>
)}
</div>
</DetailsMenu>
);
}

function CopyButton({
githubPath,
className,
}: {
githubPath: string;
className: string;
}) {
const defaultTitle = "Copy Page";
const [copiedTitle, setCopiedTitle] = useState(defaultTitle);
const isHydrated = useHydrated();

if (!isHydrated) {
return (
<a href={githubPath} className={className}>
<MenuItem
icon="copy"
title={copiedTitle}
description="Copy Page as Markdown"
/>
</a>
);
}

const copyMarkdown = async () => {
try {
const response = await fetch(githubPath);
if (!response.ok) throw new Error("Failed to fetch markdown");

const markdown = await response.text();
await navigator.clipboard.writeText(markdown);

setCopiedTitle("Copied!");
setTimeout(() => setCopiedTitle(defaultTitle), 2000);
} catch (error) {
console.error("Failed to copy markdown:", error);
try {
setCopiedTitle("Failed to copy");
setTimeout(() => setCopiedTitle(defaultTitle), 2000);
} catch (fallbackError) {
console.error("Fallback copy also failed:", fallbackError);
}
}
};

return (
<button onClick={copyMarkdown} className={className}>
<MenuItem
icon="copy"
title={copiedTitle}
description="Copy Page as Markdown"
/>
</button>
);
}

function MenuItem({
icon,
title,
description,
}: {
icon: string;
title: string;
description: string;
}) {
return (
<>
<svg className="size-5">
<use href={`${iconsHref}#${icon}`} />
</svg>
<div className="flex flex-col items-start text-left">
<span>{title}</span>
<span className="text-xs">{description}</span>
</div>
</>
);
}
36 changes: 0 additions & 36 deletions app/components/doc-layout.tsx

This file was deleted.

30 changes: 9 additions & 21 deletions app/components/docs-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Link } from "react-router";
import { useDoc } from "~/hooks/use-doc";
import { useDocRouteLoaderData } from "~/hooks/use-doc";
import iconsHref from "~/icons.svg";
import { useHeaderData } from "./docs-header/use-header-data";

export function Footer() {
return (
Expand All @@ -23,33 +22,22 @@ export function Footer() {
</a>
</div>
</div>
<div>
<EditLink />
</div>

<EditLink />
</div>
);
}

function EditLink() {
let doc = useDoc();
let { ref } = useHeaderData();

let isEditableRef = ref === "main" || ref === "dev";

if (!doc || !isEditableRef || !doc.filename) {
return null;
}
let routeData = useDocRouteLoaderData();

let editUrl: string;
let repoUrl = "https://github.com/remix-run/react-router";
if (doc.filename.match(/\.tsx?$/)) {
editUrl = `${repoUrl}/edit/${ref}/${doc.filename}`;
} else {
editUrl = `${repoUrl}/edit/${ref}/${doc.slug}.md`;
}
if (!routeData) return null;

return (
<a className="flex items-center gap-1 hover:underline" href={editUrl}>
<a
className="flex xl:hidden items-center gap-1 hover:underline"
href={routeData.githubEditPath}
>
Edit
<svg aria-hidden className="h-4 w-4">
<use href={`${iconsHref}#edit`} />
Expand Down
4 changes: 2 additions & 2 deletions app/components/docs-menu/menu-mobile.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import iconsHref from "~/icons.svg";
import { useDoc } from "~/hooks/use-doc";
import { useDocRouteLoaderData } from "~/hooks/use-doc";
import { DetailsMenu } from "~/modules/details-menu";

export function NavMenuMobile({ children }: { children?: React.ReactNode }) {
let doc = useDoc();
let doc = useDocRouteLoaderData()?.doc;

return (
<DetailsMenu className="group relative flex h-full flex-col lg:hidden ">
Expand Down
38 changes: 17 additions & 21 deletions app/components/on-this-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link } from "react-router";
import type { Doc } from "~/modules/gh-docs/.server";
import iconsHref from "~/icons.svg";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import classNames from "classnames";

export function LargeOnThisPage({
Expand All @@ -11,37 +11,36 @@ export function LargeOnThisPage({
doc: Doc;
mdRef: React.RefObject<HTMLDivElement | null>;
}) {
const navRef = useRef<HTMLDivElement>(null);
const [activeHeading, setActiveHeading] = useState("");

useEffect(() => {
const node = mdRef.current;
if (!node) return;
// The breakpoint for this component is at xl, which is 1280px
// sorry this is hardcoded 🙃
const xlQuery = window.matchMedia("(min-width: 1280px)");

const h2 = Array.from(node.querySelectorAll("h2"));
const h3 = Array.from(node.querySelectorAll("h3"));

const combinedHeadings = [...h2, ...h3]
.sort((a, b) => a.offsetTop - b.offsetTop)
// Iterate backwards through headings to find the last one above scroll position
.reverse();

function handleScroll() {
// bail if the nav is not visible
const node = navRef.current;
if (!node) return;
if (window.getComputedStyle(node).display !== "block") {
const handleScroll = () => {
if (!xlQuery.matches) {
return;
}

const h2 = Array.from(node.querySelectorAll("h2"));
const h3 = Array.from(node.querySelectorAll("h3"));

const combinedHeadings = [...h2, ...h3]
.sort((a, b) => a.offsetTop - b.offsetTop)
// Iterate backwards through headings to find the last one above scroll position
.reverse();

for (const heading of combinedHeadings) {
// 100px arbitrary value to to offset the height of the header (h-16)
if (window.scrollY + 100 > heading.offsetTop) {
setActiveHeading(heading.id);
break;
}
}
}
};

window.addEventListener("scroll", handleScroll);
return () => {
Expand All @@ -50,10 +49,7 @@ export function LargeOnThisPage({
}, [mdRef]);

return (
<div
ref={navRef}
className="sticky top-36 order-1 mt-20 hidden max-h-[calc(100vh-9rem)] w-56 min-w-min flex-shrink-0 self-start overflow-y-auto pb-10 xl:block"
>
<div className="max-h-[calc(100vh-9rem)] overflow-y-auto">
<nav className="mb-3 flex items-center font-semibold">On this page</nav>
<ul className="md-toc flex flex-col flex-wrap gap-3 leading-[1.125]">
{doc.headings.map((heading, i) => (
Expand All @@ -69,7 +65,7 @@ export function LargeOnThisPage({
className={classNames(
activeHeading == heading.slug &&
"text-gray-900 dark:text-gray-50",
" block py-1 text-sm text-gray-400 hover:text-gray-900 active:text-red-brand dark:text-gray-400 dark:hover:text-gray-50 dark:active:text-red-brand",
"block py-1 text-sm text-gray-400 hover:text-gray-900 active:text-red-brand dark:text-gray-400 dark:hover:text-gray-50 dark:active:text-red-brand",
)}
/>
</li>
Expand Down
9 changes: 6 additions & 3 deletions app/hooks/use-doc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useMatches } from "react-router";
import type { Doc } from "~/modules/gh-docs/.server";
import type { Route } from "../pages/+types/doc";

type DocRouteData = Route.ComponentProps["loaderData"];

/**
* Looks for a leaf route match with a `doc` key
*/
export function useDoc(): Doc | null {
export function useDocRouteLoaderData(): DocRouteData | null {
let data = useMatches().slice(-1)[0].data;
if (!data || !(typeof data === "object") || !("doc" in data)) return null;
return data.doc as Doc;

return data as DocRouteData;
}
6 changes: 6 additions & 0 deletions app/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/modules/gh-docs/.server/compat-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin } from "unified";
import type { Root, Paragraph, Text, HTML } from "mdast";
import type { Root } from "mdast";
import type { Element } from "hast";
import { visit } from "unist-util-visit";
import { fromHtml } from "hast-util-from-html";
Expand Down
Loading