@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.
@@ -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
+ }