Skip to main content

03 — Design System (@ghasi/ui-melmastoon)

Scope: the canonical design system shared across web (Next.js + the Vite-React renderer hosted by Electron in the desktop backoffice) and mobile (React Native). Tokens, primitives, components, hooks, icons, accessibility, RTL, distribution.

Companions: README · 01-web-and-mobile-specification.md · 02-theming-and-tenant-config.md · desktop/06-desktop-app-specification.md


1. Goal

@ghasi/ui-melmastoon is the single design system used by every Ghasi Melmastoon frontend:

  • apps/web-meta (Next.js — Consumer Meta Web)
  • apps/web-tenant-booking (Next.js — Tenant Booking Web)
  • apps/mobile (React Native — Consumer Mobile)
  • apps/desktop-backoffice (Electron's Vite + React renderer — Backoffice Desktop)

Promise: every primitive has the same API across platforms where reasonable, the same visual semantics, the same accessibility guarantees, and the same RTL behavior. Tenant theming flows through the same token consumer surface — there is no second design language for "operators" vs "guests".

Non-promise: the package does not own layout or page composition. Apps build their own layouts; the design system gives them tokens and primitives.


2. Package layout

packages/ui-melmastoon/
├── package.json # @ghasi/ui-melmastoon, exports map per surface
├── tsconfig.json
├── src/
│ ├── tokens/ # default Melmastoon tokens + token types
│ │ ├── default.ts
│ │ ├── highContrast.ts
│ │ ├── types.ts # re-exports DesignTokens from @ghasi/contracts-melmastoon
│ │ └── tailwind.preset.ts # Tailwind preset for web consumers
│ ├── primitives/ # cross-platform primitives (web + native variants)
│ │ ├── Box/
│ │ │ ├── Box.web.tsx
│ │ │ ├── Box.native.tsx
│ │ │ └── index.ts # platform-resolved export
│ │ ├── Stack/ HStack/ VStack/
│ │ ├── Text/ Heading/
│ │ ├── Button/ IconButton/
│ │ ├── Input/ Select/ Checkbox/ Radio/ Switch/ NumberInput/
│ │ ├── DatePicker/ # plus calendar engine (Gregorian/Hijri/Solar Hijri)
│ │ ├── Modal/ Drawer/ Sheet/ Popover/ Tooltip/
│ │ ├── Badge/ Tag/ Avatar/ Card/ Divider/
│ │ ├── Spinner/ Skeleton/
│ │ └── Image/ # next/image wrapper + RN expo-image wrapper
│ ├── components/ # higher-level domain components
│ │ ├── RoomTypeCard/ RatePlanCard/
│ │ ├── BookingDateRangePicker/ OccupancyPicker/
│ │ ├── AmenityChips/
│ │ ├── PropertyMap/ GalleryGrid/ TestimonialCard/
│ │ ├── PricingDisplay/ # multi-currency + FX-snapshot aware
│ │ ├── KeyCredentialBadge/ # mobile-key / PIN / QR
│ │ ├── HousekeepingTaskCard/ ReservationStatusPill/
│ │ ├── OperatorActivityFeed/ AISuggestionCard/
│ │ ├── AlertBanner/ OfflineBanner/ SyncStatusBadge/
│ │ └── ContactBlock/ FAQ/ MarkdownBlock/
│ ├── hooks/ # cross-platform behavioral hooks
│ ├── icons/ # icon registry + RTL manifest
│ ├── a11y/ # accessibility helpers
│ ├── rtl/ # direction context, mirroring helpers
│ ├── theme/ # ThemeProvider + token consumer
│ └── index.ts
├── stories/ # Storybook (web) stories per primitive + component
├── ladle/ # Ladle config for fast story dev
└── tests/ # unit + visual snapshot fixtures

Exports map (package.json exports field) splits surfaces:

{
".": { "web": "./dist/index.web.js", "react-native": "./dist/index.native.js" },
"./tokens": "./dist/tokens/index.js",
"./tailwind": "./dist/tokens/tailwind.preset.js",
"./icons": { "web": "./dist/icons/index.web.js", "react-native": "./dist/icons/index.native.js" },
"./hooks": "./dist/hooks/index.js",
"./a11y": "./dist/a11y/index.js"
}

3. Tokens

The design system re-exports the platform-default DesignTokens (from @ghasi/contracts-melmastoon) plus the Melmastoon-brand default values. At runtime, tenant overrides come from theme-config-service via the BFF bootstrap (see 02-theming-and-tenant-config.md).

// packages/ui-melmastoon/src/tokens/default.ts
import type { DesignTokens } from '@ghasi/contracts-melmastoon';

export const melmastoonDefaultTokens: DesignTokens = {
color: {
primary: '#0F4C81', // Melmastoon platform blue
primaryHover: '#0B3D69',
primaryActive: '#082E50',
textOnPrimary: '#FFFFFF',
secondary: '#C2A04E',
secondaryHover: '#A88838',
textOnSecondary:'#1A1A1A',
accent: '#E07A1F',
textOnAccent: '#FFFFFF',
surface: '#FFFFFF',
surfaceMuted: '#F4F6F8',
surfaceElevated:'#FFFFFF',
textOnSurface: '#0F172A',
textOnSurfaceMuted: '#475569',
border: '#E2E8F0',
divider: '#EEF2F6',
success: '#16A34A', textOnSuccess: '#FFFFFF',
warning: '#D97706', textOnWarning: '#1A1A1A',
error: '#DC2626', textOnError: '#FFFFFF',
info: '#2563EB', textOnInfo: '#FFFFFF',
focusRing: '#0F4C81',
overlay: 'rgba(15,23,42,0.55)',
},
typography: { /* see §8 */ },
spacing: { unit: 4, values: { 0:0, 1:4, 2:8, 3:12, 4:16, 6:24, 8:32, 12:48, 16:64 },
start: { 0:0, 1:4, 2:8, 3:12, 4:16, 6:24, 8:32, 12:48, 16:64 },
end: { 0:0, 1:4, 2:8, 3:12, 4:16, 6:24, 8:32, 12:48, 16:64 } },
radius: { none: 0, sm: 4, md: 8, lg: 16, pill: 9999 },
shadow: { /* see §10 */ },
motion: { /* see §11 */ },
direction: 'auto',
};

Source of truth. When a tenant override exists (theme-config-service's active ThemeVersion), it wins for that tenant. The defaults above are used by the consumer meta web (no tenant context) and by the desktop backoffice (which can be themed but defaults to platform brand).


4. Primitives

Cross-platform building blocks. Same API on web and native unless platform parity is impossible — those exceptions are called out below.

4.1 Layout

PrimitiveAPI summaryWebNativeNotes
Box{ padding?: Space; margin?: Space; bg?: ColorToken; border?: BorderToken; radius?: RadiusToken; ... }<div><View>Accepts logical paddings (paddingStart, paddingEnd) — RTL-aware
Stackdirection-agnostic stackflex column or row per direction propsamegap uses spacing tokens
HStackhorizontal stackflex rowsameRTL flips order via direction
VStackvertical stackflex columnsame
Centercenter childrenflex centersame

4.2 Typography

PrimitiveAPINotes
Text{ size: TypoSize; weight?: TypoWeight; tone?: 'default'|'muted'|'inverse'; numberAdjust?: 'tabular' }Locale-aware ligatures; bidi-safe
Heading{ level: 1|2|3|4|5|6; size?: TypoSize; ... }Renders <h1><h6> web; native uses Text with proper accessibility role
Code / Kbdinline monouses fontFamilyMono

4.3 Form

PrimitiveAPI summaryNotes
Button{ variant: 'primary'|'secondary'|'ghost'|'destructive'; size: 'sm'|'md'|'lg'; loading?: boolean; iconStart?: IconKey; iconEnd?: IconKey }Loading state preserves width; min touch target 44×44 mobile, 32×32 web
IconButton{ icon: IconKey; label: string; ... }label is required (a11y); never icon-only without label
Input{ value, onChange, label, helperText?, error?, leadingAddon?, trailingAddon?, type? }Label + helper + error wired via aria-describedby
Select{ items, value, onChange, label, ... }Native uses platform picker; web uses <select> with custom skin
Checkbox / Radio / SwitchstandardAll bidi-friendly; clickable label area meets 44 px
DatePicker / BookingDateRangePickeraccepts calendar system (gregorian|islamic|persian); always emits ISO-8601 GregorianUses calendar engine in §5
NumberInput{ value, onChange, min?, max?, step? }inputMode="numeric"; locale-aware separators on display; raw integer on emit

4.4 Overlay

PrimitiveNotes
ModalFocus trap, ESC to dismiss, overlay scrim from --color-overlay, return focus on close
DrawerSide-anchored sheet (start/end-aware)
SheetBottom sheet (mobile-friendly)
PopoverFloating UI (uses @floating-ui/react web; custom on native)
TooltipDelay 300 ms; touch devices use long-press; never the only affordance for an action

4.5 Feedback

PrimitiveNotes
Badge / TagStatus semantics; tone matches success/warning/error/info/neutral
AvatarInitials fallback; alt text required
CardContainer with surface background + radius + subtle shadow
DividerHorizontal or vertical; uses --color-divider
SpinnerHonors reduced-motion (renders a static dot)
SkeletonShimmer animation reduced-motion-safe
Image<Image src alt sizes? width height priority?> — wraps next/image (web) and expo-image (native); WebP/AVIF; LQIP placeholder; CDN-aware

4.6 Cross-platform parity table

PrimitiveWeb onlyNative onlyBoth
Box, Stack, Text, Heading, Button, IconButton, Input, Checkbox, Radio, Switch, Modal, Drawer, Tooltip, Badge, Tag, Avatar, Card, Divider, Spinner, Skeleton, Image, Sheet, Popover
Selectrich popover variantplatform picker variantbase API
DatePickerinline + popoverinline + bottom-sheetbase API
NumberInputspinner buttonsstepper buttonsbase API

5. Components

Higher-level, domain-aware components composed from primitives.

ComponentPurposeUsed by
RoomTypeCardRoom type with photo, beds, max occupancy, amenity chips, "from {price}"tenant booking, mobile
RatePlanCardRate plan summary with refundability, breakdown CTA, "Choose this rate"tenant booking, mobile
BookingDateRangePickerTwo-month range picker, blocked dates from availability, RTL-flippedmeta + tenant + mobile
OccupancyPickerAdults / children / rooms ± buttons with min/max enforcementmeta + tenant + mobile
AmenityChipsChip row with overflow "+N more" disclosurelisting card, detail page
PropertyMapLeaflet (web) / react-native-maps (mobile) wrapper with brand-colored pins, clusteringmeta search, detail
GalleryGridResponsive photo grid with lightbox; mosaic / carousel / grid layoutscontent blocks, detail
TestimonialCardQuote, author, optional rating; locale-awarecontent blocks
PricingDisplayMulti-currency, FX-snapshot aware; renders display.formatted; flags stale snapshotstenant booking, confirmation
KeyCredentialBadgeMobile-key / PIN / RFID / QR variants; pending vs delivered statesconfirmation, mobile manage
HousekeepingTaskCardTask w/ priority, room status pill, AI-suggested order indicatordesktop
ReservationStatusPillStatus (held/confirmed/checked_in/checked_out/no_show/cancelled) with a11y labeldesktop, confirmation
OperatorActivityFeedTime-ordered events (check-ins, modifications, payments, key issuances)desktop
AISuggestionCardAI proposal with provenance footer ({ model, version, promptId, traceId, local }); accept / dismiss / "Why this?"desktop, tenant authoring
AlertBannerPage-level banners (info/warning/error/success) with optional actionall
OfflineBannerSticky banner — "You're offline. Browsing still works."meta web, mobile, desktop
SyncStatusBadgeSync state for desktop (live / queued / reconciling / conflict)desktop

5.1 PricingDisplay API

<PricingDisplay
amountMinor="320000" // BigInt-string from BFF
currency="AFN"
formatted="AFN 320,000" // server-formatted (preferred render path)
perNight={false}
fxSnapshotId="fxs_01H..."
capturedAt="2026-04-23T09:14:22.041Z"
ttlExpiresAt="2026-04-23T09:15:22.041Z"
isStale={false}
onRefresh={() => refetchQuote()}
/>

Renders the server string verbatim by default; useFormatCurrency() is available for client-only contexts (rare). Stale snapshots overlay a subtle warning chip with a "Refresh" CTA.

5.2 AISuggestionCard API + provenance

<AISuggestionCard
title={t('pricing.suggestion.title')}
body={t('pricing.suggestion.body', { delta: '+8%' })}
provenance={{
model: 'gemini-1.5-pro',
version: '2026-03-14',
promptId: 'pricing.daily.v3',
traceId: '00-4bf92f3577b34da6...',
local: false,
}}
onAccept={() => acceptSuggestion(id)}
onDismiss={() => dismissSuggestion(id)}
onExplain={() => openExplanationDrawer(id)}
/>

The card always shows the model + version footer; local: true (edge ONNX) is visually distinguished. Irreversible actions require an explicit confirm dialog (HITL) — the card surfaces the warning when relevant.


6. Hooks

HookReturnsNotes
useTheme()DesignTokens (the resolved set, tenant-aware)Identity-stable; safe in deps arrays
useLocale(){ locale: Locale; setLocale: (l) => void }Triggers i18n revalidation; mobile prompts reload for direction change
useDirection()'ltr' | 'rtl'Use for direction-conditional rendering (sparingly — prefer logical CSS)
useFormatCurrency()(amountMinor: string|bigint, currency: string) => stringLatin-numeral, locale-aware
useFormatDate()(iso: string, opts?) => stringCalendar-aware (Gregorian/Hijri/Solar Hijri)
useFormatNumber()(n: number, opts?) => stringLocale-aware
useDeviceType()'mobile' | 'tablet' | 'desktop'Web: viewport-derived; native: Dimensions + tablet detector
useReducedMotion()booleanOS preference; throttled via subscription
useNetworkStatus(){ online: boolean; saveData: boolean; effectiveType?: '2g'|'3g'|'4g'|'5g' }Web: NIC API; native: @react-native-community/netinfo
useFeatureFlag(key)booleanReads from BFF bootstrap's featureFlags map; never call domain services directly
useFocusVisible(){ ref, isFocusVisible }Wrapper for :focus-visible semantics on both platforms
useDirectionalIcon(name)IconKeyReturns the mirrored variant when dir==='rtl' and the icon's manifest declares mirrorOnRtl: true

7. Icons

@ghasi/ui-melmastoon/icons exposes a registered, type-safe icon set.

  • Base set: Phosphor Icons (line + filled) covering the standard inventory.
  • 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.
  • Status icons: clean, dirty, out-of-order, out-of-service, inspecting, maintenance-pending.
  • AI icons: ai-suggestion, ai-local, ai-cloud, ai-needs-review.

7.1 Manifest + RTL

// packages/ui-melmastoon/src/icons/manifest.ts
export const ICON_MANIFEST = {
'chevron-right': { mirrorOnRtl: true, category: 'navigation' },
'chevron-left': { mirrorOnRtl: true, category: 'navigation' },
'arrow-right': { mirrorOnRtl: true, category: 'navigation' },
'arrow-left': { mirrorOnRtl: true, category: 'navigation' },
'play': { mirrorOnRtl: false, category: 'media' }, // never mirror media controls
'pause': { mirrorOnRtl: false, category: 'media' },
'logo-melmastoon': { mirrorOnRtl: false, category: 'brand' }, // never mirror brand
'halal': { mirrorOnRtl: false, category: 'hospitality' },
// ...
} as const;

export type IconKey = keyof typeof ICON_MANIFEST;

7.2 Usage

<Icon name="chevron-right" size="md" tone="muted" />
<IconButton icon="key" label={t('button.issue_key')} variant="primary" />

Web renders inline SVG; native uses pre-built React Native components compiled from the same SVG sources at build time (single source of truth, no SVG runtime cost on native).


8. Typography pairs

Locale groupUI font (priority)Fallback chainNumeral systemNotes
Latin (en-*, fr-*, de-*, es-*, it-*)Inter (variable)system-ui, -apple-system, sans-serifLatinDefault body + heading
Pashto (ps-*), Dari (fa-AF)VazirmatnNoto Naskh Arabic, system-ui, sans-serifLatin (UI)Vazirmatn covers Pashto + Dari + Persian
Persian (fa-IR)VazirmatnNoto Naskh Arabic, system-ui, sans-serifLatin (toggle to Persian via locale pack)
Arabic (ar-*)Noto Naskh ArabicVazirmatn, system-ui, sans-serifLatin
Urdu (ur-PK)Noto Nastaliq UrduNoto Naskh Arabic, system-ui, sans-serifLatinPhase 1
Mono (all)JetBrains Monoui-monospace, monospaceLatinCode, IDs, hashes

Numerals are always Latin in the UI (prices, totals, IDs, counts). Locale packs may opt-in to Persian-Indic numerals only in narrative text — never for monetary or quantity values, which are read by finance staff in audits.

8.1 Loading

  • Web: Subset per locale (woff2 with unicode-range); font-display: swap. Total UI font payload ≤ 2 weights × 2 styles per locale.
  • Mobile: Bundled in the binary for the platform's primary locales (en + ps + fa + ar). Other locales fall back to system fonts.

9. Color system

  • Naming: semantic only (no gray-500, no blue-700). Token names express role, not value.
  • Modes: light, dark, high-contrast — three published bundles per tenant. Default tenants ship light only; opting into dark or high-contrast enables additional bundles.
  • Auto-generated tints/shades: at publish time, theme-config-service derives primaryHover/primaryActive from primary if a tenant doesn't override — using the OKLCH luminance step (-6% / -12%).
  • Contrast at build: WCAG AA contrast minima are checked at publish in theme-config-service and at every PR in the design system CI (against the Melmastoon defaults). Failures block the build.
  • Status palette: success / warning / error / info are tenant-overridable but constrained to a hue range to remain semantically obvious (e.g., error must remain red-orange spectrum).
// Build-time check (runs in @ghasi/ui-melmastoon CI)
import { contrastRatio } from '@ghasi/contracts-melmastoon/contrast';
import { melmastoonDefaultTokens as t } from './src/tokens/default';

assertOK(contrastRatio(t.color.textOnPrimary, t.color.primary) >= 4.5);
assertOK(contrastRatio(t.color.textOnSurface, t.color.surface) >= 4.5);
// ... and so on for every documented pair

10. Spacing & layout

  • Scale: 0, 1, 2, 3, 4, 6, 8, 12, 16 × unit (default 4 px). Tenant may pick unit ∈ {4, 6, 8}.
  • Density modes: cozy / comfortable (default) / compact. Density is consumer-chosen on the desktop backoffice (operator preference); guest surfaces stay comfortable.
type Density = 'cozy' | 'comfortable' | 'compact';
// Scales padding tokens; the design system handles the remap centrally.
  • Responsive grid (web): 4-column (mobile, < 768 px), 8-column (tablet, 768–1199 px), 12-column (desktop, ≥ 1200 px). Gutters: space.4 / space.6 / space.8.
  • Touch targets: ≥ 44×44 mobile, ≥ 32×32 web (per WCAG 2.5.5). Linted via a custom ESLint rule on Button, IconButton, Pressable.
  • Logical properties only. Margins/paddings use padding-inline-start / margin-block-end etc.; ESLint rejects raw paddingLeft/marginRight in app code.
<HStack gap="space.4" paddingInlineStart="space.4" paddingBlockEnd="space.6">
<RoomTypeCard ... />
<RatePlanCard ... />
</HStack>

11. Motion

  • Tokens: durations (fast: 120 ms, base: 200 ms, slow: 320 ms); easings (standard: cubic-bezier(0.2, 0, 0, 1), entrance, exit).
  • Reduced-motion fallback: every animated primitive checks useReducedMotion(); on true, animations collapse to opacity-only or no-op.
  • Banned: parallax, infinite spinners (must have a max duration before showing a static "Still working…"), background bouncing, anything > 400 ms unless it's a one-time celebration (booking confirmed).
  • Mobile: all animations off the JS thread via Reanimated 3.

12. Accessibility primitives

HelperPurpose
<VisuallyHidden>Screen-reader-only text
<SkipToContent />Skip link in every web page header
<FocusTrap>Used by Modal, Drawer, Sheet
useAnnounce(message, polite|assertive)ARIA live region announcement
<FocusRing />Renders the focus-visible ring (token: --shadow-focus)
useAccessibleTouchArea({ ref, minSize: 44 })Auto-extends the touchable area where visual size < min
roleProps(role, opts)Returns the right role/aria-*/accessibilityRole pair per platform

Every primitive renders semantically correct elements (web: <button> for Button, <input type="checkbox"> for Checkbox; native: matching accessibilityRole).


13. RTL toolkit

HelperPurpose
<DirectionProvider value={'ltr'|'rtl'|'auto'}>Sets dir (web) / I18nManager.forceRTL (native)
useDirection()Returns the resolved direction
mirror(component)Higher-order wrap for RTL-aware mirroring (rare; logical CSS preferred)
logical({ start, end })Helper that emits the right padding-inline-start / margin-inline-end per platform
<BidiText value={...}>Wraps text with bidi marks where the value's direction differs from the page
// Logical helpers in JS where CSS isn't available (RN)
import { logical } from '@ghasi/ui-melmastoon/rtl';

<View style={{ ...logical({ paddingStart: 16, paddingEnd: 8 }) }}>
...
</View>

14. Storybook + Ladle

  • Storybook (web): every primitive and component has stories; per-story args control tone, size, state (default/hover/focus/disabled/loading), dir (ltr/rtl), locale (en-US/ps-AF/ar-SA), theme (default / Kabul-Grand demo / high-contrast).
  • Ladle: used for fast day-to-day primitive iteration (≪ 1 s HMR). CI uses Storybook (richer addons).
  • Visual regression: Chromatic captures Storybook snapshots on every PR. Baselines per (theme, locale, dir) matrix. Failures require approver sign-off.
  • a11y: @storybook/addon-a11y + axe-core run on every story; PRs blocked on serious/critical violations.
  • Native stories: parallel stories live in apps/mobile/storybook for native-specific behavior; visual regression via Loki on a Pixel 4a + iPhone 13 emulator.

15. Linting & quality

A package-internal ESLint config is published as @ghasi/ui-melmastoon-eslint and consumed by every app. Key rules:

RulePurposeEnforcement
no-raw-colorForbid hex / rgb / hsl in apps/* and packages/feature-*Error
no-raw-spacingForbid raw px / rem in style props or className utilities (p-[12px])Error
no-raw-fontForbid font-family overrides outside @ghasi/ui-melmastoon/tokensError
no-physical-propertiesForbid padding-left/padding-right, margin-left/margin-right, left/right (use logical)Error
no-non-translated-textDetect string literals in JSX (with allow-list for testids, hrefs, etc.)Warning → Error in CI
no-direct-domain-callImports from @ghasi/api-clients/<bff> only — direct service URLs forbiddenError
min-touch-targetButton/IconButton/Pressable must declare a size that resolves ≥ 44×44 mobile / 32×32 webWarning
prefer-primitiveDiscourage ad-hoc <div> for layout when Box/Stack would doWarning
icon-only-needs-labelIconButton must have non-empty labelError
i18n-key-existsTranslation keys passed to t() must exist in the catalog at build timeError in CI
axe-a11yStorybook + Playwright @axe-core checks run in CIBlock on serious/critical
zod-schema-requiredComponents accepting external data must declare a Zod schema (optional but conventional for feature-*)Warning

16. Theming consumer (how tenant overrides plug in)

Primitives consume tokens through one consumer surface — the ThemeProvider set up at the app root with the bootstrap response (web) or the bootstrap context (mobile).

// apps/web-tenant-booking/app/layout.tsx
<ThemeProvider value={bootstrap}>{children}</ThemeProvider>

// apps/mobile/App.tsx
<ThemeProvider value={bootstrap}>...</ThemeProvider>

The ThemeProvider:

  1. Loads tokens (defaults merged with tenant overrides).
  2. Emits CSS variables on web (memoized stable object).
  3. Provides the resolved DesignTokens via context for useTheme() consumers.
  4. Wraps DirectionProvider so RTL detection works.
  5. Wraps the Tailwind preset's CSS-var system implicitly (since Tailwind classes resolve to var(--...)).

App code is unaware of whether it's running in tenant context or platform-default context — same primitives, same hooks, different tokens at runtime.


17. Distribution

  • Repository: internal monorepo ghasi-melmastoon, package packages/ui-melmastoon.
  • Versioning: semver via Changesets. Major bump for any token-shape removal, primitive API removal, or RTL contract change. Minor for additive primitives, additive tokens, additive props. Patch for non-API fixes.
  • Publishing: pnpm publish to a private GCP Artifact Registry (npm format). Apps pin to caret ranges in pre-prod, exact in production.
  • Consumers: apps/web-meta, apps/web-tenant-booking, apps/mobile, apps/desktop-backoffice. Plus internal control-plane app and the future Storybook deployment.
  • Browserslist + RN compat: web supports last-2 of evergreen + Safari 15+. Native targets RN 0.74+, Hermes only.
  • Bundle size budgets: index.web.js ≤ 60 KiB gzipped, index.native.js ≤ 90 KiB gzipped (native variants ship more code due to per-platform polyfills).
  • Release notes: auto-generated via Changesets; published to the design system's Storybook index.
  • Migration guides: every major bump ships a MIGRATION.md with codemods (@ghasi/ui-melmastoon-codemods) for mechanical rewrites.

18. Anti-patterns

Anti-patternWhy it's bannedCorrect approach
Raw hex colors in app code (color: '#0F4C81', bg-[#0F4C81])Defeats tenant theming + contrast invariants; breaks dark/high-contrast modesUse semantic tokens via Tailwind utilities (web) or useTheme() (mobile)
Bypassing primitives for ad-hoc styled <div>sInconsistent spacing/typography/focus; loses RTL behaviorCompose Box, Stack, Text, Heading
Ignoring RTL (using paddingLeft, hard-coded chevron direction, text-align: right)Half the user base (Pashto/Dari/Arabic/Persian) sees broken layoutsUse logical properties; <Icon name="chevron-right" /> mirrors automatically; text-align: start/end
Non-translatable text in components (<p>Book now</p>)Breaks i18n and auditUse useTranslations / pass an i18n key prop
Icon-only buttons without labelScreen reader users have no clue what the button does<IconButton icon="key" label={t('button.issue_key')} /> — label is required
Spinner indefinitely without a fallbackReduced-motion users get nothing; long waits give no escape hatchPair with a "Still working…" text after 2 s + cancel CTA
Tooltip as the only affordance (e.g., to explain a destructive action)Touch users never see tooltipsAlways pair with helper text or aria-describedby
<input> / <button> not driven by primitivesLoses focus rings, hit areas, density modes, locale-aware numeric handlingUse Input / Button
Embedding raw SVG in componentsBypasses the icon manifest (no RTL, no naming, no a11y guarantees)Add to @ghasi/icons, use <Icon name="...">
Custom date picker in app codeCalendar fragmentation (Gregorian vs Hijri vs Solar Hijri) breaks user expectationsUse BookingDateRangePicker / DatePicker
App-level dangerouslySetInnerHTML outside MarkdownBlockXSS surfaceRender through MarkdownBlock (sanitized)
Mutating melmastoonDefaultTokens at runtimeBreaks identity stability + memoization assumptionsOverride via tenant theme; never mutate the default object
Skipping Storybook stories for new componentsReviewers can't validate states, locales, RTL; visual regression has no baselineStories are required for any new public export — CI enforces
Per-app forks of a primitiveDiverges API; breaks design system promiseAdd a variant to the primitive in @ghasi/ui-melmastoon instead

19. References