From cb438a34db0ac07fb9fbc4a3e5cfd487f25189b5 Mon Sep 17 00:00:00 2001 From: Misaka Date: Wed, 1 Apr 2026 20:56:52 +0800 Subject: [PATCH] feat(ui): theme-adapt LogPanel/ProgressBar and remove stale deps Replace hardcoded slate-* colors with shadcn theme tokens (bg-card, text-foreground, text-muted-foreground) in LogPanel and SegmentedProgressBar. Remove react-focus-lock and useDialogFocus hook no longer needed after Modal migration to Radix Dialog. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 61 +--- package.json | 1 - src/renderer/src/components/ui/LogPanel.tsx | 24 +- .../components/ui/SegmentedProgressBar.tsx | 18 +- src/renderer/src/hooks/useDialogFocus.ts | 322 ------------------ 5 files changed, 25 insertions(+), 401 deletions(-) delete mode 100644 src/renderer/src/hooks/useDialogFocus.ts diff --git a/package-lock.json b/package-lock.json index 52d1b29..a4c7d55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "mysql2": "^3.18.2", "playwright": "^1.58.2", "playwright-core": "^1.58.2", - "react-focus-lock": "^2.13.7", "react-markdown": "^10.1.0", "recharts": "^3.8.0", "reflect-metadata": "^0.2.2", @@ -1451,15 +1450,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -9578,18 +9568,6 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/focus-lock": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.6.tgz", - "integrity": "sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -11226,6 +11204,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -11962,6 +11941,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -13524,6 +13504,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14192,6 +14173,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -14263,18 +14245,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-clientside-effect": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz", - "integrity": "sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", @@ -14288,29 +14258,6 @@ "react": "^19.2.4" } }, - "node_modules/react-focus-lock": { - "version": "2.13.7", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.7.tgz", - "integrity": "sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^1.3.6", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.7", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 06d75a1..857cdac 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "mysql2": "^3.18.2", "playwright": "^1.58.2", "playwright-core": "^1.58.2", - "react-focus-lock": "^2.13.7", "react-markdown": "^10.1.0", "recharts": "^3.8.0", "reflect-metadata": "^0.2.2", diff --git a/src/renderer/src/components/ui/LogPanel.tsx b/src/renderer/src/components/ui/LogPanel.tsx index e174ca5..d5433b7 100644 --- a/src/renderer/src/components/ui/LogPanel.tsx +++ b/src/renderer/src/components/ui/LogPanel.tsx @@ -10,15 +10,15 @@ interface LogPanelProps { const getLogColor = (level: LogLevel): string => { switch (level) { case 'error': - return 'text-red-400' + return 'text-red-500 dark:text-red-400' case 'warning': - return 'text-amber-400' + return 'text-amber-500 dark:text-amber-400' case 'success': - return 'text-emerald-400' + return 'text-emerald-500 dark:text-emerald-400' case 'system': - return 'text-blue-400' + return 'text-blue-500 dark:text-blue-400' default: - return 'text-slate-400' + return 'text-muted-foreground' } } @@ -30,14 +30,14 @@ const LogPanel: React.FC = ({ logs, onClear }) => { }, [logs]) return ( -
-
-
+
+
+
执行日志
{logs.length === 0 ? ( -
等待执行...
+
等待执行...
) : ( logs.map((log, index) => (
- [{log.timestamp}]{' '} - [{log.level.toUpperCase()}] {log.message} + [{log.timestamp}]{' '} + [{log.level.toUpperCase()}] {log.message}
)) )} diff --git a/src/renderer/src/components/ui/SegmentedProgressBar.tsx b/src/renderer/src/components/ui/SegmentedProgressBar.tsx index 741da37..3e22cda 100644 --- a/src/renderer/src/components/ui/SegmentedProgressBar.tsx +++ b/src/renderer/src/components/ui/SegmentedProgressBar.tsx @@ -60,13 +60,13 @@ export const SegmentedProgressBar: React.FC = ({ const getStatusDot = (status: string) => { if (status === 'completed') return 'bg-emerald-600' if (status === 'active') return 'bg-blue-600 animate-pulse' - return 'bg-slate-300' + return 'bg-muted-foreground/40' } const getStatusText = (status: string) => { if (status === 'completed') return 'text-emerald-600' if (status === 'active') return 'text-blue-600 font-semibold' - return 'text-slate-400' + return 'text-muted-foreground' } const getDetailText = () => { @@ -121,7 +121,7 @@ export const SegmentedProgressBar: React.FC = ({ const segments = getSegments() return ( -
+
{/* 阶段标签 */}
{PHASES.map((p, index) => { @@ -139,7 +139,7 @@ export const SegmentedProgressBar: React.FC = ({
{/* 分段进度条 */} -
+
{segments.map((segment, index) => { const prevEnd = index === 0 ? 0 : segments[index - 1].end const segmentWidth = segment.end - prevEnd @@ -161,7 +161,7 @@ export const SegmentedProgressBar: React.FC = ({ }} /> {index < PHASES.length - 1 && ( -
+
)}
) @@ -170,11 +170,11 @@ export const SegmentedProgressBar: React.FC = ({ {/* 详细信息 */}
-
- 当前进度: - {getDetailText()} +
+ 当前进度: + {getDetailText()}
-
{Math.round(progress)}%
+
{Math.round(progress)}%
) diff --git a/src/renderer/src/hooks/useDialogFocus.ts b/src/renderer/src/hooks/useDialogFocus.ts deleted file mode 100644 index 03dfd8b..0000000 --- a/src/renderer/src/hooks/useDialogFocus.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { useEffect, RefObject } from 'react' - -/** - * Options for configuring dialog focus management - */ -export interface UseDialogFocusOptions { - /** Whether the dialog is currently open */ - isOpen: boolean - /** Ref to the dialog container element */ - dialogRef: RefObject - /** Callback to close the dialog */ - onClose: () => void - /** Optional ref to the element that triggered opening the dialog */ - triggerRef?: RefObject - /** Optional selector for the element to focus initially inside the dialog */ - initialFocusSelector?: string - /** Whether to lock body scroll when dialog is open (default: true) */ - lockBodyScroll?: boolean - /** - * Whether Escape key should close the dialog (default: true) - * Can be a boolean or a function that receives the keyboard event and returns a boolean - */ - shouldCloseOnEscape?: boolean | ((event: KeyboardEvent) => boolean) -} - -/** - * Return type for useDialogFocus hook - */ -export interface UseDialogFocusReturn { - /** Whether focus lock should be enabled */ - focusLockEnabled: boolean - /** Props to spread on FocusLock component */ - focusLockProps: { - disabled: boolean - } -} - -/** - * React Hook for managing focus in modal dialogs - * - * Integrates with react-focus-lock to provide: - * - Focus trapping within dialog - * - Initial focus management (first interactive element or custom selector) - * - Focus restoration to trigger element on close - * - Escape key handling - * - Body scroll locking - * - * @param options - Configuration options for focus management - * @returns Focus lock props and state - * - * @example - * ```typescript - * function MyDialog({ isOpen, onClose, isExecuting }) { - * const dialogRef = useRef(null) - * const triggerRef = useRef(null) - * const { focusLockEnabled, focusLockProps } = useDialogFocus({ - * isOpen, - * dialogRef, - * onClose, - * triggerRef, - * shouldCloseOnEscape: () => !isExecuting // Only close when not executing - * }) - * - * return ( - * - *
- *

Dialog Title

- * - *
- *
- * ) - * } - * ``` - */ -export function useDialogFocus(options: UseDialogFocusOptions): UseDialogFocusReturn { - const { - isOpen, - dialogRef, - onClose, - triggerRef, - initialFocusSelector, - lockBodyScroll = true, - shouldCloseOnEscape = true - } = options - - // Handle Escape key press - useEffect(() => { - if (!isOpen) return - - const handleKeyDown = (event: KeyboardEvent): void => { - if (event.key === 'Escape' || event.keyCode === 27) { - // Check if we should close on escape - let shouldClose = true - if (shouldCloseOnEscape !== undefined) { - if (typeof shouldCloseOnEscape === 'function') { - shouldClose = shouldCloseOnEscape(event) - } else { - shouldClose = shouldCloseOnEscape - } - } - - if (shouldClose) { - event.preventDefault() - event.stopPropagation() - onClose() - } - } - } - - window.addEventListener('keydown', handleKeyDown) - return (): void => { - window.removeEventListener('keydown', handleKeyDown) - } - }, [isOpen, onClose, shouldCloseOnEscape]) - - // Manage body scroll locking - useEffect(() => { - if (!lockBodyScroll) return - - if (isOpen) { - // Store current scroll position - const scrollY = window.scrollY - document.body.style.position = 'fixed' - document.body.style.top = `-${scrollY}px` - document.body.style.left = '0' - document.body.style.right = '0' - document.body.style.overflow = 'hidden' - } else { - // Restore scroll position - const scrollY = document.body.style.top - document.body.style.position = '' - document.body.style.top = '' - document.body.style.left = '' - document.body.style.right = '' - document.body.style.overflow = '' - if (scrollY) { - window.scrollTo(0, parseInt(scrollY, 10) * -1) - } - } - - return (): void => { - // Cleanup on unmount or when isOpen changes - if (isOpen) { - const scrollY = document.body.style.top - document.body.style.position = '' - document.body.style.top = '' - document.body.style.left = '' - document.body.style.right = '' - document.body.style.overflow = '' - if (scrollY) { - window.scrollTo(0, parseInt(scrollY, 10) * -1) - } - } - } - }, [isOpen, lockBodyScroll]) - - // Manage initial focus when dialog opens - useEffect(() => { - if (!isOpen || !dialogRef.current) return - - const setupFocus = (): void => { - const dialogElement = dialogRef.current - if (!dialogElement) return - - // If initialFocusSelector is provided, try to focus that element - if (initialFocusSelector) { - const focusElement = dialogElement.querySelector(initialFocusSelector) as HTMLElement - if (focusElement && typeof focusElement.focus === 'function') { - // Check if element is visible and focusable - const style = window.getComputedStyle(focusElement) - if (style.display !== 'none' && style.visibility !== 'hidden') { - requestAnimationFrame(() => { - focusElement.focus({ preventScroll: true }) - }) - return - } - // Fallback: element found but not visible, log warning and try default - console.warn(`Focus element found but not visible: ${initialFocusSelector}`) - } else { - // Fallback: element not found, log warning and try default - console.warn(`Focus element not found for selector: ${initialFocusSelector}`) - } - } - - // Otherwise, focus the first interactive element - const focusableSelectors = [ - 'button:not([disabled]):not([tabindex="-1"])', - 'a[href]', - 'input:not([disabled]):not([tabindex="-1"])', - 'select:not([disabled]):not([tabindex="-1"])', - 'textarea:not([disabled]):not([tabindex="-1"])', - '[tabindex]:not([tabindex="-1"])' - ] - const firstFocusable = dialogElement.querySelector( - focusableSelectors.join(', ') - ) as HTMLElement - if (firstFocusable && typeof firstFocusable.focus === 'function') { - requestAnimationFrame(() => { - firstFocusable.focus({ preventScroll: true }) - }) - } - } - - // Delay to ensure portal content is rendered - requestAnimationFrame(setupFocus) - }, [isOpen, dialogRef, initialFocusSelector]) - - // Restore focus to trigger element when dialog closes - useEffect(() => { - if (isOpen || !triggerRef?.current) return - - const restoreFocus = (): void => { - const triggerElement = triggerRef.current - - // Check if element still exists in DOM - if (!triggerElement || !document.contains(triggerElement)) { - if (import.meta.env.DEV) { - console.warn('[useDialogFocus] Trigger element not found in DOM, cannot restore focus') - } - return - } - - // Check if element has a focus method - if (typeof triggerElement.focus !== 'function') { - if (import.meta.env.DEV) { - console.warn('[useDialogFocus] Trigger element does not have a focus method') - } - return - } - - // Check if element is visible (not display: none) - const style = window.getComputedStyle(triggerElement) - if (style.display === 'none') { - if (import.meta.env.DEV) { - console.warn('[useDialogFocus] Trigger element is display: none, cannot restore focus') - } - return - } - - if (style.visibility === 'hidden') { - if (import.meta.env.DEV) { - console.warn( - '[useDialogFocus] Trigger element is visibility: hidden, cannot restore focus' - ) - } - return - } - - // Check if element is disabled - if (triggerElement instanceof HTMLButtonElement && triggerElement.disabled) { - if (import.meta.env.DEV) { - console.warn('[useDialogFocus] Trigger element is disabled, cannot restore focus') - } - // Try to find nearest enabled ancestor or fallback to body - const focusableParent = findNearestFocusableElement(triggerElement) - if (focusableParent) { - focusableParent.focus({ preventScroll: true }) - if (import.meta.env.DEV) { - console.info('[useDialogFocus] Restored focus to nearest focusable ancestor') - } - } - return - } - - // All checks passed, restore focus - try { - triggerElement.focus({ preventScroll: true }) - if (import.meta.env.DEV) { - console.info('[useDialogFocus] Successfully restored focus to trigger element') - } - } catch (error) { - if (import.meta.env.DEV) { - console.error('[useDialogFocus] Error restoring focus:', error) - } - } - } - - // Helper function to find nearest focusable ancestor - const findNearestFocusableElement = (element: HTMLElement): HTMLElement | null => { - let parent = element.parentElement - const focusableSelectors = [ - 'button:not([disabled]):not([tabindex="-1"])', - 'a[href]', - 'input:not([disabled]):not([tabindex="-1"])', - 'select:not([disabled]):not([tabindex="-1"])', - 'textarea:not([disabled]):not([tabindex="-1"])', - '[tabindex]:not([tabindex="-1"])' - ] - - while (parent && parent !== document.body) { - // Check if parent itself is focusable - if ( - focusableSelectors.some((selector) => parent?.matches(selector)) && - window.getComputedStyle(parent).display !== 'none' && - window.getComputedStyle(parent).visibility !== 'hidden' - ) { - return parent - } - // Check if parent contains focusable element - const focusableChild = parent.querySelector(focusableSelectors.join(', ')) as HTMLElement - if (focusableChild) { - return focusableChild - } - parent = parent.parentElement - } - return null - } - - // Use microtask queue to ensure this runs after DOM cleanup - queueMicrotask(restoreFocus) - }, [isOpen, triggerRef]) - - // Return focus lock configuration - return { - focusLockEnabled: isOpen, - focusLockProps: { - disabled: !isOpen - } - } -} - -export default useDialogFocus