chore: replace local components with mdx-components lib

feat: directory selection working
feat: theme toggle working
This commit is contained in:
andres
2023-10-17 17:31:31 +02:00
parent ca68482061
commit 99ce64db38
31 changed files with 791 additions and 990 deletions

View File

@@ -24,10 +24,11 @@
"@electron-toolkit/utils": "^2.0.0",
"@fontsource/roboto": "^5.0.8",
"@it-incubator/md-bundler": "0.0.8",
"@it-incubator/mdx-components": "0.0.3",
"@it-incubator/ui-kit": "0.2.17",
"@it-incubator/mdx-components": "0.0.4",
"@it-incubator/ui-kit": "0.2.18",
"builtin-modules": "^3.3.0",
"chokidar": "^3.5.3",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.1",
"esbuild": "^0.19.3",
"mdx-bundler": "^9.2.1",

131
pnpm-lock.yaml generated
View File

@@ -18,17 +18,20 @@ dependencies:
specifier: 0.0.8
version: 0.0.8
'@it-incubator/mdx-components':
specifier: 0.0.3
version: 0.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(esbuild@0.19.3)(react-dom@18.2.0)(react@18.2.0)
specifier: 0.0.4
version: 0.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(esbuild@0.19.3)(react-dom@18.2.0)(react@18.2.0)
'@it-incubator/ui-kit':
specifier: 0.2.17
version: 0.2.17(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react-toastify@9.1.3)(react@18.2.0)
specifier: 0.2.18
version: 0.2.18(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react-toastify@9.1.3)(react@18.2.0)
builtin-modules:
specifier: ^3.3.0
version: 3.3.0
chokidar:
specifier: ^3.5.3
version: 3.5.3
electron-store:
specifier: ^8.1.0
version: 8.1.0
electron-updater:
specifier: ^6.1.1
version: 6.1.4
@@ -1224,8 +1227,8 @@ packages:
- supports-color
dev: false
/@it-incubator/mdx-components@0.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(esbuild@0.19.3)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-S8L7MtXkgPgTCclmMxgeHUxk/6a3W6cbzZX9Q4mXfvnBaSVh2SJrVqq9YdxQQkEK+0q2Vu04BoTLyruD5ewunw==}
/@it-incubator/mdx-components@0.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(esbuild@0.19.3)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GEqGlg/5u9Tu1xahabRMEKv2g/FxMEhWhMmdIYclJDCqTJfguUkmIWeYjKgJVgJFqxeuJQNpzd6D850RuviEJQ==}
peerDependencies:
react: '>=18.0.2'
react-dom: '>=18.0.2'
@@ -1269,8 +1272,8 @@ packages:
- typescript
dev: true
/@it-incubator/ui-kit@0.2.17(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react-toastify@9.1.3)(react@18.2.0):
resolution: {integrity: sha512-x3V3dqh2HY/wJZJ8hhO0c+5C0hiUxz/dhdkWoaDrqqaiAUGb+laj7uzh8SFJ7noWwXT4uFWX57cPnlzvdUqieA==}
/@it-incubator/ui-kit@0.2.18(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react-toastify@9.1.3)(react@18.2.0):
resolution: {integrity: sha512-xmDIEKCfkiBFVg+k1k5065Cw08/9fL39LkdgzyJ7nSbYENVW4gLw1m57jOuh0PvzzVOk1qTkF869Z9rgbPfm6Q==}
peerDependencies:
react: '>=18.0.2'
react-dom: '>=18.0.2'
@@ -2943,6 +2946,17 @@ packages:
indent-string: 4.0.0
dev: false
/ajv-formats@2.1.1(ajv@8.12.0):
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
dependencies:
ajv: 8.12.0
dev: false
/ajv-keywords@3.5.2(ajv@6.12.6):
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
peerDependencies:
@@ -2967,7 +2981,6 @@ packages:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
dev: true
/ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
@@ -3187,6 +3200,11 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
/atomically@1.7.0:
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
engines: {node: '>=10.12.0'}
dev: false
/attr-accept@2.2.2:
resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
engines: {node: '>=4'}
@@ -3658,6 +3676,22 @@ packages:
typedarray: 0.0.6
dev: false
/conf@10.2.0:
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
engines: {node: '>=12'}
dependencies:
ajv: 8.12.0
ajv-formats: 2.1.1(ajv@8.12.0)
atomically: 1.7.0
debounce-fn: 4.0.0
dot-prop: 6.0.1
env-paths: 2.2.1
json-schema-typed: 7.0.3
onetime: 5.1.2
pkg-up: 3.1.0
semver: 7.5.4
dev: false
/config-file-ts@0.2.4:
resolution: {integrity: sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==}
dependencies:
@@ -3766,6 +3800,13 @@ packages:
'@babel/runtime': 7.22.15
dev: false
/debounce-fn@4.0.0:
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
engines: {node: '>=10'}
dependencies:
mimic-fn: 3.1.0
dev: false
/debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
dev: false
@@ -3983,6 +4024,13 @@ packages:
esutils: 2.0.3
dev: true
/dot-prop@6.0.1:
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
engines: {node: '>=10'}
dependencies:
is-obj: 2.0.0
dev: false
/dotenv-expand@5.1.0:
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
dev: true
@@ -4034,6 +4082,13 @@ packages:
- supports-color
dev: true
/electron-store@8.1.0:
resolution: {integrity: sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==}
dependencies:
conf: 10.2.0
type-fest: 2.19.0
dev: false
/electron-to-chromium@1.4.523:
resolution: {integrity: sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==}
@@ -4723,7 +4778,6 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
@@ -4815,6 +4869,13 @@ packages:
resolution: {integrity: sha512-wRkO8crYqjaTvCnqEfQGuV8LOp4JO0Ctjn6qROGPcradK+6jQ7giLMGLnKlNxQm6dEdYD3/TBABQ7Xi/5ZhWcg==}
dev: false
/find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
dependencies:
locate-path: 3.0.0
dev: false
/find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@@ -5770,6 +5831,11 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
/is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
dev: false
/is-obj@3.0.0:
resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==}
engines: {node: '>=12'}
@@ -5970,7 +6036,10 @@ packages:
/json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
dev: true
/json-schema-typed@7.0.3:
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
dev: false
/json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -6064,6 +6133,14 @@ packages:
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
/locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
dev: false
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -7090,6 +7167,11 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
/mimic-fn@3.1.0:
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
engines: {node: '>=8'}
dev: false
/mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
@@ -7365,6 +7447,13 @@ packages:
yocto-queue: 0.1.0
dev: true
/p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
dependencies:
p-limit: 2.3.0
dev: false
/p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
@@ -7440,6 +7529,11 @@ packages:
engines: {node: '>=10'}
dev: false
/path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
dev: false
/path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -7495,6 +7589,13 @@ packages:
find-up: 4.1.0
dev: false
/pkg-up@3.1.0:
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
engines: {node: '>=8'}
dependencies:
find-up: 3.0.0
dev: false
/plist@3.1.0:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
@@ -7642,7 +7743,6 @@ packages:
/punycode@2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
dev: true
/querystring@0.2.0:
resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
@@ -8046,7 +8146,6 @@ packages:
/require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
dev: true
/resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -8898,6 +8997,11 @@ packages:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
/type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: false
/typed-array-buffer@1.0.0:
resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
engines: {node: '>= 0.4'}
@@ -9110,7 +9214,6 @@ packages:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
punycode: 2.3.0
dev: true
/url@0.10.3:
resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==}

View File

@@ -1 +0,0 @@
export const CODE_BLOCK_FILENAME_REGEX = /filename="([^"]+)"/

View File

@@ -3,11 +3,14 @@ import path from 'node:path'
import { join } from 'path'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { bundleMdx } from '@it-incubator/md-bundler'
import { bundleMdx, generateToc } from '@it-incubator/md-bundler'
import { BrowserWindow, app, ipcMain, shell } from 'electron'
import Store from 'electron-store'
import icon from '../../resources/icon.png?asset'
const chokidar = require('chokidar')
const store = new Store()
let mainWindow: BrowserWindow | null = null
@@ -35,6 +38,11 @@ function createWindow(): void {
return { action: 'deny' }
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
@@ -79,8 +87,10 @@ app.on('window-all-closed', () => {
})
let watcher: any = null
let currentContent: any = null
function setupWatcher(filePath: string) {
store.set('lastFilePath', filePath)
// Close the existing watcher if it exists
if (watcher) {
watcher.close()
@@ -101,13 +111,18 @@ function setupWatcher(filePath: string) {
// Send file content to renderer
if (mainWindow && !mainWindow.isDestroyed()) {
const bundled = await bundleMdx(content)
const toc = await generateToc(content, {})
const newContent = { ...bundled, fileName: path, toc }
mainWindow.webContents.send('file-changed', { ...bundled, fileName: path })
currentContent = newContent
mainWindow.webContents.send('current-content', newContent)
}
})
await shell.openPath(path)
// await shell.openPath(path)
}
bundleAndSend(filePath)
// Add your event listeners
watcher
.on('add', async (path: string) => {
@@ -119,15 +134,147 @@ function setupWatcher(filePath: string) {
.on('unlink', (path: string) => console.warn(`File ${path} has been removed`))
}
function prepareAndSendDir(dir: string) {
const files = fs.readdirSync(dir)
const dirName = path.basename(dir)
const data = [
{
children: getFilesRecursive(
dir,
['.md', '.mdx'],
['node_modules', 'README.md'],
true,
dir + '/'
),
name: dirName,
path: dir,
type: FsEntryType.Directory,
},
]
// Send the list of files to the renderer process
mainWindow?.webContents.send('directory-contents', { data, dir, dirName, files })
store.set('lastOpenDir', dir)
}
ipcMain.on('dropped-file', (event, arg) => {
console.warn('Dropped File(s):', arg)
event.returnValue = `Received ${arg.length} paths.` // Synchronous reply
// Assuming the user only dropped one file, update the watcher to watch that file.
event.returnValue = `Received ${arg.length} paths.`
if (!mainWindow) {
throw new Error('mainWindow is not defined')
}
if (arg.length > 0) {
setupWatcher(arg[0])
const pathToCheck = arg[0]
if (fs.statSync(pathToCheck).isDirectory()) {
// If it's a directory, get the list of files
prepareAndSendDir(pathToCheck)
} else {
setupWatcher(pathToCheck)
}
}
})
ipcMain.on('get-current-content', event => {
event.reply('current-content', currentContent)
})
ipcMain.on('get-current-dir', () => {
const lastOpenDir = store.get('lastOpenDir') as string | undefined
if (!lastOpenDir) {
return
}
prepareAndSendDir(lastOpenDir)
})
ipcMain.on('open-file', (_event, filePath) => {
setupWatcher(filePath)
})
const lastFilePath = store.get('lastFilePath') as string | undefined
const lastOpenDir = store.get('lastOpenDir') as string | undefined
if (lastOpenDir) {
prepareAndSendDir(lastOpenDir)
}
// Initially setup watcher for 'hello.md'
setupWatcher(path.resolve(__dirname, '../../../hello.md'))
setupWatcher(lastFilePath ?? path.resolve(__dirname, '../../../hello.md'))
function getFilesRecursive(
directory: string,
allowedExtensions: string[] = [],
ignoredPaths: string[] = [],
includeParent = true,
prefix = ''
): FileOrDirectory[] {
const fileList: FileOrDirectory[] = []
const filesAndDirs = fs.readdirSync(directory)
for (const fileOrDir of filesAndDirs) {
const absolutePath = path.join(directory, fileOrDir)
const relativePath = path.join(prefix, fileOrDir)
// Skip dotfiles and dot directories
if (fileOrDir.startsWith('.')) {
continue
}
// Skip ignored files and directories
if (ignoredPaths.some(ignoredPath => absolutePath.includes(ignoredPath))) {
continue
}
if (fs.statSync(absolutePath).isDirectory()) {
const nestedFiles = getFilesRecursive(
absolutePath,
allowedExtensions,
ignoredPaths,
includeParent,
relativePath + '/'
)
fileList.push({
children: nestedFiles,
name: fileOrDir,
path: relativePath,
type: FsEntryType.Directory,
})
} else {
const extension = path.extname(fileOrDir).toLowerCase()
// Check the file has an allowed extension
if (
allowedExtensions.length === 0 ||
allowedExtensions.map(e => e.toLowerCase()).includes(extension)
) {
fileList.push({
name: fileOrDir,
path: relativePath,
type: FsEntryType.File,
})
}
}
}
return fileList
}
enum FsEntryType {
Directory = 'directory',
File = 'file',
}
type File = {
name: string
path: string
type: FsEntryType.File
}
type Directory = {
children: Array<FileOrDirectory>
name: string
path: string
type: FsEntryType.Directory
}
type FileOrDirectory = Directory | File

View File

@@ -1,45 +0,0 @@
// @ts-nocheck
import { CODE_BLOCK_FILENAME_REGEX } from './constants.js'
function visit(node, tagNames, handler) {
if (tagNames.includes(node.tagName)) {
handler(node)
return
}
if ('children' in node) {
for (const n of node.children) {
visit(n, tagNames, handler)
}
}
}
export const parseMeta =
({ defaultShowCopyCode }) =>
tree => {
visit(tree, ['pre'], preEl => {
const [codeEl] = preEl.children
// Add default language `text` for code-blocks without languages
codeEl.properties.className ||= ['language-text']
const meta = codeEl.data?.meta
preEl.__nextra_filename = meta?.match(CODE_BLOCK_FILENAME_REGEX)?.[1]
preEl.__nextra_hasCopyCode = meta
? (defaultShowCopyCode && !/( |^)copy=false($| )/.test(meta)) || /( |^)copy($| )/.test(meta)
: defaultShowCopyCode
})
}
export const attachMeta = () => tree => {
visit(tree, ['div', 'pre'], node => {
if ('data-rehype-pretty-code-fragment' in node.properties) {
// remove <div data-rehype-pretty-code-fragment /> element that wraps <pre /> element
// because we'll wrap with our own <div />
Object.assign(node, node.children[0])
}
node.properties.filename = node.__nextra_filename
node.properties.hasCopyCode = node.__nextra_hasCopyCode
})
}

View File

@@ -1,224 +0,0 @@
{
"name": "css-variables",
"type": "css",
"colors": {
"editor.foreground": "#000001",
"editor.background": "#000002",
"terminal.ansiBlack": "#A00000",
"terminal.ansiRed": "#A00001",
"terminal.ansiGreen": "#A00002",
"terminal.ansiYellow": "#A00003",
"terminal.ansiBlue": "#A00004",
"terminal.ansiMagenta": "#A00005",
"terminal.ansiCyan": "#A00006",
"terminal.ansiWhite": "#A00007",
"terminal.ansiBrightBlack": "#A00008",
"terminal.ansiBrightRed": "#A00009",
"terminal.ansiBrightGreen": "#A00010",
"terminal.ansiBrightYellow": "#A00011",
"terminal.ansiBrightBlue": "#A00012",
"terminal.ansiBrightMagenta": "#A00013",
"terminal.ansiBrightCyan": "#A00014",
"terminal.ansiBrightWhite": "#A00015"
},
"tokenColors": [
{
"settings": {
"foreground": "#000001"
}
},
{
"scope": [
"markup.deleted",
"meta.diff.header.from-file",
"punctuation.definition.deleted"
],
"settings": {
"foreground": "#ef6270"
}
},
{
"scope": [
"markup.inserted",
"meta.diff.header.to-file",
"punctuation.definition.inserted"
],
"settings": {
"foreground": "#4bb74a"
}
},
{
"scope": [
"keyword.operator.accessor",
"meta.group.braces.round.function.arguments",
"meta.template.expression",
"markup.fenced_code meta.embedded.block"
],
"settings": {
"foreground": "#000001"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": [
"strong",
"markup.heading.markdown",
"markup.bold.markdown"
],
"settings": {
"fontStyle": "bold"
}
},
{
"scope": [
"markup.italic.markdown"
],
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "meta.link.inline.markdown",
"settings": {
"fontStyle": "underline",
"foreground": "#000004"
}
},
{
"scope": [
"string",
"markup.fenced_code",
"markup.inline"
],
"settings": {
"foreground": "#000005"
}
},
{
"scope": [
"comment",
"string.quoted.docstring.multi"
],
"settings": {
"foreground": "#000006"
}
},
{
"scope": [
"constant.numeric",
"constant.language",
"constant.other.placeholder",
"constant.character.format.placeholder",
"variable.language.this",
"variable.other.object",
"variable.other.class",
"variable.other.constant",
"meta.property-name",
"meta.property-value",
"support"
],
"settings": {
"foreground": "#000004"
}
},
{
"scope": [
"keyword",
"storage.modifier",
"storage.type",
"storage.control.clojure",
"entity.name.function.clojure",
"entity.name.tag.yaml",
"support.function.node",
"support.type.property-name.json",
"punctuation.separator.key-value",
"punctuation.definition.template-expression"
],
"settings": {
"foreground": "#000007"
}
},
{
"scope": "variable.parameter.function",
"settings": {
"foreground": "#000008"
}
},
{
"scope": [
"support.function",
"entity.name.type",
"entity.other.inherited-class",
"meta.function-call",
"meta.instance.constructor",
"entity.other.attribute-name",
"entity.name.function",
"constant.keyword.clojure"
],
"settings": {
"foreground": "#000009"
}
},
{
"scope": [
"entity.name.tag",
"string.quoted",
"string.regexp",
"string.interpolated",
"string.template",
"string.unquoted.plain.out.yaml",
"keyword.other.template"
],
"settings": {
"foreground": "#000010"
}
},
{
"scope": [
"punctuation.definition.arguments",
"punctuation.definition.dict",
"punctuation.separator",
"meta.function-call.arguments"
],
"settings": {
"foreground": "#000011"
}
},
{
"name": "[Custom] Markdown links",
"scope": [
"markup.underline.link",
"punctuation.definition.metadata.markdown"
],
"settings": {
"foreground": "#000012"
}
},
{
"name": "[Custom] Markdown list",
"scope": [
"beginning.punctuation.definition.list.markdown"
],
"settings": {
"foreground": "#000005"
}
},
{
"name": "[Custom] Markdown punctuation definition brackets",
"scope": [
"punctuation.definition.string.begin.markdown",
"punctuation.definition.string.end.markdown",
"string.other.link.title.markdown",
"string.other.link.description.markdown"
],
"settings": {
"foreground": "#000007"
}
}
]
}

View File

@@ -1,4 +0,0 @@
export type BundledMdx = {
code: string
frontmatter: Record<string, any>
}

View File

@@ -1,65 +1,16 @@
import { ComponentProps, ReactElement, useEffect, useState } from 'react'
import { useState } from 'react'
import { IpcRendererListener } from '@electron-toolkit/preload'
import * as components from '@it-incubator/mdx-components'
import { ImagePreview, Typography } from '@it-incubator/ui-kit'
import { getMDXComponent } from 'mdx-bundler/client'
import { View } from './components/view/view'
import { Layout } from './layout'
import s from './view.module.scss'
import { Pre } from './components/pre'
function App() {
const [code, setCode] = useState<string>('')
const [fileName, setFileName] = useState<string>('')
const [srcPreview, setSrcPreview] = useState<string>('')
useEffect(() => {
const listener: IpcRendererListener = (_event, content) => {
setCode(content.code)
setFileName(content.fileName)
}
window.electron.ipcRenderer.on('file-changed', listener)
return () => {
window.electron.ipcRenderer.removeAllListeners('file-changed')
}
}, [])
if (!code) {
return null
}
const Component = getMDXComponent(code, { components: components })
return (
<div>
<Typography.H1>{fileName}</Typography.H1>
<article className={s.root}>
<ImagePreview onClose={() => setSrcPreview('')} open={!!srcPreview} src={srcPreview} />
<Component
components={{
code: Code,
img: props => (
<img
{...props}
onClick={() => setSrcPreview(props.src || '')}
style={{ cursor: 'pointer' }}
/>
),
pre: Pre,
}}
/>
</article>
</div>
<Layout fileName={fileName}>
<View setFileName={setFileName} />
</Layout>
)
}
export default App
const Code = ({ children, ...props }: ComponentProps<'code'>): ReactElement => {
return (
<code className={s.inline} dir={'ltr'} {...props}>
{children}
</code>
)
}

View File

@@ -1,206 +1,13 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.code-toolbar {
position: relative;
}
.toolbar {
position: absolute;
top: 10px;
right: 20px;
}
.copy-to-clipboard-button {
cursor: pointer;
font-size: 20px;
background-color: #011727;
border: none;
}
a {
color: #4891f7;
text-decoration: underline;
}
:root {
--shiki-color-text: oklch(37.53% 0 0);
--shiki-color-background: hsl(var(--primary-hue) 100% 39% / 5%);
--shiki-token-constant: oklch(56.45% 0.163 253.27);
--shiki-token-string: oklch(54.64% 0.144 147.32);
--shiki-token-comment: oklch(73.8% 0 0);
--shiki-token-keyword: oklch(56.8% 0.2 26.41);
--shiki-token-parameter: oklch(77.03% 0.174 64.05);
--shiki-token-function: oklch(50.15% 0.188 294.99);
--shiki-token-string-expression: var(--shiki-token-string);
--shiki-token-punctuation: oklch(24.78% 0 0);
--shiki-token-link: var(--shiki-token-string);
/* from github-light */
--shiki-color-ansi-black: #24292e;
--shiki-color-ansi-black-dim: #24292e80;
--shiki-color-ansi-red: #d73a49;
--shiki-color-ansi-red-dim: #d73a4980;
--shiki-color-ansi-green: #28a745;
--shiki-color-ansi-green-dim: #28a74580;
--shiki-color-ansi-yellow: #dbab09;
--shiki-color-ansi-yellow-dim: #dbab0980;
--shiki-color-ansi-blue: #0366d6;
--shiki-color-ansi-blue-dim: #0366d680;
--shiki-color-ansi-magenta: #5a32a3;
--shiki-color-ansi-magenta-dim: #5a32a380;
--shiki-color-ansi-cyan: #1b7c83;
--shiki-color-ansi-cyan-dim: #1b7c8380;
--shiki-color-ansi-white: #6a737d;
--shiki-color-ansi-white-dim: #6a737d80;
--shiki-color-ansi-bright-black: #959da5;
--shiki-color-ansi-bright-black-dim: #959da580;
--shiki-color-ansi-bright-red: #cb2431;
--shiki-color-ansi-bright-red-dim: #cb243180;
--shiki-color-ansi-bright-green: #22863a;
--shiki-color-ansi-bright-green-dim: #22863a80;
--shiki-color-ansi-bright-yellow: #b08800;
--shiki-color-ansi-bright-yellow-dim: #b0880080;
--shiki-color-ansi-bright-blue: #005cc5;
--shiki-color-ansi-bright-blue-dim: #005cc580;
--shiki-color-ansi-bright-magenta: #5a32a3;
--shiki-color-ansi-bright-magenta-dim: #5a32a380;
--shiki-color-ansi-bright-cyan: #3192aa;
--shiki-color-ansi-bright-cyan-dim: #3192aa80;
--shiki-color-ansi-bright-white: #d1d5da;
--shiki-color-ansi-bright-white-dim: #d1d5da80;
--primary-hue: 212deg;
--header-height: 60px;
}
.dark-mode {
--primary-hue: 204deg;
--shiki-color-background: hsl(var(--primary-hue) 100% 77% / 10%);
--shiki-color-text: oklch(86.07% 0 0);
--shiki-token-constant: oklch(76.85% 0.121 252.34);
--shiki-token-string: oklch(81.11% 0.124 55.08);
--shiki-token-comment: oklch(55.18% 0.017 251.27);
--shiki-token-keyword: oklch(72.14% 0.162 15.49);
/* --shiki-token-parameter: #ff9800; is same as in light mode */
--shiki-token-function: oklch(72.67% 0.137 299.15);
--shiki-token-string-expression: oklch(69.28% 0.179 143.2);
--shiki-token-punctuation: oklch(79.21% 0 0);
--shiki-token-link: var(--shiki-token-string);
/* from github-dark */
--shiki-color-ansi-black: #586069;
--shiki-color-ansi-black-dim: #58606980;
--shiki-color-ansi-red: #ea4a5a;
--shiki-color-ansi-red-dim: #ea4a5a80;
--shiki-color-ansi-green: #34d058;
--shiki-color-ansi-green-dim: #34d05880;
--shiki-color-ansi-yellow: #ffea7f;
--shiki-color-ansi-yellow-dim: #ffea7f80;
--shiki-color-ansi-blue: #2188ff;
--shiki-color-ansi-blue-dim: #2188ff80;
--shiki-color-ansi-magenta: #b392f0;
--shiki-color-ansi-magenta-dim: #b392f080;
--shiki-color-ansi-cyan: #39c5cf;
--shiki-color-ansi-cyan-dim: #39c5cf80;
--shiki-color-ansi-white: #d1d5da;
--shiki-color-ansi-white-dim: #d1d5da80;
--shiki-color-ansi-bright-black: #959da5;
--shiki-color-ansi-bright-black-dim: #959da580;
--shiki-color-ansi-bright-red: #f97583;
--shiki-color-ansi-bright-red-dim: #f9758380;
--shiki-color-ansi-bright-green: #85e89d;
--shiki-color-ansi-bright-green-dim: #85e89d80;
--shiki-color-ansi-bright-yellow: #ffea7f;
--shiki-color-ansi-bright-yellow-dim: #ffea7f80;
--shiki-color-ansi-bright-blue: #79b8ff;
--shiki-color-ansi-bright-blue-dim: #79b8ff80;
--shiki-color-ansi-bright-magenta: #b392f0;
--shiki-color-ansi-bright-magenta-dim: #b392f080;
--shiki-color-ansi-bright-cyan: #56d4dd;
--shiki-color-ansi-bright-cyan-dim: #56d4dd80;
--shiki-color-ansi-bright-white: #fafbfc;
--shiki-color-ansi-bright-white-dim: #fafbfc80;
#root,
html,
body {
height: 100%;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
counter-reset: line;
font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1;
box-decoration-break: slice;
&[data-line-numbers] > [data-line] {
padding-inline: 1rem;
&::before {
content: counter(line);
counter-increment: line;
float: left;
height: 100%;
padding-right: 1rem;
color: #6b7280;
text-align: right;
}
}
}
html[data-nextra-word-wrap] & {
word-break: break-word;
@apply whitespace-pre-wrap md:whitespace-pre;
[data-line] {
@apply inline-block;
}
}
.nextra-copy-icon {
animation: fade-in 0.3s ease forwards;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.nextra-button {
@apply backdrop-blur-md bg-opacity-[.85] dark:bg-opacity-80;
}
}
.ordered-list {
padding-left: 20px;
list-style-type: decimal;
}
.unordered-list {
padding-left: 20px;
list-style-type: disc;
}
.list-item {
margin-bottom: 5px;
}
.paragraph {
margin: 0;
}
.link {
color: blue;
text-decoration: none;
}
.text {
color: black;
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}

View File

@@ -0,0 +1,17 @@
.container {
> [class*='tree'] {
margin-top: 0;
> [class*='container'] {
border: none;
}
}
}
.fileCustomLabel {
all: unset;
cursor: pointer;
&:focus-visible {
outline: 1px solid var(--mdx-color-outline-focus);
}
}

View File

@@ -0,0 +1,79 @@
import { FileTree } from '@it-incubator/ui-kit'
import s from './file-selector.module.scss'
export enum EntryType {
Directory = 'directory',
File = 'file',
}
export type FileOrDirectory = {
children?: FileOrDirectory[]
name: string
path: string
type: EntryType
}
type Props = {
data: FileOrDirectory[]
selectedMdx: string
setSelectedMdx: (s: string) => void
}
export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) => {
return (
<div className={s.container}>
<FileTree>
{data?.map((item, index) => {
return (
<RenderItem
isFirst={index === 0 && !selectedMdx}
item={item}
key={item.path}
onFileClick={setSelectedMdx}
selectedItemPath={selectedMdx}
/>
)
})}
</FileTree>
</div>
)
}
const shouldOpenFolder = (dirPath: string, filePath: string): boolean => {
return filePath.startsWith(dirPath)
}
type RenderItemProps = {
isFirst?: boolean
item: FileOrDirectory
onFileClick: (path: string) => void
selectedItemPath: string
}
const RenderItem = ({ isFirst, item, onFileClick, selectedItemPath }: RenderItemProps) => {
const isOpen = selectedItemPath ? shouldOpenFolder(item.path, selectedItemPath) : isFirst
if (item.type === EntryType.Directory) {
return (
<FileTree.Folder defaultOpen={isOpen} name={item.name}>
{item.children &&
item.children.map(childItem => (
<RenderItem
item={childItem}
key={childItem.path}
onFileClick={onFileClick}
selectedItemPath={selectedItemPath}
/>
))}
</FileTree.Folder>
)
}
const handleFileSelected = () => {
onFileClick(item.path)
}
const isSelected = selectedItemPath === item.path
return <FileTree.File active={isSelected} name={item.name} onClick={handleFileSelected} />
}

View File

@@ -1,15 +0,0 @@
import type { ComponentProps, ReactElement } from 'react'
export function CheckIcon(props: ComponentProps<'svg'>): ReactElement {
return (
<svg fill={'currentColor'} height={'1em'} viewBox={'0 0 20 20'} width={'1em'} {...props}>
<path
clipRule={'evenodd'}
d={
'M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'
}
fillRule={'evenodd'}
/>
</svg>
)
}

View File

@@ -1,11 +0,0 @@
.root {
pointer-events: none;
width: 1rem;
height: 1rem;
}
.button {
all: unset;
cursor: pointer;
display: flex;
}

View File

@@ -1,50 +0,0 @@
import { ComponentProps, ReactElement, useCallback, useEffect, useState } from 'react'
import { clsx } from 'clsx'
import s from './copy-to-clipboard.module.scss'
import { CheckIcon } from './check'
import { CopyIcon } from './copy'
export const CopyToClipboard = ({
getValue,
...props
}: {
getValue: () => string
} & ComponentProps<'button'>): ReactElement => {
const [isCopied, setCopied] = useState(false)
useEffect(() => {
if (!isCopied) {
return
}
const timerId = setTimeout(() => {
setCopied(false)
}, 2000)
return () => {
clearTimeout(timerId)
}
}, [isCopied])
const handleClick = useCallback<NonNullable<ComponentProps<'button'>['onClick']>>(async () => {
setCopied(true)
if (!navigator?.clipboard) {
console.error('Access to clipboard rejected!')
}
try {
await navigator.clipboard.writeText(getValue())
} catch {
console.error('Failed to copy!')
}
}, [getValue])
const IconToUse = isCopied ? CheckIcon : CopyIcon
return (
<button className={s.button} onClick={handleClick} tabIndex={0} title={'Copy code'} {...props}>
<IconToUse className={clsx('nextra-copy-icon', s.root)} />
</button>
)
}

View File

@@ -1,34 +0,0 @@
import type { ComponentProps, ReactElement } from 'react'
export function CopyIcon(props: ComponentProps<'svg'>): ReactElement {
return (
<svg
fill={'none'}
height={'24'}
stroke={'currentColor'}
viewBox={'0 0 24 24'}
width={'24'}
xmlns={'http://www.w3.org/2000/svg'}
{...props}
>
<rect
height={'13'}
rx={'2'}
strokeLinecap={'round'}
strokeLinejoin={'round'}
strokeWidth={'2'}
width={'13'}
x={'9'}
y={'9'}
/>
<path
d={
'M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5'
}
strokeLinecap={'round'}
strokeLinejoin={'round'}
strokeWidth={'2'}
/>
</svg>
)
}

View File

@@ -1 +0,0 @@
export * from './pre'

View File

@@ -1,80 +0,0 @@
.codeBlock {
position: relative;
margin-top: 1.5rem;
&:first-child {
margin-top: 0;
}
}
.filename {
position: absolute;
z-index: 1;
top: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.5rem 1rem;
font-size: 0.75rem;
text-overflow: ellipsis;
white-space: nowrap;
background-color: hsl(var(--primary-hue) 100% 39% / 5%);
border-radius: 0.375rem 0.375rem 0 0;
}
.preCommon {
overflow-x: auto;
margin-bottom: 1rem;
font-size: 0.9em;
border-radius: 0.5rem;
-webkit-font-smoothing: antialiased;
}
.preWithFilename {
padding-top: 3rem;
padding-bottom: 1rem;
}
.preWithoutFilename {
padding: 1rem 0;
}
.controlDiv {
position: absolute;
right: 0;
display: flex;
gap: 0.25rem;
margin: 11px 0 0 -11px;
}
.top8 {
top: 2rem;
}
.top0 {
top: 0;
}
.mdHidden {
@media (width >= 768px) {
display: none;
}
}
.iconDimensions {
pointer-events: none;
width: 1rem;
height: 1rem;
}

View File

@@ -1,43 +0,0 @@
import { ComponentProps, ReactElement, useRef } from 'react'
import { Scrollbar } from '@it-incubator/ui-kit'
import { clsx } from 'clsx'
import styles from './pre.module.scss'
import { CopyToClipboard } from './copy-to-clipboard'
export const Pre = ({
children,
className,
filename,
...props
}: ComponentProps<'pre'> & {
filename?: string
hasCopyCode?: boolean
}): ReactElement => {
const preRef = useRef<HTMLPreElement | null>(null)
return (
<Scrollbar className={styles.codeBlock} type={'hover'}>
{filename && (
<div className={styles.filename}>
{filename}
<CopyToClipboard
getValue={() => preRef.current?.querySelector('code')?.textContent || ''}
/>
</div>
)}
<pre
className={clsx(
styles.preCommon,
filename ? styles.preWithFilename : styles.preWithoutFilename,
className
)}
ref={preRef}
{...props}
>
{children}
</pre>
</Scrollbar>
)
}

View File

@@ -0,0 +1 @@
export * from './toc'

View File

@@ -0,0 +1,23 @@
.list {
padding-left: 0.5rem;
list-style: none;
}
.link {
all: unset;
cursor: pointer;
font-size: var(--font-size-xs);
line-height: var(--line-height-m);
&[data-depth='1'] {
font-weight: var(--font-weight-semi-bold);
}
&[data-depth='2'] {
font-weight: var(--font-weight-regular);
}
&.active {
color: var(--color-accent-500);
}
}

View File

@@ -0,0 +1,89 @@
import { MouseEvent } from 'react'
import { clsx } from 'clsx'
import s from './toc-node.module.scss'
import { NodeData } from './toc-node.types'
interface NodeProps {
currentHeading?: string
data: NodeData
depth: number
onLinkClick: (e: MouseEvent<HTMLAnchorElement>) => void
}
export function TocNode({ currentHeading, data, depth, onLinkClick }: NodeProps) {
if (!data) {
return null
}
switch (data.type) {
case 'list':
return (
<ul className={s.list}>
{data.children.map((child, index) => (
<TocNode
currentHeading={currentHeading}
data={child}
depth={depth}
key={index}
onLinkClick={onLinkClick}
/>
))}
</ul>
)
case 'listItem':
return (
<li className={s.listItem}>
{data.children.map((child, index) => (
<TocNode
currentHeading={currentHeading}
data={child}
depth={depth + 1}
key={index}
onLinkClick={onLinkClick}
/>
))}
</li>
)
case 'paragraph':
return (
<>
{data.children.map((child, index) => (
<TocNode
currentHeading={currentHeading}
data={child}
depth={depth}
key={index}
onLinkClick={onLinkClick}
/>
))}
</>
)
case 'link':
return (
<a
className={clsx(s.link, data.url === `#${currentHeading}` && s.active)}
data-depth={depth}
href={data.url}
onClick={onLinkClick}
title={data.title ?? undefined}
>
{data.children.map((child, index) => (
<TocNode
currentHeading={currentHeading}
data={child}
depth={depth}
key={index}
onLinkClick={onLinkClick}
/>
))}
</a>
)
case 'text':
return <>{data.value}</>
default:
return null
}
}

View File

@@ -0,0 +1,31 @@
export type TextNode = {
type: 'text'
value: string
}
export type LinkNode = {
children: Array<TextNode>
title: null | string
type: 'link'
url: string
}
export type ParagraphNode = {
children: Array<LinkNode>
type: 'paragraph'
}
export type ListItemNode = {
children: Array<ListNode | ParagraphNode>
spread: boolean
type: 'listItem'
}
export type ListNode = {
children: Array<ListItemNode>
ordered: boolean
spread: boolean
type: 'list'
}
export type NodeData = LinkNode | ListItemNode | ListNode | ParagraphNode | TextNode

View File

@@ -0,0 +1,8 @@
.toc {
position: sticky;
top: 19px;
> ul {
padding-left: 12px;
}
}

View File

@@ -0,0 +1,73 @@
import { MouseEvent, useEffect, useRef, useState } from 'react'
import { Typography } from '@it-incubator/ui-kit'
import s from './toc.module.scss'
import { TocNode } from './toc-node'
import { NodeData } from './toc-node.types'
type Props = {
tocMap: NodeData
}
export const TableOfContents = ({ tocMap }: Props) => {
const [currentHeading, setCurrentHeading] = useState('')
const headingsObserverRef = useRef<IntersectionObserver | null>(null)
useEffect(() => {
const setCurrent: IntersectionObserverCallback = entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
setCurrentHeading(entry.target.id)
break
}
}
}
const observerOptions: IntersectionObserverInit = {
// Negative top margin accounts for `scroll-margin`.
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
rootMargin: '-60px 0% -66%',
threshold: 1,
}
if (!headingsObserverRef.current) {
headingsObserverRef.current = new IntersectionObserver(setCurrent, observerOptions)
}
const headingsObserver = headingsObserverRef.current
setTimeout(() => {
document.querySelectorAll('article :is(h1,h2,h3)').forEach(h => headingsObserver.observe(h))
}, 100)
return () => {
headingsObserver.disconnect()
}
}, [])
useEffect(() => {
if (headingsObserverRef.current) {
const headingsObserver = headingsObserverRef.current
// Disconnect and reconnect the observer to refresh it
headingsObserver.disconnect()
setTimeout(() => {
document.querySelectorAll('article :is(h1,h2,h3)').forEach(h => headingsObserver.observe(h))
}, 100)
}
}, [tocMap])
const onLinkClick = (e: MouseEvent<HTMLAnchorElement>) => {
setCurrentHeading(e.currentTarget.getAttribute('href')!.replace('#', ''))
}
return (
<aside className={s.toc}>
<Typography.Subtitle2 mb={'13px'} ml={'12px'} mt={'19px'}>
Содержание:
</Typography.Subtitle2>
<TocNode currentHeading={currentHeading} data={tocMap} depth={0} onLinkClick={onLinkClick} />
</aside>
)
}

