diff --git a/CHANGELOG.md b/CHANGELOG.md index 7434513..14e0e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- Rebuilt the nine non-default layouts against the application's own data. + ### Fixed ### Removed diff --git a/apps/admin/.env.example b/apps/admin/.env.example index a1aa4c7..3b4694d 100755 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -1,3 +1,4 @@ VITE_API_URL=http://localhost:3333 VITE_TINYMCE_API_KEY= -VITE_WEB_URL=http://localhost:3000 \ No newline at end of file +VITE_WEB_URL=http://localhost:3000 +VITE_LAYOUT= \ No newline at end of file diff --git a/apps/admin/src/app/config/app.ts b/apps/admin/src/app/config/app.ts index f8b7ae5..bf74923 100755 --- a/apps/admin/src/app/config/app.ts +++ b/apps/admin/src/app/config/app.ts @@ -4,6 +4,8 @@ export const TINYMCE_API_KEY = import.meta.env['VITE_TINYMCE_API_KEY']; export const API_URL = import.meta.env['VITE_API_URL']; +export const LAYOUT = import.meta.env['VITE_LAYOUT']; + export const IS_DEV_MODE = import.meta.env.MODE === 'development'; export const IS_PROD_MODE = import.meta.env.MODE === 'production'; diff --git a/apps/admin/src/app/config/settings.ts b/apps/admin/src/app/config/settings.ts index 2850248..6dfe117 100755 --- a/apps/admin/src/app/config/settings.ts +++ b/apps/admin/src/app/config/settings.ts @@ -1,28 +1,30 @@ -import { LayoutNames, Settings } from './types'; +import { LayoutNames, LayoutOptions, Settings } from './types'; + +const DEFAULT_LAYOUT_OPTIONS: LayoutOptions = { + sidebarCollapse: false, + sidebarTheme: 'light', + headerSticky: true, + headerStickyOffset: 200 +}; + +const layouts = Object.fromEntries( + Object.values(LayoutNames).map((layout) => [layout, { ...DEFAULT_LAYOUT_OPTIONS }]) +) as Record; + +export const DEFAULT_CONTAINER_BY_LAYOUT: Record = { + [LayoutNames.DEFAULT]: 'fixed', + [LayoutNames.HORIZONTAL_MENU]: 'fixed', + [LayoutNames.COMPACT_SIDEBAR]: 'fixed', + [LayoutNames.TWO_COLUMN_SIDEBAR]: 'fixed', + [LayoutNames.DUAL_ROW_HEADER]: 'fixed', + [LayoutNames.MULTIPLE_MENUS]: 'fixed', + [LayoutNames.ADVANCE_MEGA_MENU]: 'fluid', + [LayoutNames.DROPDOWN_MENU]: 'fixed', + [LayoutNames.EXTENDED_HEADER]: 'fixed', + [LayoutNames.DARK_SIDEBAR]: 'fixed' +}; export const APP_SETTINGS: Settings = { - layout: '', container: 'fixed', - layouts: { - [LayoutNames.DEFAULT]: { - sidebarCollapse: false, - sidebarTheme: 'light' - }, - [LayoutNames.HORIZONTAL_MENU]: { - headerSticky: true, - headerStickyOffset: 200 - }, - [LayoutNames.DUAL_ROW_HEADER]: { - headerSticky: true, - headerStickyOffset: 200 - }, - [LayoutNames.ADVANCE_MEGA_MENU]: { - headerSticky: true, - headerStickyOffset: 200 - }, - [LayoutNames.EXTENDED_HEADER]: { - headerSticky: true, - headerStickyOffset: 200 - } - } + layouts }; diff --git a/apps/admin/src/app/config/types.ts b/apps/admin/src/app/config/types.ts index 39509c9..356fc6a 100755 --- a/apps/admin/src/app/config/types.ts +++ b/apps/admin/src/app/config/types.ts @@ -33,29 +33,14 @@ export enum LayoutNames { DARK_SIDEBAR = 'dark-sidebar-layout', // demo10 } +export interface LayoutOptions { + sidebarCollapse: boolean; + sidebarTheme: 'light' | 'dark'; + headerSticky: boolean; + headerStickyOffset: number; +} + export interface Settings { container: 'fixed' | 'fluid'; - layout: string; - layouts: { - [LayoutNames.DEFAULT]: { - sidebarCollapse: boolean; - sidebarTheme: 'light' | 'dark'; - }; - [LayoutNames.HORIZONTAL_MENU]: { - headerSticky: boolean; - headerStickyOffset: number; - }; - [LayoutNames.DUAL_ROW_HEADER]: { - headerSticky: boolean; - headerStickyOffset: number; - }; - [LayoutNames.ADVANCE_MEGA_MENU]: { - headerSticky: boolean; - headerStickyOffset: number; - }; - [LayoutNames.EXTENDED_HEADER]: { - headerSticky: boolean; - headerStickyOffset: number; - }; - }; + layouts: Record; } diff --git a/apps/admin/src/app/layouts/01__Default/README.md b/apps/admin/src/app/layouts/01__Default/README.md new file mode 100644 index 0000000..420dcd9 --- /dev/null +++ b/apps/admin/src/app/layouts/01__Default/README.md @@ -0,0 +1,24 @@ +# Default layout + +**Metronic 9 reference:** [Demo 1](https://keenthemes.com/metronic/tailwind/react/demo1) + +A fixed sidebar carrying the application menu next to a fixed header. The sidebar can be collapsed +to icons, and the choice is remembered in settings under +`layouts.default-layout.sidebarCollapse`. Below `lg` the sidebar moves into a sheet opened from the +header. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Sidebar menu | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Sidebar`, `SidebarHeader` (logo plus the collapse toggle), `Header`. + +## Notes + +- Body classes `sidebar-fixed`, `header-fixed` and `layout-initialized` are added on mount and + removed on unmount, so switching to another layout leaves no styling behind. diff --git a/apps/admin/src/app/layouts/01__Default/components/Breadcrumb.tsx b/apps/admin/src/app/layouts/01__Default/components/Breadcrumb.tsx deleted file mode 100755 index c210121..0000000 --- a/apps/admin/src/app/layouts/01__Default/components/Breadcrumb.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ChevronRight } from 'lucide-react'; -import { Fragment } from 'react'; -import { useLocation } from 'react-router'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; - -export function Breadcrumb() { - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const last = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - - {item.title} - - {!last && ( - - )} - - ); - })} -
- ); -} diff --git a/apps/admin/src/app/layouts/01__Default/components/Content.tsx b/apps/admin/src/app/layouts/01__Default/components/Content.tsx deleted file mode 100755 index 1142bec..0000000 --- a/apps/admin/src/app/layouts/01__Default/components/Content.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Outlet } from 'react-router'; -import { useIsMobile } from '@crux-kit/hooks'; -import { Container } from '#shared/widgets/common/Container'; -import { Breadcrumb } from './Breadcrumb'; - -export function Content() { - const mobile = useIsMobile(); - - return ( -
- {mobile && ( - - - - )} - -
- ); -} diff --git a/apps/admin/src/app/layouts/01__Default/components/Footer.tsx b/apps/admin/src/app/layouts/01__Default/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/01__Default/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/01__Default/components/Header.tsx b/apps/admin/src/app/layouts/01__Default/components/Header.tsx index 0014ab7..8cd7d0a 100755 --- a/apps/admin/src/app/layouts/01__Default/components/Header.tsx +++ b/apps/admin/src/app/layouts/01__Default/components/Header.tsx @@ -1,99 +1,57 @@ -import { Menu, SquareChevronRight } from 'lucide-react'; +import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { useLocation } from 'react-router'; -import { Link } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useIsMobile, useScrollPosition } from '@crux-kit/hooks'; import { cn } from '@crux-kit/utils'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; import { Container } from '#shared/widgets/common/Container'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; -import { MegaMenu } from './MegaMenu'; -import { MegaMenuMobile } from './MegaMenuMobile'; +import { AppLogo, UserAvatarMenu } from '../../shared'; import { SidebarMenu } from './SidebarMenu'; export function Header() { const [isSidebarSheetOpen, setIsSidebarSheetOpen] = useState(false); - const [isMegaMenuSheetOpen, setIsMegaMenuSheetOpen] = useState(false); const { pathname } = useLocation(); - const mobileMode = useIsMobile(); + const isMobile = useIsMobile(); const scrollPosition = useScrollPosition(); const headerSticky: boolean = scrollPosition > 0; - // Close sheet when route changes useEffect(() => { setIsSidebarSheetOpen(false); - setIsMegaMenuSheetOpen(false); }, [pathname]); return (
- {/* HeaderLogo */}
- - mini-logo - -
- {mobileMode && ( - - - - - - - - - - - - )} - {mobileMode && ( - - - - - - - - - - - - )} -
+ + + {isMobile && ( + + + + + + + + + + + + )}
- {/* Main Content (MegaMenu) */} - {!mobileMode && } - - {/* HeaderTopbar */} -
- - } - /> +
+
diff --git a/apps/admin/src/app/layouts/01__Default/components/MegaMenu.tsx b/apps/admin/src/app/layouts/01__Default/components/MegaMenu.tsx deleted file mode 100755 index 5df9b1d..0000000 --- a/apps/admin/src/app/layouts/01__Default/components/MegaMenu.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { - NavigationMenu, - // NavigationMenuContent, - // NavigationMenuItem, - // NavigationMenuLink, - NavigationMenuList - // NavigationMenuTrigger, -} from '#shared/ui/NavigationMenu'; - -// import { useMenu } from '#shared/hooks/use_menu'; -// import { cn } from '@crux-kit/utils'; -// import { MegaMenuSubAccount } from '#shared/widgets/mega_menu/mega_menu_sub_account'; -// import { MegaMenuSubAuth } from '#shared/widgets/mega_menu/mega_menu_sub_auth'; -// import { MegaMenuSubNetwork } from '#shared/widgets/mega_menu/mega_menu_sub_network'; -// import { MegaMenuSubProfiles } from '#shared/widgets/mega_menu/mega_menu_sub_profiles'; -// import { MegaMenuSubStore } from '#shared/widgets/mega_menu/mega_menu_sub_store'; - -export function MegaMenu() { - // const { pathname } = useLocation(); - // const { isActive, hasActiveChild } = useMenu(pathname); - // const homeItem = MENU_MEGA[0]; - // const publicProfilesItem = MENU_MEGA[1]; - // const myAccountItem = MENU_MEGA[2]; - // const networkItem = MENU_MEGA[3]; - // const authItem = MENU_MEGA[4]; - // const storeItem = MENU_MEGA[5]; - - // const linkClass = ` - // text-sm text-secondary-foreground font-medium - // hover:text-primary hover:bg-transparent - // focus:text-primary focus:bg-transparent - // data-[active=true]:text-primary data-[active=true]:bg-transparent - // data-[state=open]:text-primary data-[state=open]:bg-transparent - // `; - - return ( - - - {/* Home Item */} - {/* - - - {homeItem.title} - - - */} - - {/* Public Profiles Item */} - {/* - - {publicProfilesItem.title} - - - - - */} - - {/* My Account Item */} - {/* - - {myAccountItem.title} - - - - - */} - - {/* Network Item */} - {/* - - {networkItem.title} - - - - - */} - - {/* Store Item */} - {/* - - {storeItem.title} - - - - - */} - - {/* Authentication Item */} - {/* - - {authItem.title} - - - - - */} - - - ); -} diff --git a/apps/admin/src/app/layouts/01__Default/components/MegaMenuMobile.tsx b/apps/admin/src/app/layouts/01__Default/components/MegaMenuMobile.tsx deleted file mode 100755 index eca44f3..0000000 --- a/apps/admin/src/app/layouts/01__Default/components/MegaMenuMobile.tsx +++ /dev/null @@ -1,197 +0,0 @@ -'use client'; - -import { LucideIcon } from 'lucide-react'; -import { JSX, useCallback } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -// import { MENU_MEGA_MOBILE } from '#app/menu'; -import { - AccordionMenu, - AccordionMenuClassNames, - AccordionMenuGroup, - AccordionMenuItem, - // AccordionMenuLabel, - AccordionMenuSub, - AccordionMenuSubContent, - AccordionMenuSubTrigger -} from '#shared/ui/AccordionMenu'; -import { Badge } from '#shared/ui/Badge'; - -export interface MenuItem { - title?: string; - icon?: LucideIcon; - path?: string; - rootPath?: string; - childrenIndex?: number; - heading?: string; - children?: MenuItem[]; - disabled?: boolean; - collapse?: boolean; - collapseTitle?: string; - expandTitle?: string; - badge?: string; - separator?: boolean; -} - -export type MenuConfig = MenuItem[]; - -export function MegaMenuMobile() { - const { pathname } = useLocation(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useCallback( - (path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), - [pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'space-y-1', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - sub: '', - subTrigger: - 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - subContent: 'py-0', - indicator: '' - }; - - // const buildMenu = (items: MenuConfig): JSX.Element[] => { - // return items.map((item: MenuItem, index: number) => { - // if (item.heading) { - // return buildMenuHeading(item, index); - // } else if (!item.disabled) { - // return buildMenuItemRoot(item, index); - // } else { - // return <>; - // } - // }); - // }; - - // const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - // if (item.children) { - // return ( - // - // - // {item.icon && } - //
- // {item.title} - // {item.badge && ( - // - // {item.badge} - // - // )} - //
- //
- // - // {buildMenuItemChildren(item.children, 1)} - // - //
- // ); - // } else { - // return ( - // - // - // {item.icon && } - //
- // {item.title} - // {item.badge && ( - // - // {item.badge} - // - // )} - //
- // - //
- // ); - // } - // }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.disabled) { - return buildMenuItemChild(item, index, level); - } else { - return <>; - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - {item.badge && ( - - {item.badge} - - )} - - - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - - {item.icon && } -
- {item.title} - {item.badge && ( - - {item.badge} - - )} -
- -
- ); - } - }; - - // const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => { - // return {item.heading}; - // }; - - return ( -
- - {/* {buildMenu(MENU_MEGA_MOBILE)} */} - -
- ); -} diff --git a/apps/admin/src/app/layouts/01__Default/components/Sidebar.tsx b/apps/admin/src/app/layouts/01__Default/components/Sidebar.tsx index d7d1e0b..f35f32d 100755 --- a/apps/admin/src/app/layouts/01__Default/components/Sidebar.tsx +++ b/apps/admin/src/app/layouts/01__Default/components/Sidebar.tsx @@ -1,4 +1,3 @@ -import { useLocation } from 'react-router-dom'; import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; @@ -7,15 +6,12 @@ import { SidebarMenu } from './SidebarMenu'; export function Sidebar() { const { settings } = useSettings(); - const { pathname } = useLocation(); return (
diff --git a/apps/admin/src/app/layouts/01__Default/components/SidebarHeader.tsx b/apps/admin/src/app/layouts/01__Default/components/SidebarHeader.tsx index e1ef1a1..94e932c 100755 --- a/apps/admin/src/app/layouts/01__Default/components/SidebarHeader.tsx +++ b/apps/admin/src/app/layouts/01__Default/components/SidebarHeader.tsx @@ -1,59 +1,34 @@ import { ChevronFirst } from 'lucide-react'; -import { Link } from 'react-router-dom'; import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Button } from '#shared/ui/Button'; +import { AppLogo } from '../../shared'; export function SidebarHeader() { const { settings, storeOption } = useSettings(); + const collapsed = settings.layouts[LayoutNames.DEFAULT].sidebarCollapse; + const handleToggleClick = () => { - storeOption( - `layouts.${LayoutNames.DEFAULT}.sidebarCollapse`, - !settings.layouts[LayoutNames.DEFAULT].sidebarCollapse - ); + storeOption(`layouts.${LayoutNames.DEFAULT}.sidebarCollapse`, !collapsed); }; return ( -
- -
- {/* Default Logo - Mini Logo */} -
-
- {/* Default Dark Logo */} - Mini Logo -
- +
+ + + C.R.U.X +
); diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/README.md b/apps/admin/src/app/layouts/02__HorizontalMenu/README.md new file mode 100644 index 0000000..5621bf9 --- /dev/null +++ b/apps/admin/src/app/layouts/02__HorizontalMenu/README.md @@ -0,0 +1,25 @@ +# Horizontal Menu layout + +**Metronic 9 reference:** [Demo 2](https://keenthemes.com/metronic/tailwind/react/demo2) + +A header carrying the logo and the account menu, with the application menu on a horizontal menubar +underneath it. The header becomes sticky and shrinks once the page is scrolled past +`layouts.horizontal-menu-layout.headerStickyOffset`. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Navigation menubar | `NavMenubar` from `../shared` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Header`, `HeaderLogo`, `Navbar`. + +## Notes + +- Below `lg` the menubar is replaced by an accordion in a sheet, since a horizontal menubar cannot + hold the full menu on a narrow viewport. +- The `data-sticky-header` body attribute is removed on unmount so the next layout starts clean. diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Footer.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Header.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Header.tsx index 0a78112..30ae944 100755 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Header.tsx +++ b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Header.tsx @@ -1,44 +1,45 @@ -import { useEffect, useState } from 'react'; // Add useState to the imports - +import { useEffect, useState } from 'react'; import { useScrollPosition } from '@crux-kit/hooks'; import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; import { Container } from '#shared/widgets/common/Container'; +import { UserAvatarMenu } from '../../shared'; import { HeaderLogo } from './HeaderLogo'; -import { HeaderTopbar } from './HeaderTopbar'; export function Header() { const { settings } = useSettings(); const scrollPosition = useScrollPosition(); const [headerStickyOn, setHeaderStickyOn] = useState(false); - useEffect(() => { - const isSticky = - scrollPosition > settings.layouts[LayoutNames.HORIZONTAL_MENU].headerStickyOffset; - setHeaderStickyOn(isSticky); - }, [scrollPosition, settings]); + const { headerSticky, headerStickyOffset } = settings.layouts[LayoutNames.HORIZONTAL_MENU]; useEffect(() => { - if (headerStickyOn === true) { + setHeaderStickyOn(scrollPosition > headerStickyOffset); + }, [scrollPosition, headerStickyOffset]); + + useEffect(() => { + if (headerStickyOn) { document.body.setAttribute('data-sticky-header', 'on'); } else { document.body.removeAttribute('data-sticky-header'); } + + return () => document.body.removeAttribute('data-sticky-header'); }, [headerStickyOn]); return (
- +
); diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderLogo.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderLogo.tsx index 19dc848..99db9e7 100755 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderLogo.tsx +++ b/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderLogo.tsx @@ -1,71 +1,41 @@ -// import { useEffect, useState } from 'react'; -// import { ChevronDown } from 'lucide-react'; -import { Link } from 'react-router-dom'; -// import { MENU_ROOT } from '#app/menu'; -// import { -// DropdownMenu, -// DropdownMenuContent, -// DropdownMenuItem, -// DropdownMenuTrigger, -// } from '#shared/ui/dropdown_menu'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; - -// import { cn } from '@crux-kit/utils'; +import { Menu } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useIsMobile } from '@crux-kit/hooks'; +import { Button } from '#shared/ui/Button'; +import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; +import { AppLogo, AppWordmark, SidebarAccordionMenu } from '../../shared'; export function HeaderLogo() { - // const { pathname } = useLocation(); - // const [selectedMenuItem, setSelectedMenuItem] = useState(MENU_ROOT[1]); + const { pathname } = useLocation(); + const isMobile = useIsMobile(); + const [isSheetOpen, setIsSheetOpen] = useState(false); - // useEffect(() => { - // MENU_ROOT.forEach((item) => { - // if (item.rootPath && pathname.includes(item.rootPath)) { - // setSelectedMenuItem(item); - // } - // }); - // }, [pathname]); + useEffect(() => { + setIsSheetOpen(false); + }, [pathname]); return (
- {/* Logo Section */} - - logo - logo - + - {/* Menu Section */} -
-

Metronic Team

- / + - {/* - - {selectedMenuItem.title} - - - - {MENU_ROOT.map((item, index) => ( - - - {item.icon && } - {item.title} - - - ))} - - */} -
+ {isMobile && ( + + + + + + + + + + + + )}
); } diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderTopbar.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderTopbar.tsx deleted file mode 100755 index 1779a68..0000000 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/HeaderTopbar.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// import { MessageCircleMore, MessageSquareDot, Search } from 'lucide-react'; -// import { Button } from '#shared/ui/button'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -// import { NotificationsSheet } from '#shared/partials/topbar/notifications-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -export function HeaderTopbar() { - return ( -
-
- {/* - - - } - /> - - - - } - /> - - - - } - /> */} -
- - } - /> -
- ); -} diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Navbar.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Navbar.tsx index 36cca9e..0c1c8c0 100755 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Navbar.tsx +++ b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Navbar.tsx @@ -1,13 +1,18 @@ +import { useIsMobile } from '@crux-kit/hooks'; import { Container } from '#shared/widgets/common/Container'; -import { NavbarLinks } from './NavbarLinks'; -import { NavbarMenu } from './NavbarMenu'; +import { NavMenubar } from '../../shared'; export function Navbar() { + const isMobile = useIsMobile(); + + if (isMobile) { + return null; + } + return (
- - +
); diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarLinks.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarLinks.tsx deleted file mode 100755 index 765416b..0000000 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarLinks.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export function NavbarLinks() { - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarMenu.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarMenu.tsx deleted file mode 100755 index 162a07d..0000000 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/NavbarMenu.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { ChevronDown } from 'lucide-react'; -import { useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; -import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar'; - -export function NavbarMenu() { - const { pathname } = useLocation(); - const menuConfig = useMenuConfig(); - - let navbarMenu; - - if (pathname.includes('/public-profile/')) { - navbarMenu = menuConfig?.[2]; - } else if (pathname.includes('/network/')) { - navbarMenu = menuConfig?.[4]; - } else if (pathname.includes('/store-client/')) { - navbarMenu = menuConfig?.[0]; - } else if (pathname.includes('/authentication/')) { - navbarMenu = menuConfig?.[5]; - } else { - navbarMenu = menuConfig?.[3]; - } - - const { hasActiveChild } = useMenu(pathname); - // A leaf highlights only when nothing deeper in the menu matches the route more closely. - const isActive = useMemo( - () => createMenuPathMatcher((navbarMenu?.children ?? []) as MenuConfig, pathname), - [navbarMenu, pathname] - ); - - const buildMenu = (items: MenuConfig) => { - return items.map((item, index) => { - if (item.children) { - return ( - - - {item.title} - - - - {buildSubMenu(item.children)} - - - ); - } else { - return ( - - - - {item.title} - - - - ); - } - }); - }; - - const buildSubMenu = (items: MenuConfig) => { - return items.map((item, index) => { - if (item.children) { - return ( - - - {item.title} - - - {buildSubMenu(item.children)} - - - ); - } else { - return ( - - {item.title} - - ); - } - }); - }; - - return ( -
-
- - {buildMenu(navbarMenu.children as MenuConfig)} - -
-
- ); -} diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Toolbar.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/components/Toolbar.tsx deleted file mode 100755 index 95cbf35..0000000 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/components/Toolbar.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ChevronRight } from 'lucide-react'; -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( -
- - {children} - -
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const menuConfig = useMenuConfig(); - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
-
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && } - - ); - })} -
-
- ); -} - -const ToolbarHeading = ({ title = '' }: ToolbarHeadingProps) => { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -}; - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/02__HorizontalMenu/layout.tsx b/apps/admin/src/app/layouts/02__HorizontalMenu/layout.tsx index aa6701e..21ddf93 100755 --- a/apps/admin/src/app/layouts/02__HorizontalMenu/layout.tsx +++ b/apps/admin/src/app/layouts/02__HorizontalMenu/layout.tsx @@ -1,42 +1,23 @@ // DEMO 2 // https://keenthemes.com/metronic/tailwind/react/demo2 -import { addDays, format } from 'date-fns'; -import { CalendarDays } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Navbar } from './components/Navbar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export function HorizontalMenuLayout() { - const { pathname } = useLocation(); - const { setOption } = useSettings(); - - useEffect(() => { - // Set current layout - setOption('layout', LayoutNames.HORIZONTAL_MENU); - }, [setOption]); + useLayoutSettings(LayoutNames.HORIZONTAL_MENU); + const { showToolbar, showFooter } = useLayoutChrome(); useBodyClass(` - [--header-height-default:100px] + [--header-height-default:100px] data-[sticky-header=on]:[--header-height:60px] - [--header-height:var(--header-height-default)] + [--header-height:var(--header-height-default)] `); - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); - return ( <>
@@ -45,45 +26,15 @@ export function HorizontalMenuLayout() {
- {!pathname.includes('/public-profile/') && ( + {showToolbar && ( - - - - - - - - - - )}
-
+ {showFooter && }
); diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/README.md b/apps/admin/src/app/layouts/03__CompactSidebar/README.md new file mode 100644 index 0000000..78b985a --- /dev/null +++ b/apps/admin/src/app/layouts/03__CompactSidebar/README.md @@ -0,0 +1,26 @@ +# Compact Sidebar layout + +**Metronic 9 reference:** [Demo 3](https://keenthemes.com/metronic/tailwind/react/demo3) + +A narrow icon rail listing the top-level menu sections, with the selected section's children on a +horizontal navbar above the content card. + +## Composition + +| Piece | Source | +| --------------------------- | ------------------------------------------------------------ | +| Section chooser (icon rail) | `useAppMenu` + `firstMenuPath` from `../shared` | +| Section navbar | `NavMenubar` from `../shared`, fed by `useActiveMenuSection` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Header`, `HeaderLogo`, `Sidebar`, `SidebarMenu`, `Navbar`. + +## Notes + +- A section with no children renders no navbar, so a single-page section such as the dashboard fills + the card. +- This layout sets `container` to `fluid`; the setting is per-layout, so other layouts keep theirs. +- Body variables: `--header-height: 58px`, `--sidebar-width: 58px`, `--navbar-height: 56px`. diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/Footer.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/Header.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/Header.tsx index 685d266..4340d07 100755 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/Header.tsx +++ b/apps/admin/src/app/layouts/03__CompactSidebar/components/Header.tsx @@ -1,13 +1,13 @@ import { Container } from '#shared/widgets/common/Container'; +import { UserAvatarMenu } from '../../shared'; import { HeaderLogo } from './HeaderLogo'; -import { HeaderTopbar } from './HeaderTopbar'; export function Header() { return (
- +
); diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderLogo.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderLogo.tsx index 4088a69..a4a61c5 100755 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderLogo.tsx +++ b/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderLogo.tsx @@ -1,87 +1,39 @@ import { Menu } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -// import { cn } from '@crux-kit/utils'; +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { Button } from '#shared/ui/Button'; -// import { -// DropdownMenu, -// DropdownMenuContent, -// DropdownMenuItem, -// DropdownMenuTrigger, -// } from '#shared/ui/dropdown_menu'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; -import { SidebarMenu } from './SidebarMenu'; +import { AppLogo, AppWordmark, SidebarAccordionMenu } from '../../shared'; export function HeaderLogo() { - // const { pathname } = useLocation(); - // const [selectedMenuItem, setSelectedMenuItem] = useState(MENU_ROOT[1]); + const { pathname } = useLocation(); + const [isSheetOpen, setIsSheetOpen] = useState(false); - // useEffect(() => { - // MENU_ROOT.forEach((item) => { - // if (item.rootPath && pathname.includes(item.rootPath)) { - // setSelectedMenuItem(item); - // } - // }); - // }, [pathname]); + useEffect(() => { + setIsSheetOpen(false); + }, [pathname]); return (
- {/* Logo */}
- + - + - - + + - - logo - logo - +
- {/* Menu Section */} -
-

Metronic Team

- / - - {/* - - {selectedMenuItem.title} - - - - {MENU_ROOT.map((item, index) => ( - - - {item.icon && } - {item.title} - - - ))} - - */} -
+
); } diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderTopbar.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderTopbar.tsx deleted file mode 100755 index 293aed2..0000000 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/HeaderTopbar.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// import { LayoutGrid, MessageCircleMore, MessageSquareDot, Search } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -import { Button } from '#shared/ui/Button'; -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; -// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -// import { NotificationsSheet } from '#shared/partials/topbar/notifications-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -export function HeaderTopbar() { - return ( -
- - {/* - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> */} - - } - /> -
- ); -} diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/Navbar.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/Navbar.tsx index 966ce6c..9534db5 100755 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/Navbar.tsx +++ b/apps/admin/src/app/layouts/03__CompactSidebar/components/Navbar.tsx @@ -1,14 +1,20 @@ import { Container } from '#shared/widgets/common/Container'; -import { NavbarLinks } from './NavbarLinks'; -import { NavbarMenu } from './NavbarMenu'; +import { NavMenubar, useActiveMenuSection } from '../../shared'; +/** The children of the section selected in the compact sidebar. */ export function Navbar() { + const section = useActiveMenuSection(); + const items = section?.children ?? []; + + if (items.length === 0) { + return null; + } + return (
- - +
diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarLinks.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarLinks.tsx deleted file mode 100755 index f4c86f9..0000000 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarLinks.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { addDays, format } from 'date-fns'; -import { CalendarDays } from 'lucide-react'; -import { useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; - -export function NavbarLinks() { - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); - - return ( -
- - - - - - - - -
- ); -} diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarMenu.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarMenu.tsx deleted file mode 100755 index 99fefa8..0000000 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/NavbarMenu.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { ChevronDown } from 'lucide-react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '#shared/ui/MenuBar'; - -export function NavbarMenu() { - const { pathname } = useLocation(); - const menuConfig = useMenuConfig(); - - let navbarMenu; - - if (pathname.includes('/public-profile/')) { - navbarMenu = menuConfig?.[2]; - } else if (pathname.includes('/network/')) { - navbarMenu = menuConfig?.[4]; - } else if (pathname.includes('/store-client/')) { - navbarMenu = menuConfig?.[0]; - } else if (pathname.includes('/authentication/')) { - navbarMenu = menuConfig?.[5]; - } else { - navbarMenu = menuConfig?.[3]; - } - - const { isActive, hasActiveChild } = useMenu(pathname); - - const buildMenu = (items: MenuConfig) => { - return items.map((item, index) => { - if (item.children) { - return ( - - - {item.title} - - - - {buildSubMenu(item.children)} - - - ); - } else { - return ( - - - - {item.title} - - - - ); - } - }); - }; - - const buildSubMenu = (items: MenuConfig) => { - return items.map((item, index) => { - if (item.children) { - return ( - - - {item.title} - - - {buildSubMenu(item.children)} - - - ); - } else { - return ( - - {item.title} - - ); - } - }); - }; - - return ( -
-
- - {buildMenu(navbarMenu.children as MenuConfig)} - -
-
- ); -} diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/SidebarMenu.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/SidebarMenu.tsx index 9fc9023..589faf4 100755 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/SidebarMenu.tsx +++ b/apps/admin/src/app/layouts/03__CompactSidebar/components/SidebarMenu.tsx @@ -1,99 +1,52 @@ -import { BarChart3, Bell, CheckSquare, Code, HelpCircle, MessageSquare, Settings, Shield, UserCircle, Users } from 'lucide-react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { cn } from '@crux-kit/utils'; +import { useMenu } from '#shared/hooks/useMenu'; import { Button } from '#shared/ui/Button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '#shared/ui/Tooltip'; +import { firstMenuPath, useAppMenu } from '../../shared'; -export interface Item { - icon: React.ComponentType<{ className?: string }>; - path: string; - title: string; - newTab?: boolean; - active?: boolean; -} - +/** + * The section chooser: one icon per top-level menu entry. Its children are shown by the navbar, so + * this rail never needs to expand. + */ export function SidebarMenu() { - const items: Item[] = [ - { - icon: BarChart3, - path: '/', - title: 'Dashboard' - }, - { - icon: UserCircle, - path: '/public-profile/profiles/default', - title: 'Profile' - }, - { - icon: Settings, - path: '/account/home/get-started', - title: 'Account' - }, - { - icon: Users, - path: '/network/get-started', - title: 'Network', - active: true - }, - { - icon: Shield, - path: '/account/billing/plans', - title: 'Plans' - }, - { - icon: MessageSquare, - path: '/account/security/security-log', - title: 'Security Logs' - }, - { - icon: Bell, - path: '/account/notifications', - title: 'Notifications' - }, - { - icon: CheckSquare, - path: '/account/members/roles', - title: 'ACL' - }, - { - icon: Code, - path: '/account/api-keys', - title: 'API Keys' - }, - { - icon: HelpCircle, - path: 'https://docs.keenthemes.com/metronic-vite', - title: 'Docs' - } - ]; + const menuConfig = useAppMenu(); + const { pathname } = useLocation(); + const { isItemActive } = useMenu(pathname); return (
- {items.map((item, index) => ( - - - - - {item.title} - - ))} + + {item.icon && } + + + + {item.title} + + ); + })}
); diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/components/Toolbar.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/components/Toolbar.tsx deleted file mode 100755 index 594197e..0000000 --- a/apps/admin/src/app/layouts/03__CompactSidebar/components/Toolbar.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { ChevronRight } from 'lucide-react'; -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( -
- - {children} - -
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const menuConfig = useMenuConfig(); - - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
-
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && } - - ); - })} -
-
- ); -} - -const ToolbarHeading = ({ title = '' }: ToolbarHeadingProps) => { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -}; - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/03__CompactSidebar/layout.tsx b/apps/admin/src/app/layouts/03__CompactSidebar/layout.tsx index 1b0d9ed..688ae2c 100755 --- a/apps/admin/src/app/layouts/03__CompactSidebar/layout.tsx +++ b/apps/admin/src/app/layouts/03__CompactSidebar/layout.tsx @@ -1,66 +1,48 @@ // DEMO 3 // https://keenthemes.com/metronic/tailwind/react/demo3 -import { Download } from 'lucide-react'; -import { useEffect } from 'react'; -import { Link, Outlet, useLocation } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Navbar } from './components/Navbar'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export function CompactSidebarLayout() { - const { pathname } = useLocation(); - const { setOption } = useSettings(); - const isMobileMode = useIsMobile(); + useLayoutSettings(LayoutNames.COMPACT_SIDEBAR); + const isMobile = useIsMobile(); + const { showToolbar, showFooter } = useLayoutChrome(); useBodyClass(` - [--header-height:58px] - [--sidebar-width:58px] - [--navbar-height:56px] - lg:overflow-hidden + [--header-height:58px] + [--sidebar-width:58px] + [--navbar-height:56px] + lg:overflow-hidden bg-muted! `); - useEffect(() => { - setOption('layout', LayoutNames.COMPACT_SIDEBAR); - setOption('container', 'fluid'); - }, [setOption]); - return ( <>
- {!isMobileMode && } + {!isMobile && }
- {pathname !== '/' && ( + {showToolbar && ( - - - )}
-
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/README.md b/apps/admin/src/app/layouts/04__TwoColumnSidebar/README.md new file mode 100644 index 0000000..d0bb722 --- /dev/null +++ b/apps/admin/src/app/layouts/04__TwoColumnSidebar/README.md @@ -0,0 +1,24 @@ +# Two Column Sidebar layout + +**Metronic 9 reference:** [Demo 4](https://keenthemes.com/metronic/tailwind/react/demo4) + +Two sidebar columns: a narrow icon rail choosing the top-level section, and a wider column with that +section's children as an accordion. Below `lg` both columns move into a single sheet. + +## Composition + +| Piece | Source | +| ----------------------- | ---------------------------------------------------------------------- | +| Section chooser | `useAppMenu` + `firstMenuPath` from `../shared` | +| Section menu | `SidebarAccordionMenu` from `../shared`, fed by `useActiveMenuSection` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Sidebar`, `SidebarPrimary`, `SidebarSecondary`, `Header`. + +## Notes + +- When the active section has no children, the secondary column shows the whole menu rather than + going blank. +- Body variables: `--header-height: 60px`, `--sidebar-width: 290px`. diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Footer.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Header.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Header.tsx index a30498d..72f5ba8 100755 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Header.tsx +++ b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Header.tsx @@ -1,10 +1,10 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { useLocation } from 'react-router-dom'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; import { Container } from '#shared/widgets/common/Container'; +import { AppLogo } from '../../shared'; import { SidebarPrimary } from './SidebarPrimary'; import { SidebarSecondary } from './SidebarSecondary'; @@ -12,7 +12,6 @@ export function Header() { const [isSheetOpen, setIsSheetOpen] = useState(false); const { pathname } = useLocation(); - // Close sheet when route changes useEffect(() => { setIsSheetOpen(false); }, [pathname]); @@ -20,18 +19,7 @@ export function Header() { return (
- - - - + diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDashboard.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDashboard.tsx deleted file mode 100755 index 6757b4b..0000000 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDashboard.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { Badge, ChevronDown, FileText, Settings, SquareCode, UserCircle } from 'lucide-react'; -import { useCallback, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel } from '#shared/ui/AccordionMenu'; -import { Button } from '#shared/ui/Button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; - -interface DropdownItem { - title: string; - path: string; - icon: React.ComponentType<{ className?: string }>; - active?: boolean; -} - -interface MenuItem { - title: string; - path?: string; - active?: boolean; -} - -interface MenuLabel { - label: string; -} - -type MenuNode = MenuItem | MenuLabel; - -export function SidebarMenuDashboard() { - const { pathname } = useLocation(); - - const dropdownItems: DropdownItem[] = [ - { - title: 'Client API', - path: '/account/home/user-profile', - icon: SquareCode, - active: true - }, - { - title: 'Profile', - path: '/public-profile/profiles/company', - icon: UserCircle - }, - { - title: 'My Account', - path: '/account/integrations', - icon: Settings - }, - { - title: 'Projects', - path: '/public-profile/projects/3-columns', - icon: FileText - }, - { - title: 'Personal info', - path: '/public-profile/profiles/creator', - icon: Badge - } - ]; - - const currentDropdownItem = dropdownItems[0]; - - const menuItems = useMemo( - () => [ - { label: 'Configuration' }, - { title: 'API Setup', path: '/account/api-keys' }, - { title: 'Team Settings', path: '/account/home/settings-sidebar' }, - { title: 'Authentication', path: '/authentication/classic/sign-in' }, - { title: 'Endpoints Configs', path: '/account/appearance' }, - { title: 'Rate Limiting', path: '/public-profile/network' }, - { label: 'Security' }, - { title: 'Data Encryption', path: '/account/billing/enterprise' }, - { title: 'Text', path: '/account/security/overview' }, - { title: 'Access Control', path: '/account/security/privacy-settings' }, - { label: 'Analytics' }, - { - title: 'Incident Response', - path: '/account/security/current-sessions' - }, - { title: 'Fetching Data', path: '/account/members/team-info' }, - { title: 'Custom Reports', path: '/account/home/user-profile' }, - { - title: 'Real Time Analytics', - path: '/account/home/settings-enterprise' - }, - { title: 'Exporting Data', path: '/account/members/import-members' }, - { title: 'Dashboard Integration', path: '/account/members/team-info' } - ], - [] - ); - - const classNames: AccordionMenuClassNames = { - root: 'space-y-1', - label: 'uppercase text-xs font-medium text-muted-foreground/80 pt-6 mb-2 pb-0', - item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium' - }; - - const matchPath = useCallback( - (path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), - [pathname] - ); - - const buildDropdown = () => { - return ( - - - - - - {dropdownItems.map((item, index) => ( - - - - {item.title} - - - ))} - - - ); - }; - - const buildMenu = () => { - return ( - - - {menuItems.map((item, index) => - 'label' in item ? ( - {item.label} - ) : ( - - {item.title} - - ) - )} - - - ); - }; - - return ( -
- {buildDropdown()} - {buildMenu()} -
- ); -} diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDefault.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDefault.tsx deleted file mode 100755 index 5e70a92..0000000 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarMenuDefault.tsx +++ /dev/null @@ -1,183 +0,0 @@ -'use client'; - -import { JSX, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig, MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; -import { Badge } from '#shared/ui/Badge'; - -export function SidebarMenuDefault() { - const { pathname } = useLocation(); - const fullMenuConfig = useMenuConfig(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useMemo( - () => createMenuPathMatcher(fullMenuConfig, pathname), - [fullMenuConfig, pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'lg:ps-1 space-y-1', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium', - sub: '', - subTrigger: - 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-transparent data-[selected=true]:font-medium', - subContent: 'ps-3 py-0', - indicator: '' - }; - - const buildMenu = (items: MenuConfig): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (item.heading) { - return buildMenuHeading(item, index); - } else if (item.disabled) { - return buildMenuItemRootDisabled(item, index); - } else { - return buildMenuItemRoot(item, index); - } - }); - }; - - const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.title} - - - {buildMenuItemChildren(item.children, 1)} - - - ); - } else { - return ( - - - {item.icon && } - {item.title} - - - ); - } - }; - - const buildMenuItemRootDisabled = (item: MenuItem, index: number): JSX.Element => { - return ( - - {item.icon && } - {item.title} - {item.disabled && ( - - Soon - - )} - - ); - }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (item.disabled) { - return buildMenuItemChildDisabled(item, index, level); - } else { - return buildMenuItemChild(item, index, level); - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - - 0 ? '' : '') - )} - > - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - {item.title} - - ); - } - }; - - const buildMenuItemChildDisabled = ( - item: MenuItem, - index: number, - level: number = 0 - ): JSX.Element => { - return ( - - {item.title} - {item.disabled && ( - - Soon - - )} - - ); - }; - - const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => { - return {item.heading}; - }; - - // Memoize the menu to prevent unnecessary re-renders - const { getChildren } = useMenu(pathname); - const menuItems = getChildren(fullMenuConfig, 0); - - return ( - - {buildMenu(menuItems || [])} - - ); -} diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarPrimary.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarPrimary.tsx index 6e4e40e..689c605 100755 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarPrimary.tsx +++ b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarPrimary.tsx @@ -1,184 +1,57 @@ -import { BarChart3, Bell, CheckSquare, Code, MessageSquare, Settings, Shield, ShoppingCart, UserCircle, Users } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { useViewport } from '@crux-kit/hooks'; -import { cn, getHeight } from '@crux-kit/utils'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { cn } from '@crux-kit/utils'; +import { useMenu } from '#shared/hooks/useMenu'; import { Button } from '#shared/ui/Button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '#shared/ui/Tooltip'; -// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -interface MenuItem { - icon: React.ComponentType<{ className?: string }>; - tooltip: string; - path: string; - rootPath?: string; -} - -const menuItems: MenuItem[] = [ - { icon: BarChart3, tooltip: 'Dashboard', path: '/', rootPath: '/' }, - { - icon: UserCircle, - tooltip: 'Profile', - path: '/public-profile/profiles/default', - rootPath: '/public-profile/' - }, - { - icon: Settings, - tooltip: 'Account', - path: '/account/home/get-started', - rootPath: '/account/' - }, - { - icon: Users, - tooltip: 'Network', - path: '/network/get-started', - rootPath: '/network/' - }, - { - icon: ShoppingCart, - tooltip: 'Store - Client', - path: '/store-client/home', - rootPath: '' - }, - { - icon: Shield, - tooltip: 'Authentication', - path: '/authentication/get-started', - rootPath: '/authentication/' - }, - { - icon: MessageSquare, - tooltip: 'Security Logs', - path: '/account/security/security-log', - rootPath: '/account/' - }, - { - icon: Bell, - tooltip: 'Notifications', - path: '/account/notifications', - rootPath: '' - }, - { - icon: CheckSquare, - tooltip: 'ACL', - path: '/account/members/roles', - rootPath: '' - }, - { icon: Code, tooltip: 'API Keys', path: '/account/api-keys', rootPath: '' } -]; +import { AppLogo, firstMenuPath, useAppMenu, UserAvatarMenu } from '../../shared'; export function SidebarPrimary() { - const headerRef = useRef(null); - const footerRef = useRef(null); - const [scrollableHeight, setScrollableHeight] = useState(0); - const [viewportHeight] = useViewport(); - const scrollableOffset = 80; + const menuConfig = useAppMenu(); const { pathname } = useLocation(); - - useEffect(() => { - if (headerRef.current && footerRef.current) { - const headerHeight = getHeight(headerRef.current); - const footerHeight = getHeight(footerRef.current); - const availableHeight = viewportHeight - headerHeight - footerHeight - scrollableOffset; - setScrollableHeight(availableHeight); - } else { - setScrollableHeight(viewportHeight); - } - }, [viewportHeight]); - - const [selectedMenuItem, setSelectedMenuItem] = useState(menuItems[0]); - - useEffect(() => { - menuItems.forEach((item) => { - if (item.rootPath === pathname || (item.rootPath && pathname.includes(item.rootPath))) { - setSelectedMenuItem(item); - } - }); - }, [pathname]); + const { isItemActive } = useMenu(pathname); return (
-
- - - - +
+
-
-
- {menuItems.map((item, index) => ( +
+ {menuConfig.map((item, index) => { + const path = firstMenuPath(item); + + if (!path) { + return null; + } + + return ( - {item.tooltip} + {item.title} - ))} -
+ ); + })}
-
- {/* - - - } - /> - - - - } - /> */} - - } - /> +
+
diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarSecondary.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarSecondary.tsx index 469a516..cdefb0b 100755 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarSecondary.tsx +++ b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/SidebarSecondary.tsx @@ -1,13 +1,30 @@ -import { useLocation } from 'react-router'; -import { SidebarMenuDashboard } from './SidebarMenuDashboard'; -import { SidebarMenuDefault } from './SidebarMenuDefault'; +import { SidebarAccordionMenu, useActiveMenuSection } from '../../shared'; +const CLASS_NAMES = { + root: 'lg:ps-1 space-y-1', + group: 'gap-px', + label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', + separator: '', + item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium', + sub: '', + subTrigger: + 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-transparent data-[selected=true]:font-medium', + subContent: 'ps-3 py-0', + indicator: '' +}; + +/** + * The children of the section chosen in the primary column. A section without children (a + * single-page entry such as the dashboard) falls back to the whole menu, so this column is never + * blank. + */ export function SidebarSecondary() { - const { pathname } = useLocation(); + const section = useActiveMenuSection(); + const items = section?.children; return (
- {pathname === '/' ? : } +
); } diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Toolbar.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Toolbar.tsx deleted file mode 100755 index 196da5b..0000000 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/components/Toolbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( -
- - {children} - -
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const { pathname } = useLocation(); - const menuConfig = useMenuConfig(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -const ToolbarHeading = ({ title = '' }: ToolbarHeadingProps) => { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -}; - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/04__TwoColumnSidebar/layout.tsx b/apps/admin/src/app/layouts/04__TwoColumnSidebar/layout.tsx index 4d34533..47eab23 100755 --- a/apps/admin/src/app/layouts/04__TwoColumnSidebar/layout.tsx +++ b/apps/admin/src/app/layouts/04__TwoColumnSidebar/layout.tsx @@ -1,90 +1,46 @@ // DEMO 4 // https://keenthemes.com/metronic/tailwind/react/demo4 -import { Download } from 'lucide-react'; -import { useEffect } from 'react'; -import { Link, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; -// import { NotificationsSheet } from '#shared/partials/topbar/notifications-sheet'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export function TwoColumnSidebarLayout() { - const { setOption } = useSettings(); - const isMobileMode = useIsMobile(); + useLayoutSettings(LayoutNames.TWO_COLUMN_SIDEBAR); + const isMobile = useIsMobile(); + const { showToolbar, showFooter } = useLayoutChrome(); - // Using the custom hook to set multiple CSS variables and class properties useBodyClass(` - [--header-height:60px] - [--sidebar-width:290px] - lg:overflow-hidden + [--header-height:60px] + [--sidebar-width:290px] + lg:overflow-hidden bg-muted! `); - useEffect(() => { - setOption('layout', LayoutNames.TWO_COLUMN_SIDEBAR); - }, [setOption]); - return ( <>
- {isMobileMode &&
} + {isMobile &&
}
- {!isMobileMode && } + {!isMobile && }
- - - - - {/* - - - } - /> - - - - } - /> */} - - - + {showToolbar && ( + + + + )}
-
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/README.md b/apps/admin/src/app/layouts/05__DualRowsHeader/README.md new file mode 100644 index 0000000..415f03c --- /dev/null +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/README.md @@ -0,0 +1,25 @@ +# Dual Rows Header layout + +**Metronic 9 reference:** [Demo 5](https://keenthemes.com/metronic/tailwind/react/demo5) + +A two-row header — logo and account menu on top, the top-level sections on a menubar below — with +the selected section's children in a slim sidebar beside the content. + +## Composition + +| Piece | Source | +| ----------------------- | ---------------------------------------------------------------------- | +| Section menubar | `NavMenubar` from `../shared` | +| Section sidebar | `SidebarAccordionMenu` from `../shared`, fed by `useActiveMenuSection` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Header`, `HeaderLogo`, `Navbar`, `Sidebar`. + +## Notes + +- A section with no children renders no sidebar, so the content spans the full width. +- This layout sets `container` to `fluid`. +- Body variables: `--header-height: 54px`, `--sidebar-width: 200px`. diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Footer.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Header.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Header.tsx index de86bd0..cfa0000 100755 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Header.tsx +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Header.tsx @@ -4,42 +4,43 @@ import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; import { Container } from '#shared/widgets/common/Container'; +import { UserAvatarMenu } from '../../shared'; import { HeaderLogo } from './HeaderLogo'; -import { HeaderTopbar } from './HeaderTopbar'; -const Header = () => { +export function Header() { const { settings } = useSettings(); const scrollPosition = useScrollPosition(); const [headerStickyOn, setHeaderStickyOn] = useState(false); - useEffect(() => { - const isSticky = - scrollPosition > settings.layouts[LayoutNames.HORIZONTAL_MENU].headerStickyOffset; - setHeaderStickyOn(isSticky); - }, [scrollPosition, settings]); + const { headerSticky, headerStickyOffset } = settings.layouts[LayoutNames.DUAL_ROW_HEADER]; useEffect(() => { - if (headerStickyOn === true) { + setHeaderStickyOn(scrollPosition > headerStickyOffset); + }, [scrollPosition, headerStickyOffset]); + + useEffect(() => { + if (headerStickyOn) { document.body.setAttribute('data-sticky-header', 'on'); } else { document.body.removeAttribute('data-sticky-header'); } + + return () => document.body.removeAttribute('data-sticky-header'); }, [headerStickyOn]); return (
- + - +
); -}; - -export { Header }; +} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderLogo.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderLogo.tsx index b4ad080..00dfd7a 100755 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderLogo.tsx +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderLogo.tsx @@ -1,108 +1,25 @@ -import { ChevronDown, Menu, Settings, Shield, UserCircle, Users, type LucideIcon } from 'lucide-react'; +import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useIsMobile } from '@crux-kit/hooks'; -import { cn } from '@crux-kit/utils'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Button } from '#shared/ui/Button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; -import { SidebarMenuDashboard } from './SidebarMenuDashboard'; -import { SidebarMenuDefault } from './SidebarMenuDefault'; - -interface HeaderLogoTeam { - title: string; - icon?: LucideIcon; - urlPartial: string; - path: string; -} -type HeaderLogoTeams = Array; - -interface HeaderLogoItem { - title: string; - icon?: LucideIcon; -} -type HeaderLogoItems = Array; - -interface HeaderLogoStaging { - title: string; - icon?: LucideIcon; -} -type HeaderLogoStagings = Array; +import { AppLogo, AppWordmark, SidebarAccordionMenu } from '../../shared'; export function HeaderLogo() { const { pathname } = useLocation(); const [isSheetOpen, setIsSheetOpen] = useState(false); const isMobile = useIsMobile(); - const teams: HeaderLogoTeams = [ - { - title: 'MetronicTeam', - icon: UserCircle, - urlPartial: '/public-profile/', - path: '/public-profile/profiles/default' - }, - { - title: 'KeenTeam', - icon: Settings, - urlPartial: '/account/', - path: '/' - } - ]; - - const items: HeaderLogoItems = [ - { - title: 'Campaign', - icon: UserCircle - }, - { - title: 'Fall Winter 2024 ', - icon: Settings - }, - { - title: 'Barberry Autmn 24', - icon: Users - }, - { - title: 'PF24 Advertising', - icon: Shield - } - ]; - - const stagings: HeaderLogoStagings = [ - { - title: 'Staging', - icon: UserCircle - }, - { - title: 'Account', - icon: Settings - } - ]; - - const [selectedTeam, setSelectedTeam] = useState(teams[0]); - const [selectedItem, setSelectedItem] = useState(items[0]); - const [selectedStaging, setSelectedStaging] = useState(stagings[0]); - - // Close sheet when route changes useEffect(() => { setIsSheetOpen(false); }, [pathname]); return (
- - logo - logo - + + + {isMobile && ( @@ -113,89 +30,12 @@ export function HeaderLogo() { - - {pathname === '/' ? : } + + )} - - {!isMobile && ( -
- {/* Teams Dropdown */} - - - {selectedTeam.title} - - - - {teams.map((team, index) => ( - setSelectedTeam(team)} - > - - {team.icon && } - {team.title} - - - ))} - - - - / - - {/* Items Dropdown */} - - - {selectedItem.title} - - - - {items.map((item, index) => ( - setSelectedItem(item)} - > - - {item.icon && } - {item.title} - - - ))} - - - - / - - {/* Staging Dropdown */} - - - {selectedStaging.title} - - - - {stagings.map((staging, index) => ( - setSelectedStaging(staging)} - > - - {staging.icon && } - {staging.title} - - - ))} - - -
- )}
); } diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderTopbar.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderTopbar.tsx deleted file mode 100755 index 52e22c2..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/HeaderTopbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Users } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -import { Button } from '#shared/ui/Button'; -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; -// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -// import { NotificationsSheet } from '#shared/partials/topbar/notifications-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -export function HeaderTopbar() { - return ( -
- - -
- {/* - - - } - /> */} - {/* - - - } - /> */} - {/* - - - } - /> */} - {/* - - - } - /> */} -
- - - } - /> -
- ); -} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Navbar.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Navbar.tsx index c5173b0..a71da8d 100755 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Navbar.tsx +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Navbar.tsx @@ -1,11 +1,19 @@ +import { useIsMobile } from '@crux-kit/hooks'; import { Container } from '#shared/widgets/common/Container'; -import { NavbarMenu } from './NavbarMenu'; +import { NavMenubar } from '../../shared'; +/** The top-level sections, on the header's second row. */ const Navbar = () => { + const isMobile = useIsMobile(); + + if (isMobile) { + return null; + } + return (
- +
); diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/NavbarMenu.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/NavbarMenu.tsx deleted file mode 100755 index ed2f7da..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/NavbarMenu.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; - -export function NavbarMenu() { - const { pathname } = useLocation(); - - const items = useMemo( - () => [ - { - title: 'Dashboards', - path: '/', - partial: '/' - }, - { - title: 'Public Profiles', - path: '/public-profile/profiles/default', - partial: '/public-profile' - }, - { - title: 'Account Settings', - path: '/account/home/get-started', - partial: '/account' - }, - { - title: 'Network', - path: '/network/get-started', - partial: '/network' - }, - { - title: 'Store - Client', - path: '/store-client/home', - partial: '/store-client' - }, - { - title: 'Authentication', - path: '/authentication/get-started', - partial: '/authentication' - } - ], - [] // Empty dependency array since the data is static - ); - - const [selectedItem, setSelectedItem] = useState(items[0]); - - useEffect(() => { - items.forEach((item) => { - if (item.partial !== '' && pathname.startsWith(item.partial)) { - setSelectedItem(item); - } - }); - }, [items, pathname]); - - return ( -
-
-
- {items.map((item, index) => ( - - {item.title} - - ))} -
-
-
- ); -} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Sidebar.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Sidebar.tsx index 845f6fb..95b88f9 100755 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Sidebar.tsx +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Sidebar.tsx @@ -1,13 +1,30 @@ -import { useLocation } from 'react-router-dom'; -import { SidebarMenuDashboard } from './SidebarMenuDashboard'; -import { SidebarMenuDefault } from './SidebarMenuDefault'; +import { SidebarAccordionMenu, useActiveMenuSection } from '../../shared'; +const CLASS_NAMES = { + root: 'space-y-1 py-5', + group: 'gap-px', + label: 'uppercase text-xs font-medium text-muted-foreground/80 pt-6 mb-2 pb-0', + separator: '', + item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium', + sub: '', + subTrigger: + 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-transparent data-[selected=true]:font-medium', + subContent: 'ps-3 py-0', + indicator: '' +}; + +/** The children of the section selected on the navbar. */ export function Sidebar() { - const { pathname } = useLocation(); + const section = useActiveMenuSection(); + const items = section?.children; + + if (!items || items.length === 0) { + return null; + } return (
- {pathname === '/' ? : } +
); } diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDashboard.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDashboard.tsx deleted file mode 100755 index 58e44f3..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDashboard.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { Badge, ChevronDown, FileText, LucideIcon, Settings, SquareCode, UserCircle } from 'lucide-react'; -import { useCallback, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel } from '#shared/ui/AccordionMenu'; -import { Button } from '#shared/ui/Button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; - -interface DropdownItem { - title: string; - path: string; - icon: LucideIcon; - active?: boolean; -} - -interface MenuItem { - title: string; - path?: string; - active?: boolean; -} - -interface MenuLabel { - label: string; -} - -type MenuNode = MenuItem | MenuLabel; - -export function SidebarMenuDashboard() { - const { pathname } = useLocation(); - - const dropdownItems = useMemo( - () => [ - { - title: 'Client API', - path: '/account/home/user-profile', - icon: SquareCode, - active: true - }, - { - title: 'Profile', - path: '/public-profile/profiles/company', - icon: UserCircle - }, - { - title: 'My Account', - path: '/account/integrations', - icon: Settings - }, - { - title: 'Projects', - path: '/public-profile/projects/3-columns', - icon: FileText - }, - { - title: 'Personal info', - path: '/public-profile/profiles/creator', - icon: Badge - } - ], - [] - ); - - const currentDropdownItem = dropdownItems[0]; - - const menuItems = useMemo( - () => [ - { label: 'Configuration' }, - { title: 'API Setup', path: '/account/api-keys' }, - { title: 'Team Settings', path: '/account/home/settings-sidebar' }, - { title: 'Authentication', path: '/authentication/classic/sign-in' }, - { title: 'Endpoints Configs', path: '/account/appearance' }, - { title: 'Rate Limiting', path: '/public-profile/network' }, - { label: 'Security' }, - { title: 'Data Encryption', path: '/account/billing/enterprise' }, - { title: 'Text', path: '/account/security/overview' }, - { title: 'Access Control', path: '/account/security/privacy-settings' }, - { label: 'Analytics' }, - { - title: 'Incident Response', - path: '/account/security/current-sessions' - }, - { title: 'Fetching Data', path: '/account/members/team-info' }, - { title: 'Custom Reports', path: '/account/home/user-profile' }, - { - title: 'Real Time Analytics', - path: '/account/home/settings-enterprise' - }, - { title: 'Exporting Data', path: '/account/members/import-members' }, - { title: 'Dashboard Integration', path: '/account/members/team-info' } - ], - [] // Empty dependency array since the data is static - ); - - const classNames: AccordionMenuClassNames = { - root: 'space-y-1', - label: 'uppercase text-xs font-medium text-muted-foreground/80 pt-6 mb-2 pb-0', - item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium' - }; - - const matchPath = useCallback( - (path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), - [pathname] - ); - - const buildDropdown = () => { - return ( - - - - - - {dropdownItems.map((item, index) => ( - - - - {item.title} - - - ))} - - - ); - }; - - const memoizedMenu = useMemo( - () => ( - - {menuItems.map((item, index) => - 'label' in item ? ( - {item.label} - ) : ( - - {item.title} - - ) - )} - - ), - [menuItems] - ); - - const buildMenu = () => { - return ( - - {memoizedMenu} - - ); - }; - - return ( -
- {buildDropdown()} - {buildMenu()} -
- ); -} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDefault.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDefault.tsx deleted file mode 100755 index fd9031a..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/SidebarMenuDefault.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { JSX, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig, MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; -import { Badge } from '#shared/ui/Badge'; - -export function SidebarMenuDefault() { - const { pathname } = useLocation(); - const menuConfig = useMenuConfig(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useMemo( - () => createMenuPathMatcher(menuConfig, pathname), - [menuConfig, pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'lg:ps-1 space-y-1', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'border border-transparent h-9 hover:bg-background text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium data-[selected=true]:border-border', - sub: '', - subTrigger: - 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-transparent data-[selected=true]:font-medium', - subContent: 'ps-3 py-0', - indicator: '' - }; - - const buildMenu = (items: MenuConfig): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (item.heading) { - return buildMenuHeading(item, index); - } else if (item.disabled) { - return buildMenuItemRootDisabled(item, index); - } else { - return buildMenuItemRoot(item, index); - } - }); - }; - - const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.title} - - - {buildMenuItemChildren(item.children, 1)} - - - ); - } else { - return ( - - - {item.icon && } - {item.title} - - - ); - } - }; - - const buildMenuItemRootDisabled = (item: MenuItem, index: number): JSX.Element => { - return ( - - {item.icon && } - {item.title} - {item.disabled && ( - - Soon - - )} - - ); - }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (item.disabled) { - return buildMenuItemChildDisabled(item, index, level); - } else { - return buildMenuItemChild(item, index, level); - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - - 0 ? '' : '') - )} - > - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - {item.title} - - ); - } - }; - - const buildMenuItemChildDisabled = ( - item: MenuItem, - index: number, - level: number = 0 - ): JSX.Element => { - return ( - - {item.title} - {item.disabled && ( - - Soon - - )} - - ); - }; - - const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => { - return {item.heading}; - }; - - // Memoize the menu to prevent unnecessary re-renders - const { getChildren } = useMenu(pathname); - const menuItems = getChildren(menuConfig, 0); - - return ( - - {buildMenu(menuItems || [])} - - ); -} diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Toolbar.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/components/Toolbar.tsx deleted file mode 100755 index 9a81bb6..0000000 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/components/Toolbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( -
- - {children} - -
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const menuConfig = useMenuConfig(); - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -const ToolbarHeading = ({ title = '' }: ToolbarHeadingProps) => { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -}; - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/05__DualRowsHeader/layout.tsx b/apps/admin/src/app/layouts/05__DualRowsHeader/layout.tsx index ae7a60d..51b6c32 100755 --- a/apps/admin/src/app/layouts/05__DualRowsHeader/layout.tsx +++ b/apps/admin/src/app/layouts/05__DualRowsHeader/layout.tsx @@ -1,44 +1,24 @@ // DEMO 5 // https://keenthemes.com/metronic/tailwind/react/demo5 -import { addDays, format } from 'date-fns'; -import { CalendarDays } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Outlet, useLocation } from 'react-router'; +import { Outlet } from 'react-router'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Navbar } from './components/Navbar'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export const DualRowsHeaderLayout = () => { const isMobile = useIsMobile(); - const { pathname } = useLocation(); - const { setOption } = useSettings(); + useLayoutSettings(LayoutNames.DUAL_ROW_HEADER); + const { showToolbar, showFooter } = useLayoutChrome(); - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); - - // Using the custom hook to set multiple CSS variables and class properties useBodyClass(` [--header-height:54px] - [--sidebar-width:200px] + [--sidebar-width:200px] `); - useEffect(() => { - setOption('layout', LayoutNames.DUAL_ROW_HEADER); - setOption('container', 'fluid'); - }, [setOption]); - return ( <>
@@ -50,44 +30,15 @@ export const DualRowsHeaderLayout = () => { {!isMobile && }
- {!pathname.includes('/public-profile/') && ( + {showToolbar && ( - - - - - - - - - - )} -
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/README.md b/apps/admin/src/app/layouts/06__MultipleMenus/README.md new file mode 100644 index 0000000..eaa2ad0 --- /dev/null +++ b/apps/admin/src/app/layouts/06__MultipleMenus/README.md @@ -0,0 +1,24 @@ +# Multiple Menus layout + +**Metronic 9 reference:** [Demo 6](https://keenthemes.com/metronic/tailwind/react/demo6) + +A fixed light sidebar holding the whole application menu as an accordion, with the page content in a +rounded card beside it. Below `lg` the sidebar collapses into a sheet opened from a compact header. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Sidebar menu | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Sidebar`, `SidebarHeader` (logo plus search box), `SidebarFooter`, `Header` +(mobile sheet). + +## Notes + +- `RoosterPeek` is a development-only easter egg kept with this layout; it renders nothing in + production builds. +- Body variables: `--header-height: 60px`, `--sidebar-width: 270px`. diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/Header.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/components/Header.tsx index d049bfa..a936e56 100755 --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/Header.tsx +++ b/apps/admin/src/app/layouts/06__MultipleMenus/components/Header.tsx @@ -1,11 +1,10 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { useIsMobile } from '@crux-kit/hooks'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { useLocation } from 'react-router-dom'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; import { Container } from '#shared/widgets/common/Container'; +import { AppLogo } from '../../shared'; import { SidebarFooter } from './SidebarFooter'; import { SidebarHeader } from './SidebarHeader'; import { SidebarMenu } from './SidebarMenu'; @@ -13,7 +12,6 @@ import { SidebarMenu } from './SidebarMenu'; const Header = () => { const { pathname } = useLocation(); const [isSheetOpen, setIsSheetOpen] = useState(false); - const isMobile = useIsMobile(); useEffect(() => { setIsSheetOpen(false); @@ -22,36 +20,23 @@ const Header = () => { return (
- - Logo - Logo - + - {isMobile && ( - - - - - - - - - - - - - - )} + + + + + + + + + + + + +
); diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarFooter.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarFooter.tsx index 899cb07..b42b503 100755 --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarFooter.tsx +++ b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarFooter.tsx @@ -1,24 +1,9 @@ -import useIntlName from '#app/i18n/useIntlName'; -import { useAuth } from '#core/Auth/providers/AuthProvider'; -import { Avatar, AvatarFallback, AvatarImage } from '#shared/ui/Avatar'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; +import { UserAvatarMenu } from '../../shared'; export function SidebarFooter() { - const { user } = useAuth(); - const formatName = useIntlName(); - return (
- {user && ( - - - {formatName(user, 'INITIALS_LF')} - - } - /> - )} +
); } diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarHeader.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarHeader.tsx index d83a759..830865a 100755 --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarHeader.tsx +++ b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarHeader.tsx @@ -1,7 +1,6 @@ import { Search } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Input } from '#shared/ui/Input'; +import { AppLogo } from '../../shared'; export function SidebarHeader() { const handleInputChange = () => {}; @@ -9,18 +8,7 @@ export function SidebarHeader() { return (
- - - - +
C R U X Goat
diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenu.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenu.tsx index fff5c3d..ca95120 100755 --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenu.tsx +++ b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenu.tsx @@ -1,9 +1,9 @@ -import { SidebarMenuPrimary } from './SidebarMenuPrimary'; +import { SidebarAccordionMenu } from '../../shared'; export function SidebarMenu() { return (
- +
); } diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenuPrimary.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenuPrimary.tsx deleted file mode 100755 index e2a1499..0000000 --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/SidebarMenuPrimary.tsx +++ /dev/null @@ -1,142 +0,0 @@ -'use client'; - -import { Fragment, JSX, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig, MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useAuth } from '#core/Auth/providers/AuthProvider'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; -import { getPermittedMenuItems } from '../lib'; - -export function SidebarMenuPrimary() { - const { pathname } = useLocation(); - const { user } = useAuth(); - const menuConfig = useMenuConfig(); - - const userPermissions = user?.permissions ?? []; - - const matchPath = useMemo( - () => createMenuPathMatcher(menuConfig, pathname), - [menuConfig, pathname] - ); - - const classNames: AccordionMenuClassNames = { - root: 'space-y-2.5 px-3.5', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'h-9 hover:bg-transparent border border-transparent text-accent-foreground hover:text-mono data-[selected=true]:text-mono data-[selected=true]:bg-background data-[selected=true]:border-border data-[selected=true]:font-medium', - sub: '', - subTrigger: - 'h-9 hover:bg-transparent border border-transparent text-accent-foreground hover:text-mono data-[selected=true]:text-mono data-[selected=true]:bg-background data-[selected=true]:border-border data-[selected=true]:font-medium', - subContent: 'py-0', - indicator: '' - }; - - const buildMenu = (items: MenuConfig): JSX.Element[] => { - const menuItems = getPermittedMenuItems(items, userPermissions); - - return menuItems.map((item: MenuItem, index: number) => { - if (!item.heading && !item.disabled) { - return buildMenuItemRoot(item, index); - } else { - return <>; - } - }); - }; - - const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.title} - - - {buildMenuItemChildren(item.children, 1)} - - - ); - } else { - return ( - - - {item.icon && } - {item.title} - - - ); - } - }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.heading && !item.disabled) { - return buildMenuItemChild(item, index, level); - } else { - return ; - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - - 0 ? '' : '') - )} - > - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - {item.title} - - ); - } - }; - - return ( - - {buildMenu(menuConfig)} - - ); -} diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/layout.tsx b/apps/admin/src/app/layouts/06__MultipleMenus/layout.tsx index 79c2d7f..b998ad6 100755 --- a/apps/admin/src/app/layouts/06__MultipleMenus/layout.tsx +++ b/apps/admin/src/app/layouts/06__MultipleMenus/layout.tsx @@ -1,20 +1,18 @@ // DEMO 6 // https://keenthemes.com/metronic/tailwind/react/demo6 -import { Fragment, useEffect, useMemo } from 'react'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Fragment } from 'react'; +import { Outlet } from 'react-router-dom'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarHeading } from './components/Toolbar'; export const MultipleMenusLayout = () => { const isMobile = useIsMobile(); - const { setOption } = useSettings(); - const { pathname } = useLocation(); + useLayoutSettings(LayoutNames.MULTIPLE_MENUS); + const { showToolbar, showFooter } = useLayoutChrome(); useBodyClass(` [--header-height:60px] @@ -23,28 +21,6 @@ export const MultipleMenusLayout = () => { bg-muted! `); - useEffect(() => { - setOption('layout', LayoutNames.MULTIPLE_MENUS); - }, [setOption]); - - const toolbar = useMemo(() => { - if (pathname.includes('/devtools')) return null; - if (pathname.includes('/access-control')) return null; - if (pathname.includes('/file-manager')) return null; - if (pathname.includes('/web-studio')) return null; - - return ( - - - - ); - }, [pathname]); - - const footer = useMemo(() => { - if (pathname.includes('/file-manager')) return null; - return
; - }, [pathname]); - return (
@@ -56,11 +32,15 @@ export const MultipleMenusLayout = () => {
- {toolbar} + {showToolbar && ( + + + + )}
- {footer} + {showFooter && }
diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/lib.ts b/apps/admin/src/app/layouts/06__MultipleMenus/lib.ts deleted file mode 100755 index fb1a7e4..0000000 --- a/apps/admin/src/app/layouts/06__MultipleMenus/lib.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MenuItem } from '#app/config/types'; - -export function getPermittedMenuItems(items: MenuItem[], userPermission: string[]): MenuItem[] { - return items - .map((item) => { - const filteredChildren = item.children - ? getPermittedMenuItems(item.children, userPermission) - : undefined; - - const hasPermission = item.permissions - ? item.permissions.every((permission) => userPermission.includes(permission)) - : true; - - if (hasPermission || (filteredChildren && filteredChildren.length > 0)) { - return { - ...item, - children: filteredChildren - }; - } - - return null; - }) - .filter((item) => item != null) - .filter((item) => (item.children ? item.children.length > 0 : true)); -} diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/README.md b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/README.md new file mode 100644 index 0000000..9be076e --- /dev/null +++ b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/README.md @@ -0,0 +1,25 @@ +# Advanced Mega Menu layout + +**Metronic 9 reference:** [Demo 7](https://keenthemes.com/metronic/tailwind/react/demo7) + +A header-only layout: the whole application menu lives in a mega menu beside the logo, and the page +content runs full width beneath it. The header becomes sticky and shrinks past +`layouts.advanced-mega-menu-layout.headerStickyOffset`. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Mega menu | `MegaNavMenu` from `../shared` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Header`, `HeaderLogo`. + +## Notes + +- The mega menu panels show two levels — a section's children as columns, their children as links. + Deeper entries stay reachable through the toolbar breadcrumbs and the mobile accordion. +- The `data-sticky-header` body attribute is removed on unmount. diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Footer.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Header.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Header.tsx index 31fa3d0..bbaa49f 100755 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Header.tsx +++ b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Header.tsx @@ -4,39 +4,42 @@ import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; import { Container } from '#shared/widgets/common/Container'; +import { UserAvatarMenu } from '../../shared'; import { HeaderLogo } from './HeaderLogo'; -import { HeaderTopbar } from './HeaderTopbar'; const Header = () => { const { settings } = useSettings(); const scrollPosition = useScrollPosition(); const [headerStickyOn, setHeaderStickyOn] = useState(false); - useEffect(() => { - const isSticky = - scrollPosition > settings.layouts[LayoutNames.HORIZONTAL_MENU].headerStickyOffset; - setHeaderStickyOn(isSticky); - }, [scrollPosition, settings]); + const { headerSticky, headerStickyOffset } = settings.layouts[LayoutNames.ADVANCE_MEGA_MENU]; useEffect(() => { - if (headerStickyOn === true) { + setHeaderStickyOn(scrollPosition > headerStickyOffset); + }, [scrollPosition, headerStickyOffset]); + + useEffect(() => { + if (headerStickyOn) { document.body.setAttribute('data-sticky-header', 'on'); } else { document.body.removeAttribute('data-sticky-header'); } + + return () => document.body.removeAttribute('data-sticky-header'); }, [headerStickyOn]); return (
- +
); diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderLogo.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderLogo.tsx index df6f8b7..d317211 100755 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderLogo.tsx +++ b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderLogo.tsx @@ -1,12 +1,10 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useIsMobile } from '@crux-kit/hooks'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; -import { MegaMenu } from './MegaMenu'; -import { MegaMenuMobile } from './MegaMenuMobile'; +import { AppLogo, AppWordmark, MegaNavMenu, SidebarAccordionMenu } from '../../shared'; const HeaderLogo = () => { const { pathname } = useLocation(); @@ -20,24 +18,12 @@ const HeaderLogo = () => { return (
- - logo - logo - - -

Metronic

+ +
{!isMobile ? ( - + ) : ( @@ -47,8 +33,8 @@ const HeaderLogo = () => { - - + + diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderTopbar.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderTopbar.tsx deleted file mode 100755 index d178abf..0000000 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/HeaderTopbar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ArrowUp, Crown } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { Button } from '#shared/ui/Button'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -const HeaderTopbar = () => { - return ( -
-
-
- - - free - - - 182/200 Uploads - -
- - -
- -
- - - S - - - -
- } - /> -
- ); -}; - -export { HeaderTopbar }; diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenu.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenu.tsx deleted file mode 100755 index 86426db..0000000 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenu.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig } from '#app/config/types'; -import { useMenu } from '#shared/hooks/useMenu'; -import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger } from '#shared/ui/NavigationMenu'; -import { MegaMenuSubAccount } from '#shared/widgets/MegaMenu/MegaMenuSubAccount'; -import { MegaMenuSubAuth } from '#shared/widgets/MegaMenu/MegaMenuSubAuth'; -import { MegaMenuSubNetwork } from '#shared/widgets/MegaMenu/MegaMenuSubNetwork'; -import { MegaMenuSubProfiles } from '#shared/widgets/MegaMenu/MegaMenuSubProfiles'; -import { MegaMenuSubStore } from '#shared/widgets/MegaMenu/MegaMenuSubStore'; - -const MENU_MEGA: MenuConfig = []; - -export function MegaMenu() { - const { pathname } = useLocation(); - const { isActive, hasActiveChild } = useMenu(pathname); - const homeItem = MENU_MEGA[0]; - const publicProfilesItem = MENU_MEGA[1]; - const myAccountItem = MENU_MEGA[2]; - const networkItem = MENU_MEGA[3]; - const storeItem = MENU_MEGA[5]; - const authItem = MENU_MEGA[4]; - const linkClass = ` - text-sm text-secondary-foreground font-medium rounded-none px-0 border-b border-transparent - hover:text-primary hover:bg-transparent - focus:text-primary focus:bg-transparent - data-[active=true]:text-mono data-[active=true]:bg-transparent data-[active=true]:border-mono - data-[state=open]:text-mono data-[state=open]:bg-transparent - `; - - return ( - - - {/* Home Item */} - - - - {homeItem.title} - - - - - {/* Public Profiles Item */} - - - {publicProfilesItem.title} - - - - - - - {/* My Account Item */} - - - {myAccountItem.title} - - - - - - - {/* Network Item */} - - - {networkItem.title} - - - - - - - {/* Store Item */} - - - {storeItem.title} - - - - - - - {/* Authentication Item */} - - - {authItem.title} - - - - - - - - ); -} diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenuMobile.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenuMobile.tsx deleted file mode 100755 index e834086..0000000 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/MegaMenuMobile.tsx +++ /dev/null @@ -1,188 +0,0 @@ -'use client'; - -import { LucideIcon } from 'lucide-react'; -import { JSX, useCallback } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; -import { Badge } from '#shared/ui/Badge'; - -export interface MenuItem { - title?: string; - icon?: LucideIcon; - path?: string; - rootPath?: string; - childrenIndex?: number; - heading?: string; - children?: MenuItem[]; - disabled?: boolean; - collapse?: boolean; - collapseTitle?: string; - expandTitle?: string; - badge?: string; - separator?: boolean; -} - -export type MenuConfig = MenuItem[]; -const MENU_MEGA_MOBILE: MenuConfig = []; - -export function MegaMenuMobile() { - const { pathname } = useLocation(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useCallback( - (path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), - [pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'space-y-1', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - sub: '', - subTrigger: - 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - subContent: 'py-0', - indicator: '' - }; - - const buildMenu = (items: MenuConfig): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (item.heading) { - return buildMenuHeading(item, index); - } else if (!item.disabled) { - return buildMenuItemRoot(item, index); - } else { - return <>; - } - }); - }; - - const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } -
- {item.title} - {item.badge && ( - - {item.badge} - - )} -
-
- - {buildMenuItemChildren(item.children, 1)} - -
- ); - } else { - return ( - - - {item.icon && } -
- {item.title} - {item.badge && ( - - {item.badge} - - )} -
- -
- ); - } - }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.disabled) { - return buildMenuItemChild(item, index, level); - } else { - return <>; - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - {item.badge && ( - - {item.badge} - - )} - - - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - - {item.icon && } -
- {item.title} - {item.badge && ( - - {item.badge} - - )} -
- -
- ); - } - }; - - const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => { - return {item.heading}; - }; - - return ( -
- - {buildMenu(MENU_MEGA_MOBILE)} - -
- ); -} diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Toolbar.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Toolbar.tsx deleted file mode 100755 index f9853d7..0000000 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/components/Toolbar.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( - -
-
- {children} -
-
-
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const menuConfig = useMenuConfig(); - - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -function ToolbarHeading({ title = '' }: ToolbarHeadingProps) { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -} - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/layout.tsx b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/layout.tsx index d6c4f6b..f3638ed 100755 --- a/apps/admin/src/app/layouts/07__AdvancedMegaMenu/layout.tsx +++ b/apps/admin/src/app/layouts/07__AdvancedMegaMenu/layout.tsx @@ -1,94 +1,38 @@ // DEMO 7 // https://keenthemes.com/metronic/tailwind/react/demo7 -import { addDays, format } from 'date-fns'; -import { CalendarDays, Download } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Link, Outlet, useLocation } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export const AdvancedMegaMenuLayout = () => { - const { setOption } = useSettings(); - const { pathname } = useLocation(); + useLayoutSettings(LayoutNames.ADVANCE_MEGA_MENU); + const { showToolbar, showFooter } = useLayoutChrome(); - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); - - // Using the custom hook to set multiple CSS variables and class properties useBodyClass(` [--header-height-default:95px] data-[sticky-header=on]:[--header-height:60px] - [--header-height:var(--header-height-default)] - [--header-height-mobile:70px] + [--header-height:var(--header-height-default)] + [--header-height-mobile:70px] `); - useEffect(() => { - setOption('layout', LayoutNames.ADVANCE_MEGA_MENU); - }, [setOption]); - return ( <>
-
- {!pathname.includes('/public-profile/') && ( +
+ {showToolbar && ( - - - - - - - - - - - - - )} -
+ -
+ {showFooter && }
); diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/README.md b/apps/admin/src/app/layouts/08__DropdownMenu/README.md new file mode 100644 index 0000000..551d0bf --- /dev/null +++ b/apps/admin/src/app/layouts/08__DropdownMenu/README.md @@ -0,0 +1,24 @@ +# Dropdown Menu layout + +**Metronic 9 reference:** [Demo 8](https://keenthemes.com/metronic/tailwind/react/demo8) + +A 90px sidebar of labelled icon tiles, one per top-level section. A section with children opens its +whole subtree in a nested dropdown rather than expanding in place. + +## Composition + +| Piece | Source | +| --------------------------- | --------------------------------------------- | +| Sidebar tiles and dropdowns | `useAppMenu` from `../shared` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Sidebar`, `SidebarHeader`, `SidebarMenu`, `SidebarFooter`, `Header`. + +## Notes + +- Below `lg` the tiles give way to the shared accordion in a sheet, which reads better on a narrow + viewport than nested dropdowns. +- Body variables: `--header-height: 60px`, `--sidebar-width: 90px`. diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/Footer.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/Header.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/Header.tsx index 70b59de..eb5e5ae 100755 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/Header.tsx +++ b/apps/admin/src/app/layouts/08__DropdownMenu/components/Header.tsx @@ -1,18 +1,16 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { useLocation } from 'react-router-dom'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; import { Container } from '#shared/widgets/common/Container'; +import { AppLogo, SidebarAccordionMenu } from '../../shared'; import { SidebarFooter } from './SidebarFooter'; -import { SidebarMenu } from './SidebarMenu'; export function Header() { const { pathname } = useLocation(); const [isSheetOpen, setIsSheetOpen] = useState(false); - // Close sheet when route changes useEffect(() => { setIsSheetOpen(false); }, [pathname]); @@ -20,18 +18,7 @@ export function Header() { return (
- - image - image - + @@ -39,10 +26,10 @@ export function Header() { - + - + diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarFooter.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarFooter.tsx index 7e8f9ba..f194fda 100755 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarFooter.tsx +++ b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarFooter.tsx @@ -1,47 +1,9 @@ -// import { LayoutGrid, MessageCircleMore } from 'lucide-react'; -// import { Button } from '#shared/ui/button'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; +import { UserAvatarMenu } from '../../shared'; export function SidebarFooter() { return ( -
-
- {/* - - - } - /> */} - {/* - - - } - /> */} -
- - - } - /> +
+
); } diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarHeader.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarHeader.tsx index befdb22..2c6c12f 100755 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarHeader.tsx +++ b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarHeader.tsx @@ -1,21 +1,9 @@ -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { AppLogo } from '../../shared'; export function SidebarHeader() { return (
- - - - +
); } diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarMenu.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarMenu.tsx index 51b0106..4239e9a 100755 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarMenu.tsx +++ b/apps/admin/src/app/layouts/08__DropdownMenu/components/SidebarMenu.tsx @@ -1,97 +1,30 @@ -import { BarChart2, Settings, ShoppingCart, User, Users } from 'lucide-react'; import { Link, useLocation } from 'react-router-dom'; import { cn } from '@crux-kit/utils'; -import { MenuConfig } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; +import { MenuConfig, MenuItem } from '#app/config/types'; import { useMenu } from '#shared/hooks/useMenu'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; import { Button } from '#shared/ui/Button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; +import { useAppMenu } from '../../shared'; + +const TILE_CLASS_NAMES = cn( + 'flex h-15 w-15.5 flex-col items-center justify-center gap-1 rounded-lg p-2', + 'text-muted-foreground bg-transparent text-xs font-medium shadow-none', + 'hover:text-primary hover:bg-background hover:border-border', + 'data-[active=true]:text-primary data-[active=true]:bg-background data-[active=true]:border-border', + 'data-[here=true]:text-primary data-[here=true]:bg-background data-[here=true]:border-border' +); export function SidebarMenu() { const { pathname } = useLocation(); - const { hasActiveChild } = useMenu(pathname); - const config = useMenuConfig(); + const { hasActiveChild, isSelected } = useMenu(pathname); + const menuConfig = useAppMenu(); - const menuConfig: MenuConfig = [ - { - title: 'Boards', - icon: BarChart2, - path: '/' - }, - { - title: 'Profiles', - icon: User, - children: config?.[2].children - }, - { - title: 'Account', - icon: Settings, - children: config?.[3].children - }, - { - title: 'Store', - icon: ShoppingCart, - children: config?.[7].children - }, - { - title: 'Network', - icon: Users, - children: config?.[4].children - } - ]; + const buildChildren = (items: MenuConfig) => + items.map((item: MenuItem, index: number) => { + if (item.separator) { + return ; + } - // A leaf highlights only when nothing deeper in the menu matches the route more closely. - const isActive = createMenuPathMatcher(menuConfig, pathname); - - const buildMenu = (items: MenuConfig) => { - return items.map((item, index) => ( -
- {item.children ? ( - - - - - - {item.title} - {buildMenuChildren(item.children)} - - - ) : ( - - {item.icon && } - {item.title} - - )} -
- )); - }; - - const buildMenuChildren = (items: MenuConfig) => { - return items.map((item, index) => { - if (item.disabled) return null; - if (item.separator) return ; if (item.children) { return ( @@ -100,22 +33,50 @@ export function SidebarMenu() { {item.title} - {buildMenuChildren(item.children)} + {buildChildren(item.children)} ); } + return ( {item.title} ); }); - }; return ( -
- {buildMenu(menuConfig)} +
+ {menuConfig.map((item, index) => + item.children ? ( + + + + + + {item.title} + {buildChildren(item.children)} + + + ) : ( + + {item.icon && } + {item.title} + + ) + )}
); } diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/components/Toolbar.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/components/Toolbar.tsx deleted file mode 100755 index 5499c01..0000000 --- a/apps/admin/src/app/layouts/08__DropdownMenu/components/Toolbar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( -
- - {children} - -
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -function ToolbarHeading({ title = '' }: ToolbarHeadingProps) { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -} - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/08__DropdownMenu/layout.tsx b/apps/admin/src/app/layouts/08__DropdownMenu/layout.tsx index 6e181ec..e06c867 100755 --- a/apps/admin/src/app/layouts/08__DropdownMenu/layout.tsx +++ b/apps/admin/src/app/layouts/08__DropdownMenu/layout.tsx @@ -1,35 +1,24 @@ // DEMO 8 // https://keenthemes.com/metronic/tailwind/react/demo8 -import { Download } from 'lucide-react'; -import { useEffect } from 'react'; -import { Link, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export function DropdownMenuLayout() { const isMobile = useIsMobile(); - const { setOption } = useSettings(); + useLayoutSettings(LayoutNames.DROPDOWN_MENU); + const { showToolbar, showFooter } = useLayoutChrome(); - // Using the custom hook to set classes on the body useBodyClass(` [--header-height:60px] [--sidebar-width:90px] bg-muted! `); - useEffect(() => { - setOption('layout', LayoutNames.DROPDOWN_MENU); - }, [setOption]); - return ( <>
@@ -41,42 +30,17 @@ export function DropdownMenuLayout() {
- - - - - {/* - - - } - /> - - - - } - /> */} - - - + {showToolbar && ( + + + + )}
-
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/README.md b/apps/admin/src/app/layouts/09__ExtendedHeader/README.md new file mode 100644 index 0000000..0e1c95d --- /dev/null +++ b/apps/admin/src/app/layouts/09__ExtendedHeader/README.md @@ -0,0 +1,24 @@ +# Extended Header layout + +**Metronic 9 reference:** [Demo 9](https://keenthemes.com/metronic/tailwind/react/demo9) + +A tall header holding the logo, a search box and the account menu, with the application menu on a +mega-menu strip directly beneath it. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Mega menu strip | `MegaNavMenu` from `../shared` | +| Mobile menu (sheet) | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Header`, `HeaderLogo`, `HeaderSearch`, `Navbar`. + +## Notes + +- `HeaderSearch` is presentational; it has no query handling wired up yet. +- The mega-menu strip is hidden below `lg`, where the sheet accordion takes over. +- The `data-sticky-header` body attribute is removed on unmount. diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Footer.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Header.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Header.tsx index 4f2717f..931a8c6 100755 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Header.tsx +++ b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Header.tsx @@ -4,41 +4,44 @@ import { cn } from '@crux-kit/utils'; import { LayoutNames } from '#app/config/types'; import { useSettings } from '#app/providers/SettingsProvider'; import { Container } from '#shared/widgets/common/Container'; +import { UserAvatarMenu } from '../../shared'; import { HeaderLogo } from './HeaderLogo'; import { HeaderSearch } from './HeaderSearch'; -import { HeaderTopbar } from './HeaderTopbar'; export function Header() { const { settings } = useSettings(); const scrollPosition = useScrollPosition(); const [headerStickyOn, setHeaderStickyOn] = useState(false); - useEffect(() => { - const isSticky = - scrollPosition > settings.layouts[LayoutNames.HORIZONTAL_MENU].headerStickyOffset; - setHeaderStickyOn(isSticky); - }, [scrollPosition, settings]); + const { headerSticky, headerStickyOffset } = settings.layouts[LayoutNames.EXTENDED_HEADER]; useEffect(() => { - if (headerStickyOn === true) { + setHeaderStickyOn(scrollPosition > headerStickyOffset); + }, [scrollPosition, headerStickyOffset]); + + useEffect(() => { + if (headerStickyOn) { document.body.setAttribute('data-sticky-header', 'on'); } else { document.body.removeAttribute('data-sticky-header'); } + + return () => document.body.removeAttribute('data-sticky-header'); }, [headerStickyOn]); return (
- +
); diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderLogo.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderLogo.tsx index b116cc1..98a32fe 100755 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderLogo.tsx +++ b/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderLogo.tsx @@ -1,18 +1,16 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useIsMobile } from '@crux-kit/hooks'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; -import { MegaMenuMobile } from './MegaMenuMobile'; +import { AppLogo, AppWordmark, SidebarAccordionMenu } from '../../shared'; export function HeaderLogo() { const isMobile = useIsMobile(); const { pathname } = useLocation(); const [isSheetOpen, setIsSheetOpen] = useState(false); - // Close sheet when route changes useEffect(() => { setIsSheetOpen(false); }, [pathname]); @@ -20,19 +18,8 @@ export function HeaderLogo() { return (
- - logo - logo - -

Metronic

+ +
{isMobile && ( @@ -44,8 +31,8 @@ export function HeaderLogo() { - - + + diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderTopbar.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderTopbar.tsx deleted file mode 100755 index fd83887..0000000 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/HeaderTopbar.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// import { ChevronDown, MessageCircleMore, MessageSquareDot } from 'lucide-react'; -// import { Button } from '#shared/ui/button'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -import { Label } from '#shared/ui/Label'; -import { Switch } from '#shared/ui/Switch'; -// import { DropdownMenu2 } from '#shared/partials/dropdown-menu/dropdown-menu-2'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -// import { NotificationsSheet } from '#shared/partials/topbar/notifications-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; - -export function HeaderTopbar() { - return ( -
-
- {/* - - - } - /> - - - - - } - /> */} - - - } - /> -
- -
- -
- - -
- -
- - {/* - Create - - - } - /> */} -
- ); -} diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenu.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenu.tsx deleted file mode 100755 index be5c210..0000000 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenu.tsx +++ /dev/null @@ -1,122 +0,0 @@ -// import { Link, useLocation } from 'react-router-dom'; -// import { MENU_MEGA } from '#app/menu'; -import { - NavigationMenu, - // NavigationMenuContent, - // NavigationMenuItem, - // NavigationMenuLink, - NavigationMenuList - // NavigationMenuTrigger, -} from '#shared/ui/NavigationMenu'; - -// import { useMenu } from '#shared/hooks/use_menu'; -// import { cn } from '@crux-kit/utils'; -// import { MegaMenuSubAccount } from '#shared/widgets/mega_menu/mega_menu_sub_account'; -// import { MegaMenuSubAuth } from '#shared/widgets/mega_menu/mega_menu_sub_auth'; -// import { MegaMenuSubNetwork } from '#shared/widgets/mega_menu/mega_menu_sub_network'; -// import { MegaMenuSubProfiles } from '#shared/widgets/mega_menu/mega_menu_sub_profiles'; -// import { MegaMenuSubStore } from '#shared/widgets/mega_menu/mega_menu_sub_store'; - -export function MegaMenu() { - // const { pathname } = useLocation(); - // const { isActive, hasActiveChild } = useMenu(pathname); - // const homeItem = MENU_MEGA[0]; - // const publicProfilesItem = MENU_MEGA[1]; - // const myAccountItem = MENU_MEGA[2]; - // const networkItem = MENU_MEGA[3]; - // const storeItem = MENU_MEGA[5]; - // const authItem = MENU_MEGA[4]; - // const linkClass = ` - // inline-flex flex-row items-center h-12 py-0 border-b border-transparent rounded-none bg-transparent -mb-[1px] - // text-sm text-secondary-foreground font-medium - // hover:text-mono hover:bg-transparent - // focus:text-mono focus:bg-transparent - // data-[active=true]:text-mono data-[active=true]:bg-transparent data-[active=true]:border-mono - // data-[here=true]:text-mono data-[here=true]:bg-transparent data-[here=true]:border-mono - // data-[state=open]:text-mono data-[state=open]:bg-transparent - // `; - - return ( - - - {/* Home Item */} - {/* - - - {homeItem.title} - - - */} - - {/* Public Profiles Item */} - {/* - - {publicProfilesItem.title} - - - - - */} - - {/* My Account Item */} - {/* - - {myAccountItem.title} - - - - - */} - - {/* Network Item */} - {/* - - {networkItem.title} - - - - - */} - - {/* Store Item */} - {/* - - {storeItem.title} - - - - - */} - - {/* Authentication Item */} - {/* - - {authItem.title} - - - - - */} - - - ); -} diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenuMobile.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenuMobile.tsx deleted file mode 100755 index eca44f3..0000000 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/MegaMenuMobile.tsx +++ /dev/null @@ -1,197 +0,0 @@ -'use client'; - -import { LucideIcon } from 'lucide-react'; -import { JSX, useCallback } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -// import { MENU_MEGA_MOBILE } from '#app/menu'; -import { - AccordionMenu, - AccordionMenuClassNames, - AccordionMenuGroup, - AccordionMenuItem, - // AccordionMenuLabel, - AccordionMenuSub, - AccordionMenuSubContent, - AccordionMenuSubTrigger -} from '#shared/ui/AccordionMenu'; -import { Badge } from '#shared/ui/Badge'; - -export interface MenuItem { - title?: string; - icon?: LucideIcon; - path?: string; - rootPath?: string; - childrenIndex?: number; - heading?: string; - children?: MenuItem[]; - disabled?: boolean; - collapse?: boolean; - collapseTitle?: string; - expandTitle?: string; - badge?: string; - separator?: boolean; -} - -export type MenuConfig = MenuItem[]; - -export function MegaMenuMobile() { - const { pathname } = useLocation(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useCallback( - (path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)), - [pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'space-y-1', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px', - separator: '', - item: 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - sub: '', - subTrigger: - 'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-muted data-[selected=true]:font-medium', - subContent: 'py-0', - indicator: '' - }; - - // const buildMenu = (items: MenuConfig): JSX.Element[] => { - // return items.map((item: MenuItem, index: number) => { - // if (item.heading) { - // return buildMenuHeading(item, index); - // } else if (!item.disabled) { - // return buildMenuItemRoot(item, index); - // } else { - // return <>; - // } - // }); - // }; - - // const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - // if (item.children) { - // return ( - // - // - // {item.icon && } - //
- // {item.title} - // {item.badge && ( - // - // {item.badge} - // - // )} - //
- //
- // - // {buildMenuItemChildren(item.children, 1)} - // - //
- // ); - // } else { - // return ( - // - // - // {item.icon && } - //
- // {item.title} - // {item.badge && ( - // - // {item.badge} - // - // )} - //
- // - //
- // ); - // } - // }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.disabled) { - return buildMenuItemChild(item, index, level); - } else { - return <>; - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - {item.badge && ( - - {item.badge} - - )} - - - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - - {item.icon && } -
- {item.title} - {item.badge && ( - - {item.badge} - - )} -
- -
- ); - } - }; - - // const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => { - // return {item.heading}; - // }; - - return ( -
- - {/* {buildMenu(MENU_MEGA_MOBILE)} */} - -
- ); -} diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Navbar.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Navbar.tsx index 021d008..092cda8 100755 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Navbar.tsx +++ b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Navbar.tsx @@ -1,11 +1,11 @@ import { Container } from '#shared/widgets/common/Container'; -import { MegaMenu } from './MegaMenu'; +import { MegaNavMenu } from '../../shared'; export function Navbar() { return (
- +
); diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Toolbar.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/components/Toolbar.tsx deleted file mode 100755 index 4d58c47..0000000 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/components/Toolbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( - -
- {children} -
-
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const menuConfig = useMenuConfig(); - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -function ToolbarHeading({ title = '' }: ToolbarHeadingProps) { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -} - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/09__ExtendedHeader/layout.tsx b/apps/admin/src/app/layouts/09__ExtendedHeader/layout.tsx index 8e8e96a..507a97a 100755 --- a/apps/admin/src/app/layouts/09__ExtendedHeader/layout.tsx +++ b/apps/admin/src/app/layouts/09__ExtendedHeader/layout.tsx @@ -1,41 +1,23 @@ // DEMO 9 // https://keenthemes.com/metronic/tailwind/react/demo9 -import { addDays, format } from 'date-fns'; -import { CalendarDays, Download } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Link, Outlet, useLocation } from 'react-router'; +import { Outlet } from 'react-router'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Navbar } from './components/Navbar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export function ExtendedHeaderLayout() { - const { setOption } = useSettings(); - const { pathname } = useLocation(); + useLayoutSettings(LayoutNames.EXTENDED_HEADER); const isMobile = useIsMobile(); - - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); + const { showToolbar, showFooter } = useLayoutChrome(); useBodyClass(` [--header-height:78px] bg-background! `); - useEffect(() => { - setOption('layout', LayoutNames.EXTENDED_HEADER); - }, [setOption]); - return ( <>
@@ -44,52 +26,15 @@ export function ExtendedHeaderLayout() { {!isMobile && }
- {!pathname.includes('/public-profile/') && ( - + {showToolbar && ( + - - - - - - - - - - - - - )} -
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/README.md b/apps/admin/src/app/layouts/10__DarkSidebar/README.md new file mode 100644 index 0000000..32290f5 --- /dev/null +++ b/apps/admin/src/app/layouts/10__DarkSidebar/README.md @@ -0,0 +1,22 @@ +# Dark Sidebar layout + +**Metronic 9 reference:** [Demo 10](https://keenthemes.com/metronic/tailwind/react/demo10) + +The Multiple Menus arrangement rendered dark: a fixed sidebar forced into the `dark` theme class +regardless of the visitor's colour scheme, against a content panel that follows the theme. + +## Composition + +| Piece | Source | +| ----------------------- | --------------------------------------------- | +| Sidebar menu | `SidebarAccordionMenu` from `../shared` | +| Toolbar and breadcrumbs | `Toolbar` / `ToolbarHeading` from `../shared` | +| Footer | `AppFooter` from `../shared` | +| Account menu | `UserAvatarMenu` from `../shared` | + +Layout-specific: `Sidebar`, `SidebarHeader`, `SidebarFooter`, `Header` (mobile sheet, also `dark`). + +## Notes + +- The `dark` class is applied to the sidebar and to the mobile sheet, so the two stay consistent. +- Body variables: `--header-height: 60px`, `--sidebar-width: 270px`. diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/Footer.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/Footer.tsx deleted file mode 100755 index 39d24e5..0000000 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/Footer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { generalSettings } from '#app/config/general'; -import { Container } from '#shared/widgets/common/Container'; - -export function Footer() { - const currentYear = new Date().getFullYear(); - - return ( - - ); -} diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/Header.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/Header.tsx index 0292d7e..1c8e475 100755 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/Header.tsx +++ b/apps/admin/src/app/layouts/10__DarkSidebar/components/Header.tsx @@ -1,21 +1,18 @@ import { Menu } from 'lucide-react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { useIsMobile } from '@crux-kit/hooks'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; +import { useLocation } from 'react-router-dom'; import { Button } from '#shared/ui/Button'; import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet'; import { Container } from '#shared/widgets/common/Container'; +import { AppLogo } from '../../shared'; import { SidebarFooter } from './SidebarFooter'; import { SidebarHeader } from './SidebarHeader'; import { SidebarMenu } from './SidebarMenu'; export function Header() { - const isMobile = useIsMobile(); const { pathname } = useLocation(); const [isSheetOpen, setIsSheetOpen] = useState(false); - // Close sheet when route changes useEffect(() => { setIsSheetOpen(false); }, [pathname]); @@ -23,31 +20,23 @@ export function Header() { return (
- - - + - {isMobile && ( - - - - - - - - - - - - - - )} + + + + + + + + + + + + +
); diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarFooter.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarFooter.tsx index ff3269b..d605b22 100755 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarFooter.tsx +++ b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarFooter.tsx @@ -1,48 +1,9 @@ -// import { LayoutGrid, MessageCircleMore } from 'lucide-react'; -// import { Button } from '#shared/ui/button'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu'; -// import { ChatSheet } from '#shared/partials/topbar/chat-sheet'; -import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu'; +import { UserAvatarMenu } from '../../shared'; export function SidebarFooter() { return (
- - } - /> - -
- {/* - - - } - /> */} - - {/* - - - } - /> */} -
+
); } diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarHeader.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarHeader.tsx index 2e280d1..8201243 100755 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarHeader.tsx +++ b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarHeader.tsx @@ -1,83 +1,11 @@ -// import { useEffect, useState } from 'react'; -import { ChevronsUpDown, Plus } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; -// import { cn } from '@crux-kit/utils'; -// import { MENU_ROOT } from '#app/menu'; -import { Button } from '#shared/ui/Button'; -import { - DropdownMenu, - DropdownMenuContent, - // DropdownMenuItem, - DropdownMenuTrigger -} from '#shared/ui/DropdownMenu'; - -// import { SearchDialog } from '#shared/partials/dialogs/search/search-dialog'; +import { AppLogo, AppWordmark } from '../../shared'; export function SidebarHeader() { - // const { pathname } = useLocation(); - // const [selectedMenuItem, setSelectedMenuItem] = useState(MENU_ROOT[1]); - - // useEffect(() => { - // MENU_ROOT.forEach((item) => { - // if (item.rootPath && pathname.includes(item.rootPath)) { - // setSelectedMenuItem(item); - // } - // }); - // }, [pathname]); - return (
-
- - - - - - - Metronic - - - - {/* {MENU_ROOT.map((item, index) => ( - - - {item.icon && } - {item.title} - - - ))} */} - - -
- -
- - - {/* - - - } - /> */} +
+ +
); diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenu.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenu.tsx index 9a3bdc7..090b568 100755 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenu.tsx +++ b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenu.tsx @@ -1,11 +1,9 @@ -import { SidebarMenuPrimary } from './SidebarMenuPrimary'; -import { SidebarMenuSecondary } from './SidebarMenuSecondary'; +import { SidebarAccordionMenu } from '../../shared'; export function SidebarMenu() { return (
- - +
); } diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuPrimary.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuPrimary.tsx deleted file mode 100755 index 30d5325..0000000 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuPrimary.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import { JSX, useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuConfig, MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { createMenuPathMatcher } from '#shared/lib/menuPath'; -import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu'; - -export function SidebarMenuPrimary() { - const { pathname } = useLocation(); - const menuConfig = useMenuConfig(); - - // Memoize matchPath to prevent unnecessary re-renders - const matchPath = useMemo( - () => createMenuPathMatcher(menuConfig, pathname), - [menuConfig, pathname] - ); - - // Global classNames for consistent styling - const classNames: AccordionMenuClassNames = { - root: 'space-y-2.5 px-3.5', - group: 'gap-px', - label: 'uppercase text-xs font-medium text-muted-foreground pt-2.25 pb-px', - separator: '', - item: 'h-10 border-0 hover:bg-transparent border border-transparent text-white/70 hover:text-white data-[selected=true]:text-white data-[selected=true]:bg-secondary/70', - sub: '', - subTrigger: - 'h-10 border-0 hover:bg-transparent border border-transparent text-white/70 hover:text-white data-[selected=true]:text-white data-[selected=true]:bg-secondary/70', - subContent: 'p-0', - indicator: '' - }; - - const buildMenu = (items: MenuConfig): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.heading && !item.disabled) { - return buildMenuItemRoot(item, index); - } else { - return <>; - } - }); - }; - - const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => { - if (item.children) { - return ( - - - {item.icon && } - {item.title} - - - {buildMenuItemChildren(item.children, 1)} - - - ); - } else { - return ( - - - {item.icon && } - {item.title} - - - ); - } - }; - - const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => { - return items.map((item: MenuItem, index: number) => { - if (!item.heading && !item.disabled) { - return buildMenuItemChild(item, index, level); - } else { - return <>; - } - }); - }; - - const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => { - if (item.children) { - return ( - - - {item.collapse ? ( - - - {item.collapseTitle} - - {item.expandTitle} - - ) : ( - item.title - )} - - 0 ? '' : '') - )} - > - - {buildMenuItemChildren(item.children, item.collapse ? level : level + 1)} - - - - ); - } else { - return ( - - {item.title} - - ); - } - }; - - return ( - - Pages - {buildMenu(menuConfig)} - - ); -} diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuSecondary.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuSecondary.tsx deleted file mode 100755 index 68a86a4..0000000 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/SidebarMenuSecondary.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Link } from 'react-router-dom'; -import { toAbsoluteUrl } from '#shared/lib/helpers'; - -const items = [ - { - logo: 'x-dark.svg', - title: '@keenthemes', - path: 'https://keenthemes.com/metronic/tailwind/docs/' - }, - { - logo: 'slack.svg', - title: '@keenthemes_hub', - path: 'https://github.com/keenthemes/' - }, - { - logo: 'figma.svg', - title: 'metronic', - path: 'https://keenthemes.com/metronic/tailwind/docs/' - } -]; - -export function SidebarMenuSecondary() { - return ( -
-

Outline

-
- {items.map((item, index) => ( - - - {item.title} - - - {item.title} - - - ))} -
-
- ); -} diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/Toolbar.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/Toolbar.tsx deleted file mode 100755 index 2c95599..0000000 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/Toolbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Fragment, ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@crux-kit/utils'; -import { MenuItem } from '#app/config/types'; -import { useMenuConfig } from '#app/menu'; -import { useMenu } from '#shared/hooks/useMenu'; -import { Container } from '#shared/widgets/common/Container'; - -export interface ToolbarHeadingProps { - title?: string | ReactNode; - description?: string | ReactNode; -} - -function Toolbar({ children }: { children?: ReactNode }) { - return ( - -
- {children} -
-
- ); -} - -function ToolbarActions({ children }: { children?: ReactNode }) { - return
{children}
; -} - -function ToolbarBreadcrumbs() { - const { pathname } = useLocation(); - const { getBreadcrumb, isActive } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const items: MenuItem[] = getBreadcrumb(menuConfig); - - if (items.length === 0) { - return null; - } - - return ( -
- {items.map((item, index) => { - const isLast = index === items.length - 1; - const active = item.path ? isActive(item.path) : false; - - return ( - - {item.path ? ( - - {item.title} - - ) : ( - - {item.title} - - )} - {!isLast && /} - - ); - })} -
- ); -} - -function ToolbarHeading({ title = '' }: ToolbarHeadingProps) { - const { pathname } = useLocation(); - const { getCurrentItem } = useMenu(pathname); - const menuConfig = useMenuConfig(); - const item = getCurrentItem(menuConfig); - - return ( -
-

{title || item?.title}

- -
- ); -} - -export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading }; diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/components/ToolbarMenu.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/components/ToolbarMenu.tsx deleted file mode 100755 index 06bbb86..0000000 --- a/apps/admin/src/app/layouts/10__DarkSidebar/components/ToolbarMenu.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import { Calendar, ChevronDown } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { Button } from '#shared/ui/Button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#shared/ui/DropdownMenu'; - -export function ToolbarMenu() { - const months = [ - { title: 'January, 2024' }, - { title: 'February, 2024' }, - { title: 'March, 2024', active: true }, - { title: 'April, 2024' }, - { title: 'May, 2024' }, - { title: 'June, 2024' }, - { title: 'July, 2024' }, - { title: 'August, 2024' }, - { title: 'September, 2024' }, - { title: 'October, 2024' }, - { title: 'November, 2024' }, - { title: 'December, 2024' } - ]; - - return ( - - - - - - {months.map((item, index) => ( - - - {item.title} - - - ))} - - - ); -} diff --git a/apps/admin/src/app/layouts/10__DarkSidebar/layout.tsx b/apps/admin/src/app/layouts/10__DarkSidebar/layout.tsx index f52ca13..6d41d0a 100755 --- a/apps/admin/src/app/layouts/10__DarkSidebar/layout.tsx +++ b/apps/admin/src/app/layouts/10__DarkSidebar/layout.tsx @@ -1,30 +1,17 @@ // DEMO 10 // https://keenthemes.com/metronic/tailwind/react/demo10 -import { addDays, format } from 'date-fns'; -import { CalendarDays, Download } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { Link, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useBodyClass, useIsMobile } from '@crux-kit/hooks'; import { LayoutNames } from '#app/config/types'; -import { useSettings } from '#app/providers/SettingsProvider'; -import { Button } from '#shared/ui/Button'; -import { Calendar } from '#shared/ui/Calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '#shared/ui/Popover'; -import { Footer } from './components/Footer'; +import { AppFooter, Toolbar, ToolbarHeading, useLayoutChrome, useLayoutSettings } from '../shared'; import { Header } from './components/Header'; import { Sidebar } from './components/Sidebar'; -import { Toolbar, ToolbarActions, ToolbarHeading } from './components/Toolbar'; export const DarkSidebarLayout = () => { - const { setOption } = useSettings(); + useLayoutSettings(LayoutNames.DARK_SIDEBAR); const isMobile = useIsMobile(); - - const [date, setDate] = useState({ - from: new Date(2025, 0, 20), - to: addDays(new Date(2025, 0, 20), 20) - }); + const { showToolbar, showFooter } = useLayoutChrome(); useBodyClass(` [--header-height:60px] @@ -32,10 +19,6 @@ export const DarkSidebarLayout = () => { bg-mono! dark:bg-background! `); - useEffect(() => { - setOption('layout', LayoutNames.DARK_SIDEBAR); - }, [setOption]); - return ( <>
@@ -47,51 +30,16 @@ export const DarkSidebarLayout = () => {
- - - - - - - - - - - - - - - - + {showToolbar && ( + + + + )}
-
+ {showFooter && }
diff --git a/apps/admin/src/app/layouts/registry.ts b/apps/admin/src/app/layouts/registry.ts new file mode 100644 index 0000000..5443db5 --- /dev/null +++ b/apps/admin/src/app/layouts/registry.ts @@ -0,0 +1,35 @@ +import { type ComponentType } from 'react'; +import { LAYOUT } from '#app/config/app'; +import { LayoutNames } from '#app/config/types'; +import { DefaultLayout } from './01__Default/layout'; +import { HorizontalMenuLayout } from './02__HorizontalMenu/layout'; +import { CompactSidebarLayout } from './03__CompactSidebar/layout'; +import { TwoColumnSidebarLayout } from './04__TwoColumnSidebar/layout'; +import { DualRowsHeaderLayout } from './05__DualRowsHeader/layout'; +import { MultipleMenusLayout } from './06__MultipleMenus/layout'; +import { AdvancedMegaMenuLayout } from './07__AdvancedMegaMenu/layout'; +import { DropdownMenuLayout } from './08__DropdownMenu/layout'; +import { ExtendedHeaderLayout } from './09__ExtendedHeader/layout'; +import { DarkSidebarLayout } from './10__DarkSidebar/layout'; + +export const LAYOUTS: Record = + /* prettier-ignore */ { + [LayoutNames.DEFAULT] : DefaultLayout, + [LayoutNames.HORIZONTAL_MENU] : HorizontalMenuLayout, + [LayoutNames.COMPACT_SIDEBAR] : CompactSidebarLayout, + [LayoutNames.TWO_COLUMN_SIDEBAR] : TwoColumnSidebarLayout, + [LayoutNames.DUAL_ROW_HEADER] : DualRowsHeaderLayout, + [LayoutNames.MULTIPLE_MENUS] : MultipleMenusLayout, + [LayoutNames.ADVANCE_MEGA_MENU] : AdvancedMegaMenuLayout, + [LayoutNames.DROPDOWN_MENU] : DropdownMenuLayout, + [LayoutNames.EXTENDED_HEADER] : ExtendedHeaderLayout, + [LayoutNames.DARK_SIDEBAR] : DarkSidebarLayout +}; + +export const DEFAULT_LAYOUT = LayoutNames.MULTIPLE_MENUS; + +export const APP_LAYOUT: LayoutNames = LAYOUT in LAYOUTS ? (LAYOUT as LayoutNames) : DEFAULT_LAYOUT; + +export function getLayout(layout: LayoutNames = APP_LAYOUT): ComponentType { + return LAYOUTS[layout] ?? LAYOUTS[DEFAULT_LAYOUT]; +} diff --git a/apps/admin/src/app/layouts/shared/README.md b/apps/admin/src/app/layouts/shared/README.md new file mode 100644 index 0000000..4034f5a --- /dev/null +++ b/apps/admin/src/app/layouts/shared/README.md @@ -0,0 +1,44 @@ +# Shared layout kit + +The pieces every layout is built from. A layout supplies chrome — where the menu sits, how the +header behaves, which panels exist — and takes its behaviour from here, so switching the active +layout changes the arrangement and nothing else. + +## What lives here + +| Export | Purpose | +| ------------------------------------------------------------------- | --------------------------------------------------------------- | +| `useAppMenu` | The module-provided menu, narrowed to the visitor's permissions | +| `useActiveMenuSection` | The top-level entry the current route belongs to | +| `useLayoutChrome` | Whether the current route wants the toolbar and the footer | +| `getPermittedMenuItems`, `firstMenuPath` | Menu helpers behind those hooks | +| `SidebarAccordionMenu` | The accordion rendering of the menu | +| `NavMenubar` | The horizontal menubar rendering of the menu | +| `MegaNavMenu` | The mega-menu rendering of the menu | +| `Toolbar`, `ToolbarHeading`, `ToolbarBreadcrumbs`, `ToolbarActions` | Page heading and breadcrumbs | +| `AppFooter` | The application footer | +| `AppLogo` | The application mark, light and dark | +| `UserAvatarMenu` | The signed-in visitor's avatar and account menu | + +## Rules a layout follows + +1. **Menu entries come from `useAppMenu`.** Never from a hard-coded list and never from raw + `useMenuConfig`, which skips the permission filter. +2. **Toolbar and footer visibility come from `useLayoutChrome`.** Sections that draw their own + heading, or need the whole viewport, are listed there once for every layout. +3. **The account menu is `UserAvatarMenu`.** It reads the signed-in user and renders nothing without + one. +4. **The logo is `AppLogo`.** The project ships exactly two logo assets. +5. **Report the layout on mount** with `setOption('layout', LayoutNames.…)`, and undo any body + classes or `data-` attributes on unmount. +6. **Read options from your own layout's key** in `settings.layouts`. Every layout has a full set of + options, so the lookup is always defined. + +## Switching layouts + +`app/layouts/registry.ts` maps every `LayoutNames` value to its component, and `AppRoutingSetup` +mounts `getLayout()`. + +`APP_LAYOUT` in the same file takes the raw `VITE_LAYOUT` value from `app/config/app.ts` and falls +back to `DEFAULT_LAYOUT` when the variable is unset or names a layout that does not exist. Set +`VITE_LAYOUT` to any `LayoutNames` value to swap the mounted layout without touching code. diff --git a/apps/admin/src/app/layouts/06__MultipleMenus/components/Footer.tsx b/apps/admin/src/app/layouts/shared/components/AppFooter.tsx old mode 100755 new mode 100644 similarity index 90% rename from apps/admin/src/app/layouts/06__MultipleMenus/components/Footer.tsx rename to apps/admin/src/app/layouts/shared/components/AppFooter.tsx index c10dae4..ed494cc --- a/apps/admin/src/app/layouts/06__MultipleMenus/components/Footer.tsx +++ b/apps/admin/src/app/layouts/shared/components/AppFooter.tsx @@ -1,12 +1,17 @@ +import { cn } from '@crux-kit/utils'; import { APP_VERSION } from '#app/config/app'; import { Dialog, DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '#shared/ui/Dialog'; import { Container } from '#shared/widgets/common/Container'; const currentYear = new Date().getFullYear(); -export function Footer() { +export interface AppFooterProps { + className?: string; +} + +export function AppFooter({ className }: AppFooterProps) { return ( -