Skip to content

LayrzLayout

Kenny Mochizuki Escalona edited this page Aug 19, 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)

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