@firecms/core 3.1.0-canary.1df3b2c → 3.1.0-canary.75005e4

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 (209) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  2. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  3. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +5 -10
  4. package/dist/components/ErrorBoundary.d.ts +4 -2
  5. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  6. package/dist/components/LanguageToggle.d.ts +1 -0
  7. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  8. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  11. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  12. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  13. package/dist/editor/components/editor-bubble.d.ts +8 -0
  14. package/dist/editor/components/index.d.ts +14 -0
  15. package/dist/editor/editor.d.ts +30 -0
  16. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  17. package/dist/editor/extensions/Image/index.d.ts +6 -0
  18. package/dist/editor/extensions/Image.d.ts +6 -0
  19. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  20. package/dist/editor/extensions/clipboard.d.ts +7 -0
  21. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  22. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  23. package/dist/editor/hooks/useProseMirror.d.ts +14 -0
  24. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  25. package/dist/editor/index.d.ts +2 -0
  26. package/dist/editor/markdown.d.ts +5 -0
  27. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  28. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  29. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  30. package/dist/editor/nodeViews/index.d.ts +6 -0
  31. package/dist/editor/plugins/index.d.ts +2 -0
  32. package/dist/editor/plugins/inputrules.d.ts +6 -0
  33. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  34. package/dist/editor/plugins/slashCommandPlugin.d.ts +11 -0
  35. package/dist/editor/schema.d.ts +2 -0
  36. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  37. package/dist/editor/selectors/color-selector.d.ts +10 -0
  38. package/dist/editor/selectors/link-selector.d.ts +8 -0
  39. package/dist/editor/selectors/node-selector.d.ts +15 -0
  40. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  41. package/dist/editor/types.d.ts +5 -0
  42. package/dist/editor/useProseMirror.d.ts +16 -0
  43. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  44. package/dist/editor/utils/remove_classes.d.ts +1 -0
  45. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  46. package/dist/form/components/ErrorFocus.d.ts +1 -1
  47. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  48. package/dist/hooks/index.d.ts +1 -0
  49. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  50. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  51. package/dist/hooks/useTranslation.d.ts +17 -0
  52. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.es.js +11441 -2215
  55. package/dist/index.es.js.map +1 -1
  56. package/dist/index.umd.js +11423 -2216
  57. package/dist/index.umd.js.map +1 -1
  58. package/dist/internal/useRestoreScroll.d.ts +1 -1
  59. package/dist/locales/de.d.ts +2 -0
  60. package/dist/locales/en.d.ts +10 -0
  61. package/dist/locales/es.d.ts +10 -0
  62. package/dist/locales/fr.d.ts +2 -0
  63. package/dist/locales/hi.d.ts +2 -0
  64. package/dist/locales/it.d.ts +2 -0
  65. package/dist/locales/pt.d.ts +7 -0
  66. package/dist/types/analytics.d.ts +1 -1
  67. package/dist/types/collections.d.ts +8 -0
  68. package/dist/types/customization_controller.d.ts +2 -1
  69. package/dist/types/firecms.d.ts +2 -1
  70. package/dist/types/index.d.ts +1 -0
  71. package/dist/types/navigation.d.ts +2 -2
  72. package/dist/types/plugins.d.ts +23 -0
  73. package/dist/types/translations.d.ts +646 -0
  74. package/dist/util/entities.d.ts +1 -1
  75. package/dist/util/resolutions.d.ts +2 -2
  76. package/package.json +47 -13
  77. package/src/app/Scaffold.tsx +7 -5
  78. package/src/components/AIIcon.tsx +3 -1
  79. package/src/components/ArrayContainer.tsx +6 -4
  80. package/src/components/ClearFilterSortButton.tsx +6 -3
  81. package/src/components/ConfirmationDialog.tsx +4 -2
  82. package/src/components/DeleteEntityDialog.tsx +10 -7
  83. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  84. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  85. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  86. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  87. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  88. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  89. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  90. package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
  91. package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
  92. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +39 -46
  93. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  94. package/src/components/EntityCollectionView/EntityCollectionView.tsx +73 -31
  95. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  96. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  97. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  98. package/src/components/EntityCollectionView/ViewModeToggle.tsx +37 -37
  99. package/src/components/EntityView.tsx +3 -2
  100. package/src/components/ErrorBoundary.tsx +27 -15
  101. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  102. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  103. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  104. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  105. package/src/components/LanguageToggle.tsx +66 -0
  106. package/src/components/NotFoundPage.tsx +5 -3
  107. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  108. package/src/components/ReferenceWidget.tsx +3 -2
  109. package/src/components/SearchIconsView.tsx +3 -1
  110. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  111. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  112. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  113. package/src/components/UnsavedChangesDialog.tsx +6 -4
  114. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  115. package/src/components/VirtualTable/VirtualTable.tsx +116 -113
  116. package/src/components/VirtualTable/VirtualTableHeader.tsx +54 -52
  117. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
  119. package/src/components/common/default_entity_actions.tsx +4 -0
  120. package/src/components/common/useDataSourceTableController.tsx +12 -4
  121. package/src/components/index.tsx +1 -0
  122. package/src/core/DefaultAppBar.tsx +15 -11
  123. package/src/core/DefaultDrawer.tsx +8 -2
  124. package/src/core/DrawerNavigationGroup.tsx +5 -3
  125. package/src/core/EntityEditView.tsx +4 -3
  126. package/src/core/EntityEditViewFormActions.tsx +24 -17
  127. package/src/core/EntitySidePanel.tsx +32 -29
  128. package/src/core/FireCMS.tsx +33 -6
  129. package/src/core/field_configs.tsx +14 -9
  130. package/src/editor/components/SlashCommandMenu.tsx +348 -0
  131. package/src/editor/components/editor-bubble-item.tsx +32 -0
  132. package/src/editor/components/editor-bubble.tsx +118 -0
  133. package/src/editor/components/index.ts +12 -0
  134. package/src/editor/editor.tsx +307 -0
  135. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  136. package/src/editor/extensions/Image/index.ts +133 -0
  137. package/src/editor/extensions/Image.ts +144 -0
  138. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  139. package/src/editor/extensions/clipboard.ts +72 -0
  140. package/src/editor/extensions/custom-keymap.ts +24 -0
  141. package/src/editor/extensions/drag-and-drop.tsx +472 -0
  142. package/src/editor/hooks/useProseMirror.ts +115 -0
  143. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  144. package/src/editor/index.ts +2 -0
  145. package/src/editor/markdown.ts +110 -0
  146. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  147. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  148. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  149. package/src/editor/nodeViews/index.ts +35 -0
  150. package/src/editor/plugins/index.ts +55 -0
  151. package/src/editor/plugins/inputrules.ts +82 -0
  152. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  153. package/src/editor/plugins/slashCommandPlugin.ts +49 -0
  154. package/src/editor/schema.ts +228 -0
  155. package/src/editor/selectors/ai-selector.tsx +111 -0
  156. package/src/editor/selectors/color-selector.tsx +200 -0
  157. package/src/editor/selectors/link-selector.tsx +118 -0
  158. package/src/editor/selectors/node-selector.tsx +157 -0
  159. package/src/editor/selectors/text-buttons.tsx +86 -0
  160. package/src/editor/types.ts +6 -0
  161. package/src/editor/useProseMirror.ts +126 -0
  162. package/src/editor/utils/prosemirror-utils.ts +78 -0
  163. package/src/editor/utils/remove_classes.ts +17 -0
  164. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  165. package/src/form/EntityForm.tsx +76 -63
  166. package/src/form/EntityFormActions.tsx +19 -12
  167. package/src/form/PropertyFieldBinding.tsx +6 -5
  168. package/src/form/components/ErrorFocus.tsx +3 -3
  169. package/src/form/components/LocalChangesMenu.tsx +13 -13
  170. package/src/form/components/StorageItemPreview.tsx +3 -2
  171. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  172. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  173. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  174. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  175. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -4
  176. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  177. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +87 -85
  178. package/src/hooks/index.tsx +1 -0
  179. package/src/hooks/useBuildNavigationController.tsx +49 -22
  180. package/src/hooks/useCollapsedGroups.ts +7 -6
  181. package/src/hooks/useTranslation.ts +31 -0
  182. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  183. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  184. package/src/index.ts +4 -0
  185. package/src/internal/useBuildDataSource.ts +1 -2
  186. package/src/internal/useBuildSideEntityController.tsx +22 -20
  187. package/src/locales/de.ts +691 -0
  188. package/src/locales/en.ts +703 -0
  189. package/src/locales/es.ts +703 -0
  190. package/src/locales/fr.ts +691 -0
  191. package/src/locales/hi.ts +691 -0
  192. package/src/locales/it.ts +691 -0
  193. package/src/locales/pt.ts +700 -0
  194. package/src/preview/PropertyPreview.tsx +1 -0
  195. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  196. package/src/preview/components/UserPreview.tsx +3 -1
  197. package/src/types/analytics.ts +10 -0
  198. package/src/types/collections.ts +9 -0
  199. package/src/types/customization_controller.tsx +2 -1
  200. package/src/types/firecms.tsx +2 -1
  201. package/src/types/index.ts +1 -0
  202. package/src/types/navigation.ts +2 -2
  203. package/src/types/plugins.tsx +26 -0
  204. package/src/types/translations.ts +725 -0
  205. package/src/util/entities.ts +1 -1
  206. package/src/util/join_collections.ts +10 -8
  207. package/src/util/previews.ts +2 -2
  208. package/src/util/property_utils.tsx +1 -1
  209. package/src/util/resolutions.ts +5 -3
