-
Notifications
You must be signed in to change notification settings - Fork 9
feat(components): move the snippet and copy to code components from gonfalon to LP #1795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
62f82e9
81e62f0
1d4b555
a714c81
93ac482
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@launchpad-ui/components": minor | ||
| --- | ||
|
|
||
| added the CopyToClipboard and Snippet components |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| import { PressResponder } from '@react-aria/interactions'; | ||
|
|
||
| import { Tooltip, TooltipTrigger } from './Tooltip'; | ||
| import { copyToClipboard } from './utils'; | ||
|
|
||
| type CopyToClipboardProps = { | ||
| onCopy?: () => void; | ||
| children: ReactNode; | ||
| text: string; | ||
| tooltip?: string; | ||
| showTooltip?: boolean; | ||
| }; | ||
|
|
||
| export const CopyToClipboard = ({ | ||
| onCopy, | ||
| children, | ||
| text, | ||
| tooltip = 'Copy to clipboard', | ||
| showTooltip = true, | ||
| }: CopyToClipboardProps) => { | ||
| const handlePress = async () => { | ||
| await copyToClipboard(text, 'Copied!'); | ||
| if (onCopy) { | ||
| onCopy(); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <TooltipTrigger> | ||
| <PressResponder onPress={handlePress}>{children}</PressResponder> | ||
| {showTooltip && <Tooltip placement="bottom">{tooltip}</Tooltip>} | ||
| </TooltipTrigger> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import Prism from 'prismjs'; | ||
| import { type JSX, useLayoutEffect, useRef } from 'react'; | ||
|
|
||
| import { IconButton } from './IconButton'; | ||
|
|
||
| // Import languages based on what you need | ||
| import 'prismjs/components/prism-apex'; | ||
| import 'prismjs/components/prism-bash'; | ||
| import 'prismjs/components/prism-brightscript'; | ||
| import 'prismjs/components/prism-c'; | ||
| import 'prismjs/components/prism-clike'; | ||
| import 'prismjs/components/prism-cpp'; | ||
| import 'prismjs/components/prism-csharp'; | ||
| import 'prismjs/components/prism-erlang'; | ||
| import 'prismjs/components/prism-go'; | ||
| import 'prismjs/components/prism-gradle'; | ||
| import 'prismjs/components/prism-haskell'; | ||
| import 'prismjs/components/prism-java'; | ||
| import 'prismjs/components/prism-javascript'; | ||
| import 'prismjs/components/prism-json'; | ||
| import 'prismjs/components/prism-jsx'; | ||
| import 'prismjs/components/prism-kotlin'; | ||
| import 'prismjs/components/prism-lua'; | ||
| import 'prismjs/components/prism-makefile'; | ||
| import 'prismjs/components/prism-markup'; | ||
| import 'prismjs/components/prism-markup-templating'; | ||
| import 'prismjs/components/prism-objectivec'; | ||
| import 'prismjs/components/prism-php'; | ||
| import 'prismjs/components/prism-powershell'; | ||
| import 'prismjs/components/prism-python'; | ||
| import 'prismjs/components/prism-ruby'; | ||
| import 'prismjs/components/prism-rust'; | ||
| import 'prismjs/components/prism-sql'; | ||
| import 'prismjs/components/prism-swift'; | ||
| import 'prismjs/components/prism-tsx'; | ||
| import 'prismjs/components/prism-typescript'; | ||
| import 'prismjs/components/prism-yaml'; | ||
| // Import plugins | ||
| import 'prismjs/plugins/keep-markup/prism-keep-markup'; | ||
| import 'prismjs/plugins/line-highlight/prism-line-highlight'; | ||
| import 'prismjs/plugins/line-numbers/prism-line-numbers'; | ||
|
|
||
| import { CopyToClipboard } from './CopyToClipboard'; | ||
|
|
||
| import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; | ||
|
|
||
| import styles from './styles/Snippet.module.css'; | ||
|
|
||
| export const languages = [ | ||
| 'bash', | ||
| 'shell', | ||
| 'json', | ||
| 'html', | ||
| 'xml', | ||
| 'js', | ||
| 'javascript', | ||
| 'lua', | ||
| 'ts', | ||
| 'typescript', | ||
| 'php', | ||
| 'java', | ||
| 'ruby', | ||
| 'python', | ||
| 'go', | ||
| 'csharp', | ||
| 'c', | ||
| 'cpp', | ||
| 'objectivec', | ||
| 'swift', | ||
| 'makefile', | ||
| 'haskell', | ||
| 'brightscript', | ||
| 'dart', | ||
| 'rust', | ||
| 'tsx', | ||
| 'gradle', | ||
| 'powershell', | ||
| 'kotlin', | ||
| 'erlang', | ||
| 'yaml', | ||
| 'apex', | ||
| // text and empty string are not a recognized languages by prism, we use it here as a default option for when you don't want styling. | ||
| 'text', | ||
| '', | ||
| ] as const; | ||
|
|
||
| export type SnippetLang = (typeof languages)[number]; | ||
|
|
||
| type SnippetProps = { | ||
| children: string | JSX.Element; | ||
| className?: string; | ||
| highlightRange?: string; | ||
| highlightOffset?: number; | ||
| lang: SnippetLang; | ||
| label?: string; | ||
| withHeader?: boolean; | ||
| withLineNumbers?: boolean; | ||
| useDefaultHighlighting?: boolean; | ||
| withCopyButton?: boolean; | ||
| trackAnalyticsOnClick?: () => void; | ||
| }; | ||
|
|
||
| // Example usage: | ||
| // | ||
| // const json = JSON.stringify({ | ||
| // 'key': 'test@test.com', | ||
| // 'ip': '192.168.0.1', | ||
| // 'custom': { | ||
| // 'customer_ranking': 10004 | ||
| // } | ||
| // }, null, 2); | ||
| // | ||
| // <Snippet withCopyButton={true} lang="json">{json}</Snippet> | ||
| export function Snippet({ | ||
| children, | ||
| className, | ||
| highlightRange, | ||
| highlightOffset, | ||
| lang, | ||
| label, | ||
| withHeader, | ||
| withLineNumbers, | ||
| useDefaultHighlighting = false, | ||
| withCopyButton, | ||
| trackAnalyticsOnClick, | ||
| }: SnippetProps) { | ||
| const codeEl = useRef<HTMLElement>(null); | ||
|
|
||
| // biome-ignore lint/correctness/useExhaustiveDependencies: children and lang are intentionally included to re-highlight when they change | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the |
||
| useLayoutEffect(() => { | ||
| const element = codeEl.current; | ||
| if (!element) { | ||
| return; | ||
| } | ||
|
|
||
| // Use requestAnimationFrame to ensure that the element is mounted | ||
| // before highlighting it. | ||
| const frame = requestAnimationFrame(() => { | ||
| Prism.highlightElement(element); | ||
| }); | ||
|
|
||
| // Cancel the animation frame when the component unmounts. | ||
| return () => cancelAnimationFrame(frame); | ||
| }, [children, lang]); | ||
|
|
||
| return ( | ||
| <> | ||
| {withHeader && ( | ||
| <div className={styles.header}> | ||
| {label && <span>{label}</span>} | ||
| {lang && <span>{lang}</span>} | ||
| </div> | ||
| )} | ||
| <div | ||
| className={`${styles.snippet} ${className ?? ''} ${withCopyButton ? styles.copyable : ''} ${useDefaultHighlighting ? styles.useDefaultHighlighting : ''}`} | ||
| > | ||
| <pre | ||
| className={withLineNumbers ? styles['line-numbers'] : ''} | ||
| data-start={1} | ||
| data-line-offset={highlightOffset ? highlightOffset.toString() : ''} | ||
| data-line={highlightRange} | ||
| > | ||
| <code className={`language-${lang}`} ref={codeEl}> | ||
| {children} | ||
| </code> | ||
| {withCopyButton && ( | ||
| <CopyToClipboard text={children as string} showTooltip={false}> | ||
| <IconButton | ||
| className={styles.copyButton} | ||
| aria-label="Copy code snippet" | ||
| variant="minimal" | ||
| icon="copy-code" | ||
| onPress={trackAnalyticsOnClick} | ||
| /> | ||
| </CopyToClipboard> | ||
| )} | ||
| </pre> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| @import 'prism-themes/themes/prism-ghcolors.css'; | ||
|
|
||
| .snippet { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed this from |
||
| border: 1px solid var(--lp-color-border-ui-secondary); | ||
| border-radius: 3px; | ||
| overflow-x: auto; | ||
| overflow-y: auto; | ||
| } | ||
|
|
||
| .snippet > pre { | ||
| position: relative; | ||
| white-space: pre-wrap; | ||
| word-break: break-word; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .copyable { | ||
| display: flex; | ||
| align-items: flex-start; | ||
| justify-content: space-between; | ||
| position: relative; | ||
| } | ||
|
|
||
| .header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| color: var(--lp-color-text-ui-secondary); | ||
| font-size: var(--lp-font-size-200); | ||
| line-height: var(--lp-line-height-200); | ||
| width: 100%; | ||
| } | ||
|
|
||
| .copyable [class*='_CopyToClipboard'] { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed this from |
||
| position: absolute; | ||
| top: var(--lp-spacing-400); | ||
| right: var(--lp-spacing-400); | ||
| visibility: hidden; | ||
| } | ||
|
|
||
| .copyable:hover [class*='_CopyToClipboard'] { | ||
| visibility: visible; | ||
| } | ||
|
|
||
| .Snippet--inline { | ||
| border: none; | ||
| } | ||
|
|
||
| .snippet pre[class*='language-'] { | ||
| margin: 0; | ||
| border: none; | ||
| background-color: var(--lp-color-bg-ui-secondary); | ||
| padding-right: 2rem; | ||
| } | ||
|
|
||
| .snippet pre[class*='language-'] [data-theme='dark'] { | ||
| background-color: var(--lp-color-bg-ui-tertiary); | ||
| color: var(--lp-color-gray-200); | ||
| } | ||
|
|
||
| .snippet pre[class*='language-'], | ||
| .snippet code[class*='language-'] { | ||
| font-family: var(--lp-font-family-monospace); | ||
| line-height: 140%; | ||
| color: var(--lp-color-text-code-base); | ||
| } | ||
|
|
||
| .snippet code { | ||
| padding-left: 0; | ||
| background-color: transparent; | ||
| z-index: var(--stacking-above-new-context); | ||
| white-space: pre-wrap; | ||
| word-break: break-word; | ||
| border: none; | ||
| } | ||
|
|
||
| .snippet:not(.useDefaultHighlighting) .line-highlight { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed |
||
| margin-top: 0; | ||
| background: var(--lp-color-brand-yellow-light); | ||
| z-index: var(--stacking-new-context); | ||
| } | ||
|
|
||
| .snippet:not(.useDefaultHighlighting) .line-highlight [data-theme='dark'] { | ||
| background: #3c4200; | ||
| } | ||
|
|
||
| .snippet pre[class*='language-'].line-numbers { | ||
| line-height: 0.625rem; | ||
| padding-bottom: var(--lp-spacing-400); | ||
| padding-top: var(--lp-spacing-400); | ||
| overflow-y: hidden; | ||
| white-space: pre-wrap; | ||
| z-index: var(--stacking-above-new-context); | ||
| } | ||
|
|
||
| .snippet .line-numbers .line-numbers-rows { | ||
| border: none; | ||
| left: -3em; | ||
| width: 2em; | ||
| } | ||
|
|
||
| .snippet .token.operator, | ||
| .snippet .token.punctuation { | ||
| color: var(--lp-color-text-ui-primary-base); | ||
| } | ||
|
|
||
| .snippet .token.function { | ||
| color: var(--lp-color-text-code-function); | ||
| font-weight: var(--lp-font-weight-regular); | ||
| } | ||
|
|
||
| .snippet .token.tag { | ||
| color: var(--lp-color-text-code-tag); | ||
| } | ||
|
|
||
| .token.attr-value, | ||
| .token.string { | ||
| color: var(--lp-color-text-code-string); | ||
| } | ||
|
|
||
| .snippet .token.property { | ||
| color: var(--lp-color-text-code-keyword); | ||
| } | ||
|
|
||
| .snippet .token.keyword { | ||
| color: var(--lp-color-text-code-keyword); | ||
| } | ||
|
|
||
| .copyButton { | ||
| position: absolute; | ||
| right: 0; | ||
| top: 0; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be in devDep? I see all the other
@react-ariadeps are devDeps. I'm not sure why, but best to stay consistent. Better still find out why they are devDep and not dep.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure on that one. The reason being that we had originally had it as a dependency in the dependencies list and I didn't want to stray from that. What'd you think?
https://github.com/launchdarkly/gonfalon/blob/main/packages/forms-experimental/package.json#L6-L28
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @apucacao @vezaynk