fix(sidebar): add draggable horizontal scrollbar

This commit is contained in:
zipg authored and GitHub committed 2026-07-25 19:40:49 +08:00
1 parent cff987f748
commit 30641f3c89
5 files changed
+351 -32

No files matched your search

@@ -41,8 +41,9 @@ import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState";
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
import { sortConnectionListForDisplay } from "@/lib/sidebar/connectionListSort";
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
import { alignedSidebarCommentLabelWidths, isSidebarCommentAlignableNode, sidebarTreeNodeComment } from "@/lib/sidebar/sidebarTreeItemLayout";
import { sidebarTableStorageScopes, supportsSidebarTableStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
import { alignedSidebarCommentLabelWidths, isSidebarCommentAlignableNode, sidebarTreeNaturalContentWidth, sidebarTreeNodeComment, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
import { formatSidebarObjectStorage, sidebarTableStorageScopes, supportsSidebarTableStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
import { sidebarScrollbarGeometry as calculateSidebarScrollbarGeometry } from "@/lib/sidebar/sidebarScrollbar";
const { t } = useI18n();
const store = useConnectionStore();
@@ -58,6 +59,7 @@ const pointerInsideTree = ref(false);
const treeScrollerRef = ref<InstanceType<typeof RecycleScroller> | null>(null);
const plainTreeScrollerRef = ref<HTMLElement | null>(null);
const sidebarScrollbarTrackRef = ref<HTMLElement | null>(null);
const sidebarHorizontalScrollbarTrackRef = ref<HTMLElement | null>(null);
const sidebarContextMenuRef = ref<{ close: () => void } | null>(null);
const sidebarContextMenuItems = ref<ContextMenuItem[]>([]);
const sidebarContextMenuTarget = ref<SidebarActionTarget | null>(null);
@@ -377,7 +379,10 @@ const flatNodes = computed<FlatTreeNode[]>(() =>
const sidebarCommentLabelWidths = shallowRef(new Map<string, number>());
let sidebarCommentMeasureFrame = 0;
const sidebarTreeContentWidth = ref(0);
let sidebarTreeContentMeasureFrame = 0;
const sidebarTableNameDisplayTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "mongo-collection", "vector-collection", "elasticsearch-index"]);
const sidebarStorageDisplayTypes = new Set<TreeNodeType>(["database", "table", "materialized_view"]);
function sidebarCommentLabel(node: TreeNode): string {
const label = sidebarTableNameDisplayTypes.has(node.type) ? sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes) : node.label;
@@ -415,11 +420,55 @@ function scheduleSidebarCommentLabelMeasure() {
sidebarCommentMeasureFrame = window.requestAnimationFrame(measureSidebarCommentLabelWidths);
}
function sidebarNodeHasTrailingMetadata(node: TreeNode): boolean {
const mode = settingsStore.editorSettings.sidebarObjectInfoMode;
if (mode.startsWith("comment-") && sidebarTreeNodeComment(node)) return true;
return mode === "size" && sidebarStorageDisplayTypes.has(node.type) && !!formatSidebarObjectStorage(node.sizeBytes);
}
const sidebarTreeNaturalWidthItems = computed(() =>
flatNodes.value.map(({ depth, node }) => ({
depth,
label: sidebarCommentLabel(node),
usesNaturalWidth: usesFullWidthTreeLabel(node.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll, sidebarNodeHasTrailingMetadata(node)),
trailingWidth: node.pinned || store.isTreeNodePinned(node) ? 20 : 0,
})),
);
function measureSidebarTreeContentWidth() {
sidebarTreeContentMeasureFrame = 0;
if (!settingsStore.editorSettings.sidebarAllowHorizontalScroll || typeof document === "undefined" || !rootRef.value) {
sidebarTreeContentWidth.value = 0;
return;
}
const context = document.createElement("canvas").getContext("2d");
if (!context) return;
const style = window.getComputedStyle(rootRef.value);
context.font = style.font || `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
sidebarTreeContentWidth.value = sidebarTreeNaturalContentWidth(sidebarTreeNaturalWidthItems.value, (text) => context.measureText(text).width);
void nextTick(scheduleSidebarScrollMetricsUpdate);
}
function scheduleSidebarTreeContentWidthMeasure() {
if (typeof window === "undefined") {
measureSidebarTreeContentWidth();
return;
}
if (sidebarTreeContentMeasureFrame) window.cancelAnimationFrame(sidebarTreeContentMeasureFrame);
sidebarTreeContentMeasureFrame = window.requestAnimationFrame(measureSidebarTreeContentWidth);
}
watch([flatNodes, () => settingsStore.editorSettings.sidebarObjectInfoMode, () => settingsStore.editorSettings.sidebarHiddenTablePrefixes, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale], scheduleSidebarCommentLabelMeasure, {
flush: "post",
immediate: true,
});
watch([sidebarTreeNaturalWidthItems, () => settingsStore.editorSettings.uiFontFamily, () => settingsStore.editorSettings.uiScale], scheduleSidebarTreeContentWidthMeasure, {
flush: "post",
immediate: true,
});
const visibleSidebarTableStorageScopes = computed(() => {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size") return [];
return sidebarTableStorageScopes(flatNodes.value.map(({ node }) => node)).filter((scope) => supportsSidebarTableStorage(store.getConfig(scope.connectionId)));
@@ -454,26 +503,31 @@ const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryS
// component, tracking scroll offset to find the topmost visible database-level
// ancestor. The overlay reuses <TreeItem>, so collapse/expand comes for free.
const stickyScrollTop = ref(0);
const sidebarScrollMetrics = ref({ scrollTop: 0, clientHeight: 0, scrollHeight: 0 });
const sidebarScrollMetrics = ref({ scrollTop: 0, scrollLeft: 0, clientHeight: 0, clientWidth: 0, scrollHeight: 0, scrollWidth: 0 });
const isScrollingSidebar = ref(false);
const isDraggingSidebarScrollbar = ref(false);
const isDraggingSidebarHorizontalScrollbar = ref(false);
let sidebarScrollbarResizeObserver: ResizeObserver | null = null;
let sidebarScrollbarAnimationFrame = 0;
let sidebarScrollbarDragOffset = 0;
let sidebarHorizontalScrollbarDragOffset = 0;
let sidebarScrollingTimer = 0;
function updateSidebarScrollMetrics() {
const scroller = currentTreeScroller();
if (!scroller) {
sidebarScrollMetrics.value = { scrollTop: 0, clientHeight: 0, scrollHeight: 0 };
sidebarScrollMetrics.value = { scrollTop: 0, scrollLeft: 0, clientHeight: 0, clientWidth: 0, scrollHeight: 0, scrollWidth: 0 };
return;
}
if (useVirtualTree.value) stickyScrollTop.value = scroller.scrollTop;
sidebarScrollMetrics.value = {
scrollTop: scroller.scrollTop,
scrollLeft: scroller.scrollLeft,
clientHeight: scroller.clientHeight,
clientWidth: scroller.clientWidth,
scrollHeight: scroller.scrollHeight,
scrollWidth: Math.max(scroller.scrollWidth, sidebarTreeContentWidth.value),
};
}
@@ -515,6 +569,8 @@ watch(
sidebarScrollbarResizeObserver = new ResizeObserver(scheduleSidebarScrollMetricsUpdate);
sidebarScrollbarResizeObserver.observe(scroller);
const content = scroller.querySelector<HTMLElement>(".connection-tree-content");
if (content) sidebarScrollbarResizeObserver.observe(content);
scheduleSidebarScrollMetricsUpdate();
onCleanup(() => {
@@ -566,21 +622,32 @@ watch(flatNodes, () => {
});
const sidebarTreeOverflowClass = computed(() => (settingsStore.editorSettings.sidebarAllowHorizontalScroll ? "overflow-x-auto sidebar-tree-horizontal-scroll" : "overflow-x-hidden"));
const sidebarTreeScrollerStyle = computed<CSSProperties>(() => ({ "--sidebar-tree-content-width": `${sidebarTreeContentWidth.value}px` }) as CSSProperties);
const hasSidebarVerticalOverflow = computed(() => sidebarScrollMetrics.value.scrollHeight > sidebarScrollMetrics.value.clientHeight + 1);
const hasSidebarHorizontalOverflow = computed(() => settingsStore.editorSettings.sidebarAllowHorizontalScroll && sidebarScrollMetrics.value.scrollWidth > sidebarScrollMetrics.value.clientWidth + 1);
watch(
() => settingsStore.editorSettings.sidebarAllowHorizontalScroll,
(enabled) =>
void nextTick(() => {
const scroller = currentTreeScroller();
if (!enabled && scroller) scroller.scrollLeft = 0;
scheduleSidebarScrollMetricsUpdate();
}),
{ flush: "post" },
);
function sidebarScrollbarGeometry() {
const { scrollTop, clientHeight, scrollHeight } = sidebarScrollMetrics.value;
const trackHeight = sidebarScrollbarTrackRef.value?.clientHeight ?? Math.max(0, clientHeight - 8);
if (trackHeight <= 0 || scrollHeight <= clientHeight) {
return { thumbTop: 0, thumbHeight: 0, maxThumbTop: 0, maxScrollTop: 0 };
}
const thumbHeight = Math.max(24, Math.min(trackHeight, (clientHeight / scrollHeight) * trackHeight));
const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
const maxScrollTop = Math.max(1, scrollHeight - clientHeight);
const thumbTop = Math.min(maxThumbTop, Math.max(0, (scrollTop / maxScrollTop) * maxThumbTop));
return { thumbTop, thumbHeight, maxThumbTop, maxScrollTop };
const { thumbOffset, thumbSize, maxThumbOffset, maxScrollOffset } = calculateSidebarScrollbarGeometry({
scrollOffset: scrollTop,
viewportSize: clientHeight,
contentSize: scrollHeight,
trackSize: trackHeight,
});
return { thumbTop: thumbOffset, thumbHeight: thumbSize, maxThumbTop: maxThumbOffset, maxScrollTop: maxScrollOffset };
}
const sidebarScrollbarThumbStyle = computed<CSSProperties>(() => {
@@ -644,6 +711,79 @@ function onSidebarScrollbarThumbPointerDown(event: PointerEvent) {
window.addEventListener("pointercancel", stopSidebarScrollbarDrag);
}
function sidebarHorizontalScrollbarGeometry() {
const { scrollLeft, clientWidth, scrollWidth } = sidebarScrollMetrics.value;
const trackWidth = sidebarHorizontalScrollbarTrackRef.value?.clientWidth ?? clientWidth;
const { thumbOffset, thumbSize, maxThumbOffset, maxScrollOffset } = calculateSidebarScrollbarGeometry({
scrollOffset: scrollLeft,
viewportSize: clientWidth,
contentSize: scrollWidth,
trackSize: trackWidth,
});
return { thumbLeft: thumbOffset, thumbWidth: thumbSize, maxThumbLeft: maxThumbOffset, maxScrollLeft: maxScrollOffset };
}
const sidebarHorizontalScrollbarThumbStyle = computed<CSSProperties>(() => {
const { thumbLeft, thumbWidth } = sidebarHorizontalScrollbarGeometry();
return {
width: `${thumbWidth}px`,
transform: `translateX(${thumbLeft}px)`,
};
});
function setSidebarHorizontalScrollFromPointer(clientX: number, offset: number) {
const scroller = currentTreeScroller();
const track = sidebarHorizontalScrollbarTrackRef.value;
if (!scroller || !track) return;
const rect = track.getBoundingClientRect();
const { maxThumbLeft, maxScrollLeft } = sidebarHorizontalScrollbarGeometry();
if (maxThumbLeft <= 0) return;
const thumbLeft = Math.min(maxThumbLeft, Math.max(0, clientX - rect.left - offset));
scroller.scrollLeft = (thumbLeft / maxThumbLeft) * maxScrollLeft;
updateSidebarScrollMetrics();
}
function stopSidebarHorizontalScrollbarDrag() {
isDraggingSidebarHorizontalScrollbar.value = false;
window.removeEventListener("pointermove", onSidebarHorizontalScrollbarPointerMove);
window.removeEventListener("pointerup", stopSidebarHorizontalScrollbarDrag);
window.removeEventListener("pointercancel", stopSidebarHorizontalScrollbarDrag);
}
function onSidebarHorizontalScrollbarPointerMove(event: PointerEvent) {
event.preventDefault();
setSidebarHorizontalScrollFromPointer(event.clientX, sidebarHorizontalScrollbarDragOffset);
}
function onSidebarHorizontalScrollbarTrackPointerDown(event: PointerEvent) {
if (event.button !== 0) return;
event.preventDefault();
const { thumbWidth } = sidebarHorizontalScrollbarGeometry();
sidebarHorizontalScrollbarDragOffset = thumbWidth / 2;
setSidebarHorizontalScrollFromPointer(event.clientX, sidebarHorizontalScrollbarDragOffset);
isDraggingSidebarHorizontalScrollbar.value = true;
window.addEventListener("pointermove", onSidebarHorizontalScrollbarPointerMove);
window.addEventListener("pointerup", stopSidebarHorizontalScrollbarDrag);
window.addEventListener("pointercancel", stopSidebarHorizontalScrollbarDrag);
}
function onSidebarHorizontalScrollbarThumbPointerDown(event: PointerEvent) {
if (event.button !== 0) return;
event.preventDefault();
const track = sidebarHorizontalScrollbarTrackRef.value;
if (!track) return;
const rect = track.getBoundingClientRect();
const { thumbLeft } = sidebarHorizontalScrollbarGeometry();
sidebarHorizontalScrollbarDragOffset = event.clientX - rect.left - thumbLeft;
isDraggingSidebarHorizontalScrollbar.value = true;
window.addEventListener("pointermove", onSidebarHorizontalScrollbarPointerMove);
window.addEventListener("pointerup", stopSidebarHorizontalScrollbarDrag);
window.addEventListener("pointercancel", stopSidebarHorizontalScrollbarDrag);
}
const pasteHandlerRegistry = createSidebarPasteHandlerRegistry();
provide(sidebarTreeContextKey, {
@@ -1440,10 +1580,12 @@ onUnmounted(() => {
tableSearchFocusRestoreTokens.clear();
latestTableSearchInteractionParentId = null;
stopSidebarScrollbarDrag();
stopSidebarHorizontalScrollbarDrag();
sidebarScrollbarResizeObserver?.disconnect();
window.cancelAnimationFrame(sidebarScrollbarAnimationFrame);
window.clearTimeout(sidebarScrollingTimer);
if (sidebarCommentMeasureFrame) window.cancelAnimationFrame(sidebarCommentMeasureFrame);
if (sidebarTreeContentMeasureFrame) window.cancelAnimationFrame(sidebarTreeContentMeasureFrame);
});
defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
@@ -1541,11 +1683,12 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
</div>
</div>
<CustomContextMenu ref="sidebarContextMenuRef" :items="sidebarContextMenuItems" v-slot="contextMenuSlot">
<div v-if="flatNodes.length > 0 && useVirtualTree" class="connection-tree-scroll-shell relative min-h-0 flex-1">
<div v-if="flatNodes.length > 0 && useVirtualTree" class="connection-tree-scroll-shell relative min-h-0 flex-1" :class="{ 'connection-tree-scroll-shell--horizontal-overflow': hasSidebarHorizontalOverflow }">
<RecycleScroller
ref="treeScrollerRef"
class="sidebar-tree connection-tree-scroller h-full overflow-y-auto"
:class="sidebarTreeOverflowClass"
:style="sidebarTreeScrollerStyle"
@click="clearSidebarSelection"
:items="flatNodes"
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
@@ -1554,6 +1697,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
:skip-hover="true"
key-field="id"
type-field="poolType"
list-class="connection-tree-content"
flow-mode
>
<template #default="{ item }">
@@ -1573,29 +1717,61 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" :comment-label-width="sidebarCommentLabelWidths.get(stickyNode.node.id)" @context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)" />
</div>
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div
v-if="hasSidebarVerticalOverflow"
ref="sidebarScrollbarTrackRef"
class="sidebar-tree-scrollbar"
:class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar, 'sidebar-tree-scrollbar--with-horizontal': hasSidebarHorizontalOverflow }"
@pointerdown="onSidebarScrollbarTrackPointerDown"
>
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
</div>
<div
v-if="hasSidebarHorizontalOverflow"
ref="sidebarHorizontalScrollbarTrackRef"
class="sidebar-tree-horizontal-scrollbar"
:class="{ 'sidebar-tree-horizontal-scrollbar--with-vertical': hasSidebarVerticalOverflow, 'sidebar-tree-horizontal-scrollbar--dragging': isDraggingSidebarHorizontalScrollbar }"
@pointerdown="onSidebarHorizontalScrollbarTrackPointerDown"
>
<div class="sidebar-tree-horizontal-scrollbar__thumb" :style="sidebarHorizontalScrollbarThumbStyle" @pointerdown.stop="onSidebarHorizontalScrollbarThumbPointerDown" />
</div>
</div>
<div v-else-if="flatNodes.length > 0" class="connection-tree-scroll-shell relative min-h-0 flex-1">
<div ref="plainTreeScrollerRef" class="sidebar-tree connection-tree-scroller h-full overflow-y-auto" :class="sidebarTreeOverflowClass" @click="clearSidebarSelection" @scroll.passive="onTreeScroll">
<TreeItem
v-for="item in flatNodes"
:key="item.id"
:node="item.node"
:depth="item.depth"
:drag-disabled="isRootListPartial || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.id"
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
@rename-started="pendingRenameGroupId = null"
@group-created="startRenamingCreatedGroup"
/>
<div v-else-if="flatNodes.length > 0" class="connection-tree-scroll-shell relative min-h-0 flex-1" :class="{ 'connection-tree-scroll-shell--horizontal-overflow': hasSidebarHorizontalOverflow }">
<div ref="plainTreeScrollerRef" class="sidebar-tree connection-tree-scroller h-full overflow-y-auto" :class="sidebarTreeOverflowClass" :style="sidebarTreeScrollerStyle" @click="clearSidebarSelection" @scroll.passive="onTreeScroll">
<div class="connection-tree-content">
<TreeItem
v-for="item in flatNodes"
:key="item.id"
:node="item.node"
:depth="item.depth"
:drag-disabled="isRootListPartial || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.id"
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
@rename-started="pendingRenameGroupId = null"
@group-created="startRenamingCreatedGroup"
/>
</div>
</div>
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div
v-if="hasSidebarVerticalOverflow"
ref="sidebarScrollbarTrackRef"
class="sidebar-tree-scrollbar"
:class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar, 'sidebar-tree-scrollbar--with-horizontal': hasSidebarHorizontalOverflow }"
@pointerdown="onSidebarScrollbarTrackPointerDown"
>
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
</div>
<div
v-if="hasSidebarHorizontalOverflow"
ref="sidebarHorizontalScrollbarTrackRef"
class="sidebar-tree-horizontal-scrollbar"
:class="{ 'sidebar-tree-horizontal-scrollbar--with-vertical': hasSidebarVerticalOverflow, 'sidebar-tree-horizontal-scrollbar--dragging': isDraggingSidebarHorizontalScrollbar }"
@pointerdown="onSidebarHorizontalScrollbarTrackPointerDown"
>
<div class="sidebar-tree-horizontal-scrollbar__thumb" :style="sidebarHorizontalScrollbarThumbStyle" @pointerdown.stop="onSidebarHorizontalScrollbarThumbPointerDown" />
</div>
</div>
</CustomContextMenu>
<SidebarDdlViewDialog
@@ -1743,6 +1919,15 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
width: max-content;
}
.connection-tree-scroller.sidebar-tree-horizontal-scroll :deep(.connection-tree-content),
.connection-tree-scroller.sidebar-tree-horizontal-scroll > .connection-tree-content {
min-width: max(100%, var(--sidebar-tree-content-width));
}
.connection-tree-scroll-shell--horizontal-overflow .connection-tree-scroller {
height: calc(100% - 10px);
}
.sidebar-tree-scrollbar {
position: absolute;
top: 0;
@@ -1755,6 +1940,10 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
transition: opacity 120ms ease;
}
.sidebar-tree-scrollbar--with-horizontal {
bottom: 10px;
}
.sidebar-tree-scrollbar--scrolling,
.sidebar-tree-scrollbar:hover,
.sidebar-tree-scrollbar--dragging {
@@ -1780,4 +1969,40 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
width: 8px;
background: color-mix(in oklch, var(--foreground) 48%, transparent);
}
.sidebar-tree-horizontal-scrollbar {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
height: 10px;
cursor: default;
background: color-mix(in oklch, var(--muted) 45%, transparent);
}
.sidebar-tree-horizontal-scrollbar--with-vertical {
right: 10px;
}
.sidebar-tree-horizontal-scrollbar__thumb {
position: absolute;
bottom: 2px;
left: 0;
height: 6px;
min-width: 24px;
border-radius: 999px;
background: color-mix(in oklch, var(--foreground) 34%, transparent);
transition:
background-color 120ms ease,
height 120ms ease,
bottom 120ms ease;
}
.sidebar-tree-horizontal-scrollbar:hover .sidebar-tree-horizontal-scrollbar__thumb,
.sidebar-tree-horizontal-scrollbar--dragging .sidebar-tree-horizontal-scrollbar__thumb {
bottom: 1px;
height: 8px;
background: color-mix(in oklch, var(--foreground) 50%, transparent);
}
</style>