mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
Add left sidebar appearance setting
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -136,6 +136,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
||||
terminalWindowsPowerShellImplementation: 'powershell.exe',
|
||||
enableGitHubAttribution: true,
|
||||
...overrides,
|
||||
leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default',
|
||||
appFontFamily,
|
||||
agentStatusHooksEnabled,
|
||||
tabAutoGenerateTitle
|
||||
|
||||
@@ -140,6 +140,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
||||
terminalWindowsPowerShellImplementation: 'powershell.exe',
|
||||
enableGitHubAttribution: true,
|
||||
...overrides,
|
||||
leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default',
|
||||
appFontFamily,
|
||||
agentStatusHooksEnabled,
|
||||
tabAutoGenerateTitle
|
||||
|
||||
@@ -97,6 +97,11 @@ import { normalizeOpenInApplications } from '../shared/open-in-applications'
|
||||
import { normalizeTerminalShortcutPolicy } from '../shared/keybindings'
|
||||
import { normalizeAppIconId } from '../shared/app-icon'
|
||||
import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes'
|
||||
import {
|
||||
normalizeLeftSidebarAppearanceMode,
|
||||
normalizeLeftSidebarTintColor,
|
||||
normalizeLeftSidebarTintOpacity
|
||||
} from '../shared/left-sidebar-appearance'
|
||||
import {
|
||||
compareFeatureInteractionUsageBuckets,
|
||||
getFeatureInteractionCategory,
|
||||
@@ -2308,6 +2313,15 @@ export class Store {
|
||||
terminalCustomThemes: normalizeTerminalCustomThemes(
|
||||
parsed.settings?.terminalCustomThemes
|
||||
),
|
||||
leftSidebarAppearanceMode: normalizeLeftSidebarAppearanceMode(
|
||||
parsed.settings?.leftSidebarAppearanceMode
|
||||
),
|
||||
leftSidebarTintColor: normalizeLeftSidebarTintColor(
|
||||
parsed.settings?.leftSidebarTintColor
|
||||
),
|
||||
leftSidebarTintOpacity: normalizeLeftSidebarTintOpacity(
|
||||
parsed.settings?.leftSidebarTintOpacity
|
||||
),
|
||||
appIcon: normalizeAppIconId(parsed.settings?.appIcon),
|
||||
uiLanguage: normalizeUiLanguage(parsed.settings?.uiLanguage),
|
||||
defaultTaskSource: taskProviderSettings.defaultTaskSource,
|
||||
@@ -3635,6 +3649,21 @@ export class Store {
|
||||
updates.terminalCustomThemes
|
||||
)
|
||||
}
|
||||
if ('leftSidebarAppearanceMode' in updates) {
|
||||
sanitizedUpdates.leftSidebarAppearanceMode = normalizeLeftSidebarAppearanceMode(
|
||||
updates.leftSidebarAppearanceMode
|
||||
)
|
||||
}
|
||||
if ('leftSidebarTintColor' in updates) {
|
||||
sanitizedUpdates.leftSidebarTintColor = normalizeLeftSidebarTintColor(
|
||||
updates.leftSidebarTintColor
|
||||
)
|
||||
}
|
||||
if ('leftSidebarTintOpacity' in updates) {
|
||||
sanitizedUpdates.leftSidebarTintOpacity = normalizeLeftSidebarTintOpacity(
|
||||
updates.leftSidebarTintOpacity
|
||||
)
|
||||
}
|
||||
if ('visibleTaskProviders' in updates || 'defaultTaskSource' in updates) {
|
||||
const taskProviderSettings = normalizeTaskProviderSettings({
|
||||
visibleTaskProviders:
|
||||
|
||||
@@ -194,6 +194,27 @@ describe('AppearancePane', () => {
|
||||
expect(updateSettings).toHaveBeenCalledWith({ uiLanguage: 'zh' })
|
||||
})
|
||||
|
||||
it('updates the left sidebar appearance from sidebar settings', async () => {
|
||||
mocks.state.settingsSearchQuery = 'left sidebar'
|
||||
const updateSettings = vi.fn()
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
|
||||
const container = await renderAppearancePane(settings, updateSettings)
|
||||
const matchTerminalButton = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>('button[role="radio"]')
|
||||
).find((button) => button.textContent === 'Match Terminal')
|
||||
|
||||
expect(matchTerminalButton).toBeDefined()
|
||||
|
||||
await act(async () => {
|
||||
matchTerminalButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
leftSidebarAppearanceMode: 'match-terminal'
|
||||
})
|
||||
})
|
||||
|
||||
it('restores the Automations sidebar button from the sidebar settings switch', async () => {
|
||||
const updateSettings = vi.fn()
|
||||
const settings = {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getAppearancePaneSearchEntries,
|
||||
getLanguageEntries,
|
||||
getLayoutEntries,
|
||||
getLeftSidebarAppearanceEntry,
|
||||
getSidebarEntries,
|
||||
getStatusBarEntries,
|
||||
getStatusBarToggles,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
} from '@/i18n/supported-languages'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { UiLanguage } from '../../../../shared/ui-language'
|
||||
import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting'
|
||||
export { getAppearancePaneSearchEntries }
|
||||
|
||||
type AppearancePaneProps = {
|
||||
@@ -104,6 +106,7 @@ export function AppearancePane({
|
||||
const terminalAppearanceSearchEntries = getTerminalAppearanceSearchEntries({
|
||||
showWarpImport: !isWebClientLocation()
|
||||
})
|
||||
const leftSidebarAppearanceEntry = getLeftSidebarAppearanceEntry()
|
||||
const visibleSections = [
|
||||
matchesSettingsSearch(searchQuery, getThemeEntries()) ||
|
||||
(SHOW_UI_LANGUAGE_SETTING && matchesSettingsSearch(searchQuery, getLanguageEntries())) ||
|
||||
@@ -412,6 +415,15 @@ export function AppearancePane({
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title={leftSidebarAppearanceEntry.title}
|
||||
description={leftSidebarAppearanceEntry.description}
|
||||
keywords={leftSidebarAppearanceEntry.keywords}
|
||||
className="space-y-2"
|
||||
>
|
||||
<LeftSidebarAppearanceSetting settings={settings} updateSettings={updateSettings} />
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.AppearancePane.cf81907069',
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import type React from 'react'
|
||||
import type { GlobalSettings, LeftSidebarAppearanceMode } from '../../../../shared/types'
|
||||
import {
|
||||
DEFAULT_LEFT_SIDEBAR_TINT_COLOR,
|
||||
DEFAULT_LEFT_SIDEBAR_TINT_OPACITY,
|
||||
MAX_LEFT_SIDEBAR_TINT_OPACITY
|
||||
} from '../../../../shared/left-sidebar-appearance'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
ColorField,
|
||||
NumberField,
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl
|
||||
} from './SettingsFormControls'
|
||||
|
||||
type LeftSidebarAppearanceSettingProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
}
|
||||
|
||||
export function LeftSidebarAppearanceSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: LeftSidebarAppearanceSettingProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<SettingsRow
|
||||
alignTop
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.title',
|
||||
'Left Sidebar Appearance'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.rowDescription',
|
||||
'Make the left sidebar match your terminal, stay default, or use a tint.'
|
||||
)}
|
||||
control={
|
||||
<SettingsSegmentedControl<LeftSidebarAppearanceMode>
|
||||
size="sm"
|
||||
value={settings.leftSidebarAppearanceMode ?? 'default'}
|
||||
onChange={(leftSidebarAppearanceMode) => updateSettings({ leftSidebarAppearanceMode })}
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.title',
|
||||
'Left Sidebar Appearance'
|
||||
)}
|
||||
options={[
|
||||
{
|
||||
value: 'default',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.default',
|
||||
'Default'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'match-terminal',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.matchTerminal',
|
||||
'Match Terminal'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'soft-contrast',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.softContrast',
|
||||
'Soft Contrast'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'tinted',
|
||||
label: translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.tinted',
|
||||
'Tinted'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{(settings.leftSidebarAppearanceMode ?? 'default') === 'tinted' ? (
|
||||
<div className="space-y-2">
|
||||
<ColorField
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintColor',
|
||||
'Sidebar Tint'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintColorDescription',
|
||||
'The color mixed into the left sidebar surface.'
|
||||
)}
|
||||
value={settings.leftSidebarTintColor ?? DEFAULT_LEFT_SIDEBAR_TINT_COLOR}
|
||||
fallback={DEFAULT_LEFT_SIDEBAR_TINT_COLOR}
|
||||
onChange={(leftSidebarTintColor) => updateSettings({ leftSidebarTintColor })}
|
||||
/>
|
||||
<NumberField
|
||||
label={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintOpacity',
|
||||
'Tint Strength'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintOpacityDescription',
|
||||
'Controls how strongly the tint is mixed into the sidebar.'
|
||||
)}
|
||||
value={settings.leftSidebarTintOpacity ?? DEFAULT_LEFT_SIDEBAR_TINT_OPACITY}
|
||||
defaultValue={DEFAULT_LEFT_SIDEBAR_TINT_OPACITY}
|
||||
min={0}
|
||||
max={MAX_LEFT_SIDEBAR_TINT_OPACITY}
|
||||
step={0.01}
|
||||
suffix={`0 to ${MAX_LEFT_SIDEBAR_TINT_OPACITY}`}
|
||||
onChange={(leftSidebarTintOpacity) => updateSettings({ leftSidebarTintOpacity })}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -947,6 +947,7 @@ function Settings(): React.JSX.Element {
|
||||
>
|
||||
<SettingsSidebar
|
||||
activeSectionId={activeSectionId}
|
||||
settings={settings}
|
||||
generalGroups={generalNavGroups}
|
||||
repoSections={repoNavSections}
|
||||
hasRepos={repos.length > 0}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { Bot, Mic, Network } from 'lucide-react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { SettingsSidebar } from './SettingsSidebar'
|
||||
import { TooltipProvider } from '../ui/tooltip'
|
||||
import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useSettingsSetupGuideProgress: vi.fn()
|
||||
@@ -29,11 +31,15 @@ function makeSetupGuideProgress(
|
||||
}
|
||||
}
|
||||
|
||||
function renderSidebar(activeSectionId = 'orchestration'): string {
|
||||
function renderSidebar(
|
||||
activeSectionId = 'orchestration',
|
||||
settings: GlobalSettings = getDefaultSettings('/tmp')
|
||||
): string {
|
||||
return renderToStaticMarkup(
|
||||
<TooltipProvider>
|
||||
<SettingsSidebar
|
||||
activeSectionId={activeSectionId}
|
||||
settings={settings}
|
||||
generalGroups={[
|
||||
{
|
||||
id: 'capabilities',
|
||||
@@ -88,6 +94,20 @@ describe('SettingsSidebar', () => {
|
||||
mocks.useSettingsSetupGuideProgress.mockReturnValue(makeSetupGuideProgress())
|
||||
})
|
||||
|
||||
it('applies left sidebar appearance styles to the settings navigation', () => {
|
||||
const markup = renderSidebar('orchestration', {
|
||||
...getDefaultSettings('/tmp'),
|
||||
leftSidebarAppearanceMode: 'match-terminal',
|
||||
terminalColorOverrides: {
|
||||
background: '#101820',
|
||||
foreground: '#f0f4f8'
|
||||
}
|
||||
})
|
||||
|
||||
expect(markup).toContain('--worktree-sidebar:#101820')
|
||||
expect(markup).toContain('--worktree-sidebar-foreground:#f0f4f8')
|
||||
})
|
||||
|
||||
it('renders install state labels separately from static badges', () => {
|
||||
const markup = renderSidebar()
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { CSSProperties, RefObject } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ArrowLeft, Search, Server } from 'lucide-react'
|
||||
import type { RepoIcon } from '../../../../shared/repo-icon'
|
||||
import type { SettingsNavIcon, SettingsNavInstallStatus } from '@/lib/settings-navigation-types'
|
||||
import type { GitHubRepositoryIdentity } from '../../../../shared/types'
|
||||
import type { GitHubRepositoryIdentity, GlobalSettings } from '../../../../shared/types'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { RepoIconGlyph } from '../repo/repo-icon'
|
||||
@@ -13,6 +14,8 @@ import { SetupGuideProgressRing } from '../setup-guide/SetupGuideProgressRing'
|
||||
import { useSettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { resolveLeftSidebarStyleVariables } from '@/lib/left-sidebar-appearance'
|
||||
import { useSystemPrefersDark } from '../terminal-pane/use-system-prefers-dark'
|
||||
|
||||
type NavSection = {
|
||||
id: string
|
||||
@@ -37,6 +40,7 @@ type RepoNavSection = NavSection & {
|
||||
|
||||
type SettingsSidebarProps = {
|
||||
activeSectionId: string
|
||||
settings: GlobalSettings | null
|
||||
generalGroups: NavGroup[]
|
||||
repoSections: RepoNavSection[]
|
||||
hasRepos: boolean
|
||||
@@ -112,6 +116,7 @@ function SettingsSetupGuideNavRow({
|
||||
|
||||
export function SettingsSidebar({
|
||||
activeSectionId,
|
||||
settings,
|
||||
generalGroups,
|
||||
repoSections,
|
||||
hasRepos,
|
||||
@@ -122,6 +127,11 @@ export function SettingsSidebar({
|
||||
onSelectSection
|
||||
}: SettingsSidebarProps): React.JSX.Element {
|
||||
const setupGuideProgress = useSettingsSetupGuideProgress(true)
|
||||
const systemPrefersDark = useSystemPrefersDark()
|
||||
const leftSidebarStyle = useMemo(
|
||||
() => resolveLeftSidebarStyleVariables(settings, systemPrefersDark),
|
||||
[settings, systemPrefersDark]
|
||||
) as CSSProperties | undefined
|
||||
const setupActive = activeSectionId === 'setup-guide'
|
||||
// Why: "Hide from sidebar" only hides the top-left app sidebar prompt;
|
||||
// Settings should remain a stable place to reopen the checklist.
|
||||
@@ -159,7 +169,10 @@ export function SettingsSidebar({
|
||||
)
|
||||
|
||||
return (
|
||||
<aside className="flex w-[280px] shrink-0 flex-col border-r border-worktree-sidebar-border bg-worktree-sidebar">
|
||||
<aside
|
||||
className="flex w-[280px] shrink-0 flex-col border-r border-worktree-sidebar-border bg-worktree-sidebar"
|
||||
style={leftSidebarStyle}
|
||||
>
|
||||
<div className="border-b border-worktree-sidebar-border px-3 py-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SettingsSearchEntry } from './settings-search'
|
||||
import { getTerminalAppearanceSearchEntries } from './terminal-search'
|
||||
import { getLeftSidebarAppearanceEntry, getSidebarEntries } from './appearance-sidebar-search'
|
||||
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { translateSearchKeyword } from './settings-search-keywords'
|
||||
@@ -153,71 +154,7 @@ export const getStatusBarEntries = createLocalizedCatalog((): SettingsSearchEntr
|
||||
}))
|
||||
)
|
||||
|
||||
export const getSidebarEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [
|
||||
{
|
||||
title: translate('auto.components.settings.appearance.search.155a1e7438', 'Show Tasks Button'),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.9a248333c7',
|
||||
'Show the Tasks button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.0d5a74b606', 'tasks'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.2ee4810f38', 'github'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6b846424cc', 'linear')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.appearance.search.caa27e1a8e',
|
||||
'Show Automations Button'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.ae13a0d340',
|
||||
'Show the Automations button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.b186f3cefb',
|
||||
'automations'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.58f4e22fa2',
|
||||
'automation'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.4c920ab2d1',
|
||||
'schedule'
|
||||
),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.appearance.search.1de96ec8a6',
|
||||
'Show Orca Mobile Button'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.682293cadf',
|
||||
'Show the Orca Mobile button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.74618577c7', 'mobile'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5e5b8878bf', 'phone'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.839fb1e3ed', 'toolbox')
|
||||
]
|
||||
}
|
||||
])
|
||||
export { getLeftSidebarAppearanceEntry, getSidebarEntries }
|
||||
|
||||
export const getAppIconEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { SettingsSearchEntry } from './settings-search'
|
||||
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { translateSearchKeyword } from './settings-search-keywords'
|
||||
|
||||
export const getLeftSidebarAppearanceEntry = createLocalizedCatalog(
|
||||
(): SettingsSearchEntry => ({
|
||||
title: translate(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.title',
|
||||
'Left Sidebar Appearance'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.description',
|
||||
'Make the left sidebar match your terminal, stay default, or use a tint.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.project',
|
||||
'project'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.terminal',
|
||||
'terminal'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.background',
|
||||
'background'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.tint',
|
||||
'tint'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.leftSidebarAppearance.contrast',
|
||||
'contrast'
|
||||
)
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
export const getSidebarEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [
|
||||
{
|
||||
title: translate('auto.components.settings.appearance.search.155a1e7438', 'Show Tasks Button'),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.9a248333c7',
|
||||
'Show the Tasks button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.0d5a74b606', 'tasks'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.2ee4810f38', 'github'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6b846424cc', 'linear')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.appearance.search.caa27e1a8e',
|
||||
'Show Automations Button'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.ae13a0d340',
|
||||
'Show the Automations button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.b186f3cefb',
|
||||
'automations'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.58f4e22fa2',
|
||||
'automation'
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.appearance.search.4c920ab2d1',
|
||||
'schedule'
|
||||
),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.appearance.search.1de96ec8a6',
|
||||
'Show Orca Mobile Button'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.appearance.search.682293cadf',
|
||||
'Show the Orca Mobile button at the top of the left sidebar.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.74618577c7', 'mobile'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5e5b8878bf', 'phone'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.6cf5f54ce1', 'button'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.648eeada79', 'hide'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.ac79fe4a04', 'show'),
|
||||
...translateSearchKeyword('auto.components.settings.appearance.search.839fb1e3ed', 'toolbox')
|
||||
]
|
||||
},
|
||||
getLeftSidebarAppearanceEntry()
|
||||
])
|
||||
@@ -35,6 +35,7 @@ import { makeWorkspaceStatusId } from '../../../../shared/workspace-statuses'
|
||||
import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour'
|
||||
|
||||
type WorkspaceKanbanDrawerProps = {
|
||||
leftSidebarStyle?: React.CSSProperties
|
||||
open: boolean
|
||||
preserveOpenForMenu: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -42,6 +43,7 @@ type WorkspaceKanbanDrawerProps = {
|
||||
}
|
||||
|
||||
export default function WorkspaceKanbanDrawer({
|
||||
leftSidebarStyle,
|
||||
open,
|
||||
preserveOpenForMenu,
|
||||
onOpenChange,
|
||||
@@ -481,6 +483,7 @@ export default function WorkspaceKanbanDrawer({
|
||||
overlayStyle={{ top: 36, left: drawerLeftCss, pointerEvents: 'none' }}
|
||||
style={
|
||||
{
|
||||
...leftSidebarStyle,
|
||||
// Why: the board is a companion to the workspace sidebar, so it
|
||||
// expands from the sidebar edge instead of covering the sidebar.
|
||||
left: drawerLeftCss,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { useSidebarResize } from '@/hooks/useSidebarResize'
|
||||
@@ -14,6 +14,8 @@ import { cn } from '@/lib/utils'
|
||||
import { FolderPlus, Loader2 } from 'lucide-react'
|
||||
import { useSidebarProjectDrop } from './useSidebarProjectDrop'
|
||||
import { useWorkspaceBoardPanel } from './useWorkspaceBoardPanel'
|
||||
import { useSystemPrefersDark } from '../terminal-pane/use-system-prefers-dark'
|
||||
import { resolveLeftSidebarStyleVariables } from '@/lib/left-sidebar-appearance'
|
||||
|
||||
const WorktreeMetaDialog = React.lazy(() => import('./WorktreeMetaDialog'))
|
||||
const NonGitFolderDialog = React.lazy(() => import('./NonGitFolderDialog'))
|
||||
@@ -43,9 +45,15 @@ function Sidebar({
|
||||
const sidebarWidth = useAppStore((s) => s.sidebarWidth)
|
||||
const setSidebarWidth = useAppStore((s) => s.setSidebarWidth)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const fetchAllWorktrees = useAppStore((s) => s.fetchAllWorktrees)
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
const { nativeDropTarget, dropHandlers, affordance } = useSidebarProjectDrop()
|
||||
const systemPrefersDark = useSystemPrefersDark()
|
||||
const leftSidebarStyle = useMemo(
|
||||
() => resolveLeftSidebarStyleVariables(settings, systemPrefersDark),
|
||||
[settings, systemPrefersDark]
|
||||
) as React.CSSProperties | undefined
|
||||
const [shouldMountAddRepoDialog, setShouldMountAddRepoDialog] = React.useState(false)
|
||||
const unmountAddRepoDialogTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const {
|
||||
@@ -116,6 +124,7 @@ function Sidebar({
|
||||
ref={containerRef}
|
||||
data-native-file-drop-target={sidebarOpen ? nativeDropTarget : undefined}
|
||||
className="relative min-h-0 flex-shrink-0 bg-worktree-sidebar flex flex-col overflow-hidden scrollbar-sleek-parent"
|
||||
style={leftSidebarStyle}
|
||||
{...dropHandlers}
|
||||
>
|
||||
{sidebarOpen && (
|
||||
@@ -183,6 +192,7 @@ function Sidebar({
|
||||
</React.Suspense>
|
||||
{sidebarOpen ? (
|
||||
<WorkspaceKanbanDrawer
|
||||
leftSidebarStyle={leftSidebarStyle}
|
||||
open={workspaceBoardOpen}
|
||||
preserveOpenForMenu={workspaceBoardMenuOpen}
|
||||
onOpenChange={handleWorkspaceBoardOpenChange}
|
||||
|
||||
@@ -4177,7 +4177,20 @@
|
||||
"7d26ccabe8": "Dark",
|
||||
"fb0e0b4453": "System",
|
||||
"932ff1fbff": "Theme",
|
||||
"0f28e7b30c": "Choose how Orca looks in the app window."
|
||||
"0f28e7b30c": "Choose how Orca looks in the app window.",
|
||||
"leftSidebarAppearance": {
|
||||
"title": "Left Sidebar Appearance",
|
||||
"description": "Make the left sidebar match your terminal, stay default, or use a tint.",
|
||||
"rowDescription": "Make the left sidebar match your terminal, stay default, or use a tint.",
|
||||
"default": "Default",
|
||||
"matchTerminal": "Match Terminal",
|
||||
"softContrast": "Soft Contrast",
|
||||
"tinted": "Tinted",
|
||||
"tintColor": "Sidebar Tint",
|
||||
"tintColorDescription": "The color mixed into the left sidebar surface.",
|
||||
"tintOpacity": "Tint Strength",
|
||||
"tintOpacityDescription": "Controls how strongly the tint is mixed into the sidebar."
|
||||
}
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
@@ -6215,6 +6228,10 @@
|
||||
"locale": "locale",
|
||||
"i18n": "i18n",
|
||||
"translation": "translation"
|
||||
},
|
||||
"leftSidebarAppearance": {
|
||||
"title": "Left Sidebar Appearance",
|
||||
"description": "Make the left sidebar match your terminal, stay default, or use a tint."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4177,7 +4177,20 @@
|
||||
"7d26ccabe8": "Oscuro",
|
||||
"fb0e0b4453": "Sistema",
|
||||
"932ff1fbff": "Tema",
|
||||
"0f28e7b30c": "Elige cómo se ve Orca en la ventana de la aplicación."
|
||||
"0f28e7b30c": "Elige cómo se ve Orca en la ventana de la aplicación.",
|
||||
"leftSidebarAppearance": {
|
||||
"title": "Apariencia de la barra lateral izquierda",
|
||||
"description": "Haz que la barra lateral izquierda coincida con tu terminal, conserve el valor predeterminado o use un tinte.",
|
||||
"rowDescription": "Haz que la barra lateral izquierda coincida con tu terminal, conserve el valor predeterminado o use un tinte.",
|
||||
"default": "Predeterminado",
|
||||
"matchTerminal": "Coincidir con terminal",
|
||||
"softContrast": "Contraste suave",
|
||||
"tinted": "Con tinte",
|
||||
"tintColor": "Tinte de la barra lateral",
|
||||
"tintColorDescription": "El color que se mezcla en la superficie de la barra lateral izquierda.",
|
||||
"tintOpacity": "Intensidad del tinte",
|
||||
"tintOpacityDescription": "Controla con qué fuerza se mezcla el tinte en la barra lateral."
|
||||
}
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
@@ -6179,6 +6192,10 @@
|
||||
"locale": "lugar",
|
||||
"i18n": "i18n",
|
||||
"translation": "traducción"
|
||||
},
|
||||
"leftSidebarAppearance": {
|
||||
"title": "Apariencia de la barra lateral izquierda",
|
||||
"description": "Haz que la barra lateral izquierda coincida con tu terminal, conserve el valor predeterminado o use un tinte."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4162,7 +4162,20 @@
|
||||
"7d26ccabe8": "ダーク",
|
||||
"fb0e0b4453": "システム",
|
||||
"932ff1fbff": "テーマ",
|
||||
"0f28e7b30c": "アプリ ウィンドウで Orca がどのように見えるかを選択します。"
|
||||
"0f28e7b30c": "アプリ ウィンドウで Orca がどのように見えるかを選択します。",
|
||||
"leftSidebarAppearance": {
|
||||
"title": "左サイドバーの外観",
|
||||
"description": "左サイドバーをターミナルに合わせるか、既定のままにするか、色合いを加えます。",
|
||||
"rowDescription": "左サイドバーをターミナルに合わせるか、既定のままにするか、色合いを加えます。",
|
||||
"default": "デフォルト",
|
||||
"matchTerminal": "ターミナルに合わせる",
|
||||
"softContrast": "ソフトコントラスト",
|
||||
"tinted": "色合い",
|
||||
"tintColor": "サイドバーの色合い",
|
||||
"tintColorDescription": "左サイドバーの表面に混ぜる色です。",
|
||||
"tintOpacity": "色合いの強さ",
|
||||
"tintOpacityDescription": "サイドバーに色合いをどの程度強く混ぜるかを調整します。"
|
||||
}
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
@@ -6200,6 +6213,10 @@
|
||||
"locale": "ロケール",
|
||||
"i18n": "i18n",
|
||||
"translation": "翻訳"
|
||||
},
|
||||
"leftSidebarAppearance": {
|
||||
"title": "左サイドバーの外観",
|
||||
"description": "左サイドバーをターミナルに合わせるか、既定のままにするか、色合いを加えます。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4162,7 +4162,20 @@
|
||||
"7d26ccabe8": "다크",
|
||||
"fb0e0b4453": "체계",
|
||||
"932ff1fbff": "주제",
|
||||
"0f28e7b30c": "Orca가 앱 창에 표시되는 방식을 선택합니다."
|
||||
"0f28e7b30c": "Orca가 앱 창에 표시되는 방식을 선택합니다.",
|
||||
"leftSidebarAppearance": {
|
||||
"title": "왼쪽 사이드바 모양",
|
||||
"description": "왼쪽 사이드바를 터미널에 맞추거나 기본값을 유지하거나 은은한 색조를 적용합니다.",
|
||||
"rowDescription": "왼쪽 사이드바를 터미널에 맞추거나 기본값을 유지하거나 은은한 색조를 적용합니다.",
|
||||
"default": "기본값",
|
||||
"matchTerminal": "터미널에 맞춤",
|
||||
"softContrast": "부드러운 대비",
|
||||
"tinted": "색조 적용",
|
||||
"tintColor": "사이드바 색조",
|
||||
"tintColorDescription": "왼쪽 사이드바 표면에 섞을 색상입니다.",
|
||||
"tintOpacity": "색조 강도",
|
||||
"tintOpacityDescription": "사이드바에 색조를 얼마나 강하게 섞을지 조절합니다."
|
||||
}
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
@@ -6164,6 +6177,10 @@
|
||||
"locale": "로케일",
|
||||
"i18n": "i18n",
|
||||
"translation": "번역"
|
||||
},
|
||||
"leftSidebarAppearance": {
|
||||
"title": "왼쪽 사이드바 모양",
|
||||
"description": "왼쪽 사이드바를 터미널에 맞추거나 기본값을 유지하거나 은은한 색조를 적용합니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4162,7 +4162,20 @@
|
||||
"7d26ccabe8": "深色",
|
||||
"fb0e0b4453": "系统",
|
||||
"932ff1fbff": "主题",
|
||||
"0f28e7b30c": "选择 Orca 在应用程序窗口中的外观。"
|
||||
"0f28e7b30c": "选择 Orca 在应用程序窗口中的外观。",
|
||||
"leftSidebarAppearance": {
|
||||
"title": "左侧边栏外观",
|
||||
"description": "让左侧边栏匹配终端、保持默认,或使用色调。",
|
||||
"rowDescription": "让左侧边栏匹配终端、保持默认,或使用色调。",
|
||||
"default": "默认",
|
||||
"matchTerminal": "匹配终端",
|
||||
"softContrast": "柔和对比",
|
||||
"tinted": "色调",
|
||||
"tintColor": "边栏色调",
|
||||
"tintColorDescription": "混入左侧边栏表面的颜色。",
|
||||
"tintOpacity": "色调强度",
|
||||
"tintOpacityDescription": "控制色调混入边栏的强度。"
|
||||
}
|
||||
},
|
||||
"AutoRenameBranchFromWorkSetting": {
|
||||
"1626524572": "Nautilus",
|
||||
@@ -6164,6 +6177,10 @@
|
||||
"locale": "区域设置",
|
||||
"i18n": "国际化",
|
||||
"translation": "翻译"
|
||||
},
|
||||
"leftSidebarAppearance": {
|
||||
"title": "左侧边栏外观",
|
||||
"description": "让左侧边栏匹配终端、保持默认,或使用色调。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { tmpdir } from 'node:os'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import { resolveLeftSidebarStyleVariables } from './left-sidebar-appearance'
|
||||
|
||||
function settings(overrides = {}) {
|
||||
return {
|
||||
...getDefaultSettings(tmpdir()),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('resolveLeftSidebarStyleVariables', () => {
|
||||
it('leaves the default sidebar token surface untouched', () => {
|
||||
expect(resolveLeftSidebarStyleVariables(settings(), true)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('matches terminal background, foreground, and scoped text tokens', () => {
|
||||
const vars = resolveLeftSidebarStyleVariables(
|
||||
settings({
|
||||
leftSidebarAppearanceMode: 'match-terminal',
|
||||
terminalColorOverrides: {
|
||||
background: '#101820',
|
||||
foreground: '#f0f4f8'
|
||||
}
|
||||
}),
|
||||
true
|
||||
)
|
||||
|
||||
expect(vars).toMatchObject({
|
||||
'--worktree-sidebar': '#101820',
|
||||
'--worktree-sidebar-foreground': '#f0f4f8',
|
||||
'--sidebar': '#101820',
|
||||
'--sidebar-foreground': '#f0f4f8',
|
||||
'--background': '#101820',
|
||||
'--foreground': '#f0f4f8'
|
||||
})
|
||||
expect(vars?.['--worktree-sidebar-accent']).toContain('#f0f4f8 9%')
|
||||
expect(vars?.['--sidebar-accent']).toBe(vars?.['--worktree-sidebar-accent'])
|
||||
})
|
||||
|
||||
it('honors terminal background opacity for matched terminal surfaces', () => {
|
||||
const vars = resolveLeftSidebarStyleVariables(
|
||||
settings({
|
||||
leftSidebarAppearanceMode: 'match-terminal',
|
||||
terminalColorOverrides: { background: '#123456' },
|
||||
terminalBackgroundOpacity: 0.5
|
||||
}),
|
||||
true
|
||||
)
|
||||
|
||||
expect(vars?.['--worktree-sidebar']).toBe('rgba(18, 52, 86, 0.5)')
|
||||
})
|
||||
|
||||
it('builds a tinted surface from normalized tint settings', () => {
|
||||
const vars = resolveLeftSidebarStyleVariables(
|
||||
settings({
|
||||
leftSidebarAppearanceMode: 'tinted',
|
||||
leftSidebarTintColor: '336699',
|
||||
leftSidebarTintOpacity: 0.125
|
||||
}),
|
||||
true
|
||||
)
|
||||
|
||||
expect(vars?.['--worktree-sidebar']).toBe(
|
||||
'color-mix(in srgb, #336699 12.5%, var(--background))'
|
||||
)
|
||||
expect(vars?.['--sidebar']).toBe(vars?.['--worktree-sidebar'])
|
||||
expect(vars?.['--worktree-sidebar-foreground']).toBe('var(--foreground)')
|
||||
})
|
||||
|
||||
it('caps tinted opacity so arbitrary tint colors stay subtle', () => {
|
||||
const vars = resolveLeftSidebarStyleVariables(
|
||||
settings({
|
||||
leftSidebarAppearanceMode: 'tinted',
|
||||
leftSidebarTintColor: '#000000',
|
||||
leftSidebarTintOpacity: 1
|
||||
}),
|
||||
true
|
||||
)
|
||||
|
||||
expect(vars?.['--worktree-sidebar']).toBe('color-mix(in srgb, #000000 35%, var(--background))')
|
||||
})
|
||||
|
||||
it('uses the soft contrast token recipe without overriding app text tokens', () => {
|
||||
const vars = resolveLeftSidebarStyleVariables(
|
||||
settings({ leftSidebarAppearanceMode: 'soft-contrast' }),
|
||||
true
|
||||
)
|
||||
|
||||
expect(vars?.['--worktree-sidebar']).toBe(
|
||||
'color-mix(in srgb, var(--background) 96%, var(--foreground) 4%)'
|
||||
)
|
||||
expect(vars?.['--sidebar']).toBe(vars?.['--worktree-sidebar'])
|
||||
expect(vars?.['--foreground']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
import { HEX_COLOR_RE } from '../../../shared/color-validation'
|
||||
import {
|
||||
normalizeLeftSidebarTintColor,
|
||||
normalizeLeftSidebarTintOpacity
|
||||
} from '../../../shared/left-sidebar-appearance'
|
||||
import { resolveEffectiveTerminalAppearance } from './terminal-theme'
|
||||
|
||||
type LeftSidebarAppearanceSettings = Pick<
|
||||
GlobalSettings,
|
||||
| 'leftSidebarAppearanceMode'
|
||||
| 'leftSidebarTintColor'
|
||||
| 'leftSidebarTintOpacity'
|
||||
| 'theme'
|
||||
| 'terminalThemeDark'
|
||||
| 'terminalDividerColorDark'
|
||||
| 'terminalUseSeparateLightTheme'
|
||||
| 'terminalThemeLight'
|
||||
| 'terminalCustomThemes'
|
||||
| 'terminalDividerColorLight'
|
||||
| 'terminalColorOverrides'
|
||||
| 'terminalBackgroundOpacity'
|
||||
>
|
||||
|
||||
export type LeftSidebarStyleVariables = Record<string, string>
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const normalized = normalizeLeftSidebarTintColor(hex)
|
||||
let clean = normalized.replace('#', '')
|
||||
if (clean.length === 3) {
|
||||
clean = clean
|
||||
.split('')
|
||||
.map((part) => part + part)
|
||||
.join('')
|
||||
}
|
||||
const r = parseInt(clean.slice(0, 2), 16)
|
||||
const g = parseInt(clean.slice(2, 4), 16)
|
||||
const b = parseInt(clean.slice(4, 6), 16)
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||
}
|
||||
|
||||
function applyAlpha(color: string, alpha: number | undefined): string {
|
||||
if (alpha === undefined || alpha >= 1 || !HEX_COLOR_RE.test(color.trim())) {
|
||||
return color
|
||||
}
|
||||
return hexToRgba(color, Math.min(1, Math.max(0, alpha)))
|
||||
}
|
||||
|
||||
function buildSurfaceVariables(args: {
|
||||
background: string
|
||||
foreground: string
|
||||
overrideTextTokens?: boolean
|
||||
}): LeftSidebarStyleVariables {
|
||||
const { background, foreground, overrideTextTokens = false } = args
|
||||
const accent = `color-mix(in srgb, ${foreground} 9%, ${background})`
|
||||
const border = `color-mix(in srgb, ${foreground} 14%, ${background})`
|
||||
const ring = `color-mix(in srgb, ${foreground} 44%, ${background})`
|
||||
const vars: LeftSidebarStyleVariables = {
|
||||
'--worktree-sidebar': background,
|
||||
'--worktree-sidebar-foreground': foreground,
|
||||
'--worktree-sidebar-accent': accent,
|
||||
'--worktree-sidebar-accent-foreground': foreground,
|
||||
'--worktree-sidebar-border': border,
|
||||
'--worktree-sidebar-ring': ring,
|
||||
// Why: older worktree-sidebar descendants still consume the shadcn sidebar
|
||||
// token family; mirror it inside this scoped root so every left-sidebar
|
||||
// surface follows the selected appearance.
|
||||
'--sidebar': background,
|
||||
'--sidebar-foreground': foreground,
|
||||
'--sidebar-accent': accent,
|
||||
'--sidebar-accent-foreground': foreground,
|
||||
'--sidebar-border': border,
|
||||
'--sidebar-ring': ring
|
||||
}
|
||||
if (overrideTextTokens) {
|
||||
vars['--background'] = background
|
||||
vars['--foreground'] = foreground
|
||||
vars['--card'] = `color-mix(in srgb, ${foreground} 4%, ${background})`
|
||||
vars['--card-foreground'] = foreground
|
||||
vars['--accent'] = `color-mix(in srgb, ${foreground} 9%, ${background})`
|
||||
vars['--accent-foreground'] = foreground
|
||||
vars['--muted'] = `color-mix(in srgb, ${foreground} 7%, ${background})`
|
||||
vars['--muted-foreground'] = `color-mix(in srgb, ${foreground} 62%, ${background})`
|
||||
vars['--border'] = `color-mix(in srgb, ${foreground} 14%, ${background})`
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
function resolveTerminalSurfaceVariables(
|
||||
settings: LeftSidebarAppearanceSettings,
|
||||
systemPrefersDark: boolean
|
||||
): LeftSidebarStyleVariables {
|
||||
const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark)
|
||||
const background = applyAlpha(
|
||||
settings.terminalColorOverrides?.background ?? appearance.theme?.background ?? '#000000',
|
||||
settings.terminalBackgroundOpacity
|
||||
)
|
||||
const foreground =
|
||||
settings.terminalColorOverrides?.foreground ?? appearance.theme?.foreground ?? '#fafafa'
|
||||
return buildSurfaceVariables({ background, foreground, overrideTextTokens: true })
|
||||
}
|
||||
|
||||
function resolveTintedSurfaceVariables(
|
||||
settings: LeftSidebarAppearanceSettings
|
||||
): LeftSidebarStyleVariables {
|
||||
const tintColor = normalizeLeftSidebarTintColor(settings.leftSidebarTintColor)
|
||||
const tintOpacity = normalizeLeftSidebarTintOpacity(settings.leftSidebarTintOpacity)
|
||||
const tintPercent = Number((tintOpacity * 100).toFixed(2))
|
||||
const background = `color-mix(in srgb, ${tintColor} ${tintPercent}%, var(--background))`
|
||||
return buildSurfaceVariables({ background, foreground: 'var(--foreground)' })
|
||||
}
|
||||
|
||||
function resolveSoftContrastSurfaceVariables(): LeftSidebarStyleVariables {
|
||||
const background = 'color-mix(in srgb, var(--background) 96%, var(--foreground) 4%)'
|
||||
return buildSurfaceVariables({ background, foreground: 'var(--foreground)' })
|
||||
}
|
||||
|
||||
export function resolveLeftSidebarStyleVariables(
|
||||
settings: LeftSidebarAppearanceSettings | null | undefined,
|
||||
systemPrefersDark: boolean
|
||||
): LeftSidebarStyleVariables | undefined {
|
||||
if (!settings) {
|
||||
return undefined
|
||||
}
|
||||
switch (settings.leftSidebarAppearanceMode) {
|
||||
case 'default':
|
||||
return undefined
|
||||
case 'match-terminal':
|
||||
return resolveTerminalSurfaceVariables(settings, systemPrefersDark)
|
||||
case 'tinted':
|
||||
return resolveTintedSurfaceVariables(settings)
|
||||
case 'soft-contrast':
|
||||
return resolveSoftContrastSurfaceVariables()
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ import { DEFAULT_BROWSER_PAGE_ZOOM_LEVEL } from './browser-page-zoom'
|
||||
import { DEFAULT_DISABLED_TUI_AGENTS } from './tui-agent-selection'
|
||||
import { DEFAULT_TUI_AGENT_ARGS, DEFAULT_TUI_AGENT_ENV } from './tui-agent-launch-defaults'
|
||||
import { UI_LANGUAGE_SYSTEM } from './ui-language'
|
||||
import {
|
||||
DEFAULT_LEFT_SIDEBAR_TINT_COLOR,
|
||||
DEFAULT_LEFT_SIDEBAR_TINT_OPACITY
|
||||
} from './left-sidebar-appearance'
|
||||
|
||||
export { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults'
|
||||
export {
|
||||
@@ -181,6 +185,9 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
||||
branchPrefixCustom: '',
|
||||
enableGitHubAttribution: false,
|
||||
theme: 'system',
|
||||
leftSidebarAppearanceMode: 'default',
|
||||
leftSidebarTintColor: DEFAULT_LEFT_SIDEBAR_TINT_COLOR,
|
||||
leftSidebarTintOpacity: DEFAULT_LEFT_SIDEBAR_TINT_OPACITY,
|
||||
uiLanguage: UI_LANGUAGE_SYSTEM,
|
||||
appIcon: DEFAULT_APP_ICON_ID,
|
||||
appFontFamily: DEFAULT_APP_FONT_FAMILY,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { HEX_COLOR_RE } from './color-validation'
|
||||
import type { LeftSidebarAppearanceMode } from './types'
|
||||
|
||||
export const LEFT_SIDEBAR_APPEARANCE_MODES = [
|
||||
'default',
|
||||
'match-terminal',
|
||||
'tinted',
|
||||
'soft-contrast'
|
||||
] as const
|
||||
|
||||
export const DEFAULT_LEFT_SIDEBAR_TINT_COLOR = '#18181b'
|
||||
export const DEFAULT_LEFT_SIDEBAR_TINT_OPACITY = 0.08
|
||||
export const MAX_LEFT_SIDEBAR_TINT_OPACITY = 0.35
|
||||
|
||||
export function normalizeLeftSidebarAppearanceMode(value: unknown): LeftSidebarAppearanceMode {
|
||||
return LEFT_SIDEBAR_APPEARANCE_MODES.includes(value as LeftSidebarAppearanceMode)
|
||||
? (value as LeftSidebarAppearanceMode)
|
||||
: 'default'
|
||||
}
|
||||
|
||||
export function normalizeLeftSidebarTintColor(value: unknown): string {
|
||||
if (typeof value !== 'string') {
|
||||
return DEFAULT_LEFT_SIDEBAR_TINT_COLOR
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed || !HEX_COLOR_RE.test(trimmed)) {
|
||||
return DEFAULT_LEFT_SIDEBAR_TINT_COLOR
|
||||
}
|
||||
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`
|
||||
}
|
||||
|
||||
export function normalizeLeftSidebarTintOpacity(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return DEFAULT_LEFT_SIDEBAR_TINT_OPACITY
|
||||
}
|
||||
return Math.min(MAX_LEFT_SIDEBAR_TINT_OPACITY, Math.max(0, value))
|
||||
}
|
||||
@@ -19,7 +19,7 @@ describe('hasNativeFileDragTypes', () => {
|
||||
})
|
||||
|
||||
describe('resolveNativeFileDropPath', () => {
|
||||
it('routes drops on the project sidebar to the add-project surface', () => {
|
||||
it('routes drops on the left sidebar to the add-project surface', () => {
|
||||
expect(
|
||||
resolveNativeFileDropPath([{ nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.projectSidebar }])
|
||||
).toEqual({ target: NATIVE_FILE_DROP_TARGET.projectSidebar })
|
||||
|
||||
@@ -2042,6 +2042,8 @@ export type OpenInApplication = {
|
||||
|
||||
export type SourceControlViewMode = 'list' | 'tree'
|
||||
|
||||
export type LeftSidebarAppearanceMode = 'default' | 'match-terminal' | 'tinted' | 'soft-contrast'
|
||||
|
||||
export type FloatingTerminalCwdRequest = {
|
||||
path?: string
|
||||
requireTrusted?: boolean
|
||||
@@ -2066,6 +2068,10 @@ export type GlobalSettings = {
|
||||
branchPrefixCustom: string
|
||||
enableGitHubAttribution: boolean
|
||||
theme: 'system' | 'dark' | 'light'
|
||||
/** Controls the left sidebar surface without changing terminal brightness. */
|
||||
leftSidebarAppearanceMode: LeftSidebarAppearanceMode
|
||||
leftSidebarTintColor?: string
|
||||
leftSidebarTintOpacity?: number
|
||||
uiLanguage: UiLanguage
|
||||
appIcon: AppIconId
|
||||
appFontFamily: string
|
||||
|
||||
Reference in New Issue
Block a user