@lalternative/editor 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/editor.css CHANGED
@@ -29,30 +29,56 @@
29
29
  }
30
30
  }
31
31
 
32
- /* Docked to the bottom of the screen by default, floating beside the selection
33
- only where there is a pointer and room for it.
32
+ /* A pill above the keyboard by default, floating beside the selection only
33
+ where there is a pointer and room for it.
34
34
 
35
- A bar floating next to a selection is the worst place for it on a phone: the
36
- finger that made the selection covers it, and iOS draws its own callout in
37
- the same spot. Bottom-docked also puts it in thumb reach — and unlike a
38
- floating bar, it cannot be pushed off-screen by a selection near an edge. */
35
+ Near the selection is the worst place for it on a phone: the finger that
36
+ made the selection covers it, and iOS draws its own callout in the same
37
+ spot. Sitting above the keyboard puts it in thumb reach instead, and unlike
38
+ a floating bar it cannot be pushed off-screen by a selection near an edge.
39
+
40
+ Shaped as a pill rather than a full-width bar, which is the form the
41
+ platform already uses for a selection: it reads as something hovering over
42
+ the document rather than as a piece of the page's chrome, and its rounded
43
+ ends keep the text either side of it visible.
44
+
45
+ Lifted by whatever covers the bottom of the layout viewport: the on-screen
46
+ keyboard hides the bar without shrinking that viewport, so `bottom: 0` alone
47
+ parks it under the keys. The component measures the strip and sets the
48
+ variable; it is 0 whenever nothing covers anything. */
39
49
  .lalt-toolbar {
40
50
  position: fixed;
41
- left: 0;
42
- right: 0;
43
- bottom: 0;
51
+ left: 0.5rem;
52
+ right: 0.5rem;
53
+ bottom: calc(var(--lalt-keyboard-inset, 0px) + 0.5rem);
44
54
  z-index: 100;
45
55
  display: flex;
46
56
  align-items: center;
47
- gap: 0.25rem;
48
- padding: 0.5rem;
49
- padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
57
+ justify-content: center;
58
+ gap: 0.125rem;
59
+ width: fit-content;
60
+ max-width: calc(100% - 1rem);
61
+ margin: 0 auto;
62
+ margin-bottom: env(safe-area-inset-bottom);
63
+ padding: 0.25rem;
64
+ border-radius: 999px;
50
65
  background: var(--lalt-editor-surface, #ffffff);
51
- border-top: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
52
- box-shadow: 0 -4px 24px rgb(0 0 0 / 0.18);
66
+ border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
67
+ box-shadow:
68
+ 0 6px 20px rgb(0 0 0 / 0.18),
69
+ 0 1px 3px rgb(0 0 0 / 0.1);
53
70
  color: var(--lalt-editor-text, #111827);
54
71
  user-select: none;
72
+ /* A long row scrolls rather than stretching the pill past the screen. This
73
+ clips vertically too — `overflow-y: visible` beside it is ignored by the
74
+ spec — so the format menu is positioned against the viewport instead of
75
+ inside this box, and escapes the clip that way. */
55
76
  overflow-x: auto;
77
+ scrollbar-width: none;
78
+ }
79
+
80
+ .lalt-toolbar::-webkit-scrollbar {
81
+ display: none;
56
82
  }
57
83
 
58
84
  @media (hover: hover) and (pointer: fine) {
@@ -61,11 +87,9 @@
61
87
  top: var(--lalt-toolbar-top, 0);
62
88
  right: auto;
63
89
  bottom: auto;
64
- gap: 0.25rem;
65
- padding: 0.25rem;
66
- padding-bottom: 0.25rem;
67
- border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
68
- border-radius: 0.5rem;
90
+ max-width: none;
91
+ margin: 0;
92
+ padding: 0.1875rem;
69
93
  /* Deep enough to read as floating over the text rather than sitting in
70
94
  it — the bar appears mid-paragraph, with prose on every side. */
71
95
  box-shadow:
@@ -75,35 +99,50 @@
75
99
  }
76
100
  }
77
101
 
102
+ /* Transparent by default: inside a pill the buttons are read as a row by the
103
+ separators between them, and a filled rectangle each would turn the bar into
104
+ a strip of chips. The fill is what pressing one looks like. */
78
105
  .lalt-toolbar__button {
79
106
  display: inline-flex;
80
107
  align-items: center;
81
108
  justify-content: center;
82
109
  gap: 0.375rem;
83
- /* 44px: what a finger needs. Shrinks to a compact control only where a
84
- pointer is doing the aiming. */
85
- min-height: 2.75rem;
86
- padding: 0.5rem 0.75rem;
87
- border-radius: 0.375rem;
88
- background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
110
+ /* 40px tall inside a 44px pill: still the target a finger needs, once the
111
+ bar's own padding is counted. */
112
+ min-height: 2.5rem;
113
+ padding: 0.5rem 0.875rem;
114
+ border-radius: 999px;
115
+ background: none;
89
116
  border: none;
90
117
  font: inherit;
91
- font-size: 0.875rem;
118
+ font-size: 0.9375rem;
92
119
  font-weight: 500;
93
120
  white-space: nowrap;
94
121
  cursor: pointer;
95
122
  color: inherit;
96
123
  }
97
124
 
125
+ /* A hairline between actions, the way the platform's own callout separates
126
+ Copy from Look Up — cheaper than a box around each. */
127
+ .lalt-toolbar > * + * > .lalt-toolbar__button,
128
+ .lalt-toolbar > .lalt-toolbar__button + .lalt-toolbar__button {
129
+ box-shadow: inset 1px 0 0 var(--lalt-editor-border, rgb(0 0 0 / 0.1));
130
+ }
131
+
132
+ .lalt-toolbar__button:active {
133
+ background: var(--lalt-editor-active, rgb(0 0 0 / 0.12));
134
+ }
135
+
98
136
  @media (hover: hover) and (pointer: fine) {
99
137
  .lalt-toolbar__button {
100
138
  min-height: 0;
101
- padding: 0.25rem 0.5rem;
139
+ padding: 0.3125rem 0.625rem;
140
+ font-size: 0.875rem;
102
141
  }
103
142
  }
104
143
 
105
144
  .lalt-toolbar__button:hover {
106
- background: var(--lalt-editor-active, rgb(0 0 0 / 0.12));
145
+ background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
107
146
  }
108
147
 
109
148
  /* The format trigger opens a menu; the caret is what says so. */
@@ -121,19 +160,25 @@
121
160
  position: relative;
122
161
  }
123
162
 
