Merge branch 'develop'
This commit is contained in:
commit
4817641831
@ -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() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="app-surface-muted mt-4 rounded-2xl p-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-[var(--foreground)]">Боковые панели в расширенном режиме</p>
|
||||
<p className="app-text-muted mt-1 text-sm">
|
||||
Определяет поведение компактного меню и календаря на этом устройстве.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{desktopCompactPanelCards.map(({ value, title, description, Icon }) => {
|
||||
const active = desktopCompactPanelBehavior === value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setDesktopCompactPanelBehavior(value)}
|
||||
className={`rounded-2xl p-4 text-left transition ${
|
||||
active ? "app-selected" : "app-surface hover:bg-[var(--surface-tertiary)]"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<span
|
||||
className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl ${
|
||||
active ? "app-action-primary text-white" : "bg-[var(--surface-elevated)] text-[var(--muted-foreground)]"
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</span>
|
||||
<span className={`app-badge inline-flex h-6 min-w-6 justify-center px-2 text-xs font-semibold ${active ? "app-badge-accent" : ""}`}>
|
||||
{active ? <Check size={14} /> : value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-semibold text-[var(--foreground)]">{title}</p>
|
||||
<p className="app-text-muted mt-1 text-sm">{description}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
|
||||
@ -11,6 +11,7 @@ import { NotificationCenter, NotificationPanel } from "@/components/Notification
|
||||
import { CalendarSidebar } from "@/components/calendar/CalendarSidebar";
|
||||
import { PushOnboardingPrompt } from "@/components/PushOnboardingPrompt";
|
||||
import { useCalendarModals } from "@/hooks/useCalendarModals";
|
||||
import { useDesktopCompactPanelBehavior } from "@/hooks/useDesktopCompactPanelBehavior";
|
||||
import { CalendarModals } from "@/components/layout/CalendarModals";
|
||||
import { MobileLeftDrawer, MobileCalendarDrawer } from "@/components/layout/MobileDrawers";
|
||||
import { usePwa } from "@/contexts/PwaContext";
|
||||
@ -416,17 +417,38 @@ function RoutedLeftNavContent(props: Omit<LeftNavContentProps, "documentSection"
|
||||
);
|
||||
}
|
||||
|
||||
function LeftNav({ fixedDesktop = false, compact = false }: { fixedDesktop?: boolean; compact?: boolean }) {
|
||||
function LeftNav({
|
||||
fixedDesktop = false,
|
||||
compact = false,
|
||||
expandOnHover = true,
|
||||
}: {
|
||||
fixedDesktop?: boolean;
|
||||
compact?: boolean;
|
||||
expandOnHover?: boolean;
|
||||
}) {
|
||||
const [hoverExpanded, setHoverExpanded] = useState(false);
|
||||
const expanded = compact && expandOnHover && hoverExpanded;
|
||||
|
||||
return (
|
||||
<aside className={`hidden shrink-0 transition-[width] duration-200 lg:block ${compact ? "w-14" : "w-64"}`}>
|
||||
<div
|
||||
className={`space-y-4 lg:overflow-y-auto ${
|
||||
className={`space-y-4 transition-[width] duration-200 lg:overflow-y-auto ${expanded ? "z-[35] lg:w-64" : ""} ${
|
||||
fixedDesktop
|
||||
? `lg:fixed lg:top-0 lg:bottom-0 lg:pt-[4.5rem] lg:pb-8 ${compact ? "lg:w-14" : "lg:w-64"}`
|
||||
? `lg:fixed lg:top-0 lg:bottom-0 lg:pt-[4.5rem] lg:pb-8 ${compact ? `lg:left-3 ${expanded ? "lg:w-64" : "lg:w-14"}` : "lg:w-64"}`
|
||||
: "lg:sticky lg:top-8 lg:max-h-[calc(100dvh-7.5rem)] lg:pb-2"
|
||||
}`}
|
||||
onMouseEnter={() => {
|
||||
if (compact && expandOnHover) setHoverExpanded(true);
|
||||
}}
|
||||
onMouseLeave={() => setHoverExpanded(false)}
|
||||
onFocus={() => {
|
||||
if (compact && expandOnHover) setHoverExpanded(true);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) setHoverExpanded(false);
|
||||
}}
|
||||
>
|
||||
<RoutedLeftNavContent compact={compact} />
|
||||
<RoutedLeftNavContent compact={compact && !expanded} />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
@ -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({
|
||||
/>
|
||||
</div>
|
||||
<div className={`mx-auto flex w-full flex-1 min-h-0 max-w-6xl ${desktopWideMode ? "lg:max-w-none" : ""} ${standaloneAppMode ? "gap-0 px-0 py-0" : desktopImmersiveMode ? "gap-0 px-0 py-0 lg:gap-0 lg:px-3 lg:py-3" : mobileFullscreen ? `gap-0 px-0 py-0 lg:py-4 ${desktopWideMode ? "lg:gap-3 lg:px-3" : "lg:gap-6 lg:px-8"}` : `gap-6 px-4 py-4 sm:px-8 lg:py-4 ${desktopWideMode ? "lg:gap-3 lg:px-3" : ""}`}`}>
|
||||
{!desktopImmersiveMode && !standaloneAppMode ? <LeftNav fixedDesktop compact={desktopWideMode} /> : null}
|
||||
{!desktopImmersiveMode && !standaloneAppMode ? (
|
||||
<LeftNav
|
||||
fixedDesktop
|
||||
compact={desktopWideMode}
|
||||
expandOnHover={expandCompactPanelsOnHover}
|
||||
/>
|
||||
) : null}
|
||||
<main className={`flex-1 min-w-0 min-h-0 space-y-6 ${mobileFullscreen ? 'overflow-visible' : ''} ${desktopWideMode ? "lg:flex lg:flex-col lg:gap-4 lg:space-y-0 lg:overflow-hidden" : ""}`}>
|
||||
{!mobileFullscreen && !standaloneAppMode && !suppressPushOnboardingPrompt ? <PushOnboardingPrompt /> : null}
|
||||
{children}
|
||||
@ -723,6 +753,7 @@ export function AppShell({
|
||||
onCalendarChange={cal.handleCalendarChange}
|
||||
fixedDesktop
|
||||
compact={desktopWideMode}
|
||||
expandOnHover={expandCompactPanelsOnHover}
|
||||
onExpand={() => onDesktopWideModeChange?.(false)}
|
||||
/> : null}
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<aside className="hidden w-14 shrink-0 transition-[width] duration-200 lg:block">
|
||||
<div
|
||||
className={`lg:w-14 ${
|
||||
className={`z-[35] overflow-hidden transition-[width] duration-200 ${expanded ? "lg:w-72" : "lg:w-14"} ${
|
||||
fixedDesktop
|
||||
? "lg:fixed lg:top-0 lg:bottom-0 lg:pt-[4.5rem] lg:pb-8"
|
||||
? "lg:fixed lg:bottom-0 lg:right-3 lg:top-0 lg:pt-[4.5rem] lg:pb-8"
|
||||
: "lg:sticky lg:top-8 lg:max-h-[calc(100dvh-7.5rem)] lg:pb-2"
|
||||
}`}
|
||||
onMouseEnter={() => {
|
||||
if (!expandOnHover) return;
|
||||
setCalendarActivated(true);
|
||||
setHoverExpanded(true);
|
||||
}}
|
||||
onMouseLeave={() => setHoverExpanded(false)}
|
||||
>
|
||||
<div className="app-surface flex justify-center rounded-2xl p-2">
|
||||
<div className={`${expanded ? "hidden" : "app-surface flex"} justify-center rounded-2xl p-2`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExpand}
|
||||
className="app-icon-button flex h-10 w-10 items-center justify-center rounded-lg"
|
||||
title="Развернуть календарь"
|
||||
aria-label="Развернуть календарь"
|
||||
title={expandOnHover ? "Наведите, чтобы открыть календарь; нажмите, чтобы закрепить" : "Развернуть календарь"}
|
||||
aria-label={expandOnHover ? "Открыть календарь; нажатие закрепляет обычный режим" : "Развернуть календарь"}
|
||||
>
|
||||
<CalendarDays size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{calendarActivated ? (
|
||||
<div className={expanded ? "block w-72" : "hidden w-72"}>
|
||||
<CalendarCard {...calendarProps} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
46
frontend/src/hooks/useDesktopCompactPanelBehavior.ts
Normal file
46
frontend/src/hooks/useDesktopCompactPanelBehavior.ts
Normal file
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user