Skip to content

Commit 2012267

Browse files
committed
markdown components that were hidden from git for some reason???
1 parent 954aa82 commit 2012267

File tree

12 files changed

+1276
-0
lines changed

12 files changed

+1276
-0
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import * as React from 'react'
2+
import { twMerge } from 'tailwind-merge'
3+
import { useToast } from '~/components/ToastProvider'
4+
import { Copy } from 'lucide-react'
5+
import type { Mermaid } from 'mermaid'
6+
import { transformerNotationDiff } from '@shikijs/transformers'
7+
import { createHighlighter, type HighlighterGeneric } from 'shiki'
8+
import { Button } from '../Button'
9+
10+
// Language aliases mapping
11+
const LANG_ALIASES: Record<string, string> = {
12+
ts: 'typescript',
13+
js: 'javascript',
14+
sh: 'bash',
15+
shell: 'bash',
16+
console: 'bash',
17+
zsh: 'bash',
18+
md: 'markdown',
19+
txt: 'plaintext',
20+
text: 'plaintext',
21+
}
22+
23+
// Lazy highlighter singleton
24+
let highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null
25+
let mermaidInstance: Mermaid | null = null
26+
const genSvgMap = new Map<string, string>()
27+
28+
async function getHighlighter(language: string) {
29+
if (!highlighterPromise) {
30+
highlighterPromise = createHighlighter({
31+
themes: ['github-light', 'vitesse-dark'],
32+
langs: [
33+
'typescript',
34+
'javascript',
35+
'tsx',
36+
'jsx',
37+
'bash',
38+
'json',
39+
'html',
40+
'css',
41+
'markdown',
42+
'plaintext',
43+
],
44+
})
45+
}
46+
47+
const highlighter = await highlighterPromise
48+
const normalizedLang = LANG_ALIASES[language] || language
49+
const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
50+
51+
// Load language if not already loaded
52+
if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
53+
try {
54+
await highlighter.loadLanguage(langToLoad as any)
55+
} catch {
56+
console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
57+
}
58+
}
59+
60+
return highlighter
61+
}
62+
63+
// Lazy load mermaid only when needed
64+
async function getMermaid(): Promise<Mermaid> {
65+
if (!mermaidInstance) {
66+
const { default: mermaid } = await import('mermaid')
67+
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' })
68+
mermaidInstance = mermaid
69+
}
70+
return mermaidInstance
71+
}
72+
73+
function extractPreAttributes(html: string): {
74+
class: string | null
75+
style: string | null
76+
} {
77+
const match = html.match(/<pre\b([^>]*)>/i)
78+
if (!match) {
79+
return { class: null, style: null }
80+
}
81+
82+
const attributes = match[1]
83+
84+
const classMatch = attributes.match(/\bclass\s*=\s*["']([^"']*)["']/i)
85+
const styleMatch = attributes.match(/\bstyle\s*=\s*["']([^"']*)["']/i)
86+
87+
return {
88+
class: classMatch ? classMatch[1] : null,
89+
style: styleMatch ? styleMatch[1] : null,
90+
}
91+
}
92+
93+
export function CodeBlock({
94+
isEmbedded,
95+
showTypeCopyButton = true,
96+
...props
97+
}: React.HTMLProps<HTMLPreElement> & {
98+
isEmbedded?: boolean
99+
showTypeCopyButton?: boolean
100+
}) {
101+
// Extract title from data-code-title attribute, handling both camelCase and kebab-case
102+
const rawTitle = ((props as any)?.dataCodeTitle ||
103+
(props as any)?.['data-code-title']) as string | undefined
104+
105+
// Filter out "undefined" strings, null, and empty strings
106+
const title =
107+
rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
108+
? rawTitle.trim()
109+
: undefined
110+
111+
const childElement = props.children as
112+
| undefined
113+
| { props?: { className?: string; children?: string } }
114+
let lang = childElement?.props?.className?.replace('language-', '')
115+
116+
if (lang === 'diff') {
117+
lang = 'plaintext'
118+
}
119+
120+
const children = props.children as
121+
| undefined
122+
| {
123+
props: {
124+
children: string
125+
}
126+
}
127+
128+
const [copied, setCopied] = React.useState(false)
129+
const ref = React.useRef<any>(null)
130+
const { notify } = useToast()
131+
132+
const code = children?.props.children
133+
134+
const [codeElement, setCodeElement] = React.useState(
135+
<pre ref={ref} className={`shiki h-full github-light dark:vitesse-dark`}>
136+
<code>{lang === 'mermaid' ? <svg /> : code}</code>
137+
</pre>,
138+
)
139+
140+
React[
141+
typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect'
142+
](() => {
143+
;(async () => {
144+
const themes = ['github-light', 'vitesse-dark']
145+
const langStr = lang || 'plaintext'
146+
const normalizedLang = LANG_ALIASES[langStr] || langStr
147+
const effectiveLang =
148+
normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
149+
150+
const highlighter = await getHighlighter(langStr)
151+
// Trim trailing newlines to prevent empty lines at end of code block
152+
const trimmedCode = (code || '').trimEnd()
153+
154+
const htmls = await Promise.all(
155+
themes.map(async (theme) => {
156+
const output = highlighter.codeToHtml(trimmedCode, {
157+
lang: effectiveLang,
158+
theme,
159+
transformers: [transformerNotationDiff()],
160+
})
161+
162+
if (lang === 'mermaid') {
163+
const preAttributes = extractPreAttributes(output)
164+
let svgHtml = genSvgMap.get(trimmedCode)
165+
if (!svgHtml) {
166+
const mermaid = await getMermaid()
167+
const { svg } = await mermaid.render('foo', trimmedCode)
168+
genSvgMap.set(trimmedCode, svg)
169+
svgHtml = svg
170+
}
171+
return `<div class='${preAttributes.class} py-4 bg-neutral-50'>${svgHtml}</div>`
172+
}
173+
174+
return output
175+
}),
176+
)
177+
178+
setCodeElement(
179+
<div
180+
className={twMerge(
181+
isEmbedded ? 'h-full [&>pre]:h-full [&>pre]:rounded-none' : '',
182+
)}
183+
dangerouslySetInnerHTML={{ __html: htmls.join('') }}
184+
ref={ref}
185+
/>,
186+
)
187+
})()
188+
}, [code, lang])
189+
190+
return (
191+
<div
192+
className={twMerge(
193+
'codeblock w-full max-w-full relative not-prose border border-gray-500/20 rounded-md [&_pre]:rounded-md',
194+
props.className,
195+
)}
196+
style={props.style}
197+
>
198+
{(title || showTypeCopyButton) && (
199+
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 dark:bg-gray-900">
200+
<div className="text-xs text-gray-700 dark:text-gray-300">
201+
{title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
202+
</div>
203+
204+
<Button
205+
className={twMerge('border-0 rounded-md transition-opacity')}
206+
onClick={() => {
207+
let copyContent =
208+
typeof ref.current?.innerText === 'string'
209+
? ref.current.innerText
210+
: ''
211+
212+
if (copyContent.endsWith('\n')) {
213+
copyContent = copyContent.slice(0, -1)
214+
}
215+
216+
navigator.clipboard.writeText(copyContent)
217+
setCopied(true)
218+
setTimeout(() => setCopied(false), 2000)
219+
notify(
220+
<div className="flex flex-col">
221+
<span className="font-medium">Copied code</span>
222+
<span className="text-gray-500 dark:text-gray-400 text-xs">
223+
Code block copied to clipboard
224+
</span>
225+
</div>,
226+
)
227+
}}
228+
aria-label="Copy code to clipboard"
229+
>
230+
{copied ? (
231+
<span className="text-xs">Copied!</span>
232+
) : (
233+
<Copy className="w-4 h-4" />
234+
)}
235+
</Button>
236+
</div>
237+
)}
238+
{codeElement}
239+
</div>
240+
)
241+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as React from 'react'
2+
3+
export type FileTabDefinition = {
4+
slug: string
5+
name: string
6+
}
7+
8+
export type FileTabsProps = {
9+
tabs: Array<FileTabDefinition>
10+
children: Array<React.ReactNode> | React.ReactNode
11+
id: string
12+
}
13+
14+
export function FileTabs({ tabs, id, children }: FileTabsProps) {
15+
const childrenArray = React.Children.toArray(children)
16+
const [activeSlug, setActiveSlug] = React.useState(tabs[0]?.slug ?? '')
17+
18+
if (tabs.length === 0) return null
19+
20+
return (
21+
<div className="not-prose my-4">
22+
<div className="flex items-center justify-start gap-0 overflow-x-auto overflow-y-hidden bg-gray-100 dark:bg-gray-900 border border-b-0 border-gray-500/20 rounded-t-md">
23+
{tabs.map((tab) => (
24+
<button
25+
key={`${id}-${tab.slug}`}
26+
type="button"
27+
onClick={() => setActiveSlug(tab.slug)}
28+
aria-label={tab.name}
29+
title={tab.name}
30+
className={`px-3 py-1.5 text-sm font-medium transition-colors border-b-2 -mb-[1px] ${
31+
activeSlug === tab.slug
32+
? 'border-current text-current bg-white dark:bg-gray-950'
33+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-800'
34+
}`}
35+
>
36+
{tab.name}
37+
</button>
38+
))}
39+
</div>
40+
<div>
41+
{childrenArray.map((child, index) => {
42+
const tab = tabs[index]
43+
if (!tab) return null
44+
return (
45+
<div
46+
key={`${id}-${tab.slug}-panel`}
47+
data-tab={tab.slug}
48+
hidden={tab.slug !== activeSlug}
49+
className="file-tabs-panel"
50+
>
51+
{child}
52+
</div>
53+
)
54+
})}
55+
</div>
56+
</div>
57+
)
58+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useLocalCurrentFramework } from '../FrameworkSelect'
2+
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
3+
import { useParams } from '@tanstack/react-router'
4+
import { Tabs } from './Tabs'
5+
import type { Framework } from '~/libraries/types'
6+
import { Children, ReactNode } from 'react'
7+
8+
type CodeBlockMeta = {
9+
title: string
10+
code: string
11+
language: string
12+
}
13+
14+
type FrameworkContentProps = {
15+
id: string
16+
codeBlocksByFramework: Record<string, CodeBlockMeta[]>
17+
availableFrameworks: string[]
18+
/** Pre-rendered React children for each framework (from domToReact) */
19+
panelsByFramework: Record<string, ReactNode>
20+
}
21+
22+
/**
23+
* Renders content for the currently selected framework.
24+
* - If no content for framework: shows nothing
25+
* - If 1 code block: shows just the code block (minimal style)
26+
* - If multiple code blocks: shows as file tabs
27+
* - If no code blocks but has content: shows the content directly
28+
*/
29+
export function FrameworkContent({
30+
id,
31+
codeBlocksByFramework,
32+
panelsByFramework,
33+
}: FrameworkContentProps) {
34+
const { framework: paramsFramework } = useParams({ strict: false })
35+
const localCurrentFramework = useLocalCurrentFramework()
36+
const userQuery = useCurrentUserQuery()
37+
const userFramework = userQuery.data?.lastUsedFramework
38+
39+
const actualFramework = (paramsFramework ||
40+
userFramework ||
41+
localCurrentFramework.currentFramework ||
42+
'react') as Framework
43+
44+
const normalizedFramework = actualFramework.toLowerCase()
45+
46+
// Find the framework's code blocks
47+
const frameworkBlocks = codeBlocksByFramework[normalizedFramework] || []
48+
const frameworkPanel = panelsByFramework[normalizedFramework]
49+
50+
// If no panel content at all for this framework, show nothing
51+
if (!frameworkPanel) {
52+
return null
53+
}
54+
55+
// If no code blocks, just render the content directly
56+
if (frameworkBlocks.length === 0) {
57+
return <div className="framework-content">{frameworkPanel}</div>
58+
}
59+
60+
// If 1 code block, render minimal style
61+
if (frameworkBlocks.length === 1) {
62+
return <div className="framework-content">{frameworkPanel}</div>
63+
}
64+
65+
// Multiple code blocks - show as file tabs
66+
const tabs = frameworkBlocks.map((block, index) => ({
67+
slug: `file-${index}`,
68+
name: block.title || 'Untitled',
69+
}))
70+
71+
const childrenArray = Children.toArray(frameworkPanel)
72+
73+
return (
74+
<div className="framework-content">
75+
<Tabs id={`${id}-${normalizedFramework}`} tabs={tabs} variant="files">
76+
{childrenArray}
77+
</Tabs>
78+
</div>
79+
)
80+
}

0 commit comments

Comments
 (0)