feat(desktop): unify tab organization menu and adaptive collapsed rail

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-12 21:46:05 +08:00
1 parent 177aba9979
commit bf4874906c
17 files changed
+212 -83

No files matched your search

@@ -55,7 +55,7 @@ const layoutClass = computed(() => {
});
const navigationStyle = computed<CSSProperties>(() => {
if (!isVerticalLayout.value) return { maxHeight: "50%" };
const width = props.tabBarCollapsed ? "3.5rem" : `${props.tabBarWidth ?? 240}px`;
const width = props.tabBarCollapsed ? "var(--collapsed-tab-rail-width)" : `${props.tabBarWidth ?? 240}px`;
return { width, flex: `0 0 ${width}` };
});
function setTabBarTarget(groupId: string, element: unknown) {
@@ -30,7 +30,37 @@ const TAB_DRAG_HORIZONTAL_THRESHOLD = 24;
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { ArrowDown, ArrowDownUp, ArrowRight, ChevronDown, ChevronsDownUp, ChevronsLeft, ChevronsRight, ChevronsUpDown, Copy, ListFilter, Maximize2, Minimize2, Package, PanelTop, Pencil, Pin, RotateCcw, RotateCw, Search, Settings, X } from "@lucide/vue";
import {
ArrowDown,
ArrowDownAZ,
ArrowRight,
ChevronDown,
ChevronsDownUp,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Clock3,
Copy,
Database,
ListFilter,
ListOrdered,
Maximize2,
Minimize2,
Package,
PanelBottom,
PanelLeft,
PanelRight,
PanelTop,
Pencil,
Pin,
RotateCcw,
RotateCw,
Search,
Server,
Settings,
Ungroup,
X,
} from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@@ -134,7 +164,9 @@ const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.edi
const isTabBarCollapsed = computed(() => isVerticalLayout.value && !!props.tabBarCollapsed);
const tabBarStyle = computed<CSSProperties | undefined>(() => {
if (!isVerticalLayout.value) return undefined;
if (props.tabBarCollapsed) return { width: "3.5rem", flex: "0 0 3.5rem" };
if (props.tabBarCollapsed) {
return { width: "100%", flex: "0 0 100%" };
}
const width = props.tabBarWidth ?? 240;
return { width: `${width}px`, flex: `0 0 ${width}px` };
});
@@ -725,36 +757,19 @@ function closeTabGroup(tab: QueryTab) {
queryStore.closeTabsByIds(tabsToClose, finalActiveTabId);
}
function getTabPreferenceMenuItems(): ContextMenuItem[] {
return [
{
label: t("settings.tabPlacement"),
icon: PanelTop,
children: tabPlacementItems.value.map((item) => ({
label: item.label,
checked: item.value === settingsStore.editorSettings.tabPlacement,
action: () => updateTabPlacement(item.value),
})),
},
{
label: t("settings.tabGroup"),
icon: ListFilter,
children: tabGroupItems.value.map((item) => ({
label: item.label,
checked: item.value === settingsStore.editorSettings.tabGroupMode,
action: () => updateTabGroupMode(item.value),
})),
},
{
label: t("settings.tabSort"),
icon: ArrowDownUp,
children: tabSortItems.value.map((item) => ({
label: item.label,
checked: item.value === settingsStore.editorSettings.tabSortMode,
action: () => updateTabSortMode(item.value),
})),
},
];
const tabOrganizationItems = computed(() => [
...tabPlacementItems.value.map((item, index) => ({ ...item, value: `placement:${item.value}`, icon: { top: PanelTop, bottom: PanelBottom, left: PanelLeft, right: PanelRight }[item.value], groupLabel: index === 0 ? t("settings.tabPlacement") : undefined })),
...tabGroupItems.value.map((item, index) => ({ ...item, value: `group:${item.value}`, icon: { none: Ungroup, "database-type": Database, database: Database, connection: Server }[item.value], separatorBefore: index === 0, groupLabel: index === 0 ? t("settings.tabGroup") : undefined })),
...tabSortItems.value.map((item, index) => ({ ...item, value: `sort:${item.value}`, icon: { manual: ListOrdered, "created-asc": Clock3, "title-asc": ArrowDownAZ }[item.value], separatorBefore: index === 0, groupLabel: index === 0 ? t("settings.tabSort") : undefined })),
]);
const selectedTabOrganizationItems = computed(() => [`placement:${settingsStore.editorSettings.tabPlacement}`, `group:${settingsStore.editorSettings.tabGroupMode}`, `sort:${settingsStore.editorSettings.tabSortMode}`]);
function selectTabOrganizationItem(value: string) {
const [section, option] = value.split(":");
if (!option) return;
if (section === "placement") updateTabPlacement(option);
else if (section === "group") updateTabGroupMode(option);
else if (section === "sort") updateTabSortMode(option);
}
function getTabGroupMenuItems(tab: QueryTab): ContextMenuItem[] {
@@ -785,8 +800,6 @@ function getTabGroupMenuItems(tab: QueryTab): ContextMenuItem[] {
visible: settingsStore.editorSettings.tabGroupMode !== "none",
},
{ label: "", separator: true },
...getTabPreferenceMenuItems(),
{ label: "", separator: true },
{
label: t("contextMenu.closeTabGroup"),
action: () => closeTabGroup(tab),
@@ -1085,8 +1098,6 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
onLocate: () => emit("locate-tab", tab),
}),
{ label: "", separator: true },
...getTabPreferenceMenuItems(),
{ label: "", separator: true },
createPinTabMenuItem({
label: tab.pinned ? t("contextMenu.unpinTab") : t("contextMenu.pinTab"),
onToggle: () => queryStore.togglePinnedTab(tab.id),
@@ -1402,33 +1413,58 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
:data-group-mode="settingsStore.editorSettings.tabGroupMode"
:data-placement="settingsStore.editorSettings.tabPlacement"
>
<!-- Compact vertical toolbar: search, grouping preference, collapse. -->
<!-- Compact vertical toolbar: search, tab organization, collapse. -->
<div v-if="isVerticalLayout" class="flex h-9 shrink-0 items-center gap-0.5 border-b p-1" :class="isTabBarCollapsed ? 'justify-center' : ''">
<div v-if="!isTabBarCollapsed" class="relative min-w-0 flex-1">
<Search class="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input v-model="tabSearchQuery" type="search" :placeholder="t('tabs.searchOpenTabs')" class="h-7 w-full pl-7 text-sm" />
</div>
<div v-if="!isTabBarCollapsed" class="flex shrink-0 items-center gap-0">
<LightDropdown
:model-value="settingsStore.editorSettings.tabGroupMode"
:items="tabGroupItems"
:aria-label="t('settings.tabGroup')"
:trigger-title="t('settings.tabGroup')"
:trigger-icon="ListFilter"
:trigger-class="verticalTabToolbarButtonClass"
:show-trigger-label="false"
:show-chevron="false"
check-position="right"
:match-trigger-width="false"
align="end"
@update:model-value="updateTabGroupMode"
/>
</div>
<LightDropdown
v-if="!isTabBarCollapsed"
model-value=""
:items="tabOrganizationItems"
:selected-values="selectedTabOrganizationItems"
:aria-label="t('settings.tabOrganization')"
:trigger-title="t('settings.tabOrganization')"
:trigger-icon="ListFilter"
:trigger-class="verticalTabToolbarButtonClass"
trigger-icon-class="h-4 w-4"
item-icon-class="h-3.5 w-3.5"
content-class="w-max min-w-0"
selected-item-class="bg-primary/10 text-primary"
selected-check-class="text-primary"
:show-trigger-label="false"
:show-chevron="false"
:close-on-select="false"
:match-trigger-width="false"
align="end"
@update:model-value="selectTabOrganizationItem"
/>
<button type="button" :class="verticalTabToolbarButtonClass" :title="tabBarCollapseLabel" :aria-label="tabBarCollapseLabel" :aria-expanded="!isTabBarCollapsed" @click="emit('toggle-collapse')">
<component :is="tabBarCollapseIcon" class="h-4 w-4" />
</button>
</div>
<div class="relative flex w-full min-w-0 shrink-0 overflow-hidden" :class="[isVerticalLayout ? ['min-h-0 flex-1 flex-col items-stretch'] : isClassicLayout ? 'h-9 items-stretch' : 'h-10 items-center px-2', { 'has-tab-overflow-control': showOverflowControl }]">
<LightDropdown
v-if="!isVerticalLayout"
model-value=""
:items="tabOrganizationItems"
:selected-values="selectedTabOrganizationItems"
:aria-label="t('settings.tabOrganization')"
:trigger-title="t('settings.tabOrganization')"
:trigger-icon="ListFilter"
trigger-class="tab-organization-button"
trigger-icon-class="h-4 w-4"
item-icon-class="h-3.5 w-3.5"
content-class="w-max min-w-0"
selected-item-class="bg-primary/10 text-primary"
selected-check-class="text-primary"
:show-trigger-label="false"
:show-chevron="false"
:close-on-select="false"
:match-trigger-width="false"
@update:model-value="selectTabOrganizationItem"
/>
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
<div v-if="showOverflowControl && !hasHorizontalFixedRows" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="tabScrollbarThumbStyle" />
@@ -1482,7 +1518,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
@contextmenu="openTabGroupContextMenu($event, onContextMenu)"
>
<span class="tab-group-header-content">
<span v-if="isVerticalLayout" class="tab-group-marker" aria-hidden="true" />
<span class="tab-group-marker" aria-hidden="true" />
<Pin v-if="entry.pinned" class="tab-group-pin" aria-hidden="true" />
<ChevronDown class="tab-group-chevron" :class="{ 'tab-group-chevron--collapsed': isTabGroupCollapsed(entry.tab) }" aria-hidden="true" />
<DatabaseIcon :db-type="tabDatabaseIconType(entry.tab)" class="tab-group-database-icon" aria-hidden="true" />
@@ -123,7 +123,7 @@ provide(GROUP_TAB_BAR_PORTAL, {
},
});
const tabNavigationStyle = computed(() => {
const width = props.tabBarCollapsed ? "3.5rem" : `${props.tabBarWidth ?? 240}px`;
const width = props.tabBarCollapsed ? "var(--collapsed-tab-rail-width)" : `${props.tabBarWidth ?? 240}px`;
return { width, flex: `0 0 ${width}` };
});
function setTabBarTarget(groupId: string, element: unknown) {
@@ -363,8 +363,8 @@ describe("AppTabBar group navigation portal", () => {
expect(bar.style.width).toBe("320px");
navigation.collapsed = true;
await settle();
expect(rail.style.width).toBe("3.5rem");
expect(bar.style.width).toBe("3.5rem");
expect(rail.style.width).toBe("var(--collapsed-tab-rail-width)");
expect(bar.style.width).toBe("100%");
expect(element(rail, "[data-main-tab-bar]")).toBe(bar);
navigation.collapsed = false;
await settle();
@@ -61,17 +61,9 @@ describe("EditorGroupTabBar semantic tab groups", () => {
expect(source).toContain('type="color"');
});
it("exposes placement, grouping, and sorting preferences from the group context menu", () => {
const preferences = sourceBetween("function getTabPreferenceMenuItems", "function getTabGroupMenuItems");
it("keeps placement, grouping, and sorting out of the group context menu", () => {
const groupMenu = sourceBetween("function getTabGroupMenuItems", "function openTabGroupContextMenu");
expect(preferences).toContain('label: t("settings.tabPlacement")');
expect(preferences).toContain("action: () => updateTabPlacement(item.value)");
expect(preferences).toContain('label: t("settings.tabGroup")');
expect(preferences).toContain("action: () => updateTabGroupMode(item.value)");
expect(preferences).toContain('label: t("settings.tabSort")');
expect(preferences).toContain("action: () => updateTabSortMode(item.value)");
expect(preferences.match(/checked: item\.value === settingsStore\.editorSettings\./g)).toHaveLength(3);
expect(groupMenu).toContain("...getTabPreferenceMenuItems()");
expect(groupMenu).not.toContain("getTabPreferenceMenuItems");
});
it("exposes preferences and the group close from each tab context menu", () => {
@@ -80,7 +72,7 @@ describe("EditorGroupTabBar semantic tab groups", () => {
expect(menuStart).toBeGreaterThanOrEqual(0);
expect(menuEnd).toBeGreaterThan(menuStart);
const menu = source.slice(menuStart, menuEnd);
expect(menu).toContain("...getTabPreferenceMenuItems()");
expect(menu).not.toContain("getTabPreferenceMenuItems");
expect(menu).toContain("action: () => closeTabGroup(tab)");
expect(menu).toContain('visible: settingsStore.editorSettings.tabGroupMode !== "none"');
});
@@ -89,7 +81,7 @@ describe("EditorGroupTabBar semantic tab groups", () => {
// D1 (amended): closing a cluster is destructive and stays bar-local —
// scoped to this pane's tabs (props.tabs), still within the trigger's
// pinned section. Profile edits (rename/color/reset) keep global reach.
const closeGroup = sourceBetween("function tabsInSemanticGroup", "function getTabPreferenceMenuItems");
const closeGroup = sourceBetween("function tabsInSemanticGroup", "function getTabGroupMenuItems");
expect(closeGroup).toContain("props.tabs.filter((item) => item.pinned === tab.pinned && tabGroupKey(item) === groupKey)");
expect(closeGroup).toContain("queryStore.closeTabsByIds(tabsToClose, finalActiveTabId)");
});
@@ -572,9 +564,9 @@ describe("EditorGroupTabBar group behavior", () => {
try {
await settle();
const header = host.querySelector<HTMLButtonElement>(".tab-group-header")!;
// Horizontal headers lead with the database glyph; the rail marker stays vertical-only.
// Group headers consistently expose the placement marker.
expect(header.querySelector(".tab-group-database-icon")).not.toBeNull();
expect(header.querySelector(".tab-group-marker")).toBeNull();
expect(header.querySelector(".tab-group-marker")).not.toBeNull();
const chevron = header.querySelector<HTMLElement>(".tab-group-chevron")!;
expect(getComputedStyle(chevron).width).toBe("14px");
expect(getComputedStyle(chevron).height).toBe("14px");
@@ -475,13 +475,38 @@
}
.tab-group-marker {
width: 0.4rem;
height: 0.4rem;
width: 0.22rem;
height: 1rem;
flex: none;
border-radius: 9999px;
background: var(--tab-group-color);
}
.tab-organization-button {
display: inline-flex;
height: 1.75rem;
width: 1.75rem;
flex: 0 0 auto;
align-items: center;
justify-content: center;
align-self: flex-start;
margin-block: 0.25rem;
margin-inline: 0.25rem;
border: 1px solid transparent;
border-radius: 0.375rem;
color: var(--muted-foreground);
}
.tab-organization-button:hover {
border-color: var(--border);
background: var(--muted);
color: var(--foreground);
}
.tab-organization-button:focus-visible {
outline: 2px solid var(--ring);
outline-offset: -2px;
}
.tab-group-chevron {
width: 0.875rem;
height: 0.875rem;
@@ -544,6 +569,35 @@
line-height: 1;
}
/* Modern split bars use h-7 (1.75rem) pills. Keep the group header on that
* exact baseline; the surrounding 2.5rem strip supplies the outer breathing
* room. Wrapped rows opt out and use their intrinsic row height below. */
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-header {
height: 1.75rem;
align-self: center;
}
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-header-content {
height: 1.75rem;
}
/* The modern top/bottom bar joins the group header and its pills into one
* continuous horizontal rail. Rounded pseudo-elements at the seam create a
* visible bump (the “turn” between the two pieces), so keep that rail flat. */
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-header::after,
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-tab::after {
border-radius: 0;
}
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-header::after {
bottom: 0;
}
.app-tab-bar.separated-tab-layout:not(.vertical-tab-layout):not(:has(.wrap-mode))[data-placement="bottom"] .tab-group-header::after {
top: 0;
bottom: auto;
}
/* Wrapped rows must use their own content height. In particular, avoid
* height: 100% here: when a group expands into another row it makes the
* auto-sized flex container feed its height back into the row measurement. */
@@ -811,6 +865,9 @@
.app-tab-bar:not(.vertical-tab-layout):not(:has(.wrap-mode)) .tab-group-entry--collapsing {
pointer-events: none;
/* The keyframe already owns the collapse. Without this, removing the
* class at the end starts the base max-width transition a second time. */
transition: none;
animation: tab-group-collapse 140ms ease forwards;
}
@@ -1017,15 +1074,52 @@
box-shadow: 0 0 0 1px var(--background);
}
/* Both portal hosts size from the badges actually rendered, including search
* filtering and multiple editor groups. All bars fill the same host width. */
[data-workspace-tab-navigation],
[data-special-page-navigation] {
--collapsed-tab-rail-width: 3.5rem;
}
[data-workspace-tab-navigation]:has(.vertical-tab-layout--collapsed .tab-group-count),
[data-special-page-navigation]:has(.vertical-tab-layout--collapsed .tab-group-count) {
--collapsed-tab-rail-width: 4.5rem;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-header {
width: 2.5rem !important;
width: 100% !important;
height: 1.75rem;
margin: 0.35rem auto 0.1rem;
margin: 0.35rem 0 0.1rem;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-header-content {
justify-content: center;
padding: 0;
padding-inline: 0.55rem;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-header-content:has(.tab-group-count) {
justify-content: space-between;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-label,
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-pin {
display: none;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-count {
position: static;
z-index: auto;
margin-inline-start: auto;
min-width: 1.15rem;
height: 1.15rem;
padding-inline: 0.2rem;
transform: none;
box-shadow: none;
}
/* Reserve a separate slot for the count in the compact group header. */
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-header:has(.tab-group-count) .tab-group-database-icon {
display: none;
}
.vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-tab {
@@ -1074,12 +1168,6 @@
color: var(--tab-group-color);
}
.vertical-tab-layout .tab-group-marker {
width: 0.22rem;
height: 1rem;
border-radius: 9999px;
}
.vertical-tab-layout .tab-group-header:not(.tab-group-header--collapsed)::after {
content: "";
position: absolute;
+1
View File
@@ -6815,6 +6815,7 @@ export default withEnglishFallback({
tabPlacementLeft: "Sol tərəf",
tabPlacementRight: "Sağ tərəf",
tabPlacementDescription: "Yan vərəq zolaqları şaquli sürüşdürülür; bir neçə sətrə keçirmə yalnız məzmunun üstündə və ya altında tətbiq olunur.",
tabOrganization: "Mövqe, qruplaşdırma, sıralama",
tabGroup: "Qruplaşdırma meyarı",
tabGroupNone: "Qruplaşdırma yoxdur",
tabGroupDatabaseType: "Verilənlər bazasının növünə görə qruplaşdır",
+1
View File
@@ -6850,6 +6850,7 @@ export default {
tabPlacementLeft: "Left side",
tabPlacementRight: "Right side",
tabPlacementDescription: "Side tab bars scroll vertically; multi-row wrapping applies only above or below content.",
tabOrganization: "Position, grouping, sorting",
tabGroup: "Group by",
tabGroupNone: "No grouping",
tabGroupDatabaseType: "Group by database type",
+1
View File
@@ -6424,6 +6424,7 @@ export default withEnglishFallback({
tabPlacementLeft: "Lado izquierdo",
tabPlacementRight: "Lado derecho",
tabPlacementDescription: "Las barras laterales se desplazan verticalmente; el ajuste multilínea solo se aplica arriba o abajo.",
tabOrganization: "Posición, agrupación y ordenación",
tabGroup: "Agrupar por",
tabGroupNone: "Sin agrupación",
tabGroupDatabaseType: "Agrupar por tipo de base de datos",
+1
View File
@@ -6424,6 +6424,7 @@ export default withEnglishFallback({
tabPlacementLeft: "Lato sinistro",
tabPlacementRight: "Lato destro",
tabPlacementDescription: "Le barre laterali scorrono verticalmente; le righe multiple valgono solo sopra o sotto il contenuto.",
tabOrganization: "Posizione, raggruppamento e ordinamento",
tabGroup: "Raggruppa per",
tabGroupNone: "Nessun raggruppamento",
tabGroupDatabaseType: "Raggruppa per tipo di database",
+1
View File
@@ -6462,6 +6462,7 @@ export default withEnglishFallback({
tabPlacementLeft: "左側",
tabPlacementRight: "右側",
tabPlacementDescription: "サイドのタブバーは縦にスクロールします。複数行表示はコンテンツの上下でのみ利用できます。",
tabOrganization: "位置・グループ化・並び順",
tabGroup: "グループ化",
tabGroupNone: "グループ化しない",
tabGroupDatabaseType: "データベース種別でグループ化",
+1
View File
@@ -6167,6 +6167,7 @@ export default withEnglishFallback({
tabPlacementLeft: "왼쪽",
tabPlacementRight: "오른쪽",
tabPlacementDescription: "사이드 탭 표시줄은 세로로 스크롤됩니다. 여러 줄 표시는 콘텐츠 위 또는 아래에서만 적용됩니다.",
tabOrganization: "위치, 그룹화, 정렬",
tabGroup: "그룹 기준",
tabGroupNone: "그룹화 안 함",
tabGroupDatabaseType: "데이터베이스 유형별 그룹화",
+1
View File
@@ -6426,6 +6426,7 @@ export default withEnglishFallback({
tabPlacementLeft: "Lado esquerdo",
tabPlacementRight: "Lado direito",
tabPlacementDescription: "As barras laterais rolam verticalmente; a quebra em múltiplas linhas só se aplica acima ou abaixo do conteúdo.",
tabOrganization: "Posição, agrupamento e ordenação",
tabGroup: "Agrupar por",
tabGroupNone: "Sem agrupamento",
tabGroupDatabaseType: "Agrupar por tipo de banco de dados",
+1
View File
@@ -6697,6 +6697,7 @@ export default withEnglishFallback({
tabPlacementLeft: "Sol taraf",
tabPlacementRight: "Sağ taraf",
tabPlacementDescription: "Yan sekme çubukları dikey kayar; çok satırlı sarma yalnızca içeriğin üstünde veya altında geçerlidir.",
tabOrganization: "Konum, gruplama, sıralama",
tabGroup: "Gruplama ölçütü",
tabGroupNone: "Gruplama yok",
tabGroupDatabaseType: "Veritabanı türüne göre grupla",
+1
View File
@@ -6833,6 +6833,7 @@ export default withEnglishFallback({
tabPlacementLeft: "左侧",
tabPlacementRight: "右侧",
tabPlacementDescription: "侧边标签栏使用纵向滚动;多行平铺仅适用于上方或下方。",
tabOrganization: "位置、分组、排序",
tabGroup: "分组方式",
tabGroupNone: "不分组",
tabGroupDatabaseType: "按数据库类型分组",
+1
View File
@@ -5743,6 +5743,7 @@ export default withEnglishFallback({
tabPlacementLeft: "左側",
tabPlacementRight: "右側",
tabPlacementDescription: "側邊標籤欄使用垂直捲動;多行平鋪僅適用於上方或下方。",
tabOrganization: "位置、分組、排序",
tabGroup: "分組方式",
tabGroupNone: "不分組",
tabGroupDatabaseType: "依資料庫類型分組",