StoreManagement

Tablet-first layout

The tablet is the device this runs on. The phone has to work; the tablet has to be good.

Three layouts, chosen by width

Not by device type. A half-width desktop window is a tablet as far as the layout is concerned, and treating it as one means there’s no such thing as a size that falls between the cases.

WidthLayoutTypical device
≥ 1000Rail + section list + page, all visibleiPad landscape, desktop
≥ 620Rail + page; sections in a draweriPad portrait
< 620Page + bottom bar; sections in a drawerPhone
>= 1000 — iPad landscape, desktop ┌───────────┬────────────────┬──────────────────────────┐ │ DailyCash │ ENTRY │ Daily close Export │ │ StockRoom │ Daily close │ │ │ BookKeeper│ Expenses │ +--------+ +--------+ │ │ │ Receivables │ | TOTAL | | CASH | │ │ Theme │ REPORTS │ +--------+ +--------+ │ │ Sign out │ Monthly │ │ └───────────┴────────────────┴──────────────────────────┘ rail sections page >= 620 — iPad portrait < 620 — phone ┌──────┬────────────────┐ ┌────────────────────────┐ │ menu │ DailyCash │ │ menu DailyCash │ ├──────┼────────────────┤ ├────────────────────────┤ │ Cash │ Daily close │ │ Daily close │ │ Stock│ │ │ +------------------+ │ │ Books│ +----------+ │ │ | TOTAL | │ │ │ | TOTAL | │ │ +------------------+ │ │ │ +----------+ │ │ | CASH | │ │ │ │ │ +------------------+ │ └──────┴────────────────┘ ├────────────────────────┤ │ Cash Stock Books │ └────────────────────────┘

The rail is the point

As separate apps, moving from the day’s cash to the stock it implies meant going back to a portal and loading another application. The rail carries all three features at every width — a bottom bar on a phone, a vertical rail above that — so switching is one tap from anywhere. It replaced the launcher screen outright: there is nowhere to go “back” to.

The pane must tell the truth about its width

The bug this prevents

isNarrow(context) reads MediaQuery, which describes the window. On an 820pt iPad the rail takes about 80 and the section list 236, leaving roughly 500 for the page — but the page would still read 820, decide it had room for three columns, and overflow.

Rather than change every page, the shell wraps the content in a MediaQuery describing the pane. Every existing responsive helper then works untouched, because the number they read is finally the number that matters.

app/test/shell_layout_test.dart
testWidgets('pane width excludes the rail and the section list',
    (tester) async {
  await _pump(tester, _tabletLandscape, '/cash/entry/daily');

  final pane = _paneWidth(tester);
  expect(pane, lessThan(_tabletLandscape.width),
      reason: 'the rail and section list take real width');
  // Rail (~80) + list (236) + dividers. Anything close to the full 1180
  // means a page would lay out columns that do not fit.
  expect(pane, lessThan(_tabletLandscape.width - 300));
});

What pages get for free

mblrc_shared/responsive.dart holds the pieces that respond to the pane width, so individual pages contain almost no layout branching:

WidgetWideNarrow
PageHeaderTitle left, actions rightActions wrap to their own line rather than squeezing the title away
FieldRowFields side by sideStacked — three peso fields on one line are unreadable
StatCardsOne card per figureA single card, headline figure first, the rest as rows
HorizontalTableTable as-isScrolls sideways at a legible minimum width instead of crushing columns

Checking it without a database

The layouts are covered by widget tests at real device sizes — iPhone 390×844, iPad 820×1180 and 1180×820 — and a RenderFlex overflow fails a widget test, so anything that doesn’t fit is caught before it ships.

For looking at it rather than asserting on it, there’s a throwaway entrypoint that mounts the shell with stub pages and needs no Supabase credentials:

flutter build web -t tool/shell_preview.dart -o build/preview