176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
// Path: Frontend/src/components/shell/AppShell/AppShell.tsx
|
|
|
|
import { createSignal, onCleanup, onMount, Show, type JSX } from "solid-js";
|
|
import { getDocumentTheme, setTheme, type Theme } from "../../../theme/runtime";
|
|
import { WorkspaceHome } from "../../workspace-home/WorkspaceHome/WorkspaceHome";
|
|
import { AppShellDataProvider, useAppShellData } from "../data/app-shell.context";
|
|
import { LeftRail } from "../LeftRail/LeftRail";
|
|
import { MobileBottomNav } from "../MobileBottomNav/MobileBottomNav";
|
|
import { MobileWorkspaceBrowser } from "../MobileWorkspaceBrowser/MobileWorkspaceBrowser";
|
|
import { ServerDock } from "../ServerDock/ServerDock";
|
|
import { NotificationsMenu } from "../TopBar/NotificationsMenu";
|
|
import { ProfileMenu } from "../TopBar/ProfileMenu";
|
|
import { TopBar } from "../TopBar/TopBar";
|
|
import { WorkspaceSidebar } from "../WorkspaceSidebar/WorkspaceSidebar";
|
|
import styles from "./AppShell.module.scss";
|
|
|
|
type MobileWorkspaceView = "notifications" | "profile" | null;
|
|
const MOBILE_VIEWPORT_QUERY = "(max-width: 48rem)";
|
|
|
|
const AppShellContent = (): JSX.Element => {
|
|
const [themeState, setThemeState] = createSignal<Theme>("light");
|
|
const [isRailCollapsed, setIsRailCollapsed] = createSignal(false);
|
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = createSignal(false);
|
|
const [isMobileViewport, setIsMobileViewport] = createSignal(false);
|
|
const [isMobileWorkspaceBrowserOpen, setIsMobileWorkspaceBrowserOpen] = createSignal(false);
|
|
const [activeMobileWorkspaceView, setActiveMobileWorkspaceView] = createSignal<MobileWorkspaceView>(null);
|
|
const appShellData = useAppShellData();
|
|
|
|
onMount((): void => {
|
|
setThemeState(getDocumentTheme());
|
|
|
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
return;
|
|
}
|
|
|
|
const mediaQuery = window.matchMedia(MOBILE_VIEWPORT_QUERY);
|
|
const syncMobileViewport = (): void => {
|
|
setIsMobileViewport(mediaQuery.matches);
|
|
|
|
if (!mediaQuery.matches) {
|
|
setIsMobileWorkspaceBrowserOpen(false);
|
|
setActiveMobileWorkspaceView(null);
|
|
}
|
|
};
|
|
|
|
syncMobileViewport();
|
|
mediaQuery.addEventListener("change", syncMobileViewport);
|
|
|
|
onCleanup(() => {
|
|
mediaQuery.removeEventListener("change", syncMobileViewport);
|
|
});
|
|
});
|
|
|
|
const toggleTheme = (): void => {
|
|
const next: Theme = themeState() === "dark" ? "light" : "dark";
|
|
|
|
setTheme(next);
|
|
setThemeState(next);
|
|
};
|
|
|
|
const openMobileWorkspaceView = (view: Exclude<MobileWorkspaceView, null>): void => {
|
|
setIsMobileWorkspaceBrowserOpen(false);
|
|
setActiveMobileWorkspaceView((current) => (current === view ? null : view));
|
|
};
|
|
|
|
const toggleMobileWorkspaceBrowser = (): void => {
|
|
setActiveMobileWorkspaceView(null);
|
|
setIsMobileWorkspaceBrowserOpen((open) => !open);
|
|
};
|
|
|
|
const toggleMobileNotifications = (): void => {
|
|
openMobileWorkspaceView("notifications");
|
|
};
|
|
|
|
const toggleMobileProfile = (): void => {
|
|
openMobileWorkspaceView("profile");
|
|
};
|
|
|
|
const closeMobileWorkspaceView = (): void => {
|
|
setActiveMobileWorkspaceView(null);
|
|
};
|
|
|
|
return (
|
|
<div class={styles.shell} data-ui="app-shell" data-app-shell-status={appShellData.status()}>
|
|
<TopBar
|
|
theme={themeState()}
|
|
onToggleTheme={toggleTheme}
|
|
isMobileViewport={isMobileViewport()}
|
|
isNotificationsOpen={activeMobileWorkspaceView() === "notifications"}
|
|
isProfileOpen={activeMobileWorkspaceView() === "profile"}
|
|
onToggleNotifications={toggleMobileNotifications}
|
|
onToggleProfile={toggleMobileProfile}
|
|
/>
|
|
|
|
<div
|
|
classList={{
|
|
[styles.body]: true,
|
|
[styles.bodyRailCollapsed]: isRailCollapsed(),
|
|
[styles.bodySidebarCollapsed]: isSidebarCollapsed(),
|
|
}}
|
|
data-slot="shell-body"
|
|
data-rail-collapsed={isRailCollapsed() ? "true" : "false"}
|
|
data-sidebar-collapsed={isSidebarCollapsed() ? "true" : "false"}
|
|
>
|
|
{/* Left server rail */}
|
|
<div class={styles.railColumn} data-slot="rail-column">
|
|
<LeftRail collapsed={isRailCollapsed()} />
|
|
</div>
|
|
|
|
{/* Sidebar + main workspace frame */}
|
|
<div class={styles.workspaceRegion} data-slot="workspace-region">
|
|
<div class={styles.sidebarColumn} data-slot="sidebar-column">
|
|
<WorkspaceSidebar
|
|
collapsed={isSidebarCollapsed()}
|
|
railCollapsed={isRailCollapsed()}
|
|
onToggleRailCollapse={(): void => {
|
|
setIsRailCollapsed((collapsed) => !collapsed);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class={styles.workspaceMain} data-slot="workspace-main">
|
|
{/* On mobile, top-bar menus become full workspace views instead of popovers. */}
|
|
<Show
|
|
when={isMobileViewport() && activeMobileWorkspaceView() !== null}
|
|
fallback={
|
|
<WorkspaceHome
|
|
sidebarCollapsed={isSidebarCollapsed()}
|
|
onToggleSidebarCollapse={(): void => {
|
|
setIsSidebarCollapsed((collapsed) => !collapsed);
|
|
}}
|
|
/>
|
|
}
|
|
>
|
|
<div class={styles.mobileWorkspaceView} data-slot="mobile-workspace-view" data-view={activeMobileWorkspaceView() ?? undefined}>
|
|
<Show when={activeMobileWorkspaceView() === "notifications"}>
|
|
<NotificationsMenu id="mobile-workspace-notifications" onSelect={closeMobileWorkspaceView} variant="workspace" />
|
|
</Show>
|
|
<Show when={activeMobileWorkspaceView() === "profile"}>
|
|
<ProfileMenu id="mobile-workspace-profile" onSelect={closeMobileWorkspaceView} variant="workspace" />
|
|
</Show>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Floating server dock overlay */}
|
|
<div class={styles.sidebarDock} data-slot="sidebar-dock">
|
|
<ServerDock />
|
|
</div>
|
|
</div>
|
|
|
|
<MobileBottomNav
|
|
isBrowseOpen={isMobileWorkspaceBrowserOpen()}
|
|
onBrowseToggle={(): void => {
|
|
toggleMobileWorkspaceBrowser();
|
|
}}
|
|
/>
|
|
<MobileWorkspaceBrowser
|
|
open={isMobileWorkspaceBrowserOpen()}
|
|
onClose={(): void => {
|
|
setIsMobileWorkspaceBrowserOpen(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const AppShell = (): JSX.Element => {
|
|
return (
|
|
<AppShellDataProvider>
|
|
<AppShellContent />
|
|
</AppShellDataProvider>
|
|
);
|
|
};
|