Skip to content

Commit 194eb04

Browse files
committed
fix(web): unify themed scrollbar tokens
1 parent e08cb3c commit 194eb04

5 files changed

Lines changed: 92 additions & 7 deletions

File tree

packages/web/src/styles/base.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,13 @@ a:hover {
171171
::-webkit-scrollbar {
172172
width: var(--scrollbar-width);
173173
height: var(--scrollbar-width);
174+
background: var(--scrollbar-track);
174175
}
175176

176-
::-webkit-scrollbar-track {
177+
::-webkit-scrollbar-track,
178+
::-webkit-scrollbar-track-piece,
179+
::-webkit-scrollbar-button,
180+
::-webkit-scrollbar-corner {
177181
background: var(--scrollbar-track);
178182
}
179183

packages/web/src/styles/base.theme.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,29 @@ describe("base.css theme-sensitive shells", () => {
8585
);
8686
});
8787

88+
it("defines a single token-driven native scrollbar contract for themed surfaces", () => {
89+
const scrollbar = getRuleBlock("::-webkit-scrollbar");
90+
const track = getRuleBlock("::-webkit-scrollbar-track");
91+
const trackPiece = getRuleBlock("::-webkit-scrollbar-track-piece");
92+
const thumb = getRuleBlock("::-webkit-scrollbar-thumb");
93+
const thumbHover = getRuleBlock("::-webkit-scrollbar-thumb:hover");
94+
const button = getRuleBlock("::-webkit-scrollbar-button");
95+
const corner = getRuleBlock("::-webkit-scrollbar-corner");
96+
const firefox = getRuleBlock("*");
97+
98+
expect(scrollbar).toContain("width: var(--scrollbar-width)");
99+
expect(scrollbar).toContain("height: var(--scrollbar-width)");
100+
expect(track).toContain("background: var(--scrollbar-track)");
101+
expect(trackPiece).toContain("background: var(--scrollbar-track)");
102+
expect(thumb).toContain("background: var(--scrollbar-thumb)");
103+
expect(thumb).toContain("border-radius: var(--radius-full)");
104+
expect(thumbHover).toContain("background: var(--border-focus)");
105+
expect(button).toContain("background: var(--scrollbar-track)");
106+
expect(corner).toContain("background: var(--scrollbar-track)");
107+
expect(firefox).toContain("scrollbar-width: thin");
108+
expect(firefox).toContain("scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track)");
109+
});
110+
88111
it("defines app-shell background variables and shared background dim hooks", () => {
89112
const app = getRuleBlock(".app");
90113
const appBefore = getRuleBlock(".app::before");

packages/web/src/styles/components.theme.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,30 @@ describe("components.css theme-sensitive surfaces", () => {
16131613
expect(monacoSliderHover).toContain("var(--border-focus)");
16141614
});
16151615

1616+
it("limits component scrollbar overrides to hidden affordances or shared-token bridges", () => {
1617+
const mobileTerminalKeys = getLastRuleBlock(".mobile-terminal-input-bar__keys");
1618+
const mobileTerminalKeysScrollbar = getLastRuleBlock(
1619+
".mobile-terminal-input-bar__keys::-webkit-scrollbar"
1620+
);
1621+
const tabsScroller = getLastRuleBlockFrom(
1622+
tabsStyles,
1623+
'.tabList[data-orientation="horizontal"]'
1624+
);
1625+
const tabsScrollbar = getLastRuleBlockFrom(
1626+
tabsStyles,
1627+
'.tabList[data-orientation="horizontal"]::-webkit-scrollbar'
1628+
);
1629+
1630+
expect(mobileTerminalKeys).toContain("scrollbar-width: none");
1631+
expect(mobileTerminalKeysScrollbar).toContain("display: none");
1632+
expect(tabsScroller).toContain("scrollbar-width: none");
1633+
expect(tabsScrollbar).toContain("display: none");
1634+
1635+
expect(stylesheet).not.toContain("::-webkit-scrollbar-thumb");
1636+
expect(stylesheet).not.toContain("::-webkit-scrollbar-track");
1637+
expect(stylesheet).not.toContain("scrollbar-color:");
1638+
});
1639+
16161640
it("keeps xterm viewport touch-scrollable on mobile browsers", () => {
16171641
const xtermViewport = getLastRuleBlock(".xterm-host .xterm-viewport");
16181642

packages/web/src/styles/tokens-touch.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,44 @@ describe("tokens.css touch tokens", () => {
277277
expect(root).toContain("--diff-removed-border: var(--status-danger-border)");
278278
});
279279

280+
it("derives the shared native scrollbar palette from semantic surface tokens", () => {
281+
const root = getRuleBlock(":root");
282+
283+
expect(getCustomProperty(root, "--scrollbar-width")).toBe("8px");
284+
expect(getCustomProperty(root, "--scrollbar-track")).toBe(
285+
"color-mix(in srgb, var(--surface-panel) 86%, var(--border-default) 14%)"
286+
);
287+
expect(getCustomProperty(root, "--scrollbar-thumb")).toBe(
288+
"color-mix(in srgb, var(--border-default) 74%, var(--status-info-fg) 26%)"
289+
);
290+
});
291+
292+
it("limits explicit scrollbar thumb overrides to the high-contrast themes", () => {
293+
const inheritedThemes = [
294+
"mint-light",
295+
"graphite-dark",
296+
"graphite-light",
297+
"nord-dark",
298+
"nord-light",
299+
] as const;
300+
301+
for (const theme of inheritedThemes) {
302+
const themeSpecificBlock = getRuleBlocks(`[data-theme="${theme}"]`).at(-1) ?? "";
303+
304+
expect(
305+
getCustomProperty(themeSpecificBlock, "--scrollbar-thumb"),
306+
`${theme} should inherit the shared scrollbar thumb palette`
307+
).toBeNull();
308+
}
309+
310+
expect(
311+
getCustomProperty(getRuleBlocks('[data-theme="hc-dark"]').at(-1) ?? "", "--scrollbar-thumb")
312+
).toBe("#ffffff");
313+
expect(
314+
getCustomProperty(getRuleBlocks('[data-theme="hc-light"]').at(-1) ?? "", "--scrollbar-thumb")
315+
).toBe("#5c5c5c");
316+
});
317+
280318
it("defines the shared foundation tokens on :root without changing code font-size plumbing", () => {
281319
const root = getRuleBlock(":root");
282320

packages/web/src/styles/tokens.css

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@
358358

359359
/* Scrollbar */
360360
--scrollbar-width: 8px;
361-
--scrollbar-track: transparent;
361+
--scrollbar-track: color-mix(in srgb, var(--surface-panel) 86%, var(--border-default) 14%);
362+
--scrollbar-thumb: color-mix(in srgb, var(--border-default) 74%, var(--status-info-fg) 26%);
362363

363364
/* ========== Touch / Pointer (Mobile-Friendly Phase 0) ========== */
364365
/* Desktop defaults - overridden below for narrow viewport or coarse pointer. */
@@ -2017,7 +2018,6 @@
20172018
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12);
20182019
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.15);
20192020
--shadow-glow: 0 0 12px rgba(21, 143, 119, 0.18);
2020-
--scrollbar-thumb: #d0d7de;
20212021

20222022
--accent-purple: #8250df;
20232023
}
@@ -2058,7 +2058,6 @@
20582058
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.64);
20592059
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.74);
20602060
--shadow-glow: 0 0 12px rgba(122, 162, 247, 0.24);
2061-
--scrollbar-thumb: #353c46;
20622061

20632062
--accent-purple: #c099ff;
20642063
}
@@ -2123,7 +2122,6 @@
21232122
--shadow-lg: 0 8px 32px rgba(15, 23, 42, 0.12);
21242123
--shadow-xl: 0 16px 48px rgba(15, 23, 42, 0.16);
21252124
--shadow-glow: 0 0 12px rgba(49, 95, 221, 0.16);
2126-
--scrollbar-thumb: #c7d0da;
21272125

21282126
--accent-purple: #7c3aed;
21292127
}
@@ -2166,7 +2164,6 @@
21662164
--shadow-lg: 0 8px 32px rgba(17, 24, 39, 0.58);
21672165
--shadow-xl: 0 16px 48px rgba(17, 24, 39, 0.68);
21682166
--shadow-glow: 0 0 12px rgba(136, 192, 208, 0.26);
2169-
--scrollbar-thumb: #4c566a;
21702167

21712168
--accent-purple: #b48ead;
21722169
}
@@ -2231,7 +2228,6 @@
22312228
--shadow-lg: 0 8px 32px rgba(46, 52, 64, 0.12);
22322229
--shadow-xl: 0 16px 48px rgba(46, 52, 64, 0.15);
22332230
--shadow-glow: 0 0 12px rgba(91, 127, 168, 0.18);
2234-
--scrollbar-thumb: #c6ceda;
22352231

22362232
--accent-purple: #8f5b9c;
22372233
}

0 commit comments

Comments
 (0)