@tessera-editor/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +903 -0
- package/dist/index.js +2538 -0
- package/dist/index.js.map +1 -0
- package/dist/tessera.css +1491 -0
- package/package.json +72 -0
- package/src/__tests__/markdown.test.ts +138 -0
- package/src/__tests__/v11.test.ts +184 -0
- package/src/__tests__/v112.test.ts +184 -0
- package/src/__tests__/writeback.test.ts +123 -0
- package/src/blockmenu.ts +38 -0
- package/src/diff.ts +150 -0
- package/src/extensions/context-menu.ts +55 -0
- package/src/extensions/emoji.ts +148 -0
- package/src/extensions/find-replace.ts +229 -0
- package/src/extensions/gallery.ts +111 -0
- package/src/extensions/history.ts +133 -0
- package/src/extensions/input-rules.ts +34 -0
- package/src/extensions/metrics.ts +61 -0
- package/src/extensions/shortcuts.ts +118 -0
- package/src/extensions/slash.ts +272 -0
- package/src/extensions/word-paste.ts +25 -0
- package/src/i18n.ts +320 -0
- package/src/index.ts +80 -0
- package/src/markdown.ts +260 -0
- package/src/marks/ai.ts +55 -0
- package/src/marks/comment.ts +137 -0
- package/src/marks/placeholder.ts +63 -0
- package/src/nodes/collapsible.ts +171 -0
- package/src/nodes/embed.ts +55 -0
- package/src/nodes/hint.ts +70 -0
- package/src/nodes/image.ts +77 -0
- package/src/nodes/table.ts +286 -0
- package/src/nodes/toc.ts +38 -0
- package/src/preset.ts +117 -0
- package/src/services.ts +103 -0
- package/src/styles/tessera.css +1491 -0
- package/src/wordpaste.ts +56 -0
- package/src/writeback.ts +156 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
import type { Editor, Range } from '@tiptap/core'
|
|
3
|
+
import Suggestion from '@tiptap/suggestion'
|
|
4
|
+
import type { SuggestionOptions } from '@tiptap/suggestion'
|
|
5
|
+
import { PluginKey } from '@tiptap/pm/state'
|
|
6
|
+
import { createTesseraT, type TesseraLocale, type TesseraTranslator } from '../i18n'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Slash menu (acceptance §1): framework-neutral extension built on
|
|
10
|
+
* @tiptap/suggestion. The binding injects a `render` implementation; items are
|
|
11
|
+
* data (title/description/shortcut/command) so React or Vue can render them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface SlashMenuItem {
|
|
15
|
+
id: string
|
|
16
|
+
group: 'basic' | 'advanced' | 'ai'
|
|
17
|
+
title: string
|
|
18
|
+
description?: string
|
|
19
|
+
/** display-only hint, e.g. "⌘⇧9" */
|
|
20
|
+
shortcut?: string
|
|
21
|
+
keywords?: string[]
|
|
22
|
+
command: (props: { editor: Editor; range: Range }) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SlashRenderFactory = SuggestionOptions<SlashMenuItem>['render']
|
|
26
|
+
|
|
27
|
+
export interface SlashMenuOptions {
|
|
28
|
+
locale: TesseraLocale
|
|
29
|
+
/** extra host items (e.g. AI actions registered by @tessera-editor/ai) */
|
|
30
|
+
extraItems?: (ctx: { editor: Editor; t: TesseraTranslator }) => SlashMenuItem[]
|
|
31
|
+
includeAiItems?: boolean
|
|
32
|
+
render?: SlashRenderFactory
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function chainDelete(editor: Editor, range: Range) {
|
|
36
|
+
return editor.chain().focus().deleteRange(range)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Default block items — mirrors the Slite slash palette for the M1 scope. */
|
|
40
|
+
export function defaultSlashItems(t: TesseraTranslator): SlashMenuItem[] {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
id: 'text',
|
|
44
|
+
group: 'basic',
|
|
45
|
+
title: t('itemText'),
|
|
46
|
+
description: t('itemTextDesc'),
|
|
47
|
+
keywords: ['text', 'paragraph', '文本', '段落'],
|
|
48
|
+
command: ({ editor, range }) => chainDelete(editor, range).setParagraph().run(),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'h1',
|
|
52
|
+
group: 'basic',
|
|
53
|
+
title: t('itemH1'),
|
|
54
|
+
shortcut: '⌘⇧1',
|
|
55
|
+
keywords: ['heading', 'title', '标题'],
|
|
56
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 1 }).run(),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'h2',
|
|
60
|
+
group: 'basic',
|
|
61
|
+
title: t('itemH2'),
|
|
62
|
+
shortcut: '⌘⇧2',
|
|
63
|
+
keywords: ['heading', 'title', '标题'],
|
|
64
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 2 }).run(),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'h3',
|
|
68
|
+
group: 'basic',
|
|
69
|
+
title: t('itemH3'),
|
|
70
|
+
shortcut: '⌘⇧3',
|
|
71
|
+
keywords: ['heading', 'title', '标题'],
|
|
72
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 3 }).run(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'h4',
|
|
76
|
+
group: 'basic',
|
|
77
|
+
title: t('itemH4'),
|
|
78
|
+
shortcut: '⌘⇧4',
|
|
79
|
+
keywords: ['heading', 'title', '标题'],
|
|
80
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleHeading({ level: 4 }).run(),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'bulletList',
|
|
84
|
+
group: 'basic',
|
|
85
|
+
title: t('itemBullet'),
|
|
86
|
+
description: t('itemBulletDesc'),
|
|
87
|
+
shortcut: '⌘⇧8',
|
|
88
|
+
keywords: ['bullet', 'list', 'unordered', '列表'],
|
|
89
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleBulletList().run(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'orderedList',
|
|
93
|
+
group: 'basic',
|
|
94
|
+
title: t('itemOrdered'),
|
|
95
|
+
description: t('itemOrderedDesc'),
|
|
96
|
+
shortcut: '⌘⇧7',
|
|
97
|
+
keywords: ['ordered', 'list', 'numbered', '列表'],
|
|
98
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleOrderedList().run(),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'taskList',
|
|
102
|
+
group: 'basic',
|
|
103
|
+
title: t('itemTask'),
|
|
104
|
+
description: t('itemTaskDesc'),
|
|
105
|
+
shortcut: '⌘⇧C',
|
|
106
|
+
keywords: ['task', 'todo', 'checkbox', '任务', '清单'],
|
|
107
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleTaskList().run(),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'quote',
|
|
111
|
+
group: 'basic',
|
|
112
|
+
title: t('itemQuote'),
|
|
113
|
+
description: t('itemQuoteDesc'),
|
|
114
|
+
shortcut: '⌘⇧.',
|
|
115
|
+
keywords: ['quote', 'blockquote', '引用'],
|
|
116
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleBlockquote().run(),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'codeBlock',
|
|
120
|
+
group: 'advanced',
|
|
121
|
+
title: t('itemCode'),
|
|
122
|
+
description: t('itemCodeDesc'),
|
|
123
|
+
shortcut: '⌘⇧9',
|
|
124
|
+
keywords: ['code', '代码'],
|
|
125
|
+
command: ({ editor, range }) => chainDelete(editor, range).toggleCodeBlock().run(),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'divider',
|
|
129
|
+
group: 'advanced',
|
|
130
|
+
title: t('itemDivider'),
|
|
131
|
+
description: t('itemDividerDesc'),
|
|
132
|
+
keywords: ['divider', 'hr', 'rule', '分割线'],
|
|
133
|
+
command: ({ editor, range }) => chainDelete(editor, range).setHorizontalRule().run(),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'hint',
|
|
137
|
+
group: 'advanced',
|
|
138
|
+
title: t('itemHint'),
|
|
139
|
+
description: t('itemHintDesc'),
|
|
140
|
+
shortcut: '⌘⌥H',
|
|
141
|
+
keywords: ['hint', 'callout', 'info', '提示'],
|
|
142
|
+
command: ({ editor, range }) => chainDelete(editor, range).setHint().run(),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'collapsible',
|
|
146
|
+
group: 'advanced',
|
|
147
|
+
title: t('itemCollapsible'),
|
|
148
|
+
description: t('itemCollapsibleDesc'),
|
|
149
|
+
keywords: ['collapsible', 'fold', 'details', '折叠'],
|
|
150
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertCollapsible().run(),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'image',
|
|
154
|
+
group: 'advanced',
|
|
155
|
+
title: t('itemImage'),
|
|
156
|
+
description: t('itemImageDesc'),
|
|
157
|
+
keywords: ['image', 'picture', 'photo', '图片'],
|
|
158
|
+
command: ({ editor, range }) => {
|
|
159
|
+
// no file dialog in core — emit for the binding (host opens picker)
|
|
160
|
+
chainDelete(editor, range).run()
|
|
161
|
+
editor.emit('tessera:insertImage', {})
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'table',
|
|
166
|
+
group: 'advanced',
|
|
167
|
+
title: t('itemTable'),
|
|
168
|
+
description: t('itemTableDesc'),
|
|
169
|
+
shortcut: '⌘⌥S',
|
|
170
|
+
keywords: ['table', '表格'],
|
|
171
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertTableTyped({ withHeaderRow: true }).run(),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'table-simple',
|
|
175
|
+
group: 'advanced',
|
|
176
|
+
title: t('itemTableSimple'),
|
|
177
|
+
description: t('itemTableSimpleDesc'),
|
|
178
|
+
shortcut: '⌘⌥T',
|
|
179
|
+
keywords: ['table', 'simple', '表格'],
|
|
180
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertTableTyped({ withHeaderRow: false }).run(),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'embed',
|
|
184
|
+
group: 'advanced',
|
|
185
|
+
title: t('itemEmbed'),
|
|
186
|
+
description: t('itemEmbedDesc'),
|
|
187
|
+
keywords: ['embed', 'iframe', '嵌入'],
|
|
188
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertEmbed({ src: '' }).run(),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'toc',
|
|
192
|
+
group: 'advanced',
|
|
193
|
+
title: t('itemToc'),
|
|
194
|
+
description: t('itemTocDesc'),
|
|
195
|
+
keywords: ['toc', 'outline', '目录', '大纲'],
|
|
196
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertToc().run(),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'placeholder-person',
|
|
200
|
+
group: 'advanced',
|
|
201
|
+
title: t('placeholderPerson'),
|
|
202
|
+
keywords: ['somebody', 'person', 'owner', '待填人'],
|
|
203
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertPlaceholderToken('person', t('placeholderPerson')).run(),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'placeholder-date',
|
|
207
|
+
group: 'advanced',
|
|
208
|
+
title: t('placeholderDate'),
|
|
209
|
+
keywords: ['date', 'due', '待填日期'],
|
|
210
|
+
command: ({ editor, range }) => chainDelete(editor, range).insertPlaceholderToken('date', t('placeholderDate')).run(),
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'history',
|
|
214
|
+
group: 'advanced',
|
|
215
|
+
title: t('itemHistory'),
|
|
216
|
+
keywords: ['history', 'version', 'snapshot', '历史', '版本'],
|
|
217
|
+
command: ({ editor, range }) => {
|
|
218
|
+
chainDelete(editor, range).run()
|
|
219
|
+
editor.emit('tessera:historyPanel', {})
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const SlashMenu = Extension.create<SlashMenuOptions>({
|
|
226
|
+
name: 'tesseraSlashMenu',
|
|
227
|
+
|
|
228
|
+
addOptions() {
|
|
229
|
+
return {
|
|
230
|
+
locale: 'zh-CN',
|
|
231
|
+
extraItems: undefined,
|
|
232
|
+
includeAiItems: false,
|
|
233
|
+
render: undefined,
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
addProseMirrorPlugins() {
|
|
238
|
+
const editor = this.editor
|
|
239
|
+
const options = this.options
|
|
240
|
+
const t = createTesseraT(options.locale)
|
|
241
|
+
|
|
242
|
+
const items = [
|
|
243
|
+
...defaultSlashItems(t),
|
|
244
|
+
...(options.extraItems?.({ editor, t }) ?? []),
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
return [
|
|
248
|
+
Suggestion({
|
|
249
|
+
editor,
|
|
250
|
+
pluginKey: new PluginKey('tesseraSlashMenu'),
|
|
251
|
+
char: '/',
|
|
252
|
+
startOfLine: true,
|
|
253
|
+
items: ({ query }) => {
|
|
254
|
+
const q = query.toLowerCase()
|
|
255
|
+
if (!q) {
|
|
256
|
+
return items
|
|
257
|
+
}
|
|
258
|
+
return items.filter(
|
|
259
|
+
item =>
|
|
260
|
+
item.title.toLowerCase().includes(q) ||
|
|
261
|
+
item.id.toLowerCase().includes(q) ||
|
|
262
|
+
item.keywords?.some(k => k.toLowerCase().includes(q)),
|
|
263
|
+
)
|
|
264
|
+
},
|
|
265
|
+
command: ({ editor: e, range, props }) => {
|
|
266
|
+
;(props as SlashMenuItem).command({ editor: e, range })
|
|
267
|
+
},
|
|
268
|
+
render: options.render as SuggestionOptions<SlashMenuItem>['render'],
|
|
269
|
+
}),
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
3
|
+
import { cleanWordHtml, isWordHtml } from '../wordpaste'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Word-source paste cleaning (acceptance §9, v1.1): when the pasted HTML is
|
|
7
|
+
* detected as Word export, the rules table in wordpaste.ts runs before the
|
|
8
|
+
* schema-based parse. Non-Word pastes pass through untouched.
|
|
9
|
+
*/
|
|
10
|
+
export const TesseraWordPaste = Extension.create({
|
|
11
|
+
name: 'tesseraWordPaste',
|
|
12
|
+
|
|
13
|
+
addProseMirrorPlugins() {
|
|
14
|
+
return [
|
|
15
|
+
new Plugin({
|
|
16
|
+
key: new PluginKey('tesseraWordPaste'),
|
|
17
|
+
props: {
|
|
18
|
+
transformPastedHTML(html) {
|
|
19
|
+
return isWordHtml(html) ? cleanWordHtml(html) : html
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
})
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tessera i18n: zh-CN / en-US dictionaries for every UI string the component
|
|
3
|
+
* family renders (menus, toolbars, panels, AI actions). Hosts may extend with
|
|
4
|
+
* additional locales by supplying messages to the binding.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type TesseraLocale = 'zh-CN' | 'en-US'
|
|
8
|
+
|
|
9
|
+
export const tesseraMessages = {
|
|
10
|
+
'zh-CN': {
|
|
11
|
+
// placeholder
|
|
12
|
+
placeholderEmpty: "输入 / 唤起菜单,或直接开始书写…",
|
|
13
|
+
// slash menu groups
|
|
14
|
+
groupBasic: '基础块',
|
|
15
|
+
groupAdvanced: '高级块',
|
|
16
|
+
groupAi: 'AI',
|
|
17
|
+
// slash items
|
|
18
|
+
itemText: '文本',
|
|
19
|
+
itemTextDesc: '普通段落',
|
|
20
|
+
itemH1: '标题 1',
|
|
21
|
+
itemH2: '标题 2',
|
|
22
|
+
itemH3: '标题 3',
|
|
23
|
+
itemH4: '标题 4',
|
|
24
|
+
itemBullet: '无序列表',
|
|
25
|
+
itemBulletDesc: '简单的项目符号列表',
|
|
26
|
+
itemOrdered: '有序列表',
|
|
27
|
+
itemOrderedDesc: '带编号的列表',
|
|
28
|
+
itemTask: '任务清单',
|
|
29
|
+
itemTaskDesc: '用复选框跟踪待办',
|
|
30
|
+
itemQuote: '引用',
|
|
31
|
+
itemQuoteDesc: '引用一段文字',
|
|
32
|
+
itemCode: '代码块',
|
|
33
|
+
itemCodeDesc: '带语法高亮的代码',
|
|
34
|
+
itemDivider: '分割线',
|
|
35
|
+
itemDividerDesc: '水平分割线',
|
|
36
|
+
itemHint: '提示块',
|
|
37
|
+
itemHintDesc: '醒目的信息/警告容器(!! + 空格)',
|
|
38
|
+
itemCollapsible: '折叠块',
|
|
39
|
+
itemCollapsibleDesc: '可折叠的内容区(>> + 空格)',
|
|
40
|
+
itemImage: '图片',
|
|
41
|
+
itemImageDesc: '上传或粘贴图片',
|
|
42
|
+
itemImageUrl: '图片链接',
|
|
43
|
+
// AI slash items
|
|
44
|
+
itemSummarize: 'AI 摘要',
|
|
45
|
+
itemSummarizeDesc: '为整篇文档生成 TL;DR',
|
|
46
|
+
itemAsk: 'AI 问答',
|
|
47
|
+
itemAskDesc: '针对当前文档提问',
|
|
48
|
+
itemImprove: 'AI 改写',
|
|
49
|
+
itemImproveDesc: '改写选中的文字',
|
|
50
|
+
// selection toolbar
|
|
51
|
+
tooltipBold: '加粗',
|
|
52
|
+
tooltipItalic: '斜体',
|
|
53
|
+
tooltipUnderline: '下划线',
|
|
54
|
+
tooltipStrike: '删除线',
|
|
55
|
+
tooltipCode: '行内代码',
|
|
56
|
+
tooltipColor: '文字颜色',
|
|
57
|
+
tooltipHighlight: '高亮',
|
|
58
|
+
tooltipLink: '链接',
|
|
59
|
+
tooltipComment: '评论(v1.x)',
|
|
60
|
+
tooltipTurnCollapsible: '转为折叠块',
|
|
61
|
+
tooltipMore: '更多',
|
|
62
|
+
tooltipImprove: 'AI 改写',
|
|
63
|
+
tooltipCopyMarkdown: '复制为 Markdown',
|
|
64
|
+
// color/highlight panel
|
|
65
|
+
colorDefault: '默认',
|
|
66
|
+
highlightNone: '无高亮',
|
|
67
|
+
// link panel
|
|
68
|
+
linkPlaceholder: '链接地址…',
|
|
69
|
+
linkApply: '应用',
|
|
70
|
+
linkRemove: '移除链接',
|
|
71
|
+
linkOpen: '打开',
|
|
72
|
+
// empty-line toolbar
|
|
73
|
+
emptyLineExpand: '展开全部块',
|
|
74
|
+
// find & replace
|
|
75
|
+
findPlaceholder: '查找…',
|
|
76
|
+
replacePlaceholder: '替换为…',
|
|
77
|
+
findNext: '下一个',
|
|
78
|
+
findPrev: '上一个',
|
|
79
|
+
replaceOne: '替换',
|
|
80
|
+
replaceAll: '全部替换',
|
|
81
|
+
findCount: (n: number) => `${n} 个结果`,
|
|
82
|
+
findClose: '关闭',
|
|
83
|
+
// image
|
|
84
|
+
imageUploadFailed: '图片上传失败',
|
|
85
|
+
imageAlignLeft: '左对齐',
|
|
86
|
+
imageAlignCenter: '居中',
|
|
87
|
+
imageAlignFull: '全宽',
|
|
88
|
+
// AI panel
|
|
89
|
+
aiTitle: 'AI',
|
|
90
|
+
aiAskPlaceholder: '针对本文档提问…',
|
|
91
|
+
aiSend: '发送',
|
|
92
|
+
aiThinking: '思考中…',
|
|
93
|
+
aiAccept: '接受',
|
|
94
|
+
aiReject: '拒绝',
|
|
95
|
+
aiImprovePrompt: '改写要求(可选)…',
|
|
96
|
+
aiImproveRun: '执行',
|
|
97
|
+
aiStreaming: '生成中…',
|
|
98
|
+
aiRuntimeMissing: '未注入 AI Runtime,此操作不可用',
|
|
99
|
+
// v1.1: tables
|
|
100
|
+
itemTable: '表格',
|
|
101
|
+
itemTableDesc: '带类型化列的结构化表格(⌘⌥S)',
|
|
102
|
+
itemTableSimple: '无表头表格',
|
|
103
|
+
itemTableSimpleDesc: '纯文本简易布局表(⌘⌥T)',
|
|
104
|
+
colTypeText: '文本',
|
|
105
|
+
colTypeCheckbox: '复选框',
|
|
106
|
+
colTypeSelect: '单选标签',
|
|
107
|
+
colTypeMultiSelect: '多选标签',
|
|
108
|
+
colTypeNumber: '数字',
|
|
109
|
+
colTypeDate: '日期',
|
|
110
|
+
colTypeLink: '链接',
|
|
111
|
+
colMenuTitle: '列操作',
|
|
112
|
+
colMenuSortAsc: '升序排序',
|
|
113
|
+
colMenuSortDesc: '降序排序',
|
|
114
|
+
colMenuInsertLeft: '左侧插入列',
|
|
115
|
+
colMenuInsertRight: '右侧插入列',
|
|
116
|
+
colMenuDelete: '删除此列',
|
|
117
|
+
colMenuType: '列类型',
|
|
118
|
+
rowMenuTitle: '行操作',
|
|
119
|
+
rowMenuInsertAbove: '上方插入行',
|
|
120
|
+
rowMenuInsertBelow: '下方插入行',
|
|
121
|
+
rowMenuDelete: '删除此行',
|
|
122
|
+
tableDelete: '删除表格',
|
|
123
|
+
tableCopyCsv: '复制为 CSV',
|
|
124
|
+
tableToggleHeader: '切换表头',
|
|
125
|
+
cellEmpty: '空',
|
|
126
|
+
// v1.1: history
|
|
127
|
+
itemHistory: '版本历史',
|
|
128
|
+
historyTitle: '版本历史',
|
|
129
|
+
historyEmpty: '暂无快照(编辑后空闲自动保存,或手动捕获)',
|
|
130
|
+
historyCapture: '捕获快照',
|
|
131
|
+
historyRestore: '恢复此版本',
|
|
132
|
+
historyCurrent: '当前',
|
|
133
|
+
historyDiffAdded: '新增',
|
|
134
|
+
historyDiffRemoved: '删除',
|
|
135
|
+
historyDiffChanged: '修改',
|
|
136
|
+
historyConfirmRestore: '恢复到该版本?当前内容将被替换(可撤销)',
|
|
137
|
+
// v1.1: comments
|
|
138
|
+
tooltipCommentV11: '评论',
|
|
139
|
+
commentTitle: '评论',
|
|
140
|
+
commentEmpty: '暂无评论',
|
|
141
|
+
commentPlaceholder: '写下评论…(Shift+Enter 换行)',
|
|
142
|
+
commentSend: '发送',
|
|
143
|
+
commentResolve: '解决',
|
|
144
|
+
commentReopen: '重新打开',
|
|
145
|
+
commentDelete: '删除',
|
|
146
|
+
commentResolvedBadge: '已解决',
|
|
147
|
+
commentCount: (n: number) => `${n} 条评论`,
|
|
148
|
+
// v1.1: embed / toc / placeholder
|
|
149
|
+
itemEmbed: '嵌入',
|
|
150
|
+
itemEmbedDesc: '嵌入外部网页(iframe 沙箱)',
|
|
151
|
+
itemToc: '目录',
|
|
152
|
+
itemTocDesc: '自动生成全文大纲(H1–H4)',
|
|
153
|
+
embedPlaceholder: '粘贴要嵌入的链接…',
|
|
154
|
+
embedApply: '嵌入',
|
|
155
|
+
embedInvalid: '无效链接',
|
|
156
|
+
embedOpen: '打开原链接',
|
|
157
|
+
tocEmpty: '暂无标题——添加 H1–H4 后自动出现',
|
|
158
|
+
placeholderText: '待补充',
|
|
159
|
+
placeholderPerson: '待填人',
|
|
160
|
+
placeholderDate: '待填日期',
|
|
161
|
+
// v1.1: block context menu
|
|
162
|
+
menuCopyAnchor: '复制锚链接',
|
|
163
|
+
menuCopyBlockId: '复制块 ID',
|
|
164
|
+
menuDeleteBlock: '删除此块',
|
|
165
|
+
},
|
|
166
|
+
'en-US': {
|
|
167
|
+
placeholderEmpty: "Type / for blocks, or just start writing…",
|
|
168
|
+
groupBasic: 'Basic blocks',
|
|
169
|
+
groupAdvanced: 'Advanced blocks',
|
|
170
|
+
groupAi: 'AI',
|
|
171
|
+
itemText: 'Text',
|
|
172
|
+
itemTextDesc: 'Plain paragraph',
|
|
173
|
+
itemH1: 'Heading 1',
|
|
174
|
+
itemH2: 'Heading 2',
|
|
175
|
+
itemH3: 'Heading 3',
|
|
176
|
+
itemH4: 'Heading 4',
|
|
177
|
+
itemBullet: 'Bullet list',
|
|
178
|
+
itemBulletDesc: 'Simple bulleted list',
|
|
179
|
+
itemOrdered: 'Numbered list',
|
|
180
|
+
itemOrderedDesc: 'List with numbering',
|
|
181
|
+
itemTask: 'Task list',
|
|
182
|
+
itemTaskDesc: 'Track to-dos with checkboxes',
|
|
183
|
+
itemQuote: 'Quote',
|
|
184
|
+
itemQuoteDesc: 'Quote a passage',
|
|
185
|
+
itemCode: 'Code block',
|
|
186
|
+
itemCodeDesc: 'Code with syntax highlighting',
|
|
187
|
+
itemDivider: 'Divider',
|
|
188
|
+
itemDividerDesc: 'Horizontal rule',
|
|
189
|
+
itemHint: 'Hint',
|
|
190
|
+
itemHintDesc: 'Callout container (!! + space)',
|
|
191
|
+
itemCollapsible: 'Collapsible',
|
|
192
|
+
itemCollapsibleDesc: 'Foldable section (>> + space)',
|
|
193
|
+
itemImage: 'Image',
|
|
194
|
+
itemImageDesc: 'Upload or paste an image',
|
|
195
|
+
itemImageUrl: 'Image URL',
|
|
196
|
+
itemSummarize: 'AI Summarize',
|
|
197
|
+
itemSummarizeDesc: 'Generate a TL;DR for this doc',
|
|
198
|
+
itemAsk: 'AI Ask',
|
|
199
|
+
itemAskDesc: 'Ask questions about this doc',
|
|
200
|
+
itemImprove: 'AI Improve',
|
|
201
|
+
itemImproveDesc: 'Rewrite the selection',
|
|
202
|
+
tooltipBold: 'Bold',
|
|
203
|
+
tooltipItalic: 'Italic',
|
|
204
|
+
tooltipUnderline: 'Underline',
|
|
205
|
+
tooltipStrike: 'Strikethrough',
|
|
206
|
+
tooltipCode: 'Inline code',
|
|
207
|
+
tooltipColor: 'Text color',
|
|
208
|
+
tooltipHighlight: 'Highlight',
|
|
209
|
+
tooltipLink: 'Link',
|
|
210
|
+
tooltipComment: 'Comment (v1.x)',
|
|
211
|
+
tooltipTurnCollapsible: 'Turn into collapsible',
|
|
212
|
+
tooltipMore: 'More',
|
|
213
|
+
tooltipImprove: 'AI Improve',
|
|
214
|
+
tooltipCopyMarkdown: 'Copy as Markdown',
|
|
215
|
+
colorDefault: 'Default',
|
|
216
|
+
highlightNone: 'No highlight',
|
|
217
|
+
linkPlaceholder: 'Link URL…',
|
|
218
|
+
linkApply: 'Apply',
|
|
219
|
+
linkRemove: 'Remove link',
|
|
220
|
+
linkOpen: 'Open',
|
|
221
|
+
emptyLineExpand: 'Show all blocks',
|
|
222
|
+
findPlaceholder: 'Find…',
|
|
223
|
+
replacePlaceholder: 'Replace with…',
|
|
224
|
+
findNext: 'Next',
|
|
225
|
+
findPrev: 'Prev',
|
|
226
|
+
replaceOne: 'Replace',
|
|
227
|
+
replaceAll: 'Replace all',
|
|
228
|
+
findCount: (n: number) => `${n} result${n === 1 ? '' : 's'}`,
|
|
229
|
+
findClose: 'Close',
|
|
230
|
+
imageUploadFailed: 'Image upload failed',
|
|
231
|
+
imageAlignLeft: 'Align left',
|
|
232
|
+
imageAlignCenter: 'Center',
|
|
233
|
+
imageAlignFull: 'Full width',
|
|
234
|
+
aiTitle: 'AI',
|
|
235
|
+
aiAskPlaceholder: 'Ask about this doc…',
|
|
236
|
+
aiSend: 'Send',
|
|
237
|
+
aiThinking: 'Thinking…',
|
|
238
|
+
aiAccept: 'Accept',
|
|
239
|
+
aiReject: 'Reject',
|
|
240
|
+
aiImprovePrompt: 'Instruction (optional)…',
|
|
241
|
+
aiImproveRun: 'Run',
|
|
242
|
+
aiStreaming: 'Generating…',
|
|
243
|
+
aiRuntimeMissing: 'No AI Runtime injected; action unavailable',
|
|
244
|
+
// v1.1: tables
|
|
245
|
+
itemTable: 'Table',
|
|
246
|
+
itemTableDesc: 'Structured table with typed columns (⌘⌥S)',
|
|
247
|
+
itemTableSimple: 'Table (no header)',
|
|
248
|
+
itemTableSimpleDesc: 'Plain layout table without header (⌘⌥T)',
|
|
249
|
+
colTypeText: 'Text',
|
|
250
|
+
colTypeCheckbox: 'Checkbox',
|
|
251
|
+
colTypeSelect: 'Select tag',
|
|
252
|
+
colTypeMultiSelect: 'Multi-select',
|
|
253
|
+
colTypeNumber: 'Number',
|
|
254
|
+
colTypeDate: 'Date',
|
|
255
|
+
colTypeLink: 'Link',
|
|
256
|
+
colMenuTitle: 'Column',
|
|
257
|
+
colMenuSortAsc: 'Sort ascending',
|
|
258
|
+
colMenuSortDesc: 'Sort descending',
|
|
259
|
+
colMenuInsertLeft: 'Insert column left',
|
|
260
|
+
colMenuInsertRight: 'Insert column right',
|
|
261
|
+
colMenuDelete: 'Delete column',
|
|
262
|
+
colMenuType: 'Column type',
|
|
263
|
+
rowMenuTitle: 'Row',
|
|
264
|
+
rowMenuInsertAbove: 'Insert row above',
|
|
265
|
+
rowMenuInsertBelow: 'Insert row below',
|
|
266
|
+
rowMenuDelete: 'Delete row',
|
|
267
|
+
tableDelete: 'Delete table',
|
|
268
|
+
tableCopyCsv: 'Copy as CSV',
|
|
269
|
+
tableToggleHeader: 'Toggle header row',
|
|
270
|
+
cellEmpty: 'Empty',
|
|
271
|
+
// v1.1: history
|
|
272
|
+
itemHistory: 'Version history',
|
|
273
|
+
historyTitle: 'Version history',
|
|
274
|
+
historyEmpty: 'No snapshots yet (auto-captured when idle, or capture manually)',
|
|
275
|
+
historyCapture: 'Capture snapshot',
|
|
276
|
+
historyRestore: 'Restore this version',
|
|
277
|
+
historyCurrent: 'Current',
|
|
278
|
+
historyDiffAdded: 'Added',
|
|
279
|
+
historyDiffRemoved: 'Removed',
|
|
280
|
+
historyDiffChanged: 'Changed',
|
|
281
|
+
historyConfirmRestore: 'Restore this version? Current content will be replaced (undoable)',
|
|
282
|
+
// v1.1: comments
|
|
283
|
+
tooltipCommentV11: 'Comment',
|
|
284
|
+
commentTitle: 'Comments',
|
|
285
|
+
commentEmpty: 'No comments yet',
|
|
286
|
+
commentPlaceholder: 'Write a comment… (Shift+Enter for newline)',
|
|
287
|
+
commentSend: 'Send',
|
|
288
|
+
commentResolve: 'Resolve',
|
|
289
|
+
commentReopen: 'Reopen',
|
|
290
|
+
commentDelete: 'Delete',
|
|
291
|
+
commentResolvedBadge: 'Resolved',
|
|
292
|
+
commentCount: (n: number) => `${n} comment${n === 1 ? '' : 's'}`,
|
|
293
|
+
// v1.1: embed / toc / placeholder
|
|
294
|
+
itemEmbed: 'Embed',
|
|
295
|
+
itemEmbedDesc: 'Embed an external page (sandboxed iframe)',
|
|
296
|
+
itemToc: 'Table of contents',
|
|
297
|
+
itemTocDesc: 'Auto outline from H1–H4',
|
|
298
|
+
embedPlaceholder: 'Paste a link to embed…',
|
|
299
|
+
embedApply: 'Embed',
|
|
300
|
+
embedInvalid: 'Invalid URL',
|
|
301
|
+
embedOpen: 'Open original',
|
|
302
|
+
tocEmpty: 'No headings yet — add H1–H4 and they appear here',
|
|
303
|
+
placeholderText: 'to fill in',
|
|
304
|
+
placeholderPerson: 'assignee',
|
|
305
|
+
placeholderDate: 'due date',
|
|
306
|
+
// v1.1: block context menu
|
|
307
|
+
menuCopyAnchor: 'Copy anchor link',
|
|
308
|
+
menuCopyBlockId: 'Copy block ID',
|
|
309
|
+
menuDeleteBlock: 'Delete block',
|
|
310
|
+
},
|
|
311
|
+
} as const
|
|
312
|
+
|
|
313
|
+
export type TesseraMessageKey = keyof typeof tesseraMessages['zh-CN']
|
|
314
|
+
|
|
315
|
+
export type TesseraTranslator = (key: TesseraMessageKey) => string
|
|
316
|
+
|
|
317
|
+
export function createTesseraT(locale: TesseraLocale = 'zh-CN'): TesseraTranslator {
|
|
318
|
+
const table = tesseraMessages[locale] ?? tesseraMessages['zh-CN']
|
|
319
|
+
return key => (table as Record<string, string | ((n: number) => string)>)[key] as string ?? key
|
|
320
|
+
}
|