|
6 | 6 | * The store keeps per-path note content (`noteContents`) shared across |
7 | 7 | * all panes, so the same note open in two panes stays in sync on edit. |
8 | 8 | */ |
| 9 | +import { createPortal } from 'react-dom' |
9 | 10 | import { |
10 | 11 | Fragment, |
11 | 12 | useCallback, |
@@ -98,7 +99,7 @@ import { autocompletion } from '@codemirror/autocomplete' |
98 | 99 | import { useStore } from '../store' |
99 | 100 | import type { LineNumberMode } from '../store' |
100 | 101 | import type { PaneEdge, PaneLeaf } from '../lib/pane-layout' |
101 | | -import { findLeaf, inferPaneDropEdge } from '../lib/pane-layout' |
| 102 | +import { findLeaf, allLeaves, inferPaneDropEdge } from '../lib/pane-layout' |
102 | 103 | import { livePreviewPlugin } from '../lib/cm-live-preview' |
103 | 104 | import { codeBlockFlairPlugin } from '../lib/cm-code-block-flair' |
104 | 105 | import { tablePlugin, tableVimEntry } from '../lib/cm-table' |
@@ -870,6 +871,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
870 | 871 | const textFont = useStore((s) => s.textFont) |
871 | 872 | const tabsEnabled = useStore((s) => s.tabsEnabled) |
872 | 873 | const wrapTabs = useStore((s) => s.wrapTabs) |
| 874 | + const titlebarTabs = useStore((s) => s.titlebarTabs) |
| 875 | + const isSinglePane = useStore((s) => allLeaves(s.paneLayout).length === 1) |
873 | 876 | const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote) |
874 | 877 | const jumpToNextNote = useStore((s) => s.jumpToNextNote) |
875 | 878 | const canGoBack = useStore((s) => s.noteBackstack.length > 0) |
@@ -3425,11 +3428,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3425 | 3428 | el?.scrollIntoView({ inline: 'nearest', block: 'nearest' }) |
3426 | 3429 | }, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey]) |
3427 | 3430 |
|
3428 | | - // Outer header holds the back/forward nav buttons + the (flex-1) tab strip. |
3429 | | - const tabStripHeaderClass = [ |
3430 | | - 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', |
3431 | | - wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]' |
3432 | | - ].join(' ') |
| 3431 | + const titlebarTabsActive = titlebarTabs && isSinglePane && hasTabs |
| 3432 | + const tabStripHeaderClass = titlebarTabsActive |
| 3433 | + ? 'titlebar-tab-strip flex shrink-0 items-stretch' |
| 3434 | + : [ |
| 3435 | + 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', |
| 3436 | + wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]' |
| 3437 | + ].join(' ') |
3433 | 3438 | const tabStripClass = [ |
3434 | 3439 | 'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0', |
3435 | 3440 | wrapTabs |
@@ -3650,6 +3655,72 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3650 | 3655 | isActive ? '' : 'opacity-[0.98]' |
3651 | 3656 | ].join(' ') |
3652 | 3657 |
|
| 3658 | + const tabStripHost = |
| 3659 | + typeof document !== 'undefined' ? document.getElementById('titlebar-tabs-host') : null |
| 3660 | + const tabStrip = ( |
| 3661 | + <div className={tabStripHeaderClass}> |
| 3662 | + <div className="flex shrink-0 items-center gap-0.5 self-center"> |
| 3663 | + {!titlebarTabsActive && !sidebarOpen && ( |
| 3664 | + <IconBtn |
| 3665 | + title="Show sidebar (⌘1)" |
| 3666 | + onClick={toggleSidebar} |
| 3667 | + tooltipAlign="left" |
| 3668 | + > |
| 3669 | + <PanelLeftIcon width={16} height={16} /> |
| 3670 | + </IconBtn> |
| 3671 | + )} |
| 3672 | + <IconBtn |
| 3673 | + title={`Go back (${getKeymapDisplay( |
| 3674 | + tabNavOverrides, |
| 3675 | + vimMode ? 'vim.historyBack' : 'global.historyBack' |
| 3676 | + )})`} |
| 3677 | + onClick={() => void jumpToPreviousNote()} |
| 3678 | + disabled={!canGoBack} |
| 3679 | + tooltipAlign="left" |
| 3680 | + > |
| 3681 | + <ArrowLeftIcon width={16} height={16} /> |
| 3682 | + </IconBtn> |
| 3683 | + <IconBtn |
| 3684 | + title={`Go forward (${getKeymapDisplay( |
| 3685 | + tabNavOverrides, |
| 3686 | + vimMode ? 'vim.historyForward' : 'global.historyForward' |
| 3687 | + )})`} |
| 3688 | + onClick={() => void jumpToNextNote()} |
| 3689 | + disabled={!canGoForward} |
| 3690 | + tooltipAlign="left" |
| 3691 | + > |
| 3692 | + <ArrowRightIcon width={16} height={16} /> |
| 3693 | + </IconBtn> |
| 3694 | + </div> |
| 3695 | + <div |
| 3696 | + ref={tabStripRef} |
| 3697 | + className={tabStripClass} |
| 3698 | + onDragOver={handleTabStripDragOver} |
| 3699 | + onDrop={handleTabStripDrop} |
| 3700 | + > |
| 3701 | + {tabItems.map((tab, i) => { |
| 3702 | + // Draw a subtle vertical separator between the last pinned |
| 3703 | + // tab and the first unpinned one (VSCode convention). The |
| 3704 | + // separator is a flex sibling, not a wrapper, so drag hit- |
| 3705 | + // detection on the tab itself is unchanged. |
| 3706 | + const prevPinned = i > 0 ? tabItems[i - 1].pinned : false |
| 3707 | + const needsSeparator = prevPinned && !tab.pinned |
| 3708 | + return ( |
| 3709 | + <Fragment key={tab.path}> |
| 3710 | + {needsSeparator && ( |
| 3711 | + <div |
| 3712 | + aria-hidden |
| 3713 | + className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70" |
| 3714 | + /> |
| 3715 | + )} |
| 3716 | + {renderTab(tab)} |
| 3717 | + </Fragment> |
| 3718 | + ) |
| 3719 | + })} |
| 3720 | + </div> |
| 3721 | + </div> |
| 3722 | + ) |
| 3723 | + |
3653 | 3724 | return ( |
3654 | 3725 | <section |
3655 | 3726 | ref={paneRootRef} |
@@ -3677,69 +3748,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3677 | 3748 | setFocusedPanel('editor') |
3678 | 3749 | }} |
3679 | 3750 | > |
3680 | | - {hasTabs && ( |
3681 | | - <div className={tabStripHeaderClass}> |
3682 | | - <div className="flex shrink-0 items-center gap-0.5 self-center"> |
3683 | | - {!sidebarOpen && ( |
3684 | | - <IconBtn |
3685 | | - title="Show sidebar (⌘1)" |
3686 | | - onClick={toggleSidebar} |
3687 | | - tooltipAlign="left" |
3688 | | - > |
3689 | | - <PanelLeftIcon width={16} height={16} /> |
3690 | | - </IconBtn> |
3691 | | - )} |
3692 | | - <IconBtn |
3693 | | - title={`Go back (${getKeymapDisplay( |
3694 | | - tabNavOverrides, |
3695 | | - vimMode ? 'vim.historyBack' : 'global.historyBack' |
3696 | | - )})`} |
3697 | | - onClick={() => void jumpToPreviousNote()} |
3698 | | - disabled={!canGoBack} |
3699 | | - tooltipAlign="left" |
3700 | | - > |
3701 | | - <ArrowLeftIcon width={16} height={16} /> |
3702 | | - </IconBtn> |
3703 | | - <IconBtn |
3704 | | - title={`Go forward (${getKeymapDisplay( |
3705 | | - tabNavOverrides, |
3706 | | - vimMode ? 'vim.historyForward' : 'global.historyForward' |
3707 | | - )})`} |
3708 | | - onClick={() => void jumpToNextNote()} |
3709 | | - disabled={!canGoForward} |
3710 | | - tooltipAlign="left" |
3711 | | - > |
3712 | | - <ArrowRightIcon width={16} height={16} /> |
3713 | | - </IconBtn> |
3714 | | - </div> |
3715 | | - <div |
3716 | | - ref={tabStripRef} |
3717 | | - className={tabStripClass} |
3718 | | - onDragOver={handleTabStripDragOver} |
3719 | | - onDrop={handleTabStripDrop} |
3720 | | - > |
3721 | | - {tabItems.map((tab, i) => { |
3722 | | - // Draw a subtle vertical separator between the last pinned |
3723 | | - // tab and the first unpinned one (VSCode convention). The |
3724 | | - // separator is a flex sibling, not a wrapper, so drag hit- |
3725 | | - // detection on the tab itself is unchanged. |
3726 | | - const prevPinned = i > 0 ? tabItems[i - 1].pinned : false |
3727 | | - const needsSeparator = prevPinned && !tab.pinned |
3728 | | - return ( |
3729 | | - <Fragment key={tab.path}> |
3730 | | - {needsSeparator && ( |
3731 | | - <div |
3732 | | - aria-hidden |
3733 | | - className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70" |
3734 | | - /> |
3735 | | - )} |
3736 | | - {renderTab(tab)} |
3737 | | - </Fragment> |
3738 | | - ) |
3739 | | - })} |
3740 | | - </div> |
3741 | | - </div> |
3742 | | - )} |
| 3751 | + {hasTabs && !titlebarTabsActive && tabStrip} |
| 3752 | + {hasTabs && titlebarTabsActive && tabStripHost && createPortal(tabStrip, tabStripHost)} |
3743 | 3753 | {content && !zenMode && ( |
3744 | 3754 | <header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4"> |
3745 | 3755 | <div className="flex min-w-0 flex-1 items-center gap-1"> |
|
0 commit comments