feat: restore HoloLake page blocks and visible themes

This commit is contained in:
冰朔 2026-07-17 10:47:52 +08:00
parent 025f22bf8f
commit 6fb1c02ddd
7 changed files with 1069 additions and 10 deletions

View File

@ -12,7 +12,8 @@
## 当前界面迭代 · 冰湖视觉层
`patches/` 中的补丁按编号顺序应用:
`patches/` 中的补丁以系列入口恢复。`0003` 已包含 `0001``0002`
因此不能将 `0001``0002``0003` 连续重复应用:
1. `0001-feat-add-Guanghu-ice-lake-visual-layer.patch`
- 为浅色与深色模式增加冰湖光层、网格层和语义颜色;
@ -28,20 +29,35 @@
- 收敛为轻量、可切换的光湖配套主题;
- 修正 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、表格、白板与媒体块。
## 恢复与验证
```bash
git clone https://guanghubingshuo.com/code/bingshuo/guanghu.git
cd guanghu
git am /path/to/0001-feat-add-Guanghu-ice-lake-visual-layer.patch
git am /path/to/0002-test-align-updater-coverage-with-Guanghu-policy.patch
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
pnpm build
pnpm lint
```
`0001``0002` 保留为早期两补丁拆分档案;只有在不使用 `0003`
原型系列时才单独应用。上述 `0003 → 0007` 顺序已于 2026-07-17
在服务器最新 `bingshuo/guanghu@514ab19` 的新克隆上完整复现。
## 研发规则
- 新的页面、渲染模块和交互协议先在本目录登记,再以独立补丁落到可编译基线;

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -4,7 +4,7 @@
>
> 适用仓库: `bingshuo/hololake-platform`
>
> 状态: TODO_REGISTERED
> 状态: PAGE_BLOCKS_COMPLETE_RUNNER_PENDING
## 1 · 补 Gitea Actions Runner
@ -37,6 +37,16 @@ docs/ops/GITEA-ACTIONS-RUNNER-WINDOWS-INTERNAL.md
## 2 · 光湖 App 页面内容块增强
2026-07-17 恢复进度:
```text
已完成并登记为平台仓补丁 0005—0007。
已完成:彩色提示块、批注块、关系卡、路径包、折叠块恢复、主题可见性修复、默认世界首页。
现有能力已确认代码块、引用、Mermaid、表格、白板、图片/音视频。
前端定向测试5 个文件24 项通过。
TypeScript、ESLint 与 Vite production build 通过。
```
冰朔反馈:
```text
@ -98,8 +108,7 @@ research/progress/HLP-DEV-0002-ICE-GL-INFINITY-HOLOLAKE-ERA-PROTOTYPE-REVIEW.md
先补 runner:
docs/ops/GITEA-ACTIONS-RUNNER-WINDOWS-INTERNAL.md
再开页面内容块:
页面内容块已进入验收与打包:
apps/tolaria/modules/02-knowledge-rendering/README.md
docs/NEXT-TODO-20260717.md
```

View File

@ -3,7 +3,7 @@ registry:
name: 光湖研发模块注册表
status: active
source_of_truth: true
updated_at: "2026-07-13T22:30:00+08:00"
updated_at: "2026-07-17T10:30:00+08:00"
rules:
- 模块编号永久唯一,废弃后不可复用。
@ -41,9 +41,9 @@ modules:
broadcast: research/broadcast/HLP-BRD-0003-ICE-GL-INFINITY-ZERO-CORE-ACTIVATION.md
code_home: apps/tolaria/modules/02-knowledge-rendering/
summary: 轻量知识页、路径卡、媒体和图表的可读渲染;重型全局皮肤不作为当前方向。
evidence: apps/tolaria/patches/0001-feat-add-Guanghu-ice-lake-visual-layer.patch
latest_progress: research/progress/HLP-DEV-0003-ICE-GL-INFINITY-TOLARIA-LIGHTWEIGHT-UI-AND-DRAG-FIX.md
updated_at: "2026-07-15T17:00:00+08:00"
evidence: apps/tolaria/patches/0007-feat-complete-portable-page-block-set.patch
latest_progress: research/progress/HLP-DEV-0004-HOLOLAKE-PAGE-BLOCKS-AND-THEME-RECOVERY.md
updated_at: "2026-07-17T10:30:00+08:00"
- id: HLP-MOD-0003
name: 仓库路径与知识索引
product: Tolaria / 光湖 App

View File

@ -0,0 +1,49 @@
# HLP-DEV-0004 · HoloLake Era 页面块与主题恢复
> 日期2026-07-17
>
> 关联模块:`HLP-MOD-0002`
>
> 状态:已验证
## 为什么做
桌面同时存在三个同 Bundle ID、同图标的 App 副本macOS LaunchServices
会将它们视为同一应用。上一轮所谓“Notion 类页面组件”实际只完成了
四种 callout首页默认隐藏浅色主题也没有同步应用的基础亮暗模式。
## 实际改动
1. 恢复并完善可携带 Markdown callout 块info / success / warning / danger。
2. 增加 annotation / relationship / path 三种语义块,支持中英文斜杠命令搜索。
3. 重新开放 BlockNote 原生 toggle heading / toggle list 入口。
4. 保留并复用现有代码、引用、Mermaid、表格、白板和媒体块。
5. HoloLake Era 世界首页改为默认可见,首页直接显示页面组件已就绪。
6. 湖光浅色与湖心深色现在会切换实际 `data-theme` 亮/暗模式。
## 代码与补丁路径
- 可编译开发基线:`bingshuo/guanghu`REPO-004只作零件/上游基线)。
- 产品事实与付款路径:`bingshuo/hololake-platform` 的独立补丁。
- `apps/tolaria/patches/0005-Add-portable-callout-page-blocks.patch`
- `apps/tolaria/patches/0006-fix-make-Guanghu-home-and-themes-visible.patch`
- `apps/tolaria/patches/0007-feat-complete-portable-page-block-set.patch`
## 验证
- Vitest5 个定向文件24 项全部通过。
- ESLint本次修改文件通过。
- TypeScript`tsc --noEmit` 通过。
- Viteproduction build 通过。
- TauriApple Silicon release App 与 DMG 构建通过。
- 原生视觉验收:默认世界首页、光湖 Logo、页面组件入口、独立 0.1.3 版本号可见。
- 内测 Bundle ID`com.guanghu.desktop.review.20260717`,不与旧 `com.guanghu.desktop` 副本串版。
- DMG SHA-256 本地已核验,仓库仅记缩略值:`29bda0c9…40fda73`
- 在服务器最新 `bingshuo/guanghu@514ab19` 新克隆上,按 `0003 → 0004 → 0005 → 0006 → 0007` 应用成功。
- CodeScene / Codacy当前本机没有 MCP、CLI 或 API 环境,未能执行,不伪造通过结果。
## 未完成项
1. 用户确认 0.1.3 Review 后,将桌面三个旧副本移入归档或废纸篓。
2. 安装 CodeScene / Codacy 验证环境后补跑两项质量门。
3. Windows 自动打包仍需 Gitea Actions runner。