04 — Frontend Design Guidelines
Status: populated Last updated: 2026-04-23 Scope: the platform-wide design philosophy for every Ghasi Melmastoon frontend — Consumer Meta Web, Tenant Booking Web, Consumer Mobile, Electron Desktop Backoffice, and the Control Plane. This is the cross-cutting "how we think" document; for the token shape and theming pipeline see
02-theming-and-tenant-config.md, and for the@ghasi/ui-melmastoonpackage contents (primitives, components, hooks, icons, ESLint rules) see03-design-system.md.Companions:
README·01-product-overview-frontend.md·02-architecture-overview-frontend.md·06-theming-and-tenant-config.md·03-design-system.md·desktop/21-desktop-app-specification.md·05-frontend-workflows.md·09-non-functional-requirements.md·../../01-product-overview.md·../../07-security-compliance-tenancy.md·../../08-ai-architecture.md
This document is intentionally opinionated. It states the design principles, the surface shells, the density model, the page architecture, the AI affordance rules, the performance budgets, and the rationale that bind four codebases (Next.js × 2, React Native, Electron + Vite + React) into one coherent platform UX. Anything specific to tokens, primitives, or theming pipelines lives in its companion document; this one is read by every frontend contributor on day one.
1. Design principles
The platform serves five very different personas (guest, front-desk, housekeeper, GM, owner) across two equally different audiences (the public consumer and the operator at the property) — and it does so in a market where bandwidth is scarce, electricity is intermittent, four scripts compete for typographic real estate, and tenants insist on owning their brand. The design is shaped by those facts, not by Silicon Valley defaults.
| # | Principle | Implication |
|---|---|---|
| 1 | One shared design system, four surfaces | Every primitive in @ghasi/ui-melmastoon works identically on web, native, and Electron renderer. No "operator-only" design language; no "guest-only" components — variation is a token, a density mode, or a layout preset, never a forked component. |
| 2 | All tenant variation is data, not code | Tenants pick tokens, layout presets, content blocks, navigation, booking-flow toggles, email theme — never write code. Per-tenant if (tenantId === …) branches in shared apps are an ESLint error. |
| 3 | Direction-agnostic layout | RTL is a layout attribute, not a branch. padding-inline-start, margin-block-end, inset-inline-end, text-align: start/end everywhere. <Icon name="chevron-right" /> mirrors itself on RTL via the icon manifest. |
| 4 | Offline as a first-class state | Every surface declares its offline capability up-front (full / partial / read-only / online-only). The desktop is genuinely offline-first; consumer surfaces degrade gracefully. The user is always told what works and what does not. |
| 5 | AI is explicit, auditable, reversible | No frontend ever auto-applies an AI suggestion that mutates guest-facing state (rate, refund, message send, key issuance) without a human accept. Every AI artifact carries a provenance footer (model, version, prompt, traceId, local/cloud). |
| 6 | Performance is a budget, not an aspiration | Web LCP, mobile cold-start, desktop time-to-interactive are gated by CI. Regressions block merge unless an ADR-lite note bumps the baseline with rationale. |
| 7 | Accessibility is a contract, not a checklist | WCAG 2.2 AA is the baseline platform-wide; AAA on critical-confirmation screens (booking confirmation, payment receipt, key issuance, regulatory submission). axe-clean PRs only. |
| 8 | The BFF is the only API the frontend sees | Three BFFs (bff-consumer-service, bff-tenant-booking-service, bff-backoffice-service) own the wire. Direct calls from app code to domain microservices are forbidden — they bypass tenant resolution, theming, CSP, idempotency, and audit. |
| 9 | Locale, calendar, currency are user choices, not assumptions | The platform never auto-switches calendar based on OS or location; Solar Hijri / Hijri / Gregorian is a user preference. Numerals stay Latin in the UI for finance audit reasons. |
| 10 | Reversibility everywhere | Every publish (theme, content, rate plan, configuration) has an O(1) rollback. The frontend always honors the active published version; preview surfaces are non-indexable and read-only. |
2. Surfaces & app shells
Five coordinated surfaces share one design system, one i18n bundle, one BFF client stack, and one accessibility baseline. Density, navigation, and offline affordances diverge — everything else is shared.
| Surface | Target persona | Stack | Density | Offline capability | Source location |
|---|---|---|---|---|---|
| Consumer Meta Web | Guest discovering across tenants | Next.js 14 App Router · TS strict · Tailwind · React Query · Leaflet · PWA | comfortable | PWA shell + last-seen results cache | apps/web-meta |
| Tenant Booking Web | Guest in one tenant context | Next.js 14 App Router · TS strict · Tailwind · React Query | comfortable | Bootstrap + last-seen detail cache; funnel is online-only | apps/web-tenant-booking |
| Consumer Mobile | Guest on a phone (browse + book + manage) | React Native 0.74+ (New Architecture) · Hermes · Expo · React Query · Reanimated 3 · MMKV | comfortable (thumb-first) | Read of last-known browse + reservations; writes are online-only | apps/mobile |
| Electron Desktop Backoffice | Front-desk, housekeeping, maintenance, GM, finance | Electron 30 · Vite + React (renderer) · better-sqlite3 + SQLCipher · ONNX Runtime Node · electron-builder | compact (operator-tunable) | Full offline (1–12 h target, up to 7 days grace) | apps/desktop-backoffice |
| Control Plane (Phase 2) | Platform admin, tenant onboarding, theming authoring | Next.js 14 App Router · TS strict · Tailwind | compact | Online-only | apps/control-plane |
App-shell anatomy. Every surface mounts a
ThemeProvider → DirectionProvider → I18nProvider → QueryClientProvider → ErrorBoundary → Router → AppShell. TheAppShelldiffers per surface (top-bar + drawer for consumer; sidebar + workspace-switcher for desktop) but the provider chain is identical. TheAppShellis the only place permitted to render the Sync Status Pill (desktop), the Offline Banner (cross-cutting), and the Tenant Identity Bar (tenant booking + desktop).
3. Frontend clean architecture (per surface)
The shared layout below is enforced by eslint-plugin-boundaries. It is the same on web, native, and the Electron renderer; only the adapter implementations differ. Apps depend on packages; packages depend only on packages with a strictly lower position in the layered DAG.
/app — Next.js route segments (web) | screens/ (mobile) | views/ (desktop renderer)
/components — Presentational components composed from @ghasi/ui-melmastoon primitives
/components/server — Server components (RSC, web only)
/hooks — Orchestration: typed BFF clients wrapped in React Query / Zustand selectors
/services — API ports (typed BFF clients per BFF) — never raw fetch in components
/state — Zustand stores per domain (booking-draft, sync-status, filter-panel, …)
/lib/domain — Pure TS primitives — Money (BigInt micro-units), FXSnapshot, ULID, Locale, AIProvenance
/lib/adapters — Service worker (web) · MMKV (mobile) · better-sqlite3 IPC bridge (desktop)
/i18n — ICU messages per locale (next-intl-compatible bundles from @ghasi/i18n)
/styles — Tailwind preset import; global base; print stylesheet (where applicable)
/types — Shared TS types mirroring BFF DTOs; generated from OpenAPI in @ghasi/api-clients
Rules:
lib/domainhas no React, no Next.js, no fetch, no React Native imports. Pure TypeScript.services/defines ports;lib/adapters/supplies concrete implementations bound at bootstrap.- Components depend on
hooks/+services/only. Never directfetchin a component. - BFF DTOs are imported from
@ghasi/api-clients— no ad-hoc shapes. app/(orscreens//views/) is the only layer permitted to depend on routing.- The Electron renderer has no Node access; it speaks to the Electron main through the typed
window.melmastooncontextBridgesurface only (seedesktop/06-desktop-app-specification.md§3).
4. Density model
A single data-density="cozy|comfortable|compact" attribute on the root element switches token multipliers (row height, font base, button padding). This is the only legitimate way to vary spacing per surface.
| Mode | Default for | Row height | Font base | Button padding-inline |
|---|---|---|---|---|
comfortable | Consumer surfaces (meta web, tenant booking web, mobile) | 48 px | --text-base (16 px) | --space-5 |
compact | Electron desktop backoffice, control plane | 32 px | --text-sm (14 px) | --space-3 |
cozy | Operator override on desktop (large monitors, busy shifts) | 56 px | --text-base | --space-6 |
Operators can flip density per device on the desktop; preference is synced to the operator profile via bff-backoffice-service (not per-session). Guest surfaces are never offered a density toggle — they always render comfortable. Density is not a tenant token — tenants do not get to choose it; that decision is platform-wide.
5. Page architecture
5.1 Layout shells
| Shell | Used for | Composition |
|---|---|---|
| App shell | Default app surfaces (meta home, tenant booking home, desktop home, mobile main tabs) | Top bar / sidebar + nav + content + secondary rail (where relevant) + sync pill (desktop) |
| Funnel shell | Booking funnel steps, multi-step authoring flows (theme publish, onboarding) | Stripped chrome · progress indicator · step heading · step body · primary action footer · "Save and exit" affordance |
| Detail shell | Property detail, reservation detail, room detail | Hero region · breadcrumb · tabbed body · sticky action bar (mobile/desktop) |
| Print shell | Cash-drawer report, regulatory submission, folio receipt, tax export | Stripped theme · serif typography · 11 pt base · monochrome unless semantic color carries meaning · A4 + Letter |
| Authoring shell | Theme editor (control plane), preview pages | Live preview iframe + side panel of token controls + save/preview/publish bar pinned bottom |
| Modal shell | Critical confirmations (refund, override, break-glass) | Focus trap · ESC closes · destructive actions require typing the verb (REFUND, OVERRIDE) |
5.2 Loading
- Route-level Suspense + skeletons. Skeletons mirror the post-load layout — never spinners over already-rendered content; never spinners as the only loading affordance. The desktop additionally cross-fades when transitioning between cached SQLite reads and fresh server reads (no flash).
- Network awareness. Long requests (> 2 s) escalate from skeleton → "Still working…" + Cancel. Long outboxes (> 30 s) on the desktop surface a sync pill animation and an "Open Sync Center" link.
- Reduced-motion fallback. Skeletons collapse from shimmer to a static low-contrast tone when
prefers-reduced-motionis set.
5.3 Empty states
Every list, every grid, every queue has an opinionated empty state — copy + illustration + one primary next action — localized to the tenant's likely context.
| Surface | Example |
|---|---|
| Meta search "no results" | "We're not in this city yet — tell us where to launch next" + waitlist email capture (per J-01). |
| Wishlist (anonymous, empty) | "Save properties to compare them side-by-side" + jump to search. |
| Front-desk arrivals (zero today) | "No arrivals today. Walk-in capture is one shortcut away." + Walk-In CTA. |
| Housekeeping queue (zero tasks) | "All rooms are at status. AI would suggest a deep-clean for Room 204 — see suggestions." (links to the AI inbox). |
5.4 Error states
problem+jsoncodes are mapped to user-friendly messages with a recovery affordance (retry, contact, escalate, open sync center). Never raw stack. Never leak PHI / PII / financial detail into error copy.- Validation errors are inline,
aria-describedby-tied, and surface the stable error code in a small monospace badge for support escalation. - ABAC denials show a neutral message and (where applicable) invite a documented break-glass path with a reason code.
- Audit-write failures roll back the mutation entirely and surface "Try again" — the user must know the action did not land.
- Sync-conflict errors on the desktop never auto-resolve; they open the Sync Center with a side-by-side diff (see
desktop/06-desktop-app-specification.md§6 and W-12 in05-frontend-workflows.md).
5.5 Notifications
| Channel | Use | Component |
|---|---|---|
| Toast | Ephemeral confirmations and non-blocking warnings | <Toast /> from @ghasi/ui-melmastoon — auto-dismiss 5 s default; never used for destructive confirmations. |
| Banner | Persistent state ("You're offline", "Preview mode", "Hold expires in 0:42") | <AlertBanner /> / <OfflineBanner /> — sticks under the app bar; respects safe-area insets on mobile. |
| In-app inbox | Persistent, user-actionable items (AI suggestions, regulatory pending submissions, refund approvals, sync conflicts) | <NotificationInbox /> on desktop sidebar; pull-to-refresh on mobile Manage tab. |
| Outbound (SMS/WhatsApp/email/push) | Owned by notification-service per the templates in docs/04-event-driven-architecture.md. The frontend never composes outbound text directly; tenant-customizable templates flow through EmailTheme + LocalePack. |
PII (full name, email, phone, document number, payment card) is never sent to telemetry or push payloads — templates are generic; detail is read only inside an authenticated surface.
6. Layout grid
| Breakpoint | Columns | Gutter | Side rails |
|---|---|---|---|
| Phone (< 768 px) | 4 | 16 px | bottom tab nav, drawer for secondary |
| Tablet (768 – 1023 px) | 8 | 16 px | single primary + drawer left rail |
| Laptop (1024 – 1279 px) | 12 | 16 px | right rail collapses to a tab strip |
| Desktop (≥ 1280 px) | 12 | 24 px | 80 px side rails on consumer; full sidebar on backoffice |
| Print (A4 / Letter) | n/a | n/a | stripped theme; 11 pt serif; monochrome unless semantic color carries meaning |
Desktop backoffice layout. Three regions: 240 px left navigation (workspace switcher + module list), fluid center (active module — arrivals, in-house, departures, housekeeping queue, finance), 320 px right rail (notifications inbox, AI suggestions, sync status detail). The right rail collapses to a tab strip below 1280 px.
7. Iconography
- Primary set: Phosphor Icons (line + filled) —
@ghasi/ui-melmastoon/iconsre-exports the curated subset. - Hospitality extension:
room,key,mobile-key,rfid-card,bed,family-room,halal,prayer-area,women-only-floor,generator,hot-water,parking,breakfast,airport-shuttle,pet-friendly,non-smoking,accessible. - Operational extension:
clean,dirty,out-of-order,out-of-service,inspecting,maintenance-pending. - AI extension:
ai-suggestion,ai-local,ai-cloud,ai-needs-review. - Format: SVG only;
currentColor-driven; web renders inline SVG; native uses pre-built RN components compiled from the same SVG sources at build time (no runtime SVG cost on native). - Direction: every icon declares
mirrorOnRtl: booleanin the manifest. Chevrons, arrows, undo/redo flip; brand glyphs, media controls (play/pause), numerals, and hospitality glyphs (halal,prayer-area) never flip. - Pairing rule: every status pairs icon + color + short text label. Icons alone are never the carrier of meaning (colorblind / low-contrast).
8. Accessibility baseline
Target WCAG 2.2 Level AA platform-wide; aspire AAA on critical-confirmation screens (booking confirmation, payment receipt, key issuance, refund approval, regulatory submission).
| Concern | Rule |
|---|---|
| Contrast | ≥ 4.5:1 body, ≥ 3:1 large text and UI components. Token contrast invariants are validated at publish in theme-config-service (02-theming-and-tenant-config.md §2.1) and on every PR in @ghasi/ui-melmastoon CI. |
| Keyboard | 100% of flows reachable via keyboard; visible focus ring via --shadow-focus; Esc closes any dialog; route changes move focus to the <h1 tabIndex={-1}>. |
| Screen reader | Semantic HTML first; ARIA only where semantics are not natively expressible; role="alert" for live banner updates (sync state, hold expiry, payment failure). |
| Reduced motion | All motion gated on @media (prefers-reduced-motion) (web) / useReducedMotion() (native + desktop). Spinners and skeletons collapse to opacity fades. |
| Target size | Minimum 44 × 44 pt mobile, 32 × 32 px web/desktop (WCAG 2.5.5). Linted on Button, IconButton, Pressable. |
| Language | lang attr per passage; bilingual strings (Pashto + Latin transliteration) wrapped in <bdi>/<BidiText>. |
| Form errors | aria-describedby to message; programmatic label on every input; error summary at top of form on submit. |
| Live regions | Hold expiry, payment status changes, sync conflicts announced via role="alert" / useAnnounce(). |
| Zoom | 200% zoom + 320 px reflow without loss of function on web. |
| Media | Captions on all educational video; transcripts downloadable. |
| Confirmation surfaces | Booking confirmation, payment receipt, key issuance, refund approval, regulatory submission — readable by screen reader first before chrome; presented in user's locale; printable/exportable. |
Automated gate: axe-playwright (web) + Detox a11y pass (mobile) + axe-storybook on every PR. Violations at serious or critical block merge. Manual NVDA + VoiceOver + TalkBack review per release.
9. Internationalization design philosophy
The wire details (locale fallback chain, ICU plural rules, bidi mark insertion) live in
01-web-and-mobile-specification.md§9. This section captures the design decisions behind those choices.
| Decision | Rationale |
|---|---|
| Latin numerals locked in UI for prices, totals, IDs, counts, dates in finance contexts | Finance staff audit folios and tax reports across locales; Persian-Indic numerals in monetary fields cause real misreads in cash drawer reconciliation. Narrative text may opt into Persian-Indic via locale pack — never prices. |
| Calendar is a user choice, never an OS / location guess | A Pashtun guest may prefer Solar Hijri presentation; a regulator export must be Gregorian; a Hijri religious holiday calendar may overlay both. The user picks the display calendar; storage is always ISO-8601 Gregorian. |
| Direction follows locale, not user toggle | Direction is a property of the locale, not a preference. Switching locale switches direction; switching direction independently of locale is not offered. |
| Bilingual capture is a first-class form pattern | Guest names are captured in two paired inputs — native script (Pashto/Dari/Persian/Arabic/Urdu) + Latin transliteration. The Latin field auto-suggests via ai-orchestrator-service.transliterateName with HITL accept/edit. Both scripts are persisted; the platform never silently picks one. |
| Day-one languages, not phased | Pashto, Dari, Persian, Arabic, English are all day-one. French, Urdu, Russian are Phase 2. We do not ship "English first, then localize" — RTL/LTR parity is engineered, not retrofitted. |
| Tenant-default locale, platform fallback | Per-tenant defaultLocale (e.g., ps-AF) is honored on cold visits when the requested locale is unavailable; final fallback is platform en-US. Missing translations never render the raw key — fallbacks always end at a real string. |
10. AI affordances (UX rules)
Ghasi Melmastoon positions AI as explicit, auditable assistance — not autonomous action. These rules apply to every AI-touched surface across every frontend; the underlying provenance + HITL contract lives in docs/08-ai-architecture.md.
| Rule | Rationale |
|---|---|
| Explicit trigger | No AI action without a user click or a tenant-published auto-policy. Frontend never auto-populates guest-facing fields silently. |
| Provenance badge | Every AI artifact renders a footer: `model · version · prompt · traceId · local |
| HITL signature on irreversible actions | Rate change, refund, message send, key issuance, regulatory submission require an explicit accept; accept = counter-signature recorded with reviewedBy + reviewedAt on AIProvenance. |
| Streaming UX with a Cancel | Long generations stream token-by-token with a visible Cancel button. Mobile streams via Reanimated worklets to keep the JS thread free. |
| Refusal UX | Neutral, non-judgmental message; user can rephrase. Never present refusal as a system error. |
| Local-vs-cloud indicator | Edge-ONNX (desktop) inferences carry an ai-local icon and a "Local model — limited accuracy" tooltip. Operators must know when they are getting cloud-grade vs degraded inference. |
| Cost visibility | Admin / GM views show per-tenant AI usage and budget consumption (cumulative spend, rate limit headroom). |
| Acceptance pattern | AI suggestions render with a subtle dashed border until accepted; accepted suggestions become solid and merge into the host record. Dismiss = recorded as dismissedReason for AI-eval feedback loops. |
| Explainability affordance | Every <AISuggestionCard /> exposes a "Why this?" link that opens a drawer with the input features, prompt template version, and (where available) a model-emitted rationale. |
| No PII in prompts unless needed | The frontend never includes PII in prompts unless the tenant has consented for that surface (e.g., transliteration). The orchestrator strips PII at the edge before logging. |
11. Performance budgets
Enforced in CI; regressions block merge unless an ADR-lite note in the PR bumps the baseline with rationale.
F6 — Single source of truth for perf budgets: All measurable perf targets (LCP, INP, CLS, TTFB, cold-start, RAM, bundle size, sync round-trip) are defined and maintained in
09-non-functional-requirements.md§1. This section intentionally does not duplicate those numbers — do not add per-metric tables here. Engineers and CI gates must link to09-NFR.md §1. Per-surface budget summaries are in09-NFR.md §§1.1-1.3.
11.5 Cross-cutting techniques
- Route-level code-split; dynamic import of heavy editors (rich-text, image cropper, signature pad).
- Explicit
width/heighton hero media; LQIP placeholders. - Preload only critical font weights; web font payload ≤ 2 weights × 2 styles per locale; subsetting required.
- No third-party scripts in tenant booking funnel or backoffice operational shells.
- React Server Components default for non-interactive subtrees on web.
- React Native: New Architecture (Fabric + TurboModules); off-thread animations via Reanimated 3;
@shopify/flash-listfor long lists. - Electron: utility processes for AI inference (ONNX Runtime Node) + sync engine; renderer stays pure UI.
12. State management conventions
| Layer | Owner | Persistence | Examples |
|---|---|---|---|
| Server state | TanStack Query (React Query v5) | In-memory + IndexedDB (web) / MMKV (mobile) / better-sqlite3 (desktop) | searchHotels, getHotel, getBootstrap, getQuote, getReservation, getArrivalsToday |
| Client UI state | Zustand (per-domain stores) | IndexedDB (web) / MMKV (mobile) / better-sqlite3 KV (desktop) for booking-draft and sync-status; in-memory for ephemera | Booking-draft, filter-panel-open, map-vs-list, sync-status, AI-suggestion-inbox |
| URL state | nuqs (web) / React Navigation params (mobile) / react-router search params (desktop renderer) | URL itself | Search filters, sort, pagination, active tab, selected reservation, date range |
| Form state | React Hook Form + Zod | Per-form scoped | Guest details, walk-in capture, refund approval, theme authoring |
| Session state | BFF + cookie | Server-side (Memorystore) keyed by gms_id (consumer) / tnt_id (tenant) / OS keychain (desktop) | Locale, currency, wishlist (cookie pre-auth → account post-auth, merge on login), recently viewed |
Rules:
- No global Redux store. Zustand stores are domain-scoped; no monolith.
- URL state is canonical for filters, sort, pagination, active tab, selected entity. Deep-linking and copy-paste must work everywhere.
- Optimistic mutations are allowed only when the BFF returns an idempotency-keyed result the optimistic update can reconcile against; otherwise wait for server confirmation. The desktop additionally writes to the local outbox before the optimistic UI update — the local DB is the source of truth offline.
- Cache invalidation mirrors the BFF's
Cache-Controldirectives;staleTimeandgcTimeare explicit per query key.
13. Cross-surface conventions
These are the small consistencies that keep four codebases feeling like one product.
| Concern | Convention |
|---|---|
| Identity bar | Tenant-scoped surfaces (tenant booking, desktop) always show the active tenant identity in the top-left (logo + name); switching tenants on desktop pops a confirmation modal and unmounts in-progress flows. |
| Sync Status Pill (desktop only) | Always visible top-right: `online |
| Connection state pill (mobile) | When connectivity drops, a slim banner slides in under the safe-area top inset; never a full-screen modal. |
| Date entry | BookingDateRangePicker on consumer; DatePicker on operator. Calendar system follows user preference; emitted value is always ISO-8601 Gregorian. |
| Currency display | <PricingDisplay /> always renders the BFF-formatted string and surfaces isStale chips when the FX snapshot has expired. The frontend never re-formats prices client-side except via useFormatCurrency() (rare, debug). |
| Confirmation pattern | Destructive irreversible actions (refund, override, break-glass, theme rollback) require typing the verb in a confirmation modal (REFUND, OVERRIDE, ROLLBACK). |
| Save vs Apply vs Publish | "Save" = persists to draft; "Apply" = activates for current session/preview; "Publish" = creates a new immutable version visible to users. The verb in a button always means one of these three things and nothing else. |
| Audit visibility | Every state-changing operator action shows a "View audit" link in the result toast for 10 s; the link opens the action's row in the audit log timeline. |
| Help affordance | Every funnel step has a contextual help link (right-rail on desktop, bottom-sheet on mobile) — never modal-blocking. |
| Time formatting | Relative (2 minutes ago) for ≤ 24 h; absolute thereafter; tooltip always shows full ISO-8601 in user's calendar + UTC. |
14. Print, email, and exports
The platform produces operator and guest artifacts that travel outside the app. Their design is governed here so they remain consistent.
| Artifact | Surface | Constraints |
|---|---|---|
| Booking confirmation PDF (multilingual) | Tenant booking | A4 + Letter; tenant logo + theme; primary locale + EN; QR code with reservation code; cash-on-arrival voucher block when applicable. |
| Folio receipt | Desktop print shell | A4; itemized in BigInt-rendered amounts; FX-snapshot footer; tenant invoice number. |
| Cash drawer report | Desktop print shell (J-12) | A4; daily totals; variance highlighted; signed by closer + supervisor. |
| Regulatory submission | Desktop export (J-17, J-18) | PDF + XLSX + CSV per regulator schema; locale-specific date format; deterministic column order; signed receipt persisted. |
| Transactional emails | notification-service | MJML templates rendered with EmailTheme (02-theming-and-tenant-config.md §8); email-safe font stacks only; logo ≤ 1 MiB; locale-aware footer (address + legal). |
| Push payloads | Mobile (expo-notifications) | No PII; deeplinks via Universal Links / App Links; channel-categorized; deferred opt-in (asked at first booking). |
15. Testing & visual regression (design surface)
Per-surface stack details live in 01-web-and-mobile-specification.md §16. This section captures the design-surface testing conventions that bind the surfaces together.
| Concern | Tool | Coverage rule |
|---|---|---|
| Component tests | Vitest (web/desktop) + Jest (mobile) + Testing Library | Every primitive ships with stories and tests; behavior-based assertions, not snapshots. |
| Visual regression | Chromatic (Storybook web) + Loki (mobile) | Per (theme, locale, dir) matrix: 2 themes × 4 locales (en, ps-AF, ar-SA, fa-IR) × 2 directions × 3 densities. Failures require approver sign-off. |
| Accessibility | axe-playwright + axe-storybook + Detox a11y | Every story; every E2E run; serious/critical violations block merge. |
| RTL parity | Playwright (web) screenshots + Detox + ScreenShoter (mobile) | Every primary screen in dir=rtl. |
| Pseudo-locale | en-XA rendered for every PR | Truncation + RTL flips checked. |
| Bundle size | size-limit (per package) + Lighthouse-CI (per route) | Per §11 budgets. |
| Hydration mismatch | React strict + Playwright assertion | console.error empty on first paint. |
| Density modes | Storybook stories per primitive in cozy/comfortable/compact | Visual regression snapshots per density on critical components. |
16. Anti-patterns (design surface)
These echo the per-document anti-patterns in 01, 02, and 03, distilled to the design philosophy here.
| Anti-pattern | Why banned | Correct approach |
|---|---|---|
Tenant-conditional code in shared apps (if (tenantId === …) { … }) | Tenant-coupled code prevents safe deploys and contradicts "all tenant variation is data" | Drive variation through theme-config-service (tokens, presets, content blocks, flow toggles) |
OS-driven theme switching (prefers-color-scheme for tenant brand) | Conflicts with tenant brand intent; not all tenants ship dark mode | Tenant-published light/dark/high-contrast bundles only |
| Auto-applying AI suggestions to guest-facing state | Defeats HITL; trust loss | Suggestions render dashed; explicit accept counter-signs |
| Spinners as the only loading state | Reduced-motion users see nothing; long waits feel hung | Pair with skeleton + escalation copy + Cancel after 2 s |
| Color alone as status carrier | Colorblind / low-contrast users miss meaning | Pair icon + color + text label always |
| Bilingual capture in a single input | Forces user into one script; transliteration ambiguity | Two paired inputs with HITL transliteration suggestion |
| OS calendar / location-driven calendar swap | User context ≠ device context (regulator export needs Gregorian regardless of UI choice) | User picks display calendar; storage is ISO-8601 Gregorian |
| Density toggle on guest surfaces | Adds load + UX confusion to non-operator personas | Density is operator-only |
| Hardcoded English copy / aria-labels | Breaks i18n + RTL audits | Always useTranslations / t() from @ghasi/i18n |
| Direct domain-service calls from app code | Bypasses tenant resolution, theming, CSP, rate limit, audit | Always via the appropriate BFF (@ghasi/api-clients/<bff>) |
Mutating melmastoonDefaultTokens at runtime | Breaks identity stability + memoization assumptions | Override via tenant theme; never mutate the default object |
| Embedding tenant assets in the bundle | Defeats CDN; per-tenant invalidation impossible | Reference MediaRef URLs; never bundled |
Per-tenant React component files (KabulGrandHero.tsx) | Doesn't scale; per-tenant deploys | Compose from primitives + content blocks |
| Skipping the contrast / preset validation locally because "publish will catch it" | Slows feedback; surfaces broken builds in shared environments | Run validators in pre-commit hooks (provided by @ghasi/ui-melmastoon) |
| OTA-pushing native code on mobile | Breaks store policies | Native changes go through EAS Build + store review |
| Operator action without "View audit" link | Operator loses recourse to verify what fired | Every state-changing toast surfaces audit link for 10 s |
17. Rationale
A single token set + component system + density model lets one design language serve a guest browsing on a $80 Android phone in Kabul, a clerk on a 1024×768 monitor at the front desk during a power outage, and a chain GM on a 27" 4K display reviewing portfolio dashboards. Direction-agnostic CSS makes RTL a layout attribute, not a branch — the same React tree serves Pashto and English without forks. Density modes let the same primitives serve a thumb-first booking funnel and a dense operator arrivals grid. Offline, AI, and accessibility are first-class because in our reference markets they are the difference between a platform that works during a 6-hour blackout in February and one that does not.
The theme-config-service-driven tenant variation surface is the most important architectural commitment in the frontend slice: it lets dozens of tenants ship distinct brands on one binary while keeping per-tenant code at zero. The design system enforces this with semantic tokens (no raw hex), logical CSS (no left/right), iconography manifest (RTL-aware mirroring), and ESLint rules (block forks before they merge). Performance budgets are gated in CI because in a low-bandwidth market, "feels fast" is the difference between booking and abandonment — we cannot ship a regression and audit it later.
The desktop is genuinely offline-first because the property cannot stop operating when the link drops; the consumer surfaces are gracefully degrading because guests browsing on patchy 3G need the app to feel alive, not error-y. The Electron renderer reuses the same @ghasi/ui-melmastoon primitives as web and mobile so an operator who learns one surface knows the others — and so the design system itself stays exercised across four very different runtime contexts.
18. Open questions
- Whether the Control Plane (Phase 2) ships as a fourth Next.js app or as a privileged sub-route of
apps/web-tenant-bookingbehind a platform-admin scope. Trade: deployment isolation vs shared bootstrap surface. - Whether the desktop should expose a TV-mode arrivals grid (kiosk display in the lobby) as a stripped variant of the operator arrivals view, or whether that surface gets a separate read-only Electron build.
- Final shape of the AI explainability drawer — model-emitted rationale vs feature-attribution heatmap vs prompt-template inspector — pending AI-eval signal from R2.
- Whether to ship a chain operator portfolio shell (Phase 2) as an additional density mode (
portfolio— multi-property tile grid) or as its own layout shell. - Whether to add a guest sign-in identity bar on the meta web (today anonymous-first) once Phase 2 personalization lands.
19. References
- Web/mobile spec (stack, routing, state, offline, security):
01-web-and-mobile-specification.md - Theming domain & token model:
02-theming-and-tenant-config.md,services/theme-config-service/DOMAIN_MODEL.md - Design system package (
@ghasi/ui-melmastoon):03-design-system.md - Frontend workflows (state diagrams, role variants, offline fallbacks):
05-frontend-workflows.md - Desktop spec (Electron offline-first backoffice):
desktop/06-desktop-app-specification.md - Core user journeys (J-01 … J-22):
docs/journeys/01-core-user-journeys.md - Epics & user stories (EP-MEL-01 … EP-MEL-20):
docs/07-epics-and-user-stories.md - AI architecture (provenance, HITL, edge inference):
docs/08-ai-architecture.md - Security, compliance, multi-tenancy:
docs/07-security-compliance-tenancy.md - Testing strategy:
docs/11-testing-strategy-qa.md