Files
trackgo-fe/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md
T
shancheas 71046973ae feat: add currency input field and enhance currency formatting utilities
- Introduced `FieldCurrencyInput` component for handling currency input in forms, supporting formatted display while maintaining numeric values.
- Implemented `RenderCurrency` component for displaying currency values with proper formatting.
- Enhanced `CurrencyUtils` to include parsing and formatting functions for Rupiah, ensuring accurate representation in UI.
- Updated various components and forms to utilize the new currency input and rendering capabilities, improving user experience in financial data entry.
- Added unit tests for currency input and formatting functionalities to ensure reliability and correctness.

These changes enhance the application's handling of currency inputs and displays, providing a more user-friendly experience for financial transactions.
2026-08-31 15:36:53 +07:00

25 KiB

outline
outline
2
3

Core App Shell — Layout Engine

Architectural Foundation: Mantine AppShell · @mantine/hooks

Description: Configuration-driven layout engine wrapping Mantine's AppShell, providing three layout variants (header-first, sidebar-first, top-nav), double sidebar support, responsive mobile drawers, and state persistence via Context API.

Package: @repo/ui · Module Path: @repo/ui/components > Dependencies: React 18+, Mantine v8 (AppShell), @mantine/hooks


Table of Contents


Overview

The Core App Shell is a configuration-driven layout engine that wraps Mantine's AppShell component. It provides a single <CoreAppShell> component that renders enterprise-grade application frames — complete with headers, sidebars, aside panels, utility bars, and footers — controlled entirely through a declarative config object and slot-based content injection.

Key capabilities:

  • Three layout variantsheader-first, sidebar-first, and top-nav — covering the most common enterprise SaaS patterns
  • Double sidebar — Google-style rail + contextual panel navigation
  • Smart defaults — Slots auto-detect presence; no explicit feature flags needed for basic layouts
  • Responsive out-of-the-box — Mobile drawer, desktop collapse, and mini-sidebar are all built-in
  • State persistence — Optional localStorage-backed sidebar state via @mantine/hooks
  • Context API — All layout toggle methods (toggleMobile, toggleDesktop, setSidebarVariant, etc.) are available to any descendant component via useCoreAppShell()

Architecture

Composition Model

The layout engine uses a Provider → Inner composition pattern:

CoreAppShell (Public API)
  └── CoreAppShellProvider (Context — state management)
        └── CoreAppShellInner (Layout rendering — consumes context)
              └── Mantine <AppShell> (CSS Grid engine)
                    ├── AppShell.Header    ← slots.utilityBar + slots.header
                    ├── AppShell.Navbar    ← slots.sidebar | slots.sidebarRail + slots.sidebarPanel
                    ├── AppShell.Main      ← children
                    ├── AppShell.Aside     ← slots.aside
                    └── AppShell.Footer    ← slots.footer

The outer CoreAppShell is a thin wrapper that instantiates the provider and passes config down. The inner component subscribes to context and derives all layout calculations (navbar width, header height, collapse states) from the live config + user interactions.

File Structure

packages/ui/src/components/core-app-shell/
├── types.ts                       # All TypeScript interfaces and union types
├── core-app-shell-context.tsx     # Context provider + useCoreAppShell hook
├── core-app-shell.tsx             # Main component (Public API + Inner renderer)
├── core-page-container.tsx        # Companion page-level content wrapper
└── index.ts                       # Barrel exports

Source: packages/ui/src/components/core-app-shell/


API Reference

CoreAppShellConfig

The top-level configuration object that controls the entire layout:

interface CoreAppShellConfig {
  variant: LayoutVariant;
  dimensions?: CoreAppShellDimensions;
  features?: CoreAppShellFeatures;
}
Property Type Required Description
variant LayoutVariant Determines the structural layout mode
dimensions CoreAppShellDimensions Override default pixel dimensions
features CoreAppShellFeatures Toggle optional layout regions and behaviors

Layout Variants

type LayoutVariant = 'header-first' | 'sidebar-first' | 'top-nav';
Variant Mantine layout Visual Description
header-first default Header spans the full viewport width. Sidebar and aside sit below the header, stretching to the bottom of the screen. Footer is inset between the sidebar and aside. This is the most common enterprise/dashboard pattern (e.g., Azure Portal, Jira).
sidebar-first alt Sidebar spans the full viewport height. Header sits to the right of the sidebar. Produces a "desktop application" feel (e.g., VS Code, Slack). Footer spans full width beneath the sidebar.
top-nav default Header-only layout with no visible desktop sidebar. The sidebar is hidden on desktop but remains accessible as a mobile drawer on small screens. Ideal for documentation sites or marketing pages.

