@lalternative/editor 0.2.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/editor.css CHANGED
@@ -6,12 +6,14 @@
6
6
  --lalt-editor-delete: #b91c1c;
7
7
  --lalt-editor-delete-bg: rgb(185 28 28 / 0.06);
8
8
  --lalt-editor-surface: #ffffff;
9
+ --lalt-editor-text: #111827;
9
10
  --lalt-editor-border: rgb(0 0 0 / 0.1);
10
11
  --lalt-editor-hover: rgb(0 0 0 / 0.06);
11
12
  }
12
13
 
13
- /* Defaults only: a host that owns a theme overrides these on :root and never
14
- reaches this block. Without it the toolbar is a white slab on a dark page. */
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. */
15
17
  @media (prefers-color-scheme: dark) {
16
18
  :root {
17
19
  --lalt-editor-insert: #93c5fd;
@@ -19,37 +21,80 @@
19
21
  --lalt-editor-delete: #fca5a5;
20
22
  --lalt-editor-delete-bg: rgb(252 165 165 / 0.1);
21
23
  --lalt-editor-surface: #1f2937;
24
+ --lalt-editor-text: #f3f4f6;
22
25
  --lalt-editor-border: rgb(255 255 255 / 0.14);
23
26
  --lalt-editor-hover: rgb(255 255 255 / 0.08);
24
27
  }
25
28
  }
26
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. */
27
37
  .lalt-toolbar {
38
+ position: fixed;
39
+ left: 0;
40
+ right: 0;
41
+ bottom: 0;
42
+ z-index: 100;
28
43
  display: flex;
29
44
  align-items: center;
30
- gap: 0.125rem;
31
- padding: 0.25rem;
32
- border-radius: 0.5rem;
45
+ gap: 0.25rem;
46
+ padding: 0.5rem;
47
+ padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
33
48
  background: var(--lalt-editor-surface, #ffffff);
34
- border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
35
- box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
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);
36
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
+ }
37
70
  }
38
71
 
39
72
  .lalt-toolbar__button {
40
73
  display: inline-flex;
41
74
  align-items: center;
75
+ justify-content: center;
42
76
  gap: 0.375rem;
43
- padding: 0.25rem 0.5rem;
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;
44
81
  border-radius: 0.375rem;
45
82
  background: none;
46
83
  border: none;
47
84
  font: inherit;
48
85
  font-size: 0.875rem;
86
+ white-space: nowrap;
49
87
  cursor: pointer;
50
88
  color: inherit;
51
89
  }
52
90
 
91
+ @media (min-width: 640px) {
92
+ .lalt-toolbar__button {
93
+ min-height: 0;
94
+ padding: 0.25rem 0.5rem;
95
+ }
96
+ }
97
+
53
98
  .lalt-toolbar__button:hover {
54
99
  background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
55
100
  }
@@ -58,16 +103,34 @@
58
103
  position: relative;
59
104
  }
60
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. */
61
108
  .lalt-toolbar__menu {
62
- position: absolute;
63
- top: calc(100% + 0.375rem);
109
+ position: fixed;
110
+ left: 0;
64
111
  right: 0;
65
- min-width: 14rem;
112
+ bottom: 100%;
113
+ max-height: 60vh;
114
+ overflow-y: auto;
66
115
  padding: 0.25rem;
67
- border-radius: 0.5rem;
68
116
  background: var(--lalt-editor-surface, #ffffff);
69
- border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
70
- box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
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
+ }
71
134
  }
72
135
 
73
136
  .lalt-toolbar__item {
@@ -76,17 +139,26 @@
76
139
  justify-content: space-between;
77
140
  gap: 1.5rem;
78
141
  width: 100%;
79
- padding: 0.375rem 0.5rem;
142
+ min-height: 2.75rem;
143
+ padding: 0.5rem 0.75rem;
80
144
  border-radius: 0.375rem;
81
145
  background: none;
82
146
  border: none;
83
147
  font: inherit;
84
- font-size: 0.875rem;
148
+ font-size: 0.9375rem;
85
149
  text-align: left;
86
150
  cursor: pointer;
87
151
  color: inherit;
88
152
  }
89
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
+
90
162
  .lalt-toolbar__item:hover {
91
163
  background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
92
164
  }
@@ -95,11 +167,19 @@
95
167
  font-weight: 600;
96
168
  }
97
169
 
170
+ /* Hidden where there is no keyboard to press them with. */
98
171
  .lalt-toolbar__shortcut {
172
+ display: none;
99
173
  font-size: 0.75rem;
100
174
  opacity: 0.5;
101
175
  }
102
176
 
