Skip to content

Commit 9ec37fc

Browse files
feat(components): move the snippet and copy to code components from gonfalon to LP (#1795)
1 parent 926a148 commit 9ec37fc

File tree

8 files changed

+493
-3
lines changed

8 files changed

+493
-3
lines changed

.changeset/silver-cobras-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@launchpad-ui/components": minor
3+
---
4+
5+
added the CopyToClipboard and Snippet components

packages/components/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333
"@internationalized/date": "3.10.0",
3434
"@launchpad-ui/icons": "workspace:~",
3535
"@launchpad-ui/tokens": "workspace:~",
36-
"class-variance-authority": "0.7.0"
36+
"@react-aria/live-announcer": "3.4.4",
37+
"class-variance-authority": "0.7.0",
38+
"prism-themes": "1.9.0"
3739
},
3840
"devDependencies": {
3941
"@react-aria/focus": "3.21.2",
4042
"@react-aria/interactions": "3.25.6",
4143
"@react-aria/utils": "3.31.0",
4244
"@react-stately/utils": "3.10.8",
4345
"@react-types/shared": "3.32.1",
46+
"@types/prismjs": "1.26.5",
4447
"copyfiles": "2.4.1",
4548
"react": "19.2.0",
4649
"react-aria": "3.44.0",
@@ -55,6 +58,7 @@
5558
"@react-aria/utils": "3.31.0",
5659
"@react-stately/utils": "3.10.8",
5760
"@react-types/shared": "3.32.1",
61+
"prismjs": "1.30.0",
5862
"react": "19.2.0",
5963
"react-aria": "3.44.0",
6064
"react-aria-components": "1.13.0",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ReactNode } from 'react';
2+
3+
import { PressResponder } from '@react-aria/interactions';
4+
5+
import { Tooltip, TooltipTrigger } from './Tooltip';
6+
import { copyToClipboard } from './utils';
7+
8+
type CopyToClipboardProps = {
9+
onCopy?: () => void;
10+
children: ReactNode;
11+
text: string;
12+
tooltip?: string;
13+
showTooltip?: boolean;
14+
};
15+
16+
export const CopyToClipboard = ({
17+
onCopy,
18+
children,
19+
text,
20+
tooltip = 'Copy to clipboard',
21+
showTooltip = true,
22+
}: CopyToClipboardProps) => {
23+
const handlePress = async () => {
24+
await copyToClipboard(text, 'Copied!');
25+
if (onCopy) {
26+
onCopy();
27+
}
28+
};
29+
30+
return (
31+
<TooltipTrigger>
32+
<PressResponder onPress={handlePress}>{children}</PressResponder>
33+
{showTooltip && <Tooltip placement="bottom">{tooltip}</Tooltip>}
34+
</TooltipTrigger>
35+
);
36+
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import Prism from 'prismjs';
2+
import { type JSX, useLayoutEffect, useRef } from 'react';
3+
4+
import { IconButton } from './IconButton';
5+
6+
// Import languages based on what you need
7+
import 'prismjs/components/prism-apex';
8+
import 'prismjs/components/prism-bash';
9+
import 'prismjs/components/prism-brightscript';
10+
import 'prismjs/components/prism-c';
11+
import 'prismjs/components/prism-clike';
12+
import 'prismjs/components/prism-cpp';
13+
import 'prismjs/components/prism-csharp';
14+
import 'prismjs/components/prism-erlang';
15+
import 'prismjs/components/prism-go';
16+
import 'prismjs/components/prism-gradle';
17+
import 'prismjs/components/prism-haskell';
18+
import 'prismjs/components/prism-java';
19+
import 'prismjs/components/prism-javascript';
20+
import 'prismjs/components/prism-json';
21+
import 'prismjs/components/prism-jsx';
22+
import 'prismjs/components/prism-kotlin';
23+
import 'prismjs/components/prism-lua';
24+
import 'prismjs/components/prism-makefile';
25+
import 'prismjs/components/prism-markup';
26+
import 'prismjs/components/prism-markup-templating';
27+
import 'prismjs/components/prism-objectivec';
28+
import 'prismjs/components/prism-php';
29+
import 'prismjs/components/prism-powershell';
30+
import 'prismjs/components/prism-python';
31+
import 'prismjs/components/prism-ruby';
32+
import 'prismjs/components/prism-rust';
33+
import 'prismjs/components/prism-sql';
34+
import 'prismjs/components/prism-swift';
35+
import 'prismjs/components/prism-tsx';
36+
import 'prismjs/components/prism-typescript';
37+
import 'prismjs/components/prism-yaml';
38+
// Import plugins
39+
import 'prismjs/plugins/keep-markup/prism-keep-markup';
40+
import 'prismjs/plugins/line-highlight/prism-line-highlight';
41+
import 'prismjs/plugins/line-numbers/prism-line-numbers';
42+
43+
import { CopyToClipboard } from './CopyToClipboard';
44+
45+
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
46+
47+
import styles from './styles/Snippet.module.css';
48+
49+
export const languages = [
50+
'bash',
51+
'shell',
52+
'json',
53+
'html',
54+
'xml',
55+
'js',
56+
'javascript',
57+
'lua',
58+
'ts',
59+
'typescript',
60+
'php',
61+
'java',
62+
'ruby',
63+
'python',
64+
'go',
65+
'csharp',
66+
'c',
67+
'cpp',
68+
'objectivec',
69+
'swift',
70+
'makefile',
71+
'haskell',
72+
'brightscript',
73+
'dart',
74+
'rust',
75+
'tsx',
76+
'gradle',
77+
'powershell',
78+
'kotlin',
79+
'erlang',
80+
'yaml',
81+
'apex',
82+
// 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.
83+
'text',
84+
'',
85+
] as const;
86+
87+
export type SnippetLang = (typeof languages)[number];
88+
89+
type SnippetProps = {
90+
children: string | JSX.Element;
91+
className?: string;
92+
highlightRange?: string;
93+
highlightOffset?: number;
94+
lang: SnippetLang;
95+
label?: string;
96+
withHeader?: boolean;
97+
withLineNumbers?: boolean;
98+
useDefaultHighlighting?: boolean;
99+
withCopyButton?: boolean;
100+
trackAnalyticsOnClick?: () => void;
101+
};
102+
103+
// Example usage:
104+
//
105+
// const json = JSON.stringify({
106+
// 'key': '[email protected]',
107+
// 'ip': '192.168.0.1',
108+
// 'custom': {
109+
// 'customer_ranking': 10004
110+
// }
111+
// }, null, 2);
112+
//
113+
// <Snippet withCopyButton={true} lang="json">{json}</Snippet>
114+
export function Snippet({
115+
children,
116+
className,
117+
highlightRange,
118+
highlightOffset,
119+
lang,
120+
label,
121+
withHeader,
122+
withLineNumbers,
123+
useDefaultHighlighting = false,
124+
withCopyButton,
125+
trackAnalyticsOnClick,
126+
}: SnippetProps) {
127+
const codeEl = useRef<HTMLElement>(null);
128+
129+
// biome-ignore lint/correctness/useExhaustiveDependencies: children and lang are intentionally included to re-highlight when they change
130+
useLayoutEffect(() => {
131+
const element = codeEl.current;
132+
if (!element) {
133+
return;
134+
}
135+
136+
// Use requestAnimationFrame to ensure that the element is mounted
137+
// before highlighting it.
138+
const frame = requestAnimationFrame(() => {
139+
Prism.highlightElement(element);
140+
});
141+
142+
// Cancel the animation frame when the component unmounts.
143+
return () => cancelAnimationFrame(frame);
144+
}, [children, lang]);
145+
146+
return (
147+
<>
148+
{withHeader && (
149+
<div className={styles.header}>
150+
{label && <span>{label}</span>}
151+
{lang && <span>{lang}</span>}
152+
</div>
153+
)}
154+
<div
155+
className={`${styles.snippet} ${className ?? ''} ${withCopyButton ? styles.copyable : ''} ${useDefaultHighlighting ? styles.useDefaultHighlighting : ''}`}
156+
>
157+
<pre
158+
className={withLineNumbers ? styles['line-numbers'] : ''}
159+
data-start={1}
160+
data-line-offset={highlightOffset ? highlightOffset.toString() : ''}
161+
data-line={highlightRange}
162+
>
163+
<code className={`language-${lang}`} ref={codeEl}>
164+
{children}
165+
</code>
166+
{withCopyButton && (
167+
<CopyToClipboard text={children as string} showTooltip={false}>
168+
<IconButton
169+
className={styles.copyButton}
170+
aria-label="Copy code snippet"
171+
variant="minimal"
172+
icon="copy-code"
173+
onPress={trackAnalyticsOnClick}
174+
/>
175+
</CopyToClipboard>
176+
)}
177+
</pre>
178+
</div>
179+
</>
180+
);
181+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
@import 'prism-themes/themes/prism-ghcolors.css';
2+
3+
.snippet {
4+
border: 1px solid var(--lp-color-border-ui-secondary);
5+
border-radius: 3px;
6+
overflow-x: auto;
7+
overflow-y: auto;
8+
}
9+
10+
.snippet > pre {
11+
position: relative;
12+
white-space: pre-wrap;
13+
word-break: break-word;
14+
width: 100%;
15+
}
16+
17+
.copyable {
18+
display: flex;
19+
align-items: flex-start;
20+
justify-content: space-between;
21+
position: relative;
22+
}
23+
24+
.header {
25+
display: flex;
26+
justify-content: space-between;
27+
color: var(--lp-color-text-ui-secondary);
28+
font-size: var(--lp-font-size-200);
29+
line-height: var(--lp-line-height-200);
30+
width: 100%;
31+
}
32+
33+
.copyable [class*='_CopyToClipboard'] {
34+
position: absolute;
35+
top: var(--lp-spacing-400);
36+
right: var(--lp-spacing-400);
37+
visibility: hidden;
38+
}
39+
40+
.copyable:hover [class*='_CopyToClipboard'] {
41+
visibility: visible;
42+
}
43+
44+
.Snippet--inline {
45+
border: none;
46+
}
47+
48+
.snippet pre[class*='language-'] {
49+
margin: 0;
50+
border: none;
51+
background-color: var(--lp-color-bg-ui-secondary);
52+
padding-right: 2rem;
53+
}
54+
55+
.snippet pre[class*='language-'] [data-theme='dark'] {
56+
background-color: var(--lp-color-bg-ui-tertiary);
57+
color: var(--lp-color-gray-200);
58+
}
59+
60+
.snippet pre[class*='language-'],
61+
.snippet code[class*='language-'] {
62+
font-family: var(--lp-font-family-monospace);
63+
line-height: 140%;
64+
color: var(--lp-color-text-code-base);
65+
}
66+
67+
.snippet code {
68+
padding-left: 0;
69+
background-color: transparent;
70+
z-index: var(--stacking-above-new-context);
71+
white-space: pre-wrap;
72+
word-break: break-word;
73+
border: none;
74+
}
75+
76+
.snippet:not(.useDefaultHighlighting) .line-highlight {
77+
margin-top: 0;
78+
background: var(--lp-color-brand-yellow-light);
79+
z-index: var(--stacking-new-context);
80+
}
81+
82+
.snippet:not(.useDefaultHighlighting) .line-highlight [data-theme='dark'] {
83+
background: #3c4200;
84+
}
85+
86+
.snippet pre[class*='language-'].line-numbers {
87+
line-height: 0.625rem;
88+
padding-bottom: var(--lp-spacing-400);
89+
padding-top: var(--lp-spacing-400);
90+
overflow-y: hidden;
91+
white-space: pre-wrap;
92+
z-index: var(--stacking-above-new-context);
93+
}
94+
95+
.snippet .line-numbers .line-numbers-rows {
96+
border: none;
97+
left: -3em;
98+
width: 2em;
99+
}
100+
101+
.snippet .token.operator,
102+
.snippet .token.punctuation {
103+
color: var(--lp-color-text-ui-primary-base);
104+
}
105+
106+
.snippet .token.function {
107+
color: var(--lp-color-text-code-function);
108+
font-weight: var(--lp-font-weight-regular);
109+
}
110+
111+
.snippet .token.tag {
112+
color: var(--lp-color-text-code-tag);
113+
}
114+
115+
.token.attr-value,
116+
.token.string {
117+
color: var(--lp-color-text-code-string);
118+
}
119+
120+
.snippet .token.property {
121+
color: var(--lp-color-text-code-keyword);
122+
}
123+
124+
.snippet .token.keyword {
125+
color: var(--lp-color-text-code-keyword);
126+
}
127+
128+
.copyButton {
129+
position: absolute;
130+
right: 0;
131+
top: 0;
132+
}

0 commit comments

Comments
 (0)