|
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' |
@@ -854,6 +855,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
854 | 855 | const textFont = useStore((s) => s.textFont) |
855 | 856 | const tabsEnabled = useStore((s) => s.tabsEnabled) |
856 | 857 | const wrapTabs = useStore((s) => s.wrapTabs) |
| 858 | + const titlebarTabs = useStore((s) => s.titlebarTabs) |
| 859 | + const isSinglePane = useStore((s) => allLeaves(s.paneLayout).length === 1) |
857 | 860 | const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote) |
858 | 861 | const jumpToNextNote = useStore((s) => s.jumpToNextNote) |
859 | 862 | const canGoBack = useStore((s) => s.noteBackstack.length > 0) |
@@ -3441,11 +3444,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3441 | 3444 | el?.scrollIntoView({ inline: 'nearest', block: 'nearest' }) |
3442 | 3445 | }, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey]) |
3443 | 3446 |
|
3444 | | - // Outer header holds the back/forward nav buttons + the (flex-1) tab strip. |
3445 | | - const tabStripHeaderClass = [ |
3446 | | - 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', |
3447 | | - wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]' |
3448 | | - ].join(' ') |
| 3447 | + const titlebarTabsActive = titlebarTabs && isSinglePane && hasTabs |
| 3448 | + const tabStripHeaderClass = titlebarTabsActive |
| 3449 | + ? 'titlebar-tab-strip flex shrink-0 items-stretch' |
| 3450 | + : [ |
| 3451 | + 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', |
| 3452 | + wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]' |
| 3453 | + ].join(' ') |
3449 | 3454 | const tabStripClass = [ |
3450 | 3455 | 'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0', |
3451 | 3456 | wrapTabs |
@@ -3666,6 +3671,72 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3666 | 3671 | isActive ? '' : 'opacity-[0.98]' |
3667 | 3672 | ].join(' ') |
3668 | 3673 |
|
| 3674 | + const tabStripHost = |
| 3675 | + typeof document !== 'undefined' ? document.getElementById('titlebar-tabs-host') : null |
| 3676 | + const tabStrip = ( |
| 3677 | + <div className={tabStripHeaderClass}> |
| 3678 | + <div className="flex shrink-0 items-center gap-0.5 self-center"> |
| 3679 | + {!titlebarTabsActive && !sidebarOpen && ( |
| 3680 | + <IconBtn |
| 3681 | + title="Show sidebar (⌘1)" |
| 3682 | + onClick={toggleSidebar} |
| 3683 | + tooltipAlign="left" |
| 3684 | + > |
| 3685 | + <PanelLeftIcon width={16} height={16} /> |
| 3686 | + </IconBtn> |
| 3687 | + )} |
| 3688 | + <IconBtn |
| 3689 | + title={labelWithShortcut( |
| 3690 | + 'Go back', |
| 3691 | + getKeymapDisplay(tabNavOverrides, vimMode ? 'vim.historyBack' : 'global.historyBack') |
| 3692 | + )} |
| 3693 | + onClick={() => void jumpToPreviousNote()} |
| 3694 | + disabled={!canGoBack} |
| 3695 | + tooltipAlign="left" |
| 3696 | + > |
| 3697 | + <ArrowLeftIcon width={16} height={16} /> |
| 3698 | + </IconBtn> |
| 3699 | + <IconBtn |
| 3700 | + title={labelWithShortcut( |
| 3701 | + 'Go forward', |
| 3702 | + getKeymapDisplay(tabNavOverrides, vimMode ? 'vim.historyForward' : 'global.historyForward') |
| 3703 | + )} |
| 3704 | + onClick={() => void jumpToNextNote()} |
| 3705 | + disabled={!canGoForward} |
| 3706 | + tooltipAlign="left" |
| 3707 | + > |
| 3708 | + <ArrowRightIcon width={16} height={16} /> |
| 3709 | + </IconBtn> |
| 3710 | + </div> |
| 3711 | + <div |
| 3712 | + ref={tabStripRef} |
| 3713 | + className={tabStripClass} |
| 3714 | + onDragOver={handleTabStripDragOver} |
| 3715 | + onDrop={handleTabStripDrop} |
| 3716 | + > |
| 3717 | + {tabItems.map((tab, i) => { |
| 3718 | + // Draw a subtle vertical separator between the last pinned |
| 3719 | + // tab and the first unpinned one (VSCode convention). The |
| 3720 | + // separator is a flex sibling, not a wrapper, so drag hit- |
| 3721 | + // detection on the tab itself is unchanged. |
| 3722 | + const prevPinned = i > 0 ? tabItems[i - 1].pinned : false |
| 3723 | + const needsSeparator = prevPinned && !tab.pinned |
| 3724 | + return ( |
| 3725 | + <Fragment key={tab.path}> |
| 3726 | + {needsSeparator && ( |
| 3727 | + <div |
| 3728 | + aria-hidden |
| 3729 | + className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70" |
| 3730 | + /> |
| 3731 | + )} |
| 3732 | + {renderTab(tab)} |
| 3733 | + </Fragment> |
| 3734 | + ) |
| 3735 | + })} |
| 3736 | + </div> |
| 3737 | + </div> |
| 3738 | + ) |
| 3739 | + |
3669 | 3740 | return ( |
3670 | 3741 | <section |
3671 | 3742 | ref={paneRootRef} |
@@ -3693,69 +3764,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { |
3693 | 3764 | setFocusedPanel('editor') |
3694 | 3765 | }} |
3695 | 3766 | > |
3696 | | - {hasTabs && ( |
3697 | | - <div className={tabStripHeaderClass}> |
3698 | | - <div className="flex shrink-0 items-center gap-0.5 self-center"> |
3699 | | - {!sidebarOpen && ( |
3700 | | - <IconBtn |
3701 | | - title="Show sidebar (⌘1)" |
3702 | | - onClick={toggleSidebar} |
3703 | | - tooltipAlign="left" |
3704 | | - > |
3705 | | - <PanelLeftIcon width={16} height={16} /> |
3706 | | - </IconBtn> |
3707 | | - )} |
3708 | | - <IconBtn |
3709 | | - title={labelWithShortcut( |
3710 | | - 'Go back', |
3711 | | - getKeymapDisplay(tabNavOverrides, vimMode ? 'vim.historyBack' : 'global.historyBack') |
3712 | | - )} |
3713 | | - onClick={() => void jumpToPreviousNote()} |
3714 | | - disabled={!canGoBack} |
3715 | | - tooltipAlign="left" |
3716 | | - > |
3717 | | - <ArrowLeftIcon width={16} height={16} /> |
3718 | | - </IconBtn> |
3719 | | - <IconBtn |
3720 | | - title={labelWithShortcut( |
3721 | | - 'Go forward', |
3722 | | - getKeymapDisplay(tabNavOverrides, vimMode ? 'vim.historyForward' : 'global.historyForward') |
3723 | | - )} |
3724 | | - onClick={() => void jumpToNextNote()} |
3725 | | - disabled={!canGoForward} |
3726 | | - tooltipAlign="left" |
3727 | | - > |
3728 | | - <ArrowRightIcon width={16} height={16} /> |
3729 | | - </IconBtn> |
3730 | | - </div> |
3731 | | - <div |
3732 | | - ref={tabStripRef} |
3733 | | - className={tabStripClass} |
3734 | | - onDragOver={handleTabStripDragOver} |
3735 | | - onDrop={handleTabStripDrop} |
3736 | | - > |
3737 | | - {tabItems.map((tab, i) => { |
3738 | | - // Draw a subtle vertical separator between the last pinned |
3739 | | - // tab and the first unpinned one (VSCode convention). The |
3740 | | - // separator is a flex sibling, not a wrapper, so drag hit- |
3741 | | - // detection on the tab itself is unchanged. |
3742 | | - const prevPinned = i > 0 ? tabItems[i - 1].pinned : false |
3743 | | - const needsSeparator = prevPinned && !tab.pinned |
3744 | | - return ( |
3745 | | - <Fragment key={tab.path}> |
3746 | | - {needsSeparator && ( |
3747 | | - <div |
3748 | | - aria-hidden |
3749 | | - className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70" |
3750 | | - /> |
3751 | | - )} |
3752 | | - {renderTab(tab)} |
3753 | | - </Fragment> |
3754 | | - ) |
3755 | | - })} |
3756 | | - </div> |
3757 | | - </div> |
3758 | | - )} |
| 3767 | + {hasTabs && !titlebarTabsActive && tabStrip} |
| 3768 | + {hasTabs && titlebarTabsActive && tabStripHost && createPortal(tabStrip, tabStripHost)} |
3759 | 3769 | {content && !zenMode && ( |
3760 | 3770 | <header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4"> |
3761 | 3771 | <div className="flex min-w-0 flex-1 items-center gap-1"> |
|
0 commit comments