diff --git a/package.json b/package.json index 7bb51b1..fbbd266 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 055996b..52fcf80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/src/main/constants.ts b/src/main/constants.ts deleted file mode 100644 index f2fc18c..0000000 --- a/src/main/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const CODE_BLOCK_FILENAME_REGEX = /filename="([^"]+)"/ diff --git a/src/main/index.ts b/src/main/index.ts index 20fe447..ae23535 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 + name: string + path: string + type: FsEntryType.Directory +} + +type FileOrDirectory = Directory | File diff --git a/src/main/rehype.ts b/src/main/rehype.ts deleted file mode 100644 index 533bd2f..0000000 --- a/src/main/rehype.ts +++ /dev/null @@ -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
element that wraps
 element
-      // because we'll wrap with our own 
- Object.assign(node, node.children[0]) - } - - node.properties.filename = node.__nextra_filename - node.properties.hasCopyCode = node.__nextra_hasCopyCode - }) -} diff --git a/src/main/theme.json b/src/main/theme.json deleted file mode 100644 index 4827388..0000000 --- a/src/main/theme.json +++ /dev/null @@ -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" - } - } - ] -} diff --git a/src/main/types.ts b/src/main/types.ts deleted file mode 100644 index c6c89df..0000000 --- a/src/main/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type BundledMdx = { - code: string - frontmatter: Record -} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index abd0bac..55895be 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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('') const [fileName, setFileName] = useState('') - const [srcPreview, setSrcPreview] = useState('') - - 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 ( -
- {fileName} -
- setSrcPreview('')} open={!!srcPreview} src={srcPreview} /> - ( - setSrcPreview(props.src || '')} - style={{ cursor: 'pointer' }} - /> - ), - pre: Pre, - }} - /> -
-
+ + + ) } export default App -const Code = ({ children, ...props }: ComponentProps<'code'>): ReactElement => { - return ( - - {children} - - ) -} diff --git a/src/renderer/src/assets/index.css b/src/renderer/src/assets/index.css index 55d7f8b..e895c9f 100644 --- a/src/renderer/src/assets/index.css +++ b/src/renderer/src/assets/index.css @@ -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; } diff --git a/src/renderer/src/components/file-selector/file-selector.module.scss b/src/renderer/src/components/file-selector/file-selector.module.scss new file mode 100644 index 0000000..893115d --- /dev/null +++ b/src/renderer/src/components/file-selector/file-selector.module.scss @@ -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); + } +} diff --git a/src/renderer/src/components/file-selector/file-selector.tsx b/src/renderer/src/components/file-selector/file-selector.tsx new file mode 100644 index 0000000..dc909b6 --- /dev/null +++ b/src/renderer/src/components/file-selector/file-selector.tsx @@ -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 ( +
+ + {data?.map((item, index) => { + return ( + + ) + })} + +
+ ) +} + +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 ( + + {item.children && + item.children.map(childItem => ( + + ))} + + ) + } + + const handleFileSelected = () => { + onFileClick(item.path) + } + + const isSelected = selectedItemPath === item.path + + return +} diff --git a/src/renderer/src/components/pre/check.tsx b/src/renderer/src/components/pre/check.tsx deleted file mode 100644 index 99c2949..0000000 --- a/src/renderer/src/components/pre/check.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ComponentProps, ReactElement } from 'react' - -export function CheckIcon(props: ComponentProps<'svg'>): ReactElement { - return ( - - - - ) -} diff --git a/src/renderer/src/components/pre/copy-to-clipboard.module.scss b/src/renderer/src/components/pre/copy-to-clipboard.module.scss deleted file mode 100644 index f266e1d..0000000 --- a/src/renderer/src/components/pre/copy-to-clipboard.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -.root { - pointer-events: none; - width: 1rem; - height: 1rem; -} - -.button { - all: unset; - cursor: pointer; - display: flex; -} diff --git a/src/renderer/src/components/pre/copy-to-clipboard.tsx b/src/renderer/src/components/pre/copy-to-clipboard.tsx deleted file mode 100644 index 2108d21..0000000 --- a/src/renderer/src/components/pre/copy-to-clipboard.tsx +++ /dev/null @@ -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['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 ( - - ) -} diff --git a/src/renderer/src/components/pre/copy.tsx b/src/renderer/src/components/pre/copy.tsx deleted file mode 100644 index a782a1a..0000000 --- a/src/renderer/src/components/pre/copy.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { ComponentProps, ReactElement } from 'react' - -export function CopyIcon(props: ComponentProps<'svg'>): ReactElement { - return ( - - - - - ) -} diff --git a/src/renderer/src/components/pre/index.ts b/src/renderer/src/components/pre/index.ts deleted file mode 100644 index f8eeefe..0000000 --- a/src/renderer/src/components/pre/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './pre' diff --git a/src/renderer/src/components/pre/pre.module.scss b/src/renderer/src/components/pre/pre.module.scss deleted file mode 100644 index 0bc896e..0000000 --- a/src/renderer/src/components/pre/pre.module.scss +++ /dev/null @@ -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; -} diff --git a/src/renderer/src/components/pre/pre.tsx b/src/renderer/src/components/pre/pre.tsx deleted file mode 100644 index 4c1a582..0000000 --- a/src/renderer/src/components/pre/pre.tsx +++ /dev/null @@ -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(null) - - return ( - - {filename && ( -
- {filename} - preRef.current?.querySelector('code')?.textContent || ''} - /> -
- )} -
-        {children}
-      
-
- ) -} diff --git a/src/renderer/src/components/toc/index.ts b/src/renderer/src/components/toc/index.ts new file mode 100644 index 0000000..65052b1 --- /dev/null +++ b/src/renderer/src/components/toc/index.ts @@ -0,0 +1 @@ +export * from './toc' diff --git a/src/renderer/src/components/toc/toc-node.module.scss b/src/renderer/src/components/toc/toc-node.module.scss new file mode 100644 index 0000000..9d84ccf --- /dev/null +++ b/src/renderer/src/components/toc/toc-node.module.scss @@ -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); + } +} diff --git a/src/renderer/src/components/toc/toc-node.tsx b/src/renderer/src/components/toc/toc-node.tsx new file mode 100644 index 0000000..8478d17 --- /dev/null +++ b/src/renderer/src/components/toc/toc-node.tsx @@ -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) => void +} + +export function TocNode({ currentHeading, data, depth, onLinkClick }: NodeProps) { + if (!data) { + return null + } + + switch (data.type) { + case 'list': + return ( +
    + {data.children.map((child, index) => ( + + ))} +
+ ) + case 'listItem': + return ( +
  • + {data.children.map((child, index) => ( + + ))} +
  • + ) + case 'paragraph': + return ( + <> + {data.children.map((child, index) => ( + + ))} + + ) + case 'link': + return ( + + {data.children.map((child, index) => ( + + ))} + + ) + case 'text': + return <>{data.value} + default: + return null + } +} diff --git a/src/renderer/src/components/toc/toc-node.types.ts b/src/renderer/src/components/toc/toc-node.types.ts new file mode 100644 index 0000000..9d49194 --- /dev/null +++ b/src/renderer/src/components/toc/toc-node.types.ts @@ -0,0 +1,31 @@ +export type TextNode = { + type: 'text' + value: string +} + +export type LinkNode = { + children: Array + title: null | string + type: 'link' + url: string +} + +export type ParagraphNode = { + children: Array + type: 'paragraph' +} + +export type ListItemNode = { + children: Array + spread: boolean + type: 'listItem' +} + +export type ListNode = { + children: Array + ordered: boolean + spread: boolean + type: 'list' +} + +export type NodeData = LinkNode | ListItemNode | ListNode | ParagraphNode | TextNode diff --git a/src/renderer/src/components/toc/toc.module.scss b/src/renderer/src/components/toc/toc.module.scss new file mode 100644 index 0000000..21e574e --- /dev/null +++ b/src/renderer/src/components/toc/toc.module.scss @@ -0,0 +1,8 @@ +.toc { + position: sticky; + top: 19px; + + > ul { + padding-left: 12px; + } +} diff --git a/src/renderer/src/components/toc/toc.tsx b/src/renderer/src/components/toc/toc.tsx new file mode 100644 index 0000000..0f4242b --- /dev/null +++ b/src/renderer/src/components/toc/toc.tsx @@ -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(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) => { + setCurrentHeading(e.currentTarget.getAttribute('href')!.replace('#', '')) + } + + return ( + + ) +} diff --git a/src/renderer/src/components/view/view.module.scss b/src/renderer/src/components/view/view.module.scss new file mode 100644 index 0000000..180a00a --- /dev/null +++ b/src/renderer/src/components/view/view.module.scss @@ -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; +} diff --git a/src/renderer/src/components/view/view.tsx b/src/renderer/src/components/view/view.tsx new file mode 100644 index 0000000..6b1ddac --- /dev/null +++ b/src/renderer/src/components/view/view.tsx @@ -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('') + const [code, setCode] = useState('') + const [toc, setToc] = useState({}) + const [srcPreview, setSrcPreview] = useState('') + + 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 ( +
    + setSrcPreview('')} open={!!srcPreview} src={srcPreview} /> +
    +
    + {directoryContents && ( + + )} +
    + + + +
    + +
    +
    +
    + ) +} diff --git a/src/renderer/src/layout/index.ts b/src/renderer/src/layout/index.ts new file mode 100644 index 0000000..626835b --- /dev/null +++ b/src/renderer/src/layout/index.ts @@ -0,0 +1 @@ +export * from './layout' diff --git a/src/renderer/src/layout/layout.module.scss b/src/renderer/src/layout/layout.module.scss new file mode 100644 index 0000000..b736a99 --- /dev/null +++ b/src/renderer/src/layout/layout.module.scss @@ -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; + } +} diff --git a/src/renderer/src/layout/layout.tsx b/src/renderer/src/layout/layout.tsx new file mode 100644 index 0000000..d0c031a --- /dev/null +++ b/src/renderer/src/layout/layout.tsx @@ -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(false) + + const handleThemeChanged = () => { + setIsDark(!isDark) + document.body.classList.toggle('dark-mode', !isDark) + } + + return ( + <> +
    + {fileName} + +
    + +
    {children}
    +
    + + ) +} diff --git a/src/renderer/src/view.module.scss b/src/renderer/src/view.module.scss deleted file mode 100644 index a690426..0000000 --- a/src/renderer/src/view.module.scss +++ /dev/null @@ -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; - } -} diff --git a/src/renderer/src/view.tsx b/src/renderer/src/view.tsx deleted file mode 100644 index d4c9b34..0000000 --- a/src/renderer/src/view.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const View = ({ code }: { code: string }) => { - return
    View
    -}