Skip to content

Commit

Permalink
docs: add ExampleSnippet component
Browse files Browse the repository at this point in the history
  • Loading branch information
mrholek committed Nov 30, 2024
1 parent 9680566 commit 29e5aff
Show file tree
Hide file tree
Showing 13 changed files with 536 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/coreui-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useClipboard } from './useClipboard'
import { useColorModes } from './useColorModes'
import { useForkedRef } from './useForkedRef'
import { usePopper } from './usePopper'

export { useColorModes, useForkedRef, usePopper }
export { useClipboard, useColorModes, useForkedRef, usePopper }
38 changes: 38 additions & 0 deletions packages/coreui-react/src/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState, useCallback } from 'react'

/**
* useClipboard Hook
*
* Provides functionality to copy text to the clipboard and track the copy status.
*
* @returns An object containing the copy function, copy status, and any error encountered.
*/
export const useClipboard = () => {
const [isCopied, setIsCopied] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)

/**
* Copies the provided text to the clipboard.
*
* @param text - The text to be copied to the clipboard.
*/
const copy = useCallback(async (text: string) => {
if (!navigator?.clipboard) {
setError(new Error('Clipboard API is not available'))
return
}

try {
await navigator.clipboard.writeText(text)
setIsCopied(true)
setError(null)
// Reset the isCopied state after 2 seconds
setTimeout(() => setIsCopied(false), 2000)
} catch (_error) {
setError(_error as Error)
setIsCopied(false)
}
}, [])

return { copy, isCopied, error }
}
1 change: 1 addition & 0 deletions packages/docs/gatsby-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const createPages = async ({
context: {
id: node.id,
route: node.frontmatter.route,
regex: `/^${node.frontmatter.route}/`,
},
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"author": "The CoreUI Team (https://github.com/orgs/coreui/people)",
"scripts": {
"api": "rimraf \"content/api/*\" & node build/api.mjs",
"build": "gatsby build",
"build": "gatsby build --prefix-paths",
"develop": "gatsby develop",
"dist": "run-s api build",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
Expand All @@ -33,6 +33,7 @@
"@docsearch/css": "^3.6.2",
"@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@stackblitz/sdk": "^1.11.0",
"@types/react-helmet": "^6.1.11",
"gatsby": "^5.13.7",
"gatsby-plugin-google-tagmanager": "^5.13.1",
Expand All @@ -58,7 +59,6 @@
"react-helmet": "^6.1.0",
"react-imask": "^7.6.1",
"react-markdown": "^9.0.1",
"remark-mdx": "^1.6.22",
"rimraf": "^6.0.1",
"sass": "^1.80.4"
},
Expand Down
172 changes: 172 additions & 0 deletions packages/docs/src/components/ExampleSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { FC, ReactNode, useState } from 'react'
import { Highlight, Language } from 'prism-react-renderer'

import CIcon from '@coreui/icons-react'
import { cibCodesandbox, cilCheckAlt, cilCopy } from '@coreui/icons'
import { CNav, CNavLink, CTooltip, useClipboard } from '@coreui/react'

import { openStackBlitzProject } from '../utils/stackblitz'
import { openCodeSandboxProject } from '../utils/codesandbox'

interface CodeSnippets {
js?: string
ts?: string
}

interface ExampleSnippetProps {
children: ReactNode
className?: string
code: string | CodeSnippets
codeSandbox?: boolean
componentName?: string
stackBlitz?: boolean
}

const ExampleSnippet: FC<ExampleSnippetProps> = ({
children,
className = '',
code,
codeSandbox = true,
componentName,
stackBlitz = true,
}) => {
const [language, setLanguage] = useState<'js' | 'ts'>('js')
const { copy, isCopied } = useClipboard()

// Type Guards to determine the shape of 'code' prop
const isCodeString = typeof code === 'string'
const codeJS = isCodeString ? code : code.js || code.ts
const codeTS = isCodeString ? code : code.ts
const hasJS = Boolean(codeJS)
const hasTS = Boolean(codeTS)

// Set initial language based on available code snippets
React.useEffect(() => {
if (!hasJS && hasTS) {
setLanguage('ts')
} else {
setLanguage('js')
}
}, [hasJS, hasTS])

const handleCopy = () => {
const codeToCopy = language === 'js' ? codeJS : codeTS
if (codeToCopy) {
copy(codeToCopy)
}
}

const prismLanguage: Language = language === 'js' ? 'jsx' : 'tsx'

// Determine if both languages are available
const showJSTab = hasJS && (isCodeString || code.js !== code.ts)
const showTSTab = hasTS

return (
<div className="docs-example-snippet">
{children && <div className={`docs-example ${className}`}>{children}</div>}
<div className="highlight-toolbar border-top">
<CNav className="px-3" variant="underline-border">
{showJSTab && (
<CNavLink as="button" active={language === 'js'} onClick={() => setLanguage('js')}>
JavaScript
</CNavLink>
)}
{showTSTab && (
<CNavLink as="button" active={language === 'ts'} onClick={() => setLanguage('ts')}>
TypeScript
</CNavLink>
)}
<span className="ms-auto"></span>
{codeSandbox && (
<CTooltip content="Try it on CodeSandbox">
<button
type="button"
className="btn btn-transparent"
aria-label="Try it on CodeSandbox"
onClick={() =>
openCodeSandboxProject({
name: React.isValidElement(children) && (children as any).type?.name,
language: language,
code: language === 'js' ? codeJS : codeTS || '',
componentName,
})
}
disabled={language === 'ts' && !hasTS}
>
<CIcon icon={cibCodesandbox} />
</button>
</CTooltip>
)}
{stackBlitz && (
<CTooltip content="Try it on StackBlitz">
<button
type="button"
className="btn btn-transparent px-1"
aria-label="Try it on StackBlitz"
onClick={() =>
openStackBlitzProject({
name: React.isValidElement(children) && (children as any).type?.name,
language: language,
code: language === 'js' ? codeJS : codeTS || '',
componentName,
})
}
disabled={language === 'ts' && !hasTS}
>
<svg
className="icon"
width="56"
height="78"
viewBox="0 0 56 78"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="currentColor"
/>
</svg>
</button>
</CTooltip>
)}
<CTooltip content={isCopied ? 'Copied' : 'Copy to clipboard'}>
<button
type="button"
className="btn btn-transparent px-1"
aria-label="Copy to clipboard"
onClick={handleCopy}
disabled={(language === 'js' && !hasJS) || (language === 'ts' && !hasTS)}
>
<CIcon icon={isCopied ? cilCheckAlt : cilCopy} />
</button>
</CTooltip>
</CNav>
</div>

<div className="highlight">
<Highlight
code={language === 'js' ? codeJS : codeTS || ''}
language={prismLanguage}
theme={{ plain: {}, styles: [] }}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })} key={i}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} key={key} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
</div>
)
}

