Skip to main content

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-melmastoon package contents (primitives, components, hooks, icons, ESLint rules) see 03-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.

#PrincipleImplication
1One shared design system, four surfacesEvery 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.
2All tenant variation is data, not codeTenants 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.
3Direction-agnostic layoutRTL 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.
4Offline as a first-class stateEvery 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.
5AI is explicit, auditable, reversibleNo 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).
6Performance is a budget, not an aspirationWeb 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.
7Accessibility is a contract, not a checklistWCAG 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.
8The BFF is the only API the frontend seesThree 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.
9Locale, calendar, currency are user choices, not assumptionsThe 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.
10Reversibility everywhereEvery 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.

SurfaceTarget personaStackDensityOffline capabilitySource location
Consumer Meta WebGuest discovering across tenantsNext.js 14 App Router · TS strict · Tailwind · React Query · Leaflet · PWAcomfortablePWA shell + last-seen results cacheapps/web-meta
Tenant Booking WebGuest in one tenant contextNext.js 14 App Router · TS strict · Tailwind · React QuerycomfortableBootstrap + last-seen detail cache; funnel is online-onlyapps/web-tenant-booking
Consumer MobileGuest on a phone (browse + book + manage)React Native 0.74+ (New Architecture) · Hermes · Expo · React Query · Reanimated 3 · MMKVcomfortable (thumb-first)Read of last-known browse + reservations; writes are online-onlyapps/mobile
Electron Desktop BackofficeFront-desk, housekeeping, maintenance, GM, financeElectron 30 · Vite + React (renderer) · better-sqlite3 + SQLCipher · ONNX Runtime Node · electron-buildercompact (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 authoringNext.js 14 App Router · TS strict · TailwindcompactOnline-onlyapps/control-plane

App-shell anatomy. Every surface mounts a ThemeProvider → DirectionProvider → I18nProvider → QueryClientProvider → ErrorBoundary → Router → AppShell. The AppShell differs per surface (top-bar + drawer for consumer; sidebar + workspace-switcher for desktop) but the provider chain is identical. The AppShell is 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/domain has 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 direct fetch in a component.
  • BFF DTOs are imported from @ghasi/api-clients — no ad-hoc shapes.
  • app/ (or screens/ / 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.melmastoon contextBridge surface only (see desktop/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.

ModeDefault forRow heightFont baseButton padding-inline
comfortableConsumer surfaces (meta web, tenant booking web, mobile)48 px--text-base (16 px)--space-5
compactElectron desktop backoffice, control plane32 px--text-sm (14 px)--space-3
cozyOperator 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

ShellUsed forComposition
App shellDefault 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 shellBooking 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 shellProperty detail, reservation detail, room detailHero region · breadcrumb · tabbed body · sticky action bar (mobile/desktop)
Print shellCash-drawer report, regulatory submission, folio receipt, tax exportStripped theme · serif typography · 11 pt base · monochrome unless semantic color carries meaning · A4 + Letter
Authoring shellTheme editor (control plane), preview pagesLive preview iframe + side panel of token controls + save/preview/publish bar pinned bottom
Modal shellCritical 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-motion is 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.

SurfaceExample
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+json codes 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 in 05-frontend-workflows.md).

5.5 Notifications

ChannelUseComponent
ToastEphemeral confirmations and non-blocking warnings<Toast /> from @ghasi/ui-melmastoon — auto-dismiss 5 s default; never used for destructive confirmations.
BannerPersistent 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 inboxPersistent, 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

BreakpointColumnsGutterSide rails
Phone (< 768 px)416 pxbottom tab nav, drawer for secondary
Tablet (768 – 1023 px)816 pxsingle primary + drawer left rail
Laptop (1024 – 1279 px)1216 pxright rail collapses to a tab strip
Desktop (≥ 1280 px)1224 px80 px side rails on consumer; full sidebar on backoffice
Print (A4 / Letter)n/an/astripped 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/icons re-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: boolean in 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).

ConcernRule
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.
Keyboard100% 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 readerSemantic HTML first; ARIA only where semantics are not natively expressible; role="alert" for live banner updates (sync state, hold expiry, payment failure).
Reduced motionAll motion gated on @media (prefers-reduced-motion) (web) / useReducedMotion() (native + desktop). Spinners and skeletons collapse to opacity fades.
Target sizeMinimum 44 × 44 pt mobile, 32 × 32 px web/desktop (WCAG 2.5.5). Linted on Button, IconButton, Pressable.
Languagelang attr per passage; bilingual strings (Pashto + Latin transliteration) wrapped in <bdi>/<BidiText>.
Form errorsaria-describedby to message; programmatic label on every input; error summary at top of form on submit.
Live regionsHold expiry, payment status changes, sync conflicts announced via role="alert" / useAnnounce().
Zoom200% zoom + 320 px reflow without loss of function on web.
MediaCaptions on all educational video; transcripts downloadable.
Confirmation surfacesBooking 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.

DecisionRationale
Latin numerals locked in UI for prices, totals, IDs, counts, dates in finance contextsFinance 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 guessA 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 toggleDirection 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 patternGuest 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 phasedPashto, 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 fallbackPer-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.

RuleRationale
Explicit triggerNo AI action without a user click or a tenant-published auto-policy. Frontend never auto-populates guest-facing fields silently.
Provenance badgeEvery AI artifact renders a footer: `model · version · prompt · traceId · local
HITL signature on irreversible actionsRate 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 CancelLong generations stream token-by-token with a visible Cancel button. Mobile streams via Reanimated worklets to keep the JS thread free.
Refusal UXNeutral, non-judgmental message; user can rephrase. Never present refusal as a system error.
Local-vs-cloud indicatorEdge-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 visibilityAdmin / GM views show per-tenant AI usage and budget consumption (cumulative spend, rate limit headroom).
Acceptance patternAI 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 affordanceEvery <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 neededThe 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 to 09-NFR.md §1. Per-surface budget summaries are in 09-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/height on 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-list for long lists.
  • Electron: utility processes for AI inference (ONNX Runtime Node) + sync engine; renderer stays pure UI.

12. State management conventions

LayerOwnerPersistenceExamples
Server stateTanStack Query (React Query v5)In-memory + IndexedDB (web) / MMKV (mobile) / better-sqlite3 (desktop)searchHotels, getHotel, getBootstrap, getQuote, getReservation, getArrivalsToday
Client UI stateZustand (per-domain stores)IndexedDB (web) / MMKV (mobile) / better-sqlite3 KV (desktop) for booking-draft and sync-status; in-memory for ephemeraBooking-draft, filter-panel-open, map-vs-list, sync-status, AI-suggestion-inbox
URL statenuqs (web) / React Navigation params (mobile) / react-router search params (desktop renderer)URL itselfSearch filters, sort, pagination, active tab, selected reservation, date range
Form stateReact Hook Form + ZodPer-form scopedGuest details, walk-in capture, refund approval, theme authoring
Session stateBFF + cookieServer-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-Control directives; staleTime and gcTime are explicit per query key.

13. Cross-surface conventions

These are the small consistencies that keep four codebases feeling like one product.

ConcernConvention
Identity barTenant-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 entryBookingDateRangePicker 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 patternDestructive 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 visibilityEvery 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 affordanceEvery funnel step has a contextual help link (right-rail on desktop, bottom-sheet on mobile) — never modal-blocking.
Time formattingRelative (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.

ArtifactSurfaceConstraints
Booking confirmation PDF (multilingual)Tenant bookingA4 + Letter; tenant logo + theme; primary locale + EN; QR code with reservation code; cash-on-arrival voucher block when applicable.
Folio receiptDesktop print shellA4; itemized in BigInt-rendered amounts; FX-snapshot footer; tenant invoice number.
Cash drawer reportDesktop print shell (J-12)A4; daily totals; variance highlighted; signed by closer + supervisor.
Regulatory submissionDesktop export (J-17, J-18)PDF + XLSX + CSV per regulator schema; locale-specific date format; deterministic column order; signed receipt persisted.
Transactional emailsnotification-serviceMJML 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 payloadsMobile (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.

ConcernToolCoverage rule
Component testsVitest (web/desktop) + Jest (mobile) + Testing LibraryEvery primitive ships with stories and tests; behavior-based assertions, not snapshots.
Visual regressionChromatic (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.
Accessibilityaxe-playwright + axe-storybook + Detox a11yEvery story; every E2E run; serious/critical violations block merge.
RTL parityPlaywright (web) screenshots + Detox + ScreenShoter (mobile)Every primary screen in dir=rtl.
Pseudo-localeen-XA rendered for every PRTruncation + RTL flips checked.
Bundle sizesize-limit (per package) + Lighthouse-CI (per route)Per §11 budgets.
Hydration mismatchReact strict + Playwright assertionconsole.error empty on first paint.
Density modesStorybook stories per primitive in cozy/comfortable/compactVisual 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-patternWhy bannedCorrect 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 modeTenant-published light/dark/high-contrast bundles only
Auto-applying AI suggestions to guest-facing stateDefeats HITL; trust lossSuggestions render dashed; explicit accept counter-signs
Spinners as the only loading stateReduced-motion users see nothing; long waits feel hungPair with skeleton + escalation copy + Cancel after 2 s
Color alone as status carrierColorblind / low-contrast users miss meaningPair icon + color + text label always
Bilingual capture in a single inputForces user into one script; transliteration ambiguityTwo paired inputs with HITL transliteration suggestion
OS calendar / location-driven calendar swapUser context ≠ device context (regulator export needs Gregorian regardless of UI choice)User picks display calendar; storage is ISO-8601 Gregorian
Density toggle on guest surfacesAdds load + UX confusion to non-operator personasDensity is operator-only
Hardcoded English copy / aria-labelsBreaks i18n + RTL auditsAlways useTranslations / t() from @ghasi/i18n
Direct domain-service calls from app codeBypasses tenant resolution, theming, CSP, rate limit, auditAlways via the appropriate BFF (@ghasi/api-clients/<bff>)
Mutating melmastoonDefaultTokens at runtimeBreaks identity stability + memoization assumptionsOverride via tenant theme; never mutate the default object
Embedding tenant assets in the bundleDefeats CDN; per-tenant invalidation impossibleReference MediaRef URLs; never bundled
Per-tenant React component files (KabulGrandHero.tsx)Doesn't scale; per-tenant deploysCompose from primitives + content blocks
Skipping the contrast / preset validation locally because "publish will catch it"Slows feedback; surfaces broken builds in shared environmentsRun validators in pre-commit hooks (provided by @ghasi/ui-melmastoon)
OTA-pushing native code on mobileBreaks store policiesNative changes go through EAS Build + store review
Operator action without "View audit" linkOperator loses recourse to verify what firedEvery 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-booking behind 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