124
- /* Opens upward from a bottom-docked bar, full width: a menu anchored below it
125
- would open off the bottom of the screen. */
163
+ /* Opens upward from the pill: a menu anchored below it would open off the
164
+ bottom of the screen, or into the keyboard. Card-shaped rather than
165
+ full-bleed, so it reads as belonging to the bar it grew out of. */
126
166
  .lalt-toolbar__menu {
127
167
  position: fixed;
128
- left: 0;
129
- right: 0;
130
- bottom: 100%;
131
- max-height: 60vh;
168
+ left: 50%;
169
+ bottom: calc(var(--lalt-keyboard-inset, 0px) + env(safe-area-inset-bottom) + 3.75rem);
170
+ transform: translateX(-50%);
171
+ min-width: 12rem;
172
+ max-width: calc(100vw - 2rem);
173
+ max-height: 50vh;
132
174
  overflow-y: auto;
133
175
  padding: 0.25rem;
134
176
  background: var(--lalt-editor-surface, #ffffff);
135
- border-top: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
136
- box-shadow: 0 -8px 24px rgb(0 0 0 / 0.16);
177
+ border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
178
+ border-radius: 0.875rem;
179
+ box-shadow:
180
+ 0 8px 24px rgb(0 0 0 / 0.18),
181
+ 0 1px 3px rgb(0 0 0 / 0.1);
137
182
  color: var(--lalt-editor-text, #111827);
138
183
  }
139
184
 
@@ -144,9 +189,10 @@
144
189
  right: 0;
145
190
  bottom: auto;
146
191
  top: calc(100% + 0.375rem);
192
+ transform: none;
147
193
  min-width: 14rem;
148
- border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
149
- border-radius: 0.5rem;
194
+ max-width: none;
195
+ border-radius: 0.625rem;
150
196
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
151
197
  }
152
198
  }
@@ -159,7 +205,7 @@
159
205
  width: 100%;
160
206
  min-height: 2.75rem;
161
207
  padding: 0.5rem 0.75rem;
162
- border-radius: 0.375rem;
208
+ border-radius: 0.625rem;
163
209
  background: none;
164
210
  border: none;
165
211
  font: inherit;
