fix(web): inset sidebar scrollbar by 2px

This commit is contained in:
_Kerman
2026-08-04 14:56:30 +08:00
parent c1ac85c15f
commit 369fad9faf
4 changed files with 33 additions and 9 deletions

View File

@@ -106,7 +106,9 @@ interface ListMetrics {
overflows: boolean overflows: boolean
/** Border-box width minus client width: the space the scrollbar takes out of the content area. */ /** Border-box width minus client width: the space the scrollbar takes out of the content area. */
band: number band: number
/** Distance from the first row background's right edge to the list border box. */ /** Distance from the scrollbar's right edge to the sidebar edge. */
scrollbarEdgeOffset: number
/** Distance from the first row background's right edge to the sidebar edge. */
rowEdgeInset: number rowEdgeInset: number
/** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */ /** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */
clientRight: number clientRight: number
@@ -172,6 +174,8 @@ function measureList(page: Page): Promise<ListMetrics> {
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth) const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
const listRect = list.getBoundingClientRect() const listRect = list.getBoundingClientRect()
const sidebarEdge = list.parentElement?.getBoundingClientRect().right
if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
return { return {
gutter: style.scrollbarGutter, gutter: style.scrollbarGutter,
width: pseudoWidth, width: pseudoWidth,
@@ -183,7 +187,8 @@ function measureList(page: Page): Promise<ListMetrics> {
hoverToken: resolve('--dsh-scrollbar-thumb-hover'), hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
overflows: list.scrollHeight > list.clientHeight, overflows: list.scrollHeight > list.clientHeight,
band: listRect.width - list.clientWidth, band: listRect.width - list.clientWidth,
rowEdgeInset: listRect.right - row.getBoundingClientRect().right, scrollbarEdgeOffset: sidebarEdge - listRect.right,
rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
clientRight: listRect.left + list.clientWidth, clientRight: listRect.left + list.clientWidth,
borderRight: listRect.right, borderRight: listRect.right,
timeRight: time.getBoundingClientRect().right, timeRight: time.getBoundingClientRect().right,
@@ -211,9 +216,11 @@ function measureRowInset(page: Page): Promise<Pick<ListMetrics, 'overflows' | 'r
if (list === null) throw new Error('sidebar session list not in the DOM') if (list === null) throw new Error('sidebar session list not in the DOM')
const row = list.querySelector<HTMLElement>('[role="treeitem"]') const row = list.querySelector<HTMLElement>('[role="treeitem"]')
if (row === null) throw new Error('no row in the sidebar list') if (row === null) throw new Error('no row in the sidebar list')
const sidebarEdge = list.parentElement?.getBoundingClientRect().right
if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
return { return {
overflows: list.scrollHeight > list.clientHeight, overflows: list.scrollHeight > list.clientHeight,
rowEdgeInset: list.getBoundingClientRect().right - row.getBoundingClientRect().right, rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
} }
}) })
} }
@@ -247,6 +254,7 @@ function renderGeometry(light: ListMetrics, dark: ListMetrics): string {
`- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`, `- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`,
`- list overflows: ${String(metrics.overflows)}`, `- list overflows: ${String(metrics.overflows)}`,
`- reserved band: ${String(metrics.band)}px`, `- reserved band: ${String(metrics.band)}px`,
`- scrollbar inset from the sidebar edge: ${String(metrics.scrollbarEdgeOffset)}px`,
`- row background inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`, `- row background inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`,
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`, `- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`, `- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
@@ -325,6 +333,7 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
// drawn over it. Removing the declaration makes it exactly 0. The value // drawn over it. Removing the declaration makes it exactly 0. The value
// itself is not pinned — it tracks `scrollbar-width` and the platform. // itself is not pinned — it tracks `scrollbar-width` and the platform.
expect(metrics.band).toBeGreaterThan(0) expect(metrics.band).toBeGreaterThan(0)
expect(metrics.scrollbarEdgeOffset).toBe(2)
expect(metrics.rowEdgeInset).toBe(12) expect(metrics.rowEdgeInset).toBe(12)
// The reported symptom, stated directly: no part of the row's relative time // The reported symptom, stated directly: no part of the row's relative time
// lies under the bar. Measures 7 on clean master — the `h` of `1h` is the // lies under the bar. Measures 7 on clean master — the `h` of `1h` is the

View File

@@ -12,6 +12,7 @@
- --dsh-scrollbar-thumb-hover: rgb(212, 212, 212) - --dsh-scrollbar-thumb-hover: rgb(212, 212, 212)
- list overflows: true - list overflows: true
- reserved band: 8px - reserved band: 8px
- scrollbar inset from the sidebar edge: 2px
- row background inset from the sidebar edge: 12px - row background inset from the sidebar edge: 12px
- relative time covered by the bar: 0px - relative time covered by the bar: 0px
- relative time ends inside the content area: true - relative time ends inside the content area: true
@@ -29,6 +30,7 @@
- --dsh-scrollbar-thumb-hover: rgb(84, 85, 87) - --dsh-scrollbar-thumb-hover: rgb(84, 85, 87)
- list overflows: true - list overflows: true
- reserved band: 8px - reserved band: 8px
- scrollbar inset from the sidebar edge: 2px
- row background inset from the sidebar edge: 12px - row background inset from the sidebar edge: 12px
- relative time covered by the bar: 0px - relative time covered by the bar: 0px
- relative time ends inside the content area: true - relative time ends inside the content area: true

View File

@@ -6,6 +6,7 @@
.root { .root {
--dsh-session-list-edge-inset: var(--dsh-sidebar-inline-padding); --dsh-session-list-edge-inset: var(--dsh-sidebar-inline-padding);
--dsh-session-list-scrollbar-width: 8px; --dsh-session-list-scrollbar-width: 8px;
--dsh-session-list-scrollbar-offset: 2px;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
display: flex; display: flex;
@@ -214,13 +215,19 @@
} }
/* List: the only scrolling region. Block children keep their design heights /* List: the only scrolling region. Block children keep their design heights
under content overflow. The stable 8px themed scrollbar and the remaining under content overflow. The 2px edge offset, stable 8px themed scrollbar,
padding together equal the shell's right inset, with or without overflow. */ and remaining padding equal the shell's right inset, with or without
overflow, so moving the bar does not move the rows. */
.list { .list {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
padding-right: calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width)); margin-right: var(--dsh-session-list-scrollbar-offset);
padding-right: calc(
var(--dsh-session-list-edge-inset)
- var(--dsh-session-list-scrollbar-width)
- var(--dsh-session-list-scrollbar-offset)
);
padding-bottom: 12px; padding-bottom: 12px;
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }

