diff --git a/README.md b/README.md index 739dd1e..9397a5d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,18 @@ Implemented today: plaintext-vault migration support on desktop. - Today, Notes, Reminders, Settings, profile switching, and diagnostics UI, shared between desktop and Android. +- Desktop Zen Mode (focus icon beside pin) shows unfinished tasks from today's + list, including long-running tasks, one at a time. Next cycles through the + unfinished tasks without changing their status; completed tasks leave the ring. + The final screen says "All done for today" only when no unfinished tasks remain. + Zen Mode hides the normal header and fits window height to the task and Next + button (72–180 px limits), leaving a 4 px gap without unused space underneath, + with an exit button at the top right. Exiting restores the previous window size; + compact geometry is not persisted as the normal size. Task titles wrap, and + task lists and rename dialogs use the same thin scrollbar style as Settings. + Task actions stay beside the title when both fit; otherwise they move above it. + Tasks can be renamed from their inline actions + or details dialog; title changes are persisted and included in VDS sync. - Audio note capture with encrypted desktop audio blobs. - Reminder alert window with snooze and acknowledge actions. - VDS sync server: accounts, device linking, operation-log exchange, diff --git a/apps/desktop/src-tauri/src/commands/overlay.rs b/apps/desktop/src-tauri/src/commands/overlay.rs index d89aab0..289962b 100644 --- a/apps/desktop/src-tauri/src/commands/overlay.rs +++ b/apps/desktop/src-tauri/src/commands/overlay.rs @@ -3,6 +3,27 @@ use tauri_plugin_opener::OpenerExt; const PRIVACY_POLICY_URL: &str = "https://focus.proanima.net/privacy"; +#[tauri::command] +pub(crate) fn get_zen_mode() -> bool { + crate::zen_window::is_active() +} + +#[tauri::command] +pub(crate) fn resize_zen_window(window: tauri::WebviewWindow, height: u32) -> Result<(), String> { + if window.label() != "main" { + return Err("Zen Mode is available in the main window only".into()); + } + crate::zen_window::resize_to_content(&window, height) +} + +#[tauri::command] +pub(crate) fn set_zen_mode(window: tauri::WebviewWindow, enabled: bool) -> Result<(), String> { + if window.label() != "main" { + return Err("Zen Mode is available in the main window only".into()); + } + crate::zen_window::set_mode(&window, enabled) +} + #[tauri::command] pub(crate) fn open_privacy_policy(app: tauri::AppHandle) -> Result<(), String> { app.opener() diff --git a/apps/desktop/src-tauri/src/commands/plan_items.rs b/apps/desktop/src-tauri/src/commands/plan_items.rs index 1a9a1a6..56ac53c 100644 --- a/apps/desktop/src-tauri/src/commands/plan_items.rs +++ b/apps/desktop/src-tauri/src/commands/plan_items.rs @@ -1,6 +1,26 @@ use super::trigger_server_sync; use crate::{db, plan_items, profiles, sync_log}; +#[tauri::command] +pub(crate) fn rename_plan_item( + app: tauri::AppHandle, + db: tauri::State, + hlc_state: tauri::State, + profiles_state: tauri::State, + id: String, + title: String, +) -> Result { + let mut conn = db.0.lock().map_err(|e| e.to_string())?; + let mut clock = hlc_state.0.lock().map_err(|e| e.to_string())?; + let profile_id = profiles::active_profile_id(&profiles_state)?; + let item = plan_items::rename(&mut conn, &mut clock, &profile_id, &id, &title) + .map_err(|e| e.to_string())?; + drop(clock); + drop(conn); + trigger_server_sync(&app); + Ok(item) +} + #[tauri::command] pub(crate) fn list_plan_items( db: tauri::State, diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 4ed947a..14a1c0d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ mod sync_status; #[cfg(feature = "cloud-providers")] mod sync_tokens; mod window_state; +mod zen_window; use serde::Serialize; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; @@ -332,6 +333,10 @@ pub fn run() { commands::plan_items::list_plan_items, commands::plan_items::list_plan_items_range, commands::plan_items::create_plan_item, + commands::plan_items::rename_plan_item, + commands::overlay::set_zen_mode, + commands::overlay::get_zen_mode, + commands::overlay::resize_zen_window, commands::plan_items::toggle_plan_item_done, commands::plan_items::cycle_plan_item_progress, commands::plan_items::toggle_plan_item_deferred, diff --git a/apps/desktop/src-tauri/src/plan_items.rs b/apps/desktop/src-tauri/src/plan_items.rs index f3721a3..c9b7380 100644 --- a/apps/desktop/src-tauri/src/plan_items.rs +++ b/apps/desktop/src-tauri/src/plan_items.rs @@ -2,6 +2,9 @@ use crate::sync_log::{self, HlcClock}; use rusqlite::{params, Connection, Row}; use serde::Serialize; +mod rename; +pub use rename::rename; + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct PlanItemDto { @@ -391,6 +394,37 @@ mod tests { .unwrap() } + #[test] + fn rename_preserves_state_and_records_title_patch() { + let mut conn = setup_conn(); + let mut clock = test_clock(); + let item = create(&mut conn, &mut clock, PROFILE, "Task", TODAY).unwrap(); + toggle_long_running(&mut conn, &mut clock, PROFILE, &item.id).unwrap(); + cycle_progress(&mut conn, &mut clock, PROFILE, &item.id).unwrap(); + let renamed = rename(&mut conn, &mut clock, PROFILE, &item.id, " Updated ").unwrap(); + assert_eq!(renamed.title, "Updated"); + assert_eq!(renamed.status, "partial"); + assert_eq!(renamed.progress_percent, Some(25)); + assert!(renamed.is_long_running); + assert_eq!(renamed.plan_date, TODAY); + let patch: String = conn + .query_row( + "SELECT patch FROM sync_operations WHERE entity_id = ?1 ORDER BY hlc DESC LIMIT 1", + params![item.id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!( + serde_json::from_str::(&patch).unwrap(), + serde_json::json!({"title": "Updated"}) + ); + let count = operation_count(&conn); + assert!(rename(&mut conn, &mut clock, PROFILE, &item.id, " ").is_err()); + assert!(rename(&mut conn, &mut clock, PROFILE, "missing", "Title").is_err()); + assert_eq!(operation_count(&conn), count); + assert_eq!(fetch(&conn, &item.id).unwrap().title, "Updated"); + } + #[test] fn create_starts_open_with_date_and_no_progress() { let mut conn = setup_conn(); diff --git a/apps/desktop/src-tauri/src/plan_items/rename.rs b/apps/desktop/src-tauri/src/plan_items/rename.rs new file mode 100644 index 0000000..7bc0b96 --- /dev/null +++ b/apps/desktop/src-tauri/src/plan_items/rename.rs @@ -0,0 +1,35 @@ +use super::{fetch, PlanItemDto}; +use crate::sync_log::{self, HlcClock}; +use rusqlite::{params, Connection}; + +pub fn rename( + conn: &mut Connection, + clock: &mut HlcClock, + profile_id: &str, + id: &str, + title: &str, +) -> rusqlite::Result { + let title = title.trim(); + if title.is_empty() { + return Err(rusqlite::Error::InvalidParameterName("title".into())); + } + let tx = conn.transaction()?; + fetch(&tx, id)?; + tx.execute( + "UPDATE plan_items SET title = ?1 WHERE id = ?2", + params![title, id], + )?; + let hlc = clock.next(&tx)?; + sync_log::record_operation( + &tx, + &hlc, + profile_id, + "plan_item", + id, + "update", + &serde_json::json!({ "title": title }), + )?; + let dto = fetch(&tx, id)?; + tx.commit()?; + Ok(dto) +} diff --git a/apps/desktop/src-tauri/src/server_sync/apply/tests.rs b/apps/desktop/src-tauri/src/server_sync/apply/tests.rs index 40b85df..047a28e 100644 --- a/apps/desktop/src-tauri/src/server_sync/apply/tests.rs +++ b/apps/desktop/src-tauri/src/server_sync/apply/tests.rs @@ -136,6 +136,41 @@ fn stale_remote_update_does_not_overwrite_newer_local_plan_item() -> Result<(), Ok(()) } +#[test] +fn remote_title_update_renames_plan_item() -> Result<(), String> { + let conn = sync_test_conn(); + conn.execute( + "INSERT INTO plan_items + (id, title, status, progress_percent, plan_date, created_at) + VALUES ('task-1', 'Call', 'open', NULL, '2026-07-07', datetime('now'))", + [], + ) + .map_err(|e| e.to_string())?; + let rename = RemoteOperation { + device_id: "phone-device".to_string(), + entity_id: "task-1".to_string(), + entity_type: "plan_item".to_string(), + hlc: "2026-07-07T10:00:00.000Z-0000-phone-device".to_string(), + op: "update".to_string(), + operation_id: "remote-rename-op".to_string(), + payload_ciphertext: serde_json::json!({ "title": "Call the bank" }).to_string(), + schema_version: 1, + }; + + apply_remote_operation(&conn, "profile-1", "desktop-device", &rename, None)?; + + let (title, status): (String, String) = conn + .query_row( + "SELECT title, status FROM plan_items WHERE id = 'task-1'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|e| e.to_string())?; + assert_eq!(title, "Call the bank"); + assert_eq!(status, "open"); + Ok(()) +} + #[test] fn newer_remote_delete_wins_over_older_local_plan_item() -> Result<(), String> { let conn = sync_test_conn(); diff --git a/apps/desktop/src-tauri/src/server_sync/apply_entities.rs b/apps/desktop/src-tauri/src/server_sync/apply_entities.rs index 3a958ee..f2ae85e 100644 --- a/apps/desktop/src-tauri/src/server_sync/apply_entities.rs +++ b/apps/desktop/src-tauri/src/server_sync/apply_entities.rs @@ -37,6 +37,13 @@ pub(super) fn apply_plan_item( ) } "update" => { + if let Some(title) = string_field(patch, "title") { + conn.execute( + "UPDATE plan_items SET title = ?1 WHERE id = ?2", + params![title, operation.entity_id], + ) + .map_err(|e| e.to_string())?; + } if let Some(status) = string_field(patch, "status") { let progress = patch.get("progressPercent").and_then(Value::as_i64); conn.execute( diff --git a/apps/desktop/src-tauri/src/window_state.rs b/apps/desktop/src-tauri/src/window_state.rs index cd0a5a2..8ec7248 100644 --- a/apps/desktop/src-tauri/src/window_state.rs +++ b/apps/desktop/src-tauri/src/window_state.rs @@ -127,6 +127,9 @@ pub fn reapply_size_after_scale_change( data_dir: &Path, scale_factor: f64, ) { + if crate::zen_window::is_active() { + return; + } let Some(state) = load_state(data_dir) else { return; }; @@ -134,7 +137,7 @@ pub fn reapply_size_after_scale_change( } pub fn save(window: &tauri::WebviewWindow, data_dir: &Path) -> Result<(), String> { - if window.label() != "main" { + if window.label() != "main" || crate::zen_window::is_active() { return Ok(()); } let physical_size = window.outer_size().map_err(|e| e.to_string())?; diff --git a/apps/desktop/src-tauri/src/zen_window.rs b/apps/desktop/src-tauri/src/zen_window.rs new file mode 100644 index 0000000..3196137 --- /dev/null +++ b/apps/desktop/src-tauri/src/zen_window.rs @@ -0,0 +1,79 @@ +//! Temporary desktop geometry; normal window persistence is suspended during Zen Mode. +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; +use tauri::{LogicalSize, WebviewWindow}; + +static ACTIVE: AtomicBool = AtomicBool::new(false); +static NORMAL_SIZE: Mutex>> = Mutex::new(None); + +pub fn is_active() -> bool { + ACTIVE.load(Ordering::SeqCst) +} + +fn apply_geometry(window: &WebviewWindow, zen: bool, size: LogicalSize) -> tauri::Result<()> { + let position = window.outer_position()?; + window.set_max_size(None::>)?; + window.set_min_size(Some(LogicalSize::new( + 280.0, + if zen { 72.0 } else { 260.0 }, + )))?; + window.set_size(size)?; + window.set_max_size(Some(LogicalSize::new( + 520.0, + if zen { 180.0 } else { 1200.0 }, + )))?; + window.set_position(position)?; + Ok(()) +} + +pub fn set_mode(window: &WebviewWindow, enabled: bool) -> Result<(), String> { + let mut normal = NORMAL_SIZE + .lock() + .map_err(|_| "window state unavailable".to_string())?; + if enabled == is_active() { + return Ok(()); + } + if enabled { + let size = window + .inner_size() + .map_err(|e| e.to_string())? + .to_logical::(window.scale_factor().map_err(|e| e.to_string())?); + ACTIVE.store(true, Ordering::SeqCst); + if let Err(error) = apply_geometry(window, true, LogicalSize::new(size.width, 96.0)) { + if apply_geometry(window, false, size).is_err() { + log::warn!("failed to restore normal window geometry after Zen transition"); + } + ACTIVE.store(false, Ordering::SeqCst); + return Err(error.to_string()); + } + *normal = Some(size); + } else if let Some(size) = *normal { + apply_geometry(window, false, size).map_err(|e| e.to_string())?; + ACTIVE.store(false, Ordering::SeqCst); + *normal = None; + } + Ok(()) +} + +pub fn resize_to_content(window: &WebviewWindow, height: u32) -> Result<(), String> { + let _normal = NORMAL_SIZE + .lock() + .map_err(|_| "window state unavailable".to_string())?; + if !is_active() { + return Ok(()); + } + let size = window + .inner_size() + .map_err(|e| e.to_string())? + .to_logical::(window.scale_factor().map_err(|e| e.to_string())?); + let position = window.outer_position().map_err(|e| e.to_string())?; + window + .set_size(LogicalSize::new( + size.width, + f64::from(height.clamp(72, 180)), + )) + .map_err(|e| e.to_string())?; + window.set_position(position).map_err(|e| e.to_string()) +} diff --git a/apps/desktop/src/App.css b/apps/desktop/src/App.css index 4c5e4ca..19db2fb 100644 --- a/apps/desktop/src/App.css +++ b/apps/desktop/src/App.css @@ -2,6 +2,7 @@ @import "./styles/shell.css"; @import "./styles/profile.css"; @import "./styles/plan-items.css"; +@import "./styles/zen.css"; @import "./styles/notes.css"; @import "./styles/reminders.css"; @import "./styles/settings.css"; diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 03e7e2b..6eec8ea 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import App from "./App"; // Раздел 21 ТЗ: "layout snapshots для compact desktop/mobile" — снимок всей @@ -12,6 +13,8 @@ vi.mock("./shared/commands", () => { return { commands: { overlay: { + getZenMode: () => Promise.resolve(false), + resizeZenWindow: () => Promise.resolve(), toggle: reject, onLayerChanged: () => Promise.resolve(() => {}), getShortcutStatus: () => Promise.resolve(null), @@ -104,6 +107,20 @@ function normalizeDynamicDate(container: HTMLElement) { } describe("App layout snapshots", () => { + it("enters Zen Mode from the header and restores navigation on exit", async () => { + isDesktop.mockResolvedValue(true); + const user = userEvent.setup(); + render(); + await user.click(await screen.findByRole("button", { name: "Включить Zen Mode" })); + expect(await screen.findByText("На сегодня всё")).toBeInTheDocument(); + expect(screen.queryByText("FocusNook")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Убрать с переднего плана" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Настройки" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Закрыть" })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText("Добавить дело...")).not.toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Выйти из Zen Mode" })); + expect(await screen.findByText("На сегодня пока ничего не запланировано")).toBeInTheDocument(); + }); it("matches the desktop shell layout", async () => { isDesktop.mockResolvedValue(true); const { container } = render(); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 08ae438..7bbdaa7 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -19,6 +19,9 @@ import { useTheme } from "./shared/useTheme"; import type { ResolvedTheme } from "./shared/theme-context"; import { SettingsPanel } from "./components/SettingsPanel"; import { DayView } from "./components/DayView"; +import { ZenView } from "./components/ZenView"; +import { useZenMode } from "./shared/useZenMode"; +import { ZenHeader } from "./components/ZenHeader"; import { NotesView } from "./components/NotesView"; import { RemindersView } from "./components/RemindersView"; import { ReminderAlert } from "./components/ReminderAlert"; @@ -86,23 +89,26 @@ interface DesktopShellProps { function DesktopShell({ front, toggleLayer, shortcutInfo, theme, folderRailSide, accounts }: DesktopShellProps) { const [activeTab, setActiveTab] = useState("day"); const [showSettings, setShowSettings] = useState(false); + const { active: zenMode, toggle: toggleZen } = useZenMode(); const { activeProfileId, activeProfile, logout } = accounts; const mainTabs = useMainTabs(); return ( -
+
- void toggleZen()} /> : { void toggleZen(); setShowSettings(false); }} front={front} onToggleLayer={toggleLayer} showSettings={showSettings} onToggleSettings={() => setShowSettings((value) => !value)} account={activeProfile!} onLogout={() => void logout()} - /> + />} - {!showSettings && } + {!showSettings && !zenMode && }
{showSettings ? ( @@ -111,6 +117,8 @@ function DesktopShell({ front, toggleLayer, shortcutInfo, theme, folderRailSide, onClose={() => setShowSettings(false)} isDesktop /> + ) : zenMode ? ( + ) : ( )}
- +
); diff --git a/apps/desktop/src/__snapshots__/App.test.tsx.snap b/apps/desktop/src/__snapshots__/App.test.tsx.snap index eb1e6a4..e481128 100644 --- a/apps/desktop/src/__snapshots__/App.test.tsx.snap +++ b/apps/desktop/src/__snapshots__/App.test.tsx.snap @@ -39,8 +39,47 @@ exports[`App layout snapshots > matches the desktop shell layout 1`] = `
+ @@ -27,6 +28,8 @@ function HeaderButton({ } interface OverlayHeaderProps { + zenMode: boolean; + onToggleZen: () => void; front: boolean; onToggleLayer: () => void; showSettings: boolean; @@ -38,6 +41,8 @@ interface OverlayHeaderProps { // Только для десктопа (см. App.tsx::DesktopShell) — на Android своя шапка // (MobileShell), always-on-top и это меню там не нужны. export function OverlayHeader({ + zenMode, + onToggleZen, front, onToggleLayer, showSettings, @@ -56,6 +61,7 @@ export function OverlayHeader({ FocusNook
+ void; onToggleLongRunning: () => void; onCycleProgress: () => void; onToggleDeferred: () => void; @@ -54,6 +56,7 @@ export function PlanItemDetailsDialog({ return (

{item.title}

+ {item.status !== "partial" && ( {item.status === "partial" && ( diff --git a/apps/desktop/src/components/PlanItemRenameDialog.test.tsx b/apps/desktop/src/components/PlanItemRenameDialog.test.tsx new file mode 100644 index 0000000..ea99888 --- /dev/null +++ b/apps/desktop/src/components/PlanItemRenameDialog.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { PlanItemRenameDialog } from "./PlanItemRenameDialog"; +import type { PlanItem } from "../shared/commands"; + +const item: PlanItem = { id: "test", title: "Old", status: "open", planDate: "2026-10-12", progressPercent: null, isLongRunning: false }; +describe("PlanItemRenameDialog", () => { + it("prefills, trims and saves a title", async () => { + const user = userEvent.setup(); + const save = vi.fn().mockResolvedValue({ ...item, title: "New" }); + const close = vi.fn(); + render(); + const input = screen.getByRole("textbox", { name: "Переименовать" }); + expect(input).toHaveValue("Old"); + await user.clear(input); + expect(screen.getByRole("button", { name: "Сохранить" })).toBeDisabled(); + await user.type(input, " New "); + await user.click(screen.getByRole("button", { name: "Сохранить" })); + expect(save).toHaveBeenCalledWith("test", "New"); + expect(close).toHaveBeenCalledOnce(); + }); + it("keeps the draft when saving fails and allows retry", async () => { + const user = userEvent.setup(); + const save = vi.fn().mockResolvedValue(null); + const close = vi.fn(); + render(); + await user.click(screen.getByRole("button", { name: "Сохранить" })); + expect(await screen.findByRole("alert")).toBeInTheDocument(); + expect(close).not.toHaveBeenCalled(); + expect(screen.getByRole("textbox")).toHaveValue("Old"); + }); +}); diff --git a/apps/desktop/src/components/PlanItemRenameDialog.tsx b/apps/desktop/src/components/PlanItemRenameDialog.tsx new file mode 100644 index 0000000..5091b6a --- /dev/null +++ b/apps/desktop/src/components/PlanItemRenameDialog.tsx @@ -0,0 +1,36 @@ +import { useState, type FormEvent } from "react"; +import type { PlanItem } from "../shared/commands"; +import { useLocale } from "../shared/useLocale"; +import { ItemDetailsDialog } from "./ItemDetailsDialog"; + +export function PlanItemRenameDialog({ item, onSave, onClose }: { + item: PlanItem; + onSave: (id: string, title: string) => Promise; + onClose: () => void; +}) { + const { t } = useLocale(); + const [title, setTitle] = useState(item.title); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(false); + async function submit(event: FormEvent) { + event.preventDefault(); + if (!title.trim() || saving) return; + setSaving(true); + const updated = await onSave(item.id, title.trim()); + setSaving(false); + if (updated) onClose(); + else setError(true); + } + return ( + + void submit(event)}> + + {error &&

{t("day.renameError")}

} +
+ + +
+ +
+ ); +} diff --git a/apps/desktop/src/components/WindowResizeHandles.test.tsx b/apps/desktop/src/components/WindowResizeHandles.test.tsx index 64bb82e..6cc45c4 100644 --- a/apps/desktop/src/components/WindowResizeHandles.test.tsx +++ b/apps/desktop/src/components/WindowResizeHandles.test.tsx @@ -33,4 +33,9 @@ describe("WindowResizeHandles", () => { expect(resizeBy).toHaveBeenCalledWith(16, 0); }); + it("uses compact height limits for Zen keyboard resizing", () => { + render(); + fireEvent.keyDown(screen.getByRole("button", { name: "header.resize" }), { key: "ArrowDown" }); + expect(resizeBy).toHaveBeenCalledWith(0, 16, true); + }); }); diff --git a/apps/desktop/src/components/WindowResizeHandles.tsx b/apps/desktop/src/components/WindowResizeHandles.tsx index 6aeeb5f..ef7e139 100644 --- a/apps/desktop/src/components/WindowResizeHandles.tsx +++ b/apps/desktop/src/components/WindowResizeHandles.tsx @@ -18,7 +18,7 @@ function startResize(direction: ResizeDirection, event: PointerEvent) { void commands.overlay.startResize(direction); } -function keyboardResize(event: KeyboardEvent) { +function keyboardResize(event: KeyboardEvent, zenMode: boolean) { const step = event.shiftKey ? 48 : 16; const delta = { ArrowLeft: [-step, 0], @@ -28,10 +28,11 @@ function keyboardResize(event: KeyboardEvent) { }[event.key]; if (!delta) return; event.preventDefault(); - void commands.overlay.resizeBy(delta[0], delta[1]); + if (zenMode) void commands.overlay.resizeBy(delta[0], delta[1], true); + else void commands.overlay.resizeBy(delta[0], delta[1]); } -export function WindowResizeHandles() { +export function WindowResizeHandles({ zenMode = false }: { zenMode?: boolean }) { const { t } = useLocale(); return ( <> @@ -50,7 +51,7 @@ export function WindowResizeHandles() { type="button" aria-label={t("header.resize")} title={t("header.resize")} - onKeyDown={keyboardResize} + onKeyDown={(event) => keyboardResize(event, zenMode)} onPointerDown={(event) => startResize("SouthEast", event)} >