177
+ @media (min-width: 640px) {
178
+ .lalt-toolbar__shortcut {
179
+ display: inline;
180
+ }
181
+ }
182
+
103
183
  .lalt-revision {
104
184
  display: block;
105
185
  border-radius: 0.375rem;
@@ -123,3 +203,62 @@
123
203
  opacity: 0.45;
124
204
  background: none;
125
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,6 +1,9 @@
1
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
2
  import * as react from 'react';
3
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';
4
7
 
5
8
  interface SelectionRect {
6
9
  /** Horizontal centre of the selection. */
@@ -89,4 +92,58 @@ declare const defaultFormatLabels: FormatLabels;
89
92
  */
90
93
  declare function blockFormats(editor: Editor | null, labels?: FormatLabels): BlockFormat[];
91
94
 
92
- export { type BlockFormat, type FormatLabels, type PlaceToolbarInput, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type ToolbarAction, type ToolbarPlacement, blockFormats, defaultFormatLabels, placeToolbar, useSelectionToolbar };
95
+ interface InlineSuggestion {
96
+ id: string;
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 {
127
+ before: string;
128
+ after: string;
129
+ }
130
+ /**
131
+ * expandToWords grows a range out to whole words.
132
+ *
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.
140
+ *
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.
146
+ */
147
+ declare function splitRevision(before: string, after: string): RevisionPart[];
148
+
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
@@ -67,7 +67,10 @@ function SelectionToolbar({
67
67
  ref,
68
68
  role: "toolbar",
69
69
  className: "lalt-toolbar",
70
- style: { position: "fixed", left: place.left, top: place.top, zIndex: 100 },
70
+ style: {
71
+ "--lalt-toolbar-left": `${place.left}px`,
72
+ "--lalt-toolbar-top": `${place.top}px`
73
+ },
71
74
  children: [
72
75
  actions.map((action) => /* @__PURE__ */ jsxs(
73
76
  "button",
@@ -176,46 +179,157 @@ var defaultFormatLabels = {
176
179
  };
177
180
  function blockFormats(editor, labels = defaultFormatLabels) {
178
181
  if (!editor) return [];
182
+ const mod = isApple() ? "\u2318" : "Ctrl";
179
183
  return [
180
184
  {
181
185
  id: "paragraph",
182
186
  label: labels.paragraph,
183
- shortcut: "Ctrl+Alt+0",
184
187
  isActive: editor.isActive("paragraph"),
185
188
  onSelect: () => editor.chain().focus().setParagraph().run()
186
189
  },
187
190
  ...[1, 2, 3].map((level) => ({
188
191
  id: `heading${level}`,
189
192
  label: labels[`heading${level}`],
190
- shortcut: `Ctrl+Alt+${level}`,
193
+ shortcut: `${mod}+Alt+${level}`,
191
194
  isActive: editor.isActive("heading", { level }),
192
195
  onSelect: () => editor.chain().focus().toggleHeading({ level }).run()
193
196
  })),
194
197
  {
195
198
  id: "orderedList",
196
199
  label: labels.orderedList,
197
- shortcut: "Ctrl+Alt+4",
200
+ shortcut: `${mod}+Shift+7`,
198
201
  isActive: editor.isActive("orderedList"),
199
202
  onSelect: () => editor.chain().focus().toggleOrderedList().run()
200
203
  },
201
204
  {
202
205
  id: "bulletList",
203
206
  label: labels.bulletList,
204
- shortcut: "Ctrl+Alt+5",
207
+ shortcut: `${mod}+Shift+8`,
205
208
  isActive: editor.isActive("bulletList"),
206
209
  onSelect: () => editor.chain().focus().toggleBulletList().run()
207
210
  }
208
211
  ];
209
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
+ }
210
320
  export {
321
+ InlineSuggestions,
211
322
  SelectionToolbar,
212
323
  blockFormats,
213
324
  blocksFromDoc,
214
325
  defaultFormatLabels,
215
326
  diffBlocks,
216
327
  docFromBlocks,
328
+ expandToWords,
329
+ inlineSuggestionsKey,
217
330
  placeToolbar,
218
331
  shapesFromDoc,
332
+ splitRevision,
219
333
  useSelectionToolbar
220
334
  };
221
335
  //# 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"],"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 style={{ position: \"fixed\", left: place.left, top: place.top, zIndex: 100 }}\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 return [\n {\n id: \"paragraph\",\n label: labels.paragraph,\n shortcut: \"Ctrl+Alt+0\",\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: `Ctrl+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: \"Ctrl+Alt+4\",\n isActive: editor.isActive(\"orderedList\"),\n onSelect: () => editor.chain().focus().toggleOrderedList().run(),\n },\n {\n id: \"bulletList\",\n label: labels.bulletList,\n shortcut: \"Ctrl+Alt+5\",\n isActive: editor.isActive(\"bulletList\"),\n onSelect: () => editor.chain().focus().toggleBulletList().run(),\n },\n ];\n}\n"],"mappings":"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","names":["useEffect","useState"]}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/editor",
3
- "version": "0.2.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",