feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
11
product-source/hololake-platform/apps/README.md
Normal file
11
product-source/hololake-platform/apps/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Apps
|
||||
|
||||
此目录用于人类可见的光湖 App 与 Tolaria 侧界面扩展。
|
||||
|
||||
第一版只实现工单、状态和回执的可视化闭环。
|
||||
|
||||
Tolaria / 光湖 App 的产品边界、授权模型和版本路线见
|
||||
[`../architecture/TOLARIA-DESKTOP-ARCHITECTURE.md`](../architecture/TOLARIA-DESKTOP-ARCHITECTURE.md)。
|
||||
|
||||
当前可编译基线、光湖定制补丁与验证入口见
|
||||
[`tolaria/README.md`](tolaria/README.md)。
|
||||
82
product-source/hololake-platform/apps/tolaria/README.md
Normal file
82
product-source/hololake-platform/apps/tolaria/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Tolaria / 光湖 App · 研发入口
|
||||
|
||||
> 产品名:冰朔通感语言核操作系统的人类协作界面
|
||||
>
|
||||
> 研发主仓:HoloLake Platform
|
||||
>
|
||||
> 源码基线:`bingshuo/guanghu`(上游 Tolaria 兼容零件基线)
|
||||
|
||||
这里是 Tolaria / 光湖 App 的研发入口,不把整份第三方上游源码复制进
|
||||
平台仓。平台仓保存产品边界、可审计的定制补丁、验证约定与发布说明;
|
||||
`guanghu` 仅承担上游兼容与零件基线的职责。
|
||||
|
||||
## 当前界面迭代 · 冰湖视觉层
|
||||
|
||||
`patches/` 中的补丁以系列入口恢复。`0003` 已包含 `0001`、`0002`,
|
||||
因此不能将 `0001`、`0002`、`0003` 连续重复应用:
|
||||
|
||||
1. `0001-feat-add-Guanghu-ice-lake-visual-layer.patch`
|
||||
- 为浅色与深色模式增加冰湖光层、网格层和语义颜色;
|
||||
- 升级侧栏、文档面与编辑区的视觉层次;
|
||||
- 将 HLDP 光湖入口渲染为有状态、可聚焦的路径卡片。
|
||||
2. `0002-test-align-updater-coverage-with-Guanghu-policy.patch`
|
||||
- 固化“光湖不使用 Tolaria 上游应用更新源”的既有策略;
|
||||
- 使更新器测试验证不联网、不下载、不重启的安全边界。
|
||||
3. `0003-20260713-hololake-era-shell-prototype-series.patch`
|
||||
- 登记 HoloLake Era 原型壳层与产品入口探索结果;
|
||||
- 仅作为原型证据,不代表采用重型全局皮肤。
|
||||
4. `0004-20260715-tolaria-lightweight-themes-drag-fix.patch`
|
||||
- 收敛为轻量、可切换的光湖配套主题;
|
||||
- 修正 macOS 顶栏间距、深色应用图标和桌面窗口拖拽;
|
||||
- 增强本地 smoke 测试在大规模模拟笔记列表中的可重复性。
|
||||
5. `0005-Add-portable-callout-page-blocks.patch`
|
||||
- 在 BlockNote 编辑器中增加 info / success / warning / danger 彩色提示块;
|
||||
- 以可携带 Markdown callout 语法保存,重新打开不丢失。
|
||||
6. `0006-fix-make-Guanghu-home-and-themes-visible.patch`
|
||||
- 首次启动默认显示 HoloLake Era 世界首页;
|
||||
- 使湖光浅色与湖心深色同步实际亮/暗模式,不再只改按钮文字。
|
||||
7. `0007-feat-complete-portable-page-block-set.patch`
|
||||
- 恢复 toggle heading / toggle list 折叠块入口;
|
||||
- 增加批注块、关系卡和路径包,继续复用可携带 Markdown callout 协议;
|
||||
- 保留现有代码块、引用、Mermaid、表格、白板与媒体块。
|
||||
8. `0008-fix-restore-internal-note-links-and-reading-rhythm.patch`
|
||||
- 将 Notion 导出的相对 Markdown 页面链接接回 Tolaria 内部导航;
|
||||
- 普通单击直接打开目标笔记,外部网址仍保留 `⌘ + 点击`;
|
||||
- 增强引用提示面板、段落节奏、代码块、表格与行内代码层次。
|
||||
9. `0009-test-lock-in-readable-editor-theme-defaults.patch`
|
||||
- 固化更宽的阅读列与带颜色的行内代码主题默认值。
|
||||
10. `0010-fix-resolve-relative-page-links-from-source-notes.patch`
|
||||
- 按当前笔记所在目录归一化 `../` 与 `./` 页面路径;
|
||||
- 避免多个页面都叫 `README.md` 时跳到错误页面。
|
||||
11. `0011-fix-resolve-links-inside-mounted-workspaces.patch`
|
||||
- 使用来源页面所属的挂载工作区解析相对路径;
|
||||
- 主仓与挂载知识库同时打开时仍能跳到正确页面。
|
||||
|
||||
## 恢复与验证
|
||||
|
||||
```bash
|
||||
git clone https://guanghubingshuo.com/code/bingshuo/guanghu.git
|
||||
cd guanghu
|
||||
git am /path/to/0003-20260713-hololake-era-shell-prototype-series.patch
|
||||
git am /path/to/0004-20260715-tolaria-lightweight-themes-drag-fix.patch
|
||||
git am /path/to/0005-Add-portable-callout-page-blocks.patch
|
||||
git am /path/to/0006-fix-make-Guanghu-home-and-themes-visible.patch
|
||||
git am /path/to/0007-feat-complete-portable-page-block-set.patch
|
||||
git am /path/to/0008-fix-restore-internal-note-links-and-reading-rhythm.patch
|
||||
git am /path/to/0009-test-lock-in-readable-editor-theme-defaults.patch
|
||||
git am /path/to/0010-fix-resolve-relative-page-links-from-source-notes.patch
|
||||
git am /path/to/0011-fix-resolve-links-inside-mounted-workspaces.patch
|
||||
pnpm build
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
`0001`、`0002` 保留为早期两补丁拆分档案;只有在不使用 `0003`
|
||||
原型系列时才单独应用。上述 `0003 → 0011` 顺序已于 2026-07-17
|
||||
在服务器最新 `bingshuo/guanghu@514ab19` 的新克隆上完整复现。
|
||||
|
||||
## 研发规则
|
||||
|
||||
- 新的页面、渲染模块和交互协议先在本目录登记,再以独立补丁落到可编译基线;
|
||||
- 知识库、仓库路径、关系和编号以仓库文件为事实源,界面只做可读转译;
|
||||
- UI 不保存服务器 token、密码、验证码或私钥;现实操作仍须走工单、授权、回执闭环;
|
||||
- 后续模块以可安装/可移除包发布,并声明依赖、权限与兼容版本。
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# HLP-MOD-0001 · 系统壳与产品内核
|
||||
|
||||
负责 Tolaria / 光湖 App 的桌面与未来移动端运行骨架:窗口、深链接、工作区切换、
|
||||
本地设置与产品级安全边界。它提供界面承载能力,不保存目标服务器密钥,也不直接执行任意命令。
|
||||
|
||||
代码基线与定制补丁入口见 [`../../README.md`](../../README.md)。
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# HLP-MOD-0002 · 视觉与知识渲染
|
||||
|
||||
把仓库中的 Markdown、编号、关系和媒体内容转译成可读页面。包含冰湖视觉层、路径入口卡、
|
||||
表格、Mermaid、媒体、白板与后续 Notion 式内容块。它只渲染正式事实源,不复制出第二套知识库。
|
||||
|
||||
当前冰湖 UI 补丁位于 [`../../patches/`](../../patches/)。
|
||||
|
||||
## 当前恢复状态
|
||||
|
||||
- 彩色 callout、批注、关系卡、路径包与折叠块已经恢复;
|
||||
- 普通引用现在显示为有背景、有边界的提示面板;
|
||||
- 标题、正文、代码块和表格之间已恢复清楚的阅读节奏;
|
||||
- Notion 导出的相对 `.md` 页面链接会在 Tolaria 内普通单击跳转;
|
||||
- 外部网址仍使用 `⌘ + 点击`,避免编辑时误开浏览器。
|
||||
|
||||
## 已完成内容块
|
||||
|
||||
冰朔在 2026-07-16 反馈:当前页面太单一,缺少类似 Notion 的内容块能力,尤其是彩色提示块和高亮代码块。
|
||||
|
||||
首批完成项:
|
||||
|
||||
```text
|
||||
1. 高亮代码块
|
||||
2. 彩色提示块
|
||||
3. 折叠块
|
||||
4. 引用 / 标注块
|
||||
5. 关系卡
|
||||
6. 路径面包屑
|
||||
7. 媒体 / 图表块
|
||||
```
|
||||
|
||||
当前验收入口见:
|
||||
|
||||
- [NEXT-TODO-20260717 · 明日待办](../../../../docs/NEXT-TODO-20260717.md)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0003 · 仓库路径与知识索引
|
||||
|
||||
负责把代码仓库的文件树、编号和嵌套路径转成两种视图:人类可读的知识导航,及 AI 可执行的路径包。
|
||||
它解决“页面知道上下文、人格体也能沿同一编号找路”的问题,不负责替人类授予执行权限。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0004 · Agent 与 HLDP 协议
|
||||
|
||||
负责光湖自有 Agent 框架、HLDP 原生寻址与上下文恢复。MCP 在这里是外部工具适配层,
|
||||
不是系统内部身份、记忆或权限的唯一表达。该模块只能生成计划和受限调用请求;现实动作仍由授权模块裁决。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0005 · 工单、授权与审计
|
||||
|
||||
负责把自然语言意图转成可审阅的工单、计划、风险提示、人工确认、一次性受限动作和回执。
|
||||
企业总灯塔与企业微信审批适配属于此类;模块本身不保存目标服务器 root 密钥。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0006 · 协作与移动云端
|
||||
|
||||
负责账号、个人工作区、跨设备访问、手机端与“人类—人格体”群聊式协作空间。正常对话只产生协作记录;
|
||||
涉及真实操作时,必须显示工单和授权卡,不能把聊天文本直接变成系统执行。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0007 · 模块运行时与发布
|
||||
|
||||
负责模块热插拔、版本兼容、依赖、权限声明、安装、更新与移除。每个模块都应可独立发布;
|
||||
安装高权限模块时需要单独展示权限范围,并且默认不继承服务器执行权。
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# HLP-MOD-0008 · 外部集成适配
|
||||
|
||||
负责与 MCP、代码仓库、企业灯塔、通知渠道和未来第三方服务的适配。适配器只暴露完成目标所需的最小能力,
|
||||
不把外部 token 写进代码、文档或页面;凭证仅由各自的本地/受控环境保存。
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Tolaria 模块目录
|
||||
|
||||
本目录是 `HLP-CHANNEL-0001`(冰朔 · 零点原核频道)的代码与设计归档位。
|
||||
模块状态、负责人和广播关联以
|
||||
[`research/module-registry.yml`](../../../research/module-registry.yml) 为唯一事实源;
|
||||
每个模块目录只记录可读介绍、代码位置、接口与验证材料。
|
||||
|
||||
| 编号 | 软件类别 | 目录 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| HLP-MOD-0001 | 系统壳与产品内核 | `01-system-shell` | App 运行骨架、深链接、账户与工作区边界 |
|
||||
| HLP-MOD-0002 | 视觉与知识渲染 | `02-knowledge-rendering` | 冰湖皮肤、知识页、路径卡、图文与图表渲染 |
|
||||
| HLP-MOD-0003 | 仓库路径与知识索引 | `03-repository-navigation` | 编号映射、嵌套关系、AI 路径包与可追溯跳转 |
|
||||
| HLP-MOD-0004 | Agent 与 HLDP 协议 | `04-agent-hldp-runtime` | 自有 Agent 框架、上下文寻址与外部工具适配 |
|
||||
| HLP-MOD-0005 | 工单、授权与审计 | `05-workorder-authorization` | 计划、确认、回执和企业灯塔协作 |
|
||||
| HLP-MOD-0006 | 协作与移动云端 | `06-collaboration-mobile` | 账号、工作区、手机端与群聊式协作 |
|
||||
| HLP-MOD-0007 | 模块运行时与发布 | `07-module-runtime` | 热插拔、依赖、权限声明、安装和更新 |
|
||||
| HLP-MOD-0008 | 外部集成适配 | `08-integrations` | MCP 与其他外部系统的最小权限适配 |
|
||||
|
||||
新模块先在模块登记册取得 `HLP-MOD-*` 编号,再新增本目录及说明文件;
|
||||
不要把 token、密码、验证码或私钥放进任何模块目录。
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
From 7124ee354529adc521cd88c2636b303e4b06572b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Mon, 13 Jul 2026 22:10:51 +0800
|
||||
Subject: [PATCH 1/2] feat: add Guanghu ice-lake visual layer
|
||||
|
||||
|
||||
diff --git a/src/App.css b/src/App.css
|
||||
index 9567c88ea05326be90cfc8ac186e4d63915ae926..6fda5f493429d1ad1d80eaeb52c0695178d64d22 100644
|
||||
--- a/src/App.css
|
||||
+++ b/src/App.css
|
||||
@@ -5,7 +5,11 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
- box-shadow: inset 0 0 0 1px var(--border-primary);
|
||||
+ background:
|
||||
+ radial-gradient(58rem 35rem at 82% -16%, var(--guanghu-aurora), transparent 62%),
|
||||
+ radial-gradient(42rem 30rem at -12% 112%, var(--guanghu-glow), transparent 65%),
|
||||
+ var(--surface-app);
|
||||
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-primary) 82%, var(--accent-blue));
|
||||
}
|
||||
|
||||
.app {
|
||||
@@ -20,6 +24,7 @@
|
||||
min-width: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
+ background: linear-gradient(180deg, color-mix(in srgb, var(--surface-sidebar) 92%, var(--guanghu-glow)), var(--surface-sidebar));
|
||||
}
|
||||
|
||||
.app__sidebar > * {
|
||||
@@ -37,7 +42,9 @@
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
- background: var(--surface-sidebar);
|
||||
+ background:
|
||||
+ linear-gradient(180deg, color-mix(in srgb, var(--surface-sidebar) 94%, var(--guanghu-glow)), var(--surface-sidebar));
|
||||
+ box-shadow: 14px 0 30px color-mix(in srgb, var(--surface-app) 60%, transparent);
|
||||
}
|
||||
|
||||
.app__note-list > * {
|
||||
@@ -54,6 +61,26 @@
|
||||
flex-direction: column;
|
||||
container-type: inline-size;
|
||||
container-name: editor;
|
||||
+ position: relative;
|
||||
+ background:
|
||||
+ linear-gradient(var(--guanghu-grid) 1px, transparent 1px),
|
||||
+ linear-gradient(90deg, var(--guanghu-grid) 1px, transparent 1px),
|
||||
+ color-mix(in srgb, var(--surface-editor) 94%, var(--guanghu-glow));
|
||||
+ background-size: 32px 32px, 32px 32px, auto;
|
||||
+}
|
||||
+
|
||||
+.app__editor::before {
|
||||
+ content: '';
|
||||
+ position: absolute;
|
||||
+ inset: 0;
|
||||
+ pointer-events: none;
|
||||
+ background: linear-gradient(180deg, color-mix(in srgb, var(--surface-editor) 28%, transparent), transparent 13rem);
|
||||
+ z-index: 0;
|
||||
+}
|
||||
+
|
||||
+.app__editor > * {
|
||||
+ position: relative;
|
||||
+ z-index: 1;
|
||||
}
|
||||
|
||||
.app__editor > * {
|
||||
diff --git a/src/components/Editor.css b/src/components/Editor.css
|
||||
index 499b72fa1f58e39317a74c1515a6d073bffa04d3..132fc3fc5c6e2cfb3a85ac5263f18ce0e6697bd9 100644
|
||||
--- a/src/components/Editor.css
|
||||
+++ b/src/components/Editor.css
|
||||
@@ -24,14 +24,36 @@
|
||||
|
||||
.guanghu-portal-card {
|
||||
width: 100%;
|
||||
- margin: 0.5rem 0;
|
||||
- padding: 1rem 1.1rem;
|
||||
- border: 1px solid color-mix(in srgb, var(--colors-accent) 38%, var(--colors-border));
|
||||
- border-radius: 0.85rem;
|
||||
+ margin: 1.1rem 0;
|
||||
+ padding: 1.15rem 1.25rem 1.2rem;
|
||||
+ border: 1px solid color-mix(in srgb, var(--colors-accent) 46%, var(--colors-border));
|
||||
+ border-radius: 1rem;
|
||||
background:
|
||||
- radial-gradient(circle at top right, color-mix(in srgb, var(--colors-accent) 16%, transparent), transparent 48%),
|
||||
- var(--colors-surface);
|
||||
- box-shadow: 0 0.35rem 1.25rem color-mix(in srgb, black 10%, transparent);
|
||||
+ linear-gradient(135deg, color-mix(in srgb, var(--colors-accent) 16%, transparent), transparent 42%),
|
||||
+ radial-gradient(circle at 92% 16%, color-mix(in srgb, var(--colors-accent) 25%, transparent), transparent 36%),
|
||||
+ var(--guanghu-card-highlight, var(--colors-surface));
|
||||
+ box-shadow:
|
||||
+ 0 0.8rem 2.2rem color-mix(in srgb, black 14%, transparent),
|
||||
+ inset 0 1px 0 color-mix(in srgb, white 28%, transparent);
|
||||
+ overflow: hidden;
|
||||
+ position: relative;
|
||||
+ transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card::before {
|
||||
+ content: '';
|
||||
+ position: absolute;
|
||||
+ inset: 0 auto 0 0;
|
||||
+ width: 3px;
|
||||
+ background: linear-gradient(var(--colors-accent), color-mix(in srgb, var(--colors-accent) 12%, transparent));
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card:hover {
|
||||
+ border-color: color-mix(in srgb, var(--colors-accent) 72%, var(--colors-border));
|
||||
+ box-shadow:
|
||||
+ 0 1rem 2.6rem color-mix(in srgb, var(--colors-accent) 18%, transparent),
|
||||
+ inset 0 1px 0 color-mix(in srgb, white 32%, transparent);
|
||||
+ transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.guanghu-portal-card__eyebrow {
|
||||
@@ -39,31 +61,36 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
- margin-bottom: 0.55rem;
|
||||
+ margin-bottom: 0.7rem;
|
||||
color: var(--colors-text-muted);
|
||||
font-size: 0.72rem;
|
||||
- letter-spacing: 0.08em;
|
||||
+ font-weight: 650;
|
||||
+ letter-spacing: 0.1em;
|
||||
+ text-transform: uppercase;
|
||||
}
|
||||
|
||||
.guanghu-portal-card__status {
|
||||
- padding: 0.18rem 0.55rem;
|
||||
+ padding: 0.22rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
- background: color-mix(in srgb, var(--colors-accent) 18%, transparent);
|
||||
+ border: 1px solid color-mix(in srgb, var(--colors-accent) 28%, transparent);
|
||||
+ background: color-mix(in srgb, var(--colors-accent) 16%, transparent);
|
||||
color: var(--colors-text);
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.guanghu-portal-card__title {
|
||||
display: inline-flex;
|
||||
- font-size: 1.08rem;
|
||||
- font-weight: 650;
|
||||
+ font-size: 1.15rem;
|
||||
+ font-weight: 720;
|
||||
+ letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.guanghu-portal-card__description {
|
||||
- margin: 0.45rem 0 0;
|
||||
+ max-width: 68ch;
|
||||
+ margin: 0.55rem 0 0;
|
||||
color: var(--colors-text-muted);
|
||||
font-size: 0.9rem;
|
||||
- line-height: 1.55;
|
||||
+ line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Breadcrumb bar: border can still react to the data attribute, but the
|
||||
@@ -116,6 +143,7 @@ body.mac-chrome .app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadc
|
||||
overflow-anchor: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
+ scrollbar-color: color-mix(in srgb, var(--accent-blue) 42%, transparent) transparent;
|
||||
}
|
||||
|
||||
.editor-scroll-area--sheet {
|
||||
@@ -165,7 +193,7 @@ body.mac-chrome .app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadc
|
||||
.editor__blocknote-container .bn-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
- background: var(--bg-primary);
|
||||
+ background: color-mix(in srgb, var(--bg-primary) 92%, transparent);
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
/* Override BlockNote's internal color variables so .bn-editor background
|
||||
@@ -187,7 +215,28 @@ body.mac-chrome .app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadc
|
||||
|
||||
.editor__blocknote-container .bn-editor {
|
||||
width: 100%;
|
||||
- padding: 20px 0;
|
||||
+ padding: 36px 0 72px;
|
||||
+}
|
||||
+
|
||||
+.editor__blocknote-container .bn-editor > .bn-block-group {
|
||||
+ padding: 1.25rem clamp(1rem, 3vw, 2.25rem) 3rem;
|
||||
+ border: 1px solid color-mix(in srgb, var(--border-primary) 82%, transparent);
|
||||
+ border-radius: 1.25rem;
|
||||
+ background: color-mix(in srgb, var(--surface-editor) 86%, transparent);
|
||||
+ box-shadow: 0 1.3rem 3.5rem color-mix(in srgb, black 8%, transparent);
|
||||
+ backdrop-filter: blur(16px);
|
||||
+}
|
||||
+
|
||||
+@media (max-width: 720px) {
|
||||
+ .app__editor {
|
||||
+ background-size: 22px 22px, 22px 22px, auto;
|
||||
+ }
|
||||
+
|
||||
+ .editor__blocknote-container .bn-editor > .bn-block-group {
|
||||
+ border-inline: 0;
|
||||
+ border-radius: 0;
|
||||
+ box-shadow: none;
|
||||
+ }
|
||||
}
|
||||
|
||||
.editor__blocknote-container .bn-suggestion-menu {
|
||||
diff --git a/src/index.css b/src/index.css
|
||||
index 9cbf80a57238229b320ffb779703f5ef4e129839..7bbaa4c5260d434a23997eedc54430f71acde88e 100644
|
||||
--- a/src/index.css
|
||||
+++ b/src/index.css
|
||||
@@ -179,6 +179,12 @@
|
||||
--hover: var(--state-hover);
|
||||
--link-color: var(--accent-blue);
|
||||
--link-hover: var(--accent-blue-hover);
|
||||
+
|
||||
+ /* Guanghu visual layer — an app-owned, ice-lake palette. */
|
||||
+ --guanghu-aurora: rgba(91, 181, 255, 0.2);
|
||||
+ --guanghu-glow: rgba(38, 131, 235, 0.15);
|
||||
+ --guanghu-grid: rgba(42, 113, 188, 0.06);
|
||||
+ --guanghu-card-highlight: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
:root.dark,
|
||||
@@ -330,6 +336,12 @@
|
||||
--hover: var(--state-hover);
|
||||
--link-color: var(--accent-blue);
|
||||
--link-hover: var(--accent-blue-hover);
|
||||
+
|
||||
+ /* Guanghu visual layer — deep lake, not a flat black canvas. */
|
||||
+ --guanghu-aurora: rgba(76, 174, 255, 0.2);
|
||||
+ --guanghu-glow: rgba(20, 118, 214, 0.24);
|
||||
+ --guanghu-grid: rgba(126, 202, 255, 0.055);
|
||||
+ --guanghu-card-highlight: rgba(28, 54, 80, 0.72);
|
||||
}
|
||||
|
||||
/* --- Tailwind v4 theme inline: register colors + radii --- */
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
From 831c82f7a89d2a7865cd28a9ddf1fc71807f002c Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Mon, 13 Jul 2026 22:15:22 +0800
|
||||
Subject: [PATCH 2/2] test: align updater coverage with Guanghu policy
|
||||
|
||||
|
||||
diff --git a/src/hooks/useUpdater.test.ts b/src/hooks/useUpdater.test.ts
|
||||
index ad1a70fe0c54acf8f78aa9eeb5e00d07c5fd585d..cada95a14349a130cf4dd0dde01815bb7f088d4a 100644
|
||||
--- a/src/hooks/useUpdater.test.ts
|
||||
+++ b/src/hooks/useUpdater.test.ts
|
||||
@@ -1,10 +1,6 @@
|
||||
-import { renderHook, act } from '@testing-library/react'
|
||||
-import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
|
||||
+import { act, renderHook } from '@testing-library/react'
|
||||
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useUpdater } from './useUpdater'
|
||||
-import {
|
||||
- clearRestartRequiredAfterUpdate,
|
||||
- isRestartRequiredAfterUpdate,
|
||||
-} from '../lib/appUpdater'
|
||||
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: vi.fn(() => false),
|
||||
@@ -16,278 +12,68 @@ vi.mock('../utils/url', () => ({
|
||||
}))
|
||||
|
||||
const mockInvoke = vi.fn()
|
||||
-
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||
- Channel: class {
|
||||
- onmessage: (response: unknown) => void
|
||||
-
|
||||
- constructor(onmessage?: (response: unknown) => void) {
|
||||
- this.onmessage = onmessage ?? (() => {})
|
||||
- }
|
||||
- },
|
||||
}))
|
||||
|
||||
import { isTauri } from '../mock-tauri'
|
||||
|
||||
-interface AppUpdateMetadata {
|
||||
- currentVersion: string
|
||||
- version: string
|
||||
- date?: string
|
||||
- body?: string
|
||||
-}
|
||||
-
|
||||
-type DownloadArgs = {
|
||||
- releaseChannel: string
|
||||
- expectedVersion: string
|
||||
- onEvent: {
|
||||
- onmessage: (
|
||||
- response:
|
||||
- | { event: 'Started'; data: { contentLength?: number } }
|
||||
- | { event: 'Progress'; data: { chunkLength: number } }
|
||||
- | { event: 'Finished' },
|
||||
- ) => void
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-function makeUpdate(overrides: Partial<AppUpdateMetadata> = {}): AppUpdateMetadata {
|
||||
- return {
|
||||
- currentVersion: '2026.4.15',
|
||||
- version: '2026.4.16',
|
||||
- body: 'Bug fixes and improvements',
|
||||
- ...overrides,
|
||||
- }
|
||||
-}
|
||||
-
|
||||
-function installInvokeHandlers({
|
||||
- checkResult = null,
|
||||
- downloadImpl,
|
||||
-}: {
|
||||
- checkResult?: AppUpdateMetadata | null | Error
|
||||
- downloadImpl?: (args: DownloadArgs) => Promise<void>
|
||||
-}) {
|
||||
- mockInvoke.mockImplementation((command: string, args?: unknown) => {
|
||||
- if (command === 'check_for_app_update') {
|
||||
- if (checkResult instanceof Error) return Promise.reject(checkResult)
|
||||
- return Promise.resolve(checkResult)
|
||||
- }
|
||||
-
|
||||
- if (command === 'download_and_install_app_update') {
|
||||
- if (downloadImpl) return downloadImpl(args as DownloadArgs)
|
||||
- return Promise.resolve(null)
|
||||
- }
|
||||
-
|
||||
- return Promise.resolve(null)
|
||||
- })
|
||||
-}
|
||||
-
|
||||
-function renderUpdater(releaseChannel: string, automaticChecksEnabled = true) {
|
||||
- return renderHook(() => useUpdater(releaseChannel, automaticChecksEnabled))
|
||||
-}
|
||||
-
|
||||
-async function performManualCheck(
|
||||
- releaseChannel: string,
|
||||
- checkResult: AppUpdateMetadata | null | Error,
|
||||
-) {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({ checkResult })
|
||||
-
|
||||
- const hook = renderUpdater(releaseChannel)
|
||||
- let outcome: string | undefined
|
||||
-
|
||||
- await act(async () => {
|
||||
- outcome = await hook.result.current.actions.checkForUpdates()
|
||||
- })
|
||||
-
|
||||
- return { result: hook.result, outcome }
|
||||
-}
|
||||
-
|
||||
-async function advanceAutoCheck() {
|
||||
- await act(async () => {
|
||||
- await vi.advanceTimersByTimeAsync(3500)
|
||||
- })
|
||||
+function renderUpdater(automaticChecksEnabled = true) {
|
||||
+ return renderHook(() => useUpdater('stable', automaticChecksEnabled))
|
||||
}
|
||||
|
||||
describe('useUpdater', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
- clearRestartRequiredAfterUpdate()
|
||||
- vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
- vi.restoreAllMocks()
|
||||
- })
|
||||
-
|
||||
- it('starts in idle state', () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(false)
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
-
|
||||
- expect(result.current.status).toEqual({ state: 'idle' })
|
||||
})
|
||||
|
||||
- it('does not check for updates when not running in Tauri', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(false)
|
||||
-
|
||||
- renderUpdater('stable')
|
||||
- await advanceAutoCheck()
|
||||
-
|
||||
- expect(mockInvoke).not.toHaveBeenCalled()
|
||||
+ it('starts in an idle state', () => {
|
||||
+ expect(renderUpdater().result.current.status).toEqual({ state: 'idle' })
|
||||
})
|
||||
|
||||
- it('checks for stable updates after the startup delay', async () => {
|
||||
+ it('never schedules an updater request in the Guanghu distribution', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({ checkResult: null })
|
||||
|
||||
- renderUpdater('stable')
|
||||
- await advanceAutoCheck()
|
||||
-
|
||||
- expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', {
|
||||
- releaseChannel: 'stable',
|
||||
+ renderUpdater()
|
||||
+ await act(async () => {
|
||||
+ await vi.advanceTimersByTimeAsync(3500)
|
||||
})
|
||||
- })
|
||||
-
|
||||
- it('does not automatically check for updates when automatic checks are disabled', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({ checkResult: makeUpdate() })
|
||||
-
|
||||
- renderUpdater('stable', false)
|
||||
- await advanceAutoCheck()
|
||||
|
||||
expect(mockInvoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
- it('keeps manual update checks available when automatic checks are disabled', async () => {
|
||||
+ it('keeps a manual check local and idle', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({ checkResult: null })
|
||||
-
|
||||
- const { result } = renderUpdater('stable', false)
|
||||
+ const { result } = renderUpdater(false)
|
||||
|
||||
let outcome: unknown
|
||||
await act(async () => {
|
||||
outcome = await result.current.actions.checkForUpdates()
|
||||
})
|
||||
|
||||
- expect(outcome).toEqual({ kind: 'up-to-date' })
|
||||
- expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', {
|
||||
- releaseChannel: 'stable',
|
||||
- })
|
||||
- })
|
||||
-
|
||||
- it('transitions to available when an alpha update is found', async () => {
|
||||
- const { result } = await performManualCheck(
|
||||
- 'alpha',
|
||||
- makeUpdate({ version: '2026.4.16-alpha.3' }),
|
||||
- )
|
||||
-
|
||||
- expect(result.current.status).toEqual({
|
||||
- state: 'available',
|
||||
- version: '2026.4.16-alpha.3',
|
||||
- displayVersion: 'Alpha 2026.4.16.3',
|
||||
- notes: 'Bug fixes and improvements',
|
||||
- })
|
||||
-
|
||||
- expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', {
|
||||
- releaseChannel: 'alpha',
|
||||
- })
|
||||
- })
|
||||
-
|
||||
- it('returns up-to-date when no update is available', async () => {
|
||||
- const { result, outcome } = await performManualCheck('stable', null)
|
||||
-
|
||||
expect(outcome).toEqual({ kind: 'up-to-date' })
|
||||
expect(result.current.status).toEqual({ state: 'idle' })
|
||||
+ expect(mockInvoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
- it('shows checking state while a manual update check is in flight', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- let resolveCheck: (value: AppUpdateMetadata | null) => void = () => {}
|
||||
- mockInvoke.mockImplementation((command: string) => {
|
||||
- if (command === 'check_for_app_update') {
|
||||
- return new Promise<AppUpdateMetadata | null>((resolve) => {
|
||||
- resolveCheck = resolve
|
||||
- })
|
||||
- }
|
||||
- return Promise.resolve(null)
|
||||
- })
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
+ it('does not download or restart from a disabled update channel', async () => {
|
||||
+ const { result } = renderUpdater()
|
||||
|
||||
- let checkPromise: Promise<unknown> | null = null
|
||||
await act(async () => {
|
||||
- checkPromise = result.current.actions.checkForUpdates()
|
||||
- await Promise.resolve()
|
||||
- })
|
||||
-
|
||||
- expect(result.current.status).toEqual({ state: 'checking' })
|
||||
-
|
||||
- await act(async () => {
|
||||
- resolveCheck(null)
|
||||
- expect(await checkPromise).toEqual({ kind: 'up-to-date' })
|
||||
+ await result.current.actions.startDownload()
|
||||
})
|
||||
|
||||
expect(result.current.status).toEqual({ state: 'idle' })
|
||||
+ expect(mockInvoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
- it('returns available and sets status when an update exists', async () => {
|
||||
- const { result, outcome } = await performManualCheck(
|
||||
- 'stable',
|
||||
- makeUpdate({ body: undefined }),
|
||||
- )
|
||||
-
|
||||
- expect(outcome).toEqual({
|
||||
- kind: 'available',
|
||||
- version: '2026.4.16',
|
||||
- displayVersion: '2026.4.16',
|
||||
- })
|
||||
- expect(result.current.status).toEqual({
|
||||
- state: 'available',
|
||||
- version: '2026.4.16',
|
||||
- displayVersion: '2026.4.16',
|
||||
- notes: undefined,
|
||||
- })
|
||||
- })
|
||||
-
|
||||
- it('strips stable prerelease suffixes from the display version', async () => {
|
||||
- const { result } = await performManualCheck(
|
||||
- 'stable',
|
||||
- makeUpdate({ version: '2026.4.16-stable.1' }),
|
||||
- )
|
||||
-
|
||||
- expect(result.current.status).toEqual({
|
||||
- state: 'available',
|
||||
- version: '2026.4.16-stable.1',
|
||||
- displayVersion: '2026.4.16',
|
||||
- notes: 'Bug fixes and improvements',
|
||||
- })
|
||||
- })
|
||||
-
|
||||
- it('returns error when the update check fails', async () => {
|
||||
- const { result, outcome } = await performManualCheck(
|
||||
- 'stable',
|
||||
- new Error('network error'),
|
||||
- )
|
||||
-
|
||||
- expect(outcome).toEqual({
|
||||
- kind: 'error',
|
||||
- message: 'Could not check for updates: network error',
|
||||
- })
|
||||
- expect(console.warn).toHaveBeenCalledWith('[updater] Failed to check for updates')
|
||||
- expect(result.current.status).toEqual({ state: 'error' })
|
||||
- })
|
||||
-
|
||||
- it('dismiss resets the banner state', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({ checkResult: makeUpdate() })
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
-
|
||||
- await act(async () => {
|
||||
- await result.current.actions.checkForUpdates()
|
||||
- })
|
||||
+ it('keeps dismiss as a safe idle-state reset', () => {
|
||||
+ const { result } = renderUpdater()
|
||||
|
||||
act(() => {
|
||||
result.current.actions.dismiss()
|
||||
@@ -296,72 +82,13 @@ describe('useUpdater', () => {
|
||||
expect(result.current.status).toEqual({ state: 'idle' })
|
||||
})
|
||||
|
||||
- it('openReleaseNotes opens the release notes page', () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(false)
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
+ it('opens release notes without enabling application updates', () => {
|
||||
+ const { result } = renderUpdater()
|
||||
|
||||
act(() => {
|
||||
result.current.actions.openReleaseNotes()
|
||||
})
|
||||
|
||||
- expect(mockOpenExternalUrl).toHaveBeenCalledWith(
|
||||
- 'https://tolaria.md/releases/'
|
||||
- )
|
||||
- })
|
||||
-
|
||||
- it('downloads and installs the available update with progress', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({
|
||||
- checkResult: makeUpdate(),
|
||||
- downloadImpl: async (args) => {
|
||||
- expect(args.releaseChannel).toBe('stable')
|
||||
- expect(args.expectedVersion).toBe('2026.4.16')
|
||||
- args.onEvent.onmessage({ event: 'Started', data: { contentLength: 1000 } })
|
||||
- args.onEvent.onmessage({ event: 'Progress', data: { chunkLength: 500 } })
|
||||
- args.onEvent.onmessage({ event: 'Progress', data: { chunkLength: 500 } })
|
||||
- args.onEvent.onmessage({ event: 'Finished' })
|
||||
- },
|
||||
- })
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
-
|
||||
- await act(async () => {
|
||||
- await result.current.actions.checkForUpdates()
|
||||
- })
|
||||
-
|
||||
- await act(async () => {
|
||||
- await result.current.actions.startDownload()
|
||||
- })
|
||||
-
|
||||
- expect(result.current.status).toEqual({
|
||||
- state: 'ready',
|
||||
- version: '2026.4.16',
|
||||
- displayVersion: '2026.4.16',
|
||||
- })
|
||||
- expect(isRestartRequiredAfterUpdate()).toBe(true)
|
||||
- })
|
||||
-
|
||||
- it('transitions to error when download fails', async () => {
|
||||
- vi.mocked(isTauri).mockReturnValue(true)
|
||||
- installInvokeHandlers({
|
||||
- checkResult: makeUpdate(),
|
||||
- downloadImpl: async () => {
|
||||
- throw new Error('download failed')
|
||||
- },
|
||||
- })
|
||||
-
|
||||
- const { result } = renderUpdater('stable')
|
||||
-
|
||||
- await act(async () => {
|
||||
- await result.current.actions.checkForUpdates()
|
||||
- })
|
||||
-
|
||||
- await act(async () => {
|
||||
- await result.current.actions.startDownload()
|
||||
- })
|
||||
-
|
||||
- expect(console.warn).toHaveBeenCalledWith('[updater] Download failed')
|
||||
- expect(result.current.status).toEqual({ state: 'error' })
|
||||
+ expect(mockOpenExternalUrl).toHaveBeenCalledWith('https://tolaria.md/releases/')
|
||||
})
|
||||
})
|
||||
diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts
|
||||
index c99843b66cc82d282ae96fee4b5b2115c7a6197b..f6bb8796f59995dc6417652f7a6206b89a03c2ea 100644
|
||||
--- a/src/hooks/useUpdater.ts
|
||||
+++ b/src/hooks/useUpdater.ts
|
||||
@@ -10,6 +10,7 @@ import { formatCalendarVersionForDisplay } from '../utils/calendarVersion'
|
||||
import { openExternalUrl } from '../utils/url'
|
||||
|
||||
const RELEASE_NOTES_URL = 'https://tolaria.md/releases/'
|
||||
+const GUANGHU_UPDATES_ENABLED = false
|
||||
|
||||
interface UpdateVersionInfo {
|
||||
version: string
|
||||
@@ -99,12 +100,11 @@ export function useUpdater(
|
||||
): { status: UpdateStatus; actions: UpdateActions } {
|
||||
// Guanghu is an independent open-source fork. Until Guanghu has its own
|
||||
// signed release feed, never contact or install from Tolaria's updater.
|
||||
- const updatesEnabled = false
|
||||
const [status, setStatus] = useState<UpdateStatus>({ state: 'idle' })
|
||||
const updateRef = useRef<AppUpdateMetadata | null>(null)
|
||||
|
||||
const checkForUpdates = useCallback(async (): Promise<UpdateCheckResult> => {
|
||||
- if (!updatesEnabled) return { kind: 'up-to-date' }
|
||||
+ if (!GUANGHU_UPDATES_ENABLED) return { kind: 'up-to-date' }
|
||||
if (!isTauri()) return { kind: 'up-to-date' }
|
||||
|
||||
setStatus({ state: 'checking' })
|
||||
@@ -129,7 +129,7 @@ export function useUpdater(
|
||||
}, [releaseChannel])
|
||||
|
||||
useEffect(() => {
|
||||
- if (!updatesEnabled) return
|
||||
+ if (!GUANGHU_UPDATES_ENABLED) return
|
||||
if (!automaticChecksEnabled) return
|
||||
if (!isTauri()) return
|
||||
const timer = setTimeout(() => { checkForUpdates() }, 3000)
|
||||
@@ -137,7 +137,7 @@ export function useUpdater(
|
||||
}, [automaticChecksEnabled, checkForUpdates])
|
||||
|
||||
const startDownload = useCallback(async () => {
|
||||
- if (!updatesEnabled) return
|
||||
+ if (!GUANGHU_UPDATES_ENABLED) return
|
||||
const update = updateRef.current
|
||||
if (!update) return
|
||||
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,635 @@
|
|||
From edf46c67334ed8bd5adbb0be719ab0dd51ffa266 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Thu, 16 Jul 2026 22:30:25 +0800
|
||||
Subject: [PATCH 5/7] Add portable callout page blocks
|
||||
|
||||
---
|
||||
src/components/Editor.css | 62 +++++++++++
|
||||
src/components/editorSchema.tsx | 46 ++++++++
|
||||
.../tolariaEditorFormatting.test.tsx | 58 ++++++++++
|
||||
.../tolariaEditorFormattingConfig.ts | 101 +++++++++++++++++-
|
||||
src/utils/blockNoteDirectMarkdown.test.ts | 12 +++
|
||||
src/utils/blockNoteDirectMarkdown.ts | 12 +++
|
||||
src/utils/calloutMarkdown.ts | 70 ++++++++++++
|
||||
src/utils/editorDurableMarkdown.test.ts | 17 +++
|
||||
src/utils/editorDurableMarkdown.ts | 3 +-
|
||||
9 files changed, 379 insertions(+), 2 deletions(-)
|
||||
create mode 100644 src/utils/calloutMarkdown.ts
|
||||
|
||||
diff --git a/src/components/Editor.css b/src/components/Editor.css
|
||||
index 24f0738..5db3fe1 100644
|
||||
--- a/src/components/Editor.css
|
||||
+++ b/src/components/Editor.css
|
||||
@@ -80,6 +80,68 @@
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
+.tolaria-callout {
|
||||
+ --tolaria-callout-accent: #2563eb;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #2563eb 9%, var(--bg-primary));
|
||||
+ display: grid;
|
||||
+ grid-template-columns: 4px minmax(0, 1fr);
|
||||
+ gap: 12px;
|
||||
+ width: 100%;
|
||||
+ margin: 0.45rem 0;
|
||||
+ padding: 11px 13px 11px 0;
|
||||
+ border: 1px solid color-mix(in srgb, var(--tolaria-callout-accent) 28%, var(--border));
|
||||
+ border-radius: 8px;
|
||||
+ background: var(--tolaria-callout-bg);
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout[data-kind="success"] {
|
||||
+ --tolaria-callout-accent: #168a48;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #168a48 10%, var(--bg-primary));
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout[data-kind="warning"] {
|
||||
+ --tolaria-callout-accent: #b7791f;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #b7791f 13%, var(--bg-primary));
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout[data-kind="danger"] {
|
||||
+ --tolaria-callout-accent: #dc2626;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #dc2626 10%, var(--bg-primary));
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout__marker {
|
||||
+ width: 4px;
|
||||
+ min-height: 100%;
|
||||
+ border-radius: 999px;
|
||||
+ background: var(--tolaria-callout-accent);
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout__body {
|
||||
+ display: grid;
|
||||
+ gap: 4px;
|
||||
+ min-width: 0;
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout__label {
|
||||
+ color: var(--tolaria-callout-accent);
|
||||
+ font-size: 11px;
|
||||
+ font-weight: 700;
|
||||
+ line-height: 1.2;
|
||||
+ text-transform: uppercase;
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout__content {
|
||||
+ min-width: 0;
|
||||
+ color: var(--text-primary);
|
||||
+ font-size: 14px;
|
||||
+ line-height: 1.6;
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout__content:empty::before {
|
||||
+ color: var(--text-muted);
|
||||
+ content: 'Write a note';
|
||||
+}
|
||||
+
|
||||
/* Breadcrumb bar: border can still react to the data attribute, but the
|
||||
breadcrumb filename/title stays visible at all times. */
|
||||
.breadcrumb-bar {
|
||||
diff --git a/src/components/editorSchema.tsx b/src/components/editorSchema.tsx
|
||||
index 2984eff..7d62518 100644
|
||||
--- a/src/components/editorSchema.tsx
|
||||
+++ b/src/components/editorSchema.tsx
|
||||
@@ -25,6 +25,11 @@ import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown
|
||||
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
||||
import { MARKDOWN_HIGHLIGHT_STYLE } from '../utils/markdownHighlightMarkdown'
|
||||
import { GUANGHU_PORTAL_BLOCK_TYPE } from '../utils/guanghuPortalMarkdown'
|
||||
+import {
|
||||
+ calloutKind,
|
||||
+ TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ type TolariaCalloutKind,
|
||||
+} from '../utils/calloutMarkdown'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { createTolariaCodeBlockOptions } from './codeBlockOptions'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
@@ -350,6 +355,45 @@ const GuanghuPortalBlock = createReactBlockSpec(
|
||||
},
|
||||
)
|
||||
|
||||
+const CALLOUT_LABELS: Record<TolariaCalloutKind, string> = {
|
||||
+ danger: 'Danger',
|
||||
+ info: 'Info',
|
||||
+ success: 'Success',
|
||||
+ warning: 'Warning',
|
||||
+}
|
||||
+
|
||||
+function TolariaCalloutRenderer({
|
||||
+ block,
|
||||
+ contentRef,
|
||||
+}: {
|
||||
+ block: { props: { kind?: string } }
|
||||
+ contentRef: (node: HTMLElement | null) => void
|
||||
+}) {
|
||||
+ const kind = calloutKind(block.props.kind)
|
||||
+ return (
|
||||
+ <aside className="tolaria-callout" data-kind={kind}>
|
||||
+ <span className="tolaria-callout__marker" aria-hidden="true" />
|
||||
+ <div className="tolaria-callout__body">
|
||||
+ <span className="tolaria-callout__label">{CALLOUT_LABELS[kind]}</span>
|
||||
+ <div ref={contentRef} className="tolaria-callout__content" />
|
||||
+ </div>
|
||||
+ </aside>
|
||||
+ )
|
||||
+}
|
||||
+
|
||||
+const TolariaCalloutBlock = createReactBlockSpec(
|
||||
+ {
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ propSchema: {
|
||||
+ kind: { default: 'info' },
|
||||
+ },
|
||||
+ content: 'inline',
|
||||
+ },
|
||||
+ {
|
||||
+ render: TolariaCalloutRenderer,
|
||||
+ },
|
||||
+)
|
||||
+
|
||||
export function mediaBlockPropsForPreviewRuntime<T extends MediaBlockPreviewProps>(
|
||||
props: T,
|
||||
externalMediaPreview: boolean,
|
||||
@@ -450,6 +494,7 @@ const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions())
|
||||
const audioBlock = AudioBlockSpec()
|
||||
const mathBlock = MathBlock()
|
||||
const guanghuPortalBlock = GuanghuPortalBlock()
|
||||
+const tolariaCalloutBlock = TolariaCalloutBlock()
|
||||
const mermaidBlock = MermaidBlock()
|
||||
const tldrawBlock = TldrawBlock()
|
||||
const videoBlock = VideoBlockSpec()
|
||||
@@ -485,6 +530,7 @@ export const schema = BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
audio: audioBlock,
|
||||
guanghuPortalBlock,
|
||||
+ [TOLARIA_CALLOUT_BLOCK_TYPE]: tolariaCalloutBlock,
|
||||
mathBlock,
|
||||
mermaidBlock,
|
||||
tldrawBlock,
|
||||
diff --git a/src/components/tolariaEditorFormatting.test.tsx b/src/components/tolariaEditorFormatting.test.tsx
|
||||
index 5c5143b..7bd06c2 100644
|
||||
--- a/src/components/tolariaEditorFormatting.test.tsx
|
||||
+++ b/src/components/tolariaEditorFormatting.test.tsx
|
||||
@@ -7,7 +7,9 @@ vi.mock('../lib/telemetry', () => ({
|
||||
}))
|
||||
|
||||
import {
|
||||
+ addItemsAfterKey,
|
||||
addItemsToMediaGroup,
|
||||
+ createCalloutSlashMenuItems,
|
||||
createMathSlashMenuItem,
|
||||
filterTolariaFormattingToolbarItems,
|
||||
filterTolariaSlashMenuItems,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
||||
import { mermaidFenceSource } from '../utils/mermaidMarkdown'
|
||||
+import { TOLARIA_CALLOUT_BLOCK_TYPE } from '../utils/calloutMarkdown'
|
||||
|
||||
describe('tolariaEditorFormatting', () => {
|
||||
it('keeps the markdown-safe toolbar controls and block type select', () => {
|
||||
@@ -73,6 +76,7 @@ describe('tolariaEditorFormatting', () => {
|
||||
{ key: 'heading_4', title: 'Heading 4', onItemClick: () => {} },
|
||||
{ key: 'bullet_list', title: 'Bullet List', subtext: 'Default list copy', onItemClick: () => {} },
|
||||
{ key: 'code_block', title: 'Code Block', subtext: 'Default code copy', onItemClick: () => {} },
|
||||
+ { key: 'tolaria_callout_info', title: 'Info callout', onItemClick: () => {} },
|
||||
{ key: 'heading_5', title: 'Heading 5', onItemClick: () => {} },
|
||||
{ key: 'heading_6', title: 'Heading 6', onItemClick: () => {} },
|
||||
] satisfies TolariaSlashMenuTestItem[])
|
||||
@@ -82,12 +86,14 @@ describe('tolariaEditorFormatting', () => {
|
||||
'heading_4',
|
||||
'bullet_list',
|
||||
'code_block',
|
||||
+ 'tolaria_callout_info',
|
||||
])
|
||||
expect(items.map((item) => item.subtext)).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
+ undefined,
|
||||
])
|
||||
})
|
||||
|
||||
@@ -224,6 +230,58 @@ describe('tolariaEditorFormatting', () => {
|
||||
])
|
||||
})
|
||||
|
||||
+ it('places callout commands after quote blocks', () => {
|
||||
+ type TolariaSlashMenuTestItem = {
|
||||
+ key: string
|
||||
+ title: string
|
||||
+ group: string
|
||||
+ onItemClick: () => void
|
||||
+ }
|
||||
+
|
||||
+ const items = addItemsAfterKey([
|
||||
+ { key: 'paragraph', title: 'Paragraph', group: 'Basic blocks', onItemClick: () => {} },
|
||||
+ { key: 'quote', title: 'Quote', group: 'Basic blocks', onItemClick: () => {} },
|
||||
+ { key: 'code_block', title: 'Code Block', group: 'Basic blocks', onItemClick: () => {} },
|
||||
+ ] satisfies TolariaSlashMenuTestItem[], 'quote', [
|
||||
+ { key: 'tolaria_callout_info', title: 'Info callout', group: 'Basic blocks', onItemClick: () => {} },
|
||||
+ ])
|
||||
+
|
||||
+ expect(items.map(item => item.key)).toEqual([
|
||||
+ 'paragraph',
|
||||
+ 'quote',
|
||||
+ 'tolaria_callout_info',
|
||||
+ 'code_block',
|
||||
+ ])
|
||||
+ })
|
||||
+
|
||||
+ it('keeps callout slash commands searchable and inserts an editable callout block', async () => {
|
||||
+ const block = { id: 'active-block' }
|
||||
+ const editor = {
|
||||
+ getTextCursorPosition: () => ({ block }),
|
||||
+ replaceBlocks: () => {},
|
||||
+ }
|
||||
+ const replaceBlocks = vi.spyOn(editor, 'replaceBlocks')
|
||||
+ const items = createCalloutSlashMenuItems(editor as never)
|
||||
+ const warningItem = items.find(item => item.key === 'tolaria_callout_warning')
|
||||
+
|
||||
+ expect(warningItem).toEqual(expect.objectContaining({
|
||||
+ title: 'Warning callout',
|
||||
+ aliases: expect.arrayContaining(['warning', '警告']),
|
||||
+ }))
|
||||
+
|
||||
+ warningItem?.onItemClick()
|
||||
+
|
||||
+ expect(replaceBlocks).toHaveBeenCalledWith([block], [{
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: { kind: 'warning' },
|
||||
+ content: [{
|
||||
+ type: 'text',
|
||||
+ text: 'Warning callout',
|
||||
+ styles: {},
|
||||
+ }],
|
||||
+ }])
|
||||
+ })
|
||||
+
|
||||
it('creates a math slash command with a default display equation', () => {
|
||||
const block = { id: 'active-block' }
|
||||
const editor = {
|
||||
diff --git a/src/components/tolariaEditorFormattingConfig.ts b/src/components/tolariaEditorFormattingConfig.ts
|
||||
index b55dede..614cbf5 100644
|
||||
--- a/src/components/tolariaEditorFormattingConfig.ts
|
||||
+++ b/src/components/tolariaEditorFormattingConfig.ts
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
import { createElement, type ReactElement } from 'react'
|
||||
import {
|
||||
CodeBlock,
|
||||
+ CheckCircle,
|
||||
File,
|
||||
FlowArrow,
|
||||
ImageSquare,
|
||||
+ Info,
|
||||
ListBullets,
|
||||
ListChecks,
|
||||
ListNumbers,
|
||||
@@ -26,6 +28,8 @@ import {
|
||||
TextHFour,
|
||||
TextHFive,
|
||||
TextHSix,
|
||||
+ Warning,
|
||||
+ XCircle,
|
||||
Video,
|
||||
type Icon as PhosphorIcon,
|
||||
} from '@phosphor-icons/react'
|
||||
@@ -38,6 +42,10 @@ import {
|
||||
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
||||
import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown'
|
||||
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
||||
+import {
|
||||
+ TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ type TolariaCalloutKind,
|
||||
+} from '../utils/calloutMarkdown'
|
||||
|
||||
type TolariaSlashMenuItem = DefaultReactSuggestionItem & { key: string }
|
||||
type TolariaBlockTypeSelectItem = RichEditorBlockTypeDefinition & {
|
||||
@@ -55,6 +63,12 @@ type BlockSlashMenuItemConfig = {
|
||||
title: string
|
||||
type: string
|
||||
}
|
||||
+type CalloutSlashMenuItemConfig = {
|
||||
+ aliases: string[]
|
||||
+ key: string
|
||||
+ kind: TolariaCalloutKind
|
||||
+ title: string
|
||||
+}
|
||||
type TolariaSlashMenuLabels = {
|
||||
mathTitle: string
|
||||
}
|
||||
@@ -115,6 +129,10 @@ const TOLARIA_SLASH_MENU_ICONS: Partial<Record<string, PhosphorIcon>> = {
|
||||
numbered_list: ListNumbers,
|
||||
paragraph: Paragraph,
|
||||
quote: Quotes,
|
||||
+ tolaria_callout_danger: XCircle,
|
||||
+ tolaria_callout_info: Info,
|
||||
+ tolaria_callout_success: CheckCircle,
|
||||
+ tolaria_callout_warning: Warning,
|
||||
table: Table,
|
||||
toggle_heading: TextHOne,
|
||||
toggle_heading_2: TextHTwo,
|
||||
@@ -164,6 +182,64 @@ function createMermaidSlashMenuItem(
|
||||
})
|
||||
}
|
||||
|
||||
+function createCalloutSlashMenuItem(
|
||||
+ editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||
+ config: CalloutSlashMenuItemConfig,
|
||||
+): TolariaSlashMenuItem {
|
||||
+ const blockEditor = editor as unknown as SlashInsertEditor
|
||||
+
|
||||
+ return {
|
||||
+ key: config.key,
|
||||
+ title: config.title,
|
||||
+ aliases: config.aliases,
|
||||
+ group: 'Basic blocks',
|
||||
+ onItemClick: () => {
|
||||
+ const block = blockEditor.getTextCursorPosition().block
|
||||
+ blockEditor.replaceBlocks([block], [{
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: { kind: config.kind },
|
||||
+ content: [{
|
||||
+ type: 'text',
|
||||
+ text: config.title,
|
||||
+ styles: {},
|
||||
+ }],
|
||||
+ }])
|
||||
+ trackEvent('editor_callout_slash_command_used', { kind: config.kind })
|
||||
+ },
|
||||
+ } as TolariaSlashMenuItem
|
||||
+}
|
||||
+
|
||||
+export function createCalloutSlashMenuItems(
|
||||
+ editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||
+): TolariaSlashMenuItem[] {
|
||||
+ return [
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_info',
|
||||
+ title: 'Info callout',
|
||||
+ aliases: ['callout', 'note', 'info', 'tip', '提示', '高亮'],
|
||||
+ kind: 'info',
|
||||
+ }),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_success',
|
||||
+ title: 'Success callout',
|
||||
+ aliases: ['callout', 'success', 'done', 'ok', '完成', '成功'],
|
||||
+ kind: 'success',
|
||||
+ }),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_warning',
|
||||
+ title: 'Warning callout',
|
||||
+ aliases: ['callout', 'warning', 'alert', 'todo', '警告', '注意'],
|
||||
+ kind: 'warning',
|
||||
+ }),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_danger',
|
||||
+ title: 'Danger callout',
|
||||
+ aliases: ['callout', 'danger', 'risk', 'error', '风险', '危险'],
|
||||
+ kind: 'danger',
|
||||
+ }),
|
||||
+ ]
|
||||
+}
|
||||
+
|
||||
export function createMathSlashMenuItem(
|
||||
editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||
labels: TolariaSlashMenuLabels = { mathTitle: 'Math' },
|
||||
@@ -218,6 +294,23 @@ export function addItemsToMediaGroup(
|
||||
return nextItems
|
||||
}
|
||||
|
||||
+export function addItemsAfterKey(
|
||||
+ items: TolariaSlashMenuItem[],
|
||||
+ insertAfterKey: string,
|
||||
+ insertedItems: TolariaSlashMenuItem[],
|
||||
+): TolariaSlashMenuItem[] {
|
||||
+ const nextItems = [...items]
|
||||
+ const insertIndex = nextItems.findIndex((item) => item.key === insertAfterKey)
|
||||
+
|
||||
+ if (insertIndex === -1) {
|
||||
+ nextItems.unshift(...insertedItems)
|
||||
+ return nextItems
|
||||
+ }
|
||||
+
|
||||
+ nextItems.splice(insertIndex + 1, 0, ...insertedItems)
|
||||
+ return nextItems
|
||||
+}
|
||||
+
|
||||
function createTolariaSlashMenuIcon(Icon: PhosphorIcon) {
|
||||
return createElement(
|
||||
'span',
|
||||
@@ -273,8 +366,14 @@ export function getTolariaSlashMenuItems(
|
||||
query: string,
|
||||
labels?: TolariaSlashMenuLabels,
|
||||
) {
|
||||
+ const defaultItems = getDefaultReactSlashMenuItems(editor) as TolariaSlashMenuItem[]
|
||||
+ const itemsWithCallouts = addItemsAfterKey(
|
||||
+ defaultItems,
|
||||
+ 'quote',
|
||||
+ createCalloutSlashMenuItems(editor),
|
||||
+ )
|
||||
const items = addItemsToMediaGroup(
|
||||
- getDefaultReactSlashMenuItems(editor) as TolariaSlashMenuItem[],
|
||||
+ itemsWithCallouts,
|
||||
[
|
||||
createMermaidSlashMenuItem(editor),
|
||||
createMathSlashMenuItem(editor, labels),
|
||||
diff --git a/src/utils/blockNoteDirectMarkdown.test.ts b/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
index 524b29c..cabba60 100644
|
||||
--- a/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
+++ b/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
serializeBlockNoteMarkdown,
|
||||
type DirectMarkdownCapableSerializer,
|
||||
} from './blockNoteDirectMarkdown'
|
||||
+import { TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||
import { serializeRichEditorBodyToMarkdown } from './richEditorMarkdown'
|
||||
|
||||
function makeEditor(document: unknown[]): DirectMarkdownCapableSerializer & { document: unknown[] } {
|
||||
@@ -117,6 +118,17 @@ describe('BlockNote direct Markdown serialization', () => {
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
+ it('serializes Tolaria callout blocks as portable Markdown callouts', () => {
|
||||
+ expect(blocksToMarkdownDirect([
|
||||
+ {
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: { kind: 'success' },
|
||||
+ content: [{ type: 'text', text: 'Package is ready', styles: {} }],
|
||||
+ children: [],
|
||||
+ },
|
||||
+ ]).markdown).toBe('> [!success] Package is ready')
|
||||
+ })
|
||||
+
|
||||
it('caches unchanged block objects across rich-editor body serialization', () => {
|
||||
const block = {
|
||||
type: 'paragraph',
|
||||
diff --git a/src/utils/blockNoteDirectMarkdown.ts b/src/utils/blockNoteDirectMarkdown.ts
|
||||
index c075831..2ac99a0 100644
|
||||
--- a/src/utils/blockNoteDirectMarkdown.ts
|
||||
+++ b/src/utils/blockNoteDirectMarkdown.ts
|
||||
@@ -1,3 +1,5 @@
|
||||
+import { calloutKind, TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||
+
|
||||
interface TextStyles {
|
||||
[style: string]: string | boolean | undefined
|
||||
}
|
||||
@@ -228,6 +230,15 @@ function quoteMarkdown(block: BlockLike): string {
|
||||
return text.split('\n').map(line => `> ${line}`).join('\n')
|
||||
}
|
||||
|
||||
+function calloutMarkdown(block: BlockLike): string {
|
||||
+ const kind = calloutKind(block.props?.kind)
|
||||
+ const text = serializeInlineContent(contentArray(block.content)) || 'Write a note'
|
||||
+ return `[!${kind}] ${text}`
|
||||
+ .split('\n')
|
||||
+ .map(line => `> ${line}`)
|
||||
+ .join('\n')
|
||||
+}
|
||||
+
|
||||
function tableCellMarkdown(cell: TableCellValue): string {
|
||||
const text = typeof cell === 'string'
|
||||
? cell
|
||||
@@ -273,6 +284,7 @@ const BLOCK_MARKDOWN_HANDLERS: Record<string, BlockMarkdownHandler> = {
|
||||
heading: headingMarkdown,
|
||||
quote: quoteMarkdown,
|
||||
table: tableMarkdown,
|
||||
+ [TOLARIA_CALLOUT_BLOCK_TYPE]: calloutMarkdown,
|
||||
}
|
||||
|
||||
function blockMarkdownWithoutChildren(block: BlockLike, context: SerializeContext): string | null {
|
||||
diff --git a/src/utils/calloutMarkdown.ts b/src/utils/calloutMarkdown.ts
|
||||
new file mode 100644
|
||||
index 0000000..b6881b6
|
||||
--- /dev/null
|
||||
+++ b/src/utils/calloutMarkdown.ts
|
||||
@@ -0,0 +1,70 @@
|
||||
+import type { BlockLike } from './durableMarkdownBlocks'
|
||||
+
|
||||
+export const TOLARIA_CALLOUT_BLOCK_TYPE = 'tolariaCallout'
|
||||
+
|
||||
+export const TOLARIA_CALLOUT_KINDS = ['info', 'success', 'warning', 'danger'] as const
|
||||
+
|
||||
+export type TolariaCalloutKind = typeof TOLARIA_CALLOUT_KINDS[number]
|
||||
+
|
||||
+const CALLOUT_MARKER_RE = /^\[!(info|success|warning|danger)\]\s*(.*)$/iu
|
||||
+
|
||||
+function isCalloutKind(value: unknown): value is TolariaCalloutKind {
|
||||
+ return typeof value === 'string' && TOLARIA_CALLOUT_KINDS.includes(value as TolariaCalloutKind)
|
||||
+}
|
||||
+
|
||||
+function readTextOnlyContent(content: BlockLike['content']): string | null {
|
||||
+ if (!Array.isArray(content) || content.length === 0) return null
|
||||
+
|
||||
+ let text = ''
|
||||
+ for (const item of content) {
|
||||
+ if (item.type !== 'text' || typeof item.text !== 'string') return null
|
||||
+ text += item.text
|
||||
+ }
|
||||
+ return text
|
||||
+}
|
||||
+
|
||||
+function calloutMatchFromQuote(block: BlockLike) {
|
||||
+ if (block.type !== 'quote') return null
|
||||
+
|
||||
+ const text = readTextOnlyContent(block.content)
|
||||
+ if (text === null) return null
|
||||
+
|
||||
+ const match = CALLOUT_MARKER_RE.exec(text)
|
||||
+ if (!match) return null
|
||||
+
|
||||
+ const kind = match[1]?.toLowerCase()
|
||||
+ if (!isCalloutKind(kind)) return null
|
||||
+
|
||||
+ return {
|
||||
+ kind,
|
||||
+ text: match[2] ?? '',
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+export function injectTolariaCalloutBlocks(blocks: unknown[]): unknown[] {
|
||||
+ return (blocks as BlockLike[]).map((block) => {
|
||||
+ const match = calloutMatchFromQuote(block)
|
||||
+ const children = Array.isArray(block.children)
|
||||
+ ? injectTolariaCalloutBlocks(block.children) as BlockLike[]
|
||||
+ : block.children
|
||||
+
|
||||
+ if (!match) return { ...block, children }
|
||||
+
|
||||
+ return {
|
||||
+ ...block,
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: {
|
||||
+ ...(block.props ?? {}),
|
||||
+ kind: match.kind,
|
||||
+ },
|
||||
+ content: match.text
|
||||
+ ? [{ type: 'text', text: match.text, styles: {} }]
|
||||
+ : [],
|
||||
+ children,
|
||||
+ }
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
+export function calloutKind(value: unknown): TolariaCalloutKind {
|
||||
+ return isCalloutKind(value) ? value : 'info'
|
||||
+}
|
||||
diff --git a/src/utils/editorDurableMarkdown.test.ts b/src/utils/editorDurableMarkdown.test.ts
|
||||
index f5a230d..4e6679e 100644
|
||||
--- a/src/utils/editorDurableMarkdown.test.ts
|
||||
+++ b/src/utils/editorDurableMarkdown.test.ts
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
preProcessDurableEditorMarkdown,
|
||||
serializeDurableEditorBlocks,
|
||||
} from './editorDurableMarkdown'
|
||||
+import { TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||
import { MERMAID_BLOCK_TYPE } from './mermaidMarkdown'
|
||||
import { TLDRAW_BLOCK_TYPE } from './tldrawMarkdown'
|
||||
|
||||
@@ -70,4 +71,20 @@ describe('editor durable markdown blocks', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
+
|
||||
+ it('restores Markdown callouts as Tolaria page blocks', () => {
|
||||
+ const blocks = injectDurableEditorMarkdownBlocks([
|
||||
+ {
|
||||
+ type: 'quote',
|
||||
+ content: [{ type: 'text', text: '[!warning] Check signing before sharing.', styles: {} }],
|
||||
+ children: [],
|
||||
+ },
|
||||
+ ]) as Array<{ type: string; props?: Record<string, string>; content?: Array<{ text?: string }> }>
|
||||
+
|
||||
+ expect(blocks[0]).toMatchObject({
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: { kind: 'warning' },
|
||||
+ content: [{ text: 'Check signing before sharing.' }],
|
||||
+ })
|
||||
+ })
|
||||
})
|
||||
diff --git a/src/utils/editorDurableMarkdown.ts b/src/utils/editorDurableMarkdown.ts
|
||||
index 6e6d30a..baa5670 100644
|
||||
--- a/src/utils/editorDurableMarkdown.ts
|
||||
+++ b/src/utils/editorDurableMarkdown.ts
|
||||
@@ -16,6 +16,7 @@ import { serializeMathAwareBlocks } from './mathMarkdown'
|
||||
import { mermaidMarkdownCodec } from './mermaidMarkdown'
|
||||
import { tldrawMarkdownCodec } from './tldrawMarkdown'
|
||||
import { guanghuPortalMarkdownCodec } from './guanghuPortalMarkdown'
|
||||
+import { injectTolariaCalloutBlocks } from './calloutMarkdown'
|
||||
|
||||
const EDITOR_DURABLE_MARKDOWN_CODECS = [
|
||||
guanghuPortalMarkdownCodec,
|
||||
@@ -36,7 +37,7 @@ export function injectDurableEditorMarkdownBlocks(blocks: unknown[]): unknown[]
|
||||
blocks,
|
||||
codecs: EDITOR_DURABLE_MARKDOWN_CODECS,
|
||||
})
|
||||
- return injectFileAttachmentBlocks(withDurableBlocks)
|
||||
+ return injectTolariaCalloutBlocks(injectFileAttachmentBlocks(withDurableBlocks))
|
||||
}
|
||||
|
||||
export function serializeDurableEditorBlocks(
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
From 9fc5e885742a48c2794a4b4fcd8ddae66501aa28 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 10:30:04 +0800
|
||||
Subject: [PATCH 6/7] fix: make Guanghu home and themes visible
|
||||
|
||||
---
|
||||
src/App.tsx | 4 ++--
|
||||
src/components/HoloLakeHome.tsx | 1 +
|
||||
src/hooks/useGuanghuTheme.test.ts | 30 ++++++++++++++++++++++++++++++
|
||||
src/hooks/useGuanghuTheme.ts | 12 +++++++++++-
|
||||
src/lib/guanghuTheme.ts | 6 +++---
|
||||
5 files changed, 47 insertions(+), 6 deletions(-)
|
||||
create mode 100644 src/hooks/useGuanghuTheme.test.ts
|
||||
|
||||
diff --git a/src/App.tsx b/src/App.tsx
|
||||
index 2b4ef17..66c879d 100644
|
||||
--- a/src/App.tsx
|
||||
+++ b/src/App.tsx
|
||||
@@ -170,8 +170,7 @@ function App() {
|
||||
|
||||
function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | null }) {
|
||||
const aiWorkspaceWindow = false
|
||||
- const [showHoloLakeHome, setShowHoloLakeHome] = useState(false)
|
||||
- const { theme: guanghuTheme, cycleTheme } = useGuanghuTheme()
|
||||
+ const [showHoloLakeHome, setShowHoloLakeHome] = useState(true)
|
||||
const { dragRegionRef } = useDragRegion<HTMLElement>()
|
||||
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
|
||||
const [noteListFilter, setNoteListFilter] = useState<NoteListFilter>('open')
|
||||
@@ -426,6 +425,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
|
||||
settings,
|
||||
settingsLoaded,
|
||||
})
|
||||
+ const { theme: guanghuTheme, cycleTheme } = useGuanghuTheme()
|
||||
const quickPromptTarget = lastAiWorkspaceTarget ?? aiAgentPreferences.defaultAiTarget
|
||||
const quickPromptTargetReady = aiTargetReady(quickPromptTarget, aiAgentsStatus)
|
||||
|
||||
diff --git a/src/components/HoloLakeHome.tsx b/src/components/HoloLakeHome.tsx
|
||||
index 86b2891..49127ca 100644
|
||||
--- a/src/components/HoloLakeHome.tsx
|
||||
+++ b/src/components/HoloLakeHome.tsx
|
||||
@@ -4,6 +4,7 @@ type HoloLakeHomeProps = {
|
||||
|
||||
const modules = [
|
||||
['知识湖', '把代码仓库、文档与路径转译成可阅读、可追溯的知识页面。', '进入知识库'],
|
||||
+ ['页面组件', '提示块、批注、关系卡、路径包、折叠块与高亮代码已接入笔记编辑器。', '在笔记中输入 /'],
|
||||
['协作空间', '人类与人格体在同一上下文中对话、确认、留存回执。', '筹备中'],
|
||||
['授权中心', '任何真实操作先形成可读计划,再由人类一次确认。', '筹备中'],
|
||||
]
|
||||
diff --git a/src/hooks/useGuanghuTheme.test.ts b/src/hooks/useGuanghuTheme.test.ts
|
||||
new file mode 100644
|
||||
index 0000000..8fa948a
|
||||
--- /dev/null
|
||||
+++ b/src/hooks/useGuanghuTheme.test.ts
|
||||
@@ -0,0 +1,30 @@
|
||||
+import { act, renderHook } from '@testing-library/react'
|
||||
+import { beforeEach, describe, expect, it } from 'vitest'
|
||||
+import { GUANGHU_THEME_STORAGE_KEY } from '../lib/guanghuTheme'
|
||||
+import { THEME_MODE_STORAGE_KEY } from '../lib/themeMode'
|
||||
+import { useGuanghuTheme } from './useGuanghuTheme'
|
||||
+
|
||||
+describe('useGuanghuTheme', () => {
|
||||
+ beforeEach(() => {
|
||||
+ window.localStorage.clear()
|
||||
+ document.documentElement.removeAttribute('data-guanghu-theme')
|
||||
+ document.documentElement.removeAttribute('data-theme')
|
||||
+ document.documentElement.classList.remove('dark')
|
||||
+ })
|
||||
+
|
||||
+ it('makes the lake presets visibly switch the document between light and dark', () => {
|
||||
+ window.localStorage.setItem(GUANGHU_THEME_STORAGE_KEY, 'lake-light')
|
||||
+ const { result } = renderHook(() => useGuanghuTheme())
|
||||
+
|
||||
+ expect(document.documentElement).toHaveAttribute('data-guanghu-theme', 'lake-light')
|
||||
+ expect(document.documentElement).toHaveAttribute('data-theme', 'light')
|
||||
+ expect(window.localStorage.getItem(THEME_MODE_STORAGE_KEY)).toBe('light')
|
||||
+
|
||||
+ act(() => result.current.cycleTheme())
|
||||
+
|
||||
+ expect(document.documentElement).toHaveAttribute('data-guanghu-theme', 'lake-dark')
|
||||
+ expect(document.documentElement).toHaveAttribute('data-theme', 'dark')
|
||||
+ expect(document.documentElement).toHaveClass('dark')
|
||||
+ expect(window.localStorage.getItem(THEME_MODE_STORAGE_KEY)).toBe('dark')
|
||||
+ })
|
||||
+})
|
||||
diff --git a/src/hooks/useGuanghuTheme.ts b/src/hooks/useGuanghuTheme.ts
|
||||
index 2c42f2d..ec175ce 100644
|
||||
--- a/src/hooks/useGuanghuTheme.ts
|
||||
+++ b/src/hooks/useGuanghuTheme.ts
|
||||
@@ -6,6 +6,16 @@ import {
|
||||
writeGuanghuTheme,
|
||||
type GuanghuTheme,
|
||||
} from '../lib/guanghuTheme'
|
||||
+import { applyThemeModeToDocument, writeStoredThemeMode } from '../lib/themeMode'
|
||||
+
|
||||
+function applyGuanghuTheme(theme: GuanghuTheme): void {
|
||||
+ document.documentElement.setAttribute('data-guanghu-theme', theme)
|
||||
+ if (theme === 'native') return
|
||||
+
|
||||
+ const mode = theme === 'lake-light' ? 'light' : 'dark'
|
||||
+ applyThemeModeToDocument(document, mode)
|
||||
+ writeStoredThemeMode(window.localStorage, mode)
|
||||
+}
|
||||
|
||||
export function useGuanghuTheme(): { theme: GuanghuTheme; cycleTheme: () => void } {
|
||||
const [theme, setTheme] = useState<GuanghuTheme>(() => (
|
||||
@@ -13,7 +23,7 @@ export function useGuanghuTheme(): { theme: GuanghuTheme; cycleTheme: () => void
|
||||
))
|
||||
|
||||
useEffect(() => {
|
||||
- document.documentElement.setAttribute('data-guanghu-theme', theme)
|
||||
+ applyGuanghuTheme(theme)
|
||||
writeGuanghuTheme(window.localStorage, theme)
|
||||
}, [theme])
|
||||
|
||||
diff --git a/src/lib/guanghuTheme.ts b/src/lib/guanghuTheme.ts
|
||||
index 711a589..7ed2806 100644
|
||||
--- a/src/lib/guanghuTheme.ts
|
||||
+++ b/src/lib/guanghuTheme.ts
|
||||
@@ -4,9 +4,9 @@ export const GUANGHU_THEMES = ['native', 'lake-light', 'lake-dark'] as const
|
||||
export type GuanghuTheme = typeof GUANGHU_THEMES[number]
|
||||
|
||||
export const GUANGHU_THEME_LABELS: Record<GuanghuTheme, string> = {
|
||||
- native: '原生',
|
||||
- 'lake-light': '湖光浅色',
|
||||
- 'lake-dark': '湖心深色',
|
||||
+ native: '跟随原生',
|
||||
+ 'lake-light': '湖光浅色(亮)',
|
||||
+ 'lake-dark': '湖心深色(暗)',
|
||||
}
|
||||
|
||||
export function normalizeGuanghuTheme(value: unknown): GuanghuTheme {
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
From 2b57543d7855487e75d52fce12714501811fb650 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 10:30:04 +0800
|
||||
Subject: [PATCH 7/7] feat: complete portable page block set
|
||||
|
||||
---
|
||||
src/components/Editor.css | 16 +++++++++++
|
||||
src/components/editorSchema.tsx | 3 +++
|
||||
.../tolariaEditorFormatting.test.tsx | 12 ++++++++-
|
||||
.../tolariaEditorFormattingConfig.ts | 27 ++++++++++++++++---
|
||||
src/utils/blockNoteDirectMarkdown.test.ts | 9 +++++++
|
||||
src/utils/calloutMarkdown.ts | 12 +++++++--
|
||||
6 files changed, 72 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/src/components/Editor.css b/src/components/Editor.css
|
||||
index 5db3fe1..11a0813 100644
|
||||
--- a/src/components/Editor.css
|
||||
+++ b/src/components/Editor.css
|
||||
@@ -109,6 +109,22 @@
|
||||
--tolaria-callout-bg: color-mix(in srgb, #dc2626 10%, var(--bg-primary));
|
||||
}
|
||||
|
||||
+.tolaria-callout[data-kind="annotation"] {
|
||||
+ --tolaria-callout-accent: #8b5cf6;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #8b5cf6 11%, var(--bg-primary));
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout[data-kind="relationship"] {
|
||||
+ --tolaria-callout-accent: #0f9f8f;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #0f9f8f 11%, var(--bg-primary));
|
||||
+}
|
||||
+
|
||||
+.tolaria-callout[data-kind="path"] {
|
||||
+ --tolaria-callout-accent: #4f7ecb;
|
||||
+ --tolaria-callout-bg: color-mix(in srgb, #4f7ecb 11%, var(--bg-primary));
|
||||
+ border-style: dashed;
|
||||
+}
|
||||
+
|
||||
.tolaria-callout__marker {
|
||||
width: 4px;
|
||||
min-height: 100%;
|
||||
diff --git a/src/components/editorSchema.tsx b/src/components/editorSchema.tsx
|
||||
index 7d62518..75ceddf 100644
|
||||
--- a/src/components/editorSchema.tsx
|
||||
+++ b/src/components/editorSchema.tsx
|
||||
@@ -356,8 +356,11 @@ const GuanghuPortalBlock = createReactBlockSpec(
|
||||
)
|
||||
|
||||
const CALLOUT_LABELS: Record<TolariaCalloutKind, string> = {
|
||||
+ annotation: '批注',
|
||||
danger: 'Danger',
|
||||
info: 'Info',
|
||||
+ path: '路径',
|
||||
+ relationship: '关系',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
}
|
||||
diff --git a/src/components/tolariaEditorFormatting.test.tsx b/src/components/tolariaEditorFormatting.test.tsx
|
||||
index 7bd06c2..b32ebb7 100644
|
||||
--- a/src/components/tolariaEditorFormatting.test.tsx
|
||||
+++ b/src/components/tolariaEditorFormatting.test.tsx
|
||||
@@ -60,7 +60,7 @@ describe('tolariaEditorFormatting', () => {
|
||||
])
|
||||
})
|
||||
|
||||
- it('filters unsupported toggle slash-menu variants and removes command descriptions', () => {
|
||||
+ it('keeps Notion-style toggles, filters oversized headings, and removes command descriptions', () => {
|
||||
type TolariaSlashMenuTestItem = {
|
||||
key: string
|
||||
title: string
|
||||
@@ -82,6 +82,8 @@ describe('tolariaEditorFormatting', () => {
|
||||
] satisfies TolariaSlashMenuTestItem[])
|
||||
|
||||
expect(items.map((item) => item.key)).toEqual([
|
||||
+ 'toggle_heading',
|
||||
+ 'toggle_list',
|
||||
'heading',
|
||||
'heading_4',
|
||||
'bullet_list',
|
||||
@@ -94,6 +96,8 @@ describe('tolariaEditorFormatting', () => {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
+ undefined,
|
||||
+ undefined,
|
||||
])
|
||||
})
|
||||
|
||||
@@ -280,6 +284,12 @@ describe('tolariaEditorFormatting', () => {
|
||||
styles: {},
|
||||
}],
|
||||
}])
|
||||
+
|
||||
+ expect(items.map(item => item.key)).toEqual(expect.arrayContaining([
|
||||
+ 'tolaria_callout_annotation',
|
||||
+ 'tolaria_callout_relationship',
|
||||
+ 'tolaria_callout_path',
|
||||
+ ]))
|
||||
})
|
||||
|
||||
it('creates a math slash command with a default display equation', () => {
|
||||
diff --git a/src/components/tolariaEditorFormattingConfig.ts b/src/components/tolariaEditorFormattingConfig.ts
|
||||
index 614cbf5..41d9225 100644
|
||||
--- a/src/components/tolariaEditorFormattingConfig.ts
|
||||
+++ b/src/components/tolariaEditorFormattingConfig.ts
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CheckCircle,
|
||||
File,
|
||||
FlowArrow,
|
||||
+ GitBranch,
|
||||
ImageSquare,
|
||||
Info,
|
||||
ListBullets,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
ScribbleLoop,
|
||||
Smiley,
|
||||
SpeakerHigh,
|
||||
+ Tag,
|
||||
Table,
|
||||
TextHOne,
|
||||
TextHTwo,
|
||||
@@ -90,10 +92,6 @@ const UNSUPPORTED_FORMATTING_TOOLBAR_KEYS = new Set([
|
||||
const UNSUPPORTED_SLASH_MENU_KEYS = new Set([
|
||||
'heading_5',
|
||||
'heading_6',
|
||||
- 'toggle_heading',
|
||||
- 'toggle_heading_2',
|
||||
- 'toggle_heading_3',
|
||||
- 'toggle_list',
|
||||
])
|
||||
|
||||
const TOLARIA_BLOCK_TYPE_SELECT_ICONS: Record<RichEditorBlockTypeKey, PhosphorIcon> = {
|
||||
@@ -131,6 +129,9 @@ const TOLARIA_SLASH_MENU_ICONS: Partial<Record<string, PhosphorIcon>> = {
|
||||
quote: Quotes,
|
||||
tolaria_callout_danger: XCircle,
|
||||
tolaria_callout_info: Info,
|
||||
+ tolaria_callout_annotation: Tag,
|
||||
+ tolaria_callout_path: FlowArrow,
|
||||
+ tolaria_callout_relationship: GitBranch,
|
||||
tolaria_callout_success: CheckCircle,
|
||||
tolaria_callout_warning: Warning,
|
||||
table: Table,
|
||||
@@ -237,6 +238,24 @@ export function createCalloutSlashMenuItems(
|
||||
aliases: ['callout', 'danger', 'risk', 'error', '风险', '危险'],
|
||||
kind: 'danger',
|
||||
}),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_annotation',
|
||||
+ title: '批注 / Annotation',
|
||||
+ aliases: ['annotation', 'comment', 'note', '批注', '注释'],
|
||||
+ kind: 'annotation',
|
||||
+ }),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_relationship',
|
||||
+ title: '关系卡 / Relationship',
|
||||
+ aliases: ['relationship', 'relation', 'wikilink', '关系', '关联'],
|
||||
+ kind: 'relationship',
|
||||
+ }),
|
||||
+ createCalloutSlashMenuItem(editor, {
|
||||
+ key: 'tolaria_callout_path',
|
||||
+ title: '路径包 / Path map',
|
||||
+ aliases: ['path', 'breadcrumb', 'route', 'map', '路径', '面包屑', '导航'],
|
||||
+ kind: 'path',
|
||||
+ }),
|
||||
]
|
||||
}
|
||||
|
||||
diff --git a/src/utils/blockNoteDirectMarkdown.test.ts b/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
index cabba60..358df27 100644
|
||||
--- a/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
+++ b/src/utils/blockNoteDirectMarkdown.test.ts
|
||||
@@ -127,6 +127,15 @@ describe('BlockNote direct Markdown serialization', () => {
|
||||
children: [],
|
||||
},
|
||||
]).markdown).toBe('> [!success] Package is ready')
|
||||
+
|
||||
+ expect(blocksToMarkdownDirect([
|
||||
+ {
|
||||
+ type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||
+ props: { kind: 'relationship' },
|
||||
+ content: [{ type: 'text', text: '[[TCS]] → [[HoloLake]]', styles: {} }],
|
||||
+ children: [],
|
||||
+ },
|
||||
+ ]).markdown).toBe('> [!relationship] [[TCS]] → [[HoloLake]]')
|
||||
})
|
||||
|
||||
it('caches unchanged block objects across rich-editor body serialization', () => {
|
||||
diff --git a/src/utils/calloutMarkdown.ts b/src/utils/calloutMarkdown.ts
|
||||
index b6881b6..2fa2449 100644
|
||||
--- a/src/utils/calloutMarkdown.ts
|
||||
+++ b/src/utils/calloutMarkdown.ts
|
||||
@@ -2,11 +2,19 @@ import type { BlockLike } from './durableMarkdownBlocks'
|
||||
|
||||
export const TOLARIA_CALLOUT_BLOCK_TYPE = 'tolariaCallout'
|
||||
|
||||
-export const TOLARIA_CALLOUT_KINDS = ['info', 'success', 'warning', 'danger'] as const
|
||||
+export const TOLARIA_CALLOUT_KINDS = [
|
||||
+ 'info',
|
||||
+ 'success',
|
||||
+ 'warning',
|
||||
+ 'danger',
|
||||
+ 'annotation',
|
||||
+ 'relationship',
|
||||
+ 'path',
|
||||
+] as const
|
||||
|
||||
export type TolariaCalloutKind = typeof TOLARIA_CALLOUT_KINDS[number]
|
||||
|
||||
-const CALLOUT_MARKER_RE = /^\[!(info|success|warning|danger)\]\s*(.*)$/iu
|
||||
+const CALLOUT_MARKER_RE = /^\[!(info|success|warning|danger|annotation|relationship|path)\]\s*(.*)$/iu
|
||||
|
||||
function isCalloutKind(value: unknown): value is TolariaCalloutKind {
|
||||
return typeof value === 'string' && TOLARIA_CALLOUT_KINDS.includes(value as TolariaCalloutKind)
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
From 5dcdcd8a6b2a983af46d37f5e89d522402badcbd Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 11:12:33 +0800
|
||||
Subject: [PATCH 8/9] fix: restore internal note links and reading rhythm
|
||||
|
||||
---
|
||||
src/components/Editor.css | 6 ++--
|
||||
src/components/EditorTheme.css | 32 ++++++++++++++++--
|
||||
.../useEditorLinkActivation.test.tsx | 25 ++++++++++++++
|
||||
src/components/useEditorLinkActivation.ts | 33 ++++++++++++++++++-
|
||||
src/theme.json | 20 +++++------
|
||||
5 files changed, 99 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/src/components/Editor.css b/src/components/Editor.css
|
||||
index 11a0813..910def1 100644
|
||||
--- a/src/components/Editor.css
|
||||
+++ b/src/components/Editor.css
|
||||
@@ -87,10 +87,10 @@
|
||||
grid-template-columns: 4px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
- margin: 0.45rem 0;
|
||||
- padding: 11px 13px 11px 0;
|
||||
+ margin: 0.8rem 0;
|
||||
+ padding: 15px 16px 15px 0;
|
||||
border: 1px solid color-mix(in srgb, var(--tolaria-callout-accent) 28%, var(--border));
|
||||
- border-radius: 8px;
|
||||
+ border-radius: 10px;
|
||||
background: var(--tolaria-callout-bg);
|
||||
}
|
||||
|
||||
diff --git a/src/components/EditorTheme.css b/src/components/EditorTheme.css
|
||||
index 862e474..3885574 100644
|
||||
--- a/src/components/EditorTheme.css
|
||||
+++ b/src/components/EditorTheme.css
|
||||
@@ -350,6 +350,12 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
+.editor__blocknote-container .bn-editor a[href*=".md"] {
|
||||
+ cursor: pointer;
|
||||
+ text-decoration-thickness: 1px;
|
||||
+ text-underline-offset: 0.18em;
|
||||
+}
|
||||
+
|
||||
/* --- Wikilinks (override from Editor.css with theme vars) --- */
|
||||
.editor__blocknote-container .wikilink {
|
||||
color: var(--inline-styles-wikilink-color);
|
||||
@@ -657,9 +663,15 @@ body.tldraw-whiteboard-fullscreen-open {
|
||||
background-color: var(--editor-code-block-background) !important;
|
||||
border: 1px solid var(--editor-code-block-border);
|
||||
color: var(--editor-code-block-text) !important;
|
||||
+ border-radius: 10px;
|
||||
+ box-shadow: 0 1px 0 color-mix(in srgb, var(--editor-code-block-border) 55%, transparent);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
+.editor__blocknote-container .bn-block-outer:has(> .bn-block > [data-content-type="codeBlock"]) {
|
||||
+ margin-block: 10px 16px;
|
||||
+}
|
||||
+
|
||||
.editor__code-block-copy {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
@@ -681,6 +693,8 @@ body.tldraw-whiteboard-fullscreen-open {
|
||||
|
||||
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] > pre {
|
||||
color: inherit;
|
||||
+ scrollbar-color: color-mix(in srgb, var(--accent-blue) 48%, transparent) transparent;
|
||||
+ scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.editor__blocknote-container .bn-block-content[data-content-type="codeBlock"] > div > select {
|
||||
@@ -706,12 +720,24 @@ body.tldraw-whiteboard-fullscreen-open {
|
||||
/* --- Blockquote --- */
|
||||
.editor__blocknote-container [data-content-type="blockquote"],
|
||||
.editor__blocknote-container blockquote {
|
||||
- border-inline-start: var(--blockquote-border-left-width) solid var(--blockquote-border-left-color);
|
||||
- padding-inline-start: var(--blockquote-padding-left);
|
||||
+ background: color-mix(in srgb, var(--accent-blue) 12%, var(--bg-primary));
|
||||
+ border: 1px solid color-mix(in srgb, var(--accent-blue) 28%, var(--border-primary));
|
||||
+ border-inline-start: 4px solid var(--blockquote-border-left-color);
|
||||
+ border-radius: 10px;
|
||||
+ padding: 16px 18px;
|
||||
margin-top: var(--blockquote-margin-vertical);
|
||||
margin-bottom: var(--blockquote-margin-vertical);
|
||||
color: var(--blockquote-color);
|
||||
- font-style: var(--blockquote-font-style);
|
||||
+ font-style: normal;
|
||||
+}
|
||||
+
|
||||
+.editor__blocknote-container .bn-block-outer:has(> .bn-block > [data-content-type="table"]) {
|
||||
+ margin-block: 12px 18px;
|
||||
+}
|
||||
+
|
||||
+.editor__blocknote-container [data-content-type="table"] table {
|
||||
+ border-radius: 8px;
|
||||
+ overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Table --- */
|
||||
diff --git a/src/components/useEditorLinkActivation.test.tsx b/src/components/useEditorLinkActivation.test.tsx
|
||||
index ab3b62a..bbfcaee 100644
|
||||
--- a/src/components/useEditorLinkActivation.test.tsx
|
||||
+++ b/src/components/useEditorLinkActivation.test.tsx
|
||||
@@ -128,6 +128,31 @@ describe('useEditorLinkActivation', () => {
|
||||
expect(modifiedClick.defaultPrevented).toBe(true)
|
||||
})
|
||||
|
||||
+ it('opens relative Markdown note links inside Tolaria on a plain click', async () => {
|
||||
+ const { container, onNavigateWikilink } = renderHarness()
|
||||
+ const link = appendUrl(container, '../Migration/Related%20Page%20abc123.md#current-status')
|
||||
+
|
||||
+ const plainClick = dispatchMouseEvent(link, 'click')
|
||||
+
|
||||
+ expect(plainClick.defaultPrevented).toBe(true)
|
||||
+ expect(mockOpenLocalFile).not.toHaveBeenCalled()
|
||||
+ await Promise.resolve()
|
||||
+ expect(onNavigateWikilink).toHaveBeenCalledWith('../Migration/Related Page abc123')
|
||||
+ })
|
||||
+
|
||||
+ it('consumes Markdown note link mousedown before editor internals can select it', async () => {
|
||||
+ const { container, onNavigateWikilink } = renderHarness()
|
||||
+ const link = appendUrl(container, 'docs/Related.md')
|
||||
+
|
||||
+ const mouseDown = dispatchMouseEvent(link, 'mousedown')
|
||||
+ const click = dispatchMouseEvent(link, 'click')
|
||||
+
|
||||
+ expect(mouseDown.defaultPrevented).toBe(true)
|
||||
+ expect(click.defaultPrevented).toBe(true)
|
||||
+ await Promise.resolve()
|
||||
+ expect(onNavigateWikilink).toHaveBeenCalledWith('docs/Related')
|
||||
+ })
|
||||
+
|
||||
it('opens modified URL mousedown before editor internals see stale link nodes', () => {
|
||||
const { container } = renderHarness()
|
||||
const link = appendUrl(container, 'https://example.com')
|
||||
diff --git a/src/components/useEditorLinkActivation.ts b/src/components/useEditorLinkActivation.ts
|
||||
index f29c624..79a2dad 100644
|
||||
--- a/src/components/useEditorLinkActivation.ts
|
||||
+++ b/src/components/useEditorLinkActivation.ts
|
||||
@@ -26,6 +26,24 @@ function resolveAnchorHref(target: HTMLElement) {
|
||||
return target.closest<HTMLAnchorElement>('a[href]')?.getAttribute('href')?.trim() ?? null
|
||||
}
|
||||
|
||||
+function decodeLinkPath(path: string) {
|
||||
+ try {
|
||||
+ return decodeURIComponent(path)
|
||||
+ } catch {
|
||||
+ return path
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function resolveMarkdownNoteTarget(href: string) {
|
||||
+ const path = href.split(/[?#]/u, 1)[0]?.trim() ?? ''
|
||||
+ if (!path || path.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(path)) return null
|
||||
+
|
||||
+ const decodedPath = decodeLinkPath(path).replace(/\\/gu, '/')
|
||||
+ if (!/\.md(?:own)?$/iu.test(decodedPath)) return null
|
||||
+
|
||||
+ return decodedPath.replace(/\.md(?:own)?$/iu, '')
|
||||
+}
|
||||
+
|
||||
function blurActiveEditable(container: HTMLElement) {
|
||||
const active = document.activeElement
|
||||
if (!(active instanceof HTMLElement) || !container.contains(active)) return
|
||||
@@ -84,7 +102,15 @@ function handleEditorLinkClick(
|
||||
}
|
||||
|
||||
const href = resolveAnchorHref(target)
|
||||
- if (href) activateUrl(event, href, vaultPath)
|
||||
+ if (!href) return
|
||||
+
|
||||
+ const markdownTarget = resolveMarkdownNoteTarget(href)
|
||||
+ if (markdownTarget) {
|
||||
+ activateWikilink(event, container, markdownTarget, onNavigateWikilink)
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ activateUrl(event, href, vaultPath)
|
||||
}
|
||||
|
||||
function handleEditorLinkMouseDown(event: MouseEvent, vaultPath?: string): string | null {
|
||||
@@ -97,6 +123,11 @@ function handleEditorLinkMouseDown(event: MouseEvent, vaultPath?: string): strin
|
||||
}
|
||||
|
||||
const href = resolveAnchorHref(target)
|
||||
+ if (href && resolveMarkdownNoteTarget(href)) {
|
||||
+ consumeEditorLinkEvent(event)
|
||||
+ return null
|
||||
+ }
|
||||
+
|
||||
if (hasFollowModifier(event) && href) {
|
||||
activateUrl(event, href, vaultPath)
|
||||
return href
|
||||
diff --git a/src/theme.json b/src/theme.json
|
||||
index 84b5e4c..92999d2 100644
|
||||
--- a/src/theme.json
|
||||
+++ b/src/theme.json
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"editor": {
|
||||
"fontFamily": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
- "fontSize": 15,
|
||||
- "lineHeight": 1.5,
|
||||
- "maxWidth": 820,
|
||||
+ "fontSize": 16,
|
||||
+ "lineHeight": 1.65,
|
||||
+ "maxWidth": 880,
|
||||
"paddingHorizontal": 40,
|
||||
- "paddingVertical": 20,
|
||||
- "paragraphSpacing": 8,
|
||||
+ "paddingVertical": 28,
|
||||
+ "paragraphSpacing": 12,
|
||||
"dividerFollowedByHeadingMarginTop": 8
|
||||
},
|
||||
"headings": {
|
||||
@@ -80,11 +80,11 @@
|
||||
"code": {
|
||||
"fontFamily": "'SF Mono', 'Fira Code', monospace",
|
||||
"fontSize": 14,
|
||||
- "backgroundColor": "var(--bg-hover-subtle)",
|
||||
- "paddingHorizontal": 4,
|
||||
+ "backgroundColor": "color-mix(in srgb, var(--accent-blue) 11%, var(--bg-hover-subtle))",
|
||||
+ "paddingHorizontal": 5,
|
||||
"paddingVertical": 2,
|
||||
"borderRadius": 3,
|
||||
- "color": "var(--text-secondary)"
|
||||
+ "color": "var(--accent-blue)"
|
||||
},
|
||||
"link": {
|
||||
"color": "var(--accent-blue)",
|
||||
@@ -100,8 +100,8 @@
|
||||
"blockquote": {
|
||||
"borderLeftWidth": 3,
|
||||
"borderLeftColor": "var(--accent-blue)",
|
||||
- "paddingLeft": 16,
|
||||
- "marginVertical": 12,
|
||||
+ "paddingLeft": 18,
|
||||
+ "marginVertical": 16,
|
||||
"color": "var(--text-secondary)",
|
||||
"fontStyle": "italic"
|
||||
},
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
From 59ce6261d3dff070a138934d8038e0a935c003c7 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 11:15:45 +0800
|
||||
Subject: [PATCH 9/9] test: lock in readable editor theme defaults
|
||||
|
||||
---
|
||||
src/hooks/useTheme.test.ts | 6 +++---
|
||||
1 file changed, 3 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/src/hooks/useTheme.test.ts b/src/hooks/useTheme.test.ts
|
||||
index aecc7bd..27d1f6c 100644
|
||||
--- a/src/hooks/useTheme.test.ts
|
||||
+++ b/src/hooks/useTheme.test.ts
|
||||
@@ -3,11 +3,11 @@ import { renderHook } from '@testing-library/react'
|
||||
import { useEditorTheme } from './useTheme'
|
||||
|
||||
describe('useEditorTheme', () => {
|
||||
- it('keeps inline code on the muted editor surface without exporting code block overrides', () => {
|
||||
+ it('gives inline code a subtle accent surface without exporting code block overrides', () => {
|
||||
const { result } = renderHook(() => useEditorTheme())
|
||||
|
||||
expect(result.current.cssVars['--inline-styles-code-background-color']).toBe(
|
||||
- 'var(--bg-hover-subtle)'
|
||||
+ 'color-mix(in srgb, var(--accent-blue) 11%, var(--bg-hover-subtle))'
|
||||
)
|
||||
expect(result.current.cssVars['--code-blocks-background-color']).toBeUndefined()
|
||||
})
|
||||
@@ -27,6 +27,6 @@ describe('useEditorTheme', () => {
|
||||
it('exports the default editor max width', () => {
|
||||
const { result } = renderHook(() => useEditorTheme())
|
||||
|
||||
- expect(result.current.cssVars['--editor-max-width']).toBe('820px')
|
||||
+ expect(result.current.cssVars['--editor-max-width']).toBe('880px')
|
||||
})
|
||||
})
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
From 63a544d9fbb30bdcdb7377947fe22947a5276005 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 11:26:04 +0800
|
||||
Subject: [PATCH] fix: resolve relative page links from source notes
|
||||
|
||||
---
|
||||
src/components/SingleEditorView.tsx | 2 +-
|
||||
.../useEditorLinkActivation.test.tsx | 22 +++++++---
|
||||
src/components/useEditorLinkActivation.ts | 44 ++++++++++++++++---
|
||||
3 files changed, 57 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx
|
||||
index df43bd1..0440e5a 100644
|
||||
--- a/src/components/SingleEditorView.tsx
|
||||
+++ b/src/components/SingleEditorView.tsx
|
||||
@@ -1208,7 +1208,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange
|
||||
handleMouseMove: handleCodeBlockCopyMouseMove,
|
||||
} = useCodeBlockCopyTarget(containerRef)
|
||||
useBlockNoteSideMenuHoverGuard(containerRef)
|
||||
- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
||||
+ useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourceEntry?.path)
|
||||
|
||||
useEffect(() => {
|
||||
_wikilinkEntriesRef.current = entries
|
||||
diff --git a/src/components/useEditorLinkActivation.test.tsx b/src/components/useEditorLinkActivation.test.tsx
|
||||
index bbfcaee..321bc00 100644
|
||||
--- a/src/components/useEditorLinkActivation.test.tsx
|
||||
+++ b/src/components/useEditorLinkActivation.test.tsx
|
||||
@@ -20,17 +20,19 @@ const mockOpenLocalFile = vi.mocked(openLocalFile)
|
||||
function Harness({
|
||||
onNavigateWikilink,
|
||||
vaultPath,
|
||||
+ sourcePath,
|
||||
}: {
|
||||
onNavigateWikilink: (target: string) => void
|
||||
vaultPath?: string
|
||||
+ sourcePath?: string
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
||||
+ useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourcePath)
|
||||
return <div ref={containerRef} data-testid="editor-link-container" />
|
||||
}
|
||||
|
||||
-function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string) {
|
||||
- render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} />)
|
||||
+function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string, sourcePath?: string) {
|
||||
+ render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} sourcePath={sourcePath} />)
|
||||
return {
|
||||
container: screen.getByTestId('editor-link-container') as HTMLDivElement,
|
||||
onNavigateWikilink,
|
||||
@@ -129,7 +131,7 @@ describe('useEditorLinkActivation', () => {
|
||||
})
|
||||
|
||||
it('opens relative Markdown note links inside Tolaria on a plain click', async () => {
|
||||
- const { container, onNavigateWikilink } = renderHarness()
|
||||
+ const { container, onNavigateWikilink } = renderHarness(vi.fn(), '/vault', '/vault/docs/NEXT.md')
|
||||
const link = appendUrl(container, '../Migration/Related%20Page%20abc123.md#current-status')
|
||||
|
||||
const plainClick = dispatchMouseEvent(link, 'click')
|
||||
@@ -137,7 +139,17 @@ describe('useEditorLinkActivation', () => {
|
||||
expect(plainClick.defaultPrevented).toBe(true)
|
||||
expect(mockOpenLocalFile).not.toHaveBeenCalled()
|
||||
await Promise.resolve()
|
||||
- expect(onNavigateWikilink).toHaveBeenCalledWith('../Migration/Related Page abc123')
|
||||
+ expect(onNavigateWikilink).toHaveBeenCalledWith('Migration/Related Page abc123')
|
||||
+ })
|
||||
+
|
||||
+ it('resolves duplicate README links from the current note directory', async () => {
|
||||
+ const { container, onNavigateWikilink } = renderHarness(vi.fn(), '/vault', '/vault/docs/NEXT.md')
|
||||
+ const link = appendUrl(container, '../apps/tolaria/modules/02-knowledge-rendering/README.md')
|
||||
+
|
||||
+ dispatchMouseEvent(link, 'click')
|
||||
+
|
||||
+ await Promise.resolve()
|
||||
+ expect(onNavigateWikilink).toHaveBeenCalledWith('apps/tolaria/modules/02-knowledge-rendering/README')
|
||||
})
|
||||
|
||||
it('consumes Markdown note link mousedown before editor internals can select it', async () => {
|
||||
diff --git a/src/components/useEditorLinkActivation.ts b/src/components/useEditorLinkActivation.ts
|
||||
index 79a2dad..31c49e0 100644
|
||||
--- a/src/components/useEditorLinkActivation.ts
|
||||
+++ b/src/components/useEditorLinkActivation.ts
|
||||
@@ -34,14 +34,46 @@ function decodeLinkPath(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
-function resolveMarkdownNoteTarget(href: string) {
|
||||
+function normalizePathSegments(path: string) {
|
||||
+ const segments: string[] = []
|
||||
+ for (const segment of path.split('/')) {
|
||||
+ if (!segment || segment === '.') continue
|
||||
+ if (segment === '..') {
|
||||
+ segments.pop()
|
||||
+ continue
|
||||
+ }
|
||||
+ segments.push(segment)
|
||||
+ }
|
||||
+ return segments.join('/')
|
||||
+}
|
||||
+
|
||||
+function sourceRelativeDirectory(vaultPath?: string, sourcePath?: string) {
|
||||
+ if (!vaultPath || !sourcePath) return null
|
||||
+
|
||||
+ const normalizedVault = vaultPath.replace(/\\/gu, '/').replace(/\/+$/u, '')
|
||||
+ const normalizedSource = sourcePath.replace(/\\/gu, '/')
|
||||
+ const prefix = `${normalizedVault}/`
|
||||
+ if (!normalizedSource.toLowerCase().startsWith(prefix.toLowerCase())) return null
|
||||
+
|
||||
+ const relativeSource = normalizedSource.slice(prefix.length)
|
||||
+ const slashIndex = relativeSource.lastIndexOf('/')
|
||||
+ return slashIndex === -1 ? '' : relativeSource.slice(0, slashIndex)
|
||||
+}
|
||||
+
|
||||
+function resolveMarkdownNoteTarget(href: string, vaultPath?: string, sourcePath?: string) {
|
||||
const path = href.split(/[?#]/u, 1)[0]?.trim() ?? ''
|
||||
if (!path || path.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(path)) return null
|
||||
|
||||
const decodedPath = decodeLinkPath(path).replace(/\\/gu, '/')
|
||||
if (!/\.md(?:own)?$/iu.test(decodedPath)) return null
|
||||
|
||||
- return decodedPath.replace(/\.md(?:own)?$/iu, '')
|
||||
+ const pathWithoutExtension = decodedPath.replace(/\.md(?:own)?$/iu, '')
|
||||
+ if (pathWithoutExtension.startsWith('/')) return normalizePathSegments(pathWithoutExtension)
|
||||
+
|
||||
+ const sourceDirectory = sourceRelativeDirectory(vaultPath, sourcePath)
|
||||
+ return normalizePathSegments(
|
||||
+ sourceDirectory === null ? pathWithoutExtension : `${sourceDirectory}/${pathWithoutExtension}`,
|
||||
+ )
|
||||
}
|
||||
|
||||
function blurActiveEditable(container: HTMLElement) {
|
||||
@@ -91,6 +123,7 @@ function handleEditorLinkClick(
|
||||
container: HTMLElement,
|
||||
onNavigateWikilink: (target: string) => void,
|
||||
vaultPath?: string,
|
||||
+ sourcePath?: string,
|
||||
) {
|
||||
const target = elementFromEventTarget(event.target)
|
||||
if (!target || isInsideCodeContext(target)) return
|
||||
@@ -104,7 +137,7 @@ function handleEditorLinkClick(
|
||||
const href = resolveAnchorHref(target)
|
||||
if (!href) return
|
||||
|
||||
- const markdownTarget = resolveMarkdownNoteTarget(href)
|
||||
+ const markdownTarget = resolveMarkdownNoteTarget(href, vaultPath, sourcePath)
|
||||
if (markdownTarget) {
|
||||
activateWikilink(event, container, markdownTarget, onNavigateWikilink)
|
||||
return
|
||||
@@ -146,6 +179,7 @@ export function useEditorLinkActivation(
|
||||
containerRef: RefObject<HTMLDivElement | null>,
|
||||
onNavigateWikilink: (target: string) => void,
|
||||
vaultPath?: string,
|
||||
+ sourcePath?: string,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
@@ -187,7 +221,7 @@ export function useEditorLinkActivation(
|
||||
}
|
||||
|
||||
clearHandledMouseDownUrl()
|
||||
- handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath)
|
||||
+ handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath, sourcePath)
|
||||
}
|
||||
|
||||
container.addEventListener('mousedown', handleMouseDown, true)
|
||||
@@ -207,5 +241,5 @@ export function useEditorLinkActivation(
|
||||
clearHandledMouseDownUrl()
|
||||
resetModifierState()
|
||||
}
|
||||
- }, [containerRef, onNavigateWikilink, vaultPath])
|
||||
+ }, [containerRef, onNavigateWikilink, sourcePath, vaultPath])
|
||||
}
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
From 387ce84da12f33c64d179d7e2582124ae952091f Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
||||
Date: Fri, 17 Jul 2026 11:31:05 +0800
|
||||
Subject: [PATCH] fix: resolve links inside mounted workspaces
|
||||
|
||||
---
|
||||
src/components/SingleEditorView.tsx | 3 ++-
|
||||
1 file changed, 2 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx
|
||||
index 0440e5a..a0e2dc8 100644
|
||||
--- a/src/components/SingleEditorView.tsx
|
||||
+++ b/src/components/SingleEditorView.tsx
|
||||
@@ -1208,7 +1208,8 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange
|
||||
handleMouseMove: handleCodeBlockCopyMouseMove,
|
||||
} = useCodeBlockCopyTarget(containerRef)
|
||||
useBlockNoteSideMenuHoverGuard(containerRef)
|
||||
- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourceEntry?.path)
|
||||
+ const sourceVaultPath = sourceEntry?.workspace?.path ?? vaultPath
|
||||
+ useEditorLinkActivation(containerRef, onNavigateWikilink, sourceVaultPath, sourceEntry?.path)
|
||||
|
||||
useEffect(() => {
|
||||
_wikilinkEntriesRef.current = entries
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
Loading…
Reference in a new issue