@tutti-os/workspace-file-manager 0.0.95 → 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,10 +26,11 @@ interface WorkspaceFileManagerProps {
26
26
  hostOs?: NodeJS.Platform;
27
27
  i18n: WorkspaceFileManagerI18nRuntime;
28
28
  session: WorkspaceFileManagerSession;
29
+ showInternalOpenWithActions?: boolean;
29
30
  showPreviewPanel?: boolean;
30
31
  surface?: "card" | "embedded";
31
32
  }
32
- declare function WorkspaceFileManager({ className, dateLocale, entryDragMode, i18n, onCopyEntry, onCopyPath, onDirectoryExpanded, onEntryDragStart, openInAppBrowserIcon, resolveOpenWithApplicationIcon, resolveEntryIconUrl, renderExternalLocationContent, hostOs, session, showPreviewPanel, 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;
33
34
 
34
35
  interface WorkspaceFileManagerContextMenuProps {
35
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,
@@ -3291,7 +3440,7 @@ function hasFileDragPayload(dataTransfer) {
3291
3440
 
3292
3441
  // src/ui/WorkspaceFileManagerToolbar.tsx
3293
3442
  import {
3294
- ArrowLeftIcon,
3443
+ ArrowLeftIcon as ArrowLeftIcon2,
3295
3444
  ArrowRightIcon as ArrowRightIcon3,
3296
3445
  Button as Button2,
3297
3446
  ChevronDownIcon,
@@ -3352,7 +3501,7 @@ function WorkspaceFileManagerToolbar({
3352
3501
  disabled: !canGoBack || isLoading || isBusy,
3353
3502
  title: copy.t("backLabel"),
3354
3503
  onClick: onGoBack,
3355
- children: /* @__PURE__ */ jsx7(ArrowLeftIcon, { className: "size-4" })
3504
+ children: /* @__PURE__ */ jsx7(ArrowLeftIcon2, { className: "size-4" })
3356
3505
  }
3357
3506
  ),
3358
3507
  /* @__PURE__ */ jsx7(
@@ -4206,6 +4355,7 @@ function WorkspaceFileManager({
4206
4355
  renderExternalLocationContent,
4207
4356
  hostOs = "linux",
4208
4357
  session,
4358
+ showInternalOpenWithActions = true,
4209
4359
  showPreviewPanel = true,
4210
4360
  surface = "card"
4211
4361
  }) {
@@ -4583,7 +4733,8 @@ function WorkspaceFileManager({
4583
4733
  onCopyPath,
4584
4734
  openInAppBrowserIcon,
4585
4735
  resolveOpenWithApplicationIcon,
4586
- session
4736
+ session,
4737
+ showInternalOpenWithActions
4587
4738
  }
4588
4739
  )
4589
4740
  ] }) : null