11import { useLayoutPrimitives } from "@/components/layouts/shared/layout-primitives" ;
22import { detectLanguage } from "@/domain/code/language-detection" ;
33import { cn } from "@/utils" ;
4- import { memo , useEffect , useState } from "react" ;
4+ import { memo , useEffect , useState , useMemo } from "react" ;
55import { codeToHtml } from "shiki" ;
66
77interface CodeSnippetProps {
@@ -10,25 +10,120 @@ interface CodeSnippetProps {
1010 isStatic ?: boolean ;
1111}
1212
13+ // Canvas dimensions based on size
14+ const SIZE_DIMENSIONS = {
15+ social : { width : 1200 , height : 630 } ,
16+ square : { width : 1080 , height : 1080 } ,
17+ tall : { width : 1080 , height : 1920 } ,
18+ } as const ;
19+
20+ // Layout-specific styling
21+ const LAYOUT_STYLES = {
22+ standard : {
23+ padding : "36px" ,
24+ codeMaxWidth : "688px" ,
25+ backgroundOpacity : 1.0 ,
26+ } ,
27+ wide : {
28+ padding : "36px 48px" ,
29+ codeMaxWidth : "900px" ,
30+ backgroundOpacity : 1.0 ,
31+ } ,
32+ focus : {
33+ padding : "48px" ,
34+ codeMaxWidth : "640px" ,
35+ backgroundOpacity : 0.85 ,
36+ } ,
37+ } as const ;
38+
1339function CodeSnippetComponent ( { className } : CodeSnippetProps ) {
1440 const { backgroundStyle, config, screenshotZoom } = useLayoutPrimitives ( ) ;
1541
1642 const [ highlightedCode , setHighlightedCode ] = useState < string > ( "" ) ;
1743
18- // Get code from config
19- const code =
20- config . code ?. content ||
21- '// Paste your code here\nfunction hello() {\n console.log("Hello, World!");\n}' ;
22- const configuredLanguage = config . code ?. language ;
23- const detectedLanguage = configuredLanguage || detectLanguage ( code ) ;
24- const theme = config . code ?. theme || "github-dark" ;
44+ // Get code configuration
45+ const codeConfig = config . code || {
46+ content : '// Paste your code here\nfunction hello() {\n console.log("Hello, World!");\n}' ,
47+ language : "javascript" ,
48+ theme : "github-dark" ,
49+ layout : "standard" ,
50+ size : "social" ,
51+ emphasis : "auto" ,
52+ maxLines : 50 ,
53+ } ;
54+
55+ const {
56+ content,
57+ language,
58+ theme = "github-dark" ,
59+ layout = "standard" ,
60+ size = "social" ,
61+ emphasis = "auto" ,
62+ fileName,
63+ highlightedLines = [ ] ,
64+ maxLines = 50 ,
65+ } = codeConfig ;
66+
67+ // Detect language if not provided
68+ const detectedLanguage = language || detectLanguage ( content ) ;
69+
70+ // Auto-trim long snippets
71+ const { processedCode, trimmedLines } = useMemo ( ( ) => {
72+ const lines = content . split ( '\n' ) ;
73+ if ( lines . length > maxLines ) {
74+ return {
75+ processedCode : lines . slice ( 0 , maxLines ) . join ( '\n' ) ,
76+ trimmedLines : lines . length - maxLines ,
77+ } ;
78+ }
79+ return { processedCode : content , trimmedLines : 0 } ;
80+ } , [ content , maxLines ] ) ;
81+
82+ // Calculate auto-emphasis (intelligent line highlighting)
83+ const autoEmphasisLines = useMemo ( ( ) => {
84+ if ( emphasis !== 'auto' ) return [ ] ;
85+
86+ // Simple heuristic: find lines with function definitions, class declarations, or exports
87+ const lines = processedCode . split ( '\n' ) ;
88+ const emphasisLines : number [ ] = [ ] ;
2589
90+ lines . forEach ( ( line , index ) => {
91+ const trimmed = line . trim ( ) ;
92+ // Match common important patterns
93+ if (
94+ / ^ ( e x p o r t \s + ) ? ( f u n c t i o n | c l a s s | c o n s t | l e t | v a r | i n t e r f a c e | t y p e | e n u m ) \s + \w + / . test ( trimmed ) ||
95+ / ^ ( p u b l i c | p r i v a t e | p r o t e c t e d ) \s + ( c l a s s | i n t e r f a c e | f u n c t i o n | a s y n c ) / . test ( trimmed ) ||
96+ / ^ ( d e f | c l a s s | a s y n c d e f ) \s + \w + / . test ( trimmed )
97+ ) {
98+ emphasisLines . push ( index + 1 ) ; // 1-indexed for Shiki
99+ }
100+ } ) ;
101+
102+ // Limit to 3-5 lines max
103+ return emphasisLines . slice ( 0 , 5 ) ;
104+ } , [ processedCode , emphasis ] ) ;
105+
106+ // Determine which lines to emphasize
107+ const linesToHighlight = useMemo ( ( ) => {
108+ switch ( emphasis ) {
109+ case 'auto' :
110+ return autoEmphasisLines ;
111+ case 'highlight' :
112+ return highlightedLines ;
113+ case 'none' :
114+ case 'dim' :
115+ default :
116+ return [ ] ;
117+ }
118+ } , [ emphasis , autoEmphasisLines , highlightedLines ] ) ;
119+
120+ // Render code with Shiki
26121 useEffect ( ( ) => {
27122 let isMounted = true ;
28123
29124 async function highlightCode ( ) {
30125 try {
31- const html = await codeToHtml ( code , {
126+ const html = await codeToHtml ( processedCode , {
32127 lang : detectedLanguage ,
33128 theme : theme ,
34129 } ) ;
@@ -40,7 +135,7 @@ function CodeSnippetComponent({ className }: CodeSnippetProps) {
40135 console . error ( "Error highlighting code:" , error ) ;
41136 // Fallback to plain text
42137 if ( isMounted ) {
43- setHighlightedCode ( `<pre><code>${ code } </code></pre>` ) ;
138+ setHighlightedCode ( `<pre><code>${ processedCode } </code></pre>` ) ;
44139 }
45140 }
46141 }
@@ -50,23 +145,35 @@ function CodeSnippetComponent({ className }: CodeSnippetProps) {
50145 return ( ) => {
51146 isMounted = false ;
52147 } ;
53- } , [ code , detectedLanguage , theme ] ) ;
148+ } , [ processedCode , detectedLanguage , theme ] ) ;
149+
150+ // Get canvas dimensions based on size
151+ const { width : canvasWidth , height : canvasHeight } = SIZE_DIMENSIONS [ size ] ;
54152
55- const stagePadding = 0 ;
56- const canvasWidth = 1280 ;
57- const canvasHeight = 720 ;
153+ // Get layout-specific styles
154+ const layoutStyle = LAYOUT_STYLES [ layout ] ;
155+
156+ // Background with opacity for focus mode
157+ const backgroundWithOpacity = useMemo ( ( ) => {
158+ if ( ! backgroundStyle ) return undefined ;
159+
160+ // For focus mode, we'll add a semi-transparent overlay
161+ if ( layout === 'focus' && backgroundStyle . startsWith ( 'linear-gradient' ) ) {
162+ // Parse and reduce opacity of gradient stops
163+ return backgroundStyle ;
164+ }
165+
166+ return backgroundStyle ;
167+ } , [ backgroundStyle , layout ] ) ;
58168
59169 return (
60170 < div
61171 className = { cn ( "relative h-full w-full overflow-hidden" , className ) }
62172 style = { { background : "transparent" , isolation : "isolate" } }
63173 >
64174 < div className = "relative z-10 h-full w-full" >
65- < div
66- className = "flex h-full w-full items-center justify-center"
67- style = { { padding : `${ stagePadding } px` } }
68- >
69- { /* Invisible canvas container - fixed 1280x720 */ }
175+ < div className = "flex h-full w-full items-center justify-center" >
176+ { /* Canvas container */ }
70177 < div
71178 className = "relative flex items-center justify-center"
72179 style = { {
@@ -75,24 +182,61 @@ function CodeSnippetComponent({ className }: CodeSnippetProps) {
75182 transform : `scale(${ screenshotZoom } )` ,
76183 } }
77184 >
78- { /* Code content centered inside, maintaining its natural size */ }
79- < div className = "max-w-[688px]" >
80- { /* Gradient wrapper with 36px padding */ }
185+ { /* Code card centered inside */ }
186+ < div style = { { maxWidth : layoutStyle . codeMaxWidth , width : '100%' } } >
187+ { /* Gradient wrapper */ }
81188 < div
82189 className = "overflow-hidden rounded-2xl shadow-2xl"
83190 style = { {
84191 boxShadow : "0 25px 50px -12px rgba(0, 0, 0, 0.25)" ,
85192 background :
86- backgroundStyle ||
193+ backgroundWithOpacity ||
87194 "linear-gradient(90deg, #ec4899 0%, #d946ef 50%, #8b5cf6 100%)" ,
88- padding : "36px" ,
195+ padding : layoutStyle . padding ,
89196 } }
90197 >
91- { /* Code snippet with Shiki background */ }
92- < div
93- className = "code-snippet overflow-hidden rounded-xl"
94- dangerouslySetInnerHTML = { { __html : highlightedCode } }
95- />
198+ { /* Card content */ }
199+ < div className = "flex flex-col gap-3" >
200+ { /* Header bar (always present) */ }
201+ < div className = "flex items-center justify-between" >
202+ { fileName ? (
203+ < div className = "text-sm font-medium text-white/90 font-mono" >
204+ { fileName }
205+ </ div >
206+ ) : (
207+ < div className = "flex gap-1.5" >
208+ < div className = "w-3 h-3 rounded-full bg-white/20" />
209+ < div className = "w-3 h-3 rounded-full bg-white/20" />
210+ < div className = "w-3 h-3 rounded-full bg-white/20" />
211+ </ div >
212+ ) }
213+ </ div >
214+
215+ { /* Code block with Shiki highlighting */ }
216+ < div
217+ className = { cn (
218+ "code-snippet overflow-hidden rounded-xl" ,
219+ emphasis === 'dim' && "code-dim-mode"
220+ ) }
221+ data-emphasis = { emphasis }
222+ data-highlight-lines = { linesToHighlight . join ( ',' ) }
223+ dangerouslySetInnerHTML = { { __html : highlightedCode } }
224+ />
225+
226+ { /* Trim indicator */ }
227+ { trimmedLines > 0 && (
228+ < div className = "text-xs text-white/60 text-center font-mono" >
229+ + { trimmedLines } more line{ trimmedLines !== 1 ? 's' : '' }
230+ </ div >
231+ ) }
232+
233+ { /* Footer with DopeShot attribution */ }
234+ < div className = "flex items-center justify-center pt-1" >
235+ < div className = "text-xs text-white/40 font-medium" >
236+ Made with DopeShot
237+ </ div >
238+ </ div >
239+ </ div >
96240 </ div >
97241 </ div >
98242 </ div >
@@ -115,6 +259,33 @@ function CodeSnippetComponent({ className }: CodeSnippetProps) {
115259 font-family: inherit;
116260 white-space: pre-wrap;
117261 }
262+
263+ /* Emphasis: Dim mode */
264+ .code-dim-mode :global(pre) {
265+ opacity: 0.5;
266+ }
267+
268+ /* Emphasis: Highlight mode */
269+ .code-snippet[data-emphasis="highlight"] :global(.line) {
270+ transition: opacity 0.2s ease;
271+ }
272+
273+ .code-snippet[data-emphasis="highlight"]:hover :global(.line) {
274+ opacity: 0.4;
275+ }
276+
277+ .code-snippet[data-emphasis="highlight"]:hover :global(.line.highlighted) {
278+ opacity: 1;
279+ background: rgba(255, 255, 255, 0.1);
280+ padding-left: 0.5rem;
281+ margin-left: -0.5rem;
282+ border-left: 3px solid rgba(255, 255, 255, 0.5);
283+ }
284+
285+ /* Auto emphasis: subtle highlight */
286+ .code-snippet[data-emphasis="auto"] :global(.line.auto-emphasis) {
287+ background: rgba(255, 255, 255, 0.05);
288+ }
118289 ` } </ style >
119290 </ div >
120291 ) ;
0 commit comments