From a404be6ceecbfecb5646455920b616cb64b94dbc Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Mon, 11 May 2026 02:28:23 -0500 Subject: [PATCH] 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 --- apps/web/src/appSettings.test.ts | 17 ++++----- apps/web/src/appSettings.ts | 35 +++++++++++++------ apps/web/src/components/Sidebar.tsx | 27 +++++++------- .../src/components/settings/SettingsUi.tsx | 10 ++++-- apps/web/src/routes/_chat.settings.style.tsx | 8 +++-- 5 files changed, 61 insertions(+), 36 deletions(-) diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index 19622be83..90747fde3 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -68,11 +68,11 @@ describe("AppSettingsSchema", () => { describe("clampSidebarProjectRowHeight", () => { it("exposes the expected accessibility-minded bounds", () => { expect(SIDEBAR_PROJECT_ROW_HEIGHT_MIN).toBe(32); - expect(SIDEBAR_PROJECT_ROW_HEIGHT_MAX).toBe(72); + expect(SIDEBAR_PROJECT_ROW_HEIGHT_MAX).toBe(48); expect(DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT).toBe(32); }); - it("clamps below-floor values up to the new floor of 32", () => { + it("clamps below-floor values up to the floor of 32", () => { expect(clampSidebarProjectRowHeight(0)).toBe(32); expect(clampSidebarProjectRowHeight(24)).toBe(32); // legacy floor expect(clampSidebarProjectRowHeight(28)).toBe(32); // legacy default @@ -81,15 +81,16 @@ describe("clampSidebarProjectRowHeight", () => { it("accepts in-range values and rounds fractional input", () => { expect(clampSidebarProjectRowHeight(32)).toBe(32); + expect(clampSidebarProjectRowHeight(40)).toBe(40); + expect(clampSidebarProjectRowHeight(47.4)).toBe(47); expect(clampSidebarProjectRowHeight(48)).toBe(48); - expect(clampSidebarProjectRowHeight(71.4)).toBe(71); - expect(clampSidebarProjectRowHeight(72)).toBe(72); }); - it("clamps above-ceiling values down to the new max of 72", () => { - expect(clampSidebarProjectRowHeight(73)).toBe(72); - expect(clampSidebarProjectRowHeight(120)).toBe(72); - expect(clampSidebarProjectRowHeight(Number.POSITIVE_INFINITY)).toBe(72); + it("clamps above-ceiling values down to the new max of 48", () => { + expect(clampSidebarProjectRowHeight(49)).toBe(48); + expect(clampSidebarProjectRowHeight(72)).toBe(48); // legacy ceiling + expect(clampSidebarProjectRowHeight(120)).toBe(48); + expect(clampSidebarProjectRowHeight(Number.POSITIVE_INFINITY)).toBe(48); }); }); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index d2b5c4a79..45911f64a 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -21,18 +21,30 @@ const MAX_CUSTOM_MODEL_COUNT = 32; export const MAX_CUSTOM_MODEL_LENGTH = 256; const BACKGROUND_IMAGE_KEY = "okcode:background-image"; const BACKGROUND_OPACITY_KEY = "okcode:background-opacity"; +// Sidebar density constraints are tuned to keep rows within +// best-practice layouts and ratios. Tap targets stay >=28px, +// rows stay below an upper bound that preserves list density, +// and font/spacing values stay inside a legibility window. export const SIDEBAR_PROJECT_ROW_HEIGHT_MIN = 32; -export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 72; +export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 48; export const DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT = 32; -export const SIDEBAR_THREAD_ROW_HEIGHT_MIN = 24; -export const SIDEBAR_THREAD_ROW_HEIGHT_MAX = 44; +export const SIDEBAR_THREAD_ROW_HEIGHT_MIN = 28; +export const SIDEBAR_THREAD_ROW_HEIGHT_MAX = 40; export const DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT = 28; -export const SIDEBAR_FONT_SIZE_MIN = 10; -export const SIDEBAR_FONT_SIZE_MAX = 16; +export const SIDEBAR_FONT_SIZE_MIN = 11; +export const SIDEBAR_FONT_SIZE_MAX = 15; export const DEFAULT_SIDEBAR_FONT_SIZE = 12; -export const SIDEBAR_SPACING_MIN = 4; +export const SIDEBAR_SPACING_MIN = 6; export const SIDEBAR_SPACING_MAX = 12; export const DEFAULT_SIDEBAR_SPACING = 8; +// Transparency floors keep the sidebar readable and prevent +// background images from competing with foreground content. +export const SIDEBAR_OPACITY_MIN = 0.6; +export const SIDEBAR_OPACITY_MAX = 1; +export const DEFAULT_SIDEBAR_OPACITY = 1; +export const BACKGROUND_IMAGE_OPACITY_MIN = 0.05; +export const BACKGROUND_IMAGE_OPACITY_MAX = 0.35; +export const DEFAULT_BACKGROUND_IMAGE_OPACITY = 0.15; export const DEFAULT_BROWSER_PREVIEW_START_PAGE_URL = "https://www.google.com/"; export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]); @@ -94,7 +106,7 @@ export const AppSettingsSchema = Schema.Struct({ codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), backgroundImageUrl: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), - backgroundImageOpacity: Schema.Number.pipe(withDefaults(() => 0.15)), + backgroundImageOpacity: Schema.Number.pipe(withDefaults(() => DEFAULT_BACKGROUND_IMAGE_OPACITY)), defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "worktree" as const satisfies EnvMode)), autoUpdateWorktreeBaseBranch: Schema.Boolean.pipe(withDefaults(() => false)), confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)), @@ -117,7 +129,7 @@ export const AppSettingsSchema = Schema.Struct({ withDefaults(() => DEFAULT_SIDEBAR_THREAD_SORT_ORDER), ), timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)), - sidebarOpacity: Schema.Number.pipe(withDefaults(() => 1)), + sidebarOpacity: Schema.Number.pipe(withDefaults(() => DEFAULT_SIDEBAR_OPACITY)), sidebarProjectRowHeight: Schema.Number.pipe( withDefaults(() => DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT), ), @@ -230,11 +242,14 @@ export function normalizeCustomModelSlugs( } function clampOpacity(value: number): number { - return Math.max(0.3, Math.min(1, value)); + return Math.max(SIDEBAR_OPACITY_MIN, Math.min(SIDEBAR_OPACITY_MAX, value)); } function clampBackgroundOpacity(value: number): number { - return Math.max(0.05, Math.min(1, value)); + return Math.max( + BACKGROUND_IMAGE_OPACITY_MIN, + Math.min(BACKGROUND_IMAGE_OPACITY_MAX, value), + ); } export function clampSidebarProjectRowHeight(value: number): number { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c1aac0034..f4d5e9b5c 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -444,14 +444,20 @@ const MemoizedThreadRow = memo( return ( + {isActive ? ( + + ) : null} } size="sm" isActive={isActive} className={cn( - "h-auto translate-x-0 items-center rounded-md text-left", + "h-auto translate-x-0 items-center rounded-md text-left transition-colors duration-150", isActive - ? "bg-accent/60 text-foreground" + ? "bg-accent/70 text-foreground" : isSelected ? "bg-accent/40 text-foreground" : "text-muted-foreground hover:bg-accent/40 hover:text-foreground", @@ -1569,7 +1575,6 @@ export default function Sidebar() { visualIndex: number, ) { const projectThreads = sidebarThreadsByProjectId.get(project.id) ?? EMPTY_THREADS; - const hasProjectChat = projectChatThreadByProjectId.has(project.id); const activeThreadId = routeThreadId ?? undefined; const isThreadListExpanded = expandedThreadListsByProject.has(project.id); const pinnedCollapsedThread = @@ -1598,7 +1603,10 @@ export default function Sidebar() { return (
{ event.preventDefault(); event.stopPropagation(); @@ -1624,9 +1632,9 @@ export default function Sidebar() { ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined} size="sm" className={cn( - "h-auto min-w-0 flex-1 gap-1.5 rounded-md px-2 text-left transition-colors hover:bg-transparent", + "h-auto min-w-0 flex-1 gap-1.5 rounded-md px-2 text-left transition-colors duration-150 hover:bg-transparent", isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer", - isActiveProject && "bg-background/70 text-foreground shadow-sm", + isActiveProject && "bg-background/80 text-foreground", )} style={SIDEBAR_PROJECT_ROW_STYLE} {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})} @@ -1680,11 +1688,6 @@ export default function Sidebar() { > {project.name} - {hasProjectChat ? ( - - Chat - - ) : null} {isMissingOnDisk ? : null} )} diff --git a/apps/web/src/components/settings/SettingsUi.tsx b/apps/web/src/components/settings/SettingsUi.tsx index e9b4e5e05..e30150005 100644 --- a/apps/web/src/components/settings/SettingsUi.tsx +++ b/apps/web/src/components/settings/SettingsUi.tsx @@ -5,6 +5,10 @@ import { Input } from "../ui/input"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "../../lib/utils"; import { Undo2Icon } from "lucide-react"; +import { + BACKGROUND_IMAGE_OPACITY_MAX, + BACKGROUND_IMAGE_OPACITY_MIN, +} from "../../appSettings"; export function SettingsSection({ title, @@ -168,13 +172,13 @@ export function BackgroundImageSettings({ {hasBackground && ( { const value = Number(e.target.value) / 100; diff --git a/apps/web/src/routes/_chat.settings.style.tsx b/apps/web/src/routes/_chat.settings.style.tsx index d2b301624..a03369a12 100644 --- a/apps/web/src/routes/_chat.settings.style.tsx +++ b/apps/web/src/routes/_chat.settings.style.tsx @@ -9,6 +9,8 @@ import { DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT, SIDEBAR_FONT_SIZE_MAX, SIDEBAR_FONT_SIZE_MIN, + SIDEBAR_OPACITY_MAX, + SIDEBAR_OPACITY_MIN, SIDEBAR_PROJECT_ROW_HEIGHT_MAX, SIDEBAR_PROJECT_ROW_HEIGHT_MIN, SIDEBAR_SPACING_MAX, @@ -426,7 +428,7 @@ function SettingsStyleRouteView() { > { const value = Number(e.target.value) / 100;