@lalternative/editor 0.1.0 → 0.4.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/dist/diff-uFiP8hD5.d.ts +52 -0
- package/dist/editor.css +232 -0
- package/dist/index.d.ts +132 -35
- package/dist/index.js +323 -1
- package/dist/index.js.map +1 -1
- package/dist/revisions.d.ts +2 -2
- package/package.json +6 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RichNode is one node of a TipTap document.
|
|
3
|
+
*
|
|
4
|
+
* The index signature is what lets a document cross the boundary with an
|
|
5
|
+
* editor handle typed as a plain object, which is how TipTap hands its JSON
|
|
6
|
+
* back. Without it every call site pays for the mismatch in casts.
|
|
7
|
+
*/
|
|
8
|
+
interface RichNode {
|
|
9
|
+
type?: string;
|
|
10
|
+
attrs?: Record<string, unknown>;
|
|
11
|
+
content?: RichNode[];
|
|
12
|
+
text?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
type RichDoc = RichNode;
|
|
16
|
+
interface BlockShape {
|
|
17
|
+
type: "paragraph" | "heading";
|
|
18
|
+
level?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* blocksFromDoc flattens a TipTap document to the text of its top-level nodes.
|
|
22
|
+
*
|
|
23
|
+
* Marks are dropped rather than encoded: the diff compares prose, and a bold
|
|
24
|
+
* span that moved inside an otherwise identical sentence is not a change worth
|
|
25
|
+
* asking the reviewer about.
|
|
26
|
+
*/
|
|
27
|
+
declare function blocksFromDoc(doc: RichDoc): string[];
|
|
28
|
+
/** shapesFromDoc records what each block was, so a revision can be rebuilt
|
|
29
|
+
* without turning every heading into a paragraph. */
|
|
30
|
+
declare function shapesFromDoc(doc: RichDoc): BlockShape[];
|
|
31
|
+
declare function docFromBlocks(blocks: string[], shapes?: BlockShape[]): RichDoc;
|
|
32
|
+
|
|
33
|
+
type ChangeKind = "keep" | "insert" | "delete" | "replace";
|
|
34
|
+
interface BlockChange {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: ChangeKind;
|
|
37
|
+
before: string;
|
|
38
|
+
after: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* diffBlocks compares two documents one block at a time.
|
|
42
|
+
*
|
|
43
|
+
* The block is the unit because it is the unit a reviewer accepts or rejects:
|
|
44
|
+
* a finer diff turns one editorial decision into a dozen clicks, and a coarser
|
|
45
|
+
* one makes rejecting a single sentence cost the whole section.
|
|
46
|
+
*
|
|
47
|
+
* Accepting every change rebuilds `after` exactly; rejecting every change
|
|
48
|
+
* rebuilds `before` exactly. The UI relies on both.
|
|
49
|
+
*/
|
|
50
|
+
declare function diffBlocks(before: string[], after: string[]): BlockChange[];
|
|
51
|
+
|
|
52
|
+
export { type BlockChange as B, type ChangeKind as C, type RichDoc as R, type BlockShape as a, type RichNode as b, blocksFromDoc as c, diffBlocks as d, docFromBlocks as e, shapesFromDoc as s };
|
package/dist/editor.css
CHANGED
|
@@ -5,6 +5,179 @@
|
|
|
5
5
|
--lalt-editor-insert-bg: rgb(29 78 216 / 0.08);
|
|
6
6
|
--lalt-editor-delete: #b91c1c;
|
|
7
7
|
--lalt-editor-delete-bg: rgb(185 28 28 / 0.06);
|
|
8
|
+
--lalt-editor-surface: #ffffff;
|
|
9
|
+
--lalt-editor-text: #111827;
|
|
10
|
+
--lalt-editor-border: rgb(0 0 0 / 0.1);
|
|
11
|
+
--lalt-editor-hover: rgb(0 0 0 / 0.06);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Defaults only, and only for a host with no theme of its own: an app that
|
|
15
|
+
toggles dark by class must override these tokens instead, or the toolbar
|
|
16
|
+
follows the system while the page follows the class. */
|
|
17
|
+
@media (prefers-color-scheme: dark) {
|
|
18
|
+
:root {
|
|
19
|
+
--lalt-editor-insert: #93c5fd;
|
|
20
|
+
--lalt-editor-insert-bg: rgb(147 197 253 / 0.12);
|
|
21
|
+
--lalt-editor-delete: #fca5a5;
|
|
22
|
+
--lalt-editor-delete-bg: rgb(252 165 165 / 0.1);
|
|
23
|
+
--lalt-editor-surface: #1f2937;
|
|
24
|
+
--lalt-editor-text: #f3f4f6;
|
|
25
|
+
--lalt-editor-border: rgb(255 255 255 / 0.14);
|
|
26
|
+
--lalt-editor-hover: rgb(255 255 255 / 0.08);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Docked to the bottom of the screen by default, floating beside the selection
|
|
31
|
+
only where there is a pointer and room for it.
|
|
32
|
+
|
|
33
|
+
A bar floating next to a selection is the worst place for it on a phone: the
|
|
34
|
+
finger that made the selection covers it, and iOS draws its own callout in
|
|
35
|
+
the same spot. Bottom-docked also puts it in thumb reach — and unlike a
|
|
36
|
+
floating bar, it cannot be pushed off-screen by a selection near an edge. */
|
|
37
|
+
.lalt-toolbar {
|
|
38
|
+
position: fixed;
|
|
39
|
+
left: 0;
|
|
40
|
+
right: 0;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
z-index: 100;
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 0.25rem;
|
|
46
|
+
padding: 0.5rem;
|
|
47
|
+
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
|
|
48
|
+
background: var(--lalt-editor-surface, #ffffff);
|
|
49
|
+
border-top: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
50
|
+
box-shadow: 0 -4px 16px rgb(0 0 0 / 0.12);
|
|
51
|
+
color: var(--lalt-editor-text, #111827);
|
|
52
|
+
user-select: none;
|
|
53
|
+
overflow-x: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (min-width: 640px) {
|
|
57
|
+
.lalt-toolbar {
|
|
58
|
+
left: var(--lalt-toolbar-left, 0);
|
|
59
|
+
top: var(--lalt-toolbar-top, 0);
|
|
60
|
+
right: auto;
|
|
61
|
+
bottom: auto;
|
|
62
|
+
gap: 0.125rem;
|
|
63
|
+
padding: 0.25rem;
|
|
64
|
+
padding-bottom: 0.25rem;
|
|
65
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
66
|
+
border-radius: 0.5rem;
|
|
67
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
|
|
68
|
+
overflow-x: visible;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.lalt-toolbar__button {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
gap: 0.375rem;
|
|
77
|
+
/* 44px: what a finger needs. Shrinks to a compact control only where a
|
|
78
|
+
pointer is doing the aiming. */
|
|
79
|
+
min-height: 2.75rem;
|
|
80
|
+
padding: 0.5rem 0.75rem;
|
|
81
|
+
border-radius: 0.375rem;
|
|
82
|
+
background: none;
|
|
83
|
+
border: none;
|
|
84
|
+
font: inherit;
|
|
85
|
+
font-size: 0.875rem;
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
color: inherit;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (min-width: 640px) {
|
|
92
|
+
.lalt-toolbar__button {
|
|
93
|
+
min-height: 0;
|
|
94
|
+
padding: 0.25rem 0.5rem;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.lalt-toolbar__button:hover {
|
|
99
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.lalt-toolbar__formats {
|
|
103
|
+
position: relative;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Opens upward from a bottom-docked bar, full width: a menu anchored below it
|
|
107
|
+
would open off the bottom of the screen. */
|
|
108
|
+
.lalt-toolbar__menu {
|
|
109
|
+
position: fixed;
|
|
110
|
+
left: 0;
|
|
111
|
+
right: 0;
|
|
112
|
+
bottom: 100%;
|
|
113
|
+
max-height: 60vh;
|
|
114
|
+
overflow-y: auto;
|
|
115
|
+
padding: 0.25rem;
|
|
116
|
+
background: var(--lalt-editor-surface, #ffffff);
|
|
117
|
+
border-top: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
118
|
+
box-shadow: 0 -8px 24px rgb(0 0 0 / 0.16);
|
|
119
|
+
color: var(--lalt-editor-text, #111827);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@media (min-width: 640px) {
|
|
123
|
+
.lalt-toolbar__menu {
|
|
124
|
+
position: absolute;
|
|
125
|
+
left: auto;
|
|
126
|
+
right: 0;
|
|
127
|
+
bottom: auto;
|
|
128
|
+
top: calc(100% + 0.375rem);
|
|
129
|
+
min-width: 14rem;
|
|
130
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
131
|
+
border-radius: 0.5rem;
|
|
132
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.lalt-toolbar__item {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: space-between;
|
|
140
|
+
gap: 1.5rem;
|
|
141
|
+
width: 100%;
|
|
142
|
+
min-height: 2.75rem;
|
|
143
|
+
padding: 0.5rem 0.75rem;
|
|
144
|
+
border-radius: 0.375rem;
|
|
145
|
+
background: none;
|
|
146
|
+
border: none;
|
|
147
|
+
font: inherit;
|
|
148
|
+
font-size: 0.9375rem;
|
|
149
|
+
text-align: left;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
color: inherit;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (min-width: 640px) {
|
|
155
|
+
.lalt-toolbar__item {
|
|
156
|
+
min-height: 0;
|
|
157
|
+
padding: 0.375rem 0.5rem;
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.lalt-toolbar__item:hover {
|
|
163
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.lalt-toolbar__item[aria-current="true"] {
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Hidden where there is no keyboard to press them with. */
|
|
171
|
+
.lalt-toolbar__shortcut {
|
|
172
|
+
display: none;
|
|
173
|
+
font-size: 0.75rem;
|
|
174
|
+
opacity: 0.5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (min-width: 640px) {
|
|
178
|
+
.lalt-toolbar__shortcut {
|
|
179
|
+
display: inline;
|
|
180
|
+
}
|
|
8
181
|
}
|
|
9
182
|
|
|
10
183
|
.lalt-revision {
|
|
@@ -30,3 +203,62 @@
|
|
|
30
203
|
opacity: 0.45;
|
|
31
204
|
background: none;
|
|
32
205
|
}
|
|
206
|
+
|
|
207
|
+
/* An edit shown where it happens, rather than in a panel the author has to
|
|
208
|
+
map back onto the text. The old passage stays visible struck through, the
|
|
209
|
+
proposal follows it, and the two decisions sit within thumb reach of both. */
|
|
210
|
+
.lalt-suggestion__old {
|
|
211
|
+
color: var(--lalt-editor-delete);
|
|
212
|
+
background: var(--lalt-editor-delete-bg);
|
|
213
|
+
text-decoration: line-through;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.lalt-suggestion {
|
|
217
|
+
display: inline;
|
|
218
|
+
white-space: normal;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.lalt-suggestion__new {
|
|
222
|
+
color: var(--lalt-editor-insert);
|
|
223
|
+
background: var(--lalt-editor-insert-bg);
|
|
224
|
+
border-radius: 0.25rem;
|
|
225
|
+
padding: 0.05em 0.15em;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.lalt-suggestion__action {
|
|
229
|
+
display: inline-flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
width: 1.75rem;
|
|
233
|
+
height: 1.75rem;
|
|
234
|
+
margin-left: 0.25rem;
|
|
235
|
+
vertical-align: middle;
|
|
236
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
237
|
+
border-radius: 999px;
|
|
238
|
+
background: var(--lalt-editor-surface, #ffffff);
|
|
239
|
+
color: var(--lalt-editor-text, #111827);
|
|
240
|
+
font-size: 0.8125rem;
|
|
241
|
+
line-height: 1;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* Bigger where a finger is doing the aiming. */
|
|
246
|
+
@media (max-width: 639px) {
|
|
247
|
+
.lalt-suggestion__action {
|
|
248
|
+
width: 2.25rem;
|
|
249
|
+
height: 2.25rem;
|
|
250
|
+
font-size: 1rem;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.lalt-suggestion__action--accept {
|
|
255
|
+
color: var(--lalt-editor-insert);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.lalt-suggestion__action--reject {
|
|
259
|
+
color: var(--lalt-editor-delete);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.lalt-suggestion__action:hover {
|
|
263
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
264
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,52 +1,149 @@
|
|
|
1
|
+
export { B as BlockChange, a as BlockShape, C as ChangeKind, R as RichDoc, b as RichNode, c as blocksFromDoc, d as diffBlocks, e as docFromBlocks, s as shapesFromDoc } from './diff-uFiP8hD5.js';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { Editor } from '@tiptap/react';
|
|
4
|
+
import { Extension } from '@tiptap/core';
|
|
5
|
+
import { PluginKey } from '@tiptap/pm/state';
|
|
6
|
+
import { DecorationSet } from '@tiptap/pm/view';
|
|
7
|
+
|
|
8
|
+
interface SelectionRect {
|
|
9
|
+
/** Horizontal centre of the selection. */
|
|
10
|
+
x: number;
|
|
11
|
+
top: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
}
|
|
14
|
+
interface Size {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
interface PlaceToolbarInput {
|
|
19
|
+
selection: SelectionRect;
|
|
20
|
+
toolbar: Size;
|
|
21
|
+
viewport: Size;
|
|
22
|
+
/** iOS renders its own callout above a selection, which covers a toolbar
|
|
23
|
+
* placed there, so the caller asks for below on that platform. */
|
|
24
|
+
preferBelow?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface ToolbarPlacement {
|
|
27
|
+
left: number;
|
|
28
|
+
top: number;
|
|
29
|
+
below: boolean;
|
|
30
|
+
}
|
|
1
31
|
/**
|
|
2
|
-
*
|
|
32
|
+
* placeToolbar positions a floating toolbar against a selection.
|
|
3
33
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
34
|
+
* Kept apart from the component because it is the part that is wrong in subtle
|
|
35
|
+
* ways — off-screen at the edges, hidden under a system callout — and the part
|
|
36
|
+
* a test can pin down without a browser.
|
|
7
37
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
interface
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
declare function placeToolbar({ selection, toolbar, viewport, preferBelow, }: PlaceToolbarInput): ToolbarPlacement;
|
|
39
|
+
|
|
40
|
+
interface ToolbarAction {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
icon?: React.ReactNode;
|
|
44
|
+
onSelect: (selectedText: string) => void;
|
|
45
|
+
}
|
|
46
|
+
interface BlockFormat {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
shortcut?: string;
|
|
50
|
+
isActive?: boolean;
|
|
51
|
+
onSelect: () => void;
|
|
52
|
+
}
|
|
53
|
+
interface SelectionToolbarProps {
|
|
54
|
+
selectedText: string;
|
|
55
|
+
selection: SelectionRect;
|
|
56
|
+
actions?: ToolbarAction[];
|
|
57
|
+
/** Heading levels and list kinds, rendered behind a single trigger. */
|
|
58
|
+
formats?: BlockFormat[];
|
|
59
|
+
formatsLabel?: string;
|
|
60
|
+
/** iOS draws its own callout above a selection; the host detects it. */
|
|
61
|
+
preferBelow?: boolean;
|
|
62
|
+
container?: HTMLElement | null;
|
|
63
|
+
}
|
|
64
|
+
declare function SelectionToolbar({ selectedText, selection, actions, formats, formatsLabel, preferBelow, container, }: SelectionToolbarProps): react.ReactPortal | null;
|
|
65
|
+
|
|
66
|
+
interface SelectionInfo {
|
|
67
|
+
text: string;
|
|
68
|
+
rect: SelectionRect;
|
|
19
69
|
}
|
|
20
70
|
/**
|
|
21
|
-
*
|
|
71
|
+
* useSelectionToolbar reports the current non-empty selection of an editor.
|
|
22
72
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* asking the reviewer about.
|
|
73
|
+
* Returns null whenever there is nothing to act on, so the caller renders the
|
|
74
|
+
* toolbar only while it has a target.
|
|
26
75
|
*/
|
|
27
|
-
declare function
|
|
28
|
-
/** shapesFromDoc records what each block was, so a revision can be rebuilt
|
|
29
|
-
* without turning every heading into a paragraph. */
|
|
30
|
-
declare function shapesFromDoc(doc: RichDoc): BlockShape[];
|
|
31
|
-
declare function docFromBlocks(blocks: string[], shapes?: BlockShape[]): RichDoc;
|
|
76
|
+
declare function useSelectionToolbar(editor: Editor | null): SelectionInfo | null;
|
|
32
77
|
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
interface FormatLabels {
|
|
79
|
+
paragraph: string;
|
|
80
|
+
heading1: string;
|
|
81
|
+
heading2: string;
|
|
82
|
+
heading3: string;
|
|
83
|
+
orderedList: string;
|
|
84
|
+
bulletList: string;
|
|
85
|
+
}
|
|
86
|
+
declare const defaultFormatLabels: FormatLabels;
|
|
87
|
+
/**
|
|
88
|
+
* blockFormats is the set a writer reaches for on a selected passage.
|
|
89
|
+
*
|
|
90
|
+
* Labels are passed in rather than translated here: the package carries no
|
|
91
|
+
* i18n runtime, so the host names things in whatever language it speaks.
|
|
92
|
+
*/
|
|
93
|
+
declare function blockFormats(editor: Editor | null, labels?: FormatLabels): BlockFormat[];
|
|
94
|
+
|
|
95
|
+
interface InlineSuggestion {
|
|
35
96
|
id: string;
|
|
36
|
-
|
|
97
|
+
/** Document positions of the passage this replaces. */
|
|
98
|
+
from: number;
|
|
99
|
+
to: number;
|
|
100
|
+
after: string;
|
|
101
|
+
}
|
|
102
|
+
interface SuggestionActions {
|
|
103
|
+
onAccept: (id: string) => void;
|
|
104
|
+
onReject: (id: string) => void;
|
|
105
|
+
acceptLabel: string;
|
|
106
|
+
rejectLabel: string;
|
|
107
|
+
}
|
|
108
|
+
declare const inlineSuggestionsKey: PluginKey<DecorationSet>;
|
|
109
|
+
/**
|
|
110
|
+
* InlineSuggestions renders proposed edits where they belong: over the passage
|
|
111
|
+
* they replace, with the new text right after it.
|
|
112
|
+
*
|
|
113
|
+
* Decorations, never document changes. The editor's content stays exactly what
|
|
114
|
+
* the author wrote until they accept, so the draft is saveable mid-review and
|
|
115
|
+
* a rejected suggestion leaves nothing behind — which a document-level insert
|
|
116
|
+
* could not promise.
|
|
117
|
+
*/
|
|
118
|
+
declare const InlineSuggestions: Extension<{
|
|
119
|
+
actions: SuggestionActions | null;
|
|
120
|
+
}, any>;
|
|
121
|
+
|
|
122
|
+
interface TextRange {
|
|
123
|
+
from: number;
|
|
124
|
+
to: number;
|
|
125
|
+
}
|
|
126
|
+
interface RevisionPart {
|
|
37
127
|
before: string;
|
|
38
128
|
after: string;
|
|
39
129
|
}
|
|
40
130
|
/**
|
|
41
|
-
*
|
|
131
|
+
* expandToWords grows a range out to whole words.
|
|
42
132
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
133
|
+
* A selection dragged across a line lands mid-word more often than not, and a
|
|
134
|
+
* model handed "ns de production" answers for a fragment — which then reads as
|
|
135
|
+
* a truncated diff the author has to decode.
|
|
136
|
+
*/
|
|
137
|
+
declare function expandToWords(text: string, from: number, to: number): TextRange;
|
|
138
|
+
/**
|
|
139
|
+
* splitRevision turns one model answer into the decisions it really contains.
|
|
46
140
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
141
|
+
* A three-paragraph rewrite accepted or rejected as a whole forces the author
|
|
142
|
+
* to keep a weak paragraph for a strong one, so each paragraph becomes its own
|
|
143
|
+
* decision — but only when both sides line up. When the model merged or split
|
|
144
|
+
* paragraphs, position no longer maps them, and pairing by index would show a
|
|
145
|
+
* diff that never existed.
|
|
49
146
|
*/
|
|
50
|
-
declare function
|
|
147
|
+
declare function splitRevision(before: string, after: string): RevisionPart[];
|
|
51
148
|
|
|
52
|
-
export { type
|
|
149
|
+
export { type BlockFormat, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type RevisionPart, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, blockFormats, defaultFormatLabels, expandToWords, inlineSuggestionsKey, placeToolbar, splitRevision, useSelectionToolbar };
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,332 @@ import {
|
|
|
4
4
|
docFromBlocks,
|
|
5
5
|
shapesFromDoc
|
|
6
6
|
} from "./chunk-7GOSVZTI.js";
|
|
7
|
+
|
|
8
|
+
// src/placement.ts
|
|
9
|
+
var GAP = 8;
|
|
10
|
+
function placeToolbar({
|
|
11
|
+
selection,
|
|
12
|
+
toolbar,
|
|
13
|
+
viewport,
|
|
14
|
+
preferBelow = false
|
|
15
|
+
}) {
|
|
16
|
+
const fitsAbove = selection.top - GAP - toolbar.height >= 0;
|
|
17
|
+
const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;
|
|
18
|
+
const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
|
|
19
|
+
const top = below ? selection.bottom + GAP : Math.max(0, selection.top - GAP - toolbar.height);
|
|
20
|
+
const centred = selection.x - toolbar.width / 2;
|
|
21
|
+
const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));
|
|
22
|
+
return { left, top, below };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/SelectionToolbar.tsx
|
|
26
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
27
|
+
import { createPortal } from "react-dom";
|
|
28
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
+
var FALLBACK = { width: 320, height: 40 };
|
|
30
|
+
function SelectionToolbar({
|
|
31
|
+
selectedText,
|
|
32
|
+
selection,
|
|
33
|
+
actions = [],
|
|
34
|
+
formats = [],
|
|
35
|
+
formatsLabel = "Format",
|
|
36
|
+
preferBelow = false,
|
|
37
|
+
container
|
|
38
|
+
}) {
|
|
39
|
+
const ref = useRef(null);
|
|
40
|
+
const [size, setSize] = useState(FALLBACK);
|
|
41
|
+
const [formatsOpen, setFormatsOpen] = useState(false);
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
const el = ref.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
const rect = el.getBoundingClientRect();
|
|
46
|
+
if (rect.width > 0) setSize({ width: rect.width, height: rect.height });
|
|
47
|
+
}, [selectedText, actions.length, formats.length]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!formatsOpen) return;
|
|
50
|
+
const close = (e) => {
|
|
51
|
+
if (ref.current && !ref.current.contains(e.target)) setFormatsOpen(false);
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("mousedown", close);
|
|
54
|
+
return () => document.removeEventListener("mousedown", close);
|
|
55
|
+
}, [formatsOpen]);
|
|
56
|
+
if (typeof document === "undefined") return null;
|
|
57
|
+
const place = placeToolbar({
|
|
58
|
+
selection,
|
|
59
|
+
toolbar: size,
|
|
60
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
61
|
+
preferBelow
|
|
62
|
+
});
|
|
63
|
+
return createPortal(
|
|
64
|
+
/* @__PURE__ */ jsxs(
|
|
65
|
+
"div",
|
|
66
|
+
{
|
|
67
|
+
ref,
|
|
68
|
+
role: "toolbar",
|
|
69
|
+
className: "lalt-toolbar",
|
|
70
|
+
style: {
|
|
71
|
+
"--lalt-toolbar-left": `${place.left}px`,
|
|
72
|
+
"--lalt-toolbar-top": `${place.top}px`
|
|
73
|
+
},
|
|
74
|
+
children: [
|
|
75
|
+
actions.map((action) => /* @__PURE__ */ jsxs(
|
|
76
|
+
"button",
|
|
77
|
+
{
|
|
78
|
+
type: "button",
|
|
79
|
+
className: "lalt-toolbar__button",
|
|
80
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
81
|
+
onClick: () => action.onSelect(selectedText),
|
|
82
|
+
children: [
|
|
83
|
+
action.icon,
|
|
84
|
+
/* @__PURE__ */ jsx("span", { children: action.label })
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
action.id
|
|
88
|
+
)),
|
|
89
|
+
formats.length > 0 && /* @__PURE__ */ jsxs("div", { className: "lalt-toolbar__formats", children: [
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
"button",
|
|
92
|
+
{
|
|
93
|
+
type: "button",
|
|
94
|
+
className: "lalt-toolbar__button",
|
|
95
|
+
"aria-haspopup": "menu",
|
|
96
|
+
"aria-expanded": formatsOpen,
|
|
97
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
98
|
+
onClick: () => setFormatsOpen((open) => !open),
|
|
99
|
+
children: formatsLabel
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
formatsOpen && /* @__PURE__ */ jsx("div", { role: "menu", className: "lalt-toolbar__menu", children: formats.map((format) => /* @__PURE__ */ jsxs(
|
|
103
|
+
"button",
|
|
104
|
+
{
|
|
105
|
+
type: "button",
|
|
106
|
+
role: "menuitem",
|
|
107
|
+
className: "lalt-toolbar__item",
|
|
108
|
+
"aria-current": format.isActive,
|
|
109
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
110
|
+
onClick: () => {
|
|
111
|
+
format.onSelect();
|
|
112
|
+
setFormatsOpen(false);
|
|
113
|
+
},
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx("span", { children: format.label }),
|
|
116
|
+
format.shortcut && /* @__PURE__ */ jsx("kbd", { className: "lalt-toolbar__shortcut", children: format.shortcut })
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
format.id
|
|
120
|
+
)) })
|
|
121
|
+
] })
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
container ?? document.body
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/useSelectionToolbar.ts
|
|
130
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
131
|
+
function useSelectionToolbar(editor) {
|
|
132
|
+
const [selection, setSelection] = useState2(null);
|
|
133
|
+
useEffect2(() => {
|
|
134
|
+
if (!editor) return;
|
|
135
|
+
const read = () => {
|
|
136
|
+
const { state } = editor;
|
|
137
|
+
const { from, to, empty } = state.selection;
|
|
138
|
+
if (empty || !editor.isFocused) {
|
|
139
|
+
setSelection(null);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const text = state.doc.textBetween(from, to, " ").trim();
|
|
143
|
+
if (text === "") {
|
|
144
|
+
setSelection(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const start = editor.view.coordsAtPos(from);
|
|
148
|
+
const end = editor.view.coordsAtPos(to);
|
|
149
|
+
setSelection({
|
|
150
|
+
text,
|
|
151
|
+
rect: {
|
|
152
|
+
x: (start.left + end.right) / 2,
|
|
153
|
+
top: Math.min(start.top, end.top),
|
|
154
|
+
bottom: Math.max(start.bottom, end.bottom)
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
read();
|
|
159
|
+
editor.on("selectionUpdate", read);
|
|
160
|
+
editor.on("blur", read);
|
|
161
|
+
editor.on("focus", read);
|
|
162
|
+
return () => {
|
|
163
|
+
editor.off("selectionUpdate", read);
|
|
164
|
+
editor.off("blur", read);
|
|
165
|
+
editor.off("focus", read);
|
|
166
|
+
};
|
|
167
|
+
}, [editor]);
|
|
168
|
+
return selection;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/formats.ts
|
|
172
|
+
var defaultFormatLabels = {
|
|
173
|
+
paragraph: "Texte",
|
|
174
|
+
heading1: "Titre 1",
|
|
175
|
+
heading2: "Titre 2",
|
|
176
|
+
heading3: "Titre 3",
|
|
177
|
+
orderedList: "Liste num\xE9rot\xE9e",
|
|
178
|
+
bulletList: "Liste \xE0 puces"
|
|
179
|
+
};
|
|
180
|
+
function blockFormats(editor, labels = defaultFormatLabels) {
|
|
181
|
+
if (!editor) return [];
|
|
182
|
+
const mod = isApple() ? "\u2318" : "Ctrl";
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
id: "paragraph",
|
|
186
|
+
label: labels.paragraph,
|
|
187
|
+
isActive: editor.isActive("paragraph"),
|
|
188
|
+
onSelect: () => editor.chain().focus().setParagraph().run()
|
|
189
|
+
},
|
|
190
|
+
...[1, 2, 3].map((level) => ({
|
|
191
|
+
id: `heading${level}`,
|
|
192
|
+
label: labels[`heading${level}`],
|
|
193
|
+
shortcut: `${mod}+Alt+${level}`,
|
|
194
|
+
isActive: editor.isActive("heading", { level }),
|
|
195
|
+
onSelect: () => editor.chain().focus().toggleHeading({ level }).run()
|
|
196
|
+
})),
|
|
197
|
+
{
|
|
198
|
+
id: "orderedList",
|
|
199
|
+
label: labels.orderedList,
|
|
200
|
+
shortcut: `${mod}+Shift+7`,
|
|
201
|
+
isActive: editor.isActive("orderedList"),
|
|
202
|
+
onSelect: () => editor.chain().focus().toggleOrderedList().run()
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "bulletList",
|
|
206
|
+
label: labels.bulletList,
|
|
207
|
+
shortcut: `${mod}+Shift+8`,
|
|
208
|
+
isActive: editor.isActive("bulletList"),
|
|
209
|
+
onSelect: () => editor.chain().focus().toggleBulletList().run()
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
function isApple() {
|
|
214
|
+
if (typeof navigator === "undefined") return false;
|
|
215
|
+
return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/InlineSuggestions.ts
|
|
219
|
+
import { Extension } from "@tiptap/core";
|
|
220
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
221
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
222
|
+
var inlineSuggestionsKey = new PluginKey("lalt-inline-suggestions");
|
|
223
|
+
var InlineSuggestions = Extension.create({
|
|
224
|
+
name: "lalt-inline-suggestions",
|
|
225
|
+
addOptions() {
|
|
226
|
+
return { actions: null };
|
|
227
|
+
},
|
|
228
|
+
addProseMirrorPlugins() {
|
|
229
|
+
const options = this.options;
|
|
230
|
+
return [
|
|
231
|
+
new Plugin({
|
|
232
|
+
key: inlineSuggestionsKey,
|
|
233
|
+
state: {
|
|
234
|
+
init: () => DecorationSet.empty,
|
|
235
|
+
apply(tr, old) {
|
|
236
|
+
const set = tr.getMeta(inlineSuggestionsKey);
|
|
237
|
+
if (set) return build(tr.doc, set, options.actions);
|
|
238
|
+
return old.map(tr.mapping, tr.doc);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
props: {
|
|
242
|
+
decorations(state) {
|
|
243
|
+
return inlineSuggestionsKey.getState(state) ?? DecorationSet.empty;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
function build(doc, suggestions, actions) {
|
|
251
|
+
const decorations = [];
|
|
252
|
+
for (const s of suggestions) {
|
|
253
|
+
if (s.from < 0 || s.to > doc.content.size || s.from >= s.to) continue;
|
|
254
|
+
decorations.push(
|
|
255
|
+
Decoration.inline(s.from, s.to, { class: "lalt-suggestion__old" }),
|
|
256
|
+
Decoration.widget(s.to, () => renderProposal(s, actions), {
|
|
257
|
+
// The widget belongs to the suggestion that precedes it, so typing at
|
|
258
|
+
// the boundary does not push it around.
|
|
259
|
+
side: 1,
|
|
260
|
+
ignoreSelection: true
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return DecorationSet.create(doc, decorations);
|
|
265
|
+
}
|
|
266
|
+
function renderProposal(s, actions) {
|
|
267
|
+
const wrap = document.createElement("span");
|
|
268
|
+
wrap.className = "lalt-suggestion";
|
|
269
|
+
wrap.setAttribute("contenteditable", "false");
|
|
270
|
+
const text = document.createElement("span");
|
|
271
|
+
text.className = "lalt-suggestion__new";
|
|
272
|
+
text.textContent = s.after;
|
|
273
|
+
wrap.appendChild(text);
|
|
274
|
+
if (actions) {
|
|
275
|
+
wrap.appendChild(button("reject", actions.rejectLabel, "\u2715", () => actions.onReject(s.id)));
|
|
276
|
+
wrap.appendChild(button("accept", actions.acceptLabel, "\u2713", () => actions.onAccept(s.id)));
|
|
277
|
+
}
|
|
278
|
+
return wrap;
|
|
279
|
+
}
|
|
280
|
+
function button(kind, label, glyph, onClick) {
|
|
281
|
+
const el = document.createElement("button");
|
|
282
|
+
el.type = "button";
|
|
283
|
+
el.className = `lalt-suggestion__action lalt-suggestion__action--${kind}`;
|
|
284
|
+
el.setAttribute("aria-label", label);
|
|
285
|
+
el.textContent = glyph;
|
|
286
|
+
el.addEventListener("mousedown", (e) => e.preventDefault());
|
|
287
|
+
el.addEventListener("click", (e) => {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
onClick();
|
|
290
|
+
});
|
|
291
|
+
return el;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/passage.ts
|
|
295
|
+
var WORD = /[\p{L}\p{N}'’-]/u;
|
|
296
|
+
function expandToWords(text, from, to) {
|
|
297
|
+
if (from >= to) return { from, to };
|
|
298
|
+
let start = from;
|
|
299
|
+
while (start > 0 && WORD.test(text[start - 1]) && WORD.test(text[start])) start--;
|
|
300
|
+
let end = to;
|
|
301
|
+
while (end < text.length && WORD.test(text[end]) && WORD.test(text[end - 1])) end++;
|
|
302
|
+
return { from: start, to: end };
|
|
303
|
+
}
|
|
304
|
+
function splitRevision(before, after) {
|
|
305
|
+
if (before.trim() === after.trim()) return [];
|
|
306
|
+
const olds = paragraphs(before);
|
|
307
|
+
const news = paragraphs(after);
|
|
308
|
+
if (olds.length !== news.length || olds.length === 0) {
|
|
309
|
+
return [{ before: before.trim(), after: after.trim() }];
|
|
310
|
+
}
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (let i = 0; i < olds.length; i++) {
|
|
313
|
+
if (olds[i] !== news[i]) parts.push({ before: olds[i], after: news[i] });
|
|
314
|
+
}
|
|
315
|
+
return parts;
|
|
316
|
+
}
|
|
317
|
+
function paragraphs(s) {
|
|
318
|
+
return s.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p !== "");
|
|
319
|
+
}
|
|
7
320
|
export {
|
|
321
|
+
InlineSuggestions,
|
|
322
|
+
SelectionToolbar,
|
|
323
|
+
blockFormats,
|
|
8
324
|
blocksFromDoc,
|
|
325
|
+
defaultFormatLabels,
|
|
9
326
|
diffBlocks,
|
|
10
327
|
docFromBlocks,
|
|
11
|
-
|
|
328
|
+
expandToWords,
|
|
329
|
+
inlineSuggestionsKey,
|
|
330
|
+
placeToolbar,
|
|
331
|
+
shapesFromDoc,
|
|
332
|
+
splitRevision,
|
|
333
|
+
useSelectionToolbar
|
|
12
334
|
};
|
|
13
335
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useSelectionToolbar.ts","../src/formats.ts","../src/InlineSuggestions.ts","../src/passage.ts"],"sourcesContent":["export interface SelectionRect {\n /** Horizontal centre of the selection. */\n x: number;\n top: number;\n bottom: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport interface PlaceToolbarInput {\n selection: SelectionRect;\n toolbar: Size;\n viewport: Size;\n /** iOS renders its own callout above a selection, which covers a toolbar\n * placed there, so the caller asks for below on that platform. */\n preferBelow?: boolean;\n}\n\nexport interface ToolbarPlacement {\n left: number;\n top: number;\n below: boolean;\n}\n\nconst GAP = 8;\n\n/**\n * placeToolbar positions a floating toolbar against a selection.\n *\n * Kept apart from the component because it is the part that is wrong in subtle\n * ways — off-screen at the edges, hidden under a system callout — and the part\n * a test can pin down without a browser.\n */\nexport function placeToolbar({\n selection,\n toolbar,\n viewport,\n preferBelow = false,\n}: PlaceToolbarInput): ToolbarPlacement {\n const fitsAbove = selection.top - GAP - toolbar.height >= 0;\n const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;\n\n const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;\n\n const top = below\n ? selection.bottom + GAP\n : Math.max(0, selection.top - GAP - toolbar.height);\n\n const centred = selection.x - toolbar.width / 2;\n const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));\n\n return { left, top, below };\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { placeToolbar, type SelectionRect } from \"./placement\";\n\nexport interface ToolbarAction {\n id: string;\n label: string;\n icon?: React.ReactNode;\n onSelect: (selectedText: string) => void;\n}\n\nexport interface BlockFormat {\n id: string;\n label: string;\n shortcut?: string;\n isActive?: boolean;\n onSelect: () => void;\n}\n\nexport interface SelectionToolbarProps {\n selectedText: string;\n selection: SelectionRect;\n actions?: ToolbarAction[];\n /** Heading levels and list kinds, rendered behind a single trigger. */\n formats?: BlockFormat[];\n formatsLabel?: string;\n /** iOS draws its own callout above a selection; the host detects it. */\n preferBelow?: boolean;\n container?: HTMLElement | null;\n}\n\nconst FALLBACK = { width: 320, height: 40 };\n\nexport function SelectionToolbar({\n selectedText,\n selection,\n actions = [],\n formats = [],\n formatsLabel = \"Format\",\n preferBelow = false,\n container,\n}: SelectionToolbarProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(FALLBACK);\n const [formatsOpen, setFormatsOpen] = useState(false);\n\n // Measured after paint rather than assumed: the toolbar's width depends on\n // labels the host supplies, and guessing it puts the toolbar off-centre.\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n if (rect.width > 0) setSize({ width: rect.width, height: rect.height });\n }, [selectedText, actions.length, formats.length]);\n\n useEffect(() => {\n if (!formatsOpen) return;\n const close = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setFormatsOpen(false);\n };\n document.addEventListener(\"mousedown\", close);\n return () => document.removeEventListener(\"mousedown\", close);\n }, [formatsOpen]);\n\n if (typeof document === \"undefined\") return null;\n\n const place = placeToolbar({\n selection,\n toolbar: size,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n preferBelow,\n });\n\n return createPortal(\n <div\n ref={ref}\n role=\"toolbar\"\n className=\"lalt-toolbar\"\n // Custom properties rather than left/top directly: under the mobile\n // breakpoint the stylesheet docks the toolbar to the bottom of the\n // screen and ignores these, which inline left/top would override.\n style={\n {\n \"--lalt-toolbar-left\": `${place.left}px`,\n \"--lalt-toolbar-top\": `${place.top}px`,\n } as React.CSSProperties\n }\n >\n {actions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n className=\"lalt-toolbar__button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => action.onSelect(selectedText)}\n >\n {action.icon}\n <span>{action.label}</span>\n </button>\n ))}\n\n {formats.length > 0 && (\n <div className=\"lalt-toolbar__formats\">\n <button\n type=\"button\"\n className=\"lalt-toolbar__button\"\n aria-haspopup=\"menu\"\n aria-expanded={formatsOpen}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => setFormatsOpen((open) => !open)}\n >\n {formatsLabel}\n </button>\n {formatsOpen && (\n <div role=\"menu\" className=\"lalt-toolbar__menu\">\n {formats.map((format) => (\n <button\n key={format.id}\n type=\"button\"\n role=\"menuitem\"\n className=\"lalt-toolbar__item\"\n aria-current={format.isActive}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n format.onSelect();\n setFormatsOpen(false);\n }}\n >\n <span>{format.label}</span>\n {format.shortcut && (\n <kbd className=\"lalt-toolbar__shortcut\">{format.shortcut}</kbd>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>,\n container ?? document.body,\n );\n}\n","import { useEffect, useState } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\n\nimport type { SelectionRect } from \"./placement\";\n\nexport interface SelectionInfo {\n text: string;\n rect: SelectionRect;\n}\n\n/**\n * useSelectionToolbar reports the current non-empty selection of an editor.\n *\n * Returns null whenever there is nothing to act on, so the caller renders the\n * toolbar only while it has a target.\n */\nexport function useSelectionToolbar(editor: Editor | null): SelectionInfo | null {\n const [selection, setSelection] = useState<SelectionInfo | null>(null);\n\n useEffect(() => {\n if (!editor) return;\n\n const read = () => {\n const { state } = editor;\n const { from, to, empty } = state.selection;\n if (empty || !editor.isFocused) {\n setSelection(null);\n return;\n }\n\n const text = state.doc.textBetween(from, to, \" \").trim();\n if (text === \"\") {\n setSelection(null);\n return;\n }\n\n const start = editor.view.coordsAtPos(from);\n const end = editor.view.coordsAtPos(to);\n setSelection({\n text,\n rect: {\n x: (start.left + end.right) / 2,\n top: Math.min(start.top, end.top),\n bottom: Math.max(start.bottom, end.bottom),\n },\n });\n };\n\n read();\n editor.on(\"selectionUpdate\", read);\n editor.on(\"blur\", read);\n editor.on(\"focus\", read);\n return () => {\n editor.off(\"selectionUpdate\", read);\n editor.off(\"blur\", read);\n editor.off(\"focus\", read);\n };\n }, [editor]);\n\n return selection;\n}\n","import type { Editor } from \"@tiptap/react\";\n\n// Type-only: these commands are contributed by StarterKit's extensions, and\n// without the reference TypeScript does not know they exist on a chain. No\n// runtime import, so the host stays in charge of which extensions it mounts —\n// blockFormats simply returns entries whose commands no-op if one is missing.\nimport type {} from \"@tiptap/starter-kit\";\n\nimport type { BlockFormat } from \"./SelectionToolbar\";\n\nexport interface FormatLabels {\n paragraph: string;\n heading1: string;\n heading2: string;\n heading3: string;\n orderedList: string;\n bulletList: string;\n}\n\nexport const defaultFormatLabels: FormatLabels = {\n paragraph: \"Texte\",\n heading1: \"Titre 1\",\n heading2: \"Titre 2\",\n heading3: \"Titre 3\",\n orderedList: \"Liste numérotée\",\n bulletList: \"Liste à puces\",\n};\n\n/**\n * blockFormats is the set a writer reaches for on a selected passage.\n *\n * Labels are passed in rather than translated here: the package carries no\n * i18n runtime, so the host names things in whatever language it speaks.\n */\nexport function blockFormats(\n editor: Editor | null,\n labels: FormatLabels = defaultFormatLabels,\n): BlockFormat[] {\n if (!editor) return [];\n\n // Shortcuts are the ones TipTap's own extensions bind, read from their\n // source rather than invented: heading binds Mod-Alt-<level>, the list\n // extensions bind Mod-Shift-7/8, and setParagraph binds nothing at all.\n // A label promising a shortcut that does nothing is worse than no label.\n const mod = isApple() ? \"⌘\" : \"Ctrl\";\n\n return [\n {\n id: \"paragraph\",\n label: labels.paragraph,\n isActive: editor.isActive(\"paragraph\"),\n onSelect: () => editor.chain().focus().setParagraph().run(),\n },\n ...([1, 2, 3] as const).map((level) => ({\n id: `heading${level}`,\n label: labels[`heading${level}` as keyof FormatLabels],\n shortcut: `${mod}+Alt+${level}`,\n isActive: editor.isActive(\"heading\", { level }),\n onSelect: () => editor.chain().focus().toggleHeading({ level }).run(),\n })),\n {\n id: \"orderedList\",\n label: labels.orderedList,\n shortcut: `${mod}+Shift+7`,\n isActive: editor.isActive(\"orderedList\"),\n onSelect: () => editor.chain().focus().toggleOrderedList().run(),\n },\n {\n id: \"bulletList\",\n label: labels.bulletList,\n shortcut: `${mod}+Shift+8`,\n isActive: editor.isActive(\"bulletList\"),\n onSelect: () => editor.chain().focus().toggleBulletList().run(),\n },\n ];\n}\n\nfunction isApple(): boolean {\n if (typeof navigator === \"undefined\") return false;\n return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);\n}\n","import { Extension } from \"@tiptap/core\";\nimport { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\nimport type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\n\nexport interface InlineSuggestion {\n id: string;\n /** Document positions of the passage this replaces. */\n from: number;\n to: number;\n after: string;\n}\n\nexport interface SuggestionActions {\n onAccept: (id: string) => void;\n onReject: (id: string) => void;\n acceptLabel: string;\n rejectLabel: string;\n}\n\nexport const inlineSuggestionsKey = new PluginKey<DecorationSet>(\"lalt-inline-suggestions\");\n\n/**\n * InlineSuggestions renders proposed edits where they belong: over the passage\n * they replace, with the new text right after it.\n *\n * Decorations, never document changes. The editor's content stays exactly what\n * the author wrote until they accept, so the draft is saveable mid-review and\n * a rejected suggestion leaves nothing behind — which a document-level insert\n * could not promise.\n */\nexport const InlineSuggestions = Extension.create<{ actions: SuggestionActions | null }>({\n name: \"lalt-inline-suggestions\",\n\n addOptions() {\n return { actions: null };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n\n return [\n new Plugin<DecorationSet>({\n key: inlineSuggestionsKey,\n\n state: {\n init: () => DecorationSet.empty,\n apply(tr, old) {\n const set = tr.getMeta(inlineSuggestionsKey) as InlineSuggestion[] | undefined;\n if (set) return build(tr.doc, set, options.actions);\n // Positions follow the document as the author keeps typing around\n // a pending suggestion.\n return old.map(tr.mapping, tr.doc);\n },\n },\n\n props: {\n decorations(state) {\n return inlineSuggestionsKey.getState(state) ?? DecorationSet.empty;\n },\n },\n }),\n ];\n },\n});\n\nfunction build(\n doc: ProseMirrorNode,\n suggestions: InlineSuggestion[],\n actions: SuggestionActions | null,\n): DecorationSet {\n const decorations: Decoration[] = [];\n\n for (const s of suggestions) {\n if (s.from < 0 || s.to > doc.content.size || s.from >= s.to) continue;\n\n decorations.push(\n Decoration.inline(s.from, s.to, { class: \"lalt-suggestion__old\" }),\n Decoration.widget(s.to, () => renderProposal(s, actions), {\n // The widget belongs to the suggestion that precedes it, so typing at\n // the boundary does not push it around.\n side: 1,\n ignoreSelection: true,\n }),\n );\n }\n return DecorationSet.create(doc, decorations);\n}\n\nfunction renderProposal(s: InlineSuggestion, actions: SuggestionActions | null): HTMLElement {\n const wrap = document.createElement(\"span\");\n wrap.className = \"lalt-suggestion\";\n wrap.setAttribute(\"contenteditable\", \"false\");\n\n const text = document.createElement(\"span\");\n text.className = \"lalt-suggestion__new\";\n text.textContent = s.after;\n wrap.appendChild(text);\n\n if (actions) {\n wrap.appendChild(button(\"reject\", actions.rejectLabel, \"✕\", () => actions.onReject(s.id)));\n wrap.appendChild(button(\"accept\", actions.acceptLabel, \"✓\", () => actions.onAccept(s.id)));\n }\n return wrap;\n}\n\nfunction button(kind: string, label: string, glyph: string, onClick: () => void): HTMLElement {\n const el = document.createElement(\"button\");\n el.type = \"button\";\n el.className = `lalt-suggestion__action lalt-suggestion__action--${kind}`;\n el.setAttribute(\"aria-label\", label);\n el.textContent = glyph;\n // mousedown would move the selection into the widget before the click lands.\n el.addEventListener(\"mousedown\", (e) => e.preventDefault());\n el.addEventListener(\"click\", (e) => {\n e.preventDefault();\n onClick();\n });\n return el;\n}\n","export interface TextRange {\n from: number;\n to: number;\n}\n\nexport interface RevisionPart {\n before: string;\n after: string;\n}\n\n// Letters, digits, and the marks that belong inside a French word.\nconst WORD = /[\\p{L}\\p{N}'’-]/u;\n\n/**\n * expandToWords grows a range out to whole words.\n *\n * A selection dragged across a line lands mid-word more often than not, and a\n * model handed \"ns de production\" answers for a fragment — which then reads as\n * a truncated diff the author has to decode.\n */\nexport function expandToWords(text: string, from: number, to: number): TextRange {\n if (from >= to) return { from, to };\n\n let start = from;\n while (start > 0 && WORD.test(text[start - 1]) && WORD.test(text[start])) start--;\n\n let end = to;\n while (end < text.length && WORD.test(text[end]) && WORD.test(text[end - 1])) end++;\n\n return { from: start, to: end };\n}\n\n/**\n * splitRevision turns one model answer into the decisions it really contains.\n *\n * A three-paragraph rewrite accepted or rejected as a whole forces the author\n * to keep a weak paragraph for a strong one, so each paragraph becomes its own\n * decision — but only when both sides line up. When the model merged or split\n * paragraphs, position no longer maps them, and pairing by index would show a\n * diff that never existed.\n */\nexport function splitRevision(before: string, after: string): RevisionPart[] {\n if (before.trim() === after.trim()) return [];\n\n const olds = paragraphs(before);\n const news = paragraphs(after);\n\n if (olds.length !== news.length || olds.length === 0) {\n return [{ before: before.trim(), after: after.trim() }];\n }\n\n const parts: RevisionPart[] = [];\n for (let i = 0; i < olds.length; i++) {\n if (olds[i] !== news[i]) parts.push({ before: olds[i], after: news[i] });\n }\n return parts;\n}\n\nfunction paragraphs(s: string): string[] {\n return s\n .split(/\\n\\s*\\n/)\n .map((p) => p.trim())\n .filter((p) => p !== \"\");\n}\n"],"mappings":"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","names":["useEffect","useState"]}
|
package/dist/revisions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BlockChange, RichDoc } from './
|
|
2
|
-
export { ChangeKind } from './
|
|
1
|
+
import { B as BlockChange, R as RichDoc } from './diff-uFiP8hD5.js';
|
|
2
|
+
export { C as ChangeKind } from './diff-uFiP8hD5.js';
|
|
3
3
|
|
|
4
4
|
type Decision = "pending" | "accepted" | "rejected";
|
|
5
5
|
interface RevisionState {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalternative/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared TipTap editor for L'Alternative apps: rich text, format menu, selection toolbar and inline AI revisions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,5 +54,10 @@
|
|
|
54
54
|
"type": "git",
|
|
55
55
|
"url": "https://github.com/lalternative/packages.git",
|
|
56
56
|
"directory": "editor"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@tiptap/starter-kit": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
}
|