Skip to content

LayrzLayout

Kenny Mochizuki Escalona edited this page Aug 20, 2026 · 10 revisions

LayrzLayout

A Material-free application shell providing top-level navigation, user chrome, and notifications in a single, deliberate design. Two presentations resolved from container width via LayoutBuilder constraints: expanded sidebar on desktop, off-canvas drawer on mobile.

Metadata
Mirrors: ThemedLayout (layrz_theme)
Phase: M5 (Layout, navigation, feedback)
Domain: Layout
Primitive: Hand-rolled (GestureDetector + AnimatedContainer + OverlayPortal + RawMenuAnchor for dropdowns)
Status: Confirmed scope (D37).


Overview

LayrzLayout is the top-level application scaffold for layrz_ui. It orchestrates navigation, user identity chrome, and main content area presentation. Unlike ThemedLayout in layrz_theme (which supports multiple configurable presentations), LayrzLayout commits to a single, deliberate design that works for both desktop and mobile contexts.

Design Principles

  • One design system: layrz_ui does not support multiple competing layout presentations. The chosen design (sidebar + drawer) is locked for all consuming apps.
  • Container-driven breakpoints: Presentations resolve via LayoutBuilder constraints + LayrzTheme.of(context).breakpoints.bandAt(), NOT viewport dimensions. This decouples presentation from global screen size and makes the component more reusable in sub-windows.
  • Flat navigation: A single list of LayrzNavigatorPage and LayrzNavigatorLabel items. No persistent vs. transient distinction; no separators; no custom action buttons.
  • User chrome via dropdown: User profile (avatar, name) lives in the top bar / sidebar, opening a RawMenuAnchor dropdown for settings, profile, logout, or theme switch via userMenuItems.
  • Notifications in footer: A labelled "Notifications" row at the bottom of the sidebar or drawer, with a RawMenuAnchor dropdown panel for viewing notifications. Never a route or modal.
  • Consumer-owned routing: LayrzLayout does not push routes. Navigation selection is driven by the consumer via LayrzNavigatorPage.isSelected flag, and the consumer owns route integration.

API Structure

Core Constructor

class LayrzLayout extends StatefulWidget {
  /// The logo image source (URL or asset path) displayed at the top of the layout.
  ///
  /// This is a [String] source suitable for [LayrzImage], not a Widget.
  /// Typically a URL or 'assets/logo.png' path.
  /// On mobile, displayed in the drawer header.
  /// On desktop, displayed at the top of the sidebar.
  final String logo;

  /// The application title displayed in the sidebar or drawer header.
  ///
  /// Example: "Layrz Admin", "Logistics Dashboard"
  final String appTitle;

  /// The body content to display alongside or below the navigation.
  ///
  /// This is the main application view, e.g., a Page, a router outlet, or a Screen.
  /// The layout provides padding and safe area handling; the body is rendered
  /// as-is within those constraints.
  final Widget body;

  /// Navigation items: a list of [LayrzNavigatorPage] and [LayrzNavigatorLabel].
  ///
  /// Pages are rendered as tappable rows with icon and label.
  /// Labels are full-bleed bands (optional tint colour at [tonalOpacity]).
  /// Consumers must pass the active page's [isSelected] flag to indicate the current selection.
  /// The layout does not manage selection state or push routes.
  final List<LayrzNavigatorItem> items;

  /// Optional colour tint for [LayrzNavigatorLabel] bands.
  ///
  /// When null, labels use the surface background.
  /// When present, the colour is flattened over the surface at [LayrzTokens.tonalOpacity].
  /// Defaults to null.
  final Color? labelColour;

  /// User menu items: a list of [LayrzDropdownItem] opened by the user block dropdown.
  ///
  /// Example:
  /// ```dart
  /// userMenuItems: [
  ///   LayrzDropdownItem(label: 'Profile', onTap: _showProfile),
  ///   LayrzDropdownItem(label: 'Settings', onTap: _showSettings),
  ///   LayrzDropdownItem(label: 'Logout', onTap: _logout),
  /// ]
  /// ```
  ///
  /// If empty or null, no dropdown is shown, but the user block still displays
  /// the avatar and name.
  final List<LayrzDropdownItem> userMenuItems;

