@firecms/core 3.1.0-canary.24c8270 → 3.1.0-canary.501d471

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 (224) hide show
  1. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  2. package/dist/components/ErrorBoundary.d.ts +3 -1
  3. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  4. package/dist/components/LanguageToggle.d.ts +1 -0
  5. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  6. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
  7. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  8. package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
  9. package/dist/components/VirtualTable/types.d.ts +1 -0
  10. package/dist/components/index.d.ts +1 -0
  11. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  12. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  13. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  14. package/dist/editor/components/editor-bubble.d.ts +8 -0
  15. package/dist/editor/components/image-bubble.d.ts +5 -0
  16. package/dist/editor/components/index.d.ts +16 -0
  17. package/dist/editor/components/table-bubble.d.ts +5 -0
  18. package/dist/editor/editor.d.ts +30 -0
  19. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  20. package/dist/editor/extensions/Image/index.d.ts +6 -0
  21. package/dist/editor/extensions/Image.d.ts +6 -0
  22. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  23. package/dist/editor/extensions/clipboard.d.ts +7 -0
  24. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  25. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  26. package/dist/editor/hooks/useProseMirror.d.ts +13 -0
  27. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  28. package/dist/editor/index.d.ts +2 -0
  29. package/dist/editor/markdown.d.ts +5 -0
  30. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  31. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  32. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  33. package/dist/editor/nodeViews/index.d.ts +6 -0
  34. package/dist/editor/plugins/index.d.ts +2 -0
  35. package/dist/editor/plugins/inputrules.d.ts +6 -0
  36. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  37. package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
  38. package/dist/editor/schema.d.ts +2 -0
  39. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  40. package/dist/editor/selectors/color-selector.d.ts +10 -0
  41. package/dist/editor/selectors/link-selector.d.ts +8 -0
  42. package/dist/editor/selectors/node-selector.d.ts +15 -0
  43. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  44. package/dist/editor/types.d.ts +5 -0
  45. package/dist/editor/useProseMirror.d.ts +16 -0
  46. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  47. package/dist/editor/utils/remove_classes.d.ts +1 -0
  48. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  49. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  50. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  51. package/dist/hooks/index.d.ts +1 -0
  52. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  53. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  54. package/dist/hooks/useTranslation.d.ts +17 -0
  55. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.es.js +29889 -18645
  58. package/dist/index.es.js.map +1 -1
  59. package/dist/index.umd.js +29883 -18659
  60. package/dist/index.umd.js.map +1 -1
  61. package/dist/locales/de.d.ts +2 -0
  62. package/dist/locales/en.d.ts +10 -0
  63. package/dist/locales/es.d.ts +10 -0
  64. package/dist/locales/fr.d.ts +2 -0
  65. package/dist/locales/hi.d.ts +2 -0
  66. package/dist/locales/it.d.ts +2 -0
  67. package/dist/locales/pt.d.ts +7 -0
  68. package/dist/types/collections.d.ts +38 -0
  69. package/dist/types/customization_controller.d.ts +2 -1
  70. package/dist/types/firecms.d.ts +2 -1
  71. package/dist/types/index.d.ts +1 -0
  72. package/dist/types/navigation.d.ts +2 -2
  73. package/dist/types/plugins.d.ts +7 -0
  74. package/dist/types/properties.d.ts +9 -8
  75. package/dist/types/storage.d.ts +1 -0
  76. package/dist/types/translations.d.ts +669 -0
  77. package/dist/util/index.d.ts +1 -0
  78. package/dist/util/lazy_eager.d.ts +7 -0
  79. package/dist/util/objects.d.ts +1 -0
  80. package/dist/util/useStorageUploadController.d.ts +10 -1
  81. package/package.json +45 -9
  82. package/src/app/Scaffold.tsx +7 -5
  83. package/src/components/AIIcon.tsx +3 -1
  84. package/src/components/ArrayContainer.tsx +6 -4
  85. package/src/components/ClearFilterSortButton.tsx +6 -3
  86. package/src/components/ConfirmationDialog.tsx +4 -2
  87. package/src/components/DeleteEntityDialog.tsx +10 -7
  88. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
  89. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  90. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  91. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  92. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  93. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  94. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  95. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  96. package/src/components/EntityCollectionView/EntityCollectionView.tsx +24 -18
  97. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  98. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  99. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  100. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  101. package/src/components/EntityJsonPreview.tsx +2 -1
  102. package/src/components/EntityView.tsx +3 -2
  103. package/src/components/ErrorBoundary.tsx +27 -15
  104. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  105. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  106. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  107. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  108. package/src/components/LanguageToggle.tsx +66 -0
  109. package/src/components/NotFoundPage.tsx +5 -3
  110. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  111. package/src/components/ReferenceWidget.tsx +3 -2
  112. package/src/components/SearchIconsView.tsx +3 -1
  113. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  114. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  115. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  116. package/src/components/UnsavedChangesDialog.tsx +6 -4
  117. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  118. package/src/components/VirtualTable/VirtualTable.tsx +5 -3
  119. package/src/components/VirtualTable/VirtualTableHeader.tsx +21 -18
  120. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
  121. package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
  122. package/src/components/VirtualTable/types.tsx +1 -0
  123. package/src/components/common/default_entity_actions.tsx +4 -0
  124. package/src/components/common/useDataSourceTableController.tsx +5 -14
  125. package/src/components/index.tsx +1 -0
  126. package/src/core/DefaultAppBar.tsx +14 -10
  127. package/src/core/DefaultDrawer.tsx +8 -2
  128. package/src/core/DrawerNavigationGroup.tsx +5 -3
  129. package/src/core/EntityEditView.tsx +53 -7
  130. package/src/core/EntityEditViewFormActions.tsx +24 -17
  131. package/src/core/EntitySidePanel.tsx +6 -4
  132. package/src/core/FireCMS.tsx +33 -6
  133. package/src/core/field_configs.tsx +4 -2
  134. package/src/editor/components/SlashCommandMenu.tsx +516 -0
  135. package/src/editor/components/editor-bubble-item.tsx +32 -0
  136. package/src/editor/components/editor-bubble.tsx +118 -0
  137. package/src/editor/components/image-bubble.tsx +156 -0
  138. package/src/editor/components/index.ts +14 -0
  139. package/src/editor/components/table-bubble.tsx +165 -0
  140. package/src/editor/editor.tsx +455 -0
  141. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  142. package/src/editor/extensions/Image/index.ts +133 -0
  143. package/src/editor/extensions/Image.ts +159 -0
  144. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  145. package/src/editor/extensions/clipboard.ts +72 -0
  146. package/src/editor/extensions/custom-keymap.ts +24 -0
  147. package/src/editor/extensions/drag-and-drop.tsx +480 -0
  148. package/src/editor/hooks/useProseMirror.ts +124 -0
  149. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  150. package/src/editor/index.ts +2 -0
  151. package/src/editor/markdown.ts +172 -0
  152. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  153. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  154. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  155. package/src/editor/nodeViews/index.ts +35 -0
  156. package/src/editor/plugins/index.ts +58 -0
  157. package/src/editor/plugins/inputrules.ts +82 -0
  158. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  159. package/src/editor/plugins/slashCommandPlugin.ts +61 -0
  160. package/src/editor/schema.ts +240 -0
  161. package/src/editor/selectors/ai-selector.tsx +111 -0
  162. package/src/editor/selectors/color-selector.tsx +200 -0
  163. package/src/editor/selectors/link-selector.tsx +118 -0
  164. package/src/editor/selectors/node-selector.tsx +157 -0
  165. package/src/editor/selectors/text-buttons.tsx +86 -0
  166. package/src/editor/types.ts +6 -0
  167. package/src/editor/useProseMirror.ts +126 -0
  168. package/src/editor/utils/prosemirror-utils.ts +108 -0
  169. package/src/editor/utils/remove_classes.ts +17 -0
  170. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  171. package/src/form/EntityForm.tsx +80 -7
  172. package/src/form/EntityFormActions.tsx +19 -12
  173. package/src/form/PropertyFieldBinding.tsx +7 -5
  174. package/src/form/components/LocalChangesMenu.tsx +13 -13
  175. package/src/form/components/StorageItemPreview.tsx +3 -2
  176. package/src/form/components/StorageUploadProgress.tsx +18 -3
  177. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
  178. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +22 -9
  179. package/src/form/field_bindings/BlockFieldBinding.tsx +26 -9
  180. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
  181. package/src/form/field_bindings/KeyValueFieldBinding.tsx +46 -24
  182. package/src/form/field_bindings/MapFieldBinding.tsx +27 -11
  183. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +73 -36
  184. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
  185. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
  186. package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
  187. package/src/form/field_bindings/RepeatFieldBinding.tsx +21 -6
  188. package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
  189. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +28 -10
  190. package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
  191. package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
  192. package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
  193. package/src/hooks/index.tsx +1 -0
  194. package/src/hooks/useBuildNavigationController.tsx +20 -13
  195. package/src/hooks/useCollapsedGroups.ts +7 -6
  196. package/src/hooks/useTranslation.ts +31 -0
  197. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  198. package/src/index.ts +5 -0
  199. package/src/locales/de.ts +718 -0
  200. package/src/locales/en.ts +730 -0
  201. package/src/locales/es.ts +730 -0
  202. package/src/locales/fr.ts +718 -0
  203. package/src/locales/hi.ts +718 -0
  204. package/src/locales/it.ts +718 -0
  205. package/src/locales/pt.ts +727 -0
  206. package/src/preview/PropertyPreview.tsx +3 -2
  207. package/src/preview/components/ReferencePreview.tsx +2 -1
  208. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  209. package/src/preview/components/UserPreview.tsx +3 -1
  210. package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
  211. package/src/routes/FireCMSRoute.tsx +63 -54
  212. package/src/types/collections.ts +40 -0
  213. package/src/types/customization_controller.tsx +2 -1
  214. package/src/types/firecms.tsx +2 -1
  215. package/src/types/index.ts +1 -0
  216. package/src/types/navigation.ts +2 -2
  217. package/src/types/plugins.tsx +8 -0
  218. package/src/types/properties.ts +12 -10
  219. package/src/types/storage.ts +2 -1
  220. package/src/types/translations.ts +752 -0
  221. package/src/util/index.ts +1 -0
  222. package/src/util/lazy_eager.tsx +33 -0
  223. package/src/util/objects.ts +15 -0
  224. package/src/util/useStorageUploadController.tsx +23 -29
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useEffect, useMemo } from "react";
2
2
 
