@alpaca-editor/core 1.0.4172 → 1.0.4174

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 (170) hide show
  1. package/dist/agents-view/AgentsView.d.ts +5 -0
  2. package/dist/agents-view/AgentsView.js +213 -0
  3. package/dist/agents-view/AgentsView.js.map +1 -0
  4. package/dist/components/ui/context-menu.js +4 -4
  5. package/dist/components/ui/context-menu.js.map +1 -1
  6. package/dist/config/config.js +56 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/editor/ConfirmationDialog.js +2 -1
  9. package/dist/editor/ConfirmationDialog.js.map +1 -1
  10. package/dist/editor/ContentTree.d.ts +2 -1
  11. package/dist/editor/ContentTree.js +18 -3
  12. package/dist/editor/ContentTree.js.map +1 -1
  13. package/dist/editor/ContextMenu.js +1 -1
  14. package/dist/editor/ContextMenu.js.map +1 -1
  15. package/dist/editor/FieldList.js +7 -3
  16. package/dist/editor/FieldList.js.map +1 -1
  17. package/dist/editor/FieldListField.d.ts +3 -2
  18. package/dist/editor/FieldListField.js +4 -4
  19. package/dist/editor/FieldListField.js.map +1 -1
  20. package/dist/editor/FieldListFieldWithFallbacks.d.ts +2 -1
  21. package/dist/editor/FieldListFieldWithFallbacks.js +5 -2
  22. package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
  23. package/dist/editor/MainLayout.js +1 -1
  24. package/dist/editor/QuickItemSwitcher.d.ts +9 -0
  25. package/dist/editor/QuickItemSwitcher.js +74 -0
  26. package/dist/editor/QuickItemSwitcher.js.map +1 -0
  27. package/dist/editor/ai/AgentCostDisplay.js +7 -11
  28. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  29. package/dist/editor/ai/AgentProfilesOverview.d.ts +9 -0
  30. package/dist/editor/ai/AgentProfilesOverview.js +16 -0
  31. package/dist/editor/ai/AgentProfilesOverview.js.map +1 -0
  32. package/dist/editor/ai/AgentTerminal.js +285 -748
  33. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  34. package/dist/editor/ai/Agents.js +112 -54
  35. package/dist/editor/ai/Agents.js.map +1 -1
  36. package/dist/editor/ai/AiResponseMessage.d.ts +2 -1
  37. package/dist/editor/ai/AiResponseMessage.js +4 -2
  38. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  39. package/dist/editor/ai/ContextInfoBar.js +17 -17
  40. package/dist/editor/ai/useAgentStatus.js +7 -0
  41. package/dist/editor/ai/useAgentStatus.js.map +1 -1
  42. package/dist/editor/client/EditorShell.js +230 -4
  43. package/dist/editor/client/EditorShell.js.map +1 -1
  44. package/dist/editor/client/hooks/useSocketMessageHandler.js +0 -12
  45. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  46. package/dist/editor/client/ui/EditorChrome.js +1 -1
  47. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  48. package/dist/editor/commands/itemCommands.js +1 -0
  49. package/dist/editor/commands/itemCommands.js.map +1 -1
  50. package/dist/editor/control-center/parhelia-setup/Overview.d.ts +1 -0
  51. package/dist/editor/control-center/parhelia-setup/Overview.js +91 -0
  52. package/dist/editor/control-center/parhelia-setup/Overview.js.map +1 -0
  53. package/dist/editor/field-types/AttachmentEditor.js +3 -6
  54. package/dist/editor/field-types/AttachmentEditor.js.map +1 -1
  55. package/dist/editor/field-types/DropLinkEditor.js +2 -2
  56. package/dist/editor/field-types/DropLinkEditor.js.map +1 -1
  57. package/dist/editor/field-types/DropListEditor.js +2 -1
  58. package/dist/editor/field-types/DropListEditor.js.map +1 -1
  59. package/dist/editor/field-types/InternalLinkFieldEditor.js +3 -3
  60. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  61. package/dist/editor/field-types/MultiLineText.d.ts +2 -1
  62. package/dist/editor/field-types/MultiLineText.js +2 -2
  63. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  64. package/dist/editor/field-types/RawEditor.d.ts +2 -1
  65. package/dist/editor/field-types/RawEditor.js +2 -2
  66. package/dist/editor/field-types/RawEditor.js.map +1 -1
  67. package/dist/editor/field-types/SingleLineText.d.ts +2 -1
  68. package/dist/editor/field-types/SingleLineText.js +2 -2
  69. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  70. package/dist/editor/field-types/TreeListEditor.js +9 -7
  71. package/dist/editor/field-types/TreeListEditor.js.map +1 -1
  72. package/dist/editor/media-selector/MediaFolderBrowser.js +68 -7
  73. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  74. package/dist/editor/media-selector/TreeSelector.js +1 -1
  75. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  76. package/dist/editor/menubar/ActiveUsers.js +2 -2
  77. package/dist/editor/menubar/FavoritesControls.js +2 -2
  78. package/dist/editor/menubar/FavoritesControls.js.map +1 -1
  79. package/dist/editor/page-editor-chrome/FrameMenu.js +10 -1
  80. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  81. package/dist/editor/page-viewer/pageModelSkeletonBuilder.js +14 -0
  82. package/dist/editor/page-viewer/pageModelSkeletonBuilder.js.map +1 -1
  83. package/dist/editor/services/agentService.d.ts +21 -3
  84. package/dist/editor/services/agentService.js +24 -8
  85. package/dist/editor/services/agentService.js.map +1 -1
  86. package/dist/editor/services/aiService.d.ts +6 -1
  87. package/dist/editor/services/aiService.js +36 -5
  88. package/dist/editor/services/aiService.js.map +1 -1
  89. package/dist/editor/services/contentService.d.ts +1 -1
  90. package/dist/editor/services/contentService.js +4 -2
  91. package/dist/editor/services/contentService.js.map +1 -1
  92. package/dist/editor/services/editService.d.ts +5 -1
  93. package/dist/editor/services/editService.js +1 -1
  94. package/dist/editor/services/editService.js.map +1 -1
  95. package/dist/editor/services/setupService.d.ts +21 -0
  96. package/dist/editor/services/setupService.js +10 -0
  97. package/dist/editor/services/setupService.js.map +1 -0
  98. package/dist/editor/sidebar/ComponentTree.js +15 -1
  99. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  100. package/dist/editor/sidebar/SidebarView.js +1 -1
  101. package/dist/editor/sidebar/SidebarView.js.map +1 -1
  102. package/dist/editor/ui/ItemSearch.d.ts +1 -0
  103. package/dist/editor/ui/ItemSearch.js +2 -2
  104. package/dist/editor/ui/ItemSearch.js.map +1 -1
  105. package/dist/editor/ui/PerfectTree.d.ts +5 -1
  106. package/dist/editor/ui/PerfectTree.js +308 -29
  107. package/dist/editor/ui/PerfectTree.js.map +1 -1
  108. package/dist/editor/utils/keyboardNavigation.d.ts +2 -0
  109. package/dist/editor/utils/keyboardNavigation.js +80 -2
  110. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  111. package/dist/editor/views/SingleEditView.js +6 -4
  112. package/dist/editor/views/SingleEditView.js.map +1 -1
  113. package/dist/revision.d.ts +2 -2
  114. package/dist/revision.js +2 -2
  115. package/dist/splash-screen/SplashScreen.js +78 -4
  116. package/dist/splash-screen/SplashScreen.js.map +1 -1
  117. package/dist/styles.css +157 -23
  118. package/dist/types.d.ts +13 -0
  119. package/package.json +1 -1
  120. package/src/agents-view/AgentsView.tsx +431 -0
  121. package/src/components/ui/context-menu.tsx +4 -4
  122. package/src/config/config.tsx +61 -0
  123. package/src/editor/ConfirmationDialog.tsx +42 -10
  124. package/src/editor/ContentTree.tsx +20 -1
  125. package/src/editor/ContextMenu.tsx +4 -1
  126. package/src/editor/FieldList.tsx +10 -4
  127. package/src/editor/FieldListField.tsx +7 -0
  128. package/src/editor/FieldListFieldWithFallbacks.tsx +10 -0
  129. package/src/editor/MainLayout.tsx +1 -1
  130. package/src/editor/QuickItemSwitcher.tsx +217 -0
  131. package/src/editor/ai/AgentCostDisplay.tsx +59 -60
  132. package/src/editor/ai/AgentProfilesOverview.tsx +81 -0
  133. package/src/editor/ai/AgentTerminal.tsx +321 -775
  134. package/src/editor/ai/Agents.tsx +157 -91
  135. package/src/editor/ai/AiResponseMessage.tsx +12 -1
  136. package/src/editor/ai/ContextInfoBar.tsx +17 -17
  137. package/src/editor/ai/useAgentStatus.ts +6 -0
  138. package/src/editor/client/EditorShell.tsx +288 -3
  139. package/src/editor/client/hooks/useSocketMessageHandler.ts +0 -15
  140. package/src/editor/client/ui/EditorChrome.tsx +1 -1
  141. package/src/editor/commands/itemCommands.tsx +1 -0
  142. package/src/editor/control-center/parhelia-setup/Overview.tsx +184 -0
  143. package/src/editor/field-types/AttachmentEditor.tsx +13 -50
  144. package/src/editor/field-types/DropLinkEditor.tsx +2 -2
  145. package/src/editor/field-types/DropListEditor.tsx +2 -1
  146. package/src/editor/field-types/InternalLinkFieldEditor.tsx +3 -3
  147. package/src/editor/field-types/MultiLineText.tsx +3 -0
  148. package/src/editor/field-types/RawEditor.tsx +3 -0
  149. package/src/editor/field-types/SingleLineText.tsx +3 -0
  150. package/src/editor/field-types/TreeListEditor.tsx +32 -24
  151. package/src/editor/media-selector/MediaFolderBrowser.tsx +93 -9
  152. package/src/editor/media-selector/TreeSelector.tsx +1 -0
  153. package/src/editor/menubar/ActiveUsers.tsx +2 -2
  154. package/src/editor/menubar/FavoritesControls.tsx +5 -5
  155. package/src/editor/page-editor-chrome/FrameMenu.tsx +10 -1
  156. package/src/editor/page-viewer/pageModelSkeletonBuilder.ts +17 -0
  157. package/src/editor/services/agentService.ts +46 -11
  158. package/src/editor/services/aiService.ts +48 -7
  159. package/src/editor/services/contentService.ts +4 -1
  160. package/src/editor/services/editService.ts +8 -3
  161. package/src/editor/services/setupService.ts +35 -0
  162. package/src/editor/sidebar/ComponentTree.tsx +16 -1
  163. package/src/editor/sidebar/SidebarView.tsx +1 -1
  164. package/src/editor/ui/ItemSearch.tsx +3 -1
  165. package/src/editor/ui/PerfectTree.tsx +393 -42
  166. package/src/editor/utils/keyboardNavigation.ts +97 -1
  167. package/src/editor/views/SingleEditView.tsx +27 -13
  168. package/src/revision.ts +2 -2
  169. package/src/splash-screen/SplashScreen.tsx +134 -2
  170. package/src/types.ts +15 -0