View File

@@ -0,0 +1,35 @@
.page {
width: 100%;
padding: 22px 0 43px;
}
.container {
display: grid;
grid-template-columns: 200px minmax(65ch, 100%) 190px;
width: 100%;
background-color: var(--color-light-mode-100);
border: 1px solid var(--color-border-primary);
:global(.dark-mode) & {
background-color: var(--color-dark-mode-600);
}
> div {
flex-shrink: 0;
}
> :first-child {
border-right: 1px solid var(--color-border-primary);
}
> :last-child {
border-left: 1px solid var(--color-border-primary);
}
}
.root {
overflow: auto;
padding: 31px 24px;
}

View File

@@ -0,0 +1,81 @@
import { useEffect, useState } from 'react'
import { IpcRendererListener } from '@electron-toolkit/preload'
import { MdxComponent, Prose } from '@it-incubator/mdx-components'
import { ImagePreview } from '@it-incubator/ui-kit'
import s from './view.module.scss'
import { FileOrDirectory, MdxFileSelector } from '../file-selector/file-selector'
import { TableOfContents } from '../toc'
export const View = ({ setFileName }) => {
const [selectedFile, setSelectedFile] = useState<string>('')
const [code, setCode] = useState<string>('')
const [toc, setToc] = useState<any>({})
const [srcPreview, setSrcPreview] = useState<string>('')
const [directoryContents, setDirectoryContents] = useState<{
data: FileOrDirectory[]
dir: string
dirName: string
files: string[]
} | null>(null)
const handleFileClick = (filePath: string) => {
setSelectedFile(filePath)
window.electron.ipcRenderer.send('open-file', filePath)
}
useEffect(() => {
const contentListener: IpcRendererListener = (_event, content) => {
setCode(content.code)
setFileName(content.fileName)
setSelectedFile(content.fileName)
setToc(content.toc)
}
const directoryContentsListener: IpcRendererListener = (_event, content) => {
setDirectoryContents(content)
}
window.electron.ipcRenderer.on('current-content', contentListener)
window.electron.ipcRenderer.on('directory-contents', directoryContentsListener)
window.electron.ipcRenderer.send('get-current-dir')
window.electron.ipcRenderer.send('get-current-content')
return () => {
window.electron.ipcRenderer.removeAllListeners('file-changed')
window.electron.ipcRenderer.removeAllListeners('current-content')
window.electron.ipcRenderer.removeAllListeners('directory-contents')
}
}, [])
if (!code) {
return null
}
return (
<div className={s.page}>
<ImagePreview onClose={() => setSrcPreview('')} open={!!srcPreview} src={srcPreview} />
<div className={s.container}>
<div>
{directoryContents && (
<MdxFileSelector
data={directoryContents.data}
selectedMdx={selectedFile}
setSelectedMdx={handleFileClick}
/>
)}
</div>
<Prose as={'article'} className={s.root}>
<MdxComponent code={code} onImageClick={setSrcPreview} />
</Prose>
<div>
<TableOfContents tocMap={toc?.map} />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export * from './layout'

View File

@@ -0,0 +1,36 @@
.scrollbar {
scroll-behavior: smooth;
scroll-padding-top: calc(1.5rem + var(--header-height));
position: initial !important;
overflow: auto;
height: 100%;
padding-top: var(--header-height);
> div[class*='scrollbar'] {
margin-top: var(--header-height);
}
@media screen and (width <= 768px) {
padding-left: 0;
}
}
.main {
display: flex;
justify-content: center;
max-width: 1186px;
height: 100%;
margin-right: auto;
margin-left: auto;
@media screen and (width <= 1300px) {
padding: 0 20px;
}
@media screen and (width <= 768px) {
padding: 0 16px;
}
}

View File

@@ -0,0 +1,30 @@
import { ReactNode, useState } from 'react'
import { Button, Header, Scrollbar, Typography } from '@it-incubator/ui-kit'
import s from './layout.module.scss'
type Props = {
children: ReactNode
fileName: string
}
export const Layout = ({ children, fileName }: Props) => {
const [isDark, setIsDark] = useState<boolean>(false)
const handleThemeChanged = () => {
setIsDark(!isDark)
document.body.classList.toggle('dark-mode', !isDark)
}
return (
<>
<Header>
<Typography.H1>{fileName}</Typography.H1>
<Button onClick={() => handleThemeChanged()}>Toggle theme</Button>
</Header>
<Scrollbar className={s.scrollbar}>
<main className={s.main}>{children}</main>
</Scrollbar>
</>
)
}

View File

@@ -1,201 +0,0 @@
.page {
width: 100%;
padding: 22px 0 43px;
}
.container {
display: grid;
grid-template-columns: 200px minmax(65ch, 100%) 190px;
width: 100%;
background-color: var(--color-light-mode-100);
border: 1px solid var(--color-border-primary);
:global(.dark-mode) & {
background-color: var(--color-dark-mode-600);
}
> div {
flex-shrink: 0;
}
> :first-child {
border-right: 1px solid var(--color-border-primary);
}
> :last-child {
border-left: 1px solid var(--color-border-primary);
}
}
.root {
overflow: auto;
padding: 31px 24px;
:first-child {
margin-top: 0;
}
& p {
margin-top: 1.5rem;
line-height: 1.75rem;
&:first-child {
margin-top: 0;
}
}
& h1 {
margin-top: 0.5rem;
font-size: 2.25rem;
font-weight: 700;
line-height: 2.5rem;
letter-spacing: -0.025em;
}
& h2 {
margin-top: 2.5rem;
padding-bottom: 0.25rem;
font-size: 1.875rem;
font-weight: 600;
line-height: 2.25rem;
letter-spacing: -0.025em;
border-bottom-width: 1px;
}
& h3 {
margin-top: 2rem;
font-size: 1.5rem;
font-weight: 600;
line-height: 2rem;
letter-spacing: -0.025em;
}
& h4 {
margin-top: 2rem;
font-size: 1.25rem;
font-weight: 600;
line-height: 1.75rem;
letter-spacing: -0.025em;
}
& h5 {
margin-top: 2rem;
font-size: 1.125rem;
font-weight: 600;
line-height: 1.75rem;
letter-spacing: -0.025em;
}
& h6 {
margin-top: 2rem;
font-size: 1rem;
font-weight: 600;
line-height: 1.5rem;
letter-spacing: -0.025em;
}
& ul {
margin: 1.5rem 0 0 1.5rem;
list-style-type: disc;
&:first-child {
margin-top: 0;
}
}
& ol {
margin: 1.5rem 0 0 1.5rem;
list-style-type: decimal;
&:first-child {
margin-top: 0;
}
}
& li {
margin-inline: 0.5rem;
}
& blockquote {
margin-top: 1.5rem;
padding-left: 1.5rem;
font-style: italic;
color: #374151;
border-left: 4px solid #d1d5db;
:global(.dark-mode) & {
color: rgb(156 163 175);
border-color: #9ca3af;
}
&:first-child {
margin-top: 0;
}
}
& hr {
margin-block: 2rem;
}
& code {
padding: 0.125rem 0.25em;
font-size: 0.9em;
overflow-wrap: break-word;
background-color: rgb(0 0 0 / 3%);
border: 1px solid rgb(0 0 0 / 4%);
border-radius: 0.375rem;
}
& pre {
contain: paint;
& code {
display: grid;
min-width: 100%;
padding: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: currentcolor;
background-color: transparent;
border-style: none;
border-radius: 0;
[data-line] {
padding-inline: 1rem;
}
}
}
& img {
display: block;
max-width: 100%;
height: auto;
vertical-align: middle;
}
:global(.dark-mode) & code {
background-color: rgb(255 255 255 / 6%);
border: 1px solid rgb(255 255 255 / 7%);
}
:global(.dark-mode) & pre code {
background-color: transparent;
border-style: none;
}
}

View File

@@ -1,3 +0,0 @@
export const View = ({ code }: { code: string }) => {
return <div>View</div>
}