@lalternative/editor 0.7.0 → 0.8.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
@@ -326,3 +326,255 @@
326
326
  .lalt-suggestion__action:hover {
327
327
  background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
328
328
  }
329
+
330
+ /* The `/` menu, anchored to the caret by the extension and painted here.
331
+
332
+ Fixed to the viewport rather than positioned in the document: the menu is
333
+ appended to the body so an editor inside an `overflow: hidden` pane cannot
334
+ clip it. */
335
+ .lalt-slash-anchor {
336
+ position: fixed;
337
+ z-index: 10000;
338
+ }
339
+
340
+ .lalt-slash {
341
+ min-width: 16rem;
342
+ max-width: min(20rem, calc(100vw - 1.5rem));
343
+ overflow: hidden;
344
+ border: 1px solid var(--lalt-editor-border);
345
+ border-radius: 0.5rem;
346
+ background: var(--lalt-editor-surface);
347
+ color: var(--lalt-editor-text);
348
+ box-shadow: 0 10px 20px rgb(0 0 0 / 0.12);
349
+ }
350
+
351
+ .lalt-slash--empty {
352
+ padding: 0.5rem 0.75rem;
353
+ font-size: 0.75rem;
354
+ opacity: 0.6;
355
+ }
356
+
357
+ .lalt-slash__item {
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 0.625rem;
361
+ width: 100%;
362
+ padding: 0.5rem 0.75rem;
363
+ border: 0;
364
+ background: transparent;
365
+ color: inherit;
366
+ font: inherit;
367
+ font-size: 0.8125rem;
368
+ text-align: left;
369
+ cursor: pointer;
370
+ }
371
+
372
+ /* Room for a thumb where there is no pointer to aim with. */
373
+ @media not (hover: hover), (pointer: coarse) {
374
+ .lalt-slash__item {
375
+ padding: 0.6875rem 0.75rem;
376
+ }
377
+ }
378
+
379
+ .lalt-slash__item--active,
380
+ .lalt-slash__item:hover {
381
+ background: var(--lalt-editor-hover);
382
+ }
383
+
384
+ .lalt-slash__icon {
385
+ display: flex;
386
+ flex-shrink: 0;
387
+ opacity: 0.55;
388
+ }
389
+
390
+ .lalt-slash__text {
391
+ display: flex;
392
+ flex-direction: column;
393
+ min-width: 0;
394
+ flex: 1;
395
+ }
396
+
397
+ .lalt-slash__title {
398
+ font-weight: 500;
399
+ line-height: 1.25;
400
+ }
401
+
402
+ .lalt-slash__description {
403
+ overflow: hidden;
404
+ font-size: 0.6875rem;
405
+ line-height: 1.25;
406
+ text-overflow: ellipsis;
407
+ white-space: nowrap;
408
+ opacity: 0.55;
409
+ }
410
+
411
+ .lalt-slash__shortcut {
412
+ flex-shrink: 0;
413
+ margin-left: 0.5rem;
414
+ padding: 0.125rem 0.375rem;
415
+ border: 1px solid var(--lalt-editor-border);
416
+ border-radius: 0.25rem;
417
+ font-family: ui-monospace, monospace;
418
+ font-size: 0.625rem;
419
+ opacity: 0.6;
420
+ }
421
+
422
+ /* The page a writing surface sits in.
423
+
424
+ Sized in dvh rather than vh: on mobile Safari a vh unit keeps counting the
425
+ browser chrome that scrolls away, so the column runs past the bottom of the
426
+ screen and the last paragraph is unreachable. */
427
+ .lalt-screen {
428
+ display: flex;
429
+ flex-direction: column;
430
+ height: var(--lalt-screen-height, 100dvh);
431
+ min-height: 0;
432
+ color: var(--lalt-editor-text);
433
+ }
434
+
435
+ /* A host mounting the screen inside its own shell — a phone frame, a panel,
436
+ a route that already owns the page height — sets the height to 100% and
437
+ lets its container do the sizing. */
438
+ .lalt-screen--fill {
439
+ --lalt-screen-height: 100%;
440
+ }
441
+
442
+ .lalt-screen__bar {
443
+ position: sticky;
444
+ top: 0;
445
+ z-index: 20;
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 0.5rem;
449
+ padding: 0.5rem 0.75rem;
450
+ border-bottom: 1px solid var(--lalt-editor-border);
451
+ background: var(--lalt-editor-surface);
452
+ }
453
+
454
+ .lalt-screen__status {
455
+ min-width: 0;
456
+ font-size: 0.75rem;
457
+ font-variant-numeric: tabular-nums;
458
+ opacity: 0.7;
459
+ }
460
+
461
+ .lalt-screen__actions {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 0.375rem;
465
+ margin-left: auto;
466
+ }
467
+
468
+ .lalt-screen__subnav {
469
+ position: sticky;
470
+ top: 0;
471
+ z-index: 15;
472
+ border-bottom: 1px solid var(--lalt-editor-border);
473
+ background: var(--lalt-editor-surface);
474
+ }
475
+
476
+ /* The scroller is here, not on the body: the bar above it has to stay put
477
+ while the prose moves, and a document taller than the screen must not drag
478
+ the whole page under a raised keyboard. */
479
+ .lalt-screen__scroll {
480
+ flex: 1;
481
+ min-height: 0;
482
+ overflow-y: auto;
483
+ -webkit-overflow-scrolling: touch;
484
+ }
485
+
486
+ .lalt-screen__column {
487
+ width: 100%;
488
+ max-width: 48rem;
489
+ margin: 0 auto;
490
+ padding: 1.25rem 1rem;
491
+ /* Clears the on-screen keyboard, plus room to scroll the last line up out
492
+ from under it. Zero wherever nothing covers the viewport. */
493
+ padding-bottom: calc(var(--lalt-keyboard, 0px) + 40vh);
494
+ }
495
+
496
+ /* No border, no box: the page is the document, not a form with a content
497
+ field in it. */
498
+ .lalt-screen__title {
499
+ width: 100%;
500
+ border: 0;
501
+ background: transparent;
502
+ color: inherit;
503
+ font-family: inherit;
504
+ font-size: 1.5rem;
505
+ font-weight: 600;
506
+ line-height: 1.25;
507
+ outline: none;
508
+ }
509
+
510
+ .lalt-screen__title::placeholder {
511
+ opacity: 0.35;
512
+ }
513
+
514
+ .lalt-screen__aside {
515
+ display: flex;
516
+ align-items: center;
517
+ justify-content: flex-end;
518
+ gap: 0.5rem;
519
+ margin-top: 0.5rem;
520
+ }
521
+
522
+ .lalt-screen__footer {
523
+ margin-top: 1.5rem;
524
+ }
525
+
526
+ .lalt-screen__save {
527
+ display: flex;
528
+ flex-shrink: 0;
529
+ align-items: center;
530
+ width: 0.5rem;
531
+ }
532
+
533
+ .lalt-screen__dot {
534
+ width: 0.5rem;
535
+ height: 0.5rem;
536
+ border-radius: 9999px;
537
+ transition: background-color 150ms;
538
+ }
539
+
540
+ .lalt-screen__dot--saving {
541
+ background: #f59e0b;
542
+ }
543
+
544
+ .lalt-screen__dot--saved {
545
+ background: #10b981;
546
+ }
547
+
548
+ .lalt-screen__dot--dirty {
549
+ background: currentColor;
550
+ opacity: 0.3;
551
+ }
552
+
553
+ .lalt-screen__sr {
554
+ position: absolute;
555
+ width: 1px;
556
+ height: 1px;
557
+ margin: -1px;
558
+ padding: 0;
559
+ overflow: hidden;
560
+ border: 0;
561
+ clip-path: inset(50%);
562
+ white-space: nowrap;
563
+ }
564
+
565
+ /* Room to breathe once there is a screen to spend it on. The phone layout
566
+ above is the one that has to work; this is the widening. */
567
+ @media (min-width: 640px) {
568
+ .lalt-screen__bar {
569
+ padding: 0.5rem 1.5rem;
570
+ }
571
+
572
+ .lalt-screen__column {
573
+ padding: 2rem 2rem;
574
+ padding-bottom: calc(var(--lalt-keyboard, 0px) + 30vh);
575
+ }
576
+
577
+ .lalt-screen__title {
578
+ font-size: 1.875rem;
579
+ }
580
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { R as RichDoc } from './diff-uFiP8hD5.js';
2
2
  export { B as BlockChange, a as BlockShape, C as ChangeKind, b as RichNode, c as blocksFromDoc, d as diffBlocks, e as docFromBlocks, s as shapesFromDoc } from './diff-uFiP8hD5.js';
3
3
  import * as react from 'react';
4
4
  import { Editor } from '@tiptap/react';
5
- import { Extension } from '@tiptap/core';
5
+ import { Range, Extension } from '@tiptap/core';
6
6
  import { PluginKey } from '@tiptap/pm/state';
7
7
  import { DecorationSet } from '@tiptap/pm/view';
8
8
 
@@ -132,6 +132,133 @@ declare const defaultFormatLabels: FormatLabels;
132
132
  */
133
133
  declare function blockFormats(editor: Editor | null, labels?: FormatLabels): BlockFormat[];
134
134
 
135
+ type SaveState = "idle" | "saving" | "saved" | "dirty";
136
+ interface EditorScreenProps {
137
+ /** The writing surface. Mounted by the host, which owns the TipTap instance
138
+ * and whichever extensions it needs. */
139
+ children: React.ReactNode;
140
+ title?: string;
141
+ onTitleChange?: (title: string) => void;
142
+ titlePlaceholder?: string;
143
+ titleLabel?: string;
144
+ saveState?: SaveState;
145
+ saveLabels?: SaveLabels;
146
+ /** Drawn at the far left of the bar; a back control belongs here. */
147
+ lead?: React.ReactNode;
148
+ /** Drawn at the far right. Keep it to what a thumb can reach on a phone —
149
+ * one primary control and an overflow, not a row of five. */
150
+ actions?: React.ReactNode;
151
+ /** Sits between the save dot and the actions: a character count, a badge. */
152
+ status?: React.ReactNode;
153
+ /** Between the title and the writing surface — a dictation control, a hint. */
154
+ aside?: React.ReactNode;
155
+ /** Below the writing surface, inside the scrolling column. */
156
+ footer?: React.ReactNode;
157
+ /** Above the writing surface and below the bar, pinned while the page
158
+ * scrolls: tabs over variants of one document belong here. */
159
+ subnav?: React.ReactNode;
160
+ /** Fills its container instead of the viewport, for a host whose shell
161
+ * already owns the page height. */
162
+ fill?: boolean;
163
+ className?: string;
164
+ }
165
+ interface SaveLabels {
166
+ saving: string;
167
+ saved: string;
168
+ dirty: string;
169
+ }
170
+ declare const defaultSaveLabels: SaveLabels;
171
+ /**
172
+ * EditorScreen is the page a writing surface sits in: a command bar, a title,
173
+ * one column of prose, and slots for whatever the host puts around it.
174
+ *
175
+ * It renders and measures; it never fetches, saves or routes. `saveState` is
176
+ * displayed, not computed — the host owns the debounce and the request, so the
177
+ * same screen serves an autosaving draft and a form with an explicit button.
178
+ *
179
+ * Commands sit at the top rather than in a bar at the bottom. Below the fold
180
+ * they are gone the moment the document runs past one screen, and pinned to
181
+ * the bottom they eat a sixth of what the on-screen keyboard leaves.
182
+ */
183
+ declare function EditorScreen({ children, title, onTitleChange, titlePlaceholder, titleLabel, saveState, saveLabels, lead, actions, status, aside, footer, subnav, fill, className, }: EditorScreenProps): react.JSX.Element;
184
+
185
+ /**
186
+ * SlashItem is one entry of the `/` menu.
187
+ *
188
+ * icon is a node rather than a component type: the package carries no icon
189
+ * library, so the host renders whichever one it already ships.
190
+ */
191
+ interface SlashItem {
192
+ id: string;
193
+ title: string;
194
+ description?: string;
195
+ shortcut?: string;
196
+ icon?: React.ReactNode;
197
+ command: (args: {
198
+ editor: Editor;
199
+ range: Range;
200
+ }) => void;
201
+ }
202
+ interface SlashLabels {
203
+ heading1: string;
204
+ heading2: string;
205
+ bulletList: string;
206
+ orderedList: string;
207
+ blockquote: string;
208
+ codeBlock: string;
209
+ horizontalRule: string;
210
+ }
211
+ declare const defaultSlashLabels: SlashLabels;
212
+ interface SlashDescriptions {
213
+ heading1?: string;
214
+ heading2?: string;
215
+ bulletList?: string;
216
+ orderedList?: string;
217
+ blockquote?: string;
218
+ codeBlock?: string;
219
+ horizontalRule?: string;
220
+ }
221
+ interface DefaultSlashItemsOptions {
222
+ labels?: SlashLabels;
223
+ descriptions?: SlashDescriptions;
224
+ icons?: Partial<Record<DefaultSlashId, React.ReactNode>>;
225
+ }
226
+ type DefaultSlashId = "heading1" | "heading2" | "bulletList" | "orderedList" | "blockquote" | "codeBlock" | "horizontalRule";
227
+ /**
228
+ * defaultSlashItems is the block set every writing surface shares.
229
+ *
230
+ * Returned as data rather than rendered behind flags: a host adds, removes or
231
+ * reorders entries by composing the array, so a surface that needs one more
232
+ * command does not push another boolean into this signature.
233
+ */
234
+ declare function defaultSlashItems({ labels, descriptions, icons, }?: DefaultSlashItemsOptions): SlashItem[];
235
+ /** filterSlashItems matches the query against titles the way the menu does. */
236
+ declare function filterSlashItems(items: SlashItem[], query: string): SlashItem[];
237
+
238
+ interface SlashMenuHandle {
239
+ onKeyDown: (props: {
240
+ event: KeyboardEvent;
241
+ }) => boolean;
242
+ }
243
+ interface SlashMenuProps {
244
+ items: SlashItem[];
245
+ command: (item: SlashItem) => void;
246
+ emptyLabel?: string;
247
+ }
248
+ declare const SlashMenu: react.ForwardRefExoticComponent<SlashMenuProps & react.RefAttributes<SlashMenuHandle>>;
249
+
250
+ interface SlashCommandsOptions {
251
+ /** Read on every keystroke, so a host can swap the set without remounting
252
+ * TipTap — rebuilding the extensions array wipes the text being typed. */
253
+ items: () => SlashItem[];
254
+ char: string;
255
+ emptyLabel?: string;
256
+ /** Distinguishes concurrently mounted editors: two surfaces sharing one key
257
+ * would answer each other's suggestions. */
258
+ pluginKey: PluginKey;
259
+ }
260
+ declare const SlashCommands: Extension<SlashCommandsOptions, any>;
261
+
135
262
  interface InlineSuggestion {
136
263
  id: string;
137
264
  /** Document positions of the passage this replaces. */
@@ -196,4 +323,4 @@ declare function splitRevision(before: string, after: string): RevisionPart[];
196
323
  */
197
324
  declare function docToMarkdown(doc: RichDoc): string;
198
325
 
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 };
326
+ export { type BlockFormat, type DefaultSlashId, type DefaultSlashItemsOptions, EditorScreen, type EditorScreenProps, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type ReadViewportInput, type RevisionPart, RichDoc, type SaveLabels, type SaveState, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, SlashCommands, type SlashCommandsOptions, type SlashDescriptions, type SlashItem, type SlashLabels, SlashMenu, type SlashMenuHandle, type SlashMenuProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, type ViewportMetrics, type VisualViewportLike, blockFormats, defaultFormatLabels, defaultSaveLabels, defaultSlashItems, defaultSlashLabels, docToMarkdown, expandToWords, filterSlashItems, inlineSuggestionsKey, placeToolbar, readViewport, splitRevision, useSelectionToolbar, useViewport };
package/dist/index.js CHANGED
@@ -270,12 +270,313 @@ function isApple() {
270
270
  return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);
271
271
  }
