feat(tabs): color editor tabs by connection

This commit is contained in:
二丫讲梵
2026-09-29 01:46:01 +08:00
committed by GitHub
parent 130fc1cf77
commit 3c697feabf
19 changed files with 100 additions and 9 deletions
@@ -683,6 +683,7 @@ const editSavedSqlOpenTargetMode = ref<SavedSqlOpenTargetMode>(settingsStore.edi
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
const editTabLayout = ref(settingsStore.editorSettings.tabLayout);
const editTabPlacement = ref<TabPlacement>(settingsStore.editorSettings.tabPlacement);
const editColorizeConnectionTabs = ref(settingsStore.editorSettings.colorizeConnectionTabs);
const editTabGroupMode = ref<TabGroupMode>(settingsStore.editorSettings.tabGroupMode);
const editTabSortMode = ref<TabSortMode>(settingsStore.editorSettings.tabSortMode);
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
@@ -1044,6 +1045,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
appLayout: editAppLayout.value,
tabLayout: editTabLayout.value,
tabPlacement: editTabPlacement.value,
colorizeConnectionTabs: editColorizeConnectionTabs.value,
tabGroupMode: editTabGroupMode.value,
tabSortMode: editTabSortMode.value,
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
@@ -1707,6 +1709,7 @@ function syncEditorSettingsDraftFromStore() {
editAppLayout.value = settingsStore.editorSettings.appLayout;
editTabLayout.value = settingsStore.editorSettings.tabLayout;
editTabPlacement.value = settingsStore.editorSettings.tabPlacement;
editColorizeConnectionTabs.value = settingsStore.editorSettings.colorizeConnectionTabs;
editTabGroupMode.value = settingsStore.editorSettings.tabGroupMode;
editTabSortMode.value = settingsStore.editorSettings.tabSortMode;
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
@@ -1845,6 +1848,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
appLayout: editAppLayout,
tabLayout: editTabLayout,
tabPlacement: editTabPlacement,
colorizeConnectionTabs: editColorizeConnectionTabs,
tabGroupMode: editTabGroupMode,
tabSortMode: editTabSortMode,
showColumnCommentsInHeader: editShowColumnCommentsInHeader,
@@ -2355,6 +2359,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
editTabLayout.value = DEFAULT_EDITOR_SETTINGS.tabLayout;
editTabPlacement.value = DEFAULT_EDITOR_SETTINGS.tabPlacement;
editColorizeConnectionTabs.value = DEFAULT_EDITOR_SETTINGS.colorizeConnectionTabs;
editTabGroupMode.value = DEFAULT_EDITOR_SETTINGS.tabGroupMode;
editTabSortMode.value = DEFAULT_EDITOR_SETTINGS.tabSortMode;
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
@@ -7235,6 +7240,14 @@ onUnmounted(() => {
</div>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="colorize-connection-tabs">{{ t("settings.colorizeConnectionTabs") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.colorizeConnectionTabsDescription") }}</p>
</div>
<Switch id="colorize-connection-tabs" v-model="editColorizeConnectionTabs" />
</div>
<div class="settings-appearance-group">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="space-y-2">
@@ -84,7 +84,7 @@ import { hexToRgba } from "@/lib/common/color";
import { copyToClipboard } from "@/lib/common/clipboard";
import { parseTabDragPayload, serializeTabDragPayload } from "@/lib/tabs/tabDrag";
import { createCloseAllTabMenuItem, createCloseLeftTabMenuItem, createCloseOtherTabMenuItem, createCloseRightTabMenuItem, createCloseTabMenuItem, createLocateTabMenuItem, createPinTabMenuItem, createRenameDuplicateTabItems } from "@/lib/tabs/tabMenu";
import { connectionColor, dirtyTabTitleStyle, tabColorStyle as sharedTabColorStyle, tabDatabaseIconType, tabDisplayTitle, tabDisplayTitles, tabIconClass, tabTooltipLines } from "@/lib/tabs/tabPresentation";
import { tabConnectionColor, dirtyTabTitleStyle, tabColorStyle as sharedTabColorStyle, tabDatabaseIconType, tabDisplayTitle, tabDisplayTitles, tabIconClass, tabTooltipLines } from "@/lib/tabs/tabPresentation";
import { activeTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
import "./appTabBar.css";
import type { QueryTab } from "@/types/database";
@@ -691,7 +691,7 @@ function expandTabGroupForTab(tabId: string | null) {
}
function defaultTabGroupColor(tab: QueryTab) {
const connectionGroupColor = settingsStore.editorSettings.tabGroupMode === "connection" ? connectionColor(tab.connectionId) : undefined;
const connectionGroupColor = settingsStore.editorSettings.tabGroupMode === "connection" && settingsStore.editorSettings.colorizeConnectionTabs ? tabConnectionColor(tab.connectionId) : undefined;
let hash = 0;
for (const character of tabGroupKey(tab)) hash = (hash * 31 + character.codePointAt(0)!) | 0;
return connectionGroupColor || tabGroupPalette[Math.abs(hash) % tabGroupPalette.length]!;
@@ -903,7 +903,7 @@ function tabColorStyle(tab: QueryTab): CSSProperties | undefined {
if (!isTabActive(tab)) {
return undefined;
}
const color = connectionColor(tab.connectionId);
const color = settingsStore.editorSettings.colorizeConnectionTabs ? tabConnectionColor(tab.connectionId) : "";
return { "--app-tab-background": color ? hexToRgba(color, 0.12) : "var(--accent)" } as CSSProperties;
}
return sharedTabColorStyle(tab, isTabActive(tab), isClassicLayout.value);
+2
View File
@@ -7968,6 +7968,8 @@ export default {
tabLayoutScrollDescription: "Tabs scroll horizontally in one row when overflowing.",
tabLayoutWrap: "Multi-row wrap",
tabLayoutWrapDescription: "Tabs wrap to multiple rows to show all at once.",
colorizeConnectionTabs: "Color tabs by connection",
colorizeConnectionTabsDescription: "Use each connection’s configured color for editor tabs.",
tabPlacement: "Tab bar position",
tabPlacementTop: "Top",
tabPlacementBottom: "Bottom",
+2
View File
@@ -7322,6 +7322,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "Las pestañas se desplazan horizontalmente al desbordarse.",
tabLayoutWrap: "Ajuste multilínea",
tabLayoutWrapDescription: "Las pestañas se ajustan en varias filas para mostrarlas todas.",
colorizeConnectionTabs: "Colorear pestañas por conexión",
colorizeConnectionTabsDescription: "Usar el color configurado de cada conexión para las pestañas del editor.",
tabPlacement: "Posición de la barra de pestañas",
tabPlacementTop: "Arriba",
tabPlacementBottom: "Abajo",
+2
View File
@@ -7246,6 +7246,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "Le schede scorrono orizzontalmente quando traboccano.",
tabLayoutWrap: "Righe multiple",
tabLayoutWrapDescription: "Le schede vanno a capo su più righe per mostrarle tutte.",
colorizeConnectionTabs: "Colora le schede in base alla connessione",
colorizeConnectionTabsDescription: "Usa il colore configurato di ogni connessione per le schede dell'editor.",
tabPlacement: "Posizione barra schede",
tabPlacementTop: "In alto",
tabPlacementBottom: "In basso",
+2
View File
@@ -7277,6 +7277,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "タブが1行に収まらないとき水平スクロールします。",
tabLayoutWrap: "複数行折り返し",
tabLayoutWrapDescription: "タブが1行に収まらないとき折り返してすべて表示します。",
colorizeConnectionTabs: "接続ごとにタブを色分け",
colorizeConnectionTabsDescription: "各接続に設定された色をエディターのタブに使用します。",
tabPlacement: "タブバーの位置",
tabPlacementTop: "上",
tabPlacementBottom: "下",
+2
View File
@@ -7022,6 +7022,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "탭이 넘칠 때 한 행에서 가로로 스크롤됩니다.",
tabLayoutWrap: "다중 행 줄바꿈",
tabLayoutWrapDescription: "탭이 여러 행으로 줄바꿈되어 모두 한 번에 표시됩니다.",
colorizeConnectionTabs: "연결별로 탭 색상 표시",
colorizeConnectionTabsDescription: "각 연결에 설정된 색상을 편집기 탭에 사용합니다.",
tabPlacement: "탭 표시줄 위치",
tabPlacementTop: "위쪽",
tabPlacementBottom: "아래쪽",
+2
View File
@@ -7248,6 +7248,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "As abas rolam horizontalmente quando excedem o espaço.",
tabLayoutWrap: "Quebra em múltiplas linhas",
tabLayoutWrapDescription: "As abas quebram em múltiplas linhas para exibir todas.",
colorizeConnectionTabs: "Colorir abas por conexão",
colorizeConnectionTabsDescription: "Usar a cor configurada de cada conexão nas abas do editor.",
tabPlacement: "Posição da barra de abas",
tabPlacementTop: "Superior",
tabPlacementBottom: "Inferior",
+2
View File
@@ -8474,6 +8474,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "При переполнении вкладки прокручиваются горизонтально в одну строку.",
tabLayoutWrap: "Перенос на несколько строк",
tabLayoutWrapDescription: "Вкладки переносятся на несколько строк, показывая все сразу.",
colorizeConnectionTabs: "Раскрашивать вкладки по подключению",
colorizeConnectionTabsDescription: "Использовать настроенный цвет каждого подключения для вкладок редактора.",
tabPlacement: "Расположение панели вкладок",
tabPlacementTop: "Сверху",
tabPlacementBottom: "Снизу",
+2
View File
@@ -7934,6 +7934,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "标签页超出一行时水平滚动显示。",
tabLayoutWrap: "多行平铺",
tabLayoutWrapDescription: "标签页超出一行时自动换行,平铺显示所有标签。",
colorizeConnectionTabs: "按连接颜色区分标签页",
colorizeConnectionTabsDescription: "使用连接配置的颜色显示编辑器标签页。",
tabPlacement: "标签栏位置",
tabPlacementTop: "上方",
tabPlacementBottom: "下方",
+2
View File
@@ -6570,6 +6570,8 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "標籤頁超出一行時水平滾動顯示。",
tabLayoutWrap: "多行平鋪",
tabLayoutWrapDescription: "標籤頁超出一行時自動換行,平鋪顯示所有標籤。",
colorizeConnectionTabs: "依連線顏色區分分頁",
colorizeConnectionTabsDescription: "使用每個連線設定的顏色顯示編輯器分頁。",
tabPlacement: "標籤欄位置",
tabPlacementTop: "上方",
tabPlacementBottom: "下方",
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import {
connectionGroupDisplayName,
executionSummaryItems,
@@ -14,6 +15,7 @@ import {
resultSourceRange,
statementExecutionMarkers,
tabColorStyle,
tabConnectionColor,
tabDatabaseIconType,
tabDisplayTitle,
tabDisplayTitles,
@@ -704,6 +706,7 @@ describe("shared tab presentation helpers", () => {
});
it("builds active/inactive color styles for classic and non-classic layouts", () => {
useSettingsStore().editorSettings.colorizeConnectionTabs = false;
// Node has no window/CSS globals: pin both to modern-engine answers.
vi.stubGlobal("CSS", { supports: () => true });
vi.stubGlobal("window", {
@@ -725,6 +728,7 @@ describe("shared tab presentation helpers", () => {
});
it("swaps inline color-mix tab colors for concrete rgba on legacy WebViews", () => {
useSettingsStore().editorSettings.colorizeConnectionTabs = false;
// WebKit without color-mix() invalidates the inline values at computed-value
// time, which left the active tab with no background at all (macOS 12); and
// it cannot substitute var() inside inline custom properties, so the legacy
@@ -743,6 +747,18 @@ describe("shared tab presentation helpers", () => {
}
});
it("assigns stable palette colors to unconfigured connections", () => {
const connectionStore = useConnectionStore();
connectionStore.connections = [
{ id: "conn-1", name: "One", db_type: "mysql", color: "" },
{ id: "conn-2", name: "Two", db_type: "mysql", color: "#abcdef" },
] as ConnectionConfig[];
expect(tabConnectionColor("conn-1")).toBe(tabConnectionColor("conn-1"));
expect(tabConnectionColor("conn-1")).not.toBe("");
expect(tabConnectionColor("conn-1")).not.toBe(tabConnectionColor("conn-3"));
expect(tabConnectionColor("conn-2")).toBe("#abcdef");
});
it("resolves MQ driver icons from the connection store", () => {
const connectionStore = useConnectionStore();
connectionStore.connections = [
@@ -40,6 +40,11 @@ const translate = (key: string) => translations[key] ?? key;
const allCategories = new Set(Object.keys(categoryLabels) as SettingsCategory[]);
describe("settings search", () => {
it("finds connection tab colors by their Chinese label", () => {
const entries = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { isWeb: false, visibleCategories: allCategories }, (key) => (key === "settings.colorizeConnectionTabs" ? "按连接颜色区分标签页" : key), categoryLabels);
expect(searchSettings(entries, "连接颜色", "zh-CN")).toEqual(expect.arrayContaining([expect.objectContaining({ id: "appearance-connection-tab-colors", category: "appearance", targetId: "appearance" })]));
});
const definitions: readonly SettingsSearchDefinition[] = [
{ id: "font", category: "editor", titleKey: "font", descriptionKey: "fontDescription" },
{ id: "export", category: "data", titleKey: "export" },
@@ -46,6 +46,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"appLayout",
"tabLayout",
"tabPlacement",
"colorizeConnectionTabs",
"tabGroupMode",
"tabSortMode",
"showColumnCommentsInHeader",
@@ -188,6 +188,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "appearance-corners", category: "appearance", titleKey: "settings.cornerStyle", targetId: "appearance" },
{ id: "appearance-layout", category: "appearance", titleKey: "settings.appLayout", targetId: "appearance" },
{ id: "appearance-tab-layout", category: "appearance", titleKey: "settings.tabLayout", targetId: "appearance" },
{ id: "appearance-connection-tab-colors", category: "appearance", titleKey: "settings.colorizeConnectionTabs", descriptionKey: "settings.colorizeConnectionTabsDescription", targetId: "appearance" },
{ id: "appearance-icons", category: "appearance", titleKey: "settings.iconTheme", targetId: "appearance", visible: desktopOnly },
{ id: "appearance-tray", category: "appearance", titleKey: "settings.showTrayIcon", descriptionKey: "settings.showTrayIconDescription", targetId: "appearance", visible: desktopOnly },
{ id: "appearance-quit", category: "appearance", titleKey: "settings.quitOnClose", descriptionKey: "settings.quitOnCloseDescription", targetId: "appearance", visible: desktopOnly },
@@ -116,6 +116,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"tabLayout",
"tabPlacement",
"tabGroupMode",
"colorizeConnectionTabs",
"tabSortMode",
"sidebarActivation",
"sidebarObjectDisplay",
+35 -6
View File
@@ -42,6 +42,36 @@ export function connectionColor(connectionId: string): string {
return connectionStore.getConfig(connectionId)?.color || "";
}
function tabConnectionGeneratedColor(connectionId: string): string {
let hash = 2166136261;
for (const character of connectionId) {
hash ^= character.codePointAt(0)!;
hash = Math.imul(hash, 16777619);
}
const hue = (hash >>> 0) % 360;
const saturation = 68;
const lightness = 52;
const chroma = (1 - Math.abs((2 * lightness) / 100 - 1)) * (saturation / 100);
const hueSector = hue / 60;
const x = chroma * (1 - Math.abs((hueSector % 2) - 1));
const match = lightness / 100 - chroma / 2;
const [red, green, blue] = hueSector < 1 ? [chroma, x, 0] : hueSector < 2 ? [x, chroma, 0] : hueSector < 3 ? [0, chroma, x] : hueSector < 4 ? [0, x, chroma] : hueSector < 5 ? [x, 0, chroma] : [chroma, 0, x];
return `#${[red, green, blue]
.map((channel) =>
Math.round((channel + match) * 255)
.toString(16)
.padStart(2, "0"),
)
.join("")}`;
}
/** Returns a stable visual identity for a tab's connection when no custom color is set. */
export function tabConnectionColor(connectionId: string): string {
const configured = connectionColor(connectionId);
if (configured) return configured;
return tabConnectionGeneratedColor(connectionId);
}
export function isConnectionReadonly(connectionId: string): boolean {
const connectionStore = useConnectionStore();
return connectionStore.getConfig(connectionId)?.read_only ?? false;
@@ -710,15 +740,14 @@ export function appTabActiveIndicator(): string {
}
export function tabColorStyle(tab: QueryTab, active: boolean, isClassic: boolean): CSSProperties | undefined {
const activeIndicator = appTabActiveIndicator();
const color = connectionColor(tab.connectionId);
if (!color) {
if (!useSettingsStore().editorSettings.colorizeConnectionTabs) {
const activeIndicator = appTabActiveIndicator();
const background = appTabActiveBackground();
if (isClassic) {
return active ? { "--app-tab-background": background, boxShadow: activeIndicator } : undefined;
}
if (isClassic) return active ? { "--app-tab-background": background, boxShadow: activeIndicator } : undefined;
return active ? { "--app-tab-background": background, borderColor: "var(--ring)" } : undefined;
}
const activeIndicator = appTabActiveIndicator();
const color = tabConnectionColor(tab.connectionId);
if (isClassic) {
return {
"--app-tab-background": hexToRgba(color, active ? 0.24 : 0.07),
@@ -1280,7 +1280,9 @@ describe("settingsStore persisted settings initialization", () => {
const { useSettingsStore } = await import("@/stores/settingsStore");
const firstStore = useSettingsStore();
await firstStore.initEditorSettings();
expect(firstStore.editorSettings.colorizeConnectionTabs).toBe(true);
await firstStore.updateEditorSettingsAndPersist({
colorizeConnectionTabs: false,
tabPlacement: "left",
tabGroupMode: "connection",
tabSortMode: "title-asc",
@@ -1294,6 +1296,7 @@ describe("settingsStore persisted settings initialization", () => {
await restartedStore.initEditorSettings();
expect(restartedStore.editorSettings).toMatchObject({
colorizeConnectionTabs: false,
tabPlacement: "left",
tabGroupMode: "connection",
tabSortMode: "title-asc",
+4
View File
@@ -881,6 +881,7 @@ export interface EditorSettings {
compactTabTitle: boolean;
tabLayout: TabLayoutMode;
tabPlacement: TabPlacement;
colorizeConnectionTabs: boolean;
tabGroupMode: TabGroupMode;
tabGroupCustomizations: Record<string, TabGroupCustomization>;
tabSortMode: TabSortMode;
@@ -1170,6 +1171,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
compactTabTitle: false,
tabLayout: "scroll",
tabPlacement: "top",
colorizeConnectionTabs: true,
tabGroupMode: "none",
tabGroupCustomizations: {},
tabSortMode: "manual",
@@ -1741,6 +1743,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
tabLayout: normalizeTabLayout(settings.tabLayout),
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
colorizeConnectionTabs: typeof settings.colorizeConnectionTabs === "boolean" ? settings.colorizeConnectionTabs : DEFAULT_EDITOR_SETTINGS.colorizeConnectionTabs,
tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
tabGroupCustomizations: normalizeTabGroupCustomizations(settings.tabGroupCustomizations),
tabSortMode: normalizeTabSortMode(settings.tabSortMode),
@@ -2578,6 +2581,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
if (partial.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
if (partial.colorizeConnectionTabs !== undefined) editorSettings.value.colorizeConnectionTabs = partial.colorizeConnectionTabs === true;
if (partial.tabGroupMode !== undefined) editorSettings.value.tabGroupMode = normalizeTabGroupMode(partial.tabGroupMode);
if (partial.tabGroupCustomizations !== undefined) editorSettings.value.tabGroupCustomizations = normalizeTabGroupCustomizations(partial.tabGroupCustomizations);
if (partial.tabSortMode !== undefined) editorSettings.value.tabSortMode = normalizeTabSortMode(partial.tabSortMode);