Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 9 additions & 8 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
});
});

Expand Down
35 changes: 25 additions & 10 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -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)),
Expand All @@ -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),
),
Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 15 additions & 12 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,20 @@ const MemoizedThreadRow = memo(

return (
<SidebarMenuSubItem key={thread.id} className="relative w-full" data-thread-item>
{isActive ? (
<span
aria-hidden
className="pointer-events-none absolute left-0 top-1/2 h-[60%] w-[2px] -translate-y-1/2 rounded-full bg-primary/70"
/>
) : null}
<SidebarMenuSubButton
render={<div role="button" tabIndex={0} />}
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",
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -1598,7 +1603,10 @@ export default function Sidebar() {
return (
<Collapsible className="group/collapsible" open={shouldShowThreadPanel}>
<div
className="group/project-header relative flex items-center rounded-md"
className={cn(
"group/project-header relative flex items-center rounded-md transition-shadow duration-150",
isActiveProject && "ring-1 ring-border/60 shadow-sm",
)}
style={{
...SIDEBAR_PROJECT_HEADER_STYLE,
backgroundColor: isDark ? pColor.bgDark : pColor.bg,
Expand All @@ -1607,7 +1615,7 @@ export default function Sidebar() {
<button
type="button"
aria-label={project.expanded ? "Collapse project threads" : "Expand project threads"}
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"
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"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -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 : {})}
Expand Down Expand Up @@ -1680,11 +1688,6 @@ export default function Sidebar() {
>
{project.name}
</span>
{hasProjectChat ? (
<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">
Chat
</span>
) : null}
{isMissingOnDisk ? <MissingOnDiskBadge path={project.cwd} /> : null}
</span>
)}
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/components/settings/SettingsUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -168,13 +172,13 @@ export function BackgroundImageSettings({
{hasBackground && (
<SettingsRow
title="Background opacity"
description="Adjust the visibility of the custom background image."
description="Adjust the visibility of the custom background image. Capped to keep foreground content readable."
control={
<div className="flex items-center gap-2">
<input
type="range"
min={5}
max={100}
min={Math.round(BACKGROUND_IMAGE_OPACITY_MIN * 100)}
max={Math.round(BACKGROUND_IMAGE_OPACITY_MAX * 100)}
value={Math.round(backgroundImageOpacity * 100)}
onChange={(e) => {
const value = Number(e.target.value) / 100;
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/routes/_chat.settings.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -426,7 +428,7 @@ function SettingsStyleRouteView() {
>
<SettingsRow
title="Sidebar opacity"
description="Adjust the transparency of the side panel and project list."
description="Adjust the transparency of the side panel and project list. Constrained to keep text readable."
resetAction={
settings.sidebarOpacity !== defaults.sidebarOpacity ? (
<SettingResetButton
Expand All @@ -439,8 +441,8 @@ function SettingsStyleRouteView() {
<div className="flex items-center gap-2">
<input
type="range"
min={30}
max={100}
min={Math.round(SIDEBAR_OPACITY_MIN * 100)}
max={Math.round(SIDEBAR_OPACITY_MAX * 100)}
value={Math.round(settings.sidebarOpacity * 100)}
onChange={(e) => {
const value = Number(e.target.value) / 100;
Expand Down
Loading