272
272
 
273
- // src/InlineSuggestions.ts
273
+ // src/EditorScreen.tsx
274
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
275
+ var defaultSaveLabels = {
276
+ saving: "Enregistrement en cours",
277
+ saved: "Enregistr\xE9",
278
+ dirty: "Modifications non enregistr\xE9es"
279
+ };
280
+ function EditorScreen({
281
+ children,
282
+ title,
283
+ onTitleChange,
284
+ titlePlaceholder = "Titre",
285
+ titleLabel = "Titre",
286
+ saveState,
287
+ saveLabels = defaultSaveLabels,
288
+ lead,
289
+ actions,
290
+ status,
291
+ aside,
292
+ footer,
293
+ subnav,
294
+ fill = false,
295
+ className
296
+ }) {
297
+ const { bottomInset } = useViewport();
298
+ const withTitle = onTitleChange !== void 0;
299
+ return /* @__PURE__ */ jsxs2(
300
+ "div",
301
+ {
302
+ className: [
303
+ "lalt-screen",
304
+ fill ? "lalt-screen--fill" : "",
305
+ className ?? ""
306
+ ].filter(Boolean).join(" "),
307
+ style: { "--lalt-keyboard": `${bottomInset}px` },
308
+ children: [
309
+ /* @__PURE__ */ jsxs2("header", { className: "lalt-screen__bar", children: [
310
+ lead,
311
+ saveState && /* @__PURE__ */ jsx2(SaveDot, { state: saveState, labels: saveLabels }),
312
+ status && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__status", children: status }),
313
+ actions && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__actions", children: actions })
314
+ ] }),
315
+ subnav && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__subnav", children: subnav }),
316
+ /* @__PURE__ */ jsx2("div", { className: "lalt-screen__scroll", children: /* @__PURE__ */ jsxs2("div", { className: "lalt-screen__column", children: [
317
+ withTitle && /* @__PURE__ */ jsx2(
318
+ "input",
319
+ {
320
+ className: "lalt-screen__title",
321
+ value: title ?? "",
322
+ onChange: (event) => onTitleChange(event.target.value),
323
+ placeholder: titlePlaceholder,
324
+ "aria-label": titleLabel
325
+ }
326
+ ),
327
+ aside && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__aside", children: aside }),
328
+ children,
329
+ footer && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__footer", children: footer })
330
+ ] }) })
331
+ ]
332
+ }
333
+ );
334
+ }
335
+ function SaveDot({ state, labels }) {
336
+ if (state === "idle") return /* @__PURE__ */ jsx2("span", { className: "lalt-screen__save" });
337
+ const label = state === "saving" ? labels.saving : state === "dirty" ? labels.dirty : labels.saved;
338
+ return /* @__PURE__ */ jsxs2("span", { className: "lalt-screen__save", role: "status", "aria-live": "polite", children: [
339
+ /* @__PURE__ */ jsx2("span", { className: `lalt-screen__dot lalt-screen__dot--${state}`, "aria-hidden": true }),
340
+ /* @__PURE__ */ jsx2("span", { className: "lalt-screen__sr", children: label })
341
+ ] });
342
+ }
343
+
344
+ // src/slash.ts
345
+ var defaultSlashLabels = {
346
+ heading1: "Titre 1",
347
+ heading2: "Titre 2",
348
+ bulletList: "Liste \xE0 puces",
349
+ orderedList: "Liste num\xE9rot\xE9e",
350
+ blockquote: "Citation",
351
+ codeBlock: "Bloc de code",
352
+ horizontalRule: "S\xE9parateur"
353
+ };
354
+ function defaultSlashItems({
355
+ labels = defaultSlashLabels,
356
+ descriptions = {},
357
+ icons = {}
358
+ } = {}) {
359
+ const item = (id, shortcut, run) => ({
360
+ id,
361
+ title: labels[id],
362
+ description: descriptions[id],
363
+ shortcut,
364
+ icon: icons[id],
365
+ command: ({ editor, range }) => {
366
+ editor.chain().focus().deleteRange(range).run();
367
+ run(editor);
368
+ }
369
+ });
370
+ return [
371
+ item(
372
+ "heading1",
373
+ "#",
374
+ (editor) => editor.chain().focus().setNode("heading", { level: 1 }).run()
375
+ ),
376
+ item(
377
+ "heading2",
378
+ "##",
379
+ (editor) => editor.chain().focus().setNode("heading", { level: 2 }).run()
380
+ ),
381
+ item(
382
+ "bulletList",
383
+ "-",
384
+ (editor) => editor.chain().focus().toggleBulletList().run()
385
+ ),
386
+ item(
387
+ "orderedList",
388
+ "1.",
389
+ (editor) => editor.chain().focus().toggleOrderedList().run()
390
+ ),
391
+ item(
392
+ "blockquote",
393
+ ">",
394
+ (editor) => editor.chain().focus().toggleBlockquote().run()
395
+ ),
396
+ item(
397
+ "codeBlock",
398
+ "```",
399
+ (editor) => editor.chain().focus().toggleCodeBlock().run()
400
+ ),
401
+ item(
402
+ "horizontalRule",
403
+ "---",
404
+ (editor) => editor.chain().focus().setHorizontalRule().run()
405
+ )
406
+ ];
407
+ }
408
+ function filterSlashItems(items, query) {
409
+ const q = query.trim().toLowerCase();
410
+ if (q === "") return items;
411
+ return items.filter((item) => item.title.toLowerCase().includes(q));
412
+ }
413
+
414
+ // src/SlashMenu.tsx
415
+ import {
416
+ forwardRef,
417
+ useCallback,
418
+ useEffect as useEffect4,
419
+ useImperativeHandle,
420
+ useRef as useRef2,
421
+ useState as useState4
422
+ } from "react";
423
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
424
+ var SlashMenu = forwardRef(
425
+ function SlashMenu2({ items, command, emptyLabel = "Aucune commande" }, ref) {
426
+ const [selectedIndex, setSelectedIndex] = useState4(0);
427
+ const itemsRef = useRef2(items);
428
+ itemsRef.current = items;
429
+ const commandRef = useRef2(command);
430
+ commandRef.current = command;
431
+ const selectedRef = useRef2(selectedIndex);
432
+ selectedRef.current = selectedIndex;
433
+ const ids = items.map((item) => item.id).join(" ");
434
+ useEffect4(() => {
435
+ setSelectedIndex(0);
436
+ selectedRef.current = 0;
437
+ }, [ids]);
438
+ const selectItem = useCallback((index) => {
439
+ const item = itemsRef.current[index];
440
+ if (item) commandRef.current(item);
441
+ }, []);
442
+ const move = useCallback((delta) => {
443
+ const count = itemsRef.current.length;
444
+ if (count === 0) return;
445
+ const next = (selectedRef.current + delta + count) % count;
446
+ selectedRef.current = next;
447
+ setSelectedIndex(next);
448
+ }, []);
449
+ useImperativeHandle(ref, () => ({
450
+ onKeyDown: ({ event }) => {
451
+ if (event.key === "ArrowUp") {
452
+ move(-1);
453
+ return true;
454
+ }
455
+ if (event.key === "ArrowDown") {
456
+ move(1);
457
+ return true;
458
+ }
459
+ if (event.key === "Enter" || event.key === "Tab") {
460
+ if (itemsRef.current.length === 0) return false;
461
+ selectItem(selectedRef.current);
462
+ return true;
463
+ }
464
+ return false;
465
+ }
466
+ }));
467
+ if (items.length === 0) {
468
+ return /* @__PURE__ */ jsx3("div", { className: "lalt-slash lalt-slash--empty", children: emptyLabel });
469
+ }
470
+ return /* @__PURE__ */ jsx3("div", { className: "lalt-slash", role: "listbox", children: items.map((item, index) => /* @__PURE__ */ jsxs3(
471
+ "button",
472
+ {
473
+ type: "button",
474
+ role: "option",
475
+ "aria-selected": index === selectedIndex,
476
+ className: index === selectedIndex ? "lalt-slash__item lalt-slash__item--active" : "lalt-slash__item",
477
+ onMouseDown: (event) => {
478
+ event.preventDefault();
479
+ selectItem(index);
480
+ },
481
+ onMouseEnter: () => {
482
+ selectedRef.current = index;
483
+ setSelectedIndex(index);
484
+ },
485
+ children: [
486
+ item.icon && /* @__PURE__ */ jsx3("span", { className: "lalt-slash__icon", "aria-hidden": true, children: item.icon }),
487
+ /* @__PURE__ */ jsxs3("span", { className: "lalt-slash__text", children: [
488
+ /* @__PURE__ */ jsx3("span", { className: "lalt-slash__title", children: item.title }),
489
+ item.description && /* @__PURE__ */ jsx3("span", { className: "lalt-slash__description", children: item.description })
490
+ ] }),
491
+ item.shortcut && /* @__PURE__ */ jsx3("kbd", { className: "lalt-slash__shortcut", children: item.shortcut })
492
+ ]
493
+ },
494
+ item.id
495
+ )) });
496
+ }
497
+ );
498
+
499
+ // src/SlashCommands.ts
274
500
  import { Extension } from "@tiptap/core";
