Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/superpowers/plans/2026-07-29-pet-import-action-order.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions electron/petdex-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
27 changes: 27 additions & 0 deletions electron/petdex-download.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isAllowedPetdexNavigation,
isSupportedPetArchive,
MAX_PETDEX_ARCHIVE_BYTES,
revealSettingsAfterPetdexDownload,
} from './petdex-download.js';

describe('Petdex download policy', () => {
Expand All @@ -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',
]);
});
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
11 changes: 8 additions & 3 deletions src/features/pets/components/PetLibrary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
12 changes: 7 additions & 5 deletions src/features/pets/components/PetLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,13 @@ export function PetLibrary({
aria-label={t('pet.import.chooseFiles')}
onChange={chooseFiles}
/>
<button ref={importTriggerRef} type="button" onClick={() => inputRef.current?.click()}>
{t('pet.import.action')}
</button>
<button type="button" onClick={openPetdex}>{t('pet.import.petdexAction')}</button>
<p className="pet-import-description">{t('pet.import.petdexHint')}</p>
<div className="pet-import-actions">
<button type="button" onClick={openPetdex}>{t('pet.import.petdexAction')}</button>
<button ref={importTriggerRef} type="button" onClick={() => inputRef.current?.click()}>
{t('pet.import.action')}
</button>
<p className="pet-import-description">{t('pet.import.petdexHint')}</p>
</div>
<div
className="pet-drop-zone"
role="button"
Expand Down
11 changes: 11 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,17 @@ input:focus-visible {
min-width: 0;
}

.pet-import-actions {
display: grid;
gap: 0.65rem;
width: min(18rem, 100%);
}

.pet-import-actions > button {
width: 100%;
min-height: 2.75rem;
}

.pet-import-description {
display: flex;
flex-wrap: wrap;
Expand Down
Loading