@alpaca-editor/core 1.0.3942 → 1.0.3944

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 (308) hide show
  1. package/.prettierrc +3 -0
  2. package/build.css +3 -0
  3. package/components.json +21 -0
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +23 -21
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/FieldActionsOverlay.js +0 -2
  8. package/dist/editor/FieldActionsOverlay.js.map +1 -1
  9. package/dist/editor/ScrollingContentTree.js +1 -1
  10. package/dist/editor/ScrollingContentTree.js.map +1 -1
  11. package/dist/editor/Titlebar.js +1 -1
  12. package/dist/editor/Titlebar.js.map +1 -1
  13. package/dist/editor/ai/GhostWriter.js +24 -3
  14. package/dist/editor/ai/GhostWriter.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +7 -7
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/field-types/InternalLinkFieldEditor.js +60 -10
  18. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  19. package/dist/editor/media-selector/MediaFolderBrowser.js +48 -1
  20. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  21. package/dist/editor/menubar/PageSelector.js +116 -65
  22. package/dist/editor/menubar/PageSelector.js.map +1 -1
  23. package/dist/editor/page-viewer/EditorForm.js +5 -2
  24. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  25. package/dist/editor/ui/ItemSearch.js +14 -8
  26. package/dist/editor/ui/ItemSearch.js.map +1 -1
  27. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  28. package/dist/editor/ui/PerfectTree.js +78 -4
  29. package/dist/editor/ui/PerfectTree.js.map +1 -1
  30. package/dist/editor/ui/Splitter.js +1 -1
  31. package/dist/revision.d.ts +2 -2
  32. package/dist/revision.js +2 -2
  33. package/dist/styles.css +8 -2
  34. package/eslint.config.mjs +4 -0
  35. package/images/bg-shape-black.webp +0 -0
  36. package/images/wizard-bg.png +0 -0
  37. package/images/wizard-tour.png +0 -0
  38. package/images/wizard.png +0 -0
  39. package/package.json +2 -8
  40. package/src/client-components/api.ts +6 -0
  41. package/src/client-components/index.ts +19 -0
  42. package/src/components/ActionButton.tsx +50 -0
  43. package/src/components/Error.tsx +57 -0
  44. package/src/components/ui/CardConnector.tsx +56 -0
  45. package/src/components/ui/button.tsx +62 -0
  46. package/src/components/ui/card.tsx +372 -0
  47. package/src/components/ui/context-menu.tsx +250 -0
  48. package/src/config/config.tsx +917 -0
  49. package/src/config/types.ts +286 -0
  50. package/src/editor/ComponentInfo.tsx +90 -0
  51. package/src/editor/ConfirmationDialog.tsx +103 -0
  52. package/src/editor/ContentTree.tsx +733 -0
  53. package/src/editor/ContextMenu.tsx +230 -0
  54. package/src/editor/Editor.tsx +90 -0
  55. package/src/editor/EditorWarning.tsx +34 -0
  56. package/src/editor/EditorWarnings.tsx +33 -0
  57. package/src/editor/FieldActionsOverlay.tsx +296 -0
  58. package/src/editor/FieldEditorPopup.tsx +65 -0
  59. package/src/editor/FieldHistory.tsx +75 -0
  60. package/src/editor/FieldList.tsx +190 -0
  61. package/src/editor/FieldListField.tsx +391 -0
  62. package/src/editor/FieldListFieldWithFallbacks.tsx +217 -0
  63. package/src/editor/FloatingToolbar.tsx +163 -0
  64. package/src/editor/ImageEditor.tsx +128 -0
  65. package/src/editor/ItemInfo.tsx +90 -0
  66. package/src/editor/LinkEditorDialog.tsx +196 -0
  67. package/src/editor/MainLayout.tsx +95 -0
  68. package/src/editor/MobileLayout.tsx +68 -0
  69. package/src/editor/NewEditorClient.tsx +11 -0
  70. package/src/editor/PictureCropper.tsx +568 -0
  71. package/src/editor/PictureEditor.tsx +301 -0
  72. package/src/editor/PictureEditorDialog.tsx +381 -0
  73. package/src/editor/PublishDialog.ignore +74 -0
  74. package/src/editor/ScrollingContentTree.tsx +68 -0
  75. package/src/editor/Terminal.tsx +227 -0
  76. package/src/editor/Titlebar.tsx +104 -0
  77. package/src/editor/ai/AiPopup.tsx +59 -0
  78. package/src/editor/ai/AiResponseMessage.tsx +106 -0
  79. package/src/editor/ai/AiTerminal.tsx +503 -0
  80. package/src/editor/ai/AiToolCall.tsx +61 -0
  81. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  82. package/src/editor/ai/GhostWriter.tsx +480 -0
  83. package/src/editor/ai/aiPageModel.ts +108 -0
  84. package/src/editor/ai/editorAiContext.ts +18 -0
  85. package/src/editor/client/AboutDialog.tsx +44 -0
  86. package/src/editor/client/EditorClient.tsx +2241 -0
  87. package/src/editor/client/GenericDialog.tsx +50 -0
  88. package/src/editor/client/editContext.ts +416 -0
  89. package/src/editor/client/helpers.ts +44 -0
  90. package/src/editor/client/itemsRepository.ts +574 -0
  91. package/src/editor/client/operations.ts +768 -0
  92. package/src/editor/client/pageModelBuilder.ts +219 -0
  93. package/src/editor/commands/commands.ts +22 -0
  94. package/src/editor/commands/componentCommands.tsx +431 -0
  95. package/src/editor/commands/createVersionCommand.ts +33 -0
  96. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  97. package/src/editor/commands/itemCommands.tsx +351 -0
  98. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  99. package/src/editor/commands/localizeItem/LocalizeItemUtils.ts +27 -0
  100. package/src/editor/commands/undo.ts +39 -0
  101. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  102. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  103. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  104. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  105. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  106. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  107. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  108. package/src/editor/component-designer/PlaceholdersEditor.tsx +179 -0
  109. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  110. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  111. package/src/editor/component-designer/aiContext.ts +23 -0
  112. package/src/editor/componentTreeHelper.tsx +116 -0
  113. package/src/editor/context-menu/CopyMoveMenu.tsx +103 -0
  114. package/src/editor/context-menu/InsertMenu.tsx +347 -0
  115. package/src/editor/control-center/About.tsx +342 -0
  116. package/src/editor/control-center/ControlCenterMenu.tsx +76 -0
  117. package/src/editor/control-center/IndexOverview.tsx +50 -0
  118. package/src/editor/control-center/IndexSettings.tsx +266 -0
  119. package/src/editor/control-center/Info.tsx +104 -0
  120. package/src/editor/control-center/QuotaInfo.tsx +301 -0
  121. package/src/editor/control-center/Status.tsx +113 -0
  122. package/src/editor/control-center/WebSocketMessages.tsx +155 -0
  123. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  124. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  125. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  126. package/src/editor/editor-warnings/NoWriteAccess.tsx +16 -0
  127. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  128. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  129. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  130. package/src/editor/field-types/DropLinkEditor.tsx +80 -0
  131. package/src/editor/field-types/DropListEditor.tsx +84 -0
  132. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  133. package/src/editor/field-types/InternalLinkFieldEditor.tsx +188 -0
  134. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  135. package/src/editor/field-types/MultiLineText.tsx +82 -0
  136. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  137. package/src/editor/field-types/RawEditor.tsx +53 -0
  138. package/src/editor/field-types/ReactQuill.tsx +580 -0
  139. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  140. package/src/editor/field-types/RichTextEditorComponent.tsx +127 -0
  141. package/src/editor/field-types/SingleLineText.tsx +174 -0
  142. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  143. package/src/editor/fieldTypes.ts +140 -0
  144. package/src/editor/media-selector/AiImageSearch.tsx +185 -0
  145. package/src/editor/media-selector/AiImageSearchPrompt.tsx +94 -0
  146. package/src/editor/media-selector/MediaFolderBrowser.tsx +321 -0
  147. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  148. package/src/editor/media-selector/Preview.tsx +14 -0
  149. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  150. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  151. package/src/editor/media-selector/UploadZone.tsx +137 -0
  152. package/src/editor/media-selector/index.ts +8 -0
  153. package/src/editor/menubar/ActionsMenu.tsx +94 -0
  154. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  155. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  156. package/src/editor/menubar/BrowseHistory.tsx +28 -0
  157. package/src/editor/menubar/ItemLanguageVersion.tsx +76 -0
  158. package/src/editor/menubar/LanguageSelector.tsx +226 -0
  159. package/src/editor/menubar/Menu.tsx +83 -0
  160. package/src/editor/menubar/NavButtons.tsx +74 -0
  161. package/src/editor/menubar/PageSelector.tsx +278 -0
  162. package/src/editor/menubar/PageViewerControls.tsx +120 -0
  163. package/src/editor/menubar/PreviewSecondaryControls.tsx +18 -0
  164. package/src/editor/menubar/SecondaryControls.tsx +45 -0
  165. package/src/editor/menubar/Separator.tsx +12 -0
  166. package/src/editor/menubar/SiteInfo.tsx +53 -0
  167. package/src/editor/menubar/User.tsx +27 -0
  168. package/src/editor/menubar/VersionSelector.tsx +142 -0
  169. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +307 -0
  170. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  171. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +59 -0
  172. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  173. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  174. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  175. package/src/editor/page-editor-chrome/FrameMenu.tsx +338 -0
  176. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  177. package/src/editor/page-editor-chrome/InlineEditor.tsx +765 -0
  178. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  179. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  180. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +122 -0
  181. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +161 -0
  182. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +169 -0
  183. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +315 -0
  184. package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +300 -0
  185. package/src/editor/page-editor-chrome/SuggestionHighlightings.tsx +40 -0
  186. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +828 -0
  187. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  188. package/src/editor/page-viewer/EditorForm.tsx +262 -0
  189. package/src/editor/page-viewer/MiniMap.tsx +362 -0
  190. package/src/editor/page-viewer/PageViewer.tsx +169 -0
  191. package/src/editor/page-viewer/PageViewerFrame.tsx +1022 -0
  192. package/src/editor/page-viewer/pageModelSkeletonBuilder.ts +412 -0
  193. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  194. package/src/editor/pageModel.ts +220 -0
  195. package/src/editor/picture-shared.tsx +53 -0
  196. package/src/editor/reviews/Comment.tsx +308 -0
  197. package/src/editor/reviews/Comments.tsx +125 -0
  198. package/src/editor/reviews/DiffView.tsx +109 -0
  199. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  200. package/src/editor/reviews/Reviews.tsx +280 -0
  201. package/src/editor/reviews/SuggestedEdit.tsx +316 -0
  202. package/src/editor/reviews/reviewCommands.tsx +47 -0
  203. package/src/editor/reviews/useReviews.tsx +70 -0
  204. package/src/editor/services/aiService.ts +173 -0
  205. package/src/editor/services/componentDesignerService.ts +151 -0
  206. package/src/editor/services/contentService.ts +180 -0
  207. package/src/editor/services/editService.ts +488 -0
  208. package/src/editor/services/indexService.ts +24 -0
  209. package/src/editor/services/reviewsService.ts +53 -0
  210. package/src/editor/services/serviceHelper.ts +95 -0
  211. package/src/editor/services/suggestedEditsService.ts +39 -0
  212. package/src/editor/services/systemService.ts +5 -0
  213. package/src/editor/services/translationService.ts +21 -0
  214. package/src/editor/services-server/api.ts +150 -0
  215. package/src/editor/services-server/graphQL.ts +106 -0
  216. package/src/editor/sidebar/ComponentPalette.tsx +161 -0
  217. package/src/editor/sidebar/ComponentTree.tsx +549 -0
  218. package/src/editor/sidebar/Debug.tsx +111 -0
  219. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  220. package/src/editor/sidebar/EditHistory.tsx +134 -0
  221. package/src/editor/sidebar/GraphQL.tsx +164 -0
  222. package/src/editor/sidebar/Insert.tsx +35 -0
  223. package/src/editor/sidebar/MainContentTree.tsx +102 -0
  224. package/src/editor/sidebar/Performance.tsx +53 -0
  225. package/src/editor/sidebar/Sessions.tsx +35 -0
  226. package/src/editor/sidebar/Sidebar.tsx +20 -0
  227. package/src/editor/sidebar/SidebarView.tsx +152 -0
  228. package/src/editor/sidebar/Translations.tsx +295 -0
  229. package/src/editor/sidebar/Validation.tsx +102 -0
  230. package/src/editor/sidebar/ViewSelector.tsx +60 -0
  231. package/src/editor/sidebar/Workbox.tsx +209 -0
  232. package/src/editor/ui/CenteredMessage.tsx +7 -0
  233. package/src/editor/ui/CopyMoveTargetSelectorDialog.tsx +81 -0
  234. package/src/editor/ui/CopyToClipboardButton.tsx +24 -0
  235. package/src/editor/ui/DialogButtons.tsx +11 -0
  236. package/src/editor/ui/Icons.tsx +709 -0
  237. package/src/editor/ui/ItemList.tsx +76 -0
  238. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  239. package/src/editor/ui/ItemSearch.tsx +159 -0
  240. package/src/editor/ui/PerfectTree.tsx +676 -0
  241. package/src/editor/ui/Section.tsx +35 -0
  242. package/src/editor/ui/SimpleIconButton.tsx +54 -0
  243. package/src/editor/ui/SimpleMenu.tsx +40 -0
  244. package/src/editor/ui/SimpleTable.tsx +60 -0
  245. package/src/editor/ui/SimpleTabs.tsx +60 -0
  246. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  247. package/src/editor/ui/Spinner.tsx +9 -0
  248. package/src/editor/ui/Splitter.tsx +420 -0
  249. package/src/editor/ui/StackedPanels.tsx +134 -0
  250. package/src/editor/ui/Toolbar.tsx +7 -0
  251. package/src/editor/utils/id-helper.ts +3 -0
  252. package/src/editor/utils/insertOptions.ts +69 -0
  253. package/src/editor/utils/itemutils.ts +29 -0
  254. package/src/editor/utils/useMemoDebug.ts +28 -0
  255. package/src/editor/utils.ts +486 -0
  256. package/src/editor/views/CompareView.tsx +245 -0
  257. package/src/editor/views/EditView.tsx +27 -0
  258. package/src/editor/views/ItemEditor.tsx +58 -0
  259. package/src/editor/views/MediaFolderEditView.tsx +66 -0
  260. package/src/editor/views/SingleEditView.tsx +57 -0
  261. package/src/fonts/Geist-Black.woff2 +0 -0
  262. package/src/fonts/Geist-Bold.woff2 +0 -0
  263. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  264. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  265. package/src/fonts/Geist-Light.woff2 +0 -0
  266. package/src/fonts/Geist-Medium.woff2 +0 -0
  267. package/src/fonts/Geist-Regular.woff2 +0 -0
  268. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  269. package/src/fonts/Geist-Thin.woff2 +0 -0
  270. package/src/fonts/Geist[wght].woff2 +0 -0
  271. package/src/fonts/index.ts +10 -0
  272. package/src/index.ts +23 -0
  273. package/src/lib/safelist.tsx +16 -0
  274. package/src/lib/utils.ts +6 -0
  275. package/src/page-wizard/PageWizard.tsx +139 -0
  276. package/src/page-wizard/WizardBox.tsx +4 -0
  277. package/src/page-wizard/WizardBoxConnector.tsx +56 -0
  278. package/src/page-wizard/WizardSteps.tsx +458 -0
  279. package/src/page-wizard/service.ts +35 -0
  280. package/src/page-wizard/startPageWizardCommand.ts +26 -0
  281. package/src/page-wizard/steps/BuildPageStep.tsx +259 -0
  282. package/src/page-wizard/steps/CollectStep.tsx +296 -0
  283. package/src/page-wizard/steps/ComponentTypesSelector.tsx +454 -0
  284. package/src/page-wizard/steps/Components.tsx +193 -0
  285. package/src/page-wizard/steps/ContentStep.tsx +890 -0
  286. package/src/page-wizard/steps/EditButton.tsx +34 -0
  287. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  288. package/src/page-wizard/steps/Generate.tsx +60 -0
  289. package/src/page-wizard/steps/ImagesStep.tsx +382 -0
  290. package/src/page-wizard/steps/LayoutStep.tsx +227 -0
  291. package/src/page-wizard/steps/MetaDataStep.tsx +173 -0
  292. package/src/page-wizard/steps/SelectStep.tsx +281 -0
  293. package/src/page-wizard/steps/schema.ts +180 -0
  294. package/src/page-wizard/steps/usePageCreator.ts +325 -0
  295. package/src/page-wizard/usePageWizard.ts +79 -0
  296. package/src/revision.ts +2 -0
  297. package/src/splash-screen/NewPage.tsx +294 -0
  298. package/src/splash-screen/OpenPage.tsx +113 -0
  299. package/src/splash-screen/RecentPages.tsx +123 -0
  300. package/src/splash-screen/SectionHeadline.tsx +21 -0
  301. package/src/splash-screen/SplashScreen.tsx +195 -0
  302. package/src/tour/Tour.tsx +566 -0
  303. package/src/tour/default-tour.tsx +301 -0
  304. package/src/tour/preview-tour.tsx +128 -0
  305. package/src/types.ts +335 -0
  306. package/styles.css +765 -0
  307. package/tsconfig.build.json +31 -0
  308. package/tsconfig.json +14 -0
