Skip to content

Commit 29e5aff

Browse files
committed
docs: add ExampleSnippet component
1 parent 9680566 commit 29e5aff

File tree

13 files changed

+536
-31
lines changed

13 files changed

+536
-31
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { useClipboard } from './useClipboard'
12
import { useColorModes } from './useColorModes'
23
import { useForkedRef } from './useForkedRef'
34
import { usePopper } from './usePopper'
45

5-
export { useColorModes, useForkedRef, usePopper }
6+
export { useClipboard, useColorModes, useForkedRef, usePopper }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useState, useCallback } from 'react'
2+
3+
/**
4+
* useClipboard Hook
5+
*
6+
* Provides functionality to copy text to the clipboard and track the copy status.
7+
*
8+
* @returns An object containing the copy function, copy status, and any error encountered.
9+
*/
10+
export const useClipboard = () => {
11+
const [isCopied, setIsCopied] = useState<boolean>(false)
12+
const [error, setError] = useState<Error | null>(null)
13+
14+
/**
15+
* Copies the provided text to the clipboard.
16+
*
17+
* @param text - The text to be copied to the clipboard.
18+
*/
19+
const copy = useCallback(async (text: string) => {
20+
if (!navigator?.clipboard) {
21+
setError(new Error('Clipboard API is not available'))
22+
return
23+
}
24+
25+
try {
26+
await navigator.clipboard.writeText(text)
27+
setIsCopied(true)
28+
setError(null)
29+
// Reset the isCopied state after 2 seconds
30+
setTimeout(() => setIsCopied(false), 2000)
31+
} catch (_error) {
32+
setError(_error as Error)
33+
setIsCopied(false)
34+
}
35+
}, [])
36+
37+
return { copy, isCopied, error }
38+
}

packages/docs/gatsby-node.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const createPages = async ({
6868
context: {
6969
id: node.id,
7070
route: node.frontmatter.route,
71+
regex: `/^${node.frontmatter.route}/`,
7172
},
7273
})
7374
})

