@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/src/preset.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { Extensions } from '@tiptap/core'
2
+ import StarterKit from '@tiptap/starter-kit'
3
+ import { TaskList, TaskItem } from '@tiptap/extension-list'
4
+ import { TextStyle } from '@tiptap/extension-text-style'
5
+ import { Color } from '@tiptap/extension-color'
6
+ import { Highlight } from '@tiptap/extension-highlight'
7
+ import { UniqueID } from '@tiptap/extension-unique-id'
8
+ import { Placeholder } from '@tiptap/extensions'
9
+ import { Hint } from './nodes/hint'
10
+ import { Collapsible, CollapsibleSummary, CollapsibleContent } from './nodes/collapsible'
11
+ import { ImageBlock } from './nodes/image'
12
+ import { AiTable, AiTableRow, AiTableCell, AiTableHeader } from './nodes/table'
13
+ import { EmbedBlock } from './nodes/embed'
14
+ import { TocBlock } from './nodes/toc'
15
+ import { AiAttribution } from './marks/ai'
16
+ import { CommentMark, CommentCommands } from './marks/comment'
17
+ import { PlaceholderMark, PlaceholderCommands } from './marks/placeholder'
18
+ import { TesseraInputRules } from './extensions/input-rules'
19
+ import { TesseraShortcuts } from './extensions/shortcuts'
20
+ import { TesseraFindReplace } from './extensions/find-replace'
21
+ import { SlashMenu } from './extensions/slash'
22
+ import { EmojiMenu } from './extensions/emoji'
23
+ import { TesseraHistory } from './extensions/history'
24
+ import { BlockContextMenu } from './extensions/context-menu'
25
+ import { TesseraGallery } from './extensions/gallery'
26
+ import { TesseraMetrics } from './extensions/metrics'
27
+ import { TesseraWordPaste } from './extensions/word-paste'
28
+ import { TesseraServices } from './services'
29
+ import { createTesseraT, type TesseraLocale } from './i18n'
30
+
31
+ export interface TesseraPresetOptions {
32
+ locale?: TesseraLocale
33
+ /** v1.1 history auto-capture idle window (playground uses a short one) */
34
+ historyIdleMs?: number
35
+ }
36
+
37
+ /** Node types that receive stable block IDs (write-back protocol basis). */
38
+ export const ID_BLOCK_TYPES = [
39
+ 'paragraph',
40
+ 'heading',
41
+ 'bulletList',
42
+ 'orderedList',
43
+ 'taskList',
44
+ 'listItem',
45
+ 'taskItem',
46
+ 'blockquote',
47
+ 'codeBlock',
48
+ 'horizontalRule',
49
+ 'hint',
50
+ 'collapsible',
51
+ 'imageBlock',
52
+ 'table',
53
+ 'tableRow',
54
+ 'embedBlock',
55
+ 'tocBlock',
56
+ ]
57
+
58
+ /**
59
+ * The Tessera extension preset: framework-agnostic, UI-free. The binding layers
60
+ * rendering (slash menu renderer, node views) on top of this.
61
+ */
62
+ export function createTesseraExtensions(options: TesseraPresetOptions = {}): Extensions {
63
+ const t = createTesseraT(options.locale ?? 'zh-CN')
64
+
65
+ return [
66
+ StarterKit.configure({
67
+ heading: { levels: [1, 2, 3, 4] },
68
+ link: {
69
+ openOnClick: false,
70
+ autolink: true,
71
+ defaultProtocol: 'https',
72
+ },
73
+ // undoRedo keeps defaults (newGroupDelay 500ms): streaming AI chunks
74
+ // arriving faster than that already merge into one undo step.
75
+ }),
76
+ TextStyle,
77
+ Color,
78
+ Highlight.configure({ multicolor: true }),
79
+ TaskList,
80
+ TaskItem.configure({ nested: true }),
81
+ Hint,
82
+ Collapsible,
83
+ CollapsibleSummary,
84
+ CollapsibleContent,
85
+ ImageBlock,
86
+ AiTable,
87
+ AiTableRow,
88
+ AiTableCell,
89
+ AiTableHeader,
90
+ EmbedBlock,
91
+ TocBlock,
92
+ AiAttribution,
93
+ CommentMark,
94
+ CommentCommands,
95
+ PlaceholderMark,
96
+ PlaceholderCommands,
97
+ UniqueID.configure({
98
+ types: ID_BLOCK_TYPES,
99
+ attributeName: 'id',
100
+ }),
101
+ Placeholder.configure({
102
+ placeholder: ({ node }) => (node.type.name === 'paragraph' ? t('placeholderEmpty') : ''),
103
+ showOnlyWhenEditable: true,
104
+ }),
105
+ TesseraInputRules,
106
+ TesseraShortcuts,
107
+ TesseraFindReplace,
108
+ TesseraHistory.configure({ idleMs: options.historyIdleMs }),
109
+ BlockContextMenu,
110
+ SlashMenu.configure({ locale: options.locale ?? 'zh-CN' }),
111
+ EmojiMenu,
112
+ TesseraGallery,
113
+ TesseraMetrics,
114
+ TesseraWordPaste,
115
+ TesseraServices,
116
+ ]
117
+ }
@@ -0,0 +1,103 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import type { JSONContent } from '@tiptap/core'
3
+
4
+ /**
5
+ * Injected services (ADR-0001 family): the component family never performs
6
+ * network or storage I/O itself. Hosts provide implementations through the
7
+ * binding; the editor reads them from `editor.storage.tesseraServices`.
8
+ */
9
+
10
+ export interface UploadedAsset {
11
+ url: string
12
+ name?: string
13
+ mime?: string
14
+ }
15
+
16
+ export interface UploadService {
17
+ uploadImage(file: File | Blob): Promise<UploadedAsset>
18
+ uploadFile?(file: File | Blob): Promise<UploadedAsset>
19
+ }
20
+
21
+ /** v1.1: persistent version history storage (snapshots keyed by time). */
22
+ export interface DocSnapshot {
23
+ id: string
24
+ ts: number
25
+ doc: JSONContent
26
+ label?: string
27
+ }
28
+
29
+ export interface StorageService {
30
+ saveSnapshot(snapshot: DocSnapshot): Promise<void>
31
+ listSnapshots(): Promise<DocSnapshot[]>
32
+ deleteSnapshot?(id: string): Promise<void>
33
+ }
34
+
35
+ /** v1.1: inline comments. */
36
+ export interface CommentEntry {
37
+ id: string
38
+ authorId: string
39
+ authorName: string
40
+ text: string
41
+ ts: number
42
+ }
43
+
44
+ export interface CommentThread {
45
+ id: string
46
+ quote: string
47
+ resolved: boolean
48
+ createdAt: number
49
+ entries: CommentEntry[]
50
+ }
51
+
52
+ export interface CommentStore {
53
+ list(): Promise<CommentThread[]>
54
+ upsert(thread: CommentThread): Promise<void>
55
+ remove(id: string): Promise<void>
56
+ }
57
+
58
+ /** v1.1: who is editing (author of comments / history labels). */
59
+ export interface IdentityService {
60
+ getCurrentUser(): { id: string; name: string } | null
61
+ }
62
+
63
+ export interface TesseraServicesStorage {
64
+ upload?: UploadService
65
+ storage?: StorageService
66
+ comments?: CommentStore
67
+ identity?: IdentityService
68
+ }
69
+
70
+ export const TesseraServices = Extension.create({
71
+ name: 'tesseraServices',
72
+
73
+ addStorage() {
74
+ return {
75
+ upload: undefined,
76
+ storage: undefined,
77
+ comments: undefined,
78
+ identity: undefined,
79
+ } satisfies TesseraServicesStorage
80
+ },
81
+ })
82
+
83
+ type ServicesEditor = { storage: unknown }
84
+
85
+ function servicesBag(editor: ServicesEditor): TesseraServicesStorage | undefined {
86
+ return (editor.storage as Record<string, TesseraServicesStorage | undefined>).tesseraServices
87
+ }
88
+
89
+ export function getUploadService(editor: ServicesEditor): UploadService | undefined {
90
+ return servicesBag(editor)?.upload
91
+ }
92
+
93
+ export function getStorageService(editor: ServicesEditor): StorageService | undefined {
94
+ return servicesBag(editor)?.storage
95
+ }
96
+
97
+ export function getCommentStore(editor: ServicesEditor): CommentStore | undefined {
98
+ return servicesBag(editor)?.comments
99
+ }
100
+
101
+ export function getIdentityService(editor: ServicesEditor): IdentityService | undefined {
102
+ return servicesBag(editor)?.identity
103
+ }