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, collapse 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 |
| Collapse window | Send to the bottom tab bar (Cmd/Ctrl+M) |
Windows have z-order. Clicking a window raises it.
Title Bar Actions
The title bar is 36px tall. Left to right:
| # | Item | Icon | Shortcut | Action |
|---|---|---|---|---|
| 1 | Previous issue | ‹ | — | Swap to the previous issue in the list (dimmed at the start) |
| 2 | Next issue | › | — | Swap to the next issue in the list (dimmed at the end) |
| 3 | Position | — | — | QANOTE-16 · 3 / 37 (issue key only when there is no list) |
| 4 | Title | — | — | Truncated. Highlights for 2.2s when a remote edit lands |
| 5 | Open in full page | ↗ | — | Switch to /issues/[issueNumber] |
| 6 | Collapse window | − | Cmd/Ctrl+M | Send to the bottom tab bar |
| 7 | Maximize ↔ restore | ⤢ | — | Double-clicking the title bar toggles it too |
| 8 | Close window | × | Esc | Remove the window (the issue stays) |
Cmd/Ctrl+M and Esc apply only to the focused (topmost) window, and are ignored while the caret is in an input or editor.
Dedicated Page
Windows are small — data gets compressed. To dig in, hit ↗ on the title bar to switch to the full-screen page (/issues/[issueNumber]). The URL changes, so it's shareable.
The set of open windows is mirrored in the address bar as ?issues=16,12. Copy that URL and the same arrangement comes back.
Collapsed Window Tab Bar
Collapsing a window (the − button or Cmd/Ctrl+M) does not discard it — it moves into a pill-shaped bar centered at the bottom of the screen. Each tab keeps the issue key and title, and a {collapsed} / 8 counter sits at the right end.
| Action | Result |
|---|---|
| Click a tab | Expand the window and focus it |
Click the tab's × | Close the window straight from the bar |
- A collapsed window keeps its position and size. Expanding puts it back exactly where it was.
- The collapsed flag is persisted to
localStorage, so it survives a reload. - When a bulk action bar is showing, the tab bar shifts one step up to clear it.
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. With 8 open, opening another issue adds no window — you get a "You can open up to 8 windows" toast instead. Re-opening an issue that is already open is a focus, and is not subject to the cap.
- Small screens: Windows are clamped into the viewport. Shrink the browser and open windows slide back inside, with width and height reduced to fit.
- Restore scope: localStorage is per-browser, per-domain. Incognito or a different browser starts empty.
- Min window size: 320×240. Resize stops there.
- Not a modal: The page behind is not dimmed and focus is not trapped. You can keep windows open and keep working the list or board behind them.