3
3
  import { EntityCollection, EntitySidePanelProps } from "../types";
4
- import { useNavigationController, useSideEntityController } from "../hooks";
4
+ import { useNavigationController, useSideEntityController, useTranslation } from "../hooks";
5
5
 
6
6
  import { ErrorBoundary } from "../components";
7
7
  import { EntityEditView, OnUpdateParams } from "./EntityEditView";
@@ -41,6 +41,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
41
41
  const sideEntityController = useSideEntityController();
42
42
  const navigationController = useNavigationController();
43
43
  const sideDialogsController = useSideDialogContext();
44
+ const { t } = useTranslation();
44
45
 
45
46
  const onClose = () => {
46
47
  if (props.onClose) {
@@ -77,7 +78,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
77
78
  return navigationController.getParentCollectionIds(path);
78
79
  }, [navigationController, path]);
79
80
 
80
- const collection = navigationController.getCollection(fullIdPath ?? path) ?? props.collection;
81
+ const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
81
82
 
82
83
  useEffect(() => {
83
84
  function beforeunload(e: any) {
@@ -99,10 +100,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
99
100
 
100
101
  const onValuesModified = useCallback((modified: boolean) => {
101
102
  setBlockedNavigationMessage(modified
102
- ? <> You have unsaved changes in this <b>{collection?.singularName ?? collection?.name}</b>.</>
103
+ ? t("unsaved_changes", { collectionName: collection?.singularName ?? collection?.name ?? "" })
103
104
  : undefined)
104
105
  setBlocked(modified);
105
- }, [collection?.name, setBlocked, setBlockedNavigationMessage]);
106
+ }, [collection?.name, setBlocked, setBlockedNavigationMessage, t]);
106
107
 
107
108
  if (!props || !collection) {
108
109
  return <div className={"w-full"} />;
@@ -111,6 +112,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
111
112
  return (
112
113
  <>
113
114
  <ErrorBoundary>
115
+
114
116
  <EntityEditView
115
117
  {...props}
116
118
  fullIdPath={fullIdPath}
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
 
3
- import React, { useMemo } from "react";
3
+ import React, { useEffect, useMemo } from "react";
4
4
  import { CenteredView, Typography } from "@firecms/ui";
5
+ import { AuthController } from "../types";
5
6
  import { CustomizationController, FireCMSContext, FireCMSPlugin, FireCMSProps, User } from "../types";
6
- import { AuthControllerContext } from "../contexts";
7
+ import { AuthControllerContext, ModeControllerProvider } from "../contexts";
7
8
  import { useBuildSideEntityController } from "../internal/useBuildSideEntityController";
8
- import { useCustomizationController, useFireCMSContext } from "../hooks";
9
+ import { useCustomizationController, useFireCMSContext, useTranslation, ModeController } from "../hooks";
10
+ import { useBuildModeController } from "../hooks/useBuildModeController";
9
11
  import { useBuildSideDialogsController } from "../internal/useBuildSideDialogsController";
10
12
  import { ErrorView } from "../components";
11
13
  import { StorageSourceContext } from "../contexts/StorageSourceContext";
@@ -59,6 +61,10 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
59
61
  console.warn("The `plugins` prop is deprecated in the FireCMS component. You should pass your plugins to `useBuildNavigationController` instead.");
60
62
  }
61
63
 
64
+ const { t, i18n } = useTranslation();
65
+
66
+ const modeController = useBuildModeController();
67
+
62
68
  const plugins = navigationController.plugins ?? _pluginsProp;
63
69
  const userManagement = plugins?.find(p => p.userManagement)?.userManagement
64
70
  ?? _userManagement
@@ -106,6 +112,24 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
106
112
  authController
107
113
  });
