@puredesktop/platform-editor 1.0.0-beta.1

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.
Files changed (45) hide show
  1. package/package.json +63 -0
  2. package/src/CollapsePanel.tsx +35 -0
  3. package/src/DocumentDiffView.tsx +130 -0
  4. package/src/DocumentEditor.test.ts +118 -0
  5. package/src/DocumentEditor.tsx +2631 -0
  6. package/src/alignedLineDiff.test.ts +52 -0
  7. package/src/alignedLineDiff.ts +67 -0
  8. package/src/collectionAssetSrc.ts +2 -0
  9. package/src/commentUtils.test.ts +350 -0
  10. package/src/commentUtils.ts +546 -0
  11. package/src/constants/toolKeys.ts +14 -0
  12. package/src/contentFormat.test.ts +27 -0
  13. package/src/contentFormat.ts +18 -0
  14. package/src/editorExtensions.ts +155 -0
  15. package/src/extensions/appliedChangeMark.ts +115 -0
  16. package/src/extensions/autoReviewPrompts.test.ts +529 -0
  17. package/src/extensions/autoReviewPrompts.ts +1442 -0
  18. package/src/extensions/collectionImage.ts +26 -0
  19. package/src/extensions/collectionImagePaste.ts +215 -0
  20. package/src/extensions/commentMark.ts +223 -0
  21. package/src/extensions/documentAssetIdentity.ts +54 -0
  22. package/src/extensions/figure.ts +92 -0
  23. package/src/extensions/footnote.ts +186 -0
  24. package/src/extensions/indexMarker.ts +88 -0
  25. package/src/extensions/mathEditing.ts +239 -0
  26. package/src/extensions/mermaidBlock.tsx +297 -0
  27. package/src/extensions/slashCommands.test.ts +170 -0
  28. package/src/extensions/slashCommands.ts +746 -0
  29. package/src/extensions/smartTypography.test.ts +74 -0
  30. package/src/extensions/smartTypography.ts +120 -0
  31. package/src/index.ts +137 -0
  32. package/src/insertCollectionImage.test.ts +60 -0
  33. package/src/insertCollectionImage.ts +63 -0
  34. package/src/insertInlineAssetKind.test.ts +67 -0
  35. package/src/insertInlineAssetKind.ts +49 -0
  36. package/src/mermaidPreview.test.ts +54 -0
  37. package/src/mermaidPreview.ts +115 -0
  38. package/src/styled.ts +676 -0
  39. package/src/toolbar/ToolBtn.tsx +63 -0
  40. package/src/toolbar/Toolbar.test.tsx +325 -0
  41. package/src/toolbar/Toolbar.tsx +624 -0
  42. package/src/toolbar/groups/HeadingGroup.tsx +153 -0
  43. package/src/toolbar/overlayTypes.ts +28 -0
  44. package/src/useEditorExtensions.ts +22 -0
  45. package/src/utils/markdownUtils.ts +331 -0
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@puredesktop/platform-editor",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "TipTap editor package for PureScience plugin apps",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Nikau-Dev/ps-suite.git",
9
+ "directory": "packages/editor"
10
+ },
11
+ "publishConfig": {
12
+ "access": "restricted",
13
+ "registry": "https://registry.npmjs.org"
14
+ },
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./*": "./src/*"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@ant-design/icons": "^6.0.0",
28
+ "@tiptap/extension-character-count": "^3.23.6",
29
+ "@tiptap/extension-code-block-lowlight": "^3.23.6",
30
+ "@tiptap/extension-highlight": "^3.0.0",
31
+ "@tiptap/extension-image": "^3.23.1",
32
+ "@tiptap/extension-mathematics": "^3.23.6",
33
+ "@tiptap/extension-placeholder": "^3.0.0",
34
+ "@tiptap/extension-subscript": "^3.23.6",
35
+ "@tiptap/extension-superscript": "^3.23.6",
36
+ "@tiptap/extension-table": "^3.0.0",
37
+ "@tiptap/extension-task-item": "^3.0.0",
38
+ "@tiptap/extension-task-list": "^3.0.0",
39
+ "@tiptap/extension-text-align": "^3.0.0",
40
+ "@tiptap/pm": "^3.0.0",
41
+ "@tiptap/react": "^3.0.0",
42
+ "@tiptap/starter-kit": "^3.0.0",
43
+ "katex": "^0.16.22",
44
+ "lowlight": "^3.3.0",
45
+ "marked": "^17.0.0",
46
+ "diff": "^7.0.0",
47
+ "mermaid": "^11.15.0",
48
+ "styled-components": "^6.1.18",
49
+ "turndown": "^7.2.0"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.0.0",
53
+ "react-dom": "^19.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^19.1.6",
57
+ "@types/diff": "^7.0.0",
58
+ "@types/turndown": "^5.0.5",
59
+ "react": "^19.1.0",
60
+ "react-dom": "^19.1.0",
61
+ "vitest": "^3.2.4"
62
+ }
63
+ }
@@ -0,0 +1,35 @@
1
+ import { type ReactNode } from 'react'
2
+ import { styled } from 'styled-components'
3
+
4
+ export interface CollapsePanelProps {
5
+ open: boolean
6
+ children: ReactNode
7
+ className?: string
8
+ }
9
+
10
+ export function CollapsePanel({
11
+ open,
12
+ children,
13
+ className,
14
+ }: CollapsePanelProps): React.ReactElement {
15
+ return (
16
+ <StyledPanel data-open={open} className={className}>
17
+ <StyledInner>{children}</StyledInner>
18
+ </StyledPanel>
19
+ )
20
+ }
21
+
22
+ const StyledPanel = styled.div`
23
+ display: grid;
24
+ grid-template-rows: 0fr;
25
+ transition: grid-template-rows 0.2s ease;
26
+ overflow: hidden;
27
+
28
+ &[data-open='true'] {
29
+ grid-template-rows: 1fr;
30
+ }
31
+ `
32
+
33
+ const StyledInner = styled.div`
34
+ min-height: 0;
35
+ `
@@ -0,0 +1,130 @@
1
+ import { useMemo, type ReactElement } from 'react'
2
+ import { styled } from 'styled-components'
3
+ import {
4
+ buildAlignedLineDiff,
5
+ type AlignedDiffLine,
6
+ type AlignedDiffLineKind,
7
+ } from './alignedLineDiff.js'
8
+
9
+ export interface DocumentDiffViewProps {
10
+ left: string
11
+ right: string
12
+ leftLabel?: string
13
+ rightLabel?: string
14
+ }
15
+
16
+ export function DocumentDiffView({
17
+ left,
18
+ right,
19
+ leftLabel = 'Yours',
20
+ rightLabel = 'On disk',
21
+ }: DocumentDiffViewProps): ReactElement {
22
+ const { leftLines, rightLines } = useMemo(
23
+ () => buildAlignedLineDiff(left, right),
24
+ [left, right],
25
+ )
26
+
27
+ return (
28
+ <StyledRoot>
29
+ <StyledHeader>
30
+ <StyledColumnHeader $tone="left">{leftLabel}</StyledColumnHeader>
31
+ <StyledColumnHeader $tone="right">{rightLabel}</StyledColumnHeader>
32
+ </StyledHeader>
33
+ <StyledGrid>
34
+ <StyledColumn>
35
+ {leftLines.map((line, index) => (
36
+ <DiffLineRow key={`left-${index}`} line={line} />
37
+ ))}
38
+ </StyledColumn>
39
+ <StyledColumn>
40
+ {rightLines.map((line, index) => (
41
+ <DiffLineRow key={`right-${index}`} line={line} />
42
+ ))}
43
+ </StyledColumn>
44
+ </StyledGrid>
45
+ </StyledRoot>
46
+ )
47
+ }
48
+
49
+ function DiffLineRow({ line }: { line: AlignedDiffLine }): ReactElement {
50
+ return (
51
+ <StyledLine $kind={line.kind}>
52
+ <StyledLineNo>{line.lineNumber ?? ''}</StyledLineNo>
53
+ <StyledLineText>{line.text || '\u00a0'}</StyledLineText>
54
+ </StyledLine>
55
+ )
56
+ }
57
+
58
+ //#region styled-components
59
+
60
+ const StyledRoot = styled.div`
61
+ display: flex;
62
+ flex-direction: column;
63
+ min-height: 0;
64
+ `
65
+
66
+ const StyledHeader = styled.div`
67
+ display: grid;
68
+ grid-template-columns: 1fr 1fr;
69
+ gap: var(--platform-spacing-sm);
70
+ margin-bottom: var(--platform-spacing-sm);
71
+ `
72
+
73
+ const StyledColumnHeader = styled.div<{ $tone: 'left' | 'right' }>`
74
+ padding: var(--platform-spacing-xs) var(--platform-spacing-sm);
75
+ font-size: var(--platform-typography-font-size-sm);
76
+ font-weight: var(--platform-typography-font-weight-medium);
77
+ color: ${({ $tone }) =>
78
+ $tone === 'left'
79
+ ? 'var(--platform-colors-accent)'
80
+ : 'var(--platform-colors-text-secondary)'};
81
+ `
82
+
83
+ const StyledGrid = styled.div`
84
+ display: grid;
85
+ grid-template-columns: 1fr 1fr;
86
+ gap: var(--platform-spacing-sm);
87
+ max-height: 60vh;
88
+ overflow: auto;
89
+ border: 1px solid var(--platform-colors-border);
90
+ border-radius: 0;
91
+ background: var(--platform-colors-bg);
92
+ `
93
+
94
+ const StyledColumn = styled.div`
95
+ font-family: var(--platform-typography-font-family-mono);
96
+ font-size: 12px;
97
+ line-height: 1.45;
98
+ padding: var(--platform-spacing-xs) 0;
99
+ `
100
+
101
+ const StyledLine = styled.div<{ $kind: AlignedDiffLineKind }>`
102
+ display: grid;
103
+ grid-template-columns: 36px 1fr;
104
+ background: ${({ $kind }) =>
105
+ $kind === 'added'
106
+ ? 'rgb(from var(--platform-colors-success) r g b / 14%)'
107
+ : $kind === 'removed'
108
+ ? 'rgb(from var(--platform-colors-danger) r g b / 14%)'
109
+ : 'transparent'};
110
+ color: ${({ $kind }) =>
111
+ $kind === 'pad'
112
+ ? 'var(--platform-colors-text-disabled)'
113
+ : 'var(--platform-colors-text)'};
114
+ min-height: 17px;
115
+ `
116
+
117
+ const StyledLineNo = styled.div`
118
+ color: var(--platform-colors-text-disabled);
119
+ text-align: right;
120
+ padding-right: 8px;
121
+ user-select: none;
122
+ `
123
+
124
+ const StyledLineText = styled.div`
125
+ white-space: pre-wrap;
126
+ word-break: break-word;
127
+ padding-right: var(--platform-spacing-sm);
128
+ `
129
+
130
+ //#endregion
@@ -0,0 +1,118 @@
1
+ // @vitest-environment happy-dom
2
+ import type { Editor } from '@tiptap/core'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import {
5
+ isReviewRailCommentType,
6
+ isSelectionAnnotatable,
7
+ progressiveAutoReviewBatches,
8
+ } from './DocumentEditor.js'
9
+
10
+ function mockEditorForNodes(
11
+ startNode: Node,
12
+ endNode: Node,
13
+ options: { editable?: boolean; destroyed?: boolean } = {},
14
+ ): Editor {
15
+ return {
16
+ isDestroyed: options.destroyed ?? false,
17
+ isEditable: options.editable ?? true,
18
+ view: {
19
+ domAtPos: vi.fn((pos: number) => ({
20
+ node: pos <= 1 ? startNode : endNode,
21
+ })),
22
+ },
23
+ } as unknown as Editor
24
+ }
25
+
26
+ describe('DocumentEditor annotation selection rules', () => {
27
+ it('shows rail badges for editorial review objects', () => {
28
+ expect(isReviewRailCommentType('copyedit')).toBe(true)
29
+ expect(isReviewRailCommentType('voice')).toBe(true)
30
+ expect(isReviewRailCommentType('claim')).toBe(true)
31
+ expect(isReviewRailCommentType('fact-check')).toBe(true)
32
+ expect(isReviewRailCommentType('general')).toBe(false)
33
+ })
34
+
35
+ it('allows non-empty selections in editable manuscript content', () => {
36
+ const paragraph = document.createElement('p')
37
+ const start = document.createTextNode('Selected')
38
+ const end = document.createTextNode(' text')
39
+ paragraph.append(start, end)
40
+ const editor = mockEditorForNodes(start, end)
41
+
42
+ expect(isSelectionAnnotatable(editor, 1, 4)).toBe(true)
43
+ })
44
+
45
+ it('does not offer annotation creation inside protected generated content', () => {
46
+ const referenceEntry = document.createElement('li')
47
+ referenceEntry.setAttribute('data-pm-reference-entry', 'true')
48
+ referenceEntry.setAttribute('contenteditable', 'false')
49
+ const start = document.createTextNode('Generated')
50
+ const end = document.createTextNode(' reference')
51
+ referenceEntry.append(start, end)
52
+ const editor = mockEditorForNodes(start, end)
53
+
54
+ expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
55
+ })
56
+
57
+ it('does not offer annotation creation inside citation atoms', () => {
58
+ const citation = document.createElement('cite')
59
+ citation.className = 'ps-citation'
60
+ const start = document.createTextNode('[1')
61
+ const end = document.createTextNode(']')
62
+ citation.append(start, end)
63
+ const editor = mockEditorForNodes(start, end)
64
+
65
+ expect(isSelectionAnnotatable(editor, 1, 4)).toBe(false)
66
+ })
67
+
68
+ it('does not offer annotation creation for collapsed or read-only selections', () => {
69
+ const text = document.createTextNode('Text')
70
+
71
+ expect(isSelectionAnnotatable(mockEditorForNodes(text, text), 2, 2)).toBe(
72
+ false,
73
+ )
74
+ expect(
75
+ isSelectionAnnotatable(
76
+ mockEditorForNodes(text, text, { editable: false }),
77
+ 1,
78
+ 4,
79
+ ),
80
+ ).toBe(false)
81
+ })
82
+ })
83
+
84
+ describe('DocumentEditor progressive editorial review', () => {
85
+ it('front-loads small batches before larger background batches', () => {
86
+ const paragraphs = Array.from({ length: 31 }, (_, index) => ({
87
+ id: `p-${index + 1}`,
88
+ text: `Paragraph ${index + 1}`,
89
+ from: index + 1,
90
+ pos: index + 1,
91
+ to: index + 2,
92
+ }))
93
+
94
+ const batches = progressiveAutoReviewBatches(paragraphs)
95
+
96
+ expect(batches.map(batch => batch.length)).toEqual([3, 3, 3, 3, 12, 7])
97
+ expect(batches[0]?.map(paragraph => paragraph.id)).toEqual([
98
+ 'p-1',
99
+ 'p-2',
100
+ 'p-3',
101
+ ])
102
+ expect(batches[4]?.[0]?.id).toBe('p-13')
103
+ })
104
+
105
+ it('keeps short documents in small startup batches only', () => {
106
+ const paragraphs = Array.from({ length: 5 }, (_, index) => ({
107
+ id: `p-${index + 1}`,
108
+ text: `Paragraph ${index + 1}`,
109
+ from: index + 1,
110
+ pos: index + 1,
111
+ to: index + 2,
112
+ }))
113
+
114
+ expect(
115
+ progressiveAutoReviewBatches(paragraphs).map(batch => batch.length),
116
+ ).toEqual([3, 2])
117
+ })
118
+ })