275
- import { Plugin, PluginKey } from "@tiptap/pm/state";
501
+ import { ReactRenderer } from "@tiptap/react";
502
+ import { PluginKey } from "@tiptap/pm/state";
503
+ import Suggestion from "@tiptap/suggestion";
504
+ var OFFSET = 6;
505
+ var SlashCommands = Extension.create({
506
+ name: "laSlashCommands",
507
+ addOptions() {
508
+ return {
509
+ items: () => [],
510
+ char: "/",
511
+ pluginKey: new PluginKey("laSlashCommands")
512
+ };
513
+ },
514
+ addProseMirrorPlugins() {
515
+ const { items, char, emptyLabel, pluginKey } = this.options;
516
+ const suggestion = {
517
+ char,
518
+ pluginKey,
519
+ startOfLine: false,
520
+ items: ({ query }) => filterSlashItems(items(), query),
521
+ render: () => {
522
+ let component = null;
523
+ let container = null;
524
+ const place = (clientRect) => {
525
+ if (!container || !clientRect) return;
526
+ const rect = clientRect();
527
+ if (!rect) return;
528
+ container.style.top = `${rect.bottom + OFFSET}px`;
529
+ container.style.left = `${rect.left}px`;
530
+ };
531
+ const close = () => {
532
+ container?.remove();
533
+ container = null;
534
+ component?.destroy();
535
+ component = null;
536
+ };
537
+ return {
538
+ onStart: (props) => {
539
+ component = new ReactRenderer(SlashMenu, {
540
+ props: { ...props, emptyLabel },
541
+ editor: props.editor
542
+ });
543
+ container = document.createElement("div");
544
+ container.className = "lalt-slash-anchor";
545
+ container.appendChild(component.element);
546
+ document.body.appendChild(container);
547
+ place(props.clientRect);
548
+ },
549
+ onUpdate: (props) => {
550
+ component?.updateProps({ ...props, emptyLabel });
551
+ place(props.clientRect);
552
+ },
553
+ onKeyDown: (props) => {
554
+ if (props.event.key === "Escape") {
555
+ close();
556
+ return true;
557
+ }
558
+ return component?.ref?.onKeyDown(props) ?? false;
559
+ },
560
+ onExit: close
561
+ };
562
+ }
563
+ };
564
+ return [
565
+ Suggestion({
566
+ editor: this.editor,
567
+ ...suggestion,
568
+ command: ({ editor, range, props }) => props.command({ editor, range })
569
+ })
570
+ ];
571
+ }
572
+ });
573
+
574
+ // src/InlineSuggestions.ts
575
+ import { Extension as Extension2 } from "@tiptap/core";
576
+ import { Plugin, PluginKey as PluginKey2 } from "@tiptap/pm/state";
276
577
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
277
- var inlineSuggestionsKey = new PluginKey("lalt-inline-suggestions");
278
- var InlineSuggestions = Extension.create({
578
+ var inlineSuggestionsKey = new PluginKey2("lalt-inline-suggestions");
579
+ var InlineSuggestions = Extension2.create({
279
580
  name: "lalt-inline-suggestions",
280
581
  addOptions() {
281
582
  return { actions: null };
@@ -440,15 +741,22 @@ function text(node) {
440
741
  return out;
441
742
  }
442
743
  export {
744
+ EditorScreen,
443
745
  InlineSuggestions,
444
746
  SelectionToolbar,
747
+ SlashCommands,
748
+ SlashMenu,
445
749
  blockFormats,
446
750
  blocksFromDoc,
447
751
  defaultFormatLabels,
752
+ defaultSaveLabels,
753
+ defaultSlashItems,
754
+ defaultSlashLabels,
448
755
  diffBlocks,
449
756
  docFromBlocks,
450
757
  docToMarkdown,
451
758
  expandToWords,
759
+ filterSlashItems,
452
760
  inlineSuggestionsKey,
453
761
  placeToolbar,
454
762
  readViewport,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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"]}
1
+ {"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useViewport.ts","../src/viewport.ts","../src/useSelectionToolbar.ts","../src/formats.ts","../src/EditorScreen.tsx","../src/slash.ts","../src/SlashMenu.tsx","../src/SlashCommands.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 { useViewport } from \"./useViewport\";\n\nexport type SaveState = \"idle\" | \"saving\" | \"saved\" | \"dirty\";\n\nexport interface EditorScreenProps {\n /** The writing surface. Mounted by the host, which owns the TipTap instance\n * and whichever extensions it needs. */\n children: React.ReactNode;\n title?: string;\n onTitleChange?: (title: string) => void;\n titlePlaceholder?: string;\n titleLabel?: string;\n saveState?: SaveState;\n saveLabels?: SaveLabels;\n /** Drawn at the far left of the bar; a back control belongs here. */\n lead?: React.ReactNode;\n /** Drawn at the far right. Keep it to what a thumb can reach on a phone —\n * one primary control and an overflow, not a row of five. */\n actions?: React.ReactNode;\n /** Sits between the save dot and the actions: a character count, a badge. */\n status?: React.ReactNode;\n /** Between the title and the writing surface — a dictation control, a hint. */\n aside?: React.ReactNode;\n /** Below the writing surface, inside the scrolling column. */\n footer?: React.ReactNode;\n /** Above the writing surface and below the bar, pinned while the page\n * scrolls: tabs over variants of one document belong here. */\n subnav?: React.ReactNode;\n /** Fills its container instead of the viewport, for a host whose shell\n * already owns the page height. */\n fill?: boolean;\n className?: string;\n}\n\nexport interface SaveLabels {\n saving: string;\n saved: string;\n dirty: string;\n}\n\nexport const defaultSaveLabels: SaveLabels = {\n saving: \"Enregistrement en cours\",\n saved: \"Enregistré\",\n dirty: \"Modifications non enregistrées\",\n};\n\n/**\n * EditorScreen is the page a writing surface sits in: a command bar, a title,\n * one column of prose, and slots for whatever the host puts around it.\n *\n * It renders and measures; it never fetches, saves or routes. `saveState` is\n * displayed, not computed — the host owns the debounce and the request, so the\n * same screen serves an autosaving draft and a form with an explicit button.\n *\n * Commands sit at the top rather than in a bar at the bottom. Below the fold\n * they are gone the moment the document runs past one screen, and pinned to\n * the bottom they eat a sixth of what the on-screen keyboard leaves.\n */\nexport function EditorScreen({\n children,\n title,\n onTitleChange,\n titlePlaceholder = \"Titre\",\n titleLabel = \"Titre\",\n saveState,\n saveLabels = defaultSaveLabels,\n lead,\n actions,\n status,\n aside,\n footer,\n subnav,\n fill = false,\n className,\n}: EditorScreenProps) {\n const { bottomInset } = useViewport();\n const withTitle = onTitleChange !== undefined;\n\n return (\n <div\n className={[\n \"lalt-screen\",\n fill ? \"lalt-screen--fill\" : \"\",\n className ?? \"\",\n ]\n .filter(Boolean)\n .join(\" \")}\n // The keyboard covers the layout viewport without shrinking it, so the\n // column's last lines sit under the keys unless the strip is padded out.\n style={{ \"--lalt-keyboard\": `${bottomInset}px` } as React.CSSProperties}\n >\n <header className=\"lalt-screen__bar\">\n {lead}\n {saveState && (\n <SaveDot state={saveState} labels={saveLabels} />\n )}\n {status && <div className=\"lalt-screen__status\">{status}</div>}\n {actions && <div className=\"lalt-screen__actions\">{actions}</div>}\n </header>\n\n {subnav && <div className=\"lalt-screen__subnav\">{subnav}</div>}\n\n <div className=\"lalt-screen__scroll\">\n <div className=\"lalt-screen__column\">\n {withTitle && (\n <input\n className=\"lalt-screen__title\"\n value={title ?? \"\"}\n onChange={(event) => onTitleChange(event.target.value)}\n placeholder={titlePlaceholder}\n aria-label={titleLabel}\n />\n )}\n {aside && <div className=\"lalt-screen__aside\">{aside}</div>}\n {children}\n {footer && <div className=\"lalt-screen__footer\">{footer}</div>}\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * A dot rather than a word: the state is worth a glance, never a read, and a\n * label that appears and vanishes leaves the author wondering what it meant\n * once it is gone. Screen readers get the sentence instead.\n */\nfunction SaveDot({ state, labels }: { state: SaveState; labels: SaveLabels }) {\n if (state === \"idle\") return <span className=\"lalt-screen__save\" />;\n\n const label =\n state === \"saving\" ? labels.saving : state === \"dirty\" ? labels.dirty : labels.saved;\n\n return (\n <span className=\"lalt-screen__save\" role=\"status\" aria-live=\"polite\">\n <span className={`lalt-screen__dot lalt-screen__dot--${state}`} aria-hidden />\n <span className=\"lalt-screen__sr\">{label}</span>\n </span>\n );\n}\n","import type { Editor } from \"@tiptap/react\";\nimport type { Range } from \"@tiptap/core\";\n\n// Type-only, as in formats.ts: StarterKit contributes these commands to a\n// chain, and without the reference TypeScript does not know they exist. The\n// host stays in charge of which extensions it actually mounts.\nimport type {} from \"@tiptap/starter-kit\";\n\n/**\n * SlashItem is one entry of the `/` menu.\n *\n * icon is a node rather than a component type: the package carries no icon\n * library, so the host renders whichever one it already ships.\n */\nexport interface SlashItem {\n id: string;\n title: string;\n description?: string;\n shortcut?: string;\n icon?: React.ReactNode;\n command: (args: { editor: Editor; range: Range }) => void;\n}\n\nexport interface SlashLabels {\n heading1: string;\n heading2: string;\n bulletList: string;\n orderedList: string;\n blockquote: string;\n codeBlock: string;\n horizontalRule: string;\n}\n\nexport const defaultSlashLabels: SlashLabels = {\n heading1: \"Titre 1\",\n heading2: \"Titre 2\",\n bulletList: \"Liste à puces\",\n orderedList: \"Liste numérotée\",\n blockquote: \"Citation\",\n codeBlock: \"Bloc de code\",\n horizontalRule: \"Séparateur\",\n};\n\nexport interface SlashDescriptions {\n heading1?: string;\n heading2?: string;\n bulletList?: string;\n orderedList?: string;\n blockquote?: string;\n codeBlock?: string;\n horizontalRule?: string;\n}\n\nexport interface DefaultSlashItemsOptions {\n labels?: SlashLabels;\n descriptions?: SlashDescriptions;\n icons?: Partial<Record<DefaultSlashId, React.ReactNode>>;\n}\n\nexport type DefaultSlashId =\n | \"heading1\"\n | \"heading2\"\n | \"bulletList\"\n | \"orderedList\"\n | \"blockquote\"\n | \"codeBlock\"\n | \"horizontalRule\";\n\n/**\n * defaultSlashItems is the block set every writing surface shares.\n *\n * Returned as data rather than rendered behind flags: a host adds, removes or\n * reorders entries by composing the array, so a surface that needs one more\n * command does not push another boolean into this signature.\n */\nexport function defaultSlashItems({\n labels = defaultSlashLabels,\n descriptions = {},\n icons = {},\n}: DefaultSlashItemsOptions = {}): SlashItem[] {\n const item = (\n id: DefaultSlashId,\n shortcut: string,\n run: (editor: Editor) => void,\n ): SlashItem => ({\n id,\n title: labels[id],\n description: descriptions[id],\n shortcut,\n icon: icons[id],\n command: ({ editor, range }) => {\n editor.chain().focus().deleteRange(range).run();\n run(editor);\n },\n });\n\n return [\n item(\"heading1\", \"#\", (editor) =>\n editor.chain().focus().setNode(\"heading\", { level: 1 }).run(),\n ),\n item(\"heading2\", \"##\", (editor) =>\n editor.chain().focus().setNode(\"heading\", { level: 2 }).run(),\n ),\n item(\"bulletList\", \"-\", (editor) =>\n editor.chain().focus().toggleBulletList().run(),\n ),\n item(\"orderedList\", \"1.\", (editor) =>\n editor.chain().focus().toggleOrderedList().run(),\n ),\n item(\"blockquote\", \">\", (editor) =>\n editor.chain().focus().toggleBlockquote().run(),\n ),\n item(\"codeBlock\", \"```\", (editor) =>\n editor.chain().focus().toggleCodeBlock().run(),\n ),\n item(\"horizontalRule\", \"---\", (editor) =>\n editor.chain().focus().setHorizontalRule().run(),\n ),\n ];\n}\n\n/** filterSlashItems matches the query against titles the way the menu does. */\nexport function filterSlashItems(items: SlashItem[], query: string): SlashItem[] {\n const q = query.trim().toLowerCase();\n if (q === \"\") return items;\n return items.filter((item) => item.title.toLowerCase().includes(q));\n}\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from \"react\";\n\nimport type { SlashItem } from \"./slash\";\n\nexport interface SlashMenuHandle {\n onKeyDown: (props: { event: KeyboardEvent }) => boolean;\n}\n\nexport interface SlashMenuProps {\n items: SlashItem[];\n command: (item: SlashItem) => void;\n emptyLabel?: string;\n}\n\nexport const SlashMenu = forwardRef<SlashMenuHandle, SlashMenuProps>(\n function SlashMenu({ items, command, emptyLabel = \"Aucune commande\" }, ref) {\n const [selectedIndex, setSelectedIndex] = useState(0);\n\n // The suggestion plugin rebuilds these props on every keystroke, so the\n // imperative handle reads refs rather than a captured render closure —\n // otherwise Enter runs the command the menu showed one keystroke ago.\n const itemsRef = useRef(items);\n itemsRef.current = items;\n const commandRef = useRef(command);\n commandRef.current = command;\n const selectedRef = useRef(selectedIndex);\n selectedRef.current = selectedIndex;\n\n const ids = items.map((item) => item.id).join(\" \");\n useEffect(() => {\n setSelectedIndex(0);\n selectedRef.current = 0;\n }, [ids]);\n\n const selectItem = useCallback((index: number) => {\n const item = itemsRef.current[index];\n if (item) commandRef.current(item);\n }, []);\n\n const move = useCallback((delta: number) => {\n const count = itemsRef.current.length;\n if (count === 0) return;\n const next = (selectedRef.current + delta + count) % count;\n selectedRef.current = next;\n setSelectedIndex(next);\n }, []);\n\n useImperativeHandle(ref, () => ({\n onKeyDown: ({ event }) => {\n if (event.key === \"ArrowUp\") {\n move(-1);\n return true;\n }\n if (event.key === \"ArrowDown\") {\n move(1);\n return true;\n }\n if (event.key === \"Enter\" || event.key === \"Tab\") {\n if (itemsRef.current.length === 0) return false;\n selectItem(selectedRef.current);\n return true;\n }\n return false;\n },\n }));\n\n if (items.length === 0) {\n return <div className=\"lalt-slash lalt-slash--empty\">{emptyLabel}</div>;\n }\n\n return (\n <div className=\"lalt-slash\" role=\"listbox\">\n {items.map((item, index) => (\n <button\n key={item.id}\n type=\"button\"\n role=\"option\"\n aria-selected={index === selectedIndex}\n className={\n index === selectedIndex\n ? \"lalt-slash__item lalt-slash__item--active\"\n : \"lalt-slash__item\"\n }\n // Losing editor focus tears the suggestion plugin down before a\n // click lands, so the item would never run its command.\n onMouseDown={(event) => {\n event.preventDefault();\n selectItem(index);\n }}\n onMouseEnter={() => {\n selectedRef.current = index;\n setSelectedIndex(index);\n }}\n >\n {item.icon && (\n <span className=\"lalt-slash__icon\" aria-hidden>\n {item.icon}\n </span>\n )}\n <span className=\"lalt-slash__text\">\n <span className=\"lalt-slash__title\">{item.title}</span>\n {item.description && (\n <span className=\"lalt-slash__description\">{item.description}</span>\n )}\n </span>\n {item.shortcut && (\n <kbd className=\"lalt-slash__shortcut\">{item.shortcut}</kbd>\n )}\n </button>\n ))}\n </div>\n );\n },\n);\n","import { Extension } from \"@tiptap/core\";\nimport { ReactRenderer } from \"@tiptap/react\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport Suggestion, { type SuggestionOptions, type SuggestionProps } from \"@tiptap/suggestion\";\n\nimport { SlashMenu, type SlashMenuHandle, type SlashMenuProps } from \"./SlashMenu\";\nimport { filterSlashItems, type SlashItem } from \"./slash\";\n\nexport interface SlashCommandsOptions {\n /** Read on every keystroke, so a host can swap the set without remounting\n * TipTap — rebuilding the extensions array wipes the text being typed. */\n items: () => SlashItem[];\n char: string;\n emptyLabel?: string;\n /** Distinguishes concurrently mounted editors: two surfaces sharing one key\n * would answer each other's suggestions. */\n pluginKey: PluginKey;\n}\n\nconst OFFSET = 6;\n\nexport const SlashCommands = Extension.create<SlashCommandsOptions>({\n name: \"laSlashCommands\",\n\n addOptions() {\n return {\n items: () => [],\n char: \"/\",\n pluginKey: new PluginKey(\"laSlashCommands\"),\n };\n },\n\n addProseMirrorPlugins() {\n const { items, char, emptyLabel, pluginKey } = this.options;\n\n const suggestion: Omit<SuggestionOptions<SlashItem>, \"editor\"> = {\n char,\n pluginKey,\n startOfLine: false,\n items: ({ query }) => filterSlashItems(items(), query),\n render: () => {\n let component: ReactRenderer<SlashMenuHandle, SlashMenuProps> | null = null;\n let container: HTMLDivElement | null = null;\n\n const place = (clientRect: SuggestionProps<SlashItem>[\"clientRect\"]) => {\n if (!container || !clientRect) return;\n const rect = clientRect();\n if (!rect) return;\n container.style.top = `${rect.bottom + OFFSET}px`;\n container.style.left = `${rect.left}px`;\n };\n\n const close = () => {\n container?.remove();\n container = null;\n component?.destroy();\n component = null;\n };\n\n return {\n onStart: (props) => {\n component = new ReactRenderer(SlashMenu, {\n props: { ...props, emptyLabel },\n editor: props.editor,\n });\n container = document.createElement(\"div\");\n container.className = \"lalt-slash-anchor\";\n container.appendChild(component.element);\n document.body.appendChild(container);\n place(props.clientRect);\n },\n onUpdate: (props) => {\n component?.updateProps({ ...props, emptyLabel });\n place(props.clientRect);\n },\n onKeyDown: (props) => {\n if (props.event.key === \"Escape\") {\n close();\n return true;\n }\n return component?.ref?.onKeyDown(props) ?? false;\n },\n onExit: close,\n };\n },\n };\n\n return [\n Suggestion({\n editor: this.editor,\n ...suggestion,\n command: ({ editor, range, props }) => props.command({ editor, range }),\n }),\n ];\n },\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","jsx","jsxs","useEffect","useRef","useState","jsx","jsxs","SlashMenu","Extension","PluginKey","text","text"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/editor",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -32,7 +32,8 @@
32
32
  "@tiptap/react": ">=3.0.0",
33
33
  "@tiptap/starter-kit": ">=3.0.0",
34
34
  "react": ">=18.0.0",
35
- "react-dom": ">=18.0.0"
35
+ "react-dom": ">=18.0.0",
36
+ "@tiptap/suggestion": ">=3.0.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@tiptap/core": "^3.29.2",
@@ -47,7 +48,8 @@
47
48
  "tsup": "^8.5.1",
48
49
  "typescript": "^5.7.0",
49
50
  "vite": "^7.1.7",
50
- "vitest": "^3.0.0"
51
+ "vitest": "^3.0.0",
52
+ "@tiptap/suggestion": "^3.29.2"
51
53
  },
52
54
  "publishConfig": {
53
55
  "registry": "https://registry.npmjs.org",
@@ -61,6 +63,9 @@
61
63
  "peerDependenciesMeta": {
62
64
  "@tiptap/starter-kit": {
63
65
  "optional": true
66
+ },
67
+ "@tiptap/suggestion": {
68
+ "optional": true
64
69
  }
65
70
  }
66
71
  }