-
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 (AnimationController + Transform + GestureDetector + PopScope + 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, 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.
- 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 tosurface3on hover andsurface2on 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 (viaPopScope), 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).
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.
-
Colors: Surface, on-surface, primary, and divider colours from
LayrzTokens -
Top bar: Uses
tokens.colors.sf1with 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)
@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,
),
],
);
}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)
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;- 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.
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput