mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 23:23:21 +00:00
fix(app): keep web scrollbars from shifting content
Use an overlay scrollbar for desktop chat timelines so scrollbar gutters cannot move centered content. Share its behavior with existing workspace scrollbars and derive resting and hover colors from semantic foreground tokens.
This commit is contained in:
@@ -15,7 +15,7 @@ export function installWebScrollbarStyles(): () => void {
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
* {
|
||||
scrollbar-color: ${webScrollbarColor("var(--colors-scrollbar-handle)")};
|
||||
scrollbar-color: ${webScrollbarColor("var(--colors-foreground-extra-muted)")};
|
||||
scrollbar-width: ${WEB_SCROLLBAR_WIDTH};
|
||||
}
|
||||
|
||||
@@ -37,9 +37,18 @@ export function installWebScrollbarStyles(): () => void {
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: ${webScrollbarThumbColor("var(--colors-scrollbar-handle)")};
|
||||
background: ${webScrollbarThumbColor("var(--colors-foreground-extra-muted)")};
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: ${webScrollbarThumbColor("var(--colors-foreground-muted)", 1)};
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
[data-overlay-scrollbar="true"]::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
document.head.append(style);
|
||||
|
||||
|
||||
@@ -221,9 +221,6 @@ const lightSemanticColors = {
|
||||
foregroundMuted: "#71717a",
|
||||
foregroundExtraMuted: "#a1a1aa",
|
||||
|
||||
// Controls
|
||||
scrollbarHandle: "#3f3f46", // zinc-700
|
||||
|
||||
// Borders - shifted one step lighter
|
||||
border: "#e4e4e7", // (was zinc-200, now zinc-200 - keep for contrast)
|
||||
borderAccent: "#ececf1", // Softer accent border for low-emphasis outlines
|
||||
@@ -300,7 +297,6 @@ interface DarkThemeConfig {
|
||||
surfaceSidebarHover: string;
|
||||
foregroundMuted: string;
|
||||
foregroundExtraMuted: string;
|
||||
scrollbarHandle: string;
|
||||
border: string;
|
||||
borderAccent: string;
|
||||
accent: string;
|
||||
@@ -344,8 +340,6 @@ function buildDarkSemanticColors(tint: DarkThemeConfig) {
|
||||
foregroundMuted: tint.foregroundMuted,
|
||||
foregroundExtraMuted: tint.foregroundExtraMuted,
|
||||
|
||||
scrollbarHandle: tint.scrollbarHandle,
|
||||
|
||||
border: tint.border,
|
||||
borderAccent: tint.borderAccent,
|
||||
|
||||
@@ -406,7 +400,6 @@ const paseoDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#1c1f1e",
|
||||
foregroundMuted: "#A1A5A4",
|
||||
foregroundExtraMuted: "#717574",
|
||||
scrollbarHandle: "#717574",
|
||||
border: "#252B2A",
|
||||
borderAccent: "#2F3534",
|
||||
accent: "#20744A",
|
||||
@@ -428,7 +421,6 @@ const zincDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#1b1b1e",
|
||||
foregroundMuted: "#a1a1aa",
|
||||
foregroundExtraMuted: "#71717a",
|
||||
scrollbarHandle: "#71717a",
|
||||
border: "#27272a",
|
||||
borderAccent: "#303036",
|
||||
accent: "#e4e4e7",
|
||||
@@ -451,7 +443,6 @@ const midnightDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#1a1c28",
|
||||
foregroundMuted: "#9a9db0",
|
||||
foregroundExtraMuted: "#6b6e82",
|
||||
scrollbarHandle: "#6b6e82",
|
||||
border: "#242636",
|
||||
borderAccent: "#2e3040",
|
||||
accent: "#3b6fcf",
|
||||
@@ -473,7 +464,6 @@ const claudeDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#222120",
|
||||
foregroundMuted: "#ada9a5",
|
||||
foregroundExtraMuted: "#78746f",
|
||||
scrollbarHandle: "#78746f",
|
||||
border: "#2c2a27",
|
||||
borderAccent: "#36332f",
|
||||
accent: "#d97757",
|
||||
@@ -495,7 +485,6 @@ const ghosttyDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#292d36",
|
||||
foregroundMuted: "#c8ccd8",
|
||||
foregroundExtraMuted: "#a0a4b2",
|
||||
scrollbarHandle: "#a0a4b2",
|
||||
border: "#353a47",
|
||||
borderAccent: "#3f4454",
|
||||
accent: "#89b4fa",
|
||||
@@ -671,7 +660,6 @@ const pureBlackDarkColors = buildDarkSemanticColors({
|
||||
surfaceSidebarHover: "#161616",
|
||||
foregroundMuted: "#a1a1aa",
|
||||
foregroundExtraMuted: "#71717a",
|
||||
scrollbarHandle: "#71717a",
|
||||
border: "#1c1c1c",
|
||||
borderAccent: "#242424",
|
||||
accent: "#20744A",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { webScrollbarThumbColor } from "./web-scrollbar";
|
||||
|
||||
describe("web scrollbar color", () => {
|
||||
it("uses the resting opacity by default and full opacity when requested", () => {
|
||||
expect(webScrollbarThumbColor("gray")).toBe("color-mix(in srgb, gray 40%, transparent)");
|
||||
expect(webScrollbarThumbColor("gray", 1)).toBe("color-mix(in srgb, gray 100%, transparent)");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
export const WEB_SCROLLBAR_WIDTH = "thin";
|
||||
export const WEB_SCROLLBAR_SIZE_PX = 8;
|
||||
export const WEB_SCROLLBAR_RESTING_OPACITY = 0.4;
|
||||
|
||||
export function webScrollbarThumbColor(handleColor: string): string {
|
||||
return `color-mix(in srgb, ${handleColor} 62%, transparent)`;
|
||||
export function webScrollbarThumbColor(
|
||||
handleColor: string,
|
||||
opacity = WEB_SCROLLBAR_RESTING_OPACITY,
|
||||
): string {
|
||||
return `color-mix(in srgb, ${handleColor} ${opacity * 100}%, transparent)`;
|
||||
}
|
||||
|
||||
export function webScrollbarColor(handleColor: string): string {
|
||||
|
||||
Reference in New Issue
Block a user