@@ -88,6 +88,10 @@ export interface TreeProps<T = any> {
88
88
  disableAutoSelectOnExpand?: boolean;
89
89
  /** Noun used in multi-select drag preview (e.g., "items", "components"). Default: "items" */
90
90
  multiDragNoun?: string;
91
+ /** Whether to enable keyboard navigation (default: true) */
92
+ enableKeyboardNavigation?: boolean;
93
+ /** Called when keyboard navigation focus changes */
94
+ onKeyboardNavigationChange?: (key: string) => void;
91
95
  }
92
96
 
93
97
  /**
@@ -346,6 +350,38 @@ const filterTreeNodes = (
346
350
  .filter((node): node is TreeNode<any> => node !== null);
347
351
  };
348
352
 
353
+ // Helper types for keyboard navigation
354
+ interface FlattenedNode<T = any> {
355
+ node: TreeNode<T>;
356
+ parent: TreeNode<T> | null;
357
+ depth: number;
358
+ }
359
+
360
+ // Helper function to flatten tree nodes into a depth-first list of visible nodes
361
+ const flattenVisibleNodes = <T,>(
362
+ nodes: TreeNode<T>[],
363
+ expandedKeys: string[],
364
+ parent: TreeNode<T> | null = null,
365
+ depth: number = 0,
366
+ ): FlattenedNode<T>[] => {
367
+ const result: FlattenedNode<T>[] = [];
368
+
369
+ for (const node of nodes) {
370
+ // Add current node
371
+ result.push({ node, parent, depth });
372
+
373
+ // If expanded and has children, recursively add children
374
+ const isExpanded = expandedKeys.includes(node.key);
375
+ if (isExpanded && node.children && Array.isArray(node.children)) {
376
+ result.push(
377
+ ...flattenVisibleNodes(node.children, expandedKeys, node, depth + 1),
378
+ );
379
+ }
380
+ }
381
+
382
+ return result;
383
+ };
384
+
349
385
  // NodeContent component extracted and memoized
350
386
  const NodeContent = memo(
351
387
  ({
@@ -580,9 +616,14 @@ export const PerfectTree = <T,>({
580
616
  searchClearDelay = 1500,
581
617
  disableAutoSelectOnExpand = false,
582
618
  multiDragNoun = "items",
619
+ enableKeyboardNavigation = true,
620
+ onKeyboardNavigationChange,
583
621
  }: TreeProps<T>) => {
584
622
  const [searchTerm, setSearchTerm] = useState("");
585
623
  const [isFocused, setIsFocused] = useState(false);
624
+ const [keyboardNavPosition, setKeyboardNavPosition] = useState<string | null>(
625
+ null,
626
+ );
586
627
  const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
587
628
 
588
629
  // When toggling a node, notify parent and trigger external lazy load if needed.
@@ -686,47 +727,6 @@ export const PerfectTree = <T,>({
686
727
  };
687
728
  }, [onDragEnd]);
688
729
 
689
- const handleDragEnd = useCallback(
690
- (event: React.DragEvent<HTMLDivElement> | null) => {
691
- isDraggingRef.current = false;
692
- // Clear timeout when drag ends normally
693
- if (dragTimeoutRef.current !== undefined) {
694
- clearTimeout(dragTimeoutRef.current!);
695
- dragTimeoutRef.current = undefined;
696
- }
697
- if (onDragEnd) {
698
- onDragEnd(event);
699
- }
700
- },
701
- [onDragEnd],
702
- );
703
-
704
- // Wrapper for onStartDrag that has access to the refs
705
- const handleStartDragWithTimeout = useCallback(
706
- (data: {
707
- node: TreeNode<T>;
708
- event: React.DragEvent<any>;
709
- isMultiSelect: boolean;
710
- }) => {
711
- // Set the dragging ref to true when drag starts
712
- isDraggingRef.current = true;
713
-
714
- // Set up a timeout to auto-reset drag state if it gets stuck
715
- dragTimeoutRef.current = setTimeout(() => {
716
- if (isDraggingRef.current && onDragEnd) {
717
- onDragEnd(null);
718
- isDraggingRef.current = false;
719
- }
720
- }, 30000); // 30 second timeout
721
-
722
- // Call the original onStartDrag
723
- if (onStartDrag) {
724
- onStartDrag(data);
725
- }
726
- },
727
- [onStartDrag, onDragEnd],
728
- );
729
-
730
730
  // Scroll to selected node when scrollToSelected is enabled and selectedKeys change
731
731
  const treeRef = useRef<HTMLDivElement>(null);
732
732
 
@@ -864,6 +864,46 @@ export const PerfectTree = <T,>({
864
864
  return filterTreeNodes(nodes, searchTerm, expandedKeys);
865
865
  }, [nodes, searchTerm, expandedKeys]);
866
866
 
867
+ // Wrapper for onStartDrag that has access to the refs
868
+ const handleStartDragWithTimeout = useCallback(
869
+ (data: {
870
+ node: TreeNode<T>;
871
+ event: React.DragEvent<any>;
872
+ isMultiSelect: boolean;
873
+ }) => {
874
+ // Mark dragging as active
875
+ isDraggingRef.current = true;
876
+ // Failsafe timeout to recover if the browser fails to emit dragend
877
+ dragTimeoutRef.current = setTimeout(() => {
878
+ if (isDraggingRef.current && onDragEnd) {
879
+ onDragEnd(null);
880
+ isDraggingRef.current = false;
881
+ }
882
+ }, 30000); // 30s safety timeout
883
+
884
+ // Delegate to consumer
885
+ if (onStartDrag) {
886
+ onStartDrag(data);
887
+ }
888
+ },
889
+ [onStartDrag, onDragEnd],
890
+ );
891
+
892
+ const handleDragEnd = useCallback(
893
+ (event: React.DragEvent<HTMLDivElement> | null) => {
894
+ isDraggingRef.current = false;
895
+ // Clear timeout when drag ends normally
896
+ if (dragTimeoutRef.current !== undefined) {
897
+ clearTimeout(dragTimeoutRef.current!);
898
+ dragTimeoutRef.current = undefined;
899
+ }
900
+ if (onDragEnd) {
901
+ onDragEnd(event);
902
+ }
903
+ },
904
+ [onDragEnd],
905
+ );
906
+
867
907
  // Recursive function to render tree nodes along with drop zones.
868
908
  const renderTreeList = useCallback(
869
909
  (
@@ -964,6 +1004,300 @@ export const PerfectTree = <T,>({
964
1004
  [filteredNodes, renderTreeList],
965
1005
  );
966
1006
 
1007
+ // Initialize keyboard navigation position when tree gains focus
1008
+ useEffect(() => {
1009
+ if (isFocused && !keyboardNavPosition) {
1010
+ // Set initial position to first selected node, or first node
1011
+ if (selectedKeys.length > 0 && selectedKeys[0]) {
1012
+ setKeyboardNavPosition(selectedKeys[0]);
1013
+ } else if (filteredNodes.length > 0 && filteredNodes[0]) {
1014
+ setKeyboardNavPosition(filteredNodes[0].key);
1015
+ }
1016
+ }
1017
+ }, [isFocused, keyboardNavPosition, selectedKeys, filteredNodes]);
1018
+
1019
+ // Update keyboard navigation position when selection changes externally (e.g., via click)
1020
+ useEffect(() => {
1021
+ if (isFocused && selectedKeys.length > 0 && selectedKeys[0]) {
1022
+ // Only update if the current nav position is not in the selected keys
1023
+ // This preserves nav position during shift-select
1024
+ if (!keyboardNavPosition || !selectedKeys.includes(keyboardNavPosition)) {
1025
+ setKeyboardNavPosition(selectedKeys[0]);
1026
+ }
1027
+ }
1028
+ }, [selectedKeys, isFocused, keyboardNavPosition]);
1029
+
1030
+ // Helper function to scroll a node into view
1031
+ const scrollNodeIntoView = useCallback((nodeKey: string) => {
1032
+ if (!treeRef.current) return;
1033
+
1034
+ setTimeout(() => {
1035
+ const treeContainer = treeRef.current;
1036
+ if (!treeContainer) return;
1037
+
1038
+ const targetNode = treeContainer.querySelector(
1039
+ `[data-node-key="${CSS.escape(nodeKey)}"]`,
1040
+ ) as HTMLElement;
1041
+
1042
+ if (targetNode) {
1043
+ let scrollContainer = targetNode.closest(
1044
+ ".overflow-auto",
1045
+ ) as HTMLElement;
1046
+
1047
+ if (!scrollContainer) {
1048
+ scrollContainer = targetNode.closest(
1049
+ '[style*="overflow"]',
1050
+ ) as HTMLElement;
1051
+ }
1052
+
1053
+ if (!scrollContainer) {
1054
+ scrollContainer = treeContainer.parentElement as HTMLElement;
1055
+ }
1056
+
1057
+ if (scrollContainer) {
1058
+ const containerRect = scrollContainer.getBoundingClientRect();
1059
+ const nodeRect = targetNode.getBoundingClientRect();
1060
+
1061
+ const isVisible =
1062
+ nodeRect.top >= containerRect.top &&
1063
+ nodeRect.bottom <= containerRect.bottom;
1064
+
1065
+ if (!isVisible) {
1066
+ const scrollTop = scrollContainer.scrollTop;
1067
+ const containerTop = containerRect.top;
1068
+ const nodeTop = nodeRect.top;
1069
+ const offset = nodeTop - containerTop;
1070
+
1071
+ const newScrollTop =
1072
+ scrollTop +
1073
+ offset -
1074
+ containerRect.height / 2 +
1075
+ nodeRect.height / 2;
1076
+
1077
+ scrollContainer.scrollTo({
1078
+ top: newScrollTop,
1079
+ behavior: "smooth",
1080
+ });
1081
+ }
1082
+ } else {
1083
+ targetNode.scrollIntoView({
1084
+ behavior: "smooth",
1085
+ block: "nearest",
1086
+ });
1087
+ }
1088
+ }
1089
+ }, 0);
1090
+ }, []);
1091
+
1092
+ // Keyboard navigation functionality
1093
+ useEffect(() => {
1094
+ if (!enableKeyboardNavigation || !isFocused) return;
1095
+
1096
+ const handleKeyDown = (event: KeyboardEvent) => {
1097
+ // Ignore if user is typing in an input, textarea, or contenteditable element
1098
+ const target = event.target as HTMLElement;
1099
+ if (
1100
+ target.tagName === "INPUT" ||
1101
+ target.tagName === "TEXTAREA" ||
1102
+ target.contentEditable === "true" ||
1103
+ target.isContentEditable
1104
+ ) {
1105
+ return;
1106
+ }
1107
+
1108
+ // Get flattened list of visible nodes
1109
+ const visibleNodes = flattenVisibleNodes(filteredNodes, expandedKeys);
1110
+ if (visibleNodes.length === 0) return;
1111
+
1112
+ // Use keyboard navigation position as the current position
1113
+ const currentIndex = keyboardNavPosition
1114
+ ? visibleNodes.findIndex((n) => n.node.key === keyboardNavPosition)
1115
+ : -1;
1116
+
1117
+ let newSelectedKey: string | null = null;
1118
+ let shouldUseShift = false;
1119
+
1120
+ switch (event.key) {
1121
+ case "ArrowDown": {
1122
+ event.preventDefault();
1123
+ shouldUseShift = event.shiftKey;
1124
+ if (currentIndex < visibleNodes.length - 1) {
1125
+ const nextNode = visibleNodes[currentIndex + 1];
1126
+ if (nextNode) {
1127
+ newSelectedKey = nextNode.node.key;
1128
+ }
1129
+ } else if (
1130
+ currentIndex === -1 &&
1131
+ visibleNodes.length > 0 &&
1132
+ visibleNodes[0]
1133
+ ) {
1134
+ // No selection, select first node
1135
+ newSelectedKey = visibleNodes[0].node.key;
1136
+ }
1137
+ break;
1138
+ }
1139
+
1140
+ case "ArrowUp": {
1141
+ event.preventDefault();
1142
+ shouldUseShift = event.shiftKey;
1143
+ if (currentIndex > 0) {
1144
+ const prevNode = visibleNodes[currentIndex - 1];
1145
+ if (prevNode) {
1146
+ newSelectedKey = prevNode.node.key;
1147
+ }
1148
+ } else if (
1149
+ currentIndex === -1 &&
1150
+ visibleNodes.length > 0 &&
1151
+ visibleNodes[0]
1152
+ ) {
1153
+ // No selection, select first node
1154
+ newSelectedKey = visibleNodes[0].node.key;
1155
+ }
1156
+ break;
1157
+ }
1158
+
1159
+ case "ArrowRight": {
1160
+ event.preventDefault();
1161
+ const currentNode =
1162
+ currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
1163
+ if (currentNode) {
1164
+ const node = currentNode.node;
1165
+ const isExpanded = expandedKeys.includes(node.key);
1166
+
1167
+ if (node.hasChildren || node.children?.length) {
1168
+ if (!isExpanded) {
1169
+ // Expand the node
1170
+ if (onToggleExpand) {
1171
+ onToggleExpand(node.key);
1172
+ }
1173
+ if (
1174
+ node.hasChildren &&
1175
+ node.children === undefined &&
1176
+ onLazyLoad
1177
+ ) {
1178
+ onLazyLoad(node);
1179
+ }
1180
+ } else if (
1181
+ node.children &&
1182
+ node.children.length > 0 &&
1183
+ node.children[0]
1184
+ ) {
1185
+ // Move to first child
1186
+ newSelectedKey = node.children[0].key;
1187
+ }
1188
+ }
1189
+ }
1190
+ break;
1191
+ }
1192
+
1193
+ case "ArrowLeft": {
1194
+ event.preventDefault();
1195
+ const currentNode =
1196
+ currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
1197
+ if (currentNode) {
1198
+ const node = currentNode.node;
1199
+ const isExpanded = expandedKeys.includes(node.key);
1200
+
1201
+ if (isExpanded && (node.hasChildren || node.children?.length)) {
1202
+ // Collapse the node
1203
+ if (onToggleExpand) {
1204
+ onToggleExpand(node.key);
1205
+ }
1206
+ } else if (currentNode.parent) {
1207
+ // Move to parent node
1208
+ newSelectedKey = currentNode.parent.key;
1209
+ }
1210
+ }
1211
+ break;
1212
+ }
1213
+
1214
+ case "Enter": {
1215
+ event.preventDefault();
1216
+ if (keyboardNavPosition) {
1217
+ // Toggle expand/collapse if it has children
1218
+ const currentNode =
1219
+ currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
1220
+ if (currentNode) {
1221
+ const node = currentNode.node;
1222
+ if (node.hasChildren || node.children?.length) {
1223
+ if (onToggleExpand) {
1224
+ onToggleExpand(node.key);
1225
+ }
1226
+ if (
1227
+ node.hasChildren &&
1228
+ node.children === undefined &&
1229
+ onLazyLoad
1230
+ ) {
1231
+ onLazyLoad(node);
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ break;
1237
+ }
1238
+
1239
+ case "Home": {
1240
+ event.preventDefault();
1241
+ if (visibleNodes.length > 0) {
1242
+ const firstNode = visibleNodes[0];
1243
+ if (firstNode) {
1244
+ newSelectedKey = firstNode.node.key;
1245
+ }
1246
+ }
1247
+ break;
1248
+ }
1249
+
1250
+ case "End": {
1251
+ event.preventDefault();
1252
+ if (visibleNodes.length > 0) {
1253
+ const lastNode = visibleNodes[visibleNodes.length - 1];
1254
+ if (lastNode) {
1255
+ newSelectedKey = lastNode.node.key;
1256
+ }
1257
+ }
1258
+ break;
1259
+ }
1260
+ }
1261
+
1262
+ if (newSelectedKey && newSelectedKey !== keyboardNavPosition) {
1263
+ // Update keyboard navigation position
1264
+ setKeyboardNavPosition(newSelectedKey);
1265
+
1266
+ if (onSelect) {
1267
+ // Create a synthetic mouse event with shiftKey set for range selection
1268
+ const syntheticEvent = {
1269
+ shiftKey: shouldUseShift,
1270
+ ctrlKey: false,
1271
+ metaKey: false,
1272
+ button: 0,
1273
+ } as React.MouseEvent;
1274
+ onSelect(newSelectedKey, syntheticEvent);
1275
+ }
1276
+ scrollNodeIntoView(newSelectedKey);
1277
+ if (onKeyboardNavigationChange) {
1278
+ onKeyboardNavigationChange(newSelectedKey);
1279
+ }
1280
+ }
1281
+ };
1282
+
1283
+ document.addEventListener("keydown", handleKeyDown);
1284
+
1285
+ return () => {
1286
+ document.removeEventListener("keydown", handleKeyDown);
1287
+ };
1288
+ }, [
1289
+ enableKeyboardNavigation,
1290
+ isFocused,
1291
+ keyboardNavPosition,
1292
+ filteredNodes,
1293
+ expandedKeys,
1294
+ onToggleExpand,
1295
+ onSelect,
1296
+ onLazyLoad,
1297
+ scrollNodeIntoView,
1298
+ onKeyboardNavigationChange,
1299
+ ]);
1300
+
967
1301
  // Keyboard search functionality
968
1302
  useEffect(() => {
969
1303
  if (!enableKeyboardSearch || !isFocused) return;
@@ -980,6 +1314,21 @@ export const PerfectTree = <T,>({
980
1314
  return;
981
1315
  }
982
1316
 
1317
+ // Ignore navigation keys for search
1318
+ if (
1319
+ [
1320
+ "ArrowUp",
1321
+ "ArrowDown",
1322
+ "ArrowLeft",
1323
+ "ArrowRight",
1324
+ "Enter",
1325
+ "Home",
1326
+ "End",
1327
+ ].includes(event.key)
1328
+ ) {
1329
+ return;
1330
+ }
1331
+
983
1332
  // Handle different key types
984
1333
  if (event.key === "Escape") {
985
1334
  // Clear search on Escape
@@ -1039,6 +1388,7 @@ export const PerfectTree = <T,>({
1039
1388
  onBlur={() => {
1040
1389
  setIsFocused(false);
1041
1390
  setSearchTerm("");
1391
+ setKeyboardNavPosition(null);
1042
1392
  if (searchTimeoutRef.current) {
1043
1393
  clearTimeout(searchTimeoutRef.current);
1044
1394
  searchTimeoutRef.current = null;
@@ -1073,7 +1423,8 @@ const arePropsEqual = <T,>(
1073
1423
  prevProps.searchClearDelay !== nextProps.searchClearDelay ||
1074
1424
  prevProps.disableAutoSelectOnExpand !==
1075
1425
  nextProps.disableAutoSelectOnExpand ||
1076
- prevProps.multiDragNoun !== nextProps.multiDragNoun
1426
+ prevProps.multiDragNoun !== nextProps.multiDragNoun ||
1427
+ prevProps.enableKeyboardNavigation !== nextProps.enableKeyboardNavigation
1077
1428
  ) {
1078
1429
  return false;
1079
1430
  }
@@ -23,6 +23,8 @@ export interface KeyboardNavigationDependencies {
23
23
  data?: any;
24
24
  event?: React.SyntheticEvent;
25
25
  }) => Promise<any>;
26
+ showQuickSwitcher?: (show: boolean) => void;
27
+ cycleQuickSwitcher?: (direction: "next" | "prev" | "up" | "down") => void;
26
28
  }
27
29
 
28
30
  export function useKeyboardNavigation(deps: KeyboardNavigationDependencies) {
@@ -36,6 +38,8 @@ export function useKeyboardNavigation(deps: KeyboardNavigationDependencies) {
36
38
  loadItem,
37
39
  showInfoToast,
38
40
  executeCommand,
41
+ showQuickSwitcher,
42
+ cycleQuickSwitcher,
39
43
  } = deps;
40
44
 
41
45
  const handleKeyDownDebounced = useDebouncedCallback(
@@ -169,6 +173,18 @@ export function useKeyboardNavigation(deps: KeyboardNavigationDependencies) {
169
173
  );
170
174
 
171
175
  if (command) {
176
+ // Check if user is typing in an input field
177
+ const target = event.target as HTMLElement;
178
+ const isTyping =
179
+ target instanceof HTMLInputElement ||
180
+ target instanceof HTMLTextAreaElement ||
181
+ target.isContentEditable;
182
+
183
+ // Don't execute commands when typing in text fields
184
+ // F2 works everywhere except when typing
185
+ // Delete also won't trigger when typing
186
+ if (isTyping) return;
187
+
172
188
  event.preventDefault();
173
189
  const contentEditorItem = editContextRef.current?.contentEditorItem;
174
190
  if (!contentEditorItem) return;
@@ -211,6 +227,80 @@ export function useKeyboardNavigation(deps: KeyboardNavigationDependencies) {
211
227
  return;
212
228
  }
213
229
 
230
+ // Check if quick switcher is currently visible
231
+ const isQuickSwitcherVisible = editContextRef.current
232
+ ? (editContextRef.current as any).isQuickSwitcherVisible
233
+ : false;
234
+
235
+ // Handle navigation keys when switcher is visible (check this FIRST)
236
+ if (isQuickSwitcherVisible && editContextRef.current) {
237
+ // Shift key acts as ArrowRight for easy one-handed navigation
238
+ if (event.key === "Shift" && event.ctrlKey) {
239
+ console.log(`[KeyNav] Shift pressed (Ctrl held) - moving right`);
240
+ event.preventDefault();
241
+ event.stopPropagation();
242
+ cycleQuickSwitcher?.("next");
243
+ return;
244
+ }
245
+
246
+ // Arrow keys for navigation
247
+ if (
248
+ event.key === "ArrowLeft" ||
249
+ event.key === "ArrowRight" ||
250
+ event.key === "ArrowUp" ||
251
+ event.key === "ArrowDown"
252
+ ) {
253
+ console.log(`[KeyNav] Arrow key navigation: ${event.key}`);
254
+ event.preventDefault();
255
+ event.stopPropagation();
256
+
257
+ let direction: "next" | "prev" | "up" | "down";
258
+ switch (event.key) {
259
+ case "ArrowLeft":
260
+ direction = "prev";
261
+ break;
262
+ case "ArrowRight":
263
+ direction = "next";
264
+ break;
265
+ case "ArrowUp":
266
+ direction = "up";
267
+ break;
268
+ case "ArrowDown":
269
+ direction = "down";
270
+ break;
271
+ }
272
+ cycleQuickSwitcher?.(direction);
273
+ return;
274
+ }
275
+
276
+ // Enter or Escape to close
277
+ if (event.key === "Enter" || event.key === "Escape") {
278
+ console.log(
279
+ `[KeyNav] ${event.key} pressed - closing switcher with selection: ${event.key === "Enter"}`,
280
+ );
281
+ event.preventDefault();
282
+ event.stopPropagation();
283
+ // showQuickSwitcher(false) will be handled by the Enter key handler in EditorShell
284
+ // For now, we just let the keyup handler in EditorShell handle it
285
+ return;
286
+ }
287
+ }
288
+
289
+ // Handle quick switcher trigger (Ctrl+Shift pressed together) - only if NOT already visible
290
+ if (
291
+ !isQuickSwitcherVisible &&
292
+ ((event.key === "Shift" && event.ctrlKey) ||
293
+ (event.key === "Control" && event.shiftKey)) &&
294
+ editContextRef.current &&
295
+ browseHistory.length > 1
296
+ ) {
297
+ console.log("[KeyNav] Quick switcher trigger: Ctrl+Shift");
298
+ event.preventDefault();
299
+ event.stopPropagation();
300
+ showQuickSwitcher?.(true);
301
+ return;
302
+ }
303
+
214
304
  const target = event.target as HTMLElement;
215
305
  const isTyping =
216
306
  target instanceof HTMLInputElement ||
@@ -230,7 +320,13 @@ export function useKeyboardNavigation(deps: KeyboardNavigationDependencies) {
230
320
  }
231
321
  handleKeyDownDebounced(event);
232
322
  },
233
- [handleKeyDownDebounced, editContextRef],
323
+ [
324
+ handleKeyDownDebounced,
325
+ editContextRef,
326
+ showQuickSwitcher,
327
+ cycleQuickSwitcher,
328
+ browseHistory,
329
+ ],
234
330
  );
235
331
 
236
332
  return { handleKeyDown };
@@ -38,23 +38,37 @@ export function SingleEditView({
38
38
  item?.templateId?.toLowerCase() ===
39
39
  "fe5dd826-48c6-436d-b87a-7c4210c7413b";
40
40
 
41
- if (isMediaFolder) {
42
- return <MediaFolderEditView item={item} />;
41
+ // Check if this is the media library root item (typically has template ID eb22c1a2-8899-4d2d-b36c-4aeb209e66b7)
42
+ const isMediaLibraryRoot =
43
+ item?.id?.toLowerCase() === "3d6658d8-a0bf-4e75-b3e2-d050fabcf4e1";
44
+
45
+ if (isMediaFolder || isMediaLibraryRoot) {
46
+ return (
47
+ <div data-view="media-folder-edit-view" className="h-full w-full">
48
+ <MediaFolderEditView item={item} />
49
+ </div>
50
+ );
43
51
  }
44
52
 
45
- return <ItemEditor item={item} />;
53
+ return (
54
+ <div data-view="item-editor-view" className="h-full w-full">
55
+ <ItemEditor item={item} />
56
+ </div>
57
+ );
46
58
  }
47
59
 
48
60
  return (
49
- <PageViewer
50
- pageViewContext={pageViewContext}
51
- showFormEditor={
52
- editContext?.viewName == "page-editor" || !editContext?.isMobile
53
- }
54
- compareView={compareView}
55
- name={name}
56
- followEditsDefault={false}
57
- className={className}
58
- />
61
+ <div data-view="single-edit-view" className="h-full w-full">
62
+ <PageViewer
63
+ pageViewContext={pageViewContext}
64
+ showFormEditor={
65
+ editContext?.viewName == "page-editor" || !editContext?.isMobile
66
+ }
67
+ compareView={compareView}
68
+ name={name}
69
+ followEditsDefault={false}
70
+ className={className}
71
+ />
72
+ </div>
59
73
  );
60
74
  }
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.4172";
2
- export const buildDate = "2025-10-17 14:01:33";
1
+ export const version = "1.0.4174";
2
+ export const buildDate = "2025-10-21 01:59:37";