- Added project renaming capability in the workspace with appropriate API endpoints. - Implemented project deletion functionality, ensuring project directories are removed. - Updated UI to support project renaming and deletion, including inline editing and confirmation dialogs. - Enhanced batch generation feature for project items with selection and progress tracking. - Added tests for project renaming and deletion to ensure functionality and integrity.
255 lines
8.9 KiB
Markdown
255 lines
8.9 KiB
Markdown
# Import Window Responsive 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:** Make the floating import window recompute a viewport-appropriate size and position every time it opens, and stack collection/item panes vertically on narrower viewports.
|
|
|
|
**Architecture:** Keep the existing floating-window shell and collection importer flow, but replace reopen-time geometry reuse with viewport-derived geometry helpers. Add a narrow-layout breakpoint in the template CSS and a resize handler that constrains normal-mode geometry without overriding minimized or maximized state.
|
|
|
|
**Tech Stack:** FastAPI template rendering, inline HTML/CSS/JavaScript, pytest, Node syntax checking
|
|
|
|
---
|
|
|
|
### Task 1: Update UI Contract Tests For Responsive Window Behavior
|
|
|
|
**Files:**
|
|
- Modify: `tests/test_ui.py`
|
|
- Test: `tests/test_ui.py`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```python
|
|
def test_index_has_responsive_import_window_geometry_helpers(tmp_path) -> None:
|
|
html = _get_index_html(tmp_path)
|
|
|
|
assert "function computeResponsiveWindowRect()" in html
|
|
assert "function applyResponsiveWindowRect()" in html
|
|
assert "function syncWindowToViewport()" in html
|
|
assert "window.addEventListener(\"resize\"" in html
|
|
```
|
|
|
|
```python
|
|
def test_index_has_responsive_import_window_layout_rules(tmp_path) -> None:
|
|
html = _get_index_html(tmp_path)
|
|
|
|
assert ".window-pane.items" in html
|
|
assert "@media (max-width: 980px)" in html
|
|
assert ".window-body {" in html
|
|
assert "grid-template-columns: 1fr;" in html
|
|
assert "border-bottom: 1px solid var(--line);" in html
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: FAIL because the new responsive geometry helper names are not present yet.
|
|
|
|
- [ ] **Step 3: Keep the existing syntax guard test**
|
|
|
|
Do not remove `test_index_inline_script_is_valid_javascript`; it remains the regression guard for the inline script.
|
|
|
|
- [ ] **Step 4: Run test to verify the contract file still parses**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py::test_index_inline_script_is_valid_javascript -q`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add tests/test_ui.py
|
|
git commit -m "test: define responsive import window contract"
|
|
```
|
|
|
|
### Task 2: Implement Viewport-Derived Window Geometry
|
|
|
|
**Files:**
|
|
- Modify: `src/zotero_kb/templates/index.html`
|
|
- Test: `tests/test_ui.py`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Use the Task 1 tests as the failing contract. Do not add production code first.
|
|
|
|
- [ ] **Step 2: Run the failing test**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: FAIL with missing responsive helper function names or missing layout assertions.
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Add responsive geometry helpers to the inline script in `src/zotero_kb/templates/index.html`:
|
|
|
|
```javascript
|
|
function computeResponsiveWindowRect() {
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
const margin = viewportWidth <= 980 ? 8 : 24;
|
|
const minWidth = viewportWidth <= 980 ? 320 : 640;
|
|
const minHeight = viewportHeight <= 980 ? 420 : 480;
|
|
const maxWidth = Math.max(minWidth, viewportWidth - margin * 2);
|
|
const maxHeight = Math.max(minHeight, viewportHeight - margin * 2);
|
|
const width = Math.min(maxWidth, Math.max(minWidth, Math.round(viewportWidth * 0.88)));
|
|
const height = Math.min(maxHeight, Math.max(minHeight, Math.round(viewportHeight * 0.82)));
|
|
const left = Math.max(margin, Math.round((viewportWidth - width) / 2));
|
|
const top = Math.max(margin, Math.round((viewportHeight - height) / 2));
|
|
return { width, height, left, top };
|
|
}
|
|
|
|
function applyResponsiveWindowRect() {
|
|
const fw = elements.importWindow;
|
|
if (!fw || state.isWindowMinimized || state.isWindowMaximized) return;
|
|
const rect = computeResponsiveWindowRect();
|
|
fw.style.width = `${rect.width}px`;
|
|
fw.style.height = `${rect.height}px`;
|
|
fw.style.left = `${rect.left}px`;
|
|
fw.style.top = `${rect.top}px`;
|
|
state.windowWidth = rect.width;
|
|
state.windowHeight = rect.height;
|
|
state.windowX = rect.left;
|
|
state.windowY = rect.top;
|
|
}
|
|
|
|
function syncWindowToViewport() {
|
|
const fw = elements.importWindow;
|
|
if (!fw || state.isWindowMinimized || state.isWindowMaximized) return;
|
|
const margin = window.innerWidth <= 980 ? 8 : 24;
|
|
const rect = computeResponsiveWindowRect();
|
|
const currentWidth = parseFloat(fw.style.width) || rect.width;
|
|
const currentHeight = parseFloat(fw.style.height) || rect.height;
|
|
const width = Math.min(currentWidth, rect.width);
|
|
const height = Math.min(currentHeight, rect.height);
|
|
const maxLeft = Math.max(margin, window.innerWidth - width - margin);
|
|
const maxTop = Math.max(margin, window.innerHeight - height - margin);
|
|
const left = Math.min(Math.max(parseFloat(fw.style.left) || rect.left, margin), maxLeft);
|
|
const top = Math.min(Math.max(parseFloat(fw.style.top) || rect.top, margin), maxTop);
|
|
|
|
fw.style.width = `${width}px`;
|
|
fw.style.height = `${height}px`;
|
|
fw.style.left = `${left}px`;
|
|
fw.style.top = `${top}px`;
|
|
state.windowWidth = width;
|
|
state.windowHeight = height;
|
|
state.windowX = left;
|
|
state.windowY = top;
|
|
}
|
|
```
|
|
|
|
Update `openImportWindow()` to call `applyResponsiveWindowRect()` after `initWindowFromSession()` when the window is not minimized or maximized.
|
|
|
|
- [ ] **Step 4: Run the focused tests**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/zotero_kb/templates/index.html tests/test_ui.py
|
|
git commit -m "feat: derive import window geometry from viewport"
|
|
```
|
|
|
|
### Task 3: Add Responsive Layout Switching And Resize Wiring
|
|
|
|
**Files:**
|
|
- Modify: `src/zotero_kb/templates/index.html`
|
|
- Test: `tests/test_ui.py`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Use the layout assertions from Task 1 as the failing contract. No new production code before running them.
|
|
|
|
- [ ] **Step 2: Run the failing test**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: FAIL if the resize listener or stacked layout rules are still missing.
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Extend the existing media query and event wiring in `src/zotero_kb/templates/index.html`:
|
|
|
|
```css
|
|
@media (max-width: 980px) {
|
|
.window-frame {
|
|
min-width: 0;
|
|
width: calc(100vw - 1rem);
|
|
height: calc(100vh - 1rem);
|
|
left: 0.5rem;
|
|
top: 0.5rem;
|
|
}
|
|
.window-toolbar,
|
|
.window-toolbar-actions {
|
|
align-items: stretch;
|
|
}
|
|
.window-body {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.window-pane.collections {
|
|
border-right: 0;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
}
|
|
```
|
|
|
|
Wire the resize handler:
|
|
|
|
```javascript
|
|
window.addEventListener("resize", () => {
|
|
syncWindowToViewport();
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 4: Run the focused tests**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/zotero_kb/templates/index.html tests/test_ui.py
|
|
git commit -m "feat: make import window layout responsive"
|
|
```
|
|
|
|
### Task 4: End-To-End Verification
|
|
|
|
**Files:**
|
|
- Modify: none
|
|
- Test: `tests/test_ui.py`, `tests/test_api.py`
|
|
|
|
- [ ] **Step 1: Run the UI test suite**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q`
|
|
Expected: PASS with all UI contract tests green.
|
|
|
|
- [ ] **Step 2: Run the API regression suite**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_api.py -q`
|
|
Expected: PASS with all API tests green.
|
|
|
|
- [ ] **Step 3: Run fresh JS syntax verification through pytest**
|
|
|
|
Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py::test_index_inline_script_is_valid_javascript -q`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 4: Manual verification**
|
|
|
|
Run the app:
|
|
|
|
```bash
|
|
UV_CACHE_DIR=/tmp/uv-cache uv run python main.py
|
|
```
|
|
|
|
Manual checks:
|
|
|
|
- open the import window at one browser zoom level and note the size
|
|
- close it, change browser zoom, reopen it, confirm it recenters and resizes
|
|
- shrink the viewport below the narrow breakpoint, reopen it, confirm the panes stack vertically
|
|
- maximize and minimize the window, confirm those modes still behave correctly
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/zotero_kb/templates/index.html tests/test_ui.py docs/superpowers/specs/2026-04-16-import-window-responsive-design.md docs/superpowers/plans/2026-04-16-import-window-responsive.md
|
|
git commit -m "feat: make import window adapt to viewport changes"
|
|
```
|