package:mobile/core/layout/layout.dart.
Rule
Build page bodies with the page primitives. Use the full-bleed variant only for the screen types in the allowlist. Do not hand-write page gutters (EdgeInsets.fromLTRB(16, …)), wrap a scrollable in
ResponsiveWrapper, or add N + MediaQuery.paddingOf(context).bottom for the
scroll end in new or substantially edited screens.
Breakpoints
DeviceClass follows the Material 3 window-size classes
(core/responsive/breakpoints.dart):
Page primitives resolve the class from the width the page actually receives,
so a page beside the shell rail uses the gutters for its own available width.
Tokens
layout_tokens.dart defines the only spacing values a page should need.
Max content width (gutters included) per
AppPageKind:
Compact screens never constrain width.
ResponsivePadding is a compatibility
layer that derives from these tokens; its names and values are unchanged.
Page primitives
Every primitive:
- applies the device-class gutter plus horizontal safe areas;
- centers content at the page kind’s max width by padding, so the scrollable, scrollbar, and drag area stay full width;
- adds
AppLayout.pageTopplus an optionaltopInset. Root pages passfloatingShellHeaderInset(context); - reserves the shell’s dock clearance (
MediaQuery.padding.bottom, published byFloatingShellDock) plus the end gap. PassconsumeBottomClearance: falsewhen an inner scrollable should run beneath the dock and read the clearance itself.
MediaQuery. A
legacy SafeArea or MediaQuery.paddingOf(context).bottom inside a migrated
page therefore adds nothing instead of counting the dock clearance twice.
SliverAppPage(includeBottom: false) leaves the bottom clearance for the
sliver that consumes it.
Grouped sections
AppGroupedSection(caption:, footer:, children:) renders a muted caption, a
heavily rounded translucent AppGroupedCard, inset hairline dividers, and an
optional muted footer. Use AppGroupedSection.spaced in a list to add
AppLayout.sectionGap after the section. The card only groups and divides
rows. Row layout belongs to the owning feature (for example SettingsTile).
Cards use theme surface / outlineVariant and become solid in high-contrast
mode.
Full-bleed allowlist
Only these screen types may useAppPageBody.fullBleed or skip the gutters:
- calendar timelines, month, and year grids;
- task board kanban, board timeline, and planner/gantt canvases;
- chat and Assistant conversation threads, composers, and Assistant Live;
- Meet call stages and other live video surfaces;
- media, document-page, and map viewers, plus full-canvas editors.
consumeBottomClearance: false), it also stops above
the dock. Headers, toolbars, and empty/error states inside a full-bleed screen
should still use the gutter token.
Adaptive shell
shellNavigationModeFor(size) in
features/shell/view/shell_navigation_rail.dart picks the navigation mode:
- Dock: phones, including landscape phones, and portrait tablets. The floating bottom dock owns destinations and in-app Back.
- Rail: width >= 840dp and shortest side >= 600dp (landscape tablets,
laptops, desktops).
ShellNavigationRailshows the same destinations on the leading edge, including the mini-app Back item. The dock keeps only the Assistant/Notes composer and page actions; an idle dock paints no surface. Scroll content reserves dock clearance only while the dock shows a composer slot or dock actions for the current route. Rail destinations show their existing localized label as a hover-only tooltip on pointer devices.
ShellPage independently of the layout), and
scroll clearance.
AdaptiveShellFrame builds the same tree in both modes: a Stack whose
first, keyed child holds the body. Rotation (for example an iPad going from
820x1180 to 1180x820), a window resize across either threshold, or an
immersive/fullscreen toggle only adds or removes the rail, so the dock and
every root tab keep their state. Routed pages survive through GoRouter’s
navigator key, but offstage root tabs do not, so regressions must assert on a
visited root tab. The rail paints after the body because routed pages can
contain BlockSemantics, which would otherwise drop the rail from the
accessibility tree.
Layout audit (October 2026)
Before this foundation:- 179 feature files hard-coded 16/20/24dp page insets. 103 of them set
horizontal gutters without
ResponsivePadding, so tablets and desktops got phone gutters. Phones mixed 16 and 20dp (for example the Documents list versus the document editor). - Scroll ends used at least eight different constants (16, 20, 24, 28, 32,
40, 96, 108) on top of
MediaQuery.paddingOf(context).bottom. - 38 files wrapped scrollables in
ResponsiveWrapper, which narrowed the scroll and drag area on wide screens. - Device classes came from the window width, which is wrong beside a rail.
ShellPagedocumented a rail and sidebar, but it always rendered the floating dock.- Root pages added
floatingShellHeaderInsetwith ad-hoc +8/+10 offsets.
Page kinds by screen
Feature screens use the primitives with these page kinds:
Full-bleed screens: calendar day/multi-day/month views, the task board kanban
canvas, chat threads and composers, Assistant Live, the Meet call grid, and
media/attachment viewers. Their headers, empty states and loading states still
use the gutter. When a page passes the dock clearance through
(
consumeBottomClearance: false), a scrolling canvas pads its own scroll end.
A fixed, non-scrolling canvas such as the month grid must stop above the dock
by reserving MediaQuery.paddingOf(context).bottom below itself.
Shell detail routes that are not mini-app nav items, such as a portfolio
project, register ShellTitleOverride and a ShellMiniNav with a
deepLinkBackRoute, so the dock or rail Back returns to their parent list.
Documented exceptions:
- Modal sheets and dialogs keep their own internal padding; they are not page bodies.
- Mail swipe rows stay edge to edge on phones and centre at the detail width on wider windows. The mail reader keeps a larger named end gap so long messages clear the Mail dock.
- Standalone (non-shell) forms with their own footer reserve the system inset in that footer instead of consuming dock clearance.
- Settings, Home, Notifications and Profile migrate in their own lanes. The full-screen task detail view keeps its own inset math until it is reworked.
LayoutBuilder), not MediaQuery.sizeOf(context). Beside the rail, the
window is wider than the body.
Regression coverage
test/core/layout/layout_tokens_test.dart: token derivation,ResponsivePaddingcompatibility, and the inset resolver.test/core/layout/app_page_test.dart: gutters per device class, max-width centering, dock clearance, full-bleed opt-out, slivers, and grouped sections.app_page_media_query_test.dartcovers the consumed-inset removal.test/features/documents/view/documents_layout_test.dart: the migrated Documents list and editor.test/features/shell/view/shell_navigation_rail_test.dartandshell_rail_navigation_test.dart: rail geometry, RTL, Back reachability, system Back, deep links, landscape phones, and body-width gutters. SetDESIGN_RENDER_DIRto write phone, tablet, and desktop PNG captures.shell_rail_layout_test.dart: no remount across rotation, resize, or immersive toggles, rail-mode clearance, RTL throughShellPage, and hover-only tooltips.shell_viewport_checks.dart: the dock/rail viewport matrix, including the root page’s first content onepageTopgap below the floating header.test/core/layout/page_padding_matrix_test.dart: representative detail and form screens at phone, portrait/landscape tablet and desktop widths. It checks that the page list uses the resolved insets, the scrollable stays full width, and content stays within the page kind’s max width.DESIGN_RENDER_DIRwrites PNG captures.