FileExplorer.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <script setup lang="ts">
  2. import { ref, computed, watchEffect, onMounted, onUnmounted, inject, watch } from 'vue'
  3. import JSZip from 'jszip'
  4. import FileTreeNode from './FileTreeNode.vue'
  5. import type { File, TreeNode } from './types'
  6. interface Props {
  7. /**
  8. * List of files to display in the explorer.
  9. * Example: [{ name: 'xmake.lua', code: '...' }, { name: 'src/main.cpp', code: '...' }]
  10. * If not provided, will try to load from global 'codes' data using 'project' prop.
  11. */
  12. files?: File[]
  13. /**
  14. * Project directory path to load from global codes data.
  15. * Example: 'examples/cpp/basic_console'
  16. * Used only if 'files' prop is not provided.
  17. */
  18. rootFilesDir?: string
  19. /**
  20. * Whether to show the fullscreen toggle button.
  21. * @default true
  22. */
  23. showFullscreen?: boolean
  24. /**
  25. * Whether the sidebar should be open initially.
  26. * @default true
  27. */
  28. initialSidebarOpen?: boolean
  29. /**
  30. * Path of the file to open by default.
  31. * Example: 'src/main.cpp'
  32. * If not provided, defaults to 'xmake.lua' or the first file.
  33. */
  34. defaultOpenPath?: string
  35. /**
  36. * Name of the root folder to display in the sidebar header.
  37. * Example: 'my_project'
  38. * @default 'EXPLORER'
  39. */
  40. rootPath?: string
  41. /**
  42. * Height of the explorer container.
  43. * Can be a CSS value or 'auto' to fit content.
  44. * Examples: '500px', '40vh', 'auto'
  45. * @default 'auto'
  46. */
  47. height?: string
  48. /**
  49. * Aspect ratio of the explorer container.
  50. * If provided, overrides height.
  51. * Examples: '16/9', '4/3'
  52. */
  53. aspectRatio?: string
  54. /**
  55. * Title to display in a bar above the explorer content.
  56. * Example: 'Console Application Example'
  57. */
  58. title?: string
  59. /**
  60. * Width of the sidebar.
  61. * Can be a CSS value or 'auto' to fit content.
  62. * @default 'auto'
  63. */
  64. sidebarWidth?: string
  65. /**
  66. * Whether to show line numbers in the code view.
  67. * @default true
  68. */
  69. showLineNumbers?: boolean
  70. /**
  71. * Highlight configuration for files.
  72. * Map of filename to highlight ranges string.
  73. * Example:
  74. * {
  75. * 'src/main.cpp': '5-7',
  76. * 'xmake.lua': '1-3, 5'
  77. * }
  78. */
  79. highlights?: Record<string, string>
  80. }
  81. const props = withDefaults(defineProps<Props>(), {
  82. showFullscreen: true,
  83. initialSidebarOpen: true,
  84. height: 'auto',
  85. sidebarWidth: 'auto',
  86. showLineNumbers: true
  87. })
  88. const globalCodes = inject<Record<string, File[]>>('codes')
  89. const effectiveFiles = computed(() => {
  90. if (props.files && props.files.length > 0) {
  91. return props.files
  92. }
  93. if (props.rootFilesDir && globalCodes && globalCodes[props.rootFilesDir]) {
  94. return globalCodes[props.rootFilesDir]
  95. }
  96. return []
  97. })
  98. const activeFileIndex = ref(0)
  99. const tree = ref<TreeNode[]>([])
  100. const copied = ref(false)
  101. const downloaded = ref(false)
  102. const isFullscreen = ref(false)
  103. const isSidebarOpen = ref(props.initialSidebarOpen)
  104. const containerRef = ref<HTMLElement | null>(null)
  105. const shared = ref(false)
  106. // Initialize active file index (prefer defaultOpenPath -> xmake.lua -> first file)
  107. const initActiveFileIndex = () => {
  108. const files = effectiveFiles.value
  109. if (files.length === 0) return
  110. // 1. Try defaultOpenPath prop
  111. if (props.defaultOpenPath) {
  112. const index = files.findIndex(f => f.name === props.defaultOpenPath)
  113. if (index !== -1) {
  114. activeFileIndex.value = index
  115. return
  116. }
  117. }
  118. // 2. Try xmake.lua
  119. const xmakeIndex = files.findIndex(f => f.name === 'xmake.lua' || f.name.endsWith('/xmake.lua'))
  120. if (xmakeIndex !== -1) {
  121. activeFileIndex.value = xmakeIndex
  122. } else {
  123. activeFileIndex.value = 0
  124. }
  125. }
  126. // Call immediately to set default state
  127. initActiveFileIndex()
  128. // Build tree from files
  129. const buildTree = (files: File[]) => {
  130. const root: TreeNode[] = []
  131. files.forEach((file) => {
  132. const parts = file.name.split('/')
  133. let currentLevel = root
  134. parts.forEach((part, partIndex) => {
  135. const isFile = partIndex === parts.length - 1
  136. const existingNode = currentLevel.find(node => node.name === part)
  137. if (existingNode) {
  138. if (isFile) {
  139. existingNode.fileData = file
  140. } else {
  141. currentLevel = existingNode.children!
  142. }
  143. } else {
  144. const newNode: TreeNode = {
  145. name: part,
  146. path: parts.slice(0, partIndex + 1).join('/'),
  147. type: isFile ? 'file' : 'folder',
  148. children: isFile ? undefined : [],
  149. isOpen: true, // Default open folders
  150. fileData: isFile ? file : undefined
  151. }
  152. currentLevel.push(newNode)
  153. if (!isFile) {
  154. currentLevel = newNode.children!
  155. }
  156. }
  157. })
  158. })
  159. // Sort: Folders first, then files
  160. const sortNodes = (nodes: TreeNode[]) => {
  161. nodes.sort((a, b) => {
  162. if (a.type === b.type) return a.name.localeCompare(b.name)
  163. return a.type === 'folder' ? -1 : 1
  164. })
  165. nodes.forEach(node => {
  166. if (node.children) sortNodes(node.children)
  167. })
  168. }
  169. sortNodes(root)
  170. return root
  171. }
  172. watchEffect(() => {
  173. tree.value = buildTree(effectiveFiles.value)
  174. // If files change (e.g. project loaded), reset/re-init active file
  175. if (effectiveFiles.value.length > 0 && activeFileIndex.value >= effectiveFiles.value.length) {
  176. initActiveFileIndex()
  177. }
  178. })
  179. const activeFile = computed(() => {
  180. if (effectiveFiles.value.length === 0) return { name: '', code: '', language: '' } as File
  181. return effectiveFiles.value[activeFileIndex.value] || effectiveFiles.value[0]
  182. })
  183. const activeHighlights = computed(() => {
  184. if (!props.highlights || !activeFile.value) return new Set<number>()
  185. const rangeStr = props.highlights[activeFile.value.name]
  186. if (!rangeStr) return new Set<number>()
  187. const lines = new Set<number>()
  188. const ranges = rangeStr.split(',').map(s => s.trim())
  189. for (const range of ranges) {
  190. if (range.includes('-')) {
  191. const [start, end] = range.split('-').map(Number)
  192. if (!isNaN(start) && !isNaN(end)) {
  193. for (let i = start; i <= end; i++) lines.add(i)
  194. }
  195. } else {
  196. const line = Number(range)
  197. if (!isNaN(line)) lines.add(line)
  198. }
  199. }
  200. return lines
  201. })
  202. const lineCount = computed(() => {
  203. if (!activeFile.value.code) return 0
  204. const lines = activeFile.value.code.split('\n')
  205. // Ignore the last empty line if it exists, as editors usually don't count it
  206. if (lines.length > 0 && lines[lines.length - 1] === '') {
  207. return lines.length - 1
  208. }
  209. return lines.length
  210. })
  211. const sidebarStyle = computed(() => {
  212. if (props.sidebarWidth === 'auto') {
  213. return {
  214. width: 'auto',
  215. minWidth: '200px',
  216. maxWidth: '40%'
  217. }
  218. }
  219. return { width: props.sidebarWidth }
  220. })
  221. const selectFile = (file: File) => {
  222. const index = effectiveFiles.value.findIndex(f => f.name === file.name)
  223. if (index !== -1) {
  224. activeFileIndex.value = index
  225. copied.value = false
  226. }
  227. }
  228. const toggleFolder = (node: TreeNode) => {
  229. node.isOpen = !node.isOpen
  230. }
  231. const copyCode = async () => {
  232. try {
  233. await navigator.clipboard.writeText(activeFile.value.code)
  234. copied.value = true
  235. setTimeout(() => {
  236. copied.value = false
  237. }, 2000)
  238. } catch (e) {
  239. console.error('Failed to copy:', e)
  240. }
  241. }
  242. const toggleFullscreen = () => {
  243. isFullscreen.value = !isFullscreen.value
  244. if (isFullscreen.value) {
  245. document.body.style.overflow = 'hidden'
  246. } else {
  247. document.body.style.overflow = ''
  248. }
  249. }
  250. const downloadZip = async () => {
  251. try {
  252. const zip = new JSZip()
  253. effectiveFiles.value.forEach(file => {
  254. zip.file(file.name, file.code)
  255. })
  256. const blob = await zip.generateAsync({ type: 'blob' })
  257. const url = URL.createObjectURL(blob)
  258. const a = document.createElement('a')
  259. a.href = url
  260. a.download = 'example.zip'
  261. document.body.appendChild(a)
  262. a.click()
  263. document.body.removeChild(a)
  264. URL.revokeObjectURL(url)
  265. downloaded.value = true
  266. setTimeout(() => {
  267. downloaded.value = false
  268. }, 2000)
  269. } catch (e) {
  270. console.error('Failed to download zip:', e)
  271. }
  272. }
  273. const explorerId = computed(() => props.id || props.rootFilesDir)
  274. const shareLink = async () => {
  275. try {
  276. const url = new URL(window.location.href)
  277. // Add file path as hash param to be somewhat compatible with potential future routing
  278. // Using query param for simplicity in static site
  279. url.searchParams.set('file', activeFile.value.name)
  280. if (explorerId.value) {
  281. url.searchParams.set('explorer_id', explorerId.value)
  282. }
  283. await navigator.clipboard.writeText(url.toString())
  284. shared.value = true
  285. setTimeout(() => {
  286. shared.value = false
  287. }, 2000)
  288. } catch (e) {
  289. console.error('Failed to share:', e)
  290. }
  291. }
  292. // Handle ESC to exit fullscreen
  293. const handleKeydown = (e: KeyboardEvent) => {
  294. if (e.key === 'Escape' && isFullscreen.value) {
  295. toggleFullscreen()
  296. }
  297. }
  298. const toggleSidebar = () => {
  299. isSidebarOpen.value = !isSidebarOpen.value
  300. }
  301. const containerStyle = computed(() => {
  302. if (isFullscreen.value) return {}
  303. const style: Record<string, string> = {}
  304. if (props.aspectRatio) {
  305. style.aspectRatio = props.aspectRatio
  306. style.height = 'auto' // Allow aspect-ratio to determine height
  307. } else {
  308. style.height = props.height
  309. }
  310. // When height is auto, we need to ensure the container allows expansion
  311. if (style.height === 'auto') {
  312. style.minHeight = '200px' // Reasonable minimum
  313. }
  314. return style
  315. })
  316. const urlParamsHandled = ref(false)
  317. const checkUrlParams = () => {
  318. if (urlParamsHandled.value) return
  319. if (effectiveFiles.value.length === 0) return
  320. if (typeof window !== 'undefined') {
  321. const params = new URLSearchParams(window.location.search)
  322. const fileName = params.get('file')
  323. const targetExplorerId = params.get('explorer_id')
  324. // First try to set default to xmake.lua
  325. initActiveFileIndex()
  326. if (fileName) {
  327. // If explorer_id param is present, only process if it matches our id
  328. if (targetExplorerId && targetExplorerId !== explorerId.value) {
  329. urlParamsHandled.value = true
  330. return
  331. }
  332. const index = effectiveFiles.value.findIndex(f => f.name === fileName)
  333. if (index !== -1) {
  334. activeFileIndex.value = index
  335. urlParamsHandled.value = true
  336. // Scroll to the component if a specific file was requested via URL
  337. setTimeout(() => {
  338. containerRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  339. }, 300)
  340. }
  341. } else {
  342. urlParamsHandled.value = true
  343. }
  344. }
  345. }
  346. watch(() => effectiveFiles.value, () => {
  347. checkUrlParams()
  348. })
  349. onMounted(() => {
  350. window.addEventListener('keydown', handleKeydown)
  351. checkUrlParams()
  352. })
  353. onUnmounted(() => {
  354. window.removeEventListener('keydown', handleKeydown)
  355. if (isFullscreen.value) {
  356. document.body.style.overflow = ''
  357. }
  358. })
  359. </script>
  360. <template>
  361. <div class="file-explorer-container" :class="{ fullscreen: isFullscreen }" ref="containerRef" :style="containerStyle">
  362. <div v-if="title" class="file-explorer-title-bar">
  363. {{ title }}
  364. </div>
  365. <div class="file-explorer-main-area">
  366. <div class="file-explorer-sidebar" v-if="isSidebarOpen" :style="sidebarStyle">
  367. <div class="explorer-header">{{ rootPath || 'EXPLORER' }}</div>
  368. <div class="explorer-tree">
  369. <template v-for="node in tree" :key="node.path">
  370. <div class="tree-node-wrapper">
  371. <FileTreeNode
  372. :node="node"
  373. :active-file="activeFile"
  374. @select-file="selectFile"
  375. @toggle-folder="toggleFolder"
  376. />
  377. </div>
  378. </template>
  379. </div>
  380. </div>
  381. <div class="file-explorer-content">
  382. <div class="editor-tabs">
  383. <button class="action-button sidebar-toggle" @click="toggleSidebar" :title="isSidebarOpen ? 'Hide Sidebar' : 'Show Sidebar'">
  384. <svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
  385. </button>
  386. <div class="editor-tab active">
  387. <span class="file-icon-sm">{{ activeFile.name.endsWith('.cpp') ? 'C++' : (activeFile.name.endsWith('.lua') ? 'Lua' : 'File') }}</span>
  388. {{ activeFile.name.split('/').pop() }}
  389. </div>
  390. <div class="spacer"></div>
  391. <button class="action-button" :class="{ active: downloaded }" @click="downloadZip" :title="downloaded ? 'Downloaded' : 'Download Zip'">
  392. <svg v-if="!downloaded" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
  393. <svg v-else class="check-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
  394. </button>
  395. <button class="action-button" :class="{ active: shared }" @click="shareLink" :title="shared ? 'Link Copied' : 'Share Link'">
  396. <svg v-if="!shared" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>
  397. <svg v-else class="check-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
  398. </button>
  399. <button v-if="showFullscreen" class="action-button" @click="toggleFullscreen" :title="isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'">
  400. <svg v-if="!isFullscreen" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
  401. <svg v-else width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>
  402. </button>
  403. <button class="action-button" :class="{ active: copied }" @click="copyCode" :title="copied ? 'Copied' : 'Copy Code'">
  404. <svg v-if="!copied" class="copy-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
  405. <svg v-else class="check-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
  406. </button>
  407. </div>
  408. <div class="code-viewport">
  409. <div class="code-layout">
  410. <div v-if="showLineNumbers" class="line-numbers">
  411. <span v-for="n in lineCount" :key="n" class="line-number">{{ n }}</span>
  412. </div>
  413. <div class="code-wrapper">
  414. <div class="highlight-layer" aria-hidden="true">
  415. <div
  416. v-for="n in lineCount"
  417. :key="n"
  418. class="highlight-line"
  419. :class="{ 'highlighted': activeHighlights.has(n) }"
  420. ></div>
  421. </div>
  422. <!-- Use shiki highlighted code -->
  423. <div v-if="activeFile.highlightedCode" class="file-explorer-shiki" v-html="activeFile.highlightedCode"></div>
  424. <pre v-else><code>{{ activeFile.code }}</code></pre>
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. </div>
  431. </template>
  432. <style scoped>
  433. .file-explorer-container {
  434. display: flex;
  435. flex-direction: column;
  436. height: 500px;
  437. border: 1px solid var(--vp-c-divider);
  438. border-radius: 8px;
  439. background-color: var(--vp-c-bg);
  440. overflow: hidden;
  441. font-size: 13px;
  442. margin: 16px 0;
  443. transition: all 0.3s ease;
  444. }
  445. .file-explorer-container.fullscreen {
  446. position: fixed;
  447. top: 0;
  448. left: 0;
  449. width: 100vw;
  450. height: 100vh;
  451. z-index: 1000;
  452. border-radius: 0;
  453. border: none;
  454. margin: 0;
  455. }
  456. .file-explorer-title-bar {
  457. padding: 10px 16px;
  458. font-size: 13px;
  459. font-weight: 600;
  460. color: var(--vp-c-text-1);
  461. background-color: var(--vp-c-bg-alt);
  462. border-bottom: 1px solid var(--vp-c-divider);
  463. display: flex;
  464. align-items: center;
  465. }
  466. .file-explorer-main-area {
  467. display: flex;
  468. flex: 1;
  469. min-height: 0;
  470. }
  471. .file-explorer-sidebar {
  472. width: 240px;
  473. border-right: 1px solid var(--vp-c-divider);
  474. background-color: var(--vp-c-bg-alt);
  475. display: flex;
  476. flex-direction: column;
  477. }
  478. .explorer-header {
  479. padding: 8px 16px;
  480. font-size: 11px;
  481. font-weight: 600;
  482. color: var(--vp-c-text-2);
  483. text-transform: uppercase;
  484. letter-spacing: 0.5px;
  485. }
  486. .explorer-tree {
  487. flex: 1;
  488. overflow: auto;
  489. padding-bottom: 10px;
  490. }
  491. .file-explorer-content {
  492. flex: 1;
  493. display: flex;
  494. flex-direction: column;
  495. min-width: 0;
  496. background-color: var(--vp-code-block-bg);
  497. /* Ensure content can determine height when container is auto */
  498. min-height: 0;
  499. }
  500. .editor-tabs {
  501. display: flex;
  502. background-color: var(--vp-c-bg-alt);
  503. border-bottom: 1px solid var(--vp-c-divider);
  504. height: 36px;
  505. overflow-x: auto;
  506. }
  507. .editor-tab {
  508. padding: 0 16px;
  509. display: flex;
  510. align-items: center;
  511. gap: 6px;
  512. border-right: 1px solid var(--vp-c-divider);
  513. background-color: var(--vp-c-bg-mute);
  514. color: var(--vp-c-text-2);
  515. cursor: pointer;
  516. font-size: 13px;
  517. min-width: 120px;
  518. }
  519. .editor-tab.active {
  520. background-color: var(--vp-code-block-bg);
  521. color: var(--vp-c-text-1);
  522. border-top: 2px solid var(--vp-c-brand);
  523. }
  524. .spacer {
  525. flex: 1;
  526. }
  527. .action-button {
  528. display: flex;
  529. align-items: center;
  530. justify-content: center;
  531. width: 36px;
  532. height: 36px;
  533. color: var(--vp-c-text-2);
  534. cursor: pointer;
  535. transition: all 0.2s;
  536. background-color: transparent;
  537. border: none;
  538. border-left: 1px solid var(--vp-c-divider);
  539. }
  540. .action-button:hover {
  541. color: var(--vp-c-text-1);
  542. background-color: var(--vp-c-bg-mute);
  543. }
  544. .action-button.active {
  545. color: var(--vp-c-green-1);
  546. }
  547. .sidebar-toggle {
  548. border-left: none;
  549. border-right: 1px solid var(--vp-c-divider);
  550. }
  551. .file-icon-sm {
  552. font-size: 10px;
  553. opacity: 0.7;
  554. }
  555. .code-viewport {
  556. flex: 1;
  557. overflow: auto;
  558. padding: 0;
  559. }
  560. .code-layout {
  561. display: flex;
  562. min-height: 100%;
  563. }
  564. .line-numbers {
  565. flex-shrink: 0;
  566. display: flex;
  567. flex-direction: column;
  568. padding: 20px 0 20px 10px;
  569. background-color: var(--vp-code-block-bg);
  570. user-select: none;
  571. text-align: right;
  572. min-width: 40px;
  573. }
  574. .line-number {
  575. font-family: var(--vp-font-family-mono);
  576. font-size: 14px;
  577. line-height: 24px;
  578. height: 24px;
  579. color: var(--vp-c-text-3);
  580. padding-right: 10px;
  581. }
  582. .code-wrapper {
  583. flex-grow: 1;
  584. overflow-x: auto;
  585. display: grid;
  586. }
  587. .highlight-layer {
  588. grid-area: 1 / 1;
  589. width: 100%;
  590. display: flex;
  591. flex-direction: column;
  592. padding: 20px 0;
  593. pointer-events: none;
  594. z-index: 0;
  595. font-size: 14px;
  596. line-height: 24px;
  597. font-family: var(--vp-font-family-mono);
  598. }
  599. .highlight-line {
  600. width: 100%;
  601. height: 24px;
  602. }
  603. .highlight-line.highlighted {
  604. background-color: var(--vp-code-line-highlight-color, rgba(142, 150, 170, 0.14));
  605. border-left: 2px solid var(--vp-c-brand);
  606. }
  607. .file-explorer-shiki,
  608. .code-wrapper > pre {
  609. grid-area: 1 / 1;
  610. position: relative;
  611. z-index: 1;
  612. }
  613. @media (max-width: 768px) {
  614. .file-explorer-main-area {
  615. flex-direction: column;
  616. }
  617. .file-explorer-sidebar {
  618. width: 100% !important;
  619. min-width: 100% !important;
  620. max-width: 100% !important;
  621. height: auto;
  622. max-height: 200px;
  623. border-right: none;
  624. border-bottom: 1px solid var(--vp-c-divider);
  625. }
  626. .file-explorer-content {
  627. height: 100%;
  628. }
  629. }
  630. </style>
  631. <style>
  632. /* Global styles for Shiki in FileExplorer to avoid scoped CSS issues */
  633. .file-explorer-shiki pre.shiki {
  634. margin: 0 !important;
  635. padding: 20px !important;
  636. background-color: transparent !important;
  637. border-radius: 0 !important;
  638. overflow: visible !important;
  639. width: fit-content;
  640. min-width: 100%;
  641. height: 100%;
  642. font-family: var(--vp-font-family-mono) !important;
  643. font-size: 14px !important;
  644. line-height: 24px !important;
  645. }
  646. .file-explorer-shiki code {
  647. font-family: var(--vp-font-family-mono) !important;
  648. font-size: 14px !important;
  649. line-height: 24px !important;
  650. }
  651. /* Dark mode overrides */
  652. html.dark .file-explorer-shiki .shiki,
  653. html.dark .file-explorer-shiki .shiki span {
  654. color: var(--shiki-dark) !important;
  655. font-style: var(--shiki-dark-font-style) !important;
  656. font-weight: var(--shiki-dark-font-weight) !important;
  657. text-decoration: var(--shiki-dark-text-decoration) !important;
  658. }
  659. </style>