Add tab reordering, sidebar resize and collapse, and outline panel

- Drag tabs to reorder; context menu to close, close others, close right
- Resizable sidebar width (synced setting), collapsible with Mod+\
- Outline panel (Mod+Shift+O): right column on desktop, overlay on mobile
- Mobile: title opens quick switcher, swipe left closes the drawer,
  44px touch targets
This commit is contained in:
eezd committed 2026-10-17 17:44:05 +08:00
1 parent 91da6e116e
commit 64915a7b94
7 files changed
+322 -15

No files matched your search

+86 -2
View File
@@ -1,21 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue'
import Icon from './Icon.vue'
import { noteTitle } from './paths'
import { closeTab, ws } from './workspace'
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
const TAB_TYPE = 'application/x-rawmark-tab'
const dragging = ref<string | null>(null)
const dropAt = ref<{ path: string; after: boolean } | null>(null)
function onDragStart(e: DragEvent, path: string) {
dragging.value = path
e.dataTransfer!.setData(TAB_TYPE, path)
e.dataTransfer!.effectAllowed = 'move'
}
function onDragOver(e: DragEvent, path: string) {
if (!e.dataTransfer?.types.includes(TAB_TYPE)) return
e.preventDefault()
const r = (e.currentTarget as HTMLElement).getBoundingClientRect()
dropAt.value = { path, after: e.clientX > r.left + r.width / 2 }
}
function onDrop(e: DragEvent) {
const from = dragging.value
const target = dropAt.value
dragging.value = dropAt.value = null
if (!from || !target || from === target.path) return
e.preventDefault()
const tab = ws.tabs.splice(
ws.tabs.findIndex((t) => t.path === from),
1,
)[0]
const i = ws.tabs.findIndex((t) => t.path === target.path) + (target.after ? 1 : 0)
ws.tabs.splice(i, 0, tab)
}
function onDragEnd() {
dragging.value = dropAt.value = null
}
// ---- 右键菜单 ----
const menu = ref<{ path: string; x: number; y: number } | null>(null)
function openMenu(e: MouseEvent, path: string) {
menu.value = { path, x: Math.min(e.clientX, innerWidth - 200), y: e.clientY }
}
async function closeMany(keep: (index: number, path: string) => boolean) {
const paths = ws.tabs.filter((t, i) => !keep(i, t.path)).map((t) => t.path)
menu.value = null
for (const p of paths) await closeTab(p)
}
function closeOthers(path: string) {
void closeMany((_, p) => p === path)
}
function closeRight(path: string) {
const at = ws.tabs.findIndex((t) => t.path === path)
void closeMany((i) => i <= at)
}
</script>
<template>
<nav class="rm-tabs" role="tablist">
<nav class="rm-tabs" :class="{ empty: !ws.tabs.length && !ws.sidebarCollapsed }" role="tablist">
<button v-if="ws.sidebarCollapsed" class="rm-icon-btn rm-tabs-side" title="显示侧栏(Ctrl+\)" @click="ws.sidebarCollapsed = false">
<Icon name="menu" />
</button>
<div
v-for="tab in ws.tabs"
:key="tab.path"
class="rm-tab"
:class="{ active: ws.active === tab.path }"
:class="{
active: ws.active === tab.path,
dragging: dragging === tab.path,
'drop-before': dropAt?.path === tab.path && !dropAt.after,
'drop-after': dropAt?.path === tab.path && dropAt.after,
}"
role="tab"
:aria-selected="ws.active === tab.path"
:title="tab.path"
draggable="true"
@click="ws.active = tab.path"
@auxclick.prevent="$event.button === 1 && closeTab(tab.path)"
@contextmenu.prevent="openMenu($event, tab.path)"
@dragstart="onDragStart($event, tab.path)"
@dragover="onDragOver($event, tab.path)"
@drop="onDrop"
@dragend="onDragEnd"
>
<span class="rm-tab-title">{{ noteTitle(tab.path) }}</span>
<button class="rm-tab-close" :class="{ dirty: tab.dirty }" title="关闭" @click.stop="closeTab(tab.path)">
@@ -23,5 +96,16 @@ import { closeTab, ws } from './workspace'
<Icon name="close" :size="12" />
</button>
</div>
<button v-if="ws.active" class="rm-icon-btn rm-tabs-outline" :class="{ on: ws.outlineOpen }" title="大纲(Ctrl+Shift+O)" @click="ws.outlineOpen = !ws.outlineOpen">
<Icon name="outline" />
</button>
<template v-if="menu">
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
<div class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
<button @click="closeMany((_, p) => p !== menu!.path)">关闭</button>
<button @click="closeOthers(menu.path)">关闭其他标签页</button>
<button @click="closeRight(menu.path)">关闭右侧标签页</button>
</div>
</template>
</nav>
</template>