Responsive & PWA Strategy¶
Single source of truth for how the GrinSystem frontend behaves across device classes. Every other FE doc cross-links here; every module plan inherits its policy from this document.
GrinSystem is a desktop-first ERP — the primary surface for all day-to-day operations is a laptop or desktop browser. Phone is a deliberate secondary surface for owners' ad-hoc workflows; tablet is a free-ride at the md breakpoint and above. The factory floor kiosk (/floor/*) is a separate, deferred surface with its own shell.
1. Product surfaces¶
| Surface | Role | What it must do well |
|---|---|---|
| Desktop | Primary | Every operation in the app — list browse, complex forms, table-heavy admin, bulk operations, drag-and-drop builders, side-by-side comparisons. Assumed minimum lg (1024px+); planned for xl (1280px+). |
| Phone | Secondary | Owners' ad-hoc workflows on the go: invoice / order quick-create (≤ 8 fields), dashboard glance, list browse (cards), search, single-record detail. Heavy admin work is reachable but slower. |
| Tablet | Free-ride | At md (768px) and above every screen renders with full feature parity as a side effect of building the phone + desktop variants well. No tablet-specific design work — if the phone variant works at <sm and desktop at ≥lg, tablet is handled. |
Factory /floor/* kiosk |
Deferred | Mounted on a wall-tablet on the shop floor. Different shell (FloorLayout — bottom action bar, no sidebar, big touch targets). Ships when the manufacturing module lands. Not foundation work. |
Native mobile apps are explicitly not on the roadmap — PWA covers the owner-on-the-go use case at a fraction of the maintenance cost.
2. Breakpoint tokens¶
Tailwind v4 defaults — no custom values. Use the prefix names directly in component code.
| Token | Min width | Typical device |
|---|---|---|
sm |
640px | Large phone landscape, small tablet portrait |
md |
768px | Tablet portrait — feature-parity threshold |
lg |
1024px | Small laptop, tablet landscape |
xl |
1280px | Laptop, primary desktop assumption |
2xl |
1536px | Large desktop / external monitor |
Two breakpoints carry policy weight:
<sm(≤ 639px) — the phone bucket. Soft-gates apply here, the DataTable swaps to DataCardList, the FilterBar collapses into a Sheet, dialogs go fullscreen.≥md(768px+) — the feature-parity floor. Every screen renders the same controls and data it would on desktop; above this we only adjust layout, never functionality.
lg and xl exist for chrome adjustments (sidebar rail vs drawer, multi-column detail layouts) but never gate features.
3. Three-bucket per-screen rubric¶
Every page declares which bucket it falls into. The bucket lives in the page's pages.md row and in the implementing Linear ticket's acceptance criteria.
| Bucket | Definition | Examples |
|---|---|---|
| Excellent on phone | First-class on <sm. Tested for finger ergonomics, scroll length, and one-handed reach. Same data, same actions as desktop — only the layout adapts. |
Read views, dashboards, list pages (rendered as cards), search, single-record detail, quick-create forms with ≤ 8 fields. |
| Workable on phone | Functions on <sm, but slower and more scroll-heavy than desktop. Not optimised — owners can complete the task if they have to, but they wouldn't choose to. |
Long forms (9–30 fields), table-heavy admin pages with many columns, bulk operations that benefit from a wider canvas. |
| Soft-gated on phone | Renders <DesktopOnly> at <sm — a polite card saying "this screen works better on a larger device" with a "continue anyway" link. Continuing degrades, doesn't break. |
Drag-and-drop builders, side-by-side comparisons, any screen that needs ≥ 2 columns of structured content to make sense, complex grids with ≥ 6 simultaneously-visible action columns. |
The bucket affects implementation but not the data layer — the same Orval hooks, Zod schemas, and mutation hooks back every variant.
4. Same-component rule¶
Wherever possible, one component renders responsively across every breakpoint. We only fork into paired components when the information density is so different that one component can't honestly serve both surfaces.
Single-component, responsive via conditional styling:
Button,Input,SemanticButtonfamily (SaveButton,CancelButton, …)PageHeader(actions wrap to a second row at<md; overflow···menu when actions exceed threshold)Sidebar(drawer at<lg, rail-or-expanded on desktop)Sheetprimitive (used as-is everywhere)
Paired components — page picks at runtime:
DataTable(≥md) ↔DataCardList(<md). Never try to render a 20-column table as cards from one component; it lies to both surfaces. Pages pick viauseBreakpoint()and pass the same column / field definitions to both.FilterBar(inline at≥ md) ↔FilterBar-in-Sheet(drawer at< md). Same controls, different container.Dialog(modal at≥ sm) ↔Dialog-as-Sheet(fullscreen at< sm). Same content, different shell — handled inside the Dialog primitive itself.
Full per-component contract lives in docs/architecture.md § Responsive contracts. Visual specs (breakpoint values, touch targets, tooltip behaviour) live in docs/design-system.md § Responsive design.
5. PWA scope¶
The app installs as a Progressive Web App. PWA support is wired in the same milestone as the responsive policy; the install prompt and service worker land alongside the responsive primitives so phone owners can pin GrinSystem to their home screen on day one.
Installable surfaces:
- iOS Safari — Add to Home Screen.
- Android Chrome — install prompt + standalone display mode.
- Desktop Chrome / Edge — installable as a standalone window.
Caching strategy: Workbox-driven, scoped per route class:
- App shell (
index.html, JS/CSS bundles) —StaleWhileRevalidate. Service worker updates push a "new version available — reload" toast. - Static assets (icons, fonts) —
CacheFirstwith a long max-age. - API requests — network-only by default; opt-in
NetworkFirstfor read-only summary endpoints (e.g./stats) so glance dashboards still resolve offline. Writes (POST/PUT/PATCH/DELETE) are never queued — they fail loud, the user retries when online.
Manifest: name + short name + theme colour + icon set (192×192, 512×512 maskable). Icons rendered from the same logo mark used in the sidebar.
Install prompt UX: the browser-native prompt fires once per session per device, gated on a small beforeinstallprompt listener; the user can dismiss permanently from settings.
6. What we explicitly defer¶
- Factory
/floor/*kiosk app. Separate route tree, separate shell (FloorLayout), shares the data layer with the main app. Built when themanufacturingmodule ships. Seedocs/architecture.md§/floor/*placeholder. - Native iOS / Android apps. The PWA covers the owner-on-the-go use case. Native apps are not on the roadmap.
- Offline-first writes. Mutations fail loud when offline; we do not queue background syncs. Re-evaluate when there's an actual field-use complaint.
- Push notifications. Not in scope. Owners get notifications from BE-side email / SMS until there is a use case that demands a browser push.