diff --git a/docs/superpowers/plans/2026-07-29-pet-import-action-order.md b/docs/superpowers/plans/2026-07-29-pet-import-action-order.md new file mode 100644 index 0000000..ff11cbc --- /dev/null +++ b/docs/superpowers/plans/2026-07-29-pet-import-action-order.md @@ -0,0 +1,55 @@ +# Pet Import Action Order Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Place the manual Codex pet import button below the Petdex button and keep both controls the same size. + +**Architecture:** Keep both existing actions and handlers unchanged. Group the controls in one vertical CSS grid so their order and dimensions share a single layout contract. + +**Tech Stack:** React, TypeScript, CSS, Vitest, Testing Library + +## Global Constraints + +- Petdex automatic import remains the first action. +- Manual Codex pet import remains available as the second action. +- Both action buttons use the same width and minimum height. +- Download validation and import processing behavior do not change. + +--- + +### Task 1: Align Pet Import Actions + +**Files:** +- Modify: `src/features/pets/components/PetLibrary.tsx` +- Modify: `src/styles/global.css` +- Test: `src/features/pets/components/PetLibrary.test.tsx` + +**Interfaces:** +- Consumes: Existing `openPetdex` handler and `inputRef` file picker. +- Produces: `.pet-import-actions`, a vertical action group with Petdex first and manual import second. + +- [ ] **Step 1: Write the failing test** + +Assert that both action buttons have the same `.pet-import-actions` parent and that the Petdex button precedes the manual import button. + +- [ ] **Step 2: Run the focused test to verify it fails** + +Run: `npm run test:run -- src/features/pets/components/PetLibrary.test.tsx` + +Expected: FAIL because the buttons are not grouped and manual import currently appears first. + +- [ ] **Step 3: Implement the action group** + +Wrap both buttons and the Petdex hint in `.pet-import-actions`, move the Petdex button above the manual import button, and make direct button children fill the group width with the same minimum height. + +- [ ] **Step 4: Run the focused test** + +Run: `npm run test:run -- src/features/pets/components/PetLibrary.test.tsx` + +Expected: PASS. + +- [ ] **Step 5: Run the desktop release gate** + +Run: `npm run check:desktop-release` + +Expected: All Electron, application, E2E, build, and packaged-resource checks pass. diff --git a/electron/main.js b/electron/main.js index 120ec3c..ea28615 100644 --- a/electron/main.js +++ b/electron/main.js @@ -18,6 +18,7 @@ import { isSupportedPetArchive, MAX_PETDEX_ARCHIVE_BYTES, PETDEX_URL, + revealSettingsAfterPetdexDownload, } from './petdex-download.js'; const IS_DEV = process.argv.includes('--dev') || process.env.NODE_ENV === 'development'; @@ -399,6 +400,7 @@ function registerIpcHandlers() { function sendPetdexImport(payload) { if (settingsWindow === undefined || settingsWindow.isDestroyed()) return; settingsWindow.webContents.send('pet:petdex-import', payload); + revealSettingsAfterPetdexDownload(settingsWindow, petdexWindow); } function handlePetdexDownload(_event, item) { diff --git a/electron/petdex-download.js b/electron/petdex-download.js index 1ecf09e..7702dce 100644 --- a/electron/petdex-download.js +++ b/electron/petdex-download.js @@ -25,3 +25,14 @@ export function isSupportedPetArchive(filename, mimeType = '', rawUrl = '') { || normalizedMime === 'application/zip' || normalizedMime === 'application/x-zip-compressed'; } + +export function revealSettingsAfterPetdexDownload(settingsWindow, petdexWindow) { + if (petdexWindow !== undefined && !petdexWindow.isDestroyed()) { + petdexWindow.hide(); + petdexWindow.close(); + } + if (settingsWindow === undefined || settingsWindow.isDestroyed()) return; + if (settingsWindow.isMinimized()) settingsWindow.restore(); + settingsWindow.show(); + settingsWindow.focus(); +} diff --git a/electron/petdex-download.test.js b/electron/petdex-download.test.js index 3dd6727..b1b9da4 100644 --- a/electron/petdex-download.test.js +++ b/electron/petdex-download.test.js @@ -3,6 +3,7 @@ import { isAllowedPetdexNavigation, isSupportedPetArchive, MAX_PETDEX_ARCHIVE_BYTES, + revealSettingsAfterPetdexDownload, } from './petdex-download.js'; describe('Petdex download policy', () => { @@ -20,4 +21,30 @@ describe('Petdex download policy', () => { expect(isSupportedPetArchive('pet.json', 'application/json')).toBe(false); expect(MAX_PETDEX_ARCHIVE_BYTES).toBe(32 * 1024 * 1024); }); + + it('closes Petdex and reveals the settings window after a download handoff', () => { + const calls = []; + const petdexWindow = { + isDestroyed: () => false, + hide: () => calls.push('hide-petdex'), + close: () => calls.push('close-petdex'), + }; + const settingsWindow = { + isDestroyed: () => false, + isMinimized: () => true, + restore: () => calls.push('restore-settings'), + show: () => calls.push('show-settings'), + focus: () => calls.push('focus-settings'), + }; + + revealSettingsAfterPetdexDownload(settingsWindow, petdexWindow); + + expect(calls).toEqual([ + 'hide-petdex', + 'close-petdex', + 'restore-settings', + 'show-settings', + 'focus-settings', + ]); + }); }); diff --git a/package-lock.json b/package-lock.json index a63c645..7487a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-pet-pause", - "version": "0.2.4", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-pet-pause", - "version": "0.2.4", + "version": "0.2.5", "license": "MIT", "dependencies": { "@zip.js/zip.js": "^2.8.34", diff --git a/package.json b/package.json index 7358439..dd1385f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex-pet-pause", "private": true, - "version": "0.2.4", + "version": "0.2.5", "description": "A playful, local-first break reminder PWA with interactive and Codex-compatible pets.", "license": "MIT", "repository": { diff --git a/src/features/pets/components/PetLibrary.test.tsx b/src/features/pets/components/PetLibrary.test.tsx index eda8572..f9dedd6 100644 --- a/src/features/pets/components/PetLibrary.test.tsx +++ b/src/features/pets/components/PetLibrary.test.tsx @@ -138,9 +138,14 @@ test('opens Petdex in the desktop shell and previews an intercepted ZIP', async }; const { extractArchive } = await renderLibrary(); - await userEvent.setup().click(screen.getByRole('button', { - name: '浏览 Petdex 并自动导入', - })); + const petdexButton = screen.getByRole('button', { name: '浏览 Petdex 并自动导入' }); + const manualImportButton = screen.getByRole('button', { name: '导入 Codex 宠物' }); + expect(petdexButton.parentElement).toBe(manualImportButton.parentElement); + expect(petdexButton.parentElement).toHaveClass('pet-import-actions'); + expect(petdexButton.compareDocumentPosition(manualImportButton) + & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + + await userEvent.setup().click(petdexButton); expect(openPetdex).toHaveBeenCalledOnce(); await act(async () => receiveImport({ diff --git a/src/features/pets/components/PetLibrary.tsx b/src/features/pets/components/PetLibrary.tsx index 47600e0..51adebc 100644 --- a/src/features/pets/components/PetLibrary.tsx +++ b/src/features/pets/components/PetLibrary.tsx @@ -348,11 +348,13 @@ export function PetLibrary({ aria-label={t('pet.import.chooseFiles')} onChange={chooseFiles} /> - - -

{t('pet.import.petdexHint')}

+
+ + +

{t('pet.import.petdexHint')}

+
button { + width: 100%; + min-height: 2.75rem; +} + .pet-import-description { display: flex; flex-wrap: wrap;