Skip to content

LayrzLayout

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 9 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 (AnimationController + Transform + GestureDetector + PopScope + 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, displaying avatar and user name
  • Navigation: Below user block, scrollable if items exceed available height
  • Notifications: Footer of sidebar

Safe Area Handling: The sidebar surface paints edge-to-edge under the status bar and notch, so its fill and elevation reach the physical screen edge. Content is inset via SafeArea(right: false, ...) since the right edge meets the body. The body itself is deliberately NOT inset — the page owns its own insets.

Drawer (sm/xs)

  • Top bar: 56px fixed height, full width, displaying logo on the left and user block on the right
  • Drawer trigger: A 40×40 icon button (MdiIcons.menu) in the top bar's leading edge, expands/collapses the navigation drawer. The button tints to surface3 on hover and surface2 on press, with no geometry changes (decision D15)
  • User block: Right side of top bar, dropdown menu showing user avatar and name
  • Navigation drawer: Off-canvas 260px drawer revealed by a floating-page transition. When the drawer opens, the page scales down to 0.88 (anchored at Alignment.centerLeft) and translates right by 260px, revealing the drawer as a flat backdrop behind it. The page gets rounded corners (r3, 16 logical pixels) and an elevation shadow (elevation4) while the drawer behind remains flat. Closes when tapping the visible page sliver, using the back button (via PopScope), or selecting a navigator page; also responds to edge drag (20px strip opens when closed, full page sliver drags to close). Fling velocity above 365 px/s settles the drawer; below that threshold, position at ≥50% opens, otherwise closes (see Drawer Interaction below)
  • Content area: Full width below top bar
  • Notifications: Bottom row of drawer (if drawer is open)

Safe Area Handling: The top bar surface paints edge-to-edge under the status bar, notch, and home indicator. The top bar content stays at kLayrzLayoutTopBarHeight (56px), inset from the status bar via SafeArea. Drawer content is inset via SafeArea(right: false, ...) since its right edge meets the body. The body is deliberately NOT inset — the page owns its own insets. The floating page layer carries the elevation shadow, not the drawer (which is flat).

Drawer Interaction

The navigation drawer 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
  • Top bar: Uses tokens.colors.sf1 with no elevation, reading as one continuous surface with the page
  • Sidebar width: Fixed at 178px (constant kLayrzLayoutRailWidth)
  • 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: MdiIcons.homeOutline,
        isSelected: _currentPage == 'home',
        onTap: () => setState(() => _currentPage = 'home'),
      ),
      LayrzNavigatorPage(
        labelText: 'Settings',
        icon: MdiIcons.cogOutline,
        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

Layout design constants are defined in lib/src/constants/src/layout.dart. Key constants for drawer presentation:

const double kLayrzLayoutDrawerWidth = 260.0;
const double kLayrzLayoutTopBarHeight = 56.0;
const double kLayrzLayoutTopBarIconButtonSize = 40.0;  // Drawer trigger button hit target
const double kLayrzLayoutDrawerEdgeDragWidth = 20.0;   // Left edge drag zone when closed
const double kLayrzLayoutDrawerOpenScale = 0.88;       // Page scale when drawer open (anchored centerLeft)
const double kLayrzLayoutDrawerDragSettleVelocity = 365.0;  // Fling velocity threshold (px/s)

Rail presentation constants:

const double kLayrzLayoutRailWidth = 178.0;
const double kLayrzLayoutItemRadius = 9.0;

Top bar constants:

const double kLayrzLayoutTopBarPaddingHorizontal = 12.0;
const double kLayrzLayoutTopBarGap = 12.0;
const double kLayrzLayoutDrawerTriggerIconSize = 24.0;
const double kLayrzLayoutTopBarLogoWidth = 200.0;
const double kLayrzLayoutTopBarLogoHeight = 40.0;

Related


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

Clone this wiki locally