Skip to content

Commit c16829d

Browse files
committed
feat: tabs in title bar
1 parent a29bd21 commit c16829d

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'
@@ -892,6 +893,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
892893
const textFont = useStore((s) => s.textFont)
893894
const tabsEnabled = useStore((s) => s.tabsEnabled)
894895
const wrapTabs = useStore((s) => s.wrapTabs)
896+
const titlebarTabs = useStore((s) => s.titlebarTabs)
897+
const isSinglePane = useStore((s) => allLeaves(s.paneLayout).length === 1)
895898
const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote)
896899
const jumpToNextNote = useStore((s) => s.jumpToNextNote)
897900
const canGoBack = useStore((s) => s.noteBackstack.length > 0)
@@ -3438,11 +3441,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
34383441
el?.scrollIntoView({ inline: 'nearest', block: 'nearest' })
34393442
}, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey])
34403443

3441-
// Outer header holds the back/forward nav buttons + the (flex-1) tab strip.
3442-
const tabStripHeaderClass = [
3443-
'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1',
3444-
wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]'
3445-
].join(' ')
3444+
const titlebarTabsActive = titlebarTabs && isSinglePane && hasTabs
3445+
const tabStripHeaderClass = titlebarTabsActive
3446+
? 'titlebar-tab-strip flex shrink-0 items-stretch'
3447+
: [
3448+
'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1',
3449+
wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]'
3450+
].join(' ')
34463451
const tabStripClass = [
34473452
'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0',
34483453
wrapTabs
@@ -3663,6 +3668,72 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
36633668
isActive ? '' : 'opacity-[0.98]'
36643669
].join(' ')
36653670

