diff --git a/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeBody/index.tsx b/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeBody/index.tsx new file mode 100644 index 0000000000..49643296f7 --- /dev/null +++ b/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeBody/index.tsx @@ -0,0 +1,20 @@ +import { Source } from '@storybook/blocks'; +import * as React from 'react'; + +import { DetailedCodeBodyWrapper, FloatingIndicator } from '../elements'; +import { DetailedCodeBodyProps } from '../types'; + +export const DetailedCodeBody: React.FC = ({ + code, + language, + showFloatingBadge = false, +}) => { + return ( + + + {showFloatingBadge && ( + ... + )} + + ); +}; diff --git a/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeButton/index.tsx b/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeButton/index.tsx new file mode 100644 index 0000000000..7a2c75eadd --- /dev/null +++ b/packages/styleguide/.storybook/components/Elements/DetailedCode/DetailedCodeButton/index.tsx @@ -0,0 +1,46 @@ +import { MiniChevronDownIcon } from '@codecademy/gamut-icons'; +import * as React from 'react'; +import { Anchor, Rotation, FlexBox, Text } from '@codecademy/gamut'; + +import { DetailedCodeButtonProps } from '../types'; + +export const DetailedCodeButton: React.FC = ({ + isExpanded, + setIsExpanded, + language, +}) => { + const handleClick = () => { + if (setIsExpanded) { + setIsExpanded((prev) => !prev); + } + }; + + return ( + + + {language} + + + {isExpanded ? 'Show Less Code' : 'Show More Code'} + + + + + + + + ); +}; diff --git a/packages/styleguide/.storybook/components/Elements/DetailedCode/elements.tsx b/packages/styleguide/.storybook/components/Elements/DetailedCode/elements.tsx new file mode 100644 index 0000000000..84d7e4ff5f --- /dev/null +++ b/packages/styleguide/.storybook/components/Elements/DetailedCode/elements.tsx @@ -0,0 +1,46 @@ +import { css } from '@codecademy/gamut-styles'; +import styled from '@emotion/styled'; +import { FlexBox } from '@codecademy/gamut'; + +export const DetailedCodeWrapper = styled(FlexBox)( + css({ + width: '100%', + flexDirection: 'column', + borderRadius: 'md', + border: 1, + bg: 'background', + }) +); + +export const DetailedCodeBodyWrapper = styled(FlexBox)<{ + hasFloatingBadge?: boolean; +}>(({ hasFloatingBadge }) => + css({ + position: 'relative', + flexDirection: 'column', + /* Override Storybook's Source component default styles to remove unwanted spacing and borders in the container */ + '& .docblock-source': { + borderRadius: 'none', + margin: 0, + pb: hasFloatingBadge ? 48 : 0, + }, + }) +); + +export const FloatingIndicator = styled(FlexBox)( + css({ + position: 'absolute', + bottom: 16, + left: '50%', + transform: 'translateX(-50%)', + zIndex: 1, + bg: 'inherit', + px: 12, + py: 4, + borderRadius: 'lg', + fontSize: 26, + fontWeight: 700, + color: 'white', + letterSpacing: '0.1em', + }) +); diff --git a/packages/styleguide/.storybook/components/Elements/DetailedCode/index.tsx b/packages/styleguide/.storybook/components/Elements/DetailedCode/index.tsx new file mode 100644 index 0000000000..7ec1a3e2a7 --- /dev/null +++ b/packages/styleguide/.storybook/components/Elements/DetailedCode/index.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; + +import { DetailedCodeBody } from './DetailedCodeBody'; +import { DetailedCodeButton } from './DetailedCodeButton'; +import { DetailedCodeWrapper } from './elements'; +import { DetailedCodeProps } from './types'; + +const DEFAULT_PREVIEW_LINES = 20; +const DEFAULT_LANGUAGE = 'tsx'; + +const getPreviewCode = (code: string, previewLines: number) => { + const lines = code.split('\n'); + + if (lines.length <= previewLines) { + return code; + } + + return lines.slice(0, previewLines).join('\n'); +}; + +export const DetailedCode: React.FC = ({ + code, + initiallyExpanded = false, + language = DEFAULT_LANGUAGE, + preview = false, + previewLines = DEFAULT_PREVIEW_LINES, +}) => { + const [isExpanded, setIsExpanded] = useState(initiallyExpanded); + const normalizedPreviewLines = Math.max(0, previewLines); + const previewEnabled = preview && normalizedPreviewLines > 0; + const previewCode = previewEnabled + ? getPreviewCode(code, normalizedPreviewLines) + : code; + + const hasMoreCode = + previewEnabled && code.split('\n').length > normalizedPreviewLines; + + const displayedCode = isExpanded ? code : previewCode; + + return ( + + + {hasMoreCode && ( + + )} + + ); +}; diff --git a/packages/styleguide/.storybook/components/Elements/DetailedCode/types.ts b/packages/styleguide/.storybook/components/Elements/DetailedCode/types.ts new file mode 100644 index 0000000000..a449dd70c2 --- /dev/null +++ b/packages/styleguide/.storybook/components/Elements/DetailedCode/types.ts @@ -0,0 +1,24 @@ +import { Source } from '@storybook/blocks'; +import { ComponentProps } from 'react'; + +type SourceLanguage = ComponentProps['language']; + +export interface DetailedCodeProps { + code: string; + language?: SourceLanguage; + initiallyExpanded?: boolean; + preview?: boolean; + previewLines?: number; +} + +export interface DetailedCodeButtonProps { + isExpanded: boolean; + setIsExpanded: React.Dispatch>; + language: SourceLanguage; +} + +export interface DetailedCodeBodyProps { + code: string; + language: SourceLanguage; + showFloatingBadge?: boolean; +} diff --git a/packages/styleguide/.storybook/components/index.tsx b/packages/styleguide/.storybook/components/index.tsx index 86b39cc1bb..4be3198b4a 100644 --- a/packages/styleguide/.storybook/components/index.tsx +++ b/packages/styleguide/.storybook/components/index.tsx @@ -2,6 +2,7 @@ export * from './ImageWrapper'; export * from './TokenTable'; export * from './Headers'; export * from './Elements/Callout'; +export * from './Elements/DetailedCode'; export * from './Elements/KeyboardKey'; export * from './Elements/Markdown'; export * from './Elements/ImageGallery'; diff --git a/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/ConnectedForm.mdx b/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/ConnectedForm.mdx index b71619aed5..618a0850d4 100644 --- a/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/ConnectedForm.mdx +++ b/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/ConnectedForm.mdx @@ -1,8 +1,9 @@ import { Canvas, Controls, Meta } from '@storybook/blocks'; -import { ComponentHeader, LinkTo } from '~styleguide/blocks'; +import { ComponentHeader, DetailedCode, LinkTo } from '~styleguide/blocks'; import * as ConnectedFormStories from './ConnectedForm.stories'; +import { example } from './example'; export const parameters = { title: 'ConnectedForm', @@ -40,75 +41,7 @@ This hook also returns the `FormRequiredText` component - include this before yo ### Example code -```tsx -import { - ConnectedCheckbox, - ConnectedInput, - ConnectedSelect, - useConnectedForm, -} from '@codecademy/gamut'; - -import { TerminalIcon } from '@codecademy/gamut-icons'; - -export const GoodForm = () => { - const { - ConnectedFormGroup, - ConnectedForm, - connectedFormProps, - FormRequiredText, - } = useConnectedForm({ - defaultValues: { - thisField: true, - thatField: 'zero', - anotherField: 'state your name.', - }, - validationRules: { - thisField: { required: 'you need to check this.' }, - thatField: { - pattern: { - value: /^(?:(?!zero).)*$/, - message: 'literally anything but zero', - }, - }, - }, - }); - - return ( - console.log(thisField)} - resetOnSubmit - {...connectedFormProps} - > - submit this form. - - - - - - ); -}; -``` + ## Variants diff --git a/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/example.ts b/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/example.ts new file mode 100644 index 0000000000..ec23ed9156 --- /dev/null +++ b/packages/styleguide/src/lib/Organisms/ConnectedForm/ConnectedForm/example.ts @@ -0,0 +1,67 @@ +export const example = `import { + ConnectedCheckbox, + ConnectedInput, + ConnectedSelect, + useConnectedForm, +} from '@codecademy/gamut'; + +import { TerminalIcon } from '@codecademy/gamut-icons'; + +export const GoodForm = () => { + const { + ConnectedFormGroup, + ConnectedForm, + connectedFormProps, + FormRequiredText, + } = useConnectedForm({ + defaultValues: { + thisField: true, + thatField: 'zero', + anotherField: 'state your name.', + }, + validationRules: { + thisField: { required: 'you need to check this.' }, + thatField: { + pattern: { + value: /^(?:(?!zero).)*$/, + message: 'literally anything but zero', + }, + }, + }, + }); + + return ( + console.log(thisField)} + resetOnSubmit + {...connectedFormProps} + > + submit this form. + + + + + + ); +};`;