Skip to content

Commit

Permalink
feat #299 - Code & Tabs components enhancements (#301)
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-dassana authored Apr 22, 2021
1 parent 05a7ece commit 9aac591
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 319 deletions.
560 changes: 299 additions & 261 deletions src/__snapshots__/storybook.test.ts.snap

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions src/components/Code/CodeControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import cn from 'classnames'
import { createUseStyles } from 'react-jss'
import { faCopy } from '@fortawesome/pro-regular-svg-icons'
import { generateThemedControlsStyles } from './utils'
import { IconButton } from 'components/IconButton'
import { Tooltip } from 'components/Tooltip'
import React, { FC } from 'react'
import { styleguide, ThemeType } from 'components/assets/styles'

const { borderRadius, spacing } = styleguide

const { light, dark } = ThemeType

const useStyles = createUseStyles({
codeControls: {
...generateThemedControlsStyles(light),
borderRadius,
padding: `${spacing.xs}px ${spacing.s}px`,
position: 'absolute',
right: spacing['s+'],
top: spacing['s+'],
zIndex: 1
},
iconButton: {
padding: {
left: spacing.s,
right: spacing.s
}
},
// eslint-disable-next-line sort-keys
'@global': {
[`.${dark}`]: {
'& $codeControls': generateThemedControlsStyles(dark)
}
}
})

// This is left as an object so more controls (e.g. download) can be added later
export interface DisplayCodeControls {
copy?: boolean
}

interface Props {
classes?: string[]
displayControls?: DisplayCodeControls
isCopied?: boolean
onClickCopyCode: () => void
}

export const CodeControls: FC<Props> = ({
classes = [],
displayControls = {},
isCopied = false,
onClickCopyCode
}: Props) => {
const compClasses = useStyles()

const { copy = true } = displayControls

return (
<div className={cn(compClasses.codeControls, classes)}>
{copy && (
<Tooltip placement='top' title={isCopied ? 'Copied!' : 'Copy'}>
<IconButton
classes={[compClasses.iconButton]}
icon={faCopy}
onClick={onClickCopyCode}
size={14}
/>
</Tooltip>
)}
</div>
)
}
49 changes: 36 additions & 13 deletions src/components/Code/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import './prism.css'
import cn from 'classnames'
import Mark from 'mark.js'
import Prism from 'prismjs'
import { useStyles } from './utils'
import { CodeControls, DisplayCodeControls } from './CodeControls'
import { CodeType, copyToClipboard, stringifyCode, useStyles } from './utils'
import { Input, InputProps } from 'components/Input'
import React, { ChangeEvent, FC, useEffect, useRef } from 'react'
import React, { ChangeEvent, FC, useEffect, useRef, useState } from 'react'

require('prismjs/plugins/line-numbers/prism-line-numbers')
require('prismjs/components/prism-json')