@@ -0,0 +1,676 @@
1
+ import React, { useEffect, useMemo, useCallback, memo, useRef } 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?: React.ReactNode;
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
+ isValidDropZone?: (parent: TreeNode<T> | null, index: number) => boolean;
43
+ /**
44
+ * Called when an item is dropped into a drop zone between nodes.
45
+ * @param parent The parent node of the current list (null for root level)
46
+ * @param index The position where a new node should be inserted (0 = before first node)
47
+ */
48
+ onDrop?: (
49
+ parent: TreeNode<T> | null,
50
+ index: number,
51
+ event: React.DragEvent,
52
+ ) => void;
53
+ isDragging?: boolean;
54
+ onStartDrag?: (data: {
55
+ node: TreeNode<T>;
56
+ event: React.DragEvent<any>;
57
+ isMultiSelect: boolean;
58
+ }) => void;
59
+ onDragEnd?: (event: React.DragEvent | null) => void;
60
+ enableDragAndDrop?: boolean;
61
+
62
+ /**
63
+ * Callback that notifies when a lazy load should occur.
64
+ * When a node is toggled and its children haven't been loaded yet (i.e. undefined),
65
+ * this callback is invoked so you can load the children asynchronously and update your tree model.
66
+ */
67
+ onLazyLoad?: (node: TreeNode<T>) => void;
68
+ onDoubleClick?: (node: TreeNode<T>) => void;
69
+ onContextMenu?: (node: TreeNode<T>, event: React.MouseEvent) => void;
70
+ /** Whether to automatically scroll to the first selected node when selection changes */
71
+ scrollToSelected?: boolean;
72
+ }
73
+
74
+ // Local DropZone component to handle drag-over state.
75
+ const DropZone = memo(
76
+ ({
77
+ parent,
78
+ index,
79
+ isDragging,
80
+ onDragOverZone,
81
+ onDrop,
82
+ onDragEnd,
83
+ isLast,
84
+ isValidDropZone,
85
+ }: {
86
+ parent: TreeNode<any> | null;
87
+ index: number;
88
+ isDragging: boolean;
89
+ onDragOverZone?: (
90
+ parent: TreeNode<any> | null,
91
+ index: number,
92
+ e: React.DragEvent,
93
+ ) => boolean;
94
+ onDrop?: (
95
+ parent: TreeNode<any> | null,
96
+ index: number,
97
+ e: React.DragEvent,
98
+ ) => void;
99
+ onDragEnd?: (event: React.DragEvent | null) => void;
100
+ isLast?: boolean;
101
+ isValidDropZone?: (parent: TreeNode<any> | null, index: number) => boolean;
102
+ }) => {
103
+ const [isDragOver, setIsDragOver] = React.useState(false);
104
+ const [isValidDrop, setIsValidDrop] = React.useState(true);
105
+
106
+ useEffect(() => {
107
+ if (isDragging) {
108
+ if (isValidDropZone) {
109
+ const isValid = isValidDropZone(parent, index);
110
+ setIsValidDrop(isValid);
111
+ } else {
112
+ setIsValidDrop(true);
113
+ }
114
+ }
115
+ }, [isValidDropZone, parent, index, isDragging]);
116
+
117
+ const handleDragEnter = useCallback(
118
+ (e: React.DragEvent<HTMLDivElement>) => {
119
+ e.preventDefault();
120
+ e.stopPropagation();
121
+ if (onDragOverZone) {
122
+ const allowed = onDragOverZone(parent, index, e);
123
+ setIsDragOver(allowed);
124
+ // e.dataTransfer.dropEffect = allowed ? "move" : "none";
125
+ }
126
+ },
127
+ [onDragOverZone, parent, index],
128
+ );
129
+
130
+ const handleDragLeave = useCallback(
131
+ (e: React.DragEvent<HTMLDivElement>) => {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ setIsDragOver(false);
135
+ },
136
+ [],
137
+ );
138
+
139
+ const handleDragOver = useCallback(
140
+ (e: React.DragEvent<HTMLDivElement>) => {
141
+ e.preventDefault();
142
+ e.stopPropagation();
143
+ if (onDragOverZone) {
144
+ const allowed = onDragOverZone(parent, index, e);
145
+ setIsDragOver(allowed);
146
+ }
147
+ },
148
+ [onDragOverZone, parent, index],
149
+ );
150
+
151
+ const handleDrop = useCallback(
152
+ (e: React.DragEvent<HTMLDivElement>) => {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ setIsDragOver(false);
156
+ if (onDrop) {
157
+ onDrop(parent, index, e);
158
+ }
159
+ },
160
+ [onDrop, parent, index],
161
+ );
162
+
163
+ if (!isDragging || !isValidDrop) return null;
164
+
165
+ return (
166
+ <div className={`relative ${isLast ? "h-3" : ""}`}>
167
+ <div
168
+ className={`drop-zone absolute top-[-5px] right-0 left-[45px] z-1000 h-3 rounded-md transition-colors duration-100 ${
169
+ isDragOver ? "bg-sky-200" : ""
170
+ }`}
171
+ onDragEnter={handleDragEnter}
172
+ onDragOver={handleDragOver}
173
+ onDrop={handleDrop}
174
+ onDragLeave={handleDragLeave}
175
+ />
176
+ </div>
177
+ );
178
+ },
179
+ );
180
+
181
+ // NodeContent component extracted and memoized
182
+ const NodeContent = memo(
183
+ ({
184
+ node,
185
+ isExpanded,
186
+ isSelected,
187
+ onSelect,
188
+ onToggleNode,
189
+ onStartDrag,
190
+ onDragEnd,
191
+ onDragOverZone,
192
+ onDrop,
193
+ onDoubleClick,
194
+ renderNode,
195
+ onContextMenu,
196
+ enableDragAndDrop = false,
197
+ selectedKeys,
198
+ isDragging,
199
+ }: {
200
+ node: TreeNode<any>;
201
+ isExpanded: boolean;
202
+ isSelected: boolean;
203
+ onSelect: (nodeKey: string, e: React.MouseEvent) => void;
204
+ onToggleNode: (node: TreeNode<any>) => void;
205
+ onStartDrag?: (data: {
206
+ node: TreeNode<any>;
207
+ event: React.DragEvent<any>;
208
+ isMultiSelect: boolean;
209
+ }) => void;
210
+ onDragEnd?: (event: React.DragEvent<any>) => void;
211
+ onDragOverZone?: (
212
+ parent: TreeNode<any> | null,
213
+ index: number,
214
+ e: React.DragEvent<any>,
215
+ ) => boolean;
216
+ onDrop?: (
217
+ parent: TreeNode<any> | null,
218
+ index: number,
219
+ e: React.DragEvent<any>,
220
+ ) => void;
221
+ onDoubleClick?: (node: TreeNode<any>) => void;
222
+ onContextMenu?: (node: TreeNode<any>, event: React.MouseEvent) => void;
223
+ renderNode: (node: TreeNode<any>) => React.ReactNode;
224
+ enableDragAndDrop?: boolean;
225
+ selectedKeys?: string[];
226
+ isDragging: boolean;
227
+ }) => {
228
+ const [isDragOver, setIsDragOver] = React.useState(false);
229
+
230
+ useEffect(() => {
231
+ if (!isDragging) {
232
+ setIsDragOver(false);
233
+ }
234
+ }, [isDragging]);
235
+
236
+ const handleDragStart = useCallback(
237
+ (event: React.DragEvent<any>) => {
238
+ const isMultiSelect =
239
+ isSelected && selectedKeys && selectedKeys.length > 1;
240
+
241
+ // Set drag preview for multiple items if applicable
242
+ if (isMultiSelect) {
243
+ // Create custom drag image showing count of selected items
244
+ const dragPreview = document.createElement("div");
245
+ dragPreview.className =
246
+ "bg-white shadow-md rounded-md border border-gray-300 px-2 py-1 text-sm max-w-24 absolute top-[-1000px] left-[-1000px]";
247
+ dragPreview.innerHTML = `<div class="flex items-center"><span class="font-bold mr-1">${selectedKeys.length}</span> items</div>`;
248
+ document.body.appendChild(dragPreview);
249
+
250
+ // Set custom drag image
251
+ event.dataTransfer.setDragImage(dragPreview, 15, 15);
252
+ event.dataTransfer.dropEffect = "move";
253
+
254
+ // Remove the element after drag starts
255
+ setTimeout(() => {
256
+ document.body.removeChild(dragPreview);
257
+ }, 0);
258
+ }
259
+ if (onStartDrag) {
260
+ onStartDrag({ node, event, isMultiSelect: isMultiSelect ?? false });
261
+ }
262
+ },
263
+ [node, onStartDrag],
264
+ );
265
+
266
+ const handleDragLeave = useCallback((event: React.DragEvent<any>) => {
267
+ event.preventDefault();
268
+ setIsDragOver(false);
269
+ }, []);
270
+
271
+ const handleDragEnter = useCallback(
272
+ (event: React.DragEvent<any>) => {
273
+ event.preventDefault();
274
+ if (onDragOverZone) {
275
+ const allowed = onDragOverZone(node, -1, event);
276
+ setIsDragOver(allowed);
277
+ }
278
+ },
279
+ [node, onDragOverZone],
280
+ );
281
+
282
+ const handleDragOver = useCallback(
283
+ (event: React.DragEvent<any>) => {
284
+ event.preventDefault();
285
+ if (onDragOverZone) {
286
+ const allowed = onDragOverZone(node, -1, event);
287
+ setIsDragOver(allowed);
288
+ if (!allowed) {
289
+ event.dataTransfer.dropEffect = "none";
290
+ }
291
+ }
292
+ },
293
+ [node, onDragOverZone],
294
+ );
295
+
296
+ const handleDrop = useCallback(
297
+ (e: React.DragEvent<any>) => {
298
+ console.log("Dropping", node, -1, e);
299
+ e.preventDefault();
300
+ e.stopPropagation();
301
+ if (onDrop) {
302
+ onDrop(node, -1, e);
303
+ }
304
+ },
305
+ [node, onDrop],
306
+ );
307
+
308
+ const handleDoubleClick = useCallback(
309
+ (e: React.MouseEvent<any>) => {
310
+ e.stopPropagation();
311
+ onDoubleClick?.(node);
312
+ },
313
+ [node, onDoubleClick],
314
+ );
315
+
316
+ const handleSelect = useCallback(
317
+ (e: React.MouseEvent<any>) => {
318
+ e.stopPropagation();
319
+ onSelect(node.key, e);
320
+ },
321
+ [node.key, onSelect],
322
+ );
323
+
324
+ const handleToggle = useCallback(
325
+ (e: React.MouseEvent<any>) => {
326
+ e.stopPropagation();
327
+ onToggleNode(node);
328
+ },
329
+ [node, onToggleNode],
330
+ );
331
+
332
+ const renderToggle = () => {
333
+ if (node.hasChildren && node.children === null) {
334
+ return (
335
+ <ProgressSpinner
336
+ style={{ width: "16px", height: "16px" }}
337
+ className="text-gray-500"
338
+ />
339
+ );
340
+ }
341
+
342
+ return (
343
+ <span
344
+ onClick={handleToggle}
345
+ className={`ml-0.5 inline-block transform cursor-pointer text-gray-500 transition duration-150 select-none ${
346
+ isExpanded ? "rotate-90" : "rotate-0"
347
+ }`}
348
+ >
349
+ <svg
350
+ width="14"
351
+ height="14"
352
+ viewBox="0 0 14 14"
353
+ fill="none"
354
+ xmlns="http://www.w3.org/2000/svg"
355
+ aria-hidden="true"
356
+ data-pc-section="togglericon"
357
+ >
358
+ <path
359
+ 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"
360
+ fill="currentColor"
361
+ ></path>
362
+ </svg>
363
+ </span>
364
+ );
365
+ };
366
+
367
+ const handleContextMenu = useCallback(
368
+ (e: React.MouseEvent<any>) => {
369
+ e.stopPropagation();
370
+ e.preventDefault();
371
+ onContextMenu?.(node, e);
372
+ },
373
+ [node, onContextMenu],
374
+ );
375
+
376
+ return (
377
+ <div
378
+ className="tree-node mb-0.5 flex cursor-pointer items-center gap-0.5"
379
+ draggable={enableDragAndDrop}
380
+ onClick={handleSelect}
381
+ onDragStart={(event) => handleDragStart(event)}
382
+ onDragEnd={onDragEnd as any}
383
+ onDragLeave={handleDragLeave}
384
+ onDragEnter={handleDragEnter}
385
+ onDragOver={handleDragOver}
386
+ onDrop={handleDrop}
387
+ onDoubleClick={handleDoubleClick}
388
+ onContextMenu={handleContextMenu}
389
+ data-node-key={node.key}
390
+ data-selected={isSelected}
391
+ >
392
+ {/* Render toggle arrow only if the node is expandable */}
393
+ {node.hasChildren || node.children?.length ? (
394
+ renderToggle()
395
+ ) : (
396
+ <div className="w-[16px]" />
397
+ )}
398
+ <div
399
+ className={`flex-1 rounded-md border border-transparent p-0.5 pr-1.5 hover:border-gray-300 ${
400
+ isDragOver ? "bg-sky-200" : isSelected ? "bg-blue-100" : ""
401
+ }`}
402
+ onClick={handleSelect}
403
+ >
404
+ {renderNode(node)}
405
+ </div>
406
+ </div>
407
+ );
408
+ },
409
+ );
410
+
411
+ export const PerfectTree = <T,>({
412
+ nodes,
413
+ selectedKeys = [],
414
+ expandedKeys = [],
415
+ renderNode,
416
+ onToggleExpand,
417
+ onSelect,
418
+ onDragOverZone,
419
+ onDrop,
420
+ isDragging = false,
421
+ onStartDrag,
422
+ onDragEnd,
423
+ onLazyLoad,
424
+ onDoubleClick,
425
+ onContextMenu,
426
+ enableDragAndDrop = false,
427
+ isValidDropZone,
428
+ scrollToSelected = false,
429
+ }: TreeProps<T>) => {
430
+ // When toggling a node, notify parent and trigger external lazy load if needed.
431
+ const handleToggle = useCallback(
432
+ (node: TreeNode<T>) => {
433
+ if (onToggleExpand) {
434
+ onToggleExpand(node.key);
435
+ }
436
+ // If the node is expandable and its children haven't been loaded,
437
+ // call onLazyLoad (external async loading should update the node to `null` while loading)
438
+ if (node.hasChildren && node.children === undefined && onLazyLoad) {
439
+ onLazyLoad(node);
440
+ }
441
+ },
442
+ [onToggleExpand, onLazyLoad],
443
+ );
444
+
445
+ const handleSelect = useCallback(
446
+ (nodeKey: string, event: React.MouseEvent) => {
447
+ if (onSelect) {
448
+ onSelect(nodeKey, event);
449
+ }
450
+ },
451
+ [onSelect],
452
+ );
453
+
454
+ // Global drag end handler.
455
+ const isDraggingRef = React.useRef(false);
456
+ useEffect(() => {
457
+ const handleGlobalDragEnd = (event: DragEvent) => {
458
+ if (isDraggingRef.current && onDragEnd) {
459
+ onDragEnd(event as unknown as React.DragEvent);
460
+ isDraggingRef.current = false;
461
+ }
462
+ };
463
+
464
+ document.addEventListener("dragend", handleGlobalDragEnd);
465
+ return () => {
466
+ document.removeEventListener("dragend", handleGlobalDragEnd);
467
+ };
468
+ }, [onDragEnd]);
469
+
470
+ const handleDragEnd = useCallback(
471
+ (event: React.DragEvent<HTMLDivElement>) => {
472
+ isDraggingRef.current = false;
473
+ if (onDragEnd) {
474
+ onDragEnd(event);
475
+ }
476
+ },
477
+ [onDragEnd],
478
+ );
479
+
480
+ // Scroll to selected node when scrollToSelected is enabled and selectedKeys change
481
+ const treeRef = useRef<HTMLDivElement>(null);
482
+ useEffect(() => {
483
+ if (scrollToSelected && selectedKeys.length > 0 && treeRef.current) {
484
+ const timeoutId = setTimeout(() => {
485
+ const treeContainer = treeRef.current;
486
+ if (!treeContainer) return;
487
+
488
+ // Try multiple selection strategies
489
+ let selectedNode: HTMLElement | null = null;
490
+
491
+ // Strategy 1: Use data-selected attribute
492
+ selectedNode = treeContainer.querySelector(
493
+ '[data-selected="true"]',
494
+ ) as HTMLElement;
495
+
496
+ // Strategy 2: Use bg-blue-100 class
497
+ if (!selectedNode) {
498
+ selectedNode = treeContainer.querySelector(
499
+ ".bg-blue-100",
500
+ ) as HTMLElement;
501
+ }
502
+
503
+ // Strategy 3: Use data-node-key attribute
504
+ if (!selectedNode && selectedKeys.length > 0) {
505
+ const nodeKey = selectedKeys[0];
506
+ if (nodeKey) {
507
+ selectedNode = treeContainer.querySelector(
508
+ `[data-node-key="${nodeKey}"]`,
509
+ ) as HTMLElement;
510
+
511
+ // Fallback: CSS.escape for special characters
512
+ if (!selectedNode) {
513
+ try {
514
+ const escapedKey = CSS.escape(nodeKey);
515
+ selectedNode = treeContainer.querySelector(
516
+ `[data-node-key="${escapedKey}"]`,
517
+ ) as HTMLElement;
518
+ } catch (e) {
519
+ // Silently fail if CSS.escape doesn't work
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ if (selectedNode) {
526
+ // Find the scrollable parent container
527
+ let scrollContainer = selectedNode.closest(
528
+ ".overflow-auto",
529
+ ) as HTMLElement;
530
+
531
+ if (!scrollContainer) {
532
+ scrollContainer = selectedNode.closest(
533
+ '[style*="overflow"]',
534
+ ) as HTMLElement;
535
+ }
536
+
537
+ if (!scrollContainer) {
538
+ scrollContainer = treeContainer.parentElement as HTMLElement;
539
+ }
540
+
541
+ if (scrollContainer) {
542
+ const containerRect = scrollContainer.getBoundingClientRect();
543
+ const nodeRect = selectedNode.getBoundingClientRect();
544
+
545
+ // Check if the node is already visible
546
+ const isVisible =
547
+ nodeRect.top >= containerRect.top &&
548
+ nodeRect.bottom <= containerRect.bottom;
549
+
550
+ if (!isVisible) {
551
+ const scrollTop = scrollContainer.scrollTop;
552
+ const containerTop = containerRect.top;
553
+ const nodeTop = nodeRect.top;
554
+ const offset = nodeTop - containerTop;
555
+
556
+ const newScrollTop =
557
+ scrollTop +
558
+ offset -
559
+ containerRect.height / 2 +
560
+ nodeRect.height / 2;
561
+
562
+ scrollContainer.scrollTo({
563
+ top: newScrollTop,
564
+ behavior: "smooth",
565
+ });
566
+ }
567
+ } else {
568
+ selectedNode.scrollIntoView({
569
+ behavior: "smooth",
570
+ block: "nearest",
571
+ });
572
+ }
573
+ }
574
+ }, 300);
575
+
576
+ return () => clearTimeout(timeoutId);
577
+ }
578
+ }, [scrollToSelected, selectedKeys, nodes]);
579
+
580
+ // Recursive function to render tree nodes along with drop zones.
581
+ const renderTreeList = useCallback(
582
+ (
583
+ nodes: TreeNode<T>[],
584
+ depth: number,
585
+ parent: TreeNode<T> | null = null,
586
+ ) => {
587
+ return (
588
+ <div className="flex flex-col">
589
+ {nodes.map((node, index) => {
590
+ const children = node.children;
591
+ const isExpanded = expandedKeys.includes(node.key);
592
+ const isSelected = selectedKeys.includes(node.key);
593
+
594
+ return (
595
+ <React.Fragment key={node.key}>
596
+ <DropZone
597
+ parent={parent}
598
+ index={index}
599
+ isDragging={isDragging}
600
+ onDragOverZone={onDragOverZone}
601
+ onDrop={onDrop}
602
+ onDragEnd={onDragEnd}
603
+ isValidDropZone={isValidDropZone}
604
+ />
605
+ <div
606
+ style={{
607
+ marginLeft: depth > 0 ? "21px" : undefined,
608
+ }}
609
+ className="flex flex-col"
610
+ >
611
+ <NodeContent
612
+ node={node}
613
+ isExpanded={isExpanded}
614
+ isSelected={isSelected}
615
+ onSelect={handleSelect}
616
+ onToggleNode={handleToggle}
617
+ onStartDrag={onStartDrag}
618
+ onDragEnd={handleDragEnd}
619
+ onDragOverZone={onDragOverZone}
620
+ onDrop={onDrop}
621
+ onDoubleClick={onDoubleClick}
622
+ onContextMenu={onContextMenu}
623
+ renderNode={renderNode}
624
+ enableDragAndDrop={enableDragAndDrop}
625
+ selectedKeys={selectedKeys}
626
+ isDragging={isDragging}
627
+ />
628
+ {isExpanded && (
629
+ <>
630
+ {children && children.length > 0 ? (
631
+ <div>{renderTreeList(children, depth + 1, node)}</div>
632
+ ) : null}
633
+ </>
634
+ )}
635
+ </div>
636
+ </React.Fragment>
637
+ );
638
+ })}
639
+ <DropZone
640
+ parent={parent}
641
+ index={nodes.length}
642
+ isDragging={isDragging}
643
+ onDragOverZone={onDragOverZone}
644
+ onDrop={onDrop}
645
+ onDragEnd={onDragEnd}
646
+ isLast={true}
647
+ isValidDropZone={isValidDropZone}
648
+ />
649
+ </div>
650
+ );
651
+ },
652
+ [
653
+ expandedKeys,
654
+ selectedKeys,
655
+ isDragging,
656
+ onDragOverZone,
657
+ onDrop,
658
+ onDragEnd,
659
+ onStartDrag,
660
+ onDoubleClick,
661
+ handleSelect,
662
+ handleToggle,
663
+ renderNode,
664
+ ],
665
+ );
666
+
667
+ // Memoize the tree structure
668
+ const treeContent = useMemo(
669
+ () => renderTreeList(nodes, 0),
670
+ [nodes, renderTreeList],
671
+ );
672
+
673
+ return <div ref={treeRef}>{treeContent}</div>;
674
+ };
675
+
676
+ 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:border-gray-300 hover:bg-gray-500",
20
+ "flex cursor-pointer items-center justify-between border-l-[8px] px-3 py-2 text-xs text-white",
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="bg-gray-50 px-2 pt-4 pb-6">{children}</div>}
33
+ </div>
34
+ );
35
+ }