package/dist/index.d.ts CHANGED
@@ -76,6 +76,45 @@ interface SelectionInfo {
76
76
  */
77
77
  declare function useSelectionToolbar(editor: Editor | null): SelectionInfo | null;
78
78
 
79
+ interface VisualViewportLike {
80
+ width: number;
81
+ height: number;
82
+ offsetTop: number;
83
+ }
84
+ interface ViewportMetrics {
85
+ width: number;
86
+ height: number;
87
+ offsetTop: number;
88
+ /** How much of the layout viewport's bottom is covered, the on-screen
89
+ * keyboard included. Zero on a desktop browser and wherever the API is
90
+ * missing. */
91
+ bottomInset: number;
92
+ }
93
+ interface ReadViewportInput {
94
+ innerWidth: number;
95
+ innerHeight: number;
96
+ visual?: VisualViewportLike | null;
97
+ }
98
+ /**
99
+ * readViewport reports the area a fixed element can actually occupy.
100
+ *
101
+ * The distinction the on-screen keyboard turns on: `innerHeight` measures the
102
+ * layout viewport, which the keyboard covers without shrinking, so an element
103
+ * pinned to its bottom edge ends up underneath the keys. Only `visualViewport`
104
+ * narrows, and the difference is the inset a docked toolbar has to clear.
105
+ */
106
+ declare function readViewport({ innerWidth, innerHeight, visual, }: ReadViewportInput): ViewportMetrics;
107
+
108
+ /**
109
+ * useViewport tracks the area a fixed element can occupy, keyboard included.
110
+ *
111
+ * The visual viewport's own events are the only ones a raised keyboard fires:
112
+ * `window.resize` does not run on iOS when the keyboard opens, so a component
113
+ * listening to it alone keeps the measurements it took before the keys
114
+ * appeared.
115
+ */
116
+ declare function useViewport(): ViewportMetrics;
117
+
79
118
  interface FormatLabels {
80
119
  paragraph: string;
81
120
  heading1: string;
@@ -157,4 +196,4 @@ declare function splitRevision(before: string, after: string): RevisionPart[];
157
196
  */
158
197
  declare function docToMarkdown(doc: RichDoc): string;
159
198
 
160
- export { type BlockFormat, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type RevisionPart, RichDoc, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, blockFormats, defaultFormatLabels, docToMarkdown, expandToWords, inlineSuggestionsKey, placeToolbar, splitRevision, useSelectionToolbar };
199
+ export { type BlockFormat, type FormatLabels, type InlineSuggestion, InlineSuggestions, type PlaceToolbarInput, type ReadViewportInput, type RevisionPart, RichDoc, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type SuggestionActions, type TextRange, type ToolbarAction, type ToolbarPlacement, type ViewportMetrics, type VisualViewportLike, blockFormats, defaultFormatLabels, docToMarkdown, expandToWords, inlineSuggestionsKey, placeToolbar, readViewport, splitRevision, useSelectionToolbar, useViewport };
package/dist/index.js CHANGED
@@ -23,8 +23,61 @@ function placeToolbar({
23
23
  }
24
24
 
25
25
  // src/SelectionToolbar.tsx
26
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
26
+ import { useEffect as useEffect2, useLayoutEffect, useRef, useState as useState2 } from "react";
27
27
  import { createPortal } from "react-dom";
28
+
29
+ // src/useViewport.ts
30
+ import { useEffect, useState } from "react";
31
+
32
+ // src/viewport.ts
33
+ function readViewport({
34
+ innerWidth,
35
+ innerHeight,
36
+ visual
37
+ }) {
38
+ if (!visual) {
39
+ return { width: innerWidth, height: innerHeight, offsetTop: 0, bottomInset: 0 };
40
+ }
41
+ const covered = innerHeight - visual.height - visual.offsetTop;
42
+ return {
43
+ width: visual.width,
44
+ height: visual.height,
45
+ offsetTop: visual.offsetTop,
46
+ bottomInset: Math.max(0, Math.round(covered))
47
+ };
48
+ }
49
+
50
+ // src/useViewport.ts
51
+ var SERVER = { width: 0, height: 0, offsetTop: 0, bottomInset: 0 };
52
+ function read() {
53
+ if (typeof window === "undefined") return SERVER;
54
+ return readViewport({
55
+ innerWidth: window.innerWidth,
56
+ innerHeight: window.innerHeight,
57
+ visual: window.visualViewport
58
+ });
59
+ }
60
+ function useViewport() {
61
+ const [metrics, setMetrics] = useState(read);
62
+ useEffect(() => {
63
+ const update = () => setMetrics(read());
64
+ update();
65
+ const visual = window.visualViewport;
66
+ visual?.addEventListener("resize", update);
67
+ visual?.addEventListener("scroll", update);
68
+ window.addEventListener("resize", update);
69
+ window.addEventListener("orientationchange", update);
70
+ return () => {
71
+ visual?.removeEventListener("resize", update);
72
+ visual?.removeEventListener("scroll", update);
73
+ window.removeEventListener("resize", update);
74
+ window.removeEventListener("orientationchange", update);
75
+ };
76
+ }, []);
77
+ return metrics;
78
+ }
79
+
80
+ // src/SelectionToolbar.tsx
28
81
  import { jsx, jsxs } from "react/jsx-runtime";
29
82
  var FALLBACK = { width: 320, height: 40 };
30
83
  function SelectionToolbar({
@@ -37,15 +90,16 @@ function SelectionToolbar({
37
90
  container
38
91
  }) {
39
92
  const ref = useRef(null);
40
- const [size, setSize] = useState(FALLBACK);
41
- const [formatsOpen, setFormatsOpen] = useState(false);
93
+ const [size, setSize] = useState2(FALLBACK);
94
+ const [formatsOpen, setFormatsOpen] = useState2(false);
95
+ const viewport = useViewport();
42
96
  useLayoutEffect(() => {
43
97
  const el = ref.current;
44
98
  if (!el) return;
45
99
  const rect = el.getBoundingClientRect();
46
100
  if (rect.width > 0) setSize({ width: rect.width, height: rect.height });
47
101
  }, [selectedText, actions.length, formats.length]);
48
- useEffect(() => {
102
+ useEffect2(() => {
49
103
  if (!formatsOpen) return;
50
104
  const close = (e) => {
51
105
  if (ref.current && !ref.current.contains(e.target)) setFormatsOpen(false);
@@ -57,7 +111,7 @@ function SelectionToolbar({
57
111
  const place = placeToolbar({
58
112
  selection,
59
113
  toolbar: size,
60
- viewport: { width: window.innerWidth, height: window.innerHeight },
114
+ viewport: { width: viewport.width, height: viewport.height },
61
115
  preferBelow
62
116
  });
63
117
  return createPortal(
@@ -69,7 +123,8 @@ function SelectionToolbar({
69
123
  className: "lalt-toolbar",
70
124
  style: {
71
125
  "--lalt-toolbar-left": `${place.left}px`,
72
- "--lalt-toolbar-top": `${place.top}px`
126
+ "--lalt-toolbar-top": `${place.top}px`,
127
+ "--lalt-keyboard-inset": `${viewport.bottomInset}px`
73
128
  },
74
129
  children: [
75
130
  actions.map((action) => /* @__PURE__ */ jsxs(
@@ -127,12 +182,12 @@ function SelectionToolbar({
127
182
  }
128
183
 
129
184
  // src/useSelectionToolbar.ts
130
- import { useEffect as useEffect2, useState as useState2 } from "react";
185
+ import { useEffect as useEffect3, useState as useState3 } from "react";
131
186
  function useSelectionToolbar(editor) {
132
- const [selection, setSelection] = useState2(null);
133
- useEffect2(() => {
187
+ const [selection, setSelection] = useState3(null);
188
+ useEffect3(() => {
134
189
  if (!editor) return;
135
- const read = () => {
190
+ const read2 = () => {
136
191
  const { state } = editor;
137
192
  const { from, to, empty } = state.selection;
138
193
  if (empty || !editor.isFocused) {
@@ -155,14 +210,14 @@ function useSelectionToolbar(editor) {
155
210
  }
156
211
  });
157
212
  };
158
- read();
159
- editor.on("selectionUpdate", read);
160
- editor.on("blur", read);
161
- editor.on("focus", read);
213
+ read2();
214
+ editor.on("selectionUpdate", read2);
215
+ editor.on("blur", read2);
216
+ editor.on("focus", read2);
162
217
  return () => {
163
- editor.off("selectionUpdate", read);
164
- editor.off("blur", read);
165
- editor.off("focus", read);
218
+ editor.off("selectionUpdate", read2);
219
+ editor.off("blur", read2);
220
+ editor.off("focus", read2);
166
221
  };
167
222
  }, [editor]);
168
223
  return selection;
@@ -396,8 +451,10 @@ export {
396
451
  expandToWords,
397
452
  inlineSuggestionsKey,
398
453
  placeToolbar,
454
+ readViewport,
399
455
  shapesFromDoc,
400
456
  splitRevision,
401
- useSelectionToolbar
457
+ useSelectionToolbar,
458
+ useViewport
402
459
  };
403
460
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useSelectionToolbar.ts","../src/formats.ts","../src/InlineSuggestions.ts","../src/passage.ts","../src/markdown.ts"],"sourcesContent":["export interface SelectionRect {\n /** Horizontal centre of the selection. */\n x: number;\n top: number;\n bottom: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport interface PlaceToolbarInput {\n selection: SelectionRect;\n toolbar: Size;\n viewport: Size;\n /** iOS renders its own callout above a selection, which covers a toolbar\n * placed there, so the caller asks for below on that platform. */\n preferBelow?: boolean;\n}\n\nexport interface ToolbarPlacement {\n left: number;\n top: number;\n below: boolean;\n}\n\nconst GAP = 8;\n\n/**\n * placeToolbar positions a floating toolbar against a selection.\n *\n * Kept apart from the component because it is the part that is wrong in subtle\n * ways — off-screen at the edges, hidden under a system callout — and the part\n * a test can pin down without a browser.\n */\nexport function placeToolbar({\n selection,\n toolbar,\n viewport,\n preferBelow = false,\n}: PlaceToolbarInput): ToolbarPlacement {\n const fitsAbove = selection.top - GAP - toolbar.height >= 0;\n const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;\n\n const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;\n\n const top = below\n ? selection.bottom + GAP\n : Math.max(0, selection.top - GAP - toolbar.height);\n\n const centred = selection.x - toolbar.width / 2;\n const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));\n\n return { left, top, below };\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { placeToolbar, type SelectionRect } from \"./placement\";\n\nexport interface ToolbarAction {\n id: string;\n label: string;\n icon?: React.ReactNode;\n onSelect: (selectedText: string) => void;\n}\n\nexport interface BlockFormat {\n id: string;\n label: string;\n shortcut?: string;\n isActive?: boolean;\n onSelect: () => void;\n}\n\nexport interface SelectionToolbarProps {\n selectedText: string;\n selection: SelectionRect;\n actions?: ToolbarAction[];\n /** Heading levels and list kinds, rendered behind a single trigger. */\n formats?: BlockFormat[];\n formatsLabel?: string;\n /** iOS draws its own callout above a selection; the host detects it. */\n preferBelow?: boolean;\n container?: HTMLElement | null;\n}\n\nconst FALLBACK = { width: 320, height: 40 };\n\nexport function SelectionToolbar({\n selectedText,\n selection,\n actions = [],\n formats = [],\n formatsLabel = \"Format\",\n preferBelow = false,\n container,\n}: SelectionToolbarProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(FALLBACK);\n const [formatsOpen, setFormatsOpen] = useState(false);\n\n // Measured after paint rather than assumed: the toolbar's width depends on\n // labels the host supplies, and guessing it puts the toolbar off-centre.\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n if (rect.width > 0) setSize({ width: rect.width, height: rect.height });\n }, [selectedText, actions.length, formats.length]);\n\n useEffect(() => {\n if (!formatsOpen) return;\n const close = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setFormatsOpen(false);\n };\n document.addEventListener(\"mousedown\", close);\n return () => document.removeEventListener(\"mousedown\", close);\n }, [formatsOpen]);\n\n if (typeof document === \"undefined\") return null;\n\n const place = placeToolbar({\n selection,\n toolbar: size,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n preferBelow,\n });\n\n return createPortal(\n <div\n ref={ref}\n role=\"toolbar\"\n className=\"lalt-toolbar\"\n // Custom properties rather than left/top directly: under the mobile\n // breakpoint the stylesheet docks the toolbar to the bottom of the\n // screen and ignores these, which inline left/top would override.\n style={\n {\n \"--lalt-toolbar-left\": `${place.left}px`,\n \"--lalt-toolbar-top\": `${place.top}px`,\n } as React.CSSProperties\n }\n >\n {actions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n className=\"lalt-toolbar__button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => action.onSelect(selectedText)}\n >\n {action.icon}\n <span>{action.label}</span>\n </button>\n ))}\n\n {formats.length > 0 && (\n <div className=\"lalt-toolbar__formats\">\n <button\n type=\"button\"\n className=\"lalt-toolbar__button\"\n aria-haspopup=\"menu\"\n aria-expanded={formatsOpen}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => setFormatsOpen((open) => !open)}\n >\n {formatsLabel}\n </button>\n {formatsOpen && (\n <div role=\"menu\" className=\"lalt-toolbar__menu\">\n {formats.map((format) => (\n <button\n key={format.id}\n type=\"button\"\n role=\"menuitem\"\n className=\"lalt-toolbar__item\"\n aria-current={format.isActive}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n format.onSelect();\n setFormatsOpen(false);\n }}\n >\n <span>{format.label}</span>\n {format.shortcut && (\n <kbd className=\"lalt-toolbar__shortcut\">{format.shortcut}</kbd>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>,\n container ?? document.body,\n );\n}\n","import { useEffect, useState } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\n\nimport type { SelectionRect } from \"./placement\";\n\nexport interface SelectionInfo {\n text: string;\n rect: SelectionRect;\n}\n\n/**\n * useSelectionToolbar reports the current non-empty selection of an editor.\n *\n * Returns null whenever there is nothing to act on, so the caller renders the\n * toolbar only while it has a target.\n */\nexport function useSelectionToolbar(editor: Editor | null): SelectionInfo | null {\n const [selection, setSelection] = useState<SelectionInfo | null>(null);\n\n useEffect(() => {\n if (!editor) return;\n\n const read = () => {\n const { state } = editor;\n const { from, to, empty } = state.selection;\n if (empty || !editor.isFocused) {\n setSelection(null);\n return;\n }\n\n const text = state.doc.textBetween(from, to, \" \").trim();\n if (text === \"\") {\n setSelection(null);\n return;\n }\n\n const start = editor.view.coordsAtPos(from);\n const end = editor.view.coordsAtPos(to);\n setSelection({\n text,\n rect: {\n x: (start.left + end.right) / 2,\n top: Math.min(start.top, end.top),\n bottom: Math.max(start.bottom, end.bottom),\n },\n });\n };\n\n read();\n editor.on(\"selectionUpdate\", read);\n editor.on(\"blur\", read);\n editor.on(\"focus\", read);\n return () => {\n editor.off(\"selectionUpdate\", read);\n editor.off(\"blur\", read);\n editor.off(\"focus\", read);\n };\n }, [editor]);\n\n return selection;\n}\n","import type { Editor } from \"@tiptap/react\";\n\n// Type-only: these commands are contributed by StarterKit's extensions, and\n// without the reference TypeScript does not know they exist on a chain. No\n// runtime import, so the host stays in charge of which extensions it mounts —\n// blockFormats simply returns entries whose commands no-op if one is missing.\nimport type {} from \"@tiptap/starter-kit\";\n\nimport type { BlockFormat } from \"./SelectionToolbar\";\n\nexport interface FormatLabels {\n paragraph: string;\n heading1: string;\n heading2: string;\n heading3: string;\n orderedList: string;\n bulletList: string;\n}\n\nexport const defaultFormatLabels: FormatLabels = {\n paragraph: \"Texte\",\n heading1: \"Titre 1\",\n heading2: \"Titre 2\",\n heading3: \"Titre 3\",\n orderedList: \"Liste numérotée\",\n bulletList: \"Liste à puces\",\n};\n\n/**\n * blockFormats is the set a writer reaches for on a selected passage.\n *\n * Labels are passed in rather than translated here: the package carries no\n * i18n runtime, so the host names things in whatever language it speaks.\n */\nexport function blockFormats(\n editor: Editor | null,\n labels: FormatLabels = defaultFormatLabels,\n): BlockFormat[] {\n if (!editor) return [];\n\n // Shortcuts are the ones TipTap's own extensions bind, read from their\n // source rather than invented: heading binds Mod-Alt-<level>, the list\n // extensions bind Mod-Shift-7/8, and setParagraph binds nothing at all.\n // A label promising a shortcut that does nothing is worse than no label.\n const mod = isApple() ? \"⌘\" : \"Ctrl\";\n\n return [\n {\n id: \"paragraph\",\n label: labels.paragraph,\n isActive: editor.isActive(\"paragraph\"),\n onSelect: () => editor.chain().focus().setParagraph().run(),\n },\n ...([1, 2, 3] as const).map((level) => ({\n id: `heading${level}`,\n label: labels[`heading${level}` as keyof FormatLabels],\n shortcut: `${mod}+Alt+${level}`,\n isActive: editor.isActive(\"heading\", { level }),\n onSelect: () => editor.chain().focus().toggleHeading({ level }).run(),\n })),\n {\n id: \"orderedList\",\n label: labels.orderedList,\n shortcut: `${mod}+Shift+7`,\n isActive: editor.isActive(\"orderedList\"),\n onSelect: () => editor.chain().focus().toggleOrderedList().run(),\n },\n {\n id: \"bulletList\",\n label: labels.bulletList,\n shortcut: `${mod}+Shift+8`,\n isActive: editor.isActive(\"bulletList\"),\n onSelect: () => editor.chain().focus().toggleBulletList().run(),\n },\n ];\n}\n\nfunction isApple(): boolean {\n if (typeof navigator === \"undefined\") return false;\n return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);\n}\n","import { Extension } from \"@tiptap/core\";\nimport { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\nimport type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\n\nexport interface InlineSuggestion {\n id: string;\n /** Document positions of the passage this replaces. */\n from: number;\n to: number;\n after: string;\n}\n\nexport interface SuggestionActions {\n onAccept: (id: string) => void;\n onReject: (id: string) => void;\n acceptLabel: string;\n rejectLabel: string;\n}\n\nexport const inlineSuggestionsKey = new PluginKey<DecorationSet>(\"lalt-inline-suggestions\");\n\n/**\n * InlineSuggestions renders proposed edits where they belong: over the passage\n * they replace, with the new text right after it.\n *\n * Decorations, never document changes. The editor's content stays exactly what\n * the author wrote until they accept, so the draft is saveable mid-review and\n * a rejected suggestion leaves nothing behind — which a document-level insert\n * could not promise.\n */\nexport const InlineSuggestions = Extension.create<{ actions: SuggestionActions | null }>({\n name: \"lalt-inline-suggestions\",\n\n addOptions() {\n return { actions: null };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n\n return [\n new Plugin<DecorationSet>({\n key: inlineSuggestionsKey,\n\n state: {\n init: () => DecorationSet.empty,\n apply(tr, old) {\n const set = tr.getMeta(inlineSuggestionsKey) as InlineSuggestion[] | undefined;\n if (set) return build(tr.doc, set, options.actions);\n // Positions follow the document as the author keeps typing around\n // a pending suggestion.\n return old.map(tr.mapping, tr.doc);\n },\n },\n\n props: {\n decorations(state) {\n return inlineSuggestionsKey.getState(state) ?? DecorationSet.empty;\n },\n },\n }),\n ];\n },\n});\n\nfunction build(\n doc: ProseMirrorNode,\n suggestions: InlineSuggestion[],\n actions: SuggestionActions | null,\n): DecorationSet {\n const decorations: Decoration[] = [];\n\n for (const s of suggestions) {\n if (s.from < 0 || s.to > doc.content.size || s.from >= s.to) continue;\n\n decorations.push(\n Decoration.inline(s.from, s.to, { class: \"lalt-suggestion__old\" }),\n Decoration.widget(s.to, () => renderProposal(s, actions), {\n // The widget belongs to the suggestion that precedes it, so typing at\n // the boundary does not push it around.\n side: 1,\n ignoreSelection: true,\n }),\n );\n }\n return DecorationSet.create(doc, decorations);\n}\n\nfunction renderProposal(s: InlineSuggestion, actions: SuggestionActions | null): HTMLElement {\n const wrap = document.createElement(\"span\");\n wrap.className = \"lalt-suggestion\";\n wrap.setAttribute(\"contenteditable\", \"false\");\n\n const text = document.createElement(\"span\");\n text.className = \"lalt-suggestion__new\";\n text.textContent = s.after;\n wrap.appendChild(text);\n\n if (actions) {\n wrap.appendChild(button(\"reject\", actions.rejectLabel, \"✕\", () => actions.onReject(s.id)));\n wrap.appendChild(button(\"accept\", actions.acceptLabel, \"✓\", () => actions.onAccept(s.id)));\n }\n return wrap;\n}\n\nfunction button(kind: string, label: string, glyph: string, onClick: () => void): HTMLElement {\n const el = document.createElement(\"button\");\n el.type = \"button\";\n el.className = `lalt-suggestion__action lalt-suggestion__action--${kind}`;\n el.setAttribute(\"aria-label\", label);\n el.textContent = glyph;\n // mousedown would move the selection into the widget before the click lands.\n el.addEventListener(\"mousedown\", (e) => e.preventDefault());\n el.addEventListener(\"click\", (e) => {\n e.preventDefault();\n onClick();\n });\n return el;\n}\n","export interface TextRange {\n from: number;\n to: number;\n}\n\nexport interface RevisionPart {\n before: string;\n after: string;\n}\n\n// Letters, digits, and the marks that belong inside a French word.\nconst WORD = /[\\p{L}\\p{N}'’-]/u;\n\n/**\n * expandToWords grows a range out to whole words.\n *\n * A selection dragged across a line lands mid-word more often than not, and a\n * model handed \"ns de production\" answers for a fragment — which then reads as\n * a truncated diff the author has to decode.\n */\nexport function expandToWords(text: string, from: number, to: number): TextRange {\n if (from >= to) return { from, to };\n\n let start = from;\n while (start > 0 && WORD.test(text[start - 1]) && WORD.test(text[start])) start--;\n\n let end = to;\n while (end < text.length && WORD.test(text[end]) && WORD.test(text[end - 1])) end++;\n\n return { from: start, to: end };\n}\n\n/**\n * splitRevision turns one model answer into the decisions it really contains.\n *\n * A three-paragraph rewrite accepted or rejected as a whole forces the author\n * to keep a weak paragraph for a strong one, so each paragraph becomes its own\n * decision — but only when both sides line up. When the model merged or split\n * paragraphs, position no longer maps them, and pairing by index would show a\n * diff that never existed.\n */\nexport function splitRevision(before: string, after: string): RevisionPart[] {\n if (before.trim() === after.trim()) return [];\n\n const olds = paragraphs(before);\n const news = paragraphs(after);\n\n if (olds.length !== news.length || olds.length === 0) {\n return [{ before: before.trim(), after: after.trim() }];\n }\n\n const parts: RevisionPart[] = [];\n for (let i = 0; i < olds.length; i++) {\n if (olds[i] !== news[i]) parts.push({ before: olds[i], after: news[i] });\n }\n return parts;\n}\n\nfunction paragraphs(s: string): string[] {\n return s\n .split(/\\n\\s*\\n/)\n .map((p) => p.trim())\n .filter((p) => p !== \"\");\n}\n","import type { RichDoc, RichNode } from \"./document\";\n\ninterface Mark {\n type?: string;\n attrs?: Record<string, unknown>;\n}\n\n/**\n * docToMarkdown renders a TipTap document as the markdown a blog accepts.\n *\n * Written here rather than taken from a library: the editor only ever produces\n * the handful of nodes StarterKit gives it, and a general markdown serializer\n * brings a dependency and its escaping rules for output a human is about to\n * paste into their own site.\n */\nexport function docToMarkdown(doc: RichDoc): string {\n return blocks(doc.content ?? [])\n .filter((b) => b !== \"\")\n .join(\"\\n\\n\");\n}\n\nfunction blocks(nodes: RichNode[]): string[] {\n return nodes.map(block);\n}\n\nfunction block(node: RichNode): string {\n switch (node.type) {\n case \"heading\": {\n const level = Number(node.attrs?.level ?? 2);\n return `${\"#\".repeat(Math.min(6, Math.max(1, level)))} ${inline(node.content)}`;\n }\n case \"blockquote\":\n return blocks(node.content ?? [])\n .filter((b) => b !== \"\")\n .map((b) =>\n b\n .split(\"\\n\")\n .map((line) => `> ${line}`)\n .join(\"\\n\"),\n )\n .join(\"\\n>\\n\");\n case \"codeBlock\": {\n const language = String(node.attrs?.language ?? \"\");\n return `\\`\\`\\`${language}\\n${inline(node.content)}\\n\\`\\`\\``;\n }\n case \"bulletList\":\n return listItems(node).map((item) => `- ${item}`).join(\"\\n\");\n case \"orderedList\":\n return listItems(node)\n .map((item, i) => `${i + 1}. ${item}`)\n .join(\"\\n\");\n case \"horizontalRule\":\n return \"---\";\n default:\n return inline(node.content);\n }\n}\n\nfunction listItems(list: RichNode): string[] {\n return (list.content ?? []).map((item) =>\n blocks(item.content ?? [])\n .filter((b) => b !== \"\")\n // Continuation lines are indented so the item stays one item.\n .join(\"\\n\\n\")\n .split(\"\\n\")\n .join(\"\\n \"),\n );\n}\n\nfunction inline(nodes: RichNode[] | undefined): string {\n return (nodes ?? []).map(text).join(\"\");\n}\n\nfunction text(node: RichNode): string {\n if (node.type === \"hardBreak\") return \"\\n\";\n if (typeof node.text !== \"string\") return inline(node.content);\n\n let out = node.text;\n // Innermost first, so bold inside a link wraps the text and not the URL.\n for (const mark of ((node.marks as Mark[] | undefined) ?? []).slice().reverse()) {\n switch (mark.type) {\n case \"bold\":\n out = `**${out}**`;\n break;\n case \"italic\":\n out = `*${out}*`;\n break;\n case \"strike\":\n out = `~~${out}~~`;\n break;\n case \"code\":\n out = `\\`${out}\\``;\n break;\n case \"link\":\n out = `[${out}](${String(mark.attrs?.href ?? \"\")})`;\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":["useEffect","useState","text","text","text"]}
1
+ {"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useViewport.ts","../src/viewport.ts","../src/useSelectionToolbar.ts","../src/formats.ts","../src/InlineSuggestions.ts","../src/passage.ts","../src/markdown.ts"],"sourcesContent":["export interface SelectionRect {\n /** Horizontal centre of the selection. */\n x: number;\n top: number;\n bottom: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport interface PlaceToolbarInput {\n selection: SelectionRect;\n toolbar: Size;\n viewport: Size;\n /** iOS renders its own callout above a selection, which covers a toolbar\n * placed there, so the caller asks for below on that platform. */\n preferBelow?: boolean;\n}\n\nexport interface ToolbarPlacement {\n left: number;\n top: number;\n below: boolean;\n}\n\nconst GAP = 8;\n\n/**\n * placeToolbar positions a floating toolbar against a selection.\n *\n * Kept apart from the component because it is the part that is wrong in subtle\n * ways — off-screen at the edges, hidden under a system callout — and the part\n * a test can pin down without a browser.\n */\nexport function placeToolbar({\n selection,\n toolbar,\n viewport,\n preferBelow = false,\n}: PlaceToolbarInput): ToolbarPlacement {\n const fitsAbove = selection.top - GAP - toolbar.height >= 0;\n const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;\n\n const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;\n\n const top = below\n ? selection.bottom + GAP\n : Math.max(0, selection.top - GAP - toolbar.height);\n\n const centred = selection.x - toolbar.width / 2;\n const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));\n\n return { left, top, below };\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { placeToolbar, type SelectionRect } from \"./placement\";\nimport { useViewport } from \"./useViewport\";\n\nexport interface ToolbarAction {\n id: string;\n label: string;\n icon?: React.ReactNode;\n onSelect: (selectedText: string) => void;\n}\n\nexport interface BlockFormat {\n id: string;\n label: string;\n shortcut?: string;\n isActive?: boolean;\n onSelect: () => void;\n}\n\nexport interface SelectionToolbarProps {\n selectedText: string;\n selection: SelectionRect;\n actions?: ToolbarAction[];\n /** Heading levels and list kinds, rendered behind a single trigger. */\n formats?: BlockFormat[];\n formatsLabel?: string;\n /** iOS draws its own callout above a selection; the host detects it. */\n preferBelow?: boolean;\n container?: HTMLElement | null;\n}\n\nconst FALLBACK = { width: 320, height: 40 };\n\nexport function SelectionToolbar({\n selectedText,\n selection,\n actions = [],\n formats = [],\n formatsLabel = \"Format\",\n preferBelow = false,\n container,\n}: SelectionToolbarProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(FALLBACK);\n const [formatsOpen, setFormatsOpen] = useState(false);\n const viewport = useViewport();\n\n // Measured after paint rather than assumed: the toolbar's width depends on\n // labels the host supplies, and guessing it puts the toolbar off-centre.\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n if (rect.width > 0) setSize({ width: rect.width, height: rect.height });\n }, [selectedText, actions.length, formats.length]);\n\n useEffect(() => {\n if (!formatsOpen) return;\n const close = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setFormatsOpen(false);\n };\n document.addEventListener(\"mousedown\", close);\n return () => document.removeEventListener(\"mousedown\", close);\n }, [formatsOpen]);\n\n if (typeof document === \"undefined\") return null;\n\n const place = placeToolbar({\n selection,\n toolbar: size,\n viewport: { width: viewport.width, height: viewport.height },\n preferBelow,\n });\n\n return createPortal(\n <div\n ref={ref}\n role=\"toolbar\"\n className=\"lalt-toolbar\"\n // Custom properties rather than left/top directly: under the mobile\n // breakpoint the stylesheet docks the toolbar to the bottom of the\n // screen and ignores these, which inline left/top would override.\n //\n // The inset is what the docked bar sits on top of. `bottom: 0` means the\n // bottom of the layout viewport, which the on-screen keyboard covers\n // without shrinking — so the bar has to be lifted by the strip the\n // keyboard occupies to stay in sight.\n style={\n {\n \"--lalt-toolbar-left\": `${place.left}px`,\n \"--lalt-toolbar-top\": `${place.top}px`,\n \"--lalt-keyboard-inset\": `${viewport.bottomInset}px`,\n } as React.CSSProperties\n }\n >\n {actions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n className=\"lalt-toolbar__button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => action.onSelect(selectedText)}\n >\n {action.icon}\n <span>{action.label}</span>\n </button>\n ))}\n\n {formats.length > 0 && (\n <div className=\"lalt-toolbar__formats\">\n <button\n type=\"button\"\n className=\"lalt-toolbar__button\"\n aria-haspopup=\"menu\"\n aria-expanded={formatsOpen}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => setFormatsOpen((open) => !open)}\n >\n {formatsLabel}\n </button>\n {formatsOpen && (\n <div role=\"menu\" className=\"lalt-toolbar__menu\">\n {formats.map((format) => (\n <button\n key={format.id}\n type=\"button\"\n role=\"menuitem\"\n className=\"lalt-toolbar__item\"\n aria-current={format.isActive}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n format.onSelect();\n setFormatsOpen(false);\n }}\n >\n <span>{format.label}</span>\n {format.shortcut && (\n <kbd className=\"lalt-toolbar__shortcut\">{format.shortcut}</kbd>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>,\n container ?? document.body,\n );\n}\n","import { useEffect, useState } from \"react\";\n\nimport { readViewport, type ViewportMetrics } from \"./viewport\";\n\nconst SERVER: ViewportMetrics = { width: 0, height: 0, offsetTop: 0, bottomInset: 0 };\n\nfunction read(): ViewportMetrics {\n if (typeof window === \"undefined\") return SERVER;\n return readViewport({\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n visual: window.visualViewport,\n });\n}\n\n/**\n * useViewport tracks the area a fixed element can occupy, keyboard included.\n *\n * The visual viewport's own events are the only ones a raised keyboard fires:\n * `window.resize` does not run on iOS when the keyboard opens, so a component\n * listening to it alone keeps the measurements it took before the keys\n * appeared.\n */\nexport function useViewport(): ViewportMetrics {\n const [metrics, setMetrics] = useState<ViewportMetrics>(read);\n\n useEffect(() => {\n const update = () => setMetrics(read());\n update();\n\n const visual = window.visualViewport;\n visual?.addEventListener(\"resize\", update);\n // iOS scrolls the visual viewport rather than the document to keep the\n // caret above the keyboard, and reports it here and nowhere else.\n visual?.addEventListener(\"scroll\", update);\n window.addEventListener(\"resize\", update);\n window.addEventListener(\"orientationchange\", update);\n\n return () => {\n visual?.removeEventListener(\"resize\", update);\n visual?.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n window.removeEventListener(\"orientationchange\", update);\n };\n }, []);\n\n return metrics;\n}\n","export interface VisualViewportLike {\n width: number;\n height: number;\n offsetTop: number;\n}\n\nexport interface ViewportMetrics {\n width: number;\n height: number;\n offsetTop: number;\n /** How much of the layout viewport's bottom is covered, the on-screen\n * keyboard included. Zero on a desktop browser and wherever the API is\n * missing. */\n bottomInset: number;\n}\n\nexport interface ReadViewportInput {\n innerWidth: number;\n innerHeight: number;\n visual?: VisualViewportLike | null;\n}\n\n/**\n * readViewport reports the area a fixed element can actually occupy.\n *\n * The distinction the on-screen keyboard turns on: `innerHeight` measures the\n * layout viewport, which the keyboard covers without shrinking, so an element\n * pinned to its bottom edge ends up underneath the keys. Only `visualViewport`\n * narrows, and the difference is the inset a docked toolbar has to clear.\n */\nexport function readViewport({\n innerWidth,\n innerHeight,\n visual,\n}: ReadViewportInput): ViewportMetrics {\n if (!visual) {\n return { width: innerWidth, height: innerHeight, offsetTop: 0, bottomInset: 0 };\n }\n\n // offsetTop is what iOS scrolls the visual viewport by; without it the\n // covered strip reads as smaller than it is once the page is scrolled under\n // a raised keyboard. Never negative: a viewport reported as taller than the\n // layout one — Android during a rotation, a browser mid-resize — would\n // otherwise push the toolbar down off the screen.\n const covered = innerHeight - visual.height - visual.offsetTop;\n\n return {\n width: visual.width,\n height: visual.height,\n offsetTop: visual.offsetTop,\n bottomInset: Math.max(0, Math.round(covered)),\n };\n}\n","import { useEffect, useState } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\n\nimport type { SelectionRect } from \"./placement\";\n\nexport interface SelectionInfo {\n text: string;\n rect: SelectionRect;\n}\n\n/**\n * useSelectionToolbar reports the current non-empty selection of an editor.\n *\n * Returns null whenever there is nothing to act on, so the caller renders the\n * toolbar only while it has a target.\n */\nexport function useSelectionToolbar(editor: Editor | null): SelectionInfo | null {\n const [selection, setSelection] = useState<SelectionInfo | null>(null);\n\n useEffect(() => {\n if (!editor) return;\n\n const read = () => {\n const { state } = editor;\n const { from, to, empty } = state.selection;\n if (empty || !editor.isFocused) {\n setSelection(null);\n return;\n }\n\n const text = state.doc.textBetween(from, to, \" \").trim();\n if (text === \"\") {\n setSelection(null);\n return;\n }\n\n const start = editor.view.coordsAtPos(from);\n const end = editor.view.coordsAtPos(to);\n setSelection({\n text,\n rect: {\n x: (start.left + end.right) / 2,\n top: Math.min(start.top, end.top),\n bottom: Math.max(start.bottom, end.bottom),\n },\n });\n };\n\n read();\n editor.on(\"selectionUpdate\", read);\n editor.on(\"blur\", read);\n editor.on(\"focus\", read);\n return () => {\n editor.off(\"selectionUpdate\", read);\n editor.off(\"blur\", read);\n editor.off(\"focus\", read);\n };\n }, [editor]);\n\n return selection;\n}\n","import type { Editor } from \"@tiptap/react\";\n\n// Type-only: these commands are contributed by StarterKit's extensions, and\n// without the reference TypeScript does not know they exist on a chain. No\n// runtime import, so the host stays in charge of which extensions it mounts —\n// blockFormats simply returns entries whose commands no-op if one is missing.\nimport type {} from \"@tiptap/starter-kit\";\n\nimport type { BlockFormat } from \"./SelectionToolbar\";\n\nexport interface FormatLabels {\n paragraph: string;\n heading1: string;\n heading2: string;\n heading3: string;\n orderedList: string;\n bulletList: string;\n}\n\nexport const defaultFormatLabels: FormatLabels = {\n paragraph: \"Texte\",\n heading1: \"Titre 1\",\n heading2: \"Titre 2\",\n heading3: \"Titre 3\",\n orderedList: \"Liste numérotée\",\n bulletList: \"Liste à puces\",\n};\n\n/**\n * blockFormats is the set a writer reaches for on a selected passage.\n *\n * Labels are passed in rather than translated here: the package carries no\n * i18n runtime, so the host names things in whatever language it speaks.\n */\nexport function blockFormats(\n editor: Editor | null,\n labels: FormatLabels = defaultFormatLabels,\n): BlockFormat[] {\n if (!editor) return [];\n\n // Shortcuts are the ones TipTap's own extensions bind, read from their\n // source rather than invented: heading binds Mod-Alt-<level>, the list\n // extensions bind Mod-Shift-7/8, and setParagraph binds nothing at all.\n // A label promising a shortcut that does nothing is worse than no label.\n const mod = isApple() ? \"⌘\" : \"Ctrl\";\n\n return [\n {\n id: \"paragraph\",\n label: labels.paragraph,\n isActive: editor.isActive(\"paragraph\"),\n onSelect: () => editor.chain().focus().setParagraph().run(),\n },\n ...([1, 2, 3] as const).map((level) => ({\n id: `heading${level}`,\n label: labels[`heading${level}` as keyof FormatLabels],\n shortcut: `${mod}+Alt+${level}`,\n isActive: editor.isActive(\"heading\", { level }),\n onSelect: () => editor.chain().focus().toggleHeading({ level }).run(),\n })),\n {\n id: \"orderedList\",\n label: labels.orderedList,\n shortcut: `${mod}+Shift+7`,\n isActive: editor.isActive(\"orderedList\"),\n onSelect: () => editor.chain().focus().toggleOrderedList().run(),\n },\n {\n id: \"bulletList\",\n label: labels.bulletList,\n shortcut: `${mod}+Shift+8`,\n isActive: editor.isActive(\"bulletList\"),\n onSelect: () => editor.chain().focus().toggleBulletList().run(),\n },\n ];\n}\n\nfunction isApple(): boolean {\n if (typeof navigator === \"undefined\") return false;\n return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);\n}\n","import { Extension } from \"@tiptap/core\";\nimport { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\nimport type { Node as ProseMirrorNode } from \"@tiptap/pm/model\";\n\nexport interface InlineSuggestion {\n id: string;\n /** Document positions of the passage this replaces. */\n from: number;\n to: number;\n after: string;\n}\n\nexport interface SuggestionActions {\n onAccept: (id: string) => void;\n onReject: (id: string) => void;\n acceptLabel: string;\n rejectLabel: string;\n}\n\nexport const inlineSuggestionsKey = new PluginKey<DecorationSet>(\"lalt-inline-suggestions\");\n\n/**\n * InlineSuggestions renders proposed edits where they belong: over the passage\n * they replace, with the new text right after it.\n *\n * Decorations, never document changes. The editor's content stays exactly what\n * the author wrote until they accept, so the draft is saveable mid-review and\n * a rejected suggestion leaves nothing behind — which a document-level insert\n * could not promise.\n */\nexport const InlineSuggestions = Extension.create<{ actions: SuggestionActions | null }>({\n name: \"lalt-inline-suggestions\",\n\n addOptions() {\n return { actions: null };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n\n return [\n new Plugin<DecorationSet>({\n key: inlineSuggestionsKey,\n\n state: {\n init: () => DecorationSet.empty,\n apply(tr, old) {\n const set = tr.getMeta(inlineSuggestionsKey) as InlineSuggestion[] | undefined;\n if (set) return build(tr.doc, set, options.actions);\n // Positions follow the document as the author keeps typing around\n // a pending suggestion.\n return old.map(tr.mapping, tr.doc);\n },\n },\n\n props: {\n decorations(state) {\n return inlineSuggestionsKey.getState(state) ?? DecorationSet.empty;\n },\n },\n }),\n ];\n },\n});\n\nfunction build(\n doc: ProseMirrorNode,\n suggestions: InlineSuggestion[],\n actions: SuggestionActions | null,\n): DecorationSet {\n const decorations: Decoration[] = [];\n\n for (const s of suggestions) {\n if (s.from < 0 || s.to > doc.content.size || s.from >= s.to) continue;\n\n decorations.push(\n Decoration.inline(s.from, s.to, { class: \"lalt-suggestion__old\" }),\n Decoration.widget(s.to, () => renderProposal(s, actions), {\n // The widget belongs to the suggestion that precedes it, so typing at\n // the boundary does not push it around.\n side: 1,\n ignoreSelection: true,\n }),\n );\n }\n return DecorationSet.create(doc, decorations);\n}\n\nfunction renderProposal(s: InlineSuggestion, actions: SuggestionActions | null): HTMLElement {\n const wrap = document.createElement(\"span\");\n wrap.className = \"lalt-suggestion\";\n wrap.setAttribute(\"contenteditable\", \"false\");\n\n const text = document.createElement(\"span\");\n text.className = \"lalt-suggestion__new\";\n text.textContent = s.after;\n wrap.appendChild(text);\n\n if (actions) {\n wrap.appendChild(button(\"reject\", actions.rejectLabel, \"✕\", () => actions.onReject(s.id)));\n wrap.appendChild(button(\"accept\", actions.acceptLabel, \"✓\", () => actions.onAccept(s.id)));\n }\n return wrap;\n}\n\nfunction button(kind: string, label: string, glyph: string, onClick: () => void): HTMLElement {\n const el = document.createElement(\"button\");\n el.type = \"button\";\n el.className = `lalt-suggestion__action lalt-suggestion__action--${kind}`;\n el.setAttribute(\"aria-label\", label);\n el.textContent = glyph;\n // mousedown would move the selection into the widget before the click lands.\n el.addEventListener(\"mousedown\", (e) => e.preventDefault());\n el.addEventListener(\"click\", (e) => {\n e.preventDefault();\n onClick();\n });\n return el;\n}\n","export interface TextRange {\n from: number;\n to: number;\n}\n\nexport interface RevisionPart {\n before: string;\n after: string;\n}\n\n// Letters, digits, and the marks that belong inside a French word.\nconst WORD = /[\\p{L}\\p{N}'’-]/u;\n\n/**\n * expandToWords grows a range out to whole words.\n *\n * A selection dragged across a line lands mid-word more often than not, and a\n * model handed \"ns de production\" answers for a fragment — which then reads as\n * a truncated diff the author has to decode.\n */\nexport function expandToWords(text: string, from: number, to: number): TextRange {\n if (from >= to) return { from, to };\n\n let start = from;\n while (start > 0 && WORD.test(text[start - 1]) && WORD.test(text[start])) start--;\n\n let end = to;\n while (end < text.length && WORD.test(text[end]) && WORD.test(text[end - 1])) end++;\n\n return { from: start, to: end };\n}\n\n/**\n * splitRevision turns one model answer into the decisions it really contains.\n *\n * A three-paragraph rewrite accepted or rejected as a whole forces the author\n * to keep a weak paragraph for a strong one, so each paragraph becomes its own\n * decision — but only when both sides line up. When the model merged or split\n * paragraphs, position no longer maps them, and pairing by index would show a\n * diff that never existed.\n */\nexport function splitRevision(before: string, after: string): RevisionPart[] {\n if (before.trim() === after.trim()) return [];\n\n const olds = paragraphs(before);\n const news = paragraphs(after);\n\n if (olds.length !== news.length || olds.length === 0) {\n return [{ before: before.trim(), after: after.trim() }];\n }\n\n const parts: RevisionPart[] = [];\n for (let i = 0; i < olds.length; i++) {\n if (olds[i] !== news[i]) parts.push({ before: olds[i], after: news[i] });\n }\n return parts;\n}\n\nfunction paragraphs(s: string): string[] {\n return s\n .split(/\\n\\s*\\n/)\n .map((p) => p.trim())\n .filter((p) => p !== \"\");\n}\n","import type { RichDoc, RichNode } from \"./document\";\n\ninterface Mark {\n type?: string;\n attrs?: Record<string, unknown>;\n}\n\n/**\n * docToMarkdown renders a TipTap document as the markdown a blog accepts.\n *\n * Written here rather than taken from a library: the editor only ever produces\n * the handful of nodes StarterKit gives it, and a general markdown serializer\n * brings a dependency and its escaping rules for output a human is about to\n * paste into their own site.\n */\nexport function docToMarkdown(doc: RichDoc): string {\n return blocks(doc.content ?? [])\n .filter((b) => b !== \"\")\n .join(\"\\n\\n\");\n}\n\nfunction blocks(nodes: RichNode[]): string[] {\n return nodes.map(block);\n}\n\nfunction block(node: RichNode): string {\n switch (node.type) {\n case \"heading\": {\n const level = Number(node.attrs?.level ?? 2);\n return `${\"#\".repeat(Math.min(6, Math.max(1, level)))} ${inline(node.content)}`;\n }\n case \"blockquote\":\n return blocks(node.content ?? [])\n .filter((b) => b !== \"\")\n .map((b) =>\n b\n .split(\"\\n\")\n .map((line) => `> ${line}`)\n .join(\"\\n\"),\n )\n .join(\"\\n>\\n\");\n case \"codeBlock\": {\n const language = String(node.attrs?.language ?? \"\");\n return `\\`\\`\\`${language}\\n${inline(node.content)}\\n\\`\\`\\``;\n }\n case \"bulletList\":\n return listItems(node).map((item) => `- ${item}`).join(\"\\n\");\n case \"orderedList\":\n return listItems(node)\n .map((item, i) => `${i + 1}. ${item}`)\n .join(\"\\n\");\n case \"horizontalRule\":\n return \"---\";\n default:\n return inline(node.content);\n }\n}\n\nfunction listItems(list: RichNode): string[] {\n return (list.content ?? []).map((item) =>\n blocks(item.content ?? [])\n .filter((b) => b !== \"\")\n // Continuation lines are indented so the item stays one item.\n .join(\"\\n\\n\")\n .split(\"\\n\")\n .join(\"\\n \"),\n );\n}\n\nfunction inline(nodes: RichNode[] | undefined): string {\n return (nodes ?? []).map(text).join(\"\");\n}\n\nfunction text(node: RichNode): string {\n if (node.type === \"hardBreak\") return \"\\n\";\n if (typeof node.text !== \"string\") return inline(node.content);\n\n let out = node.text;\n // Innermost first, so bold inside a link wraps the text and not the URL.\n for (const mark of ((node.marks as Mark[] | undefined) ?? []).slice().reverse()) {\n switch (mark.type) {\n case \"bold\":\n out = `**${out}**`;\n break;\n case \"italic\":\n out = `*${out}*`;\n break;\n case \"strike\":\n out = `~~${out}~~`;\n break;\n case \"code\":\n out = `\\`${out}\\``;\n break;\n case \"link\":\n out = `[${out}](${String(mark.attrs?.href ?? \"\")})`;\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":["useEffect","useState","useState","useEffect","useEffect","useState","read","text","text","text"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/editor",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Shared TipTap editor for L'Alternative apps: rich text, format menu, selection toolbar and inline AI revisions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,8 @@
23
23
  "build": "tsup",
24
24
  "dev": "tsup --watch",
25
25
  "typecheck": "tsc --noEmit",
26
- "test": "vitest run"
26
+ "test": "vitest run",
27
+ "playground": "vite --config playground/vite.config.ts"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "@tiptap/core": ">=3.0.0",
@@ -40,10 +41,12 @@
40
41
  "@tiptap/starter-kit": "^3.29.2",
41
42
  "@types/react": "^19.0.0",
42
43
  "@types/react-dom": "^19.0.0",
44
+ "@vitejs/plugin-react": "^5.0.0",
43
45
  "react": "^19.0.0",
44
46
  "react-dom": "^19.0.0",
45
47
  "tsup": "^8.5.1",
46
48
  "typescript": "^5.7.0",
49
+ "vite": "^7.1.7",
47
50
  "vitest": "^3.0.0"
48
51
  },
49
52
  "publishConfig": {