108
114
 
115
+ // Inject plugin translations into the existing i18next instance
116
+ useEffect(() => {
117
+ if (!i18n) return;
118
+ plugins?.forEach(plugin => {
119
+ if (plugin.i18n) {
120
+ Object.keys(plugin.i18n).forEach(locale => {
121
+ i18n.addResourceBundle(
122
+ locale,
123
+ "firecms_core",
124
+ plugin.i18n![locale],
125
+ true, // deep merge
126
+ true // overwrite
127
+ );
128
+ });
129
+ }
130
+ });
131
+ }, [i18n, plugins]);
132
+
109
133
  if (accessResponse?.message) {
110
134
  console.warn(accessResponse.message);
111
135
  }
@@ -133,12 +157,15 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
133
157
  if (accessResponse?.blocked) {
134
158
  return (
135
159
  <CenteredView maxWidth={"md"} fullScreen={true} className={"flex flex-col gap-2"}>
160
+ {/* eslint-disable-next-line i18next/no-literal-string */}
136
161
  <Typography variant={"h4"} gutterBottom>
137
- License needed
162
+ {t("license_needed")}
138
163
  </Typography>
139
164
  <Typography>
140
- You need a valid license to use FireCMS PRO. Please reach out at <a
141
- href={"mailto:hello@firecms.co"}>hello@firecms.co</a> for more information.
165
+ {(() => {
166
+ const parts = t("license_description", { email: "%%EMAIL%%" }).split("%%EMAIL%%");
167
+ return <>{parts[0]}<a href={"mailto:hello@firecms.co"}>hello@firecms.co</a>{parts[1]}</>;
168
+ })()}
142
169
  </Typography>
143
170
  {accessResponse?.message &&
144
171
  <Typography>{accessResponse?.message}</Typography>}
@@ -8,16 +8,18 @@ import {
8
8
  DateTimeFieldBinding,
9
9
  KeyValueFieldBinding,
10
10
  MapFieldBinding,
11
- MarkdownEditorFieldBinding,
12
11
  MultiSelectFieldBinding,
13
12
  ReferenceAsStringFieldBinding,
14
13
  ReferenceFieldBinding,
15
14
  RepeatFieldBinding,
16
15
  SelectFieldBinding,
17
- StorageUploadFieldBinding,
18
16
  SwitchFieldBinding,
19
17
  TextFieldBinding
20
18
  } from "../form";
19
+ import { lazyEager } from "../util/lazy_eager";
20
+
21
+ const MarkdownEditorFieldBinding = lazyEager<typeof import("../form/field_bindings/MarkdownEditorFieldBinding")["MarkdownEditorFieldBinding"]>(() => import("../form/field_bindings/MarkdownEditorFieldBinding"), "MarkdownEditorFieldBinding");
22
+ const StorageUploadFieldBinding = lazyEager<typeof import("../form/field_bindings/StorageUploadFieldBinding")["StorageUploadFieldBinding"]>(() => import("../form/field_bindings/StorageUploadFieldBinding"), "StorageUploadFieldBinding");
21
23
  import { isPropertyBuilder, mergeDeep } from "../util";
22
24
 
23
25
  import {
@@ -0,0 +1,516 @@
1
+ import React, { useEffect, useRef, useState, ReactNode } from "react";
2
+ import { Fragment, DOMParser } from "prosemirror-model";
3
+ import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
4
+ import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
5
+ import { SlashCommandPluginKey } from "../plugins/slashCommandPlugin";
6
+ import {
7
+ cls,
8
+ defaultBorderMixin,
9
+ TextFieldsIcon,
10
+ CheckBoxIcon,
11
+ LooksOneIcon,
12
+ LooksTwoIcon,
13
+ Looks3Icon,
14
+ FormatListBulletedIcon,
15
+ FormatListNumberedIcon,
16
+ FormatQuoteIcon,
17
+ CodeIcon,
18
+ ImageIcon,
19
+ AutoFixHighIcon,
20
+ TableChartIcon
21
+ } from "@firecms/ui";
22
+ import { setBlockType, wrapIn } from "prosemirror-commands";
23
+ import { wrapInList } from "prosemirror-schema-list";
24
+ import { schema } from "../schema";
25
+ import { EditorView } from "prosemirror-view";
26
+ import { TextSelection } from "prosemirror-state";
27
+ import { EditorAIController } from "../types";
28
+ import { onFileRead, UploadFn } from "../extensions/Image";
29
+ import { textLoadingCommands } from "../extensions/TextLoadingDecorationExtension";
30
+ import { parser } from "../markdown";
31
+
32
+ interface SuggestionItem {
33
+ title: string;
34
+ description: string;
35
+ icon: ReactNode;
36
+ searchTerms?: string[];
37
+ command: (
38
+ view: EditorView,
39
+ range: { from: number; to: number },
40
+ upload: UploadFn,
41
+ aiController?: EditorAIController,
42
+ setSubView?: (viewId: string | null) => void
43
+ ) => void;
44
+ }
45
+
46
+ const suggestionItems: SuggestionItem[] = [
47
+ {
48
+ title: "Text",
49
+ description: "Just start typing with plain text.",
50
+ searchTerms: ["p", "paragraph"],
51
+ icon: <TextFieldsIcon size={18} />,
52
+ command: (view, range) => {
53
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
54
+ setBlockType(schema.nodes.paragraph)(view.state, view.dispatch);
55
+ }
56
+ },
57
+ {
58
+ title: "To-do List",
59
+ description: "Track tasks with a to-do list.",
60
+ searchTerms: ["todo", "task", "list", "check", "checkbox"],
61
+ icon: <CheckBoxIcon size={18} />,
62
+ command: (view, range) => {
63
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
64
+ wrapInList(schema.nodes.task_list)(view.state, view.dispatch);
65
+ }
66
+ },
67
+ {
68
+ title: "Heading 1",
69
+ description: "Big section heading.",
70
+ searchTerms: ["title", "big", "large"],
71
+ icon: <LooksOneIcon size={18} />,
72
+ command: (view, range) => {
73
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
74
+ setBlockType(schema.nodes.heading, { level: 1 })(view.state, view.dispatch);
75
+ }
76
+ },
77
+ {
78
+ title: "Heading 2",
79
+ description: "Medium section heading.",
80
+ searchTerms: ["subtitle", "medium"],
81
+ icon: <LooksTwoIcon size={18} />,
82
+ command: (view, range) => {
83
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
84
+ setBlockType(schema.nodes.heading, { level: 2 })(view.state, view.dispatch);
85
+ }
86
+ },
87
+ {
88
+ title: "Heading 3",
89
+ description: "Small section heading.",
90
+ searchTerms: ["subtitle", "small"],
91
+ icon: <Looks3Icon size={18} />,
92
+ command: (view, range) => {
93
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
94
+ setBlockType(schema.nodes.heading, { level: 3 })(view.state, view.dispatch);
95
+ }
96
+ },
97
+ {
98
+ title: "Bullet List",
99
+ description: "Create a simple bullet list.",
100
+ searchTerms: ["unordered", "point"],
101
+ icon: <FormatListBulletedIcon size={18} />,
102
+ command: (view, range) => {
103
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
104
+ wrapInList(schema.nodes.bullet_list)(view.state, view.dispatch);
105
+ }
106
+ },
107
+ {
108
+ title: "Numbered List",
109
+ description: "Create a list with numbering.",
110
+ searchTerms: ["ordered"],
111
+ icon: <FormatListNumberedIcon size={18} />,
112
+ command: (view, range) => {
113
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
114
+ wrapInList(schema.nodes.ordered_list)(view.state, view.dispatch);
115
+ }
116
+ },
117
+ {
118
+ title: "Quote",
119
+ description: "Capture a quote.",
120
+ searchTerms: ["blockquote"],
121
+ icon: <FormatQuoteIcon size={18} />,
122
+ command: (view, range) => {
123
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
124
+ wrapIn(schema.nodes.blockquote)(view.state, view.dispatch);
125
+ }
126
+ },
127
+ {
128
+ title: "Code",
129
+ description: "Capture a code snippet.",
130
+ searchTerms: ["codeblock"],
131
+ icon: <CodeIcon size={18} />,
132
+ command: (view, range) => {
133
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
134
+ setBlockType(schema.nodes.code_block)(view.state, view.dispatch);
135
+ }
136
+ },
137
+ {
138
+ title: "Image",
139
+ description: "Upload an image from your computer.",
140
+ searchTerms: ["photo", "picture", "media", "upload", "file"],
141
+ icon: <ImageIcon size={18} />,
142
+ command: (view, range, upload) => {
143
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
144
+
145
+ const input = document.createElement("input");
146
+ input.type = "file";
147
+ input.accept = "image/*";
148
+ input.onchange = async () => {
149
+ if (input.files?.length) {
150
+ const file = input.files[0];
151
+ if (!file) return;
152
+ const pos = view.state.selection.from;
153
+
154
+ const images = Array.from(input.files).filter(f => /image/i.test(f.type));
155
+ if (images.length === 0) return false;
156
+
157
+ images.forEach(image => {
158
+ const reader = new FileReader();
159
+ reader.onload = async (readerEvent) => {
160
+ await onFileRead(view, readerEvent, pos, upload, image);
161
+ };
162
+ reader.readAsDataURL(image);
163
+ });
164
+ }
165
+ return true;
166
+ };
167
+ input.click();
168
+ }
169
+ },
170
+ {
171
+ title: "Table",
172
+ description: "Insert a custom grid table.",
173
+ searchTerms: ["table", "grid", "row", "col"],
174
+ icon: <TableChartIcon size={18} />,
175
+ command: (view, range, upload, aiController, setSubView) => {
176
+ if (setSubView) setSubView("table-grid");
177
+ }
178
+ }
179
+ ];
180
+
181
+ const autocompleteSuggestionItem: SuggestionItem = {
182
+ title: "Autocomplete",
183
+ description: "Add text based on the context.",
184
+ searchTerms: ["ai"],
185
+ icon: <AutoFixHighIcon size={18} />,
186
+ command: async (view, range, upload, aiController) => {
187
+ if (!aiController) throw Error("No AiController");
188
+
189
+ view.dispatch(view.state.tr.deleteRange(range.from, range.to));
190
+ setBlockType(schema.nodes.paragraph)(view.state, view.dispatch);
191
+
192
+ const { state } = view;
193
+ const { from, to } = state.selection;
194
+
195
+ const textBeforeCursor = state.doc.textBetween(0, from, "\n");
196
+ const textAfterCursor = state.doc.textBetween(to, state.doc.content.size, "\n");
197
+
198
+ let buffer = "";
199
+ const result = await aiController.autocomplete(textBeforeCursor, textAfterCursor, (delta) => {
200
+ buffer += delta;
201
+ if (delta.length !== 0) {
202
+ textLoadingCommands.toggleLoadingDecoration(view.state, view.dispatch, buffer);
203
+ }
204
+ });
205
+
206
+ // Insert parsed text result at cursor natively
207
+ try {
208
+ // The AI controller might stream literal "\n" characters depending on its JSON decoding layer.
209
+ // We need to un-escape these back to genuine newlines so MarkdownIt block-parsing works.
210
+ const unescapedResult = result.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
211
+
212
+ const isHTML = /<\/?[a-z][\s\S]*>/i.test(unescapedResult);
213
+ let parsedDoc;
214
+
215
+ if (isHTML) {
216
+ const div = document.createElement("div");
217
+ div.innerHTML = unescapedResult;
218
+ parsedDoc = DOMParser.fromSchema(view.state.schema).parse(div);
219
+ } else {
220
+ parsedDoc = parser.parse(unescapedResult);
221
+ }
222
+
223
+ if (parsedDoc) {
224
+ const tr = view.state.tr.replaceWith(view.state.selection.from, view.state.selection.from, parsedDoc.content);
225
+ view.dispatch(tr);
226
+ } else {
227
+ view.dispatch(view.state.tr.insertText(unescapedResult));
228
+ }
229
+ } catch (e) {
230
+ console.error(e);
231
+ view.dispatch(view.state.tr.insertText(result));
232
+ }
233
+ }
234
+ };
235
+
236
+ export const SlashCommandMenu = ({ upload, aiController }: { upload: UploadFn, aiController?: EditorAIController }) => {
237
+ const { view, state } = useProseMirrorContext();
238
+ const menuRef = useRef<HTMLDivElement>(null);
239
+ const [selectedIndex, setSelectedIndex] = useState(0);
240
+ const [subView, setSubView] = useState<string | null>(null);
241
+ const [tableGridCoords, setTableGridCoords] = useState({ r: 0, c: 0 });
242
+
243
+ const pluginState = state ? SlashCommandPluginKey.getState(state) : null;
244
+ const isActive = pluginState?.active;
245
+ const query = pluginState?.query || "";
246
+ const range = pluginState?.range;
247
+
248
+ const filteredItems = React.useMemo(() => {
249
+ if (!isActive) return [];
250
+ const availableItems = [...suggestionItems];
251
+ if (aiController) availableItems.push(autocompleteSuggestionItem);
252
+
253
+ return availableItems.filter(item => {
254
+ const inTitle = item.title.toLowerCase().includes(query.toLowerCase());
255
+ if (inTitle) return inTitle;
256
+ return item.searchTerms?.some(term => term.toLowerCase().includes(query.toLowerCase()));
257
+ });
258
+ }, [query, isActive, aiController]);
259
+
260
+ useEffect(() => {
261
+ setSelectedIndex(0);
262
+ }, [query]);
263
+
264
+ useEffect(() => {
265
+ if (!isActive) setSubView(null);
266
+ }, [isActive]);
267
+
268
+ useEffect(() => {
269
+ if (!view || !isActive || !range || !menuRef.current) return;
270
+
271
+ const start = view.coordsAtPos(range.from);
272
+ const virtualEl = {
273
+ getBoundingClientRect() {
274
+ return {
275
+ width: 0,
276
+ height: start.bottom - start.top,
277
+ x: start.left,
278
+ y: start.top,
279
+ top: start.top,
280
+ left: start.left,
281
+ right: start.left,
282
+ bottom: start.bottom,
283
+ };
284
+ }
285
+ };
286
+
287
+ const cleanup = autoUpdate(virtualEl as any, menuRef.current, () => {
288
+ if (!menuRef.current) return;
289
+ computePosition(virtualEl as any, menuRef.current, {
290
+ placement: "bottom-start",
291
+ middleware: [offset(4), flip(), shift()],
292
+ strategy: "fixed"
293
+ }).then(({ x, y }) => {
294
+ if (menuRef.current) {
295
+ Object.assign(menuRef.current.style, {
296
+ left: `${x}px`,
297
+ top: `${y}px`,
298
+ visibility: "visible",
299
+ });
300
+ }
301
+ });
302
+ });
303
+ return () => cleanup();
304
+ }, [view, isActive, range]);
305
+
306
+ useEffect(() => {
307
+ if (!isActive || !view) return;
308
+
309
+ const handleKeyDown = (e: KeyboardEvent) => {
310
+ if (subView === "table-grid") {
311
+ if (e.key === "Escape") {
312
+ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
313
+ setSubView(null);
314
+ setTableGridCoords({ r: 0, c: 0 });
315
+ } else if (e.key === "ArrowUp") {
316
+ e.preventDefault(); e.stopPropagation();
317
+ setTableGridCoords(prev => ({ r: Math.max(0, prev.r - 1), c: prev.c }));
318
+ } else if (e.key === "ArrowDown") {
319
+ e.preventDefault(); e.stopPropagation();
320
+ setTableGridCoords(prev => ({ r: Math.min(4, prev.r + 1), c: prev.c }));
321
+ } else if (e.key === "ArrowLeft") {
322
+ e.preventDefault(); e.stopPropagation();
323
+ setTableGridCoords(prev => ({ r: prev.r, c: Math.max(0, prev.c - 1) }));
324
+ } else if (e.key === "ArrowRight") {
325
+ e.preventDefault(); e.stopPropagation();
326
+ setTableGridCoords(prev => ({ r: prev.r, c: Math.min(4, prev.c + 1) }));
327
+ } else if (e.key === "Enter") {
328
+ e.preventDefault(); e.stopPropagation();
329
+ if (range) {
330
+ const tableNode = createTableNode(view.state.schema, tableGridCoords.r + 1, tableGridCoords.c + 1);
331
+ const tr = view.state.tr.replaceWith(range.from, range.to, tableNode);
332
+ try {
333
+ const selection = TextSelection.create(tr.doc, range.from + 4);
334
+ tr.setSelection(selection);
335
+ } catch (e) {
336
+ console.warn("Could not select first cell", e);
337
+ }
338
+ tr.setMeta(SlashCommandPluginKey, { active: false });
339
+ view.dispatch(tr);
340
+ view.focus();
341
+ setSubView(null);
342
+ setTableGridCoords({ r: 0, c: 0 });
343
+ }
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (e.key === "ArrowUp") {
349
+ e.preventDefault();
350
+ e.stopPropagation();
351
+ setSelectedIndex(prev => (prev + filteredItems.length - 1) % filteredItems.length);
352
+ } else if (e.key === "ArrowDown") {
353
+ e.preventDefault();
354
+ e.stopPropagation();
355
+ setSelectedIndex(prev => (prev + 1) % filteredItems.length);
356
+ } else if (e.key === "Enter") {
357
+ e.preventDefault();
358
+ e.stopPropagation();
359
+ if (filteredItems[selectedIndex] && range) {
360
+ filteredItems[selectedIndex].command(view, range, upload, aiController, setSubView);
361
+ // Do not focus view if a subview opened
362
+ setTimeout(() => {
363
+ // Focus is managed by the caller
364
+ }, 0);
365
+ }
366
+ } else if (e.key === "Escape") {
367
+ e.preventDefault();
368
+ e.stopPropagation();
369
+ e.stopImmediatePropagation();
370
+ // Close menu gracefully and keep it dismissed
371
+ view.dispatch(view.state.tr.setMeta(SlashCommandPluginKey, { active: false, dismissed: true }));
372
+ }
373
+ };
374
+
375
+ window.addEventListener("keydown", handleKeyDown, { capture: true });
376
+ return () => window.removeEventListener("keydown", handleKeyDown, { capture: true });
377
+ }, [isActive, selectedIndex, filteredItems, view, range, upload, aiController, subView, tableGridCoords]);
378
+
379
+ const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
380
+
381
+ useEffect(() => {
382
+ if (itemRefs.current[selectedIndex]) {
383
+ itemRefs.current[selectedIndex]?.scrollIntoView({ block: "nearest" });
384
+ }
385
+ }, [selectedIndex]);
386
+
387
+ useEffect(() => {
388
+ if (!subView) {
389
+ setTableGridCoords({ r: 0, c: 0 });
390
+ }
391
+ }, [subView]);
392
+
393
+ if (!isActive || filteredItems.length === 0) return null;
394
+
395
+ if (subView === "table-grid" && range && view) {
396
+ return (
397
+ <div
398
+ ref={menuRef}
399
+ onMouseDown={(e) => e.preventDefault()}
400
+ style={{ position: "fixed", zIndex: 9999, visibility: "hidden" }}
401
+ className={cls("text-surface-900 dark:text-white rounded-md border bg-white dark:bg-surface-900 p-2 shadow transition-none", defaultBorderMixin)}
402
+ >
403
+ <TableGridPicker
404
+ hoveredRow={tableGridCoords.r}
405
+ hoveredCol={tableGridCoords.c}
406
+ onHover={(r, c) => setTableGridCoords({ r, c })}
407
+ onSelect={(rows, cols) => {
408
+ const tableNode = createTableNode(view.state.schema, rows, cols);
409
+ const tr = view.state.tr.replaceWith(range.from, range.to, tableNode);
410
+ try {
411
+ const selection = TextSelection.create(tr.doc, range.from + 4);
412
+ tr.setSelection(selection);
413
+ } catch (e) {
414
+ console.warn("Could not select first cell", e);
415
+ }
416
+ tr.setMeta(SlashCommandPluginKey, { active: false });
417
+ view.dispatch(tr);
418
+ view.focus();
419
+ setSubView(null);
420
+ }}
421
+ />
422
+ </div>
423
+ );
424
+ }
425
+
426
+ return (
427
+ <div
428
+ ref={menuRef}
429
+ style={{ position: "fixed", zIndex: 9999, visibility: "hidden" }}
430
+ className={cls("text-surface-900 dark:text-white max-h-[280px] w-72 overflow-y-auto rounded-md border bg-white dark:bg-surface-900 px-1 py-2 shadow transition-none", defaultBorderMixin)}
431
+ >
432
+ {filteredItems.map((item, index) => (
433
+ <button
434
+ key={item.title}
435
+ ref={el => { itemRefs.current[index] = el; }}
436
+ onClick={(e) => {
437
+ e.preventDefault();
438
+ if (range && view) {
439
+ item.command(view, range, upload, aiController, setSubView);
440
+ // Only focus back to editor if it didn't open a sub-view
441
+ if (!subView) view.focus();
442
+ }
443
+ }}
444
+ onMouseDown={(e) => e.preventDefault()}
445
+ className={cls("flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-blue-50 hover:dark:bg-surface-700",
446
+ index === selectedIndex ? "bg-blue-100 dark:bg-surface-accent-950" : "")}
447
+ >
448
+ <div className={cls("flex h-10 w-10 shrink-0 items-center justify-center rounded-md border bg-white dark:bg-surface-900", defaultBorderMixin)}>
449
+ {item.icon}
450
+ </div>
451
+ <div className="flex flex-col overflow-hidden">
452
+ <p className="font-medium truncate">{item.title}</p>
453
+ <p className="text-xs text-surface-700 dark:text-surface-accent-300 truncate">
454
+ {item.description}
455
+ </p>
456
+ </div>
457
+ </button>
458
+ ))}
459
+ </div>
460
+ );
461
+ };
462
+
463
+ const createTableNode = (schema: any, rowsCount: number, colsCount: number) => {
464
+ const rows = [];
465
+ for (let r = 0; r < rowsCount; r++) {
466
+ const cells = [];
467
+ for (let c = 0; c < colsCount; c++) {
468
+ const isHeader = r === 0;
469
+ const cellType = isHeader ? schema.nodes.table_header : schema.nodes.table_cell;
470
+ const cell = cellType.createAndFill();
471
+ if (cell) cells.push(cell);
472
+ }
473
+ const row = schema.nodes.table_row.create(null, Fragment.from(cells));
474
+ rows.push(row);
475
+ }
476
+ return schema.nodes.table.create(null, Fragment.from(rows));
477
+ };
478
+
479
+ const TableGridPicker = ({
480
+ hoveredRow,
481
+ hoveredCol,
482
+ onHover,
483
+ onSelect
484
+ }: {
485
+ hoveredRow: number;
486
+ hoveredCol: number;
487
+ onHover: (r: number, c: number) => void;
488
+ onSelect: (r: number, c: number) => void;
489
+ }) => {
490
+ return (
491
+ <div className="flex flex-col gap-1 items-center justify-center p-1 w-fit">
492
+ <span className="text-xs text-gray-500 font-medium mb-1">
493
+ {hoveredCol + 1} x {hoveredRow + 1} Table
494
+ </span>
495
+ <div className="flex flex-col gap-1">
496
+ {Array.from({ length: 5 }).map((_, r) => (
497
+ <div key={r} className="flex gap-1">
498
+ {Array.from({ length: 5 }).map((_, c) => (
499
+ <div
500
+ key={c}
501
+ className={cls(
502
+ "w-5 h-5 border rounded-sm cursor-pointer transition-colors duration-75",
503
+ r <= hoveredRow && c <= hoveredCol
504
+ ? "bg-blue-100 border-blue-400 dark:bg-blue-900 dark:border-blue-500"
505
+ : "bg-white dark:bg-surface-800 border-gray-200 dark:border-gray-700 hover:border-blue-300"
506
+ )}
507
+ onMouseEnter={() => onHover(r, c)}
508
+ onClick={() => onSelect(hoveredRow + 1, hoveredCol + 1)}
509
+ />
510
+ ))}
511
+ </div>
512
+ ))}
513
+ </div>
514
+ </div>
515
+ );
516
+ };
@@ -0,0 +1,32 @@
1
+ import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
4
+
5
+ interface EditorBubbleItemProps {
6
+ children: ReactNode;
7
+ asChild?: boolean;
8
+ onSelect?: () => void;
9
+ }
10
+
11
+ export const EditorBubbleItem = forwardRef<
12
+ HTMLDivElement,
13
+ EditorBubbleItemProps & Omit<ComponentPropsWithoutRef<"div">, "onSelect">
14
+ >(({ children, asChild, onSelect, ...rest }, ref) => {
15
+ const { view } = useProseMirrorContext();
16
+ const Comp = asChild ? Slot : "div";
17
+
18
+ if (!view) return null;
19
+
20
+ return (
21
+ <Comp ref={ref} {...rest} onMouseDown={(e: React.MouseEvent) => {
22
+ // Prevent default to avoid losing focus
23
+ e.preventDefault();
24
+ }} onClick={() => onSelect?.()}>
25
+ {children}
26
+ </Comp>
27
+ );
28
+ });
29
+
30
+ EditorBubbleItem.displayName = "EditorBubbleItem";
31
+
32
+ export default EditorBubbleItem;