Skip to content

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, SemanticButton family (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)
  • Sheet primitive (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 via useBreakpoint() 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) — CacheFirst with a long max-age.
  • API requests — network-only by default; opt-in NetworkFirst for 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 the manufacturing module ships. See docs/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.