zotero-kb/docs/superpowers/plans/2026-04-16-import-window-responsive.md
Saberlve cf7e2feb19 feat: Implement project management features including rename and delete functionality
- 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.
2026-04-21 16:55:42 +08:00

8.9 KiB

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

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
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
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:

      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
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:

      @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:

      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
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:

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

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"