@synerise/ds-rich-text 0.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +173 -0
  4. package/dist/RichText.d.ts +4 -0
  5. package/dist/RichText.js +179 -0
  6. package/dist/RichText.styles.d.ts +31 -0
  7. package/dist/RichText.styles.js +87 -0
  8. package/dist/RichText.types.d.ts +139 -0
  9. package/dist/RichText.types.js +44 -0
  10. package/dist/blockModel/blockModel.types.d.ts +130 -0
  11. package/dist/blockModel/blockModel.types.js +1 -0
  12. package/dist/blockModel/index.d.ts +2 -0
  13. package/dist/blockModel/index.js +14 -0
  14. package/dist/blockModel/serializers/documentToHtml.d.ts +2 -0
  15. package/dist/blockModel/serializers/documentToHtml.js +101 -0
  16. package/dist/blockModel/serializers/documentToMarkdown.d.ts +2 -0
  17. package/dist/blockModel/serializers/documentToMarkdown.js +84 -0
  18. package/dist/blockModel/serializers/documentToTiptap.d.ts +2 -0
  19. package/dist/blockModel/serializers/documentToTiptap.js +133 -0
  20. package/dist/blockModel/serializers/htmlToDocument.d.ts +2 -0
  21. package/dist/blockModel/serializers/htmlToDocument.js +242 -0
  22. package/dist/blockModel/serializers/index.d.ts +6 -0
  23. package/dist/blockModel/serializers/index.js +14 -0
  24. package/dist/blockModel/serializers/markdownToDocument.d.ts +10 -0
  25. package/dist/blockModel/serializers/markdownToDocument.js +11 -0
  26. package/dist/blockModel/serializers/tiptapToDocument.d.ts +2 -0
  27. package/dist/blockModel/serializers/tiptapToDocument.js +189 -0
  28. package/dist/components/ImagePopover.d.ts +3 -0
  29. package/dist/components/ImagePopover.js +89 -0
  30. package/dist/components/LinkPopover.d.ts +3 -0
  31. package/dist/components/LinkPopover.js +57 -0
  32. package/dist/components/Toolbar.d.ts +3 -0
  33. package/dist/components/Toolbar.js +436 -0
  34. package/dist/components/index.d.ts +3 -0
  35. package/dist/components/index.js +8 -0
  36. package/dist/extensions/CodeSnippet.d.ts +19 -0
  37. package/dist/extensions/CodeSnippet.js +76 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +52 -0
  40. package/dist/modules.d.js +1 -0
  41. package/dist/modules.d.ts +4 -0
  42. package/dist/utils/computeVisibleCount.d.ts +6 -0
  43. package/dist/utils/computeVisibleCount.js +19 -0
  44. package/dist/utils/getExtensions.d.ts +13 -0
  45. package/dist/utils/getExtensions.js +80 -0
  46. package/dist/utils/index.d.ts +3 -0
  47. package/dist/utils/index.js +8 -0
  48. package/dist/utils/normalizeUrl.d.ts +7 -0
  49. package/dist/utils/normalizeUrl.js +13 -0
  50. package/dist/utils/sanitizeUrl.d.ts +9 -0
  51. package/dist/utils/sanitizeUrl.js +30 -0
  52. package/package.json +75 -0
