mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
refactor(desktop): adopt the component library in layout and workspace
Native buttons across the three directories: 81 -> 49. `Toast` and `UpdateChecker` move in from `shared/` — one caller each, both application singletons that belong with the shell. `Sidebar` was the hard case. Its 21 icon buttons hover to `--color-sidebar-item-hover`, which differs from `--color-surface-hover` in all three themes (it is a translucent white tuned for the sidebar's gradient). Overriding via className means two arbitrary `hover:bg-[…]` values competing, with the winner decided by stylesheet order — so `IconButton` grew a `surface="sidebar"` instead. Both local `ToolbarIconButton` adapters are deleted; `pressed` was the only thing they still added, and it brings `aria-pressed` they never had. `ProjectHeaderMenu` gains `forwardRef` so its four menus can share one `useDismissable`. Their triggers are click toggles, so `triggerRef` is what keeps a second click from reopening what it just closed. Three retry buttons drop five `/N` alpha modifiers on the way. Left native (49): menu items, tab items, tree nodes, file rows, the OS titlebar controls, `WorkspaceDiffSurface`'s cells, and the two sidebar collapse toggles — their `sidebar-toggle-button` class carries an active transform and drives the chevron animation, not just size and radius. `TitleBar.tsx` is deleted: zero importers. The audit listed its settings button among the unnamed controls, but it never rendered.
This commit is contained in:
@@ -119,11 +119,11 @@ vi.mock('../../pages/TraceSession', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../shared/Toast', () => ({
|
||||
vi.mock('@/components/layout/Toast', () => ({
|
||||
ToastContainer: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../shared/UpdateChecker', () => ({
|
||||
vi.mock('@/components/layout/UpdateChecker', () => ({
|
||||
UpdateChecker: () => <div>updates loaded</div>,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useRef, useState, type HTMLAttributes } from 'react'
|
||||
import { Sidebar } from './Sidebar'
|
||||
import { ContentRouter } from './ContentRouter'
|
||||
import { ToastContainer } from '../shared/Toast'
|
||||
import { UpdateChecker } from '../shared/UpdateChecker'
|
||||
import { ToastContainer } from '@/components/layout/Toast'
|
||||
import { UpdateChecker } from '@/components/layout/UpdateChecker'
|
||||
import { StatusDot } from '@/components/ui/Badge'
|
||||
import { IconButton } from '@/components/ui/IconButton'
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useUIStore, type SettingsTab } from '../../stores/uiStore'
|
||||
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
|
||||
@@ -310,19 +312,15 @@ export function AppShell() {
|
||||
data-testid="mobile-session-header"
|
||||
className="flex shrink-0 items-center gap-3 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
data-testid="mobile-sidebar-toggle"
|
||||
icon={effectiveSidebarOpen ? 'close' : 'menu'}
|
||||
label={effectiveSidebarOpen ? t('sidebar.collapse') : t('sidebar.expand')}
|
||||
onClick={toggleEffectiveSidebar}
|
||||
size="xl"
|
||||
aria-controls="sidebar-shell"
|
||||
aria-expanded={effectiveSidebarOpen}
|
||||
aria-label={effectiveSidebarOpen ? t('sidebar.collapse') : t('sidebar.expand')}
|
||||
onClick={toggleEffectiveSidebar}
|
||||
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">
|
||||
{effectiveSidebarOpen ? 'close' : 'menu'}
|
||||
</span>
|
||||
</button>
|
||||
/>
|
||||
{isActiveChatTab ? (
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="truncate text-[15px] font-bold leading-tight text-[var(--color-text-primary)]">
|
||||
@@ -331,7 +329,7 @@ export function AppShell() {
|
||||
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap text-[10px] font-medium text-[var(--color-text-tertiary)]">
|
||||
{activeTab?.status === 'running' ? (
|
||||
<span className="flex shrink-0 items-center gap-1 text-[var(--color-text-secondary)]">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-success)] animate-pulse-dot" />
|
||||
<StatusDot tone="success" pulse />
|
||||
{t('session.active')}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { saveAndVerifyH5Connection } from '../../lib/desktopRuntime'
|
||||
import { Button } from '../shared/Button'
|
||||
import { Input } from '../shared/Input'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
type H5ConnectionViewProps = {
|
||||
initialServerUrl?: string | null
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useOpenTargetStore } from '../../stores/openTargetStore'
|
||||
import { TargetIcon } from '../common/TargetIcon'
|
||||
import { useDismissable } from '@/hooks/useDismissable'
|
||||
import { TargetIcon } from '@/components/composite/TargetIcon'
|
||||
|
||||
type Props = {
|
||||
path: string | null | undefined
|
||||
@@ -27,26 +28,14 @@ export function OpenProjectMenu({ path }: Props) {
|
||||
void ensureTargets()
|
||||
}, [ensureTargets, path])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleDismiss = useCallback(() => setOpen(false), [])
|
||||
|
||||
const handleDocumentMouseDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node
|
||||
if (buttonRef.current?.contains(target) || menuRef.current?.contains(target)) return
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setOpen(false)
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleDocumentMouseDown)
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleDocumentMouseDown)
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [open])
|
||||
useDismissable({
|
||||
open,
|
||||
refs: [menuRef],
|
||||
triggerRef: buttonRef,
|
||||
onDismiss: handleDismiss,
|
||||
})
|
||||
|
||||
const primaryTarget = useMemo(
|
||||
() => targets.find((target) => target.id === primaryTargetId) ?? targets[0] ?? null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
import { Copy, RefreshCw } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { Button } from '../shared/Button'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { DoctorPanel } from '../doctor/DoctorPanel'
|
||||
import { copyTextToClipboard } from '../chat/clipboard'
|
||||
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||
|
||||
const LOG_MARKER = '\n\nRecent server logs:\n'
|
||||
|
||||
|
||||
@@ -99,6 +99,10 @@ vi.mock('../../i18n', () => ({
|
||||
'tabs.hideWorkspace': 'Hide Workspace',
|
||||
'tabs.showBrowser': 'Show Browser',
|
||||
'tabs.hideBrowser': 'Hide Browser',
|
||||
'tabs.scrollLeft': 'Scroll tabs left',
|
||||
'tabs.scrollRight': 'Scroll tabs right',
|
||||
'tabs.closeTab': 'Close {title}',
|
||||
'tabs.untitled': 'Untitled',
|
||||
'settings.title': 'Localized Settings',
|
||||
'openProject.openProject': 'Open project',
|
||||
'openProject.openIn': 'Open in {target}',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
export function TitleBar() {
|
||||
const { activeView, setActiveView } = useUIStore()
|
||||
const t = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-[var(--titlebar-height)] flex items-center border-b border-[var(--color-border)] bg-[var(--color-surface)] select-none"
|
||||
data-desktop-drag-region
|
||||
>
|
||||
{/* macOS traffic light spacer */}
|
||||
<div className="w-[78px] flex-shrink-0" data-desktop-drag-region />
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 mr-4" data-desktop-drag-region>
|
||||
<span className="text-xs font-bold tracking-wider text-[var(--color-brand)] uppercase">Claude Code Companion</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
<div className="flex items-center gap-1 mr-4">
|
||||
<button className="p-1 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button className="p-1 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Center tabs */}
|
||||
<div className="flex-1 flex items-center justify-center gap-1" data-desktop-drag-region>
|
||||
<TabButton
|
||||
active={activeView === 'code'}
|
||||
onClick={() => setActiveView('code')}
|
||||
icon="code"
|
||||
>
|
||||
{t('titlebar.code')}
|
||||
</TabButton>
|
||||
<TabButton
|
||||
active={activeView === 'terminal'}
|
||||
onClick={() => setActiveView('terminal')}
|
||||
icon="terminal"
|
||||
>
|
||||
{t('titlebar.terminal')}
|
||||
</TabButton>
|
||||
<TabButton
|
||||
active={activeView === 'history'}
|
||||
onClick={() => setActiveView('history')}
|
||||
icon="history"
|
||||
>
|
||||
{t('titlebar.history')}
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{/* Right: Settings */}
|
||||
<div className="flex items-center gap-2 mr-4">
|
||||
<button className="p-1.5 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
|
||||
<span className="material-symbols-outlined text-[18px]">settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
active,
|
||||
onClick,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
icon: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`
|
||||
flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-[var(--radius-md)] transition-colors duration-200
|
||||
${active
|
||||
? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">{icon}</span>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IconButton } from '@/components/ui/IconButton'
|
||||
import { useUIStore, type Toast as ToastType } from '../../stores/uiStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
@@ -22,18 +23,18 @@ function ToastItem({ toast }: { toast: ToastType }) {
|
||||
bg-[var(--color-surface)] rounded-[var(--radius-md)] shadow-[var(--shadow-dropdown)]
|
||||
px-4 py-3 text-sm text-[var(--color-text-primary)]
|
||||
${typeStyles[toast.type]}
|
||||
animate-in slide-in-from-right fade-in duration-200
|
||||
animate-overlay-in-right
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span>{toast.message}</span>
|
||||
<button
|
||||
<IconButton
|
||||
icon="close"
|
||||
label={t('common.dismissNotification')}
|
||||
onClick={() => removeToast(toast.id)}
|
||||
aria-label={t('common.dismissNotification')}
|
||||
className="text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] text-lg leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
size="xs"
|
||||
tone="muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -45,7 +46,7 @@ export function ToastContainer() {
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm">
|
||||
<div className="fixed bottom-4 right-4 z-[var(--z-toast)] flex flex-col gap-2 max-w-sm">
|
||||
{toasts.map((toast) => (
|
||||
<ToastItem key={toast.id} toast={toast} />
|
||||
))}
|
||||
+6
-11
@@ -1,4 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
|
||||
import { isDesktopRuntime } from '../../lib/desktopRuntime'
|
||||
@@ -28,7 +29,7 @@ export function UpdateChecker() {
|
||||
const statusText = t('update.readyBody', { version: availableVersion })
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 z-[120] w-[min(360px,calc(100vw-2rem))] -translate-x-1/2">
|
||||
<div className="fixed bottom-4 left-1/2 z-[var(--z-toast)] w-[min(360px,calc(100vw-2rem))] -translate-x-1/2">
|
||||
<div className="bg-[var(--color-surface-container-low)] border border-[var(--color-border)] rounded-[var(--radius-lg)] shadow-[var(--shadow-dropdown)] p-3">
|
||||
<p className="text-sm font-medium text-[var(--color-text-primary)]">
|
||||
{t('update.readyTitle')}
|
||||
@@ -54,18 +55,12 @@ export function UpdateChecker() {
|
||||
|
||||
{status === 'downloaded' && (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => void installUpdate()}
|
||||
className="px-3 py-1 text-xs font-medium rounded-[var(--radius-md)] bg-[var(--color-text-accent)] text-white hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Button variant="primary" size="base" onClick={() => void installUpdate()}>
|
||||
{t('update.installAndRestart')}
|
||||
</button>
|
||||
<button
|
||||
onClick={dismissPrompt}
|
||||
className="px-3 py-1 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="ghost" size="base" onClick={dismissPrompt}>
|
||||
{t('update.later')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -40,6 +40,11 @@ describe('WindowControls', () => {
|
||||
isMaximized.mockClear()
|
||||
onResized.mockClear()
|
||||
|
||||
// The control labels are translated; assertions below spell the English strings.
|
||||
// Seeded through localStorage rather than `setState` because `vi.resetModules()`
|
||||
// below makes the test import a fresh settings store that re-reads storage.
|
||||
window.localStorage.setItem('cc-haha-locale', 'en')
|
||||
|
||||
Reflect.deleteProperty(window, '__TAURI_INTERNALS__')
|
||||
window.desktopHost = {
|
||||
kind: 'electron',
|
||||
@@ -75,6 +80,7 @@ describe('WindowControls', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.removeItem('cc-haha-locale')
|
||||
Reflect.deleteProperty(window, '__TAURI_INTERNALS__')
|
||||
Reflect.deleteProperty(window, 'desktopHost')
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getDesktopHost } from '../../lib/desktopHost'
|
||||
import type { DesktopHost } from '../../lib/desktopHost'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
const isWindows = typeof navigator !== 'undefined' && /Win/.test(navigator.platform)
|
||||
|
||||
@@ -8,6 +9,7 @@ const isWindows = typeof navigator !== 'undefined' && /Win/.test(navigator.platf
|
||||
export const showWindowControls = isWindows && getDesktopHost().capabilities.windowControls
|
||||
|
||||
export function WindowControls() {
|
||||
const t = useTranslation()
|
||||
const [maximized, setMaximized] = useState(false)
|
||||
const [win, setWin] = useState<DesktopHost['window'] | null>(null)
|
||||
|
||||
@@ -52,7 +54,7 @@ export function WindowControls() {
|
||||
{/* Minimize */}
|
||||
<button
|
||||
onClick={() => runWindowAction(() => win.minimize())}
|
||||
aria-label="Minimize window"
|
||||
aria-label={t('windowControls.minimize')}
|
||||
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">
|
||||
@@ -63,7 +65,7 @@ export function WindowControls() {
|
||||
{/* Maximize / Restore */}
|
||||
<button
|
||||
onClick={() => runWindowAction(() => win.toggleMaximize())}
|
||||
aria-label={maximized ? 'Restore window' : 'Maximize window'}
|
||||
aria-label={t(maximized ? 'windowControls.restore' : 'windowControls.maximize')}
|
||||
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||
>
|
||||
{maximized ? (
|
||||
@@ -81,7 +83,7 @@ export function WindowControls() {
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={() => runWindowAction(() => win.close())}
|
||||
aria-label="Close window"
|
||||
aria-label={t('windowControls.close')}
|
||||
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">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ArrowLeft, FolderOpen, Globe, Maximize2, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { IconButton } from '@/components/ui/IconButton'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import {
|
||||
useWorkspacePanelStore,
|
||||
@@ -78,14 +80,15 @@ export function WorkbenchPanel({ sessionId, variant = 'panel', onClose }: Workbe
|
||||
className="flex h-12 shrink-0 items-center gap-2.5 border-b border-[var(--color-text-primary)]/10 bg-[var(--color-surface)] px-4"
|
||||
>
|
||||
{isTabVariant && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="base"
|
||||
onClick={handleReturn}
|
||||
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-[7px] px-2 text-[12px] font-medium text-[var(--color-text-secondary)] transition-[color,background-color,transform] duration-200 ease-out hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-info)]/30"
|
||||
icon={<ArrowLeft size={15} strokeWidth={2} aria-hidden="true" />}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft size={15} strokeWidth={2} aria-hidden="true" />
|
||||
<span>{t('workbench.backToConversation')}</span>
|
||||
</button>
|
||||
{t('workbench.backToConversation')}
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
role="tablist"
|
||||
@@ -116,24 +119,22 @@ export function WorkbenchPanel({ sessionId, variant = 'panel', onClose }: Workbe
|
||||
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{!isTabVariant && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('workbench.expand')}
|
||||
title={t('workbench.expand')}
|
||||
<IconButton
|
||||
icon={<Maximize2 size={15} strokeWidth={2} aria-hidden="true" />}
|
||||
label={t('workbench.expand')}
|
||||
onClick={handleExpand}
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-info)]/30"
|
||||
>
|
||||
<Maximize2 size={15} strokeWidth={2} aria-hidden="true" />
|
||||
</button>
|
||||
size="sm"
|
||||
tone="muted"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('workbench.close')}
|
||||
<IconButton
|
||||
icon={<X size={16} strokeWidth={2} aria-hidden="true" />}
|
||||
label={t('workbench.close')}
|
||||
onClick={handleClose}
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-info)]/30"
|
||||
>
|
||||
<X size={16} strokeWidth={2} aria-hidden="true" />
|
||||
</button>
|
||||
size="sm"
|
||||
tone="muted"
|
||||
showTooltip={false}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getServerBaseUrl } from '../../lib/desktopRuntime'
|
||||
import { openWithContextForWorkspaceFile } from '../../lib/openWithContextForHref'
|
||||
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
|
||||
import { TargetIcon } from '../common/TargetIcon'
|
||||
import { TargetIcon } from '@/components/composite/TargetIcon'
|
||||
|
||||
function openExternal(path: string) {
|
||||
void getDesktopHost().shell.openPath(path).catch(() => {})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user