-
Notifications
You must be signed in to change notification settings - Fork 0
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).
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.
- 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
LayoutBuilderconstraints +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
LayrzNavigatorPageandLayrzNavigatorLabelitems. 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
RawMenuAnchordropdown for settings, profile, logout, or theme switch viauserMenuItems. -
Notifications in footer: A labelled "Notifications" row at the bottom of the sidebar or drawer, with a
RawMenuAnchordropdown 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.isSelectedflag, and the consumer owns route integration.
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();
}LayrzLayout supports two navigator item types, expressed as a sealed class hierarchy:
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,
});
}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,
});
}- 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
- 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)
-
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)
@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,
),
],
);
}LayrzLayout(
// ... other properties
notifications: [
LayrzNotificationItem(
labelText: 'New Messages',
count: 3,
onTap: _viewMessages,
),
LayrzNotificationItem(
labelText: 'System Alerts',
count: 1,
onTap: _viewAlerts,
),
],
)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)
)- 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)
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;- Component Catalog (Maps Layrz layouts across all components)
- Input Contract (Shared form field patterns)
- Theming (Token system and theme customization)
- Repo: D37 — LayrzLayout and LayrzScaffoldShell Scope
Last updated: 2026-08-19
Status: Implementation underway (M5)
Made with ❤️ by Golden M, Inc.
- LayrzAnchoredPanel
- LayrzBottomSheet
- LayrzDialog
- LayrzDropdownMenu
- LayrzResponsiveModal
- LayrzPageTransition
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput
- LayrzSlider
- LayrzStepper