Skip to content

Commit 94c5dd0

Browse files
committed
feat: tabs in title bar
1 parent e2fb64c commit 94c5dd0

7 files changed

Lines changed: 258 additions & 107 deletions

File tree

packages/app-core/src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ function App(): JSX.Element {
378378
const textFont = useStore((s) => s.textFont)
379379
const monoFont = useStore((s) => s.monoFont)
380380
const darkSidebar = useStore((s) => s.darkSidebar)
381+
const titlebarTabs = useStore((s) => s.titlebarTabs)
381382
const hasCompletedOnboarding = useStore((s) => s.hasCompletedOnboarding)
382383
const persistWorkspace = useStore((s) => s.persistWorkspace)
383384
const flushDirtyNotes = useStore((s) => s.flushDirtyNotes)
@@ -662,6 +663,16 @@ function App(): JSX.Element {
662663
document.documentElement.setAttribute('data-opaque', '')
663664
}, [])
664665

666+
// Title-bar tabs layout: mirror the pref onto the root element so override
667+
// CSS and keyboard-navigation smoke checks can detect the mode globally.
668+
useEffect(() => {
669+
if (titlebarTabs) {
670+
document.documentElement.setAttribute('data-titlebar-tabs', '')
671+
} else {
672+
document.documentElement.removeAttribute('data-titlebar-tabs')
673+
}
674+
}, [titlebarTabs])
675+
665676
// Sidebar darken toggle: when on, the sidebar reads `--z-bg-1`
666677
// (one step darker than the main canvas `--z-bg`) regardless of
667678
// theme, giving a subtle chrome/content separation.

packages/app-core/src/components/EditorPane.tsx

Lines changed: 79 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* The store keeps per-path note content (`noteContents`) shared across
77
* all panes, so the same note open in two panes stays in sync on edit.
88
*/
9+
import { createPortal } from 'react-dom'
910
import {
1011
Fragment,
1112
useCallback,
@@ -98,7 +99,7 @@ import { autocompletion } from '@codemirror/autocomplete'
9899
import { useStore } from '../store'
99100
import type { LineNumberMode } from '../store'
100101
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'
102103
import { livePreviewPlugin } from '../lib/cm-live-preview'
103104
import { codeBlockFlairPlugin } from '../lib/cm-code-block-flair'
104105
import { tablePlugin, tableVimEntry } from '../lib/cm-table'
@@ -870,6 +871,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
870871
const textFont = useStore((s) => s.textFont)
871872
const tabsEnabled = useStore((s) => s.tabsEnabled)
872873
const wrapTabs = useStore((s) => s.wrapTabs)
874+
const titlebarTabs = useStore((s) => s.titlebarTabs)
875+
const isSinglePane = useStore((s) => allLeaves(s.paneLayout).length === 1)
873876
const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote)
874877
const jumpToNextNote = useStore((s) => s.jumpToNextNote)
875878
const canGoBack = useStore((s) => s.noteBackstack.length > 0)
@@ -3425,11 +3428,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
34253428
el?.scrollIntoView({ inline: 'nearest', block: 'nearest' })
34263429
}, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey])
34273430

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(' ')
34333438
const tabStripClass = [
34343439
'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0',
34353440
wrapTabs
@@ -3650,6 +3655,72 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
36503655
isActive ? '' : 'opacity-[0.98]'
36513656
].join(' ')
36523657

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+
36533724
return (
36543725
<section
36553726
ref={paneRootRef}
@@ -3677,69 +3748,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
36773748
setFocusedPanel('editor')
36783749
}}
36793750
>
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)}
37433753
{content && !zenMode && (
37443754
<header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4">
37453755
<div className="flex min-w-0 flex-1 items-center gap-1">

packages/app-core/src/components/SettingsModal.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ export function SettingsModal(): JSX.Element {
526526
const setHiddenWorkflowPresets = useStore((s) => s.setHiddenWorkflowPresets);
527527
const wrapTabs = useStore((s) => s.wrapTabs);
528528
const setWrapTabs = useStore((s) => s.setWrapTabs);
529+
const titlebarTabs = useStore((s) => s.titlebarTabs);
530+
const setTitlebarTabs = useStore((s) => s.setTitlebarTabs);
529531
const quickNoteDateTitle = useStore((s) => s.quickNoteDateTitle);
530532
const setQuickNoteDateTitle = useStore((s) => s.setQuickNoteDateTitle);
531533
const quickNoteTitlePrefix = useStore((s) => s.quickNoteTitlePrefix);
@@ -1337,6 +1339,13 @@ export function SettingsModal(): JSX.Element {
13371339
"Show /-separated tags as a collapsible tree in the sidebar and Tags view instead of a flat list.",
13381340
keywords: ["hierarchical", "tree", "tags", "nested", "hierarchy"],
13391341
},
1342+
{
1343+
id: "tabs-in-title-bar",
1344+
title: "Tabs in title bar",
1345+
description:
1346+
"Move the sidebar toggle and note tabs into the title bar when only one pane is open.",
1347+
keywords: ["title bar", "tabs", "sidebar toggle"],
1348+
},
13401349
],
13411350
content: (
13421351
<div className="space-y-6">
@@ -1750,6 +1759,13 @@ export function SettingsModal(): JSX.Element {
17501759
settingId="nested-tags"
17511760
onChange={setNestedTags}
17521761
/>
1762+
<ToggleRow
1763+
label="Tabs in title bar"
1764+
description="Move the sidebar toggle and note tabs into the title bar when only one pane is open. Hides the centered window title."
1765+
value={titlebarTabs}
1766+
settingId="tabs-in-title-bar"
1767+
onChange={setTitlebarTabs}
1768+
/>
17531769
</Section>
17541770

17551771
<Section

packages/app-core/src/components/Sidebar.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ export function Sidebar(): JSX.Element {
538538
const previewNote = useStore((s) => s.previewNote);
539539
const selectedPath = useStore((s) => s.selectedPath);
540540
const tabsEnabled = useStore((s) => s.tabsEnabled);
541+
const titlebarTabs = useStore((s) => s.titlebarTabs);
541542
const openNoteInTab = useStore((s) => s.openNoteInTab);
542543
const systemFolderLabels = useStore((s) => s.systemFolderLabels);
543544
const workspaceMode = useStore((s) => s.workspaceMode);
@@ -3104,9 +3105,11 @@ export function Sidebar(): JSX.Element {
31043105
>
31053106
<PlusIcon />
31063107
</IconBtn>
3107-
<IconBtn title="Hide sidebar (⌘1)" onClick={toggleSidebar}>
3108-
<PanelLeftIcon />
3109-
</IconBtn>
3108+
{!titlebarTabs && (
3109+
<IconBtn title="Hide sidebar (⌘1)" onClick={toggleSidebar}>
3110+
<PanelLeftIcon />
3111+
</IconBtn>
3112+
)}
31103113
</div>
31113114
</div>
31123115

0 commit comments

Comments
 (0)