Feat: Add responsive workspace shell

This commit is contained in:
MangoPig
2026-06-17 10:52:14 +01:00
parent 5d86a5124b
commit 85bf971547
24 changed files with 1153 additions and 255 deletions

View File

@@ -1,21 +1,52 @@
// Path: Frontend/src/components/shell/AppShell/AppShell.tsx
import { createSignal, onMount, type JSX } from "solid-js";
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 { 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)";
export const AppShell = (): 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);
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 => {
@@ -25,9 +56,39 @@ export const AppShell = (): JSX.Element => {
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}>
<TopBar theme={themeState()} onToggleTheme={toggleTheme} />
<TopBar
theme={themeState()}
onToggleTheme={toggleTheme}
isMobileViewport={isMobileViewport()}
isNotificationsOpen={activeMobileWorkspaceView() === "notifications"}
isProfileOpen={activeMobileWorkspaceView() === "profile"}
onToggleNotifications={toggleMobileNotifications}
onToggleProfile={toggleMobileProfile}
/>
<div
classList={{
@@ -54,12 +115,27 @@ export const AppShell = (): JSX.Element => {
</div>
<div class={styles.workspaceMain}>
<WorkspaceHome
sidebarCollapsed={isSidebarCollapsed()}
onToggleSidebarCollapse={(): void => {
setIsSidebarCollapsed((collapsed) => !collapsed);
}}
/>
{/* 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}>
<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>
@@ -68,6 +144,19 @@ export const AppShell = (): JSX.Element => {
<ServerDock />
</div>
</div>
<MobileBottomNav
isBrowseOpen={isMobileWorkspaceBrowserOpen()}
onBrowseToggle={(): void => {
toggleMobileWorkspaceBrowser();
}}
/>
<MobileWorkspaceBrowser
open={isMobileWorkspaceBrowserOpen()}
onClose={(): void => {
setIsMobileWorkspaceBrowserOpen(false);
}}
/>
</div>
);
};