  /// The user's avatar source.
  ///
  /// Type is [LayrzAvatarSource] (sealed hierarchy: Url, Base64, Icon, Emoji).
  /// When null, a placeholder icon is shown.
  final LayrzAvatarSource? userAvatar;

  /// The user's display name.
  ///
  /// Displayed in the user block alongside the avatar.
  /// Example: "Alice Johnson"
  final String? userName;

  /// Notification items to display in the footer dropdown.
  ///
  /// Each [LayrzNotificationItem] has a label, optional count, icon, and tap callback.
  /// The footer shows a labelled "Notifications" row; tapping opens a [RawMenuAnchor]
  /// dropdown panel listing all items.
  /// If empty or null, the notifications row is hidden.
  final List<LayrzNotificationItem> notifications;

  /// Whether the layout should display the search field above the navigator items.
  ///
  /// When true, a search field appears and filters navigator pages by [labelText],
  /// preserving section labels whose section still contains matching pages.
  /// Defaults to true.
  final bool showSearch;

  /// Creates a new [LayrzLayout] with the given properties.
  const LayrzLayout({
    super.key,
    required this.logo,
    required this.appTitle,
    required this.body,
    this.items = const [],
    this.labelColour,
    this.userMenuItems = const [],
    this.userAvatar,
    this.userName,
    this.notifications = const [],
    this.showSearch = true,
  });

  @override
  State<LayrzLayout> createState() => _LayrzLayoutState();
}

LayrzNavigatorItem Hierarchy

LayrzLayout supports two navigator item types, expressed as a sealed class hierarchy:

LayrzNavigatorPage

A navigable page entry with icon, label, and active state.

class LayrzNavigatorPage extends LayrzNavigatorItem {
  /// The human-readable label displayed in the navigation.
  final String labelText;

  /// The icon displayed alongside the label.
  ///
  /// Type is [IconData] (from layrz_icons).
  final IconData icon;

  /// Whether this page is currently selected / active.
  ///
  /// The consumer owns this flag and passes it on every rebuild.
  /// LayrzLayout does not manage state; it simply highlights the page
  /// when [isSelected] is true.
  final bool isSelected;

  /// Optional callback when this page is tapped.
  ///
  /// When null, the page is not tappable (disabled).
  final VoidCallback? onTap;

  const LayrzNavigatorPage({
    required this.labelText,
    required this.icon,
    required this.isSelected,
    this.onTap,
  });
}

LayrzNavigatorLabel

A section header or grouping band with optional tint colour.

class LayrzNavigatorLabel extends LayrzNavigatorItem {
  /// The label text displayed in the band.
  final String labelText;

  /// Optional colour tint for this label band.
  ///
  /// When null, inherits the [LayrzLayout.labelColour] or surface background.
  /// Colour is flattened at [LayrzTokens.tonalOpacity].
  final Color? colour;

  const LayrzNavigatorLabel({
    required this.labelText,
    this.colour,
  });
}

Presentations

Expanded (md/lg/xl)

  • Sidebar: 178px fixed width, left side
  • Content area: Grows to fill remaining space, centred at xl breakpoint (capped at 1440px)
  • User block: Top of sidebar
  • Navigation: Below user block, scrollable if items exceed available height
  • Notifications: Footer of sidebar

Drawer (sm/xs)

  • Top bar: 56px fixed height, full width
  • Logo/title: Left side of top bar, expandable hamburger menu
  • User block: Right side of top bar, dropdown menu
  • Navigation drawer: Off-canvas 260px drawer, slides in from left, closes via back/tap-outside
  • Content area: Full width below top bar
  • Notifications: Bottom row of drawer (if drawer is open)

Recent Changes (M2 Touch Review)

Safe Area Handling

