@tutti-os/workspace-file-manager 0.0.210 → 0.0.212

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/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  splitWorkspaceFileName,
22
22
  workspaceFileSearchEntryToEntry,
23
23
  writeWorkspaceFileManagerArrangeMode
24
- } from "./chunk-TNFCNCZV.js";
24
+ } from "./chunk-KCYTFKPY.js";
25
25
  import {
26
26
  createWorkspaceFileManagerI18nRuntime,
27
27
  resolveRevealInFolderLabel,
@@ -59,6 +59,7 @@ import {
59
59
  useRef,
60
60
  useState
61
61
  } from "react";
62
+ import { createPortal } from "react-dom";
62
63
 
63
64
  // src/ui/contextMenuPlacement.ts
64
65
  var CONTEXT_MENU_ITEM_HEIGHT_PX = 32;
@@ -155,52 +156,60 @@ function WorkspaceFileManagerContextMenu({
155
156
  if (!contextMenu) {
156
157
  return;
157
158
  }
158
- setPosition({ x: contextMenu.x, y: contextMenu.y });
159
+ setPosition(
160
+ (current) => current.x === contextMenu.x && current.y === contextMenu.y ? current : { x: contextMenu.x, y: contextMenu.y }
161
+ );
159
162
  }, [contextMenu?.x, contextMenu?.y]);
160
163
  useLayoutEffect(() => {
161
164
  if (!contextMenu) {
162
165
  return;
163
166
  }
164
- const menu = contextMenuRef.current;
165
- if (!menu) {
167
+ const menu2 = contextMenuRef.current;
168
+ if (!menu2) {
166
169
  return;
167
170
  }
168
- const menuRect = menu.getBoundingClientRect();
169
- const boundary = positionMode === "local" ? menu.closest(
171
+ const menuRect = menu2.getBoundingClientRect();
172
+ const boundary = positionMode === "local" ? menu2.closest(
170
173
  "[data-workspace-file-menu-boundary], [data-workspace-file-manager]"
171
174
  ) : null;
172
175
  if (positionMode === "local" && !boundary) {
173
176
  return;
174
177
  }
175
178
  const boundaryRect = boundary?.getBoundingClientRect();
179
+ const next = clampContextMenuPosition({
180
+ boundaryHeight: boundaryRect?.height ?? window.innerHeight,
181
+ boundaryWidth: boundaryRect?.width ?? window.innerWidth,
182
+ menuHeight: menuRect.height,
183
+ menuWidth: menuRect.width,
184
+ x: contextMenu.x,
185
+ y: contextMenu.y
186
+ });
176
187
  setPosition(
177
- clampContextMenuPosition({
178
- boundaryHeight: boundaryRect?.height ?? window.innerHeight,
179
- boundaryWidth: boundaryRect?.width ?? window.innerWidth,
180
- menuHeight: menuRect.height,
181
- menuWidth: menuRect.width,
182
- x: contextMenu.x,
183
- y: contextMenu.y
184
- })
188
+ (current) => current.x === next.x && current.y === next.y ? current : next
185
189
  );
186
- }, [contextMenu, contextMenuRef, items, positionMode]);
190
+ }, [contextMenu?.x, contextMenu?.y, contextMenuRef, items, positionMode]);
187
191
  if (!contextMenu || items.length === 0) {
188
192
  return null;
189
193
  }
190
- return /* @__PURE__ */ jsx(
194
+ const menu = /* @__PURE__ */ jsx(
191
195
  MenuSurface,
192
196
  {
193
197
  ref: contextMenuRef,
194
198
  "data-workspace-file-manager-context-menu": "",
195
199
  className: cn(
196
- "w-[220px] overflow-visible p-1",
200
+ // `is-open` keeps host CSS that keys off the class (not only data-state)
201
+ // from leaving the surface at opacity:0.
202
+ "is-open w-[220px] overflow-visible p-1",
197
203
  positionMode === "viewport" ? "fixed" : "absolute"
198
204
  ),
199
205
  role: "menu",
200
206
  style: {
201
207
  left: `${position.x}px`,
202
208
  top: `${position.y}px`,
203
- zIndex: "calc(var(--workspace-file-manager-dialog-overlay-z-index) - 1)"
209
+ // Prefer the host CSS var when present; fall back to a stable stacking
210
+ // value so an unset var cannot invalidate the entire z-index declaration.
211
+ // Viewport menus need to clear typical workbench chrome (≈ z-10..50).
212
+ zIndex: positionMode === "viewport" ? "var(--workspace-file-manager-dialog-overlay-z-index, 10050)" : "var(--workspace-file-manager-dialog-overlay-z-index, 20)"
204
213
  },
205
214
  onContextMenu: (event) => {
206
215
  event.preventDefault();
@@ -208,6 +217,10 @@ function WorkspaceFileManagerContextMenu({
208
217
  children: items.map((item) => /* @__PURE__ */ jsx(ContextMenuItemRenderer, { item, onClose }, item.id))
209
218
  }
210
219
  );
220
+ if (positionMode === "viewport" && typeof document !== "undefined") {
221
+ return createPortal(menu, document.body);
222
+ }
223
+ return menu;
211
224
  }
212
225
  function ContextMenuItemRenderer({
213
226
  item,
@@ -791,9 +804,12 @@ function WorkspaceFileManagerContextMenuContainer({
791
804
  const contextMenuRef = useRef2(null);
792
805
  const { view } = useWorkspaceFileManagerContextMenuView(session);
793
806
  const [items, setItems] = useState2([]);
807
+ const contextMenuX = view.contextMenu?.x;
808
+ const contextMenuY = view.contextMenu?.y;
809
+ const contextMenuEntryPath = view.contextMenu?.entry?.path ?? null;
794
810
  useLayoutEffect2(() => {
795
- if (!view.contextMenu) {
796
- setItems([]);
811
+ if (contextMenuX === void 0 || contextMenuY === void 0) {
812
+ setItems((current) => current.length === 0 ? current : []);
797
813
  return;
798
814
  }
799
815
  const request = buildContextMenuRequest({
@@ -802,23 +818,29 @@ function WorkspaceFileManagerContextMenuContainer({
802
818
  });
803
819
  const resolved = resolveContextMenu(request);
804
820
  if (!isPromiseLike(resolved)) {
805
- setItems(resolved);
821
+ setItems(
822
+ (current) => areContextMenuItemsEquivalent(current, resolved) ? current : resolved
823
+ );
806
824
  return;
807
825
  }
808
826
  let cancelled = false;
809
- setItems([]);
827
+ setItems((current) => current.length === 0 ? current : []);
810
828
  void resolved.then((nextItems) => {
811
829
  if (!cancelled) {
812
- setItems(nextItems);
830
+ setItems(
831
+ (current) => areContextMenuItemsEquivalent(current, nextItems) ? current : nextItems
832
+ );
813
833
  }
814
834
  });
815
835
  return () => {
816
836
  cancelled = true;
817
837
  };
818
838
  }, [
839
+ contextMenuEntryPath,
840
+ contextMenuX,
841
+ contextMenuY,
819
842
  resolveContextMenu,
820
843
  session,
821
- view.contextMenu,
822
844
  view.currentDirectoryPath,
823
845
  view.isBusy,
824
846
  view.isLoading,
@@ -838,6 +860,7 @@ function WorkspaceFileManagerContextMenuContainer({
838
860
  contextMenu: { x: view.contextMenu.x, y: view.contextMenu.y },
839
861
  contextMenuRef,
840
862
  items,
863
+ positionMode: "viewport",
841
864
  onClose: () => {
842
865
  session.closeContextMenu();
843
866
  }
@@ -866,6 +889,35 @@ function buildContextMenuRequest(input) {
866
889
  function isPromiseLike(value) {
867
890
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
868
891
  }
892
+ function areContextMenuItemsEquivalent(left, right) {
893
+ if (left === right) {
894
+ return true;
895
+ }
896
+ if (left.length !== right.length) {
897
+ return false;
898
+ }
899
+ for (let index = 0; index < left.length; index += 1) {
900
+ const leftItem = left[index];
901
+ const rightItem = right[index];
902
+ if (!leftItem || !rightItem || leftItem.id !== rightItem.id) {
903
+ return false;
904
+ }
905
+ if (leftItem.type !== rightItem.type) {
906
+ return false;
907
+ }
908
+ if (leftItem.type === "item" && rightItem.type === "item") {
909
+ if (leftItem.label !== rightItem.label || leftItem.disabled !== rightItem.disabled || leftItem.danger !== rightItem.danger) {
910
+ return false;
911
+ }
912
+ }
913
+ if (leftItem.type === "submenu" && rightItem.type === "submenu") {
914
+ if (leftItem.label !== rightItem.label || leftItem.disabled !== rightItem.disabled || leftItem.loading !== rightItem.loading) {
915
+ return false;
916
+ }
917
+ }
918
+ }
919
+ return true;
920
+ }
869
921
  function useCloseContextMenuOnOutsideInteraction(input) {
870
922
  const { contextMenuRef, isOpen, session } = input;
871
923
  useEffect2(() => {
@@ -3839,6 +3891,7 @@ function WorkspaceFileManager({
3839
3891
  resolveEntryIconUrl,
3840
3892
  renderExternalLocationContent,
3841
3893
  session,
3894
+ showLocationSidebar = true,
3842
3895
  showPreviewPanel = true,
3843
3896
  surface = "card"
3844
3897
  }) {
@@ -3859,9 +3912,7 @@ function WorkspaceFileManager({
3859
3912
  );
3860
3913
  return location?.kind === "external" ? location : null;
3861
3914
  }, [rootView.locationSections, rootView.selectedLocationId]);
3862
- const hasLocationSidebar = rootView.locationSections.some(
3863
- (section) => section.locations.length > 0
3864
- );
3915
+ const hasLocationSidebar = showLocationSidebar && rootView.locationSections.some((section) => section.locations.length > 0);
3865
3916
  const sidebarContentMinWidth = showPreviewPanel ? workspaceFileManagerContentMinWidth : workspaceFileManagerContentWithoutPreviewMinWidth;
3866
3917
  const sidebarMaxWidth = containerWidth > 0 ? resolveWorkspaceFileManagerSidebarMaxWidth(
3867
3918
  containerWidth,
@@ -4059,20 +4110,16 @@ function WorkspaceFileManager({
4059
4110
  function openContextMenu(event, entry) {
4060
4111
  event.preventDefault();
4061
4112
  event.stopPropagation();
4062
- const rootBounds = rootRef.current?.getBoundingClientRect();
4063
- if (!rootBounds) {
4064
- return;
4065
- }
4066
4113
  const menuWidth = 220;
4067
4114
  const menuHeight = 280;
4068
4115
  const x = clampContextMenuCoordinate(
4069
- event.clientX - rootBounds.left,
4070
- rootBounds.width,
4116
+ event.clientX,
4117
+ window.innerWidth,
4071
4118
  menuWidth
4072
4119
  );
4073
4120
  const y = clampContextMenuCoordinate(
4074
- event.clientY - rootBounds.top,
4075
- rootBounds.height,
4121
+ event.clientY,
4122
+ window.innerHeight,
4076
4123
  menuHeight
4077
4124
  );
4078
4125
  session.openContextMenu({
@@ -4092,8 +4139,14 @@ function WorkspaceFileManager({
4092
4139
  "data-slot": "viewport-menu-boundary",
4093
4140
  "data-workspace-file-manager": "",
4094
4141
  ref: rootRef,
4142
+ style: {
4143
+ // Owned by the root so context menus (siblings of the panels pane)
4144
+ // can resolve overlay stacking instead of falling back to invalid
4145
+ // `calc(var(--undefined) - 1)`.
4146
+ "--workspace-file-manager-dialog-overlay-z-index": "20"
4147
+ },
4095
4148
  children: [
4096
- /* @__PURE__ */ jsx9(
4149
+ hasLocationSidebar ? /* @__PURE__ */ jsx9(
4097
4150
  WorkspaceFileManagerSidebar,
4098
4151
  {
4099
4152
  disabled: rootView.isBusy || panelsState.isLoading,
@@ -4107,7 +4160,7 @@ function WorkspaceFileManager({
4107
4160
  void session.selectLocation(location.id);
4108
4161
  }
4109
4162
  }
4110
- ),
4163
+ ) : null,
4111
4164
  hasLocationSidebar ? /* @__PURE__ */ jsx9(
4112
4165
  "div",
4113
4166
  {
@@ -4139,31 +4192,22 @@ function WorkspaceFileManager({
4139
4192
  session
4140
4193
  }
4141
4194
  ),
4142
- /* @__PURE__ */ jsx9(
4143
- "div",
4195
+ /* @__PURE__ */ jsx9("div", { className: "@max-[600px]/workspace-file-manager:flex-col @max-[600px]/workspace-file-manager:gap-3 flex min-h-0 min-w-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx9(
4196
+ WorkspaceFileManagerPanelsContainer,
4144
4197
  {
4145
- className: "@max-[600px]/workspace-file-manager:flex-col @max-[600px]/workspace-file-manager:gap-3 flex min-h-0 min-w-0 flex-1 overflow-hidden",
4146
- style: {
4147
- "--workspace-file-manager-dialog-overlay-z-index": "20"
4148
- },
4149
- children: /* @__PURE__ */ jsx9(
4150
- WorkspaceFileManagerPanelsContainer,
4151
- {
4152
- dateLocale,
4153
- entryDragMode,
4154
- arrangeMode,
4155
- i18n,
4156
- layoutMode,
4157
- onDirectoryExpanded,
4158
- onEntryDragStart,
4159
- onOpenContextMenu: openContextMenu,
4160
- resolveEntryIconUrl,
4161
- session,
4162
- showPreviewPanel
4163
- }
4164
- )
4198
+ dateLocale,
4199
+ entryDragMode,
4200
+ arrangeMode,
4201
+ i18n,
4202
+ layoutMode,
4203
+ onDirectoryExpanded,
4204
+ onEntryDragStart,
4205
+ onOpenContextMenu: openContextMenu,
4206
+ resolveEntryIconUrl,
4207
+ session,
4208
+ showPreviewPanel
4165
4209
  }
4166
- )
4210
+ ) })
4167
4211
  ] }) }),
4168
4212
  !selectedExternalLocation ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
4169
4213
  /* @__PURE__ */ jsx9(WorkspaceFileManagerDialogsContainer, { i18n, session }),