fix(library): use ghost cancel buttons in migrate-data dialog for e-ink (#4396) (#4422)

The Change Data Location dialog rendered its Cancel/Close (secondary)
buttons with `btn-outline`. Under `[data-eink='true']`, globals.css
inverts both `.btn-outline` and `.btn-primary` to the same base-content
fill + base-100 text, so Cancel and Start Migration collapsed into two
identical black buttons and became indistinguishable on e-ink screens.

Switch the secondary buttons to `btn-ghost`, matching the design-system
rule (DESIGN.md): the primary CTA keeps its solid fill while the ghost
cancel reads as borderless next to it, restoring the hierarchy.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-02 22:36:57 +08:00
committed by GitHub
parent fe853554a9
commit f9ddddb6ac
2 changed files with 86 additions and 2 deletions
@@ -0,0 +1,84 @@
import { act, cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
MigrateDataWindow,
setMigrateDataDirDialogVisible,
} from '@/app/library/components/MigrateDataWindow';
vi.mock('@/hooks/useTranslation', () => ({
useTranslation: () => (key: string, options?: Record<string, string | number>) => {
if (!options) return key;
return key.replace(/{{(\w+)}}/g, (_match, name) => String(options[name] ?? ''));
},
}));
const appService = {
isAndroidApp: false,
isDesktopApp: true,
distChannel: 'github',
resolveFilePath: vi.fn(async () => '/current/data/dir'),
readDirectory: vi.fn(async () => []),
};
vi.mock('@/context/EnvContext', () => ({
useEnv: () => ({ appService, envConfig: {} }),
}));
vi.mock('@/store/settingsStore', () => ({
useSettingsStore: () => ({ settings: {}, setSettings: vi.fn(), saveSettings: vi.fn() }),
}));
vi.mock('@tauri-apps/api/path', () => ({
documentDir: vi.fn(async () => '/docs'),
join: vi.fn(async (...parts: string[]) => parts.join('/')),
}));
vi.mock('@tauri-apps/plugin-process', () => ({ relaunch: vi.fn() }));
vi.mock('@tauri-apps/plugin-opener', () => ({ revealItemInDir: vi.fn() }));
vi.mock('@/utils/bridge', () => ({ getExternalSDCardPath: vi.fn(async () => ({ path: '' })) }));
vi.mock('@/utils/permission', () => ({ requestStoragePermission: vi.fn(async () => true) }));
// Preserve the dialog id so the component's getElementById event wiring works.
vi.mock('@/components/Dialog', () => ({
__esModule: true,
default: ({
id,
title,
children,
}: {
id?: string;
title?: string;
children: React.ReactNode;
}) => (
<div id={id} role='dialog' aria-label={title}>
{children}
</div>
),
}));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe('MigrateDataWindow e-ink button hierarchy', () => {
it('uses btn-ghost (not btn-outline) for the Cancel button so it stays distinct from the primary CTA in e-ink', async () => {
render(<MigrateDataWindow />);
await act(async () => {
setMigrateDataDirDialogVisible(true);
});
const cancelButton = await screen.findByRole('button', { name: 'Cancel' });
const startButton = screen.getByRole('button', { name: 'Start Migration' });
// The primary CTA keeps btn-primary, which e-ink inverts to a solid black fill.
expect(startButton.className).toContain('btn-primary');
// The Cancel button must NOT use btn-outline: in e-ink, btn-outline inverts
// to the SAME solid black fill as btn-primary, leaving the two buttons
// indistinguishable (see issue #4396). It must be a borderless ghost instead.
expect(cancelButton.className).not.toContain('btn-outline');
expect(cancelButton.className).toContain('btn-ghost');
});
});
@@ -448,7 +448,7 @@ export const MigrateDataWindow = () => {
<div className='flex gap-3 pt-2'>
{migrationStatus === 'completed' ? (
<>
<button className='btn btn-outline flex-1' onClick={handleClose}>
<button className='btn btn-ghost flex-1' onClick={handleClose}>
{_('Close')}
</button>
<button className='btn btn-primary flex-1' onClick={handleRestartApp}>
@@ -458,7 +458,7 @@ export const MigrateDataWindow = () => {
) : (
<>
<button
className='btn btn-outline flex-1'
className='btn btn-ghost flex-1'
onClick={handleClose}
disabled={migrationStatus === 'migrating'}
>