View File

@@ -43,12 +43,18 @@ describe('WorkspaceBrowser.module.css list', () => {
it('counts the themed scrollbar inside the shell trailing inset', () => { it('counts the themed scrollbar inside the shell trailing inset', () => {
expect(root?.get('--dsh-session-list-edge-inset')).toBe('var(--dsh-sidebar-inline-padding)') expect(root?.get('--dsh-session-list-edge-inset')).toBe('var(--dsh-sidebar-inline-padding)')
expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('8px') expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('8px')
expect(root?.get('--dsh-session-list-scrollbar-offset')).toBe('2px')
expect(root?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)') expect(root?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)')
expect(listArea?.get('margin-right')).toBe('calc(-1 * var(--dsh-session-list-edge-inset))') expect(listArea?.get('margin-right')).toBe('calc(-1 * var(--dsh-session-list-edge-inset))')
expect(declarations('.fade')?.get('right')).toBe('var(--dsh-session-list-edge-inset)') expect(declarations('.fade')?.get('right')).toBe('var(--dsh-session-list-edge-inset)')
expect(list?.get('padding-right')).toBe( expect(list?.get('margin-right')).toBe('var(--dsh-session-list-scrollbar-offset)')
'calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width))', expect(list?.get('padding-right')).toBe([
) 'calc(',
'var(--dsh-session-list-edge-inset)',
'- var(--dsh-session-list-scrollbar-width)',
'- var(--dsh-session-list-scrollbar-offset)',
')',
].join(' '))
expect(declarations('.list::-webkit-scrollbar')).toBeUndefined() expect(declarations('.list::-webkit-scrollbar')).toBeUndefined()
}) })