Important

When variant is set to top-nav, the desktop navbar is visually hidden via collapsed.desktop: true and width 0. However, the <AppShell.Navbar> DOM element remains mounted with responsive width props so the mobile drawer continues to function. This is an intentional design choice to avoid conditional DOM removal.


Features

interface CoreAppShellFeatures {
  desktopCollapseVariant?: DesktopCollapseVariant;
  withUtilityBar?: boolean;
  withAside?: boolean;
  withFooter?: boolean;
  withDoubleSidebar?: boolean;
  persistState?: boolean;
  zIndex?: number;
  disabled?: boolean;
}
Property Type Default Description
desktopCollapseVariant 'hide' | 'mini' 'hide' hide: Sidebar slides out completely (collapsed width = 0). mini: Sidebar shrinks to sidebarMiniWidth showing only icons.
withUtilityBar boolean Auto-detected Show the utility bar above the header. If omitted, the bar renders when a utilityBar slot is provided. Set explicitly to false to suppress.
withAside boolean Auto-detected Show the right-hand aside panel. Same auto-detection logic as withUtilityBar.
withFooter boolean Auto-detected Show the bottom footer. Same auto-detection logic.
withDoubleSidebar boolean false Enable the Rail + Panel double sidebar mode. When true, the navbar renders sidebarRail and sidebarPanel slots instead of the single sidebar slot.
persistState boolean true (implied) Persist sidebar variant (expanded/mini/hidden) to localStorage via useLocalStorage. Set to false for demos or ephemeral layouts.
zIndex number 200 Base z-index passed to Mantine's AppShell.
disabled boolean false Disables the AppShell layout entirely (renders children without structural chrome).

[!TIP] > Smart defaults: You rarely need to set withUtilityBar, withAside, or withFooter explicitly. The engine auto-detects presence by checking if the corresponding slot is provided and truthy. Only set them to false when you want to suppress a slot that is being passed.


Dimensions

interface CoreAppShellDimensions {
  utilityBarHeight?: number | string;
  headerHeight?: number | string;
  sidebarWidth?: number | string;
  sidebarMiniWidth?: number | string;
  sidebarRailWidth?: number | string;
  asideWidth?: number | string;
}
Property Type Default Description
utilityBarHeight number | string 32 Height of the utility bar strip above the header
headerHeight number | string 60 Height of the main header
sidebarWidth number | string 260 Width of the expanded sidebar
sidebarMiniWidth number | string 80 Width of the sidebar in mini collapse mode
sidebarRailWidth number | string 54 Width of the icon rail in double-sidebar mode
asideWidth number | string 260 Width of the right-hand aside panel

Note

All dimension values accept both pixel numbers (e.g., 260) and CSS strings (e.g., '20rem'). When both headerHeight and utilityBarHeight are numbers, they are summed directly. When either is a string, the engine wraps them in a calc() expression automatically.


Slots

Content is injected via the slots prop — a flat object of named ReactNode values:

interface CoreAppShellSlots {
  utilityBar?: ReactNode;
  header?: ReactNode;
  sidebar?: ReactNode;
  sidebarMobile?: ReactNode;
  sidebarRail?: ReactNode;
  sidebarPanel?: ReactNode;
  aside?: ReactNode;
  footer?: ReactNode;
}
Slot Location Notes
utilityBar Above the header, hidden on mobile (display: none below sm) Typically used for environment banners, announcements, or top-level links.
header Main application header Must contain its own <Burger> for mobile toggle (use useCoreAppShell() context).
sidebar Desktop navbar body (single-sidebar mode) Ignored when withDoubleSidebar is true — use sidebarRail + sidebarPanel instead.
sidebarMobile Mobile drawer content Falls back to sidebar if not provided. Use this to render a simplified mobile-specific navigation.
sidebarRail Narrow icon rail (double-sidebar mode) Only rendered when withDoubleSidebar is true. Separated from sidebarPanel by a 1px border.
sidebarPanel Contextual panel beside the rail (double-sidebar mode) Collapsible via toggleNavbarPanel(). Only rendered when withDoubleSidebar is true and navbarPanelOpened is true.
aside Right-hand panel Collapsible via toggleAside(). Only rendered when withAside is enabled.
footer Bottom application footer In header-first mode, the footer is inset between sidebar and aside. In sidebar-first mode, it spans the full width.

Context API

The useCoreAppShell() hook provides access to all layout state and toggle methods from any descendant component:

import { useCoreAppShell } from '@repo/ui/components';
Property / Method Type Description
mobileOpened boolean Whether the mobile drawer is currently open
desktopOpened boolean Whether the desktop sidebar is expanded (only applies when desktopCollapseVariant is 'hide')
sidebarVariant SidebarVariant Current sidebar mode: 'expanded' | 'mini' | 'hidden'
asideOpened boolean Whether the aside panel is currently visible
navbarPanelOpened boolean Whether the secondary panel in double-sidebar mode is expanded
config CoreAppShellConfig Read-only access to the current layout configuration
toggleMobile() () => void Toggle the mobile drawer open/closed
toggleDesktop() () => void Toggle the desktop sidebar open/closed
toggleAside() () => void Toggle the aside panel visibility
toggleNavbarPanel() () => void Toggle the double-sidebar panel open/closed
setSidebarVariant() (variant: SidebarVariant) => void Programmatically set the sidebar to 'expanded', 'mini', or 'hidden'

[!WARNING] > useCoreAppShell() must be called from within a <CoreAppShell> subtree. Calling it outside the provider will throw: "useCoreAppShell must be used within CoreAppShellProvider". If you need context access in the header slot, pass a component (not inline JSX) so it mounts inside the provider tree.


Usage Examples

Minimal Setup

The simplest possible layout — a header and sidebar with all defaults:

import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Group, Text, Box, Stack, Button, Burger } from '@repo/ui/components';

function MyHeader() {
  const { mobileOpened, toggleMobile } = useCoreAppShell();
  return (
    <Group h="100%" px="md">
      <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
      <Text fw={700}>My Application</Text>
    </Group>
  );
}

const config: CoreAppShellConfig = {
  variant: 'header-first',
};

function App() {
  return (
    <CoreAppShell
      config={config}
      slots={{
        header: <MyHeader />,
        sidebar: (
          <Stack p="md" gap="xs">
            <Button variant="subtle" fullWidth>
              Dashboard
            </Button>
            <Button variant="subtle" fullWidth>
              Settings
            </Button>
          </Stack>
        ),
      }}
    >
      <Text>Main content area</Text>
    </CoreAppShell>
  );
}

Header-First with Utility Bar

A full enterprise layout with utility bar, aside, and footer:

import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Group, Text, Box, Burger } from '@repo/ui/components';

function AppHeader() {
  const { mobileOpened, toggleMobile } = useCoreAppShell();
  return (
    <Group h="100%" px="md" justify="space-between">
      <Group>
        <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
        <Text fw={700} size="lg">
          Enterprise Dashboard
        </Text>
      </Group>
    </Group>
  );
}

const config: CoreAppShellConfig = {
  variant: 'header-first',
  features: {
    desktopCollapseVariant: 'hide',
    persistState: true,
  },
  dimensions: {
    headerHeight: 60,
    utilityBarHeight: 32,
    sidebarWidth: 280,
    asideWidth: 300,
  },
};

function App() {
  return (
    <CoreAppShell
      config={config}
      slots={{
        utilityBar: (
          <Group h="100%" px="md" justify="flex-end">
            <Text size="xs">v2.4.1 · Production</Text>
          </Group>
        ),
        header: <AppHeader />,
        sidebar: <MySidebar />,
        aside: <MyAside />,
        footer: (
          <Group h="100%" px="md">
            <Text size="sm">© 2026 Acme Corp</Text>
          </Group>
        ),
      }}
    >
      <MyPageContent />
    </CoreAppShell>
  );
}

Double Sidebar (Rail + Panel)

Google-style navigation with an icon rail and a collapsible contextual panel:

import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Stack, Box, Text, Burger, Group } from '@repo/ui/components';
import { Home, Settings, BarChart2 } from 'lucide-react';

function AppHeader() {
  const { mobileOpened, toggleMobile } = useCoreAppShell();
  return (
    <Group h="100%" px="md">
      <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
      <Text fw={700}>Admin Panel</Text>
    </Group>
  );
}

const config: CoreAppShellConfig = {
  variant: 'sidebar-first',
  features: {
    withDoubleSidebar: true,
  },
  dimensions: {
    sidebarRailWidth: 54,
    sidebarWidth: 260,
  },
};