packages/docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"author": "The CoreUI Team (https://github.com/orgs/coreui/people)",
1616
"scripts": {
1717
"api": "rimraf \"content/api/*\" & node build/api.mjs",
18-
"build": "gatsby build",
18+
"build": "gatsby build --prefix-paths",
1919
"develop": "gatsby develop",
2020
"dist": "run-s api build",
2121
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
@@ -33,6 +33,7 @@
3333
"@docsearch/css": "^3.6.2",
3434
"@mdx-js/mdx": "^3.1.0",
3535
"@mdx-js/react": "^3.1.0",
36+
"@stackblitz/sdk": "^1.11.0",
3637
"@types/react-helmet": "^6.1.11",
3738
"gatsby": "^5.13.7",
3839
"gatsby-plugin-google-tagmanager": "^5.13.1",
@@ -58,7 +59,6 @@
5859
"react-helmet": "^6.1.0",
5960
"react-imask": "^7.6.1",
6061
"react-markdown": "^9.0.1",
61-
"remark-mdx": "^1.6.22",
6262
"rimraf": "^6.0.1",
6363
"sass": "^1.80.4"
6464
},
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React, { FC, ReactNode, useState } from 'react'
2+
import { Highlight, Language } from 'prism-react-renderer'
3+
4+
import CIcon from '@coreui/icons-react'
5+
import { cibCodesandbox, cilCheckAlt, cilCopy } from '@coreui/icons'
6+
import { CNav, CNavLink, CTooltip, useClipboard } from '@coreui/react'
7+
8+
import { openStackBlitzProject } from '../utils/stackblitz'
9+
import { openCodeSandboxProject } from '../utils/codesandbox'
10+
11+
interface CodeSnippets {
12+
js?: string
13+
ts?: string
14+
}
15+
16+
interface ExampleSnippetProps {
17+
children: ReactNode
18+
className?: string
19+
code: string | CodeSnippets
20+
codeSandbox?: boolean
21+
componentName?: string
22+
stackBlitz?: boolean
23+
}
24+
25+
const ExampleSnippet: FC<ExampleSnippetProps> = ({
26+
children,
27+
className = '',
28+
code,
29+
codeSandbox = true,
30+
componentName,
31+
stackBlitz = true,
32+
}) => {
33+
const [language, setLanguage] = useState<'js' | 'ts'>('js')
34+
const { copy, isCopied } = useClipboard()
35+
36+
// Type Guards to determine the shape of 'code' prop
37+
const isCodeString = typeof code === 'string'
38+
const codeJS = isCodeString ? code : code.js || code.ts
39+
const codeTS = isCodeString ? code : code.ts
40+
const hasJS = Boolean(codeJS)
41+
const hasTS = Boolean(codeTS)
42+
43+
// Set initial language based on available code snippets
44+
React.useEffect(() => {
45+
if (!hasJS && hasTS) {
46+
setLanguage('ts')
47+
} else {
48+
setLanguage('js')
49+
}
50+
}, [hasJS, hasTS])
51+
52+
const handleCopy = () => {
53+
const codeToCopy = language === 'js' ? codeJS : codeTS
54+
if (codeToCopy) {
55+
copy(codeToCopy)
56+
}
57+
}
58+
59+
const prismLanguage: Language = language === 'js' ? 'jsx' : 'tsx'
60+
61+
// Determine if both languages are available
62+
const showJSTab = hasJS && (isCodeString || code.js !== code.ts)
63+
const showTSTab = hasTS
64+
65+
return (
66+
<div className="docs-example-snippet">
67+
{children && <div className={`docs-example ${className}`}>{children}</div>}
68+
<div className="highlight-toolbar border-top">
69+
<CNav className="px-3" variant="underline-border">
70+
{showJSTab && (
71+
<CNavLink as="button" active={language === 'js'} onClick={() => setLanguage('js')}>
72+
JavaScript
73+
</CNavLink>
74+
)}
75+
{showTSTab && (
76+
<CNavLink as="button" active={language === 'ts'} onClick={() => setLanguage('ts')}>
77+
TypeScript
78+
</CNavLink>
79+
)}
80+
<span className="ms-auto"></span>
81+
{codeSandbox && (
82+
<CTooltip content="Try it on CodeSandbox">
83+
<button
84+
type="button"
85+
className="btn btn-transparent"
86+
aria-label="Try it on CodeSandbox"
87+
onClick={() =>
88+
openCodeSandboxProject({
89+
name: React.isValidElement(children) && (children as any).type?.name,
90+
language: language,
91+
code: language === 'js' ? codeJS : codeTS || '',
92+
componentName,
93+
})
94+
}
95+
disabled={language === 'ts' && !hasTS}
96+
>
97+
<CIcon icon={cibCodesandbox} />
98+
</button>
99+
</CTooltip>
100+
)}
101+
{stackBlitz && (
102+
<CTooltip content="Try it on StackBlitz">
103+
<button
104+
type="button"
105+
className="btn btn-transparent px-1"
106+
aria-label="Try it on StackBlitz"
107+
onClick={() =>
108+
openStackBlitzProject({
109+
name: React.isValidElement(children) && (children as any).type?.name,
110+
language: language,
111+
code: language === 'js' ? codeJS : codeTS || '',
112+
componentName,
113+
})
114+
}
115+
disabled={language === 'ts' && !hasTS}
116+
>
117+
<svg
118+
className="icon"
119+
width="56"
120+
height="78"
121+
viewBox="0 0 56 78"
122+
fill="none"
123+
xmlns="http://www.w3.org/2000/svg"
124+
>
125+
<path
126+
d="M23.4273 48.2853C23.7931 47.5845 23.0614 46.8837 22.3298 46.8837H1.11228C0.0148224 46.8837 -0.350997 45.8326 0.380642 45.1318L40.9866 0.282084C41.7182 -0.418693 43.1815 0.282084 42.8157 1.33325L32.9386 30.0651C32.5727 30.7659 32.9386 31.4666 33.6702 31.4666H54.8877C55.9852 31.4666 56.351 32.5178 55.6194 33.2186L15.0134 77.7179C14.2818 78.4187 12.8185 77.7179 13.1843 76.6667L23.4273 48.2853Z"
127+
fill="currentColor"
128+
/>
129+
</svg>
130+
</button>
131+
</CTooltip>
132+
)}
133+
<CTooltip content={isCopied ? 'Copied' : 'Copy to clipboard'}>
134+
<button
135+
type="button"
136+
className="btn btn-transparent px-1"
137+
aria-label="Copy to clipboard"
138+
onClick={handleCopy}
139+
disabled={(language === 'js' && !hasJS) || (language === 'ts' && !hasTS)}
140+
>
141+
<CIcon icon={isCopied ? cilCheckAlt : cilCopy} />
142+
</button>
143+
</CTooltip>
144+
</CNav>
145+
</div>
146+
147+
<div className="highlight">
148+
<Highlight
149+
code={language === 'js' ? codeJS : codeTS || ''}
150+
language={prismLanguage}
151+
theme={{ plain: {}, styles: [] }}
152+
>
153+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
154+
<pre className={className} style={style}>
155+
{tokens.map((line, i) => (
156+
<div {...getLineProps({ line, key: i })} key={i}>
157+
{line.map((token, key) => (
158+
<span {...getTokenProps({ token, key })} key={key} />
159+
))}
160+
</div>
161+
))}
162+
</pre>
163+
)}
164+
</Highlight>
165+
</div>
166+
</div>
167+
)
168+
}
169+
170+
ExampleSnippet.displayName = 'ExampleSnippet'
171+
172+
export default ExampleSnippet