Rail, drawer, and top-bar surfaces now paint edge-to-edge — under the status bar, notch, and home indicator — so their fill and elevation reach the physical screen edge. Their content is inset via per-edge SafeArea:

  • Rail and drawer: use SafeArea(right: false, ...) since the right edge meets the body
  • Top bar: extends up behind the status bar while its content stays at kLayrzLayoutTopBarHeight (56px)
  • Body: deliberately NOT inset — the page owns its own insets

Top Bar Color and Elevation

The top bar now uses tokens.colors.background with no elevation, so it reads as one continuous surface with the page. Previously it had a distinct color/shadow.

User Avatar Location

The user avatar/profile block now lives in the drawer only. In the expanded (sidebar) presentation, the user block sits at the top of the rail; in the drawer presentation, it appears in the drawer. The top bar (drawer presentation) shows only the logo and drawer trigger icon — no user chrome.

Drawer Trigger Icon

The drawer trigger icon is now LayrzIcons.solarBoldMenuDots (previously a different hamburger icon).

Drawer Auto-Close Behavior

The navigation drawer now closes automatically when a navigator page is tapped. Section labels (like "Settings" or "Main") do NOT close the drawer — only tapping a page does. This streamlines mobile navigation: selecting a destination closes the drawer immediately without requiring an extra tap.


Theming

  • Colors: Surface, on-surface, primary, and divider colours from LayrzTokens
  • Sidebar width: Fixed at 178px (constant kLayrzLayoutSidebarWidth)
  • Drawer width: Fixed at 260px (constant kLayrzLayoutDrawerWidth)
  • Top bar height: Fixed at 56px (constant kLayrzLayoutTopBarHeight)
  • User block styling: Avatar (40px), name (14px font), optional label beneath
  • Label tint: Optional colour at LayrzTokens.tonalOpacity (default 0.12)

Usage Examples

Basic Sidebar Layout (Desktop)

@override
Widget build(BuildContext context) {
  return LayrzLayout(
    logo: 'assets/logo.png',
    appTitle: 'My App',
    body: Placeholder(), // Your main content
    items: [
      LayrzNavigatorLabel(labelText: 'Main'),
      LayrzNavigatorPage(
        labelText: 'Home',
        icon: solarOutlineHome,
        isSelected: _currentPage == 'home',
        onTap: () => setState(() => _currentPage = 'home'),
      ),
      LayrzNavigatorPage(
        labelText: 'Settings',
        icon: solarOutlineSettings,
        isSelected: _currentPage == 'settings',
        onTap: () => setState(() => _currentPage = 'settings'),
      ),
    ],
    userAvatar: LayrzAvatarUrl('https://example.com/avatar.png'),
    userName: 'Alice Johnson',
    userMenuItems: [
      LayrzDropdownItem(
        label: 'Profile',
        onTap: _showProfile,
      ),
      LayrzDropdownItem(
        label: 'Logout',
        onTap: _logout,
      ),
    ],
  );
}

With Notifications

LayrzLayout(
  // ... other properties
  notifications: [
    LayrzNotificationItem(
      labelText: 'New Messages',
      count: 3,
      onTap: _viewMessages,
    ),
    LayrzNotificationItem(
      labelText: 'System Alerts',
      count: 1,
      onTap: _viewAlerts,
    ),
  ],
)

With Search

LayrzLayout(
  // ... other properties
  showSearch: true, // Enable search field
  // Items are filtered by labelText matching; pages remain even if their label doesn't match
  // (but their parent label hides if all children are filtered out)
)

Accessibility

  • Keyboard navigation: Tab cycles through nav items and user menu; Enter/Space activates; Escape closes dropdowns
  • Screen readers: Each nav item and user menu item has semantic labels
  • Contrast: Colours meet WCAG AA minimum (4.5:1 for text)

Constants

const double kLayrzLayoutSidebarWidth = 178.0;
const double kLayrzLayoutDrawerWidth = 260.0;
const double kLayrzLayoutTopBarHeight = 56.0;
const double kLayrzLayoutAvatarSize = 40.0;
const double kLayrzLayoutUserNameFontSize = 14.0;

Related


Last updated: 2026-08-19
Status: Implementation underway (M5)

Clone this wiki locally