@tessera-editor/core 0.1.0 → 0.2.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.
@@ -1,133 +0,0 @@
1
- import { Extension } from '@tiptap/core'
2
- import type { JSONContent } from '@tiptap/core'
3
- import { Plugin, PluginKey } from '@tiptap/pm/state'
4
- import { getStorageService, type DocSnapshot } from '../services'
5
-
6
- /**
7
- * Version history capture (v1.1): snapshots the canonical JSON into the
8
- * injected StorageService. Auto-capture fires after `idleMs` of inactivity
9
- * (Slite: 5 minutes); manual capture is always available as a command.
10
- */
11
-
12
- export interface HistorySnapshotOptions {
13
- /** idle window before auto-capture (default 5 minutes) */
14
- idleMs?: number
15
- /** minimum ms between two auto-captures (default 60s) */
16
- minIntervalMs?: number
17
- /** snapshot label source */
18
- label?: () => string | undefined
19
- }
20
-
21
- declare module '@tiptap/core' {
22
- interface Commands<ReturnType> {
23
- tesseraHistory: {
24
- captureSnapshot: (label?: string) => ReturnType
25
- }
26
- }
27
- interface EditorEvents {
28
- 'tessera:snapshotSaved': { snapshot: DocSnapshot }
29
- }
30
- }
31
-
32
- export const historyKey = new PluginKey('tesseraHistory')
33
-
34
- export const TesseraHistory = Extension.create<HistorySnapshotOptions>({
35
- name: 'tesseraHistory',
36
-
37
- addOptions() {
38
- return {
39
- idleMs: 5 * 60 * 1000,
40
- minIntervalMs: 60 * 1000,
41
- label: undefined,
42
- }
43
- },
44
-
45
- addCommands() {
46
- return {
47
- captureSnapshot:
48
- (label?: string) =>
49
- ({ editor, state }) => {
50
- const storage = getStorageService(editor)
51
- if (!storage) {
52
- return false
53
- }
54
- const doc = editor.getJSON() as JSONContent
55
- const snapshot: DocSnapshot = {
56
- id: `snap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
57
- ts: Date.now(),
58
- doc,
59
- label: label ?? this.options.label?.(),
60
- }
61
- void storage.saveSnapshot(snapshot).then(() => {
62
- editor.emit('tessera:snapshotSaved', { snapshot })
63
- })
64
- const tr = state.tr.setMeta(historyKey, { type: 'captured', ts: snapshot.ts })
65
- editor.view.dispatch(tr)
66
- return true
67
- },
68
- }
69
- },
70
-
71
- addProseMirrorPlugins() {
72
- const editor = this.editor
73
- const options = this.options
74
- return [
75
- new Plugin({
76
- key: historyKey,
77
- state: {
78
- init: () => ({ lastChange: 0, lastCapture: 0, timer: null as ReturnType<typeof setTimeout> | null }),
79
- apply: (tr, prev) => {
80
- const meta = tr.getMeta(historyKey) as { type: string; ts?: number } | undefined
81
- if (meta?.type === 'captured') {
82
- return { ...prev, lastCapture: meta.ts ?? Date.now() }
83
- }
84
- if (!tr.docChanged) {
85
- return prev
86
- }
87
- return { ...prev, lastChange: Date.now() }
88
- },
89
- },
90
- view() {
91
- return {
92
- update: (_view, prevState) => {
93
- const before = historyKey.getState(prevState)
94
- const after = historyKey.getState(editor.state)
95
- if (!before || !after || after.lastChange === before.lastChange) {
96
- return
97
- }
98
- const storage = getStorageService(editor)
99
- if (!storage) {
100
- return
101
- }
102
- const s = historyKey.getState(editor.state)
103
- if (s?.timer) {
104
- clearTimeout(s.timer)
105
- }
106
- const timer = setTimeout(() => {
107
- const now = Date.now()
108
- const cur = historyKey.getState(editor.state)
109
- if (!cur || now - cur.lastChange < options.idleMs! - 50) {
110
- return
111
- }
112
- if (now - cur.lastCapture < options.minIntervalMs!) {
113
- return
114
- }
115
- editor.commands.captureSnapshot()
116
- }, options.idleMs!)
117
- const st = historyKey.getState(editor.state)
118
- if (st) {
119
- st.timer = timer
120
- }
121
- },
122
- destroy() {
123
- const s = historyKey.getState(editor.state)
124
- if (s?.timer) {
125
- clearTimeout(s.timer)
126
- }
127
- },
128
- }
129
- },
130
- }),
131
- ]
132
- },
133
- })
@@ -1,63 +0,0 @@
1
- import { Mark, mergeAttributes, Extension } from '@tiptap/core'
2
-
3
- /**
4
- * Placeholder mark (v1.1, Slite's 占位符): dashed-underline token marking a
5
- * spot to fill in later — text / person / date / doc-link. ⌘⌥P toggles it on
6
- * a selection; slash items insert ready-made tokens.
7
- */
8
- export type PlaceholderKind = 'text' | 'person' | 'date' | 'doc'
9
-
10
- export const PlaceholderMark = Mark.create({
11
- name: 'tesseraPlaceholder',
12
-
13
- addAttributes() {
14
- return {
15
- kind: {
16
- default: 'text' as PlaceholderKind,
17
- parseHTML: element => (element.getAttribute('data-kind') as PlaceholderKind) ?? 'text',
18
- renderHTML: attributes => ({ 'data-kind': String(attributes.kind) }),
19
- },
20
- }
21
- },
22
-
23
- parseHTML() {
24
- return [{ tag: 'span[data-type="tessera-placeholder"]' }]
25
- },
26
-
27
- renderHTML({ HTMLAttributes }) {
28
- return ['span', mergeAttributes({ 'data-type': 'tessera-placeholder' }, HTMLAttributes)]
29
- },
30
- })
31
-
32
- declare module '@tiptap/core' {
33
- interface Commands<ReturnType> {
34
- tesseraPlaceholder: {
35
- togglePlaceholderMark: (kind?: PlaceholderKind) => ReturnType
36
- /** Insert a placeholder token at the caret (selection replaced). */
37
- insertPlaceholderToken: (kind: PlaceholderKind, label: string) => ReturnType
38
- }
39
- }
40
- }
41
-
42
- export const PlaceholderCommands = Extension.create({
43
- name: 'tesseraPlaceholderCommands',
44
-
45
- addCommands() {
46
- return {
47
- togglePlaceholderMark:
48
- (kind: PlaceholderKind = 'text') =>
49
- ({ commands }) =>
50
- commands.toggleMark('tesseraPlaceholder', { kind }),
51
- insertPlaceholderToken:
52
- (kind: PlaceholderKind, label: string) =>
53
- ({ chain }) =>
54
- chain()
55
- .insertContent({
56
- type: 'text',
57
- text: label,
58
- marks: [{ type: 'tesseraPlaceholder', attrs: { kind } }],
59
- })
60
- .run(),
61
- }
62
- },
63
- })