diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 0c24fbc76b..ef2d142ef6 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -2,7 +2,8 @@ import clsx from "clsx"; import React, { useEffect, useRef, useState } from "react"; interface IconProps { - icon: string; + icon?: string; + customicon?: any; color?: string; className?: string; size?: string; @@ -10,7 +11,7 @@ interface IconProps { fit?: "width" | "height"; } -export const Icon = ({ icon, color, className, size, btn, fit }: IconProps) => { +export const Icon = ({ customicon, icon, color, className, size, btn, fit }: IconProps) => { const iconRef = useRef(null); const [font, setFontSize] = useState(0); const [measure, setMeasure] = useState("vw"); @@ -34,8 +35,15 @@ export const Icon = ({ icon, color, className, size, btn, fit }: IconProps) => { window.addEventListener("resize", setFont); return () => window.removeEventListener("resize", setFont); }, []); + return ( - {customicon} : ; + anchorText: string; + dark?: boolean; + rightClick?: boolean; + pos?: "top" | "bottom"; +}; +const Dropdown = ({ + anchorText, + items, + dark = false, + rightClick = false, + pos = "bottom", +}: DropdownType) => { + const [isOpen, openDrop] = useState(false); + const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(true); + + useEffect(() => { + if (!isComponentVisible) openDrop(false); + }, [isComponentVisible]); + + const escEvent = (e: KeyboardEvent) => { + if (e.key === "Escape") openDrop(false); + }; + + useEffect(() => { + window.addEventListener("keydown", escEvent); + return () => window.removeEventListener("keydown", escEvent); + }, []); + + return ( +
{ + if (!rightClick) { + setIsComponentVisible(true); + openDrop(!isOpen); + } + }} + onContextMenu={(e) => { + if (rightClick && window.innerWidth > 940) { + e.preventDefault(); + setIsComponentVisible(true); + openDrop(true); + } + }} + > +
+ + {anchorText} + +
+
{ + e.stopPropagation(); + openDrop(false); + }} + > +
+ {items.map((item: any, idx: number) => ( +
+ {item} +
+ ))} +
+
+
+ ); +}; + +export default Dropdown; diff --git a/src/components/content-dropdown/styles.module.scss b/src/components/content-dropdown/styles.module.scss new file mode 100644 index 0000000000..109943c2f6 --- /dev/null +++ b/src/components/content-dropdown/styles.module.scss @@ -0,0 +1,104 @@ +$border-color: #2d3748; + +.root { + color: var(--primary-font-color); + font-family: var(--font-family-body, Inter); + font-size: var(--font-size-xs, 14px); + font-style: normal; + font-weight: 400; + line-height: 140%; /* 19.6px */ + width: fit-content; + position: absolute; + right: 0; + top: 0; +} + +.anchor { + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + width: fit-content; + border: 1px solid var(--disabled-font-color); + background: var(--surface-primary); + &.active { + background: var(--surface-brand-grey); + } +} + +.overlayWrapper { + opacity: 0; + pointer-events: none; + @media (max-width: 599px) { + position: fixed; + height: 100vh; + z-index: 102; + width: 100vw; + background: rgb(9, 10, 21, 0.75); + top: 0; + left: 0; + } + .container { + transform: translateY(-8px); + } + &.showOverlay { + .container { + transform: translateY(0); + } + } +} + +.showOverlay { + opacity: 1; + pointer-events: auto; +} + +.container { + background: var(--surface-primary); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.10), 0 2px 4px -2px rgba(0, 0, 0, 0.10); + border-radius: 8px; + bottom: 0; + position: absolute; + width: 100%; + overflow: hidden; + right: 0; + transition: transform 300ms ease; + display: flex; + flex-direction: column; + gap: 8px; + @media (min-width: 600px) { + top: 100%; + margin-top: 16px; + bottom: unset; + width: auto; + } +} + +.item { + color: var(--primary-font-color); + width: max-content; + min-width: 100%; + cursor: pointer; + padding: 6px 12px; + background: var(--surface-primary); + transition: background 300ms ease-out; + &:hover { + background: var(--surface-brand-grey-strong); + } +} + +.topPos { + @media (min-width: 600px) { + top: unset !important; + left: 0 !important; + transform: unset !important; + bottom: 100% !important; + } + & > * { + width: 100%; + display: inline-block; + } + + a { + color: inherit; + } +} diff --git a/src/css/all.css b/src/css/all.css index c24242ad49..d5b3294268 100644 --- a/src/css/all.css +++ b/src/css/all.css @@ -844,6 +844,14 @@ content: "\f09b"; } +.fa-markdown::before { + content: "\f60f"; +} + +.fa-openai::before { + content: "\e7cf"; +} + .fa-globe::before { content: "\f0ac"; } diff --git a/src/css/theming.css b/src/css/theming.css index e0fa8efea6..28ead3d261 100644 --- a/src/css/theming.css +++ b/src/css/theming.css @@ -69,11 +69,11 @@ --surface-brand-light: #d9f9f6; --primary-font-color: var(--gray-800); --surface-primary: #ffffff; + --disabled-font-color: #CBD5E0; --navbar-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1); --navbar-gradient: linear-gradient(180deg, #fff, transparent); --navbar-items-bg: #ffffffcc; --surface-secondary: #2d3748; - --surface-primary: #fff; --secondary-font-color: #4a5568; --tertiary-font-color: #718096; --ifm-dropdown-background-color: #fff; @@ -222,6 +222,7 @@ html[data-theme="dark"] { --ifm-tabs-bg-color-active: #2d3748; --main-font-color: #e2e8f0; --primary-font-color: rgb(247, 250, 252); + --disabled-font-color: #4A5568; --surface-primary: var(--gray-1000); --navbar-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.5); --navbar-gradient: linear-gradient(180deg, #090a15, rgba(9, 10, 21, 0)); @@ -363,6 +364,7 @@ html[data-theme="dark"] { --navbar-items-bg: #ffffffcc; --surface-secondary: #2d3748; --surface-primary: #ffffff; + --disabled-font-color: #CBD5E0; --icon-wrapper-bg: rgb(5, 7, 10); --icon-svg-color: #5a67d8; --teal-link-color: #16a394; @@ -493,6 +495,7 @@ html[data-theme="dark"] { --ifm-tabs-bg-color-active: #2d3748; --main-font-color: #e2e8f0; --primary-font-color: rgb(247, 250, 252); + --disabled-font-color: #4A5568; --surface-primary: var(--gray-1000); --navbar-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.5); --navbar-gradient: linear-gradient(180deg, #090a15, rgba(9, 10, 21, 0)); diff --git a/src/theme/DocBreadcrumbs/index.tsx b/src/theme/DocBreadcrumbs/index.tsx index 093c252db7..9524377550 100644 --- a/src/theme/DocBreadcrumbs/index.tsx +++ b/src/theme/DocBreadcrumbs/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import clsx from "clsx"; import { ThemeClassNames } from "@docusaurus/theme-common"; import { useHomePageRoute } from "@docusaurus/theme-common/internal"; diff --git a/src/theme/DocItem/Layout/index.tsx b/src/theme/DocItem/Layout/index.tsx new file mode 100644 index 0000000000..f192bf9cae --- /dev/null +++ b/src/theme/DocItem/Layout/index.tsx @@ -0,0 +1,69 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {useWindowSize} from '@docusaurus/theme-common'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import DocItemPaginator from '@theme/DocItem/Paginator'; +import DocVersionBanner from '@theme/DocVersionBanner'; +import DocVersionBadge from '@theme/DocVersionBadge'; +import DocItemFooter from '@theme/DocItem/Footer'; +import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile'; +import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop'; +import DocItemContent from '@theme/DocItem/Content'; +import DocBreadcrumbs from '@theme/DocBreadcrumbs'; +import ContentVisibility from '@theme/ContentVisibility'; +import type {Props} from '@theme/DocItem/Layout'; + +import styles from './styles.module.css'; + +/** + * Decide if the toc should be rendered, on mobile or desktop viewports + */ +function useDocTOC() { + const {frontMatter, toc} = useDoc(); + const windowSize = useWindowSize(); + + const hidden = frontMatter.hide_table_of_contents; + const canRender = !hidden && toc.length > 0; + + const mobile = canRender ? : undefined; + + const desktop = + canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? ( + + ) : undefined; + + return { + hidden, + mobile, + desktop, + }; +} + +export default function DocItemLayout({children}: Props): ReactNode { + const docTOC = useDocTOC(); + const {metadata} = useDoc(); + return ( +
+
+ + +
+
+ + + {docTOC.mobile} + {children} + +
+ +
+
+ {docTOC.desktop &&
+ {React.cloneElement(docTOC.desktop as React.ReactElement, { + //@ts-ignore + metadata, + })} +
} +
+ ); +} diff --git a/src/theme/DocItem/Layout/styles.module.css b/src/theme/DocItem/Layout/styles.module.css new file mode 100644 index 0000000000..d5aaec1322 --- /dev/null +++ b/src/theme/DocItem/Layout/styles.module.css @@ -0,0 +1,10 @@ +.docItemContainer header + *, +.docItemContainer article > *:first-child { + margin-top: 0; +} + +@media (min-width: 997px) { + .docItemCol { + max-width: 75% !important; + } +} diff --git a/src/theme/DocItem/TOC/Desktop/index.tsx b/src/theme/DocItem/TOC/Desktop/index.tsx new file mode 100644 index 0000000000..ee13bced27 --- /dev/null +++ b/src/theme/DocItem/TOC/Desktop/index.tsx @@ -0,0 +1,17 @@ +import React, {type ReactNode} from 'react'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import TOC from '@site/src/theme/TOC'; + +export default function DocItemTOCDesktop({metadata}: any): ReactNode { + const {toc, frontMatter} = useDoc(); + return ( + + ); +} diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx new file mode 100644 index 0000000000..fa94c101be --- /dev/null +++ b/src/theme/TOC/index.tsx @@ -0,0 +1,64 @@ +import Dropdown from '@site/src/components/content-dropdown'; +import { Icon } from '@site/src/components/Icon'; +import TOCItems from '@theme/TOCItems'; +import clsx from 'clsx'; +import React, { ReactNode } from 'react'; + +import type {Props} from '@theme/TOC'; + +import styles from './styles.module.css'; +import { anthropic, cursor, openai, t3 } from '../TOCItems/icons'; + +// Using a custom className +// This prevents TOCInline/TOCCollapsible getting highlighted by mistake +const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight'; +const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active'; +const getMarkdown = async (link: string) => { + const response = await fetch(link); + const mdx = await response.text(); + navigator.clipboard.writeText(mdx); + return mdx; +} + +export default function TOC({className, metadata, ...props}: Props & {metadata: any }): ReactNode { + const url = `https://prisma.io/docs${metadata.slug}`; + const externalProps = { + target: '_blank', + rel: 'opener noreferrer' + } + const markdown = metadata.editUrl.replace("github", "raw.githubusercontent").replace("/blob", "").replace("/tree", ""); + return ( +
+ getMarkdown(markdown)}>Copy as Markdown
, + + + Open in Claude + + , + + + Open in ChatGPT + + , + + + Open in T3.chat + + , + + + Edit in GitHub + + + ]} /> +
+ +
+ + ); +} diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css new file mode 100644 index 0000000000..d02c7ba954 --- /dev/null +++ b/src/theme/TOC/styles.module.css @@ -0,0 +1,20 @@ +.tableOfContentsWrapper { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + position: sticky; + top: calc(var(--ifm-navbar-height) + 1rem); +} + +.tableOfContents { + overflow-y: auto; + overflow-x: visible; +} + +@media (max-width: 996px) { + .tableOfContents { + display: none; + } + + .docItemContainer { + padding: 0 0.3rem; + } +} diff --git a/src/theme/TOCItems/icons.tsx b/src/theme/TOCItems/icons.tsx new file mode 100644 index 0000000000..a90de3d369 --- /dev/null +++ b/src/theme/TOCItems/icons.tsx @@ -0,0 +1,36 @@ +export const openai = ; + +export const anthropic = + + +; + +export const cursor = + + + + + + + + + + + + + + + + + + + + + +export const t3 = + + + + + diff --git a/src/theme/Tabs/index.tsx b/src/theme/Tabs/index.tsx index acada1426f..0007c0d592 100644 --- a/src/theme/Tabs/index.tsx +++ b/src/theme/Tabs/index.tsx @@ -93,7 +93,6 @@ function TabContent({ } return cloneElement(selectedTabItem, { className: "margin-top--md" }); } - console.log("code: " + code) return (