You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. 'use client'
  2. import {
  3. Children,
  4. createContext,
  5. useContext,
  6. useEffect,
  7. useRef,
  8. useState,
  9. } from 'react'
  10. import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
  11. import { Tag } from './tag'
  12. import classNames from '@/utils/classnames'
  13. import { writeTextToClipboard } from '@/utils/clipboard'
  14. import type { PropsWithChildren, ReactElement, ReactNode } from 'react'
  15. type IChildrenProps = {
  16. children: React.ReactNode
  17. [key: string]: any
  18. }
  19. function ClipboardIcon(props: any) {
  20. return (
  21. <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
  22. <path
  23. strokeWidth="0"
  24. d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
  25. />
  26. <path
  27. fill="none"
  28. strokeLinejoin="round"
  29. d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
  30. />
  31. </svg>
  32. )
  33. }
  34. function CopyButton({ code }: { code: string }) {
  35. const [copyCount, setCopyCount] = useState(0)
  36. const copied = copyCount > 0
  37. useEffect(() => {
  38. if (copyCount > 0) {
  39. const timeout = setTimeout(() => setCopyCount(0), 1000)
  40. return () => {
  41. clearTimeout(timeout)
  42. }
  43. }
  44. }, [copyCount])
  45. return (
  46. <button
  47. type="button"
  48. className={classNames(
  49. 'group/button absolute right-4 top-1.5 overflow-hidden rounded-full py-1 pl-2 pr-3 text-2xs font-medium opacity-0 backdrop-blur transition focus:opacity-100 group-hover:opacity-100',
  50. copied
  51. ? 'bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20'
  52. : 'hover:bg-white/7.5 dark:bg-white/2.5 bg-white/5 dark:hover:bg-white/5',
  53. )}
  54. onClick={() => {
  55. writeTextToClipboard(code).then(() => {
  56. setCopyCount(count => count + 1)
  57. })
  58. }}
  59. >
  60. <span
  61. aria-hidden={copied}
  62. className={classNames(
  63. 'pointer-events-none flex items-center gap-0.5 text-zinc-400 transition duration-300',
  64. copied && '-translate-y-1.5 opacity-0',
  65. )}
  66. >
  67. <ClipboardIcon className="h-5 w-5 fill-zinc-500/20 stroke-zinc-500 transition-colors group-hover/button:stroke-zinc-400" />
  68. Copy
  69. </span>
  70. <span
  71. aria-hidden={!copied}
  72. className={classNames(
  73. 'pointer-events-none absolute inset-0 flex items-center justify-center text-emerald-400 transition duration-300',
  74. !copied && 'translate-y-1.5 opacity-0',
  75. )}
  76. >
  77. Copied!
  78. </span>
  79. </button>
  80. )
  81. }
  82. function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) {
  83. if (!tag && !label)
  84. return null
  85. return (
  86. <div className="border-b-white/7.5 bg-white/2.5 dark:bg-white/1 flex h-9 items-center gap-2 border-y border-t-transparent bg-zinc-900 px-4 dark:border-b-white/5">
  87. {tag && (
  88. <div className="dark flex">
  89. <Tag variant="small">{tag}</Tag>
  90. </div>
  91. )}
  92. {tag && label && (
  93. <span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
  94. )}
  95. {label && (
  96. <span className="font-mono text-xs text-zinc-400">{label}</span>
  97. )}
  98. </div>
  99. )
  100. }
  101. type CodeExample = {
  102. title?: string
  103. tag?: string
  104. code: string
  105. }
  106. type ICodePanelProps = {
  107. children?: React.ReactNode
  108. tag?: string
  109. label?: string
  110. code?: string
  111. title?: string
  112. targetCode?: CodeExample
  113. }
  114. function CodePanel({ tag, label, children, targetCode }: ICodePanelProps) {
  115. const child = Children.toArray(children)[0] as ReactElement<any>
  116. return (
  117. <div className="dark:bg-white/2.5 group">
  118. <CodePanelHeader
  119. tag={tag}
  120. label={label}
  121. />
  122. <div className="relative">
  123. {/* <pre className="p-4 overflow-x-auto text-xs text-white">{children}</pre> */}
  124. {/* <CopyButton code={child.props.code ?? code} /> */}
  125. {/* <CopyButton code={child.props.children.props.children} /> */}
  126. <pre className="overflow-x-auto p-4 text-xs text-white">
  127. {targetCode?.code ? (
  128. <code>{targetCode?.code}</code>
  129. ) : (
  130. child
  131. )}
  132. </pre>
  133. <CopyButton code={targetCode?.code ?? child.props.children.props.children} />
  134. </div>
  135. </div>
  136. )
  137. }
  138. type CodeGroupHeaderProps = {
  139. title?: string
  140. tabTitles?: string[]
  141. selectedIndex?: number
  142. }
  143. function CodeGroupHeader({ title, tabTitles, selectedIndex }: CodeGroupHeaderProps) {
  144. const hasTabs = (tabTitles?.length ?? 0) > 1
  145. return (
  146. <div className="flex min-h-[calc(theme(spacing.12)+1px)] flex-wrap items-start gap-x-4 border-b border-zinc-700 bg-zinc-800 px-4 dark:border-zinc-800 dark:bg-transparent">
  147. {title && (
  148. <h3 className="mr-auto pt-3 text-xs font-semibold text-white">
  149. {title}
  150. </h3>
  151. )}
  152. {hasTabs && (
  153. <TabList className="-mb-px flex gap-4 text-xs font-medium">
  154. {tabTitles!.map((tabTitle, tabIndex) => (
  155. <Tab
  156. key={tabIndex}
  157. className={classNames(
  158. 'border-b py-3 transition focus:[&:not(:focus-visible)]:outline-none',
  159. tabIndex === selectedIndex
  160. ? 'border-emerald-500 text-emerald-400'
  161. : 'border-transparent text-zinc-400 hover:text-zinc-300',
  162. )}
  163. >
  164. {tabTitle}
  165. </Tab>
  166. ))}
  167. </TabList>
  168. )}
  169. </div>
  170. )
  171. }
  172. type ICodeGroupPanelsProps = PropsWithChildren<{
  173. targetCode?: CodeExample[]
  174. [key: string]: any
  175. }>
  176. function CodeGroupPanels({ children, targetCode, ...props }: ICodeGroupPanelsProps) {
  177. if ((targetCode?.length ?? 0) > 1) {
  178. return (
  179. <TabPanels>
  180. {targetCode!.map(code => (
  181. <TabPanel>
  182. <CodePanel {...props} targetCode={code} />
  183. </TabPanel>
  184. ))}
  185. </TabPanels>
  186. )
  187. }
  188. return <CodePanel {...props} targetCode={targetCode?.[0]}>{children}</CodePanel>
  189. }
  190. function usePreventLayoutShift() {
  191. const positionRef = useRef<any>()
  192. const rafRef = useRef<any>()
  193. useEffect(() => {
  194. return () => {
  195. window.cancelAnimationFrame(rafRef.current)
  196. }
  197. }, [])
  198. return {
  199. positionRef,
  200. preventLayoutShift(callback: () => {}) {
  201. const initialTop = positionRef.current.getBoundingClientRect().top
  202. callback()
  203. rafRef.current = window.requestAnimationFrame(() => {
  204. const newTop = positionRef.current.getBoundingClientRect().top
  205. window.scrollBy(0, newTop - initialTop)
  206. })
  207. },
  208. }
  209. }
  210. function useTabGroupProps(availableLanguages: string[]) {
  211. const [preferredLanguages, addPreferredLanguage] = useState<any>([])
  212. const [selectedIndex, setSelectedIndex] = useState(0)
  213. const activeLanguage = [...(availableLanguages || [])].sort(
  214. (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a),
  215. )[0]
  216. const languageIndex = availableLanguages?.indexOf(activeLanguage) || 0
  217. const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
  218. if (newSelectedIndex !== selectedIndex)
  219. setSelectedIndex(newSelectedIndex)
  220. const { positionRef, preventLayoutShift } = usePreventLayoutShift()
  221. return {
  222. as: 'div',
  223. ref: positionRef,
  224. selectedIndex,
  225. onChange: (newSelectedIndex: number) => {
  226. preventLayoutShift(() =>
  227. (addPreferredLanguage(availableLanguages[newSelectedIndex]) as any),
  228. )
  229. },
  230. }
  231. }
  232. const CodeGroupContext = createContext(false)
  233. type CodeGroupProps = PropsWithChildren<{
  234. /** Code example(s) to display */
  235. targetCode?: string | CodeExample[]
  236. /** Example block title */
  237. title?: string
  238. /** HTTP method tag, e.g. GET, POST */
  239. tag?: string
  240. /** API path */
  241. label?: string
  242. }>
  243. export function CodeGroup({ children, title, targetCode, ...props }: CodeGroupProps) {
  244. const examples = typeof targetCode === 'string' ? [{ code: targetCode }] as CodeExample[] : targetCode
  245. const tabTitles = examples?.map(({ title }) => title || 'Code') || []
  246. const tabGroupProps = useTabGroupProps(tabTitles)
  247. const hasTabs = tabTitles.length > 1
  248. const Container = hasTabs ? TabGroup : 'div'
  249. const containerProps = hasTabs ? tabGroupProps : {}
  250. const headerProps = hasTabs
  251. ? { selectedIndex: tabGroupProps.selectedIndex, tabTitles }
  252. : {}
  253. return (
  254. <CodeGroupContext.Provider value={true}>
  255. <Container
  256. {...containerProps}
  257. className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"
  258. >
  259. <CodeGroupHeader title={title} {...headerProps} />
  260. <CodeGroupPanels {...props} targetCode={examples}>{children}</CodeGroupPanels>
  261. </Container>
  262. </CodeGroupContext.Provider>
  263. )
  264. }
  265. type IChildProps = {
  266. children: ReactNode
  267. [key: string]: any
  268. }
  269. export function Code({ children, ...props }: IChildProps) {
  270. return <code {...props}>{children}</code>
  271. }
  272. export function Pre({ children, ...props }: IChildrenProps) {
  273. const isGrouped = useContext(CodeGroupContext)
  274. if (isGrouped)
  275. return children
  276. return <CodeGroup {...props}>{children}</CodeGroup>
  277. }
  278. export function Embed({ value, ...props }: IChildrenProps) {
  279. return <span {...props}>{value}</span>
  280. }