3671+
const tabStripHost =
3672+
typeof document !== 'undefined' ? document.getElementById('titlebar-tabs-host') : null
3673+
const tabStrip = (
3674+
<div className={tabStripHeaderClass}>
3675+
<div className="flex shrink-0 items-center gap-0.5 self-center">
3676+
{!titlebarTabsActive && !sidebarOpen && (
3677+
<IconBtn
3678+
title="Show sidebar (⌘1)"
3679+
onClick={toggleSidebar}
3680+
tooltipAlign="left"
3681+
>
3682+
<PanelLeftIcon width={16} height={16} />
3683+
</IconBtn>
3684+
)}
3685+
<IconBtn
3686+
title={`Go back (${getKeymapDisplay(
3687+
tabNavOverrides,
3688+
vimMode ? 'vim.historyBack' : 'global.historyBack'
3689+
)})`}
3690+
onClick={() => void jumpToPreviousNote()}
3691+
disabled={!canGoBack}
3692+
tooltipAlign="left"
3693+
>
3694+
<ArrowLeftIcon width={16} height={16} />
3695+
</IconBtn>
3696+
<IconBtn
3697+
title={`Go forward (${getKeymapDisplay(
3698+
tabNavOverrides,
3699+
vimMode ? 'vim.historyForward' : 'global.historyForward'
3700+
)})`}
3701+
onClick={() => void jumpToNextNote()}
3702+
disabled={!canGoForward}
3703+
tooltipAlign="left"
3704+
>
3705+
<ArrowRightIcon width={16} height={16} />
3706+
</IconBtn>
3707+
</div>
3708+
<div
3709+
ref={tabStripRef}
3710+
className={tabStripClass}
3711+
onDragOver={handleTabStripDragOver}
3712+
onDrop={handleTabStripDrop}
3713+
>
3714+
{tabItems.map((tab, i) => {
3715+
// Draw a subtle vertical separator between the last pinned
3716+
// tab and the first unpinned one (VSCode convention). The
3717+
// separator is a flex sibling, not a wrapper, so drag hit-
3718+
// detection on the tab itself is unchanged.
3719+
const prevPinned = i > 0 ? tabItems[i - 1].pinned : false
3720+
const needsSeparator = prevPinned && !tab.pinned
3721+
return (
3722+
<Fragment key={tab.path}>
3723+
{needsSeparator && (
3724+
<div
3725+
aria-hidden
3726+
className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70"
3727+
/>
3728+
)}
3729+
{renderTab(tab)}
3730+
</Fragment>
3731+
)
3732+
})}
3733+
</div>
3734+
</div>
3735+
)
3736+
36663737
return (
36673738
<section
36683739
ref={paneRootRef}
@@ -3690,69 +3761,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
36903761
setFocusedPanel('editor')
36913762
}}
36923763
>
3693-
{hasTabs && (
3694-
<div className={tabStripHeaderClass}>
3695-
<div className="flex shrink-0 items-center gap-0.5 self-center">
3696-
{!sidebarOpen && (
3697-
<IconBtn
3698-
title="Show sidebar (⌘1)"
3699-
onClick={toggleSidebar}
3700-
tooltipAlign="left"
3701-
>
3702-
<PanelLeftIcon width={16} height={16} />
3703-
</IconBtn>
3704-
)}
3705-
<IconBtn
3706-
title={`Go back (${getKeymapDisplay(
3707-
tabNavOverrides,
3708-
vimMode ? 'vim.historyBack' : 'global.historyBack'
3709-
)})`}
3710-
onClick={() => void jumpToPreviousNote()}
3711-
disabled={!canGoBack}
3712-
tooltipAlign="left"
3713-
>
3714-
<ArrowLeftIcon width={16} height={16} />
3715-
</IconBtn>
3716-
<IconBtn
3717-
title={`Go forward (${getKeymapDisplay(
3718-
tabNavOverrides,
3719-
vimMode ? 'vim.historyForward' : 'global.historyForward'
3720-
)})`}
3721-
onClick={() => void jumpToNextNote()}
3722-
disabled={!canGoForward}
3723-
tooltipAlign="left"
3724-
>
3725-
<ArrowRightIcon width={16} height={16} />
3726-
</IconBtn>
3727-
</div>
3728-
<div
3729-
ref={tabStripRef}
3730-
className={tabStripClass}
3731-
onDragOver={handleTabStripDragOver}
3732-
onDrop={handleTabStripDrop}
3733-
>
3734-
{tabItems.map((tab, i) => {
3735-
// Draw a subtle vertical separator between the last pinned
3736-
// tab and the first unpinned one (VSCode convention). The
3737-
// separator is a flex sibling, not a wrapper, so drag hit-
3738-
// detection on the tab itself is unchanged.
3739-
const prevPinned = i > 0 ? tabItems[i - 1].pinned : false
3740-
const needsSeparator = prevPinned && !tab.pinned
3741-
return (
3742-
<Fragment key={tab.path}>
3743-
{needsSeparator && (
3744-
<div
3745-
aria-hidden
3746-
className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70"
3747-
/>
3748-
)}
3749-
{renderTab(tab)}
3750-
</Fragment>
3751-
)
3752-
})}
3753-
</div>
3754-
</div>
3755-
)}
3764+
{hasTabs && !titlebarTabsActive && tabStrip}
3765+
{hasTabs && titlebarTabsActive && tabStripHost && createPortal(tabStrip, tabStripHost)}
37563766
{content && !zenMode && (
37573767
<header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4">
37583768
<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);
@@ -1335,6 +1337,13 @@ export function SettingsModal(): JSX.Element {
13351337
"Show /-separated tags as a collapsible tree in the sidebar and Tags view instead of a flat list.",
13361338
keywords: ["hierarchical", "tree", "tags", "nested", "hierarchy"],
13371339
},
1340+
{
1341+
id: "tabs-in-title-bar",
1342+
title: "Tabs in title bar",
1343+
description:
1344+
"Move the sidebar toggle and note tabs into the title bar when only one pane is open.",
1345+
keywords: ["title bar", "tabs", "sidebar toggle"],
1346+
},
13381347
],
13391348
content: (
13401349
<div className="space-y-6">
@@ -1748,6 +1757,13 @@ export function SettingsModal(): JSX.Element {
17481757
settingId="nested-tags"
17491758
onChange={setNestedTags}
17501759
/>
1760+
<ToggleRow
1761+
label="Tabs in title bar"
1762+
description="Move the sidebar toggle and note tabs into the title bar when only one pane is open. Hides the centered window title."
1763+
value={titlebarTabs}
1764+
settingId="tabs-in-title-bar"
1765+
onChange={setTitlebarTabs}
1766+
/>
17511767
</Section>
17521768

17531769
<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)