From b953924eed9037e2296aec6bbe4a9965702c1fa8 Mon Sep 17 00:00:00 2001 From: Michael Johnston Date: Thu, 3 Sep 2026 19:19:51 -0700 Subject: [PATCH 1/3] feat(lastcode): add adjustable larger scrollbars --- .../settings/DesktopClientSettings.test.ts | 3 + .../components/settings/LastCodeSettings.tsx | 121 ++++++++++++++++++ .../settings/settingsSearch.test.ts | 7 + .../src/components/settings/settingsSearch.ts | 6 + apps/web/src/components/ui/scroll-area.tsx | 2 +- apps/web/src/index.css | 36 ++++-- apps/web/src/routes/__root.tsx | 26 ++++ apps/web/src/scrollbarAppearance.test.ts | 46 +++++++ apps/web/src/scrollbarAppearance.ts | 27 ++++ apps/web/src/terminal/ghostty/surface.ts | 4 +- docs/README.md | 1 + docs/user/lastcode-appearance.md | 14 ++ packages/contracts/src/settings.test.ts | 20 +++ packages/contracts/src/settings.ts | 26 ++++ 14 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/scrollbarAppearance.test.ts create mode 100644 apps/web/src/scrollbarAppearance.ts create mode 100644 docs/user/lastcode-appearance.md diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index f788263fac71..bd55691ed566 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -62,6 +62,9 @@ const clientSettings: ClientSettings = { sidebarThreadPreviewCount: 6, legacySidebarEnabled: false, legacySidebarScale: 100, + largerScrollbarsEnabled: true, + scrollbarWidth: 10, + scrollbarMargin: 4, timestampFormat: "24-hour", wordWrap: true, }; diff --git a/apps/web/src/components/settings/LastCodeSettings.tsx b/apps/web/src/components/settings/LastCodeSettings.tsx index edb4ccfe70fb..804d60e1c11e 100644 --- a/apps/web/src/components/settings/LastCodeSettings.tsx +++ b/apps/web/src/components/settings/LastCodeSettings.tsx @@ -5,9 +5,15 @@ import { } from "@t3tools/contracts"; import { DEFAULT_LEGACY_SIDEBAR_SCALE, + DEFAULT_SCROLLBAR_MARGIN, + DEFAULT_SCROLLBAR_WIDTH, LEGACY_SIDEBAR_SCALE_REFERENCE, MAX_LEGACY_SIDEBAR_SCALE, + MAX_SCROLLBAR_MARGIN, + MAX_SCROLLBAR_WIDTH, MIN_LEGACY_SIDEBAR_SCALE, + MIN_SCROLLBAR_MARGIN, + MIN_SCROLLBAR_WIDTH, } from "@t3tools/contracts/settings"; import { useAtomValue } from "@effect/atom-react"; import { DownloadIcon, MoonStarIcon, PaletteIcon, ServerIcon } from "lucide-react"; @@ -46,6 +52,56 @@ const WORKTREE_INDICATOR_PREVIEW_THREAD = { worktreePath: "/example/worktrees/example", }; +function PixelSlider({ + id, + label, + min, + max, + value, + onChange, +}: { + id: string; + label: string; + min: number; + max: number; + value: number; + onChange: (value: number) => void; +}) { + const ratio = (value - min) / (max - min); + const sliderStyle = { + "--settings-slider-progress": `${ratio * 100}%`, + "--settings-slider-fill-offset": `${0.5 - ratio}rem`, + } as CSSProperties; + + return ( +
+ + {value}px + + { + const nextValue = Number(event.currentTarget.value); + if (Number.isInteger(nextValue) && nextValue >= min && nextValue <= max) { + onChange(nextValue); + } + }} + step={1} + style={sliderStyle} + type="range" + value={value} + /> +
+ ); +} + export function LastCodeSettingsPanel() { const updateState = useDesktopUpdateState(); const clientSettings = usePrimarySettings(); @@ -182,6 +238,71 @@ export function LastCodeSettingsPanel() { /> }> + + updateClientSettings({ largerScrollbarsEnabled: Boolean(checked) }) + } + aria-label="Larger scrollbars" + /> + } + /> + {clientSettings.largerScrollbarsEnabled ? ( + <> + + updateClientSettings({ scrollbarWidth: DEFAULT_SCROLLBAR_WIDTH }) + } + /> + ) : null + } + control={ + updateClientSettings({ scrollbarWidth })} + /> + } + /> + + updateClientSettings({ scrollbarMargin: DEFAULT_SCROLLBAR_MARGIN }) + } + /> + ) : null + } + control={ + updateClientSettings({ scrollbarMargin })} + /> + } + /> + + ) : null} { }); }); + it("routes scrollbar sizing to LastCode settings", () => { + expect(searchSettings("scrollbar margin")[0]).toMatchObject({ + id: "larger-scrollbars", + to: "/settings/lastcode", + }); + }); + it("routes project icon rounding to LastCode settings", () => { expect(searchSettings("rounded project icons")[0]).toMatchObject({ id: "rounded-project-icons", diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 13e2a7ddff97..805217d7fad7 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -493,6 +493,12 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Show and install local nightlies", to: "/settings/lastcode", }, + { + id: "larger-scrollbars", + title: "Larger scrollbars", + to: "/settings/lastcode", + searchTerms: ["width margin resize handle pane drag thumb"], + }, { id: "scale-legacy-sidebar", title: "Scale legacy sidebar", diff --git a/apps/web/src/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx index bfc10825b460..4dad6bd3c084 100644 --- a/apps/web/src/components/ui/scroll-area.tsx +++ b/apps/web/src/components/ui/scroll-area.tsx @@ -73,7 +73,7 @@ function ScrollBar({ return ( + {primaryEnvironmentAuthenticated ? : null} {primaryEnvironmentAuthenticated ? : null} @@ -204,6 +206,30 @@ function ProjectIconAppearanceSync() { return null; } +function ScrollbarAppearanceSync() { + const largerScrollbarsEnabled = useClientSettings((settings) => settings.largerScrollbarsEnabled); + const scrollbarWidth = useClientSettings((settings) => settings.scrollbarWidth); + const scrollbarMargin = useClientSettings((settings) => settings.scrollbarMargin); + + useEffect(() => { + const root = document.documentElement; + applyScrollbarAppearance(root, { + enabled: largerScrollbarsEnabled, + width: scrollbarWidth, + margin: scrollbarMargin, + }); + return () => { + applyScrollbarAppearance(root, { + enabled: false, + width: scrollbarWidth, + margin: scrollbarMargin, + }); + }; + }, [largerScrollbarsEnabled, scrollbarMargin, scrollbarWidth]); + + return null; +} + function FontAppearanceSync() { const fontFamilySans = useClientSettings((settings) => settings.fontFamilySans); const fontFamilyCode = useClientSettings((settings) => settings.fontFamilyCode); diff --git a/apps/web/src/scrollbarAppearance.test.ts b/apps/web/src/scrollbarAppearance.test.ts new file mode 100644 index 000000000000..6c8871dbd854 --- /dev/null +++ b/apps/web/src/scrollbarAppearance.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +import { applyScrollbarAppearance } from "./scrollbarAppearance"; + +describe("applyScrollbarAppearance", () => { + it("sets the thumb, margin, and total hit-lane sizes", () => { + const { root, setProperty, toggleAttribute } = makeRoot(); + + applyScrollbarAppearance(root, { enabled: true, width: 9, margin: 5 }); + + expect(toggleAttribute).toHaveBeenCalledWith("data-larger-scrollbars", true); + expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-width", "9px"); + expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-margin", "5px"); + expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-lane-width", "14px"); + expect(setProperty).toHaveBeenCalledWith("--app-native-scrollbar-margin", "5px"); + expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-thumb-inset", "0px"); + }); + + it("restores the stylesheet defaults when disabled", () => { + const { root, removeProperty, toggleAttribute } = makeRoot(); + + applyScrollbarAppearance(root, { enabled: false, width: 12, margin: 6 }); + + expect(toggleAttribute).toHaveBeenCalledWith("data-larger-scrollbars", false); + expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-width"); + expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-margin"); + expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-lane-width"); + expect(removeProperty).toHaveBeenCalledWith("--app-native-scrollbar-margin"); + expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-thumb-inset"); + }); +}); + +function makeRoot() { + const setProperty = vi.fn(); + const removeProperty = vi.fn(); + const toggleAttribute = vi.fn(); + return { + root: { + style: { setProperty, removeProperty }, + toggleAttribute, + } as unknown as HTMLElement, + setProperty, + removeProperty, + toggleAttribute, + }; +} diff --git a/apps/web/src/scrollbarAppearance.ts b/apps/web/src/scrollbarAppearance.ts new file mode 100644 index 000000000000..cc6280a1e0bd --- /dev/null +++ b/apps/web/src/scrollbarAppearance.ts @@ -0,0 +1,27 @@ +import type { ScrollbarMargin, ScrollbarWidth } from "@t3tools/contracts/settings"; + +export function applyScrollbarAppearance( + root: HTMLElement, + options: { + enabled: boolean; + width: ScrollbarWidth; + margin: ScrollbarMargin; + }, +): void { + root.toggleAttribute("data-larger-scrollbars", options.enabled); + + if (!options.enabled) { + root.style.removeProperty("--app-scrollbar-width"); + root.style.removeProperty("--app-scrollbar-margin"); + root.style.removeProperty("--app-scrollbar-lane-width"); + root.style.removeProperty("--app-native-scrollbar-margin"); + root.style.removeProperty("--app-scrollbar-thumb-inset"); + return; + } + + root.style.setProperty("--app-scrollbar-width", `${options.width}px`); + root.style.setProperty("--app-scrollbar-margin", `${options.margin}px`); + root.style.setProperty("--app-scrollbar-lane-width", `${options.width + options.margin}px`); + root.style.setProperty("--app-native-scrollbar-margin", `${options.margin}px`); + root.style.setProperty("--app-scrollbar-thumb-inset", "0px"); +} diff --git a/apps/web/src/terminal/ghostty/surface.ts b/apps/web/src/terminal/ghostty/surface.ts index 84e59e10f509..05fee03fbd06 100644 --- a/apps/web/src/terminal/ghostty/surface.ts +++ b/apps/web/src/terminal/ghostty/surface.ts @@ -674,7 +674,7 @@ export class GhosttyTerminalSurface { const scrollbar = document.createElement("div"); scrollbar.className = - "group absolute top-1 right-px bottom-1 z-1 w-[var(--app-scrollbar-width)] cursor-default touch-none"; + "group absolute top-1 right-[var(--app-scrollbar-margin)] bottom-1 z-1 w-[var(--app-scrollbar-width)] cursor-default touch-none"; scrollbar.setAttribute("role", "scrollbar"); scrollbar.setAttribute("aria-label", "Terminal scrollback"); scrollbar.setAttribute("aria-orientation", "vertical"); @@ -682,7 +682,7 @@ export class GhosttyTerminalSurface { scrollbar.hidden = true; const scrollbarThumb = document.createElement("div"); scrollbarThumb.className = - "absolute inset-x-px top-0 rounded-[3px] bg-[var(--app-scrollbar-thumb)] transition-[background-color] duration-[120ms] ease-[ease-out] group-hover:bg-[var(--app-scrollbar-thumb-hover)] group-focus-visible:bg-[var(--app-scrollbar-thumb-hover)]"; + "absolute inset-x-[var(--app-scrollbar-thumb-inset)] top-0 rounded-[3px] bg-[var(--app-scrollbar-thumb)] transition-[background-color] duration-[120ms] ease-[ease-out] group-hover:bg-[var(--app-scrollbar-thumb-hover)] group-focus-visible:bg-[var(--app-scrollbar-thumb-hover)]"; scrollbar.append(scrollbarThumb); mount.replaceChildren(canvas, input, scrollbar); diff --git a/docs/README.md b/docs/README.md index 9d1e3156ee53..22294cfa3a01 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ - [Permission modes](./user/permission-modes.md) - [Keyboard shortcuts](./user/keybindings.md) - [Organizing threads](./user/thread-sidebar.md) +- [LastCode appearance preferences](./user/lastcode-appearance.md) - [Resumable Project Actions in LastCode](./user/resumable-project-actions.md) - [Use an agent to add resumable Project Actions](./user/resumable-project-actions-for-agents.md) - [Review usage](./user/usage.md) diff --git a/docs/user/lastcode-appearance.md b/docs/user/lastcode-appearance.md new file mode 100644 index 000000000000..6b6e34282758 --- /dev/null +++ b/docs/user/lastcode-appearance.md @@ -0,0 +1,14 @@ +# LastCode appearance preferences + +## Make scrollbars easier to grab + +Open **Settings → LastCode → Appearance** and enable **Larger scrollbars**. Two controls appear: + +- **Scrollbar width** sets the visible thumb from 1 px through 12 px in one-pixel increments. +- **Scrollbar margin** adds 0 px through 6 px of clear space between the thumb and the pane edge. + +The default larger-scrollbar profile uses a 10 px thumb and a 4 px margin. That margin keeps the +thumb clear of the resize handle between adjacent panes. Changes apply immediately to native app +scrollbars, styled scroll areas, and terminal scrollback, and are stored in the current LastCode +profile. Turning the setting off restores the standard scrollbar appearance without discarding the +chosen width and margin. diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index fd00e068bf21..306329928f88 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -76,6 +76,26 @@ describe("ClientSettings proactive panels", () => { }); }); +describe("ClientSettings larger scrollbars", () => { + it("is opt-in with defaults that clear the pane resize target", () => { + const settings = decodeClientSettings({}); + + expect(settings.largerScrollbarsEnabled).toBe(false); + expect(settings.scrollbarWidth).toBe(10); + expect(settings.scrollbarMargin).toBe(4); + }); + + it.each([ + ["scrollbarWidth", 1, 12], + ["scrollbarMargin", 0, 6], + ] as const)("accepts the inclusive %s range", (key, minimum, maximum) => { + expect(decodeClientSettingsPatch({ [key]: minimum })).toEqual({ [key]: minimum }); + expect(decodeClientSettingsPatch({ [key]: maximum })).toEqual({ [key]: maximum }); + expect(() => decodeClientSettingsPatch({ [key]: minimum - 1 })).toThrow(); + expect(() => decodeClientSettingsPatch({ [key]: maximum + 1 })).toThrow(); + }); +}); + describe("ClientSettings quit confirmation", () => { it("defaults to hold", () => { expect(decodeClientSettings({}).confirmQuit).toBe("hold"); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index b93bc4743501..4dd899a371aa 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -104,6 +104,22 @@ export const AppearanceContrast = Schema.Int.check( ); export type AppearanceContrast = typeof AppearanceContrast.Type; export const DEFAULT_APPEARANCE_CONTRAST: AppearanceContrast = 100; +export const MIN_SCROLLBAR_WIDTH = 1; +export const MAX_SCROLLBAR_WIDTH = 12; +export const ScrollbarWidth = Schema.Int.check( + Schema.isBetween({ minimum: MIN_SCROLLBAR_WIDTH, maximum: MAX_SCROLLBAR_WIDTH }), +); +export type ScrollbarWidth = typeof ScrollbarWidth.Type; +export const DEFAULT_SCROLLBAR_WIDTH: ScrollbarWidth = 10; +export const MIN_SCROLLBAR_MARGIN = 0; +export const MAX_SCROLLBAR_MARGIN = 6; +export const ScrollbarMargin = Schema.Int.check( + Schema.isBetween({ minimum: MIN_SCROLLBAR_MARGIN, maximum: MAX_SCROLLBAR_MARGIN }), +); +export type ScrollbarMargin = typeof ScrollbarMargin.Type; +// The inline preview resize handle reaches four pixels into its neighboring +// pane, so this default keeps the whole scrollbar thumb clear of that target. +export const DEFAULT_SCROLLBAR_MARGIN: ScrollbarMargin = 4; export const MIN_PANEL_ANIMATION_DURATION_MS = 0; export const MAX_PANEL_ANIMATION_DURATION_MS = 400; export const PanelAnimationDurationMs = Schema.Int.check( @@ -357,6 +373,13 @@ export const ClientSettingsSchema = Schema.Struct({ legacySidebarScale: LegacySidebarScale.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_LEGACY_SIDEBAR_SCALE)), ), + largerScrollbarsEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + scrollbarWidth: ScrollbarWidth.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SCROLLBAR_WIDTH)), + ), + scrollbarMargin: ScrollbarMargin.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SCROLLBAR_MARGIN)), + ), roundedProjectIcons: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), showLocalEnvironmentIcon: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( @@ -1205,6 +1228,9 @@ export const ClientSettingsPatch = Schema.Struct({ showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), legacySidebarScale: Schema.optionalKey(LegacySidebarScale), + largerScrollbarsEnabled: Schema.optionalKey(Schema.Boolean), + scrollbarWidth: Schema.optionalKey(ScrollbarWidth), + scrollbarMargin: Schema.optionalKey(ScrollbarMargin), roundedProjectIcons: Schema.optionalKey(Schema.Boolean), showLocalEnvironmentIcon: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), From 1f9d3b84e47ace418c1b2a73eb2a55a637f54473 Mon Sep 17 00:00:00 2001 From: Michael Johnston Date: Thu, 3 Sep 2026 20:04:47 -0700 Subject: [PATCH 2/3] fix(web): honor scrollbar sizing in compact surfaces --- apps/web/src/components/ChatMarkdown.tsx | 2 +- .../chat/ComposerPendingApprovalPanel.test.tsx | 4 ++-- .../src/components/chat/ComposerPendingApprovalPanel.tsx | 2 +- apps/web/src/index.css | 9 ++++++--- apps/web/src/scrollbarAppearance.test.ts | 6 ++++++ apps/web/src/scrollbarAppearance.ts | 6 ++++++ 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 5730d1fcb606..f002dcb8de58 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -1918,7 +1918,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ > {/* The full path: the chip already shows the shortened form, and a link to the workspace root collapses to a bare label that repeats it. */} -
+
{targetPath}
diff --git a/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx b/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx index a7ef6bc7d5cb..c7b52943addf 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx @@ -27,8 +27,8 @@ describe("ComposerPendingApprovalPanel", () => { expect(markup).toContain("max-h-20"); expect(markup).toContain("overflow-auto"); expect(markup).toContain("whitespace-pre"); - expect(markup).toContain("[scrollbar-width:thin]"); - expect(markup).toContain("[&::-webkit-scrollbar]:h-1.5"); + expect(markup).toContain("[scrollbar-width:var(--app-native-scrollbar-width)]"); + expect(markup).toContain("[&::-webkit-scrollbar]:h-[var(--app-compact-scrollbar-height)]"); expect(markup).not.toContain("truncate"); expect(markup).not.toContain("line-clamp"); expect(markup).toContain("min-w-0"); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx b/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx index deb5ba54c53c..8dc851bac66f 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx @@ -43,7 +43,7 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova ) : null} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 0ffc246176d6..4b6c66c8a2f8 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -81,6 +81,9 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --app-scrollbar-margin: 1px; --app-scrollbar-lane-width: var(--app-scrollbar-width); --app-native-scrollbar-margin: 0px; + --app-native-scrollbar-width: thin; + --app-compact-scrollbar-height: 6px; + --app-code-scrollbar-height: 7px; --app-scrollbar-thumb-inset: 1px; --appearance-contrast-base: 100%; --appearance-contrast-boost: 0%; @@ -2346,7 +2349,7 @@ html[data-larger-scrollbars] ::-webkit-scrollbar { border-radius: 0.75rem; background: var(--muted); padding: 0.8rem 0.9rem; - scrollbar-width: thin; + scrollbar-width: var(--app-native-scrollbar-width); scrollbar-color: color-mix(in srgb, var(--contrast-border) 78%, transparent) transparent; } @@ -2357,12 +2360,12 @@ html[data-larger-scrollbars] ::-webkit-scrollbar { } .chat-markdown pre::-webkit-scrollbar { - height: 7px; + height: var(--app-code-scrollbar-height); } .chat-markdown pre::-webkit-scrollbar-thumb { border-radius: 999px; - background: color-mix(in srgb, var(--contrast-border) 78%, transparent); + background-color: color-mix(in srgb, var(--contrast-border) 78%, transparent); } .chat-markdown .chat-markdown-codeblock-header, diff --git a/apps/web/src/scrollbarAppearance.test.ts b/apps/web/src/scrollbarAppearance.test.ts index 6c8871dbd854..08288492acad 100644 --- a/apps/web/src/scrollbarAppearance.test.ts +++ b/apps/web/src/scrollbarAppearance.test.ts @@ -13,6 +13,9 @@ describe("applyScrollbarAppearance", () => { expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-margin", "5px"); expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-lane-width", "14px"); expect(setProperty).toHaveBeenCalledWith("--app-native-scrollbar-margin", "5px"); + expect(setProperty).toHaveBeenCalledWith("--app-native-scrollbar-width", "auto"); + expect(setProperty).toHaveBeenCalledWith("--app-compact-scrollbar-height", "14px"); + expect(setProperty).toHaveBeenCalledWith("--app-code-scrollbar-height", "14px"); expect(setProperty).toHaveBeenCalledWith("--app-scrollbar-thumb-inset", "0px"); }); @@ -26,6 +29,9 @@ describe("applyScrollbarAppearance", () => { expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-margin"); expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-lane-width"); expect(removeProperty).toHaveBeenCalledWith("--app-native-scrollbar-margin"); + expect(removeProperty).toHaveBeenCalledWith("--app-native-scrollbar-width"); + expect(removeProperty).toHaveBeenCalledWith("--app-compact-scrollbar-height"); + expect(removeProperty).toHaveBeenCalledWith("--app-code-scrollbar-height"); expect(removeProperty).toHaveBeenCalledWith("--app-scrollbar-thumb-inset"); }); }); diff --git a/apps/web/src/scrollbarAppearance.ts b/apps/web/src/scrollbarAppearance.ts index cc6280a1e0bd..31c98da0b88a 100644 --- a/apps/web/src/scrollbarAppearance.ts +++ b/apps/web/src/scrollbarAppearance.ts @@ -15,6 +15,9 @@ export function applyScrollbarAppearance( root.style.removeProperty("--app-scrollbar-margin"); root.style.removeProperty("--app-scrollbar-lane-width"); root.style.removeProperty("--app-native-scrollbar-margin"); + root.style.removeProperty("--app-native-scrollbar-width"); + root.style.removeProperty("--app-compact-scrollbar-height"); + root.style.removeProperty("--app-code-scrollbar-height"); root.style.removeProperty("--app-scrollbar-thumb-inset"); return; } @@ -23,5 +26,8 @@ export function applyScrollbarAppearance( root.style.setProperty("--app-scrollbar-margin", `${options.margin}px`); root.style.setProperty("--app-scrollbar-lane-width", `${options.width + options.margin}px`); root.style.setProperty("--app-native-scrollbar-margin", `${options.margin}px`); + root.style.setProperty("--app-native-scrollbar-width", "auto"); + root.style.setProperty("--app-compact-scrollbar-height", `${options.width + options.margin}px`); + root.style.setProperty("--app-code-scrollbar-height", `${options.width + options.margin}px`); root.style.setProperty("--app-scrollbar-thumb-inset", "0px"); } From 7b1ad6458c97b619a8c49adb9b4871a4385d6c53 Mon Sep 17 00:00:00 2001 From: Michael Johnston Date: Thu, 3 Sep 2026 20:10:18 -0700 Subject: [PATCH 3/3] docs(lastcode): clarify Firefox scrollbar limits --- apps/web/src/components/settings/LastCodeSettings.tsx | 1 + docs/user/lastcode-appearance.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apps/web/src/components/settings/LastCodeSettings.tsx b/apps/web/src/components/settings/LastCodeSettings.tsx index 804d60e1c11e..9eba085335f1 100644 --- a/apps/web/src/components/settings/LastCodeSettings.tsx +++ b/apps/web/src/components/settings/LastCodeSettings.tsx @@ -241,6 +241,7 @@ export function LastCodeSettingsPanel() {