diff --git a/products/tolaria-guanghu/README.hdlp b/products/tolaria-guanghu/README.hdlp new file mode 100644 index 0000000..af53643 --- /dev/null +++ b/products/tolaria-guanghu/README.hdlp @@ -0,0 +1,44 @@ +--- +id: DEV-TOL-SRC-001 +protocol: HLDP +product: Tolaria光湖定制版 +upstream: https://github.com/refactoringhq/tolaria +upstream_commit: af6891b0af725a219427eb0ff056cbc719e40812 +local_commit: 9bd612c +status: installed +maintainer: ICE-GL-ZY001 +receipt: CC-074 +--- + +# Tolaria光湖定制版 · 源码补丁入口 + +## 当前补丁 + +`patches/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch` + +包含: + +1. 富文本wikilink普通鼠标单击跳转。 +2. 中文、Emoji与Notion长编号后缀回归测试。 +3. `guanghu-portal` Markdown持久化组件。 +4. 光湖入口卡富文本渲染样式。 + +## 恢复方法 + +```bash +git clone https://github.com/refactoringhq/tolaria.git +cd tolaria +git checkout af6891b0af725a219427eb0ff056cbc719e40812 +git am /path/to/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch +``` + +## 已验证 + +- 4873项前端测试通过。 +- TypeScript/Vite生产构建通过。 +- Tauri macOS原生构建通过。 +- 安装位置:`/Applications/Tolaria.app` +- 官方回退版:`/Applications/Tolaria-Official-2026.7.1.app` + +> 认知链与桌面验收见 CC-074。 + diff --git a/products/tolaria-guanghu/patches/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch b/products/tolaria-guanghu/patches/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch new file mode 100644 index 0000000..05236b8 --- /dev/null +++ b/products/tolaria-guanghu/patches/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch @@ -0,0 +1,391 @@ +From 9bd612ca137fb1f40b5ddd022a68a73b2f38eb98 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=E9=93=B8=E6=B8=8A?= +Date: Fri, 3 Jul 2026 13:43:14 +0800 +Subject: [PATCH] feat: add plain-click wikilinks and Guanghu portal block + +--- + src/components/Editor.css | 44 +++++++++ + src/components/editorSchema.tsx | 39 ++++++++ + .../useEditorLinkActivation.test.tsx | 11 +-- + src/components/useEditorLinkActivation.ts | 2 - + src/utils/editorDurableMarkdown.ts | 2 + + src/utils/guanghuPortalMarkdown.test.ts | 38 ++++++++ + src/utils/guanghuPortalMarkdown.ts | 93 +++++++++++++++++++ + src/utils/wikilink.test.ts | 23 +++++ + 8 files changed, 243 insertions(+), 9 deletions(-) + create mode 100644 src/utils/guanghuPortalMarkdown.test.ts + create mode 100644 src/utils/guanghuPortalMarkdown.ts + +diff --git a/src/components/Editor.css b/src/components/Editor.css +index 39866cc..499b72f 100644 +--- a/src/components/Editor.css ++++ b/src/components/Editor.css +@@ -22,6 +22,50 @@ + opacity: 0.55; + } + ++.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; ++ 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); ++} ++ ++.guanghu-portal-card__eyebrow { ++ display: flex; ++ align-items: center; ++ justify-content: space-between; ++ gap: 0.75rem; ++ margin-bottom: 0.55rem; ++ color: var(--colors-text-muted); ++ font-size: 0.72rem; ++ letter-spacing: 0.08em; ++} ++ ++.guanghu-portal-card__status { ++ padding: 0.18rem 0.55rem; ++ border-radius: 999px; ++ background: color-mix(in srgb, var(--colors-accent) 18%, transparent); ++ color: var(--colors-text); ++ letter-spacing: normal; ++} ++ ++.guanghu-portal-card__title { ++ display: inline-flex; ++ font-size: 1.08rem; ++ font-weight: 650; ++} ++ ++.guanghu-portal-card__description { ++ margin: 0.45rem 0 0; ++ color: var(--colors-text-muted); ++ font-size: 0.9rem; ++ line-height: 1.55; ++} ++ + /* 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 769cec2..2984eff 100644 +--- a/src/components/editorSchema.tsx ++++ b/src/components/editorSchema.tsx +@@ -24,6 +24,7 @@ import { MATH_BLOCK_TYPE, MATH_INLINE_TYPE, renderMathToHtml } from '../utils/ma + 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 type { VaultEntry } from '../types' + import { createTolariaCodeBlockOptions } from './codeBlockOptions' + import { NoteTitleIcon } from './NoteTitleIcon' +@@ -313,6 +314,42 @@ const MermaidBlock = createReactBlockSpec( + }, + ) + ++const GuanghuPortalBlock = createReactBlockSpec( ++ { ++ type: GUANGHU_PORTAL_BLOCK_TYPE, ++ propSchema: { ++ source: { default: '' }, ++ title: { default: '光湖入口' }, ++ target: { default: '' }, ++ description: { default: '' }, ++ status: { default: '' }, ++ }, ++ content: 'none', ++ }, ++ { ++ runsBefore: ['codeBlock'], ++ render: props => ( ++
++
++ HLDP · 光湖入口 ++ {props.block.props.status ? ( ++ {props.block.props.status} ++ ) : null} ++
++ ++ {props.block.props.title} ++ ++ {props.block.props.description ? ( ++

{props.block.props.description}

++ ) : null} ++
++ ), ++ }, ++) ++ + export function mediaBlockPropsForPreviewRuntime( + props: T, + externalMediaPreview: boolean, +@@ -412,6 +449,7 @@ const TldrawBlock = createReactBlockSpec( + const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions()) + const audioBlock = AudioBlockSpec() + const mathBlock = MathBlock() ++const guanghuPortalBlock = GuanghuPortalBlock() + const mermaidBlock = MermaidBlock() + const tldrawBlock = TldrawBlock() + const videoBlock = VideoBlockSpec() +@@ -446,6 +484,7 @@ export const schema = BlockNoteSchema.create({ + }, + blockSpecs: { + audio: audioBlock, ++ guanghuPortalBlock, + mathBlock, + mermaidBlock, + tldrawBlock, +diff --git a/src/components/useEditorLinkActivation.test.tsx b/src/components/useEditorLinkActivation.test.tsx +index 2234d5c..ab3b62a 100644 +--- a/src/components/useEditorLinkActivation.test.tsx ++++ b/src/components/useEditorLinkActivation.test.tsx +@@ -77,22 +77,18 @@ describe('useEditorLinkActivation', () => { + mockOpenLocalFile.mockClear() + }) + +- it('navigates wikilinks only on Cmd+click after the native click stack settles', async () => { ++ it('navigates wikilinks on a plain click after the native click stack settles', async () => { + const { container, onNavigateWikilink } = renderHarness() + const wikilink = appendWikilink(container, 'Alpha Project') + + dispatchMouseEvent(wikilink, 'click') + expect(onNavigateWikilink).not.toHaveBeenCalled() + +- const modifiedClick = dispatchMouseEvent(wikilink, 'click', { metaKey: true }) +- expect(modifiedClick.defaultPrevented).toBe(true) +- expect(onNavigateWikilink).not.toHaveBeenCalled() +- + await Promise.resolve() + expect(onNavigateWikilink).toHaveBeenCalledWith('Alpha Project') + }) + +- it('consumes plain wikilink mousedown and click events before editor internals see stale link nodes', () => { ++ it('consumes plain wikilink events before editor internals see stale link nodes', async () => { + const { container, onNavigateWikilink } = renderHarness() + const wikilink = appendWikilink(container, 'Alpha Project') + +@@ -101,7 +97,8 @@ describe('useEditorLinkActivation', () => { + + expect(mouseDown.defaultPrevented).toBe(true) + expect(click.defaultPrevented).toBe(true) +- expect(onNavigateWikilink).not.toHaveBeenCalled() ++ await Promise.resolve() ++ expect(onNavigateWikilink).toHaveBeenCalledWith('Alpha Project') + }) + + it('blurs an active editor before navigating a Cmd-clicked wikilink', async () => { +diff --git a/src/components/useEditorLinkActivation.ts b/src/components/useEditorLinkActivation.ts +index aa80834..f29c624 100644 +--- a/src/components/useEditorLinkActivation.ts ++++ b/src/components/useEditorLinkActivation.ts +@@ -56,8 +56,6 @@ function activateWikilink( + ) { + consumeEditorLinkEvent(event) + +- if (!hasFollowModifier(event)) return +- + blurActiveEditable(container) + scheduleAfterNativeClick(() => onNavigateWikilink(target)) + } +diff --git a/src/utils/editorDurableMarkdown.ts b/src/utils/editorDurableMarkdown.ts +index f0234f1..6e6d30a 100644 +--- a/src/utils/editorDurableMarkdown.ts ++++ b/src/utils/editorDurableMarkdown.ts +@@ -15,8 +15,10 @@ import { restoreMarkdownHighlightsInBlocks } from './markdownHighlightMarkdown' + import { serializeMathAwareBlocks } from './mathMarkdown' + import { mermaidMarkdownCodec } from './mermaidMarkdown' + import { tldrawMarkdownCodec } from './tldrawMarkdown' ++import { guanghuPortalMarkdownCodec } from './guanghuPortalMarkdown' + + const EDITOR_DURABLE_MARKDOWN_CODECS = [ ++ guanghuPortalMarkdownCodec, + mermaidMarkdownCodec, + tldrawMarkdownCodec, + ] as const +diff --git a/src/utils/guanghuPortalMarkdown.test.ts b/src/utils/guanghuPortalMarkdown.test.ts +new file mode 100644 +index 0000000..9bb518a +--- /dev/null ++++ b/src/utils/guanghuPortalMarkdown.test.ts +@@ -0,0 +1,38 @@ ++import { describe, expect, it } from 'vitest' ++import { ++ GUANGHU_PORTAL_BLOCK_TYPE, ++ guanghuPortalMarkdownCodec, ++} from './guanghuPortalMarkdown' ++import { ++ injectDurableMarkdownBlocks, ++ preProcessDurableMarkdownBlocks, ++ serializeDurableMarkdownBlocks, ++} from './durableMarkdownBlocks' ++ ++const SOURCE = `\`\`\`guanghu-portal ++{"title":"铸渊主控台","target":"🌳 铸渊 · Tolaria编号嵌套树 · NAV-CODE-TREE-001","description":"进入现实执行层","status":"运行中"} ++\`\`\` ++` ++ ++describe('guanghuPortalMarkdownCodec', () => { ++ it('round-trips a Guanghu portal card through the rich editor pipeline', () => { ++ const processed = preProcessDurableMarkdownBlocks({ ++ markdown: SOURCE, ++ codecs: [guanghuPortalMarkdownCodec], ++ }) ++ const blocks = injectDurableMarkdownBlocks({ ++ blocks: [{ type: 'paragraph', content: [{ type: 'text', text: processed.trim() }] }], ++ codecs: [guanghuPortalMarkdownCodec], ++ }) as Array<{ type: string; props: Record }> ++ ++ expect(blocks[0]?.type).toBe(GUANGHU_PORTAL_BLOCK_TYPE) ++ expect(blocks[0]?.props.title).toBe('铸渊主控台') ++ expect(blocks[0]?.props.target).toContain('铸渊') ++ ++ expect(serializeDurableMarkdownBlocks({ ++ blocks, ++ codecs: [guanghuPortalMarkdownCodec], ++ serializeOrdinaryBlocks: () => '', ++ })).toBe(SOURCE) ++ }) ++}) +diff --git a/src/utils/guanghuPortalMarkdown.ts b/src/utils/guanghuPortalMarkdown.ts +new file mode 100644 +index 0000000..f083b25 +--- /dev/null ++++ b/src/utils/guanghuPortalMarkdown.ts +@@ -0,0 +1,93 @@ ++import { ++ type BlockLike, ++ type DurableBlockCodec, ++ type DurableFencePayloadInput, ++} from './durableMarkdownBlocks' ++ ++export const GUANGHU_PORTAL_BLOCK_TYPE = 'guanghuPortalBlock' ++ ++const TOKEN_PREFIX = '@@TOLARIA_GUANGHU_PORTAL:' ++const TOKEN_SUFFIX = '@@' ++ ++export interface GuanghuPortalPayload { ++ source: string ++ title: string ++ target: string ++ description: string ++ status: string ++} ++ ++function isRecord(value: unknown): value is Record { ++ return typeof value === 'object' && value !== null && !Array.isArray(value) ++} ++ ++function readString(record: Record, key: string): string { ++ const value = record[key] ++ return typeof value === 'string' ? value : '' ++} ++ ++function decodePayload(payload: unknown): GuanghuPortalPayload | null { ++ if (!isRecord(payload)) return null ++ const source = readString(payload, 'source') ++ const title = readString(payload, 'title') ++ const target = readString(payload, 'target') ++ if (!source || !title || !target) return null ++ return { ++ source, ++ title, ++ target, ++ description: readString(payload, 'description'), ++ status: readString(payload, 'status'), ++ } ++} ++ ++function readFenceMetadata(info: string): Record | null { ++ return info.trim().toLowerCase() === 'guanghu-portal' ? {} : null ++} ++ ++function buildPayload({ lines, start, end }: DurableFencePayloadInput): GuanghuPortalPayload { ++ const source = lines.slice(start, end + 1).join('') ++ const body = lines.slice(start + 1, end).join('').trim() ++ try { ++ const decoded = decodePayload({ ...JSON.parse(body), source }) ++ if (decoded) return decoded ++ } catch { ++ // Invalid card data remains visible as an ordinary code block. ++ } ++ return { source, title: '光湖入口', target: '', description: body, status: '配置错误' } ++} ++ ++function buildBlock(block: BlockLike, payload: GuanghuPortalPayload): BlockLike { ++ if (!payload.target) return block ++ return { ++ ...block, ++ type: GUANGHU_PORTAL_BLOCK_TYPE, ++ props: { ++ ...(block.props ?? {}), ++ source: payload.source, ++ title: payload.title, ++ target: payload.target, ++ description: payload.description, ++ status: payload.status, ++ }, ++ content: undefined, ++ children: [], ++ } ++} ++ ++function isPortalBlock(block: BlockLike): boolean { ++ return block.type === GUANGHU_PORTAL_BLOCK_TYPE ++ && typeof block.props?.source === 'string' ++ && typeof block.props?.target === 'string' ++} ++ ++export const guanghuPortalMarkdownCodec: DurableBlockCodec = { ++ tokenPrefix: TOKEN_PREFIX, ++ tokenSuffix: TOKEN_SUFFIX, ++ readFenceMetadata, ++ buildPayload, ++ decodePayload, ++ buildBlock: (block, payload) => buildBlock(block, payload as GuanghuPortalPayload), ++ isBlock: isPortalBlock, ++ serializeBlock: block => block.props?.source ?? '', ++} +diff --git a/src/utils/wikilink.test.ts b/src/utils/wikilink.test.ts +index ed7864e..4aa125e 100644 +--- a/src/utils/wikilink.test.ts ++++ b/src/utils/wikilink.test.ts +@@ -273,6 +273,29 @@ describe('canonicalWikilinkTargetForTitle', () => { + }) + }) + ++describe('Unicode wikilink resolution', () => { ++ it('resolves Chinese and emoji titles across equivalent Unicode forms', () => { ++ const entry = makeEntry({ ++ path: '/vault/第五域/🌳 铸渊 · 导航地图.md', ++ filename: '🌳 铸渊 · 导航地图.md', ++ title: '🌳 铸渊 · 导航地图', ++ }) ++ ++ expect(resolveEntry([entry], '🌳 铸渊 · 导航地图')).toBe(entry) ++ expect(resolveEntry([entry], '🌳 鑄淵 · 導航地圖')).toBeUndefined() ++ }) ++ ++ it('resolves an imported file with an id suffix by its heading title', () => { ++ const entry = makeEntry({ ++ path: '/vault/🧭 霜砚 · 总控人格系统 · 母语导航页 b8463f86479348139b5744dd5e995c53.md', ++ filename: '🧭 霜砚 · 总控人格系统 · 母语导航页 b8463f86479348139b5744dd5e995c53.md', ++ title: '🧭 霜砚 · 总控人格系统 · 母语导航页', ++ }) ++ ++ expect(resolveEntry([entry], '🧭 霜砚 · 总控人格系统 · 母语导航页')).toBe(entry) ++ }) ++}) ++ + describe('formatWikilinkRef', () => { + it('wraps a canonical target in wikilink syntax', () => { + expect(formatWikilinkRef('projects/alpha')).toBe('[[projects/alpha]]') +-- +2.50.1 (Apple Git-155) +