Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/app-core/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ function App(): JSX.Element {
const textFont = useStore((s) => s.textFont)
const monoFont = useStore((s) => s.monoFont)
const darkSidebar = useStore((s) => s.darkSidebar)
const titlebarTabs = useStore((s) => s.titlebarTabs)
const hasCompletedOnboarding = useStore((s) => s.hasCompletedOnboarding)
const persistWorkspace = useStore((s) => s.persistWorkspace)
const flushDirtyNotes = useStore((s) => s.flushDirtyNotes)
Expand Down Expand Up @@ -662,6 +663,16 @@ function App(): JSX.Element {
document.documentElement.setAttribute('data-opaque', '')
}, [])

// Title-bar tabs layout: mirror the pref onto the root element so override
// CSS and keyboard-navigation smoke checks can detect the mode globally.
useEffect(() => {
if (titlebarTabs) {
document.documentElement.setAttribute('data-titlebar-tabs', '')
} else {
document.documentElement.removeAttribute('data-titlebar-tabs')
}
}, [titlebarTabs])

// Sidebar darken toggle: when on, the sidebar reads `--z-bg-1`
// (one step darker than the main canvas `--z-bg`) regardless of
// theme, giving a subtle chrome/content separation.
Expand Down
148 changes: 79 additions & 69 deletions packages/app-core/src/components/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* The store keeps per-path note content (`noteContents`) shared across
* all panes, so the same note open in two panes stays in sync on edit.
*/
import { createPortal } from 'react-dom'
import {
Fragment,
useCallback,
Expand Down Expand Up @@ -98,7 +99,7 @@ import { autocompletion } from '@codemirror/autocomplete'
import { useStore } from '../store'
import type { LineNumberMode } from '../store'
import type { PaneEdge, PaneLeaf } from '../lib/pane-layout'
import { findLeaf, inferPaneDropEdge } from '../lib/pane-layout'
import { findLeaf, allLeaves, inferPaneDropEdge } from '../lib/pane-layout'
import { livePreviewPlugin } from '../lib/cm-live-preview'
import { codeBlockFlairPlugin } from '../lib/cm-code-block-flair'
import { tablePlugin, tableVimEntry } from '../lib/cm-table'
Expand Down Expand Up @@ -892,6 +893,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
const textFont = useStore((s) => s.textFont)
const tabsEnabled = useStore((s) => s.tabsEnabled)
const wrapTabs = useStore((s) => s.wrapTabs)
const titlebarTabs = useStore((s) => s.titlebarTabs)
const isSinglePane = useStore((s) => allLeaves(s.paneLayout).length === 1)
const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote)
const jumpToNextNote = useStore((s) => s.jumpToNextNote)
const canGoBack = useStore((s) => s.noteBackstack.length > 0)
Expand Down Expand Up @@ -3438,11 +3441,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
el?.scrollIntoView({ inline: 'nearest', block: 'nearest' })
}, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey])

// Outer header holds the back/forward nav buttons + the (flex-1) tab strip.
const tabStripHeaderClass = [
'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1',
wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]'
].join(' ')
const titlebarTabsActive = titlebarTabs && isSinglePane && hasTabs
const tabStripHeaderClass = titlebarTabsActive
? 'titlebar-tab-strip flex shrink-0 items-stretch'
: [
'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1',
wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]'
].join(' ')
const tabStripClass = [
'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0',
wrapTabs
Expand Down Expand Up @@ -3663,6 +3668,72 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
isActive ? '' : 'opacity-[0.98]'
].join(' ')

const tabStripHost =
typeof document !== 'undefined' ? document.getElementById('titlebar-tabs-host') : null
const tabStrip = (
<div className={tabStripHeaderClass}>
<div className="flex shrink-0 items-center gap-0.5 self-center">
{!titlebarTabsActive && !sidebarOpen && (
<IconBtn
title="Show sidebar (⌘1)"
onClick={toggleSidebar}
tooltipAlign="left"
>
<PanelLeftIcon width={16} height={16} />
</IconBtn>
)}
<IconBtn
title={`Go back (${getKeymapDisplay(
tabNavOverrides,
vimMode ? 'vim.historyBack' : 'global.historyBack'
)})`}
onClick={() => void jumpToPreviousNote()}
disabled={!canGoBack}
tooltipAlign="left"
>
<ArrowLeftIcon width={16} height={16} />
</IconBtn>
<IconBtn
title={`Go forward (${getKeymapDisplay(
tabNavOverrides,
vimMode ? 'vim.historyForward' : 'global.historyForward'
)})`}
onClick={() => void jumpToNextNote()}
disabled={!canGoForward}
tooltipAlign="left"
>
<ArrowRightIcon width={16} height={16} />
</IconBtn>
</div>
<div
ref={tabStripRef}
className={tabStripClass}
onDragOver={handleTabStripDragOver}
onDrop={handleTabStripDrop}
>
{tabItems.map((tab, i) => {
// Draw a subtle vertical separator between the last pinned
// tab and the first unpinned one (VSCode convention). The
// separator is a flex sibling, not a wrapper, so drag hit-
// detection on the tab itself is unchanged.
const prevPinned = i > 0 ? tabItems[i - 1].pinned : false
const needsSeparator = prevPinned && !tab.pinned
return (
<Fragment key={tab.path}>
{needsSeparator && (
<div
aria-hidden
className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70"
/>
)}
{renderTab(tab)}
</Fragment>
)
})}
</div>
</div>
)

