From 19cd713682d7fb64dac09af49566a55ab27e7477 Mon Sep 17 00:00:00 2001 From: jiaqi62 Date: Tue, 21 Jul 2026 14:53:12 +0800 Subject: [PATCH] fix(wework): UI display fixes for narrow toolbar, cloud popover, and dialogs - Cap model selector width and collapse quick phrases in narrow composer toolbar - Render cloud-work error popover via portal with outside-click/ESC dismissal - Remove forced dark mode from add-device and cloud-work dialogs - Revert model selector to left-opening and narrow browser occlusion to cloud dialog - Swallow arrow keys at native input boundaries on macOS - Stabilize App auth routing test by mocking auth state --- wework/src/App.test.tsx | 34 +- wework/src/App.tsx | 5 + wework/src/components/chat/ChatInput.test.tsx | 2 +- .../chat/composer/ComposerToolbar.tsx | 2 + .../chat/composer/ModelSelector.test.tsx | 308 ++++++++++++++++++ .../chat/composer/ModelSelector.tsx | 16 +- .../chat/composer/ModelSelectorTrigger.tsx | 11 +- .../chat/composer/QuickPhraseMenu.test.tsx | 18 + .../chat/composer/model-selector-types.ts | 1 + .../components/layout/DesktopSidebar.test.tsx | 34 ++ .../WorkspaceBrowserPanel.test.tsx | 1 + .../WorkspaceBrowserPanel.tsx | 2 +- .../projects/DeviceFolderPicker.tsx | 36 +- .../projects/StandaloneProjectDialogs.tsx | 39 ++- .../settings/AddCloudDeviceDialog.tsx | 10 +- .../CloudConnectionDialog.tsx | 2 + .../src/hooks/useEmbeddedBrowserOcclusion.ts | 11 + .../src/lib/macosInputArrowKeyGuard.test.ts | 157 +++++++++ wework/src/lib/macosInputArrowKeyGuard.ts | 53 +++ 19 files changed, 681 insertions(+), 61 deletions(-) create mode 100644 wework/src/components/chat/composer/ModelSelector.test.tsx create mode 100644 wework/src/hooks/useEmbeddedBrowserOcclusion.ts create mode 100644 wework/src/lib/macosInputArrowKeyGuard.test.ts create mode 100644 wework/src/lib/macosInputArrowKeyGuard.ts diff --git a/wework/src/App.test.tsx b/wework/src/App.test.tsx index 76c6b4634eb..b883f9e44fb 100644 --- a/wework/src/App.test.tsx +++ b/wework/src/App.test.tsx @@ -1,18 +1,28 @@ -import { render, screen } from '@testing-library/react' -import { beforeEach, describe, expect, test, vi } from 'vitest' +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, test, vi } from 'vitest' import App from './App' +vi.mock('@/features/auth/AuthProvider', () => ({ + AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +vi.mock('@/features/auth/useAuth', () => ({ + useAuth: () => ({ + user: null, + isLoading: false, + adminPasswordSetupRequired: false, + adminUsername: 'admin', + login: vi.fn(), + logout: vi.fn(), + refresh: vi.fn(), + loginWithOidcToken: vi.fn(), + setupAdminPassword: vi.fn(), + }), +})) + describe('App auth routing', () => { - beforeEach(() => { - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue( - new Response(JSON.stringify({ required: false }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - ) - ) + afterEach(() => { + cleanup() }) test('renders login page on /login', async () => { diff --git a/wework/src/App.tsx b/wework/src/App.tsx index 41f49ef2529..b037c0c852d 100644 --- a/wework/src/App.tsx +++ b/wework/src/App.tsx @@ -68,6 +68,7 @@ import { import { AppshotBridge } from '@/features/appshots/AppshotBridge' import { SystemDragPanel } from '@/features/system-drag/SystemDragPanel' import { SystemDragBridge } from '@/features/system-drag/SystemDragBridge' +import { installMacOSInputArrowKeyGuard } from '@/lib/macosInputArrowKeyGuard' const WORKBENCH_STARTUP_REVEAL_TIMEOUT_MS = 6000 @@ -377,6 +378,10 @@ function AppShell() { } }, [isTauri]) + useEffect(() => { + return installMacOSInputArrowKeyGuard() + }, []) + useEffect(() => { if ( path === '/login' || diff --git a/wework/src/components/chat/ChatInput.test.tsx b/wework/src/components/chat/ChatInput.test.tsx index 7a37e20d9e0..b5f0cca139f 100644 --- a/wework/src/components/chat/ChatInput.test.tsx +++ b/wework/src/components/chat/ChatInput.test.tsx @@ -1253,7 +1253,7 @@ describe('ChatInput', () => { 'data-enter-animation', 'main' ) - expect(selectorButton).toHaveStyle({ width: '240px' }) + expect(selectorButton).toHaveStyle({ width: 'var(--model-selector-width, auto)' }) expect(screen.queryByTestId('model-selector-tooltip')).not.toBeInTheDocument() expect(screen.getByTestId('model-selector-menu').parentElement).toHaveClass( 'fixed', diff --git a/wework/src/components/chat/composer/ComposerToolbar.tsx b/wework/src/components/chat/composer/ComposerToolbar.tsx index 29dd3c6d87e..82c177349cc 100644 --- a/wework/src/components/chat/composer/ComposerToolbar.tsx +++ b/wework/src/components/chat/composer/ComposerToolbar.tsx @@ -39,6 +39,7 @@ interface ComposerToolbarProps { } const COMPACT_TOOLBAR_WIDTH = 475 +const NARROW_MODEL_SELECTOR_MAX_WIDTH = 160 export function ComposerToolbar({ canSend, @@ -131,6 +132,7 @@ export function ComposerToolbar({ onSelectModelOption={onSelectModelOption} onBlockedModelSelect={onBlockedModelSelect} buttonClassName="opacity-90 hover:opacity-100" + maxClosedWidth={compact ? NARROW_MODEL_SELECTOR_MAX_WIDTH : undefined} /> ) : (
diff --git a/wework/src/components/chat/composer/ModelSelector.test.tsx b/wework/src/components/chat/composer/ModelSelector.test.tsx new file mode 100644 index 00000000000..add4276b2b7 --- /dev/null +++ b/wework/src/components/chat/composer/ModelSelector.test.tsx @@ -0,0 +1,308 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import type { UnifiedModel } from '@/types/api' + +const isMobileMock = vi.hoisted(() => vi.fn()) +vi.mock('@/hooks/useIsMobile', () => ({ + useIsMobile: isMobileMock, +})) + +const configuredKeybindingMock = vi.hoisted(() => vi.fn()) +vi.mock('@/hooks/useConfiguredKeybinding', () => ({ + useConfiguredKeybinding: configuredKeybindingMock, +})) + +const modelExecutionMock = vi.hoisted(() => ({ + getModelExecutionOverride: vi.fn(), +})) +vi.mock('@/features/cloud-connection/modelExecution', () => modelExecutionMock) + +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => fallback ?? key, + }), +})) + +import { ModelSelector } from './ModelSelector' + +const SHELL_LEFT = 800 +const WINDOW_INNER_WIDTH = 1200 +const WINDOW_INNER_HEIGHT = 900 + +function createShellElement(options?: { hidden?: boolean }): HTMLDivElement { + const shell = document.createElement('div') + shell.id = 'right-workspace-panel-shell' + shell.setAttribute('data-testid', 'right-workspace-panel-shell') + if (options?.hidden) { + shell.setAttribute('aria-hidden', 'true') + } else { + shell.setAttribute('aria-hidden', 'false') + } + shell.getBoundingClientRect = () => + ({ + top: 0, + left: options?.hidden ? WINDOW_INNER_WIDTH : SHELL_LEFT, + right: WINDOW_INNER_WIDTH, + bottom: WINDOW_INNER_HEIGHT, + width: options?.hidden ? 0 : WINDOW_INNER_WIDTH - SHELL_LEFT, + height: WINDOW_INNER_HEIGHT, + x: options?.hidden ? WINDOW_INNER_WIDTH : SHELL_LEFT, + y: 0, + toJSON: () => {}, + }) as DOMRect + document.body.appendChild(shell) + return shell +} + +function setButtonRect(button: HTMLElement, right = 850) { + const left = right - 240 + button.getBoundingClientRect = () => + ({ + top: 500, + left, + right, + bottom: 540, + width: 240, + height: 40, + x: left, + y: 500, + toJSON: () => {}, + }) as DOMRect +} + +const SAMPLE_MODEL: UnifiedModel = { + name: 'gpt-5.5', + modelId: 'gpt-5.5', + displayName: 'GPT 5.5', + type: 'runtime', + provider: 'local', + config: { + weworkModelKind: 'codex-official', + ui: { family: 'codex-official', controls: ['speed'] }, + }, +} + +describe('ModelSelector desktop layout', () => { + const originalInnerWidth = window.innerWidth + const originalInnerHeight = window.innerHeight + + beforeEach(() => { + isMobileMock.mockReturnValue(false) + configuredKeybindingMock.mockReturnValue(null) + modelExecutionMock.getModelExecutionOverride.mockReturnValue(undefined) + Object.defineProperty(window, 'innerWidth', { + configurable: true, + value: WINDOW_INNER_WIDTH, + }) + Object.defineProperty(window, 'innerHeight', { + configurable: true, + value: WINDOW_INNER_HEIGHT, + }) + }) + + afterEach(() => { + cleanup() + document.querySelectorAll('#right-workspace-panel-shell').forEach(shell => shell.remove()) + vi.restoreAllMocks() + Object.defineProperty(window, 'innerWidth', { + configurable: true, + value: originalInnerWidth, + }) + Object.defineProperty(window, 'innerHeight', { + configurable: true, + value: originalInnerHeight, + }) + }) + + test('positions the menu to the left of the right workspace panel shell', async () => { + createShellElement() + + render( + + ) + + const button = screen.getByTestId('model-selector-button') + setButtonRect(button) + fireEvent.click(button) + + const menu = await waitFor(() => screen.getByTestId('model-selector-menu')) + const wrapper = menu.parentElement + expect(wrapper).not.toBeNull() + const left = parseInt(wrapper!.style.left, 10) + expect(left).toBeLessThanOrEqual(SHELL_LEFT - 16) + }) + + test('caps the trigger width when maxClosedWidth is provided', () => { + createShellElement({ hidden: true }) + + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect + HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() { + if (this.getAttribute('data-testid') === 'model-selector-button') { + return { + top: 500, + left: 700, + right: 940, + bottom: 540, + width: 240, + height: 40, + x: 700, + y: 500, + toJSON: () => {}, + } as DOMRect + } + if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') { + return { + top: 0, + left: 0, + right: 300, + bottom: 0, + width: 300, + height: 0, + x: 0, + y: 0, + toJSON: () => {}, + } as DOMRect + } + return originalGetBoundingClientRect.call(this) + } + + try { + render( + + ) + + const button = screen.getByTestId('model-selector-button') + expect(button.style.getPropertyValue('--model-selector-width')).toBe('160px') + expect(button.style.width).toBe('var(--model-selector-width, auto)') + } finally { + HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect + } + }) + + test('keeps the trigger capped when opened with maxClosedWidth', async () => { + createShellElement({ hidden: true }) + + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect + HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() { + if (this.getAttribute('data-testid') === 'model-selector-button') { + return { + top: 500, + left: 700, + right: 940, + bottom: 540, + width: 240, + height: 40, + x: 700, + y: 500, + toJSON: () => {}, + } as DOMRect + } + if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') { + return { + top: 0, + left: 0, + right: 300, + bottom: 0, + width: 300, + height: 0, + x: 0, + y: 0, + toJSON: () => {}, + } as DOMRect + } + return originalGetBoundingClientRect.call(this) + } + + try { + render( + + ) + + const button = screen.getByTestId('model-selector-button') + fireEvent.click(button) + await waitFor(() => screen.getByTestId('model-selector-menu')) + + expect(button.style.getPropertyValue('--model-selector-width')).toBe('160px') + expect(button.style.width).toBe('var(--model-selector-width, auto)') + } finally { + HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect + } + }) + + test('uses the default closed width cap when maxClosedWidth is omitted', () => { + createShellElement({ hidden: true }) + + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect + HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() { + if (this.getAttribute('data-testid') === 'model-selector-button') { + return { + top: 500, + left: 700, + right: 940, + bottom: 540, + width: 240, + height: 40, + x: 700, + y: 500, + toJSON: () => {}, + } as DOMRect + } + if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') { + return { + top: 0, + left: 0, + right: 300, + bottom: 0, + width: 300, + height: 0, + x: 0, + y: 0, + toJSON: () => {}, + } as DOMRect + } + return originalGetBoundingClientRect.call(this) + } + + try { + render( + + ) + + const button = screen.getByTestId('model-selector-button') + expect(button.style.getPropertyValue('--model-selector-width')).toBe('208px') + expect(button.style.width).toBe('var(--model-selector-width, auto)') + } finally { + HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect + } + }) +}) diff --git a/wework/src/components/chat/composer/ModelSelector.tsx b/wework/src/components/chat/composer/ModelSelector.tsx index 7d4c9377d6b..610de4203ba 100644 --- a/wework/src/components/chat/composer/ModelSelector.tsx +++ b/wework/src/components/chat/composer/ModelSelector.tsx @@ -60,6 +60,15 @@ const SUBMENU_VIEWPORT_VERTICAL_GAP = 128 const DESKTOP_HIDDEN_CONTROL_IDS = new Set(['collaborationMode']) type DesktopSubmenuTarget = { type: 'models' } | { type: 'control'; id: string } | { type: 'none' } +function getDesktopViewportRightBoundary(): number { + const shell = document.getElementById('right-workspace-panel-shell') + if (shell && shell.getAttribute('aria-hidden') !== 'true') { + const rect = shell.getBoundingClientRect() + if (rect.width > 0) return Math.round(rect.left) + } + return window.innerWidth +} + function isCloudModel(model: UnifiedModel): boolean { return getModelExecutionOverride(model)?.source === 'cloud' } @@ -77,6 +86,7 @@ export function ModelSelector({ menuPlacement = 'above', buttonClassName = '', menuClassName = '', + maxClosedWidth, }: ModelSelectorProps) { const { t } = useTranslation('common') const isMobile = useIsMobile() @@ -166,7 +176,8 @@ export function ModelSelector({ const maxTop = viewportBottom - menuHeight const clampedTop = Math.round(Math.max(viewportTop, Math.min(preferredTop, maxTop))) const menuWidth = menuPanel.getBoundingClientRect().width || MAIN_MENU_WIDTH - const maxLeft = window.innerWidth - VIEWPORT_MARGIN - menuWidth + const viewportRight = getDesktopViewportRightBoundary() + const maxLeft = viewportRight - VIEWPORT_MARGIN - menuWidth const preferredLeft = buttonRect.right - menuWidth const clampedLeft = Math.round(Math.max(VIEWPORT_MARGIN, Math.min(preferredLeft, maxLeft))) @@ -211,7 +222,7 @@ export function ModelSelector({ const measuredMenuWidth = menuRect.width || MAIN_MENU_WIDTH const measuredSubmenuWidth = submenuRect?.width || SUBMENU_WIDTH const rightSideLeft = measuredMenuWidth + SUBMENU_GAP - const viewportWidth = window.innerWidth + const viewportWidth = getDesktopViewportRightBoundary() const availableRight = viewportWidth - VIEWPORT_MARGIN - menuRect.left - rightSideLeft const availableLeft = menuRect.left - VIEWPORT_MARGIN - SUBMENU_GAP const rightSideWidth = Math.max(0, Math.min(measuredSubmenuWidth, availableRight)) @@ -1004,6 +1015,7 @@ export function ModelSelector({ ariaLabel={t('workbench.model_selector')} tooltipLabel={t('workbench.model_picker_title', '选择模型')} buttonClassName={buttonClassName} + maxClosedWidth={maxClosedWidth} onToggle={() => { if (disabled) return setOpen(current => { diff --git a/wework/src/components/chat/composer/ModelSelectorTrigger.tsx b/wework/src/components/chat/composer/ModelSelectorTrigger.tsx index c1334349207..be9bdf32d83 100644 --- a/wework/src/components/chat/composer/ModelSelectorTrigger.tsx +++ b/wework/src/components/chat/composer/ModelSelectorTrigger.tsx @@ -10,7 +10,6 @@ import { import { KeyboardShortcut } from '@/components/common/KeyboardShortcut' import { cn } from '@/lib/utils' -const OPEN_WIDTH = 240 const CLOSED_MAX_WIDTH = 208 const HORIZONTAL_CHROME = 36 @@ -25,6 +24,7 @@ interface ModelSelectorTriggerProps { ariaLabel: string tooltipLabel: string buttonClassName?: string + maxClosedWidth?: number onToggle: () => void } @@ -52,6 +52,7 @@ export function ModelSelectorTrigger({ ariaLabel, tooltipLabel, buttonClassName, + maxClosedWidth = CLOSED_MAX_WIDTH, onToggle, }: ModelSelectorTriggerProps) { const measureRef = useRef(null) @@ -76,8 +77,8 @@ export function ModelSelectorTrigger({ if (measuredWidth <= 0) return const naturalWidth = Math.ceil(measuredWidth) + HORIZONTAL_CHROME button.style.setProperty( - '--model-selector-closed-width', - `${Math.max(64, Math.min(CLOSED_MAX_WIDTH, naturalWidth))}px` + '--model-selector-width', + `${Math.max(64, Math.min(maxClosedWidth, naturalWidth))}px` ) } @@ -90,12 +91,12 @@ export function ModelSelectorTrigger({ observer?.disconnect() window.removeEventListener('resize', updateWidth) } - }, [buttonRef, isMobile, label]) + }, [buttonRef, isMobile, label, maxClosedWidth]) const desktopStyle: CSSProperties | undefined = isMobile ? undefined : { - width: open ? OPEN_WIDTH : 'var(--model-selector-closed-width, auto)', + width: 'var(--model-selector-width, auto)', } const showTooltip = !isMobile && !open && !tooltipSuppressed diff --git a/wework/src/components/chat/composer/QuickPhraseMenu.test.tsx b/wework/src/components/chat/composer/QuickPhraseMenu.test.tsx index e75317307d5..0b1f67688eb 100644 --- a/wework/src/components/chat/composer/QuickPhraseMenu.test.tsx +++ b/wework/src/components/chat/composer/QuickPhraseMenu.test.tsx @@ -73,6 +73,24 @@ describe('QuickPhraseMenu', () => { expect(screen.getByTestId('quick-phrase-stash-preview')).toHaveTextContent('两张图片') }) + test('renders icon-only without label when iconOnly is true', () => { + render() + + const button = screen.getByTestId('quick-phrase-button') + expect(button).not.toHaveTextContent('快捷短语') + expect(button.className).toContain('h-8') + expect(button.className).toContain('w-8') + }) + + test('renders compact circular button without label when compact is true', () => { + render() + + const button = screen.getByTestId('quick-phrase-button') + expect(button).not.toHaveTextContent('快捷短语') + expect(button.className).toContain('h-11') + expect(button.className).toContain('w-11') + }) + test('deletes a stash without selecting it', async () => { const onSelect = vi.fn() appPreferenceMocks.get.mockResolvedValue({ diff --git a/wework/src/components/chat/composer/model-selector-types.ts b/wework/src/components/chat/composer/model-selector-types.ts index 477e298e455..00e6518a34c 100644 --- a/wework/src/components/chat/composer/model-selector-types.ts +++ b/wework/src/components/chat/composer/model-selector-types.ts @@ -13,4 +13,5 @@ export interface ModelSelectorProps { menuPlacement?: 'above' | 'below' buttonClassName?: string menuClassName?: string + maxClosedWidth?: number } diff --git a/wework/src/components/layout/DesktopSidebar.test.tsx b/wework/src/components/layout/DesktopSidebar.test.tsx index 90422d4db5e..2cb60ef9299 100644 --- a/wework/src/components/layout/DesktopSidebar.test.tsx +++ b/wework/src/components/layout/DesktopSidebar.test.tsx @@ -844,6 +844,40 @@ describe('DesktopSidebar', () => { expect(screen.queryByTestId('sidebar-cloud-error-popover')).not.toBeInTheDocument() }) + test('closes cloud work error details when clicking outside', async () => { + renderSidebar({ + devices: [localDevice()], + cloudWorkStatus: cloudWorkStatus({ + availability: 'unavailable', + checks: { devices: 'unavailable', runtimeWork: 'available' }, + error: '云端设备: request timed out', + }), + }) + + await userEvent.click(screen.getByTestId('sidebar-cloud-error-button')) + expect(screen.getByTestId('sidebar-cloud-error-popover')).toBeInTheDocument() + + await userEvent.click(document.body) + expect(screen.queryByTestId('sidebar-cloud-error-popover')).not.toBeInTheDocument() + }) + + test('does not close cloud work error details when clicking inside', async () => { + renderSidebar({ + devices: [localDevice()], + cloudWorkStatus: cloudWorkStatus({ + availability: 'unavailable', + checks: { devices: 'unavailable', runtimeWork: 'available' }, + error: '云端设备: request timed out', + }), + }) + + await userEvent.click(screen.getByTestId('sidebar-cloud-error-button')) + const detail = screen.getByTestId('sidebar-cloud-error-popover') + + await userEvent.click(detail) + expect(screen.getByTestId('sidebar-cloud-error-popover')).toBeInTheDocument() + }) + test('does not open add-device guidance while cloud work checks are failing', async () => { const onGetRemoteDeviceStartupCommand = vi.fn() renderSidebar({ diff --git a/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.test.tsx b/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.test.tsx index fc3e0881f6f..57a39f2f80a 100644 --- a/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.test.tsx +++ b/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.test.tsx @@ -93,6 +93,7 @@ describe('WorkspaceBrowserPanel', () => { embeddedBrowserMocks.goForwardEmbeddedBrowser.mockResolvedValue(undefined) embeddedBrowserMocks.navigateEmbeddedBrowser.mockResolvedValue(undefined) embeddedBrowserMocks.reloadEmbeddedBrowser.mockResolvedValue(undefined) + embeddedBrowserMocks.resumeEmbeddedBrowserDownload.mockResolvedValue(undefined) embeddedBrowserMocks.setEmbeddedBrowserBounds.mockResolvedValue(undefined) }) diff --git a/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.tsx b/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.tsx index e4db98c3aa2..8155c404977 100644 --- a/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.tsx +++ b/wework/src/components/layout/workspace-panels/WorkspaceBrowserPanel.tsx @@ -1276,7 +1276,7 @@ export function WorkspaceBrowserPanel({ ) window.removeEventListener(EMBEDDED_BROWSER_OCCLUSION_EVENT, handleBrowserOcclusion) } - }, []) + }, [label]) useEffect(() => { void syncEmbeddedBrowserBounds(active).catch(error => { diff --git a/wework/src/components/projects/DeviceFolderPicker.tsx b/wework/src/components/projects/DeviceFolderPicker.tsx index 2a3c75f85ee..d12717081b3 100644 --- a/wework/src/components/projects/DeviceFolderPicker.tsx +++ b/wework/src/components/projects/DeviceFolderPicker.tsx @@ -248,7 +248,7 @@ export function DeviceFolderPicker({ data-testid="device-folder-parent-button" disabled={pickerDisabled || submitting || !currentPath || currentPath === '/'} onClick={() => browsePath(getParentPath(currentPath))} - className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-[#9a9a9a] hover:bg-white/5 hover:text-white disabled:opacity-40" + className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-text-secondary hover:bg-muted hover:text-text-primary disabled:opacity-40" aria-label={t('workbench.project_directory_parent', '返回上级目录')} > @@ -257,7 +257,7 @@ export function DeviceFolderPicker({