cc-haha/desktop/src/components/layout/WindowControls.tsx
程序员阿江(Relakkes) 76823047f4 Add a switchable dark theme for the desktop workspace
This introduces a persisted light/dark appearance setting, maps the desktop shell onto semantic theme tokens, and reworks the highest-traffic chat/settings surfaces so the new dark mode is usable without regressing the original light theme.

The same pass tightens markdown rendering for chat replies by improving inline code, table overflow handling, and safe external-link behavior so dark-mode content stays legible in real conversations.

Constraint: Preserve the existing light theme while adding a user-selectable dark theme in Settings > General
Constraint: Avoid introducing new dependencies for styling or markdown handling
Rejected: Replacing the light palette with a single dual-purpose palette | would risk broad visual regressions across the existing desktop UI
Rejected: Implementing dark mode only for shell chrome | leaves chat markdown, diffs, and permission flows visually broken
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: New desktop UI should use semantic theme variables instead of hard-coded color literals so both themes stay aligned
Tested: bun run lint; bun run test; bun run build; browser review of theme switching, provider/permission states, and chat surfaces
Not-tested: Prototype-style pages with remaining hard-coded colors (scheduled/session control mock surfaces) were not fully normalized in this change
2026-04-20 10:23:19 +08:00

90 lines
3.3 KiB
TypeScript

import { useState, useEffect } from 'react'
const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window)
const isWindows = typeof navigator !== 'undefined' && /Win/.test(navigator.platform)
/** Whether to render custom window controls (Windows + Tauri only) */
export const showWindowControls = isTauri && isWindows
export function WindowControls() {
const [maximized, setMaximized] = useState(false)
const [win, setWin] = useState<{
minimize: () => Promise<void>
toggleMaximize: () => Promise<void>
close: () => Promise<void>
isMaximized: () => Promise<boolean>
onResized: (handler: () => void) => Promise<() => void>
} | null>(null)
useEffect(() => {
if (!showWindowControls) return
let unlisten: (() => void) | undefined
import('@tauri-apps/api/window')
.then(async ({ getCurrentWindow }) => {
const w = getCurrentWindow()
setWin(w as any)
setMaximized(await w.isMaximized())
unlisten = await w.onResized(async () => {
setMaximized(await w.isMaximized())
})
})
.catch(() => {})
return () => { unlisten?.() }
}, [])
const runWindowAction = (action: () => Promise<void>) => {
void action().catch((error) => {
console.error('Window control action failed', error)
})
}
if (!showWindowControls || !win) return null
return (
<div data-testid="window-controls" className="flex items-stretch flex-shrink-0 -my-px">
{/* Minimize */}
<button
onClick={() => runWindowAction(() => win.minimize())}
aria-label="Minimize window"
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
>
<svg width="10" height="1" viewBox="0 0 10 1">
<rect width="10" height="1" fill="currentColor" />
</svg>
</button>
{/* Maximize / Restore */}
<button
onClick={() => runWindowAction(() => win.toggleMaximize())}
aria-label={maximized ? 'Restore window' : 'Maximize window'}
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
>
{maximized ? (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1">
<rect x="0" y="3" width="7" height="7" />
<polyline points="3,3 3,0 10,0 10,7 7,7" />
</svg>
) : (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1">
<rect x="0.5" y="0.5" width="9" height="9" />
</svg>
)}
</button>
{/* Close */}
<button
onClick={() => runWindowAction(() => win.close())}
aria-label="Close window"
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-window-close-hover)] hover:text-white transition-colors"
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.2">
<line x1="0" y1="0" x2="10" y2="10" />
<line x1="10" y1="0" x2="0" y2="10" />
</svg>
</button>
</div>
)
}