return (
<section
ref={paneRootRef}
Expand Down Expand Up @@ -3690,69 +3761,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
setFocusedPanel('editor')
}}
>
{hasTabs && (
<div className={tabStripHeaderClass}>
<div className="flex shrink-0 items-center gap-0.5 self-center">
{!sidebarOpen && (
<IconBtn
title="Show sidebar (⌘1)"
onClick={toggleSidebar}
tooltipAlign="left"
>
<PanelLeftIcon width={16} height={16} />
</IconBtn>
)}
<IconBtn
title={`Go back (${getKeymapDisplay(
tabNavOverrides,
vimMode ? 'vim.historyBack' : 'global.historyBack'
)})`}
onClick={() => void jumpToPreviousNote()}
disabled={!canGoBack}
tooltipAlign="left"
>
<ArrowLeftIcon width={16} height={16} />
</IconBtn>
<IconBtn
title={`Go forward (${getKeymapDisplay(
tabNavOverrides,
vimMode ? 'vim.historyForward' : 'global.historyForward'
)})`}
onClick={() => void jumpToNextNote()}
disabled={!canGoForward}
tooltipAlign="left"
>
<ArrowRightIcon width={16} height={16} />
</IconBtn>
</div>
<div
ref={tabStripRef}
className={tabStripClass}
onDragOver={handleTabStripDragOver}
onDrop={handleTabStripDrop}
>
{tabItems.map((tab, i) => {
// Draw a subtle vertical separator between the last pinned
// tab and the first unpinned one (VSCode convention). The
// separator is a flex sibling, not a wrapper, so drag hit-
// detection on the tab itself is unchanged.
const prevPinned = i > 0 ? tabItems[i - 1].pinned : false
const needsSeparator = prevPinned && !tab.pinned
return (
<Fragment key={tab.path}>
{needsSeparator && (
<div
aria-hidden
className="mx-0.5 h-5 shrink-0 self-center border-l border-paper-300/70"
/>
)}
{renderTab(tab)}
</Fragment>
)
})}
</div>
</div>
)}
{hasTabs && !titlebarTabsActive && tabStrip}
{hasTabs && titlebarTabsActive && tabStripHost && createPortal(tabStrip, tabStripHost)}
{content && !zenMode && (
<header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4">
<div className="flex min-w-0 flex-1 items-center gap-1">
Expand Down
16 changes: 16 additions & 0 deletions packages/app-core/src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@ export function SettingsModal(): JSX.Element {
const setHiddenWorkflowPresets = useStore((s) => s.setHiddenWorkflowPresets);
const wrapTabs = useStore((s) => s.wrapTabs);
const setWrapTabs = useStore((s) => s.setWrapTabs);
const titlebarTabs = useStore((s) => s.titlebarTabs);
const setTitlebarTabs = useStore((s) => s.setTitlebarTabs);
const quickNoteDateTitle = useStore((s) => s.quickNoteDateTitle);
const setQuickNoteDateTitle = useStore((s) => s.setQuickNoteDateTitle);
const quickNoteTitlePrefix = useStore((s) => s.quickNoteTitlePrefix);
Expand Down Expand Up @@ -1335,6 +1337,13 @@ export function SettingsModal(): JSX.Element {
"Show /-separated tags as a collapsible tree in the sidebar and Tags view instead of a flat list.",
keywords: ["hierarchical", "tree", "tags", "nested", "hierarchy"],
},
{
id: "tabs-in-title-bar",
title: "Tabs in title bar",
description:
"Move the sidebar toggle and note tabs into the title bar when only one pane is open.",
keywords: ["title bar", "tabs", "sidebar toggle"],
},
],
content: (
<div className="space-y-6">
Expand Down Expand Up @@ -1748,6 +1757,13 @@ export function SettingsModal(): JSX.Element {
settingId="nested-tags"
onChange={setNestedTags}
/>
<ToggleRow
label="Tabs in title bar"
description="Move the sidebar toggle and note tabs into the title bar when only one pane is open. Hides the centered window title."
value={titlebarTabs}
settingId="tabs-in-title-bar"
onChange={setTitlebarTabs}
/>
</Section>

<Section
Expand Down
9 changes: 6 additions & 3 deletions packages/app-core/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ export function Sidebar(): JSX.Element {
const previewNote = useStore((s) => s.previewNote);
const selectedPath = useStore((s) => s.selectedPath);
const tabsEnabled = useStore((s) => s.tabsEnabled);
const titlebarTabs = useStore((s) => s.titlebarTabs);
const openNoteInTab = useStore((s) => s.openNoteInTab);
const systemFolderLabels = useStore((s) => s.systemFolderLabels);
const workspaceMode = useStore((s) => s.workspaceMode);
Expand Down Expand Up @@ -3104,9 +3105,11 @@ export function Sidebar(): JSX.Element {
>
<PlusIcon />
</IconBtn>
<IconBtn title="Hide sidebar (⌘1)" onClick={toggleSidebar}>
<PanelLeftIcon />
</IconBtn>
{!titlebarTabs && (
<IconBtn title="Hide sidebar (⌘1)" onClick={toggleSidebar}>
<PanelLeftIcon />
</IconBtn>
)}
</div>
</div>

Expand Down
Loading