package:mobile/core/ui/kit/kit.dart.
Principles
- Lead with the answer. A root page opens with what matters now (a balance, the next meeting, today’s tasks), not with a toolbar or a list header.
- One surface family. Content sits on grouped cards, panels and stat tiles that share radii, borders and translucency, so appearance themes (solid, gradient, blobs) show through consistently.
- Theme roles only. Every colour comes from
AppColorTokensthrough anAppTone. No hex values,Colors.*hues or per-feature palettes. - Cheap by default. Static fills and borders, no blur behind scrolling content, no shadows on list items, no layers for opacity.
- Adaptive, not stretched. Wider bodies get more columns, side panels and two panes, never a phone layout scaled up.
- Accessible without exceptions. 48dp targets, readable contrast in every theme, unclamped text scaling and complete semantics.
Typography
UseAppTokens.of(context).typography (context.tokens.typography). Styles
carry no colour; apply a role from the colour tokens.
- Numbers that update or line up (amounts, counts, times) use
FontFeature.tabularFigures(). - Titles wrap to two lines before truncating. Never clamp
MediaQuery.textScaler; components grow vertically instead. - The shell’s top navbar owns the page title. Do not repeat it as a heading in the body; the hero or the first section header introduces the content.
Spacing, radius and elevation
Spacing uses the 4ptAppSpacing scale and the page tokens in
AppLayout. Composition rules on top of those tokens:
Elevation is reserved for floating layers (dock, sheets, dialogs, toasts).
Cards in lists separate with a border and the surface tint, never with a
shadow. Legacy
FinancePanel adds a 28px blurred shadow to every card; new
code uses AppPanel instead.
Surfaces
All of them become solid when the platform requests high contrast
(
MediaQuery.highContrastOf).
Colour roles
Colours come fromAppColorTokens, which the appearance theme resolves for
every theme, accent and brightness. Components choose an AppTone, and the
tone resolves a foreground and a quiet fill:
- Text that carries meaning (values, titles) stays in
foreground. Tones colour icons, meters, trend labels and small accents, which keeps every theme above WCAG AA for text. - Tone tints are fills behind icons, not backgrounds behind body text.
primaryfollows the user’s accent. Do not assume it is blue; in Classic it is near-black in light mode and near-white in dark mode.- Per-module hex palettes (the Apps hub cards, Drive’s green, finance positive/negative literals) are migration debt; map them to tones.
Iconography
- Use the generated Lucide icons (
LucideIcons,LucideLabIcons,AppIcons) so mobile matches the web glyph for glyph. MaterialIcons.*are legacy. - Icons sit in an
AppIconTilewhen they identify an object (a row, a stat, an eyebrow) and stand alone when they decorate metadata (AppMetaItem, 14dp) or controls (16–20dp). - Row tiles are 32dp in grouped rows and 40dp in rich rows. Stat and hero eyebrow tiles are 28dp.
- Icons never carry meaning alone. Pair them with a label or a semantics label, and give icon buttons a tooltip.
Motion
Motion explains a change; it never decorates a resting screen.
Cheap-animation policy. These rules keep the kit fast on mid-range
Android devices; the Android jank lane may add a user-facing “reduce effects”
switch on top of them.
- Animate transforms and colours of small subtrees. Do not animate
Opacityover large subtrees; useFadeTransitionor an animated colour. - No live
BackdropFilterbehind scrolling content. Frosted chrome goes through the shell’s glass policy, which draws near-opaque fills on Android. - No
ShaderMask,ColorFilteredorOpacitywrappers that force asaveLayerin lists. Fade edges with a painted gradient or not at all. - No shadows on list items or tiles.
AppElevation.raisedis for single floating cards only. - Wrap independently repainting regions (a ticking timer, a live meter, a
chart) in a
RepaintBoundary.SliverAppAdaptiveGridadds one per tile. - Skeletons are static shapes. Do not add shimmer.
- Respect
MediaQuery.disableAnimationsOf: skip to the end state.
Page anatomy
A redesigned mini-app root page reads top to bottom:- Shell header. Title and actions from the shell. No body AppBar.
- Summary hero (
AppSummaryHero) or a stat row (AppAdaptiveGridofAppStatTile,equalHeight: true). Pick one per page, not both stacked at the same weight. - Filter bar (
AppFilterBar) when the list has 2–6 views. Search is the shell search action. More than six filters belong in a sheet. - Sections.
AppSectionHeaderplus content: grouped rows, rich rows, panels or a grid. - End clearance. Handled by the page primitives.
AppPanel with AppKeyValueRows for properties. Forms use
AppGroupedSections and open with showAppSheet or as a full-screen route
above the shell.
Patterns
Section headers
AppSectionHeader(title:, count:, subtitle:, actionLabel:, onAction:) heads
content sections (lists, grids, panels). The quieter AppSectionCaption (via
AppGroupedSection.caption) heads settings-style groups. Use one style per
page level; never stack a caption under a section header.
Lists and rows
- Rich rows put a value or a status
AppBadgeon the trailing edge and a unit or time intrailingCaption. selected: truemarks the open item when a list drives a detail pane.- Show at most three
AppMetaItems. Overdue or failing facts take thedestructiverole colour, everything else stays muted. - Group long lists by meaning (Today / Yesterday / Earlier, Overdue / Next / Later) instead of one undifferentiated column.
Cards and grids
AppAdaptiveGrid(minTileWidth:, maxColumns:)lays out summaries and small collections;SliverAppAdaptiveGridlays out long collections lazily with a fixed tile height scaled by text size.- Default stat tiles use
minTileWidth: 160(two per phone, four on a tablet). Catalogues and drives use 120–180 depending on thumbnail size. AppPanel(title:, subtitle:, actionLabel:)frames a chart, breakdown or short list. Setpadded: falsewhen the child brings row insets.
Stat tiles and charts
- A stat tile shows one label, one value and at most one context line
(
trendorcaption) plus an optionalprogressmeter. AppStatTrend(positive:)decides the colour, not the direction: a falling expense is good news.AppMeteris the static progress bar for budgets, goals, quotas and streaks.- Charts live in an
AppPanel, use tone foregrounds for series (at most four series per chart), label axes inxSmallmuted text and keep values in tabular figures. Paint charts withCustomPainterinside aRepaintBoundary; do not animate them on scroll.
Filters, search and sort
AppFilterBar<T>is a single-select pill row for views of the same list (All / Open / Done). Counts sit inside the pill.AppSegmentedControl<T>switches between two to four modes of the same content (Day / Week / Month, Self / Team). It is not a filter: use it for view modes and scopes, never for more than four choices.- Sorting and scopes (a collection, a wallet) use an
AppFilterButtonthat names the current choice, opens anAppOptionTilesheet and showsactivewhile it narrows the list. - Search uses the shell search action (
AppSheetSearchin sheets). Do not put an inline search field above a list inside the shell. - Multi-select filters (labels, people, ranges) open a sheet and show their active state as a count on a filter button.
Empty, loading and error states
- First load:
AppSkeletonRowsinside the same card the rows will occupy, or static tile shapes for grids. No spinners in the middle of a page. - Empty:
AppStateMessagewith an icon, a title that says what is missing, a message that says what will appear here and one action that fixes it. - Error with no data:
AppStateMessage(tone: AppTone.destructive)with a retry action. - Error with cached data: keep the data and show
AppStateMessage.inlineabove it (“Could not refresh, showing data from 10:24”). - Load more: load automatically at the end of the list. Show a skeleton row, not a “Load more” button.
Forms and sheets
- Open forms with
showAppSheetand lay them out withAppSheetScaffoldandAppGroupedSections ofAppTextField,AppSelect,AppSwitch,AppDatePickerandAppTimePicker. Do not useAlertDialog,SwitchListTile,showDatePickeror rawshowModalBottomSheet. - Confirmations use
showAppDialogwith a destructiveAppButtonfor destructive actions. - One primary action per sheet, in the sheet’s action area.
Responsive rules
Decisions use the width the page body receives (after the shell rail), viaDeviceClass and the page primitives.
- Use the shared list/detail primitive,
AppMasterDetail(core/layout/master_detail.dart), for two-pane screens. Do not build a per-feature split. - Side panels (filters, an inspector, a summary) sit on the trailing side at 320–360dp and scroll independently.
- Pointer devices get hover states from
InkWelland tooltips on icon buttons; keyboard users get focus rings from the core controls.
Visual identity per app
“Not repetitive” means each app has a recognisable opening and rhythm while sharing the same parts. Identity comes from four levers, in this order:- The hero question. Each root answers its own question in the hero: Finance “How much do I have?”, Calendar “What is next?”, Habits “Am I on streak?”. Never reuse the same eyebrow and layout across apps.
- The app tone. One
AppToneper app for the hero, its icon tiles and meters. Other tones only appear for status. - The dominant pattern. Pick the layout that fits the data: a timeline for Calendar, a board for Tasks, a ledger of rich rows for Finance, a catalogue grid for Inventory, a gallery for Drive.
- Signature detail. One small, cheap flourish per app, such as a streak meter in Habits, a live dot on the next meeting in Meet or a storage meter in Drive.
The hub’s tiles take each app’s tone, so the launcher previews the identity
users meet inside the app.
Composition kit
package:mobile/core/ui/kit/kit.dart exports:
Rows, tones, icon tiles and states (
AppGroupedRow, AppTone,
AppIconTile, AppSkeletonRows, AppStateMessage) live in
package:mobile/core/layout/layout.dart.
Migrating from finance_ui.dart
features/finance/widgets/finance_ui.dart grew into an informal shared kit
used by about 30 files outside Finance. Replace it as each app is redesigned:
Pilot: CMS
The CMS mini app is the first app rebuilt entirely on the kit (features/cms). Use it as the reference when redesigning another app.
- Overview.
AppSummaryHeroanswers “what is ready to publish?” with the published share, drafts and scheduled counts, three facts and the two create actions. The three queues (needs attention, scheduled soon, archived backlog) are section headers over grouped cards ofAppRichRows and sit side by side on wide bodies throughAppAdaptiveGrid. AnAppPanelshows publish meters per collection. Tapping a queued item opens its entry. - Library. A lazy list of
AppGroupedSlices: collections first, then entries with anAppFilterBarof statuses (with counts) and anAppFilterButtonthat picks the collection. Rows open their editor; delete moved from inline icon buttons into the editor. - States. First load shows a static skeleton shaped like the overview.
A denied workspace shows an
AppStateMessage; a failed first load shows a destructive message with Retry; a failed refresh keeps the content under an inline notice. Empty queues and empty filters use inline messages. - Editors.
AppSheetScaffoldforms with captioned sections,AppTextField,AppSelectandAppToggleRow. Save is the primary action and shows progress; Delete appears for existing items and asks for confirmation in anAppDialog. The collection Enabled switch appears only when editing, because creation always enables a collection. An entry keeps its collection after creation. Drafts and inline errors survive a failed save, as before. - Status language. Every entry status has one tone and icon: draft
warning, scheduledinfo, publishedsuccess, archivedneutral.
test/features/cms/view/cms_render_test.dart renders the overview and the
library at every review size and theme with the same guidelines as the kit
gallery. Feature render tests should use the shared harness in
test/helpers/design_render_harness.dart (DesignRender,
DesignRenderApp).
Regression coverage
test/core/ui/kit/kit_components_test.dartcovers grid columns, filter and segment selection semantics, section header actions, trend colours, selected rows, slice positions, meter clamping, the hero’s wide layout and high-contrast surfaces.test/core/ui/kit/kit_gallery_test.dartrenders every component on three boards at 390x844, 820x1180 and 1440x900 in light, dark and Aurora blobs. Each render must lay out without exceptions and pass the tap-target, labelled-target and text-contrast guidelines. SetDESIGN_RENDER_DIR(and optionallyDESIGN_RENDER_PIXEL_RATIO) to write the PNGs for review.