diff --git a/apps/web/src/apps/index.tsx b/apps/web/src/apps/index.tsx index 7ca77b6..ec6e524 100644 --- a/apps/web/src/apps/index.tsx +++ b/apps/web/src/apps/index.tsx @@ -6,6 +6,7 @@ import { NotFound, Forbidden, Maintenance, ComingSoon } from '@repo/ui/component const AuthModule = lazy(() => import('./auth')); const AppModule = lazy(() => import('./modules')); const ShowcaseView = lazy(() => import('./showcase/showcase-view')); +const ShellDemo = lazy(() => import('./shell-demo')); export default function App() { const [colorScheme, setColorScheme] = useState('light'); @@ -29,6 +30,7 @@ export default function App() { /> } /> + } /> } /> } /> } /> diff --git a/apps/web/src/apps/shell-demo/index.tsx b/apps/web/src/apps/shell-demo/index.tsx new file mode 100644 index 0000000..5adc886 --- /dev/null +++ b/apps/web/src/apps/shell-demo/index.tsx @@ -0,0 +1,306 @@ +import { useState, useMemo, ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; +import { + Box, + Button, + Group, + SegmentedControl, + Stack, + Text, + Paper, + Switch, + Select, + Burger +} from '@repo/ui/components'; +import { + CoreAppShell, + CorePageContainer, + CoreAppShellConfig, + LayoutVariant, + DesktopCollapseVariant, + useCoreAppShell +} from '@repo/ui/components'; +import { Home, BarChart2, Settings as SettingsIcon } from 'lucide-react'; + +// Sub-component to test hook methods +function LayoutControls() { + const { toggleDesktop, toggleMobile, sidebarVariant, setSidebarVariant, toggleAside, toggleNavbarPanel } = useCoreAppShell(); + + return ( + + + + + + + + ); +} + +function MockHeader() { + const { mobileOpened, toggleMobile } = useCoreAppShell(); + return ( + + + + Mock Header (bg="green.1") + + + ); +} + +function MockMobileDrawer() { + return ( + + Mock Mobile Drawer (bg="yellow.1") + + + + + + ); +} + +function SettingRow({ title, description, control }: { title: string; description: string; control: ReactNode }) { + return ( + + + + {title} + {description} + + {control} + + + ); +} + +export default function ShellDemo() { + const [layoutVariant, setLayoutVariant] = useState('header-first'); + const [collapseVariant, setCollapseVariant] = useState('hide'); + const [withUtilityBar, setWithUtilityBar] = useState(true); + const [withAside, setWithAside] = useState(true); + const [withFooter, setWithFooter] = useState(true); + const [withDoubleSidebar, setWithDoubleSidebar] = useState(true); + const [copied, setCopied] = useState(false); + + const liveConfig = useMemo(() => { + return { + variant: layoutVariant, + features: { + desktopCollapseVariant: collapseVariant, + withUtilityBar: withUtilityBar ? undefined : false, + withAside: withAside ? undefined : false, + withFooter: withFooter ? undefined : false, + withDoubleSidebar, + } + }; + }, [layoutVariant, collapseVariant, withUtilityBar, withAside, withFooter, withDoubleSidebar]); + + const handleCopy = () => { + navigator.clipboard.writeText(JSON.stringify(liveConfig, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 2500); + }; + + const config: CoreAppShellConfig = { + variant: layoutVariant, + features: { + desktopCollapseVariant: collapseVariant, + withUtilityBar: withUtilityBar ? undefined : false, + withAside: withAside ? undefined : false, + withFooter: withFooter ? undefined : false, + withDoubleSidebar, + persistState: false, // Don't persist for the demo to avoid confusing other showcases + disabled: false, // Allow nested render overrides if needed + zIndex: 100, + } + }; + + return ( + + Mock Utility Bar (bg="blue.1") + + ), + header: , + navbarMobile: , + navbar: !withDoubleSidebar ? ( + + Mock Standard Navbar (bg="grape.1") + + + + + + + + ) : undefined, + navbarRail: withDoubleSidebar ? ( + + + Mock Rail (bg="orange.1") + + + + + + ) : undefined, + navbarPanel: withDoubleSidebar ? ( + + Mock Panel (bg="grape.1") + + + + + + + + ) : undefined, + aside: ( + + Mock Aside + This area could be used for notifications, help text, or contextual settings. + + ), + footer: ( + + Mock Footer (bg="gray.1") + + ), + + + }} + > + + + Layout Engine Interactive Demo + + + + + + } + stickyHeader + > + + + + + + setLayoutVariant((value as LayoutVariant) || 'sidebar-first')} + data={[ + { label: 'Sidebar First (Alt)', value: 'sidebar-first' }, + { label: 'Header First (Default)', value: 'header-first' }, + { label: 'Top Nav (Hidden Sidebar)', value: 'top-nav' }, + ]} + /> + } + /> + + setCollapseVariant(value as DesktopCollapseVariant)} + data={[ + { label: 'Hide (Slide Out)', value: 'hide' }, + { label: 'Mini (Shrink)', value: 'mini' }, + ]} + /> + } + /> + + setWithDoubleSidebar(event.currentTarget.checked)} + /> + } + /> + + setWithUtilityBar(event.currentTarget.checked)} + /> + } + /> + + setWithAside(event.currentTarget.checked)} + /> + } + /> + + setWithFooter(event.currentTarget.checked)} + /> + } + /> + + + + Configuration Preview + + + +
{JSON.stringify(liveConfig, null, 2)}
+
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/apps/showcase/showcase-view.tsx b/apps/web/src/apps/showcase/showcase-view.tsx index b5579c7..cfc0db6 100644 --- a/apps/web/src/apps/showcase/showcase-view.tsx +++ b/apps/web/src/apps/showcase/showcase-view.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ColorSchemeType, DensityType } from '@repo/ui/provider'; import { Button, @@ -41,6 +42,7 @@ interface ShowcaseViewProps { export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) { const [activeTab, setActiveTab] = useState('ui-components'); const { i18n } = useTranslation(); + const navigate = useNavigate(); // Mock data for the table const tableData = [ @@ -65,6 +67,8 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set return 'Global Event Bus Synchronization'; case 'hardware': return 'Hardware Integration & Printers'; + case 'layout-engine': + return 'Core Layout Engine & Variants'; default: return 'Architecture Showcase'; } @@ -76,7 +80,13 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set orientation="vertical" placement="left" value={activeTab} - onChange={setActiveTab} + onChange={(val) => { + if (val === 'layout-engine') { + navigate('/shell-demo'); + } else { + setActiveTab(val); + } + }} variant="pills" radius="md" className="h-screen" @@ -106,6 +116,9 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set }> UI Components + }> + Layout Engine + }> Form Engine @@ -360,6 +373,8 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set )} + + diff --git a/packages/ui/src/components/core-app-shell/core-app-shell-context.tsx b/packages/ui/src/components/core-app-shell/core-app-shell-context.tsx new file mode 100644 index 0000000..f7126b1 --- /dev/null +++ b/packages/ui/src/components/core-app-shell/core-app-shell-context.tsx @@ -0,0 +1,70 @@ +import { createContext, useContext, useState, ReactNode } from 'react'; +import { useLocalStorage } from '@mantine/hooks'; +import { CoreAppShellConfig, SidebarVariant } from './types'; + +interface CoreAppShellContextValue { + mobileOpened: boolean; + desktopOpened: boolean; + sidebarVariant: SidebarVariant; + asideOpened: boolean; + navbarPanelOpened: boolean; + config: CoreAppShellConfig; + toggleMobile: () => void; + toggleDesktop: () => void; + toggleAside: () => void; + toggleNavbarPanel: () => void; + setSidebarVariant: (variant: SidebarVariant) => void; +} + +const CoreAppShellContext = createContext(null); + +interface CoreAppShellProviderProps { + children: ReactNode; + config: CoreAppShellConfig; +} + +export function CoreAppShellProvider({ children, config }: CoreAppShellProviderProps) { + const [mobileOpened, setMobileOpened] = useState(false); + const [desktopOpened, setDesktopOpened] = useState(true); + const [asideOpened, setAsideOpened] = useState(false); + const [navbarPanelOpened, setNavbarPanelOpened] = useState(true); + + const [sidebarVariant, setSidebarVariant] = useLocalStorage({ + key: 'core-app-shell-sidebar-variant', + defaultValue: 'expanded', + getInitialValueInEffect: !config.features?.persistState, + }); + + const toggleMobile = () => setMobileOpened((o) => !o); + const toggleDesktop = () => setDesktopOpened((o) => !o); + const toggleAside = () => setAsideOpened((o) => !o); + const toggleNavbarPanel = () => setNavbarPanelOpened((o) => !o); + + return ( + + {children} + + ); +} + +export function useCoreAppShell() { + const context = useContext(CoreAppShellContext); + if (!context) { + throw new Error('useCoreAppShell must be used within CoreAppShellProvider'); + } + return context; +} diff --git a/packages/ui/src/components/core-app-shell/core-app-shell.tsx b/packages/ui/src/components/core-app-shell/core-app-shell.tsx new file mode 100644 index 0000000..50ab8e1 --- /dev/null +++ b/packages/ui/src/components/core-app-shell/core-app-shell.tsx @@ -0,0 +1,205 @@ +import React, { useMemo } from 'react'; +import { AppShell, Flex, Box } from '@mantine/core'; +import { CoreAppShellProvider, useCoreAppShell } from './core-app-shell-context'; +import { CoreAppShellConfig, CoreAppShellSlots, CoreAppShellDimensions } from './types'; + + +const DEFAULT_DIMENSIONS: Required = { + headerHeight: 60, + navbarWidth: 260, + navbarMiniWidth: 80, + navbarRailWidth: 54, + utilityBarHeight: 32, + asideWidth: 260, +}; + +interface CoreAppShellInnerProps { + slots: CoreAppShellSlots; + children: React.ReactNode; +} + +function CoreAppShellInner({ slots, children }: CoreAppShellInnerProps) { + const { mobileOpened, desktopOpened, sidebarVariant, asideOpened, navbarPanelOpened, config } = useCoreAppShell(); + const { variant, dimensions, features } = config; + const dims = { ...DEFAULT_DIMENSIONS, ...dimensions }; + + const isDoubleSidebar = features?.withDoubleSidebar; + const isTopNav = variant === 'top-nav'; + const isFooterOffset = variant === 'header-first'; + + // Calculate Navbar Width based on states + const navbarWidth = useMemo(() => { + if (isTopNav) return 0; + if (isDoubleSidebar) { + return navbarPanelOpened ? dims.navbarWidth : dims.navbarRailWidth; + } + if (sidebarVariant === 'mini' && dims.navbarMiniWidth) { + return dims.navbarMiniWidth; + } + return dims.navbarWidth; + }, [sidebarVariant, dims, isTopNav, isDoubleSidebar, navbarPanelOpened]); + + // Determine AppShell Layout + const appShellLayout = variant === 'sidebar-first' ? 'alt' : 'default'; + + // Smart defaults for slots + const showUtilityBar = (features?.withUtilityBar ?? Boolean(slots.utilityBar)) && Boolean(slots.utilityBar); + const showAside = (features?.withAside ?? Boolean(slots.aside)) && Boolean(slots.aside); + const showFooter = (features?.withFooter ?? Boolean(slots.footer)) && Boolean(slots.footer); + + + // Header height needs to account for utility bar if present + const totalHeaderHeight = useMemo(() => { + if (!showUtilityBar) return dims.headerHeight; + // Basic summation assuming pixel values or numeric equivalents if both are numbers + if (typeof dims.headerHeight === 'number' && typeof dims.utilityBarHeight === 'number') { + return dims.headerHeight + dims.utilityBarHeight; + } + return `calc(${dims.headerHeight}${typeof dims.headerHeight === 'number' ? 'px' : ''} + ${dims.utilityBarHeight}${typeof dims.utilityBarHeight === 'number' ? 'px' : ''})`; + }, [dims.headerHeight, dims.utilityBarHeight, showUtilityBar]); + + return ( + + + + {showUtilityBar && ( + + {slots.utilityBar} + + )} + + {slots.header} + + + + + {!isTopNav && ( + + + {isDoubleSidebar ? ( + + + {slots.navbarRail} + + {navbarPanelOpened && ( + + {slots.navbarPanel} + + )} + + ) : ( + slots.navbar + )} + + + {slots.navbarMobile || slots.navbar} + + + )} + + {showAside && ( + + {slots.aside} + + )} + + + {children} + + + {showFooter && ( + + {slots.footer} + + )} + + ); +} + +export interface CoreAppShellProps { + config: CoreAppShellConfig; + slots: CoreAppShellSlots; + children: React.ReactNode; +} + +export function CoreAppShell({ config, slots, children }: CoreAppShellProps) { + return ( + + + {children} + + + ); +} diff --git a/packages/ui/src/components/core-app-shell/core-page-container.tsx b/packages/ui/src/components/core-app-shell/core-page-container.tsx new file mode 100644 index 0000000..8527df9 --- /dev/null +++ b/packages/ui/src/components/core-app-shell/core-page-container.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from 'react'; +import { Box, Container, Stack, ContainerProps } from '@mantine/core'; + +export interface CorePageContainerProps extends ContainerProps { + headerSlot?: ReactNode; + children: ReactNode; + stickyHeader?: boolean; +} + +export function CorePageContainer({ + headerSlot, + stickyHeader = false, + children, + px = "md", + py = "md", + ...others +}: CorePageContainerProps) { + return ( + + + {headerSlot && ( + + + {headerSlot} + + + )} + + + {children} + + + + ); +} diff --git a/packages/ui/src/components/core-app-shell/index.ts b/packages/ui/src/components/core-app-shell/index.ts new file mode 100644 index 0000000..309c439 --- /dev/null +++ b/packages/ui/src/components/core-app-shell/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './core-app-shell-context'; +export * from './core-app-shell'; +export * from './core-page-container'; diff --git a/packages/ui/src/components/core-app-shell/types.ts b/packages/ui/src/components/core-app-shell/types.ts new file mode 100644 index 0000000..9b44e5c --- /dev/null +++ b/packages/ui/src/components/core-app-shell/types.ts @@ -0,0 +1,42 @@ +import { ReactNode } from 'react'; + +export type LayoutVariant = 'header-first' | 'sidebar-first' | 'top-nav'; +export type SidebarVariant = 'expanded' | 'mini' | 'hidden'; +export type DesktopCollapseVariant = 'hide' | 'mini'; + +export interface CoreAppShellDimensions { + utilityBarHeight?: number | string; + headerHeight?: number | string; + navbarWidth?: number | string; + navbarMiniWidth?: number | string; + navbarRailWidth?: number | string; + asideWidth?: number | string; +} + +export interface CoreAppShellFeatures { + desktopCollapseVariant?: DesktopCollapseVariant; + withUtilityBar?: boolean; + withAside?: boolean; + withFooter?: boolean; + withDoubleSidebar?: boolean; + persistState?: boolean; + zIndex?: number; + disabled?: boolean; +} + +export interface CoreAppShellConfig { + variant: LayoutVariant; + dimensions?: CoreAppShellDimensions; + features?: CoreAppShellFeatures; +} + +export interface CoreAppShellSlots { + utilityBar?: ReactNode; + header?: ReactNode; + navbar?: ReactNode; + navbarMobile?: ReactNode; + navbarRail?: ReactNode; + navbarPanel?: ReactNode; + aside?: ReactNode; + footer?: ReactNode; +} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 137e26a..a9338bc 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -9,3 +9,4 @@ export * from './system-pages/coming-soon'; export * from './system-pages/forbidden'; export * from './system-pages/maintenance'; export * from './system-pages/not-found'; +export * from './core-app-shell';