Skip to content

feat(tabs): Tab Position setting with left, right and bottom docks - #963

Open
ryanhoggpm wants to merge 4 commits into
sftwrdotdev:masterfrom
ryanhoggpm:feat/vertical-tabs
Open

ryanhoggpm wants to merge 4 commits into
sftwrdotdev:masterfrom
ryanhoggpm:feat/vertical-tabs

Conversation

@ryanhoggpm

Copy link
Copy Markdown

What this is

A Tab Position setting (Settings > Appearance) with four values: Top, Bottom, Left Column and Right Column. Top is the default, so nothing changes for anyone who doesn't touch it.

Refs #884. The issue asks for a file sidebar; the comment there that it's "unlikely to be added" (pointing at #149) suggests the narrower version this PR implements: "an option to view the currently open files in the sidebar, basically a vertical list of tabs. The horizontal tab bar is difficult to navigate when many files are open."

  • Left / Right Column: the open documents listed down one side of the window, under the title bar, one row per file with the full name visible. The column is resizable by dragging its inner edge (140 to 420px, persisted).
  • Bottom: the existing horizontal strip, docked along the bottom edge instead of the title bar.
  • With either dock, the title bar shows the window title, the same as it does with tabs hidden.

Mechanism

There is no second tab implementation. TabDock.svelte wraps the same TabList and Tab the title bar uses, so the context menu, middle-click close, move to window / new window, duplicate-name folder suffixes and the window tag all come along unchanged. TabList takes an orientation prop; the vertical case switches drag-to-reorder from the X axis to the Y axis and scrolls the active tab into view with offsetTop instead of offsetLeft. The nearest-slot math moved into utils/tabReorder.ts so both orientations run the same function.

The document is not re-laid out piece by piece. While a dock is shown, MarkdownViewer wraps its content in a .content-area that is position: fixed and inset by the dock's width or height. The absolutely positioned .layout-container, the outline overlay and the home page then measure from the dock's edge without knowing about it. With Top selected, .content-area is an unstyled block, so the default layout is the same DOM and CSS as before.

showTabs still decides whether tabs are shown at all, so zen mode hides the dock through the same switch it already uses for the strip. Only a strip inside the title bar keeps data-tauri-drag-region; the dock does not drag the window.

The window tag line runs along the dock's inner edge (the column's side, or the bottom strip's top), drawn once for the whole dock, following the comment on .tab-area.tagged::after.

Scope

  • No folder tree. Open Folder Functionality #149 explains why, and this doesn't reopen it.
  • English strings only for the four new labels. Other locales fall back to English, which i18nCoverage.test.ts reports but doesn't fail on.
  • No keyboard shortcut to cycle positions; the existing Toggle Tabs shortcut still hides and shows whichever position is selected.
  • The bottom strip's height is fixed at the title bar's 36px; only the columns resize.

Tests

  • settingsPersistence.spec.ts: editor.tabPosition round-trips all four values; unknown values (center, LEFT, empty) load as top; editor.tabColumnWidth is clamped to TAB_COLUMN_WIDTH_RANGE on load and on resize, and garbage loads as the default. A source-shape assertion pins the <select> options to exactly the values isTabPosition accepts, since that's the one place the two lists can drift.
  • tabReorder.test.ts: nearest slot along each axis, non-tab children skipped, and the drag threshold measured only along the layout axis.

Verification

npm audit                          found 0 vulnerabilities
npm run check                      0 errors, 0 warnings
npm test                           1116 pass, 0 fail
npm run test:vitest                493 pass, 2 fail (see below)
cargo fmt --check                  clean
cargo clippy --all-targets -- -D warnings   clean
cargo test                         186 pass

The two vitest failures are 5s timeouts in diagramThemeRefresh.spec.ts and platformSource.spec.ts. Both also time out on unmodified master on this machine (along with monacoChordOwnership, monacoAltGraph and viewerKeymap), and both pass when run on their own.

Manual run with npm run tauri dev on Ubuntu 24.04 under WSLg: all four positions; drag-reorder in the column and the bottom strip; column resize; outline pinned on the same side as the column and on the opposite side; zen mode on and off; the + button and tab context menu in each dock; edit-mode status bar sits above the bottom strip; settings survive a restart.

Not verified: Windows and macOS at runtime, including the macOS traffic-light title bar with a dock shown.

Adds a Tab Position setting (Top, Left Column, Right Column). With a
column, the open documents are listed down one side of the window under
the title bar, reusing TabList and Tab so the context menu, middle-click
close and drag-to-reorder behave the same. The title bar shows the
window title instead of the strip, the column is resizable, and the
window tag colour runs along its inner edge. The default stays Top, so
nothing changes for existing installs.
Tab Position gains Bottom: the same horizontal strip, docked along the
bottom edge of the window instead of the title bar. TabColumn becomes
TabDock, which handles the bottom strip and the side columns. Only a
strip inside the title bar is a window drag region.
- external-change-bar sits inside .content-area, so it spans the document
  instead of covering the column's first rows.
- .content-area beside the column is absolute, not fixed: with no z-index it
  opens no stacking context, so the home page's context menu stacks above
  the column and title bar again.
- the column resizer is a focusable separator with the outline handle's
  keys and its range in aria-valuemin/max.
- settings.tabPlacement is the one answer the title bar and the column read.
@PathGao

PathGao commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Pushed two commits. I reverted the bottom position: #884 asks for a vertical list, and the bottom strip sat under toasts. In the column layouts the external-change bar covered the first tabs and the home page context menu landed under the column, because a fixed .content-area opens its own stacking context. It is absolute now, with the bar inside it. The column resizer gets the separator role and keyboard handling the TOC resizer has, and the title bar and viewer read one tabPlacement value instead of each comparing tabPosition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants