From c5a5a4e55b20fd2a6cb5527955c5ecb5df16a142 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Thu, 16 Jul 2026 16:40:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=BC=96=E8=BE=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/markdown-editor-design.md | 35 +- package.json | 10 + pnpm-lock.yaml | 666 +++++++++++++++++++++++++++++- pnpm-workspace.yaml | 2 + src/components/MarkdownEditor.vue | 415 +++++++------------ src/components/SettingsModal.vue | 200 +++++---- src/tailwind.css | 92 ++++- 7 files changed, 1065 insertions(+), 355 deletions(-) diff --git a/doc/markdown-editor-design.md b/doc/markdown-editor-design.md index 6de4295..48a0bf1 100644 --- a/doc/markdown-editor-design.md +++ b/doc/markdown-editor-design.md @@ -172,9 +172,38 @@ MarkdownEditor.vue - **parseBlocks** 每次 modelValue 变化时调用,包含 N 次 `processSync`(N = 块数) - 典型文档(50 块)约需 50-100ms,在可接受范围内 - 后续可优化为增量更新(只重新渲染变化的块) -- **textarea auto-resize** 通过 `scrollHeight` 动态调整高度,无额外 DOM 操作 +- **CodeMirror 实例数** 完整编辑模式保持一个实例;混合模式只为当前活动块创建实例 - **双栏滚动同步** 使用比例计算,避免频繁的绝对位置计算 +## 代码编辑体验 + +Markdown 源码编辑由 CodeMirror 6 提供,完整编辑模式与混合模式的活动块复用 +`MarkdownCodeEditor.vue`。组件继续通过 `update:modelValue` 传递纯 Markdown 字符串, +不会改变外层的保存和文件切换数据流。 + +### 编辑能力 + +- Markdown 语法着色,以及围栏代码块内部的语言着色 +- 括号、引号自动闭合,列表续行,Tab/Shift+Tab 缩进 +- 输入三个反引号或波浪号后自动打开语言提示 +- 语言提示支持继续输入筛选、方向键选择、Enter 确认和 Ctrl+Space 手动触发 +- 围栏语言确认后按 Enter 自动补齐结束围栏,并将光标放在代码区域 +- 最近使用的五种语言通过 localStorage 排在提示列表前面 + +语言名称、别名和 Shiki 映射集中定义在 `src/lib/markdownLanguages.ts`。 +无法识别的围栏语言按纯文本处理,不阻断 Markdown 渲染或编辑。 + +### 预览高亮 + +预览仍先经过 unified 和 rehype-sanitize。安全过滤完成后,可信的 Shiki 高亮器 +替换 `
` 节点,因此不会放宽 Markdown 原始 HTML 的安全策略。
+
+Shiki 核心和语言语法均按需加载:只有文档包含代码块时才初始化高亮器,只有某种
+语言第一次出现在预览中时才加载对应语法。高亮器和已加载语言会在应用生命周期内
+复用。Shiki 与 CodeMirror 共用 `--code-*` 语义色变量,每套应用主题同时定义界面色
+和代码色;CodeMirror 围栏内容还复用 Shiki 的 token 划分,确保混合模式点击前后逐词
+颜色一致。切换主题时预览与活动编辑器会即时使用新颜色,无需重新解析文档。
+
## 样式体系
| 层级 | 设计 |
@@ -284,8 +313,8 @@ MarkdownEditor.vue
paste 事件同时绑定在两种模式的 textarea 上:
-- **edit 模式**:`@paste="onEditorPaste"` — 操作 `props.modelValue` + `editorTextareaRef`
-- **mixed 模式**:`@paste="onBlockPaste"` — 操作 `editingSource` + `blockTextareaRef`(包含空文档和 block 编辑两种场景)
+- **edit 模式**:`@paste="onEditorPaste"` — 按 CodeMirror 当前选区替换 `modelValue`
+- **mixed 模式**:`@paste="onBlockPaste"` — 按活动块 CodeMirror 选区替换 `editingSource`
粘贴后的 markdown 语法统一为:
```markdown
diff --git a/package.json b/package.json
index 6b97361..0e4edb1 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,15 @@
"tauri": "tauri"
},
"dependencies": {
+ "@codemirror/autocomplete": "^6.20.3",
+ "@codemirror/commands": "^6.10.4",
+ "@codemirror/lang-markdown": "6.4.0",
+ "@codemirror/language": "^6.12.4",
+ "@codemirror/language-data": "^6.5.2",
+ "@codemirror/search": "^6.7.1",
+ "@codemirror/state": "^6.7.1",
+ "@codemirror/view": "^6.43.6",
+ "@lezer/highlight": "^1.2.3",
"@lucide/vue": "^1.23.0",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1",
@@ -25,6 +34,7 @@
"remark-rehype": "^11.1.2",
"remixicon": "^4.9.1",
"shadcn-vue": "^2.7.4",
+ "shiki": "^4.3.1",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"unified": "^11.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 35acd38..e0f443f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,10 +4,40 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ '@codemirror/lang-markdown': 6.4.0
+
importers:
.:
dependencies:
+ '@codemirror/autocomplete':
+ specifier: ^6.20.3
+ version: 6.20.3
+ '@codemirror/commands':
+ specifier: ^6.10.4
+ version: 6.10.4
+ '@codemirror/lang-markdown':
+ specifier: 6.4.0
+ version: 6.4.0
+ '@codemirror/language':
+ specifier: ^6.12.4
+ version: 6.12.4
+ '@codemirror/language-data':
+ specifier: ^6.5.2
+ version: 6.5.2
+ '@codemirror/search':
+ specifier: ^6.7.1
+ version: 6.7.1
+ '@codemirror/state':
+ specifier: ^6.7.1
+ version: 6.7.1
+ '@codemirror/view':
+ specifier: ^6.43.6
+ version: 6.43.6
+ '@lezer/highlight':
+ specifier: ^1.2.3
+ version: 1.2.3
'@lucide/vue':
specifier: ^1.23.0
version: 1.23.0(vue@3.5.39(typescript@5.6.3))
@@ -53,6 +83,9 @@ importers:
shadcn-vue:
specifier: ^2.7.4
version: 2.7.4(eslint@10.6.0(jiti@2.7.0))(vue@3.5.39(typescript@5.6.3))
+ shiki:
+ specifier: ^4.3.1
+ version: 4.3.1
tailwind-merge:
specifier: ^3.6.0
version: 3.6.0
@@ -224,6 +257,96 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
+ '@codemirror/autocomplete@6.20.3':
+ resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
+
+ '@codemirror/commands@6.10.4':
+ resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==}
+
+ '@codemirror/lang-angular@0.1.4':
+ resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==}
+
+ '@codemirror/lang-cpp@6.0.3':
+ resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==}
+
+ '@codemirror/lang-css@6.3.1':
+ resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
+
+ '@codemirror/lang-go@6.0.1':
+ resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==}
+
+ '@codemirror/lang-html@6.4.11':
+ resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
+
+ '@codemirror/lang-java@6.0.2':
+ resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==}
+
+ '@codemirror/lang-javascript@6.2.5':
+ resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
+
+ '@codemirror/lang-jinja@6.0.1':
+ resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==}
+
+ '@codemirror/lang-json@6.0.2':
+ resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
+
+ '@codemirror/lang-less@6.0.2':
+ resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==}
+
+ '@codemirror/lang-liquid@6.3.2':
+ resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==}
+
+ '@codemirror/lang-markdown@6.4.0':
+ resolution: {integrity: sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==}
+
+ '@codemirror/lang-php@6.0.2':
+ resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==}
+
+ '@codemirror/lang-python@6.2.1':
+ resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
+
+ '@codemirror/lang-rust@6.0.2':
+ resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==}
+
+ '@codemirror/lang-sass@6.0.2':
+ resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==}
+
+ '@codemirror/lang-sql@6.10.0':
+ resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
+
+ '@codemirror/lang-vue@0.1.3':
+ resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==}
+
+ '@codemirror/lang-wast@6.0.2':
+ resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==}
+
+ '@codemirror/lang-xml@6.1.0':
+ resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
+
+ '@codemirror/lang-yaml@6.1.3':
+ resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==}
+
+ '@codemirror/language-data@6.5.2':
+ resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==}
+
+ '@codemirror/language@6.12.4':
+ resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==}
+
+ '@codemirror/legacy-modes@6.5.3':
+ resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
+
+ '@codemirror/lint@6.9.7':
+ resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
+
+ '@codemirror/search@6.7.1':
+ resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==}
+
+ '@codemirror/state@6.7.1':
+ resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==}
+
+ '@codemirror/view@6.43.6':
+ resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==}
+
'@dotenvx/dotenvx@1.75.1':
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
hasBin: true
@@ -485,11 +608,65 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@lezer/common@1.5.2':
+ resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
+
+ '@lezer/cpp@1.1.6':
+ resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==}
+
+ '@lezer/css@1.3.4':
+ resolution: {integrity: sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==}
+
+ '@lezer/go@1.0.1':
+ resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
+
+ '@lezer/highlight@1.2.3':
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
+
+ '@lezer/html@1.3.13':
+ resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
+
+ '@lezer/java@1.1.3':
+ resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==}
+
+ '@lezer/javascript@1.5.4':
+ resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
+
+ '@lezer/json@1.0.3':
+ resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
+
+ '@lezer/lr@1.4.10':
+ resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
+
+ '@lezer/markdown@1.7.1':
+ resolution: {integrity: sha512-MEBZeFSBxgteUjEC3Wxg2Dwld5/JxRKG267L3bMFdibm8KjqSdiJYBeFw1Nt1CM8+zKMpSIEHblY8FD9z38sJQ==}
+
+ '@lezer/php@1.0.5':
+ resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==}
+
+ '@lezer/python@1.1.19':
+ resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==}
+
+ '@lezer/rust@1.0.2':
+ resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==}
+
+ '@lezer/sass@1.1.0':
+ resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==}
+
+ '@lezer/xml@1.0.6':
+ resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
+
+ '@lezer/yaml@1.0.4':
+ resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==}
+
'@lucide/vue@1.23.0':
resolution: {integrity: sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==}
peerDependencies:
vue: '>=3.0.1'
+ '@marijn/find-cluster-break@1.0.3':
+ resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
+
'@modelcontextprotocol/sdk@1.29.0':
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
engines: {node: '>=18'}
@@ -654,6 +831,37 @@ packages:
cpu: [x64]
os: [win32]
+ '@shikijs/core@4.3.1':
+ resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.3.1':
+ resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.3.1':
+ resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.3.1':
+ resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.3.1':
+ resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.3.1':
+ resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.3.1':
+ resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -859,6 +1067,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+ '@types/hast@3.0.5':
+ resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1210,6 +1421,9 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
+ crelt@1.0.7:
+ resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -2223,6 +2437,12 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
+ oniguruma-parser@0.12.2:
+ resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
+
+ oniguruma-to-es@4.3.6:
+ resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
+
open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
@@ -2410,6 +2630,15 @@ packages:
resolution: {integrity: sha512-CkfWKhQiYsMQYaWUkHdERXUxT2jJLBoa5y7zFv3dUAE7Ly5oU/0hsqrENyEfrCL03pDsQYbnoz17Cbagx/c2OA==}
engines: {node: '>= 4'}
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
rehype-sanitize@6.0.0:
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
@@ -2514,6 +2743,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shiki@4.3.1:
+ resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==}
+ engines: {node: '>=20'}
+
side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'}
@@ -2606,6 +2839,9 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
+ style-mod@4.1.3:
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
+
stylus@0.57.0:
resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==}
hasBin: true
@@ -2826,6 +3062,9 @@ packages:
typescript:
optional: true
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
web-worker@1.5.0:
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
@@ -3070,6 +3309,257 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
+ '@codemirror/autocomplete@6.20.3':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+
+ '@codemirror/commands@6.10.4':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+
+ '@codemirror/lang-angular@0.1.4':
+ dependencies:
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/language': 6.12.4
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-cpp@6.0.3':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@lezer/cpp': 1.1.6
+
+ '@codemirror/lang-css@6.3.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/css': 1.3.4
+
+ '@codemirror/lang-go@6.0.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/go': 1.0.1
+
+ '@codemirror/lang-html@6.4.11':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/css': 1.3.4
+ '@lezer/html': 1.3.13
+
+ '@codemirror/lang-java@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@lezer/java': 1.1.3
+
+ '@codemirror/lang-javascript@6.2.5':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/lint': 6.9.7
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/javascript': 1.5.4
+
+ '@codemirror/lang-jinja@6.0.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-json@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@lezer/json': 1.0.3
+
+ '@codemirror/lang-less@6.0.2':
+ dependencies:
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/language': 6.12.4
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-liquid@6.3.2':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-markdown@6.4.0':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/markdown': 1.7.1
+
+ '@codemirror/lang-php@6.0.2':
+ dependencies:
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/php': 1.0.5
+
+ '@codemirror/lang-python@6.2.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/python': 1.1.19
+
+ '@codemirror/lang-rust@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@lezer/rust': 1.0.2
+
+ '@codemirror/lang-sass@6.0.2':
+ dependencies:
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/sass': 1.1.0
+
+ '@codemirror/lang-sql@6.10.0':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-vue@0.1.3':
+ dependencies:
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/language': 6.12.4
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-wast@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.12.4
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@codemirror/lang-xml@6.1.0':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/xml': 1.0.6
+
+ '@codemirror/lang-yaml@6.1.3':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/state': 6.7.1
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+ '@lezer/yaml': 1.0.4
+
+ '@codemirror/language-data@6.5.2':
+ dependencies:
+ '@codemirror/lang-angular': 0.1.4
+ '@codemirror/lang-cpp': 6.0.3
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/lang-go': 6.0.1
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/lang-java': 6.0.2
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/lang-jinja': 6.0.1
+ '@codemirror/lang-json': 6.0.2
+ '@codemirror/lang-less': 6.0.2
+ '@codemirror/lang-liquid': 6.3.2
+ '@codemirror/lang-markdown': 6.4.0
+ '@codemirror/lang-php': 6.0.2
+ '@codemirror/lang-python': 6.2.1
+ '@codemirror/lang-rust': 6.0.2
+ '@codemirror/lang-sass': 6.0.2
+ '@codemirror/lang-sql': 6.10.0
+ '@codemirror/lang-vue': 0.1.3
+ '@codemirror/lang-wast': 6.0.2
+ '@codemirror/lang-xml': 6.1.0
+ '@codemirror/lang-yaml': 6.1.3
+ '@codemirror/language': 6.12.4
+ '@codemirror/legacy-modes': 6.5.3
+
+ '@codemirror/language@6.12.4':
+ dependencies:
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+ style-mod: 4.1.3
+
+ '@codemirror/legacy-modes@6.5.3':
+ dependencies:
+ '@codemirror/language': 6.12.4
+
+ '@codemirror/lint@6.9.7':
+ dependencies:
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ crelt: 1.0.7
+
+ '@codemirror/search@6.7.1':
+ dependencies:
+ '@codemirror/state': 6.7.1
+ '@codemirror/view': 6.43.6
+ crelt: 1.0.7
+
+ '@codemirror/state@6.7.1':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.3
+
+ '@codemirror/view@6.43.6':
+ dependencies:
+ '@codemirror/state': 6.7.1
+ crelt: 1.0.7
+ style-mod: 4.1.3
+ w3c-keyname: 2.2.8
+
'@dotenvx/dotenvx@1.75.1':
dependencies:
'@dotenvx/primitives': 0.8.0
@@ -3277,10 +3767,105 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@lezer/common@1.5.2': {}
+
+ '@lezer/cpp@1.1.6':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/css@1.3.4':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/go@1.0.1':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/highlight@1.2.3':
+ dependencies:
+ '@lezer/common': 1.5.2
+
+ '@lezer/html@1.3.13':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/java@1.1.3':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/javascript@1.5.4':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/json@1.0.3':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/lr@1.4.10':
+ dependencies:
+ '@lezer/common': 1.5.2
+
+ '@lezer/markdown@1.7.1':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+
+ '@lezer/php@1.0.5':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/python@1.1.19':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/rust@1.0.2':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/sass@1.1.0':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/xml@1.0.6':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
+ '@lezer/yaml@1.0.4':
+ dependencies:
+ '@lezer/common': 1.5.2
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.10
+
'@lucide/vue@1.23.0(vue@3.5.39(typescript@5.6.3))':
dependencies:
vue: 3.5.39(typescript@5.6.3)
+ '@marijn/find-cluster-break@1.0.3': {}
+
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.14(hono@4.12.28)
@@ -3393,6 +3978,46 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true
+ '@shikijs/core@4.3.1':
+ dependencies:
+ '@shikijs/primitive': 4.3.1
+ '@shikijs/types': 4.3.1
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.3.1':
+ dependencies:
+ '@shikijs/types': 4.3.1
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.6
+
+ '@shikijs/engine-oniguruma@4.3.1':
+ dependencies:
+ '@shikijs/types': 4.3.1
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.3.1':
+ dependencies:
+ '@shikijs/types': 4.3.1
+
+ '@shikijs/primitive@4.3.1':
+ dependencies:
+ '@shikijs/types': 4.3.1
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/themes@4.3.1':
+ dependencies:
+ '@shikijs/types': 4.3.1
+
+ '@shikijs/types@4.3.1':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -3547,6 +4172,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/hast@3.0.5':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/json-schema@7.0.15': {}
'@types/mdast@4.0.4':
@@ -3907,6 +4536,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
+ crelt@1.0.7: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -4406,7 +5037,7 @@ snapshots:
hast-util-whitespace@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
he@1.2.0: {}
@@ -5048,6 +5679,14 @@ snapshots:
dependencies:
mimic-function: 5.0.1
+ oniguruma-parser@0.12.2: {}
+
+ oniguruma-to-es@4.3.6:
+ dependencies:
+ oniguruma-parser: 0.12.2
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
open@10.2.0:
dependencies:
default-browser: 5.5.0
@@ -5235,6 +5874,16 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
rehype-sanitize@6.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -5449,6 +6098,17 @@ snapshots:
shebang-regex@3.0.0: {}
+ shiki@4.3.1:
+ dependencies:
+ '@shikijs/core': 4.3.1
+ '@shikijs/engine-javascript': 4.3.1
+ '@shikijs/engine-oniguruma': 4.3.1
+ '@shikijs/langs': 4.3.1
+ '@shikijs/themes': 4.3.1
+ '@shikijs/types': 4.3.1
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
side-channel-list@1.0.1:
dependencies:
es-errors: 1.3.0
@@ -5551,6 +6211,8 @@ snapshots:
strip-final-newline@2.0.0: {}
+ style-mod@4.1.3: {}
+
stylus@0.57.0:
dependencies:
css: 3.0.0
@@ -5771,6 +6433,8 @@ snapshots:
optionalDependencies:
typescript: 5.6.3
+ w3c-keyname@2.2.8: {}
+
web-worker@1.5.0: {}
which@2.0.1:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index a23eae0..35decd1 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,3 +1,5 @@
allowBuilds:
esbuild: true
vue-demi: true
+overrides:
+ '@codemirror/lang-markdown': 6.4.0
diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue
index 4f2c5d0..8d4ecc0 100644
--- a/src/components/MarkdownEditor.vue
+++ b/src/components/MarkdownEditor.vue
@@ -5,7 +5,6 @@ import {
onBeforeUnmount,
ref,
watch,
- type ComponentPublicInstance,
} from "vue";
import { unified, type Plugin, type Transformer } from "unified";
import remarkParse from "remark-parse";
@@ -14,6 +13,9 @@ import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import { convertFileSrc, invoke, isTauri } from "@tauri-apps/api/core";
+import MarkdownCodeEditor from "./MarkdownCodeEditor.vue";
+import { highlightCodeToHtml } from "@/lib/codeHighlight";
+import { resolveMarkdownCodeLanguage } from "@/lib/markdownLanguages";
type DocumentMode = "preview" | "edit" | "mixed";
type ClipboardMediaKind = "image" | "video";
@@ -59,6 +61,13 @@ interface MarkdownBlock {
endOffset: number;
}
+interface MarkdownCodeEditorApi {
+ focus: () => void;
+ focusAtEnd: () => void;
+ replaceRange: (text: string, from?: number, to?: number) => void;
+ scrollToLine: (lineNumber: number) => void;
+}
+
const props = withDefaults(
defineProps<{
modelValue?: string;
@@ -232,9 +241,9 @@ const renderedHtml = ref("");
const renderError = ref(false);
const markdownBlocks = ref([]);
-const editorTextareaRef = ref();
+const editorCodeRef = ref();
const scrollRef = ref();
-const blockTextareaRef = ref(null);
+const blockCodeRef = ref();
const EMPTY_BLOCK_ID = "__empty__";
const editingBlockId = ref(null);
@@ -247,8 +256,8 @@ const pendingLocalModelUpdate = ref(false);
const isEditingEmptyDocument = computed(
() => editingBlockId.value === EMPTY_BLOCK_ID,
);
-let textareaResizeFrame: number | null = null;
let modeScrollRestoreFrame: number | null = null;
+let renderRequestId = 0;
interface ScrollSnapshot {
ratio: number;
@@ -278,7 +287,6 @@ function restoreScrollSnapshot(snapshot: ScrollSnapshot | null) {
function restoreScrollAfterModeChange(
snapshot: ScrollSnapshot | null,
- mode: DocumentMode,
) {
if (!snapshot) return;
@@ -288,17 +296,10 @@ function restoreScrollAfterModeChange(
}
void nextTick(() => {
- if (mode === "edit" && editorTextareaRef.value) {
- resizeTextarea(editorTextareaRef.value);
- }
-
restoreScrollSnapshot(snapshot);
modeScrollRestoreFrame = requestAnimationFrame(() => {
modeScrollRestoreFrame = null;
- if (mode === "edit" && editorTextareaRef.value) {
- resizeTextarea(editorTextareaRef.value);
- }
restoreScrollSnapshot(snapshot);
});
});
@@ -306,21 +307,62 @@ function restoreScrollAfterModeChange(
function syncRenderedState(md: string, mode = activeDocumentMode.value) {
if (mode === "preview") {
- renderFullHtml(md);
+ void renderFullHtml(md);
return;
}
if (mode === "mixed") {
- parseBlocks(md);
+ void parseBlocks(md);
}
}
-function renderFullHtml(md: string) {
+async function highlightCodeBlocks(html: string) {
+ if (!html.includes(" code"));
+
+ await Promise.all(
+ codeBlocks.map(async (code) => {
+ const languageClass = Array.from(code.classList).find((className) =>
+ className.startsWith("language-"),
+ );
+ const requestedLanguage = languageClass?.slice("language-".length) || "text";
+ const language = resolveMarkdownCodeLanguage(requestedLanguage)?.shiki || "text";
+ code.parentElement?.setAttribute("data-language", requestedLanguage);
+
+ try {
+ const highlighted = await highlightCodeToHtml(code.textContent || "", language);
+ if (!highlighted) return;
+ const fragment = new DOMParser().parseFromString(highlighted, "text/html");
+ const highlightedPre = fragment.body.firstElementChild;
+ if (highlightedPre) {
+ highlightedPre.setAttribute("data-language", requestedLanguage);
+ code.parentElement?.replaceWith(highlightedPre);
+ }
+ } catch {
+ code.parentElement?.setAttribute("data-language", requestedLanguage);
+ }
+ }),
+ );
+
+ return document.body.innerHTML;
+}
+
+async function renderMarkdownHtml(md: string) {
+ const file = md2html.processSync(md);
+ return highlightCodeBlocks(String(file.value));
+}
+
+async function renderFullHtml(md: string) {
+ const requestId = ++renderRequestId;
try {
- const file = md2html.processSync(md);
- renderedHtml.value = String(file.value);
+ const html = await renderMarkdownHtml(md);
+ if (requestId !== renderRequestId || activeDocumentMode.value !== "preview") return;
+ renderedHtml.value = html;
renderError.value = false;
} catch {
+ if (requestId !== renderRequestId) return;
renderedHtml.value = "";
renderError.value = true;
}
@@ -335,16 +377,16 @@ function escapeHtml(value: string) {
.replace(/'/g, "'");
}
-function renderBlockHtml(source: string) {
+async function renderBlockHtml(source: string) {
try {
- const file = md2html.processSync(source);
- return String(file.value);
+ return await renderMarkdownHtml(source);
} catch {
return `${escapeHtml(source)}
`;
}
}
-function parseBlocks(md: string) {
+async function parseBlocks(md: string) {
+ const requestId = ++renderRequestId;
if (!md.trim()) {
markdownBlocks.value = [];
renderError.value = false;
@@ -353,8 +395,8 @@ function parseBlocks(md: string) {
try {
const tree = mdParser.parse(md) as { children?: MarkdownAstNode[] };
- const blocks = (tree.children ?? [])
- .map((node, index): MarkdownBlock | null => {
+ const blockCandidates = (tree.children ?? [])
+ .map((node, index): Omit | null => {
const startOffset = node.position?.start?.offset;
const endOffset = node.position?.end?.offset;
@@ -371,22 +413,31 @@ function parseBlocks(md: string) {
id: `${index}-${startOffset}-${endOffset}`,
type: node.type || "unknown",
source,
- html: renderBlockHtml(source),
startOffset,
endOffset,
};
})
- .filter((block): block is MarkdownBlock => block !== null);
+ .filter((block): block is Omit => block !== null);
+ const blocks = await Promise.all(
+ blockCandidates.map(async (block) => ({
+ ...block,
+ html: await renderBlockHtml(block.source),
+ })),
+ );
+
+ if (requestId !== renderRequestId || activeDocumentMode.value !== "mixed") return;
markdownBlocks.value = blocks;
renderError.value = false;
} catch {
+ const html = await renderBlockHtml(md);
+ if (requestId !== renderRequestId || activeDocumentMode.value !== "mixed") return;
markdownBlocks.value = [
{
id: "fallback-0",
type: "unknown",
source: md,
- html: renderBlockHtml(md),
+ html,
startOffset: 0,
endOffset: md.length,
},
@@ -411,7 +462,6 @@ watch(
}
nextTick(() => {
- scheduleEditorTextareaResize();
// 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。
if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
});
@@ -419,40 +469,11 @@ watch(
{ immediate: true },
);
-// ── Editor textarea handlers ───────────────────────────────────────
-
-function setBlockTextareaRef(el: Element | ComponentPublicInstance | null) {
- blockTextareaRef.value = el instanceof HTMLTextAreaElement ? el : null;
-}
-
-function resizeTextarea(ta: HTMLTextAreaElement) {
- ta.style.height = "auto";
- ta.style.height = `${ta.scrollHeight}px`;
-}
-
-function scheduleTextareaResize(ta: HTMLTextAreaElement | undefined | null) {
- if (!ta) return;
- if (textareaResizeFrame !== null) {
- cancelAnimationFrame(textareaResizeFrame);
- }
-
- textareaResizeFrame = requestAnimationFrame(() => {
- textareaResizeFrame = null;
- if (ta.isConnected) resizeTextarea(ta);
- });
-}
-
-function scheduleEditorTextareaResize() {
- scheduleTextareaResize(editorTextareaRef.value);
-}
+// ── Editor handlers ────────────────────────────────────────────────
function focusBlockEditor() {
void nextTick(() => {
- const ta = blockTextareaRef.value;
- if (!ta) return;
- resizeTextarea(ta);
- ta.focus();
- ta.selectionStart = ta.selectionEnd = ta.value.length;
+ blockCodeRef.value?.focusAtEnd();
});
}
@@ -465,54 +486,6 @@ function applyFullEditorValue(value: string) {
emitModelValue(value);
}
-function onEditorInput(e: Event) {
- const ta = e.target as HTMLTextAreaElement;
- const val = ta.value;
- applyFullEditorValue(val);
- scheduleTextareaResize(ta);
-}
-
-function tryAutoPairForValue(
- ta: HTMLTextAreaElement,
- key: string,
- value: string,
- onChange: (nextValue: string) => void,
-): boolean {
- const pairs: Record = {
- "(": ")",
- "[": "]",
- "{": "}",
- '"': '"',
- "'": "'",
- "`": "`",
- };
- const close = pairs[key];
- if (!close) return false;
-
- const start = ta.selectionStart;
- const end = ta.selectionEnd;
- const selected = value.substring(start, end);
- const newVal =
- value.substring(0, start) +
- key +
- selected +
- close +
- value.substring(end);
-
- onChange(newVal);
-
- void nextTick(() => {
- ta.selectionStart = start + 1;
- ta.selectionEnd = start + 1 + selected.length;
- ta.focus();
- });
- return true;
-}
-
-function tryAutoPair(ta: HTMLTextAreaElement, key: string): boolean {
- return tryAutoPairForValue(ta, key, props.modelValue, applyFullEditorValue);
-}
-
function isSaveShortcut(e: KeyboardEvent) {
return (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s";
}
@@ -530,36 +503,6 @@ function onRootKeydown(e: KeyboardEvent) {
}
}
-function onEditorKeydown(e: KeyboardEvent) {
- if (isSaveShortcut(e)) {
- requestSave(e);
- return;
- }
-
- const ta = e.target as HTMLTextAreaElement;
-
- if (e.key === "Tab") {
- e.preventDefault();
- const start = ta.selectionStart;
- const end = ta.selectionEnd;
- const newVal =
- props.modelValue.substring(0, start) +
- " " +
- props.modelValue.substring(end);
- applyFullEditorValue(newVal);
- void nextTick(() => {
- ta.selectionStart = ta.selectionEnd = start + 2;
- ta.focus();
- });
- return;
- }
-
- if (tryAutoPair(ta, e.key)) {
- e.preventDefault();
- return;
- }
-}
-
// ── Mixed mode block editing ───────────────────────────────────────
function startBlockEdit(block: MarkdownBlock) {
@@ -619,52 +562,6 @@ function finalizeBlockEdit(): string | undefined {
return nextValue;
}
-function onBlockInput(e: Event) {
- const ta = e.target as HTMLTextAreaElement;
- editingSource.value = ta.value;
- scheduleTextareaResize(ta);
-}
-
-function onBlockKeydown(e: KeyboardEvent) {
- const ta = e.target as HTMLTextAreaElement;
-
- if (isSaveShortcut(e)) {
- requestSave(e);
- return;
- }
-
- if (e.key === "Escape") {
- e.preventDefault();
- cancelBlockEdit();
- return;
- }
-
- if (e.key === "Tab") {
- e.preventDefault();
- const start = ta.selectionStart;
- const end = ta.selectionEnd;
- editingSource.value =
- editingSource.value.substring(0, start) +
- " " +
- editingSource.value.substring(end);
- void nextTick(() => {
- ta.selectionStart = ta.selectionEnd = start + 2;
- scheduleTextareaResize(ta);
- ta.focus();
- });
- return;
- }
-
- if (
- tryAutoPairForValue(ta, e.key, editingSource.value, (nextValue) => {
- editingSource.value = nextValue;
- void nextTick(() => scheduleTextareaResize(ta));
- })
- ) {
- e.preventDefault();
- }
-}
-
function flushPendingChanges(): string | undefined {
if (!editingBlockId.value) return undefined;
return finalizeBlockEdit();
@@ -679,17 +576,10 @@ watch(activeDocumentMode, (mode, oldMode) => {
syncRenderedState(committedValue ?? props.modelValue, mode);
- if (mode === "edit") {
- void nextTick(scheduleEditorTextareaResize);
- }
-
- restoreScrollAfterModeChange(scrollSnapshot, mode);
+ restoreScrollAfterModeChange(scrollSnapshot);
});
onBeforeUnmount(() => {
- if (textareaResizeFrame !== null) {
- cancelAnimationFrame(textareaResizeFrame);
- }
if (modeScrollRestoreFrame !== null) {
cancelAnimationFrame(modeScrollRestoreFrame);
}
@@ -952,47 +842,38 @@ async function processPaste(
applyValue(newValue, cursorPos);
}
-async function onEditorPaste(e: ClipboardEvent) {
- const ta = editorTextareaRef.value;
- if (!ta) return;
-
+async function onEditorPaste(payload: {
+ event: ClipboardEvent;
+ value: string;
+ from: number;
+ to: number;
+}) {
await processPaste(
- e,
- props.modelValue,
- ta.selectionStart,
- ta.selectionEnd,
+ payload.event,
+ payload.value,
+ payload.from,
+ payload.to,
(newValue, cursorPos) => {
- applyFullEditorValue(newValue);
- void nextTick(() => {
- const ta = editorTextareaRef.value;
- if (ta) {
- ta.selectionStart = ta.selectionEnd = cursorPos;
- ta.focus();
- }
- });
+ const insertedText = newValue.slice(payload.from, cursorPos);
+ editorCodeRef.value?.replaceRange(insertedText, payload.from, payload.to);
},
);
}
-async function onBlockPaste(e: ClipboardEvent) {
- const ta = blockTextareaRef.value;
- if (!ta) return;
-
+async function onBlockPaste(payload: {
+ event: ClipboardEvent;
+ value: string;
+ from: number;
+ to: number;
+}) {
await processPaste(
- e,
- editingSource.value,
- ta.selectionStart,
- ta.selectionEnd,
+ payload.event,
+ payload.value,
+ payload.from,
+ payload.to,
(newValue, cursorPos) => {
- editingSource.value = newValue;
- void nextTick(() => {
- const ta = blockTextareaRef.value;
- if (ta) {
- ta.selectionStart = ta.selectionEnd = cursorPos;
- ta.focus();
- scheduleTextareaResize(ta);
- }
- });
+ const insertedText = newValue.slice(payload.from, cursorPos);
+ blockCodeRef.value?.replaceRange(insertedText, payload.from, payload.to);
},
);
}
@@ -1001,19 +882,7 @@ function scrollToLine(lineNumber: number) {
if (!scrollRef.value) return;
if (activeDocumentMode.value === "edit") {
- const ta = editorTextareaRef.value;
- if (ta) {
- const lineHeight = 24;
- const targetScroll = (lineNumber - 1) * lineHeight;
- ta.scrollTop = Math.max(0, targetScroll - 80);
- ta.focus();
- const lines = ta.value.split("\n");
- let pos = 0;
- for (let i = 0; i < Math.min(lineNumber - 1, lines.length); i++) {
- pos += lines[i].length + 1;
- }
- ta.setSelectionRange(pos, pos);
- }
+ editorCodeRef.value?.scrollToLine(lineNumber);
} else {
const previewEl = scrollRef.value.querySelector(".markdown-preview") as HTMLElement;
if (previewEl) {
@@ -1056,18 +925,14 @@ defineExpose({
}"
>
-
@@ -1084,15 +949,16 @@ defineExpose({
⚠ Markdown 解析错误
-
@@ -1114,17 +980,17 @@ defineExpose({
class="markdown-block"
:class="{ 'markdown-block-editing': editingBlockId === block.id }"
>
-
@@ -1164,14 +1030,9 @@ defineExpose({
.markdown-source-editor {
display: block;
- height: auto;
color: var(--app-text);
}
-.markdown-source-editor::placeholder {
- color: var(--app-subtle);
-}
-
.markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child {
margin-top: 0;
}
@@ -1216,10 +1077,30 @@ defineExpose({
overflow-x: auto;
margin: 0.75em 0;
}
+.markdown-preview pre.shiki {
+ position: relative;
+ background-color: var(--code-background) !important;
+ color: var(--code-foreground);
+ border: 1px solid var(--code-border);
+}
+.markdown-preview pre.shiki[data-language]::before {
+ content: attr(data-language);
+ display: block;
+ margin-bottom: 0.65rem;
+ color: var(--app-muted);
+ font-family: "Geist", "PingFang SC", "Microsoft YaHei", sans-serif;
+ font-size: 0.7rem;
+ font-weight: 600;
+ line-height: 1;
+ text-transform: lowercase;
+}
+.markdown-preview pre.shiki span {
+ color: inherit;
+}
.markdown-preview pre code {
background: none;
padding: 0;
- color: var(--app-text);
+ color: var(--code-foreground);
font-size: 0.875em;
border-radius: 0;
word-break: normal;
@@ -1324,7 +1205,7 @@ defineExpose({
width: 100%;
min-height: 1.625em;
resize: none;
- overflow: hidden;
+ overflow: visible;
border: 0;
border-radius: 0;
background: transparent;
@@ -1338,6 +1219,10 @@ defineExpose({
caret-color: var(--app-accent);
}
+.markdown-block-editor .cm-tooltip {
+ font-size: calc(0.875rem * var(--editor-zoom, 1));
+}
+
.markdown-block-editor-soft-wrap {
white-space: normal;
}
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue
index 7ba1f36..db60b24 100644
--- a/src/components/SettingsModal.vue
+++ b/src/components/SettingsModal.vue
@@ -122,14 +122,45 @@ const documentModeOptions: Array<{
const themeOptions: Array<{
value: AppTheme;
label: string;
- swatch: string;
+ description: string;
+ swatches: [string, string, string];
}> = [
- { value: "system", label: "跟随系统", swatch: "linear-gradient(135deg, #ffffff 0 50%, #17191d 50% 100%)" },
- { value: "warm", label: "柔和暖色", swatch: "#bf6a3b" },
- { value: "white", label: "白色", swatch: "#2563eb" },
- { value: "black", label: "黑色", swatch: "#8ab4ff" },
- { value: "gruvbox", label: "Gruvbox", swatch: "#fe8019" },
- { value: "gray", label: "灰色", swatch: "#4f6f93" },
+ {
+ value: "system",
+ label: "跟随系统",
+ description: "随操作系统在浅色与深色之间切换",
+ swatches: ["#ffffff", "#101113", "#7c9cff"],
+ },
+ {
+ value: "warm",
+ label: "柔和暖色",
+ description: "暖白纸张、陶土强调色",
+ swatches: ["#faf9f6", "#bf6a3b", "#7a55d9"],
+ },
+ {
+ value: "white",
+ label: "明亮白色",
+ description: "纯白背景、清晰蓝色强调",
+ swatches: ["#ffffff", "#2563eb", "#8250df"],
+ },
+ {
+ value: "gray",
+ label: "中性灰色",
+ description: "低饱和界面、沉静蓝灰强调",
+ swatches: ["#f3f4f6", "#4f6f93", "#7c5c99"],
+ },
+ {
+ value: "black",
+ label: "深色",
+ description: "近黑背景、冷调高对比代码色",
+ swatches: ["#101113", "#8ab4ff", "#c58af9"],
+ },
+ {
+ value: "gruvbox",
+ label: "Gruvbox",
+ description: "复古深色、暖橙与青绿色代码色",
+ swatches: ["#282828", "#fe8019", "#8ec07c"],
+ },
];
function loadBooleanPref(key: string, fallback: boolean) {
@@ -239,15 +270,6 @@ function onDefaultDocumentModeChange(event: Event) {
emit("updateDefaultDocumentMode", mode);
}
-function onThemeChange(event: Event) {
- const theme = (event.target as HTMLSelectElement).value as AppTheme;
- emit("updateAppTheme", theme);
-}
-
-function resetTheme() {
- emit("updateAppTheme", "warm");
-}
-
function setSyncStatus(message: string, isError = false) {
if (isError) {
syncError.value = message;
@@ -690,51 +712,35 @@ onBeforeUnmount(() => {
- 基础颜色
- 设置 {{ APP_NAME }} 的基础颜色。
+ 外观主题
+ 主题同时应用于界面、Markdown 预览和代码编辑。
-
-
-
- 主题色
- 为 {{ APP_NAME }} 选择一个主题色。主题色将影响选中、链接等元素的颜色。
-
-
+
-
-
+ >
+
+
+ {{ option.label }}
+ {{ option.description }}
+
+
+
@@ -1165,46 +1171,70 @@ onBeforeUnmount(() => {
color: #dc2626;
}
-.theme-swatch-group {
- display: inline-flex;
- align-items: center;
+.theme-option-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
+ padding: 4px 20px 20px;
}
-.theme-reset-btn,
-.theme-swatch {
- display: inline-flex;
+.theme-option {
+ display: grid;
+ grid-template-columns: 48px minmax(0, 1fr) 18px;
+ gap: 11px;
align-items: center;
- justify-content: center;
- border: none;
- cursor: pointer;
-}
-
-.theme-reset-btn {
- width: 30px;
- height: 30px;
- border-radius: 6px;
- background: transparent;
- color: var(--app-muted);
-}
-
-.theme-reset-btn:hover {
- background: var(--app-hover);
+ min-height: 64px;
+ padding: 10px;
+ border: 1px solid var(--app-border-strong);
+ border-radius: 7px;
+ background: var(--app-surface);
color: var(--app-text);
+ text-align: left;
+ cursor: pointer;
+ transition: border-color 0.15s ease, background-color 0.15s ease;
}
-.theme-swatch {
- width: 22px;
- height: 22px;
- border-radius: 999px;
- box-shadow:
- 0 0 0 1px color-mix(in srgb, var(--app-text) 18%, transparent),
- 0 2px 5px color-mix(in srgb, var(--app-shadow) 70%, transparent);
+.theme-option:hover {
+ background: var(--app-surface-muted);
}
-.theme-swatch-active {
- outline: 2px solid var(--app-accent);
- outline-offset: 3px;
+.theme-option-active {
+ border-color: var(--app-accent);
+ background: var(--app-active-bg);
+}
+
+.theme-option-preview {
+ display: flex;
+ width: 48px;
+ height: 38px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--app-text) 18%, transparent);
+ border-radius: 5px;
+ box-shadow: 0 2px 5px color-mix(in srgb, var(--app-shadow) 55%, transparent);
+}
+
+.theme-option-copy {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.theme-option-copy strong {
+ color: var(--app-text-strong);
+ font-size: 13px;
+ line-height: 1.2;
+}
+
+.theme-option-copy small {
+ color: var(--app-muted);
+ font-size: 11px;
+ line-height: 1.35;
+}
+
+.theme-option > i {
+ color: var(--app-accent);
+ font-size: 16px;
}
.settings-empty-page {
diff --git a/src/tailwind.css b/src/tailwind.css
index 49f4679..118badb 100644
--- a/src/tailwind.css
+++ b/src/tailwind.css
@@ -44,6 +44,21 @@
--app-overlay: rgba(56, 52, 46, 0.22);
--app-toast-bg: rgba(56, 52, 46, 0.8);
--app-shadow: rgba(56, 52, 46, 0.16);
+ --code-background: #f4f1ea;
+ --code-foreground: #38342e;
+ --code-border: #e0dbcf;
+ --code-token-comment: #8c877d;
+ --code-token-constant: #8c4f2b;
+ --code-token-keyword: #7651a8;
+ --code-token-parameter: #9f4f28;
+ --code-token-function: #315f78;
+ --code-token-string: #6f641f;
+ --code-token-string-expression: #6f641f;
+ --code-token-punctuation: #6e685f;
+ --code-token-link: #6342bd;
+ --code-token-inserted: #3d7a4d;
+ --code-token-deleted: #b34242;
+ --code-token-changed: #9a691f;
}
:root[data-theme="white"] {
@@ -74,6 +89,21 @@
--app-overlay: rgba(17, 24, 39, 0.2);
--app-toast-bg: rgba(17, 24, 39, 0.82);
--app-shadow: rgba(17, 24, 39, 0.15);
+ --code-background: #f4f6f8;
+ --code-foreground: #24292f;
+ --code-border: #d7dbe0;
+ --code-token-comment: #6e7781;
+ --code-token-constant: #953800;
+ --code-token-keyword: #8250df;
+ --code-token-parameter: #953800;
+ --code-token-function: #0550ae;
+ --code-token-string: #0a3069;
+ --code-token-string-expression: #0a3069;
+ --code-token-punctuation: #57606a;
+ --code-token-link: #0969da;
+ --code-token-inserted: #1a7f37;
+ --code-token-deleted: #cf222e;
+ --code-token-changed: #9a6700;
}
:root[data-theme="gray"] {
@@ -104,6 +134,21 @@
--app-overlay: rgba(31, 35, 40, 0.24);
--app-toast-bg: rgba(31, 35, 40, 0.82);
--app-shadow: rgba(31, 35, 40, 0.14);
+ --code-background: #eceff2;
+ --code-foreground: #2f3337;
+ --code-border: #cfd5dc;
+ --code-token-comment: #737a82;
+ --code-token-constant: #8b572a;
+ --code-token-keyword: #7c5c99;
+ --code-token-parameter: #8b572a;
+ --code-token-function: #3b668c;
+ --code-token-string: #537238;
+ --code-token-string-expression: #537238;
+ --code-token-punctuation: #656c74;
+ --code-token-link: #3b668c;
+ --code-token-inserted: #4e7a52;
+ --code-token-deleted: #a94a4a;
+ --code-token-changed: #8b6b2f;
}
:root[data-theme="black"] {
@@ -134,6 +179,21 @@
--app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36);
+ --code-background: #1b1e23;
+ --code-foreground: #e7e9ed;
+ --code-border: #383d45;
+ --code-token-comment: #8b949e;
+ --code-token-constant: #ffa657;
+ --code-token-keyword: #ff7b72;
+ --code-token-parameter: #ffa657;
+ --code-token-function: #d2a8ff;
+ --code-token-string: #a5d6ff;
+ --code-token-string-expression: #a5d6ff;
+ --code-token-punctuation: #b1bac4;
+ --code-token-link: #79c0ff;
+ --code-token-inserted: #7ee787;
+ --code-token-deleted: #ff7b72;
+ --code-token-changed: #e3b341;
}
:root[data-theme="gruvbox"] {
@@ -164,6 +224,21 @@
--app-overlay: rgba(29, 32, 33, 0.52);
--app-toast-bg: rgba(29, 32, 33, 0.9);
--app-shadow: rgba(0, 0, 0, 0.38);
+ --code-background: #1d2021;
+ --code-foreground: #ebdbb2;
+ --code-border: #504945;
+ --code-token-comment: #928374;
+ --code-token-constant: #d3869b;
+ --code-token-keyword: #fb4934;
+ --code-token-parameter: #fe8019;
+ --code-token-function: #fabd2f;
+ --code-token-string: #b8bb26;
+ --code-token-string-expression: #b8bb26;
+ --code-token-punctuation: #bdae93;
+ --code-token-link: #83a598;
+ --code-token-inserted: #b8bb26;
+ --code-token-deleted: #fb4934;
+ --code-token-changed: #fabd2f;
}
@media (prefers-color-scheme: dark) {
@@ -195,6 +270,21 @@
--app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36);
+ --code-background: #1b1e23;
+ --code-foreground: #e7e9ed;
+ --code-border: #383d45;
+ --code-token-comment: #8b949e;
+ --code-token-constant: #ffa657;
+ --code-token-keyword: #ff7b72;
+ --code-token-parameter: #ffa657;
+ --code-token-function: #d2a8ff;
+ --code-token-string: #a5d6ff;
+ --code-token-string-expression: #a5d6ff;
+ --code-token-punctuation: #b1bac4;
+ --code-token-link: #79c0ff;
+ --code-token-inserted: #7ee787;
+ --code-token-deleted: #ff7b72;
+ --code-token-changed: #e3b341;
}
}
@@ -374,4 +464,4 @@ body {
@apply bg-background text-foreground;
@apply font-sans;
}
-}
\ No newline at end of file
+}