Skip to content

Commit a404be6

Browse files
committed
Tighten sidebar density and readability defaults
- Reduce sidebar row, spacing, and opacity ranges - Cap background image opacity and update settings UI - Refresh sidebar visuals and related clamp tests
1 parent 132af83 commit a404be6

5 files changed

Lines changed: 61 additions & 36 deletions

File tree

apps/web/src/appSettings.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ describe("AppSettingsSchema", () => {
6868
describe("clampSidebarProjectRowHeight", () => {
6969
it("exposes the expected accessibility-minded bounds", () => {
7070
expect(SIDEBAR_PROJECT_ROW_HEIGHT_MIN).toBe(32);
71-
expect(SIDEBAR_PROJECT_ROW_HEIGHT_MAX).toBe(72);
71+
expect(SIDEBAR_PROJECT_ROW_HEIGHT_MAX).toBe(48);
7272
expect(DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT).toBe(32);
7373
});
7474

75-
it("clamps below-floor values up to the new floor of 32", () => {
75+
it("clamps below-floor values up to the floor of 32", () => {
7676
expect(clampSidebarProjectRowHeight(0)).toBe(32);
7777
expect(clampSidebarProjectRowHeight(24)).toBe(32); // legacy floor
7878
expect(clampSidebarProjectRowHeight(28)).toBe(32); // legacy default
@@ -81,15 +81,16 @@ describe("clampSidebarProjectRowHeight", () => {
8181

8282
it("accepts in-range values and rounds fractional input", () => {
8383
expect(clampSidebarProjectRowHeight(32)).toBe(32);
84+
expect(clampSidebarProjectRowHeight(40)).toBe(40);
85+
expect(clampSidebarProjectRowHeight(47.4)).toBe(47);
8486
expect(clampSidebarProjectRowHeight(48)).toBe(48);
85-
expect(clampSidebarProjectRowHeight(71.4)).toBe(71);
86-
expect(clampSidebarProjectRowHeight(72)).toBe(72);
8787
});
8888

89-
it("clamps above-ceiling values down to the new max of 72", () => {
90-
expect(clampSidebarProjectRowHeight(73)).toBe(72);
91-
expect(clampSidebarProjectRowHeight(120)).toBe(72);
92-
expect(clampSidebarProjectRowHeight(Number.POSITIVE_INFINITY)).toBe(72);
89+
it("clamps above-ceiling values down to the new max of 48", () => {
90+
expect(clampSidebarProjectRowHeight(49)).toBe(48);
91+
expect(clampSidebarProjectRowHeight(72)).toBe(48); // legacy ceiling
92+
expect(clampSidebarProjectRowHeight(120)).toBe(48);
93+
expect(clampSidebarProjectRowHeight(Number.POSITIVE_INFINITY)).toBe(48);
9394
});
9495
});
9596

apps/web/src/appSettings.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,30 @@ const MAX_CUSTOM_MODEL_COUNT = 32;
2121
export const MAX_CUSTOM_MODEL_LENGTH = 256;
2222
const BACKGROUND_IMAGE_KEY = "okcode:background-image";
2323
const BACKGROUND_OPACITY_KEY = "okcode:background-opacity";
24+
// Sidebar density constraints are tuned to keep rows within
25+
// best-practice layouts and ratios. Tap targets stay >=28px,
26+
// rows stay below an upper bound that preserves list density,
27+
// and font/spacing values stay inside a legibility window.
2428
export const SIDEBAR_PROJECT_ROW_HEIGHT_MIN = 32;
25-
export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 72;
29+
export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 48;
2630
export const DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT = 32;
27-
export const SIDEBAR_THREAD_ROW_HEIGHT_MIN = 24;
28-
export const SIDEBAR_THREAD_ROW_HEIGHT_MAX = 44;
31+
export const SIDEBAR_THREAD_ROW_HEIGHT_MIN = 28;
32+
export const SIDEBAR_THREAD_ROW_HEIGHT_MAX = 40;
2933
export const DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT = 28;
30-
export const SIDEBAR_FONT_SIZE_MIN = 10;
31-
export const SIDEBAR_FONT_SIZE_MAX = 16;
34+
export const SIDEBAR_FONT_SIZE_MIN = 11;
35+
export const SIDEBAR_FONT_SIZE_MAX = 15;
3236
export const DEFAULT_SIDEBAR_FONT_SIZE = 12;
33-
export const SIDEBAR_SPACING_MIN = 4;
37+
export const SIDEBAR_SPACING_MIN = 6;
3438
export const SIDEBAR_SPACING_MAX = 12;
3539
export const DEFAULT_SIDEBAR_SPACING = 8;
40+
// Transparency floors keep the sidebar readable and prevent
41+
// background images from competing with foreground content.
42+
export const SIDEBAR_OPACITY_MIN = 0.6;
43+
export const SIDEBAR_OPACITY_MAX = 1;
44+
export const DEFAULT_SIDEBAR_OPACITY = 1;
45+
export const BACKGROUND_IMAGE_OPACITY_MIN = 0.05;
46+
export const BACKGROUND_IMAGE_OPACITY_MAX = 0.35;
47+
export const DEFAULT_BACKGROUND_IMAGE_OPACITY = 0.15;
3648
export const DEFAULT_BROWSER_PREVIEW_START_PAGE_URL = "https://www.google.com/";
3749

3850
export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
@@ -94,7 +106,7 @@ export const AppSettingsSchema = Schema.Struct({
94106
codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
95107
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
96108
backgroundImageUrl: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
97-
backgroundImageOpacity: Schema.Number.pipe(withDefaults(() => 0.15)),
109+
backgroundImageOpacity: Schema.Number.pipe(withDefaults(() => DEFAULT_BACKGROUND_IMAGE_OPACITY)),
98110
defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "worktree" as const satisfies EnvMode)),
99111
autoUpdateWorktreeBaseBranch: Schema.Boolean.pipe(withDefaults(() => false)),
100112
confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
@@ -117,7 +129,7 @@ export const AppSettingsSchema = Schema.Struct({
117129
withDefaults(() => DEFAULT_SIDEBAR_THREAD_SORT_ORDER),
118130
),
119131
timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)),
120-
sidebarOpacity: Schema.Number.pipe(withDefaults(() => 1)),
132+
sidebarOpacity: Schema.Number.pipe(withDefaults(() => DEFAULT_SIDEBAR_OPACITY)),
121133
sidebarProjectRowHeight: Schema.Number.pipe(
122134
withDefaults(() => DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT),
123135
),
@@ -230,11 +242,14 @@ export function normalizeCustomModelSlugs(
230242
}
231243

232244
function clampOpacity(value: number): number {
233-
return Math.max(0.3, Math.min(1, value));
245+
return Math.max(SIDEBAR_OPACITY_MIN, Math.min(SIDEBAR_OPACITY_MAX, value));
234246
}
235247

236248
function clampBackgroundOpacity(value: number): number {
237-
return Math.max(0.05, Math.min(1, value));
249+
return Math.max(
250+
BACKGROUND_IMAGE_OPACITY_MIN,
251+
Math.min(BACKGROUND_IMAGE_OPACITY_MAX, value),
252+
);
238253
}
239254

240255
export function clampSidebarProjectRowHeight(value: number): number {

apps/web/src/components/Sidebar.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,20 @@ const MemoizedThreadRow = memo(
444444

445445
return (
446446
<SidebarMenuSubItem key={thread.id} className="relative w-full" data-thread-item>
447+
{isActive ? (
448+
<span
449+
aria-hidden
450+
className="pointer-events-none absolute left-0 top-1/2 h-[60%] w-[2px] -translate-y-1/2 rounded-full bg-primary/70"
451+
/>
452+
) : null}
447453
<SidebarMenuSubButton
448454
render={<div role="button" tabIndex={0} />}
449455
size="sm"
450456
isActive={isActive}
451457
className={cn(
452-
"h-auto translate-x-0 items-center rounded-md text-left",
458+
"h-auto translate-x-0 items-center rounded-md text-left transition-colors duration-150",
453459
isActive
454-
? "bg-accent/60 text-foreground"
460+
? "bg-accent/70 text-foreground"
455461
: isSelected
456462
? "bg-accent/40 text-foreground"
457463
: "text-muted-foreground hover:bg-accent/40 hover:text-foreground",
@@ -1569,7 +1575,6 @@ export default function Sidebar() {
15691575
visualIndex: number,
15701576
) {
15711577
const projectThreads = sidebarThreadsByProjectId.get(project.id) ?? EMPTY_THREADS;
1572-
const hasProjectChat = projectChatThreadByProjectId.has(project.id);
15731578
const activeThreadId = routeThreadId ?? undefined;
15741579
const isThreadListExpanded = expandedThreadListsByProject.has(project.id);
15751580
const pinnedCollapsedThread =
@@ -1598,7 +1603,10 @@ export default function Sidebar() {
15981603
return (
15991604
<Collapsible className="group/collapsible" open={shouldShowThreadPanel}>
16001605
<div
1601-
className="group/project-header relative flex items-center rounded-md"
1606+
className={cn(
1607+
"group/project-header relative flex items-center rounded-md transition-shadow duration-150",
1608+
isActiveProject && "ring-1 ring-border/60 shadow-sm",
1609+
)}
16021610
style={{
16031611
...SIDEBAR_PROJECT_HEADER_STYLE,
16041612
backgroundColor: isDark ? pColor.bgDark : pColor.bg,
@@ -1607,7 +1615,7 @@ export default function Sidebar() {
16071615
<button
16081616
type="button"
16091617
aria-label={project.expanded ? "Collapse project threads" : "Expand project threads"}
1610-
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
1618+
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors duration-150 hover:bg-accent hover:text-foreground"
16111619
onClick={(event) => {
16121620
event.preventDefault();
16131621
event.stopPropagation();
@@ -1624,9 +1632,9 @@ export default function Sidebar() {
16241632
ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
16251633
size="sm"
16261634
className={cn(
1627-
"h-auto min-w-0 flex-1 gap-1.5 rounded-md px-2 text-left transition-colors hover:bg-transparent",
1635+
"h-auto min-w-0 flex-1 gap-1.5 rounded-md px-2 text-left transition-colors duration-150 hover:bg-transparent",
16281636
isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer",
1629-
isActiveProject && "bg-background/70 text-foreground shadow-sm",
1637+
isActiveProject && "bg-background/80 text-foreground",
16301638
)}
16311639
style={SIDEBAR_PROJECT_ROW_STYLE}
16321640
{...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
@@ -1680,11 +1688,6 @@ export default function Sidebar() {
16801688
>
16811689
{project.name}
16821690
</span>
1683-
{hasProjectChat ? (
1684-
<span className="shrink-0 rounded-full bg-background/70 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground/80">
1685-
Chat
1686-
</span>
1687-
) : null}
16881691
{isMissingOnDisk ? <MissingOnDiskBadge path={project.cwd} /> : null}
16891692
</span>
16901693
)}

apps/web/src/components/settings/SettingsUi.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { Input } from "../ui/input";
55
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
66
import { cn } from "../../lib/utils";
77
import { Undo2Icon } from "lucide-react";
8+
import {
9+
BACKGROUND_IMAGE_OPACITY_MAX,
10+
BACKGROUND_IMAGE_OPACITY_MIN,
11+
} from "../../appSettings";
812

913
export function SettingsSection({
1014
title,
@@ -168,13 +172,13 @@ export function BackgroundImageSettings({
168172
{hasBackground && (
169173
<SettingsRow
170174
title="Background opacity"
171-
description="Adjust the visibility of the custom background image."
175+
description="Adjust the visibility of the custom background image. Capped to keep foreground content readable."
172176
control={
173177
<div className="flex items-center gap-2">
174178
<input
175179
type="range"
176-
min={5}
177-
max={100}
180+
min={Math.round(BACKGROUND_IMAGE_OPACITY_MIN * 100)}
181+
max={Math.round(BACKGROUND_IMAGE_OPACITY_MAX * 100)}
178182
value={Math.round(backgroundImageOpacity * 100)}
179183
onChange={(e) => {
180184
const value = Number(e.target.value) / 100;

apps/web/src/routes/_chat.settings.style.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT,
1010
SIDEBAR_FONT_SIZE_MAX,
1111
SIDEBAR_FONT_SIZE_MIN,
12+
SIDEBAR_OPACITY_MAX,
13+
SIDEBAR_OPACITY_MIN,
1214
SIDEBAR_PROJECT_ROW_HEIGHT_MAX,
1315
SIDEBAR_PROJECT_ROW_HEIGHT_MIN,
1416
SIDEBAR_SPACING_MAX,
@@ -426,7 +428,7 @@ function SettingsStyleRouteView() {
426428
>
427429
<SettingsRow
428430
title="Sidebar opacity"
429-
description="Adjust the transparency of the side panel and project list."
431+
description="Adjust the transparency of the side panel and project list. Constrained to keep text readable."
430432
resetAction={
431433
settings.sidebarOpacity !== defaults.sidebarOpacity ? (
432434
<SettingResetButton
@@ -439,8 +441,8 @@ function SettingsStyleRouteView() {
439441
<div className="flex items-center gap-2">
440442
<input
441443
type="range"
442-
min={30}
443-
max={100}
444+
min={Math.round(SIDEBAR_OPACITY_MIN * 100)}
445+
max={Math.round(SIDEBAR_OPACITY_MAX * 100)}
444446
value={Math.round(settings.sidebarOpacity * 100)}
445447
onChange={(e) => {
446448
const value = Number(e.target.value) / 100;

0 commit comments

Comments
 (0)