docs: make code blocks collapsible (#7606)

* added collapsible code feature

* fixed side shadow

* fix build errors

* change design

* make code blocks collapsible
This commit is contained in:
Shahed Nasser
2024-06-05 10:28:41 +03:00
committed by GitHub
parent dc087bf310
commit 4a6327e497
44 changed files with 815 additions and 446 deletions

View File

@@ -12,6 +12,9 @@ export type CodeBlockActionsProps = {
source: string
isSingleLine?: boolean
inHeader: boolean
showGradientBg?: boolean
inInnerCode?: boolean
isCollapsed: boolean
canShowApiTesting?: boolean
blockStyle: CodeBlockStyle
onApiTesting: React.Dispatch<React.SetStateAction<boolean>>
@@ -22,6 +25,9 @@ export type CodeBlockActionsProps = {
export const CodeBlockActions = ({
source,
inHeader,
showGradientBg = true,
inInnerCode = false,
isCollapsed,
isSingleLine = false,
canShowApiTesting = false,
blockStyle,
@@ -45,65 +51,87 @@ export const CodeBlockActions = ({
return (
<div
className={clsx(
"absolute hidden md:flex md:justify-end",
"xs:rounded xs:absolute xs:right-0 xs:top-0 xs:w-[calc(10%+24px)] xs:h-full xs:bg-transparent",
!inHeader && [
"md:right-docs_0.5",
isSingleLine && "md:top-docs_0.25",
!isSingleLine && "md:top-docs_0.5",
],
inHeader && "md:right-docs_1 md:top-docs_0.5"
"absolute hidden md:block",
"xs:rounded xs:absolute xs:right-0 xs:top-0 xs:w-[calc(17%+10px)] xs:h-full"
)}
>
{canShowApiTesting && (
<Tooltip
text="Test API"
tooltipClassName="font-base"
className={clsx(
"h-fit",
!inHeader && "p-[6px]",
inHeader && "px-[6px] pb-[6px]"
)}
>
<PlaySolid
className={clsx("cursor-pointer", iconColor)}
onClick={() => onApiTesting(true)}
/>
</Tooltip>
)}
{!noReport && (
<Tooltip
text="Report Issue"
tooltipClassName="font-base"
className={clsx(
"h-fit",
!inHeader && "p-[6px]",
inHeader && "px-[6px] pb-[6px]"
)}
>
<Link
href={`${GITHUB_ISSUES_PREFIX}&title=${encodeURIComponent(
`Docs(Code Issue): `
)}`}
target="_blank"
<div
className={clsx(
!inHeader &&
showGradientBg && [
inInnerCode &&
"xs:bg-subtle-code-fade-right-to-left dark:xs:bg-subtle-code-fade-right-to-left-dark",
!inInnerCode &&
"xs:bg-base-code-fade-right-to-left dark:xs:bg-base-code-fade-right-to-left-dark",
],
(inHeader || !showGradientBg) && "xs:bg-transparent",
"z-[9] w-full h-full absolute top-0 left-0"
)}
/>
<div
className={clsx(
"md:flex md:justify-end z-[11] relative",
!inHeader && [
"md:pr-docs_0.5",
isCollapsed && "md:pt-docs_2.5",
!isCollapsed && [
isSingleLine && "md:pt-docs_0.25",
!isSingleLine && "md:pt-docs_0.5",
],
],
inHeader && "md:pr-docs_1 md:pt-docs_0.5"
)}
>
{canShowApiTesting && (
<Tooltip
text="Test API"
tooltipClassName="font-base"
className={clsx(
"bg-transparent border-none cursor-pointer rounded",
"[&:not(:first-child)]:ml-docs_0.5",
"inline-flex justify-center items-center invisible xs:visible"
"h-fit",
!inHeader && "p-[6px]",
inHeader && "px-[6px] pb-[6px]"
)}
rel="noreferrer"
>
<ExclamationCircle className={clsx(iconColor)} />
</Link>
</Tooltip>
)}
{!noCopy && (
<CodeBlockCopyAction
source={source}
inHeader={inHeader}
iconColor={iconColor}
/>
)}
<PlaySolid
className={clsx("cursor-pointer", iconColor)}
onClick={() => onApiTesting(true)}
/>
</Tooltip>
)}
{!noReport && (
<Tooltip
text="Report Issue"
tooltipClassName="font-base"
className={clsx(
"h-fit",
!inHeader && "p-[6px]",
inHeader && "px-[6px] pb-[6px]"
)}
>
<Link
href={`${GITHUB_ISSUES_PREFIX}&title=${encodeURIComponent(
`Docs(Code Issue): `
)}`}
target="_blank"
className={clsx(
"bg-transparent border-none cursor-pointer rounded",
"[&:not(:first-child)]:ml-docs_0.5",
"inline-flex justify-center items-center invisible xs:visible"
)}
rel="noreferrer"
>
<ExclamationCircle className={clsx(iconColor)} />
</Link>
</Tooltip>
)}
{!noCopy && (
<CodeBlockCopyAction
source={source}
inHeader={inHeader}
iconColor={iconColor}
/>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import clsx from "clsx"
import React from "react"
import { CollapsibleReturn } from "../../../../hooks"
import { Button } from "@medusajs/ui"
export type CodeBlockCollapsibleButtonProps = {
type: "start" | "end"
expandButtonLabel?: string
className?: string
} & Omit<CollapsibleReturn, "getCollapsibleElms">
export const CodeBlockCollapsibleButton = ({
type,
expandButtonLabel = "Show more",
collapsed,
setCollapsed,
className,
}: CodeBlockCollapsibleButtonProps) => {
if (!collapsed) {
return <></>
}
return (
<>
{type === "start" && (
<Button
className={clsx(
"font-base w-full p-docs_0.5 !shadow-none z-10",
"bg-medusa-contrast-button hover:bg-medusa-contrast-button-hover",
"txt-compact-xsmall text-medusa-contrast-fg-secondary",
type === "start" && "rounded-t-docs_DEFAULT rounded-b-none",
className
)}
onClick={() => setCollapsed(false)}
>
{expandButtonLabel}
</Button>
)}
{type === "end" && (
<Button
className={clsx(
"font-base w-full p-docs_0.5 !shadow-none z-10",
"bg-medusa-contrast-button hover:bg-medusa-contrast-button-hover",
"txt-compact-xsmall text-medusa-contrast-fg-secondary",
"rounded-t-none rounded-b-docs_DEFAULT",
className
)}
onClick={() => setCollapsed(false)}
>
{expandButtonLabel}
</Button>
)}
</>
)
}

View File

@@ -0,0 +1,53 @@
import clsx from "clsx"
import React from "react"
import { CollapsibleReturn } from "../../../../hooks"
export type CodeBlockCollapsibleFadeProps = {
type: "start" | "end"
hasHeader?: boolean
} & Pick<CollapsibleReturn, "collapsed">
export const CodeBlockCollapsibleFade = ({
type,
hasHeader = false,
collapsed,
}: CodeBlockCollapsibleFadeProps) => {
if (!collapsed) {
return <></>
}
return (
<span
className={clsx(
"absolute flex flex-col z-10",
hasHeader && "left-[6px] w-[calc(100%-12px)]",
!hasHeader && "w-full left-0",
type === "start" && [
hasHeader && "top-[44px]",
!hasHeader && "top-[36px]",
],
type === "end" && [
hasHeader && "bottom-[44px]",
!hasHeader && "bottom-[36px]",
]
)}
>
{type === "end" && (
<span
className={clsx(
"w-full h-[56px]",
"bg-code-fade-bottom-to-top dark:bg-code-fade-bottom-to-top-dark"
)}
/>
)}
{type === "start" && (
<span
className={clsx(
"w-full h-[56px]",
"bg-code-fade-top-to-bottom dark:bg-code-fade-top-to-bottom-dark"
)}
/>
)}
</span>
)
}

View File

@@ -0,0 +1,29 @@
"use client"
import React, { useMemo } from "react"
import { CollapsibleReturn } from "../../../../hooks"
export type CodeBlockCollapsibleLinesProps = {
children: React.ReactNode
type: "start" | "end"
} & Omit<CollapsibleReturn, "setCollapsed">
export const CodeBlockCollapsibleLines = ({
children,
type,
collapsed,
getCollapsibleElms,
}: CodeBlockCollapsibleLinesProps) => {
const shownChildren: React.ReactNode = useMemo(() => {
const isStart = type === "start"
return (
<>
{collapsed && Array.isArray(children)
? children.slice(isStart ? -2 : 0, isStart ? undefined : 2)
: children}
</>
)
}, [children, collapsed])
return getCollapsibleElms(shownChildren)
}

View File

@@ -22,6 +22,7 @@ type CodeBlockLineProps = {
lineNumber: number
showLineNumber: boolean
lineNumberColorClassName: string
lineNumberBgClassName: string
noLineNumbers?: boolean
} & Pick<RenderProps, "getLineProps" | "getTokenProps">
@@ -33,6 +34,7 @@ export const CodeBlockLine = ({
getTokenProps,
showLineNumber,
lineNumberColorClassName,
lineNumberBgClassName,
}: CodeBlockLineProps) => {
const lineProps = getLineProps({ line, key: lineNumber })
@@ -241,7 +243,8 @@ export const CodeBlockLine = ({
className={clsx(
"mr-docs_1 table-cell select-none",
"sticky left-0 w-[1%] px-docs_1 text-right",
lineNumberColorClassName
lineNumberColorClassName,
lineNumberBgClassName
)}
>
{lineNumber + 1}

View File

@@ -1,15 +1,19 @@
"use client"
import React, { useMemo, useState } from "react"
import React, { useEffect, useMemo, useRef, useState } from "react"
import clsx from "clsx"
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
import { Highlight, HighlightProps, themes, Token } from "prism-react-renderer"
import { ApiRunner } from "@/components"
import { useColorMode } from "@/providers"
import { CodeBlockHeader, CodeBlockHeaderMeta } from "./Header"
import { CodeBlockLine } from "./Line"
import { ApiAuthType, ApiDataOptions, ApiMethod } from "types"
import { CSSTransition } from "react-transition-group"
import { useCollapsibleCodeLines } from "../.."
import { HighlightProps as CollapsibleHighlightProps } from "@/hooks"
import { CodeBlockActions, CodeBlockActionsProps } from "./Actions"
import { CodeBlockCollapsibleButton } from "./Collapsible/Button"
import { CodeBlockCollapsibleFade } from "./Collapsible/Fade"
export type Highlight = {
line: number
@@ -32,6 +36,8 @@ export type CodeBlockMetaFields = {
noCopy?: boolean
noReport?: boolean
noLineNumbers?: boolean
collapsibleLines?: string
expandButtonLabel?: string
} & CodeBlockHeaderMeta
export type CodeBlockStyle = "loud" | "subtle"
@@ -60,6 +66,8 @@ export const CodeBlock = ({
noReport = false,
noLineNumbers = false,
children,
collapsibleLines,
expandButtonLabel,
...rest
}: CodeBlockProps) => {
if (!source && typeof children === "string") {
@@ -68,6 +76,9 @@ export const CodeBlock = ({
const { colorMode } = useColorMode()
const [showTesting, setShowTesting] = useState(false)
const codeContainerRef = useRef<HTMLDivElement>(null)
const codeRef = useRef<HTMLElement>(null)
const [scrollable, setScrollable] = useState(false)
const hasInnerCodeBlock = useMemo(
() => hasTabs || title.length > 0,
[hasTabs, title]
@@ -107,7 +118,7 @@ export const CodeBlock = ({
const borderColor = useMemo(
() =>
clsx(
blockStyle === "loud" && "border-transparent",
blockStyle === "loud" && "border-0",
blockStyle === "subtle" && [
colorMode === "light" && "border-medusa-border-base",
colorMode === "dark" && "border-medusa-code-border",
@@ -158,10 +169,6 @@ export const CodeBlock = ({
return lowerLang === "json" ? "plain" : lowerLang
}, [lang])
if (!source.length) {
return <></>
}
const transformedHighlights: Highlight[] = highlights
.filter((highlight) => highlight.length !== 0)
.map((highlight) => ({
@@ -170,13 +177,79 @@ export const CodeBlock = ({
tooltipText: highlight.length >= 3 ? highlight[2] : undefined,
}))
const actionsProps: Omit<CodeBlockActionsProps, "inHeader"> = {
source,
canShowApiTesting,
onApiTesting: setShowTesting,
blockStyle,
noReport,
noCopy,
const getLines = (
tokens: Token[][],
highlightProps: CollapsibleHighlightProps,
lineNumberOffset = 0
) =>
tokens.map((line, i) => {
const offsettedLineNumber = i + lineNumberOffset
const highlightedLines = transformedHighlights.filter(
(highlight) => highlight.line - 1 === offsettedLineNumber
)
return (
<CodeBlockLine
line={line}
lineNumber={offsettedLineNumber}
highlights={highlightedLines}
showLineNumber={!noLineNumbers && tokens.length > 1}
key={offsettedLineNumber}
lineNumberColorClassName={lineNumbersColor}
lineNumberBgClassName={innerBgColor}
{...highlightProps}
/>
)
})
const {
getCollapsedLinesElm,
getNonCollapsedLinesElm,
type: collapsibleType,
...collapsibleResult
} = useCollapsibleCodeLines({
collapsibleLinesStr: collapsibleLines,
getLines,
})
useEffect(() => {
if (!codeContainerRef.current || !codeRef.current) {
return
}
setScrollable(
codeContainerRef.current.scrollWidth < codeRef.current.clientWidth
)
}, [codeContainerRef.current, codeRef.current])
const actionsProps: Omit<CodeBlockActionsProps, "inHeader"> = useMemo(
() => ({
source,
canShowApiTesting,
onApiTesting: setShowTesting,
blockStyle,
noReport,
noCopy,
isCollapsed: collapsibleType !== undefined && collapsibleResult.collapsed,
inInnerCode: hasInnerCodeBlock,
showGradientBg: scrollable,
}),
[
source,
canShowApiTesting,
setShowTesting,
blockStyle,
noReport,
noCopy,
collapsibleType,
collapsibleResult,
hasInnerCodeBlock,
scrollable,
]
)
if (!source.length) {
return <></>
}
return (
@@ -233,12 +306,30 @@ export const CodeBlock = ({
tokens,
...rest
}) => (
<div className={clsx(innerBorderClasses, innerBgColor)}>
<div
className={clsx(innerBorderClasses, innerBgColor, "relative")}
ref={codeContainerRef}
>
{collapsibleType === "start" && (
<>
<CodeBlockCollapsibleButton
type={collapsibleType}
expandButtonLabel={expandButtonLabel}
className={innerBorderClasses}
{...collapsibleResult}
/>
<CodeBlockCollapsibleFade
type={collapsibleType}
collapsed={collapsibleResult.collapsed}
hasHeader={hasInnerCodeBlock}
/>
</>
)}
<pre
style={{ ...style, fontStretch: "100%" }}
className={clsx(
"relative !my-0 break-words bg-transparent !outline-none",
"overflow-auto break-words p-0",
"overflow-auto break-words p-0 pr-docs_0.25",
"rounded-docs_DEFAULT",
!hasInnerCodeBlock &&
tokens.length <= 1 &&
@@ -253,24 +344,22 @@ export const CodeBlock = ({
tokens.length > 1 && "py-docs_0.75",
tokens.length <= 1 && "!py-[6px] px-docs_0.5"
)}
ref={codeRef}
>
{tokens.map((line, i) => {
const highlightedLines = transformedHighlights.filter(
(highlight) => highlight.line - 1 === i
)
return (
<CodeBlockLine
line={line}
lineNumber={i}
highlights={highlightedLines}
showLineNumber={!noLineNumbers && tokens.length > 1}
key={i}
lineNumberColorClassName={lineNumbersColor}
{...rest}
/>
)
{collapsibleType === "start" &&
getCollapsedLinesElm({
tokens,
highlightProps: rest,
})}
{getNonCollapsedLinesElm({
tokens,
highlightProps: rest,
})}
{collapsibleType === "end" &&
getCollapsedLinesElm({
tokens,
highlightProps: rest,
})}
</code>
</pre>
{!title && (
@@ -280,6 +369,21 @@ export const CodeBlock = ({
isSingleLine={tokens.length <= 1}
/>
)}
{collapsibleType === "end" && (
<>
<CodeBlockCollapsibleFade
type={collapsibleType}
collapsed={collapsibleResult.collapsed}
hasHeader={hasInnerCodeBlock}
/>
<CodeBlockCollapsibleButton
type={collapsibleType}
expandButtonLabel={expandButtonLabel}
className={innerBorderClasses}
{...collapsibleResult}
/>
</>
)}
</div>
)}
</Highlight>