function App() {
  return (
    <CoreAppShell
      config={config}
      slots={{
        header: <AppHeader />,
        sidebarRail: (
          <Stack align="center" gap="lg" pt="md">
            <Home size={24} />
            <BarChart2 size={24} />
            <Settings size={24} />
          </Stack>
        ),
        sidebarPanel: (
          <Box p="md">
            <Text fw={700} mb="sm">
              Navigation
            </Text>
            {/* Contextual links based on active rail icon */}
          </Box>
        ),
        sidebarMobile: (
          <Box p="md">
            <Text fw={700}>Mobile Nav</Text>
            {/* Simplified mobile navigation */}
          </Box>
        ),
      }}
    >
      <Text>Main content</Text>
    </CoreAppShell>
  );
}

Note

When withDoubleSidebar is true, the sidebar slot is ignored on desktop. The navbar renders sidebarRail (fixed-width icon column) and sidebarPanel (collapsible contextual panel) side-by-side. On mobile, sidebarMobile takes priority, falling back to sidebar if not provided.


Interactive Config Builder

The showcase demo at apps/showcase/src/pages/showcase-original/shell-demo/ demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing config state externally and passing it as a prop:

import { useState, useMemo } from 'react';
import { CoreAppShell, CoreAppShellConfig, LayoutVariant, DesktopCollapseVariant } from '@repo/ui/components';

function ShellDemo() {
  const [layoutVariant, setLayoutVariant] = useState<LayoutVariant>('header-first');
  const [collapseVariant, setCollapseVariant] = useState<DesktopCollapseVariant>('hide');
  const [withDoubleSidebar, setWithDoubleSidebar] = useState(false);

  const config: CoreAppShellConfig = useMemo(
    () => ({
      variant: layoutVariant,
      features: {
        desktopCollapseVariant: collapseVariant,
        withDoubleSidebar,
        persistState: false,
      },
    }),
    [layoutVariant, collapseVariant, withDoubleSidebar],
  );

  return (
    <CoreAppShell config={config} slots={{ header: <MyHeader />, sidebar: <MySidebar /> }}>
      {/* Config controls live here — they can use useCoreAppShell() for toggle methods */}
    </CoreAppShell>
  );
}

CorePageContainer

A companion component for structuring page-level content within the <AppShell.Main> area. It provides a sticky page header and a contained, padded content region.

import { CorePageContainer } from '@repo/ui/components';

Props

interface CorePageContainerProps extends ContainerProps {
  headerSlot?: ReactNode;
  children: ReactNode;
  stickyHeader?: boolean;
}
Prop Type Default Description
headerSlot ReactNode Page-level header content (title, breadcrumbs, action buttons). Rendered above the main content with a bottom border.
stickyHeader boolean false When true, the page header sticks to the top of the scroll area, offset by the AppShell header height via var(--app-shell-header-offset).
px MantineSpacing 'md' Horizontal padding for both the header and content areas
py MantineSpacing 'md' Vertical padding for both the header and content areas
...rest ContainerProps All other Mantine Container props are forwarded to the content region

Usage

<CoreAppShell config={config} slots={slots}>
  <CorePageContainer
    stickyHeader
    headerSlot={
      <Group justify="space-between">
        <Text component="h1" size="xl" fw={700}>
          Users
        </Text>
        <Button>Add User</Button>
      </Group>
    }
  >
    <UserTable />
  </CorePageContainer>
</CoreAppShell>

Design Decisions & Caveats

Mobile Navbar Lifecycle

The <AppShell.Navbar> DOM element is always mounted, even when the layout variant is top-nav. The desktop content is hidden via visibleFrom="sm" and mobile content via hiddenFrom="sm". This ensures Mantine's native drawer engine works correctly on mobile without conditional DOM removal breaking the transition animations.

In header-first mode, the footer is inset between the sidebar and aside using CSS custom properties:

left: var(--app-shell-navbar-offset, 0px);
right: var(--app-shell-aside-offset, 0px);

In sidebar-first mode, the footer spans the full viewport width (left: 0; right: 0).

Z-Index Strategy

Element header-first sidebar-first
AppShell (base) 200 (default) 200 (default)
Navbar 105 100
Aside 105 100
Footer 100 100

The elevated 105 z-index for navbar/aside in header-first mode ensures they render above the footer, which is positioned at 100.

Sidebar Width Calculation

The navbar width is dynamically computed based on multiple state variables:

navbarWidth.sm =
  isTopNav                    → 0
  isDoubleSidebar + panelOpen → sidebarWidth
  isDoubleSidebar + panelClosed → sidebarRailWidth
  sidebarVariant === 'mini'   → sidebarMiniWidth
  default                     → sidebarWidth

On mobile and xs breakpoints, the width is always 100% and sidebarWidth respectively, regardless of variant.