From 85bf97154754d7b3f5ee2a77e9b1fe81d0ffa01f Mon Sep 17 00:00:00 2001 From: MangoPig Date: Wed, 17 Jun 2026 10:52:14 +0100 Subject: [PATCH] Feat: Add responsive workspace shell --- .../shell/AppShell/AppShell.module.scss | 51 +++++- .../components/shell/AppShell/AppShell.tsx | 105 ++++++++++- .../DepartmentSelector.module.scss | 44 ++++- .../DepartmentSelector/DepartmentSelector.tsx | 6 +- .../shell/LeftRail/LeftRail.module.scss | 32 +--- .../components/shell/LeftRail/LeftRail.tsx | 9 - .../MobileBottomNav.module.scss | 108 +++++++++++ .../shell/MobileBottomNav/MobileBottomNav.tsx | 63 +++++++ .../MobileWorkspaceBrowser.module.scss | 168 ++++++++++++++++++ .../MobileWorkspaceBrowser.tsx | 112 ++++++++++++ .../ProjectSelector.module.scss | 2 +- .../TopBar/NotificationsMenu.module.scss | 74 ++++++-- .../shell/TopBar/NotificationsMenu.tsx | 15 +- .../shell/TopBar/NotificationsNav.tsx | 74 +++----- .../shell/TopBar/ProfileMenu.module.scss | 61 +++++-- .../components/shell/TopBar/ProfileMenu.tsx | 72 +++++--- .../src/components/shell/TopBar/TopBar.tsx | 17 +- .../src/components/shell/TopBar/UserNav.tsx | 72 +++----- .../TopBar/createDesktopMenuController.ts | 72 ++++++++ .../WorkspaceSidebar.module.scss | 65 ++++++- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 102 ++++++++--- .../src/components/shell/data/shell.data.ts | 69 ++++++- .../WorkspaceHome/WorkspaceHome.module.scss | 13 +- Frontend/src/lib/icons/index.ts | 2 + 24 files changed, 1153 insertions(+), 255 deletions(-) create mode 100644 Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.module.scss create mode 100644 Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.tsx create mode 100644 Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.module.scss create mode 100644 Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx create mode 100644 Frontend/src/components/shell/TopBar/createDesktopMenuController.ts diff --git a/Frontend/src/components/shell/AppShell/AppShell.module.scss b/Frontend/src/components/shell/AppShell/AppShell.module.scss index f4ac941..e17c88f 100644 --- a/Frontend/src/components/shell/AppShell/AppShell.module.scss +++ b/Frontend/src/components/shell/AppShell/AppShell.module.scss @@ -8,8 +8,10 @@ } .body { + --shell-dock-clearance: calc(var(--space-12) + var(--space-12) + var(--space-8)); --rail-width: 4.75rem; --sidebar-width: 16.75rem; + --mobile-bottom-nav-clearance: 0rem; --shell-top-left-radius: calc(var(--radius-xl) + var(--space-1)); --shell-frame-border: color-mix(in srgb, var(--color-border-strong) 44%, transparent); --shell-divider-border: color-mix(in srgb, var(--color-border-strong) 34%, transparent); @@ -101,6 +103,16 @@ border-top-right-radius: 0; } +.mobileWorkspaceView { + min-width: 0; + min-height: 0; + height: 100%; + display: grid; + box-sizing: border-box; + overflow: hidden; + background: var(--workspace-panel-surface); +} + .sidebarDock { position: absolute; bottom: var(--space-3); @@ -191,17 +203,46 @@ @include respond-down(mobile) { .body { - grid-template-columns: 4.5rem minmax(0, 1fr); - --rail-width: 4.5rem; + grid-template-columns: minmax(0, 1fr); + --rail-width: 0rem; + --sidebar-width: 0rem; + --mobile-bottom-nav-clearance: calc(var(--space-12) + var(--space-10) + env(safe-area-inset-bottom, 0px)); } .railColumn { - position: sticky; - top: 0; + display: none; } - .workspaceRegion, + .workspaceRegion { + display: grid; + grid-template-columns: minmax(0, 1fr); + border-top-left-radius: 0; + } + + .workspaceRegion::before { + background: var(--workspace-panel-surface); + border-left-width: 0; + border-right-width: 0; + border-top-left-radius: 0; + } + + .sidebarColumn, .sidebarDock { display: none; } + + .workspaceMain { + border-left-width: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; + box-sizing: border-box; + padding-bottom: var(--mobile-bottom-nav-clearance); + } + + .mobileWorkspaceView { + height: 100%; + max-height: none; + padding-bottom: 0; + background: var(--workspace-panel-surface); + } } diff --git a/Frontend/src/components/shell/AppShell/AppShell.tsx b/Frontend/src/components/shell/AppShell/AppShell.tsx index 00b8dd6..86c9fdf 100644 --- a/Frontend/src/components/shell/AppShell/AppShell.tsx +++ b/Frontend/src/components/shell/AppShell/AppShell.tsx @@ -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("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(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): 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 (
- +
{
- { - setIsSidebarCollapsed((collapsed) => !collapsed); - }} - /> + {/* On mobile, top-bar menus become full workspace views instead of popovers. */} + { + setIsSidebarCollapsed((collapsed) => !collapsed); + }} + /> + } + > +
+ + + + + + +
+
@@ -68,6 +144,19 @@ export const AppShell = (): JSX.Element => { + + { + toggleMobileWorkspaceBrowser(); + }} + /> + { + setIsMobileWorkspaceBrowserOpen(false); + }} + /> ); }; diff --git a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss index 36927a9..636c83f 100644 --- a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss +++ b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.module.scss @@ -7,7 +7,7 @@ min-width: 0; padding: 0; display: inline-flex; - align-items: center; + align-items: flex-end; justify-content: flex-start; gap: var(--space-2); border: 0; @@ -43,14 +43,25 @@ box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-soft) 14%, transparent); } +.copy { + min-width: 0; + display: inline-flex; + align-items: baseline; + gap: var(--space-1); +} + .value { @include text-title; color: var(--color-text); font-weight: var(--font-weight-title); + line-height: 1; } .meta { + @include text-caption; color: var(--color-text-muted); + line-height: 1; + padding-left: var(--space-1); } .icon { @@ -80,7 +91,7 @@ .menuSection { display: grid; - gap: 0.15rem; + gap: calc(var(--space-1) / 2); } .menuSectionLabel { @@ -171,14 +182,39 @@ } .submenuIndicator { - width: 0.35rem; - height: 0.35rem; + width: calc(var(--space-1) + (var(--space-1) / 2)); + height: calc(var(--space-1) + (var(--space-1) / 2)); flex: 0 0 auto; border-radius: 999px; background: var(--color-accent-soft); } @include respond-down(mobile) { + .selector { + align-items: flex-end; + gap: var(--space-1); + } + + .copy { + gap: var(--space-2); + } + + .value { + line-height: 0.95; + } + + .meta { + font-size: 0.68rem; + line-height: 1; + letter-spacing: 0.01em; + padding-bottom: calc(var(--space-1) / 2); + } + + .icon { + align-self: flex-end; + margin-bottom: calc(var(--space-1) / 2); + } + .menu { min-width: min(16rem, calc(100vw - (var(--space-4) * 2))); } diff --git a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx index d695c00..360a18a 100644 --- a/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx +++ b/Frontend/src/components/shell/DepartmentSelector/DepartmentSelector.tsx @@ -53,8 +53,10 @@ export const DepartmentSelector = (): JSX.Element => { aria-expanded={isOpen()} onClick={() => setIsOpen((open) => !open)} > - {selectedDepartment().name} - {selectedTeamName()} team + + {selectedDepartment().name} + {selectedTeamName()} + diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.module.scss b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss index 541c9f9..bd678ff 100644 --- a/Frontend/src/components/shell/LeftRail/LeftRail.module.scss +++ b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss @@ -1,6 +1,5 @@ .rail { --rail-workspace-size: var(--control-size-lg); - --rail-action-size: var(--control-size-md); position: relative; z-index: 3; flex: 1; @@ -9,21 +8,19 @@ flex-direction: column; align-items: center; gap: var(--space-3); - padding: var(--space-3) var(--space-2) calc(var(--space-3) + var(--rail-dock-clearance, 8rem)); + padding: var(--space-3) var(--space-2) calc(var(--space-3) + var(--rail-dock-clearance, var(--shell-dock-clearance))); overflow: visible; } .railCollapsed { --rail-workspace-size: calc(var(--control-size-md) + 0.1rem); - --rail-action-size: calc(var(--control-size-md) + 0.1rem); justify-content: flex-start; gap: 0; padding-top: var(--space-4); padding-inline: var(--space-1); } -.topCluster, -.bottomCluster { +.topCluster { width: 100%; display: flex; flex-direction: column; @@ -31,11 +28,6 @@ gap: var(--space-2); } -.bottomCluster { - margin-top: auto; - margin-bottom: var(--rail-bottom-offset, 0rem); -} - .topCluster { gap: var(--space-3); } @@ -44,8 +36,7 @@ gap: var(--space-3); } -.railCollapsed .topCluster, -.railCollapsed .bottomCluster { +.railCollapsed .topCluster { align-items: center; } @@ -191,20 +182,3 @@ border-radius: var(--radius-md); box-shadow: none; } - -.addButton { - width: var(--rail-action-size); - height: var(--rail-action-size); - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px dashed var(--color-border-strong); - border-radius: var(--radius-pill); - background: transparent; - color: var(--color-text-muted); - - &:hover { - background: var(--color-surface-hover); - color: var(--color-text); - } -} diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.tsx b/Frontend/src/components/shell/LeftRail/LeftRail.tsx index 4ab68e1..5d165b8 100644 --- a/Frontend/src/components/shell/LeftRail/LeftRail.tsx +++ b/Frontend/src/components/shell/LeftRail/LeftRail.tsx @@ -1,7 +1,6 @@ // Path: Frontend/src/components/shell/LeftRail/LeftRail.tsx import { For, Show, type JSX } from "solid-js"; -import { Plus } from "../../../lib/icons"; import { railItems, type RailItem } from "../data/shell.data"; import styles from "./LeftRail.module.scss"; @@ -77,14 +76,6 @@ export const LeftRail = (props: LeftRailProps): JSX.Element => { - - -
- -
-
); }; diff --git a/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.module.scss b/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.module.scss new file mode 100644 index 0000000..24febc5 --- /dev/null +++ b/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.module.scss @@ -0,0 +1,108 @@ +.mobileNav { + display: none; +} + +@include respond-down(mobile) { + .mobileNav { + --mobile-nav-button-gap: var(--space-1); + --mobile-nav-button-padding-inline: var(--space-2); + --mobile-nav-button-padding-top: calc(var(--space-2) + (var(--space-1) / 2)); + --mobile-nav-button-padding-bottom: var(--space-2); + position: fixed; + right: 0; + bottom: 0; + left: 0; + display: grid; + gap: var(--space-2); + padding: var(--space-2) var(--space-3) calc(var(--space-2) + env(safe-area-inset-bottom, 0px)); + background: + linear-gradient(to top, color-mix(in srgb, var(--color-canvas) 98%, transparent), color-mix(in srgb, var(--color-canvas) 92%, transparent)); + border-top: 1px solid color-mix(in srgb, var(--color-border-strong) 40%, transparent); + backdrop-filter: blur(var(--blur-overlay)); + z-index: var(--z-sticky); + } + + .contextBar { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + min-width: 0; + color: var(--color-text-muted); + } + + .contextServer, + .contextProject { + @include text-caption; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .contextServer { + max-width: 45vw; + } + + .contextProject { + max-width: 30vw; + } + + .contextDivider { + @include text-caption; + color: var(--color-text-subtle); + } + + .navGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-2); + } + + .navButton { + min-width: 0; + display: grid; + justify-items: center; + gap: var(--mobile-nav-button-gap); + padding: var(--mobile-nav-button-padding-top) var(--mobile-nav-button-padding-inline) var(--mobile-nav-button-padding-bottom); + border: 1px solid transparent; + border-radius: var(--radius-xl); + background: transparent; + color: var(--color-text-muted); + transition: + background 160ms var(--easing-standard), + color 160ms var(--easing-standard), + border-color 160ms var(--easing-standard), + transform 180ms var(--easing-standard); + } + + .navButton:hover, + .navButton:focus-visible { + color: var(--color-text); + background: color-mix(in srgb, var(--color-surface) 82%, transparent); + border-color: color-mix(in srgb, var(--color-border-strong) 28%, transparent); + } + + .navButton:active { + transform: translateY(calc(var(--space-1) / 2)); + } + + .navButtonActive { + color: var(--color-text); + background: color-mix(in srgb, var(--color-surface) 90%, transparent); + border-color: color-mix(in srgb, var(--color-border-strong) 34%, transparent); + box-shadow: var(--shadow-soft); + } + + .iconWrap { + display: inline-flex; + align-items: center; + justify-content: center; + } + + .label { + @include text-caption; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.tsx b/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.tsx new file mode 100644 index 0000000..e952ba5 --- /dev/null +++ b/Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.tsx @@ -0,0 +1,63 @@ +// Path: Frontend/src/components/shell/MobileBottomNav/MobileBottomNav.tsx + +import { For, type JSX } from "solid-js"; +import { activeProject, activeServer, mobileBottomNavItems, type MobileBottomNavItem } from "../data/shell.data"; +import styles from "./MobileBottomNav.module.scss"; + +type MobileBottomNavProps = { + isBrowseOpen: boolean; + onBrowseToggle: VoidFunction; +}; + +const MobileNavEntry = (props: { + item: MobileBottomNavItem; + isActive: boolean; + onSelect?: VoidFunction; +}): JSX.Element => { + const Icon = props.item.icon; + + return ( + + ); +}; + +export const MobileBottomNav = (props: MobileBottomNavProps): JSX.Element => { + return ( + + ); +}; diff --git a/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.module.scss b/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.module.scss new file mode 100644 index 0000000..28e7538 --- /dev/null +++ b/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.module.scss @@ -0,0 +1,168 @@ +.browserLayer { + display: none; +} + +@include respond-down(mobile) { + .browserLayer { + position: fixed; + inset: 0; + display: grid; + z-index: calc(var(--z-popover, 20) + 2); + background: color-mix(in srgb, var(--color-canvas) 96%, black 4%); + } + + .sheet { + min-height: 100dvh; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + background: + linear-gradient(180deg, color-mix(in srgb, var(--color-surface) 84%, transparent) 0%, transparent 8rem), + color-mix(in srgb, var(--color-canvas) 97%, black 3%); + overflow: clip; + } + + .sheetHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: calc(var(--space-5) + env(safe-area-inset-top, 0px)) var(--space-4) var(--space-4); + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 84%, transparent); + } + + .brandBlock { + min-width: 0; + display: grid; + gap: var(--space-1); + } + + .sectionLabel { + @include text-caption; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + } + + .brandEyebrow { + @include text-caption; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + } + + .brandTitle { + @include text-title; + color: var(--color-text); + font-weight: var(--font-weight-semibold); + } + + .brandContext { + @include text-caption; + color: var(--color-text-subtle); + } + + .closeButton { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--control-size-md); + height: var(--control-size-md); + padding: 0; + border: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--color-surface) 92%, transparent); + color: var(--color-text-subtle); + } + + .sheetBody { + min-height: 0; + display: grid; + gap: var(--space-5); + padding: var(--space-5) var(--space-5) calc(var(--space-12) + var(--space-10) + env(safe-area-inset-bottom, 0px)); + overflow: auto; + } + + .sectionBlock { + display: grid; + gap: var(--space-2); + } + + .treeList, + .treeListItem { + list-style: none; + margin: 0; + padding: 0; + } + + .treeList { + display: grid; + gap: 0; + border-top: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent); + } + + .treeListNested { + display: grid; + gap: 0; + } + + .treeRow { + min-width: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + min-height: calc(var(--control-size-lg) + var(--space-3)); + padding: var(--space-4) var(--space-2) var(--space-4) calc(var(--space-2) + (var(--tree-depth, 0) * var(--space-4))); + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent); + border-radius: 0; + background: transparent; + color: var(--color-text); + text-align: left; + } + + .treeRowActive { + color: var(--color-text); + background: color-mix(in srgb, var(--color-surface) 72%, transparent); + } + + .treeRowBranch { + font-weight: var(--font-weight-semibold); + } + + .treeRowLead, + .treeRowTrail { + min-width: 0; + display: inline-flex; + align-items: center; + gap: var(--space-2); + } + + .treeRowTrail { + flex: 0 0 auto; + color: var(--color-text-muted); + } + + .treeLabel { + @include text-label; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .treeMeta { + @include text-caption; + min-width: 1rem; + text-align: right; + } + + .treeChevron { + color: var(--color-text-subtle); + } + + .treeListNested > .treeListItem > .treeRow { + min-height: calc(var(--control-size-lg) + var(--space-1)); + } +} diff --git a/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx b/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx new file mode 100644 index 0000000..30d5385 --- /dev/null +++ b/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx @@ -0,0 +1,112 @@ +import { For, Show, type JSX } from "solid-js"; +import { ChevronRight, X } from "../../../lib/icons"; +import { activeProject, activeServer, workspaceStaticItems, workspaceTree, type SidebarItem, type WorkspaceTreeNode } from "../data/shell.data"; +import styles from "./MobileWorkspaceBrowser.module.scss"; + +type MobileWorkspaceBrowserProps = { + open: boolean; + onClose: VoidFunction; +}; + +const TreeRow = (props: { node: WorkspaceTreeNode; depth?: number }): JSX.Element => { + const depth = props.depth ?? 0; + const Icon = props.node.icon; + const hasChildren = (props.node.children?.length ?? 0) > 0; + + return ( +
  • + + + +
      + {(child): JSX.Element => } +
    +
    +
  • + ); +}; + +const StaticRow = (props: { item: SidebarItem }): JSX.Element => { + const Icon = props.item.icon; + + return ( +
  • + +
  • + ); +}; + +export const MobileWorkspaceBrowser = (props: MobileWorkspaceBrowserProps): JSX.Element => { + const sectionNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) > 0); + const looseNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) === 0); + + return ( + +
    +
    +
    +
    + Moku Work + {activeProject.name} + {activeServer.name} +
    + + +
    + +
    +
    + Workspace +
      + {(item): JSX.Element => } +
    +
    + +
    + Items +
      + {(node): JSX.Element => } +
    +
    + + 0}> +
    + More +
      + {(node): JSX.Element => } +
    +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/Frontend/src/components/shell/ProjectSelector/ProjectSelector.module.scss b/Frontend/src/components/shell/ProjectSelector/ProjectSelector.module.scss index 756b0be..04f86fa 100644 --- a/Frontend/src/components/shell/ProjectSelector/ProjectSelector.module.scss +++ b/Frontend/src/components/shell/ProjectSelector/ProjectSelector.module.scss @@ -2,7 +2,7 @@ display: grid; --project-drawer-gap: var(--space-3); --project-drawer-top: calc(var(--space-4) + var(--control-size-lg)); - --project-drawer-bottom: calc(var(--sidebar-dock-clearance) + var(--project-drawer-gap)); + --project-drawer-bottom: calc(var(--sidebar-dock-clearance, var(--shell-dock-clearance)) + var(--project-drawer-gap)); } .rootCompact { diff --git a/Frontend/src/components/shell/TopBar/NotificationsMenu.module.scss b/Frontend/src/components/shell/TopBar/NotificationsMenu.module.scss index c2483d9..20c4e3e 100644 --- a/Frontend/src/components/shell/TopBar/NotificationsMenu.module.scss +++ b/Frontend/src/components/shell/TopBar/NotificationsMenu.module.scss @@ -7,11 +7,34 @@ gap: var(--space-3); padding: var(--space-3); border: 1px solid var(--color-border-strong); - border-radius: calc(var(--radius-lg) + 0.1rem); + border-radius: calc(var(--radius-lg) + (var(--space-1) / 2)); background: color-mix(in srgb, var(--color-surface-muted) 96%, transparent); - box-shadow: 0 18px 36px color-mix(in srgb, black 16%, transparent); - backdrop-filter: blur(18px); - z-index: 30; + box-shadow: var(--shadow-strong); + backdrop-filter: blur(var(--blur-overlay)); + z-index: var(--z-dropdown); +} + +.menu.menuWorkspace { + position: static; + top: auto; + right: auto; + left: auto; + bottom: auto; + width: 100%; + max-width: none; + height: 100%; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: var(--space-4); + padding: var(--space-4); + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + z-index: auto; + overflow: hidden; } .header, @@ -30,7 +53,7 @@ .headerCopy { min-width: 0; display: grid; - gap: 0.08rem; + gap: calc(var(--space-1) / 2); } .title { @@ -125,7 +148,7 @@ .list { display: grid; - gap: 0.3rem; + gap: var(--space-1); } .item { @@ -181,7 +204,7 @@ .itemBody { min-width: 0; display: grid; - gap: 0.12rem; + gap: calc(var(--space-1) / 2); } .itemTitle { @@ -190,7 +213,7 @@ } .itemTime { - padding-top: 0.05rem; + padding-top: calc(var(--space-1) / 4); white-space: nowrap; color: var(--color-text-subtle); } @@ -202,26 +225,47 @@ flex-wrap: wrap; } +.menu.menuWorkspace .listWrap { + min-height: 0; + max-height: none; + height: 100%; + padding-right: 0; + margin-right: 0; +} + +.menu.menuWorkspace .header, +.menu.menuWorkspace .footer { + padding-left: 0; + padding-right: 0; +} + @include respond-down(mobile) { - .menu { - width: min(22rem, calc(100vw - (var(--space-3) * 2))); + .menu.menuWorkspace { + height: 100%; + min-height: 0; + padding: var(--space-5); } - .item { + .menu.menuWorkspace .listWrap { + min-height: 0; + max-height: none; + } + + .menu.menuWorkspace .item { grid-template-columns: auto minmax(0, 1fr); } - .itemTime { + .menu.menuWorkspace .itemTime { grid-column: 2; padding-top: 0; } - .footer { + .menu.menuWorkspace .footer { align-items: flex-start; flex-direction: column; } - .footerAction { + .menu.menuWorkspace .footerAction { padding: var(--space-1) 0; } - } +} diff --git a/Frontend/src/components/shell/TopBar/NotificationsMenu.tsx b/Frontend/src/components/shell/TopBar/NotificationsMenu.tsx index 30ef389..35121bc 100644 --- a/Frontend/src/components/shell/TopBar/NotificationsMenu.tsx +++ b/Frontend/src/components/shell/TopBar/NotificationsMenu.tsx @@ -5,8 +5,9 @@ import styles from "./NotificationsMenu.module.scss"; type NotificationsMenuProps = { id: string; - menuRef: (element: HTMLDivElement) => void; + menuRef?: (element: HTMLDivElement) => void; onSelect: () => void; + variant?: "popover" | "workspace"; }; export const NotificationsMenu = (props: NotificationsMenuProps): JSX.Element => { @@ -14,9 +15,19 @@ export const NotificationsMenu = (props: NotificationsMenuProps): JSX.Element => const earlierItems = notificationItems.filter((item) => !item.unread); const hasNotifications = notificationItems.length > 0; const isCaughtUp = unreadItems.length === 0 && hasNotifications; + const variant = props.variant ?? "popover"; return ( -