feat(editor): add themed callouts and desktop command boundary

This commit is contained in:
2026-09-06 11:33:19 +08:00
parent fcc319d5e1
commit 415efc4444
26 changed files with 759 additions and 13 deletions
@@ -0,0 +1,25 @@
import { afterEach, expect, it, vi } from 'vitest'
import { executeEditorCommand, registerEditorCommands, getEditorCommandCapabilities } from './editorCommandService'
let dispose: (() => void) | undefined
afterEach(() => dispose?.())
it('reports unsupported, disabled and invalid commands without side effects', async () => {
expect(await executeEditorCommand('editor.bold')).toEqual({ ok: false, reason: 'unavailable' })
const handler = vi.fn(() => ({ ok: true as const }))
let available = false
dispose = registerEditorCommands({ available: () => available, handlers: { 'editor.bold': handler } })
expect(getEditorCommandCapabilities().find(item => item.id === 'editor.bold')).toMatchObject({ supported: true, enabled: false })
await executeEditorCommand('editor.bold')
expect(handler).not.toHaveBeenCalled()
available = true
expect(await executeEditorCommand('editor.bold')).toEqual({ ok: true })
expect(await executeEditorCommand('editor.metadata.edit')).toEqual({ ok: false, reason: 'unsupported' })
expect(await executeEditorCommand('arbitrary-command')).toEqual({ ok: false, reason: 'unsupported' })
})
it('old editor disposal cannot unregister the replacement editor', async () => {
const old = registerEditorCommands({ available: () => true, handlers: {} })
dispose = registerEditorCommands({ available: () => true, handlers: { 'editor.bold': () => ({ ok: true }) } })
old()
expect(await executeEditorCommand('editor.bold')).toEqual({ ok: true })
dispose()
expect(await executeEditorCommand('editor.bold')).toEqual({ ok: false, reason: 'unavailable' })
})
@@ -0,0 +1,33 @@
/** Versioned frontend boundary for future native menus/shortcuts; no Tauri IPC yet. */
export const editorCommandVersion = 1
export const editorCommandIds = [
'editor.bold', 'editor.italic', 'editor.strikethrough', 'editor.inline-code',
'editor.paragraph', 'editor.heading', 'editor.bullet-list', 'editor.ordered-list',
'editor.task-list', 'editor.blockquote', 'editor.callout', 'editor.code-block',
'editor.inline-math', 'editor.math-block', 'editor.mermaid', 'editor.link',
'editor.image', 'editor.table', 'editor.horizontal-rule', 'editor.hard-break',
'editor.font-size', 'editor.insert-markdown', 'editor.import-note-properties',
'editor.metadata.edit', 'editor.metadata.title', 'editor.metadata.tags',
'editor.reference-link', 'editor.html', 'editor.undo', 'editor.redo',
] as const
export type EditorCommandId = typeof editorCommandIds[number]
export type CommandResult = { ok: true } | { ok: false; reason: 'unsupported' | 'unavailable' | 'invalid-params' | 'failed' }
export type CommandHandler = (params: unknown) => CommandResult | Promise<CommandResult>
type Target = { available: () => boolean; handlers: Partial<Record<EditorCommandId, CommandHandler>> }
let active: Target | undefined
export function registerEditorCommands(target: Target) {
active = target
return () => { if (active === target) active = undefined }
}
export function getEditorCommandCapabilities() {
return editorCommandIds.map(id => ({ id, supported: !!active?.handlers[id], enabled: !!active?.handlers[id] && active.available() }))
}
export async function executeEditorCommand(id: string, params?: unknown): Promise<CommandResult> {
if (!(editorCommandIds as readonly string[]).includes(id)) return { ok: false, reason: 'unsupported' }
const target = active
if (!target || !target.available()) return { ok: false, reason: 'unavailable' }
const handler = target.handlers[id as EditorCommandId]
if (!handler) return { ok: false, reason: 'unsupported' }
try { return await handler(params) } catch { return { ok: false, reason: 'failed' } }
}
+8 -2
View File
@@ -361,7 +361,7 @@ export const mockCommunityThemes: ThemeManifest[] = [
{
theme_id: 'ocean-blue',
name: 'Ocean Blue',
version: '1.3.1',
version: '1.4.0',
author: 'community',
description: '宁静的海洋蓝色主题,适合长时间阅读',
min_app_version: '0.2.0',
@@ -373,7 +373,7 @@ export const mockCommunityThemes: ThemeManifest[] = [
{
theme_id: 'midnight-purple',
name: 'Midnight Purple',
version: '2.1.1',
version: '2.2.0',
author: 'night-owl',
description: '深紫色暗夜主题,适合编码',
min_app_version: '0.2.0',
@@ -464,6 +464,12 @@ export function getCommunityThemePreviewCss(themeId: string): string {
return buildCommunityThemeCss(themeId, t.is_dark) + `
[data-theme="${themeId}"] {
color-scheme: ${t.is_dark ? 'dark' : 'light'};
--color-callout-info: ${t.is_dark ? '#9dbbff' : '#126589'};
--color-callout-success: ${t.is_dark ? '#a7d58c' : '#267049'};
--color-callout-warning: ${t.is_dark ? '#efc886' : '#885c13'};
--color-callout-danger: ${t.is_dark ? '#ff9caf' : '#b13d4d'};
--color-callout-important: ${t.is_dark ? '#d4afff' : '#7050a3'};
--color-callout-quote: ${t.is_dark ? '#b0b9dd' : '#53697d'};
--color-text-inverse: ${t.is_dark ? '#1a1b26' : '#ffffff'};
--color-text-disabled: color-mix(in srgb, var(--color-text-primary) 45%, var(--color-surface-primary));
--color-background-overlay: ${t.is_dark ? '#000000a6' : '#00000073'};