export interface CodeProps {
classes?: string[]
code: string | Record<string, any>
code: CodeType
displayControls?: DisplayCodeControls | false
language?: 'css' | 'html' | 'javascript' | 'json'
lineNumbers?: boolean
readOnly?: boolean
Expand All @@ -22,6 +25,7 @@ export interface CodeProps {
export const Code: FC<CodeProps> = ({
classes = [],
code,
displayControls = {},
language = 'json',
lineNumbers = true,
readOnly = true,
Expand All @@ -30,6 +34,16 @@ export const Code: FC<CodeProps> = ({
const compClasses = useStyles()
const codeRef = useRef<HTMLElement>(null)

const [isCopied, setIsCopied] = useState(false)

const copyCode = () => {
copyToClipboard(stringifyCode(code), () => setIsCopied(true))
}

useEffect(() => {
if (isCopied) setTimeout(() => setIsCopied(false), 1250)
}, [isCopied])

useEffect(() => {
/**
* We want to highlight the code after it is rendered onto the page,
Expand Down Expand Up @@ -71,16 +85,25 @@ export const Code: FC<CodeProps> = ({
return (
<div className={cn(classes)}>
{search && <Input {...searchProps} onChange={onSearch} />}
<pre
className={cn({ 'line-numbers': lineNumbers })}
contentEditable={!readOnly}
>
<code className={`language-${language}`} ref={codeRef}>
{typeof code === 'string'
? code
: JSON.stringify(code, null, '\t')}
</code>
</pre>
<div className={compClasses.wrapper}>
{displayControls && (
<CodeControls
classes={[compClasses.controls]}
isCopied={isCopied}
onClickCopyCode={copyCode}
/>
)}
<pre
className={cn({
'line-numbers': lineNumbers
})}
contentEditable={!readOnly}
>
<code className={`language-${language}`} ref={codeRef}>
{stringifyCode(code)}
</code>
</pre>
</div>
</div>
)
}
115 changes: 93 additions & 22 deletions src/components/Code/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { createUseStyles } from 'react-jss'
import { styleguide, themedStyles, ThemeType } from 'components/assets/styles'
import { isObject } from 'lodash'
import { ColorManipulationTypes, manipulateColor } from 'components/utils'
import {
styleguide,
themedStyles,
themes,
ThemeType
} from 'components/assets/styles'

const { dark, light } = ThemeType

const {
colors: { blacks, grays },
colors: { blacks, grays, greens, reds, oranges },
font,
fontWeight,
spacing
} = styleguide

interface CopyToClipboard {
(str: string, callback?: () => void): void
}
export const copyToClipboard: CopyToClipboard = (str, callback) =>
navigator.clipboard.writeText(str).then(callback)

const codePalette = {
[dark]: {
background: blacks['darken-20']
Expand All @@ -19,6 +32,13 @@ const codePalette = {
}
}

export type CodeType = string | Record<string, any>

export const stringifyCode = (code: CodeType): string =>
isObject(code) ? JSON.stringify(code, null, '\t') : code

/* -x-x-x-x-x-x-x-x- Styles Related -x-x-x-x-x-x-x-x- */

const generateThemedCodeStyles = (themeType: ThemeType) => {
const { background } = codePalette[themeType]

Expand All @@ -32,34 +52,74 @@ const generateThemedCodeStyles = (themeType: ThemeType) => {
}
}

export const generateThemedControlsStyles = (themeType: ThemeType) => {
const {
base: { borderColor }
} = themedStyles[themeType]

return {
background: themes[themeType].background.secondary,
border: `1px solid ${borderColor}`
}
}

/* -------------- Prism Colors -------------- */

const darkCommonColor = themedStyles[dark].base.color
const lightCommonColor = themedStyles[light].base.color

const { shade } = ColorManipulationTypes

const prismColors = {
[dark]: {
boolean: reds.base,
char: darkCommonColor,
className: darkCommonColor,
comment: darkCommonColor,
function: darkCommonColor,
keyword: darkCommonColor,
lineHighlight: darkCommonColor,
method: darkCommonColor,
number: reds.base,
operator: darkCommonColor,
primitive: darkCommonColor,
property: oranges.base,
punctuation: darkCommonColor,
string: manipulateColor(greens.base, 20, shade),
tag: darkCommonColor,
variable: darkCommonColor
},
[light]: {
boolean: oranges.base,
char: lightCommonColor,
className: lightCommonColor,
comment: lightCommonColor,
function: lightCommonColor,
keyword: lightCommonColor,
lineHighlight: lightCommonColor,
method: lightCommonColor,
number: oranges.base,
operator: lightCommonColor,
primitive: lightCommonColor,
property: reds.base,
punctuation: lightCommonColor,
string: manipulateColor(greens.base, 10, shade),
tag: lightCommonColor,
variable: lightCommonColor
}
}

/* ------------------------------------------ */

const generateThemedPreCodeStyles = (themeType: ThemeType) => {
const { background } = codePalette[themeType]

const {
base: { color }
} = themedStyles[themeType]

const prismColors = {
boolean: color,
char: color,
className: color,
comment: color,
function: color,
keyword: color,
lineHighlight: color,
method: color,
number: color,
operator: color,
primitive: color,
property: color,
punctuation: color,
string: color,
tag: color,
variable: color
}

return {
...Object.entries(prismColors).reduce(
...Object.entries(prismColors[themeType]).reduce(
(acc, [key, val]) => ({
...acc,
[`& .token.${key}`]: {
Expand Down Expand Up @@ -96,6 +156,8 @@ const preCodeSelector = "pre[class*='language-']"
/* eslint-enable quotes */

export const useStyles = createUseStyles({
controls: { opacity: 0 },
// eslint-disable-next-line sort-keys
'@global': {
[preCodeSelector]: {
'& .line-numbers-rows': {
Expand Down Expand Up @@ -125,5 +187,14 @@ export const useStyles = createUseStyles({
},
search: {
marginBottom: spacing.m
},
wrapper: {
'&:hover': {
'& $controls': { opacity: 1 }
},
height: '100%',
overflow: 'auto',
position: 'relative',
width: '100%'
}
})
45 changes: 45 additions & 0 deletions src/components/Tabs/TabPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import cn from 'classnames'
import { createUseStyles } from 'react-jss'
import { motion } from 'framer-motion'
import { styleguide } from 'components/assets/styles'
import { TabConfig } from '.'
import React, { FC } from 'react'

const { font, spacing } = styleguide

const useStyles = createUseStyles({
tabPane: {
...font.body,
display: ({ isActive }) => (isActive ? 'block' : 'none'),
padding: `${spacing.m}px ${spacing.l}px`
}
})

interface TabPaneProps {
isActive: boolean
tabConfigItem: TabConfig
}

const [ACTIVE, INACTIVE] = ['active', 'inactive']

const TabPane: FC<TabPaneProps> = ({
isActive,
tabConfigItem: { classes = [], render }
}: TabPaneProps) => {
const compClasses = useStyles({ isActive })

return (
<motion.section
animate={isActive ? ACTIVE : INACTIVE}
transition={{ duration: 0.5 }}
variants={{
[ACTIVE]: { opacity: 1 },
[INACTIVE]: { opacity: 0 }
}}
>
<div className={cn(compClasses.tabPane, classes)}>{render()}</div>
</motion.section>
)
}

export default TabPane
3 changes: 3 additions & 0 deletions src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
tabConfig: { control: { disable: true } }
},
component: Tabs,
parameters: {
storyshots: { disable: true }
},
title: 'Tabs'
} as Meta

Expand Down
Loading

0 comments on commit 9aac591

Please sign in to comment.