@@ -0,0 +1,118 @@
1
+ import { forwardRef, type ReactNode, useEffect, useRef, useState } from "react";
2
+ import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
3
+ import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
4
+ import { NodeSelection } from "prosemirror-state";
5
+
6
+ export interface EditorBubbleProps {
7
+ children: ReactNode;
8
+ options?: any;
9
+ className?: string;
10
+ }
11
+
12
+ export const EditorBubble = forwardRef<HTMLDivElement, EditorBubbleProps>(
13
+ ({ children, options, className }, ref) => {
14
+ const { view, state } = useProseMirrorContext();
15
+ const menuRef = useRef<HTMLDivElement>(null);
16
+ const [show, setShow] = useState(false);
17
+
18
+ useEffect(() => {
19
+ if (!view || !state) return;
20
+
21
+ // Delay evaluation slightly to let selection settle
22
+ const timer = setTimeout(() => {
23
+ const { selection } = state;
24
+ const { empty } = selection;
25
+
26
+ // check if image is selected
27
+ let isImage = false;
28
+ state.doc.nodesBetween(selection.from, selection.to, (node) => {
29
+ if (node.type.name === "image") isImage = true;
30
+ });
31
+
32
+ if (isImage || empty || selection instanceof NodeSelection) {
33
+ setShow(false);
34
+ return;
35
+ }
36
+
37
+ setShow(true);
38
+ }, 0);
39
+
40
+ return () => clearTimeout(timer);
41
+ }, [view, state]);
42
+
43
+ useEffect(() => {
44
+ if (!show || !view || !state || !menuRef.current) return;
45
+
46
+ const { from, to } = state.selection;
47
+
48
+ // Fallback for end selection coords
49
+ let start = view.coordsAtPos(from);
50
+ let end = view.coordsAtPos(to);
51
+
52
+ const virtualEl = {
53
+ getBoundingClientRect() {
54
+ const top = Math.min(start.top, end.top);
55
+ const bottom = Math.max(start.bottom, end.bottom);
56
+ const left = Math.min(start.left, end.left);
57
+ const right = Math.max(start.right, end.right);
58
+ return {
59
+ width: right - left,
60
+ height: bottom - top,
61
+ x: left,
62
+ y: top,
63
+ top,
64
+ left,
65
+ right,
66
+ bottom,
67
+ };
68
+ }
69
+ };
70
+
71
+ const cleanup = autoUpdate(virtualEl as any, menuRef.current, () => {
72
+ if (!menuRef.current) return;
73
+
74
+ // Recompute coords in case of scroll
75
+ try {
76
+ start = view.coordsAtPos(state.selection.from);
77
+ end = view.coordsAtPos(state.selection.to);
78
+ } catch (e) {
79
+ // Ignore error if selection is out of bounds
80
+ }
81
+
82
+ computePosition(virtualEl as any, menuRef.current, {
83
+ placement: options?.placement || "top",
84
+ middleware: [offset(options?.offset || 8), flip(), shift()],
85
+ strategy: "fixed"
86
+ }).then(({ x, y }) => {
87
+ if (menuRef.current) {
88
+ Object.assign(menuRef.current.style, {
89
+ left: `${x}px`,
90
+ top: `${y}px`,
91
+ visibility: "visible",
92
+ });
93
+ }
94
+ });
95
+ });
96
+ return () => cleanup();
97
+ }, [show, view, state, options]);
98
+
99
+ if (!show) return null;
100
+
101
+ return (
102
+ <div
103
+ ref={menuRef}
104
+ className={className}
105
+ style={{ position: "fixed", zIndex: 9999, visibility: "hidden" }}
106
+ onMouseDown={(e) => {
107
+ e.preventDefault(); // Don't lose focus inside ProseMirror
108
+ }}
109
+ >
110
+ {children}
111
+ </div>
112
+ );
113
+ }
114
+ );
115
+
116
+ EditorBubble.displayName = "EditorBubble";
117
+
118
+ export default EditorBubble;
@@ -0,0 +1,12 @@
1
+ export { EditorBubble } from "./editor-bubble";
2
+ export { EditorBubbleItem } from "./editor-bubble-item";
3
+ export { SlashCommandMenu } from "./SlashCommandMenu";
4
+
5
+ export type JSONContent = {
6
+ type?: string;
7
+ attrs?: Record<string, any>;
8
+ content?: JSONContent[];
9
+ marks?: { type: string; attrs?: Record<string, any> }[];
10
+ text?: string;
11
+ [key: string]: any;
12
+ };
@@ -0,0 +1,307 @@
1
+ "use client";
2
+ import React, { useEffect, useState } from "react";
3
+ import { cls, defaultBorderMixin, Separator, useInjectStyles } from "@firecms/ui";
4
+ import { useTranslation } from "../hooks/useTranslation";
5
+ import { EditorBubble, SlashCommandMenu, type JSONContent } from "./components";
6
+ import { NodeSelector } from "./selectors/node-selector";
7
+ import { LinkSelector } from "./selectors/link-selector";
8
+ import { TextButtons } from "./selectors/text-buttons";
9
+ import { removeClassesFromJson } from "./utils/remove_classes";
10
+ import { parser, serializer } from "./markdown";
11
+ import { EditorAIController } from "./types";
12
+ import { useProseMirror } from "./hooks/useProseMirror";
13
+ import { ProseMirrorContext } from "./hooks/useProseMirrorContext";
14
+ import { highlightCommands } from "./extensions/HighlightDecorationExtension";
15
+ import { schema } from "./schema";
16
+
17
+ export type CustomEditorComponent = {
18
+ name: string,
19
+ component: React.FC
20
+ };
21
+
22
+ export interface MarkdownEditorConfig {
23
+ html?: boolean;
24
+ transformPastedText?: boolean;
25
+ }
26
+
27
+ export type FireCMSEditorTextSize = "sm" | "base" | "lg";
28
+
29
+ export type FireCMSEditorProps = {
30
+ content?: JSONContent | string,
31
+ onMarkdownContentChange?: (content: string) => void,
32
+ onJsonContentChange?: (content: JSONContent | null) => void,
33
+ onHtmlContentChange?: (content: string) => void,
34
+ handleImageUpload: (file: File) => Promise<string>,
35
+ version?: number,
36
+ textSize?: FireCMSEditorTextSize,
37
+ highlight?: { from: number, to: number },
38
+ aiController?: EditorAIController,
39
+ customComponents?: CustomEditorComponent[];
40
+ disabled?: boolean;
41
+ markdownConfig?: MarkdownEditorConfig;
42
+ };
43
+
44
+ const proseClasses = {
45
+ "sm": "prose-sm",
46
+ "base": "prose-base",
47
+ "lg": "prose-lg"
48
+ }
49
+
50
+ export const FireCMSEditor = ({
51
+ content,
52
+ onJsonContentChange,
53
+ onHtmlContentChange,
54
+ onMarkdownContentChange,
55
+ version,
56
+ textSize = "base",
57
+ highlight,
58
+ handleImageUpload,
59
+ aiController,
60
+ disabled,
61
+ markdownConfig
62
+ }: FireCMSEditorProps) => {
63
+ const { t } = useTranslation();
64
+
65
+ const [openNode, setOpenNode] = useState(false);
66
+ const [openLink, setOpenLink] = useState(false);
67
+
68
+ useInjectStyles("Editor", cssStyles);
69
+
70
+ const { state, view, editorRef } = useProseMirror({
71
+ initialContent: content,
72
+ editable: !disabled,
73
+ handleImageUpload,
74
+ onChange: (newState, editorView) => {
75
+ if (onMarkdownContentChange) {
76
+ try {
77
+ const markdown = addLineBreakAfterImages(serializer.serialize(newState.doc));
78
+ onMarkdownContentChange(markdown);
79
+ } catch (e) {
80
+ console.warn("[FireCMSEditor] Could not serialize editor state to markdown:", e);
81
+ }
82
+ }
83
+ if (onJsonContentChange) {
84
+ const jsonContent = removeClassesFromJson(newState.doc.toJSON()) as JSONContent;
85
+ onJsonContentChange(jsonContent);
86
+ }
87
+ if (onHtmlContentChange) {
88
+ // Not strictly required for FireCMS initially, DOMParser/Serializer can be added if needed
89
+ }
90
+ }
91
+ });
92
+
93
+ useEffect(() => {
94
+ if (version !== undefined && version > -1 && view) {
95
+ if (!content) return;
96
+ const newDoc = typeof content === "string" ? parser.parse(content) : schema.nodeFromJSON(content);
97
+ if (newDoc) {
98
+ view.dispatch(view.state.tr.replaceWith(0, view.state.doc.content.size, newDoc.content));
99
+ }
100
+ }
101
+ }, [version]);
102
+
103
+ useEffect(() => {
104
+ if (view) {
105
+ if (highlight) {
106
+ highlightCommands.toggleAutocompleteHighlight(highlight)(view.state, view.dispatch);
107
+ } else {
108
+ highlightCommands.removeAutocompleteHighlight()(view.state, view.dispatch);
109
+ }
110
+ }
111
+ }, [highlight?.from, highlight?.to]);
112
+
113
+ const proseClass = proseClasses[textSize];
114
+
115
+
116
+
117
+ return (
118
+ <div className="relative min-h-[300px] w-full">
119
+ <ProseMirrorContext.Provider value={{ state, view }}>
120
+
121
+ <div
122
+ ref={editorRef}
123
+ className={cls(proseClass, "prose-headings:font-title font-default focus:outline-none max-w-full p-12")}
124
+ />
125
+
126
+ {view && (
127
+ <EditorBubble
128
+ options={{
129
+ placement: "top",
130
+ offset: 6,
131
+ }}
132
+ className={cls("flex w-fit max-w-[90vw] h-10 overflow-hidden rounded border bg-white dark:bg-surface-900 shadow", defaultBorderMixin)}
133
+ >
134
+ <NodeSelector portalContainer={editorRef.current} open={openNode} onOpenChange={setOpenNode} />
135
+ <Separator orientation="vertical" />
136
+ <LinkSelector open={openLink} onOpenChange={setOpenLink} />
137
+ <Separator orientation="vertical" />
138
+ <TextButtons />
139
+ </EditorBubble>
140
+ )}
141
+
142
+ <SlashCommandMenu upload={handleImageUpload} aiController={aiController} />
143
+
144
+ </ProseMirrorContext.Provider>
145
+ </div>
146
+ );
147
+ };
148
+
149
+ function addLineBreakAfterImages(markdown: string): string {
150
+ const imageRegex = /!\[.*?\]\((?:[^)\\]|\\.)*\)/g;
151
+ return markdown.replace(imageRegex, (match) => `${match}\n`);
152
+ }
153
+
154
+ const cssStyles = `
155
+ .ProseMirror {
156
+ box-shadow: none !important;
157
+ }
158
+ .ProseMirror .is-editor-empty:first-child::before {
159
+ content: attr(data-placeholder);
160
+ float: left;
161
+ color: rgb(100 116 139); //500
162
+ pointer-events: none;
163
+ height: 0;
164
+ }
165
+ .ProseMirror .is-empty::before {
166
+ content: attr(data-placeholder);
167
+ float: left;
168
+ color: rgb(100 116 139); //500
169
+ pointer-events: none;
170
+ height: 0;
171
+ }
172
+
173
+ [data-theme="dark"] {
174
+ .ProseMirror .is-empty::before {
175
+ color: rgb(100 116 139); //500
176
+ }
177
+ }
178
+
179
+ .is-empty {
180
+ cursor: text;
181
+ color: rgb(100 116 139); //500
182
+ }
183
+
184
+ .ProseMirror img {
185
+ transition: filter 0.1s ease-in-out;
186
+ &:hover {
187
+ cursor: pointer;
188
+ filter: brightness(90%);
189
+ }
190
+ &.ProseMirror-selectednode {
191
+ filter: brightness(90%);
192
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000) !important;
193
+ }
194
+ }
195
+
196
+ ul[data-type="taskList"] li > label {
197
+ margin-right: 0.2rem;
198
+ user-select: none;
199
+ }
200
+ @media screen and (max-width: 768px) {
201
+ ul[data-type="taskList"] li > label {
202
+ margin-right: 0.5rem;
203
+ }
204
+ }
205
+ [data-theme="dark"] {
206
+ ul[data-type="taskList"] li > label input[type="checkbox"] {
207
+ background-color: rgb(30 41 59);
208
+ border: 2px solid #666;
209
+ &:hover { background-color: rgb(51 65 85); }
210
+ &:active { background-color: rgb(71 85 105); }
211
+ }
212
+ }
213
+ ul[data-type="taskList"] li > label input[type="checkbox"] {
214
+ -webkit-appearance: none;
215
+ appearance: none;
216
+ background-color: white;
217
+ margin: 0;
218
+ cursor: pointer;
219
+ width: 1.2em;
220
+ height: 1.2em;
221
+ position: relative;
222
+ top: 5px;
223
+ border: 2px solid #777;
224
+ border-radius: 0.25em;
225
+ margin-right: 0.3rem;
226
+ display: grid;
227
+ place-content: center;
228
+ &:hover { background-color: rgb(241 245 249); }
229
+ &:active { background-color: rgb(226 232 240); }
230
+ &::before {
231
+ content: "";
232
+ width: 0.65em;
233
+ height: 0.65em;
234
+ transform: scale(0);
235
+ transition: 120ms transform ease-in-out;
236
+ box-shadow: inset 1em 1em;
237
+ transform-origin: center;
238
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
239
+ }
240
+ &:checked::before { transform: scale(1); }
241
+ }
242
+
243
+ [data-theme="dark"] {
244
+ ul[data-type="taskList"] li[data-checked="true"] > div > p {
245
+ color: rgb(226 232 240);
246
+ text-decoration: line-through;
247
+ text-decoration-thickness: 2px;
248
+ }
249
+ }
250
+ ul[data-type="taskList"] li[data-checked="true"] > div > p {
251
+ color: rgb(51 65 85);
252
+ text-decoration: line-through;
253
+ text-decoration-thickness: 2px;
254
+ }
255
+ .tippy-box { max-width: 400px !important; }
256
+
257
+ .ProseMirror:not(.dragging) .ProseMirror-selectednode {
258
+ background-color: rgb(219 234 254);
259
+ transition: background-color 0.2s;
260
+ box-shadow: none;
261
+ }
262
+ [data-theme="dark"] .ProseMirror:not(.dragging) .ProseMirror-selectednode {
263
+ background-color: rgb(51 65 85);
264
+ }
265
+ .prose-base table p { margin: 0; }
266
+
267
+ .drag-handle {
268
+ position: absolute;
269
+ opacity: 1;
270
+ transition: opacity ease-in 0.2s;
271
+ border-radius: 0.25rem;
272
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(128, 128, 128, 0.9)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7,8.44771525 7,9 7,9 C7,9.55228475 7,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
273
+ background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);
274
+ background-repeat: no-repeat;
275
+ background-position: center;
276
+ width: 1.2rem;
277
+ height: 1.5rem;
278
+ z-index: 100;
279
+ cursor: grab;
280
+
281
+ /* Create a hover area around the handle itself that doesn't overlap text */
282
+ &::before {
283
+ content: '';
284
+ position: absolute;
285
+ top: -10px;
286
+ bottom: -10px;
287
+ left: -20px;
288
+ right: 0px;
289
+ z-index: -1;
290
+ }
291
+
292
+ &:hover { background-color: rgb(241 245 249); transition: background-color 0.2s; }
293
+ &:active { background-color: rgb(226 232 240); transition: background-color 0.2s; }
294
+ &.hide { opacity: 0; pointer-events: none; }
295
+ @media screen and (max-width: 600px) { display: none; pointer-events: none; }
296
+ }
297
+ [data-theme="dark"] .drag-handle {
298
+ &:hover { background-color: rgb(51 65 85); }
299
+ &:active { background-color: rgb(51 65 85); }
300
+ }
301
+ .prosemirror-dropcursor-block {
302
+ background-color: var(--color-surface-accent-600);
303
+ }
304
+ [data-theme="dark"] .prosemirror-dropcursor-block {
305
+ background-color: var(--color-surface-accent-300);
306
+ }
307
+ `;
@@ -0,0 +1,114 @@
1
+ import { Plugin, PluginKey, Transaction, EditorState } from "prosemirror-state";
2
+ import { Decoration, DecorationSet } from "prosemirror-view";
3
+
4
+ export interface HighlightRange {
5
+ from: number
6
+ to: number
7
+ }
8
+
9
+ interface AutocompleteHighlightState {
10
+ highlight?: HighlightRange
11
+ decorationSet?: DecorationSet
12
+ }
13
+
14
+ export const highlightDecorationKey = new PluginKey<AutocompleteHighlightState>("highlightDecoration");
15
+
16
+ function buildDecorationSet(highlight: any, doc: any) {
17
+ const decorations: [any?] = [];
18
+
19
+ if (highlight) {
20
+ decorations.push(
21
+ Decoration.inline(highlight.from, highlight.to, {
22
+ class: "dark:bg-surface-accent-700 bg-surface-accent-300"
23
+ })
24
+ );
25
+ }
26
+ const decorationSet = DecorationSet.create(doc, decorations);
27
+ return decorationSet;
28
+ }
29
+
30
+ /**
31
+ * Commands to toggle the highlight
32
+ */
33
+ export const highlightCommands = {
34
+ toggleAutocompleteHighlight: (range?: HighlightRange) => (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
35
+ const { selection } = state;
36
+ const pos = selection.from;
37
+
38
+ if (!dispatch) return false;
39
+
40
+ const tr = state.tr.setMeta(highlightDecorationKey, {
41
+ pos,
42
+ type: "highlightDecoration",
43
+ remove: false,
44
+ range
45
+ });
46
+
47
+ dispatch(tr);
48
+ return true;
49
+ },
50
+
51
+ removeAutocompleteHighlight: () => (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
52
+ if (!dispatch) return false;
53
+
54
+ const tr = state.tr.setMeta(highlightDecorationKey, {
55
+ pos: 0,
56
+ type: "highlightDecoration",
57
+ remove: true
58
+ });
59
+
60
+ dispatch(tr);
61
+ return true;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * This plugin is used to highlight the current autocomplete suggestion.
67
+ * It allows to set a range and remove it.
68
+ */
69
+ export const highlightDecorationPlugin = (initialHighlight?: HighlightRange) => {
70
+ return new Plugin<AutocompleteHighlightState>({
71
+ key: highlightDecorationKey,
72
+ state: {
73
+ init: (_, { doc }) => {
74
+ const decorationSet = initialHighlight && doc ? buildDecorationSet(initialHighlight, doc) : DecorationSet.empty;
75
+ return {
76
+ decorationSet,
77
+ highlight: initialHighlight
78
+ };
79
+ },
80
+ apply(transaction, oldState) {
81
+ const action = transaction.getMeta(highlightDecorationKey);
82
+ const highlight = action?.range;
83
+ if (action?.type === "highlightDecoration") {
84
+
85
+ const doc = transaction.doc;
86
+ const { remove } = action;
87
+
88
+ if (remove) {
89
+ return {
90
+ decorationSet: DecorationSet.empty
91
+ };
92
+ }
93
+ const decorationSet = buildDecorationSet(highlight, doc);
94
+ return {
95
+ decorationSet: decorationSet,
96
+ highlight
97
+ }
98
+ } else {
99
+ return oldState
100
+ }
101
+ }
102
+ },
103
+ props: {
104
+ decorations(state) {
105
+ const autocompleteState = this.getState(state);
106
+ if (autocompleteState?.decorationSet) {
107
+ return autocompleteState.decorationSet
108
+ } else {
109
+ return DecorationSet.empty
110
+ }
111
+ }
112
+ }
113
+ });
114
+ };
@@ -0,0 +1,133 @@
1
+ import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
2
+ import { Plugin, PluginKey } from "prosemirror-state";
3
+ import { schema } from "../../schema";
4
+
5
+ export type UploadFn = (image: File) => Promise<string>;
6
+
7
+ export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<FileReader>, pos: number, upload: UploadFn, image: File) {
8
+ // @ts-ignore
9
+ const plugin = view.state.plugins.find((p: Plugin) => p.key === ImagePluginKey.key);
10
+ if (!plugin) {
11
+ console.error("Image plugin not found");
12
+ return;
13
+ }
14
+ let decorationSet = plugin.getState(view.state);
15
+
16
+ const placeholder = document.createElement("div");
17
+ const imageElement = document.createElement("img");
18
+ // basic styling for loading state
19
+ imageElement.setAttribute("class", "opacity-40 rounded-lg border");
20
+ imageElement.src = readerEvent.target?.result as string;
21
+ placeholder.appendChild(imageElement);
22
+
23
+ const deco = Decoration.widget(pos, placeholder);
24
+ decorationSet = decorationSet?.add(view.state.doc, [deco]);
25
+ view.dispatch(view.state.tr.setMeta(plugin, { decorationSet }));
26
+
27
+ // Image Upload Logic
28
+ const src = await upload(image);
29
+ console.debug("Uploaded image", src);
30
+
31
+ // Replace placeholder with actual image
32
+ const imageNode = schema.nodes.image.create({ src });
33
+ const tr = view.state.tr.replaceWith(pos, pos, imageNode);
34
+
35
+ // Remove placeholder decoration
36
+ decorationSet = decorationSet?.remove([deco]);
37
+ tr.setMeta(plugin, { decorationSet });
38
+ view.dispatch(tr);
39
+ }
40
+
41
+ export const ImagePluginKey = new PluginKey("imagePlugin");
42
+
43
+ export const createDropImagePlugin = (upload: UploadFn): Plugin => {
44
+ const plugin: Plugin = new Plugin({
45
+ key: ImagePluginKey,
46
+ state: {
47
+ // Initialize the plugin state with an empty DecorationSet
48
+ init: () => DecorationSet.empty,
49
+ // Apply transactions to update the state
50
+ apply: (tr, old) => {
51
+ // Handle custom transaction steps that update decorations
52
+ const meta = tr.getMeta(plugin);
53
+ if (meta && meta.decorationSet) {
54
+ return meta.decorationSet;
55
+ }
56
+ // Map decorations to the new document structure
57
+ return old.map(tr.mapping, tr.doc);
58
+ }
59
+ },
60
+ props: {
61
+ handleDOMEvents: {
62
+ drop: (view, event) => {
63
+ if (!event.dataTransfer?.files || event.dataTransfer?.files.length === 0) {
64
+ return false;
65
+ }
66
+ event.preventDefault();
67
+
68
+ const files = Array.from(event.dataTransfer.files);
69
+ const images = files.filter(file => /image/i.test(file.type));
70
+
71
+ if (images.length === 0) {
72
+ console.log("No images found in dropped files");
73
+ return false;
74
+ }
75
+
76
+ images.forEach(image => {
77
+ const position = view.posAtCoords({
78
+ left: event.clientX,
79
+ top: event.clientY
80
+ });
81
+ if (!position) return;
82
+
83
+ const reader = new FileReader();
84
+ reader.onload = async (readerEvent) => {
85
+ await onFileRead(view, readerEvent, position.pos, upload, image);
86
+ };
87
+ reader.readAsDataURL(image);
88
+ });
89
+
90
+ return true;
91
+ }
92
+ },
93
+ handlePaste(view, event, slice) {
94
+ const items = Array.from(event.clipboardData?.items || []);
95
+ const pos = view.state.selection.from;
96
+ let anyImageFound = false;
97
+
98
+ items.forEach((item) => {
99
+ const image = item.getAsFile();
100
+ if (image && /image/i.test(item.type)) {
101
+ anyImageFound = true;
102
+ const reader = new FileReader();
103
+
104
+ reader.onload = async (readerEvent) => {
105
+ await onFileRead(view, readerEvent, pos, upload, image);
106
+ };
107
+ reader.readAsDataURL(image);
108
+ }
109
+ });
110
+
111
+ return anyImageFound;
112
+ },
113
+ decorations(state) {
114
+ return plugin.getState(state);
115
+ }
116
+ },
117
+ view(editorView) {
118
+ // This is needed to immediately apply the decoration updates
119
+ return {
120
+ update(view, prevState) {
121
+ const prevDecos = plugin.getState(prevState);
122
+ const newDecos = plugin.getState(view.state);
123
+
124
+ if (prevDecos !== newDecos) {
125
+ view.updateState(view.state);
126
+ }
127
+ }
128
+ };
129
+ }
130
+ });
131
+
132
+ return plugin;
133
+ };