Skip to content

Commit f1e2eef

Browse files
feat(ui): add classic and modern interface styles (#915)
1 parent 63dd213 commit f1e2eef

17 files changed

Lines changed: 383 additions & 54 deletions

File tree

apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@proma/electron",
3-
"version": "0.13.3",
3+
"version": "0.13.4",
44
"description": "Proma next gen ai software with general agents - Electron App",
55
"main": "dist/main.cjs",
66
"author": {

apps/electron/src/main/ipc.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,8 +1445,12 @@ export function registerIpcHandlers(): void {
14451445
}
14461446

14471447
// 主题相关设置变化时,广播给所有窗口(跨窗口同步,如 Quick Task 面板)
1448-
if (updates.themeMode !== undefined || updates.themeStyle !== undefined) {
1449-
const payload = { themeMode: result.themeMode, themeStyle: result.themeStyle }
1448+
if (updates.themeMode !== undefined || updates.themeStyle !== undefined || updates.interfaceVariant !== undefined) {
1449+
const payload = {
1450+
themeMode: result.themeMode,
1451+
themeStyle: result.themeStyle,
1452+
interfaceVariant: result.interfaceVariant,
1453+
}
14501454
BrowserWindow.getAllWindows().forEach((win) => {
14511455
// 跳过发起者窗口,避免重复应用
14521456
if (win.webContents.id !== event.sender.id) {

apps/electron/src/main/lib/settings-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
99
import { getSettingsPath } from './config-paths'
10-
import { DEFAULT_THEME_MODE } from '../../types'
10+
import { DEFAULT_INTERFACE_VARIANT, DEFAULT_THEME_MODE } from '../../types'
1111
import type { AppSettings } from '../../types'
1212

1313
/**
@@ -21,6 +21,7 @@ export function getSettings(): AppSettings {
2121
if (!existsSync(filePath)) {
2222
return {
2323
themeMode: DEFAULT_THEME_MODE,
24+
interfaceVariant: DEFAULT_INTERFACE_VARIANT,
2425
onboardingCompleted: false,
2526
environmentCheckSkipped: false,
2627
notificationsEnabled: true,
@@ -35,6 +36,7 @@ export function getSettings(): AppSettings {
3536
return {
3637
...data,
3738
themeMode: data.themeMode || DEFAULT_THEME_MODE,
39+
interfaceVariant: data.interfaceVariant || DEFAULT_INTERFACE_VARIANT,
3840
onboardingCompleted: data.onboardingCompleted ?? false,
3941
environmentCheckSkipped: data.environmentCheckSkipped ?? false,
4042
notificationsEnabled: data.notificationsEnabled ?? true,
@@ -45,6 +47,7 @@ export function getSettings(): AppSettings {
4547
console.error('[设置] 读取失败:', error)
4648
return {
4749
themeMode: DEFAULT_THEME_MODE,
50+
interfaceVariant: DEFAULT_INTERFACE_VARIANT,
4851
onboardingCompleted: false,
4952
environmentCheckSkipped: false,
5053
notificationsEnabled: true,

apps/electron/src/preload/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export interface ElectronAPI {
328328
onSystemThemeChanged: (callback: (isDark: boolean) => void) => () => void
329329

330330
/** 订阅用户手动切换主题事件(跨窗口同步,返回清理函数) */
331-
onThemeSettingsChanged: (callback: (payload: { themeMode: string; themeStyle: string }) => void) => () => void
331+
onThemeSettingsChanged: (callback: (payload: { themeMode: string; themeStyle: string; interfaceVariant?: string }) => void) => () => void
332332

333333
// ===== Scratch Pad =====
334334

@@ -1295,8 +1295,8 @@ const electronAPI: ElectronAPI = {
12951295
return () => { ipcRenderer.removeListener(SETTINGS_IPC_CHANNELS.ON_SYSTEM_THEME_CHANGED, listener) }
12961296
},
12971297

1298-
onThemeSettingsChanged: (callback: (payload: { themeMode: string; themeStyle: string }) => void) => {
1299-
const listener = (_: unknown, payload: { themeMode: string; themeStyle: string }): void => callback(payload)
1298+
onThemeSettingsChanged: (callback: (payload: { themeMode: string; themeStyle: string; interfaceVariant?: string }) => void) => {
1299+
const listener = (_: unknown, payload: { themeMode: string; themeStyle: string; interfaceVariant?: string }): void => callback(payload)
13001300
ipcRenderer.on(SETTINGS_IPC_CHANNELS.ON_THEME_SETTINGS_CHANGED, listener)
13011301
return () => { ipcRenderer.removeListener(SETTINGS_IPC_CHANNELS.ON_THEME_SETTINGS_CHANGED, listener) }
13021302
},

apps/electron/src/renderer/atoms/theme.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
*/
1212

1313
import { atom } from 'jotai'
14-
import { THEME_STYLES, type ThemeMode, type ThemeStyle } from '../../types'
14+
import { DEFAULT_INTERFACE_VARIANT, THEME_STYLES, type InterfaceVariant, type ThemeMode, type ThemeStyle } from '../../types'
1515

1616
/** localStorage 缓存键 */
1717
const THEME_CACHE_KEY = 'proma-theme-mode'
1818
const THEME_STYLE_CACHE_KEY = 'proma-theme-style'
19+
const INTERFACE_VARIANT_CACHE_KEY = 'proma-interface-variant'
1920

2021
/**
2122
* 从 localStorage 读取缓存的主题模式
@@ -47,6 +48,21 @@ function getCachedThemeStyle(): ThemeStyle {
4748
return 'default'
4849
}
4950

51+
/**
52+
* 从 localStorage 读取缓存的界面风格
53+
*/
54+
function getCachedInterfaceVariant(): InterfaceVariant {
55+
try {
56+
const cached = localStorage.getItem(INTERFACE_VARIANT_CACHE_KEY)
57+
if (cached === 'classic' || cached === 'modern') {
58+
return cached
59+
}
60+
} catch {
61+
// localStorage 不可用时忽略
62+
}
63+
return DEFAULT_INTERFACE_VARIANT
64+
}
65+
5066
/**
5167
* 缓存主题模式到 localStorage
5268
*/
@@ -69,12 +85,26 @@ function cacheThemeStyle(style: ThemeStyle): void {
6985
}
7086
}
7187

88+
/**
89+
* 缓存界面风格到 localStorage
90+
*/
91+
function cacheInterfaceVariant(variant: InterfaceVariant): void {
92+
try {
93+
localStorage.setItem(INTERFACE_VARIANT_CACHE_KEY, variant)
94+
} catch {
95+
// localStorage 不可用时忽略
96+
}
97+
}
98+
7299
/** 用户选择的主题模式 */
73100
export const themeModeAtom = atom<ThemeMode>(getCachedThemeMode())
74101

75102
/** 用户选择的特殊风格 */
76103
export const themeStyleAtom = atom<ThemeStyle>(getCachedThemeStyle())
77104

105+
/** 用户选择的界面风格 */
106+
export const interfaceVariantAtom = atom<InterfaceVariant>(getCachedInterfaceVariant())
107+
78108
/** 系统当前是否为深色模式 */
79109
export const systemIsDarkAtom = atom<boolean>(true)
80110

@@ -150,6 +180,28 @@ export function applyThemeToDOM(themeMode: ThemeMode, themeStyle: ThemeStyle = '
150180
}
151181
}
152182

183+
/**
184+
* 应用界面风格到 DOM
185+
*/
186+
export function applyInterfaceVariantToDOM(variant: InterfaceVariant = DEFAULT_INTERFACE_VARIANT): void {
187+
const html = document.documentElement
188+
const targetClass = variant === 'classic' ? 'ui-classic' : 'ui-modern'
189+
const currentClass = html.classList.contains('ui-classic')
190+
? 'ui-classic'
191+
: html.classList.contains('ui-modern')
192+
? 'ui-modern'
193+
: null
194+
195+
if (currentClass === targetClass) {
196+
return
197+
}
198+
199+
if (currentClass) {
200+
html.classList.remove(currentClass)
201+
}
202+
html.classList.add(targetClass)
203+
}
204+
153205
/**
154206
* 初始化主题系统
155207
*
@@ -160,6 +212,7 @@ export async function initializeTheme(
160212
setThemeMode: (mode: ThemeMode) => void,
161213
setSystemIsDark: (isDark: boolean) => void,
162214
setThemeStyle?: (style: ThemeStyle) => void,
215+
setInterfaceVariant?: (variant: InterfaceVariant) => void,
163216
): Promise<() => void> {
164217
// 从主进程加载持久化设置
165218
const settings = await window.electronAPI.getSettings()
@@ -172,6 +225,12 @@ export async function initializeTheme(
172225
cacheThemeStyle(settings.themeStyle)
173226
}
174227

228+
const interfaceVariant = settings.interfaceVariant || DEFAULT_INTERFACE_VARIANT
229+
if (setInterfaceVariant) {
230+
setInterfaceVariant(interfaceVariant)
231+
}
232+
cacheInterfaceVariant(interfaceVariant)
233+
175234
// 获取系统主题
176235
const isDark = await window.electronAPI.getSystemTheme()
177236
setSystemIsDark(isDark)
@@ -185,12 +244,17 @@ export async function initializeTheme(
185244
const cleanupThemeSettings = window.electronAPI.onThemeSettingsChanged((payload) => {
186245
const mode = payload.themeMode as ThemeMode
187246
const style = (payload.themeStyle || 'default') as ThemeStyle
247+
const variant = (payload.interfaceVariant || DEFAULT_INTERFACE_VARIANT) as InterfaceVariant
188248
setThemeMode(mode)
189249
cacheThemeMode(mode)
190250
if (setThemeStyle) {
191251
setThemeStyle(style)
192252
cacheThemeStyle(style)
193253
}
254+
if (setInterfaceVariant) {
255+
setInterfaceVariant(variant)
256+
cacheInterfaceVariant(variant)
257+
}
194258
})
195259

196260
return () => {
@@ -216,3 +280,11 @@ export async function updateThemeStyle(style: ThemeStyle): Promise<void> {
216280
cacheThemeStyle(style)
217281
await window.electronAPI.updateSettings({ themeStyle: style })
218282
}
283+
284+
/**
285+
* 更新界面风格并持久化
286+
*/
287+
export async function updateInterfaceVariant(variant: InterfaceVariant): Promise<void> {
288+
cacheInterfaceVariant(variant)
289+
await window.electronAPI.updateSettings({ interfaceVariant: variant })
290+
}

apps/electron/src/renderer/components/agent/SidePanel.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
fileBrowserAutoRevealAtom,
3535
agentSelectedWorktreeAtom,
3636
} from '@/atoms/agent-atoms'
37+
import { interfaceVariantAtom } from '@/atoms/theme'
3738
import { previewFileMapAtom } from '@/atoms/preview-atoms'
3839
import { useOpenPreview } from '@/components/diff/preview-opener'
3940
import { detectIsWindows } from '@/lib/platform'
@@ -393,11 +394,14 @@ export function SidePanel({ sessionId, sessionPath, activeTab, onTabChange, widt
393394
basePathsRef.current = [sessionPath, workspaceFilesPath, ...fileAccessPathsMemo].filter(Boolean) as string[]
394395
const hasSessionAttachedItems = attachedDirs.length > 0 || attachedFiles.length > 0
395396
const hasWorkspaceAttachedItems = wsAttachedDirs.length > 0 || wsAttachedFiles.length > 0
397+
const interfaceVariant = useAtomValue(interfaceVariantAtom)
398+
const isClassic = interfaceVariant === 'classic'
396399

397400
return (
398401
<div
399402
className={cn(
400-
'relative z-0 h-full flex-shrink-0 overflow-hidden titlebar-drag-region bg-content-area rounded-2xl shadow-xl dark:shadow-md',
403+
'relative z-0 h-full flex-shrink-0 overflow-hidden titlebar-drag-region bg-content-area',
404+
isClassic && 'rounded-2xl shadow-xl dark:shadow-md',
401405
shouldAnimate && 'transition-[width] duration-300 ease-in-out',
402406
isOpen ? '' : '!w-0',
403407
)}

apps/electron/src/renderer/components/app-shell/AppShell.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { appModeAtom } from '@/atoms/app-mode'
1616
import { agentSidePanelWidthAtom, currentAgentSessionIdAtom, currentSessionSidePanelOpenAtom } from '@/atoms/agent-atoms'
1717
import { automationFormAtom } from '@/atoms/automation-atoms'
1818
import { activeViewAtom } from '@/atoms/active-view'
19+
import { interfaceVariantAtom } from '@/atoms/theme'
1920
import { WindowControls } from '@/components/WindowControls'
2021
import { detectIsWindows } from '@/lib/platform'
2122
import { cn } from '@/lib/utils'
@@ -37,6 +38,8 @@ export function AppShell({ contextValue }: AppShellProps): React.ReactElement {
3738
const currentSessionId = useAtomValue(currentAgentSessionIdAtom)
3839
const isPanelOpen = useAtomValue(currentSessionSidePanelOpenAtom)
3940
const automationForm = useAtomValue(automationFormAtom)
41+
const interfaceVariant = useAtomValue(interfaceVariantAtom)
42+
const isClassic = interfaceVariant === 'classic'
4043
// 定时任务表单打开时隐藏右侧文件面板,让中间区域扩展到全宽(表单内含自己的右栏配置)
4144
const activeView = useAtomValue(activeViewAtom)
4245
const showRightPanel = appMode === 'agent' && !!currentSessionId && !automationForm.open && activeView !== 'automations' && activeView !== 'agent-skills'
@@ -99,24 +102,41 @@ export function AppShell({ contextValue }: AppShellProps): React.ReactElement {
99102
<WindowControls />
100103

101104
<div className="shell-bg h-screen w-screen flex overflow-hidden bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-950 dark:to-zinc-900">
102-
{/* 左侧边栏:可折叠,带圆角和内边距 */}
103-
<div className="p-2 pr-0 relative z-[60] crt-sidebar">
105+
{/* 左侧边栏:可折叠 */}
106+
<div className={cn(isClassic ? 'p-2 pr-0' : '', 'relative z-[60] crt-sidebar')}>
104107
<LeftSidebar />
105108
</div>
109+
{!isClassic && (
110+
<div aria-hidden="true" className="relative z-[61] w-px flex-shrink-0 bg-border/80 dark:bg-border/70" />
111+
)}
106112

107113
{/* 中间容器:relative z-[60] 使其在 z-50 拖动区域之上 */}
108-
<div className="flex-1 min-w-0 p-2 relative z-[60]">
114+
<div className={cn('flex-1 min-w-0 relative z-[60]', isClassic && 'p-2')}>
109115
{/* 主内容区域(TabBar + TabContent) */}
110116
<MainArea />
111117
</div>
112118

113-
{/* 右侧边栏:Agent 文件面板,拖拽手柄在间距中间 */}
119+
{/* 右侧边栏:Agent 文件面板 */}
114120
{showRightPanel && (
115-
<div className={cn('relative z-[60] flex items-stretch transition-[padding] duration-300 ease-in-out crt-sidebar', isPanelOpen ? 'p-2 pl-0' : 'p-0')}>
116-
{/* 拖拽手柄 — 绝对定位,居中于主区域和右侧面板的缝隙 */}
121+
<div
122+
className={cn(
123+
'relative z-[60] flex items-stretch crt-sidebar',
124+
isClassic
125+
? 'transition-[padding] duration-300 ease-in-out'
126+
: '',
127+
isClassic && (isPanelOpen ? 'p-2 pl-0' : 'p-0')
128+
)}
129+
>
130+
{!isClassic && (
131+
<div aria-hidden="true" className="pointer-events-none absolute left-0 top-0 bottom-0 z-10 w-px bg-border/80 dark:bg-border/70" />
132+
)}
133+
{/* 拖拽手柄 */}
117134
{isPanelOpen && (
118135
<div
119-
className="absolute left-0 top-0 bottom-0 w-[8px] -translate-x-1/2 cursor-col-resize active:bg-primary/50 transition-colors z-10"
136+
className={cn(
137+
'absolute left-0 top-0 bottom-0 w-[8px] -translate-x-1/2 cursor-col-resize active:bg-primary/50 transition-colors',
138+
isClassic ? 'z-10' : 'z-20'
139+
)}
120140
onMouseDown={handleMouseDown}
121141
/>
122142
)}

0 commit comments

Comments
 (0)