@@ -0,0 +1,436 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback, useLayoutEffect, useEffect } from "react";
3
+ import Button from "@synerise/ds-button";
4
+ import { useTheme } from "@synerise/ds-core";
5
+ import Icon, { BoldM, ItalicM, StrikeM, UnderlineM, ListM, ListNumbersM, LinkM, CodeM, Grid2M, ImageM, OptionHorizontalM, AiStarM, TextM, AlignRightM, AlignCenterM, AlignLeftM, H3M, H2M, H1M, TableL, AddM, DeleteM, TrashM } from "@synerise/ds-icon";
6
+ import Popover, { PopoverTrigger, PopoverContent } from "@synerise/ds-popover";
7
+ import DSToolbar, { ToolbarButton, ToolbarGroup } from "@synerise/ds-toolbar";
8
+ import { BlockMenu, ToolbarItems, ToolbarEntry, AiGradientLabel, ToolbarSeparator } from "../RichText.styles.js";
9
+ import { computeVisibleCount } from "../utils/computeVisibleCount.js";
10
+ import { ImagePopover } from "./ImagePopover.js";
11
+ import { LinkPopover } from "./LinkPopover.js";
12
+ const HEADING_ICONS = {
13
+ 1: /* @__PURE__ */ jsx(H1M, {}),
14
+ 2: /* @__PURE__ */ jsx(H2M, {}),
15
+ 3: /* @__PURE__ */ jsx(H3M, {})
16
+ };
17
+ const MORE_BUTTON_WIDTH_PX = 40;
18
+ const ALIGN_CYCLE = ["left", "center", "right"];
19
+ const ALIGN_ICONS = {
20
+ left: /* @__PURE__ */ jsx(AlignLeftM, {}),
21
+ center: /* @__PURE__ */ jsx(AlignCenterM, {}),
22
+ right: /* @__PURE__ */ jsx(AlignRightM, {})
23
+ };
24
+ const ALIGN_LABELS = {
25
+ left: (texts) => texts.alignLeft,
26
+ center: (texts) => texts.alignCenter,
27
+ right: (texts) => texts.alignRight
28
+ };
29
+ const TABLE_ACTIONS = [{
30
+ key: "insertTable",
31
+ icon: /* @__PURE__ */ jsx(TableL, {}),
32
+ label: (t) => t.insertTable,
33
+ insert: true,
34
+ run: (editor) => editor.chain().focus().insertTable({
35
+ rows: 3,
36
+ cols: 3,
37
+ withHeaderRow: true
38
+ }).run()
39
+ }, {
40
+ key: "addRowBefore",
41
+ icon: /* @__PURE__ */ jsx(AddM, {}),
42
+ label: (t) => t.addRowBefore,
43
+ run: (editor) => editor.chain().focus().addRowBefore().run()
44
+ }, {
45
+ key: "addRowAfter",
46
+ icon: /* @__PURE__ */ jsx(AddM, {}),
47
+ label: (t) => t.addRowAfter,
48
+ run: (editor) => editor.chain().focus().addRowAfter().run()
49
+ }, {
50
+ key: "addColumnBefore",
51
+ icon: /* @__PURE__ */ jsx(AddM, {}),
52
+ label: (t) => t.addColumnBefore,
53
+ run: (editor) => editor.chain().focus().addColumnBefore().run()
54
+ }, {
55
+ key: "addColumnAfter",
56
+ icon: /* @__PURE__ */ jsx(AddM, {}),
57
+ label: (t) => t.addColumnAfter,
58
+ run: (editor) => editor.chain().focus().addColumnAfter().run()
59
+ }, {
60
+ key: "deleteRow",
61
+ icon: /* @__PURE__ */ jsx(DeleteM, {}),
62
+ label: (t) => t.deleteRow,
63
+ run: (editor) => editor.chain().focus().deleteRow().run()
64
+ }, {
65
+ key: "deleteColumn",
66
+ icon: /* @__PURE__ */ jsx(DeleteM, {}),
67
+ label: (t) => t.deleteColumn,
68
+ run: (editor) => editor.chain().focus().deleteColumn().run()
69
+ }, {
70
+ key: "deleteTable",
71
+ icon: /* @__PURE__ */ jsx(TrashM, {}),
72
+ label: (t) => t.deleteTable,
73
+ run: (editor) => editor.chain().focus().deleteTable().run()
74
+ }];
75
+ const outerWidth = (el) => {
76
+ const styles = window.getComputedStyle(el);
77
+ return el.getBoundingClientRect().width + (parseFloat(styles.marginLeft) || 0) + (parseFloat(styles.marginRight) || 0);
78
+ };
79
+ const Toolbar = ({
80
+ editor,
81
+ features,
82
+ headingLevels,
83
+ texts,
84
+ disabled,
85
+ onImageUpload,
86
+ onEditWithAI,
87
+ editWithAIOptions
88
+ }) => {
89
+ const theme = useTheme();
90
+ const [showLinkPopover, setShowLinkPopover] = useState(false);
91
+ const [showImagePopover, setShowImagePopover] = useState(false);
92
+ const [showBlockMenu, setShowBlockMenu] = useState(false);
93
+ const [showTableMenu, setShowTableMenu] = useState(false);
94
+ const [showAlignMenu, setShowAlignMenu] = useState(false);
95
+ const [showAiMenu, setShowAiMenu] = useState(false);
96
+ const [showMoreMenu, setShowMoreMenu] = useState(false);
97
+ const [morePanel, setMorePanel] = useState("menu");
98
+ const [aiLoading, setAiLoading] = useState(false);
99
+ const itemsRef = useRef(null);
100
+ const widthsRef = useRef(null);
101
+ const entriesCountRef = useRef(0);
102
+ const [visibleCount, setVisibleCount] = useState(Number.POSITIVE_INFINITY);
103
+ const visibleCountRef = useRef(visibleCount);
104
+ visibleCountRef.current = visibleCount;
105
+ const recompute = useCallback(() => {
106
+ const el = itemsRef.current;
107
+ if (!el) {
108
+ return;
109
+ }
110
+ if (el.clientWidth <= 0) {
111
+ if (Number.isFinite(visibleCountRef.current)) {
112
+ setVisibleCount(Number.POSITIVE_INFINITY);
113
+ }
114
+ return;
115
+ }
116
+ if (!Number.isFinite(visibleCountRef.current)) {
117
+ widthsRef.current = Array.from(el.children).map(outerWidth);
118
+ } else if (!widthsRef.current) {
119
+ return;
120
+ } else if (el.scrollWidth > el.clientWidth) {
121
+ widthsRef.current = null;
122
+ setVisibleCount(Number.POSITIVE_INFINITY);
123
+ return;
124
+ }
125
+ const next = computeVisibleCount(widthsRef.current, el.clientWidth, MORE_BUTTON_WIDTH_PX);
126
+ setVisibleCount(next >= entriesCountRef.current ? Number.POSITIVE_INFINITY : next);
127
+ }, []);
128
+ useLayoutEffect(() => {
129
+ recompute();
130
+ });
131
+ const hasEditor = !!editor;
132
+ useEffect(() => {
133
+ const el = itemsRef.current;
134
+ if (!el || typeof ResizeObserver === "undefined") {
135
+ return void 0;
136
+ }
137
+ const observer = new ResizeObserver(() => recompute());
138
+ observer.observe(el);
139
+ return () => observer.disconnect();
140
+ }, [recompute, hasEditor]);
141
+ useEffect(() => {
142
+ let cancelled = false;
143
+ document.fonts?.ready?.then(() => {
144
+ if (!cancelled) {
145
+ recompute();
146
+ }
147
+ });
148
+ return () => {
149
+ cancelled = true;
150
+ };
151
+ }, [recompute]);
152
+ const handleLinkToggle = useCallback(() => {
153
+ setShowLinkPopover((prev) => !prev);
154
+ }, []);
155
+ const handleLinkClose = useCallback(() => {
156
+ setShowLinkPopover(false);
157
+ }, []);
158
+ const handleImageToggle = useCallback(() => {
159
+ setShowImagePopover((prev) => !prev);
160
+ }, []);
161
+ const handleImageClose = useCallback(() => {
162
+ setShowImagePopover(false);
163
+ }, []);
164
+ const closeMore = useCallback(() => {
165
+ setShowMoreMenu(false);
166
+ setMorePanel("menu");
167
+ }, []);
168
+ const runAIAction = useCallback(async (action) => {
169
+ if (!editor) {
170
+ return;
171
+ }
172
+ const {
173
+ from,
174
+ to,
175
+ empty
176
+ } = editor.state.selection;
177
+ const selectedText = empty ? editor.getText() : editor.state.doc.textBetween(from, to, "\n");
178
+ setAiLoading(true);
179
+ try {
180
+ const result = await action(selectedText);
181
+ if (result === null || result === void 0) {
182
+ return;
183
+ }
184
+ if (empty) {
185
+ editor.chain().focus().selectAll().insertContent(result).run();
186
+ } else {
187
+ editor.chain().focus().insertContentAt({
188
+ from,
189
+ to
190
+ }, result).run();
191
+ }
192
+ } catch (error) {
193
+ console.error("RichText: Edit with AI action failed", error);
194
+ } finally {
195
+ setAiLoading(false);
196
+ }
197
+ }, [editor]);
198
+ const handleEditWithAI = useCallback(() => {
199
+ if (onEditWithAI) {
200
+ void runAIAction(onEditWithAI);
201
+ }
202
+ }, [onEditWithAI, runAIAction]);
203
+ const activeHeadingLevel = editor ? headingLevels.find((level) => editor.isActive("heading", {
204
+ level
205
+ })) : void 0;
206
+ const blockLabel = activeHeadingLevel ? `${texts.heading} ${activeHeadingLevel}` : texts.paragraph;
207
+ const measureSignature = [features.join(","), blockLabel, disabled].join("|");
208
+ useLayoutEffect(() => {
209
+ widthsRef.current = null;
210
+ setVisibleCount(Number.POSITIVE_INFINITY);
211
+ }, [measureSignature, texts, headingLevels]);
212
+ if (!editor) {
213
+ return null;
214
+ }
215
+ const has = (feature) => features.includes(feature);
216
+ const hasAiOptions = !!editWithAIOptions?.length;
217
+ const closeBlockMenu = () => setShowBlockMenu(false);
218
+ const renderHeadingMenuItems = (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
219
+ /* @__PURE__ */ jsx(Button, { type: !activeHeadingLevel ? "ghost-primary" : "ghost", mode: "icon-label", icon: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(TextM, {}) }), onClick: () => {
220
+ editor.chain().focus().setParagraph().run();
221
+ close();
222
+ }, children: texts.paragraph }),
223
+ headingLevels.map((level) => /* @__PURE__ */ jsx(Button, { type: activeHeadingLevel === level ? "ghost-primary" : "ghost", mode: "icon-label", icon: /* @__PURE__ */ jsx(Icon, { component: HEADING_ICONS[level] || HEADING_ICONS[1] }), onClick: () => {
224
+ editor.chain().focus().toggleHeading({
225
+ level
226
+ }).run();
227
+ close();
228
+ }, children: `${texts.heading} ${level}` }, level))
229
+ ] });
230
+ const renderTableMenuItems = (close) => {
231
+ const inTable = editor.isActive("table");
232
+ return TABLE_ACTIONS.map((action) => {
233
+ const enabled = action.insert || inTable;
234
+ return /* @__PURE__ */ jsx(Button, { type: "ghost", mode: "icon-label", disabled: !enabled, icon: /* @__PURE__ */ jsx(Icon, { component: action.icon }), onClick: () => {
235
+ action.run(editor);
236
+ close();
237
+ }, children: action.label(texts) }, action.key);
238
+ });
239
+ };
240
+ const renderAlignMenuItems = (close) => ALIGN_CYCLE.map((align) => /* @__PURE__ */ jsx(Button, { type: editor.isActive({
241
+ textAlign: align
242
+ }) ? "ghost-primary" : "ghost", mode: "icon-label", disabled, icon: /* @__PURE__ */ jsx(Icon, { component: ALIGN_ICONS[align] }), onClick: () => {
243
+ editor.chain().focus().setTextAlign(align).run();
244
+ close();
245
+ }, children: ALIGN_LABELS[align](texts) }, align));
246
+ const actionEntry = (key, icon, label, active, run) => ({
247
+ key,
248
+ inline: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
249
+ title: label
250
+ }, onClick: run, disabled, type: active ? "ghost-primary" : "ghost", children: /* @__PURE__ */ jsx(Icon, { component: icon }) }),
251
+ menuRow: /* @__PURE__ */ jsx(Button, { type: active ? "ghost-primary" : "ghost", mode: "icon-label", disabled, icon: /* @__PURE__ */ jsx(Icon, { component: icon }), onClick: () => {
252
+ run();
253
+ closeMore();
254
+ }, children: label }, key)
255
+ });
256
+ const panelEntryRow = (key, icon, label, active, panel) => /* @__PURE__ */ jsx(Button, { type: active ? "ghost-primary" : "ghost", mode: "icon-label", disabled, icon: /* @__PURE__ */ jsx(Icon, { component: icon }), onClick: () => setMorePanel(panel), children: label }, key);
257
+ const sections = [];
258
+ if (has("heading")) {
259
+ sections.push([{
260
+ key: "heading",
261
+ inline: /* @__PURE__ */ jsxs(Popover, { open: showBlockMenu, onOpenChange: (open) => {
262
+ if (!open) {
263
+ closeBlockMenu();
264
+ }
265
+ }, placement: "bottom-start", children: [
266
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: showBlockMenu ? "ghost-primary" : "ghost", disabled, onClick: () => setShowBlockMenu((prev) => !prev), children: blockLabel }) }),
267
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(BlockMenu, { children: renderHeadingMenuItems(closeBlockMenu) }) })
268
+ ] }),
269
+ menuRow: panelEntryRow("heading", /* @__PURE__ */ jsx(TextM, {}), blockLabel, !!activeHeadingLevel, "heading")
270
+ }]);
271
+ }
272
+ const formatting = [];
273
+ if (has("bold")) {
274
+ formatting.push(actionEntry("bold", /* @__PURE__ */ jsx(BoldM, {}), texts.bold, editor.isActive("bold"), () => editor.chain().focus().toggleBold().run()));
275
+ }
276
+ if (has("italic")) {
277
+ formatting.push(actionEntry("italic", /* @__PURE__ */ jsx(ItalicM, {}), texts.italic, editor.isActive("italic"), () => editor.chain().focus().toggleItalic().run()));
278
+ }
279
+ if (has("strikethrough")) {
280
+ formatting.push(actionEntry("strikethrough", /* @__PURE__ */ jsx(StrikeM, {}), texts.strikethrough, editor.isActive("strike"), () => editor.chain().focus().toggleStrike().run()));
281
+ }
282
+ if (has("underline")) {
283
+ formatting.push(actionEntry("underline", /* @__PURE__ */ jsx(UnderlineM, {}), texts.underline, editor.isActive("underline"), () => editor.chain().focus().toggleUnderline().run()));
284
+ }
285
+ if (formatting.length) {
286
+ sections.push(formatting);
287
+ }
288
+ const lists = [];
289
+ if (has("bulletList")) {
290
+ lists.push(actionEntry("bulletList", /* @__PURE__ */ jsx(ListM, {}), texts.bulletList, editor.isActive("bulletList"), () => editor.chain().focus().toggleBulletList().run()));
291
+ }
292
+ if (has("orderedList")) {
293
+ lists.push(actionEntry("orderedList", /* @__PURE__ */ jsx(ListNumbersM, {}), texts.orderedList, editor.isActive("orderedList"), () => editor.chain().focus().toggleOrderedList().run()));
294
+ }
295
+ if (has("textAlign")) {
296
+ const currentAlign = ALIGN_CYCLE.find((a) => editor.isActive({
297
+ textAlign: a
298
+ })) ?? "left";
299
+ lists.push({
300
+ key: "textAlign",
301
+ inline: /* @__PURE__ */ jsxs(Popover, { open: showAlignMenu, onOpenChange: (open) => {
302
+ if (!open) {
303
+ setShowAlignMenu(false);
304
+ }
305
+ }, placement: "bottom-start", children: [
306
+ /* @__PURE__ */ jsx(PopoverTrigger, { children: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
307
+ title: texts.align,
308
+ open: showAlignMenu ? false : void 0
309
+ }, onClick: () => setShowAlignMenu((prev) => !prev), disabled, type: currentAlign !== "left" || showAlignMenu ? "ghost-primary" : "ghost", children: /* @__PURE__ */ jsx(Icon, { component: ALIGN_ICONS[currentAlign] }) }) }),
310
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(BlockMenu, { children: renderAlignMenuItems(() => setShowAlignMenu(false)) }) })
311
+ ] }),
312
+ menuRow: panelEntryRow("textAlign", ALIGN_ICONS[currentAlign], texts.align, currentAlign !== "left", "align")
313
+ });
314
+ }
315
+ if (lists.length) {
316
+ sections.push(lists);
317
+ }
318
+ const extras = [];
319
+ if (has("link")) {
320
+ extras.push({
321
+ key: "link",
322
+ inline: /* @__PURE__ */ jsxs(Popover, { open: showLinkPopover, onOpenChange: (open) => {
323
+ if (!open) {
324
+ handleLinkClose();
325
+ }
326
+ }, placement: "bottom-start", children: [
327
+ /* @__PURE__ */ jsx(PopoverTrigger, { children: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
328
+ title: texts.link,
329
+ open: showLinkPopover ? false : void 0
330
+ }, onClick: handleLinkToggle, disabled, type: editor.isActive("link") ? "ghost-primary" : "ghost", children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(LinkM, {}) }) }) }),
331
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(LinkPopover, { editor, texts, onClose: handleLinkClose }) })
332
+ ] }),
333
+ menuRow: panelEntryRow("link", /* @__PURE__ */ jsx(LinkM, {}), texts.link, editor.isActive("link"), "link")
334
+ });
335
+ }
336
+ if (has("codeSnippet")) {
337
+ extras.push(actionEntry("codeSnippet", /* @__PURE__ */ jsx(CodeM, {}), texts.codeSnippet, editor.isActive("codeSnippet"), () => editor.chain().focus().insertCodeSnippet().run()));
338
+ }
339
+ if (has("codeBlock")) {
340
+ extras.push(actionEntry("codeBlock", /* @__PURE__ */ jsx(CodeM, {}), texts.codeBlock, editor.isActive("codeBlock"), () => editor.chain().focus().toggleCodeBlock().run()));
341
+ }
342
+ if (has("table")) {
343
+ const inTable = editor.isActive("table");
344
+ extras.push({
345
+ key: "table",
346
+ inline: /* @__PURE__ */ jsxs(Popover, { open: showTableMenu, onOpenChange: (open) => {
347
+ if (!open) {
348
+ setShowTableMenu(false);
349
+ }
350
+ }, placement: "bottom-start", children: [
351
+ /* @__PURE__ */ jsx(PopoverTrigger, { children: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
352
+ title: texts.table,
353
+ open: showTableMenu ? false : void 0
354
+ }, onClick: () => setShowTableMenu((prev) => !prev), disabled, type: inTable || showTableMenu ? "ghost-primary" : "ghost", children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(Grid2M, {}) }) }) }),
355
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(BlockMenu, { children: renderTableMenuItems(() => setShowTableMenu(false)) }) })
356
+ ] }),
357
+ menuRow: panelEntryRow("table", /* @__PURE__ */ jsx(Grid2M, {}), texts.table, inTable, "table")
358
+ });
359
+ }
360
+ if (has("image")) {
361
+ extras.push({
362
+ key: "image",
363
+ inline: /* @__PURE__ */ jsxs(Popover, { open: showImagePopover, onOpenChange: (open) => {
364
+ if (!open) {
365
+ handleImageClose();
366
+ }
367
+ }, placement: "bottom-start", children: [
368
+ /* @__PURE__ */ jsx(PopoverTrigger, { children: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
369
+ title: texts.image,
370
+ open: showImagePopover ? false : void 0
371
+ }, onClick: handleImageToggle, disabled, children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(ImageM, {}) }) }) }),
372
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(ImagePopover, { editor, texts, onClose: handleImageClose, onImageUpload }) })
373
+ ] }),
374
+ menuRow: panelEntryRow("image", /* @__PURE__ */ jsx(ImageM, {}), texts.image, false, "image")
375
+ });
376
+ }
377
+ if (extras.length) {
378
+ sections.push(extras);
379
+ }
380
+ const entries = [];
381
+ sections.forEach((section, index) => {
382
+ if (index > 0) {
383
+ entries.push({
384
+ key: `separator-${index}`,
385
+ isSeparator: true,
386
+ inline: /* @__PURE__ */ jsx(ToolbarSeparator, {})
387
+ });
388
+ }
389
+ entries.push(...section);
390
+ });
391
+ entriesCountRef.current = entries.length;
392
+ const shownCount = Number.isFinite(visibleCount) ? visibleCount : entries.length;
393
+ const shownEntries = entries.slice(0, shownCount);
394
+ while (shownEntries.length && shownEntries[shownEntries.length - 1].isSeparator) {
395
+ shownEntries.pop();
396
+ }
397
+ const hiddenEntries = entries.slice(shownCount).filter((entry) => !entry.isSeparator);
398
+ return /* @__PURE__ */ jsxs(DSToolbar, { children: [
399
+ /* @__PURE__ */ jsxs(ToolbarItems, { ref: itemsRef, "data-testid": "rich-text-toolbar-items", children: [
400
+ shownEntries.map((entry) => /* @__PURE__ */ jsx(ToolbarEntry, { children: entry.inline }, entry.key)),
401
+ hiddenEntries.length > 0 && /* @__PURE__ */ jsxs(Popover, { open: showMoreMenu, onOpenChange: (open) => {
402
+ if (!open) {
403
+ closeMore();
404
+ }
405
+ }, placement: "bottom-end", children: [
406
+ /* @__PURE__ */ jsx(PopoverTrigger, { children: /* @__PURE__ */ jsx(ToolbarButton, { mode: "single-icon", tooltipProps: {
407
+ title: texts.more,
408
+ open: showMoreMenu ? false : void 0
409
+ }, onClick: () => showMoreMenu ? closeMore() : setShowMoreMenu(true), disabled, type: showMoreMenu ? "ghost-primary" : "ghost", "data-testid": "rich-text-toolbar-more", children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(OptionHorizontalM, {}) }) }) }),
410
+ /* @__PURE__ */ jsx(PopoverContent, { children: morePanel === "link" ? /* @__PURE__ */ jsx(LinkPopover, { editor, texts, onClose: closeMore }) : morePanel === "image" ? /* @__PURE__ */ jsx(ImagePopover, { editor, texts, onClose: closeMore, onImageUpload }) : /* @__PURE__ */ jsx(BlockMenu, { children: morePanel === "heading" ? renderHeadingMenuItems(closeMore) : morePanel === "table" ? renderTableMenuItems(closeMore) : morePanel === "align" ? renderAlignMenuItems(closeMore) : hiddenEntries.map((entry) => entry.menuRow) }) })
411
+ ] })
412
+ ] }),
413
+ (onEditWithAI || hasAiOptions) && /* @__PURE__ */ jsx(ToolbarGroup, { isCompact: true, style: {
414
+ marginLeft: "auto"
415
+ }, children: (() => {
416
+ const aiButton = /* @__PURE__ */ jsx(Button, { type: "custom-color-ghost", color: "pink", mode: "icon-label", loading: aiLoading, disabled, icon: /* @__PURE__ */ jsx(Icon, { color: theme.palette["mars-400"], component: /* @__PURE__ */ jsx(AiStarM, {}) }), onClick: hasAiOptions ? () => setShowAiMenu((prev) => !prev) : handleEditWithAI, children: /* @__PURE__ */ jsx(AiGradientLabel, { children: texts.editWithAI }) });
417
+ if (!hasAiOptions) {
418
+ return aiButton;
419
+ }
420
+ return /* @__PURE__ */ jsxs(Popover, { open: showAiMenu, onOpenChange: (open) => {
421
+ if (!open) {
422
+ setShowAiMenu(false);
423
+ }
424
+ }, placement: "bottom-end", children: [
425
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: aiButton }),
426
+ /* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx(BlockMenu, { children: (editWithAIOptions ?? []).map((option) => /* @__PURE__ */ jsx(Button, { type: "ghost", mode: option.icon ? "icon-label" : void 0, icon: option.icon, onClick: () => {
427
+ setShowAiMenu(false);
428
+ void runAIAction(option.onSelect);
429
+ }, children: option.label }, option.key)) }) })
430
+ ] });
431
+ })() })
432
+ ] });
433
+ };
434
+ export {
435
+ Toolbar
436
+ };
@@ -0,0 +1,3 @@
1
+ export { Toolbar } from './Toolbar';
2
+ export { LinkPopover } from './LinkPopover';
3
+ export { ImagePopover } from './ImagePopover';
@@ -0,0 +1,8 @@
1
+ import { Toolbar } from "./Toolbar.js";
2
+ import { LinkPopover } from "./LinkPopover.js";
3
+ import { ImagePopover } from "./ImagePopover.js";
4
+ export {
5
+ ImagePopover,
6
+ LinkPopover,
7
+ Toolbar
8
+ };
@@ -0,0 +1,19 @@
1
+ import { Node } from '@tiptap/core';
2
+ export type CodeSnippetOptions = {
3
+ copyText: string;
4
+ copiedText: string;
5
+ };
6
+ declare module '@tiptap/core' {
7
+ interface Commands<ReturnType> {
8
+ codeSnippet: {
9
+ /** Insert an empty single-line code snippet. */
10
+ insertCodeSnippet: () => ReturnType;
11
+ };
12
+ }
13
+ }
14
+ /**
15
+ * A single-line code snippet block: monospace text in a grey box with a copy button.
16
+ * Serializes to `<pre data-type="code-snippet"><code>…</code></pre>` so it round-trips
17
+ * through the HTML and portable-document serializers without colliding with `codeBlock`.
18
+ */
19
+ export declare const CodeSnippet: Node<CodeSnippetOptions, any>;
@@ -0,0 +1,76 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import CopyIcon from "@synerise/ds-copy-icon";
3
+ import { Node, mergeAttributes } from "@tiptap/core";
4
+ import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from "@tiptap/react";
5
+ const CodeSnippetView = ({
6
+ node,
7
+ extension
8
+ }) => {
9
+ const {
10
+ copyText,
11
+ copiedText
12
+ } = extension.options;
13
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, { as: "div", className: "ds-rt-code-snippet", "data-testid": "code-snippet", children: [
14
+ /* @__PURE__ */ jsx(NodeViewContent, { as: "code", className: "ds-rt-code-snippet__content" }),
15
+ /* @__PURE__ */ jsx("span", { className: "ds-rt-code-snippet__actions", contentEditable: false, children: /* @__PURE__ */ jsx(CopyIcon, { copyValue: node.textContent ?? "", texts: {
16
+ copyTooltip: copyText,
17
+ copiedTooltip: copiedText
18
+ } }) })
19
+ ] });
20
+ };
21
+ const CodeSnippet = Node.create({
22
+ name: "codeSnippet",
23
+ group: "block",
24
+ content: "text*",
25
+ marks: "",
26
+ code: true,
27
+ defining: true,
28
+ isolating: true,
29
+ addOptions() {
30
+ return {
31
+ copyText: "Copy",
32
+ copiedText: "Copied"
33
+ };
34
+ },
35
+ parseHTML() {
36
+ return [{
37
+ tag: 'pre[data-type="code-snippet"]',
38
+ preserveWhitespace: "full"
39
+ }];
40
+ },
41
+ renderHTML({
42
+ HTMLAttributes
43
+ }) {
44
+ return ["pre", mergeAttributes(HTMLAttributes, {
45
+ "data-type": "code-snippet"
46
+ }), ["code", 0]];
47
+ },
48
+ addNodeView() {
49
+ return ReactNodeViewRenderer(CodeSnippetView);
50
+ },
51
+ addCommands() {
52
+ return {
53
+ insertCodeSnippet: () => ({
54
+ commands
55
+ }) => commands.insertContent({
56
+ type: this.name
57
+ })
58
+ };
59
+ },
60
+ addKeyboardShortcuts() {
61
+ return {
62
+ // Single-line: Enter exits the snippet into a new block below instead of adding a newline.
63
+ Enter: ({
64
+ editor
65
+ }) => {
66
+ if (!editor.isActive(this.name)) {
67
+ return false;
68
+ }
69
+ return editor.commands.exitCode();
70
+ }
71
+ };
72
+ }
73
+ });
74
+ export {
75
+ CodeSnippet
76
+ };
@@ -0,0 +1,6 @@
1
+ export { default } from './RichText';
2
+ export type { RichTextProps, RichTextFormat, RichTextTexts, RichTextAIOption, ToolbarFeature, ToolbarProps, LinkPopoverProps, ImagePopoverProps, } from './RichText.types';
3
+ export { ALL_TOOLBAR_FEATURES, DEFAULT_HEADING_LEVELS, DEFAULT_TEXTS, } from './RichText.types';
4
+ export * from './RichText.styles';
5
+ export * from './blockModel';
6
+ export { normalizeUrl, sanitizeUrl } from './utils';
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ import { default as default2 } from "./RichText.js";
2
+ import { ALL_TOOLBAR_FEATURES, DEFAULT_HEADING_LEVELS, DEFAULT_TEXTS } from "./RichText.types.js";
3
+ import { AiGradientLabel, BlockMenu, ContentAbove, ContentBelow, Description, EditorArea, EditorWrapper, ErrorText, PopoverActions, PopoverForm, RichTextWrapper, SubtleSuffix, ToolbarArea, ToolbarEntry, ToolbarItems, ToolbarSeparator } from "./RichText.styles.js";
4
+ import { tiptapJsonToDocument } from "./blockModel/serializers/tiptapToDocument.js";
5
+ import { documentToTiptapJson } from "./blockModel/serializers/documentToTiptap.js";
6
+ import { documentToHtml } from "./blockModel/serializers/documentToHtml.js";
7
+ import { documentToMarkdown } from "./blockModel/serializers/documentToMarkdown.js";
8
+ import { markdownToDocument } from "./blockModel/serializers/markdownToDocument.js";
9
+ import { htmlToDocument } from "./blockModel/serializers/htmlToDocument.js";
10
+ import "@tiptap/extension-image";
11
+ import "@tiptap/extension-link";
12
+ import "@tiptap/extension-placeholder";
13
+ import "@tiptap/extension-table";
14
+ import "@tiptap/extension-table-cell";
15
+ import "@tiptap/extension-table-header";
16
+ import "@tiptap/extension-table-row";
17
+ import "@tiptap/extension-text-align";
18
+ import "@tiptap/extension-underline";
19
+ import "@tiptap/starter-kit";
20
+ import "./extensions/CodeSnippet.js";
21
+ import { normalizeUrl } from "./utils/normalizeUrl.js";
22
+ import { sanitizeUrl } from "./utils/sanitizeUrl.js";
23
+ export {
24
+ ALL_TOOLBAR_FEATURES,
25
+ AiGradientLabel,
26
+ BlockMenu,
27
+ ContentAbove,
28
+ ContentBelow,
29
+ DEFAULT_HEADING_LEVELS,
30
+ DEFAULT_TEXTS,
31
+ Description,
32
+ EditorArea,
33
+ EditorWrapper,
34
+ ErrorText,
35
+ PopoverActions,
36
+ PopoverForm,
37
+ RichTextWrapper,
38
+ SubtleSuffix,
39
+ ToolbarArea,
40
+ ToolbarEntry,
41
+ ToolbarItems,
42
+ ToolbarSeparator,
43
+ default2 as default,
44
+ documentToHtml,
45
+ documentToMarkdown,
46
+ documentToTiptapJson,
47
+ htmlToDocument,
48
+ markdownToDocument,
49
+ normalizeUrl,
50
+ sanitizeUrl,
51
+ tiptapJsonToDocument
52
+ };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,4 @@
1
+ declare module '*.less' {
2
+ const content: { [className: string]: string };
3
+ export default content;
4
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * How many toolbar items fit into `availableWidth`. When they all fit, returns
3
+ * `itemWidths.length`; otherwise reserves `reservedWidth` for the "more"
4
+ * button and returns the number of leading items that still fit next to it.
5
+ */
6
+ export declare const computeVisibleCount: (itemWidths: number[], availableWidth: number, reservedWidth: number) => number;
@@ -0,0 +1,19 @@
1
+ const computeVisibleCount = (itemWidths, availableWidth, reservedWidth) => {
2
+ const total = itemWidths.reduce((acc, width) => acc + width, 0);
3
+ if (total <= availableWidth) {
4
+ return itemWidths.length;
5
+ }
6
+ let used = reservedWidth;
7
+ let count = 0;
8
+ for (const width of itemWidths) {
9
+ if (used + width > availableWidth) {
10
+ break;
11
+ }
12
+ used += width;
13
+ count += 1;
14
+ }
15
+ return count;
16
+ };
17
+ export {
18
+ computeVisibleCount
19
+ };
@@ -0,0 +1,13 @@
1
+ import { Extensions } from '@tiptap/react';
2
+ import { ToolbarFeature } from '../RichText.types';
3
+ type GetExtensionsOptions = {
4
+ features: ToolbarFeature[];
5
+ headingLevels: (1 | 2 | 3 | 4 | 5 | 6)[];
6
+ placeholder?: string;
7
+ copyText?: string;
8
+ copiedText?: string;
9
+ /** Show the placeholder in non-editable states too (subtle preview). */
10
+ alwaysShowPlaceholder?: boolean;
11
+ };
12
+ export declare const getExtensions: ({ features, headingLevels, placeholder, copyText, copiedText, alwaysShowPlaceholder, }: GetExtensionsOptions) => Extensions;
13
+ export {};