@tutti-os/workspace-file-manager 0.0.94 → 0.0.96

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.d.ts CHANGED
@@ -26,9 +26,11 @@ interface WorkspaceFileManagerProps {
26
26
  hostOs?: NodeJS.Platform;
27
27
  i18n: WorkspaceFileManagerI18nRuntime;
28
28
  session: WorkspaceFileManagerSession;
29
+ showInternalOpenWithActions?: boolean;
30
+ showPreviewPanel?: boolean;
29
31
  surface?: "card" | "embedded";
30
32
  }
31
- declare function WorkspaceFileManager({ className, dateLocale, entryDragMode, i18n, onCopyEntry, onCopyPath, onDirectoryExpanded, onEntryDragStart, openInAppBrowserIcon, resolveOpenWithApplicationIcon, resolveEntryIconUrl, renderExternalLocationContent, hostOs, session, surface }: WorkspaceFileManagerProps): ReactElement;
33
+ declare function WorkspaceFileManager({ className, dateLocale, entryDragMode, i18n, onCopyEntry, onCopyPath, onDirectoryExpanded, onEntryDragStart, openInAppBrowserIcon, resolveOpenWithApplicationIcon, resolveEntryIconUrl, renderExternalLocationContent, hostOs, session, showInternalOpenWithActions, showPreviewPanel, surface }: WorkspaceFileManagerProps): ReactElement;
32
34
 
33
35
  interface WorkspaceFileManagerContextMenuProps {
34
36
  busy: boolean;
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  workspaceFileSearchEntryToEntry,
34
34
  workspaceFileTextMaxBytes,
35
35
  writeWorkspaceFileManagerArrangeMode
36
- } from "./chunk-US5CLBA7.js";
36
+ } from "./chunk-KVP3Z2Y5.js";
37
37
  import {
38
38
  createWorkspaceFileManagerI18nRuntime,
39
39
  resolveRevealInFolderLabel,
@@ -57,6 +57,7 @@ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } fro
57
57
 
58
58
  // src/ui/WorkspaceFileManagerContextMenu.tsx
59
59
  import {
60
+ ArrowLeftIcon,
60
61
  ArrowRightIcon,
61
62
  CopyIcon,
62
63
  DeleteIcon,
@@ -85,6 +86,38 @@ import {
85
86
  var CONTEXT_MENU_ITEM_HEIGHT_PX = 32;
86
87
  var CONTEXT_MENU_PADDING_PX = 8;
87
88
  var CONTEXT_MENU_SUBMENU_GAP_PX = 4;
89
+ var CONTEXT_MENU_VIEWPORT_PADDING_PX = 12;
90
+ var OPEN_WITH_SUBMENU_WIDTH_PX = 220;
91
+ function resolveOpenWithSubmenuPlacement(input) {
92
+ const gap = input.gap ?? CONTEXT_MENU_SUBMENU_GAP_PX;
93
+ const padding = input.padding ?? CONTEXT_MENU_VIEWPORT_PADDING_PX;
94
+ const availableWidth = Math.max(0, input.viewportWidth - padding * 2);
95
+ const width = Math.min(
96
+ input.submenuWidth ?? OPEN_WITH_SUBMENU_WIDTH_PX,
97
+ availableWidth
98
+ );
99
+ const rightLeft = input.triggerRight + gap;
100
+ const leftLeft = input.triggerLeft - gap - width;
101
+ const rightFits = rightLeft + width <= input.viewportWidth - padding;
102
+ const leftFits = leftLeft >= padding;
103
+ const mode = rightFits ? "right" : leftFits ? "left" : "overlay";
104
+ const rawLeft = mode === "right" ? rightLeft : mode === "left" ? leftLeft : input.parentMenuLeft;
105
+ const rawTop = mode === "overlay" ? input.parentMenuTop : input.triggerTop;
106
+ const overlayHeaderHeight = mode === "overlay" ? input.overlayHeaderHeight ?? CONTEXT_MENU_ITEM_HEIGHT_PX : 0;
107
+ const availableHeight = Math.max(0, input.viewportHeight - padding * 2);
108
+ const height = Math.min(
109
+ Math.max(0, input.submenuHeight + overlayHeaderHeight),
110
+ availableHeight
111
+ );
112
+ const maxLeft = Math.max(padding, input.viewportWidth - padding - width);
113
+ const maxTop = Math.max(padding, input.viewportHeight - padding - height);
114
+ return {
115
+ left: Math.max(padding, Math.min(rawLeft, maxLeft)),
116
+ mode,
117
+ top: Math.max(padding, Math.min(rawTop, maxTop)),
118
+ width
119
+ };
120
+ }
88
121
  function clampContextMenuPosition(input) {
89
122
  const padding = input.padding ?? CONTEXT_MENU_PADDING_PX;
90
123
  const maxX = Math.max(
@@ -108,7 +141,7 @@ function estimateOpenWithSubmenuHeight(input) {
108
141
  if (input.showOpenInAppBrowser) {
109
142
  itemCount += 1;
110
143
  }
111
- if (input.showExternalSection) {
144
+ if (shouldShowOpenWithSectionDivider(input)) {
112
145
  itemCount += 1;
113
146
  }
114
147
  if (input.isLoading) {
@@ -123,6 +156,9 @@ function estimateOpenWithSubmenuHeight(input) {
123
156
  }
124
157
  return itemCount * CONTEXT_MENU_ITEM_HEIGHT_PX + CONTEXT_MENU_PADDING_PX * 2;
125
158
  }
159
+ function shouldShowOpenWithSectionDivider(input) {
160
+ return input.showExternalSection && (input.showOpenInFileViewer === true || input.showOpenInAppBrowser);
161
+ }
126
162
 
127
163
  // src/ui/WorkspaceFileManagerContextMenu.tsx
128
164
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -337,6 +373,7 @@ function WorkspaceFileManagerContextMenu({
337
373
  MenuSurface,
338
374
  {
339
375
  ref: contextMenuRef,
376
+ "data-workspace-file-manager-context-menu": "",
340
377
  className: cn(
341
378
  "w-[220px] overflow-visible p-1",
342
379
  positionMode === "viewport" ? "fixed" : "absolute"
@@ -512,10 +549,23 @@ function OpenWithMenuItem({
512
549
  onOpenWithOtherApplication
513
550
  }) {
514
551
  const triggerRef = useRef(null);
552
+ const triggerButtonRef = useRef(null);
553
+ const submenuRef = useRef(null);
554
+ const focusSubmenuOnOpenRef = useRef(false);
515
555
  const [open, setOpen] = useState(false);
516
- const [submenuPosition, setSubmenuPosition] = useState({ left: 0, top: 0 });
556
+ const [submenuPosition, setSubmenuPosition] = useState({
557
+ left: 0,
558
+ mode: "right",
559
+ top: 0,
560
+ width: OPEN_WITH_SUBMENU_WIDTH_PX
561
+ });
517
562
  const closeTimerRef = useRef(null);
518
563
  const showExternalSection = showOpenInDefaultBrowser || showOpenWithOther || isLoading || applications.length > 0;
564
+ const showSectionDivider = shouldShowOpenWithSectionDivider({
565
+ showExternalSection,
566
+ showOpenInAppBrowser,
567
+ showOpenInFileViewer
568
+ });
519
569
  const estimatedSubmenuHeight = estimateOpenWithSubmenuHeight({
520
570
  applicationCount: applications.length,
521
571
  isLoading,
@@ -539,9 +589,40 @@ function OpenWithMenuItem({
539
589
  }, 120);
540
590
  }, [cancelClose]);
541
591
  const openSubmenu = useCallback(() => {
592
+ focusSubmenuOnOpenRef.current = false;
542
593
  cancelClose();
543
594
  setOpen(true);
544
595
  }, [cancelClose]);
596
+ const closeSubmenuToTrigger = useCallback(() => {
597
+ cancelClose();
598
+ setOpen(false);
599
+ triggerButtonRef.current?.focus();
600
+ }, [cancelClose]);
601
+ const updateSubmenuPosition = useCallback(() => {
602
+ const trigger = triggerRef.current;
603
+ if (!trigger) {
604
+ return;
605
+ }
606
+ const triggerRect = trigger.getBoundingClientRect();
607
+ const parentMenuRect = trigger.closest("[data-workspace-file-manager-context-menu]")?.getBoundingClientRect() ?? triggerRect;
608
+ const maxHeight = Math.min(
609
+ estimatedSubmenuHeight,
610
+ 480,
611
+ Math.max(0, window.innerHeight - 24)
612
+ );
613
+ setSubmenuPosition(
614
+ resolveOpenWithSubmenuPlacement({
615
+ parentMenuLeft: parentMenuRect.left,
616
+ parentMenuTop: parentMenuRect.top,
617
+ submenuHeight: maxHeight,
618
+ triggerLeft: triggerRect.left,
619
+ triggerRight: triggerRect.right,
620
+ triggerTop: triggerRect.top,
621
+ viewportHeight: window.innerHeight,
622
+ viewportWidth: window.innerWidth
623
+ })
624
+ );
625
+ }, [estimatedSubmenuHeight]);
545
626
  useEffect(() => {
546
627
  return () => {
547
628
  cancelClose();
@@ -551,21 +632,31 @@ function OpenWithMenuItem({
551
632
  if (!open) {
552
633
  return;
553
634
  }
554
- const trigger = triggerRef.current;
555
- if (!trigger) {
635
+ updateSubmenuPosition();
636
+ }, [open, updateSubmenuPosition]);
637
+ useEffect(() => {
638
+ if (!open) {
556
639
  return;
557
640
  }
558
- const rect = trigger.getBoundingClientRect();
559
- const maxHeight = Math.min(
560
- estimatedSubmenuHeight,
561
- 480,
562
- Math.max(0, window.innerHeight - 24)
563
- );
564
- setSubmenuPosition({
565
- left: rect.right + CONTEXT_MENU_SUBMENU_GAP_PX,
566
- top: Math.max(12, Math.min(rect.top, window.innerHeight - maxHeight - 12))
641
+ window.addEventListener("resize", updateSubmenuPosition);
642
+ window.addEventListener("scroll", updateSubmenuPosition, true);
643
+ return () => {
644
+ window.removeEventListener("resize", updateSubmenuPosition);
645
+ window.removeEventListener("scroll", updateSubmenuPosition, true);
646
+ };
647
+ }, [open, updateSubmenuPosition]);
648
+ useEffect(() => {
649
+ if (!open || !focusSubmenuOnOpenRef.current) {
650
+ return;
651
+ }
652
+ focusSubmenuOnOpenRef.current = false;
653
+ const frame = window.requestAnimationFrame(() => {
654
+ submenuRef.current?.querySelector("button:not(:disabled)")?.focus();
567
655
  });
568
- }, [open, estimatedSubmenuHeight]);
656
+ return () => {
657
+ window.cancelAnimationFrame(frame);
658
+ };
659
+ }, [open, submenuPosition.mode]);
569
660
  return /* @__PURE__ */ jsxs(
570
661
  "div",
571
662
  {
@@ -577,6 +668,7 @@ function OpenWithMenuItem({
577
668
  /* @__PURE__ */ jsxs(
578
669
  "button",
579
670
  {
671
+ ref: triggerButtonRef,
580
672
  "aria-expanded": open,
581
673
  "aria-haspopup": "menu",
582
674
  className: cn(
@@ -586,15 +678,28 @@ function OpenWithMenuItem({
586
678
  disabled: busy,
587
679
  role: "menuitem",
588
680
  type: "button",
589
- onClick: () => {
681
+ onClick: (event) => {
682
+ focusSubmenuOnOpenRef.current = event.detail === 0;
590
683
  setOpen((current) => {
591
684
  const next = !current;
592
685
  if (next) {
593
686
  cancelClose();
687
+ } else {
688
+ focusSubmenuOnOpenRef.current = false;
594
689
  }
595
690
  return next;
596
691
  });
597
692
  },
693
+ onKeyDown: (event) => {
694
+ if (event.key !== "ArrowRight") {
695
+ return;
696
+ }
697
+ event.preventDefault();
698
+ event.stopPropagation();
699
+ focusSubmenuOnOpenRef.current = true;
700
+ cancelClose();
701
+ setOpen(true);
702
+ },
598
703
  children: [
599
704
  /* @__PURE__ */ jsx("span", { className: "grid size-4 flex-none place-items-center text-[var(--text-secondary)]", children: /* @__PURE__ */ jsx(LaunchIcon, { className: "size-4" }) }),
600
705
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: copy.t("openWithLabel") }),
@@ -612,9 +717,11 @@ function OpenWithMenuItem({
612
717
  /* @__PURE__ */ jsxs(
613
718
  ViewportMenuSurface,
614
719
  {
615
- "data-workspace-file-manager-submenu": "",
720
+ ref: submenuRef,
721
+ "aria-label": copy.t("openWithLabel"),
722
+ "data-workspace-file-manager-submenu-mode": submenuPosition.mode,
616
723
  open,
617
- className: "w-[220px] max-h-[min(480px,calc(100vh-24px))] overflow-y-auto p-1",
724
+ className: "max-h-[min(480px,calc(100vh-24px))] max-w-[calc(100vw-24px)] overflow-y-auto p-1",
618
725
  dismissOnEscape: false,
619
726
  dismissOnPointerDownOutside: false,
620
727
  dismissOnScroll: false,
@@ -622,14 +729,55 @@ function OpenWithMenuItem({
622
729
  type: "absolute",
623
730
  left: submenuPosition.left,
624
731
  top: submenuPosition.top,
625
- boundaryPoint: { x: -1, y: -1 },
626
- constrainToBoundary: false
732
+ boundaryPoint: { x: -1, y: -1 }
627
733
  },
628
734
  role: "menu",
629
- style: { zIndex: "calc(var(--z-panel-popover) + 200)" },
735
+ style: {
736
+ width: submenuPosition.width,
737
+ zIndex: "calc(var(--z-panel-popover) + 200)"
738
+ },
630
739
  onPointerEnter: openSubmenu,
631
740
  onPointerLeave: scheduleClose,
741
+ onKeyDown: (event) => {
742
+ if (event.key === "ArrowLeft" || event.key === "Escape") {
743
+ event.preventDefault();
744
+ event.stopPropagation();
745
+ closeSubmenuToTrigger();
746
+ return;
747
+ }
748
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
749
+ return;
750
+ }
751
+ const items = Array.from(
752
+ event.currentTarget.querySelectorAll(
753
+ "button:not(:disabled)"
754
+ )
755
+ );
756
+ if (items.length === 0) {
757
+ return;
758
+ }
759
+ event.preventDefault();
760
+ event.stopPropagation();
761
+ const currentIndex = items.indexOf(
762
+ document.activeElement
763
+ );
764
+ const offset = event.key === "ArrowDown" ? 1 : -1;
765
+ const nextIndex = currentIndex < 0 ? offset > 0 ? 0 : items.length - 1 : (currentIndex + offset + items.length) % items.length;
766
+ items[nextIndex]?.focus();
767
+ },
632
768
  children: [
769
+ submenuPosition.mode === "overlay" ? /* @__PURE__ */ jsxs(Fragment, { children: [
770
+ /* @__PURE__ */ jsx(
771
+ ContextMenuActionButton,
772
+ {
773
+ activateOnPointerDown: true,
774
+ icon: /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-4" }),
775
+ label: copy.t("openWithLabel"),
776
+ onClick: closeSubmenuToTrigger
777
+ }
778
+ ),
779
+ /* @__PURE__ */ jsx(ContextMenuDivider, {})
780
+ ] }) : null,
633
781
  showOpenInFileViewer ? /* @__PURE__ */ jsx(
634
782
  ContextMenuActionButton,
635
783
  {
@@ -658,7 +806,7 @@ function OpenWithMenuItem({
658
806
  }
659
807
  }
660
808
  ) : null,
661
- showExternalSection ? /* @__PURE__ */ jsx(ContextMenuDivider, {}) : null,
809
+ showSectionDivider ? /* @__PURE__ */ jsx(ContextMenuDivider, {}) : null,
662
810
  isLoading ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-[11px] text-[var(--text-tertiary)]", children: copy.t("openWithLoadingLabel") }) : null,
663
811
  applications.map((application) => {
664
812
  const resolvedIcon = resolveOpenWithApplicationIcon?.(application);
@@ -941,7 +1089,8 @@ function WorkspaceFileManagerContextMenuContainer({
941
1089
  onCopyPath,
942
1090
  openInAppBrowserIcon,
943
1091
  resolveOpenWithApplicationIcon,
944
- session
1092
+ session,
1093
+ showInternalOpenWithActions
945
1094
  }) {
946
1095
  const contextMenuRef = useRef2(null);
947
1096
  const { view } = useWorkspaceFileManagerContextMenuView(session);
@@ -998,9 +1147,9 @@ function WorkspaceFileManagerContextMenuContainer({
998
1147
  showDeleteAction: view.showDeleteAction,
999
1148
  showImportAction: view.showImportAction,
1000
1149
  showExportAction: view.showExportAction,
1001
- showOpenInAppBrowserAction: view.showOpenInAppBrowserAction && !!view.contextMenu?.entry && isWorkspaceFileBrowserOpenable(view.contextMenu.entry),
1150
+ showOpenInAppBrowserAction: showInternalOpenWithActions && view.showOpenInAppBrowserAction && !!view.contextMenu?.entry && isWorkspaceFileBrowserOpenable(view.contextMenu.entry),
1002
1151
  showOpenInDefaultBrowserAction: view.showOpenInDefaultBrowserAction && !!view.contextMenu?.entry && isWorkspaceFileBrowserOpenable(view.contextMenu.entry),
1003
- showOpenInFileViewerAction: view.showOpenInFileViewerAction,
1152
+ showOpenInFileViewerAction: showInternalOpenWithActions && view.showOpenInFileViewerAction,
1004
1153
  showOpenWithAction: view.showOpenWithAction,
1005
1154
  showOpenWithOtherAction: view.showOpenWithOtherAction,
1006
1155
  showRevealInFolderAction: view.showRevealInFolderAction,
@@ -2096,6 +2245,7 @@ function WorkspaceFileManagerPanels({
2096
2245
  onEntryIconViewportEnter,
2097
2246
  selectedEntry,
2098
2247
  selectedPath,
2248
+ showPreviewPanel: showPreviewPanelEnabled = true,
2099
2249
  state,
2100
2250
  showDropOverlay,
2101
2251
  treeRows,
@@ -2325,7 +2475,7 @@ function WorkspaceFileManagerPanels({
2325
2475
  stopMoveDragAutoScroll,
2326
2476
  updateMoveDragAutoScroll
2327
2477
  ]);
2328
- const showPreviewPanel = layoutMode === "list";
2478
+ const showPreviewPanel = showPreviewPanelEnabled && layoutMode === "list";
2329
2479
  const useStackedPreview = false;
2330
2480
  const previewPaneClassName = useStackedPreview ? "min-w-0 border-t border-[var(--border-1)]" : "min-w-[220px] border-l border-[var(--border-1)]";
2331
2481
  const tableGridClassName = useStackedPreview ? workspaceFileManagerCompactTableGridClassName : workspaceFileManagerTableGridClassName;
@@ -3290,7 +3440,7 @@ function hasFileDragPayload(dataTransfer) {
3290
3440
 
3291
3441
  // src/ui/WorkspaceFileManagerToolbar.tsx
3292
3442
  import {
3293
- ArrowLeftIcon,
3443
+ ArrowLeftIcon as ArrowLeftIcon2,
3294
3444
  ArrowRightIcon as ArrowRightIcon3,
3295
3445
  Button as Button2,
3296
3446
  ChevronDownIcon,
@@ -3351,7 +3501,7 @@ function WorkspaceFileManagerToolbar({
3351
3501
  disabled: !canGoBack || isLoading || isBusy,
3352
3502
  title: copy.t("backLabel"),
3353
3503
  onClick: onGoBack,
3354
- children: /* @__PURE__ */ jsx7(ArrowLeftIcon, { className: "size-4" })
3504
+ children: /* @__PURE__ */ jsx7(ArrowLeftIcon2, { className: "size-4" })
3355
3505
  }
3356
3506
  ),
3357
3507
  /* @__PURE__ */ jsx7(
@@ -4205,6 +4355,8 @@ function WorkspaceFileManager({
4205
4355
  renderExternalLocationContent,
4206
4356
  hostOs = "linux",
4207
4357
  session,
4358
+ showInternalOpenWithActions = true,
4359
+ showPreviewPanel = true,
4208
4360
  surface = "card"
4209
4361
  }) {
4210
4362
  const rootRef = useRef7(null);
@@ -4563,7 +4715,8 @@ function WorkspaceFileManager({
4563
4715
  onEntryDragStart,
4564
4716
  onOpenContextMenu: openContextMenu,
4565
4717
  resolveEntryIconUrl,
4566
- session
4718
+ session,
4719
+ showPreviewPanel
4567
4720
  }
4568
4721
  )
4569
4722
  }
@@ -4580,7 +4733,8 @@ function WorkspaceFileManager({
4580
4733
  onCopyPath,
4581
4734
  openInAppBrowserIcon,
4582
4735
  resolveOpenWithApplicationIcon,
4583
- session
4736
+ session,
4737
+ showInternalOpenWithActions
4584
4738
  }
4585
4739
  )
4586
4740
  ] }) : null
@@ -4680,7 +4834,8 @@ function WorkspaceFileManagerPanelsContainer({
4680
4834
  onEntryDragStart,
4681
4835
  onOpenContextMenu,
4682
4836
  resolveEntryIconUrl,
4683
- session
4837
+ session,
4838
+ showPreviewPanel
4684
4839
  }) {
4685
4840
  const { state, view } = useWorkspaceFileManagerPanelsView(session);
4686
4841
  const arrangedEntries = useMemo3(
@@ -4760,6 +4915,7 @@ function WorkspaceFileManagerPanelsContainer({
4760
4915
  onEntryIconViewportLeave: reportEntryIconViewportLeave,
4761
4916
  selectedEntry: view.selectedEntry,
4762
4917
  selectedPath: view.selectedPath,
4918
+ showPreviewPanel,
4763
4919
  showDropOverlay: view.showDropOverlay,
4764
4920
  state: {
4765
4921
  entries: displayedEntries,