feat(structure): unify scrollbars and restore column scrolling

This commit is contained in:
gggaiitx
2026-09-18 20:05:49 +08:00
committed by GitHub
parent 9788c20d3c
commit 3597a00759
5 changed files with 261 additions and 28 deletions
@@ -14661,6 +14661,39 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
font-weight: 600;
}
/* Unified scrollbar look for the DDL tab (matches the other Table Info tabs). */
.ddl-code::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.ddl-code::-webkit-scrollbar-track {
background: transparent;
}
.ddl-code::-webkit-scrollbar-thumb {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.ddl-code::-webkit-scrollbar-thumb:hover {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
html.dbx-legacy-webview.dark .ddl-code::-webkit-scrollbar-thumb {
background: rgba(212, 212, 216, 0.3);
}
html.dbx-legacy-webview.dark .ddl-code::-webkit-scrollbar-thumb:hover {
background: rgba(212, 212, 216, 0.48);
}
.ddl-code :deep(.ddl-ident) {
color: rgb(58 168 91);
color: oklch(0.65 0.15 150);
@@ -39,7 +39,7 @@ const { t } = useI18n();
</script>
<template>
<div v-if="props.activeTab === 'columns'" class="flex-1 min-h-0 overflow-auto">
<div v-if="props.activeTab === 'columns'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.columnsLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -109,7 +109,7 @@ const { t } = useI18n();
</table>
</div>
<div v-else-if="props.activeTab === 'indexes'" class="flex-1 min-h-0 overflow-auto">
<div v-else-if="props.activeTab === 'indexes'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.indexesLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -145,7 +145,7 @@ const { t } = useI18n();
</div>
</div>
<div v-else-if="props.activeTab === 'foreignKeys'" class="flex-1 min-h-0 overflow-auto">
<div v-else-if="props.activeTab === 'foreignKeys'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.foreignKeysLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -166,7 +166,7 @@ const { t } = useI18n();
</div>
</div>
<div v-else-if="props.activeTab === 'triggers'" class="flex-1 min-h-0 overflow-auto">
<div v-else-if="props.activeTab === 'triggers'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.triggersLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -187,7 +187,7 @@ const { t } = useI18n();
</div>
</div>
<div v-else-if="props.activeTab === 'constraints'" class="flex-1 min-h-0 overflow-auto">
<div v-else-if="props.activeTab === 'constraints'" class="table-info-scroller flex-1 min-h-0 overflow-auto">
<div v-if="props.constraintsLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -215,3 +215,38 @@ const { t } = useI18n();
</div>
</div>
</template>
<style scoped>
/* Unified scrollbar look for the Table Info panel tabs (matches structure editor). */
.table-info-scroller::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.table-info-scroller::-webkit-scrollbar-track {
background: transparent;
}
.table-info-scroller::-webkit-scrollbar-thumb {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.table-info-scroller::-webkit-scrollbar-thumb:hover {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
html.dbx-legacy-webview.dark .table-info-scroller::-webkit-scrollbar-thumb {
background: rgba(212, 212, 216, 0.3);
}
html.dbx-legacy-webview.dark .table-info-scroller::-webkit-scrollbar-thumb:hover {
background: rgba(212, 212, 216, 0.48);
}
</style>
@@ -2756,23 +2756,25 @@ defineExpose({
<!-- Structure mode: table structure editor -->
<template v-else-if="activeTab.mode === 'structure'">
<TableStructureEditor
ref="tableStructureEditorRef"
:key="activeTab.id"
:connection-id="activeTab.connectionId"
:database="activeTab.database"
:catalog="activeTab.catalog"
:schema="activeTab.schema"
:table-name="activeTab.structureTableName || ''"
:initial-tab="activeTab.structureInitialTab"
:initial-tab-request-id="activeTab.structureInitialTabRequestId"
:initial-target="activeTab.structureInitialTarget"
:draft="activeTab.structureDraft"
@update:draft="(draft) => (activeTab.structureDraft = draft)"
@saved="(commentChanged) => emit('structureEditorSaved', activeTab.id, commentChanged)"
@close="emit('structureEditorClose', activeTab.id)"
@open-settings="(initialTab, initialSection) => emit('openSettings', initialTab, initialSection)"
/>
<div class="flex-1 min-h-0">
<TableStructureEditor
ref="tableStructureEditorRef"
:key="activeTab.id"
:connection-id="activeTab.connectionId"
:database="activeTab.database"
:catalog="activeTab.catalog"
:schema="activeTab.schema"
:table-name="activeTab.structureTableName || ''"
:initial-tab="activeTab.structureInitialTab"
:initial-tab-request-id="activeTab.structureInitialTabRequestId"
:initial-target="activeTab.structureInitialTarget"
:draft="activeTab.structureDraft"
@update:draft="(draft) => (activeTab.structureDraft = draft)"
@saved="(commentChanged) => emit('structureEditorSaved', activeTab.id, commentChanged)"
@close="emit('structureEditorClose', activeTab.id)"
@open-settings="(initialTab, initialSection) => emit('openSettings', initialTab, initialSection)"
/>
</div>
</template>
<template v-else-if="activeTab.mode === 'users' && activeConnection">
@@ -5032,7 +5032,7 @@ watch(
<div ref="structureHorizontalScrollbarThumbRef" class="structure-horizontal-scrollbar__thumb" />
</div>
<TabsContent ref="foreignKeysScrollerRef" v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('foreignKeys', $event)">
<TabsContent ref="foreignKeysScrollerRef" v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('foreignKeys', $event)">
<div v-if="foreignKeysLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
@@ -5085,7 +5085,7 @@ watch(
</div>
</TabsContent>
<TabsContent ref="constraintsScrollerRef" v-if="tableMetadataCapabilities.constraints" value="constraints" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('constraints', $event)">
<TabsContent ref="constraintsScrollerRef" v-if="tableMetadataCapabilities.constraints" value="constraints" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('constraints', $event)">
<div v-if="constraintsLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
@@ -5111,7 +5111,7 @@ watch(
</div>
</TabsContent>
<TabsContent ref="triggersScrollerRef" v-if="tableMetadataCapabilities.triggers" value="triggers" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('triggers', $event)">
<TabsContent ref="triggersScrollerRef" v-if="tableMetadataCapabilities.triggers" value="triggers" class="structure-card-scroller m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('triggers', $event)">
<div v-if="triggersLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
@@ -5197,7 +5197,7 @@ watch(
</div>
</TabsContent>
<TabsContent ref="ddlScrollerRef" v-if="tableMetadataCapabilities.ddl" value="ddl" force-mount class="relative m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)] data-[state=inactive]:hidden" @scroll.passive="onStructureContentScroll('ddl', $event)">
<TabsContent ref="ddlScrollerRef" v-if="tableMetadataCapabilities.ddl" value="ddl" force-mount class="structure-card-scroller relative m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)] data-[state=inactive]:hidden" @scroll.passive="onStructureContentScroll('ddl', $event)">
<div v-if="ddlLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
@@ -5411,18 +5411,87 @@ watch(
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35)) !important;
}
/* DDL (CodeMirror) scrollbars match the grid scroller style. */
.structure-ddl-editor :deep(.cm-scroller::-webkit-scrollbar) {
width: 10px;
height: 10px;
}
.structure-ddl-editor :deep(.cm-scroller::-webkit-scrollbar-track) {
background: transparent;
}
.structure-ddl-editor :deep(.cm-scroller::-webkit-scrollbar-thumb) {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.structure-ddl-editor :deep(.cm-scroller::-webkit-scrollbar-thumb:hover) {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
.structure-table-scroller::-webkit-scrollbar {
width: 8px;
width: 10px;
height: 0;
}
/* Foreign keys / constraints / triggers card lists: both scrollbars match. */
.structure-card-scroller::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.structure-card-scroller::-webkit-scrollbar-track {
background: transparent;
}
.structure-card-scroller::-webkit-scrollbar-thumb {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.structure-card-scroller::-webkit-scrollbar-thumb:hover {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
.structure-table-scroller::-webkit-scrollbar-track {
background: transparent;
}
.structure-table-scroller::-webkit-scrollbar-thumb {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.structure-table-scroller::-webkit-scrollbar-thumb:hover {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
.structure-horizontal-scrollbar {
position: relative;
height: 10px;
flex-shrink: 0;
cursor: pointer;
touch-action: none;
background: var(--background);
background: transparent;
}
.structure-horizontal-scrollbar__thumb {
@@ -5431,6 +5500,7 @@ watch(
height: 4px;
min-width: 24px;
border-radius: 999px;
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
transition:
top 120ms ease,
@@ -5442,6 +5512,7 @@ watch(
.structure-horizontal-scrollbar--dragging .structure-horizontal-scrollbar__thumb {
top: 2px;
height: 6px;
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
}
@@ -5493,3 +5564,46 @@ watch(
background-color: var(--background);
}
</style>
<style>
/* Non-scoped so the scrollbar pseudo-elements on CodeMirror's cm-scroller actually match in WebView2.
Matches the grid / card scroller scrollbar style (10px track, 4px thumb, 6px on hover). */
.structure-ddl-editor .cm-scroller::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.structure-ddl-editor .cm-scroller::-webkit-scrollbar-track {
background: transparent;
}
.structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb {
background: rgba(82, 82, 82, 0.3);
background: color-mix(in oklab, var(--foreground) 30%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
border-radius: 999px;
}
.structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb:hover {
background: rgba(82, 82, 82, 0.48);
background: color-mix(in oklab, var(--foreground) 48%, transparent);
border-width: 2px;
background-clip: padding-box;
}
html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb,
html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb,
html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb,
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar__thumb {
background: rgba(212, 212, 216, 0.3);
}
html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb:hover,
html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb:hover,
html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb:hover,
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar:hover .structure-horizontal-scrollbar__thumb,
html.dbx-legacy-webview.dark .structure-horizontal-scrollbar--dragging .structure-horizontal-scrollbar__thumb {
background: rgba(212, 212, 216, 0.48);
}
</style>
@@ -24,6 +24,9 @@ const changelogPanelSource = readFileSync(new URL("../../components/settings/Cha
const editorSettingsDialogSource = readFileSync(new URL("../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
const switchSource = readFileSync(new URL("../../components/ui/switch/Switch.vue", import.meta.url), "utf8");
const aiAssistantSource = readFileSync(new URL("../../components/editor/AiAssistant.vue", import.meta.url), "utf8");
const dataGridSource = readFileSync(new URL("../../components/grid/DataGrid.vue", import.meta.url), "utf8");
const dataGridTableInfoPanelsSource = readFileSync(new URL("../../components/grid/DataGridTableInfoPanels.vue", import.meta.url), "utf8");
const tableStructureEditorSource = readFileSync(new URL("../../components/structure/TableStructureEditor.vue", import.meta.url), "utf8");
const desktopIndexSource = readFileSync(new URL("../../../index.html", import.meta.url), "utf8");
const connectionDialogLegacyCss = readFileSync(new URL("../../../public/connection-dialog-legacy.css", import.meta.url), "utf8");
const legacyWebViewSource = readFileSync(new URL("../../lib/ui/legacyWebView.ts", import.meta.url), "utf8");
@@ -293,6 +296,52 @@ describe("legacy WebView CSS fallbacks", () => {
expect(aiAssistantSource).toContain("background: rgba(212, 212, 216, 0.45);");
});
it("keeps structure and table-info scrollbars visible without OKLab color mixing", () => {
const sources = [dataGridSource, dataGridTableInfoPanelsSource, tableStructureEditorSource];
const thumbRules = sources.flatMap((source) => [...source.matchAll(/[^{}]+::-webkit-scrollbar-thumb(?:\)|)\s*\{[^{}]+\}/g)].map((match) => match[0])).filter((rule) => rule.includes("color-mix(in oklab, var(--foreground) 30%, transparent)"));
const hoverRules = sources.flatMap((source) => [...source.matchAll(/[^{}]+::-webkit-scrollbar-thumb:hover(?:\)|)\s*\{[^{}]+\}/g)].map((match) => match[0])).filter((rule) => rule.includes("color-mix(in oklab, var(--foreground) 48%, transparent)"));
expect(thumbRules).toHaveLength(6);
expect(hoverRules).toHaveLength(6);
for (const rule of thumbRules) {
expect(rule.indexOf("background: rgba(82, 82, 82, 0.3);")).toBeGreaterThan(-1);
expect(rule.indexOf("background: rgba(82, 82, 82, 0.3);")).toBeLessThan(rule.indexOf("background: color-mix(in oklab, var(--foreground) 30%, transparent);"));
}
for (const rule of hoverRules) {
expect(rule.indexOf("background: rgba(82, 82, 82, 0.48);")).toBeGreaterThan(-1);
expect(rule.indexOf("background: rgba(82, 82, 82, 0.48);")).toBeLessThan(rule.indexOf("background: color-mix(in oklab, var(--foreground) 48%, transparent);"));
}
const horizontalThumbStart = tableStructureEditorSource.indexOf(".structure-horizontal-scrollbar__thumb {");
const horizontalHoverStart = tableStructureEditorSource.indexOf(".structure-horizontal-scrollbar:hover .structure-horizontal-scrollbar__thumb,", horizontalThumbStart);
const horizontalThumb = tableStructureEditorSource.slice(horizontalThumbStart, horizontalHoverStart);
const horizontalHover = tableStructureEditorSource.slice(horizontalHoverStart, tableStructureEditorSource.indexOf("/* Editable values", horizontalHoverStart));
expect(horizontalThumb.indexOf("background: rgba(82, 82, 82, 0.3);")).toBeGreaterThan(-1);
expect(horizontalThumb.indexOf("background: rgba(82, 82, 82, 0.3);")).toBeLessThan(horizontalThumb.indexOf("background: color-mix(in oklab, var(--foreground) 30%, transparent);"));
expect(horizontalHover.indexOf("background: rgba(82, 82, 82, 0.48);")).toBeGreaterThan(-1);
expect(horizontalHover.indexOf("background: rgba(82, 82, 82, 0.48);")).toBeLessThan(horizontalHover.indexOf("background: color-mix(in oklab, var(--foreground) 48%, transparent);"));
expect(dataGridSource).toMatch(/html\.dbx-legacy-webview\.dark \.ddl-code::-webkit-scrollbar-thumb\s*\{\s*background: rgba\(212, 212, 216, 0\.3\);/);
expect(dataGridSource).toMatch(/html\.dbx-legacy-webview\.dark \.ddl-code::-webkit-scrollbar-thumb:hover\s*\{\s*background: rgba\(212, 212, 216, 0\.48\);/);
expect(dataGridTableInfoPanelsSource).toMatch(/html\.dbx-legacy-webview\.dark \.table-info-scroller::-webkit-scrollbar-thumb\s*\{\s*background: rgba\(212, 212, 216, 0\.3\);/);
expect(dataGridTableInfoPanelsSource).toMatch(/html\.dbx-legacy-webview\.dark \.table-info-scroller::-webkit-scrollbar-thumb:hover\s*\{\s*background: rgba\(212, 212, 216, 0\.48\);/);
const structureLegacyThumbStart = tableStructureEditorSource.indexOf("html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb,");
const structureLegacyHoverStart = tableStructureEditorSource.indexOf("html.dbx-legacy-webview.dark .structure-ddl-editor .cm-scroller::-webkit-scrollbar-thumb:hover,", structureLegacyThumbStart);
const structureLegacyThumb = tableStructureEditorSource.slice(structureLegacyThumbStart, structureLegacyHoverStart);
const structureLegacyHover = tableStructureEditorSource.slice(structureLegacyHoverStart);
expect(structureLegacyThumbStart).toBeGreaterThan(-1);
expect(structureLegacyHoverStart).toBeGreaterThan(structureLegacyThumbStart);
expect(structureLegacyThumb).toContain("html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb,");
expect(structureLegacyThumb).toContain("html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb,");
expect(structureLegacyThumb).toContain("html.dbx-legacy-webview.dark .structure-horizontal-scrollbar__thumb");
expect(structureLegacyThumb).toContain("background: rgba(212, 212, 216, 0.3);");
expect(structureLegacyHover).toContain("html.dbx-legacy-webview.dark .structure-card-scroller::-webkit-scrollbar-thumb:hover,");
expect(structureLegacyHover).toContain("html.dbx-legacy-webview.dark .structure-table-scroller::-webkit-scrollbar-thumb:hover,");
expect(structureLegacyHover).toContain("html.dbx-legacy-webview.dark .structure-horizontal-scrollbar:hover .structure-horizontal-scrollbar__thumb,");
expect(structureLegacyHover).toContain("html.dbx-legacy-webview.dark .structure-horizontal-scrollbar--dragging .structure-horizontal-scrollbar__thumb");
expect(structureLegacyHover).toContain("background: rgba(212, 212, 216, 0.48);");
});
it("keeps selected tiles readable in WebViews without color-mix support", () => {
const fallbackStart = globalsCss.indexOf("@supports not (background-color: color-mix(in srgb, black 10%, transparent))");
const choiceFallback = globalsCss.indexOf(".dbx-choice-selected", fallbackStart);