@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.
- package/.prettierrc +3 -0
- package/eslint.config.mjs +4 -0
- package/images/bg-shape-black.webp +0 -0
- package/package.json +52 -0
- package/src/client-components/api.ts +6 -0
- package/src/client-components/index.ts +19 -0
- package/src/components/ActionButton.tsx +43 -0
- package/src/components/Error.tsx +57 -0
- package/src/config/config.tsx +737 -0
- package/src/config/types.ts +263 -0
- package/src/editor/ComponentInfo.tsx +77 -0
- package/src/editor/ConfirmationDialog.tsx +103 -0
- package/src/editor/ContentTree.tsx +654 -0
- package/src/editor/ContextMenu.tsx +155 -0
- package/src/editor/Editor.tsx +91 -0
- package/src/editor/EditorWarning.tsx +34 -0
- package/src/editor/EditorWarnings.tsx +33 -0
- package/src/editor/FieldEditorPopup.tsx +65 -0
- package/src/editor/FieldHistory.tsx +74 -0
- package/src/editor/FieldList.tsx +190 -0
- package/src/editor/FieldListField.tsx +387 -0
- package/src/editor/FieldListFieldWithFallbacks.tsx +211 -0
- package/src/editor/FloatingToolbar.tsx +163 -0
- package/src/editor/ImageEditor.tsx +129 -0
- package/src/editor/InsertMenu.tsx +332 -0
- package/src/editor/ItemInfo.tsx +90 -0
- package/src/editor/LinkEditorDialog.tsx +192 -0
- package/src/editor/MainLayout.tsx +94 -0
- package/src/editor/NewEditorClient.tsx +11 -0
- package/src/editor/PictureCropper.tsx +505 -0
- package/src/editor/PictureEditor.tsx +206 -0
- package/src/editor/PictureEditorDialog.tsx +381 -0
- package/src/editor/PublishDialog.ignore +74 -0
- package/src/editor/ScrollingContentTree.tsx +47 -0
- package/src/editor/Terminal.tsx +215 -0
- package/src/editor/Titlebar.tsx +23 -0
- package/src/editor/ai/AiPopup.tsx +59 -0
- package/src/editor/ai/AiResponseMessage.tsx +82 -0
- package/src/editor/ai/AiTerminal.tsx +450 -0
- package/src/editor/ai/AiToolCall.tsx +46 -0
- package/src/editor/ai/EditorAiTerminal.tsx +20 -0
- package/src/editor/ai/editorAiContext.ts +18 -0
- package/src/editor/client/DialogContext.tsx +49 -0
- package/src/editor/client/EditorClient.tsx +1831 -0
- package/src/editor/client/GenericDialog.tsx +50 -0
- package/src/editor/client/editContext.ts +330 -0
- package/src/editor/client/helpers.ts +44 -0
- package/src/editor/client/itemsRepository.ts +391 -0
- package/src/editor/client/operations.ts +610 -0
- package/src/editor/client/pageModelBuilder.ts +182 -0
- package/src/editor/commands/commands.ts +23 -0
- package/src/editor/commands/componentCommands.tsx +408 -0
- package/src/editor/commands/createVersionCommand.ts +33 -0
- package/src/editor/commands/deleteVersionCommand.ts +71 -0
- package/src/editor/commands/itemCommands.tsx +186 -0
- package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
- package/src/editor/commands/undo.ts +39 -0
- package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
- package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
- package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
- package/src/editor/component-designer/ComponentEditor.tsx +97 -0
- package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
- package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
- package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
- package/src/editor/component-designer/PlaceholdersEditor.tsx +183 -0
- package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
- package/src/editor/component-designer/TemplateEditor.tsx +236 -0
- package/src/editor/component-designer/aiContext.ts +23 -0
- package/src/editor/componentTreeHelper.tsx +114 -0
- package/src/editor/control-center/ControlCenterMenu.tsx +71 -0
- package/src/editor/control-center/IndexOverview.tsx +50 -0
- package/src/editor/control-center/IndexSettings.tsx +266 -0
- package/src/editor/control-center/Status.tsx +7 -0
- package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
- package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
- package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
- package/src/editor/editor-warnings/NoWriteAccess.tsx +15 -0
- package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
- package/src/editor/field-types/AttachmentEditor.tsx +9 -0
- package/src/editor/field-types/CheckboxEditor.tsx +47 -0
- package/src/editor/field-types/DropLinkEditor.tsx +75 -0
- package/src/editor/field-types/DropListEditor.tsx +84 -0
- package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +112 -0
- package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
- package/src/editor/field-types/MultiLineText.tsx +63 -0
- package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
- package/src/editor/field-types/RawEditor.tsx +53 -0
- package/src/editor/field-types/ReactQuill.tsx +580 -0
- package/src/editor/field-types/RichTextEditor.tsx +22 -0
- package/src/editor/field-types/RichTextEditorComponent.tsx +108 -0
- package/src/editor/field-types/SingleLineText.tsx +150 -0
- package/src/editor/field-types/TreeListEditor.tsx +261 -0
- package/src/editor/fieldTypes.ts +140 -0
- package/src/editor/media-selector/AiImageSearch.tsx +186 -0
- package/src/editor/media-selector/AiImageSearchPrompt.tsx +95 -0
- package/src/editor/media-selector/MediaSelector.tsx +42 -0
- package/src/editor/media-selector/Preview.tsx +14 -0
- package/src/editor/media-selector/Thumbnails.tsx +48 -0
- package/src/editor/media-selector/TreeSelector.tsx +292 -0
- package/src/editor/media-selector/UploadZone.tsx +137 -0
- package/src/editor/menubar/ActionsMenu.tsx +47 -0
- package/src/editor/menubar/ActiveUsers.tsx +17 -0
- package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
- package/src/editor/menubar/BrowseHistory.tsx +37 -0
- package/src/editor/menubar/ItemLanguageVersion.tsx +52 -0
- package/src/editor/menubar/LanguageSelector.tsx +152 -0
- package/src/editor/menubar/Menu.tsx +83 -0
- package/src/editor/menubar/NavButtons.tsx +74 -0
- package/src/editor/menubar/PageSelector.tsx +139 -0
- package/src/editor/menubar/PageViewerControls.tsx +99 -0
- package/src/editor/menubar/Separator.tsx +12 -0
- package/src/editor/menubar/SiteInfo.tsx +53 -0
- package/src/editor/menubar/User.tsx +27 -0
- package/src/editor/menubar/VersionSelector.tsx +143 -0
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +287 -0
- package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
- package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +44 -0
- package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
- package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
- package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
- package/src/editor/page-editor-chrome/FrameMenu.tsx +263 -0
- package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
- package/src/editor/page-editor-chrome/InlineEditor.tsx +147 -0
- package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
- package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
- package/src/editor/page-editor-chrome/PageEditorChrome.tsx +119 -0
- package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +154 -0
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +171 -0
- package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +233 -0
- package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
- package/src/editor/page-viewer/EditorForm.tsx +247 -0
- package/src/editor/page-viewer/MiniMap.tsx +351 -0
- package/src/editor/page-viewer/PageViewer.tsx +127 -0
- package/src/editor/page-viewer/PageViewerFrame.tsx +1030 -0
- package/src/editor/page-viewer/pageViewContext.ts +186 -0
- package/src/editor/pageModel.ts +191 -0
- package/src/editor/picture-shared.tsx +53 -0
- package/src/editor/reviews/Comment.tsx +265 -0
- package/src/editor/reviews/Comments.tsx +50 -0
- package/src/editor/reviews/PreviewInfo.tsx +35 -0
- package/src/editor/reviews/Reviews.tsx +280 -0
- package/src/editor/reviews/reviewCommands.tsx +47 -0
- package/src/editor/reviews/useReviews.tsx +70 -0
- package/src/editor/services/aiService.ts +155 -0
- package/src/editor/services/componentDesignerService.ts +151 -0
- package/src/editor/services/contentService.ts +159 -0
- package/src/editor/services/editService.ts +462 -0
- package/src/editor/services/indexService.ts +24 -0
- package/src/editor/services/reviewsService.ts +45 -0
- package/src/editor/services/serviceHelper.ts +95 -0
- package/src/editor/services/systemService.ts +5 -0
- package/src/editor/services/translationService.ts +21 -0
- package/src/editor/services-server/api.ts +150 -0
- package/src/editor/services-server/graphQL.ts +106 -0
- package/src/editor/sidebar/ComponentPalette.tsx +146 -0
- package/src/editor/sidebar/ComponentTree.tsx +512 -0
- package/src/editor/sidebar/ComponentTree2.tsxx +490 -0
- package/src/editor/sidebar/Debug.tsx +105 -0
- package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
- package/src/editor/sidebar/EditHistory.tsx +134 -0
- package/src/editor/sidebar/GraphQL.tsx +164 -0
- package/src/editor/sidebar/Insert.tsx +35 -0
- package/src/editor/sidebar/MainContentTree.tsx +95 -0
- package/src/editor/sidebar/Performance.tsx +53 -0
- package/src/editor/sidebar/Sessions.tsx +35 -0
- package/src/editor/sidebar/Sidebar.tsx +20 -0
- package/src/editor/sidebar/SidebarView.tsx +150 -0
- package/src/editor/sidebar/Translations.tsx +276 -0
- package/src/editor/sidebar/Validation.tsx +102 -0
- package/src/editor/sidebar/ViewSelector.tsx +49 -0
- package/src/editor/sidebar/Workbox.tsx +209 -0
- package/src/editor/ui/CenteredMessage.tsx +7 -0
- package/src/editor/ui/CopyToClipboardButton.tsx +23 -0
- package/src/editor/ui/DialogButtons.tsx +11 -0
- package/src/editor/ui/Icons.tsx +585 -0
- package/src/editor/ui/ItemNameDialog.tsx +94 -0
- package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
- package/src/editor/ui/ItemSearch.tsx +173 -0
- package/src/editor/ui/PerfectTree.tsx +550 -0
- package/src/editor/ui/Section.tsx +35 -0
- package/src/editor/ui/SimpleIconButton.tsx +43 -0
- package/src/editor/ui/SimpleMenu.tsx +48 -0
- package/src/editor/ui/SimpleTable.tsx +63 -0
- package/src/editor/ui/SimpleTabs.tsx +55 -0
- package/src/editor/ui/SimpleToolbar.tsx +7 -0
- package/src/editor/ui/Spinner.tsx +7 -0
- package/src/editor/ui/Splitter.tsx +247 -0
- package/src/editor/ui/StackedPanels.tsx +134 -0
- package/src/editor/ui/Toolbar.tsx +7 -0
- package/src/editor/utils/id-helper.ts +3 -0
- package/src/editor/utils/insertOptions.ts +69 -0
- package/src/editor/utils/itemutils.ts +29 -0
- package/src/editor/utils/useMemoDebug.ts +28 -0
- package/src/editor/utils.ts +435 -0
- package/src/editor/views/CompareView.tsx +256 -0
- package/src/editor/views/EditView.tsx +27 -0
- package/src/editor/views/ItemEditor.tsx +58 -0
- package/src/editor/views/SingleEditView.tsx +44 -0
- package/src/fonts/Geist-Black.woff2 +0 -0
- package/src/fonts/Geist-Bold.woff2 +0 -0
- package/src/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/fonts/Geist-Light.woff2 +0 -0
- package/src/fonts/Geist-Medium.woff2 +0 -0
- package/src/fonts/Geist-Regular.woff2 +0 -0
- package/src/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/fonts/Geist-Thin.woff2 +0 -0
- package/src/fonts/Geist[wght].woff2 +0 -0
- package/src/index.ts +7 -0
- package/src/page-wizard/PageWizard.tsx +163 -0
- package/src/page-wizard/SelectWizard.tsx +109 -0
- package/src/page-wizard/WizardSteps.tsx +207 -0
- package/src/page-wizard/service.ts +35 -0
- package/src/page-wizard/startPageWizardCommand.ts +27 -0
- package/src/page-wizard/steps/BuildPageStep.tsx +266 -0
- package/src/page-wizard/steps/CollectStep.tsx +233 -0
- package/src/page-wizard/steps/ComponentTypesSelector.tsx +443 -0
- package/src/page-wizard/steps/Components.tsx +193 -0
- package/src/page-wizard/steps/CreatePage.tsx +285 -0
- package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +384 -0
- package/src/page-wizard/steps/EditButton.tsx +34 -0
- package/src/page-wizard/steps/FieldEditor.tsx +102 -0
- package/src/page-wizard/steps/Generate.tsx +32 -0
- package/src/page-wizard/steps/ImagesStep.tsx +318 -0
- package/src/page-wizard/steps/LayoutStep.tsx +228 -0
- package/src/page-wizard/steps/SelectStep.tsx +256 -0
- package/src/page-wizard/steps/schema.ts +180 -0
- package/src/page-wizard/steps/usePageCreator.ts +279 -0
- package/src/splash-screen/NewPage.tsx +232 -0
- package/src/splash-screen/SectionHeadline.tsx +21 -0
- package/src/splash-screen/SplashScreen.tsx +156 -0
- package/src/tour/Tour.tsx +558 -0
- package/src/tour/default-tour.tsx +300 -0
- package/src/tour/preview-tour.tsx +127 -0
- package/src/types.ts +302 -0
- package/styles.css +476 -0
- package/tsconfig.build.json +21 -0
- 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
|
+
}
|