-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Created new embed components to keep app and embed separate, added da…
…rk theme: light, dark, blue, system.
- Loading branch information
1 parent
e1ef5f0
commit f03e0ba
Showing
5 changed files
with
339 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import React, { useEffect, useRef, useState } from 'react'; | ||
import ReactMarkdown from 'react-markdown'; | ||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; | ||
import { vscDarkPlus, oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'; | ||
import { getLanguageLabel, getMonacoLanguage } from '../../../utils/language/languageUtils'; | ||
import EmbedCopyButton from './EmbedCopyButton'; | ||
|
||
export interface EmbedCodeBlockProps { | ||
code: string; | ||
language?: string; | ||
showLineNumbers?: boolean; | ||
theme?: 'light' | 'dark' | 'blue' | 'system'; | ||
} | ||
|
||
export const EmbedCodeView: React.FC<EmbedCodeBlockProps> = ({ | ||
code, | ||
language = 'plaintext', | ||
showLineNumbers = true, | ||
theme = 'system' | ||
}) => { | ||
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark' | 'blue'>(() => { | ||
if (theme === 'system') { | ||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | ||
} | ||
return theme; | ||
}); | ||
|
||
useEffect(() => { | ||
if (theme !== 'system') { | ||
setEffectiveTheme(theme); | ||
return; | ||
} | ||
|
||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); | ||
const handleChange = () => { | ||
setEffectiveTheme(mediaQuery.matches ? 'dark' : 'light'); | ||
}; | ||
mediaQuery.addEventListener('change', handleChange); | ||
return () => mediaQuery.removeEventListener('change', handleChange); | ||
}, [theme]); | ||
|
||
const isDark = effectiveTheme === 'dark' || effectiveTheme === 'blue'; | ||
const isMarkdown = getLanguageLabel(language) === 'markdown'; | ||
const [highlighterHeight, setHighlighterHeight] = useState<string>("100px"); | ||
const containerRef = useRef<HTMLDivElement>(null); | ||
const LINE_HEIGHT = 19; | ||
|
||
useEffect(() => { | ||
updateHighlighterHeight(); | ||
const resizeObserver = new ResizeObserver(updateHighlighterHeight); | ||
if (containerRef.current) { | ||
resizeObserver.observe(containerRef.current); | ||
} | ||
return () => resizeObserver.disconnect(); | ||
}, [code]); | ||
|
||
const updateHighlighterHeight = () => { | ||
if (!containerRef.current) return; | ||
|
||
const lineCount = code.split('\n').length; | ||
const contentHeight = (lineCount * LINE_HEIGHT) + 35; | ||
const newHeight = Math.min(500, Math.max(100, contentHeight)); | ||
setHighlighterHeight(`${newHeight}px`); | ||
}; | ||
|
||
const baseTheme = isDark ? vscDarkPlus : oneLight; | ||
const getBackgroundColor = () => { | ||
switch (effectiveTheme) { | ||
case 'blue': | ||
case 'dark': | ||
return '#1E1E1E'; | ||
case 'light': | ||
return '#ffffff'; | ||
} | ||
}; | ||
|
||
const backgroundColor = getBackgroundColor(); | ||
const customStyle = { | ||
...baseTheme, | ||
'pre[class*="language-"]': { | ||
...baseTheme['pre[class*="language-"]'], | ||
margin: 0, | ||
fontSize: '13px', | ||
background: backgroundColor, | ||
padding: '1rem', | ||
}, | ||
'code[class*="language-"]': { | ||
...baseTheme['code[class*="language-"]'], | ||
fontSize: '13px', | ||
background: backgroundColor, | ||
display: 'block', | ||
textIndent: 0, | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="relative"> | ||
<style> | ||
{` | ||
.markdown-content-full { | ||
color: var(--text-color); | ||
background-color: ${backgroundColor}; | ||
padding: 1rem; | ||
border-radius: 0.5rem; | ||
position: relative; | ||
} | ||
.markdown-content-full pre, | ||
.markdown-content-full code { | ||
background-color: ${isDark ? '#2d2d2d' : '#ebebeb'} !important; | ||
color: ${isDark ? '#e5e7eb' : '#1f2937'} !important; | ||
} | ||
.markdown-content-full pre code { | ||
background-color: transparent !important; | ||
padding: 0; | ||
border: none; | ||
box-shadow: none; | ||
} | ||
:root { | ||
--text-color: ${isDark ? '#ffffff' : '#000000'}; | ||
} | ||
`} | ||
</style> | ||
<div className="relative"> | ||
{isMarkdown ? ( | ||
<div className="markdown-content markdown-content-full rounded-lg" style={{ backgroundColor }}> | ||
<ReactMarkdown className={`markdown prose ${isDark ? 'prose-invert' : ''} max-w-none`}> | ||
{code} | ||
</ReactMarkdown> | ||
</div> | ||
) : ( | ||
<div | ||
ref={containerRef} | ||
style={{ maxHeight: '500px' }} | ||
> | ||
<SyntaxHighlighter | ||
language={getMonacoLanguage(language)} | ||
style={customStyle} | ||
showLineNumbers={showLineNumbers} | ||
wrapLines={true} | ||
lineProps={{ | ||
style: { | ||
whiteSpace: 'pre', | ||
wordBreak: 'break-all', | ||
paddingLeft: 0 | ||
} | ||
}} | ||
customStyle={{ | ||
height: highlighterHeight, | ||
minHeight: '100px', | ||
marginBottom: 0, | ||
marginTop: 0, | ||
textIndent: 0, | ||
paddingLeft: showLineNumbers ? 10 : 20, | ||
borderRadius: '0.5rem', | ||
background: backgroundColor | ||
}} | ||
> | ||
{code} | ||
</SyntaxHighlighter> | ||
</div> | ||
)} | ||
|
||
<EmbedCopyButton text={code} theme={theme} /> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import React, { useState } from 'react'; | ||
import { Copy, Check } from 'lucide-react'; | ||
|
||
export interface EmbedCopyButtonProps { | ||
text: string; | ||
theme: 'light' | 'dark' | 'blue' | 'system'; | ||
} | ||
|
||
const EmbedCopyButton: React.FC<EmbedCopyButtonProps> = ({ text, theme }) => { | ||
const [isCopied, setIsCopied] = useState(false); | ||
|
||
const isDark = theme === 'dark' || theme === 'blue' || | ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); | ||
|
||
const handleCopy = async (e: React.MouseEvent) => { | ||
e.stopPropagation(); | ||
try { | ||
const textArea = document.createElement('textarea'); | ||
textArea.value = text; | ||
textArea.style.position = 'fixed'; | ||
textArea.style.left = '-999999px'; | ||
textArea.style.top = '-999999px'; | ||
document.body.appendChild(textArea); | ||
textArea.focus(); | ||
textArea.select(); | ||
|
||
try { | ||
const successful = document.execCommand('copy'); | ||
if (!successful) { | ||
throw new Error('Copy command failed'); | ||
} | ||
} finally { | ||
textArea.remove(); | ||
} | ||
|
||
setIsCopied(true); | ||
setTimeout(() => setIsCopied(false), 2000); | ||
} catch (err) { | ||
console.error('Failed to copy text: ', err); | ||
setIsCopied(false); | ||
} | ||
}; | ||
|
||
const getBackgroundColor = () => { | ||
switch (theme) { | ||
case 'blue': | ||
return 'bg-dark-surface hover:bg-dark-hover'; | ||
case 'dark': | ||
return 'bg-neutral-700 hover:bg-neutral-600'; | ||
case 'light': | ||
return 'bg-light-surface hover:bg-light-hover'; | ||
case 'system': | ||
return isDark | ||
? 'bg-neutral-700 hover:bg-neutral-600' | ||
: 'bg-light-surface hover:bg-light-hover'; | ||
} | ||
}; | ||
|
||
const getTextColor = () => { | ||
if (theme === 'blue' || theme === 'dark' || (theme === 'system' && isDark)) { | ||
return 'text-dark-text'; | ||
} | ||
return 'text-light-text'; | ||
}; | ||
|
||
const getIconColor = () => { | ||
if (isCopied) { | ||
return isDark ? 'text-dark-primary' : 'text-light-primary'; | ||
} | ||
if (theme === 'blue' || theme === 'dark' || (theme === 'system' && isDark)) { | ||
return 'text-dark-text'; | ||
} | ||
return 'text-light-text'; | ||
}; | ||
|
||
return ( | ||
<button | ||
onClick={handleCopy} | ||
className={`absolute top-2 right-2 p-1 rounded-md transition-colors ${getBackgroundColor()} ${getTextColor()}`} | ||
title="Copy to clipboard" | ||
> | ||
{isCopied ? ( | ||
<Check size={16} className={getIconColor()} /> | ||
) : ( | ||
<Copy size={16} className={getIconColor()} /> | ||
)} | ||
</button> | ||
); | ||
}; | ||
|
||
export default EmbedCopyButton; |
Oops, something went wrong.