Multi-issue Windows
Open multiple issues like apps on a desktop — compare and work side by side.
Table of Contents
One issue, one window. Stack several windows on top of the dashboard, drag them around, overlap them, minimize them — the way you treat OS windows. Two screenshots side by side, one issue minimized while you skim another — the workflow falls out naturally.
Desktop Metaphor
The window model deliberately mirrors macOS / Windows window managers. Users already know how to drag the title bar, resize from a corner, double-click to maximize. We reuse those gestures so multi-issue work has zero learning cost.
| OS gesture | QA Note multi-window |
|---|---|
| Drag title bar | Move window |
| Drag corner | Resize window |
| Double-click title bar | Toggle maximize |
| Drag to screen edge | Snap to half / full |
Windows have z-order. Clicking a window raises it.
Title Bar Actions
Each window's title bar has four buttons.
| Button | Icon | Shortcut | Action |
|---|---|---|---|
| Minimize | _ | Cmd/Ctrl+M | Collapse to tray |
| Maximize | □ | Cmd/Ctrl+Shift+F | Expand to full screen (toggle) |
| Open dedicated page | ↗ | Cmd/Ctrl+O | Switch to /issues/[id] |
| Close | × | Cmd/Ctrl+W | Remove window (issue stays) |
Shortcuts apply only to the focused window.
Dedicated Page
Windows are small — data gets compressed. To dig in, hit ↗ on the title bar to switch to the full-screen page (/issues/[id]). The URL changes, so it's shareable.
The full-screen page has a "pop out" button to send it back to a window. Both modes share state, so an in-progress comment survives the switch.
Drag Snapping
Drag a window to a screen edge and it snaps to a region — like Magnet or Rectangle on macOS.
| Drag target | Snap result |
|---|---|
| Left edge | Left half (50%) |
| Right edge | Right half (50%) |
| Top edge | Full screen (maximize) |
| Top corners | Quarter (top-left or top-right) |
Drag a snapped window again to restore its previous size.
Hydration Mismatch Workaround
Window position, size, and open state live in localStorage. Next.js App Router can't read localStorage during SSR, so a naive read on first render causes a hydration mismatch.
The pattern is render empty on first paint, restore inside useEffect.
"use client";
import { useEffect, useState } from "react";
type WindowState = { id: string; x: number; y: number; w: number; h: number };
export function MultiWindowShell() {
const [windows, setWindows] = useState<WindowState[]>([]);
// Restore from localStorage only after first client render → no hydration mismatch
useEffect(() => {
const stored = localStorage.getItem("qanote:windows");
if (stored) setWindows(JSON.parse(stored));
}, []);
// Persist on change
useEffect(() => {
localStorage.setItem("qanote:windows", JSON.stringify(windows));
}, [windows]);
return (
<div className="multi-window-shell">
{windows.map((w) => (
<IssueWindow key={w.id} state={w} />
))}
</div>
);
}
Server render emits windows = []. Hydration runs, then useEffect fills in the real state. Users see a brief flicker — windows appear a beat late — but no hydration warning.
A next/dynamic import with ssr: false works too. Either way, the rule is server and first client render must match.
Limits
- Max concurrent windows: 8. Beyond that, the oldest window auto-collapses to the tray.
- No mobile support: Below 768px viewport width, the UI falls back to the full-screen page. Windows do not make sense on small screens.
- Restore scope: localStorage is per-browser, per-domain. Incognito or a different browser starts empty.
- Min window size: 320×240. Resize stops there.