packages/docs/src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Callout from './Callout'
44
import CodeBlock from './CodeBlock'
55
import ClassNamesDocs from './ClassNamesDocs'
66
import Example from './Example'
7+
import ExampleSnippet from './ExampleSnippet'
78
import Footer from './Footer'
89
import Header from './Header'
910
import JSXDocs from './JSXDocs'
@@ -20,6 +21,7 @@ export {
2021
CodeBlock,
2122
ClassNamesDocs,
2223
Example,
24+
ExampleSnippet,
2325
Footer,
2426
Header,
2527
JSXDocs,

packages/docs/src/pages/404.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react'
22
import { graphql, useStaticQuery } from 'gatsby'
33
import { CButton } from '@coreui/react/src/index'
44

5-
import Seo from './../components/Seo'
5+
import Seo from '../components/Seo'
66

77
const NotFoundPage = () => {
88
const { site } = useStaticQuery(query)

packages/docs/src/styles/_component-examples.scss

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,39 @@
2727
.docs-example-snippet {
2828
border: solid var(--cui-border-color);
2929
border-width: 1px 0;
30+
margin: 0 ($cd-gutter-x * -.5) 1rem ($cd-gutter-x * -.5);
31+
padding: 0;
32+
@include border-radius(0);
3033

31-
@include media-breakpoint-up(md) {
34+
.docs-example {
35+
margin: 0;
36+
padding: 1rem;
37+
border-width: 0 1px 0 0;
38+
}
39+
40+
.highlight-toolbar {
41+
border-top: 1px solid var(--cui-border-color);
42+
}
43+
44+
.highlight {
45+
margin: 0;
46+
padding: 1rem;
47+
}
48+
49+
.docs-example,
50+
.highlight {
51+
border: 0
52+
}
53+
54+
.highlight {
55+
margin-bottom: 0;
56+
@include border-top-radius(0);
57+
}
58+
59+
@include media-breakpoint-up(sm) {
60+
margin: 0 0 1rem 0;
3261
border-width: 1px;
62+
@include border-radius(var(--cui-border-radius));
3363
}
3464
}
3565

packages/docs/src/templates/DocsLayout.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,13 @@ const DocsLayout: FC<DocsLayoutProps> = ({ children, data, location, pageContext
3232
const frameworks = other_frameworks ? other_frameworks.split(', ') : false
3333
const otherFrameworks = JSON.parse(JSON.stringify(jsonData))
3434

35-
const hasNav = data.allMdx.edges.length > 1
36-
const hasNavAccesibility = data.allMdx.edges.filter((edge: any) =>
37-
edge.node.frontmatter.title.includes('Accesibility'),
38-
).length
39-
const hasNavAPI = data.allMdx.edges.filter((edge: any) =>
40-
edge.node.frontmatter.title.includes('API'),
41-
).length
42-
const hasNavCustomizing = data.allMdx.edges.filter((edge: any) =>
43-
edge.node.frontmatter.title.includes('Customizing'),
44-
).length
35+
const hasNav = data?.allMdx?.edges.length > 1
36+
const hasNavAccesibility =
37+
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('accesibility'))
38+
const hasNavAPI =
39+
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('api'))
40+
const hasNavCustomizing =
41+
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('customizing'))
4542

4643
return (
4744
<>
@@ -52,7 +49,7 @@ const DocsLayout: FC<DocsLayoutProps> = ({ children, data, location, pageContext
5249
<CNav className="ms-lg-4 docs-nav bg-body" variant="underline-border">
5350
<CNavItem>
5451
<CNavLink href={`${route}`} active={route === location.pathname}>
55-
Overview
52+
Features
5653
</CNavLink>
5754
</CNavItem>
5855
{hasNavAPI && (

0 commit comments

Comments
 (0)