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
90 changes: 90 additions & 0 deletions apps/app/src/views/SettingsView.appearance-layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// @vitest-environment jsdom

import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { defaultAppTheme } from "@bb/domain";
import {
resetPluginSlotStoreForTest,
setPluginSlotRegistrations,
} from "@/lib/plugin-slots";
import { makePluginRegistrationSet } from "@/test/fixtures/plugins";
import { AppearanceSettingsSection } from "./SettingsView";

afterEach(() => {
cleanup();
window.localStorage.clear();
resetPluginSlotStoreForTest();
});

it("keeps appearance choices first and groups provider controls in Interface", () => {
setPluginSlotRegistrations(
"fixture",
makePluginRegistrationSet({
experimentalSidebarHeaders: [
{ id: "header", title: "Fixture header", component: () => null },
],
sourceCodeRenderers: [
{ id: "source", title: "Fixture source", component: () => null },
],
diffRenderers: [
{ id: "diff", title: "Fixture diff", component: () => null },
],
threadLists: [
{ id: "threads", title: "Fixture threads", component: () => null },
],
experimentalSidebarNavigations: [
{
id: "navigation",
title: "Fixture navigation",
component: () => null,
},
],
}),
);

render(
<AppearanceSettingsSection
appearance={defaultAppTheme}
appearanceDisabled={false}
customThemes={[]}
pluginThemes={[]}
faviconColor="default"
onAppearanceThemeChange={vi.fn()}
onAppearanceThemePrefetch={vi.fn()}
onAppearanceThemePreview={vi.fn()}
onCreatePalette={vi.fn()}
onFaviconColorChange={vi.fn()}
onThemePreferenceChange={vi.fn()}
themePreference="system"
/>,
);

const labels = (section: HTMLElement) =>
Array.from(
section.querySelectorAll("[data-control-placement]"),
(row) => row.querySelector("p")?.textContent,
);
const appearance = screen
.getByRole("heading", { name: "Appearance" })
.closest("section");
const interfaceSection = screen
.getByRole("heading", { name: "Interface" })
.closest("section");

expect(appearance).not.toBeNull();
expect(interfaceSection).not.toBeNull();
expect(labels(appearance!)).toEqual([
"Theme",
"Palette",
"Favicon color",
"Fade inactive splits",
]);
expect(labels(interfaceSection!)).toEqual([
"Header",
"Source code",
"Diffs",
"Sidebar",
"Navigation",
"Sidebar footer",
]);
});
269 changes: 139 additions & 130 deletions apps/app/src/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -696,145 +696,154 @@ export function AppearanceSettingsSection({
onAppearanceThemeChange(themeId);
};
return (
<SettingsSection title="Appearance">
<div className="space-y-5">
<SidebarThreadListSetting />
<SidebarNavigationSetting />
<SidebarHeaderSetting />
<CodeRendererSettings />
<SettingsWithControl label="Theme">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={SETTINGS_DROPDOWN_TRIGGER_CLASS}
aria-label="Theme"
>
{THEME_PREFERENCE_LABELS[themePreference]}
<Icon
name="ChevronDown"
className="size-3.5 text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={SETTINGS_DROPDOWN_CONTENT_CLASS}
>
{THEME_PREFERENCE_OPTIONS.map((option) => (
<DropdownMenuItem
key={option.value}
onSelect={() => onThemePreferenceChange(option.value)}
<div className="space-y-6">
<SettingsSection title="Appearance">
<div className="space-y-5">
<SettingsWithControl label="Theme">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={SETTINGS_DROPDOWN_TRIGGER_CLASS}
aria-label="Theme"
>
{option.label}
{THEME_PREFERENCE_LABELS[themePreference]}
<Icon
name="Check"
className={cn(
"ml-auto",
themePreference !== option.value && "opacity-0",
COARSE_POINTER_ICON_SIZE_CLASS,
)}
name="ChevronDown"
className="size-3.5 text-muted-foreground"
/>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SettingsWithControl>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={SETTINGS_DROPDOWN_CONTENT_CLASS}
>
{THEME_PREFERENCE_OPTIONS.map((option) => (
<DropdownMenuItem
key={option.value}
onSelect={() => onThemePreferenceChange(option.value)}
>
{option.label}
<Icon
name="Check"
className={cn(
"ml-auto",
themePreference !== option.value && "opacity-0",
COARSE_POINTER_ICON_SIZE_CLASS,
)}
/>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SettingsWithControl>

<SettingsWithControl
label="Palette"
description={PALETTE_SETTING_DESCRIPTION}
>
<DropdownMenu
onOpenChange={(open) => {
if (open) {
paletteSelectedRef.current = false;
onAppearanceThemePrefetch([
...builtInThemes.map((entry) => entry.id),
...customThemes,
...pluginThemes.map((theme) => theme.id),
]);
return;
}
previewPalette(null);
}}
<SettingsWithControl
label="Palette"
description={PALETTE_SETTING_DESCRIPTION}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={SETTINGS_DROPDOWN_TRIGGER_CLASS}
aria-label="Palette"
disabled={appearanceDisabled}
>
<span className="min-w-0 truncate">
{appPaletteLabel(appearance, pluginThemes)}
</span>
<Icon
name="ChevronDown"
className="size-3.5 text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={SETTINGS_DROPDOWN_CONTENT_CLASS}
<DropdownMenu
onOpenChange={(open) => {
if (open) {
paletteSelectedRef.current = false;
onAppearanceThemePrefetch([
...builtInThemes.map((entry) => entry.id),
...customThemes,
...pluginThemes.map((theme) => theme.id),
]);
return;
}
previewPalette(null);
}}
>
{builtInThemes.map((entry) => (
<PaletteMenuItem
key={entry.id}
themeId={entry.id}
active={appearance.themeId === entry.id}
onPreview={previewPalette}
onSelect={selectPalette}
>
{entry.name}
</PaletteMenuItem>
))}
{customThemes.map((name) => (
<PaletteMenuItem
key={`custom:${name}`}
themeId={name}
active={appearance.themeId === name}
onPreview={previewPalette}
onSelect={selectPalette}
>
{name}
</PaletteMenuItem>
))}
{pluginThemes.map((theme) => (
<PaletteMenuItem
key={theme.id}
themeId={theme.id}
active={appearance.themeId === theme.id}
onPreview={previewPalette}
onSelect={selectPalette}
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={SETTINGS_DROPDOWN_TRIGGER_CLASS}
aria-label="Palette"
disabled={appearanceDisabled}
>
{theme.name}
<span className="text-muted-foreground">
({theme.pluginId})
<span className="min-w-0 truncate">
{appPaletteLabel(appearance, pluginThemes)}
</span>
</PaletteMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={onCreatePalette}>
<Icon name="Plus" className={COARSE_POINTER_ICON_SIZE_CLASS} />
Create
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SettingsWithControl>
<Icon
name="ChevronDown"
className="size-3.5 text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={SETTINGS_DROPDOWN_CONTENT_CLASS}
>
{builtInThemes.map((entry) => (
<PaletteMenuItem
key={entry.id}
themeId={entry.id}
active={appearance.themeId === entry.id}
onPreview={previewPalette}
onSelect={selectPalette}
>
{entry.name}
</PaletteMenuItem>
))}
{customThemes.map((name) => (
<PaletteMenuItem
key={`custom:${name}`}
themeId={name}
active={appearance.themeId === name}
onPreview={previewPalette}
onSelect={selectPalette}
>
{name}
</PaletteMenuItem>
))}
{pluginThemes.map((theme) => (
<PaletteMenuItem
key={theme.id}
themeId={theme.id}
active={appearance.themeId === theme.id}
onPreview={previewPalette}
onSelect={selectPalette}
>
{theme.name}
<span className="text-muted-foreground">
({theme.pluginId})
</span>
</PaletteMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={onCreatePalette}>
<Icon
name="Plus"
className={COARSE_POINTER_ICON_SIZE_CLASS}
/>
Create
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SettingsWithControl>

<FaviconColorSettingsControl
disabled={appearanceDisabled}
faviconColor={faviconColor}
onFaviconColorChange={onFaviconColorChange}
/>
<SplitDimmingSetting />
<SidebarFooterSettings />
</div>
</SettingsSection>
<FaviconColorSettingsControl
disabled={appearanceDisabled}
faviconColor={faviconColor}
onFaviconColorChange={onFaviconColorChange}
/>
<SplitDimmingSetting />
</div>
</SettingsSection>
<SettingsSection title="Interface">
<div className="space-y-5">
<SidebarHeaderSetting />
<CodeRendererSettings />
<SidebarThreadListSetting />
<SidebarNavigationSetting />
<SidebarFooterSettings />
</div>
</SettingsSection>
</div>
);
}

Expand Down
Loading