···205205 - Transient state propagates to child windows (e.g., cmd → overlay)
206206 - Use `api.izui.isTransient()` to query current session state
207207208208+### Page View Canvas Architecture
209209+210210+**DO NOT REPLACE this architecture.** It was accidentally destroyed on Feb 8 2025 (commit `nnpvvvvk`) when a coordinate-mapping bug in slides was "fixed" by removing the entire canvas model. The real fix was to exclude slides from the canvas path via `useCanvas`. This section documents the restored architecture.
211211+212212+Web pages opened as content or child-content use a **fullscreen transparent canvas**:
213213+214214+1. **Backend** (`ipc.ts`): A `useCanvas` flag determines whether a web page gets the canvas treatment. Canvas pages (`useCanvas = true`) are content/child-content web pages that are not modals, overlays, or quick-views. Non-canvas web pages (slides, modals) load their URL directly in a positioned BrowserWindow.
215215+216216+2. **Fullscreen transparent BrowserWindow**: The canvas window is sized to cover the entire display work area at position (0,0) via `screen.getDisplayNearestPoint()`. Background color is `#00000000` (fully transparent). This creates an invisible surface for positioning UI elements.
217217+218218+3. **JS positioning** (`page.js`): All elements (`<webview>`, navbar, trigger zone, resize handle, mode indicator) are `position: absolute` siblings with NO CSS top/left/right/bottom. JS `updatePositions()` sets inline styles based on a `bounds` object `{x, y, width, height}` parsed from URL params.
219219+220220+4. **Custom drag/resize**: Dragging the navbar updates `bounds.x`/`bounds.y`. Resizing via the corner handle updates `bounds.width`/`bounds.height`. Both call `updatePositions()`. No `-webkit-app-region: drag` — the navbar uses a custom mousedown handler.
221221+222222+5. **Navbar**: Sits ABOVE the webview with an 8px gap. Shown by Cmd+L or hovering the trigger zone. Has rounded corners and grab cursor.
223223+208224### Data Storage
209225210226**Settings Storage (localStorage)**:
+7-21
app/page/index.html
···2020 }
21212222 /*
2323- * Webview fills the entire window.
2424- * When the navbar is visible, the webview shifts down via .navbar-active
2525- * to avoid the Electron <webview> layer rendering over the navbar.
2323+ * Webview — positioned by JS via updatePositions() on the transparent canvas.
2424+ * No explicit top/left/right/bottom — JS sets these based on bounds.
2625 */
2726 webview {
2827 position: absolute;
2929- top: 0; left: 0; right: 0; bottom: 0;
3028 border: none;
3129 border-radius: 10px;
3230 overflow: hidden;
3331 -webkit-mask-image: -webkit-radial-gradient(white, white);
3434- transition: top 0.15s ease;
3535- }
3636-3737- /* Push webview down when navbar is active so it doesn't cover the navbar */
3838- webview.navbar-active {
3939- top: 36px;
4032 }
41334234 /*
4343- * Navbar — full-width bar at the top of the window.
4444- *
4545- * Shown via Cmd+L or hovering near the top of the window.
4646- * Same width as the page, no floating bubble effect.
4747- * Draggable via -webkit-app-region: drag.
3535+ * Navbar — positioned by JS via updatePositions() ABOVE the webview with a gap.
3636+ * Dragging is handled by custom JS (moves bounds), not -webkit-app-region.
4837 */
4938 .navbar {
5039 position: absolute;
5151- top: 0; left: 0; right: 0;
5240 height: 36px;
5341 display: none;
5442 align-items: center;
···6149 font-family: var(--theme-font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif);
6250 font-size: 15px;
6351 border: none;
6464- -webkit-app-region: drag;
5252+ border-radius: 10px;
5353+ cursor: grab;
6554 user-select: none;
6655 -webkit-user-select: none;
6756 z-index: 100;
···7261 }
73627463 .nav-btn {
7575- -webkit-app-region: no-drag;
7664 background: none;
7765 border: none;
7866 color: var(--theme-text-secondary, #bbb);
···10492 }
1059310694 .url-text {
107107- -webkit-app-region: no-drag;
10895 flex: 1;
10996 overflow: hidden;
11097 text-overflow: ellipsis;
···122109 background: var(--theme-bg, rgba(128, 128, 128, 0.18));
123110 }
124111125125- /* Trigger zone at top of window — hover here to reveal the navbar */
112112+ /* Trigger zone — positioned by JS via updatePositions() above the navbar area */
126113 .trigger-zone {
127114 position: absolute;
128128- top: 0; left: 0; right: 0;
129115 height: 50px;
130116 z-index: 50;
131117 }
+127-26
app/page/page.js
···11/**
22- * peek://page - Container for web content
22+ * peek://page - Fullscreen transparent canvas container for web content
33 *
44- * Content-sized BrowserWindow with:
55- * - Webview filling the entire window
66- * - Full-width navbar at the top (Cmd+L or hover near top to show, Escape/click-outside to dismiss)
77- * - When shown, the webview shifts down so the Electron <webview> layer doesn't cover the navbar
88- * - Drag via -webkit-app-region: drag on the navbar
99- * - Resize via IPC to resize the BrowserWindow
44+ * Architecture: A fullscreen transparent BrowserWindow covers the entire display.
55+ * All UI elements (webview, navbar, trigger zone, resize handle, mode indicator)
66+ * are position:absolute siblings, positioned by JS via updatePositions() using
77+ * a `bounds` object. The navbar sits ABOVE the webview with an 8px gap.
88+ *
99+ * - Custom drag: mousedown on navbar moves bounds
1010+ * - Custom resize: mousedown on resize handle changes bounds size
1111+ * - Hover trigger zone above navbar reveals/hides navbar
1212+ * - Cmd+L shows navbar with URL focus
1013 */
11141215import api from '../api.js';
13161417const DEBUG = true;
15181616-// Parse URL parameters
1919+// --- Constants ---
2020+const NAVBAR_HEIGHT = 36;
2121+const NAVBAR_GAP = 8;
2222+const MIN_WIDTH = 200;
2323+const MIN_HEIGHT = 150;
2424+2525+// --- Parse URL parameters ---
1726const params = new URLSearchParams(window.location.search);
1827const targetUrl = params.get('url');
2828+const initialX = parseInt(params.get('x')) || 100;
2929+const initialY = parseInt(params.get('y')) || 100;
3030+const initialWidth = parseInt(params.get('width')) || 800;
3131+const initialHeight = parseInt(params.get('height')) || 600;
19322033if (!targetUrl) {
2134 console.error('[page] No URL provided');
···2336 throw new Error('No URL provided to peek://page');
2437}
25382626-DEBUG && console.log('[page] Loading:', targetUrl);
3939+DEBUG && console.log('[page] Loading:', targetUrl, 'at', initialX, initialY, initialWidth, initialHeight);
27402828-// DOM elements
4141+// --- Bounds state ---
4242+// All positioning is derived from this single object
4343+let bounds = {
4444+ x: initialX,
4545+ y: initialY,
4646+ width: initialWidth,
4747+ height: initialHeight,
4848+};
4949+5050+// --- DOM elements ---
2951const navbar = document.getElementById('navbar');
3052const triggerZone = document.getElementById('trigger-zone');
3153const webview = document.getElementById('content');
···3658const urlText = document.getElementById('url-text');
3759const modeIndicator = document.getElementById('mode-indicator');
38603939-// Set up webview partition for session isolation and load the target URL
6161+// --- Position all elements based on bounds ---
6262+6363+function updatePositions() {
6464+ const { x, y, width, height } = bounds;
6565+6666+ // Webview — the main content area
6767+ webview.style.left = `${x}px`;
6868+ webview.style.top = `${y}px`;
6969+ webview.style.width = `${width}px`;
7070+ webview.style.height = `${height}px`;
7171+7272+ // Navbar — above the webview with a gap
7373+ const navbarTop = Math.max(0, y - NAVBAR_GAP - NAVBAR_HEIGHT);
7474+ navbar.style.left = `${x}px`;
7575+ navbar.style.top = `${navbarTop}px`;
7676+ navbar.style.width = `${width}px`;
7777+7878+ // Trigger zone — covers the area above the webview where hover reveals the navbar
7979+ const triggerTop = Math.max(0, navbarTop - 14);
8080+ triggerZone.style.left = `${x}px`;
8181+ triggerZone.style.top = `${triggerTop}px`;
8282+ triggerZone.style.width = `${width}px`;
8383+8484+ // Resize handle — bottom-right corner of the webview
8585+ resizeHandle.style.left = `${x + width - 16}px`;
8686+ resizeHandle.style.top = `${y + height - 16}px`;
8787+8888+ // Mode indicator — top-right corner of the webview
8989+ if (modeIndicator) {
9090+ modeIndicator.style.left = `${x + width - 120}px`;
9191+ modeIndicator.style.top = `${y + 8}px`;
9292+ }
9393+}
9494+9595+// Initial positioning
9696+updatePositions();
9797+9898+// --- Set up webview partition and load URL ---
9999+40100async function initWebview() {
41101 try {
42102 // Get the partition string for the current profile
···59119// Start initialization
60120initWebview();
611216262-// --- Resize via IPC ---
122122+// --- Custom drag (navbar) ---
123123+124124+let isDragging = false;
125125+let dragStartX = 0;
126126+let dragStartY = 0;
127127+let dragStartBoundsX = 0;
128128+let dragStartBoundsY = 0;
129129+130130+navbar.addEventListener('mousedown', (e) => {
131131+ // Don't start drag on buttons or URL text
132132+ if (e.target.closest('.nav-btn') || e.target.closest('.url-text')) return;
133133+ isDragging = true;
134134+ dragStartX = e.screenX;
135135+ dragStartY = e.screenY;
136136+ dragStartBoundsX = bounds.x;
137137+ dragStartBoundsY = bounds.y;
138138+ navbar.style.cursor = 'grabbing';
139139+ e.preventDefault();
140140+});
141141+142142+// --- Custom resize (resize handle) ---
6314364144let isResizing = false;
145145+let resizeStartX = 0;
146146+let resizeStartY = 0;
147147+let resizeStartWidth = 0;
148148+let resizeStartHeight = 0;
6514966150resizeHandle.addEventListener('mousedown', (e) => {
67151 isResizing = true;
152152+ resizeStartX = e.screenX;
153153+ resizeStartY = e.screenY;
154154+ resizeStartWidth = bounds.width;
155155+ resizeStartHeight = bounds.height;
68156 e.preventDefault();
69157 e.stopPropagation();
70158});
159159+160160+// --- Shared mousemove/mouseup for drag and resize ---
7116172162document.addEventListener('mousemove', (e) => {
7373- if (isResizing) {
7474- const newWidth = Math.max(200, e.screenX - window.screenX);
7575- const newHeight = Math.max(150, e.screenY - window.screenY);
7676- api.invoke('window-set-bounds', { width: newWidth, height: newHeight });
163163+ if (isDragging) {
164164+ const dx = e.screenX - dragStartX;
165165+ const dy = e.screenY - dragStartY;
166166+ bounds.x = dragStartBoundsX + dx;
167167+ bounds.y = dragStartBoundsY + dy;
168168+ updatePositions();
169169+ } else if (isResizing) {
170170+ const dx = e.screenX - resizeStartX;
171171+ const dy = e.screenY - resizeStartY;
172172+ bounds.width = Math.max(MIN_WIDTH, resizeStartWidth + dx);
173173+ bounds.height = Math.max(MIN_HEIGHT, resizeStartHeight + dy);
174174+ updatePositions();
77175 }
78176});
7917780178document.addEventListener('mouseup', () => {
8181- isResizing = false;
179179+ if (isDragging) {
180180+ isDragging = false;
181181+ navbar.style.cursor = 'grab';
182182+ }
183183+ if (isResizing) {
184184+ isResizing = false;
185185+ }
82186});
8318784188// --- State display ---
···89193}
9019491195// --- Show / Hide navbar ---
9292-// The full-width navbar is shown by:
196196+// The navbar is shown by:
93197// 1. Cmd+L (published from main process via before-input-event on guest/host webContents)
9494-// 2. Hovering near the top of the window (trigger zone)
198198+// 2. Hovering near the top of the webview (trigger zone)
95199// It is hidden by clicking outside, pressing Escape, or moving mouse away (hover mode).
9696-//
9797-// When visible, the <webview> element shifts down (via .navbar-active class) so the
9898-// Electron composited webview layer doesn't paint over the navbar.
99200100201let hideTimer = null;
101202let showSource = null; // 'hover' or 'shortcut' — determines dismiss behavior
···107208 }
108209 const wasHidden = !navbar.classList.contains('visible');
109210 navbar.classList.add('visible');
110110- webview.classList.add('navbar-active');
111211 if (opts?.source) showSource = opts.source;
112212 if (wasHidden) {
113213 updateState();
···125225}
126226127227function hide() {
228228+ // Don't hide while dragging
229229+ if (isDragging) return;
128230 if (hideTimer) {
129231 clearTimeout(hideTimer);
130232 hideTimer = null;
131233 }
132234 navbar.classList.remove('visible');
133133- webview.classList.remove('navbar-active');
134235 window.getSelection().removeAllRanges();
135236 showSource = null;
136237 DEBUG && console.log('[page] Navbar hidden');
···152253});
153254154255// --- Hover trigger zone ---
155155-// Mouse entering the thin strip at the top of the window shows the navbar.
256256+// Mouse entering the area above the webview shows the navbar.
156257// Moving away from both the trigger zone AND the navbar hides it (with a small delay).
157258158259function scheduleHide() {
···452553// Initialize mode context
453554initModeContext();
454555455455-DEBUG && console.log('[page] Container initialized for:', targetUrl);
556556+DEBUG && console.log('[page] Canvas container initialized for:', targetUrl);