@alpaca-editor/core 1.0.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 (239) hide show
  1. package/.prettierrc +3 -0
  2. package/eslint.config.mjs +4 -0
  3. package/images/bg-shape-black.webp +0 -0
  4. package/package.json +52 -0
  5. package/src/client-components/api.ts +6 -0
  6. package/src/client-components/index.ts +19 -0
  7. package/src/components/ActionButton.tsx +43 -0
  8. package/src/components/Error.tsx +57 -0
  9. package/src/config/config.tsx +737 -0
  10. package/src/config/types.ts +263 -0
  11. package/src/editor/ComponentInfo.tsx +77 -0
  12. package/src/editor/ConfirmationDialog.tsx +103 -0
  13. package/src/editor/ContentTree.tsx +654 -0
  14. package/src/editor/ContextMenu.tsx +155 -0
  15. package/src/editor/Editor.tsx +91 -0
  16. package/src/editor/EditorWarning.tsx +34 -0
  17. package/src/editor/EditorWarnings.tsx +33 -0
  18. package/src/editor/FieldEditorPopup.tsx +65 -0
  19. package/src/editor/FieldHistory.tsx +74 -0
  20. package/src/editor/FieldList.tsx +190 -0
  21. package/src/editor/FieldListField.tsx +387 -0
  22. package/src/editor/FieldListFieldWithFallbacks.tsx +211 -0
  23. package/src/editor/FloatingToolbar.tsx +163 -0
  24. package/src/editor/ImageEditor.tsx +129 -0
  25. package/src/editor/InsertMenu.tsx +332 -0
  26. package/src/editor/ItemInfo.tsx +90 -0
  27. package/src/editor/LinkEditorDialog.tsx +192 -0
  28. package/src/editor/MainLayout.tsx +94 -0
  29. package/src/editor/NewEditorClient.tsx +11 -0
  30. package/src/editor/PictureCropper.tsx +505 -0
  31. package/src/editor/PictureEditor.tsx +206 -0
  32. package/src/editor/PictureEditorDialog.tsx +381 -0
  33. package/src/editor/PublishDialog.ignore +74 -0
  34. package/src/editor/ScrollingContentTree.tsx +47 -0
  35. package/src/editor/Terminal.tsx +215 -0
  36. package/src/editor/Titlebar.tsx +23 -0
  37. package/src/editor/ai/AiPopup.tsx +59 -0
  38. package/src/editor/ai/AiResponseMessage.tsx +82 -0
  39. package/src/editor/ai/AiTerminal.tsx +450 -0
  40. package/src/editor/ai/AiToolCall.tsx +46 -0
  41. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  42. package/src/editor/ai/editorAiContext.ts +18 -0
  43. package/src/editor/client/DialogContext.tsx +49 -0
  44. package/src/editor/client/EditorClient.tsx +1831 -0
  45. package/src/editor/client/GenericDialog.tsx +50 -0
  46. package/src/editor/client/editContext.ts +330 -0
  47. package/src/editor/client/helpers.ts +44 -0
  48. package/src/editor/client/itemsRepository.ts +391 -0
  49. package/src/editor/client/operations.ts +610 -0
  50. package/src/editor/client/pageModelBuilder.ts +182 -0
  51. package/src/editor/commands/commands.ts +23 -0
  52. package/src/editor/commands/componentCommands.tsx +408 -0
  53. package/src/editor/commands/createVersionCommand.ts +33 -0
  54. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  55. package/src/editor/commands/itemCommands.tsx +186 -0
  56. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  57. package/src/editor/commands/undo.ts +39 -0
  58. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  59. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  60. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  61. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  62. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  63. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  64. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  65. package/src/editor/component-designer/PlaceholdersEditor.tsx +183 -0
  66. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  67. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  68. package/src/editor/component-designer/aiContext.ts +23 -0
  69. package/src/editor/componentTreeHelper.tsx +114 -0
  70. package/src/editor/control-center/ControlCenterMenu.tsx +71 -0
  71. package/src/editor/control-center/IndexOverview.tsx +50 -0
  72. package/src/editor/control-center/IndexSettings.tsx +266 -0
  73. package/src/editor/control-center/Status.tsx +7 -0
  74. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  75. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  76. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  77. package/src/editor/editor-warnings/NoWriteAccess.tsx +15 -0
  78. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  79. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  80. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  81. package/src/editor/field-types/DropLinkEditor.tsx +75 -0
  82. package/src/editor/field-types/DropListEditor.tsx +84 -0
  83. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  84. package/src/editor/field-types/InternalLinkFieldEditor.tsx +112 -0
  85. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  86. package/src/editor/field-types/MultiLineText.tsx +63 -0
  87. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  88. package/src/editor/field-types/RawEditor.tsx +53 -0
  89. package/src/editor/field-types/ReactQuill.tsx +580 -0
  90. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  91. package/src/editor/field-types/RichTextEditorComponent.tsx +108 -0
  92. package/src/editor/field-types/SingleLineText.tsx +150 -0
  93. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  94. package/src/editor/fieldTypes.ts +140 -0
  95. package/src/editor/media-selector/AiImageSearch.tsx +186 -0
  96. package/src/editor/media-selector/AiImageSearchPrompt.tsx +95 -0
  97. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  98. package/src/editor/media-selector/Preview.tsx +14 -0
  99. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  100. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  101. package/src/editor/media-selector/UploadZone.tsx +137 -0
  102. package/src/editor/menubar/ActionsMenu.tsx +47 -0
  103. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  104. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  105. package/src/editor/menubar/BrowseHistory.tsx +37 -0
  106. package/src/editor/menubar/ItemLanguageVersion.tsx +52 -0
  107. package/src/editor/menubar/LanguageSelector.tsx +152 -0
  108. package/src/editor/menubar/Menu.tsx +83 -0
  109. package/src/editor/menubar/NavButtons.tsx +74 -0
  110. package/src/editor/menubar/PageSelector.tsx +139 -0
  111. package/src/editor/menubar/PageViewerControls.tsx +99 -0
  112. package/src/editor/menubar/Separator.tsx +12 -0
  113. package/src/editor/menubar/SiteInfo.tsx +53 -0
  114. package/src/editor/menubar/User.tsx +27 -0
  115. package/src/editor/menubar/VersionSelector.tsx +143 -0
  116. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +287 -0
  117. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  118. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +44 -0
  119. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  120. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  121. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  122. package/src/editor/page-editor-chrome/FrameMenu.tsx +263 -0
  123. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  124. package/src/editor/page-editor-chrome/InlineEditor.tsx +147 -0
  125. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  126. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  127. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +119 -0
  128. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +154 -0
  129. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +171 -0
  130. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +233 -0
  131. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  132. package/src/editor/page-viewer/EditorForm.tsx +247 -0
  133. package/src/editor/page-viewer/MiniMap.tsx +351 -0
  134. package/src/editor/page-viewer/PageViewer.tsx +127 -0
  135. package/src/editor/page-viewer/PageViewerFrame.tsx +1030 -0
  136. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  137. package/src/editor/pageModel.ts +191 -0
  138. package/src/editor/picture-shared.tsx +53 -0
  139. package/src/editor/reviews/Comment.tsx +265 -0
  140. package/src/editor/reviews/Comments.tsx +50 -0
  141. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  142. package/src/editor/reviews/Reviews.tsx +280 -0
  143. package/src/editor/reviews/reviewCommands.tsx +47 -0
  144. package/src/editor/reviews/useReviews.tsx +70 -0
  145. package/src/editor/services/aiService.ts +155 -0
  146. package/src/editor/services/componentDesignerService.ts +151 -0
  147. package/src/editor/services/contentService.ts +159 -0
  148. package/src/editor/services/editService.ts +462 -0
  149. package/src/editor/services/indexService.ts +24 -0
  150. package/src/editor/services/reviewsService.ts +45 -0
  151. package/src/editor/services/serviceHelper.ts +95 -0
  152. package/src/editor/services/systemService.ts +5 -0
  153. package/src/editor/services/translationService.ts +21 -0
  154. package/src/editor/services-server/api.ts +150 -0
  155. package/src/editor/services-server/graphQL.ts +106 -0
  156. package/src/editor/sidebar/ComponentPalette.tsx +146 -0
  157. package/src/editor/sidebar/ComponentTree.tsx +512 -0
  158. package/src/editor/sidebar/ComponentTree2.tsxx +490 -0
  159. package/src/editor/sidebar/Debug.tsx +105 -0
  160. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  161. package/src/editor/sidebar/EditHistory.tsx +134 -0
  162. package/src/editor/sidebar/GraphQL.tsx +164 -0
  163. package/src/editor/sidebar/Insert.tsx +35 -0
  164. package/src/editor/sidebar/MainContentTree.tsx +95 -0
  165. package/src/editor/sidebar/Performance.tsx +53 -0
  166. package/src/editor/sidebar/Sessions.tsx +35 -0
  167. package/src/editor/sidebar/Sidebar.tsx +20 -0
  168. package/src/editor/sidebar/SidebarView.tsx +150 -0
  169. package/src/editor/sidebar/Translations.tsx +276 -0
  170. package/src/editor/sidebar/Validation.tsx +102 -0
  171. package/src/editor/sidebar/ViewSelector.tsx +49 -0
  172. package/src/editor/sidebar/Workbox.tsx +209 -0
  173. package/src/editor/ui/CenteredMessage.tsx +7 -0
  174. package/src/editor/ui/CopyToClipboardButton.tsx +23 -0
  175. package/src/editor/ui/DialogButtons.tsx +11 -0
  176. package/src/editor/ui/Icons.tsx +585 -0
  177. package/src/editor/ui/ItemNameDialog.tsx +94 -0
  178. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  179. package/src/editor/ui/ItemSearch.tsx +173 -0
  180. package/src/editor/ui/PerfectTree.tsx +550 -0
  181. package/src/editor/ui/Section.tsx +35 -0
  182. package/src/editor/ui/SimpleIconButton.tsx +43 -0
  183. package/src/editor/ui/SimpleMenu.tsx +48 -0
  184. package/src/editor/ui/SimpleTable.tsx +63 -0
  185. package/src/editor/ui/SimpleTabs.tsx +55 -0
  186. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  187. package/src/editor/ui/Spinner.tsx +7 -0
  188. package/src/editor/ui/Splitter.tsx +247 -0
  189. package/src/editor/ui/StackedPanels.tsx +134 -0
  190. package/src/editor/ui/Toolbar.tsx +7 -0
  191. package/src/editor/utils/id-helper.ts +3 -0
  192. package/src/editor/utils/insertOptions.ts +69 -0
  193. package/src/editor/utils/itemutils.ts +29 -0
  194. package/src/editor/utils/useMemoDebug.ts +28 -0
  195. package/src/editor/utils.ts +435 -0
  196. package/src/editor/views/CompareView.tsx +256 -0
  197. package/src/editor/views/EditView.tsx +27 -0
  198. package/src/editor/views/ItemEditor.tsx +58 -0
  199. package/src/editor/views/SingleEditView.tsx +44 -0
  200. package/src/fonts/Geist-Black.woff2 +0 -0
  201. package/src/fonts/Geist-Bold.woff2 +0 -0
  202. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  203. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  204. package/src/fonts/Geist-Light.woff2 +0 -0
  205. package/src/fonts/Geist-Medium.woff2 +0 -0
  206. package/src/fonts/Geist-Regular.woff2 +0 -0
  207. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  208. package/src/fonts/Geist-Thin.woff2 +0 -0
  209. package/src/fonts/Geist[wght].woff2 +0 -0
  210. package/src/index.ts +7 -0
  211. package/src/page-wizard/PageWizard.tsx +163 -0
  212. package/src/page-wizard/SelectWizard.tsx +109 -0
  213. package/src/page-wizard/WizardSteps.tsx +207 -0
  214. package/src/page-wizard/service.ts +35 -0
  215. package/src/page-wizard/startPageWizardCommand.ts +27 -0
  216. package/src/page-wizard/steps/BuildPageStep.tsx +266 -0
  217. package/src/page-wizard/steps/CollectStep.tsx +233 -0
  218. package/src/page-wizard/steps/ComponentTypesSelector.tsx +443 -0
  219. package/src/page-wizard/steps/Components.tsx +193 -0
  220. package/src/page-wizard/steps/CreatePage.tsx +285 -0
  221. package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +384 -0
  222. package/src/page-wizard/steps/EditButton.tsx +34 -0
  223. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  224. package/src/page-wizard/steps/Generate.tsx +32 -0
  225. package/src/page-wizard/steps/ImagesStep.tsx +318 -0
  226. package/src/page-wizard/steps/LayoutStep.tsx +228 -0
  227. package/src/page-wizard/steps/SelectStep.tsx +256 -0
  228. package/src/page-wizard/steps/schema.ts +180 -0
  229. package/src/page-wizard/steps/usePageCreator.ts +279 -0
  230. package/src/splash-screen/NewPage.tsx +232 -0
  231. package/src/splash-screen/SectionHeadline.tsx +21 -0
  232. package/src/splash-screen/SplashScreen.tsx +156 -0
  233. package/src/tour/Tour.tsx +558 -0
  234. package/src/tour/default-tour.tsx +300 -0
  235. package/src/tour/preview-tour.tsx +127 -0
  236. package/src/types.ts +302 -0
  237. package/styles.css +476 -0
  238. package/tsconfig.build.json +21 -0
  239. package/tsconfig.json +11 -0