ExampleSnippet.displayName = 'ExampleSnippet'

export default ExampleSnippet
2 changes: 2 additions & 0 deletions packages/docs/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Callout from './Callout'
import CodeBlock from './CodeBlock'
import ClassNamesDocs from './ClassNamesDocs'
import Example from './Example'
import ExampleSnippet from './ExampleSnippet'
import Footer from './Footer'
import Header from './Header'
import JSXDocs from './JSXDocs'
Expand All @@ -20,6 +21,7 @@ export {
CodeBlock,
ClassNamesDocs,
Example,
ExampleSnippet,
Footer,
Header,
JSXDocs,
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/pages/404.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { graphql, useStaticQuery } from 'gatsby'
import { CButton } from '@coreui/react/src/index'

import Seo from './../components/Seo'
import Seo from '../components/Seo'

const NotFoundPage = () => {
const { site } = useStaticQuery(query)
Expand Down
32 changes: 31 additions & 1 deletion packages/docs/src/styles/_component-examples.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,39 @@
.docs-example-snippet {
border: solid var(--cui-border-color);
border-width: 1px 0;
margin: 0 ($cd-gutter-x * -.5) 1rem ($cd-gutter-x * -.5);
padding: 0;
@include border-radius(0);

@include media-breakpoint-up(md) {
.docs-example {
margin: 0;
padding: 1rem;
border-width: 0 1px 0 0;
}

.highlight-toolbar {
border-top: 1px solid var(--cui-border-color);
}

.highlight {
margin: 0;
padding: 1rem;
}

.docs-example,
.highlight {
border: 0
}

.highlight {
margin-bottom: 0;
@include border-top-radius(0);
}

@include media-breakpoint-up(sm) {
margin: 0 0 1rem 0;
border-width: 1px;
@include border-radius(var(--cui-border-radius));
}
}

Expand Down
19 changes: 8 additions & 11 deletions packages/docs/src/templates/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,13 @@ const DocsLayout: FC<DocsLayoutProps> = ({ children, data, location, pageContext
const frameworks = other_frameworks ? other_frameworks.split(', ') : false
const otherFrameworks = JSON.parse(JSON.stringify(jsonData))

const hasNav = data.allMdx.edges.length > 1
const hasNavAccesibility = data.allMdx.edges.filter((edge: any) =>
edge.node.frontmatter.title.includes('Accesibility'),
).length
const hasNavAPI = data.allMdx.edges.filter((edge: any) =>
edge.node.frontmatter.title.includes('API'),
).length
const hasNavCustomizing = data.allMdx.edges.filter((edge: any) =>
edge.node.frontmatter.title.includes('Customizing'),
).length
const hasNav = data?.allMdx?.edges.length > 1
const hasNavAccesibility =
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('accesibility'))
const hasNavAPI =
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('api'))
const hasNavCustomizing =
hasNav && data.allMdx.edges.some((edge: any) => edge.node.fields.slug.includes('customizing'))

return (
<>
Expand All @@ -52,7 +49,7 @@ const DocsLayout: FC<DocsLayoutProps> = ({ children, data, location, pageContext
<CNav className="ms-lg-4 docs-nav bg-body" variant="underline-border">
<CNavItem>
<CNavLink href={`${route}`} active={route === location.pathname}>
Overview
Features
</CNavLink>
</CNavItem>
{hasNavAPI && (
Expand Down
Loading

0 comments on commit 29e5aff

Please sign in to comment.