Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ const clientSettings: ClientSettings = {
sidebarThreadPreviewCount: 6,
legacySidebarEnabled: false,
legacySidebarScale: 100,
largerScrollbarsEnabled: true,
scrollbarWidth: 10,
scrollbarMargin: 4,
timestampFormat: "24-hour",
wordWrap: true,
};
Expand Down
121 changes: 121 additions & 0 deletions apps/web/src/components/settings/LastCodeSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div className="flex w-full items-center gap-3 sm:w-64">
<output
className="min-w-12 rounded-md bg-muted px-2 py-1 text-center font-mono text-xs font-medium tabular-nums text-foreground"
htmlFor={id}
>
{value}px
</output>
<input
aria-label={label}
className="settings-slider block min-w-0 flex-1"
id={id}
max={max}
min={min}
onChange={(event) => {
const nextValue = Number(event.currentTarget.value);
if (Number.isInteger(nextValue) && nextValue >= min && nextValue <= max) {
onChange(nextValue);
}
}}
step={1}
style={sliderStyle}
type="range"
value={value}
/>
</div>
);
}

export function LastCodeSettingsPanel() {
const updateState = useDesktopUpdateState();
const clientSettings = usePrimarySettings();
Expand Down Expand Up @@ -182,6 +238,71 @@ export function LastCodeSettingsPanel() {
/>
</SettingsSection>
<SettingsSection title="Appearance" icon={<PaletteIcon className="size-5" />}>
<SettingsRow
{...searchableSetting("larger-scrollbars")}
description="Make scrollbar thumbs easier to grab. Margin keeps the thumb clear of pane resize handles."
control={
<Switch
checked={clientSettings.largerScrollbarsEnabled}
onCheckedChange={(checked) =>
updateClientSettings({ largerScrollbarsEnabled: Boolean(checked) })
}
aria-label="Larger scrollbars"
/>
}
/>
{clientSettings.largerScrollbarsEnabled ? (
<>
<SettingsRow
title="Scrollbar width"
description="Set the visible scrollbar thumb width in one-pixel increments."
resetAction={
clientSettings.scrollbarWidth !== DEFAULT_SCROLLBAR_WIDTH ? (
<SettingResetButton
label="scrollbar width"
onClick={() =>
updateClientSettings({ scrollbarWidth: DEFAULT_SCROLLBAR_WIDTH })
}
/>
) : null
}
control={
<PixelSlider
id="scrollbar-width"
label="Scrollbar width"
min={MIN_SCROLLBAR_WIDTH}
max={MAX_SCROLLBAR_WIDTH}
value={clientSettings.scrollbarWidth}
onChange={(scrollbarWidth) => updateClientSettings({ scrollbarWidth })}
/>
}
/>
<SettingsRow
title="Scrollbar margin"
description="Set the clear space between a scrollbar thumb and the pane edge."
resetAction={
clientSettings.scrollbarMargin !== DEFAULT_SCROLLBAR_MARGIN ? (
<SettingResetButton
label="scrollbar margin"
onClick={() =>
updateClientSettings({ scrollbarMargin: DEFAULT_SCROLLBAR_MARGIN })
}
/>
) : null
}
control={
<PixelSlider
id="scrollbar-margin"
label="Scrollbar margin"
min={MIN_SCROLLBAR_MARGIN}
max={MAX_SCROLLBAR_MARGIN}
value={clientSettings.scrollbarMargin}
onChange={(scrollbarMargin) => updateClientSettings({ scrollbarMargin })}
/>
}
/>
</>
) : null}
<SettingsRow
{...searchableSetting("scale-legacy-sidebar")}
description="Scale legacy project and thread rows while leaving the sidebar header, Search field, and Projects heading unchanged. The 75% marker matches the normalized version of the original compact-sidebar patch."
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/settings/settingsSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ describe("searchSettings", () => {
});
});

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",
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function ScrollBar({
return (
<ScrollAreaPrimitive.Scrollbar
className={cn(
"flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-px data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:my-1 data-[orientation=vertical]:mr-px data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
"flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-[var(--app-scrollbar-margin)] data-[orientation=horizontal]:h-[var(--app-scrollbar-width)] data-[orientation=vertical]:my-1 data-[orientation=vertical]:mr-[var(--app-scrollbar-margin)] data-[orientation=vertical]:w-[var(--app-scrollbar-width)] data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
className,
)}
data-slot="scroll-area-scrollbar"
Expand Down
36 changes: 27 additions & 9 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil

:root {
--app-scrollbar-width: 6px;
--app-scrollbar-margin: 1px;
--app-scrollbar-lane-width: var(--app-scrollbar-width);
--app-native-scrollbar-margin: 0px;
--app-scrollbar-thumb-inset: 1px;
--appearance-contrast-base: 100%;
--appearance-contrast-boost: 0%;
--appearance-contrast-border-boost: 0%;
Expand Down Expand Up @@ -371,7 +375,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
-webkit-mask-size:
100% var(--workspace-titlebar-scroll-fade-height),
100% calc(100% - var(--workspace-titlebar-scroll-fade-height)),
var(--app-scrollbar-width) 100%;
var(--app-scrollbar-lane-width) 100%;
mask-image:
linear-gradient(
to bottom,
Expand All @@ -389,7 +393,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
mask-size:
100% var(--workspace-titlebar-scroll-fade-height),
100% calc(100% - var(--workspace-titlebar-scroll-fade-height)),
var(--app-scrollbar-width) 100%;
var(--app-scrollbar-lane-width) 100%;
}

/* Virtualizers own their native scroll element, so they cannot use ScrollArea's
Expand All @@ -403,11 +407,11 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size:
calc(100% - var(--app-scrollbar-width)) 100%,
var(--app-scrollbar-width) 100%;
calc(100% - var(--app-scrollbar-lane-width)) 100%,
var(--app-scrollbar-lane-width) 100%;
mask-size:
calc(100% - var(--app-scrollbar-width)) 100%,
var(--app-scrollbar-width) 100%;
calc(100% - var(--app-scrollbar-lane-width)) 100%,
var(--app-scrollbar-lane-width) 100%;
}

/* Stage-channel art needs a mask and pseudo-element gradient, so keep the
Expand Down Expand Up @@ -2080,20 +2084,34 @@ code {

/* Scrollbar styling */
::-webkit-scrollbar {
width: var(--app-scrollbar-width);
width: var(--app-scrollbar-lane-width);
}

html[data-larger-scrollbars]::-webkit-scrollbar,
html[data-larger-scrollbars] ::-webkit-scrollbar {
height: var(--app-scrollbar-lane-width);
Comment thread
lastobelus marked this conversation as resolved.
}

::-webkit-scrollbar-track {
background: transparent;
}

::-webkit-scrollbar-thumb {
background: var(--app-scrollbar-thumb);
background-color: var(--app-scrollbar-thumb);
background-clip: padding-box;
border-radius: 3px;
}

::-webkit-scrollbar-thumb:vertical {
border-right: var(--app-native-scrollbar-margin) solid transparent;
}

::-webkit-scrollbar-thumb:horizontal {
border-bottom: var(--app-native-scrollbar-margin) solid transparent;
}

::-webkit-scrollbar-thumb:hover {
background: var(--app-scrollbar-thumb-hover);
background-color: var(--app-scrollbar-thumb-hover);
}

/* Chat markdown rendering */
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
import { applyAppearanceFontVariables } from "~/appearanceFonts";
import { applyAppearanceContrast } from "~/appearanceContrast";
import { applyScrollbarAppearance } from "~/scrollbarAppearance";
import { useClientSettings } from "../hooks/useSettings";
import { PlanAgentSelectionHeal } from "../planAgentSelectionHeal";
import {
Expand Down Expand Up @@ -142,6 +143,7 @@ function RootRouteView() {
<GlassAppearanceSync />
<FontAppearanceSync />
<ProjectIconAppearanceSync />
<ScrollbarAppearanceSync />
{primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null}
{primaryEnvironmentAuthenticated ? <DesktopAppActivationCoordinator /> : null}
<RelayClientInstallDialog />
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/scrollbarAppearance.test.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
27 changes: 27 additions & 0 deletions apps/web/src/scrollbarAppearance.ts
Original file line number Diff line number Diff line change
@@ -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");
}
Loading