@lalternative/editor 0.6.1 → 0.7.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/editor.css +85 -39
- package/dist/index.d.ts +40 -1
- package/dist/index.js +75 -18
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/editor.css
CHANGED
|
@@ -29,30 +29,56 @@
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/*
|
|
33
|
-
|
|
32
|
+
/* A pill above the keyboard by default, floating beside the selection only
|
|
33
|
+
where there is a pointer and room for it.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
floating bar
|
|
35
|
+
Near the selection is the worst place for it on a phone: the finger that
|
|
36
|
+
made the selection covers it, and iOS draws its own callout in the same
|
|
37
|
+
spot. Sitting above the keyboard puts it in thumb reach instead, and unlike
|
|
38
|
+
a floating bar it cannot be pushed off-screen by a selection near an edge.
|
|
39
|
+
|
|
40
|
+
Shaped as a pill rather than a full-width bar, which is the form the
|
|
41
|
+
platform already uses for a selection: it reads as something hovering over
|
|
42
|
+
the document rather than as a piece of the page's chrome, and its rounded
|
|
43
|
+
ends keep the text either side of it visible.
|
|
44
|
+
|
|
45
|
+
Lifted by whatever covers the bottom of the layout viewport: the on-screen
|
|
46
|
+
keyboard hides the bar without shrinking that viewport, so `bottom: 0` alone
|
|
47
|
+
parks it under the keys. The component measures the strip and sets the
|
|
48
|
+
variable; it is 0 whenever nothing covers anything. */
|
|
39
49
|
.lalt-toolbar {
|
|
40
50
|
position: fixed;
|
|
41
|
-
left: 0;
|
|
42
|
-
right: 0;
|
|
43
|
-
bottom: 0;
|
|
51
|
+
left: 0.5rem;
|
|
52
|
+
right: 0.5rem;
|
|
53
|
+
bottom: calc(var(--lalt-keyboard-inset, 0px) + 0.5rem);
|
|
44
54
|
z-index: 100;
|
|
45
55
|
display: flex;
|
|
46
56
|
align-items: center;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
justify-content: center;
|
|
58
|
+
gap: 0.125rem;
|
|
59
|
+
width: fit-content;
|
|
60
|
+
max-width: calc(100% - 1rem);
|
|
61
|
+
margin: 0 auto;
|
|
62
|
+
margin-bottom: env(safe-area-inset-bottom);
|
|
63
|
+
padding: 0.25rem;
|
|
64
|
+
border-radius: 999px;
|
|
50
65
|
background: var(--lalt-editor-surface, #ffffff);
|
|
51
|
-
border
|
|
52
|
-
box-shadow:
|
|
66
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
67
|
+
box-shadow:
|
|
68
|
+
0 6px 20px rgb(0 0 0 / 0.18),
|
|
69
|
+
0 1px 3px rgb(0 0 0 / 0.1);
|
|
53
70
|
color: var(--lalt-editor-text, #111827);
|
|
54
71
|
user-select: none;
|
|
72
|
+
/* A long row scrolls rather than stretching the pill past the screen. This
|
|
73
|
+
clips vertically too — `overflow-y: visible` beside it is ignored by the
|
|
74
|
+
spec — so the format menu is positioned against the viewport instead of
|
|
75
|
+
inside this box, and escapes the clip that way. */
|
|
55
76
|
overflow-x: auto;
|
|
77
|
+
scrollbar-width: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.lalt-toolbar::-webkit-scrollbar {
|
|
81
|
+
display: none;
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
@media (hover: hover) and (pointer: fine) {
|
|
@@ -61,11 +87,9 @@
|
|
|
61
87
|
top: var(--lalt-toolbar-top, 0);
|
|
62
88
|
right: auto;
|
|
63
89
|
bottom: auto;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
padding
|
|
67
|
-
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
68
|
-
border-radius: 0.5rem;
|
|
90
|
+
max-width: none;
|
|
91
|
+
margin: 0;
|
|
92
|
+
padding: 0.1875rem;
|
|
69
93
|
/* Deep enough to read as floating over the text rather than sitting in
|
|
70
94
|
it — the bar appears mid-paragraph, with prose on every side. */
|
|
71
95
|
box-shadow:
|
|
@@ -75,35 +99,50 @@
|
|
|
75
99
|
}
|
|
76
100
|
}
|
|
77
101
|
|
|
102
|
+
/* Transparent by default: inside a pill the buttons are read as a row by the
|
|
103
|
+
separators between them, and a filled rectangle each would turn the bar into
|
|
104
|
+
a strip of chips. The fill is what pressing one looks like. */
|
|
78
105
|
.lalt-toolbar__button {
|
|
79
106
|
display: inline-flex;
|
|
80
107
|
align-items: center;
|
|
81
108
|
justify-content: center;
|
|
82
109
|
gap: 0.375rem;
|
|
83
|
-
/*
|
|
84
|
-
|
|
85
|
-
min-height: 2.
|
|
86
|
-
padding: 0.5rem 0.
|
|
87
|
-
border-radius:
|
|
88
|
-
background:
|
|
110
|
+
/* 40px tall inside a 44px pill: still the target a finger needs, once the
|
|
111
|
+
bar's own padding is counted. */
|
|
112
|
+
min-height: 2.5rem;
|
|
113
|
+
padding: 0.5rem 0.875rem;
|
|
114
|
+
border-radius: 999px;
|
|
115
|
+
background: none;
|
|
89
116
|
border: none;
|
|
90
117
|
font: inherit;
|
|
91
|
-
font-size: 0.
|
|
118
|
+
font-size: 0.9375rem;
|
|
92
119
|
font-weight: 500;
|
|
93
120
|
white-space: nowrap;
|
|
94
121
|
cursor: pointer;
|
|
95
122
|
color: inherit;
|
|
96
123
|
}
|
|
97
124
|
|
|
125
|
+
/* A hairline between actions, the way the platform's own callout separates
|
|
126
|
+
Copy from Look Up — cheaper than a box around each. */
|
|
127
|
+
.lalt-toolbar > * + * > .lalt-toolbar__button,
|
|
128
|
+
.lalt-toolbar > .lalt-toolbar__button + .lalt-toolbar__button {
|
|
129
|
+
box-shadow: inset 1px 0 0 var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.lalt-toolbar__button:active {
|
|
133
|
+
background: var(--lalt-editor-active, rgb(0 0 0 / 0.12));
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
@media (hover: hover) and (pointer: fine) {
|
|
99
137
|
.lalt-toolbar__button {
|
|
100
138
|
min-height: 0;
|
|
101
|
-
padding: 0.
|
|
139
|
+
padding: 0.3125rem 0.625rem;
|
|
140
|
+
font-size: 0.875rem;
|
|
102
141
|
}
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
.lalt-toolbar__button:hover {
|
|
106
|
-
background: var(--lalt-editor-
|
|
145
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
107
146
|
}
|
|
108
147
|
|
|
109
148
|
/* The format trigger opens a menu; the caret is what says so. */
|
|
@@ -121,19 +160,25 @@
|
|
|
121
160
|
position: relative;
|
|
122
161
|
}
|
|
123
162
|
|
|
124
|
-
/* Opens upward from
|
|
125
|
-
|
|
163
|
+
/* Opens upward from the pill: a menu anchored below it would open off the
|
|
164
|
+
bottom of the screen, or into the keyboard. Card-shaped rather than
|
|
165
|
+
full-bleed, so it reads as belonging to the bar it grew out of. */
|
|
126
166
|
.lalt-toolbar__menu {
|
|
127
167
|
position: fixed;
|
|
128
|
-
left:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
left: 50%;
|
|
169
|
+
bottom: calc(var(--lalt-keyboard-inset, 0px) + env(safe-area-inset-bottom) + 3.75rem);
|
|
170
|
+
transform: translateX(-50%);
|
|
171
|
+
min-width: 12rem;
|
|
172
|
+
max-width: calc(100vw - 2rem);
|
|
173
|
+
max-height: 50vh;
|
|
132
174
|
overflow-y: auto;
|
|
133
175
|
padding: 0.25rem;
|
|
134
176
|
background: var(--lalt-editor-surface, #ffffff);
|
|
135
|
-
border
|
|
136
|
-
|
|
177
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
178
|
+
border-radius: 0.875rem;
|
|
179
|
+
box-shadow:
|
|
180
|
+
0 8px 24px rgb(0 0 0 / 0.18),
|
|
181
|
+
0 1px 3px rgb(0 0 0 / 0.1);
|
|
137
182
|
color: var(--lalt-editor-text, #111827);
|
|
138
183
|
}
|
|
139
184
|
|
|
@@ -144,9 +189,10 @@
|
|
|
144
189
|
right: 0;
|
|
145
190
|
bottom: auto;
|
|
146
191
|
top: calc(100% + 0.375rem);
|
|
192
|
+
transform: none;
|
|
147
193
|
min-width: 14rem;
|
|
148
|
-
|
|
149
|
-
border-radius: 0.
|
|
194
|
+
max-width: none;
|
|
195
|
+
border-radius: 0.625rem;
|
|
150
196
|
box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
|
|
151
197
|
}
|
|
152
198
|
}
|
|
@@ -159,7 +205,7 @@
|
|
|
159
205
|
width: 100%;
|
|
160
206
|
min-height: 2.75rem;
|
|
161
207
|
padding: 0.5rem 0.75rem;
|
|
162
|
-
border-radius: 0.
|
|
208
|
+
border-radius: 0.625rem;
|
|
163
209
|
background: none;
|
|
164
210
|
border: none;
|
|
165
211
|
font: inherit;
|
package/dist/index.d.ts
CHANGED
|
@@ -76,6 +76,45 @@ interface SelectionInfo {
|
|
|
76
76
|
*/
|
|
77
77
|
declare function useSelectionToolbar(editor: Editor | null): SelectionInfo | null;
|
|
78
78
|
|
|
79
|
+
interface VisualViewportLike {
|
|
80
|
+
width: number;
|
|
81
|
+
height: number;
|
|
82
|
+
offsetTop: number;
|
|
83
|
+
}
|
|
84
|
+
interface ViewportMetrics {
|
|
85
|
+
width: number;
|
|
86
|
+
height: number;
|
|
87
|
+
offsetTop: number;
|
|
88
|
+
/** How much of the layout viewport's bottom is covered, the on-screen
|
|
89
|
+
* keyboard included. Zero on a desktop browser and wherever the API is
|
|
90
|
+
* missing. */
|
|
91
|
+
bottomInset: number;
|
|
92
|
+
}
|
|
93
|
+
interface ReadViewportInput {
|
|
94
|
+
innerWidth: number;
|
|
95
|
+
innerHeight: number;
|
|
96
|
+
visual?: VisualViewportLike | null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* readViewport reports the area a fixed element can actually occupy.
|
|
100
|
+
*
|
|
101
|
+
* The distinction the on-screen keyboard turns on: `innerHeight` measures the
|
|
102
|
+
* layout viewport, which the keyboard covers without shrinking, so an element
|
|
103
|
+
* pinned to its bottom edge ends up underneath the keys. Only `visualViewport`
|
|
104
|
+
* narrows, and the difference is the inset a docked toolbar has to clear.
|
|
105
|
+
*/
|
|
106
|
+
declare function readViewport({ innerWidth, innerHeight, visual, }: ReadViewportInput): ViewportMetrics;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* useViewport tracks the area a fixed element can occupy, keyboard included.
|
|
110
|
+
*
|
|
111
|
+
* The visual viewport's own events are the only ones a raised keyboard fires:
|
|
112
|
+
* `window.resize` does not run on iOS when the keyboard opens, so a component
|
|
113
|
+
* listening to it alone keeps the measurements it took before the keys
|
|
114
|
+
* appeared.
|
|
115
|
+
*/
|
|
116
|
+
declare function useViewport(): ViewportMetrics;
|
|
117
|
+
|
|
79
118
|
interface FormatLabels {
|
|
80
119
|
paragraph: string;
|
|
81
120
|
heading1: string;
|
|
@@ -157,4 +196,4 @@ declare function splitRevision(before: string, after: string): RevisionPart[];
|
|
|
157
196
|
*/
|
|
158
197
|
declare function docToMarkdown(doc: RichDoc): string;
|
|
159
198
|
|
|
160
|
-
export { type BlockFormat, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type RevisionPart, RichDoc, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, blockFormats, defaultFormatLabels, docToMarkdown, expandToWords, inlineSuggestionsKey, placeToolbar, splitRevision, useSelectionToolbar };
|
|
199
|
+
export { type BlockFormat, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type ReadViewportInput, type RevisionPart, RichDoc, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, type ViewportMetrics, type VisualViewportLike, blockFormats, defaultFormatLabels, docToMarkdown, expandToWords, inlineSuggestionsKey, placeToolbar, readViewport, splitRevision, useSelectionToolbar, useViewport };
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,61 @@ function placeToolbar({
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/SelectionToolbar.tsx
|
|
26
|
-
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
26
|
+
import { useEffect as useEffect2, useLayoutEffect, useRef, useState as useState2 } from "react";
|
|
27
27
|
import { createPortal } from "react-dom";
|
|
28
|
+
|
|
29
|
+
// src/useViewport.ts
|
|
30
|
+
import { useEffect, useState } from "react";
|
|
31
|
+
|
|
32
|
+
// src/viewport.ts
|
|
33
|
+
function readViewport({
|
|
34
|
+
innerWidth,
|
|
35
|
+
innerHeight,
|
|
36
|
+
visual
|
|
37
|
+
}) {
|
|
38
|
+
if (!visual) {
|
|
39
|
+
return { width: innerWidth, height: innerHeight, offsetTop: 0, bottomInset: 0 };
|
|
40
|
+
}
|
|
41
|
+
const covered = innerHeight - visual.height - visual.offsetTop;
|
|
42
|
+
return {
|
|
43
|
+
width: visual.width,
|
|
44
|
+
height: visual.height,
|
|
45
|
+
offsetTop: visual.offsetTop,
|
|
46
|
+
bottomInset: Math.max(0, Math.round(covered))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/useViewport.ts
|
|
51
|
+
var SERVER = { width: 0, height: 0, offsetTop: 0, bottomInset: 0 };
|
|
52
|
+
function read() {
|
|
53
|
+
if (typeof window === "undefined") return SERVER;
|
|
54
|
+
return readViewport({
|
|
55
|
+
innerWidth: window.innerWidth,
|
|
56
|
+
innerHeight: window.innerHeight,
|
|
57
|
+
visual: window.visualViewport
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function useViewport() {
|
|
61
|
+
const [metrics, setMetrics] = useState(read);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const update = () => setMetrics(read());
|
|
64
|
+
update();
|
|
65
|
+
const visual = window.visualViewport;
|
|
66
|
+
visual?.addEventListener("resize", update);
|
|
67
|
+
visual?.addEventListener("scroll", update);
|
|
68
|
+
window.addEventListener("resize", update);
|
|
69
|
+
window.addEventListener("orientationchange", update);
|
|
70
|
+
return () => {
|
|
71
|
+
visual?.removeEventListener("resize", update);
|
|
72
|
+
visual?.removeEventListener("scroll", update);
|
|
73
|
+
window.removeEventListener("resize", update);
|
|
74
|
+
window.removeEventListener("orientationchange", update);
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
return metrics;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/SelectionToolbar.tsx
|
|
28
81
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
29
82
|
var FALLBACK = { width: 320, height: 40 };
|
|
30
83
|
function SelectionToolbar({
|
|
@@ -37,15 +90,16 @@ function SelectionToolbar({
|
|
|
37
90
|
container
|
|
38
91
|
}) {
|
|
39
92
|
const ref = useRef(null);
|
|
40
|
-
const [size, setSize] =
|
|
41
|
-
const [formatsOpen, setFormatsOpen] =
|
|
93
|
+
const [size, setSize] = useState2(FALLBACK);
|
|
94
|
+
const [formatsOpen, setFormatsOpen] = useState2(false);
|
|
95
|
+
const viewport = useViewport();
|
|
42
96
|
useLayoutEffect(() => {
|
|
43
97
|
const el = ref.current;
|
|
44
98
|
if (!el) return;
|
|
45
99
|
const rect = el.getBoundingClientRect();
|
|
46
100
|
if (rect.width > 0) setSize({ width: rect.width, height: rect.height });
|
|
47
101
|
}, [selectedText, actions.length, formats.length]);
|
|
48
|
-
|
|
102
|
+
useEffect2(() => {
|
|
49
103
|
if (!formatsOpen) return;
|
|
50
104
|
const close = (e) => {
|
|
51
105
|
if (ref.current && !ref.current.contains(e.target)) setFormatsOpen(false);
|
|
@@ -57,7 +111,7 @@ function SelectionToolbar({
|
|
|
57
111
|
const place = placeToolbar({
|
|
58
112
|
selection,
|
|
59
113
|
toolbar: size,
|
|
60
|
-
viewport: { width:
|
|
114
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
61
115
|
preferBelow
|
|
62
116
|
});
|
|
63
117
|
return createPortal(
|
|
@@ -69,7 +123,8 @@ function SelectionToolbar({
|
|
|
69
123
|
className: "lalt-toolbar",
|
|
70
124
|
style: {
|
|
71
125
|
"--lalt-toolbar-left": `${place.left}px`,
|
|
72
|
-
"--lalt-toolbar-top": `${place.top}px
|
|
126
|
+
"--lalt-toolbar-top": `${place.top}px`,
|
|
127
|
+
"--lalt-keyboard-inset": `${viewport.bottomInset}px`
|
|
73
128
|
},
|
|
74
129
|
children: [
|
|
75
130
|
actions.map((action) => /* @__PURE__ */ jsxs(
|
|
@@ -127,12 +182,12 @@ function SelectionToolbar({
|
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
// src/useSelectionToolbar.ts
|
|
130
|
-
import { useEffect as
|
|
185
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
131
186
|
function useSelectionToolbar(editor) {
|
|
132
|
-
const [selection, setSelection] =
|
|
133
|
-
|
|
187
|
+
const [selection, setSelection] = useState3(null);
|
|
188
|
+
useEffect3(() => {
|
|
134
189
|
if (!editor) return;
|
|
135
|
-
const
|
|
190
|
+
const read2 = () => {
|
|
136
191
|
const { state } = editor;
|
|
137
192
|
const { from, to, empty } = state.selection;
|
|
138
193
|
if (empty || !editor.isFocused) {
|
|
@@ -155,14 +210,14 @@ function useSelectionToolbar(editor) {
|
|
|
155
210
|
}
|
|
156
211
|
});
|
|
157
212
|
};
|
|
158
|
-
|
|
159
|
-
editor.on("selectionUpdate",
|
|
160
|
-
editor.on("blur",
|
|
161
|
-
editor.on("focus",
|
|
213
|
+
read2();
|
|
214
|
+
editor.on("selectionUpdate", read2);
|
|
215
|
+
editor.on("blur", read2);
|
|
216
|
+
editor.on("focus", read2);
|
|
162
217
|
return () => {
|
|
163
|
-
editor.off("selectionUpdate",
|
|
164
|
-
editor.off("blur",
|
|
165
|
-
editor.off("focus",
|
|
218
|
+
editor.off("selectionUpdate", read2);
|
|
219
|
+
editor.off("blur", read2);
|
|
220
|
+
editor.off("focus", read2);
|
|
166
221
|
};
|
|
167
222
|
}, [editor]);
|
|
168
223
|
return selection;
|
|
@@ -396,8 +451,10 @@ export {
|
|
|
396
451
|
expandToWords,
|
|
397
452
|
inlineSuggestionsKey,
|
|
398
453
|
placeToolbar,
|
|
454
|
+
readViewport,
|
|
399
455
|
shapesFromDoc,
|
|
400
456
|
splitRevision,
|
|
401
|
-
useSelectionToolbar
|
|
457
|
+
useSelectionToolbar,
|
|
458
|
+
useViewport
|
|
402
459
|
};
|
|
403
460
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useSelectionToolbar.ts","../src/formats.ts","../src/InlineSuggestions.ts","../src/passage.ts","../src/markdown.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","import type { RichDoc, RichNode } from \"./document\";\n\ninterface Mark {\n type?: string;\n attrs?: Record<string, unknown>;\n}\n\n/**\n * docToMarkdown renders a TipTap document as the markdown a blog accepts.\n *\n * Written here rather than taken from a library: the editor only ever produces\n * the handful of nodes StarterKit gives it, and a general markdown serializer\n * brings a dependency and its escaping rules for output a human is about to\n * paste into their own site.\n */\nexport function docToMarkdown(doc: RichDoc): string {\n return blocks(doc.content ?? [])\n .filter((b) => b !== \"\")\n .join(\"\\n\\n\");\n}\n\nfunction blocks(nodes: RichNode[]): string[] {\n return nodes.map(block);\n}\n\nfunction block(node: RichNode): string {\n switch (node.type) {\n case \"heading\": {\n const level = Number(node.attrs?.level ?? 2);\n return `${\"#\".repeat(Math.min(6, Math.max(1, level)))} ${inline(node.content)}`;\n }\n case \"blockquote\":\n return blocks(node.content ?? [])\n .filter((b) => b !== \"\")\n .map((b) =>\n b\n .split(\"\\n\")\n .map((line) => `> ${line}`)\n .join(\"\\n\"),\n )\n .join(\"\\n>\\n\");\n case \"codeBlock\": {\n const language = String(node.attrs?.language ?? \"\");\n return `\\`\\`\\`${language}\\n${inline(node.content)}\\n\\`\\`\\``;\n }\n case \"bulletList\":\n return listItems(node).map((item) => `- ${item}`).join(\"\\n\");\n case \"orderedList\":\n return listItems(node)\n .map((item, i) => `${i + 1}. ${item}`)\n .join(\"\\n\");\n case \"horizontalRule\":\n return \"---\";\n default:\n return inline(node.content);\n }\n}\n\nfunction listItems(list: RichNode): string[] {\n return (list.content ?? []).map((item) =>\n blocks(item.content ?? [])\n .filter((b) => b !== \"\")\n // Continuation lines are indented so the item stays one item.\n .join(\"\\n\\n\")\n .split(\"\\n\")\n .join(\"\\n \"),\n );\n}\n\nfunction inline(nodes: RichNode[] | undefined): string {\n return (nodes ?? []).map(text).join(\"\");\n}\n\nfunction text(node: RichNode): string {\n if (node.type === \"hardBreak\") return \"\\n\";\n if (typeof node.text !== \"string\") return inline(node.content);\n\n let out = node.text;\n // Innermost first, so bold inside a link wraps the text and not the URL.\n for (const mark of ((node.marks as Mark[] | undefined) ?? []).slice().reverse()) {\n switch (mark.type) {\n case \"bold\":\n out = `**${out}**`;\n break;\n case \"italic\":\n out = `*${out}*`;\n break;\n case \"strike\":\n out = `~~${out}~~`;\n break;\n case \"code\":\n out = `\\`${out}\\``;\n break;\n case \"link\":\n out = `[${out}](${String(mark.attrs?.href ?? \"\")})`;\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":["useEffect","useState","text","text","text"]}
|
|
1
|
+
{"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useViewport.ts","../src/viewport.ts","../src/useSelectionToolbar.ts","../src/formats.ts","../src/InlineSuggestions.ts","../src/passage.ts","../src/markdown.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\";\nimport { useViewport } from \"./useViewport\";\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 const viewport = useViewport();\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: viewport.width, height: viewport.height },\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 //\n // The inset is what the docked bar sits on top of. `bottom: 0` means the\n // bottom of the layout viewport, which the on-screen keyboard covers\n // without shrinking — so the bar has to be lifted by the strip the\n // keyboard occupies to stay in sight.\n style={\n {\n \"--lalt-toolbar-left\": `${place.left}px`,\n \"--lalt-toolbar-top\": `${place.top}px`,\n \"--lalt-keyboard-inset\": `${viewport.bottomInset}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\";\n\nimport { readViewport, type ViewportMetrics } from \"./viewport\";\n\nconst SERVER: ViewportMetrics = { width: 0, height: 0, offsetTop: 0, bottomInset: 0 };\n\nfunction read(): ViewportMetrics {\n if (typeof window === \"undefined\") return SERVER;\n return readViewport({\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n visual: window.visualViewport,\n });\n}\n\n/**\n * useViewport tracks the area a fixed element can occupy, keyboard included.\n *\n * The visual viewport's own events are the only ones a raised keyboard fires:\n * `window.resize` does not run on iOS when the keyboard opens, so a component\n * listening to it alone keeps the measurements it took before the keys\n * appeared.\n */\nexport function useViewport(): ViewportMetrics {\n const [metrics, setMetrics] = useState<ViewportMetrics>(read);\n\n useEffect(() => {\n const update = () => setMetrics(read());\n update();\n\n const visual = window.visualViewport;\n visual?.addEventListener(\"resize\", update);\n // iOS scrolls the visual viewport rather than the document to keep the\n // caret above the keyboard, and reports it here and nowhere else.\n visual?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n window.addEventListener(\"orientationchange\", update);\n\n return () => {\n visual?.removeEventListener(\"resize\", update);\n visual?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n window.removeEventListener(\"orientationchange\", update);\n };\n }, []);\n\n return metrics;\n}\n","export interface VisualViewportLike {\n width: number;\n height: number;\n offsetTop: number;\n}\n\nexport interface ViewportMetrics {\n width: number;\n height: number;\n offsetTop: number;\n /** How much of the layout viewport's bottom is covered, the on-screen\n * keyboard included. Zero on a desktop browser and wherever the API is\n * missing. */\n bottomInset: number;\n}\n\nexport interface ReadViewportInput {\n innerWidth: number;\n innerHeight: number;\n visual?: VisualViewportLike | null;\n}\n\n/**\n * readViewport reports the area a fixed element can actually occupy.\n *\n * The distinction the on-screen keyboard turns on: `innerHeight` measures the\n * layout viewport, which the keyboard covers without shrinking, so an element\n * pinned to its bottom edge ends up underneath the keys. Only `visualViewport`\n * narrows, and the difference is the inset a docked toolbar has to clear.\n */\nexport function readViewport({\n innerWidth,\n innerHeight,\n visual,\n}: ReadViewportInput): ViewportMetrics {\n if (!visual) {\n return { width: innerWidth, height: innerHeight, offsetTop: 0, bottomInset: 0 };\n }\n\n // offsetTop is what iOS scrolls the visual viewport by; without it the\n // covered strip reads as smaller than it is once the page is scrolled under\n // a raised keyboard. Never negative: a viewport reported as taller than the\n // layout one — Android during a rotation, a browser mid-resize — would\n // otherwise push the toolbar down off the screen.\n const covered = innerHeight - visual.height - visual.offsetTop;\n\n return {\n width: visual.width,\n height: visual.height,\n offsetTop: visual.offsetTop,\n bottomInset: Math.max(0, Math.round(covered)),\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","import type { RichDoc, RichNode } from \"./document\";\n\ninterface Mark {\n type?: string;\n attrs?: Record<string, unknown>;\n}\n\n/**\n * docToMarkdown renders a TipTap document as the markdown a blog accepts.\n *\n * Written here rather than taken from a library: the editor only ever produces\n * the handful of nodes StarterKit gives it, and a general markdown serializer\n * brings a dependency and its escaping rules for output a human is about to\n * paste into their own site.\n */\nexport function docToMarkdown(doc: RichDoc): string {\n return blocks(doc.content ?? [])\n .filter((b) => b !== \"\")\n .join(\"\\n\\n\");\n}\n\nfunction blocks(nodes: RichNode[]): string[] {\n return nodes.map(block);\n}\n\nfunction block(node: RichNode): string {\n switch (node.type) {\n case \"heading\": {\n const level = Number(node.attrs?.level ?? 2);\n return `${\"#\".repeat(Math.min(6, Math.max(1, level)))} ${inline(node.content)}`;\n }\n case \"blockquote\":\n return blocks(node.content ?? [])\n .filter((b) => b !== \"\")\n .map((b) =>\n b\n .split(\"\\n\")\n .map((line) => `> ${line}`)\n .join(\"\\n\"),\n )\n .join(\"\\n>\\n\");\n case \"codeBlock\": {\n const language = String(node.attrs?.language ?? \"\");\n return `\\`\\`\\`${language}\\n${inline(node.content)}\\n\\`\\`\\``;\n }\n case \"bulletList\":\n return listItems(node).map((item) => `- ${item}`).join(\"\\n\");\n case \"orderedList\":\n return listItems(node)\n .map((item, i) => `${i + 1}. ${item}`)\n .join(\"\\n\");\n case \"horizontalRule\":\n return \"---\";\n default:\n return inline(node.content);\n }\n}\n\nfunction listItems(list: RichNode): string[] {\n return (list.content ?? []).map((item) =>\n blocks(item.content ?? [])\n .filter((b) => b !== \"\")\n // Continuation lines are indented so the item stays one item.\n .join(\"\\n\\n\")\n .split(\"\\n\")\n .join(\"\\n \"),\n );\n}\n\nfunction inline(nodes: RichNode[] | undefined): string {\n return (nodes ?? []).map(text).join(\"\");\n}\n\nfunction text(node: RichNode): string {\n if (node.type === \"hardBreak\") return \"\\n\";\n if (typeof node.text !== \"string\") return inline(node.content);\n\n let out = node.text;\n // Innermost first, so bold inside a link wraps the text and not the URL.\n for (const mark of ((node.marks as Mark[] | undefined) ?? []).slice().reverse()) {\n switch (mark.type) {\n case \"bold\":\n out = `**${out}**`;\n break;\n case \"italic\":\n out = `*${out}*`;\n break;\n case \"strike\":\n out = `~~${out}~~`;\n break;\n case \"code\":\n out = `\\`${out}\\``;\n break;\n case \"link\":\n out = `[${out}](${String(mark.attrs?.href ?? \"\")})`;\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":["useEffect","useState","useState","useEffect","useEffect","useState","read","text","text","text"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalternative/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"build": "tsup",
|
|
24
24
|
"dev": "tsup --watch",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test": "vitest run"
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"playground": "vite --config playground/vite.config.ts"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"@tiptap/core": ">=3.0.0",
|
|
@@ -40,10 +41,12 @@
|
|
|
40
41
|
"@tiptap/starter-kit": "^3.29.2",
|
|
41
42
|
"@types/react": "^19.0.0",
|
|
42
43
|
"@types/react-dom": "^19.0.0",
|
|
44
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
43
45
|
"react": "^19.0.0",
|
|
44
46
|
"react-dom": "^19.0.0",
|
|
45
47
|
"tsup": "^8.5.1",
|
|
46
48
|
"typescript": "^5.7.0",
|
|
49
|
+
"vite": "^7.1.7",
|
|
47
50
|
"vitest": "^3.0.0"
|
|
48
51
|
},
|
|
49
52
|
"publishConfig": {
|