From 45e6ad3fe18f4edf156db2f398f91d6242eec545 Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 12 Aug 2026 13:16:40 +0300 Subject: [PATCH] feat(ui): configure compact panel behavior --- frontend/src/app/settings/page.tsx | 61 +++++++++++++++++++ frontend/src/components/AppShell.tsx | 41 +++++++++++-- .../components/calendar/CalendarSidebar.tsx | 28 +++++++-- .../hooks/useDesktopCompactPanelBehavior.ts | 46 ++++++++++++++ 4 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 frontend/src/hooks/useDesktopCompactPanelBehavior.ts diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 86e4b24c..5ee72ec6 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -17,6 +17,8 @@ import { Mail, Monitor, Moon, + MousePointer2, + PanelLeftClose, QrCode, Save, Smartphone, @@ -28,6 +30,7 @@ import { AppShell } from "@/components/AppShell"; import NotificationDeliveryApps from "@/components/settings/NotificationDeliveryApps"; import { usePwa } from "@/contexts/PwaContext"; import { useMobileNavPlacement } from "@/contexts/MobileNavPlacementContext"; +import { useDesktopCompactPanelBehavior } from "@/hooks/useDesktopCompactPanelBehavior"; import { formatSessionDateTime, getSessionDeviceKind, @@ -62,6 +65,21 @@ const mobileNavCards = [ }, ] as const; +const desktopCompactPanelCards = [ + { + value: "hover", + title: "Раскрывать при наведении", + description: "Меню и календарь временно выезжают поверх страницы при наведении мыши.", + Icon: MousePointer2, + }, + { + value: "compact", + title: "Всегда компактные", + description: "Меню и календарь остаются в виде узких панелей со значками.", + Icon: PanelLeftClose, + }, +] as const; + function getSessionDeviceIcon(deviceName?: string | null) { return getSessionDeviceKind(deviceName) === "mobile" ? Smartphone : Monitor; } @@ -136,6 +154,7 @@ function SectionCard({ export default function SettingsPage() { const { mobileNavPlacement, setMobileNavPlacement } = useMobileNavPlacement(); + const [desktopCompactPanelBehavior, setDesktopCompactPanelBehavior] = useDesktopCompactPanelBehavior(); const { canInstall, install, isInstalled, isRegistrationReady } = usePwa(); const { activeVerbCount, @@ -416,6 +435,48 @@ export default function SettingsPage() { })} + +
+
+

Боковые панели в расширенном режиме

+

+ Определяет поведение компактного меню и календаря на этом устройстве. +

+
+ +
+ {desktopCompactPanelCards.map(({ value, title, description, Icon }) => { + const active = desktopCompactPanelBehavior === value; + + return ( + + ); + })} +
+
{ + if (compact && expandOnHover) setHoverExpanded(true); + }} + onMouseLeave={() => setHoverExpanded(false)} + onFocus={() => { + if (compact && expandOnHover) setHoverExpanded(true); + }} + onBlur={(event) => { + if (!event.currentTarget.contains(event.relatedTarget)) setHoverExpanded(false); + }} > - +
); @@ -605,6 +627,8 @@ export function AppShell({ }: AppShellProps) { const { user, loading, status: authStatus, error: authError, retryAuthentication } = useUser(); const { mobileNavPlacement } = useMobileNavPlacement(); + const [desktopCompactPanelBehavior] = useDesktopCompactPanelBehavior(); + const expandCompactPanelsOnHover = desktopCompactPanelBehavior === "hover"; const router = useRouter(); const pathname = usePathname(); const [isMobileLeftNavOpen, setIsMobileLeftNavOpen] = useState(false); @@ -708,7 +732,13 @@ export function AppShell({ />
- {!desktopImmersiveMode && !standaloneAppMode ? : null} + {!desktopImmersiveMode && !standaloneAppMode ? ( + + ) : null}
{!mobileFullscreen && !standaloneAppMode && !suppressPushOnboardingPrompt ? : null} {children} @@ -723,6 +753,7 @@ export function AppShell({ onCalendarChange={cal.handleCalendarChange} fixedDesktop compact={desktopWideMode} + expandOnHover={expandCompactPanelsOnHover} onExpand={() => onDesktopWideModeChange?.(false)} /> : null}
diff --git a/frontend/src/components/calendar/CalendarSidebar.tsx b/frontend/src/components/calendar/CalendarSidebar.tsx index 3f56c868..e42adcde 100644 --- a/frontend/src/components/calendar/CalendarSidebar.tsx +++ b/frontend/src/components/calendar/CalendarSidebar.tsx @@ -1,7 +1,7 @@ "use client"; import { CalendarDays } from "lucide-react"; -import { memo } from "react"; +import { memo, useState } from "react"; import { CalendarCard } from "@/components/calendar/CalendarCard"; import { type CalendarParticipantsTarget } from "@/lib/calendar/ui"; import type { CalendarEvent, CalendarEventDraft } from "@/services/calendarService"; @@ -16,6 +16,7 @@ interface CalendarSidebarProps { onCalendarChange: (calendarId: number | null) => void; fixedDesktop?: boolean; compact?: boolean; + expandOnHover?: boolean; onExpand?: () => void; } @@ -26,31 +27,46 @@ export const CalendarSidebar = memo(function CalendarSidebar(props: CalendarSide const { fixedDesktop = false, compact = false, + expandOnHover = true, onExpand, ...calendarProps } = props; + const [hoverExpanded, setHoverExpanded] = useState(false); + const [calendarActivated, setCalendarActivated] = useState(false); + const expanded = expandOnHover && hoverExpanded; if (compact) { return ( ); diff --git a/frontend/src/hooks/useDesktopCompactPanelBehavior.ts b/frontend/src/hooks/useDesktopCompactPanelBehavior.ts new file mode 100644 index 00000000..7e0623d9 --- /dev/null +++ b/frontend/src/hooks/useDesktopCompactPanelBehavior.ts @@ -0,0 +1,46 @@ +"use client"; + +import { useCallback, useSyncExternalStore } from "react"; + +export type DesktopCompactPanelBehavior = "hover" | "compact"; + +const STORAGE_KEY = "desktop-compact-panel-behavior"; +const CHANGE_EVENT = "desktop-compact-panel-behavior-change"; +const DEFAULT_BEHAVIOR: DesktopCompactPanelBehavior = "hover"; + +function readBehavior(): DesktopCompactPanelBehavior { + try { + const stored = window.localStorage.getItem(STORAGE_KEY); + return stored === "compact" || stored === "hover" ? stored : DEFAULT_BEHAVIOR; + } catch { + return DEFAULT_BEHAVIOR; + } +} + +export function useDesktopCompactPanelBehavior() { + const subscribe = useCallback((notify: () => void) => { + const handleStorage = (event: StorageEvent) => { + if (!event.key || event.key === STORAGE_KEY) notify(); + }; + + window.addEventListener("storage", handleStorage); + window.addEventListener(CHANGE_EVENT, notify); + return () => { + window.removeEventListener("storage", handleStorage); + window.removeEventListener(CHANGE_EVENT, notify); + }; + }, []); + + const behavior = useSyncExternalStore(subscribe, readBehavior, () => DEFAULT_BEHAVIOR); + + const setBehavior = useCallback((nextBehavior: DesktopCompactPanelBehavior) => { + try { + window.localStorage.setItem(STORAGE_KEY, nextBehavior); + } catch { + // Ignore unavailable browser storage. + } + window.dispatchEvent(new Event(CHANGE_EVENT)); + }, []); + + return [behavior, setBehavior] as const; +}