@@ -0,0 +1,550 @@
1
+ import React, { useEffect, useMemo, useCallback, memo } from "react";
2
+ import { Spinner } from "./Spinner";
3
+ import { ProgressSpinner } from "primereact/progressspinner";
4
+ export interface TreeNode<T = any> {
5
+ key: string;
6
+ label: string;
7
+ icon?: string;
8
+ data?: T;
9
+ /** Indicates if the node is expandable (has or can have children) */
10
+ hasChildren?: boolean;
11
+ /**
12
+ * If present, contains the node's children.
13
+ * Use undefined to signal that children have not yet been loaded.
14
+ * Use null to signal that children are currently loading.
15
+ */
16
+ children?: TreeNode<T>[] | null;
17
+ }
18
+
19
+ export interface TreeProps<T = any> {
20
+ /** Array of tree nodes */
21
+ nodes: TreeNode<T>[];
22
+ /** Keys of currently selected nodes */
23
+ selectedKeys?: string[];
24
+ /** Keys of expanded nodes */
25
+ expandedKeys?: string[];
26
+ /** Callback to render a single node (template) */
27
+ renderNode: (node: TreeNode<T>) => React.ReactNode;
28
+ /** Called when a node's expand/collapse toggle is activated */
29
+ onToggleExpand?: (key: string) => void;
30
+ /** Called when a node is clicked for selection */
31
+ onSelect?: (key: string, event: React.MouseEvent) => void;
32
+ /**
33
+ * Called during a drag over a drop zone between nodes.
34
+ * @param parent The parent node of the current list (null for root level)
35
+ * @param index The position where a new node would be inserted (0 = before first node)
36
+ */
37
+ onDragOverZone?: (
38
+ parent: TreeNode<T> | null,
39
+ index: number,
40
+ event: React.DragEvent,
41
+ ) => boolean;
42
+ /**
43
+ * Called when an item is dropped into a drop zone between nodes.
44
+ * @param parent The parent node of the current list (null for root level)
45
+ * @param index The position where a new node should be inserted (0 = before first node)
46
+ */
47
+ onDrop?: (
48
+ parent: TreeNode<T> | null,
49
+ index: number,
50
+ event: React.DragEvent,
51
+ ) => void;
52
+ isDragging?: boolean;
53
+ onStartDrag?: (data: {
54
+ node: TreeNode<T>;
55
+ event: React.DragEvent<any>;
56
+ isMultiSelect: boolean;
57
+ }) => void;
58
+ onDragEnd?: (event: React.DragEvent | null) => void;
59
+ enableDragAndDrop?: boolean;
60
+
61
+ /**
62
+ * Callback that notifies when a lazy load should occur.
63
+ * When a node is toggled and its children haven't been loaded yet (i.e. undefined),
64
+ * this callback is invoked so you can load the children asynchronously and update your tree model.
65
+ */
66
+ onLazyLoad?: (node: TreeNode<T>) => void;
67
+ onDoubleClick?: (node: TreeNode<T>) => void;
68
+ onContextMenu?: (node: TreeNode<T>, event: React.MouseEvent) => void;
69
+ }
70
+
71
+ // Local DropZone component to handle drag-over state.
72
+ const DropZone = memo(
73
+ ({
74
+ parent,
75
+ index,
76
+ isDragging,
77
+ onDragOverZone,
78
+ onDrop,
79
+ onDragEnd,
80
+ isLast,
81
+ }: {
82
+ parent: TreeNode<any> | null;
83
+ index: number;
84
+ isDragging: boolean;
85
+ onDragOverZone?: (
86
+ parent: TreeNode<any> | null,
87
+ index: number,
88
+ e: React.DragEvent,
89
+ ) => boolean;
90
+ onDrop?: (
91
+ parent: TreeNode<any> | null,
92
+ index: number,
93
+ e: React.DragEvent,
94
+ ) => void;
95
+ onDragEnd?: (event: React.DragEvent | null) => void;
96
+ isLast?: boolean;
97
+ }) => {
98
+ const [isDragOver, setIsDragOver] = React.useState(false);
99
+
100
+ const handleDragEnter = useCallback(
101
+ (e: React.DragEvent<HTMLDivElement>) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ if (onDragOverZone) {
105
+ const allowed = onDragOverZone(parent, index, e);
106
+ setIsDragOver(allowed);
107
+ e.dataTransfer.dropEffect = allowed ? "move" : "none";
108
+ }
109
+ },
110
+ [onDragOverZone, parent, index],
111
+ );
112
+
113
+ const handleDragLeave = useCallback(
114
+ (e: React.DragEvent<HTMLDivElement>) => {
115
+ e.preventDefault();
116
+ e.stopPropagation();
117
+ setIsDragOver(false);
118
+ },
119
+ [],
120
+ );
121
+
122
+ const handleDragOver = useCallback(
123
+ (e: React.DragEvent<HTMLDivElement>) => {
124
+ e.preventDefault();
125
+ e.stopPropagation();
126
+ if (onDragOverZone) {
127
+ const allowed = onDragOverZone(parent, index, e);
128
+ setIsDragOver(allowed);
129
+ }
130
+ },
131
+ [onDragOverZone, parent, index],
132
+ );
133
+
134
+ const handleDrop = useCallback(
135
+ (e: React.DragEvent<HTMLDivElement>) => {
136
+ e.preventDefault();
137
+ e.stopPropagation();
138
+ setIsDragOver(false);
139
+ if (onDrop) {
140
+ onDrop(parent, index, e);
141
+ }
142
+ },
143
+ [onDrop, parent, index],
144
+ );
145
+
146
+ if (!isDragging) return null;
147
+
148
+ return (
149
+ <div className={`relative ${isLast ? "h-3" : ""}`}>
150
+ <div
151
+ className={`drop-zone absolute top-[-5px] right-0 left-[45px] z-1000 h-3 rounded-md transition-colors duration-100 ${
152
+ isDragOver ? "bg-sky-200" : ""
153
+ }`}
154
+ onDragEnter={handleDragEnter}
155
+ onDragOver={handleDragOver}
156
+ onDrop={handleDrop}
157
+ onDragLeave={handleDragLeave}
158
+ />
159
+ </div>
160
+ );
161
+ },
162
+ );
163
+
164
+ // NodeContent component extracted and memoized
165
+ const NodeContent = memo(
166
+ ({
167
+ node,
168
+ isExpanded,
169
+ isSelected,
170
+ onSelect,
171
+ onToggleNode,
172
+ onStartDrag,
173
+ onDragEnd,
174
+ onDragOverZone,
175
+ onDrop,
176
+ onDoubleClick,
177
+ renderNode,
178
+ onContextMenu,
179
+ enableDragAndDrop = false,
180
+ selectedKeys,
181
+ isDragging,
182
+ }: {
183
+ node: TreeNode<any>;
184
+ isExpanded: boolean;
185
+ isSelected: boolean;
186
+ onSelect: (nodeKey: string, e: React.MouseEvent) => void;
187
+ onToggleNode: (node: TreeNode<any>) => void;
188
+ onStartDrag?: (data: {
189
+ node: TreeNode<any>;
190
+ event: React.DragEvent<any>;
191
+ isMultiSelect: boolean;
192
+ }) => void;
193
+ onDragEnd?: (event: React.DragEvent<any>) => void;
194
+ onDragOverZone?: (
195
+ parent: TreeNode<any> | null,
196
+ index: number,
197
+ e: React.DragEvent<any>,
198
+ ) => boolean;
199
+ onDrop?: (
200
+ parent: TreeNode<any> | null,
201
+ index: number,
202
+ e: React.DragEvent<any>,
203
+ ) => void;
204
+ onDoubleClick?: (node: TreeNode<any>) => void;
205
+ onContextMenu?: (node: TreeNode<any>, event: React.MouseEvent) => void;
206
+ renderNode: (node: TreeNode<any>) => React.ReactNode;
207
+ enableDragAndDrop?: boolean;
208
+ selectedKeys?: string[];
209
+ isDragging: boolean;
210
+ }) => {
211
+ const [isDragOver, setIsDragOver] = React.useState(false);
212
+
213
+ useEffect(() => {
214
+ if (!isDragging) {
215
+ setIsDragOver(false);
216
+ }
217
+ }, [isDragging]);
218
+
219
+ const handleDragStart = useCallback(
220
+ (event: React.DragEvent<any>) => {
221
+ const isMultiSelect =
222
+ isSelected && selectedKeys && selectedKeys.length > 1;
223
+
224
+ // Set drag preview for multiple items if applicable
225
+ if (isMultiSelect) {
226
+ // Create custom drag image showing count of selected items
227
+ const dragPreview = document.createElement("div");
228
+ dragPreview.className =
229
+ "bg-white shadow-md rounded-md border border-gray-300 px-2 py-1 text-sm max-w-24 absolute top-[-1000px] left-[-1000px]";
230
+ dragPreview.innerHTML = `<div class="flex items-center"><span class="font-bold mr-1">${selectedKeys.length}</span> items</div>`;
231
+ document.body.appendChild(dragPreview);
232
+
233
+ // Set custom drag image
234
+ event.dataTransfer.setDragImage(dragPreview, 15, 15);
235
+
236
+ // Remove the element after drag starts
237
+ setTimeout(() => {
238
+ console.log("removing drag preview");
239
+ document.body.removeChild(dragPreview);
240
+ }, 0);
241
+ }
242
+ if (onStartDrag) {
243
+ onStartDrag({ node, event, isMultiSelect: isMultiSelect ?? false });
244
+ }
245
+ },
246
+ [node, onStartDrag],
247
+ );
248
+
249
+ const handleDragLeave = useCallback((event: React.DragEvent<any>) => {
250
+ event.preventDefault();
251
+ setIsDragOver(false);
252
+ }, []);
253
+
254
+ const handleDragEnter = useCallback(
255
+ (event: React.DragEvent<any>) => {
256
+ event.preventDefault();
257
+ if (onDragOverZone) {
258
+ const allowed = onDragOverZone(node, -1, event);
259
+ setIsDragOver(allowed);
260
+ }
261
+ },
262
+ [node, onDragOverZone],
263
+ );
264
+
265
+ const handleDragOver = useCallback(
266
+ (event: React.DragEvent<any>) => {
267
+ event.preventDefault();
268
+ if (onDragOverZone) {
269
+ const allowed = onDragOverZone(node, -1, event);
270
+ setIsDragOver(allowed);
271
+ event.dataTransfer.dropEffect = allowed ? "move" : "none";
272
+ }
273
+ },
274
+ [node, onDragOverZone],
275
+ );
276
+
277
+ const handleDrop = useCallback(
278
+ (e: React.DragEvent<any>) => {
279
+ e.preventDefault();
280
+ e.stopPropagation();
281
+ if (onDrop) {
282
+ onDrop(node, -1, e);
283
+ }
284
+ },
285
+ [node, onDrop],
286
+ );
287
+
288
+ const handleDoubleClick = useCallback(
289
+ (e: React.MouseEvent<any>) => {
290
+ e.stopPropagation();
291
+ onDoubleClick?.(node);
292
+ },
293
+ [node, onDoubleClick],
294
+ );
295
+
296
+ const handleSelect = useCallback(
297
+ (e: React.MouseEvent<any>) => {
298
+ e.stopPropagation();
299
+ onSelect(node.key, e);
300
+ },
301
+ [node.key, onSelect],
302
+ );
303
+
304
+ const handleToggle = useCallback(
305
+ (e: React.MouseEvent<any>) => {
306
+ e.stopPropagation();
307
+ onToggleNode(node);
308
+ },
309
+ [node, onToggleNode],
310
+ );
311
+
312
+ const renderToggle = () => {
313
+ if (node.hasChildren && node.children === null) {
314
+ return (
315
+ <ProgressSpinner
316
+ style={{ width: "18px", height: "18px", margin: "0 2px" }}
317
+ className="text-gray-500"
318
+ />
319
+ );
320
+ }
321
+
322
+ return (
323
+ <span
324
+ onClick={handleToggle}
325
+ className={`mr-2 inline-block transform cursor-pointer transition duration-150 select-none ${
326
+ isExpanded ? "rotate-90" : "rotate-0"
327
+ }`}
328
+ >
329
+ <svg
330
+ width="14"
331
+ height="14"
332
+ viewBox="0 0 14 14"
333
+ fill="none"
334
+ xmlns="http://www.w3.org/2000/svg"
335
+ aria-hidden="true"
336
+ data-pc-section="togglericon"
337
+ >
338
+ <path
339
+ d="M4.38708 13C4.28408 13.0005 4.18203 12.9804 4.08691 12.9409C3.99178 12.9014 3.9055 12.8433 3.83313 12.7701C3.68634 12.6231 3.60388 12.4238 3.60388 12.2161C3.60388 12.0084 3.68634 11.8091 3.83313 11.6622L8.50507 6.99022L3.83313 2.31827C3.69467 2.16968 3.61928 1.97313 3.62287 1.77005C3.62645 1.56698 3.70872 1.37322 3.85234 1.22959C3.99596 1.08597 4.18972 1.00371 4.3928 1.00012C4.59588 0.996539 4.79242 1.07192 4.94102 1.21039L10.1669 6.43628C10.3137 6.58325 10.3962 6.78249 10.3962 6.99022C10.3962 7.19795 10.3137 7.39718 10.1669 7.54416L4.94102 12.7701C4.86865 12.8433 4.78237 12.9014 4.68724 12.9409C4.59212 12.9804 4.49007 13.0005 4.38708 13Z"
340
+ fill="currentColor"
341
+ ></path>
342
+ </svg>
343
+ </span>
344
+ );
345
+ };
346
+
347
+ const handleContextMenu = useCallback(
348
+ (e: React.MouseEvent<any>) => {
349
+ e.stopPropagation();
350
+ e.preventDefault();
351
+ onContextMenu?.(node, e);
352
+ },
353
+ [node, onContextMenu],
354
+ );
355
+
356
+ return (
357
+ <div
358
+ className="tree-node mb-0.5 flex cursor-pointer items-center"
359
+ draggable={enableDragAndDrop}
360
+ onClick={handleSelect}
361
+ onDragStart={(event) => handleDragStart(event)}
362
+ onDragEnd={onDragEnd as any}
363
+ onDragLeave={handleDragLeave}
364
+ onDragEnter={handleDragEnter}
365
+ onDragOver={handleDragOver}
366
+ onDrop={handleDrop}
367
+ onDoubleClick={handleDoubleClick}
368
+ onContextMenu={handleContextMenu}
369
+ >
370
+ {/* Render toggle arrow only if the node is expandable */}
371
+ {node.hasChildren || node.children?.length ? (
372
+ renderToggle()
373
+ ) : (
374
+ <div className="w-6" />
375
+ )}
376
+ <div
377
+ className={`rounded-md border border-transparent p-0.5 hover:border-gray-300 ${
378
+ isDragOver ? "bg-sky-200" : isSelected ? "bg-blue-100" : ""
379
+ }`}
380
+ onClick={handleSelect}
381
+ >
382
+ {renderNode(node)}
383
+ </div>
384
+ </div>
385
+ );
386
+ },
387
+ );
388
+
389
+ export const PerfectTree = <T,>({
390
+ nodes,
391
+ selectedKeys = [],
392
+ expandedKeys = [],
393
+ renderNode,
394
+ onToggleExpand,
395
+ onSelect,
396
+ onDragOverZone,
397
+ onDrop,
398
+ isDragging = false,
399
+ onStartDrag,
400
+ onDragEnd,
401
+ onLazyLoad,
402
+ onDoubleClick,
403
+ onContextMenu,
404
+ enableDragAndDrop = false,
405
+ }: TreeProps<T>) => {
406
+ // When toggling a node, notify parent and trigger external lazy load if needed.
407
+ const handleToggle = useCallback(
408
+ (node: TreeNode<T>) => {
409
+ if (onToggleExpand) {
410
+ onToggleExpand(node.key);
411
+ }
412
+ // If the node is expandable and its children haven't been loaded,
413
+ // call onLazyLoad (external async loading should update the node to `null` while loading)
414
+ if (node.hasChildren && node.children === undefined && onLazyLoad) {
415
+ onLazyLoad(node);
416
+ }
417
+ },
418
+ [onToggleExpand, onLazyLoad],
419
+ );
420
+
421
+ const handleSelect = useCallback(
422
+ (nodeKey: string, event: React.MouseEvent) => {
423
+ if (onSelect) {
424
+ onSelect(nodeKey, event);
425
+ }
426
+ },
427
+ [onSelect],
428
+ );
429
+
430
+ // Global drag end handler.
431
+ const isDraggingRef = React.useRef(false);
432
+ useEffect(() => {
433
+ const handleGlobalDragEnd = (event: DragEvent) => {
434
+ if (isDraggingRef.current && onDragEnd) {
435
+ onDragEnd(event as unknown as React.DragEvent);
436
+ isDraggingRef.current = false;
437
+ }
438
+ };
439
+
440
+ document.addEventListener("dragend", handleGlobalDragEnd);
441
+ return () => {
442
+ document.removeEventListener("dragend", handleGlobalDragEnd);
443
+ };
444
+ }, [onDragEnd]);
445
+
446
+ const handleDragEnd = useCallback(
447
+ (event: React.DragEvent<HTMLDivElement>) => {
448
+ isDraggingRef.current = false;
449
+ if (onDragEnd) {
450
+ onDragEnd(event);
451
+ }
452
+ },
453
+ [onDragEnd],
454
+ );
455
+
456
+ // Recursive function to render tree nodes along with drop zones.
457
+ const renderTreeList = useCallback(
458
+ (
459
+ nodes: TreeNode<T>[],
460
+ depth: number,
461
+ parent: TreeNode<T> | null = null,
462
+ ) => {
463
+ return (
464
+ <div className="flex flex-col">
465
+ {nodes.map((node, index) => {
466
+ const children = node.children;
467
+ const isExpanded = expandedKeys.includes(node.key);
468
+ const isSelected = selectedKeys.includes(node.key);
469
+
470
+ return (
471
+ <React.Fragment key={node.key}>
472
+ <DropZone
473
+ parent={parent}
474
+ index={index}
475
+ isDragging={isDragging}
476
+ onDragOverZone={onDragOverZone}
477
+ onDrop={onDrop}
478
+ onDragEnd={onDragEnd}
479
+ />
480
+ <div
481
+ style={{
482
+ marginLeft: depth > 0 ? "21px" : undefined,
483
+ }}
484
+ className="flex flex-col"
485
+ >
486
+ <NodeContent
487
+ node={node}
488
+ isExpanded={isExpanded}
489
+ isSelected={isSelected}
490
+ onSelect={handleSelect}
491
+ onToggleNode={handleToggle}
492
+ onStartDrag={onStartDrag}
493
+ onDragEnd={handleDragEnd}
494
+ onDragOverZone={onDragOverZone}
495
+ onDrop={onDrop}
496
+ onDoubleClick={onDoubleClick}
497
+ onContextMenu={onContextMenu}
498
+ renderNode={renderNode}
499
+ enableDragAndDrop={enableDragAndDrop}
500
+ selectedKeys={selectedKeys}
501
+ isDragging={isDragging}
502
+ />
503
+ {isExpanded && (
504
+ <>
505
+ {children && children.length > 0 ? (
506
+ <div>{renderTreeList(children, depth + 1, node)}</div>
507
+ ) : null}
508
+ </>
509
+ )}
510
+ </div>
511
+ </React.Fragment>
512
+ );
513
+ })}
514
+ <DropZone
515
+ parent={parent}
516
+ index={nodes.length}
517
+ isDragging={isDragging}
518
+ onDragOverZone={onDragOverZone}
519
+ onDrop={onDrop}
520
+ onDragEnd={onDragEnd}
521
+ isLast={true}
522
+ />
523
+ </div>
524
+ );
525
+ },
526
+ [
527
+ expandedKeys,
528
+ selectedKeys,
529
+ isDragging,
530
+ onDragOverZone,
531
+ onDrop,
532
+ onDragEnd,
533
+ onStartDrag,
534
+ onDoubleClick,
535
+ handleSelect,
536
+ handleToggle,
537
+ renderNode,
538
+ ],
539
+ );
540
+
541
+ // Memoize the tree structure
542
+ const treeContent = useMemo(
543
+ () => renderTreeList(nodes, 0),
544
+ [nodes, renderTreeList],
545
+ );
546
+
547
+ return <div>{treeContent}</div>;
548
+ };
549
+
550
+ export default memo(PerfectTree);
@@ -0,0 +1,35 @@
1
+ import { classNames } from "primereact/utils";
2
+ import { useLocalStorage } from "../utils";
3
+
4
+ export function Section({
5
+ title,
6
+ children,
7
+ }: {
8
+ title: string;
9
+ children: React.ReactNode;
10
+ }) {
11
+ const [open, setOpen] = useLocalStorage("editor.showSection-" + title, true);
12
+
13
+ return (
14
+ <div>
15
+ <div
16
+ className={classNames(
17
+ open
18
+ ? "border-blue-500 bg-gray-600"
19
+ : "border-gray-400 bg-gray-400 hover:bg-gray-500 hover:border-gray-300",
20
+ "py-2.5 px-3 border-l-[8px] text-white text-xs cursor-pointer flex justify-between items-center"
21
+ )}
22
+ onClick={() => setOpen(!open)}
23
+ >
24
+ {title}
25
+ <i
26
+ className={classNames(
27
+ open ? "pi-chevron-up" : "pi-chevron-down",
28
+ "pi cursor-pointer text-xs"
29
+ )}
30
+ ></i>
31
+ </div>
32
+ {open && <div className="pt-4 pb-6 px-2 bg-gray-50">{children}</div>}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,43 @@
1
+ import { classNames } from "primereact/utils";
2
+ import { MouseEventHandler } from "react";
3
+
4
+ export function SimpleIconButton({
5
+ onClick,
6
+ className,
7
+ icon,
8
+ label,
9
+ disabled,
10
+ size,
11
+ id,
12
+ selected,
13
+ }: {
14
+ onClick: MouseEventHandler;
15
+ className?: string;
16
+ icon?: React.ReactNode;
17
+ label: string;
18
+ disabled?: boolean;
19
+ id?: string;
20
+ size?: "large" | "small";
21
+ selected?: boolean;
22
+ }) {
23
+ return (
24
+ <button
25
+ id={id}
26
+ disabled={disabled}
27
+ className={classNames(
28
+ typeof icon === "string" ? icon + " p-[6px]" : "p-[4px]",
29
+ " rounded-full",
30
+ disabled ? "text-gray-300" : " hover:bg-gray-200 cursor-pointer",
31
+ className,
32
+ size === "large" ? "text-lg" : "text-xs",
33
+ selected ? "bg-gray-200" : ""
34
+ )}
35
+ onClick={(ev) => {
36
+ if (!disabled) onClick(ev);
37
+ }}
38
+ title={label}
39
+ >
40
+ {typeof icon !== "string" && icon}
41
+ </button>
42
+ );
43
+ }
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from "react";
2
+
3
+ export type MenuItem = {
4
+ key: string;
5
+ label: string;
6
+ items?: MenuItem[];
7
+ icon?: ReactNode;
8
+ command?: () => void;
9
+ };
10
+
11
+ export function SimpleMenu({
12
+ items,
13
+ activeItemKey,
14
+ onItemClick,
15
+ }: {
16
+ items: MenuItem[];
17
+ activeItemKey: string | null;
18
+ onItemClick: (item: MenuItem) => void;
19
+ }) {
20
+ return (
21
+ <div className="flex flex-col p-1 h-full">
22
+ {items.map((item, index) => (
23
+ <div key={index} className="p-2 h-full flex flex-col gap-1">
24
+ <div className="flex flex-row items-center gap-2 border-b border-gray-200 pb-1 pl-2">
25
+ {item.icon && item.icon}
26
+ {item.label}
27
+ </div>
28
+ {item.items?.map((subItem, subIndex) => (
29
+ <div
30
+ key={subIndex}
31
+ className={`flex flex-col px-4 p-1 cursor-pointer hover:bg-gray-100 ${
32
+ activeItemKey === subItem.key ? "bg-gray-100" : ""
33
+ }`}
34
+ onClick={() => {
35
+ onItemClick(subItem);
36
+ }}
37
+ >
38
+ <div className="flex flex-row items-center gap-2 text-sm">
39
+ {subItem.icon && subItem.icon}
40
+ {subItem.label}
41
+ </div>
42
+ </div>
43
+ ))}
44
+ </div>
45
+ ))}
46
+ </div>
47
+ );
48
+ }